diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..5bdd64324 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +language: cpp +compiler: gcc +before_install: + - sudo apt-get update -qq + - sudo apt-get install -y libmysqlclient-dev libperl-dev libboost-dev liblua5.1-0-dev zlib1g-dev +script: + - cmake -G "Unix Makefiles" -DEQEMU_BUILD_TESTS=ON -DEQEMU_ENABLE_BOTS=ON + - make + - ./bin/tests +branches: + only: master +notifications: + email: false + irc: + channels: "irc.eqemulator.net#eqemucoders" +os: linux \ No newline at end of file diff --git a/README.md b/README.md index edee03cca..f2245dcf4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,53 @@ -EQEmu - Custom Game Implementation for EverQuest +EQEmu +=== -Dependencies can be obtained at http://eqemu.github.io +[![Build Status](https://travis-ci.org/EQEmu/Server.svg?branch=master)](https://travis-ci.org/EQEmu/Server) + +Overview +--- + +EQEmu is a custom server implementation for EverQuest + +Dependencies +--- + +For Windows: http://eqemu.github.io + +Login Server dependencies for Windows/Linux/OSX: http://eqemu.github.io + +For Debian based distros (adjust to your local flavor): + +- libmysqlclient-dev +- libperl-dev +- liblua5.1-0-dev (5.2 should work as well) +- libboost-dev + +Further instructions on building the source can be found on the +[wiki](http://wiki.eqemulator.org/i?M=Wiki). + +Bug reports +--- + +Please use the [issue tracker](issue-tracker) provided by GitHub to send us bug +reports or feature requests. + +The [EQEmu Forums](http://www.eqemulator.org/forums/) also have forums to submit +bugs/get help with bugs. + +Contributions +--- + +The preferred way to contribute is to fork the repo and submit a pull request on +GitHub. If you need help with your changes, you can always post on the forums or +try IRC. You can also post unified diffs (`git diff` should do the trick) on the +[Server Code Submissions](http://www.eqemulator.org/forums/forumdisplay.php?f=669) +forum, although pull requests will be much quicker and easier on all parties. + +Contact +--- + - **User IRC Channel**: `#eqemu` on `irc.eqemulator.net` + - **Developer IRC Channel**: `#eqemucoders` on `irc.eqemulator.net` + +- [EQEmulator Forums](http://www.eqemulator.org/forums) +- [EQEmulator Wiki](http://wiki.eqemulator.org/i?M=Wiki) -More Information: https://github.com/EQEmu/Server/wiki diff --git a/changelog.txt b/changelog.txt index 464669fd5..16d63a05e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,17 @@ EQEMu Changelog (Started on Sept 24, 2003 15:50) ------------------------------------------------------- +== 08/02/2014 == +Kayen: Implemented spell_news fields +- npc_no_los (check if LOS is required for spells) +- InCombat, OutofCombat - Used together to restrict spells to only be cast while +in/out of combat (beneficial) or if target is in/out of combat (detrimental). +-min_dist, min_dist_mod, max_dist, max_dist_mod - Scales spell power based on targets distance from caster. +*This will require further work to fully implement but will work with 90% of live spells as is. +*If making custom spells do not include effects that can't be scaled (like a spell trigger) +- min_rage sets minimum distance range that must be away from target. + +Required SQL: utils/sql/git/required/2014_08_02_spells_new.sql + == 07/31/2014 == Uleat: More inventory slot constant conversions. This should be the bulk of everything..but, due to the size of the server code, there may be some hidden ones. (client_packet.cpp and the client translators still need a thorough review.) diff --git a/client_files/export/main.cpp b/client_files/export/main.cpp index 8aa29bea6..d89976b88 100644 --- a/client_files/export/main.cpp +++ b/client_files/export/main.cpp @@ -70,7 +70,7 @@ void ExportSpells(SharedDatabase *db) { } char errbuf[MYSQL_ERRMSG_SIZE]; - char *query = "SELECT * FROM spells_new ORDER BY id"; + const char *query = "SELECT * FROM spells_new ORDER BY id"; MYSQL_RES *result; MYSQL_ROW row; if(db->RunQuery(query, strlen(query), errbuf, &result)) { @@ -176,7 +176,7 @@ void ExportBaseData(SharedDatabase *db) { } char errbuf[MYSQL_ERRMSG_SIZE]; - char *query = "SELECT * FROM base_data ORDER BY level, class"; + const char *query = "SELECT * FROM base_data ORDER BY level, class"; MYSQL_RES *result; MYSQL_ROW row; if(db->RunQuery(query, strlen(query), errbuf, &result)) { diff --git a/client_files/import/main.cpp b/client_files/import/main.cpp index 8115600bf..d902bc98e 100644 --- a/client_files/import/main.cpp +++ b/client_files/import/main.cpp @@ -61,7 +61,7 @@ int main(int argc, char **argv) { int GetSpellColumns(SharedDatabase *db) { char errbuf[MYSQL_ERRMSG_SIZE]; - char *query = "DESCRIBE spells_new"; + const char *query = "DESCRIBE spells_new"; MYSQL_RES *result; MYSQL_ROW row; int res = 0; @@ -234,4 +234,4 @@ void ImportBaseData(SharedDatabase *db) { } fclose(f); -} \ No newline at end of file +} diff --git a/common/shareddb.cpp b/common/shareddb.cpp index b93b57710..7a9858789 100644 --- a/common/shareddb.cpp +++ b/common/shareddb.cpp @@ -1710,7 +1710,7 @@ void SharedDatabase::LoadSpells(void *data, int max_spells) { for (y = 0; y < 16; y++) sp[tempid].deities[y]=atoi(row[126+y]); - sp[tempid].uninterruptable=atoi(row[146]); + sp[tempid].uninterruptable=atoi(row[146]) != 0; sp[tempid].ResistDiff=atoi(row[147]); sp[tempid].dot_stacking_exempt=atoi(row[148]); sp[tempid].RecourseLink = atoi(row[150]); @@ -1726,6 +1726,7 @@ void SharedDatabase::LoadSpells(void *data, int max_spells) { sp[tempid].EndurCost=atoi(row[166]); sp[tempid].EndurTimerIndex=atoi(row[167]); + sp[tempid].IsDisciplineBuff = atoi(row[168]) != 0; sp[tempid].HateAdded=atoi(row[173]); sp[tempid].EndurUpkeep=atoi(row[174]); sp[tempid].numhitstype = atoi(row[175]); @@ -1741,19 +1742,26 @@ void SharedDatabase::LoadSpells(void *data, int max_spells) { sp[tempid].viral_targets = atoi(row[191]); sp[tempid].viral_timer = atoi(row[192]); sp[tempid].NimbusEffect = atoi(row[193]); - sp[tempid].directional_start = (float)atoi(row[194]); - sp[tempid].directional_end = (float)atoi(row[195]); + sp[tempid].directional_start = static_cast(atoi(row[194])); + sp[tempid].directional_end = static_cast(atoi(row[195])); sp[tempid].not_extendable = atoi(row[197]) != 0; sp[tempid].suspendable = atoi(row[200]) != 0; + sp[tempid].viral_range = atoi(row[201]); sp[tempid].spellgroup=atoi(row[207]); + sp[tempid].rank = atoi(row[208]); sp[tempid].powerful_flag=atoi(row[209]); sp[tempid].CastRestriction = atoi(row[211]); sp[tempid].AllowRest = atoi(row[212]) != 0; - sp[tempid].NotOutofCombat = atoi(row[213]) != 0; - sp[tempid].NotInCombat = atoi(row[214]) != 0; + sp[tempid].InCombat = atoi(row[213]) != 0; + sp[tempid].OutofCombat = atoi(row[214]) != 0; sp[tempid].aemaxtargets = atoi(row[218]); sp[tempid].maxtargets = atoi(row[219]); sp[tempid].persistdeath = atoi(row[224]) != 0; + sp[tempid].min_dist = atof(row[227]); + sp[tempid].min_dist_mod = atof(row[228]); + sp[tempid].max_dist = atof(row[229]); + sp[tempid].max_dist_mod = atof(row[230]); + sp[tempid].min_range = static_cast(atoi(row[231])); sp[tempid].DamageShieldType = 0; } mysql_free_result(result); @@ -1767,7 +1775,7 @@ void SharedDatabase::LoadSpells(void *data, int max_spells) { int SharedDatabase::GetMaxBaseDataLevel() { char errbuf[MYSQL_ERRMSG_SIZE]; - char *query = "SELECT MAX(level) FROM base_data"; + const char *query = "SELECT MAX(level) FROM base_data"; MYSQL_RES *result; MYSQL_ROW row; int32 ret = 0; @@ -1818,7 +1826,7 @@ bool SharedDatabase::LoadBaseData() { void SharedDatabase::LoadBaseData(void *data, int max_level) { char *base_ptr = reinterpret_cast(data); char errbuf[MYSQL_ERRMSG_SIZE]; - char *query = "SELECT * FROM base_data ORDER BY level, class ASC"; + const char *query = "SELECT * FROM base_data ORDER BY level, class ASC"; MYSQL_RES *result; MYSQL_ROW row; diff --git a/common/spdat.cpp b/common/spdat.cpp index 75f1e9b88..8e93f4e1f 100644 --- a/common/spdat.cpp +++ b/common/spdat.cpp @@ -1040,6 +1040,15 @@ bool IsCastonFadeDurationSpell(uint16 spell_id) return false; } +bool IsPowerDistModSpell(uint16 spell_id) +{ + if (IsValidSpell(spell_id) && + (spells[spell_id].max_dist_mod || spells[spell_id].min_dist_mod) && spells[spell_id].max_dist > spells[spell_id].min_dist) + return true; + + return false; +} + uint32 GetPartialMeleeRuneReduction(uint32 spell_id) { for (int i = 0; i < EFFECT_COUNT; ++i) diff --git a/common/spdat.h b/common/spdat.h index 927550fc6..40f5e658f 100644 --- a/common/spdat.h +++ b/common/spdat.h @@ -131,10 +131,10 @@ typedef enum { /* 41 */ ST_Group = 0x29, /* 42 */ ST_Directional = 0x2a, //ae around this target between two angles /* 43 */ ST_GroupClientAndPet = 0x2b, -/* 44 */ ST_Beam = 0x2c, //like directional but facing in front of you always +/* 44 */ //ST_Beam = 0x2c, //like directional but facing in front of you always /* 45 */ //ST_Ring = 0x2d, // Like a mix of PB ae + rain spell(has ae duration) /* 46 */ ST_TargetsTarget = 0x2e, // uses the target of your target -/* 47 */ //ST_PetMaster = 0x2e, // uses the master as target +/* 47 */ ST_PetMaster = 0x2f, // uses the master as target } SpellTargetType; typedef enum { @@ -697,8 +697,8 @@ struct SPDat_Spell_Struct /* 164 */ // for most spells this appears to mimic ResistDiff /* 166 */ int EndurCost; /* 167 */ int8 EndurTimerIndex; -/* 168 */ //int IsDisciplineBuff; //Will goto the combat window when cast -/* 169 */ +/* 168 */ bool IsDisciplineBuff; //Will goto the combat window when cast +/* 169 - 172*/ //These are zero for ALL spells /* 173 */ int HateAdded; /* 174 */ int EndurUpkeep; /* 175 */ int numhitstype; // defines which type of behavior will tick down the numhit counter. @@ -721,23 +721,30 @@ struct SPDat_Spell_Struct /* 197 */ bool not_extendable; /* 198- 199 */ /* 200 */ bool suspendable; // buff is suspended in suspended buff zones -/* 201 - 202 */ +/* 201 */ int viral_range; +/* 202 */ /* 203 */ //int songcap; // individual song cap (how live currently does it, not implemented) /* 204 - 206 */ /* 207 */ int spellgroup; -/* 208 */ // int rank - increments AA effects with same name +/* 208 */ int rank; //increments AA effects with same name /* 209 */ int powerful_flag; // Need more investigation to figure out what to call this, for now we know -1 makes charm spells not break before their duration is complete, it does alot more though /* 210 */ // bool DurationFrozen; ??? /* 211 */ int CastRestriction; //Various restriction categories for spells most seem targetable race related but have also seen others for instance only castable if target hp 20% or lower or only if target out of combat /* 212 */ bool AllowRest; -/* 213 */ bool NotOutofCombat; //Fail if cast out of combat -/* 214 */ bool NotInCombat; //Fail if cast in combat +/* 213 */ bool InCombat; //Allow spell if target is in combat +/* 214 */ bool OutofCombat; //Allow spell if target is out of combat /* 215 - 217 */ -/* 219 */ int aemaxtargets; // -/* 219 */ int maxtargets; // is used for beam and ring spells for target # limits (not implemented) -/* 220 - 223 */ +/* 218 */ int aemaxtargets; //Is used for various AE effects +/* 219 */ int maxtargets; //Is used for beam and ring spells for target # limits (not implemented) +/* 220 - 223 */ /* 224 */ bool persistdeath; // buff doesn't get stripped on death -/* 225 - 236 */ // Not in DB +/* 225 - 226 */ +/* 227 */ float min_dist; //spell power modified by distance from caster (Min Distance) +/* 228 */ float min_dist_mod; //spell power modified by distance from caster (Modifier at Min Distance) +/* 229 */ float max_dist; //spell power modified by distance from caster (Max Distance) +/* 230 */ float max_dist_mod; //spell power modified by distance from caster (Modifier at Max Distance) +/* 231 */ float min_range; //Min casting range +/* 232 - 236 */ uint8 DamageShieldType; // This field does not exist in spells_us.txt }; @@ -838,6 +845,7 @@ bool IsPersistDeathSpell(uint16 spell_id); bool IsSuspendableSpell(uint16 spell_id); uint32 GetMorphTrigger(uint32 spell_id); bool IsCastonFadeDurationSpell(uint16 spell_id); +bool IsPowerDistModSpell(uint16 spell_id); uint32 GetPartialMeleeRuneReduction(uint32 spell_id); uint32 GetPartialMagicRuneReduction(uint32 spell_id); uint32 GetPartialMeleeRuneAmount(uint32 spell_id); diff --git a/utils/scripts/.gitignore b/utils/scripts/.gitignore new file mode 100644 index 000000000..eba9f59cb --- /dev/null +++ b/utils/scripts/.gitignore @@ -0,0 +1 @@ +opcode_handlers_output \ No newline at end of file diff --git a/utils/scripts/opcode_handlers.py b/utils/scripts/opcode_handlers.py new file mode 100644 index 000000000..2a1921d5d --- /dev/null +++ b/utils/scripts/opcode_handlers.py @@ -0,0 +1,920 @@ +#! /usr/bin/env python +# +# This script generates cross-references to show associated (handled) opcodes +# between the server and client. It will generate files for each client and +# server found, and provide some basic information..such as opcode names and +# values, server handler and whether opcodes are translated on tx/rx, etc... +# +# It's currently limited to the 'Zone' server..but, can be expounded upon to +# include other servers and clients, and other criteria and features. + + +import sys +import os + +from time import time, ctime + + +DEBUG = 1 # {0 - normal, 1 - verbose, 2 - in-depth} + +base_path = os.getcwd()[:-14] # '/utils/scripts' + +client_list = ['6.2', 'Titanium', 'SoF', 'SoD', 'Underfoot', 'RoF', 'RoF2', 'ClientTest'] +server_list = ['Login', 'World', 'Zone', 'UCS', 'ServerTest'] + +client_opcodes = {} +server_opcodes = {} + +client_encodes = {} +client_decodes = {} + +server_handlers = {} + +out_files = {} + + +def main(): + """ Call each method independently and track success """ + + fault = False + faults = [] + + print('') + + if fault is False: + fault = not createoutputdirectory() + + if fault is True: + faults.append('createoutputdirectory()') + + if fault is False: + fault = not opendebugfile() + + if fault is True: + faults.append('opendebugfile()') + + if fault is False: + print('Loading source data...') + + if fault is False: + fault = not loadclientopcodes() + + if fault is True: + faults.append('loadclientopcodes()') + + if fault is False: + fault = not loadserveropcodes() + + if fault is True: + faults.append('loadserveropcodes()') + + if fault is False: + fault = not loadclienttranslators() + + if fault is True: + faults.append('loadclienttranslators()') + + if fault is False: + fault = not loadserverhandlers() + + if fault is True: + faults.append('loadserverhandlers()') + + if fault is False: + fault = not discoverserverhandlers() + + if fault is True: + faults.append('discoverserverhandlers()') + + if fault is False: + fault = not clearemptyserverentries() + + if fault is True: + faults.append('clearemptyserverentries()') + + if fault is False: + print('Creating output streams...') + + if fault is False: + fault = not openoutputfiles() + + if fault is True: + faults.append('openoutputfiles()') + + if fault is False: + print('Parsing opcode data...') + + if fault is False: + fault = not parseclientopcodedata() + + if fault is True: + faults.append('parseclientopcodedata()') + + if fault is False: + fault = not parseserveropcodedata() + + if fault is True: + faults.append('parseserveropcodedata()') + + if fault is False: + print('Destroying output streams...') + + # these should always be processed..verbose or silent + if not closeoutputfiles(): + faults.append('closeoutputfiles()') + + if not closedebugfile(): + faults.append('closedebugfile()') + + if len(faults) > 0: + message = 'Script failed due to errors in:\n' + + for entry in faults: + message += ' {0}'.format(entry) + + print(message) + + return + + +def createoutputdirectory(): + """ Check for output directory - create if does not exist """ + + try: + output_path = '{0}/utils/scripts/opcode_handlers_output'.format(base_path) + + if DEBUG >= 1: + print(output_path) + + if not os.path.exists(output_path): + os.mkdir(output_path) + + return True + except: + if DEBUG >= 2: + print('EXCEPTION ERROR->createoutputdirectory({0})'.format(sys.exc_info()[0])) + + return False + + +def opendebugfile(): + file_name = '{0}/utils/scripts/opcode_handlers_output/DEBUG.txt'.format(base_path) + + if DEBUG >= 1: + print(file_name) + + out_files['DEBUG'] = open(file_name, 'w') + + return True + + +def loadclientopcodes(): + bad_clients = [] + + for client in client_list: + try: + short_name = '{0}{1}{2}'.format( + '/patch_', + client, + '.conf') + + file_name = '{0}{1}{2}'.format( + base_path, + '/utils/patches', + short_name) + + if DEBUG >= 1: + print(file_name) + + with open(file_name, 'r') as data_file: + client_opcodes[client] = {} # force empty dictionary to avoid collisions + + for data_line in data_file: + key_begin = data_line.find('OP_') + key_end = data_line.find('=', key_begin) + + if not key_begin == 0 or key_end < 0: + continue + + val_begin = data_line.find('0x', key_end) + val_end = val_begin + 6 # max size is always 6 bytes + + if val_begin < 0: + continue + + value = int(data_line[(val_begin + 2):val_end].lower(), 16) + + if value == 0: + continue + + client_opcodes[client][data_line[key_begin:key_end]] = '0x{0}'.format(hex(value)[2:].zfill(4)) + + if DEBUG >= 2: + print('[{0}][{1}] = {2} (int: {3})'.format( + client, + data_line[key_begin:key_end], + client_opcodes[client][data_line[key_begin:key_end]], + value)) + + data_file.close() + except: + if DEBUG >= 2: + print('EXCEPTION ERROR->loadclientopcodes({0})'.format(sys.exc_info()[0])) + + bad_clients.append(client) + + for bad_client in bad_clients: + if DEBUG >= 1: + print('Deleting \'{0}\' client from search criteria...'.format(bad_client)) + + client_list.remove(bad_client) + + if DEBUG >= 1: + print('Deleting stale entries for \'{0}\' client...'.format(bad_client)) + + if bad_client in client_opcodes: + del client_opcodes[bad_client] + + if not len(client_list) > 0: + return False + + return True + + +def loadserveropcodes(): + try: + value = 0 + + server_opcodes['OP_Unknown'] = value + value += 1 + + if DEBUG >= 2: + print('N[Server](OP_Unknown) = {0}'.format(server_opcodes['OP_Unknown'])) + + file_name = '{0}{1}'.format( + base_path, + '/common/emu_oplist.h') + + if DEBUG >= 1: + print(file_name) + + with open(file_name, 'r') as data_file: + for data_line in data_file: + val_begin = data_line.find('OP_', 2) + val_end = data_line.find(')', val_begin) + + if val_begin < 0 or val_end < 0: + continue + + if data_line[:1] == 'N': + server_opcodes[data_line[val_begin:val_end]] = value + value += 1 + + if DEBUG >= 2: + print('N[{0}]({1}) = {2}'.format( + 'Server', + data_line[val_begin:val_end], + server_opcodes[data_line[val_begin:val_end]])) + + data_file.close() + + file_name = '{0}{1}'.format( + base_path, + '/common/mail_oplist.h') + + if DEBUG >= 1: + print(file_name) + + with open(file_name, 'r') as data_file: + for data_line in data_file: + val_begin = data_line.find('OP_', 2) + val_end = data_line.find(')', val_begin) + + if val_begin < 0 or val_end < 0: + continue + + if data_line[:1] == 'N': + server_opcodes[data_line[val_begin:val_end]] = value + value += 1 + + if DEBUG >= 2: + print('N[{0}]({1}) = {2}'.format( + 'Server', + data_line[val_begin:val_end], + server_opcodes[data_line[val_begin:val_end]])) + + data_file.close() + except: + if DEBUG >= 2: + print('EXCEPTION ERROR->loadserveropcodes({0})'.format(sys.exc_info()[0])) + + return False + + return True + + +def loadclienttranslators(): + for client in client_list: + if client == '6.2': + short_name = '{0}'.format('/Client62_ops.h') + else: + short_name = '{0}{1}{2}'.format( + '/', + client, + '_ops.h') + + try: + file_name = '{0}{1}{2}'.format( + base_path, + '/common/patches', + short_name) + + if DEBUG >= 1: + print(file_name) + + with open(file_name, 'r') as data_file: + client_encodes[client] = [] + client_decodes[client] = [] + + for data_line in data_file: + val_begin = data_line.find('OP_', 2) + val_end = data_line.find(')', val_begin) + + if val_begin < 0 or val_end < 0: + continue + + if data_line[:1] == 'E': + client_encodes[client].append(data_line[val_begin:val_end]) + + if DEBUG >= 2: + print('E[{0}]({1}) (listed: {2})'.format( + client, + data_line[val_begin:val_end], + data_line[val_begin:val_end] in client_encodes[client])) + elif data_line[:1] == 'D': + client_decodes[client].append(data_line[val_begin:val_end]) + + if DEBUG >= 2: + print('D[{0}]({1}) (listed: {2})'.format( + client, + data_line[val_begin:val_end], + data_line[val_begin:val_end] in client_decodes[client])) + + data_file.close() + except: + if DEBUG >= 2: + print('EXCEPTION ERROR->loadclienttranslators({0})'.format(sys.exc_info()[0])) + + return False + + # there's always going to be at least one client with one encode or decode + if not len(client_encodes) > 0 and not len(client_decodes) > 0: + return False + + return True + + +def loadserverhandlers(): + """ Load pre-designated SERVER opcode handlers """ + + # TODO: handle remarked out definitions in file (i.e., // and /**/); + bad_servers = [] + + for server in server_list: + try: + if server == 'Login': + if DEBUG >= 1: + print('No pre-designated server opcode handlers for \'{0}\''.format(server)) + + continue + elif server == 'World': + if DEBUG >= 1: + print('No pre-designated server opcode handlers for \'{0}\''.format(server)) + + continue + elif server == 'Zone': + file_name = '{0}{1}'.format( + base_path, + '/zone/client_packet.cpp') + + if DEBUG >= 1: + print(file_name) + + with open(file_name, 'r') as data_file: + server_handlers[server] = {} + can_run = False + line_no = 0 + + for data_line in data_file: + line_no += 1 + + if can_run is False: + if data_line[:19] == 'void MapOpcodes() {': + can_run = True + + continue + + if data_line[0:1] == '}': + break + + key_begin = data_line.find('OP_') + key_end = data_line.find(']', key_begin) + + if key_begin < 0 or key_end < 0: + continue + + val_begin = data_line.find('Client::', key_end) + val_end = data_line.find(';', val_begin) + + if val_begin < 0 or val_end < 0: + continue + + # TODO: add continue on 'in server_opcodes' failure + + if not data_line[key_begin:key_end] in server_handlers[server]: + server_handlers[server][data_line[key_begin:key_end]] = [] + + server_handlers[server][data_line[key_begin:key_end]].append( + '../zone/client_packet.cpp({0}:{1}) \'{2}\''.format( + line_no, + key_begin, + data_line[val_begin:val_end])) + + if DEBUG >= 2: + print('[{0}][{1}]({2}) [{3}]'.format( + server, + data_line[key_begin:key_end], + data_line[val_begin:val_end], + data_line[val_begin:val_end] in server_handlers[server][data_line[key_begin:key_end]])) + + data_file.close() + elif server == 'UCS': + if DEBUG >= 1: + print('No pre-designated server opcode handlers for \'{0}\''.format(server)) + + continue + else: + if DEBUG >= 1: + print('No pre-designated server opcode handlers for \'{0}\''.format(server)) + + if DEBUG >= 2: + print('->LoadServerHandlers(Someone added a new server and forgot to code for the data load...)') + + continue + except: + if DEBUG >= 2: + print('EXCEPTION ERROR->loadserverhandlers({0})'.format(sys.exc_info()[0])) + + bad_servers.append(server) + + for bad_server in bad_servers: + if DEBUG >= 1: + print('Deleting \'{0}\' server from search criteria...'.format(bad_server)) + + server_list.remove(bad_server) + + if DEBUG >= 1: + print('Deleting stale entries for \'{0}\' server...'.format(bad_server)) + + if bad_server in server_handlers: + del server_handlers[bad_server] + + if not len(server_list) > 0: + return False + + return True + + +def discoverserverhandlers(): + """ Load undefined SERVER opcode handlers using 'discovery' method """ + + locations = {} + + for server in server_list: # initialize lists for any remaining servers + locations[server] = [] + + # manually enter search locations + #if 'Server' in locations: + # locations['Server'].append('//.') + + # TODO: if/how to include perl/lua handlers... + + if 'Login' in locations: + locations['Login'].append('/loginserver/Client.cpp') + locations['Login'].append('/loginserver/ServerManager.cpp') + locations['Login'].append('/loginserver/WorldServer.cpp') + + if 'World' in locations: + locations['World'].append('/world/client.cpp') + + # the bulk of opcodes are handled in 'Zone' - if processing occurs on a different + # server, you will need to manually trace 'ServerPacket' to the deferred location + if 'Zone' in locations: + locations['Zone'].append('/zone/AA.cpp') + locations['Zone'].append('/zone/attack.cpp') + locations['Zone'].append('/zone/bot.cpp') + locations['Zone'].append('/zone/client.cpp') + locations['Zone'].append('/zone/client_packet.cpp') + locations['Zone'].append('/zone/client_process.cpp') + locations['Zone'].append('/zone/command.cpp') + locations['Zone'].append('/zone/corpse.cpp') + locations['Zone'].append('/zone/doors.cpp') + locations['Zone'].append('/zone/effects.cpp') + locations['Zone'].append('/zone/entity.cpp') + locations['Zone'].append('/zone/exp.cpp') + locations['Zone'].append('/zone/groups.cpp') + locations['Zone'].append('/zone/guild.cpp') + locations['Zone'].append('/zone/guild_mgr.cpp') + locations['Zone'].append('/zone/horse.cpp') + locations['Zone'].append('/zone/inventory.cpp') + locations['Zone'].append('/zone/loottables.cpp') + locations['Zone'].append('/zone/merc.cpp') + locations['Zone'].append('/zone/mob.cpp') + locations['Zone'].append('/zone/MobAI.cpp') + locations['Zone'].append('/zone/Object.cpp') + locations['Zone'].append('/zone/pathing.cpp') + locations['Zone'].append('/zone/petitions.cpp') + locations['Zone'].append('/zone/questmgr.cpp') + locations['Zone'].append('/zone/raids.cpp') + locations['Zone'].append('/zone/special_attacks.cpp') + locations['Zone'].append('/zone/spells.cpp') + locations['Zone'].append('/zone/spell_effects.cpp') + locations['Zone'].append('/zone/tasks.cpp') + locations['Zone'].append('/zone/titles.cpp') + locations['Zone'].append('/zone/tradeskills.cpp') + locations['Zone'].append('/zone/trading.cpp') + locations['Zone'].append('/zone/trap.cpp') + locations['Zone'].append('/zone/tribute.cpp') + locations['Zone'].append('/zone/worldserver.cpp') + locations['Zone'].append('/zone/zone.cpp') + locations['Zone'].append('/zone/zonedb.cpp') + locations['Zone'].append('/zone/zoning.cpp') + + if 'UCS' in locations: + locations['UCS'].append('/ucs/clientlist.cpp') + locations['UCS'].append('/ucs/database.cpp') + + for server in server_list: + if not server in server_handlers: + server_handlers[server] = {} + + for location in locations[server]: + try: + file_name = '{0}{1}'.format( + base_path, + location) + + if DEBUG >= 1: + print(file_name) + + with open(file_name, 'r') as data_file: + line_no = 0 + hint = 'Near beginning of file' + + for data_line in data_file: + line_no += 1 + + if data_line[:1].isalpha(): + hint_end = data_line.find('(') + + if not hint_end < 0: + hint_begin = hint_end - 1 + + while not hint_begin < 0: + if data_line[(hint_begin - 1):hint_begin].isspace(): + if not data_line[hint_begin:(hint_begin + 1)].isalpha(): + hint_begin += 1 + + hint = 'Near {0}'.format(data_line[hint_begin:hint_end]) + + break + + hint_begin -= 1 + + op_begin = data_line.find('OP_') + + if op_begin < 0: + continue + + if data_line[(op_begin - 20):op_begin] == 'EQApplicationPacket(': + key_begin = op_begin + key_end = data_line.find(',', key_begin) + elif data_line[(op_begin - 12):op_begin] == '->SetOpcode(': + key_begin = op_begin + key_end = data_line.find(')', key_begin) + elif data_line[(op_begin - 5):op_begin] == 'case ': + key_begin = op_begin + key_end = data_line.find(':', key_begin) + else: + continue + + if key_end < 0: + continue + + if not data_line[key_begin:key_end] in server_opcodes: + out_files['DEBUG'].write('Illegal Opcode Found: ..{0} ({1}:{2}) \'{3}\'\n'.format( + location, + line_no, + key_begin, + data_line[key_begin:key_end] + )) + + continue + + if not data_line[key_begin:key_end] in server_handlers[server]: + server_handlers[server][data_line[key_begin:key_end]] = [] + + if not data_line in server_handlers[server][data_line[key_begin:key_end]]: + server_handlers[server][data_line[key_begin:key_end]].append( + '..{0}({1}:{2}) \'{3}\''.format( + location, + line_no, + key_begin, + hint)) + except: + if DEBUG >= 2: + print('EXCEPTION ERROR->discoverserverhandlers({0})'.format(sys.exc_info()[0])) + + return True + + +def clearemptyserverentries(): + bad_servers = [] + + for server in server_list: + if len(server_handlers[server]) == 0: + bad_servers.append(server) + else: + bad_opcodes = [] + + for opcode in server_handlers[server]: + if len(server_handlers[server][opcode]) == 0: + bad_opcodes.append(opcodes) + + for bad_opcode in bad_opcodes: + del server_handlers[server][bad_opcode] + + if len(server_handlers[server]) == 0: + bad_servers.append(server) + + for bad_server in bad_servers: + if DEBUG >= 1: + print('Deleting \'{0}\' server from search criteria...'.format(bad_server)) + print('Deleting stale entries for \'{0}\' server...'.format(bad_server)) + + del server_handlers[bad_server] + server_list.remove(bad_server) + + return True + + +def openoutputfiles(): + """ Open output files in 'w' mode - create/overwrite mode """ + + try: + file_name = '{0}/utils/scripts/opcode_handlers_output/Report.txt'.format(base_path) + + if DEBUG >= 1: + print(file_name) + + out_files['Report'] = open(file_name, 'w') + + for client in client_list: + file_name = '{0}{1}{2}{3}'.format( + base_path, + '/utils/scripts/opcode_handlers_output/', + client, + '_opcode_handlers.txt') + + if DEBUG >= 1: + print(file_name) + + out_files[client] = open(file_name, 'w') + + message = \ + '>> \'Opcode-Handler\' analysis for \'{0}\' client\n' \ + '>> file generated @ {1}\n' \ + '\n'.format( + client, + ctime(time())) + + out_files[client].write(message) + + if DEBUG >= 2: + print(message[:-2]) + + for server in server_list: + file_name = '{0}{1}{2}{3}'.format( + base_path, + '/utils/scripts/opcode_handlers_output/', + server, + '_opcode_handlers.txt') + + if DEBUG >= 1: + print(file_name) + + out_files[server] = open(file_name, 'w') + + message = \ + '>> \'Opcode-Handler\' analysis for \'{0}\' server\n' \ + '>> file generated @ {1}\n' \ + '\n'.format( + server, + ctime(time())) + + out_files[server].write(message) + + if DEBUG >= 2: + print(message[:-2]) + except: + if DEBUG >= 2: + print('EXCEPTION ERROR->openoutputfiles({0})'.format(sys.exc_info()[0])) + + for client in client_list: + if client in out_files: + out_files[client].close() + del out_files[client] + + if DEBUG >= 2: + print('->OpeningClientStream(exception): {0}'.format(client)) + + for server in server_list: + if server in out_files: + out_files[server].close() + del out_files[server] + + if DEBUG >= 2: + print('->OpeningServerStream(exception): {0}'.format(server)) + + if 'Report' in out_files: + out_files['Report'].close() + del out_files['Report'] + + if DEBUG >= 2: + print('->OpeningReportStream(exception)') + + return False + + return True + + +def parseclientopcodedata(): + # TODO: add metrics + for client in client_list: + server_max_len = 0 + + for server in server_list: + if len(server) > server_max_len: + server_max_len = len(server) + + client_keys = client_opcodes[client].keys() + client_keys.sort() + + for client_opcode in client_keys: + handled = client_opcode in server_opcodes + + if handled is True: + encoded = client_opcode in client_encodes[client] + decoded = client_opcode in client_decodes[client] + else: + encoded = 'N/A' + decoded = 'N/A' + + message = 'Opcode: {0} ({1}) | Handled: {2} | Encoded: {3} | Decoded: {4}\n'.format( + client_opcode, + client_opcodes[client][client_opcode], + handled, + encoded, + decoded) + + for server in server_list: + if client_opcode in server_handlers[server] and len(server_handlers[server][client_opcode]) > 0: + handler_list = server_handlers[server][client_opcode] + handler_list.sort() + + for handler_entry in handler_list: + message += ' Server: {0} ({1}) | Handler: {2}\n'.format( + server.ljust(len(server) + (server_max_len - len(server)), ' '), + '{0}'.format(server_opcodes[client_opcode]).zfill(4), + handler_entry) + else: + message += ' Server: {0} (0000) | Handler: N/A\n'.format( + server.ljust(len(server) + (server_max_len - len(server)), ' ')) + + if DEBUG >= 2: + print('->EndOfServerLoop: {0}'.format(server)) + + message += '\n' + + out_files[client].write(message) + + if DEBUG >= 2: + print(message[:-2]) + print('->EndOfOpcodeLoop: {0}'.format(client_opcode)) + + if DEBUG >= 2: + print('->EndOfClientLoop: {0}'.format(client)) + + return True + + +def parseserveropcodedata(): + # TODO: add metrics + for server in server_list: + client_max_len = 0 + + for client in client_list: + if len(client) > client_max_len: + client_max_len = len(client) + + handler_keys = server_handlers[server].keys() + handler_keys.sort() + + for handler_opcode in handler_keys: + handler_list = server_handlers[server][handler_opcode] + handler_list.sort() + + message = '' + + for handler_entry in handler_list: + message += 'Opcode: {0} ({1}) | Handler: {2}\n'.format( + handler_opcode, + server_opcodes[handler_opcode], + handler_entry) + + for client in client_list: + if handler_opcode in client_opcodes[client]: + val1 = client_opcodes[client][handler_opcode] + val2 = 'True' + val3 = '{0}'.format(handler_opcode in client_encodes[client]) + val4 = '{0}'.format(handler_opcode in client_decodes[client]) + else: + val1 = '0x0000' + val2 = 'False' + val3 = 'N/A' + val4 = 'N/A' + + message += ' Client: {0} ({1}) | Handled: {2} | Encoded: {3} | Decoded: {4}\n'.format( + client.ljust(len(client) + (client_max_len - len(client)), ' '), + val1, + val2.ljust(len(val2) + (len('False') - len(val2)), ' '), + val3.ljust(len(val3) + (len('False') - len(val3)), ' '), + val4.ljust(len(val4) + (len('False') - len(val4)), ' ')) + + if DEBUG >= 2: + print('->EndOfClientLoop: {0}'.format(client)) + + message += '\n' + + out_files[server].write(message) + + if DEBUG >= 2: + print(message[:-2]) + print('->EndOfOpcodeLoop: {0}'.format(handler_opcode)) + + if DEBUG >= 2: + print('->EndOfServerLoop: {0}'.format(server)) + + return True + + +def closeoutputfiles(): + for client in client_list: + if client in out_files: + out_files[client].close() + del out_files[client] + + if DEBUG >= 2: + print('->ClosingClientStream: {0}'.format(client)) + + for server in server_list: + if server in out_files: + out_files[server].close() + del out_files[server] + + if DEBUG >= 2: + print('->ClosingServerStream: {0}'.format(server)) + + if 'Report' in out_files: + out_files['Report'].close() + del out_files['Report'] + + if DEBUG >= 2: + print('->ClosingReportStream') + + return True + + +def closedebugfile(): + if 'DEBUG' in out_files: + out_files['DEBUG'].close() + del out_files['DEBUG'] + + if DEBUG >= 2: + print('->ClosingDEBUGStream') + + return True + + +if __name__ == '__main__': + main() diff --git a/utils/sql/git/required/2014_08_02_spells_new.sql b/utils/sql/git/required/2014_08_02_spells_new.sql new file mode 100644 index 000000000..f4e74158a --- /dev/null +++ b/utils/sql/git/required/2014_08_02_spells_new.sql @@ -0,0 +1,18 @@ +-- spells new talbe update +ALTER TABLE `spells_new` CHANGE `NotOutofCombat` `InCombat` INT(11) NOT NULL DEFAULT '0'; +ALTER TABLE `spells_new` CHANGE `NotInCombat` `OutofCombat` INT(11) NOT NULL DEFAULT '0'; +ALTER TABLE `spells_new` CHANGE `field201` `viral_range` INT(11) NOT NULL DEFAULT '0'; +ALTER TABLE `spells_new` CHANGE `field218` `aemaxtargets` INT(11) NOT NULL DEFAULT '0'; +ALTER TABLE `spells_new` ADD `field225` int( 11 ) NOT NULL DEFAULT '0' AFTER `persistdeath`; +ALTER TABLE `spells_new` ADD `field226` int( 11 ) NOT NULL DEFAULT '0' AFTER `field225`; +ALTER TABLE `spells_new` ADD `min_dist` float( 0 ) NOT NULL DEFAULT '0' AFTER `field226`; +ALTER TABLE `spells_new` ADD `min_dist_mod` float( 0 ) NOT NULL DEFAULT '0' AFTER `min_dist`; +ALTER TABLE `spells_new` ADD `max_dist` float( 0 ) NOT NULL DEFAULT '0' AFTER `min_dist_mod`; +ALTER TABLE `spells_new` ADD `max_dist_mod` float( 0 ) NOT NULL DEFAULT '0' AFTER `max_dist`; +ALTER TABLE `spells_new` ADD `min_range` int( 11 ) NOT NULL DEFAULT '0' AFTER `max_dist_mod`; +ALTER TABLE `spells_new` ADD `field232` int( 11 ) NOT NULL DEFAULT '0' AFTER `min_range`; +ALTER TABLE `spells_new` ADD `field233` int( 11 ) NOT NULL DEFAULT '0' AFTER `field232`; +ALTER TABLE `spells_new` ADD `field234` int( 11 ) NOT NULL DEFAULT '0' AFTER `field233`; +ALTER TABLE `spells_new` ADD `field235` int( 11 ) NOT NULL DEFAULT '0' AFTER `field234`; +ALTER TABLE `spells_new` ADD `field236` int( 11 ) NOT NULL DEFAULT '0' AFTER `field235`; + diff --git a/zone/StringIDs.h b/zone/StringIDs.h index 9a222d276..b1e13edca 100644 --- a/zone/StringIDs.h +++ b/zone/StringIDs.h @@ -249,6 +249,7 @@ #define CORPSEDRAG_BEGIN 4064 //You begin to drag %1. #define CORPSEDRAG_STOPALL 4065 //You stop dragging the corpses. #define CORPSEDRAG_STOP 4066 //You stop dragging the corpse. +#define TARGET_TOO_CLOSE 4602 //You are too close to your target. Get farther away. #define WHOALL_NO_RESULTS 5029 //There are no players in EverQuest that match those who filters. #define PETITION_NO_DELETE 5053 //You do not have a petition in the queue. #define PETITION_DELETED 5054 //Your petition was successfully deleted. @@ -316,6 +317,10 @@ #define PET_NOT_FOCUSING 9263 //No longer focusing on one target, Master. #define PET_NOT_CASTING 9264 //Not casting spells, Master. #define PET_CASTING 9291 //Casting spells normally, Master. +#define NO_CAST_IN_COMBAT 9190 //You can not cast this spell while in combat. +#define NO_CAST_OUT_OF_COMBAT 9191 //You can not cast this spell while out of combat. +#define NO_ABILITY_IN_COMBAT 9192 //You can not use this ability while in combat. +#define NO_ABILITY_OUT_OF_COMBAT 9194 //You can not use this ability while out of combat. #define AE_RAMPAGE 11015 //%1 goes on a WILD RAMPAGE! #define FACE_ACCEPTED 12028 //Facial features accepted. #define SPELL_LEVEL_TO_LOW 12048 //You will have to achieve level %1 before you can scribe the %2. diff --git a/zone/attack.cpp b/zone/attack.cpp index 2403293e6..3195b4234 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -311,7 +311,7 @@ bool Mob::CheckHitChance(Mob* other, SkillUseTypes skillinuse, int Hand, int16 c hitBonus += (attacker->CastToNPC()->GetAccuracyRating() / 10.0f); //Modifier from database if(skillinuse == SkillArchery) - hitBonus -= hitBonus*(RuleR(Combat, ArcheryHitPenalty)*100.0f); + hitBonus -= hitBonus*RuleR(Combat, ArcheryHitPenalty); //Calculate final chance to hit chancetohit += ((chancetohit * (hitBonus - avoidanceBonus)) / 100.0f); diff --git a/zone/effects.cpp b/zone/effects.cpp index c298974db..1b1eeb2ea 100644 --- a/zone/effects.cpp +++ b/zone/effects.cpp @@ -740,6 +740,8 @@ void EntityList::AESpell(Mob *caster, Mob *center, uint16 spell_id, bool affect_ float dist = caster->GetAOERange(spell_id); float dist2 = dist * dist; + float min_range2 = spells[spell_id].min_range * spells[spell_id].min_range; + float dist_targ = 0; bool bad = IsDetrimentalSpell(spell_id); bool isnpc = caster->IsNPC(); @@ -755,7 +757,11 @@ void EntityList::AESpell(Mob *caster, Mob *center, uint16 spell_id, bool affect_ continue; if (curmob == caster && !affect_caster) //watch for caster too continue; - if (center->DistNoRoot(*curmob) > dist2) //make sure they are in range + + dist_targ = center->DistNoRoot(*curmob); + if (dist_targ > dist2) //make sure they are in range + continue; + if (dist_targ < min_range2) //make sure they are in range continue; if (isnpc && curmob->IsNPC()) { //check npc->npc casting FACTION_VALUE f = curmob->GetReverseFactionCon(caster); @@ -786,6 +792,8 @@ void EntityList::AESpell(Mob *caster, Mob *center, uint16 spell_id, bool affect_ continue; } + curmob->CalcSpellPowerDistanceMod(spell_id, dist_targ); + //if we get here... cast the spell. if (IsTargetableAESpell(spell_id) && bad) { if (iCounter < MAX_TARGETS_ALLOWED) { diff --git a/zone/entity.cpp b/zone/entity.cpp index 9700f1c92..f988d882c 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -4552,7 +4552,7 @@ Mob *EntityList::GetClosestMobByBodyType(Mob *sender, bodyType BodyType) return ClosestMob; } -void EntityList::GetTargetsForConeArea(Mob *start, uint32 radius, uint32 height, std::list &m_list) +void EntityList::GetTargetsForConeArea(Mob *start, float min_radius, float radius, float height, std::list &m_list) { auto it = mob_list.begin(); while (it != mob_list.end()) { @@ -4561,15 +4561,15 @@ void EntityList::GetTargetsForConeArea(Mob *start, uint32 radius, uint32 height, ++it; continue; } - int32 x_diff = ptr->GetX() - start->GetX(); - int32 y_diff = ptr->GetY() - start->GetY(); - int32 z_diff = ptr->GetZ() - start->GetZ(); + float x_diff = ptr->GetX() - start->GetX(); + float y_diff = ptr->GetY() - start->GetY(); + float z_diff = ptr->GetZ() - start->GetZ(); x_diff *= x_diff; y_diff *= y_diff; z_diff *= z_diff; - if ((x_diff + y_diff) <= (radius * radius)) + if ((x_diff + y_diff) <= (radius * radius) && (x_diff + y_diff) >= (min_radius * min_radius)) if(z_diff <= (height * height)) m_list.push_back(ptr); diff --git a/zone/entity.h b/zone/entity.h index 8b94b0e0a..c1ba0936c 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -406,7 +406,7 @@ public: void GetObjectList(std::list &o_list); void GetDoorsList(std::list &d_list); void GetSpawnList(std::list &d_list); - void GetTargetsForConeArea(Mob *start, uint32 radius, uint32 height, std::list &m_list); + void GetTargetsForConeArea(Mob *start, float min_radius, float radius, float height, std::list &m_list); void DepopAll(int NPCTypeID, bool StartSpawnTimer = true); diff --git a/zone/groups.cpp b/zone/groups.cpp index 6aee5f38b..d948eb476 100644 --- a/zone/groups.cpp +++ b/zone/groups.cpp @@ -658,6 +658,7 @@ void Group::CastGroupSpell(Mob* caster, uint16 spell_id) { range = caster->GetAOERange(spell_id); float range2 = range*range; + float min_range2 = spells[spell_id].min_range * spells[spell_id].min_range; // caster->SpellOnTarget(spell_id, caster); @@ -673,7 +674,8 @@ void Group::CastGroupSpell(Mob* caster, uint16 spell_id) { else if(members[z] != nullptr) { distance = caster->DistNoRoot(*members[z]); - if(distance <= range2) { + if(distance <= range2 && distance >= min_range2) { + members[z]->CalcSpellPowerDistanceMod(spell_id, distance); caster->SpellOnTarget(spell_id, members[z]); #ifdef GROUP_BUFF_PETS if(members[z]->GetPet() && members[z]->HasPetAffinity() && !members[z]->GetPet()->IsCharmed()) diff --git a/zone/hate_list.cpp b/zone/hate_list.cpp index bb39d224a..1b585ca32 100644 --- a/zone/hate_list.cpp +++ b/zone/hate_list.cpp @@ -567,20 +567,25 @@ void HateList::SpellCast(Mob *caster, uint32 spell_id, float range) //So keep a list of entity ids and look up after std::list id_list; range = range * range; + float min_range2 = spells[spell_id].min_range * spells[spell_id].min_range; + float dist_targ = 0; auto iterator = list.begin(); while (iterator != list.end()) { tHateEntry *h = (*iterator); if(range > 0) { - if(caster->DistNoRoot(*h->ent) <= range) + dist_targ = caster->DistNoRoot(*h->ent); + if(dist_targ <= range && dist_targ >= min_range2) { id_list.push_back(h->ent->GetID()); + h->ent->CalcSpellPowerDistanceMod(spell_id, dist_targ); } } else { id_list.push_back(h->ent->GetID()); + h->ent->CalcSpellPowerDistanceMod(spell_id, 0, caster); } ++iterator; } diff --git a/zone/mob.cpp b/zone/mob.cpp index 1ed63bcaa..97befb7fa 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -184,6 +184,7 @@ Mob::Mob(const char* in_name, has_numhits = false; has_MGB = false; has_ProjectIllusion = false; + SpellPowerDistanceMod = 0; if(in_aa_title>0) aa_title = in_aa_title; diff --git a/zone/mob.h b/zone/mob.h index 1793e0864..15f1728ec 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -615,6 +615,9 @@ public: bool ImprovedTaunt(); bool TryRootFadeByDamage(int buffslot, Mob* attacker); int16 GetSlowMitigation() const {return slow_mitigation;} + void CalcSpellPowerDistanceMod(uint16 spell_id, float range, Mob* caster = nullptr); + inline int16 GetSpellPowerDistanceMod() const { return SpellPowerDistanceMod; }; + inline void SetSpellPowerDistanceMod(int16 value) { SpellPowerDistanceMod = value; }; void ModSkillDmgTaken(SkillUseTypes skill_num, int value); int16 GetModSkillDmgTaken(const SkillUseTypes skill_num); @@ -1114,6 +1117,7 @@ protected: bool has_numhits; bool has_MGB; bool has_ProjectIllusion; + int16 SpellPowerDistanceMod; // Bind wound Timer bindwound_timer; diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index 0ee88a154..8a45cf391 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -186,6 +186,9 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) buffs[buffslot].numhits = numhit; } + if (!IsPowerDistModSpell(spell_id)) + SetSpellPowerDistanceMod(0); + // iterate through the effects in the spell for (i = 0; i < EFFECT_COUNT; i++) { @@ -198,6 +201,9 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) if(spell_id == SPELL_LAY_ON_HANDS && caster && caster->GetAA(aaImprovedLayOnHands)) effect_value = GetMaxHP(); + if (GetSpellPowerDistanceMod()) + effect_value = effect_value*(GetSpellPowerDistanceMod()/100); + #ifdef SPELL_EFFECT_SPAM effect_desc[0] = 0; #endif @@ -2705,7 +2711,7 @@ bool Mob::SpellEffect(Mob* caster, uint16 spell_id, float partial) if (buffslot >= 0) break; - if(IsCasting() && MakeRandomInt(0, 100) <= spells[spell_id].base[i]) + if(!spells[spell_id].uninterruptable && IsCasting() && MakeRandomInt(0, 100) <= spells[spell_id].base[i]) InterruptSpell(); break; @@ -6444,4 +6450,24 @@ bool Mob::CheckSpellCategory(uint16 spell_id, int category_id, int effect_id){ return false; } - \ No newline at end of file + +void Mob::CalcSpellPowerDistanceMod(uint16 spell_id, float range, Mob* caster) +{ + if (IsPowerDistModSpell(spell_id)){ + + float distance = 0; + + if (caster && !range) + distance = caster->CalculateDistance(GetX(), GetY(), GetZ()); + else + distance = sqrt(range); + + float dm_range = spells[spell_id].max_dist - spells[spell_id].min_dist; + float dm_mod_interval = spells[spell_id].max_dist_mod - spells[spell_id].min_dist_mod; + float dist_from_min = distance - spells[spell_id].min_dist; + float mod = spells[spell_id].min_dist_mod + (dist_from_min * (dm_mod_interval/dm_range)); + mod *= 100.0f; + + SetSpellPowerDistanceMod(static_cast(mod)); + } +} \ No newline at end of file diff --git a/zone/spells.cpp b/zone/spells.cpp index 305156b63..5d891629e 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -1034,7 +1034,7 @@ void Mob::CastedSpellFinished(uint16 spell_id, uint32 target_id, uint16 slot, mlog(SPELLS__CASTING, "Checking Interruption: spell x: %f spell y: %f cur x: %f cur y: %f channelchance %f channeling skill %d\n", GetSpellX(), GetSpellY(), GetX(), GetY(), channelchance, GetSkill(SkillChanneling)); - if(MakeRandomFloat(0, 100) > channelchance) { + if(!spells[spell_id].uninterruptable && MakeRandomFloat(0, 100) > channelchance) { mlog(SPELLS__CASTING_ERR, "Casting of %d canceled: interrupted.", spell_id); InterruptSpell(); return; @@ -1384,6 +1384,52 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce return false; } + //Must be out of combat. (If Beneficial checks casters combat state, Deterimental checks targets) + if (!spells[spell_id].InCombat && spells[spell_id].OutofCombat){ + if (IsDetrimentalSpell(spell_id)) { + if ( (spell_target->IsNPC() && spell_target->IsEngaged()) || + (spell_target->IsClient() && spell_target->CastToClient()->GetAggroCount())){ + Message_StringID(13,SPELL_NO_EFFECT); //Unsure correct string + return false; + } + } + + else if (IsBeneficialSpell(spell_id)) { + if ( (IsNPC() && IsEngaged()) || + (IsClient() && CastToClient()->GetAggroCount())){ + if (IsDiscipline(spell_id)) + Message_StringID(13,NO_ABILITY_IN_COMBAT); + else + Message_StringID(13,NO_CAST_IN_COMBAT); + + return false; + } + } + } + + //Must be in combat. (If Beneficial checks casters combat state, Deterimental checks targets) + else if (spells[spell_id].InCombat && !spells[spell_id].OutofCombat){ + if (IsDetrimentalSpell(spell_id)) { + if ( (spell_target->IsNPC() && !spell_target->IsEngaged()) || + (spell_target->IsClient() && !spell_target->CastToClient()->GetAggroCount())){ + Message_StringID(13,SPELL_NO_EFFECT); //Unsure correct string + return false; + } + } + + else if (IsBeneficialSpell(spell_id)) { + if ( (IsNPC() && !IsEngaged()) || + (IsClient() && !CastToClient()->GetAggroCount())){ + if (IsDiscipline(spell_id)) + Message_StringID(13,NO_ABILITY_OUT_OF_COMBAT); + else + Message_StringID(13,NO_CAST_OUT_OF_COMBAT); + + return false; + } + } + } + switch (targetType) { // single target spells @@ -1734,6 +1780,24 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce break; } + case ST_PetMaster: + { + + Mob *owner = nullptr; + + if (IsPet()) + owner = GetOwner(); + else if ((IsNPC() && CastToNPC()->GetSwarmOwner())) + owner = entity_list.GetMobID(CastToNPC()->GetSwarmOwner()); + + if (!owner) + return false; + + spell_target = owner; + CastAction = SingleTarget; + break; + } + default: { mlog(SPELLS__CASTING_ERR, "I dont know Target Type: %d Spell: (%d) %s", spells[spell_id].targettype, spell_id, spells[spell_id].name); @@ -1863,12 +1927,21 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, uint16 slot, uint16 //casting a spell on somebody but ourself, make sure they are in range float dist2 = DistNoRoot(*spell_target); float range2 = range * range; + float min_range2 = spells[spell_id].min_range * spells[spell_id].min_range; if(dist2 > range2) { //target is out of range. mlog(SPELLS__CASTING, "Spell %d: Spell target is out of range (squared: %f > %f)", spell_id, dist2, range2); Message_StringID(13, TARGET_OUT_OF_RANGE); return(false); } + else if (dist2 < min_range2){ + //target is too close range. + mlog(SPELLS__CASTING, "Spell %d: Spell target is too close (squared: %f < %f)", spell_id, dist2, min_range2); + Message_StringID(13, TARGET_TOO_CLOSE); + return(false); + } + + spell_target->CalcSpellPowerDistanceMod(spell_id, dist2); } // @@ -2052,7 +2125,7 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, uint16 slot, uint16 std::list targets_in_range; std::list::iterator iter; - entity_list.GetTargetsForConeArea(this, spells[spell_id].aoerange, spells[spell_id].aoerange / 2, targets_in_range); + entity_list.GetTargetsForConeArea(this, spells[spell_id].min_range, spells[spell_id].aoerange, spells[spell_id].aoerange / 2, targets_in_range); iter = targets_in_range.begin(); while(iter != targets_in_range.end()) { @@ -2068,16 +2141,20 @@ bool Mob::SpellFinished(uint16 spell_id, Mob *spell_target, uint16 slot, uint16 if((heading_to_target >= angle_start && heading_to_target <= 360.0f) || (heading_to_target >= 0.0f && heading_to_target <= angle_end)) { - if(CheckLosFN(spell_target)) + if(CheckLosFN((*iter)) || spells[spell_id].npc_no_los){ + (*iter)->CalcSpellPowerDistanceMod(spell_id, 0, this); SpellOnTarget(spell_id, spell_target, false, true, resist_adjust); + } } } else { if(heading_to_target >= angle_start && heading_to_target <= angle_end) { - if(CheckLosFN((*iter))) + if(CheckLosFN((*iter)) || spells[spell_id].npc_no_los){ + (*iter)->CalcSpellPowerDistanceMod(spell_id, 0, this); SpellOnTarget(spell_id, (*iter), false, true, resist_adjust); + } } } ++iter; @@ -4625,12 +4702,10 @@ void Mob::Stun(int duration) if(stunned && stunned_timer.GetRemainingTime() > uint32(duration)) return; - if(casting_spell_id) { - int persistent_casting = spellbonuses.PersistantCasting + itembonuses.PersistantCasting; - if(IsClient()) - persistent_casting += aabonuses.PersistantCasting; + if(IsValidSpell(casting_spell_id) && !spells[casting_spell_id].uninterruptable) { + int persistent_casting = spellbonuses.PersistantCasting + itembonuses.PersistantCasting + aabonuses.PersistantCasting; - if(MakeRandomInt(1,99) > persistent_casting) + if(MakeRandomInt(0,99) > persistent_casting) InterruptSpell(); }