Skip to content

Bundle of updates 3#4210

Merged
Henrybk merged 6 commits into
masterfrom
bundle3
May 13, 2026
Merged

Bundle of updates 3#4210
Henrybk merged 6 commits into
masterfrom
bundle3

Conversation

@Henrybk
Copy link
Copy Markdown
Contributor

@Henrybk Henrybk commented May 12, 2026

Bundle of updates 3

Summary

This PR is a large bundle of OpenKore updates focused on routing safety, dynamic portals, teleport restrictions, eventMacro compatibility, attack/party aggression logic, combo handling, NPC shop discovery, new table systems, safer position prediction, and condition/config cleanup.

The main goal is to make OpenKore behave better in modern server environments where routing, NPC warps, teleport items, dynamic portals, party combat, cast-sensor monsters, short combo windows, generated rAthena-based data, and complex eventMacro automation interact with each other.

Videos:
https://www.youtube.com/watch?v=-KlAAMlPkW4&t=26s
https://www.youtube.com/watch?v=hsdkZiY57G4&t=6s

Highlights

  • Adds dynamic portal group blocking for route calculation.
  • Adds safer handling for temporarily missing portals, NPC warps, and airship route sources.
  • Adds a complete NPC shop cache/index system and a new searchshop command.
  • Adds new npc_shops.txt, no_teleport_maps.txt, and item_hand_type.txt table systems.
  • Expands monsters_table.txt with explicit monster AI-mode flags.
  • Adds new eCast/config conditions for monster AI modes.
  • Adds new equipment/hand type checks to checkSelfCondition.
  • Adds party cast tracking with castOnByParty and castOnToParty.
  • Improves aggressive monster detection, including cast-sensor logic.
  • Improves attack combo timing by tracking the real combo packet window.
  • Improves follow and party-follow route reset behavior.
  • Improves dead-state cleanup after respawn edge cases.
  • Adds safe fallback logic for invalid/non-walkable predicted actor positions.
  • Adds new quit 3 and quit 4 debug shutdown modes.
  • Fixes eventMacro embedded Perl parsing by preserving newlines.
  • Adds new eventMacro condition: QuestAllListInactive.
  • Cleans up default config.txt by documenting reusable condition groups instead of repeating every possible condition key in every block.

Motivation

This bundle addresses several long-standing practical issues:

  1. Route calculation could choose invalid dynamic portal paths.
    Some portals are only available depending on how a character entered an area. The old route solver did not preserve route-branch state, so it could incorrectly route through a dynamic exit that should not be available.

  2. Missing NPC/portal route sources were deleted too destructively.
    Temporarily missing or failed portal/NPC route sources could be removed from the route table, risking loss of route graph information.

  3. Teleport and warp-item logic did not fully respect map restrictions.
    Some maps do not allow random teleport, return teleport, or teleport skill behavior. The new table system makes those restrictions explicit.

  4. NPC shops were not discoverable by item.
    Users often need to know where to buy an item. This PR adds persistent NPC shop indexing and route-based closest-shop lookup.

  5. Attack logic needed better cast/party awareness.
    Combat does not always start with damage. Party members may cast on monsters, monsters may cast on party members, and cast-sensor monsters may react to casting. The new counters and config keys make this behavior configurable.

  6. Short combo windows could be missed.
    Combo delays start when the packet arrives, not when the AI loop later sees the combo. The new combo state tracks the real expiration time.

  7. calcPosFromPathfinding could return non-walkable predicted cells.
    This could break route/attack/follow/aggression logic. The new fallback normalizes bad predicted positions to nearby walkable cells.

  8. eventMacro embedded Perl blocks could break when written across multiple lines.
    The parser now preserves newlines, making multiline Perl blocks safe.


Main changes

1. EventMacro updates

1.1 New QuestAllListInactive condition

This PR adds:

eventMacro::Condition::QuestAllListInactive

This condition is fulfilled only when all quests in the configured list are inactive.

This is useful when a macro should only start if none of several mutually exclusive quests are currently active.

Example:

automacro startEdenBoardWhenNoPreviousQuestIsActive {
    QuestAllListInactive 1001,1002,1003,1004
    call start_eden_board
}

Meaning:

  • quest 1001 must not be active;
  • quest 1002 must not be active;
  • quest 1003 must not be active;
  • quest 1004 must not be active;
  • only then the macro is allowed to run.

This is useful for quest flows where starting one quest locks out another quest range, or where automation must avoid starting a new quest while an older quest is still active.

Internally, the condition:

  • inherits from eventMacro::Condition::Base::Quest;
  • listens to quest-list related updates;
  • also hooks achievement_list;
  • sets list-style fulfilled variables.

It sets:

$self->{fulfilled_ID}
$self->{fulfilled_member_index}

When every quest in the list is inactive, it mirrors the first configured quest ID as the fulfilled ID.


1.2 EventMacro Perl sub parser fix

plugins/eventMacro/eventMacro/FileParser.pm now preserves line boundaries inside embedded Perl sub blocks.

Before:

$macro_subs = join('', @perl_lines);

After:

$macro_subs = join("\n", @perl_lines);

This matters because the previous behavior collapsed multiline Perl into one continuous line, which could break valid Perl constructs.

Example that is now preserved correctly:

sub getAllowedMaps {
    my @maps = qw(
        pay_fild08
        pay_fild09
        pay_fild10
    );

    return @maps;
}

