0% found this document useful (0 votes)
358 views79 pages

Misc PM

This document contains the summary for the OpenKore Miscellaneous module. It includes functions for modifying configurations, debugging, field math, inventory management, file parsing/writing, logging, actor actions text, AI math, and miscellaneous functions. The module imports and exports various functions to provide common utilities. It also contains validity checks to validate the internal state of variables.

Uploaded by

Arjinn Uchiha
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
358 views79 pages

Misc PM

This document contains the summary for the OpenKore Miscellaneous module. It includes functions for modifying configurations, debugging, field math, inventory management, file parsing/writing, logging, actor actions text, AI math, and miscellaneous functions. The module imports and exports various functions to provide common utilities. It also contains validity checks to validate the internal state of variables.

Uploaded by

Arjinn Uchiha
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 79

#########################################################################

# OpenKore - Miscellaneous functions


#
# This software is open source, licensed under the GNU General Public
# License, version 2.
# Basically, this means that you're allowed to modify and distribute
# this software. However, if you distribute modified versions, you MUST
# also distribute the source code.
# See http://www.gnu.org/licenses/gpl.html for the full license.
#
# $Revision: 7501 $
# $Id: Misc.pm 7501 2010-09-23 16:24:16Z kLabMouse $
#
#########################################################################
##
# MODULE DESCRIPTION: Miscellaneous functions
#
# This module contains functions that do not belong in any other modules.
# The difference between Misc.pm and Utils.pm is that Misc.pm can have
# dependencies on other Kore modules.
package Misc;
use strict;
use Exporter;
use Carp::Assert;
use Data::Dumper;
use Compress::Zlib;
use base qw(Exporter);
use encoding 'utf8';
use Globals;
use Log qw(message warning error debug);
use Plugins;
use FileParsers;
use Settings;
use Utils;
use Utils::Assert;
use Skill;
use Field;
use Network;
use Network::Send ();
use AI;
use Actor;
use Actor::You;
use Actor::Player;
use Actor::Monster;
use Actor::Party;
use Actor::NPC;
use Actor::Portal;
use Actor::Pet;
use Actor::Slave;
use Actor::Unknown;
use Time::HiRes qw(time usleep);
use Translation;
use Utils::Exceptions;
our @EXPORT = (
# Config modifiers
qw/auth
configModify
bulkConfigModify
setTimeout
saveConfigFile/,
# Debugging
qw/debug_showSpots
visualDump/,
# Field math
qw/calcRectArea
calcRectArea2
checkLineSnipable
checkLineWalkable
checkWallLength
closestWalkableSpot
objectInsideSpell
objectIsMovingTowards
objectIsMovingTowardsPlayer/,
# Inventory management
qw/inInventory
inventoryItemRemoved
storageGet
cardName
itemName
itemNameSimple/,
# File Parsing and Writing
qw/chatLog
shopLog
monsterLog/,
# Logging
qw/itemLog/,
# OS specific
qw/launchURL/,
# Misc
qw/
actorAdded
actorRemoved
actorListClearing
avoidGM_talk
avoidList_talk
avoidList_ID
calcStat
center
charSelectScreen
chatLog_clear
checkAllowedMap
checkFollowMode
checkMonsterCleanness
createCharacter
deal
dealAddItem
drop
dumpData
getEmotionByCommand
getIDFromChat
getNPCName
getPlayerNameFromCache
getPortalDestName
getResponse
getSpellName
headgearName
initUserSeed
itemLog_clear
look
lookAtPosition
manualMove
meetingPosition
objectAdded
objectRemoved
items_control
pickupitems
mon_control
monsterName
positionNearPlayer
positionNearPortal
printItemDesc
processNameRequestQueue
quit
relog
sendMessage
setSkillUseTimer
setPartySkillTimer
setStatus
countCastOn
stripLanguageCode
switchConfigFile
updateDamageTables
updatePlayerNameCache
useTeleport
top10Listing
whenGroundStatus
writeStorageLog
getBestTarget
isSafe
isSafeActorQuery/,
# Actor's Actions Text
qw/attack_string
skillCast_string
skillUse_string
skillUseLocation_string
skillUseNoDamage_string
status_string/,
# AI Math
qw/lineIntersection
percent_hp
percent_sp
percent_weight/,
# Misc Functions
qw/avoidGM_near
avoidList_near
compilePortals
compilePortals_check
portalExists
portalExists2
redirectXKoreMessages
monKilled
getActorName
getActorNames
findPartyUserID
getNPCInfo
skillName
checkSelfCondition
checkPlayerCondition
checkMonsterCondition
findCartItemInit
findCartItem
makeShop
openShop
closeShop
inLockMap
parseReload/
);

# use SelfLoader; 1;
# __DATA__

sub _checkActorHash($$$$) {
my ($name, $hash, $type, $hashName) = @_;
foreach my $actor (values %{$hash}) {
if (!UNIVERSAL::isa($actor, $type)) {
die "$name\nUnblessed item in $hashName list:\n" .
Dumper($hash);
}
}
}
# Checks whether the internal state of some variables are correct.
sub checkValidity {
return if (!DEBUG || $ENV{OPENKORE_NO_CHECKVALIDITY});
my ($name) = @_;
$name = "Validity check:" if (!defined $name);
assertClass($char, 'Actor::You') if ($net && $net->getState() == Network
::IN_GAME
&& $net->isa('Network::XKore'));
assertClass($char, 'Actor::You') if ($char);
return;
_checkActorHash($name, \%items, 'Actor::Item', 'item');
_checkActorHash($name, \%monsters, 'Actor::Monster', 'monster');
_checkActorHash($name, \%players, 'Actor::Player', 'player');
_checkActorHash($name, \%pets, 'Actor::Pet', 'pet');
_checkActorHash($name, \%npcs, 'Actor::NPC', 'NPC');
_checkActorHash($name, \%portals, 'Actor::Portal', 'portals');
}

#######################################
#######################################
### CATEGORY: Configuration modifiers
#######################################
#######################################
sub auth {
my $user = shift;
my $flag = shift;
if ($flag) {
message TF("Authorized user '%s' for admin\n", $user), "success"
;
} else {
message TF("Revoked admin privilages for user '%s'\n", $user), "
success";
}
$overallAuth{$user} = $flag;
writeDataFile(Settings::getControlFilename("overallAuth.txt"), \%overall
Auth);
}
##
# void configModify(String key, String value, ...)
# key: a key name.
# value: the new value.
#
# Changes the value of the configuration option $key to $value.
# Both %config and config.txt will be updated.
#
# You may also call configModify() with additional optional options:
# `l
# - autoCreate (boolean): Whether the configuration option $key
# should be created if it doesn't already exist.
# The default is true.
# - silent (boolean): By default, output will be printed, notifying the user
# that a config option has been changed. Setting this to
# true will surpress that output.
# `l`
sub configModify {
my $key = shift;
my $val = shift;
my %args;
if (@_ == 1) {
$args{silent} = $_[0];
} else {
%args = @_;
}
$args{autoCreate} = 1 if (!exists $args{autoCreate});
Plugins::callHook('configModify', {
key => $key,
val => $val,
additionalOptions => \%args
});
if (!$args{silent} && $key !~ /password/i) {
my $oldval = $config{$key};
if (!defined $oldval) {
$oldval = "not set";
}
if (!defined $val) {
message TF("Config '%s' unset (was %s)\n", $key, $oldval
), "info";
} else {
message TF("Config '%s' set to %s (was %s)\n", $key, $va
l, $oldval), "info";
}
}
if ($args{autoCreate} && !exists $config{$key}) {
my $f;
if (open($f, ">>", Settings::getConfigFilename())) {
print $f "$key\n";
close($f);
}
}
$config{$key} = $val;
saveConfigFile();
}
##
# bulkConfigModify (r_hash, [silent])
# r_hash: key => value to change
# silent: if set to 1, do not print a message to the console.
#
# like configModify but for more than one value at the same time.
sub bulkConfigModify {
my $r_hash = shift;
my $silent = shift;
my $oldval;
foreach my $key (keys %{$r_hash}) {
Plugins::callHook('configModify', {
key => $key,
val => $r_hash->{$key},
silent => $silent
});
$oldval = $config{$key};
$config{$key} = $r_hash->{$key};
if ($key =~ /password/i) {
message TF("Config '%s' set to %s (was *not-displayed*)\
n", $key, $r_hash->{$key}), "info" unless ($silent);
} else {
message TF("Config '%s' set to %s (was %s)\n", $key, $r_
hash->{$key}, $oldval), "info" unless ($silent);
}
}
saveConfigFile();
}
##
# saveConfigFile()
#
# Writes %config to config.txt.
sub saveConfigFile {
writeDataFileIntact(Settings::getConfigFilename(), \%config);
}
sub setTimeout {
my $timeout = shift;
my $time = shift;
message TF("Timeout '%s' set to %s (was %s)\n", $timeout, $time, $timeou
t{$timeout}{timeout}), "info";
$timeout{$timeout}{'timeout'} = $time;
writeDataFileIntact2(Settings::getControlFilename("timeouts.txt"), \%tim
eout);
}

#######################################
#######################################
### Category: Debugging
#######################################
#######################################
our %debug_showSpots_list;
sub debug_showSpots {
return unless $net->clientAlive();
my $ID = shift;
my $spots = shift;
my $special = shift;
if ($debug_showSpots_list{$ID}) {
foreach (@{$debug_showSpots_list{$ID}}) {
my $msg = pack("C*", 0x20, 0x01) . pack("V", $_);
$net->clientSend($msg);
}
}
my $i = 1554;
$debug_showSpots_list{$ID} = [];
foreach (@{$spots}) {
next if !defined $_;
my $msg = pack("C*", 0x1F, 0x01)
. pack("V*", $i, 1550)
. pack("v*", $_->{x}, $_->{y})
. pack("C*", 0x93, 0);
$net->clientSend($msg);
$net->clientSend($msg);
push @{$debug_showSpots_list{$ID}}, $i;
$i++;
}
if ($special) {
my $msg = pack("C*", 0x1F, 0x01)
. pack("V*", 1553, 1550)
. pack("v*", $special->{x}, $special->{y})
. pack("C*", 0x83, 0);
$net->clientSend($msg);
$net->clientSend($msg);
push @{$debug_showSpots_list{$ID}}, 1553;
}
}
##
# visualDump(data [, label])
#
# Show the bytes in $data on screen as hexadecimal.
# Displays the label if provided.
sub visualDump {
my ($msg, $label) = @_;
my $dump;
my $puncations = quotemeta '~!@#$%^&*()_-+=|\"\'';
no encoding 'utf8';
use bytes;
$dump = "================================================\n";
if (defined $label) {
$dump .= sprintf("%-15s [%d bytes] %s\n", $label, length($msg)
, getFormattedDate(int(time)));
} else {
$dump .= sprintf("%d bytes %s\n", length($msg), getFormattedDa
te(int(time)));
}
for (my $i = 0; $i < length($msg); $i += 16) {
my $line;
my $data = substr($msg, $i, 16);
my $rawData = '';
for (my $j = 0; $j < length($data); $j++) {
my $char = substr($data, $j, 1);
if (ord($char) < 32 || ord($char) > 126) {
$rawData .= '.';
} else {
$rawData .= substr($data, $j, 1);
}
}
$line = getHex(substr($data, 0, 8));
$line .= ' ' . getHex(substr($data, 8)) if (length($data) > 8
);
$line .= ' ' x (50 - length($line)) if (length($line) < 54);
$line .= " $rawData\n";
$line = sprintf("%3d> ", $i) . $line;
$dump .= $line;
}
message $dump;
}

#######################################
#######################################
### CATEGORY: Field math
#######################################
#######################################
##
# calcRectArea($x, $y, $radius)
# Returns: an array with position hashes. Each has contains an x and a y key.
#
# Creates a rectangle with center ($x,$y) and radius $radius,
# and returns a list of positions of the border of the rectangle.
sub calcRectArea {
my ($x, $y, $radius) = @_;
my (%topLeft, %topRight, %bottomLeft, %bottomRight);
sub capX {
return 0 if ($_[0] < 0);
return $field->width - 1 if ($_[0] >= $field->width);
return int $_[0];
}
sub capY {
return 0 if ($_[0] < 0);
return $field->height - 1 if ($_[0] >= $field->height);
return int $_[0];
}
# Get the avoid area as a rectangle
$topLeft{x} = capX($x - $radius);
$topLeft{y} = capY($y + $radius);
$topRight{x} = capX($x + $radius);
$topRight{y} = capY($y + $radius);
$bottomLeft{x} = capX($x - $radius);
$bottomLeft{y} = capY($y - $radius);
$bottomRight{x} = capX($x + $radius);
$bottomRight{y} = capY($y - $radius);
# Walk through the border of the rectangle
# Record the blocks that are walkable
my @walkableBlocks;
for (my $x = $topLeft{x}; $x <= $topRight{x}; $x++) {
if ($field->isWalkable($x, $topLeft{y})) {
push @walkableBlocks, {x => $x, y => $topLeft{y}};
}
}
for (my $x = $bottomLeft{x}; $x <= $bottomRight{x}; $x++) {
if ($field->isWalkable($x, $bottomLeft{y})) {
push @walkableBlocks, {x => $x, y => $bottomLeft{y}};
}
}
for (my $y = $bottomLeft{y} + 1; $y < $topLeft{y}; $y++) {
if ($field->isWalkable($topLeft{x}, $y)) {
push @walkableBlocks, {x => $topLeft{x}, y => $y};
}
}
for (my $y = $bottomRight{y} + 1; $y < $topRight{y}; $y++) {
if ($field->isWalkable($topRight{x}, $y)) {
push @walkableBlocks, {x => $topRight{x}, y => $y};
}
}
return @walkableBlocks;
}
##
# calcRectArea2($x, $y, $radius, $minRange)
# Returns: an array with position hashes. Each has contains an x and a y key.
#
# Creates a rectangle with center ($x,$y) and radius $radius,
# and returns a list of positions inside the rectangle that are
# not closer than $minRange to the center.
sub calcRectArea2 {
my ($cx, $cy, $r, $min) = @_;
my @rectangle;
for (my $x = $cx - $r; $x <= $cx + $r; $x++) {
for (my $y = $cy - $r; $y <= $cy + $r; $y++) {
next if distance({x => $cx, y => $cy}, {x => $x, y => $y
}) < $min;
push(@rectangle, {x => $x, y => $y});
}
}
return @rectangle;
}
##
# checkLineSnipable(from, to)
# from, to: references to position hashes.
#
# Check whether you can snipe a target standing at $to,
# from the position $from, without being blocked by any
# obstacles.
# TODO: move to Field?
sub checkLineSnipable {
return 0 if (!$field);
my $from = shift;
my $to = shift;
# Simulate tracing a line to the location (modified Bresenham's algorith
m)
my ($X0, $Y0, $X1, $Y1) = ($from->{x}, $from->{y}, $to->{x}, $to->{y});
my $steep;
my $posX = 1;
my $posY = 1;
if ($X1 - $X0 < 0) {
$posX = -1;
}
if ($Y1 - $Y0 < 0) {
$posY = -1;
}
if (abs($Y0 - $Y1) < abs($X0 - $X1)) {
$steep = 0;
} else {
$steep = 1;
}
if ($steep == 1) {
my $Yt = $Y0;
$Y0 = $X0;
$X0 = $Yt;
$Yt = $Y1;
$Y1 = $X1;
$X1 = $Yt;
}
if ($X0 > $X1) {
my $Xt = $X0;
$X0 = $X1;
$X1 = $Xt;
my $Yt = $Y0;
$Y0 = $Y1;
$Y1 = $Yt;
}
my $dX = $X1 - $X0;
my $dY = abs($Y1 - $Y0);
my $E = 0;
my $dE;
if ($dX) {
$dE = $dY / $dX;
} else {
# Delta X is 0, it only occures when $from is equal to $to
return 1;
}
my $stepY;
if ($Y0 < $Y1) {
$stepY = 1;
} else {
$stepY = -1;
}
my $Y = $Y0;
my $Erate = 0.99;
if (($posY == -1 && $posX == 1) || ($posY == 1 && $posX == -1)) {
$Erate = 0.01;
}
for (my $X=$X0;$X<=$X1;$X++) {
$E += $dE;
if ($steep == 1) {
return 0 if (!$field->isSnipable($Y, $X));
} else {
return 0 if (!$field->isSnipable($X, $Y));
}
if ($E >= $Erate) {
$Y += $stepY;
$E -= 1;
}
}
return 1;
}
##
# checkLineWalkable(from, to, [min_obstacle_size = 5])
# from, to: references to position hashes.
#
# Check whether you can walk from $from to $to in an (almost)
# straight line, without obstacles that are too large.
# Obstacles are considered too large, if they are at least
# the size of a rectangle with "radius" $min_obstacle_size.
# TODO: move to Field?
sub checkLineWalkable {
return 0 if (!$field);
my $from = shift;
my $to = shift;
my $min_obstacle_size = shift;
$min_obstacle_size = 5 if (!defined $min_obstacle_size);
my $dist = round(distance($from, $to));
my %vec;
getVector(\%vec, $to, $from);
# Simulate walking from $from to $to
for (my $i = 1; $i < $dist; $i++) {
my %p;
moveAlongVector(\%p, $from, \%vec, $i);
$p{x} = int $p{x};
$p{y} = int $p{y};
if ( !$field->isWalkable($p{x}, $p{y}) ) {
# The current spot is not walkable. Check whether
# this the obstacle is small enough.
if (checkWallLength(\%p, -1, 0, $min_obstacle_size) ||
checkWallLength(\%p, 1, 0, $min_obstacle_size)
|| checkWallLength(\%p, 0, -1, $min_obstacle_size) ||
checkWallLength(\%p, 0, 1, $min_obstacle_size)
|| checkWallLength(\%p, -1, -1, $min_obstacle_size) ||
checkWallLength(\%p, 1, 1, $min_obstacle_size)
|| checkWallLength(\%p, 1, -1, $min_obstacle_size) ||
checkWallLength(\%p, -1, 1, $min_obstacle_size)) {
return 0;
}
}
}
return 1;
}
sub checkWallLength {
my $pos = shift;
my $dx = shift;
my $dy = shift;
my $length = shift;
my $x = $pos->{x};
my $y = $pos->{y};
my $len = 0;
do {
last if ($x < 0 || $x >= $field->width || $y < 0 || $y >= $field
->height);
$x += $dx;
$y += $dy;
$len++;
} while (!$field->isWalkable($x, $y) && $len < $length);
return $len >= $length;
}
##
# closestWalkableSpot(r_field, pos)
# r_field: a reference to a field hash.
# pos: reference to a position hash (which contains 'x' and 'y' keys).
# Returns: 1 if %pos has been modified, 0 of not.
#
# If the position specified in $pos is walkable, this function will do nothing.
# If it's not walkable, this function will find the closest position that is wal
kable (up to 2 blocks away),
# and modify the x and y values in $pos.
# TODO: move to Field?
sub closestWalkableSpot {
my $field = shift;
my $pos = shift;
foreach my $z ( [0,0], [0,1],[1,0],[0,-1],[-1,0], [-1,1],[1,1],[1,-1],[-
1,-1],[0,2],[2,0],[0,-2],[-2,0] ) {
next if !$field->isWalkable($pos->{x} + $z->[0], $pos->{y} + $z-
>[1]);
$pos->{x} += $z->[0];
$pos->{y} += $z->[1];
return 1;
}
return 0;
}
##
# objectInsideSpell(object, [ignore_party_members = 1])
# object: reference to a player or monster hash.
#
# Checks whether an object is inside someone else's spell area.
# (Traps are also "area spells").
sub objectInsideSpell {
return 0 if ($config{'rabidDog'} || $config{'killSteal'});
my $object = shift;
my $ignore_party_members = shift;
$ignore_party_members = 1 if (!defined $ignore_party_members);
my ($x, $y) = ($object->{pos_to}{x}, $object->{pos_to}{y});
foreach (@spellsID) {
my $spell = $spells{$_};
if ((!$ignore_party_members || !$char->{party} || !$char->{party
}{users}{$spell->{sourceID}})
&& $spell->{sourceID} ne $accountID
&& $spell->{pos}{x} == $x && $spell->{pos}{y} == $y) {
return 1;
}
}
return 0;
}
##
# objectIsMovingTowards(object1, object2, [max_variance])
#
# Check whether $object1 is moving towards $object2.
sub objectIsMovingTowards {
my $obj = shift;
my $obj2 = shift;
my $max_variance = (shift || 15);
if (!timeOut($obj->{time_move}, $obj->{time_move_calc})) {
# $obj is still moving
my %vec;
getVector(\%vec, $obj->{pos_to}, $obj->{pos});
return checkMovementDirection($obj->{pos}, \%vec, $obj2->{pos_to
}, $max_variance);
}
return 0;
}
##
# objectIsMovingTowardsPlayer(object, [ignore_party_members = 1])
#
# Check whether an object is moving towards a player.
sub objectIsMovingTowardsPlayer {
return 0 if ($config{'rabidDog'} || $config{'killSteal'});
my $obj = shift;
my $ignore_party_members = shift;
$ignore_party_members = 1 if (!defined $ignore_party_members);
if (!timeOut($obj->{time_move}, $obj->{time_move_calc}) && @playersID) {
# Monster is still moving, and there are players on screen
my %vec;
getVector(\%vec, $obj->{pos_to}, $obj->{pos});
my $players = $playersList->getItems();
foreach my $player (@{$players}) {
my $ID = $player->{ID};
next if (
($ignore_party_members && $char->{party} && $char->
{party}{users}{$ID})
|| (defined($player->{name}) && existsInList($config{t
ankersList}, $player->{name}))
|| $player->statusActive('EFFECTSTATE_SPECIALHIDING'))
;
if (checkMovementDirection($obj->{pos}, \%vec, $player->
{pos}, 15)) {
return 1;
}
}
}
return 0;
}

