From f9aa1643a0875361705974e42132ff2f86a6e082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Bosc=C3=A1?= Date: Tue, 24 Mar 2026 00:23:18 -0300 Subject: [PATCH 1/3] Add MonstersTable API and refactor plugins Introduce MonstersTable.pm to centralize access to monsters_table data and provide helper APIs (monster_exists, monster_field, monster_hp, monster_level, monster_is_looter_by_ai, monster_is_aggressive_by_ai, compact backend management, etc.). Refactor plugins to use the new API instead of directly reading %monstersTable or duplicating AI flag logic: - plugins/checkAggressive: use MonstersTable functions, remove local AI constant/flag parsing, and use monster_level() in debug messages. - plugins/checkLooter: use monster_is_looter_by_ai(), log-warning path preserved, remove duplicated AI parsing. - plugins/eCast: use monster_field()/monster_hp()/monster_level(), add improved packet message HP injection, track element changes, fix map_calc_dir_xy argument bug, and rename onPacketSkillUseNoDmg -> onPacketSkillUseNoDamage for clarity. Startup change: initialize MonstersTable compact backend at load (purging legacy %monstersTable by default) via functions.pl to enable compact storage and precompute AI flags. Overall this centralizes monster data handling, removes duplicated AI constants/logic, and enables a compact backend for performance and memory improvements. --- plugins/checkAggressive/checkAggressive.pl | 44 ++-- plugins/checkLooter/checkLooter.pl | 39 +--- plugins/eCast/eCast.pl | 36 +-- src/MonstersTable.pm | 254 +++++++++++++++++++++ src/functions.pl | 4 + 5 files changed, 299 insertions(+), 78 deletions(-) create mode 100644 src/MonstersTable.pm diff --git a/plugins/checkAggressive/checkAggressive.pl b/plugins/checkAggressive/checkAggressive.pl index f03c7915d8..6cbc5f798c 100644 --- a/plugins/checkAggressive/checkAggressive.pl +++ b/plugins/checkAggressive/checkAggressive.pl @@ -9,8 +9,9 @@ # character or one of the character's slaves. # # How it works: -# - The plugin reads monster data from %monstersTable. -# - It uses the monster Ai value to determine whether the monster is aggressive. +# - The plugin reads monster data through MonstersTable helper functions. +# - It checks the monster AI mode through MonstersTable to determine whether +# the monster is aggressive. # - It then checks whether the monster is still clean. # - It also checks whether the monster is moving toward the character or a # slave before returning true. @@ -21,8 +22,7 @@ # # How to configure it: # This plugin does not require custom config.txt entries. -# Just enable it and make sure monsters_table.txt is loaded and includes the Ai -# column. +# Just enable it and make sure monsters_table.txt is loaded. # # Examples: # 1. Use this plugin to improve how OpenKore recognizes aggressive monsters @@ -32,7 +32,8 @@ # homunculus, mercenary, or other slave are also detected correctly. # # Notes: -# - This plugin depends on monsters_table.txt, not on a separate JSON database. +# - This plugin depends on monsters_table.txt via MonstersTable APIs, not on a +# separate JSON database. # - A monster must be aggressive by AI, clean, and moving toward you or a # slave before it is treated as aggressive by this plugin. # @@ -42,6 +43,7 @@ package checkAggressive; use Plugins; use Globals; use Log qw(message error debug warning); +use MonstersTable qw(monster_exists monster_is_aggressive_by_ai monster_level); Plugins::register('checkAggressive', 'checkAggressive', \&Unload, \&Unload); @@ -54,39 +56,19 @@ package checkAggressive; PLUGIN_NAME => 'checkAggressive', }; -my %ai_constant = ( - '01' => 0x81, '02' => 0x83, '03' => 0x1089, '04' => 0x3885, - '05' => 0x2085, '06' => 0, '07' => 0x108B, '08' => 0x7085, - '09' => 0x3095, '10' => 0x84, '11' => 0x84, '12' => 0x2085, - '13' => 0x308D, '17' => 0x91, '19' => 0x3095, '20' => 0x3295, - '21' => 0x3695, '24' => 0xA1, '25' => 0x1, '26' => 0xB695, - '27' => 0x8084, 'ABR_PASSIVE' => 0x21, 'ABR_OFFENSIVE' => 0xA5 -); - sub Unload { Plugins::delHooks($hooks); message "[".PLUGIN_NAME."] Plugin unloading or reloading.\n", 'success'; } -sub is_monster_ai_aggressive { - my ($ai_str) = @_; - $ai_str = uc($ai_str); - - my $mode_value = exists $ai_constant{$ai_str} - ? $ai_constant{$ai_str} - : $ai_constant{'06'}; - - return ($mode_value & 0x4) ? 1 : 0; -} - sub on_ai_check_Aggressiveness { my ($self, $args) = @_; my $monster = $args->{monster}; my $ID = $monster->{ID}; - return unless (exists $monstersTable{$monster->{nameID}}); - return unless (is_monster_ai_aggressive($monstersTable{$monster->{nameID}}{Ai})); + return unless (monster_exists($monster->{nameID})); + return unless monster_is_aggressive_by_ai($monster->{nameID}); my $found_clean = 0; my $found_moving = 0; @@ -101,7 +83,7 @@ sub on_ai_check_Aggressiveness { return unless ($found_clean && $found_moving); - debug "[".PLUGIN_NAME."] Monster $monster at ($monster->{pos}{x} $monster->{pos}{y}) | Lvl $monstersTable{$monster->{nameID}}{Level} | is Aggressive, clean, and coming to us\n"; + debug "[".PLUGIN_NAME."] Monster $monster at ($monster->{pos}{x} $monster->{pos}{y}) | Lvl ".monster_level($monster->{nameID})." | is Aggressive, clean, and coming to us\n"; $args->{return} = 1; return; @@ -114,14 +96,14 @@ sub on_ai_slave_check_Aggressiveness { my $ID = $monster->{ID}; my $slave = $args->{slave}; - return unless (exists $monstersTable{$monster->{nameID}}); - return unless (is_monster_ai_aggressive($monstersTable{$monster->{nameID}}{Ai})); + return unless (monster_exists($monster->{nameID})); + return unless monster_is_aggressive_by_ai($monster->{nameID}); return unless (Misc::slave_checkMonsterCleanness($slave, $ID) || Misc::checkMonsterCleanness($ID)); return unless (Misc::objectIsMovingTowards($monster, $slave) || Misc::objectIsMovingTowards($monster, $char)); - debug "[".PLUGIN_NAME."] Monster $monster at ($monster->{pos}{x} $monster->{pos}{y}) | Lvl $monstersTable{$monster->{nameID}}{Level} | is Aggressive towards slave, clean, and coming to him\n"; + debug "[".PLUGIN_NAME."] Monster $monster at ($monster->{pos}{x} $monster->{pos}{y}) | Lvl ".monster_level($monster->{nameID})." | is Aggressive towards slave, clean, and coming to him\n"; $args->{return} = 1; return; diff --git a/plugins/checkLooter/checkLooter.pl b/plugins/checkLooter/checkLooter.pl index 9f7ed789ad..9d3d35a350 100644 --- a/plugins/checkLooter/checkLooter.pl +++ b/plugins/checkLooter/checkLooter.pl @@ -10,10 +10,10 @@ # # How it works: # - When OpenKore runs the check_attackLooter hook, this plugin looks up the -# target monster in %monstersTable. -# - It reads the monster Ai value from monsters_table.txt. -# - It converts that AI type into mode flags and checks whether the MD_LOOTER -# bit is enabled. +# target monster through MonstersTable helper functions. +# - It reads the monster AI value from monsters_table.txt through +# monster_is_looter_by_ai. +# - It checks whether the looter flag is enabled for that AI mode. # - If the monster is not a looter, the plugin sets $args->{return} = 1. # # How to configure it: @@ -29,8 +29,8 @@ # behavior where extended checks and looter checks are handled separately. # # Notes: -# - This plugin depends on monsters_table.txt having Ai data for each monster. -# - If a monster is missing from %monstersTable, the plugin logs a warning and +# - This plugin depends on monsters_table.txt having AI data for each monster. +# - If a monster is missing from MonstersTable, the plugin logs a warning and # leaves the hook result unchanged. # package checkLooter; @@ -39,6 +39,7 @@ package checkLooter; use Plugins; use Globals; use Log qw(debug warning); +use MonstersTable qw(monster_exists monster_is_looter_by_ai); use constant { PLUGIN_NAME => 'checkLooter', @@ -50,15 +51,6 @@ package checkLooter; ['check_attackLooter', \&oncheck_attackLooter, undef], ); -my %ai_constant = ( - '01' => 0x81, '02' => 0x83, '03' => 0x1089, '04' => 0x3885, - '05' => 0x2085, '06' => 0, '07' => 0x108B, '08' => 0x7085, - '09' => 0x3095, '10' => 0x84, '11' => 0x84, '12' => 0x2085, - '13' => 0x308D, '17' => 0x91, '19' => 0x3095, '20' => 0x3295, - '21' => 0x3695, '24' => 0xA1, '25' => 0x1, '26' => 0xB695, - '27' => 0x8084, 'ABR_PASSIVE' => 0x21, 'ABR_OFFENSIVE' => 0xA5 -); - =pod /// Monster mode definitions to clear up code reading. [Skotlex] enum e_mode { @@ -102,14 +94,12 @@ sub oncheck_attackLooter { my ($hook, $args) = @_; return 0 if (!$args->{monster} || $args->{monster}->{nameID} eq ''); - if (!exists $monstersTable{$args->{monster}->{nameID}}) { + if (!monster_exists($args->{monster}->{nameID})) { warning "[checkLooter] Monster {name '$args->{monster}->{name}'} {ID '$args->{monster}->{nameID}'} not found\n", 'checkLooter'; return; } - my $mob = $monstersTable{$args->{monster}->{nameID}}; - my $ai = $mob->{Ai}; - my $is_looter = is_monster_ai_looter($ai); + my $is_looter = monster_is_looter_by_ai($args->{monster}->{nameID}); if (!$is_looter) { debug "[checkLooter] [False] $args->{monster} ($args->{monster}->{nameID}) is not a Looter\n", 'checkLooter'; $args->{return} = 1; @@ -118,15 +108,4 @@ sub oncheck_attackLooter { } } -sub is_monster_ai_looter { - my ($ai_str) = @_; - $ai_str = uc($ai_str); - - my $mode_value = exists $ai_constant{$ai_str} - ? $ai_constant{$ai_str} - : $ai_constant{'06'}; - - return ($mode_value & 0x2) ? 1 : 0; -} - 1; diff --git a/plugins/eCast/eCast.pl b/plugins/eCast/eCast.pl index 0c66f292c2..f32fd8ab1d 100644 --- a/plugins/eCast/eCast.pl +++ b/plugins/eCast/eCast.pl @@ -76,6 +76,7 @@ package eCast; use Translation qw(T TF); use Utils; use AI; +use MonstersTable qw(monster_exists monster_field monster_hp monster_level); use POSIX qw(floor); use constant { @@ -109,7 +110,8 @@ sub onUnload { Plugins::delHooks($hooks); } -# TODO: Revisar +# Extends default monster condition checks with additional database/runtime +# filters used by attackSkillSlot, equipAuto, attackComboSlot and monsterSkill. sub extendedCheck { my (undef, $args) = @_; @@ -129,18 +131,17 @@ sub extendedCheck { } } - if (!exists $monstersTable{$args->{monster}->{nameID}}) { + if (!monster_exists($args->{monster}->{nameID})) { Log::warning("eCast: Monster {name '$args->{monster}->{name}'} {ID '$args->{monster}->{nameID}'} not found\n", 'eCast'); return 0; } my $ID = $args->{monster}->{nameID}; - my $mob = $monstersTable{$args->{monster}->{nameID}}; - my $element = $mob->{Element}; - my $element_lvl = $mob->{ElementLevel}; - my $race = $mob->{Race}; - my $size = $mob->{Size}; + my $element = monster_field($ID, 'Element'); + my $element_lvl = monster_field($ID, 'ElementLevel'); + my $race = monster_field($ID, 'Race'); + my $size = monster_field($ID, 'Size'); if ($args->{monster}->{element} && $args->{monster}->{element} ne '') { $element = $args->{monster}->{element}; @@ -192,12 +193,12 @@ sub extendedCheck { } if ($config{$args->{prefix} . '_hpLeft'} - && !inRange(($mob->{HP} + $args->{monster}->{deltaHp}),$config{$args->{prefix} . '_hpLeft'})) { + && !inRange((monster_hp($ID) + $args->{monster}->{deltaHp}),$config{$args->{prefix} . '_hpLeft'})) { return $args->{return} = 0; } if ($config{$args->{prefix} . '_Level'} - && !inRange(($mob->{Level}),$config{$args->{prefix} . '_Level'})) { + && !inRange((monster_level($ID)),$config{$args->{prefix} . '_Level'})) { return $args->{return} = 0; } @@ -210,7 +211,7 @@ sub hasFreeCellBehind { my $charPos = calcPosFromPathfinding($field, $char); my $targetPos = calcPosFromPathfinding($field, $monster); - my $dir = map_calc_dir_xy($charPos->{x}, $charPos->{y}, $targetPos->{x}, $targetPos->{x}); + my $dir = map_calc_dir_xy($charPos->{x}, $charPos->{y}, $targetPos->{x}, $targetPos->{y}); my ($x, $y); @@ -275,11 +276,11 @@ sub map_calc_dir_xy { } } -# TODO: Revisar +# Add live HP information to packet messages when possible. sub onPacketSkillUse { monsterHp($monsters{$_[1]->{targetID}}, $_[1]->{disp}) if $_[1]->{disp} } -# TODO: Revisar -sub onPacketSkillUseNoDmg { +# Track element changes from self-targeted NPC_CHANGE* skills. +sub onPacketSkillUseNoDamage { my (undef,$args) = @_; return 1 unless $monsters{$args->{targetID}} && $monsters{$args->{targetID}}{nameID}; if ( @@ -292,16 +293,17 @@ sub onPacketSkillUseNoDmg { } } -# TODO: Revisar +# Add live HP information to normal attack packet messages when possible. sub onPacketAttack { monsterHp($monsters{$_[1]->{targetID}}, $_[1]->{msg}) if $_[1]->{msg} } -# TODO: Revisar +# Inject current/base HP information into a packet message buffer. sub monsterHp { my ($monster, $message) = @_; return 1 unless $monster && $monster->{nameID}; my $ID = int($monster->{nameID}); - return 1 unless my $monsterInfo = $monstersTable{$ID}; - $$message =~ s~(?=\n)~TF(" (Hp: %d/%d)", $monsterInfo->{HP} + $monster->{deltaHp}, $monsterInfo->{HP})~se; + my $base_hp = monster_hp($ID); + return 1 unless defined $base_hp; + $$message =~ s~(?=\n)~TF(" (Hp: %d/%d)", $base_hp + $monster->{deltaHp}, $base_hp)~se; } 1; diff --git a/src/MonstersTable.pm b/src/MonstersTable.pm new file mode 100644 index 0000000000..66b396d9b0 --- /dev/null +++ b/src/MonstersTable.pm @@ -0,0 +1,254 @@ +package MonstersTable; + +use strict; +use warnings; +use Exporter qw(import); +use Globals qw(%monstersTable); + +our @EXPORT_OK = qw( + monster_exists + monster_get + monster_field + monster_ai + monster_level + monster_hp + monster_race + monster_size + monster_element + monster_element_level + monster_is_looter_by_ai + monster_is_aggressive_by_ai + initialize_compact_backend + using_compact_backend + reset_backend_state +); + +my %compact_rows; +my %compact_enums; +my $use_compact_backend = 0; +my $loaded = 0; +my $loading = 0; +my %ai_flags; + +my %FIELD_INDEX = ( + Level => 0, + HP => 1, + AttackRange => 2, + SkillRange => 3, + AttackDelay => 4, + AttackMotion => 5, + Size => 6, + Race => 7, + Element => 8, + ElementLevel => 9, + ChaseRange => 10, + Ai => 11, +); + +my %AI_MODE = ( + '01' => 0x81, '02' => 0x83, '03' => 0x1089, '04' => 0x3885, + '05' => 0x2085, '06' => 0, '07' => 0x108B, '08' => 0x7085, + '09' => 0x3095, '10' => 0x84, '11' => 0x84, '12' => 0x2085, + '13' => 0x308D, '17' => 0x91, '19' => 0x3095, '20' => 0x3295, + '21' => 0x3695, '24' => 0xA1, '25' => 0x1, '26' => 0xB695, + '27' => 0x8084, 'ABR_PASSIVE' => 0x21, 'ABR_OFFENSIVE' => 0xA5 +); + +sub _ai_mode_value { + my ($ai) = @_; + $ai = defined $ai ? uc($ai) : '06'; + return exists $AI_MODE{$ai} ? $AI_MODE{$ai} : $AI_MODE{'06'}; +} + +sub _rebuild_ai_flags_cache { + %ai_flags = (); + if ($use_compact_backend) { + for my $id (keys %compact_rows) { + my $ai = $compact_enums{'Ai'}{ids}[$compact_rows{$id}[11]]; + my $mode = _ai_mode_value($ai); + $ai_flags{$id}{looter} = ($mode & 0x2) ? 1 : 0; + $ai_flags{$id}{aggressive} = ($mode & 0x4) ? 1 : 0; + } + return; + } + + for my $id (keys %monstersTable) { + next unless ref $monstersTable{$id} eq 'HASH'; + my $mode = _ai_mode_value($monstersTable{$id}{Ai}); + $ai_flags{$id}{looter} = ($mode & 0x2) ? 1 : 0; + $ai_flags{$id}{aggressive} = ($mode & 0x4) ? 1 : 0; + } +} + +sub _exists_raw { + my ($id) = @_; + return exists $compact_rows{$id} if $use_compact_backend; + return exists $monstersTable{$id}; +} + +sub _enum_id { + my ($type, $name) = @_; + return unless defined $name; + $compact_enums{$type}{ids} ||= []; + $compact_enums{$type}{names} ||= {}; + if (!exists $compact_enums{$type}{names}{$name}) { + my $id = scalar(@{$compact_enums{$type}{ids}}); + $compact_enums{$type}{names}{$name} = $id; + $compact_enums{$type}{ids}[$id] = $name; + } + return $compact_enums{$type}{names}{$name}; +} + +sub _ensure_loaded { + return 1 if $loaded; + return 0 if $loading; + $loaded = 1; + return 1; +} + +sub initialize_compact_backend { + my %args = @_; + my $purge_legacy = $args{purge_legacy} ? 1 : 0; + + %compact_rows = (); + %compact_enums = (); + + for my $id (keys %monstersTable) { + my $entry = $monstersTable{$id}; + next unless ref $entry eq 'HASH'; + + $compact_rows{$id} = [ + defined $entry->{Level} ? $entry->{Level} : 0, + defined $entry->{HP} ? $entry->{HP} : 0, + defined $entry->{AttackRange} ? $entry->{AttackRange} : 0, + defined $entry->{SkillRange} ? $entry->{SkillRange} : 0, + defined $entry->{AttackDelay} ? $entry->{AttackDelay} : 0, + defined $entry->{AttackMotion} ? $entry->{AttackMotion} : 0, + _enum_id('Size', defined $entry->{Size} ? $entry->{Size} : 'Small'), + _enum_id('Race', defined $entry->{Race} ? $entry->{Race} : 'Formless'), + _enum_id('Element', defined $entry->{Element} ? $entry->{Element} : 'Neutral'), + defined $entry->{ElementLevel} ? $entry->{ElementLevel} : 1, + defined $entry->{ChaseRange} ? $entry->{ChaseRange} : 0, + _enum_id('Ai', defined $entry->{Ai} ? uc($entry->{Ai}) : '06'), + ]; + } + + if ($purge_legacy) { + %monstersTable = (); + } + + $use_compact_backend = 1; + _rebuild_ai_flags_cache(); + return scalar(keys %compact_rows); +} + +sub using_compact_backend { + return $use_compact_backend; +} + +sub monster_exists { + my ($id) = @_; + _ensure_loaded(); + return 0 unless defined $id && $id ne ''; + return _exists_raw($id); +} + +sub monster_get { + my ($id) = @_; + _ensure_loaded(); + return unless defined $id && $id ne ''; + return unless _exists_raw($id); + return unless !$use_compact_backend; + return $monstersTable{$id}; +} + +sub monster_field { + my ($id, $field) = @_; + _ensure_loaded(); + return unless defined $field && $field ne ''; + return unless defined $id && $id ne ''; + return unless _exists_raw($id); + + if ($use_compact_backend) { + if (!exists $FIELD_INDEX{$field}) { + return; + } + my $idx = $FIELD_INDEX{$field}; + my $value = $compact_rows{$id}[$idx]; + if ($field eq 'Size' || $field eq 'Race' || $field eq 'Element' || $field eq 'Ai') { + return $compact_enums{$field}{ids}[$value]; + } + return $value; + } + + my $entry = monster_get($id); + return unless $entry; + return $entry->{$field}; +} + +sub monster_ai { + my ($id) = @_; + my $ai = monster_field($id, 'Ai'); + return defined $ai && $ai ne '' ? $ai : '06'; +} + +sub monster_level { + my ($id) = @_; + return monster_field($id, 'Level'); +} + +sub monster_hp { + my ($id) = @_; + return monster_field($id, 'HP'); +} + +sub monster_race { + my ($id) = @_; + return monster_field($id, 'Race'); +} + +sub monster_size { + my ($id) = @_; + return monster_field($id, 'Size'); +} + +sub monster_element { + my ($id) = @_; + return monster_field($id, 'Element'); +} + +sub monster_element_level { + my ($id) = @_; + return monster_field($id, 'ElementLevel'); +} + +sub monster_is_looter_by_ai { + my ($id) = @_; + _ensure_loaded(); + return 0 unless defined $id && $id ne ''; + return 0 unless _exists_raw($id); + return $ai_flags{$id}{looter} if exists $ai_flags{$id} && exists $ai_flags{$id}{looter}; + my $mode = _ai_mode_value(monster_ai($id)); + return ($mode & 0x2) ? 1 : 0; +} + +sub monster_is_aggressive_by_ai { + my ($id) = @_; + _ensure_loaded(); + return 0 unless defined $id && $id ne ''; + return 0 unless _exists_raw($id); + return $ai_flags{$id}{aggressive} if exists $ai_flags{$id} && exists $ai_flags{$id}{aggressive}; + my $mode = _ai_mode_value(monster_ai($id)); + return ($mode & 0x4) ? 1 : 0; +} + +sub reset_backend_state { + %compact_rows = (); + %compact_enums = (); + %ai_flags = (); + $use_compact_backend = 0; + $loaded = 0; + $loading = 0; +} + +1; diff --git a/src/functions.pl b/src/functions.pl index d92e19f7ac..94e3ac5472 100644 --- a/src/functions.pl +++ b/src/functions.pl @@ -416,6 +416,10 @@ sub loadDataFiles { Settings::update_log_filenames(); + require MonstersTable; + my $rows = MonstersTable::initialize_compact_backend(purge_legacy => 1); + message TF("MonstersTable compact backend enabled by default (%d rows) [legacy table purged]\n", $rows); + Plugins::callHook('start3'); if ($config{'secureAdminPassword'} eq '1') { From 235f1d0832e43f338de04b322960a0c5ca8d166b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Bosc=C3=A1?= Date: Tue, 24 Mar 2026 00:32:53 -0300 Subject: [PATCH 2/3] Add compact backend file loader for MonstersTable Introduce MonstersTable::load_compact_backend_from_file to load compact-format monster data directly from a file (with optional purge_legacy). The new loader parses compact rows, builds internal compact_rows/enum structures, enables the compact backend, and rebuilds AI flags. monster_get now warns once and returns early when the compact backend is enabled, and reset_backend_state clears that warning flag. In loadDataFiles, switch the Settings table loader for monsters_table.txt to a no-op and attempt a direct compact load from the file; if that fails or yields no rows, fall back to legacy parsing and then initialize the compact backend. The startup message now reports the number of rows and the source (direct-file or legacy-fallback). --- src/MonstersTable.pm | 77 +++++++++++++++++++++++++++++++++++++++++++- src/functions.pl | 33 +++++++++++++++++-- 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/MonstersTable.pm b/src/MonstersTable.pm index 66b396d9b0..c6a728962b 100644 --- a/src/MonstersTable.pm +++ b/src/MonstersTable.pm @@ -19,6 +19,7 @@ our @EXPORT_OK = qw( monster_is_looter_by_ai monster_is_aggressive_by_ai initialize_compact_backend + load_compact_backend_from_file using_compact_backend reset_backend_state ); @@ -29,6 +30,7 @@ my $use_compact_backend = 0; my $loaded = 0; my $loading = 0; my %ai_flags; +my $warned_monster_get_compact = 0; my %FIELD_INDEX = ( Level => 0, @@ -142,6 +144,72 @@ sub initialize_compact_backend { return scalar(keys %compact_rows); } +sub load_compact_backend_from_file { + my %args = @_; + my $file = $args{file}; + my $purge_legacy = $args{purge_legacy} ? 1 : 0; + return 0 unless defined $file && $file ne ''; + + require Utils::TextReader; + + %compact_rows = (); + %compact_enums = (); + + my $reader = Utils::TextReader->new($file); + while (!$reader->eof()) { + my $line = $reader->readLine(); + $line =~ s/^\s+|\s+$//g; + next if $line eq ''; + next if $line =~ /^#/; + next if $line =~ /^ID\s+Level\s+HP\s+AttackRange\s+SkillRange\s+AttackDelay\s+AttackMotion\s+Size\s+Race\s+Element\s+ElementLevel\s+ChaseRange(?:\s+Ai)?$/i; + + my @fields = split /\s+/, $line; + next unless @fields >= 12; + + my ( + $id, + $level, + $hp, + $attackRange, + $skillRange, + $attackDelay, + $attackMotion, + $size, + $race, + $element, + $elementLevel, + $chaseRange, + $ai + ) = @fields; + + next unless defined $id && $id =~ /^\d+$/; + + $compact_rows{$id} = [ + defined $level ? $level : 0, + defined $hp ? $hp : 0, + defined $attackRange ? $attackRange : 0, + defined $skillRange ? $skillRange : 0, + defined $attackDelay ? $attackDelay : 0, + defined $attackMotion ? $attackMotion : 0, + _enum_id('Size', defined $size ? $size : 'Small'), + _enum_id('Race', defined $race ? $race : 'Formless'), + _enum_id('Element', defined $element ? $element : 'Neutral'), + defined $elementLevel ? $elementLevel : 1, + defined $chaseRange ? $chaseRange : 0, + _enum_id('Ai', defined $ai ? uc($ai) : '06'), + ]; + } + + if ($purge_legacy) { + %monstersTable = (); + } + + $use_compact_backend = 1; + $loaded = 1; + _rebuild_ai_flags_cache(); + return scalar(keys %compact_rows); +} + sub using_compact_backend { return $use_compact_backend; } @@ -158,7 +226,13 @@ sub monster_get { _ensure_loaded(); return unless defined $id && $id ne ''; return unless _exists_raw($id); - return unless !$use_compact_backend; + if ($use_compact_backend) { + if (!$warned_monster_get_compact) { + warn "MonstersTable::monster_get is deprecated when compact backend is enabled; use monster_field()/helper APIs instead.\n"; + $warned_monster_get_compact = 1; + } + return; + } return $monstersTable{$id}; } @@ -249,6 +323,7 @@ sub reset_backend_state { $use_compact_backend = 0; $loaded = 0; $loading = 0; + $warned_monster_get_compact = 0; } 1; diff --git a/src/functions.pl b/src/functions.pl index 94e3ac5472..5279ac31c1 100644 --- a/src/functions.pl +++ b/src/functions.pl @@ -387,7 +387,7 @@ sub loadDataFiles { loader => [\&parseAchievementFile, \%achievements], mustExist => 0); Settings::addTableFile('monsters_table.txt', internalName => 'monsters_table.txt', - loader => [\&parseMonstersTableFile, \%monstersTable], mustExist => 0); + loader => [sub { 1 }, \%monstersTable], mustExist => 0); use utf8; @@ -417,8 +417,35 @@ sub loadDataFiles { Settings::update_log_filenames(); require MonstersTable; - my $rows = MonstersTable::initialize_compact_backend(purge_legacy => 1); - message TF("MonstersTable compact backend enabled by default (%d rows) [legacy table purged]\n", $rows); + my $rows = 0; + my $source = 'legacy-fallback'; + my $monsters_file = Settings::getTableFilename('monsters_table.txt'); + + if ($monsters_file) { + my $ok = eval { + $rows = MonstersTable::load_compact_backend_from_file( + file => $monsters_file, + purge_legacy => 1 + ); + 1; + }; + if (!$ok) { + warning TF("MonstersTable direct compact load failed (%s); falling back to legacy table conversion.\n", $@); + $rows = 0; + } elsif ($rows > 0) { + $source = 'direct-file'; + } + } + + if ($rows <= 0) { + if ($monsters_file) { + parseMonstersTableFile($monsters_file, \%monstersTable); + } + $rows = MonstersTable::initialize_compact_backend(purge_legacy => 1); + $source = 'legacy-fallback'; + } + + message TF("MonstersTable compact backend enabled (%d rows) [legacy table purged, source=%s]\n", $rows, $source); Plugins::callHook('start3'); From 90fb9c354e182fafa0fb58d81f0d477ee874f0f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Bosc=C3=A1?= Date: Tue, 24 Mar 2026 00:50:14 -0300 Subject: [PATCH 3/3] Add _as_number and replace magic Ai index Introduce _as_number to coerce numeric fields with optional fallback and use it to initialize numeric monster fields in initialize_compact_backend and load_compact_backend_from_file. Also replace the hard-coded Ai field index (11) with $FIELD_INDEX{Ai} in _rebuild_ai_flags_cache. These changes normalize undefined/empty values, ensure consistent defaults (e.g. ElementLevel fallback to 1), and remove a magic array index. --- src/MonstersTable.pm | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/MonstersTable.pm b/src/MonstersTable.pm index c6a728962b..bda01f1a41 100644 --- a/src/MonstersTable.pm +++ b/src/MonstersTable.pm @@ -56,6 +56,13 @@ my %AI_MODE = ( '27' => 0x8084, 'ABR_PASSIVE' => 0x21, 'ABR_OFFENSIVE' => 0xA5 ); +sub _as_number { + my ($value, $fallback) = @_; + $fallback = 0 unless defined $fallback; + return $fallback unless defined $value && $value ne ''; + return $value + 0; +} + sub _ai_mode_value { my ($ai) = @_; $ai = defined $ai ? uc($ai) : '06'; @@ -66,7 +73,7 @@ sub _rebuild_ai_flags_cache { %ai_flags = (); if ($use_compact_backend) { for my $id (keys %compact_rows) { - my $ai = $compact_enums{'Ai'}{ids}[$compact_rows{$id}[11]]; + my $ai = $compact_enums{'Ai'}{ids}[$compact_rows{$id}[$FIELD_INDEX{Ai}]]; my $mode = _ai_mode_value($ai); $ai_flags{$id}{looter} = ($mode & 0x2) ? 1 : 0; $ai_flags{$id}{aggressive} = ($mode & 0x4) ? 1 : 0; @@ -120,17 +127,17 @@ sub initialize_compact_backend { next unless ref $entry eq 'HASH'; $compact_rows{$id} = [ - defined $entry->{Level} ? $entry->{Level} : 0, - defined $entry->{HP} ? $entry->{HP} : 0, - defined $entry->{AttackRange} ? $entry->{AttackRange} : 0, - defined $entry->{SkillRange} ? $entry->{SkillRange} : 0, - defined $entry->{AttackDelay} ? $entry->{AttackDelay} : 0, - defined $entry->{AttackMotion} ? $entry->{AttackMotion} : 0, + _as_number($entry->{Level}), + _as_number($entry->{HP}), + _as_number($entry->{AttackRange}), + _as_number($entry->{SkillRange}), + _as_number($entry->{AttackDelay}), + _as_number($entry->{AttackMotion}), _enum_id('Size', defined $entry->{Size} ? $entry->{Size} : 'Small'), _enum_id('Race', defined $entry->{Race} ? $entry->{Race} : 'Formless'), _enum_id('Element', defined $entry->{Element} ? $entry->{Element} : 'Neutral'), - defined $entry->{ElementLevel} ? $entry->{ElementLevel} : 1, - defined $entry->{ChaseRange} ? $entry->{ChaseRange} : 0, + _as_number($entry->{ElementLevel}, 1), + _as_number($entry->{ChaseRange}), _enum_id('Ai', defined $entry->{Ai} ? uc($entry->{Ai}) : '06'), ]; } @@ -185,17 +192,17 @@ sub load_compact_backend_from_file { next unless defined $id && $id =~ /^\d+$/; $compact_rows{$id} = [ - defined $level ? $level : 0, - defined $hp ? $hp : 0, - defined $attackRange ? $attackRange : 0, - defined $skillRange ? $skillRange : 0, - defined $attackDelay ? $attackDelay : 0, - defined $attackMotion ? $attackMotion : 0, + _as_number($level), + _as_number($hp), + _as_number($attackRange), + _as_number($skillRange), + _as_number($attackDelay), + _as_number($attackMotion), _enum_id('Size', defined $size ? $size : 'Small'), _enum_id('Race', defined $race ? $race : 'Formless'), _enum_id('Element', defined $element ? $element : 'Neutral'), - defined $elementLevel ? $elementLevel : 1, - defined $chaseRange ? $chaseRange : 0, + _as_number($elementLevel, 1), + _as_number($chaseRange), _enum_id('Ai', defined $ai ? uc($ai) : '06'), ]; }