This helps eventMacro files that use multiline:

  • qw(...);
  • array literals;
  • argument lists;
  • hash blocks;
  • formatted conditional logic;
  • helper subroutines.

2. Default config cleanup

control/config.txt was cleaned up to reduce duplicated condition lists.

Instead of repeating every possible condition key inside every automation block, the config now documents shared condition groups:

# [checkSelfCondition]
# [checkPlayerCondition]
# [checkMonsterCondition]

Example:

attackSkillSlot {
    lvl
    dist 1
    maxDist 1
    maxCastTime 0
    minCastTime 0
    previousDamage
    monsters
    notMonsters
    maxAttempts 0
    maxUses 0
    isSelfSkill 0
    isStartSkill 0
    equip_topHead
    equip_midHead
    equip_lowHead
    equip_leftHand
    equip_rightHand
    equip_robe
    equip_armor
    equip_shoes
    equip_arrow
    # [checkSelfCondition]
    # [checkMonsterCondition]
}

This keeps block-specific options visible while making the shared condition system easier to maintain.


3. New config keys

3.1 XKore listen-port additions

XKore_listenPort_map
XKore_listenPort_char

These allow separate map/char listen-port configuration in addition to the existing XKore listen settings.


3.2 Attack/aggression config

attackAuto_considerAggressiveIfCastOnCastSensor 0

When enabled, OpenKore can consider a cast-sensor monster aggressive when the player or party casts on it.

Example:

attackAuto 2
attackAuto_party 1
attackAuto_considerAggressiveIfCastOnCastSensor 1

With this enabled:

  • if the player casts on a cast-sensor monster, it can be treated as aggressive;
  • if a party member casts on a cast-sensor monster and attackAuto_party is enabled, it can be treated as party-aggressive;
  • slave attack logic can also use the slave-prefixed version of the config key.

3.3 Warp-item route config keys

route_warpByItem_chaining 0
route_warpItem_minGain 0
route_warpItem_routeCostHeuristic_max 10000
route_warpItem_routeCostProbe_maxPerTick 6
route_warpItem_routeCostCache_max 3000

route_warpByItem_chaining

Allows route calculations to consider warp items as part of a larger route branch.

route_warpByItem 1
route_warpByItem_chaining 1

route_warpItem_minGain

Minimum required route-cost improvement before choosing a warp item.

route_warpItem_minGain 50

This prevents wasting warp items for tiny gains.

route_warpItem_routeCostHeuristic_max

Caps the route-cost heuristic added from warp-item destination probing.

route_warpItem_routeCostHeuristic_max 10000

route_warpItem_routeCostProbe_maxPerTick

Limits expensive route-cost probes per tick.

route_warpItem_routeCostProbe_maxPerTick 6

route_warpItem_routeCostCache_max

Limits cached warp-item route-cost entries.

route_warpItem_routeCostCache_max 3000

3.4 Deal and Rodex limits

dealMaxItems 10
rodexMaxItems 5

3.5 WX/map display keys

wx_map_namesDetail 8
wx_map_playerNameZoom 8
wx_map_partyNameZoom 1
wx_map_portalDestinations 0

3.6 Project URL keys

githubURL
githubIssueURL

3.7 Logging/debug keys

logClanChat 0
debugAssertOnNetwork 0
debugPacket_ro_received 0

3.8 Skill/item automation keys

smartEncore 0
noSmartHeal 0
notPartyOnly 0
dcOnEmpty 0

Example:

useSelf_skill Heal {
    lvl 10
    hp < 60%
    noSmartHeal 1
    # [checkSelfCondition]
}

Example:

partySkill Heal {
    lvl 10
    target
    target_hp < 70%
    notPartyOnly 0
    noSmartHeal 0
    # [checkSelfCondition]
    # [checkPlayerCondition]
}

Example:

useSelf_item White Potion {
    hp < 40%
    dcOnEmpty 1
    # [checkSelfCondition]
}

3.9 buyAuto and getAuto condition keys

buyAuto now documents:

zeny
minDistance
maxDistance
dcOnEmpty 0
# [checkSelfCondition]

Example:

buyAuto Butterfly Wing {
    npc prontera 150 150
    minAmount 5
    maxAmount 20
    zeny > 10000
    maxDistance 500
    dcOnEmpty 1
    # [checkSelfCondition]
}

getAuto now documents:

dcOnEmpty 0
# [checkSelfCondition]

4. New checkSelfCondition checks

This PR adds new equipment/hand-state conditions.

whenNotEquipped
whenEquip_Right_Hand_Empty
whenEquip_Left_Hand_Empty
whenEquip_Right_Hand_Type
whenEquip_Left_Hand_Type

These are backed by the new item_hand_type.txt table.


4.1 whenNotEquipped

Opposite of whenEquipped.

Example:

equipAuto {
    rightHand Main Gauche
    whenNotEquipped Main Gauche
}

Meaning:

  • run this block only when Main Gauche is not equipped.

4.2 Empty-hand checks

whenEquip_Right_Hand_Empty
whenEquip_Left_Hand_Empty

Example:

useSelf_skill Throw Stone {
    whenEquip_Right_Hand_Empty 1
}

Example:

useSelf_skill Shield Skill {
    whenEquip_Left_Hand_Empty 0
}

4.3 Hand type checks