#########################################
#########################################
### CATEGORY: Logging
#########################################
#########################################
# TODO: merge?
sub itemLog {
my $crud = shift;
return if (!$config{'itemHistory'});
open ITEMLOG, ">>:utf8", $Settings::item_log_file;
print ITEMLOG "[".getFormattedDate(int(time))."] $crud";
close ITEMLOG;
}
sub chatLog {
my $type = shift;
my $message = shift;
open CHAT, ">>:utf8", $Settings::chat_log_file;
print CHAT "[".getFormattedDate(int(time))."][".uc($type)."] $message";
close CHAT;
}
sub shopLog {
my $crud = shift;
open SHOPLOG, ">>:utf8", $Settings::shop_log_file;
print SHOPLOG "[".getFormattedDate(int(time))."] $crud";
close SHOPLOG;
}
sub monsterLog {
my $crud = shift;
return if (!$config{'monsterLog'});
open MONLOG, ">>:utf8", $Settings::monster_log_file;
print MONLOG "[".getFormattedDate(int(time))."] $crud\n";
close MONLOG;
}

#########################################
#########################################
### CATEGORY: Operating system specific
#########################################
#########################################

##
# launchURL(url)
#
# Open $url in the operating system's preferred web browser.
sub launchURL {
my $url = shift;
if ($^O eq 'MSWin32') {
require Utils::Win32;
Utils::Win32::ShellExecute(0, undef, $url);
} else {
my $mod = 'use POSIX;';
eval $mod;
# This is a script I wrote for the autopackage project
# It autodetects the current desktop environment
my $detectionScript = <<EOF;
function detectDesktop() {
if [[ "\$DISPLAY" = "" ]]; then
return 1
fi
local LC_ALL=C
local clients
if ! clients=`xlsclients`; then
return 1
fi
if echo "\$clients" | grep -qE '(gnome-panel|nau
tilus|metacity)'; then
echo gnome
elif echo "\$clients" | grep -qE '(kicker|slicke
r|karamba|kwin)'; then
echo kde
else
echo other
fi
return 0
}
detectDesktop
EOF
my ($r, $w, $desktop);
my $pid = IPC::Open2::open2($r, $w, '/bin/bash');
print $w $detectionScript;
close $w;
$desktop = <$r>;
$desktop =~ s/\n//;
close $r;
waitpid($pid, 0);
sub checkCommand {
foreach (split(/:/, $ENV{PATH})) {
return 1 if (-x "$_/$_[0]");
}
return 0;
}
if (checkCommand('xdg-open')) {
launchApp(1, 'xdg-open', $url);
} elsif ($desktop eq "gnome" && checkCommand('gnome-open')) {
launchApp(1, 'gnome-open', $url);
} elsif ($desktop eq "kde") {
launchApp(1, 'kfmclient', 'exec', $url);
} else {
if (checkCommand('firefox')) {
launchApp(1, 'firefox', $url);
} elsif (checkCommand('mozilla')) {
launchApp(1, 'mozilla', $url);
} else {
$interface->errorDialog(TF("No suitable browser
detected. Please launch your favorite browser and go to:\n%s", $url));
}
}
}
}