whenEquip_Right_Hand_Type
whenEquip_Left_Hand_Type

Supported aliases include:

Shield
Fist
Dagger
1hSword
2hSword
1hSpear
2hSpear
1hAxe
2hAxe
Mace
2hMace
Staff
Bow
Knuckle
Musical
Whip
Book
Katar
Revolver
Rifle
Gatling
Shotgun
Grenade
Huuma
2hStaff

Example: only use a bow skill with a bow equipped.

attackSkillSlot Double Strafe {
    lvl 10
    dist 8
    maxDist 10
    whenEquip_Right_Hand_Type Bow
    # [checkSelfCondition]
    # [checkMonsterCondition]
}

Example: only use a shield skill with a shield equipped.

useSelf_skill Shield Reflect {
    lvl 10
    whenEquip_Left_Hand_Type Shield
    # [checkSelfCondition]
}

Example: allow multiple weapon types.

attackSkillSlot Bash {
    lvl 10
    whenEquip_Right_Hand_Type 1hSword,2hSword,Mace
    # [checkSelfCondition]
    # [checkMonsterCondition]
}

5. New item_hand_type.txt table

New table:

tables/item_hand_type.txt

Format:

itemID AegisName type

Example:

1101 Sword 1hSword
1105 Blade 2hSword
1201 Knife Dagger
1701 Bow Bow
2101 Guard Shield

Loaded by:

parseItemHandTypeTable

Stored in:

%itemHandType_lut

Generator added:

tables/tools/itemdb_equip_yml_to_item_hand_type_txt.pl

This lets OpenKore generate hand-type metadata from rAthena item DB sources.


6. Monster table and monster AI mode system

tables/monsters_table.txt was regenerated and expanded with explicit monster AI flags.

The new table format includes:

ID
Name
Level
Hp
AttackRange
SkillRange
AttackDelay
AttackMotion
Size
Race
Element
ElementLevel
ChaseRange
Ai
isAIMode_Aggressive
isAIMode_Looter
isAIMode_Assist
isAIMode_CanMove
isAIMode_CastSensorIdle
isAIMode_CastSensorChase
isAIMode_MVP
isAIMode_KnockbackImmune
isAIMode_Detector
isAIMode_TakesFixed_1_Damage_Melee
isAIMode_TakesFixed_1_Damage_Ranged
isAIMode_TakesFixed_1_Damage_Magic
isAIMode_TakesFixed_1_Damage_None

The parser now validates:

  • exact column count;
  • numeric fields;
  • boolean AI-mode fields;
  • valid size values;
  • valid race values;
  • valid element values;
  • valid AI identifiers.

Malformed rows are dropped with explicit errors.

Generator updated:

tables/tools/mobdb_yml_to_monsters_table_txt.pl

7. New monster condition checks

New monster condition keys:

target_is_aggressive
target_is_aggressive_party

Example:

attackSkillSlot Provoke {
    lvl 10
    target_is_aggressive 0
    # [checkSelfCondition]
    # [checkMonsterCondition]
}

Example:

attackSkillSlot Safety Wall {
    lvl 10
    target_is_aggressive_party 1
    # [checkSelfCondition]
    # [checkMonsterCondition]
}

These allow config blocks to explicitly require or reject monsters considered aggressive in player or party context.


8. eCast AI-mode conditions

plugins/eCast/eCast.pl now supports monster AI-mode target conditions.

New keys:

target_isAIMode_Aggressive
target_isAIMode_Looter
target_isAIMode_Assist
target_isAIMode_CanMove
target_isAIMode_CastSensorIdle
target_isAIMode_CastSensorChase
target_isAIMode_MVP
target_isAIMode_KnockbackImmune
target_isAIMode_Detector
target_isAIMode_TakesFixed_1_Damage_Melee
target_isAIMode_TakesFixed_1_Damage_Ranged
target_isAIMode_TakesFixed_1_Damage_Magic
target_isAIMode_TakesFixed_1_Damage_None

Each accepts:

1 = require the flag to be present
0 = require the flag to be absent

Example: only cast on aggressive-AI monsters.

attackSkillSlot Fire Bolt {
    lvl 10
    target_isAIMode_Aggressive 1
    target_Element Earth
}

Example: avoid MVPs.

attackSkillSlot Cold Bolt {
    lvl 10
    target_isAIMode_MVP 0
}

Example: avoid monsters that take fixed 1 ranged damage.

attackSkillSlot Double Strafe {
    lvl 10
    target_isAIMode_TakesFixed_1_Damage_Ranged 0
}

9. checkAggressive plugin update

The checkAggressive plugin now uses generated monster AI flags instead of local hardcoded AI constants.

It now checks whether a monster is likely aggressive toward an actor by considering:

  • whether the monster is already engaged;
  • whether the monster is moving toward the actor;
  • whether the monster is facing the actor;
  • distance to the actor;
  • whether another attackable actor is between the monster and the actor.

New helper logic includes:

is_monster_engaged_with_us
is_looking_at_actor
get_attackable_actors
get_line_points_between
has_attackable_actor_between
is_aggressive_towards_actor

This should reduce false positives where a naturally aggressive monster is nearby but is likely targeting someone else.


10. Party cast tracking

New actor counters:

$actor->{castOnByParty}
$actor->{castOnToParty}

Meaning:

  • castOnByParty: a party-relevant actor casted on this actor.
  • castOnToParty: this actor casted on a party-relevant actor.

These are updated by countCastOn.

This improves logic for combat that starts through skill casts rather than damage or misses.

Affected logic includes:

is_aggressive
is_aggressive_slave
checkMonsterCleanness
slave_checkMonsterCleanness
processAutoAttack
AI::Slave::processAutoAttack

11. Cast-sensor aggressive logic

New config key:

attackAuto_considerAggressiveIfCastOnCastSensor 0

When enabled, OpenKore can treat cast-sensor monsters as aggressive when the player or party casts on them.

Example:

attackAuto 2
attackAuto_party 1
attackAuto_considerAggressiveIfCastOnCastSensor 1

This is useful for monsters with:

isAIMode_CastSensorIdle
isAIMode_CastSensorChase

Before this PR, these monsters might not be treated as aggressive unless damage/miss counters were updated.

After this PR, cast-sensor behavior can be included explicitly.


12. Attack logic changes

12.1 Attack combo handling

Combo handling now uses structured combo_state instead of only a raw combo_packet.

Old model:

$char->{combo_packet} = $args->{delay};

New model:

$char->{combo_state} = {
    delay        => $args->{delay},
    received_at  => $received_at,
    expires_at   => $received_at + ($args->{delay} / 1000),
    source_skill => $char->{last_skill_used},
    target_id    => $char->{last_skill_target},
};

The attack loop now uses the remaining combo window instead of waiting the full delay again.

This is important because combo windows are measured from packet arrival time.

Example config:

attackComboSlot Combo Finish {
    afterSkill Triple Attack
    waitBeforeUse 0
    dist 1
    maxDist 1
    lvl 10
    autoCombo 1
    isSelfSkill 0
    monsters
    notMonsters
    # [checkSelfCondition]
    # [checkMonsterCondition]
}

With autoCombo 1, OpenKore now uses the packet-provided combo window more accurately.


12.2 Combo target matching

The combo logic now checks:

  • last_skill_used;
  • combo_state->{source_skill};
  • combo_state->{target_id};
  • fallback to last_skill_target.

This makes combo handling more resilient when packets arrive very close together.


12.3 Temporary extra range no longer persists

The attack loop no longer permanently applies:

$args->{attackMethod}{maxDistance} += $args->{temporary_extra_range};

Reason:

# Keep the extra chase tolerance scoped to the loop where we actually
# proved we could hit out of nominal range. Persisting it here lets melee
# attacks get stuck spamming from clientDist 2 without re-approaching.

This prevents melee attacks from getting stuck trying to attack from an invalid distance.


12.4 resolve_movetoattack_pos fix

resolve_movetoattack_pos now uses the actor’s own movetoattack_pos.

Before:

$char->{movetoattack_pos}

After:

$actor->{movetoattack_pos}

13. Dead logic updates

processDead now logs and handles dead-state transitions more clearly.

13.1 Dead queue cleanup

When AI action is dead but the character is no longer dead:

[processDead] Clearing Dead status and dead AI queue sequence.

13.2 Stale Dead status cleanup

If the current AI action is not dead, the character is not dead, but the Dead status is still active, OpenKore now clears stale state:

AI::clear('dead');
$char->setStatus('Dead', 0);
$char->{resurrected} = 0 if $char->{resurrected};

13.3 Death detection log

[processDead] You died, clearing AI queue, queueing dead and setting status Dead.

13.4 Respawn log

[processDead] Sending respawn.

This helps debug death/respawn loops and stale dead-state issues.


14. Safe position prediction with _normalize_calc_pos_to_walkable

New helper:

_normalize_calc_pos_to_walkable

calcPosFromPathfinding predicts actor position using:

  • pos;
  • pos_to;
  • movement time;
  • walk speed;
  • pathfinding solution;
  • subcell data.

Sometimes predicted positions can be non-walkable due to:

  • stale field data;
  • server/client path mismatch;
  • outdated movement state;
  • local field data mismatch.

Now, if a predicted position is not walkable, OpenKore tries:

$field->closestWalkableSpot($calc_pos, 1)

then:

$field->closestWalkableSpot($calc_pos, 10)

If a fallback exists, it returns the closest walkable cell and logs once per unique fallback.

Example log:

[calcPosFromPathfinding] [Actor] Predicted position (120,55) is not walkable on local field 'pay_fild08'. Falling back to closest walkable cell (121,55). Local field data may be out of sync with the server.

This protects attack, follow, route, and aggression code from consuming impossible actor positions.


15. Follow updates

New helpers:

follow_route_needs_reset
start_follow
reset_follow

15.1 New plugin hook

Plugins::callHook('processFollow' => \%plugin_args);

Plugins can set:

$plugin_args{return} = 1;

to suppress default follow behavior.

15.2 Better route reset logic

Follow state now tracks:

masterLastMap
masterLastMovePosTo
masterLastMoveTime

Follow route is reset when:

  • master map changes;
  • master destination changes;
  • coordinate availability changes.

15.3 Direct movement when possible

start_follow can use direct movement instead of ai_route when:

  • master is on same map;
  • master has valid coordinates;
  • direct movement is possible.

15.4 Party-follow coordinate validation

New helper:

_followCoordsAreValid

Invalid coordinates are ignored when they are:

  • missing;
  • zero;
  • >= 65535;
  • off-map.

15.5 Party follow avoids respawn teleport