#######################################
#######################################
### CATEGORY: Other functions
#######################################
#######################################
# TODO: move actorAdded/Removed to Actor?
sub actorAddedRemovedVars {
my ($actor) = @_;
# returns (type, list, hash)
if ($actor->isa ('Actor::Item')) {
return ('item', \@itemsID, \%items);
} elsif ($actor->isa ('Actor::Player')) {
return ('player', \@playersID, \%players);
} elsif ($actor->isa ('Actor::Monster')) {
return ('monster', \@monstersID, \%monsters);
} elsif ($actor->isa ('Actor::Portal')) {
return ('portal', \@portalsID, \%portals);
} elsif ($actor->isa ('Actor::Pet')) {
return ('pet', \@petsID, \%pets);
} elsif ($actor->isa ('Actor::NPC')) {
return ('npc', \@npcsID, \%npcs);
} elsif ($actor->isa ('Actor::Slave')) {
return ('slave', \@slavesID, \%slaves);
} else {
return (undef, undef, undef);
}
}
sub actorAdded {
my (undef, $source, $arg) = @_;
my ($actor, $index) = @{$arg};
$actor->{binID} = $index;
my ($type, $list, $hash) = actorAddedRemovedVars ($actor);
if (defined $type) {
debug TF("actorAdded: %s %s (%s), size %s\n", $type, (unpack 'V'
, $actor->{ID}), $actor->{binID}, $source->size), 'actorlist', 3;
if (DEBUG && scalar(keys %{$hash}) + 1 != $source->size()) {
use Data::Dumper;
my $ol = '';
my $items = $source->getItems();
foreach my $item (@{$items}) {
$ol .= $item->nameIdx . "\n";
}
die "$type: " . scalar(keys %{$hash}) . " + 1 != " . $so
urce->size() . "\n" .
"List:\n" .
Dumper($list) . "\n" .
"Hash:\n" .
Dumper($hash) . "\n" .
"ObjectList:\n" .
$ol;
}
assert(binSize($list) + 1 == $source->size()) if DEBUG;
binAdd($list, $actor->{ID});
$hash->{$actor->{ID}} = $actor;
objectAdded($type, $actor->{ID}, $actor);
assert(scalar(keys %{$hash}) == $source->size()) if DEBUG;
assert(binSize($list) == $source->size()) if DEBUG;
} else {
warning "Unknown actor type in actorAdded\n", 'actorlist' if DEB
UG;
}
}
sub actorRemoved {
my (undef, $source, $arg) = @_;
my ($actor, $index) = @{$arg};
my ($type, $list, $hash) = actorAddedRemovedVars ($actor);
if (defined $type) {
debug TF("actorRemoved: %s %s (%s), size %s\n", $type, (unpack '
V', $actor->{ID}), $actor->{binID}, $source->size), 'actorlist', 3;
if (DEBUG && scalar(keys %{$hash}) - 1 != $source->size()) {
use Data::Dumper;
my $ol = '';
my $items = $source->getItems();
foreach my $item (@{$items}) {
$ol .= $item->nameIdx . "\n";
}
die "$type:" . scalar(keys %{$hash}) . " - 1 != " . $sou
rce->size() . "\n" .
"List:\n" .
Dumper($list) . "\n" .
"Hash:\n" .
Dumper($hash) . "\n" .
"ObjectList:\n" .
$ol;
}
assert(binSize($list) - 1 == $source->size()) if DEBUG;
binRemove($list, $actor->{ID});
delete $hash->{$actor->{ID}};
objectRemoved($type, $actor->{ID}, $actor);
if ($type eq "player") {
binRemove(\@venderListsID, $actor->{ID});
delete $venderLists{$actor->{ID}};
}
assert(scalar(keys %{$hash}) == $source->size()) if DEBUG;
assert(binSize($list) == $source->size()) if DEBUG;
} else {
warning "Unknown actor type in actorRemoved\n", 'actorlist' if D
EBUG;
}
}
sub actorListClearing {
undef %items;
undef %players;
undef %monsters;
undef %portals;
undef %npcs;
undef %pets;
undef %slaves;
undef @itemsID;
undef @playersID;
undef @monstersID;
undef @portalsID;
undef @npcsID;
undef @petsID;
undef @slavesID;
}
sub avoidGM_talk {
return 0 if ($net->clientAlive() || !$config{avoidGM_talk});
my ($user, $msg) = @_;
# Check whether this "GM" is on the ignore list
# in order to prevent false matches
return 0 if (existsInList($config{avoidGM_ignoreList}, $user));
if ($user =~ /^([a-z]?ro)?-?(Sub)?-?\[?GM\]?/i || $user =~ /$config{avoi
dGM_namePattern}/) {
my %args = (
name => $user,
);
Plugins::callHook('avoidGM_talk', \%args);
return 1 if ($args{return});
warning T("Disconnecting to avoid GM!\n");
main::chatLog("k", TF("*** The GM %s talked to you, auto disconn
ected ***\n", $user));
warning TF("Disconnect for %s seconds...\n", $config{avoidGM_rec
onnect});
relog($config{avoidGM_reconnect}, 1);
return 1;
}
return 0;
}
sub avoidList_talk {
return 0 if ($net->clientAlive() || !$config{avoidList});
my ($user, $msg, $ID) = @_;
if ($avoid{Players}{lc($user)}{disconnect_on_chat} || $avoid{ID}{$ID}{di
sconnect_on_chat}) {
warning TF("Disconnecting to avoid %s!\n", $user);
main::chatLog("k", TF("*** %s talked to you, auto disconnected *
**\n", $user));
warning TF("Disconnect for %s seconds...\n", $config{avoidList_r
econnect});
relog($config{avoidList_reconnect}, 1);
return 1;
}
return 0;
}
sub calcStat {
my $damage = shift;
$totaldmg += $damage;
}
##
# center(string, width, [fill])
#
# This function will center $string within a field $width characters wide,
# using $fill characters for padding on either end of the string for
# centering. If $fill is not specified, a space will be used.
sub center {
my ($string, $width, $fill) = @_;
$fill ||= ' ';
my $left = int(($width - length($string)) / 2);
my $right = ($width - length($string)) - $left;
return $fill x $left . $string . $fill x $right;
}
# Returns: 0 if user chose to quit, 1 if user chose a character, 2 if user creat
ed or deleted a character
sub charSelectScreen {
my %plugin_args = (autoLogin => shift);
# A list of character names
my @charNames;
# An array which maps an index in @charNames to an index in @chars
my @charNameIndices;
my $mode;
# the client also does this
$questList = {};
TOP: {
undef $mode;
@charNames = ();
@charNameIndices = ();
}
for (my $num = 0; $num < @chars; $num++) {
next unless ($chars[$num] && %{$chars[$num]});
if (0) {
# The old (more verbose) message
swrite(
T("------- Character \@< ---------\n" .
"Name: \@<<<<<<<<<<<<<<<<<<<<<<<<\n" .
"Job: \@<<<<<<< Job Exp: \@<<<<<<<\n" .
"Lv: \@<<<<<<< Str: \@<<<<<<<<\n" .
"J.Lv: \@<<<<<<< Agi: \@<<<<<<<<\n" .
"Exp: \@<<<<<<< Vit: \@<<<<<<<<\n" .
"HP: \@||||/\@|||| Int: \@<<<<<<<<\n" .
"SP: \@||||/\@|||| Dex: \@<<<<<<<<\n" .
"zeny: \@<<<<<<<<<< Luk: \@<<<<<<<<\n" .
"-------------------------------"),
$num, $chars[$num]{'name'}, $jobs_lut{$chars[$nu
m]{'jobID'}}, $chars[$num]{'exp_job'},
$chars[$num]{'lv'}, $chars[$num]{'str'}, $chars[
$num]{'lv_job'}, $chars[$num]{'agi'},
$chars[$num]{'exp'}, $chars[$num]{'vit'}, $chars
[$num]{'hp'}, $chars[$num]{'hp_max'},
$chars[$num]{'int'}, $chars[$num]{'sp'}, $chars[
$num]{'sp_max'}, $chars[$num]{'dex'},
$chars[$num]{'zeny'}, $chars[$num]{'luk'});
}
push @charNames, TF("Slot %d: %s (%s, level %d/%d)",
$num,
$chars[$num]{name},
$jobs_lut{$chars[$num]{'jobID'}},
$chars[$num]{lv},
$chars[$num]{lv_job});
push @charNameIndices, $num;
}
if (@charNames) {
message(TF("------------- Character List -------------\n" .
"%s\n" .
"------------------------------------------\n",
join("\n", @charNames)),
"connection");
}
return 1 if $net->clientAlive;
Plugins::callHook('charSelectScreen', \%plugin_args);
return $plugin_args{return} if ($plugin_args{return});
if ($plugin_args{autoLogin} && @chars && $config{char} ne "" && $chars[$
config{char}]) {
$messageSender->sendCharLogin($config{char});
$timeout{charlogin}{time} = time;
return 1;
}
my @choices = @charNames;
push @choices, T('Create a new character');
if (@chars) {
push @choices, T('Delete a character');
} else {
message T("There are no characters on this account.\n"), "connec
tion";
}
my $choice = $interface->showMenu(
T("Please choose a character or an action."), \@choices,
title => T("Character selection"));
if ($choice == -1) {
# User cancelled
quit();
return 0;
} elsif ($choice < @charNames) {
# Character chosen
configModify('char', $charNameIndices[$choice], 1);
$messageSender->sendCharLogin($config{char});
$timeout{charlogin}{time} = time;
return 1;
} elsif ($choice == @charNames) {
# 'Create character' chosen
$mode = "create";
} else {
# 'Delete character' chosen
$mode = "delete";
}
if ($mode eq "create") {
while (1) {
my $message = T("Please enter the desired properties for
your characters, in this form:\n" .
"(slot) \"(name)\" [ (str) (agi) (vit) (int) (de
x) (luk) [ (hairstyle) [(haircolor)] ] ]");
my $input = $interface->query($message);
unless ($input =~ /\S/) {
goto TOP;
} else {
my @args = parseArgs($input);
if (@args < 2) {
$interface->errorDialog(T("You didn't sp
ecify enough parameters."), 0);
next;
}
message TF("Creating character \"%s\" in slot \"
%s\"...\n", $args[1], $args[0]), "connection";
$timeout{charlogin}{time} = time;
last if (createCharacter(@args));
}
}
} elsif ($mode eq "delete") {
my $choice = $interface->showMenu(
T("Select the character you want to delete."),
\@charNames,
title => T("Delete character"));
if ($choice == -1) {
goto TOP;
}
my $charIndex = @charNameIndices[$choice];
my $email = $interface->query("Enter your email address.");
if (!defined($email)) {
goto TOP;
}
my $confirmation = $interface->showMenu(
TF("Are you ABSOLUTELY SURE you want to delete:\n%s", $c
harNames[$choice]),
[T("No, don't delete"), T("Yes, delete")],
title => T("Confirm delete"));
if ($confirmation != 1) {
goto TOP;
}
$messageSender->sendCharDelete($chars[$charIndex]{charID}, $emai
l);
message TF("Deleting character %s...\n", $chars[$charIndex]{name
}), "connection";
$AI::temp::delIndex = $charIndex;
$timeout{charlogin}{time} = time;
}
return 2;
}
sub chatLog_clear {
if (-f $Settings::chat_log_file) {
unlink($Settings::chat_log_file);
}
}
##
# checkAllowedMap($map)
#
# Checks whether $map is in $config{allowedMaps}.
# Disconnects if it is not, and $config{allowedMaps_reaction} != 0.
sub checkAllowedMap {
my $map = shift;
return unless $AI == 2;
return unless $config{allowedMaps};
return if existsInList($config{allowedMaps}, $map);
return if $config{allowedMaps_reaction} == 0;
warning TF("The current map (%s) is not on the list of allowed maps.\n",
$map);
main::chatLog("k", TF("** The current map (%s) is not on the list of all
owed maps.\n", $map));
main::chatLog("k", T("** Exiting...\n"));
quit();
}
##
# checkFollowMode()
# Returns: 1 if in follow mode, 0 if not.
#
# Check whether we're current in follow mode.
sub checkFollowMode {
my $followIndex;
if ($config{follow} && defined($followIndex = AI::findAction("follow")))
{
return 1 if (AI::args($followIndex)->{following});
}
return 0;
}
##
# boolean checkMonsterCleanness(Bytes ID)
# ID: the monster's ID.
# Requires: $ID is a valid monster ID.
#
# Checks whether a monster is "clean" (not being attacked by anyone).
sub checkMonsterCleanness {
return 1 if ($config{'rabidDog'} || $config{'killSteal'});
return 1 if (!$config{attackAuto});
my $ID = $_[0];
return 1 if ($playersList->getByID($ID));
my $monster = $monstersList->getByID($ID);
# If party attacked monster, or if monster attacked/missed party
if ($monster->{dmgFromParty} > 0 || $monster->{dmgToParty} > 0 || $monst
er->{missedToParty} > 0) {
return 1;
}
if ($config{aggressiveAntiKS}) {
# Aggressive anti-KS mode, for people who are paranoid about not
kill stealing.
# If we attacked the monster first, do not drop it, we are being
KSed
return 1 if ($monster->{dmgFromYou} || $monster->{missedFromYou}
);
# If others attacked the monster then always drop it, wether it
attacked us or not!
return 0 if (($monster->{dmgFromPlayer} && %{$monster->{dmgFromP
layer}})
|| ($monster->{missedFromPlayer} && %{$monster->{misse
dFromPlayer}})
|| (($monster->{castOnByPlayer}) && %{$monster->{castO
nByPlayer}})
|| (($monster->{castOnToPlayer}) && %{$monster->{castO
nToPlayer}}));
}
# If monster attacked/missed you
return 1 if ($monster->{'dmgToYou'} || $monster->{'missedYou'});
# If we're in follow mode
if (defined(my $followIndex = AI::findAction("follow"))) {
my $following = AI::args($followIndex)->{following};
my $followID = AI::args($followIndex)->{ID};
if ($following) {
# And master attacked monster, or the monster attacked/m
issed master
if ($monster->{dmgToPlayer}{$followID} > 0
|| $monster->{missedToPlayer}{$followID} > 0
|| $monster->{dmgFromPlayer}{$followID} > 0) {
return 1;
}
}
}
if (objectInsideSpell($monster)) {
# Prohibit attacking this monster in the future
$monster->{dmgFromPlayer}{$char->{ID}} = 1;
return 0;
}
#check party casting on mob
my $allowed = 1;
if (scalar(keys %{$monster->{castOnByPlayer}}) > 0)
{
foreach (keys %{$monster->{castOnByPlayer}})
{
my $ID1=$_;
my $source = Actor::get($_);
unless ( existsInList($config{tankersList}, $source->{na
me}) ||
($char->{party} && %{$char->{party}} && $char->{
party}{users}{$ID1} && %{$char->{party}{users}{$ID1}}))
{
$allowed = 0;
last;
}
}
}
# If monster hasn't been attacked by other players
if (scalar(keys %{$monster->{missedFromPlayer}}) == 0
&& scalar(keys %{$monster->{dmgFromPlayer}}) == 0
#&& scalar(keys %{$monster->{castOnByPlayer}}) == 0 #change to $allo
wed
&& $allowed
# and it hasn't attacked any other player
&& scalar(keys %{$monster->{missedToPlayer}}) == 0
&& scalar(keys %{$monster->{dmgToPlayer}}) == 0
&& scalar(keys %{$monster->{castOnToPlayer}}) == 0
) {
# The monster might be getting lured by another player.
# So we check whether it's walking towards any other player, but
only
# if we haven't already attacked the monster.
if ($monster->{dmgFromYou} || $monster->{missedFromYou}) {
return 1;
} else {
return !objectIsMovingTowardsPlayer($monster);
}
}
# The monster didn't attack you.
# Other players attacked it, or it attacked other players.
if ($monster->{dmgFromYou} || $monster->{missedFromYou}) {
# If you have already attacked the monster before, then consider
it clean
return 1;
}
# If you haven't attacked the monster yet, it's unclean.
return 0;
}
##
# boolean createCharacter(int slot, String name, int [str,agi,vit,int,dex,luk] =
5)
# slot: The slot in which to create the character (1st slot is 0).
# name: The name of the character to create.
# Returns: Whether the parameters are correct. Only a character creation command
# will be sent to the server if all parameters are correct.
#
# Create a new character. You must be currently connected to the character login
server.
sub createCharacter {
my $slot = shift;
my $name = shift;
my ($str,$agi,$vit,$int,$dex,$luk, $hair_style, $hair_color) = @_;
if (!@_) {
($str,$agi,$vit,$int,$dex,$luk) = (5,5,5,5,5,5);
}
if ($net->getState() != 3) {
$interface->errorDialog(T("We're not currently connected to the
character login server."), 0);
return 0;
} elsif ($slot !~ /^\d+$/) {
$interface->errorDialog(TF("Slot \"%s\" is not a valid number.",
$slot), 0);
return 0;
} elsif ($slot < 0 || $slot > 4) {
$interface->errorDialog(T("The slot must be comprised between 0
and 4."), 0); # TODO: private servers allow more slots
return 0;
} elsif ($chars[$slot]) {
$interface->errorDialog(TF("Slot %s already contains a character
(%s).", $slot, $chars[$slot]{name}), 0);
return 0;
} elsif (length($name) > 23) {
$interface->errorDialog(T("Name must not be longer than 23 chara
cters."), 0);
return 0;
} else {
for ($str,$agi,$vit,$int,$dex,$luk) {
if ($_ > 9 || $_ < 1) {
$interface->errorDialog(T("Stats must be compris
ed between 1 and 9."), 0);
return;
}
}
for ($str+$int, $agi+$luk, $vit+$dex) {
if ($_ != 10) {
$interface->errorDialog(T("The sums Str + Int, A
gi + Luk and Vit + Dex must all be equal to 10."), 0);
return;
}
}
$messageSender->sendCharCreate($slot, $name,
$str, $agi, $vit, $int, $dex, $luk,
$hair_style, $hair_color);
return 1;
}
}
##
# void deal(Actor::Player player)
# Requires: defined($player)
# Ensures: exists $outgoingDeal{ID}
#
# Sends $player a deal request.
sub deal {
my $player = $_[0];
assert(defined $player) if DEBUG;
assert(UNIVERSAL::isa($player, 'Actor::Player')) if DEBUG;
$outgoingDeal{ID} = $player->{ID};
$messageSender->sendDeal($player->{ID});
}
##
# dealAddItem($item, $amount)
#
# Adds $amount of $item to the current deal.
sub dealAddItem {
my ($item, $amount) = @_;
$messageSender->sendDealAddItem($item->{index}, $amount);
$currentDeal{lastItemAmount} = $amount;
}
##
# drop(itemIndex, amount)
#
# Drops $amount of the item specified by $itemIndex. If $amount is not specified
or too large, it defaults
# to the number of items you have.
sub drop {
my ($itemIndex, $amount) = @_;
my $item = $char->inventory->get($itemIndex);
if ($item) {
if (!$amount || $amount > $item->{amount}) {
$amount = $item->{amount};
}
$messageSender->sendDrop($item->{index}, $amount);
}
}
sub dumpData {
my $msg = shift;
my $silent = shift;
my $dump;
my $puncations = quotemeta '~!@#$%^&*()_+|\"\'';
$dump = "\n\n================================================\n" .
getFormattedDate(int(time)) . "\n\n" .
length($msg) . " bytes\n\n";
for (my $i = 0; $i < length($msg); $i += 16) {
my $line;
my $data = substr($msg, $i, 16);
my $rawData = '';
for (my $j = 0; $j < length($data); $j++) {
my $char = substr($data, $j, 1);
if (($char =~ /\W/ && $char =~ /\S/ && !($char =~ /[$pun
cations]/))
|| ($char eq chr(10) || $char eq chr(13) || $char eq
"\t")) {
$rawData .= '.';
} else {
$rawData .= substr($data, $j, 1);
}
}
$line = getHex(substr($data, 0, 8));
$line .= ' ' . getHex(substr($data, 8)) if (length($data) > 8
);
$line .= ' ' x (50 - length($line)) if (length($line) < 54);
$line .= " $rawData\n";
$line = sprintf("%3d> ", $i) . $line;
$dump .= $line;
}
open DUMP, ">> DUMP.txt";
print DUMP $dump;
close DUMP;
debug "$dump\n", "parseMsg", 2;
message T("Message Dumped into DUMP.txt!\n"), undef, 1 unless ($silent);
}
sub getEmotionByCommand {
my $command = shift;
foreach (keys %emotions_lut) {
if (existsInList($emotions_lut{$_}{command}, $command)) {
return $_;
}
}
return undef;
}
sub getIDFromChat {
my $r_hash = shift;
my $msg_user = shift;
my $match_text = shift;
my $qm;
if ($match_text !~ /\w+/ || $match_text eq "me" || $match_text eq "") {
foreach (keys %{$r_hash}) {
next if ($_ eq "");
if ($msg_user eq $r_hash->{$_}{name}) {
return $_;
}
}
} else {
foreach (keys %{$r_hash}) {
next if ($_ eq "");
$qm = quotemeta $match_text;
if ($r_hash->{$_}{name} =~ /$qm/i) {
return $_;
}
}
}
return undef;
}
##
# getNPCName(ID)
# ID: the packed ID of the NPC
# Returns: the name of the NPC
#
# Find the name of an NPC: could be NPC, monster, or unknown.
sub getNPCName {
my $ID = shift;
if ((my $npc = $npcsList->getByID($ID))) {
return $npc->name;
} elsif ((my $monster = $monstersList->getByID($ID))) {
return $monster->name;
} else {
return "Unknown #" . unpack("V1", $ID);
}
}
##
# getPlayerNameFromCache(player)
# player: an Actor::Player object.
# Returns: 1 on success, 0 if the player isn't in cache.
#
# Retrieve a player's name from cache and modify the player object.
sub getPlayerNameFromCache {
my ($player) = @_;
return if (!$config{cachePlayerNames});
my $entry = $playerNameCache{$player->{ID}};
return if (!$entry);
# Check whether the cache entry is too old or inconsistent.
# Default cache life time: 15 minutes.
if (timeOut($entry->{time}, $config{cachePlayerNames_duration}) || $play
er->{lv} != $entry->{lv} || $player->{jobID} != $entry->{jobID}) {
binRemove(\@playerNameCacheIDs, $player->{ID});
delete $playerNameCache{$player->{ID}};
compactArray(\@playerNameCacheIDs);
return 0;
}
$player->{name} = $entry->{name};
$player->{guild} = $entry->{guild} if ($entry->{guild});
return 1;
}
sub getPortalDestName {
my $ID = shift;
my %hash; # We only want unique names, so we use a hash
foreach (keys %{$portals_lut{$ID}{'dest'}}) {
my $key = $portals_lut{$ID}{'dest'}{$_}{'map'};
$hash{$key} = 1;
}
my @destinations = sort keys %hash;
return join('/', @destinations);
}
sub getResponse {
my $type = quotemeta shift;
my @keys;
foreach my $key (keys %responses) {
if ($key =~ /^$type\_\d+$/) {
push @keys, $key;
}
}
my $msg = $responses{$keys[int(rand(@keys))]};
$msg =~ s/\%\$(\w+)/$responseVars{$1}/eig;
return $msg;
}
sub getSpellName {
my $spell = shift;
return $spells_lut{$spell} || "Unknown $spell";
}
##
# inInventory($itemName, $quantity = 1)
#
# Returns the item's index (can be 0!) if you have at least $quantity units of t
he item
# specified by $itemName in your inventory.
# Returns nothing otherwise.
sub inInventory {
my ($itemIndex, $quantity) = @_;
$quantity ||= 1;
my $item = $char->inventory->getByName($itemIndex);
return if !$item;
return unless $item->{amount} >= $quantity;
return $item->{invIndex};
}
##
# inventoryItemRemoved($invIndex, $amount)
#
# Removes $amount of $invIndex from $char->{inventory}.
# Also prints a message saying the item was removed (unless it is an arrow you
# fired).
sub inventoryItemRemoved {
my ($invIndex, $amount) = @_;
my $item = $char->inventory->get($invIndex);
if (!$char->{arrow} || ($item && $char->{arrow} != $item->{index})) {
# This item is not an equipped arrow
message TF("Inventory Item Removed: %s (%d) x %d\n", $item->{nam
e}, $invIndex, $amount), "inventory";
}
$item->{amount} -= $amount;
$char->inventory->remove($item) if ($item->{amount} <= 0);
$itemChange{$item->{name}} -= $amount;
}
# Resolve the name of a card
sub cardName {
my $cardID = shift;
# If card name is unknown, just return ?number
my $card = $items_lut{$cardID};
return "?$cardID" if !$card;
$card =~ s/ Card$//;
return $card;
}
# Resolve the name of a monster
# This function will only look at the data in monsters.txt
# DO NOT USE THIS FUNCTION when you want to get the real name of a monster,
# servers can change this name internally use getNPCName instead.
sub monsterName {
my $ID = shift;
return 'Unknown' unless defined($ID);
return 'None' unless $ID;
return $monsters_lut{$ID} || "Unknown #$ID";
}
# Resolve the name of a simple item
sub itemNameSimple {
my $ID = shift;
return 'Unknown' unless defined($ID);
return 'None' unless $ID;
return $items_lut{$ID} || "Unknown #$ID";
}
##
# itemName($item)
#
# Resolve the name of an item. $item should be a hash with these keys:
# nameID => integer index into %items_lut
# cards => 8-byte binary data as sent by server
# upgrade => integer upgrade level
sub itemName {
my $item = shift;
my $name = itemNameSimple($item->{nameID});
# Resolve item prefix/suffix (carded or forged)
my $prefix = "";
my $suffix = "";
my @cards;
my %cards;
for (my $i = 0; $i < 4; $i++) {
my $card = unpack("v1", substr($item->{cards}, $i*2, 2));
next unless $card;
push(@cards, $card);
($cards{$card} ||= 0) += 1;
}
if ($cards[0] == 254) {
# Alchemist-made potion
#
# Ignore the "cards" inside.
} elsif ($cards[0] == 65280) {
# Pet egg
# cards[0] == 65280
# substr($item->{cards}, 2, 4) = packed pet ID
# cards[3] == 1 if named, 0 if not named
} elsif ($cards[0] == 255) {
# Forged weapon
#
# Display e.g. "VVS Earth" or "Fire"
my $elementID = $cards[1] % 10;
my $elementName = $elements_lut{$elementID};
my $starCrumbs = ($cards[1] >> 8) / 5;
$prefix .= ('V'x$starCrumbs)."S " if $starCrumbs;
$prefix .= "$elementName " if ($elementName ne "");
} elsif (@cards) {
# Carded item
#
# List cards in alphabetical order.
# Stack identical cards.
# e.g. "Hydra*2,Mummy*2", "Hydra*3,Mummy"
$suffix = join(':', map {
cardName($_).($cards{$_} > 1 ? "*$cards{$_}" : '')
} sort { cardName($a) cmp cardName($b) } keys %cards);
}
my $numSlots = $itemSlotCount_lut{$item->{nameID}} if ($prefix eq "");
my $display = "";
$display .= "BROKEN " if $item->{broken};
$display .= "+$item->{upgrade} " if $item->{upgrade};
$display .= $prefix if $prefix;
$display .= $name;
$display .= " [$suffix]" if $suffix;
$display .= " [$numSlots]" if $numSlots;
return $display;
}
##
# storageGet(items, max)
# items: reference to an array of storage item hashes.
# max: the maximum amount to get, for each item, or 0 for unlimited.
#
# Get one or more items from storage.
#
# Example:
# # Get items $a and $b from storage.
# storageGet([$a, $b]);
# # Get items $a and $b from storage, but at most 30 of each item.
# storageGet([$a, $b], 30);
sub storageGet {
my $indices = shift;
my $max = shift;
if (@{$indices} == 1) {
my ($item) = @{$indices};
if (!defined($max) || $max > $item->{amount}) {
$max = $item->{amount};
}
$messageSender->sendStorageGet($item->{index}, $max);
} else {
my %args;
$args{items} = $indices;
$args{max} = $max;
$args{timeout} = 0.15;
AI::queue("storageGet", \%args);
}
}
##
# headgearName(lookID)
#
# Resolves a lookID of a headgear into a human readable string.
#
# A lookID corresponds to a line number in tables/headgears.txt.
# The number on that line is the itemID for the headgear.
sub headgearName {
my ($lookID) = @_;
return "Nothing" if $lookID == 0;
my $itemID = $headgears_lut[$lookID];
if (!defined($itemID)) {
return "Unknown lookID $lookID";
}
return main::itemName({nameID => $itemID});
}
##
# void initUserSeed()
#
# Generate a unique seed for the current user and save it to
# a file, or load the seed from that file if it exists.
sub initUserSeed {
my $seedFile = "$Settings::logs_folder/seed.txt";
my $f;
if (-f $seedFile) {
if (open($f, "<", $seedFile)) {
binmode $f;
$userSeed = <$f>;
$userSeed =~ s/\n.*//s;
close($f);
} else {
$userSeed = '0';
}
} else {
$userSeed = '';
for (0..10) {
$userSeed .= rand(2 ** 49);
}
if (open($f, ">", $seedFile)) {
binmode $f;
print $f $userSeed;
close($f);
}
}
}
sub itemLog_clear {
if (-f $Settings::item_log_file) { unlink($Settings::item_log_file); }
}
##
# look(bodydir, [headdir])
# bodydir: a number 0-7. See directions.txt.
# headdir: 0 = look directly, 1 = look right, 2 = look left
#
# Look in the given directions.
sub look {
my %args = (
look_body => shift,
look_head => shift
);
AI::queue("look", \%args);
}
##
# lookAtPosition(pos, [headdir])
# pos: a reference to a coordinate hash.
# headdir: 0 = face directly, 1 = look right, 2 = look left
#
# Turn face and body direction to position %pos.
sub lookAtPosition {
my $pos2 = shift;
my $headdir = shift;
my %vec;
my $direction;
getVector(\%vec, $pos2, $char->{pos_to});
$direction = int(sprintf("%.0f", (360 - vectorToDegree(\%vec)) / 45)) %
8;
look($direction, $headdir);
}
##
# manualMove(dx, dy)
#
# Moves the character offset from its current position.
sub manualMove {
my ($dx, $dy) = @_;
# Stop following if necessary
if ($config{'follow'}) {
configModify('follow', 0);
AI::clear('follow');
}
# Stop moving if necessary
AI::clear(qw/move route mapRoute/);
main::ai_route($field->name, $char->{pos_to}{x} + $dx, $char->{pos_to}{y
} + $dy);
}
##
# meetingPosition(ID, attackMaxDistance)
# ID: ID of the character to meet.
# attackMaxDistance: attack distance based on attack method.
#
# Returns: the position where the character should go to meet a moving monster.
sub meetingPosition {
my ($target, $attackMaxDistance) = @_;
my $monsterSpeed = ($target->{walk_speed}) ? 1 / $target->{walk_speed} :
0;
my $timeMonsterMoves = time - $target->{time_move};
my %monsterPos;
$monsterPos{x} = $target->{pos}{x};
$monsterPos{y} = $target->{pos}{y};
my %monsterPosTo;
$monsterPosTo{x} = $target->{pos_to}{x};
$monsterPosTo{y} = $target->{pos_to}{y};
my %realMonsterPos = calcPosFromTime(\%monsterPos, \%monsterPosTo, $mons
terSpeed, $timeMonsterMoves);
my $mySpeed = ($char->{walk_speed}) ? 1 / $char->{walk_speed} : 0;
my $timeCharMoves = time - $char->{time_move};
my %myPos;
$myPos{x} = $char->{pos}{x};
$myPos{y} = $char->{pos}{y};
my %myPosTo;
$myPosTo{x} = $char->{pos_to}{x};
$myPosTo{y} = $char->{pos_to}{y};
my %realMyPos = calcPosFromTime(\%myPos, \%myPosTo, $mySpeed, $timeCharM
oves);
my $timeMonsterWalks;
my $timeCharWalks;
my %monsterStep;
my %charStep;
# There can not be zero step if monster moves
for (my $monsterStep = 1; $monsterStep <= countSteps(\%realMonsterPos, \
%monsterPosTo); $monsterStep++) {
# Calculate the steps
%monsterStep = moveAlong(\%realMonsterPos, \%monsterPosTo, $mons
terStep);
# Calculate time to walk for monster
$timeMonsterWalks = calcTime(\%realMonsterPos, \%monsterStep, $m
onsterSpeed);
# Character's route to monsterStep position
for (my $charStep = 0; $charStep <= countSteps(\%realMyPos, \%mo
nsterStep); $charStep++) {
# Calculate the steps
%charStep = moveAlong(\%realMyPos, \%monsterStep, $charS
tep);
# Check whether the distance is fine
if (round(distance(\%charStep, \%monsterStep)) <= $attac
kMaxDistance) {
# Calculate time to walk for char
$timeCharWalks = calcTime(\%realMyPos, \%charSte
p, $mySpeed);
# Check whether character comes earlier or at th
e same time
if ($timeCharWalks <= $timeMonsterWalks) {
return \%charStep;
}
}
}
}
# If the monster is too fast, move to its pos_to plus attackMaxDistance
for (my $charStep = 0; $charStep <= countSteps(\%realMyPos, \%monsterPos
To); $charStep++) {
# Calculate the steps
%charStep = moveAlong(\%realMyPos, \%monsterPosTo, $charStep);
# Check whether the distance is fine
if (round(distance(\%charStep, \%monsterPosTo)) <= $attackMaxDis
tance) {
last;
}
}
return \%charStep;
}
sub objectAdded {
my ($type, $ID, $obj) = @_;
if ($type eq 'player' || $type eq 'slave') {
# Try to retrieve the player name from cache.
if (!getPlayerNameFromCache($obj)) {
push @unknownPlayers, $ID;
}
} elsif ($type eq 'npc') {
push @unknownNPCs, $ID;
}
if ($type eq 'monster') {
if (mon_control($obj->{name},$obj->{nameID})->{teleport_search})
{
$ai_v{temp}{searchMonsters}++;
}
}
Plugins::callHook('objectAdded', {
type => $type,
ID => $ID,
obj => $obj
});
}
sub objectRemoved {
my ($type, $ID, $obj) = @_;
if ($type eq 'monster') {
if (mon_control($obj->{name},$obj->{nameID})->{teleport_search})
{
$ai_v{temp}{searchMonsters}--;
}
}
Plugins::callHook('objectRemoved', {
type => $type,
ID => $ID
});
}
##
# items_control($name)
#
# Returns the items_control.txt settings for item name $name.
# If $name has no specific settings, use 'all'.
sub items_control {
my ($name) = @_;
return $items_control{lc($name)} || $items_control{all} || {};
}
##
# mon_control($name)
#
# Returns the mon_control.txt settings for monster name $name.
# If $name has no specific settings, use 'all'.
sub mon_control {
my $name = shift;
my $nameID = shift;
return $mon_control{lc($name)} || $mon_control{$nameID} || $mon_control{
all} || { attack_auto => 1 };
}
##
# pickupitems($name)
#
# Returns the pickupitems.txt settings for item name $name.
# If $name has no specific settings, use 'all'.
sub pickupitems {
my ($name) = @_;
return ($pickupitems{lc($name)} ne '') ? $pickupitems{lc($name)} : $pick
upitems{all};
}
sub positionNearPlayer {
return 0 if ($config{'rabidDog'} || $config{'killSteal'});
my $r_hash = shift;
my $dist = shift;
my $players = $playersList->getItems();
foreach my $player (@{$players}) {
my $ID = $player->{ID};
next if ($char->{party} && $char->{party}{users} &&
$char->{party}{users}{$ID});
next if (defined($player->{name}) && existsInList($config{tanker
sList}, $player->{name}));
return 1 if (distance($r_hash, $player->{pos_to}) <= $dist);
}
return 0;
}
sub positionNearPortal {
my $r_hash = shift;
my $dist = shift;
my $portals = $portalsList->getItems();
foreach my $portal (@{$portals}) {
return 1 if (distance($r_hash, $portal->{pos}) <= $dist);
}
return 0;
}
##
# printItemDesc(itemID)
#
# Print the description for $itemID.
sub printItemDesc {
my $itemID = shift;
my $itemName = itemNameSimple($itemID);
my $description = $itemsDesc_lut{$itemID} || T("Error: No description av
ailable.\n");
message TF("===============Item Description===============\nItem: %s\n\n
", $itemName), "info";
message($description, "info");
message("==============================================\n", "info");
}
sub processNameRequestQueue {
my ($queue, $actorLists, $foo) = @_;
while (@{$queue}) {
my $ID = $queue->[0];
my $actor;
foreach my $actorList (@$actorLists) {
last if $actor = $actorList->getByID($ID);
}
# Some private servers ban you if you request info for an object
with
# GM Perfect Hide status
if (!$actor || defined($actor->{name}) || $actor->statusActive('
EFFECTSTATE_SPECIALHIDING')) {
shift @{$queue};
next;
}
# Remove actors with a distance greater than clientSight. Some p
rivate servers (notably Freya) use
# a technique where they send actor_exists packets with ridiculo
us distances in order to automatically
# ban bots. By removingthose actors, we eliminate that possibili
ty and emulate the client more closely.
if (defined $actor->{pos_to} && (my $block_dist = blockDistance(
$char->{pos_to}, $actor->{pos_to})) >= ($config{clientSight} || 16)) {
debug "Removed actor at $actor->{pos_to}{x} $actor->{pos
_to}{y} (distance: $block_dist)\n";
shift @{$queue};
next;
}
$messageSender->sendGetPlayerInfo($ID) if (isSafeActorQuery($ID)
== 1); # Do not Query GM's
$actor = shift @{$queue};
push @{$queue}, $actor if ($actor);
last;
}
}
sub quit {
$quit = 1;
message T("Exiting...\n"), "system";
}
sub relog {
my $timeout = (shift || 5);
my $silent = shift;
$net->setState(1) if ($net);
undef $conState_tries;
$timeout_ex{'master'}{'time'} = time;
$timeout_ex{'master'}{'timeout'} = $timeout;
$net->serverDisconnect() if ($net);
message TF("Relogging in %d seconds...\n", $timeout), "connection" unles
s $silent;
}
##
# sendMessage(String type, String msg, String user)
# type: Specifies what kind of message this is. "c" for public chat, "g" for gui
ld chat,
# "p" for party chat, "pm" for private message, "k" for messages that only
the RO
# client will see (in X-Kore mode.)
# msg: The message to send.
# user:
#
# Send a chat message to a user.
sub sendMessage {
my ($sender, $type, $msg, $user) = @_;
my ($j, @msgs, $oldmsg, $amount, $space);
@msgs = split /\\n/, $msg;
for ($j = 0; $j < @msgs; $j++) {
my (@msg, $i);
@msg = split / /, $msgs[$j];
undef $msg;
for ($i = 0; $i < @msg; $i++) {
if (!length($msg[$i])) {
$msg[$i] = " ";
$space = 1;
}
if (length($msg[$i]) > $config{'message_length_max'}) {
while (length($msg[$i]) >= $config{'message_leng
th_max'}) {
$oldmsg = $msg;
if (length($msg)) {
$amount = $config{'message_lengt
h_max'};
if ($amount - length($msg) > 0)
{
$amount = $config{'messa
ge_length_max'} - 1;
$msg .= " " . substr($ms
g[$i], 0, $amount - length($msg));
}
} else {
$amount = $config{'message_lengt
h_max'};
$msg .= substr($msg[$i], 0, $amo
unt);
}
sendMessage_send($sender, $type, $msg, $
user);
$msg[$i] = substr($msg[$i], $amount - le
ngth($oldmsg), length($msg[$i]) - $amount - length($oldmsg));
undef $msg;
}
}
if (length($msg[$i]) && length($msg) + length($msg[$i])
<= $config{'message_length_max'}) {
if (length($msg)) {
if (!$space) {
$msg .= " " . $msg[$i];
} else {
$space = 0;
$msg .= $msg[$i];
}
} else {
$msg .= $msg[$i];
}
} else {
sendMessage_send($sender, $type, $msg, $user);
$msg = $msg[$i];
}
if (length($msg) && $i == @msg - 1) {
sendMessage_send($sender, $type, $msg, $user);
}
}
}
}
sub sendMessage_send {
my ($sender, $type, $msg, $user) = @_;
if ($type eq "c") {
$sender->sendChat($msg);
} elsif ($type eq "g") {
$sender->sendGuildChat($msg);
} elsif ($type eq "p") {
$sender->sendPartyChat($msg);
} elsif ($type eq "bg") {
$sender->sendBattlegroundChat($msg);
} elsif ($type eq "pm") {
$sender->sendPrivateMsg($user, $msg);
%lastpm = (
msg => $msg,
user => $user
);
push @lastpm, {%lastpm};
} elsif ($type eq "k") {
$sender->injectMessage($msg);
}
}
# Keep track of when we last cast a skill
sub setSkillUseTimer {
my ($skillID, $targetID, $wait) = @_;
my $skill = new Skill(idn => $skillID);
my $handle = $skill->getHandle();
$char->{skills}{$handle}{time_used} = time;
delete $char->{time_cast};
delete $char->{cast_cancelled};
$char->{last_skill_time} = time;
$char->{last_skill_used} = $skillID;
$char->{last_skill_target} = $targetID;
# increment monsterSkill maxUses counter
if (defined $targetID) {
my $actor = Actor::get($targetID);
$actor->{skillUses}{$skill->getHandle()}++;
}
# Set encore skill if applicable
$char->{encoreSkill} = $skill if $targetID eq $accountID && $skillsEncor
e{$skill->getHandle()};
}
sub setPartySkillTimer {
my ($skillID, $targetID) = @_;
my $skill = new Skill(idn => $skillID);
my $handle = $skill->getHandle();
# set partySkill target_time
my $i = $targetTimeout{$targetID}{$handle};
$ai_v{"partySkill_${i}_target_time"}{$targetID} = time if $i ne "";
}

##
# boolean setStatus(Actor actor, opt1, opt2, option)
# opt1: the state information of the actor.
# opt2: the ailment information of the actor.
# option: the "look" information of the actor.
# Returns: Whether the actor should be removed from the actor list.
#
# Sets the state, ailment, and "look" statuses of the actor.
# Does not include skillsstatus.txt items.
# TODO: move to Actor?
sub setStatus {
my ($actor, $opt1, $opt2, $option) = @_;
assert(defined $actor) if DEBUG;
assert(UNIVERSAL::isa($actor, 'Actor')) if DEBUG;
my $verbosity = $actor->{ID} eq $accountID ? 1 : 2;
my $changed = 0;
my $match_id = sub {return ($_[0] == $_[1])};
my $match_bitflag = sub {return (($_[0] & $_[1]) == $_[1])};
# TODO: we could possibly make the search faster (binary search?)
for (
[$opt1, \%stateHandle, $match_id, 'state'],
[$opt2, \%ailmentHandle, $match_bitflag, 'ailment'],
[$option, \%lookHandle, $match_bitflag, 'look'],
) {
my ($option, $handle, $match, $name) = @$_;
#next unless $option; # skip option 0 (no state, ailment, look h
as such id or bitflag) (we can't have this, the state resets its statuses using
this)
for (keys %$handle) {
if (&$match($option, $_)) {
unless ($actor->{statuses}{$handle->{$_}}) {
$actor->{statuses}{$handle->{$_}} = 1;
message status_string($actor, $name . ':
' . ($statusName{$handle->{$_}} || $handle->{$_}), 'now'), "parseMsg_status$nam
e", $verbosity;
$changed = 1;
}
#last; # stop this for loop if found (we cannot
do this because of bit flag match must loop all)
} elsif ($actor->{statuses}{$handle->{$_}}) {
delete $actor->{statuses}{$handle->{$_}};
message status_string($actor, $name . ': ' . ($s
tatusName{$handle->{$_}} || $handle->{$_}), 'no longer'), "parseMsg_status$name"
, $verbosity;
$changed = 1;
#last; # stop this for loop if found (we cannot
do this because of bit flag match must loop all)
}
}
}
=pod
foreach (keys %stateHandle) {
if ($opt1 == $_) {
if (!$actor->{statuses}{$stateHandle{$_}}) {
$actor->{statuses}{$stateHandle{$_}} = 1;
message TF("%s %s in %s state.\n", $actor, $acto
r->verb('are', 'is'), $statusName{$stateHandle{$_}} || $stateHandle{$_}), "parse
Msg_statuslook", $verbosity;
$changed = 1;
}
} elsif ($actor->{statuses}{$stateHandle{$_}}) {
delete $actor->{statuses}{$stateHandle{$_}};
message TF("%s %s out of %s state.\n", $actor, $actor->v
erb('are', 'is'), $statusName{$stateHandle{$_}} || $stateHandle{$_}), "parseMsg_
statuslook", $verbosity;
$changed = 1;
}
}
foreach (keys %ailmentHandle) {
if (($opt2 & $_) == $_) {
if (!$actor->{statuses}{$ailmentHandle{$_}}) {
$actor->{statuses}{$ailmentHandle{$_}} = 1;
if ($actor->isa('Actor::You')) {
message TF("%s have ailment: %s.\n", $ac
tor->nameString(), $statusName{$ailmentHandle{$_}} || $ailmentHandle{$_}), "pars
eMsg_statuslook", $verbosity;
} else {
message TF("%s has ailment: %s.\n", $act
or->nameString(), $statusName{$ailmentHandle{$_}} || $ailmentHandle{$_}), "parse
Msg_statuslook", $verbosity;
}
$changed = 1;
}
} elsif ($actor->{statuses}{$ailmentHandle{$_}}) {
delete $actor->{statuses}{$ailmentHandle{$_}};
message TF("%s %s out of %s ailment.\n", $actor, $actor-
>verb('are', 'is'), $statusName{$ailmentHandle{$_}} || $ailmentHandle{$_}), "par
seMsg_statuslook", $verbosity;
$changed = 1;
}
}
foreach (keys %lookHandle) {
if (($option & $_) == $_) {
if (!$actor->{statuses}{$lookHandle{$_}}) {
$actor->{statuses}{$lookHandle{$_}} = 1;
if ($actor->isa('Actor::You')) {
message TF("%s have look: %s.\n", $actor
->nameString, $statusName{$lookHandle{$_}} || $lookHandle{$_}), "parseMsg_status
look", $verbosity;
} else {
message TF("%s has look: %s.\n", $actor-
>nameString, $statusName{$lookHandle{$_}} || $lookHandle{$_}), "parseMsg_statusl
ook", $verbosity;
}
$changed = 1;
}
} elsif ($actor->{statuses}{$lookHandle{$_}}) {
delete $actor->{statuses}{$lookHandle{$_}};
message TF("%s %s out of %s look.\n", $actor, $actor->ve
rb('are', 'is'), $statusName{$lookHandle{$_}} || $lookHandle{$_}), "parseMsg_sta
tuslook", $verbosity;
$changed = 1;
}
}
=cut
Plugins::callHook('changed_status',{actor => $actor, changed => $changed
});
# Remove perfectly hidden objects
if ($actor->statusActive('EFFECTSTATE_SPECIALHIDING')) {
if (UNIVERSAL::isa($actor, "Actor::Player")) {
message TF("Found perfectly hidden %s\n", $actor->nameSt
ring());
# message TF("Remove perfectly hidden %s\n", $actor->nam
eString());
# $playersList->remove($actor);
# Call the hook when a perfectly hidden player is detect
ed
# Plugins::callHook('perfect_hidden_player',undef);
Plugins::callHook('perfect_hidden_player',{actor => $act
or, changed => $changed});
} elsif (UNIVERSAL::isa($actor, "Actor::Monster")) {
message TF("Found perfectly hidden %s\n", $actor->nameSt
ring());
# message TF("Remove perfectly hidden %s\n", $actor->nam
eString());
# $monstersList->remove($actor);
# NPCs do this on purpose (who knows why)
} elsif (UNIVERSAL::isa($actor, "Actor::NPC")) {
message TF("Found perfectly hidden %s\n", $actor->nameSt
ring());
# message TF("Remove perfectly hidden %s\n", $actor->nam
eString());
# $npcsList->remove($actor);
} elsif (UNIVERSAL::isa($actor, "Actor::Pet")) {
message TF("Found perfectly hidden %s\n", $actor->nameSt
ring());
# message TF("Remove perfectly hidden %s\n", $actor->nam
eString());
# $petsList->remove($actor);
}
return 1;
} else {
return 0;
}
}

# Increment counter for monster being casted on


sub countCastOn {
my ($sourceID, $targetID, $skillID, $x, $y) = @_;
return unless defined $targetID;
my $source = Actor::get($sourceID);
my $target = Actor::get($targetID);
assert(UNIVERSAL::isa($source, 'Actor')) if DEBUG;
assert(UNIVERSAL::isa($target, 'Actor')) if DEBUG;
if ($targetID eq $accountID) {
$source->{castOnToYou}++;
} elsif ($target->isa('Actor::Player')) {
$source->{castOnToPlayer}{$targetID}++;
} elsif ($target->isa('Actor::Monster')) {
$source->{castOnToMonster}{$targetID}++;
}
if ($sourceID eq $accountID) {
$target->{castOnByYou}++;
} elsif ($source->isa('Actor::Player')) {
$target->{castOnByPlayer}{$sourceID}++;
} elsif ($source->isa('Actor::Monster')) {
$target->{castOnByMonster}{$sourceID}++;
}
}
##
# boolean stripLanguageCode(String* msg)
# msg: a chat message, as sent by the RO server.
# Returns: whether the language code was stripped.
#
# Strip the language code character from a chat message.
sub stripLanguageCode {
my $r_msg = shift;
if ($config{chatLangCode} && $config{chatLangCode} ne "none") {
if ($$r_msg =~ /^\|..(.*)/) {
$$r_msg = $1;
return 1;
}
return 0;
} else {
return 0;
}
}
##
# void switchConf(String filename)
# filename: a configuration file.
# Returns: 1 on success, 0 if $filename does not exist.
#
# Switch to another configuration file.
sub switchConfigFile {
my $filename = shift;
if (! -f $filename) {
error TF("%s does not exist.\n", $filename);
return 0;
}
Settings::setConfigFilename($filename);
parseConfigFile($filename, \%config);
return 1;
}
sub updateDamageTables {
my ($sourceID, $targetID, $damage) = @_;
# Track deltaHp
#
# A player's "deltaHp" initially starts at 0.
# When he takes damage, the damage is subtracted from his deltaHp.
# When he is healed, this amount is added to the deltaHp.
# If the deltaHp becomes positive, it is reset to 0.
#
# Someone with a lot of negative deltaHp is probably in need of healing.
# This allows us to intelligently heal non-party members.
if (my $target = Actor::get($targetID)) {
$target->{deltaHp} -= $damage;
$target->{deltaHp} = 0 if $target->{deltaHp} > 0;
}
if ($sourceID eq $accountID) {
if ((my $monster = $monstersList->getByID($targetID))) {
# You attack monster
$monster->{dmgTo} += $damage;
$monster->{dmgFromYou} += $damage;
$monster->{numAtkFromYou}++;
if ($damage <= ($config{missDamage} || 0)) {
$monster->{missedFromYou}++;
debug "Incremented missedFromYou count to $monst
er->{missedFromYou}\n", "attackMonMiss";
$monster->{atkMiss}++;
} else {
$monster->{atkMiss} = 0;
}
if ($config{teleportAuto_atkMiss} && $monster->{atkMiss}
>= $config{teleportAuto_atkMiss}) {
message T("Teleporting because of attack miss\n"
), "teleport";
useTeleport(1);
}
if ($config{teleportAuto_atkCount} && $monster->{numAtkF
romYou} >= $config{teleportAuto_atkCount}) {
message TF("Teleporting after attacking a monste
r %d times\n", $config{teleportAuto_atkCount}), "teleport";
useTeleport(1);
}
if (AI::action eq "attack" && mon_control($monster->{nam
e},$monster->{nameID})->{attack_auto} == 3 && $damage) {
# Mob-training, you only need to attack the mons
ter once to provoke it
message TF("%s (%s) has been provoked, searching
another monster\n", $monster->{name}, $monster->{binID});
$char->stopAttack;
$char->dequeue;
}

}
=pod
} elsif ($targetID eq $accountID) {
if ((my $monster = $monstersList->getByID($sourceID))) {
# Monster attacks you
$monster->{dmgFrom} += $damage;
$monster->{dmgToYou} += $damage;
if ($damage == 0) {
$monster->{missedYou}++;
}
$monster->{attackedYou}++ unless (
scalar(keys %{$monster->{dmgFromPlayer}}
) ||
scalar(keys %{$monster->{dmgToPlayer}})
||
$monster->{missedFromPlayer} ||
$monster->{missedToPlayer}
);
$monster->{target} = $targetID;
if ($AI == 2) {
my $teleport = 0;
if (mon_control($monster->{name},$monster->{name
ID})->{teleport_auto} == 2 && $damage){
message TF("Teleporting due to attack fr
om %s\n",
$monster->{name}), "teleport";
$teleport = 1;
} elsif ($config{teleportAuto_deadly} && $damage
>= $char->{hp}
&& !$char->statusActive('EFST_ILLUSION'))
{
message TF("Next %d dmg could kill you.
Teleporting...\n",
$damage), "teleport";
$teleport = 1;
} elsif ($config{teleportAuto_maxDmg} && $damage
>= $config{teleportAuto_maxDmg}
&& !$char->statusActive('EFST_ILLUSION')
&& !($config{teleportAuto_maxDmgInLock} &&
$field->name eq $config{lockMap})) {
message TF("%s hit you for more than %d
dmg. Teleporting...\n",
$monster->{name}, $config{telepo
rtAuto_maxDmg}), "teleport";
$teleport = 1;
} elsif ($config{teleportAuto_maxDmgInLock} && $
field->name eq $config{lockMap}
&& $damage >= $config{teleportAuto_maxDmgI
nLock}
&& !$char->statusActive('EFST_ILLUSION'))
{
message TF("%s hit you for more than %d
dmg in lockMap. Teleporting...\n",
$monster->{name}, $config{telepo
rtAuto_maxDmgInLock}), "teleport";
$teleport = 1;
} elsif (AI::inQueue("sitAuto") && $config{telep
ortAuto_attackedWhenSitting}
&& $damage > 0) {
message TF("%s attacks you while you are
sitting. Teleporting...\n",
$monster->{name}), "teleport";
$teleport = 1;
} elsif ($config{teleportAuto_totalDmg}
&& $monster->{dmgToYou} >= $config{telepor
tAuto_totalDmg}
&& !$char->statusActive('EFST_ILLUSION')
&& !($config{teleportAuto_totalDmgInLock}
&& $field->name eq $config{lockMap})) {
message TF("%s hit you for a total of mo
re than %d dmg. Teleporting...\n",
$monster->{name}, $config{telepo
rtAuto_totalDmg}), "teleport";
$teleport = 1;
} elsif ($config{teleportAuto_totalDmgInLock} &&
$field->name eq $config{lockMap}
&& $monster->{dmgToYou} >= $config{telepor
tAuto_totalDmgInLock}
&& !$char->statusActive('EFST_ILLUSION'))
{
message TF("%s hit you for a total of mo
re than %d dmg in lockMap. Teleporting...\n",
$monster->{name}, $config{telepo
rtAuto_totalDmgInLock}), "teleport";
$teleport = 1;
} elsif ($config{teleportAuto_hp} && percent_hp(
$char) <= $config{teleportAuto_hp}) {
message TF("%s hit you when your HP is t
oo low. Teleporting...\n",
$monster->{name}), "teleport";
$teleport = 1;
} elsif ($config{attackChangeTarget} && ((AI::ac
tion eq "route" && AI::action(1) eq "attack") || (AI::action eq "move" && AI::ac
tion(2) eq "attack"))
&& AI::args->{attackID} && AI::args()->{attac
kID} ne $sourceID) {
my $attackTarget = Actor::get(AI::args->
{attackID});
my $attackSeq = (AI::action eq "route")
? AI::args(1) : AI::args(2);
if (!$attackTarget->{dmgToYou} && !$atta
ckTarget->{dmgFromYou} && distance($monster->{pos_to}, calcPosition($char)) <= $
attackSeq->{attackMethod}{distance}) {
my $ignore = 0;
# Don't attack ignored monsters
if ((my $control = mon_control($
monster->{name},$monster->{nameID}))) {
$ignore = 1 if ( ($contr
ol->{attack_auto} == -1)
|| ($control->{a
ttack_lvl} ne "" && $control->{attack_lvl} > $char->{lv})
|| ($control->{a
ttack_jlvl} ne "" && $control->{attack_jlvl} > $char->{lv_job})
|| ($control->{a
ttack_hp} ne "" && $control->{attack_hp} > $char->{hp})
|| ($control->{a
ttack_sp} ne "" && $control->{attack_sp} > $char->{sp})
|| ($control->{a
ttack_auto} == 3 && ($monster->{dmgToYou} || $monster->{missedYou} || $monster->
{dmgFromYou}))
);
}
if (!$ignore) {
# Change target to close
r aggressive monster
message TF("Change targe
t to aggressive : %s (%s)\n", $monster->name, $monster->{binID});
stopAttack();
AI::dequeue;
AI::dequeue if (AI::acti
on eq "route");
AI::dequeue;
attack($sourceID);
}
}
} elsif (AI::action eq "attack" && mon_control($
monster->{name},$monster->{nameID})->{attack_auto} == 3
&& ($monster->{dmgToYou} || $monster->{m
issedYou} || $monster->{dmgFromYou})) {
# Mob-training, stop attacking the monst
er if it has been attacking you
message TF("%s (%s) has been provoked, s
earching another monster\n", $monster->{name}, $monster->{binID});
stopAttack();
AI::dequeue();
}
useTeleport(1, undef, 1) if ($teleport);
}
}
=cut
} elsif ((my $monster = $monstersList->getByID($sourceID))) {
if (my $player = ($accountID eq $targetID && $char) || $playersL
ist->getByID($targetID) || $slavesList->getByID($targetID)) {
# Monster attacks player or slave
$monster->{dmgFrom} += $damage;
($accountID eq $targetID ? $monster->{dmgToYou} : $monst
er->{dmgToPlayer}{$targetID}) += $damage;
$player->{dmgFromMonster}{$sourceID} += $damage;
if ($damage == 0) {
($accountID eq $targetID ? $monster->{missedYou}
: $monster->{missedToPlayer}{$targetID}) += 1;
$player->{missedFromMonster}{$sourceID}++;
}
$accountID eq $targetID && $monster->{attackedYou}++ unl
ess (
scalar(keys %{$monster->{dmgFromPlayer}}
) ||
scalar(keys %{$monster->{dmgToPlayer}})
||
$monster->{missedFromPlayer} ||
$monster->{missedToPlayer}
);
if (existsInList($config{tankersList}, $player->{name})
||
($char->{slaves} && %{$char->{slaves}} && $char->{sl
aves}{$targetID} && %{$char->{slaves}{$targetID}}) ||
($char->{party} && %{$char->{party}} && $char->{part
y}{users}{$targetID} && %{$char->{party}{users}{$targetID}})) {
# Monster attacks party member or our slave
$monster->{dmgToParty} += $damage;
$monster->{missedToParty}++ if ($damage == 0);
}
$monster->{target} = $targetID;
OpenKoreMod::updateDamageTables($monster) if (defined &O
penKoreMod::updateDamageTables);
if ($AI == 2 && ($accountID eq $targetID or $char->{slav
es} && $char->{slaves}{$targetID})) {
# object under our control
my $teleport = 0;
if (mon_control($monster->{name},$monster->{name
ID})->{teleport_auto} == 2 && $damage){
message TF("%s hit %s. Teleporting...\n"
,
$monster, $player), "teleport";
$teleport = 1;
} elsif ($config{$player->{configPrefix}.'telepo
rtAuto_deadly'} && $damage >= $player->{hp}
&& !$player->statusActive('EFST_ILLUSION')
) {
message TF("%s can kill %s with the next
%d dmg. Teleporting...\n",
$monster, $player, $damage), "te
leport";
$teleport = 1;
} elsif ($config{$player->{configPrefix}.'telepo
rtAuto_maxDmg'} && $damage >= $config{$player->{configPrefix}.'teleportAuto_maxD
mg'}
&& !$player->statusActive('EFST_ILLUSION')
&& !($config{$player->{configPrefix}.'tele
portAuto_maxDmgInLock'} && $field->name eq $config{lockMap})) {
message TF("%s hit %s for more than %d d
mg. Teleporting...\n",
$monster, $player, $config{$play
er->{configPrefix}.'teleportAuto_maxDmg'}), "teleport";
$teleport = 1;
} elsif ($config{$player->{configPrefix}.'telepo
rtAuto_maxDmgInLock'} && $field->name eq $config{lockMap}
&& $damage >= $config{$player->{configPref
ix}.'teleportAuto_maxDmgInLock'}
&& !$player->statusActive('EFST_ILLUSION')
) {
message TF("%s hit %s for more than %d d
mg in lockMap. Teleporting...\n",
$monster, $player, $config{$play
er->{configPrefix}.'teleportAuto_maxDmgInLock'}), "teleport";
$teleport = 1;
} elsif (AI::inQueue("sitAuto") && $config{$play
er->{configPrefix}.'teleportAuto_attackedWhenSitting'}
&& $damage) {
message TF("%s hit %s while you are sitt
ing. Teleporting...\n",
$monster, $player), "teleport";
$teleport = 1;
} elsif ($config{$player->{configPrefix}.'telepo
rtAuto_totalDmg'}
&& ($accountID eq $targetID ? $monster->{d
mgToYou} : $monster->{dmgToPlayer}{$targetID}) >= $config{$player->{configPrefix
}.'teleportAuto_totalDmg'}
&& !$player->statusActive('EFST_ILLUSION')
&& !($config{$player->{configPrefix}.'tele
portAuto_totalDmgInLock'} && $field->name eq $config{lockMap})) {
message TF("%s hit %s for a total of mor
e than %d dmg. Teleporting...\n",
$monster, $player, $config{$play
er->{configPrefix}.'teleportAuto_totalDmg'}), "teleport";
$teleport = 1;
} elsif ($config{$player->{configPrefix}.'telepo
rtAuto_totalDmgInLock'} && $field->name eq $config{lockMap}
&& ($accountID eq $targetID ? $monster->{d
mgToYou} : $monster->{dmgToPlayer}{$targetID}) >= $config{$player->{configPrefix
}.'teleportAuto_totalDmgInLock'}
&& !$player->statusActive('EFST_ILLUSION')
) {
message TF("%s hit %s for a total of mor
e than %d dmg in lockMap. Teleporting...\n",
$monster, $player, $config{$play
er->{configPrefix}.'teleportAuto_totalDmgInLock'}), "teleport";
$teleport = 1;
} elsif ($config{$player->{configPrefix}.'telepo
rtAuto_hp'} && percent_hp($player) <= $config{$player->{configPrefix}.'teleportA
uto_hp'}) {
message TF("%s hit %s when %s HP is unde
r %d. Teleporting...\n",
$monster, $player, $player->verb
(T('your'), T('its')), $config{$player->{configPrefix}.'teleportAuto_hp'}), "tel
eport";
$teleport = 1;
} elsif (
$config{$player->{configPrefix}.'attackC
hangeTarget'}
&& (
$player->action eq 'route' && $p
layer->action(1) eq 'attack'
or $player->action eq 'move' &&
$player->action(2) eq 'attack'
)
&& $player->args->{attackID} && $player-
>args->{attackID} ne $sourceID
) {
my $attackTarget = Actor::get($player->a
rgs->{attackID});
my $attackSeq = ($player->action eq 'rou
te') ? $player->args(1) : $player->args(2);
if (
!($accountID eq $targetID ? $att
ackTarget->{dmgToYou} : $attackTarget->{dmgToPlayer}{$targetID})
&& !($accountID eq $targetID ? $
attackTarget->{dmgToYou} : $attackTarget->{dmgFromPlayer}{$targetID})
&& distance($monster->{pos_to},
calcPosition($player)) <= $attackSeq->{attackMethod}{distance}
) {
my $ignore = 0;
# Don't attack ignored monsters
if ((my $control = mon_control($
monster->{name},$monster->{nameID}))) {
$ignore = 1 if ( ($contr
ol->{attack_auto} == -1)
|| ($control->{a
ttack_lvl} ne "" && $control->{attack_lvl} > $char->{lv})
|| ($control->{a
ttack_jlvl} ne "" && $control->{attack_jlvl} > $char->{lv_job})
|| ($control->{a
ttack_hp} ne "" && $control->{attack_hp} > $char->{hp})
|| ($control->{a
ttack_sp} ne "" && $control->{attack_sp} > $char->{sp})
|| ($accountID e
q $targetID && $control->{attack_auto} == 3 && ($monster->{dmgToYou} || $monster
->{missedYou} || $monster->{dmgFromYou}))
);
}
unless ($ignore) {
# Change target to close
r aggressive monster
message TF("%s %s target
to aggressive %s\n",
$player, $player
->verb(T('change'), T('changes')), $monster);
$player->stopAttack;
$player->dequeue;
$player->dequeue if $pla
yer->action eq 'route';
$player->dequeue;
$player->attack($sourceI
D);
}
}
} elsif ($accountID eq $targetID && $player->act
ion eq "attack" && mon_control($monster->{name}, $monster->{nameID})->{attack_au
to} == 3
&& ($monster->{dmgToYou} || $monster->{m
issedYou} || $monster->{dmgFromYou})) {
# Mob-training, stop attacking the monst
er if it has been attacking you
message TF("%s has been provoked, search
ing another monster\n", $monster);
$player->stopAttack;
$player->dequeue;
}
useTeleport(1, undef, 1) if ($teleport);
}
}
} elsif ((my $player = $playersList->getByID($sourceID) || $slavesList->
getByID($sourceID))) {
if ((my $monster = $monstersList->getByID($targetID))) {
# Player or Slave attacks monster
$monster->{dmgTo} += $damage;
$monster->{dmgFromPlayer}{$sourceID} += $damage;
$monster->{lastAttackFrom} = $sourceID;
$player->{dmgToMonster}{$targetID} += $damage;
if ($damage == 0) {
$monster->{missedFromPlayer}{$sourceID}++;
$player->{missedToMonster}{$targetID}++;
}
if (existsInList($config{tankersList}, $player->{name})
|| ($char->{slaves} && $char->{slaves}{$sourceID}) ||
($char->{party} && %{$char->{party}} && $char->{part
y}{users}{$sourceID} && %{$char->{party}{users}{$sourceID}})) {
$monster->{dmgFromParty} += $damage;
}
OpenKoreMod::updateDamageTables($monster) if (defined &O
penKoreMod::updateDamageTables);
}
}
}
##
# updatePlayerNameCache(player)
# player: a player actor object.
sub updatePlayerNameCache {
my ($player) = @_;
return if (!$config{cachePlayerNames});
# First, cleanup the cache. Remove entries that are too old.
# Default life time: 15 minutes
my $changed = 1;
for (my $i = 0; $i < @playerNameCacheIDs; $i++) {
my $ID = $playerNameCacheIDs[$i];
if (timeOut($playerNameCache{$ID}{time}, $config{cachePlayerName
s_duration})) {
delete $playerNameCacheIDs[$i];
delete $playerNameCache{$ID};
$changed = 1;
}
}
compactArray(\@playerNameCacheIDs) if ($changed);
# Resize the cache if it's still too large.
# Default cache size: 100
while (@playerNameCacheIDs > $config{cachePlayerNames_maxSize}) {
my $ID = shift @playerNameCacheIDs;
delete $playerNameCache{$ID};
}
# Add this player name to the cache.
my $ID = $player->{ID};
if (!$playerNameCache{$ID}) {
push @playerNameCacheIDs, $ID;
my %entry = (
name => $player->{name},
guild => $player->{guild},
time => time,
lv => $player->{lv},
jobID => $player->{jobID}
);
$playerNameCache{$ID} = \%entry;
}
}
##
# useTeleport(level)
# level: 1 to teleport to a random spot, 2 to respawn.
sub useTeleport {
my ($use_lvl, $internal, $emergency) = @_;
my %args = (
level => $use_lvl, # 1 = Teleport, 2 = respawn
emergency => $emergency, # Needs a fast tele
internal => $internal # Did we call useTeleport from inside useT
eleport?
);
if ($use_lvl == 2 && $config{saveMap_warpChatCommand}) {
Plugins::callHook('teleport_sent', \%args);
sendMessage($messageSender, "c", $config{saveMap_warpChatCommand
});
return 1;
}
if ($use_lvl == 1 && $config{teleportAuto_useChatCommand}) {
Plugins::callHook('teleport_sent', \%args);
sendMessage($messageSender, "c", $config{teleportAuto_useChatCom
mand});
return 1;
}
# for possible recursive calls
if (!defined $internal) {
$internal = $config{teleportAuto_useSkill};
}
# look if the character has the skill
my $sk_lvl = 0;
if ($char->{skills}{AL_TELEPORT}) {
$sk_lvl = $char->{skills}{AL_TELEPORT}{lv};
}
# only if we want to use skill ?
return if ($char->{muted});
if ($sk_lvl > 0 && $internal > 0 && ($use_lvl == 1 || !$config{'teleport
Auto_useItemForRespawn'})) {
# We have the teleport skill, and should use it
my $skill = new Skill(handle => 'AL_TELEPORT');
if ($use_lvl == 2 || $internal == 1 || ($internal == 2 && !isSaf
e())) {
# Send skill use packet to appear legitimate
# (Always send skill use packet for level 2 so that save
Map
# autodetection works)
if ($char->{sitting}) {
Plugins::callHook('teleport_sent', \%args);
main::ai_skillUse($skill->getHandle(), $use_lvl,
0, 0, $accountID);
return 1;
} else {
$messageSender->sendSkillUse($skill->getIDN(), $
sk_lvl, $accountID);
undef $char->{permitSkill};
}
if (!$emergency && $use_lvl == 1) {
Plugins::callHook('teleport_sent', \%args);
$timeout{ai_teleport_retry}{time} = time;
AI::queue('teleport');
return 1;
}
}
delete $ai_v{temp}{teleport};
debug "Sending Teleport using Level $use_lvl\n", "useTeleport";
if ($use_lvl == 1) {
Plugins::callHook('teleport_sent', \%args);
$messageSender->sendWarpTele(26, "Random");
return 1;
} elsif ($use_lvl == 2) {
# check for possible skill level abuse
message T("Using Teleport Skill Level 2 though we not ha
ve it!\n"), "useTeleport" if ($sk_lvl == 1);
# If saveMap is not set simply use a wrong .gat.
# eAthena servers ignore it, but this trick doesn't work
# on official servers.
my $telemap = "prontera.gat";
$telemap = "$config{saveMap}.gat" if ($config{saveMap} n
e "");
Plugins::callHook('teleport_sent', \%args);
$messageSender->sendWarpTele(26, $telemap);
return 1;
}
}
# No skill try to equip a Tele clip or something,
# if teleportAuto_equip_* is set
if (Actor::Item::scanConfigAndCheck('teleportAuto_equip') && ($use_lvl =
= 1 || !$config{'teleportAuto_useItemForRespawn'})) {
return if AI::inQueue('teleport');
debug "Equipping Accessory to teleport\n", "useTeleport";
AI::queue('teleport', {lv => $use_lvl});
if ($emergency ||
!$config{teleportAuto_useSkill} ||
$config{teleportAuto_useSkill} == 3 ||
$config{teleportAuto_useSkill} == 2 && isSafe()) {
$timeout{ai_teleport_delay}{time} = 1;
}
Actor::Item::scanConfigAndEquip('teleportAuto_equip');
#Commands::run('aiv');
return 1;
}
# else if $internal == 0 or $sk_lvl == 0
# try to use item
# could lead to problems if the ItemID would be different on some server
s
# 1 Jan 2006 - instead of nameID, search for *wing in the inventory
# could lead to problems if the name is different on some servers
# 11 Mar 2010 - instead of name, use nameID, names can be different for
different servers
my $item;
if ($use_lvl == 1) {
#$item = $char->inventory->getByName("Fly Wing");
$item = $char->inventory->getByNameID(601);
} elsif ($use_lvl == 2) {
#$item = $char->inventory->getByName("Butterfly Wing");
$item = $char->inventory->getByNameID(602);
}
if ($item) {
# We have Fly Wing/Butterfly Wing.
# Don't spam the "use fly wing" packet, or we'll end up using to
o many wings.
if (timeOut($timeout{ai_teleport})) {
Plugins::callHook('teleport_sent', \%args);
$messageSender->sendItemUse($item->{index}, $accountID);
$timeout{ai_teleport}{time} = time;
}
return 1;
}
# no item, but skill is still available
if ( $sk_lvl > 0 ) {
message T("No Fly Wing or Butterfly Wing, fallback to Teleport S
kill\n"), "useTeleport";
return useTeleport($use_lvl, 1, $emergency);
}
if ($use_lvl == 1) {
message T("You don't have the Teleport skill or a Fly Wing\n"),
"teleport";
} else {
message T("You don't have the Teleport skill or a Butterfly Wing
\n"), "teleport";
}
return 0;
}
##
# top10Listing(args)
# args: a 282 bytes packet representing 10 names followed by 10 ranks
#
# Returns a formatted list of [# ], Name and points
sub top10Listing {
my ($args) = @_;
my $msg = $args->{RAW_MSG};
my @list;
my @points;
my $i;
my $textList = "";
for ($i = 0; $i < 10; $i++) {
$list[$i] = unpack("Z24", substr($msg, 2 + (24*$i), 24));
}
for ($i = 0; $i < 10; $i++) {
$points[$i] = unpack("V1", substr($msg, 242 + ($i*4), 4));
}
for ($i = 0; $i < 10; $i++) {
$textList .= swrite("[@<] @<<<<<<<<<<<<<<<<<<<<<<<< @>>>>
>>",
[$i+1, $list[$i], $points[$i]]);
}
return $textList;
}
##
# whenGroundStatus(target, statuses, mine)
# target: coordinates hash
# statuses: a comma-separated list of ground effects e.g. Safety Wall,Pneuma
# mine: if true, only consider ground effects that originated from me
#
# Returns 1 if $target has one of the ground effects specified by $statuses.
sub whenGroundStatus {
my ($pos, $statuses, $mine) = @_;
my ($x, $y) = ($pos->{x}, $pos->{y});
for my $ID (@spellsID) {
my $spell;
next unless $spell = $spells{$ID};
next if $mine && $spell->{sourceID} ne $accountID;
if ($x == $spell->{pos}{x} &&
$y == $spell->{pos}{y}) {
return 1 if existsInList($statuses, getSpellName($spell-
>{type}));
}
}
return 0;
}
sub writeStorageLog {
my ($show_error_on_fail) = @_;
my $f;
if (open($f, ">:utf8", $Settings::storage_log_file)) {
print $f TF("---------- Storage %s -----------\n", getFormattedD
ate(int(time)));
for (my $i = 0; $i < @storageID; $i++) {
next if (!$storageID[$i]);
my $item = $storage{$storageID[$i]};
my $display = sprintf "%2d %s x %s", $i, $item->{name},
$item->{amount};
# Translation Comment: Mark to show not identified items
$display .= " -- " . T("Not Identified") if !$item->{ide
ntified};
# Translation Comment: Mark to show broken items
$display .= " -- " . T("Broken") if $item->{broken};
print $f "$display\n";
}
# Translation Comment: Storage Capacity
print $f TF("\nCapacity: %d/%d\n", $storage{items}, $storage{ite
ms_max});
print $f "-------------------------------\n";
close $f;
message T("Storage logged\n"), "success";
} elsif ($show_error_on_fail) {
error TF("Unable to write to %s\n", $Settings::storage_log_file)
;
}
}
##
# getBestTarget(possibleTargets, nonLOSNotAllowed)
# possibleTargets: reference to an array of monsters' IDs
# nonLOSNotAllowed: if set, non-LOS monsters (and monsters that aren't in attack
MaxDistance) aren't checked up
#
# Returns ID of the best target
sub getBestTarget {
my ($possibleTargets, $nonLOSNotAllowed) = @_;
if (!$possibleTargets) {
return;
}
my $portalDist = $config{'attackMinPortalDistance'} || 4;
my $playerDist = $config{'attackMinPlayerDistance'} || 1;
my @noLOSMonsters;
my $myPos = calcPosition($char);
my ($highestPri, $smallestDist, $bestTarget);
# First of all we check monsters in LOS, then the rest of monsters
foreach (@{$possibleTargets}) {
my $monster = $monsters{$_};
my $pos = calcPosition($monster);
next if (positionNearPlayer($pos, $playerDist)
|| positionNearPortal($pos, $portalDist)
);
if ((my $control = mon_control($monster->{name},$monster->{nameI
D}))) {
next if ( ($control->{attack_auto} == -1)
|| ($control->{attack_lvl} ne "" && $control->{a
ttack_lvl} > $char->{lv})
|| ($control->{attack_jlvl} ne "" && $control->{
attack_jlvl} > $char->{lv_job})
|| ($control->{attack_hp} ne "" && $control->{a
ttack_hp} > $char->{hp})
|| ($control->{attack_sp} ne "" && $control->{a
ttack_sp} > $char->{sp})
|| ($control->{attack_auto} == 3 && ($monster->{
dmgToYou} || $monster->{missedYou} || $monster->{dmgFromYou}))
|| ($control->{attack_auto} == 0 && !($monster->
{dmgToYou} || $monster->{missedYou}))
);
}
if ($config{'attackCanSnipe'}) {
if (!checkLineSnipable($myPos, $pos)) {
push(@noLOSMonsters, $_);
next;
}
} else {
if (!checkLineWalkable($myPos, $pos)) {
push(@noLOSMonsters, $_);
next;
}
}
my $name = lc $monster->{name};
my $dist = round(distance($myPos, $pos));
# COMMENTED (FIX THIS): attackMaxDistance should never be used a
s indication of LOS
# The objective of attackMaxDistance is to determine the ran
ge of normal attack,
# and not the range of character's ability to engage monster
s
## Monsters that aren't in attackMaxDistance are not checked up
##if ($nonLOSNotAllowed && ($config{'attackMaxDistance'} < $dist
)) {
## next;
##}
if (!defined($highestPri) || ($priority{$name} > $highestPri)) {
$highestPri = $priority{$name};
$smallestDist = $dist;
$bestTarget = $_;
}
if ((!defined($highestPri) || $priority{$name} == $highestPri)
&& (!defined($smallestDist) || $dist < $smallestDist)) {
$highestPri = $priority{$name};
$smallestDist = $dist;
$bestTarget = $_;
}
}
if (!$nonLOSNotAllowed && !$bestTarget && scalar(@noLOSMonsters) > 0) {
foreach (@noLOSMonsters) {
# The most optimal solution is to include the path lengh
ts' comparison, however it will take
# more time and CPU resources, so, we use rough solution
with priority and distance comparison
my $monster = $monsters{$_};
my $pos = calcPosition($monster);
my $name = lc $monster->{name};
my $dist = round(distance($myPos, $pos));
if (!defined($highestPri) || ($priority{$name} > $highes
tPri)) {
$highestPri = $priority{$name};
$smallestDist = $dist;
$bestTarget = $_;
}
if ((!defined($highestPri) || $priority{$name} == $highe
stPri)
&& (!defined($smallestDist) || $dist < $smallestDist))
{
$highestPri = $priority{$name};
$smallestDist = $dist;
$bestTarget = $_;
}
}
}
return $bestTarget;
}
##
# Returns 1 if there is a player nearby (except party and homunculus) or 0 if no
t
sub isSafe {
foreach (@playersID) {
if (!$char->{party}{users}{$_}) {
return 0;
}
}
return 1;
}
##
# Returns 1 if we are safe to query actor name by given actor ID.
sub isSafeActorQuery {
my ($ID) = @_;
foreach my $list ($playersList, $monstersList, $npcsList, $petsList, $sl
avesList) {
my $actor = $list->getByID($ID);
if ($actor) {
# Do not AutoVivify here!
if (defined $actor->{statuses} && %{$actor->{statuses}})
{
if ($actor->statusActive('EFFECTSTATE_SPECIALHID
ING')) {
return 0;
}
}
}
}
return 1;
}
#######################################
#######################################
###CATEGORY: Actor's Actions Text
#######################################
#######################################
##
# String attack_string(Actor source, Actor target, int damage, int delay)
#
# Generates a proper message string for when actor $source attacks actor $target
.
sub attack_string {
my ($source, $target, $damage, $delay) = @_;
assert(UNIVERSAL::isa($source, 'Actor')) if DEBUG;
assert(UNIVERSAL::isa($target, 'Actor')) if DEBUG;
return TF("%s %s %s (Dmg: %s) (Delay: %sms)\n",
$source->nameString,
$source->verb(T('attack'), T('attacks')),
$target->nameString($source),
$damage, $delay);
}
sub skillCast_string {
my ($source, $target, $x, $y, $skillName, $delay) = @_;
assert(UNIVERSAL::isa($source, 'Actor')) if DEBUG;
assert(UNIVERSAL::isa($target, 'Actor')) if DEBUG;
return TF("%s %s %s on %s (Delay: %sms)\n",
$source->nameString(),
$source->verb(T('are casting'), T('is casting')),
$skillName,
($x != 0 || $y != 0) ? TF("location (%d, %d)", $x, $y) : $target
->nameString($source),
$delay);
}
sub skillUse_string {
my ($source, $target, $skillName, $damage, $level, $delay) = @_;
assert(UNIVERSAL::isa($source, 'Actor')) if DEBUG;
assert(UNIVERSAL::isa($target, 'Actor')) if DEBUG;
return sprintf("%s %s %s%s %s %s%s%s\n",
$source->nameString(),
$source->verb(T('use'), T('uses')),
$skillName,
($level != 65535) ? ' ' . TF("(Lv: %s)", $level) : '',
T('on'),
$target->nameString($source),
($damage != -30000) ? ' ' . TF("(Dmg: %s)", $damage || T('Miss')
) : '',
($delay) ? ' ' . TF("(Delay: %sms)", $delay) : '');
}
sub skillUseLocation_string {
my ($source, $skillName, $args) = @_;
assert(UNIVERSAL::isa($source, 'Actor')) if DEBUG;
return sprintf("%s %s %s%s %s (%d, %d)\n",
$source->nameString(),
$source->verb(T('use'), T('uses')),
$skillName,
($args->{lv} != 65535) ? ' ' . TF("(Lv: %s)", $args->{lv}) : '',
T('on location'),
$args->{x},
$args->{y});
}
# TODO: maybe add other healing skill ID's?
sub skillUseNoDamage_string {
my ($source, $target, $skillID, $skillName, $amount) = @_;
assert(UNIVERSAL::isa($source, 'Actor')) if DEBUG;
assert(UNIVERSAL::isa($target, 'Actor')) if DEBUG;
return sprintf("%s %s %s %s %s%s\n",
$source->nameString(),
$source->verb(T('use'), T('uses')),
$skillName,
T('on'),
$target->nameString($source),
($skillID == 28) ? ' ' . TF("(Gained: %s hp)", $amount) : ($amou
nt) ? ' ' . TF("(Lv: %s)", $amount) : '');
}
sub status_string {
my ($source, $statusName, $mode, $seconds) = @_;
assert(UNIVERSAL::isa($source, 'Actor')) if DEBUG;
# Translation Comment: "you/actor" "are/is now/again/nolonger" "status"
"(duration)"
TF("%s %s: %s%s\n",
$source->nameString,
($mode eq 'now') ? $source->verb(T('are now'), T('is now'))
: ($mode eq 'again') ? $source->verb(T('are again'), T('is again
'))
: ($mode eq 'no longer') ? $source->verb(T('are no longer'), T('
is no longer')) : $mode,
$statusName,
$seconds ? ' ' . TF("(Duration: %ss)", $seconds) : ''
)
}
#######################################
#######################################
###CATEGORY: AI Math
#######################################
#######################################
sub lineIntersection {
my $r_pos1 = shift;
my $r_pos2 = shift;
my $r_pos3 = shift;
my $r_pos4 = shift;
my ($x1, $x2, $x3, $x4, $y1, $y2, $y3, $y4, $result, $result1, $result2)
;
$x1 = $$r_pos1{'x'};
$y1 = $$r_pos1{'y'};
$x2 = $$r_pos2{'x'};
$y2 = $$r_pos2{'y'};
$x3 = $$r_pos3{'x'};
$y3 = $$r_pos3{'y'};
$x4 = $$r_pos4{'x'};
$y4 = $$r_pos4{'y'};
$result1 = ($x4 - $x3)*($y1 - $y3) - ($y4 - $y3)*($x1 - $x3);
$result2 = ($y4 - $y3)*($x2 - $x1) - ($x4 - $x3)*($y2 - $y1);
if ($result2 != 0) {
$result = $result1 / $result2;
}
return $result;
}
sub percent_hp {
my $r_hash = shift;
if (!$$r_hash{'hp_max'}) {
return undef;
} else {
return ($$r_hash{'hp'} / $$r_hash{'hp_max'} * 100);
}
}
sub percent_sp {
my $r_hash = shift;
if (!$$r_hash{'sp_max'}) {
return 0;
} else {
return ($$r_hash{'sp'} / $$r_hash{'sp_max'} * 100);
}
}
sub percent_weight {
my $r_hash = shift;
if (!$$r_hash{'weight_max'}) {
return 0;
} else {
return ($$r_hash{'weight'} / $$r_hash{'weight_max'} * 100);
}
}

#######################################
#######################################
###CATEGORY: Misc Functions
#######################################
#######################################
sub avoidGM_near {
my $players = $playersList->getItems();
foreach my $player (@{$players}) {
# skip this person if we dont know the name
next if (!defined $player->{name});
# Check whether this "GM" is on the ignore list
# in order to prevent false matches
last if (existsInList($config{avoidGM_ignoreList}, $player->{nam
e}));
# check if this name matches the GM filter
last unless ($config{avoidGM_namePattern} ? $player->{name} =~ /
$config{avoidGM_namePattern}/ : $player->{name} =~ /^([a-z]?ro)?-?(Sub)?-?\[?GM\
]?/i);
my %args = (
name => $player->{name},
ID => $player->{ID}
);
Plugins::callHook('avoidGM_near', \%args);
return 1 if ($args{return});
my $msg;
if ($config{avoidGM_near} == 1) {
# Mode 1: teleport & disconnect
useTeleport(1);
$msg = TF("GM %s is nearby, teleport & disconnect for %d
seconds", $player->{name}, $config{avoidGM_reconnect});
relog($config{avoidGM_reconnect}, 1);
} elsif ($config{avoidGM_near} == 2) {
# Mode 2: disconnect
$msg = TF("GM %s is nearby, disconnect for %s seconds",
$player->{name}, $config{avoidGM_reconnect});
relog($config{avoidGM_reconnect}, 1);
} elsif ($config{avoidGM_near} == 3) {
# Mode 3: teleport
useTeleport(1);
$msg = TF("GM %s is nearby, teleporting", $player->{name
});
} elsif ($config{avoidGM_near} >= 4) {
# Mode 4: respawn
useTeleport(2);
$msg = TF("GM %s is nearby, respawning", $player->{name}
);
}
warning "$msg\n";
chatLog("k", "*** $msg ***\n");
return 1;
}
return 0;
}
##
# avoidList_near()
# Returns: 1 if someone was detected, 0 if no one was detected.
#
# Checks if any of the surrounding players are on the avoid.txt avoid list.
# Disconnects / teleports if a player is detected.
sub avoidList_near {
return if ($config{avoidList_inLockOnly} && $field->name ne $config{lock
Map});
my $players = $playersList->getItems();
foreach my $player (@{$players}) {
my $avoidPlayer = $avoid{Players}{lc($player->{name})};
my $avoidID = $avoid{ID}{$player->{nameID}};
if (!$net->clientAlive() && ( ($avoidPlayer && $avoidPlayer->{di
sconnect_on_sight}) || ($avoidID && $avoidID->{disconnect_on_sight}) )) {
warning TF("%s (%s) is nearby, disconnecting...\n", $pla
yer->{name}, $player->{nameID});
chatLog("k", TF("*** Found %s (%s) nearby and disconnect
ed ***\n", $player->{name}, $player->{nameID}));
warning TF("Disconnect for %s seconds...\n", $config{avo
idList_reconnect});
relog($config{avoidList_reconnect}, 1);
return 1;
} elsif (($avoidPlayer && $avoidPlayer->{teleport_on_sight}) ||
($avoidID && $avoidID->{teleport_on_sight})) {
message TF("Teleporting to avoid player %s (%s)\n", $pla
yer->{name}, $player->{nameID}), "teleport";
chatLog("k", TF("*** Found %s (%s) nearby and teleported
***\n", $player->{name}, $player->{nameID}));
useTeleport(1);
return 1;
}
}
return 0;
}
sub avoidList_ID {
return if (!($config{avoidList}) || ($config{avoidList_inLockOnly} && $f
ield->name ne $config{lockMap}));
my $avoidID = unpack("V", shift);
if ($avoid{ID}{$avoidID} && $avoid{ID}{$avoidID}{disconnect_on_sight}) {
warning TF("%s is nearby, disconnecting...\n", $avoidID);
chatLog("k", TF("*** Found %s nearby and disconnected ***\n", $a
voidID));
warning TF("Disconnect for %s seconds...\n", $config{avoidList_r
econnect});
relog($config{avoidList_reconnect}, 1);
return 1;
}
return 0;
}
sub compilePortals {
my $checkOnly = shift;
my %mapPortals;
my %mapSpawns;
my %missingMap;
my $pathfinding;
my @solution;
my $field;
# Collect portal source and destination coordinates per map
foreach my $portal (keys %portals_lut) {
$mapPortals{$portals_lut{$portal}{source}{map}}{$portal}{x} = $p
ortals_lut{$portal}{source}{x};
$mapPortals{$portals_lut{$portal}{source}{map}}{$portal}{y} = $p
ortals_lut{$portal}{source}{y};
foreach my $dest (keys %{$portals_lut{$portal}{dest}}) {
next if $portals_lut{$portal}{dest}{$dest}{map} eq '';
$mapSpawns{$portals_lut{$portal}{dest}{$dest}{map}}{$des
t}{x} = $portals_lut{$portal}{dest}{$dest}{x};
$mapSpawns{$portals_lut{$portal}{dest}{$dest}{map}}{$des
t}{y} = $portals_lut{$portal}{dest}{$dest}{y};
}
}
$pathfinding = new PathFinding if (!$checkOnly);
# Calculate LOS values from each spawn point per map to other portals on
same map
foreach my $map (sort keys %mapSpawns) {
message TF("Processing map %s...\n", $map), "system" unless $che
ckOnly;
foreach my $spawn (keys %{$mapSpawns{$map}}) {
foreach my $portal (keys %{$mapPortals{$map}}) {
next if $spawn eq $portal;
next if $portals_los{$spawn}{$portal} ne '';
return 1 if $checkOnly;
if ((!$field || $field->{name} ne $map) && !$mis
singMap{$map}) {
eval {
$field = new Field(name => $map)
;
};
if ($@) {
$missingMap{$map} = 1;
}
}
my %start = %{$mapSpawns{$map}{$spawn}};
my %dest = %{$mapPortals{$map}{$portal}};
closestWalkableSpot($field, \%start);
closestWalkableSpot($field, \%dest);
$pathfinding->reset(
start => \%start,
dest => \%dest,
field => $field
);
my $count = $pathfinding->runcount;
$portals_los{$spawn}{$portal} = ($count >= 0) ?
$count : 0;
debug "LOS in $map from $start{x},$start{y} to $
dest{x},$dest{y}: $portals_los{$spawn}{$portal}\n";
}
}
}
return 0 if $checkOnly;
# Write new portalsLOS.txt
writePortalsLOS(Settings::getTableFilename("portalsLOS.txt"), \%portals_
los);
message TF("Wrote portals Line of Sight table to '%s'\n", Settings::getT
ableFilename("portalsLOS.txt")), "system";
# Print warning for missing fields
if (%missingMap) {
warning TF("----------------------------Error Summary-----------
-----------------\n");
warning TF("Missing: %s.fld\n", $_) foreach (sort keys %missingM
ap);
warning TF("Note: LOS information for the above listed map(s) wi
ll be inaccurate;\n" .
" however it is safe to ignore if those map(s) are
not used\n");
warning TF("----------------------------Error Summary-----------
-----------------\n");
}
}
sub compilePortals_check {
return compilePortals(1);
}
sub portalExists {
my ($map, $r_pos) = @_;
foreach (keys %portals_lut) {
if ($portals_lut{$_}{source}{map} eq $map
&& $portals_lut{$_}{source}{x} == $r_pos->{x}
&& $portals_lut{$_}{source}{y} == $r_pos->{y}) {
return $_;
}
}
return;
}
sub portalExists2 {
my ($src, $src_pos, $dest, $dest_pos) = @_;
my $srcx = $src_pos->{x};
my $srcy = $src_pos->{y};
my $destx = $dest_pos->{x};
my $desty = $dest_pos->{y};
my $destID = "$dest $destx $desty";
foreach (keys %portals_lut) {
my $entry = $portals_lut{$_};
if ($entry->{source}{map} eq $src
&& $entry->{source}{pos}{x} == $srcx
&& $entry->{source}{pos}{y} == $srcy
&& $entry->{dest}{$destID}) {
return $_;
}
}
return;
}
sub redirectXKoreMessages {
my ($type, $domain, $level, $globalVerbosity, $message, $user_data) = @_
;
return if ($config{'XKore_silent'} || $type eq "debug" || $level > 0 ||
$net->getState() != Network::IN_GAME || $XKore_dontRedirect);
return if ($domain =~ /^(connection|startup|pm|publicchat|guildchat|guil
dnotice|selfchat|emotion|drop|inventory|deal|storage|input)$/);
return if ($domain =~ /^(attack|skill|list|info|partychat|npc|route)/);
$message =~ s/\n*$//s;
$message =~ s/\n/\\n/g;
sendMessage($messageSender, "k", $message);
}
sub monKilled {
$monkilltime = time();
# if someone kills it
if (($monstarttime == 0) || ($monkilltime < $monstarttime)) {
$monstarttime = 0;
$monkilltime = 0;
}
$elasped = $monkilltime - $monstarttime;
$totalelasped = $totalelasped + $elasped;
if ($totalelasped == 0) {
$dmgpsec = 0
} else {
$dmgpsec = $totaldmg / $totalelasped;
}
}
# Resolves a player or monster ID into a name
# Obsoleted by Actor module, don't use this!
sub getActorName {
my $id = shift;
if (!$id) {
return 'Nothing';
} else {
my $hash = Actor::get($id);
return $hash->nameString;
}
}
# Resolves a pair of player/monster IDs into names
sub getActorNames {
my ($sourceID, $targetID, $verb1, $verb2) = @_;
my $source = getActorName($sourceID);
my $verb = $source eq 'You' ? $verb1 : $verb2;
my $target;
if ($targetID eq $sourceID) {
if ($targetID eq $accountID) {
$target = 'yourself';
} else {
$target = 'self';
}
} else {
$target = getActorName($targetID);
}
return ($source, $verb, $target);
}
# return ID based on name if party member is online
sub findPartyUserID {
if ($char->{party} && %{$char->{party}}) {
my $partyUserName = shift;
for (my $j = 0; $j < @partyUsersID; $j++) {
next if ($partyUsersID[$j] eq "");
if ($partyUserName eq $char->{party}{users}{$partyUsersI
D[$j]}{name}
&& $char->{party}{users}{$partyUsersID[$j]}{onli
ne}) {
return $partyUsersID[$j];
}
}
}
return undef;
}
# fill in a hash of NPC information either based on location ("map x y")
sub getNPCInfo {
my $id = shift;
my $return_hash = shift;
undef %{$return_hash};
my ($map, $x, $y) = split(/ +/, $id, 3);
$$return_hash{map} = $map;
$$return_hash{pos}{x} = $x;
$$return_hash{pos}{y} = $y;
if (($$return_hash{map} ne "") && ($$return_hash{pos}{x} ne "") && ($$re
turn_hash{pos}{y} ne "")) {
$$return_hash{ok} = 1;
} else {
error TF("Invalid NPC information for autoBuy, autoSell or autoS
torage! (%s)\n", $id);
}
}
sub checkSelfCondition {
my $prefix = shift;
return 0 if (!$prefix);
return 0 if ($config{$prefix . "_disabled"});
return 0 if $config{$prefix."_whenIdle"} && !AI::isIdle();
# *_manualAI 0 = auto only
# *_manualAI 1 = manual only
# *_manualAI 2 = auto or manual
if ($config{$prefix . "_manualAI"} == 0 || !(defined $config{$prefix . "_ma
nualAI"})) {
return 0 if ($AI != 2);
}elsif ($config{$prefix . "_manualAI"} == 1){
return 0 if ($AI != 1);
}else {
return 0 if ($AI == 0);
}
if ($config{$prefix . "_hp"}) {
if ($config{$prefix."_hp"} =~ /^(.*)\%$/) {
return 0 if (!inRange($char->hp_percent, $1));
} else {
return 0 if (!inRange($char->{hp}, $config{$prefix."_hp"
}));
}
}
if ($config{$prefix."_sp"}) {
if ($config{$prefix."_sp"} =~ /^(.*)\%$/) {
return 0 if (!inRange($char->sp_percent, $1));
} else {
return 0 if (!inRange($char->{sp}, $config{$prefix."_sp"
}));
}
}
if ($config{$prefix."_homunculus"} =~ /\S/) {
return 0 if (!!$config{$prefix."_homunculus"}) ^ ($char->{homunc
ulus} && !$char->{homunculus}{state});
}
if ($char->{homunculus}) {
if ($config{$prefix . "_homunculus_hp"}) {
if ($config{$prefix."_homunculus_hp"} =~ /^(.*)\%$/) {
return 0 if (!inRange($char->{homunculus}{hpPerc
ent}, $1));
} else {
return 0 if (!inRange($char->{homunculus}{hp}, $
config{$prefix."_homunculus_hp"}));
}
}
if ($config{$prefix."_homunculus_sp"}) {
if ($config{$prefix."_homunculus_sp"} =~ /^(.*)\%$/) {
return 0 if (!inRange($char->{homunculus}{spPerc
ent}, $1));
} else {
return 0 if (!inRange($char->{homunculus}{sp}, $
config{$prefix."_homunculus_sp"}));
}
}
if ($config{$prefix."_homunculus_dead"}) {
return 0 unless ($char->{homunculus}{state} & 4);
}
}
if ($config{$prefix."_mercenary"} =~ /\S/) {
return 0 if (!!$config{$prefix."_mercenary"}) ^ (!!$char->{merce
nary});
}
if ($char->{mercenary}) {
if ($config{$prefix . "_mercenary_hp"}) {
if ($config{$prefix."_mercenary_hp"} =~ /^(.*)\%$/) {
return 0 if (!inRange($char->{mercenary}{hpPerce
nt}, $1));
} else {
return 0 if (!inRange($char->{mercenary}{hp}, $c
onfig{$prefix."_mercenary_hp"}));
}
}
if ($config{$prefix."_mercenary_sp"}) {
if ($config{$prefix."_mercenary_sp"} =~ /^(.*)\%$/) {
return 0 if (!inRange($char->{mercenary}{spPerce
nt}, $1));
} else {
return 0 if (!inRange($char->{mercenary}{sp}, $c
onfig{$prefix."_mercenary_sp"}));
}
}
if ($config{$prefix . "_mercenary_whenStatusActive"}) {
return 0 unless $char->{mercenary}->statusActive($config
{$prefix . "_mercenary_whenStatusActive"});
}
if ($config{$prefix . "_mercenary_whenStatusInactive"}) {
return 0 if $char->{mercenary}->statusActive($config{$pr
efix . "_mercenary_whenStatusInactive"});
}
}
# check skill use SP if this is a 'use skill' condition
if ($prefix =~ /skill/i) {
my $skill = Skill->new(auto => $config{$prefix});
return 0 unless ($char->getSkillLevel($skill)
|| $config{$prefix."_equip_leftA
ccessory"}
|| $config{$prefix."_equip_right
Accessory"}
|| $config{$prefix."_equip_leftH
and"}
|| $config{$prefix."_equip_right
Hand"}
|| $config{$prefix."_equip_robe"
}
);
return 0 unless ($char->{sp} >= $skill->getSP($config{$prefix .
"_lvl"} || $char->getSkillLevel($skill)));
}
if (defined $config{$prefix . "_aggressives"}) {
return 0 unless (inRange(scalar ai_getAggressives(), $config{$pr
efix . "_aggressives"}));
}
if (defined $config{$prefix . "_partyAggressives"}) {
return 0 unless (inRange(scalar ai_getAggressives(undef, 1), $co
nfig{$prefix . "_partyAggressives"}));
}
if ($config{$prefix . "_stopWhenHit"} > 0) { return 0 if (scalar ai_getM
onstersAttacking($accountID)); }
if ($config{$prefix . "_whenFollowing"} && $config{follow}) {
return 0 if (!checkFollowMode());
}
if ($config{$prefix . "_whenStatusActive"}) {
return 0 unless $char->statusActive($config{$prefix . "_whenStat
usActive"});
}
if ($config{$prefix . "_whenStatusInactive"}) {
return 0 if $char->statusActive($config{$prefix . "_whenStatusIn
active"});
}
if ($config{$prefix . "_onAction"}) { return 0 unless (existsInList($con
fig{$prefix . "_onAction"}, AI::action())); }
if ($config{$prefix . "_notOnAction"}) { return 0 if (existsInList($conf
ig{$prefix . "_notOnAction"}, AI::action())); }
if ($config{$prefix . "_spirit"}) {return 0 unless (inRange(defined $cha
r->{spirits} ? $char->{spirits} : 0, $config{$prefix . "_spirit"})); }
if ($config{$prefix . "_timeout"}) { return 0 unless timeOut($ai_v{$pref
ix . "_time"}, $config{$prefix . "_timeout"}) }
if ($config{$prefix . "_inLockOnly"} > 0) { return 0 unless ($field->nam
e eq $config{lockMap}); }
if ($config{$prefix . "_notWhileSitting"} > 0) { return 0 if ($char->{si
tting}); }
if ($config{$prefix . "_notInTown"} > 0) { return 0 if ($field->isCity);
}
if ($config{$prefix . "_monsters"} && !($prefix =~ /skillSlot/i) && !($p
refix =~ /ComboSlot/i)) {
my $exists;
foreach (ai_getAggressives()) {
if (existsInList($config{$prefix . "_monsters"}, $monste
rs{$_}->name)) {
$exists = 1;
last;
}
}
return 0 unless $exists;
}
if ($config{$prefix . "_defendMonsters"}) {
my $exists;
foreach (ai_getMonstersAttacking($accountID)) {
if (existsInList($config{$prefix . "_defendMonsters"}, $
monsters{$_}->name)) {
$exists = 1;
last;
}
}
return 0 unless $exists;
}
if ($config{$prefix . "_notMonsters"} && !($prefix =~ /skillSlot/i) && !
($prefix =~ /ComboSlot/i)) {
my $exists;
foreach (ai_getAggressives()) {
if (existsInList($config{$prefix . "_notMonsters"}, $mon
sters{$_}->name)) {
return 0;
}
}
}
if ($config{$prefix."_inInventory"}) {
foreach my $input (split / *, */, $config{$prefix."_inInventory"
}) {
my ($itemName, $count) = $input =~ /(.*?)(?:\s+([><]=? *
\d+))?$/;
$count = '>0' if $count eq '';
my $item = $char->inventory->getByName($itemName);
return 0 if !inRange(!$item ? 0 : $item->{amount}, $coun
t);
}
}
if ($config{$prefix."_inCart"}) {
foreach my $input (split / *, */, $config{$prefix."_inCart"}) {
my ($item,$count) = $input =~ /(.*?)(?:\s+([><]=? *\d+))
?$/;
$count = '>0' if $count eq '';
my $iX = findIndexString_lc($cart{inventory}, "name", $i
tem);
my $item = $cart{inventory}[$iX];
return 0 if !inRange(!defined $iX ? 0 : $item->{amount},
$count);
}
}
if ($config{$prefix."_whenGround"}) {
return 0 unless whenGroundStatus(calcPosition($char), $config{$p
refix."_whenGround"});
}
if ($config{$prefix."_whenNotGround"}) {
return 0 if whenGroundStatus(calcPosition($char), $config{$prefi
x."_whenNotGround"});
}
if ($config{$prefix."_whenPermitSkill"}) {
return 0 unless $char->{permitSkill} &&
$char->{permitSkill}->getIDN == Skill->new(auto => $conf
ig{$prefix."_whenPermitSkill"})->getIDN;
}
if ($config{$prefix."_whenNotPermitSkill"}) {
return 0 if $char->{permitSkill} &&
$char->{permitSkill}->getIDN == Skill->new(auto => $conf
ig{$prefix."_whenNotPermitSkill"})->getIDN;
}
if ($config{$prefix."_whenFlag"}) {
return 0 unless $flags{$config{$prefix."_whenFlag"}};
}
if ($config{$prefix."_whenNotFlag"}) {
return 0 unless !$flags{$config{$prefix."_whenNotFlag"}};
}
if ($config{$prefix."_onlyWhenSafe"}) {
return 0 if !isSafe();
}
if ($config{$prefix."_inMap"}) {
return 0 unless (existsInList($config{$prefix . "_inMap"}, $fiel
d->name));
}
if ($config{$prefix."_notInMap"}) {
return 0 if (existsInList($config{$prefix . "_notInMap"}, $field
->name));
}
if ($config{$prefix."_whenEquipped"}) {
my $item = Actor::Item::get($config{$prefix."_whenEquipped"});
return 0 unless $item && $item->{equipped};
}
if ($config{$prefix."_whenNotEquipped"}) {
my $item = Actor::Item::get($config{$prefix."_whenNotEquipped"})
;
return 0 if $item && $item->{equipped};
}
if ($config{$prefix."_zeny"}) {
return 0 if (!inRange($char->{zeny}, $config{$prefix."_zeny"}));
}
# not working yet
if ($config{$prefix."_whenWater"}) {
my $pos = calcPosition($char);
return 0 if ($field->getBlock($pos->{x}, $pos->{y}) != Field::WA
LKABLE_WATER);
}
if (defined $config{$prefix.'_devotees'}) {
return 0 unless inRange(scalar keys %{$devotionList->{$accountID
}{targetIDs}}, $config{$prefix.'_devotees'});
}
my %hookArgs;
$hookArgs{prefix} = $prefix;
$hookArgs{return} = 1;
Plugins::callHook("checkSelfCondition", \%hookArgs);
return 0 if (!$hookArgs{return});
return 1;
}
sub checkPlayerCondition {
my ($prefix, $id) = @_;
return 0 if (!$id);
my $player = Actor::get($id);
return 0 unless (
UNIVERSAL::isa($player, 'Actor::You')
|| UNIVERSAL::isa($player, 'Actor::Player')
|| UNIVERSAL::isa($player, 'Actor::Slave')
);
# my $player = $playersList->getByID($id) || $slavesList->getByID($id);
if ($config{$prefix . "_timeout"}) { return 0 unless timeOut($ai_v{$pref
ix . "_time"}{$id}, $config{$prefix . "_timeout"}) }
if ($config{$prefix . "_whenStatusActive"}) {
return 0 unless $player->statusActive($config{$prefix . "_whenSt
atusActive"});
}
if ($config{$prefix . "_whenStatusInactive"}) {
return 0 if $player->statusActive($config{$prefix . "_whenStatus
Inactive"});
}
if ($config{$prefix . "_notWhileSitting"} > 0) { return 0 if ($player->{
sitting}); }
# TODO: Optimize this
if ($config{$prefix . "_hp"}) {
# Target is Actor::You
if ($char->{ID} eq $id) {
if ($config{$prefix."_hp"} =~ /^(.*)\%$/) {
return 0 if (!inRange($char->hp_percent, $1));
} else {
return 0 if (!inRange($char->{hp}, $config{$pref
ix."_hp"}));
}
# Target is Actor::Player in our Party
} elsif ($char->{party} && $char->{party}{users}{$id}) {
# Fix Heal when Target HP is not set yet.
# return 0 if (!defined($player->{hp}) || $player->{hp}
== 0);
return 0 if ($char->{party}{users}{$id}{hp} == 0);
if ($config{$prefix."_hp"} =~ /^(.*)\%$/) {
# return 0 if (!inRange(percent_hp($player), $1)
);
return 0 if (!inRange(percent_hp($char->{party}{
users}{$id}), $1));
} else {
# return 0 if (!inRange($player->{hp}, $config{$
prefix . "_hp"}));
return 0 if (!inRange($char->{party}{users}{$id}
{hp}, $config{$prefix . "_hp"}));
}
# Target is Actor::Slave 'Homunculus' type
} elsif ($char->{homunculus} && $char->{homunculus}{ID} eq $id)
{
if ($config{$prefix."_hp"} =~ /^(.*)\%$/) {
return 0 if (!inRange(percent_hp($char->{homuncu
lus}), $1));
} else {
return 0 if (!inRange($char->{homunculus}{hp}, $
config{$prefix . "_hp"}));
}
# Target is Actor::Slave 'Mercenary' type
} elsif ($char->{mercenary} && $char->{mercenary}{ID} eq $id) {
if ($config{$prefix."_hp"} =~ /^(.*)\%$/) {
return 0 if (!inRange(percent_hp($char->{mercena
ry}), $1));
} else {
return 0 if (!inRange($char->{mercenary}{hp}, $c
onfig{$prefix . "_hp"}));
}
}
}
if ($config{$prefix."_deltaHp"}){
return 0 unless inRange($player->{deltaHp}, $config{$prefix."_de
ltaHp"});
}
# check player job class
if ($config{$prefix . "_isJob"}) { return 0 unless (existsInList($config
{$prefix . "_isJob"}, $jobs_lut{$player->{jobID}})); }
if ($config{$prefix . "_isNotJob"}) { return 0 if (existsInList($config{
$prefix . "_isNotJob"}, $jobs_lut{$player->{jobID}})); }
if ($config{$prefix . "_aggressives"}) {
return 0 unless (inRange(scalar ai_getPlayerAggressives($id), $c
onfig{$prefix . "_aggressives"}));
}
if ($config{$prefix . "_defendMonsters"}) {
my $exists;
foreach (ai_getMonstersAttacking($id)) {
if (existsInList($config{$prefix . "_defendMonsters"}, $
monsters{$_}{name})) {
$exists = 1;
last;
}
}
return 0 unless $exists;
}
if ($config{$prefix . "_monsters"}) {
my $exists;
foreach (ai_getPlayerAggressives($id)) {
if (existsInList($config{$prefix . "_monsters"}, $monste
rs{$_}{name})) {
$exists = 1;
last;
}
}
return 0 unless $exists;
}
if ($config{$prefix."_whenGround"}) {
return 0 unless whenGroundStatus(calcPosition($player), $config{
$prefix."_whenGround"});
}
if ($config{$prefix."_whenNotGround"}) {
return 0 if whenGroundStatus(calcPosition($player), $config{$pre
fix."_whenNotGround"});
}
if ($config{$prefix."_dead"}) {
return 0 if !$player->{dead};
} else {
return 0 if $player->{dead};
}
# Note: This will always fail for Actor::Slave
if ($config{$prefix."_whenWeaponEquipped"}) {
return 0 unless $player->{weapon};
}
# Note: This will always fail for Actor::Slave
if ($config{$prefix."_whenShieldEquipped"}) {
return 0 unless $player->{shield};
}
# Note: This will always fail for Actor::Slave
if ($config{$prefix."_isGuild"}) {
return 0 unless ($player->{guild} && existsInList($config{$prefi
x . "_isGuild"}, $player->{guild}{name}));
}
if ($config{$prefix."_dist"}) {
return 0 unless inRange(distance(calcPosition($char), calcPositi
on($player)), $config{$prefix."_dist"});
}
if ($config{$prefix."_isNotMyDevotee"}) {
return 0 if (defined $devotionList->{$accountID}->{targetIDs}->{
$id});
}
my %args = (
player => $player,
prefix => $prefix,
return => 1
);
Plugins::callHook('checkPlayerCondition', \%args);
return $args{return};
}
sub checkMonsterCondition {
my ($prefix, $monster) = @_;
if ($config{$prefix . "_timeout"}) { return 0 unless timeOut($ai_v{$pref
ix . "_time"}{$monster->{ID}}, $config{$prefix . "_timeout"}) }
if (my $misses = $config{$prefix . "_misses"}) {
return 0 unless inRange($monster->{atkMiss}, $misses);
}
if (my $misses = $config{$prefix . "_totalMisses"}) {
return 0 unless inRange($monster->{missedFromYou}, $misses);
}
if ($config{$prefix . "_whenStatusActive"}) {
return 0 unless $monster->statusActive($config{$prefix . "_whenS
tatusActive"});
}
if ($config{$prefix . "_whenStatusInactive"}) {
return 0 if $monster->statusActive($config{$prefix . "_whenStatu
sInactive"});
}
if ($config{$prefix."_whenGround"}) {
return 0 unless whenGroundStatus(calcPosition($monster), $config
{$prefix."_whenGround"});
}
if ($config{$prefix."_whenNotGround"}) {
return 0 if whenGroundStatus(calcPosition($monster), $config{$pr
efix."_whenNotGround"});
}
if ($config{$prefix."_dist"}) {
return 0 unless inRange(distance(calcPosition($char), calcPositi
on($monster)), $config{$prefix."_dist"});
}
if ($config{$prefix."_deltaHp"}){
return 0 unless inRange($monster->{deltaHp}, $config{$prefix."_d
eltaHp"});
}
# This is only supposed to make sense for players,
# but it has to be here for attackSkillSlot PVP to work
if ($config{$prefix."_whenWeaponEquipped"}) {
return 0 unless $monster->{weapon};
}
if ($config{$prefix."_whenShieldEquipped"}) {
return 0 unless $monster->{shield};
}
my %args = (
monster => $monster,
prefix => $prefix,
return => 1
);
Plugins::callHook('checkMonsterCondition', \%args);
return $args{return};
}
##
# findCartItemInit()
#
# Resets all "found" flags in the cart to 0.
sub findCartItemInit {
for (@{$cart{inventory}}) {
next unless $_ && %{$_};
undef $_->{found};
}
}
##
# findCartItem($name [, $found [, $nounid]])
#
# Returns the integer index into $cart{inventory} for the cart item matching
# the given name, or undef.
#
# If an item is found, the "found" value for that item is set to 1. Items
# cannot be found again until you reset the "found" flags using
# findCartItemInit(), if $found is true.
#
# Unidentified items will not be returned if $nounid is true.
sub findCartItem {
my ($name, $found, $nounid) = @_;
$name = lc($name);
my $index = 0;
for (@{$cart{inventory}}) {
if (lc($_->{name}) eq $name &&
!($found && $_->{found}) &&
!($nounid && !$_->{identified})) {
$_->{found} = 1;
return $index;
}
$index++;
}
return undef;
}
##
# makeShop()
#
# Returns an array of items to sell. The array can be no larger than the
# maximum number of items that the character can vend. Each item is a hash
# reference containing the keys "index", "amount" and "price".
#
# If there is a problem with opening a shop, an error message will be printed
# and nothing will be returned.
sub makeShop {
if ($shopstarted) {
error T("A shop has already been opened.\n");
return;
}
return unless $char;
if (!$char->{skills}{MC_VENDING}{lv}) {
error T("You don't have the Vending skill.\n");
return;
}
if (!$shop{title_line}) {
error T("Your shop does not have a title.\n");
return;
}
my @items = ();
my $max_items = $char->{skills}{MC_VENDING}{lv} + 2;
# Iterate through items to be sold
findCartItemInit();
shuffleArray(\@{$shop{items}}) if ($config{'shop_random'} eq "2");
for my $sale (@{$shop{items}}) {
my $index = findCartItem($sale->{name}, 1, 1);
next unless defined($index);
# Found item to vend
my $cart_item = $cart{inventory}[$index];
my $amount = $cart_item->{amount};
my %item;
$item{name} = $cart_item->{name};
$item{index} = $index;
$item{price} = $sale->{price};
$item{amount} =
$sale->{amount} && $sale->{amount} < $amount ?
$sale->{amount} : $amount;
push(@items, \%item);
# We can't vend anymore items
last if @items >= $max_items;
}
if (!@items) {
error T("There are no items to sell.\n");
return;
}
shuffleArray(\@items) if ($config{'shop_random'} eq "1");
return @items;
}
sub openShop {
my @items = makeShop();
my @shopnames;
return unless @items;
@shopnames = split(/;;/, $shop{title_line});
$shop{title} = $shopnames[int rand($#shopnames + 1)];
$shop{title} = ($config{shopTitleOversize}) ? $shop{title} : substr($sho
p{title},0,36);
$messageSender->sendOpenShop($shop{title}, \@items);
message TF("Shop opened (%s) with %d selling items.\n", $shop{title}, @i
tems.""), "success";
$shopstarted = 1;
$shopEarned = 0;
}
sub closeShop {
if (!$shopstarted) {
error T("A shop has not been opened.\n");
return;
}
$messageSender->sendCloseShop();
$shopstarted = 0;
$timeout{'ai_shop'}{'time'} = time;
message T("Shop closed.\n");
}
##
# inLockMap()
#
# Returns 1 (true) if character is located in its lockmap.
# Returns 0 (false) if character is not located in lockmap.
sub inLockMap {
if ($field->baseName eq $config{'lockMap'}) {
return 1;
} else {
return 0;
}
}
sub parseReload {
my ($args) = @_;
eval {
my $progressHandler = sub {
my ($filename) = @_;
message TF("Loading %s...\n", $filename);
};
if ($args eq 'all') {
Settings::loadAll($progressHandler);
} else {
Settings::loadByRegexp(qr/$args/, $progressHandler);
}
Log::initLogFiles();
};
if (my $e = caught('UTF8MalformedException')) {
error TF(
"The file %s must be valid UTF-8 encoded, which it is \n
" .
"currently not. To solve this prolem, please use Notepad
\n" .
"to save that file as valid UTF-8.",
$e->textfile);
} elsif ($@) {
die $@;
}
}
sub MODINIT {
OpenKoreMod::initMisc() if (defined(&OpenKoreMod::initMisc));
}
return 1;

You might also like