Party follow now passes:

noTeleSpawn => 1

when routing to the master.

This prevents follow behavior from using save-map respawn teleport as part of party follow.


16. NPC shop system

This PR adds a complete NPC shop cache/index system.

New table:

tables/npc_shops.txt

New parser:

parseNPCShops

New updater:

updateNPCShopFile

New runtime cache updater:

update_npc_shop_cache

New index compiler:

compileItemIDtoShops

New lookup helper:

get_closest_npc_shop_for_item

New command:

searchshop

New generator:

tables/tools/rathena_npcfolder_to_shops_table_txt.pl

16.1 npc_shops.txt format

npcmap,npcx,npcy,item1id:item1price,item2id:item2price,etc

Example:

prontera,150,150,501:50,502:200,602:300
payon,180,104,501:50,1750:2
geffen,120,65,717:400,716:500

16.2 Runtime shop cache updates

When OpenKore receives NPC shop data in npc_store_info, it now collects item IDs/prices and calls:

update_npc_shop_cache($npc_id, \@shop_items);

This:

  • finds the NPC;
  • gets current map;
  • gets NPC position;
  • validates item IDs and prices;
  • updates npc_shops.txt;
  • reloads the table.

This lets OpenKore learn shops during normal gameplay.


16.3 Item-to-shop index

When npc_shops.txt is loaded, OpenKore builds:

%itemIDtoShops

Example:

$itemIDtoShops{602} = [
    {
        map => 'prontera',
        x => 150,
        y => 150,
        itemsByID => {
            602 => 300,
        },
    },
];

16.4 Closest shop lookup

get_closest_npc_shop_for_item:

  1. validates item ID;
  2. checks current route context;
  3. finds all known shops selling the item;
  4. normalizes map names;
  5. removes duplicate route targets;
  6. creates Task::CalcMapRoute;
  7. calculates the closest reachable shop;
  8. returns shop metadata.

Returned data includes:

{
    shop        => $shop,
    map         => $chosen->{map},
    x           => int($chosen->{x}),
    y           => int($chosen->{y}),
    destination => "map x y",
    price       => $price,
    move_cost   => $move_cost,
    route       => $task->getRouteString(),
}

16.5 New searchshop command

searchshop <item name | item ID>

Examples:

searchshop Butterfly Wing
searchshop 602
searchshop Red Potion
searchshop 501

The command:

  • requires the character to be in game;
  • accepts item ID or item name;
  • resolves exact item names;
  • falls back to partial item-name matches;
  • detects ambiguous names;
  • calculates the closest known NPC shop;
  • prints item, map, coordinates, price, movement cost, and route.

Example output:

---------------------- Closest NPC Shop ----------------------
Item: Butterfly Wing (602)
Map : prontera
Pos : 150, 150
Price: 300z
Move Cost: 0z
Route: prontera 150 150
----------------------------------------------------------------

Example ambiguous item name:

Item name 'Potion' is ambiguous. Matches: Red Potion, Orange Potion, Yellow Potion, White Potion

Example missing shop:

No known NPC shop sells Butterfly Wing (602).

17. No-teleport map system

New table:

tables/no_teleport_maps.txt

New parser:

parseNoTeleportMaps

New global:

%no_teleport_maps

New helpers:

getNoTeleportMapFlags
isRandomTeleportBlockedOnMap
isTeleportSkillBlockedOnMap
isReturnTeleportBlockedOnMap

17.1 Table format

map noteleport noreturn

Example:

map noteleport noreturn
ordeal_1-1 1 1
job_thief1 1 0
que_ng 1 1

Meaning:

  • noteleport = 1: random teleport and teleport skill are blocked.
  • noreturn = 1: return-to-save-map teleport is blocked.

17.2 Affected systems

Random teleport now checks:

isRandomTeleportBlockedOnMap($field->baseName)

Return teleport now checks:

isReturnTeleportBlockedOnMap($field->baseName)

Teleport skill now checks:

isTeleportSkillBlockedOnMap($field->baseName)

Affected systems include:

  • Task::Teleport::Random;
  • Task::Teleport::Respawn;
  • canUseTeleport;
  • getTeleportItemFromTable;
  • getFlyWing;
  • getButterflyWing.

17.3 Example behavior

Example table:

map noteleport noreturn
pay_dun00 1 0
tur_dun03 1 1

Expected behavior:

  • On pay_dun00:

    • random teleport is blocked;
    • Fly Wing is blocked;
    • Teleport skill is blocked;
    • return teleport may still be allowed.
  • On tur_dun03:

    • random teleport is blocked;
    • Fly Wing is blocked;
    • Teleport skill is blocked;
    • Butterfly Wing / return teleport is blocked.

18. Dynamic portal and route calculation updates

Task::CalcMapRoute was heavily refactored.

It now better handles:

  • normal portals;
  • NPC warps;
  • airships;
  • @go commands;
  • save-map respawn;
  • warp items;
  • dynamic portal groups;
  • temporarily removed route sources;
  • zeny/ticket costs;
  • route branch state.

18.1 Dynamic portal syntax

Route entries and teleport items can now use:

[GroupName]
[^GroupName]

Meaning:

[GroupName]

marks a destination as belonging to a dynamic group.

[^GroupName]

marks a route step as blocking that group for the current branch.

Example:

moc_para01 30 10 prontera 116 72 [EdenPortalExit]
rachel 125 144 moc_para01 31 14 [^EdenPortalExit] 0 0 c c r0

Meaning:

  • the Eden exit to Prontera belongs to EdenPortalExit;
  • entering Eden from Rachel blocks EdenPortalExit for that route branch;
  • the route solver should not enter Eden from Rachel and then incorrectly use an Eden exit.

18.2 Branch-specific route state

The route key can now include blocked dynamic portal group state.

This allows two branches that reach the same route node to behave differently.

Example:

Branch A:
rachel -> Eden via blocker
blocked groups: EdenPortalExit

Branch B:
starts inside Eden
blocked groups: none

Branch A cannot use [EdenPortalExit].

Branch B can.


18.3 Dynamic portal group config

Dynamic portal groups can be controlled through config keys.

Example:

EdenPortalExit prontera

This can enable only destinations in that group that match the selected map/position.

Helpers involved:

refreshDynamicPortalGroups
applyDynamicPortalStates
getDynamicPortalDestinations

18.4 Warp items can block portal groups

Teleport items can also include dynamic group markers.

Example concept:

22508 warp moc_para01 171 115 1 0 1200 [^EdenPortalExit]

Meaning:

  • item 22508 warps to moc_para01 171 115;
  • using it blocks EdenPortalExit for that route branch.

18.5 Route source suspension instead of deletion

New helpers:

suspendRouteSource
restoreSuspendedRouteSource
isRouteSourceRemoved

Instead of deleting a failed portal source:

delete $portals_lut{$portal}

OpenKore now marks it:

$portals_lut{$portal}{removed} = 1

and queues it for later restoration.

This preserves:

  • original portal data;
  • destination data;
  • portals_los graph edges;
  • airship route data;
  • route graph consistency.

18.6 Safer route point checks

New helpers:

hasMapCoords
isRoutePointDefined
isRoutePointReachableOnField
isRouteSourceRemoved

These prevent route calculation from consuming incomplete, suspended, or unreachable entries.


18.7 Invalid route coordinates now fail loudly

Task::Route::getRoute now fails on invalid coordinates instead of treating them as success.

It logs diagnostics and prints callers when called with invalid start/destination coordinates.

This helps catch bugs where map-only route data accidentally reaches cell-route logic.


19. avoidObstacles portal route compatibility

plugins/avoidObstacles/avoidObstacles.pl now understands when a planned route intentionally uses a portal.

New helpers:

get_route_portal_positions
is_route_destination_portal

This prevents portal avoidance from dropping or blocking a route whose destination is itself a known portal cell.

Example:

Route destination: pay_fild08 20 30
Known portal cell: pay_fild08 20 30

Before:

  • avoidObstacles could drop the route because the destination was near a portal obstacle.

After:

  • OpenKore keeps the route because the destination is the intended portal.

20. New quit commands

quit 3

quit 3

Behavior:

  1. run s;
  2. run st;
  3. run skills;
  4. run exp report;
  5. send quit packet;
  6. exit.

Useful for getting a final debug/status snapshot before shutdown.


quit 4

quit 4

Same as quit 3, but disables dcPause for this shutdown:

$config{dcPause} = 0

Useful for immediate controlled shutdown.


21. talk resp debug improvement

The talk resp syntax error now includes the invalid argument.

Before:

Wrong talk resp sintax.

After:

Wrong talk resp sintax ('<arg>').

22. monsterSkill logic update

processMonsterSkillUse now iterates visible monsters directly:

for my $monster (@$monstersList)

instead of only using:

ai_getAggressives(1, 1)

This allows monsterSkill to be controlled by conditions more broadly.

Example:

monsterSkill Provoke {
    target
    lvl 10
    maxUses 1
    target_isAIMode_Aggressive 1
    target_is_aggressive 0
    # [checkSelfCondition]
    # [checkMonsterCondition]
}

23. Actor status visibility

Actor::setStatus now prints status changes for:

  • self;
  • owned slaves;

as messages, while other actors remain debug-level output.

This makes important self/slave status changes more visible without spamming normal logs with every actor status update.


24. Table loading changes

functions.pl now loads these optional tables:

item_hand_type.txt
npc_shops.txt
no_teleport_maps.txt

Table registration examples:

Settings::addTableFile('item_hand_type.txt',
    internalName => 'item_hand_type.txt',
    loader => [\&parseItemHandTypeTable, \%itemHandType_lut],
    mustExist => 0
);
Settings::addTableFile('npc_shops.txt',
    internalName => 'npc_shops.txt',
    loader => [\&parseNPCShops, \%npc_shops],
    mustExist => 0,
    onLoaded => \&compileItemIDtoShops
);
Settings::addTableFile('no_teleport_maps.txt',
    internalName => 'no_teleport_maps.txt',
    loader => [\&parseNoTeleportMaps, \%no_teleport_maps],
    mustExist => 0
);

Examples

Cast-sensor aggressive config
attackAuto 2
attackAuto_party 1
attackAuto_considerAggressiveIfCastOnCastSensor 1

This allows the bot to treat cast-sensor monsters as aggressive after the player or party casts on them.

Attack only non-MVP monsters
attackSkillSlot Soul Strike {
    lvl 10
    dist 8
    maxDist 9
    target_isAIMode_MVP 0
    # [checkSelfCondition]
    # [checkMonsterCondition]
}
Attack only aggressive-AI monsters
attackSkillSlot Bash {
    lvl 10
    dist 1
    maxDist 1
    target_isAIMode_Aggressive 1
    # [checkSelfCondition]
    # [checkMonsterCondition]
}
Avoid monsters with fixed 1 magic damage
attackSkillSlot Fire Bolt {
    lvl 10
    dist 8
    maxDist 9
    target_isAIMode_TakesFixed_1_Damage_Magic 0
    # [checkSelfCondition]
    # [checkMonsterCondition]
}
Use bow skill only with bow equipped
attackSkillSlot Double Strafe {
    lvl 10
    dist 8
    maxDist 10
    whenEquip_Right_Hand_Type Bow
    # [checkSelfCondition]
    # [checkMonsterCondition]
}
Use shield skill only with shield equipped
useSelf_skill Shield Reflect {
    lvl 10
    whenEquip_Left_Hand_Type Shield
    # [checkSelfCondition]
}
Use item and disconnect if empty
useSelf_item White Potion {
    hp < 40%
    dcOnEmpty 1
    # [checkSelfCondition]
}
Buy only if enough zeny and shop is near
buyAuto Butterfly Wing {
    npc prontera 150 150
    minAmount 5
    maxAmount 20
    zeny > 10000
    maxDistance 500
    dcOnEmpty 1
    # [checkSelfCondition]
}
Search known NPC shop
searchshop Butterfly Wing
searchshop 602

Expected output includes:

Item
Map
Pos
Price
Move Cost
Route
Dynamic portal group example

Portal table concept:

moc_para01 30 10 prontera 116 72 [EdenPortalExit]
moc_para01 30 10 geffen 120 60 [EdenPortalExit]
rachel 125 144 moc_para01 31 14 [^EdenPortalExit] 0 0 c c r0

Config:

EdenPortalExit prontera

Meaning:

  • only the Prontera Eden exit is enabled globally;
  • if the route enters Eden through a blocker, that route branch cannot use Eden exits.
No-teleport table example
map noteleport noreturn
tur_dun03 1 1
job_thief1 1 0

Expected behavior:

  • tur_dun03: no random teleport, no teleport skill, no return teleport.
  • job_thief1: no random teleport and no teleport skill, but return may still be allowed.

Tests added or updated

Updated/added test files:

src/test/DynamicPortalGroupsTest.pm
src/test/FileParsersTest.pm
src/test/HandConditionsTest.pm
src/test/dynamic_portals.txt
src/test/unittests.pl

Dynamic portal tests cover

  • parsing normal dynamic portal group markers;
  • parsing dynamic portal group block markers;
  • blocking tagged dynamic exits after a blocker portal;
  • preserving blocker state through internal route nodes;
  • blocking tagged dynamic exits after an Eden warp item;
  • suspended route source behavior;
  • restoration of suspended route sources;
  • portalExists and portalExists2 ignoring suspended sources.

Hand condition tests cover

  • right-hand type checks;
  • left-hand type checks;
  • empty hand behavior;
  • Fist behavior for empty hands;
  • mismatch behavior when equipped item type does not match the configured type.

Parser tests cover

  • new table parsers;
  • stricter monster table behavior;
  • new generated table formats.

Suggested test command:

perl src/test/unittests.pl

Manual test plan

Routing and portals

  • Route normally through a simple portal.
  • Route through an NPC warp.
  • Force NPC warp failure and verify the route source is temporarily suspended, not deleted.
  • Verify suspended route sources are restored after route_reAddMissingPortals.
  • Test a dynamic portal group where one branch blocks the group.
  • Test starting inside a dynamic portal area where no blocker was used.
  • Test route through a portal while avoidObstacles is enabled.

Warp items

  • Enable route_warpByItem.
  • Test a warp item that clearly improves route cost.
  • Test a warp item that should be rejected by route_warpItem_minGain.
  • Test a warp item on cooldown.
  • Test a warp item that blocks a dynamic portal group.
  • Verify route-cost cache does not grow unbounded.

No-teleport maps

  • Add a map to no_teleport_maps.txt with noteleport 1.
  • Verify Fly Wing/random teleport is not selected.
  • Verify Teleport skill is not selected.
  • Add a map with noreturn 1.
  • Verify Butterfly Wing/return teleport is not selected.
  • Verify route/escape logic does not repeatedly try blocked teleport methods.

NPC shop system

  • Open an NPC shop.
  • Confirm npc_shops.txt is updated.
  • Reload tables.
  • Run searchshop <item ID>.
  • Run searchshop <item name>.
  • Test ambiguous item names.
  • Verify closest shop route output.
  • Verify price and move cost output.

Attack/aggression

  • Test normal aggression from damage.
  • Test party aggression from party damage.
  • Test party aggression from party casts.
  • Test monster casting on party member.
  • Test cast-sensor monster with attackAuto_considerAggressiveIfCastOnCastSensor 0.
  • Test cast-sensor monster with attackAuto_considerAggressiveIfCastOnCastSensor 1.
  • Test slave attack behavior with party cast counters.
  • Test target_is_aggressive.
  • Test target_is_aggressive_party.

eCast

  • Test target_isAIMode_Aggressive.
  • Test target_isAIMode_MVP 0.
  • Test target_isAIMode_Detector.
  • Test fixed-1-damage AI mode flags.
  • Verify debug output when a target fails an AI-mode condition.

Attack combos

  • Test a combo skill with autoCombo 1.
  • Verify combo state is created on combo packet.
  • Verify combo state is synchronized with self-skill packet.
  • Verify follow-up uses remaining combo delay.
  • Verify expired combo state is cleared.
  • Verify combo target matching still works.

Follow

  • Test same-map follow.
  • Test direct movement follow.
  • Test route-based follow.
  • Test master moving during follow route.
  • Test party follow with invalid coordinates.
  • Test party follow across maps.
  • Verify party follow does not use save-map respawn teleport.

Dead logic

  • Die and verify dead AI action is queued.
  • Verify respawn packet is sent.
  • Verify stale Dead status is cleared after respawn.
  • Verify logs show death/respawn transitions.

Safe calc position

  • Test moving actors with stale path data.
  • Test calcPosFromPathfinding when predicted cell is not walkable.
  • Verify fallback to closest walkable cell.
  • Verify repeated identical fallback does not spam logs.

New files

plugins/eventMacro/eventMacro/Condition/QuestAllListInactive.pm
src/test/HandConditionsTest.pm
tables/item_hand_type.txt
tables/no_teleport_maps.txt
tables/npc_shops.txt
tables/tools/itemdb_equip_yml_to_item_hand_type_txt.pl
tables/tools/rathena_npcfolder_to_shops_table_txt.pl

Major modified files

control/config.txt
plugins/avoidObstacles/avoidObstacles.pl
plugins/checkAggressive/checkAggressive.pl
plugins/checkLooter/checkLooter.pl
plugins/eCast/eCast.pl
plugins/eventMacro/eventMacro/FileParser.pm
src/AI.pm
src/AI/Attack.pm
src/AI/CoreLogic.pm
src/AI/Slave.pm
src/Actor.pm
src/Commands.pm
src/FileParsers.pm
src/Globals.pm
src/Misc.pm
src/Network/Receive.pm
src/Task/CalcMapRoute.pm
src/Task/MapRoute.pm
src/Task/Route.pm
src/Task/Teleport/Random.pm
src/Task/Teleport/Respawn.pm
src/Utils.pm
src/functions.pl
tables/ROla/STATUS_id_handle.txt
tables/monsters_table.txt
tables/tools/mobdb_yml_to_monsters_table_txt.pl

Compatibility notes

Most new features are opt-in or table-driven.

Important notes:

  • attackAuto_considerAggressiveIfCastOnCastSensor defaults to 0.
  • Dynamic portal blocking only applies when route entries or teleport items use [^GroupName].
  • no_teleport_maps.txt is optional.
  • npc_shops.txt is optional.
  • item_hand_type.txt is optional.
  • Existing configs should continue to work, but config.txt now documents shared condition groups instead of repeating every condition in every block.
  • Custom monsters_table.txt files must follow the new strict tab-separated schema.
  • Task::Route::getRoute now fails on invalid coordinates instead of returning success with an empty solution.
  • Code that calls Task::Route::getRoute with missing x/y should be updated to use map-route logic instead.

Review focus

Please review carefully:

  • Task::CalcMapRoute dynamic route state keys.
  • Blocked dynamic portal group propagation.
  • Route-cost calculation for portal/NPC/airship/command/respawn/warp-item branches.
  • Warp-item heuristic and cache behavior.
  • Temporary route source suspension and restoration.
  • Interaction between suspended sources and portalExists / portalExists2.
  • Task::Route::getRoute behavior change for invalid coordinates.
  • New no_teleport_maps.txt restrictions.
  • canUseTeleport behavior for random vs return teleport.
  • npc_shops.txt update/reload behavior.
  • searchshop command item-name resolution and route calculation.
  • New item_hand_type.txt parser and hand condition behavior.
  • monsters_table.txt strict parser and generated AI flags.
  • Party cast counters and cast-sensor aggressive logic.
  • Attack combo timing and expiration behavior.
  • Follow reset logic and party-follow coordinate validation.
  • EventMacro newline preservation in Perl blocks.

Short changelog

  • Added QuestAllListInactive eventMacro condition.
  • Fixed eventMacro Perl sub parsing by preserving newlines.
  • Added dynamic portal group blocker syntax with [^GroupName].
  • Added branch-specific dynamic portal blocking in Task::CalcMapRoute.
  • Added temporary route source suspension/restoration.
  • Added warp-item route heuristic/cache settings.
  • Added no_teleport_maps.txt and teleport restriction helpers.
  • Added npc_shops.txt, NPC shop cache updates, and searchshop.
  • Added item_hand_type.txt and hand/equipment condition checks.
  • Expanded monsters_table.txt with AI-mode flags.
  • Added eCast AI-mode target conditions.
  • Added target_is_aggressive and target_is_aggressive_party.
  • Added party cast tracking through castOnByParty and castOnToParty.
  • Added cast-sensor aggressive support.
  • Improved attack combo timing with combo_state.
  • Improved follow and party-follow route reset behavior.
  • Improved dead-state cleanup.
  • Added safe walkable fallback for calcPosFromPathfinding.
  • Added quit 3 and quit 4.
  • Improved talk resp error output.

@Henrybk Henrybk merged commit 7f4dd29 into master May 13, 2026
9 checks passed
@Henrybk Henrybk deleted the bundle3 branch May 13, 2026 19:07
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.

2 participants