diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fba3ed2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +# General configuration +[*] +charset = utf-8 +end_of_line = lf +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{cpp,h,lua,xml}] +indent_style = tab +indent_size = 4 + +[**.{.cpp,.h}] +# Options not ubiquitous, but useful +indent_brace_style = K&R +spaces_around_brackets = none + +[.travis.yml] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..914b148 --- /dev/null +++ b/.gitignore @@ -0,0 +1,182 @@ +################# +## Visual Studio +################# + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates +.idea/ +.vscode/ + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +build/ +[Bb]in/ +[Oo]bj/ +cmake-build-*/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.scc +*.dll +*.exe + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.opendb +*.sdf +*.cachefile +*.VC.db + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml + +# NuGet Packages Directory +## TODO: If you have NuGet Package Restore enabled, uncomment the next line +#packages/ + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml +*.pfx +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +############# +## Windows detritus +############# + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac crap +.DS_Store + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg + +# Vim files +*~ +.*.swp +.*.swo + +############# +## TFS / OT +############# +config.lua diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1673c77 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,41 @@ +sudo: required +dist: bionic +language: cpp +compiler: + - clang + - gcc +env: + global: + # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created + # via the "travis encrypt" command using the project repo's public key + - secure: "jg/5nV1Xe6ePlplxIyRmyTaSEDmKhOxF/Z3cMMU6xOnEyhMTnKgcvi88YB0JA/Y7y3SBFyHdvzSfADgTPYsE+TDp2BTlhPpaS/x987nXfB+0jWB3WyEdxtUuscis9m3vAfdO9OraOfrthQDKQgt1mEoJcdVfLabnZ2hs+wzuFGA=" + matrix: + - LUAJIT=OFF + - LUAJIT=ON +cache: apt +addons: + apt: + packages: + - libboost-date-time-dev + - libboost-system-dev + - libboost-filesystem-dev + - libboost-iostreams-dev + - libcrypto++-dev + - liblua5.2-dev + - libluajit-5.1-dev + - libmysqlclient-dev + - libpugixml-dev + coverity_scan: + project: + name: otland/forgottenserver + description: A free and open-source MMORPG server emulator written in C++ + build_command_prepend: "cmake -DCMAKE_BUILD_TYPE=Release -DUSE_LUAJIT=${LUAJIT} .." + build_command: make -j2 + branch_pattern: coverity_scan + notification_email: coverity@otland.net +before_install: + - echo -n | openssl s_client -connect https://scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee -a /etc/ssl/certs/ca- +before_script: + - mkdir build && cd build + - cmake -DCMAKE_BUILD_TYPE=Release -DUSE_LUAJIT=${LUAJIT} .. +script: if [ "${COVERITY_SCAN_BRANCH}" != 1 ]; then make -j2; fi diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a90540b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,100 @@ +cmake_minimum_required(VERSION 3.10) + +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) + +project(tfs) + +add_subdirectory(src) +add_executable(tfs ${tfs_SRC}) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +set_target_properties(tfs PROPERTIES CXX_STANDARD 11) +set_target_properties(tfs PROPERTIES CXX_STANDARD_REQUIRED ON) + +if (${CMAKE_VERSION} VERSION_GREATER "3.16.0") + target_precompile_headers(tfs PUBLIC src/otpch.h) +else () + include(cotire) + set_target_properties(tfs PROPERTIES COTIRE_CXX_PREFIX_HEADER_INIT "src/otpch.h") + set_target_properties(tfs PROPERTIES COTIRE_ADD_UNITY_BUILD FALSE) + cotire(tfs) +endif () + +if (NOT WIN32) + add_compile_options(-Wall -Werror -pipe -fvisibility=hidden) +endif () + +set(CMAKE_CXX_FLAGS_PERFORMANCE "${CMAKE_CXX_FLAGS_RELEASE} -march=native") + +if (CMAKE_COMPILER_IS_GNUCXX) + add_compile_options(-fno-strict-aliasing) +endif () + +# Find packages. +find_package(CryptoPP QUIET) +if (CryptoPP_FOUND) # vcpkg-provided cmake package called CryptoPP + set(Crypto++_LIBRARIES "cryptopp-static") +else() + find_package(Crypto++ REQUIRED) +endif () +find_package(MySQL REQUIRED) +find_package(Threads REQUIRED) +find_package(PugiXML REQUIRED) + +# Selects LuaJIT if user defines or auto-detected +if (DEFINED USE_LUAJIT AND NOT USE_LUAJIT) + set(FORCE_LUAJIT ${USE_LUAJIT}) +else () + find_package(LuaJIT) + set(FORCE_LUAJIT ${LuaJIT_FOUND}) +endif () +option(USE_LUAJIT "Use LuaJIT" ${FORCE_LUAJIT}) + +if (FORCE_LUAJIT) + if (APPLE) + set(CMAKE_EXE_LINKER_FLAGS "-pagezero_size 10000 -image_base 100000000") + endif () +else () + find_package(Lua REQUIRED) +endif () + +find_package(Boost 1.53.0 REQUIRED COMPONENTS date_time system filesystem iostreams) + +include_directories(${Boost_INCLUDE_DIRS} ${Crypto++_INCLUDE_DIR} ${LUA_INCLUDE_DIR} ${MYSQL_INCLUDE_DIR} ${PUGIXML_INCLUDE_DIR}) +target_link_libraries(tfs PRIVATE + Boost::date_time + Boost::system + Boost::filesystem + Boost::iostreams + ${CMAKE_THREAD_LIBS_INIT} + ${Crypto++_LIBRARIES} + ${LUA_LIBRARIES} + ${MYSQL_CLIENT_LIBS} + ${PUGIXML_LIBRARIES} + ) + +### INTERPROCEDURAL_OPTIMIZATION ### +cmake_policy(SET CMP0069 NEW) +include(CheckIPOSupported) +check_ipo_supported(RESULT result OUTPUT error) +if (result) + message(STATUS "IPO / LTO enabled") + set_target_properties(tfs PROPERTIES INTERPROCEDURAL_OPTIMIZATION True) +else () + message(STATUS "IPO / LTO not supported: <${error}>") +endif () +### END INTERPROCEDURAL_OPTIMIZATION ### + +### Git Version ### +# Define the two required variables before including +# the source code for watching a git repository. +set(PRE_CONFIGURE_FILE "cmake/gitmetadata.h.in") +set(POST_CONFIGURE_FILE "${CMAKE_CURRENT_BINARY_DIR}/gitmetadata.h") +include(git_watcher) +if (Git_FOUND) + add_dependencies(tfs check_git) + target_include_directories(tfs PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +endif() +### END Git Version ### diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..77edd39 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +FROM alpine:edge AS build +# crypto++-dev is in edge/testing +RUN apk add --no-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ \ + binutils \ + boost-dev \ + build-base \ + clang \ + cmake \ + crypto++-dev \ + gcc \ + gmp-dev \ + luajit-dev \ + make \ + mariadb-connector-c-dev \ + pugixml-dev + +COPY cmake /usr/src/forgottenserver/cmake/ +COPY src /usr/src/forgottenserver/src/ +COPY CMakeLists.txt /usr/src/forgottenserver/ +WORKDIR /usr/src/forgottenserver/build +RUN cmake .. && make + +FROM alpine:edge +# crypto++-dev is in edge/testing +RUN apk add --no-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ \ + boost-iostreams \ + boost-system \ + boost-filesystem \ + crypto++ \ + gmp \ + luajit \ + mariadb-connector-c \ + pugixml + +RUN ln -s /usr/lib/libcryptopp.so /usr/lib/libcryptopp.so.5.6 +COPY --from=build /usr/src/forgottenserver/build/tfs /bin/tfs +COPY data /srv/data/ +COPY LICENSE README.md *.dist *.sql key.pem /srv/ + +EXPOSE 7171 7172 +WORKDIR /srv +VOLUME /srv +ENTRYPOINT ["/bin/tfs"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..996fd24 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +forgottenserver [![Build Status](https://travis-ci.org/otland/forgottenserver.svg?branch=master)](https://travis-ci.org/otland/forgottenserver "Travis CI status") [![Build status](https://ci.appveyor.com/api/projects/status/599x38f3a0luessl?svg=true)](https://ci.appveyor.com/project/otland/forgottenserver "Download builds for Windows") [![Docker status](https://images.microbadger.com/badges/image/otland/forgottenserver.svg)](https://microbadger.com/images/otland/forgottenserver "Docker image status") +=============== + +The Forgotten Server is a free and open-source MMORPG server emulator written in C++. It is a fork of the [OpenTibia Server](https://github.com/opentibia/server) project. To connect to the server, you can use [OTClient](https://github.com/edubart/otclient) or [OpenTibiaUnity](https://github.com/slavidodo/OpenTibia-Unity). + +### Getting Started + +* [Compiling](https://github.com/otland/forgottenserver/wiki/Compiling), alternatively download [AppVeyor builds for Windows](https://ci.appveyor.com/project/otland/forgottenserver) +* [Scripting Reference](https://github.com/otland/forgottenserver/wiki/Script-Interface) + +### Support + +If you need help, please visit the [support forum on OTLand](https://otland.net/forums/support.16/). Our issue tracker is not a support forum, and using it as one will result in your issue being closed. If you were unable to get assistance in the support forum, you should consider [becoming a premium user on OTLand](https://otland.net/account/upgrades) which grants you access to the premium support forum and supports OTLand financially. + +### Issues + +We use the [issue tracker on GitHub](https://github.com/otland/forgottenserver/issues). Keep in mind that everyone who is watching the repository gets notified by e-mail when there is activity, so be thoughtful and avoid writing comments that aren't meaningful for an issue (e.g. "+1"). If you'd like for an issue to be fixed faster, you should either fix it yourself and submit a pull request, or place a bounty on the issue. diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..3f68374 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,47 @@ +image: Visual Studio 2019 + +shallow_clone: true + +platform: + - x64 + +configuration: + - Debug + - Release + +matrix: + fast_finish: true + +only_commits: + files: + - src/ + - vc14/ + - appveyor.yml + +install: + - cmd : vcpkg install boost-iostreams:x64-windows + - cmd : vcpkg install boost-asio:x64-windows + - cmd : vcpkg install boost-system:x64-windows + - cmd : vcpkg install boost-filesystem:x64-windows + - cmd : vcpkg install boost-variant:x64-windows + - cmd : vcpkg install boost-lockfree:x64-windows + - cmd : vcpkg install cryptopp:x64-windows + - cmd : vcpkg install luajit:x64-windows + - cmd : vcpkg install libmariadb:x64-windows + - cmd : vcpkg install pugixml:x64-windows + +build: + parallel: true + # MSBuild verbosity level + #verbosity: detailed + +cache: + - c:\tools\vcpkg\installed\ + +after_build: + - 7z a -tzip tfs-win-%PLATFORM%-%CONFIGURATION%.zip -r %APPVEYOR_BUILD_FOLDER%\vc14\%PLATFORM%\%CONFIGURATION%\*.dll %APPVEYOR_BUILD_FOLDER%\vc14\%PLATFORM%\%CONFIGURATION%\theforgottenserver*.exe %APPVEYOR_BUILD_FOLDER%\data %APPVEYOR_BUILD_FOLDER%\config.lua.dist %APPVEYOR_BUILD_FOLDER%\key.pem %APPVEYOR_BUILD_FOLDER%\LICENSE %APPVEYOR_BUILD_FOLDER%\README.md %APPVEYOR_BUILD_FOLDER%\schema.sql + +artifacts: + - path: vc14\%PLATFORM%\%CONFIGURATION%\theforgottenserver*.exe + - path: vc14\%PLATFORM%\%CONFIGURATION%\*.dll + - path: tfs-win-%PLATFORM%-%CONFIGURATION%.zip diff --git a/cmake/FindCrypto++.cmake b/cmake/FindCrypto++.cmake new file mode 100644 index 0000000..5f4cb92 --- /dev/null +++ b/cmake/FindCrypto++.cmake @@ -0,0 +1,13 @@ +# Locate Crypto++ library +# This module defines +# Crypto++_FOUND +# Crypto++_INCLUDE_DIR +# Crypto++_LIBRARIES + +find_path(Crypto++_INCLUDE_DIR NAMES cryptopp/cryptlib.h) +find_library(Crypto++_LIBRARIES NAMES cryptopp libcryptopp) + +include(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(Crypto++ DEFAULT_MSG Crypto++_INCLUDE_DIR Crypto++_LIBRARIES) + +mark_as_advanced(Crypto++_INCLUDE_DIR Crypto++_LIBRARIES) diff --git a/cmake/FindLuaJIT.cmake b/cmake/FindLuaJIT.cmake new file mode 100644 index 0000000..1602ff2 --- /dev/null +++ b/cmake/FindLuaJIT.cmake @@ -0,0 +1,63 @@ +# Locate LuaJIT library +# This module defines +# LUAJIT_FOUND, if false, do not try to link to Lua +# LUA_LIBRARIES +# LUA_INCLUDE_DIR, where to find lua.h +# LUAJIT_VERSION_STRING, the version of Lua found (since CMake 2.8.8) + +## Copied from default CMake FindLua51.cmake + +find_path(LUA_INCLUDE_DIR luajit.h + HINTS + ENV LUA_DIR + PATH_SUFFIXES include/luajit-2.0 include/luajit-2.1 include luajit + PATHS + ~/Library/Frameworks + /Library/Frameworks + /sw # Fink + /opt/local # DarwinPorts + /opt/csw # Blastwave + /opt +) + +find_library(LUA_LIBRARY + NAMES luajit-5.1 lua51 + HINTS + ENV LUA_DIR + PATH_SUFFIXES lib + PATHS + ~/Library/Frameworks + /Library/Frameworks + /sw + /opt/local + /opt/csw + /opt +) + +if(LUA_LIBRARY) + # include the math library for Unix + if(UNIX AND NOT APPLE) + find_library(LUA_MATH_LIBRARY m) + set( LUA_LIBRARIES "${LUA_LIBRARY};${LUA_MATH_LIBRARY}" CACHE STRING "Lua Libraries") + # For Windows and Mac, don't need to explicitly include the math library + else() + set( LUA_LIBRARIES "${LUA_LIBRARY}" CACHE STRING "Lua Libraries") + endif() +endif() + +if(LUA_INCLUDE_DIR AND EXISTS "${LUA_INCLUDE_DIR}/luajit.h") + file(STRINGS "${LUA_INCLUDE_DIR}/luajit.h" luajit_version_str REGEX "^#define[ \t]+LUAJIT_VERSION[ \t]+\"LuaJIT .+\"") + + string(REGEX REPLACE "^#define[ \t]+LUAJIT_VERSION[ \t]+\"LuaJIT ([^\"]+)\".*" "\\1" LUAJIT_VERSION_STRING "${luajit_version_str}") + unset(luajit_version_str) +endif() + +include(FindPackageHandleStandardArgs) +# handle the QUIETLY and REQUIRED arguments and set LUA_FOUND to TRUE if +# all listed variables are TRUE +FIND_PACKAGE_HANDLE_STANDARD_ARGS(LuaJIT + REQUIRED_VARS LUA_LIBRARIES LUA_INCLUDE_DIR + VERSION_VAR LUAJIT_VERSION_STRING) + +mark_as_advanced(LUA_INCLUDE_DIR LUA_LIBRARIES LUA_LIBRARY LUA_MATH_LIBRARY) + diff --git a/cmake/FindMySQL.cmake b/cmake/FindMySQL.cmake new file mode 100644 index 0000000..9ca2a99 --- /dev/null +++ b/cmake/FindMySQL.cmake @@ -0,0 +1,125 @@ +#-------------------------------------------------------- +# Copyright (C) 1995-2007 MySQL AB +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# There are special exceptions to the terms and conditions of the GPL +# as it is applied to this software. View the full text of the exception +# in file LICENSE.exceptions in the top-level directory of this software +# distribution. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# The MySQL Connector/ODBC is licensed under the terms of the +# GPL, like most MySQL Connectors. There are special exceptions +# to the terms and conditions of the GPL as it is applied to +# this software, see the FLOSS License Exception available on +# mysql.com. + +########################################################################## + + +#-------------- FIND MYSQL_INCLUDE_DIR ------------------ +FIND_PATH(MYSQL_INCLUDE_DIR mysql.h + $ENV{MYSQL_INCLUDE_DIR} + $ENV{MYSQL_DIR}/include + /usr/include/mysql + /usr/local/include/mysql + /opt/mysql/mysql/include + /opt/mysql/mysql/include/mysql + /opt/mysql/include + /opt/local/include/mysql5 + /usr/local/mysql/include + /usr/local/mysql/include/mysql + $ENV{ProgramFiles}/MySQL/*/include + $ENV{SystemDrive}/MySQL/*/include + PATH_SUFFIXES mysql) + +#----------------- FIND MYSQL_LIB_DIR ------------------- +IF (WIN32) + # Set lib path suffixes + # dist = for mysql binary distributions + # build = for custom built tree + IF (CMAKE_BUILD_TYPE STREQUAL Debug) + SET(libsuffixDist debug) + SET(libsuffixBuild Debug) + ELSE (CMAKE_BUILD_TYPE STREQUAL Debug) + SET(libsuffixDist opt) + SET(libsuffixBuild Release) + ADD_DEFINITIONS(-DDBUG_OFF) + ENDIF (CMAKE_BUILD_TYPE STREQUAL Debug) + + FIND_LIBRARY(MYSQL_LIB NAMES mysqlclient libmariadb + PATHS + $ENV{MYSQL_DIR}/lib/${libsuffixDist} + $ENV{MYSQL_DIR}/libmysql + $ENV{MYSQL_DIR}/libmysql/${libsuffixBuild} + $ENV{MYSQL_DIR}/client/${libsuffixBuild} + $ENV{MYSQL_DIR}/libmysql/${libsuffixBuild} + $ENV{ProgramFiles}/MySQL/*/lib/${libsuffixDist} + $ENV{SystemDrive}/MySQL/*/lib/${libsuffixDist}) +ELSE (WIN32) + FIND_LIBRARY(MYSQL_LIB NAMES mysqlclient mariadbclient libmariadb libmariadbclient + PATHS + $ENV{MYSQL_DIR}/libmysql/.libs + $ENV{MYSQL_DIR}/lib + $ENV{MYSQL_DIR}/lib/mysql + /usr/lib/mysql + /usr/local/lib/mysql + /usr/local/mysql/lib + /usr/local/mysql/lib/mysql + /opt/local/mysql5/lib + /opt/local/lib/mysql5/mysql + /opt/mysql/mysql/lib/mysql + /opt/mysql/lib/mysql) +ENDIF (WIN32) + +IF(MYSQL_LIB) + GET_FILENAME_COMPONENT(MYSQL_LIB_DIR ${MYSQL_LIB} PATH) +ENDIF(MYSQL_LIB) + +IF (MYSQL_INCLUDE_DIR AND MYSQL_LIB_DIR) + SET(MYSQL_FOUND TRUE) + + INCLUDE_DIRECTORIES(${MYSQL_INCLUDE_DIR}) + LINK_DIRECTORIES(${MYSQL_LIB_DIR}) + + FIND_LIBRARY(MYSQL_ZLIB zlib PATHS ${MYSQL_LIB_DIR}) + FIND_LIBRARY(MYSQL_YASSL yassl PATHS ${MYSQL_LIB_DIR}) + FIND_LIBRARY(MYSQL_TAOCRYPT taocrypt PATHS ${MYSQL_LIB_DIR}) + SET(MYSQL_CLIENT_LIBS ${MYSQL_LIB}) + IF (MYSQL_ZLIB) + SET(MYSQL_CLIENT_LIBS ${MYSQL_CLIENT_LIBS} ${MYSQL_ZLIB}) + ENDIF (MYSQL_ZLIB) + IF (MYSQL_YASSL) + SET(MYSQL_CLIENT_LIBS ${MYSQL_CLIENT_LIBS} ${MYSQL_YASSL}) + ENDIF (MYSQL_YASSL) + IF (MYSQL_TAOCRYPT) + SET(MYSQL_CLIENT_LIBS ${MYSQL_CLIENT_LIBS} ${MYSQL_TAOCRYPT}) + ENDIF (MYSQL_TAOCRYPT) + # Add needed mysqlclient dependencies on Windows + IF (WIN32) + SET(MYSQL_CLIENT_LIBS ${MYSQL_CLIENT_LIBS} ws2_32) + ENDIF (WIN32) + IF (APPLE) + SET(MYSQL_CLIENT_LIBS ${MYSQL_CLIENT_LIBS} iconv) + ENDIF (APPLE) + IF (UNIX) + SET(MYSQL_CLIENT_LIBS ${MYSQL_CLIENT_LIBS} "-ldl") + ENDIF (UNIX) + + MESSAGE(STATUS "MySQL Include dir: ${MYSQL_INCLUDE_DIR} library dir: ${MYSQL_LIB_DIR}") + MESSAGE(STATUS "MySQL client libraries: ${MYSQL_CLIENT_LIBS}") +ELSE (MYSQL_INCLUDE_DIR AND MYSQL_LIB_DIR) + MESSAGE(FATAL_ERROR "Cannot find MySQL. Include dir: ${MYSQL_INCLUDE_DIR} library dir: ${MYSQL_LIB_DIR}") +ENDIF (MYSQL_INCLUDE_DIR AND MYSQL_LIB_DIR) + diff --git a/cmake/FindPugiXML.cmake b/cmake/FindPugiXML.cmake new file mode 100644 index 0000000..609a1e2 --- /dev/null +++ b/cmake/FindPugiXML.cmake @@ -0,0 +1,19 @@ +if(APPLE) + find_package(PkgConfig REQUIRED) + pkg_check_modules(PC_PUGIXML QUIET pugixml) + set(PUGIXML_DEFINITIONS ${PC_PUGIXML_CFLAGS_OTHER}) + + find_path(PUGIXML_INCLUDE_DIR pugixml.hpp HINTS ${PC_PUGIXML_INCLUDEDIR} ${PC_PUGIXML_INCLUDE_DIRS}) + find_library(PUGIXML_LIBRARIES NAMES pugixml HINTS ${PC_PUGIXML_LIBDIR} ${PC_PUGIXML_LIBRARY_DIRS}) +else() + find_path(PUGIXML_INCLUDE_DIR NAMES pugixml.hpp) + if(CMAKE_BUILD_TYPE STREQUAL Debug) + find_library(PUGIXML_LIBRARIES NAMES pugixml_d pugixml) + else() + find_library(PUGIXML_LIBRARIES NAMES pugixml) + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(PugiXML REQUIRED_VARS PUGIXML_INCLUDE_DIR PUGIXML_LIBRARIES) +mark_as_advanced(PUGIXML_INCLUDE_DIR PUGIXML_LIBRARIES) diff --git a/cmake/cotire.cmake b/cmake/cotire.cmake new file mode 100644 index 0000000..72c034b --- /dev/null +++ b/cmake/cotire.cmake @@ -0,0 +1,4212 @@ +# - cotire (compile time reducer) +# +# See the cotire manual for usage hints. +# +#============================================================================= +# Copyright 2012-2018 Sascha Kratky +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +#============================================================================= + +if(__COTIRE_INCLUDED) + return() +endif() +set(__COTIRE_INCLUDED TRUE) + +# call cmake_minimum_required, but prevent modification of the CMake policy stack in include mode +# cmake_minimum_required also sets the policy version as a side effect, which we have to avoid +if (NOT CMAKE_SCRIPT_MODE_FILE) + cmake_policy(PUSH) +endif() +cmake_minimum_required(VERSION 2.8.12) +if (NOT CMAKE_SCRIPT_MODE_FILE) + cmake_policy(POP) +endif() + +set (COTIRE_CMAKE_MODULE_FILE "${CMAKE_CURRENT_LIST_FILE}") +set (COTIRE_CMAKE_MODULE_VERSION "1.8.1") + +# activate select policies +if (POLICY CMP0025) + # Compiler id for Apple Clang is now AppleClang + cmake_policy(SET CMP0025 NEW) +endif() + +if (POLICY CMP0026) + # disallow use of the LOCATION target property + cmake_policy(SET CMP0026 NEW) +endif() + +if (POLICY CMP0038) + # targets may not link directly to themselves + cmake_policy(SET CMP0038 NEW) +endif() + +if (POLICY CMP0039) + # utility targets may not have link dependencies + cmake_policy(SET CMP0039 NEW) +endif() + +if (POLICY CMP0040) + # target in the TARGET signature of add_custom_command() must exist + cmake_policy(SET CMP0040 NEW) +endif() + +if (POLICY CMP0045) + # error on non-existent target in get_target_property + cmake_policy(SET CMP0045 NEW) +endif() + +if (POLICY CMP0046) + # error on non-existent dependency in add_dependencies + cmake_policy(SET CMP0046 NEW) +endif() + +if (POLICY CMP0049) + # do not expand variables in target source entries + cmake_policy(SET CMP0049 NEW) +endif() + +if (POLICY CMP0050) + # disallow add_custom_command SOURCE signatures + cmake_policy(SET CMP0050 NEW) +endif() + +if (POLICY CMP0051) + # include TARGET_OBJECTS expressions in a target's SOURCES property + cmake_policy(SET CMP0051 NEW) +endif() + +if (POLICY CMP0053) + # simplify variable reference and escape sequence evaluation + cmake_policy(SET CMP0053 NEW) +endif() + +if (POLICY CMP0054) + # only interpret if() arguments as variables or keywords when unquoted + cmake_policy(SET CMP0054 NEW) +endif() + +if (POLICY CMP0055) + # strict checking for break() command + cmake_policy(SET CMP0055 NEW) +endif() + +include(CMakeParseArguments) +include(ProcessorCount) + +function (cotire_get_configuration_types _configsVar) + set (_configs "") + if (CMAKE_CONFIGURATION_TYPES) + list (APPEND _configs ${CMAKE_CONFIGURATION_TYPES}) + endif() + if (CMAKE_BUILD_TYPE) + list (APPEND _configs "${CMAKE_BUILD_TYPE}") + endif() + if (_configs) + list (REMOVE_DUPLICATES _configs) + set (${_configsVar} ${_configs} PARENT_SCOPE) + else() + set (${_configsVar} "None" PARENT_SCOPE) + endif() +endfunction() + +function (cotire_get_source_file_extension _sourceFile _extVar) + # get_filename_component returns extension from first occurrence of . in file name + # this function computes the extension from last occurrence of . in file name + string (FIND "${_sourceFile}" "." _index REVERSE) + if (_index GREATER -1) + math (EXPR _index "${_index} + 1") + string (SUBSTRING "${_sourceFile}" ${_index} -1 _sourceExt) + else() + set (_sourceExt "") + endif() + set (${_extVar} "${_sourceExt}" PARENT_SCOPE) +endfunction() + +macro (cotire_check_is_path_relative_to _path _isRelativeVar) + set (${_isRelativeVar} FALSE) + if (IS_ABSOLUTE "${_path}") + foreach (_dir ${ARGN}) + file (RELATIVE_PATH _relPath "${_dir}" "${_path}") + if (NOT _relPath OR (NOT IS_ABSOLUTE "${_relPath}" AND NOT "${_relPath}" MATCHES "^\\.\\.")) + set (${_isRelativeVar} TRUE) + break() + endif() + endforeach() + endif() +endmacro() + +function (cotire_filter_language_source_files _language _target _sourceFilesVar _excludedSourceFilesVar _cotiredSourceFilesVar) + if (CMAKE_${_language}_SOURCE_FILE_EXTENSIONS) + set (_languageExtensions "${CMAKE_${_language}_SOURCE_FILE_EXTENSIONS}") + else() + set (_languageExtensions "") + endif() + if (CMAKE_${_language}_IGNORE_EXTENSIONS) + set (_ignoreExtensions "${CMAKE_${_language}_IGNORE_EXTENSIONS}") + else() + set (_ignoreExtensions "") + endif() + if (COTIRE_UNITY_SOURCE_EXCLUDE_EXTENSIONS) + set (_excludeExtensions "${COTIRE_UNITY_SOURCE_EXCLUDE_EXTENSIONS}") + else() + set (_excludeExtensions "") + endif() + if (COTIRE_DEBUG AND _languageExtensions) + message (STATUS "${_language} source file extensions: ${_languageExtensions}") + endif() + if (COTIRE_DEBUG AND _ignoreExtensions) + message (STATUS "${_language} ignore extensions: ${_ignoreExtensions}") + endif() + if (COTIRE_DEBUG AND _excludeExtensions) + message (STATUS "${_language} exclude extensions: ${_excludeExtensions}") + endif() + if (CMAKE_VERSION VERSION_LESS "3.1.0") + set (_allSourceFiles ${ARGN}) + else() + # as of CMake 3.1 target sources may contain generator expressions + # since we cannot obtain required property information about source files added + # through generator expressions at configure time, we filter them out + string (GENEX_STRIP "${ARGN}" _allSourceFiles) + endif() + set (_filteredSourceFiles "") + set (_excludedSourceFiles "") + foreach (_sourceFile ${_allSourceFiles}) + get_source_file_property(_sourceIsHeaderOnly "${_sourceFile}" HEADER_FILE_ONLY) + get_source_file_property(_sourceIsExternal "${_sourceFile}" EXTERNAL_OBJECT) + get_source_file_property(_sourceIsSymbolic "${_sourceFile}" SYMBOLIC) + if (NOT _sourceIsHeaderOnly AND NOT _sourceIsExternal AND NOT _sourceIsSymbolic) + cotire_get_source_file_extension("${_sourceFile}" _sourceExt) + if (_sourceExt) + list (FIND _ignoreExtensions "${_sourceExt}" _ignoreIndex) + if (_ignoreIndex LESS 0) + list (FIND _excludeExtensions "${_sourceExt}" _excludeIndex) + if (_excludeIndex GREATER -1) + list (APPEND _excludedSourceFiles "${_sourceFile}") + else() + list (FIND _languageExtensions "${_sourceExt}" _sourceIndex) + if (_sourceIndex GREATER -1) + # consider source file unless it is excluded explicitly + get_source_file_property(_sourceIsExcluded "${_sourceFile}" COTIRE_EXCLUDED) + if (_sourceIsExcluded) + list (APPEND _excludedSourceFiles "${_sourceFile}") + else() + list (APPEND _filteredSourceFiles "${_sourceFile}") + endif() + else() + get_source_file_property(_sourceLanguage "${_sourceFile}" LANGUAGE) + if ("${_sourceLanguage}" STREQUAL "${_language}") + # add to excluded sources, if file is not ignored and has correct language without having the correct extension + list (APPEND _excludedSourceFiles "${_sourceFile}") + endif() + endif() + endif() + endif() + endif() + endif() + endforeach() + # separate filtered source files from already cotired ones + # the COTIRE_TARGET property of a source file may be set while a target is being processed by cotire + set (_sourceFiles "") + set (_cotiredSourceFiles "") + foreach (_sourceFile ${_filteredSourceFiles}) + get_source_file_property(_sourceIsCotired "${_sourceFile}" COTIRE_TARGET) + if (_sourceIsCotired) + list (APPEND _cotiredSourceFiles "${_sourceFile}") + else() + get_source_file_property(_sourceCompileFlags "${_sourceFile}" COMPILE_FLAGS) + if (_sourceCompileFlags) + # add to excluded sources, if file has custom compile flags + list (APPEND _excludedSourceFiles "${_sourceFile}") + else() + get_source_file_property(_sourceCompileOptions "${_sourceFile}" COMPILE_OPTIONS) + if (_sourceCompileOptions) + # add to excluded sources, if file has list of custom compile options + list (APPEND _excludedSourceFiles "${_sourceFile}") + else() + list (APPEND _sourceFiles "${_sourceFile}") + endif() + endif() + endif() + endforeach() + if (COTIRE_DEBUG) + if (_sourceFiles) + message (STATUS "Filtered ${_target} ${_language} sources: ${_sourceFiles}") + endif() + if (_excludedSourceFiles) + message (STATUS "Excluded ${_target} ${_language} sources: ${_excludedSourceFiles}") + endif() + if (_cotiredSourceFiles) + message (STATUS "Cotired ${_target} ${_language} sources: ${_cotiredSourceFiles}") + endif() + endif() + set (${_sourceFilesVar} ${_sourceFiles} PARENT_SCOPE) + set (${_excludedSourceFilesVar} ${_excludedSourceFiles} PARENT_SCOPE) + set (${_cotiredSourceFilesVar} ${_cotiredSourceFiles} PARENT_SCOPE) +endfunction() + +function (cotire_get_objects_with_property_on _filteredObjectsVar _property _type) + set (_filteredObjects "") + foreach (_object ${ARGN}) + get_property(_isSet ${_type} "${_object}" PROPERTY ${_property} SET) + if (_isSet) + get_property(_propertyValue ${_type} "${_object}" PROPERTY ${_property}) + if (_propertyValue) + list (APPEND _filteredObjects "${_object}") + endif() + endif() + endforeach() + set (${_filteredObjectsVar} ${_filteredObjects} PARENT_SCOPE) +endfunction() + +function (cotire_get_objects_with_property_off _filteredObjectsVar _property _type) + set (_filteredObjects "") + foreach (_object ${ARGN}) + get_property(_isSet ${_type} "${_object}" PROPERTY ${_property} SET) + if (_isSet) + get_property(_propertyValue ${_type} "${_object}" PROPERTY ${_property}) + if (NOT _propertyValue) + list (APPEND _filteredObjects "${_object}") + endif() + endif() + endforeach() + set (${_filteredObjectsVar} ${_filteredObjects} PARENT_SCOPE) +endfunction() + +function (cotire_get_source_file_property_values _valuesVar _property) + set (_values "") + foreach (_sourceFile ${ARGN}) + get_source_file_property(_propertyValue "${_sourceFile}" ${_property}) + if (_propertyValue) + list (APPEND _values "${_propertyValue}") + endif() + endforeach() + set (${_valuesVar} ${_values} PARENT_SCOPE) +endfunction() + +function (cotire_resolve_config_properties _configurations _propertiesVar) + set (_properties "") + foreach (_property ${ARGN}) + if ("${_property}" MATCHES "") + foreach (_config ${_configurations}) + string (TOUPPER "${_config}" _upperConfig) + string (REPLACE "" "${_upperConfig}" _configProperty "${_property}") + list (APPEND _properties ${_configProperty}) + endforeach() + else() + list (APPEND _properties ${_property}) + endif() + endforeach() + set (${_propertiesVar} ${_properties} PARENT_SCOPE) +endfunction() + +function (cotire_copy_set_properties _configurations _type _source _target) + cotire_resolve_config_properties("${_configurations}" _properties ${ARGN}) + foreach (_property ${_properties}) + get_property(_isSet ${_type} ${_source} PROPERTY ${_property} SET) + if (_isSet) + get_property(_propertyValue ${_type} ${_source} PROPERTY ${_property}) + set_property(${_type} ${_target} PROPERTY ${_property} "${_propertyValue}") + endif() + endforeach() +endfunction() + +function (cotire_get_target_usage_requirements _target _config _targetRequirementsVar) + set (_targetRequirements "") + get_target_property(_librariesToProcess ${_target} LINK_LIBRARIES) + while (_librariesToProcess) + # remove from head + list (GET _librariesToProcess 0 _library) + list (REMOVE_AT _librariesToProcess 0) + if (_library MATCHES "^\\$<\\$:([A-Za-z0-9_:-]+)>$") + set (_library "${CMAKE_MATCH_1}") + elseif (_config STREQUAL "None" AND _library MATCHES "^\\$<\\$:([A-Za-z0-9_:-]+)>$") + set (_library "${CMAKE_MATCH_1}") + endif() + if (TARGET ${_library}) + list (FIND _targetRequirements ${_library} _index) + if (_index LESS 0) + list (APPEND _targetRequirements ${_library}) + # BFS traversal of transitive libraries + get_target_property(_libraries ${_library} INTERFACE_LINK_LIBRARIES) + if (_libraries) + list (APPEND _librariesToProcess ${_libraries}) + list (REMOVE_DUPLICATES _librariesToProcess) + endif() + endif() + endif() + endwhile() + set (${_targetRequirementsVar} ${_targetRequirements} PARENT_SCOPE) +endfunction() + +function (cotire_filter_compile_flags _language _flagFilter _matchedOptionsVar _unmatchedOptionsVar) + if (WIN32 AND CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel") + set (_flagPrefix "[/-]") + else() + set (_flagPrefix "--?") + endif() + set (_optionFlag "") + set (_matchedOptions "") + set (_unmatchedOptions "") + foreach (_compileFlag ${ARGN}) + if (_compileFlag) + if (_optionFlag AND NOT "${_compileFlag}" MATCHES "^${_flagPrefix}") + # option with separate argument + list (APPEND _matchedOptions "${_compileFlag}") + set (_optionFlag "") + elseif ("${_compileFlag}" MATCHES "^(${_flagPrefix})(${_flagFilter})$") + # remember option + set (_optionFlag "${CMAKE_MATCH_2}") + elseif ("${_compileFlag}" MATCHES "^(${_flagPrefix})(${_flagFilter})(.+)$") + # option with joined argument + list (APPEND _matchedOptions "${CMAKE_MATCH_3}") + set (_optionFlag "") + else() + # flush remembered option + if (_optionFlag) + list (APPEND _matchedOptions "${_optionFlag}") + set (_optionFlag "") + endif() + # add to unfiltered options + list (APPEND _unmatchedOptions "${_compileFlag}") + endif() + endif() + endforeach() + if (_optionFlag) + list (APPEND _matchedOptions "${_optionFlag}") + endif() + if (COTIRE_DEBUG AND _matchedOptions) + message (STATUS "Filter ${_flagFilter} matched: ${_matchedOptions}") + endif() + if (COTIRE_DEBUG AND _unmatchedOptions) + message (STATUS "Filter ${_flagFilter} unmatched: ${_unmatchedOptions}") + endif() + set (${_matchedOptionsVar} ${_matchedOptions} PARENT_SCOPE) + set (${_unmatchedOptionsVar} ${_unmatchedOptions} PARENT_SCOPE) +endfunction() + +function (cotire_is_target_supported _target _isSupportedVar) + if (NOT TARGET "${_target}") + set (${_isSupportedVar} FALSE PARENT_SCOPE) + return() + endif() + get_target_property(_imported ${_target} IMPORTED) + if (_imported) + set (${_isSupportedVar} FALSE PARENT_SCOPE) + return() + endif() + get_target_property(_targetType ${_target} TYPE) + if (NOT _targetType MATCHES "EXECUTABLE|(STATIC|SHARED|MODULE|OBJECT)_LIBRARY") + set (${_isSupportedVar} FALSE PARENT_SCOPE) + return() + endif() + set (${_isSupportedVar} TRUE PARENT_SCOPE) +endfunction() + +function (cotire_get_target_compile_flags _config _language _target _flagsVar) + string (TOUPPER "${_config}" _upperConfig) + # collect options from CMake language variables + set (_compileFlags "") + if (CMAKE_${_language}_FLAGS) + set (_compileFlags "${_compileFlags} ${CMAKE_${_language}_FLAGS}") + endif() + if (CMAKE_${_language}_FLAGS_${_upperConfig}) + set (_compileFlags "${_compileFlags} ${CMAKE_${_language}_FLAGS_${_upperConfig}}") + endif() + if (_target) + # add target compile flags + get_target_property(_targetflags ${_target} COMPILE_FLAGS) + if (_targetflags) + set (_compileFlags "${_compileFlags} ${_targetflags}") + endif() + endif() + if (UNIX) + separate_arguments(_compileFlags UNIX_COMMAND "${_compileFlags}") + elseif(WIN32) + separate_arguments(_compileFlags WINDOWS_COMMAND "${_compileFlags}") + else() + separate_arguments(_compileFlags) + endif() + # target compile options + if (_target) + get_target_property(_targetOptions ${_target} COMPILE_OPTIONS) + if (_targetOptions) + list (APPEND _compileFlags ${_targetOptions}) + endif() + endif() + # interface compile options from linked library targets + if (_target) + set (_linkedTargets "") + cotire_get_target_usage_requirements(${_target} ${_config} _linkedTargets) + foreach (_linkedTarget ${_linkedTargets}) + get_target_property(_targetOptions ${_linkedTarget} INTERFACE_COMPILE_OPTIONS) + if (_targetOptions) + list (APPEND _compileFlags ${_targetOptions}) + endif() + endforeach() + endif() + # handle language standard properties + if (CMAKE_${_language}_STANDARD_DEFAULT) + # used compiler supports language standard levels + if (_target) + get_target_property(_targetLanguageStandard ${_target} ${_language}_STANDARD) + if (_targetLanguageStandard) + set (_type "EXTENSION") + get_property(_isSet TARGET ${_target} PROPERTY ${_language}_EXTENSIONS SET) + if (_isSet) + get_target_property(_targetUseLanguageExtensions ${_target} ${_language}_EXTENSIONS) + if (NOT _targetUseLanguageExtensions) + set (_type "STANDARD") + endif() + endif() + if (CMAKE_${_language}${_targetLanguageStandard}_${_type}_COMPILE_OPTION) + list (APPEND _compileFlags "${CMAKE_${_language}${_targetLanguageStandard}_${_type}_COMPILE_OPTION}") + endif() + endif() + endif() + endif() + # handle the POSITION_INDEPENDENT_CODE target property + if (_target) + get_target_property(_targetPIC ${_target} POSITION_INDEPENDENT_CODE) + if (_targetPIC) + get_target_property(_targetType ${_target} TYPE) + if (_targetType STREQUAL "EXECUTABLE" AND CMAKE_${_language}_COMPILE_OPTIONS_PIE) + list (APPEND _compileFlags "${CMAKE_${_language}_COMPILE_OPTIONS_PIE}") + elseif (CMAKE_${_language}_COMPILE_OPTIONS_PIC) + list (APPEND _compileFlags "${CMAKE_${_language}_COMPILE_OPTIONS_PIC}") + endif() + endif() + endif() + # handle visibility target properties + if (_target) + get_target_property(_targetVisibility ${_target} ${_language}_VISIBILITY_PRESET) + if (_targetVisibility AND CMAKE_${_language}_COMPILE_OPTIONS_VISIBILITY) + list (APPEND _compileFlags "${CMAKE_${_language}_COMPILE_OPTIONS_VISIBILITY}${_targetVisibility}") + endif() + get_target_property(_targetVisibilityInlines ${_target} VISIBILITY_INLINES_HIDDEN) + if (_targetVisibilityInlines AND CMAKE_${_language}_COMPILE_OPTIONS_VISIBILITY_INLINES_HIDDEN) + list (APPEND _compileFlags "${CMAKE_${_language}_COMPILE_OPTIONS_VISIBILITY_INLINES_HIDDEN}") + endif() + endif() + # platform specific flags + if (APPLE) + get_target_property(_architectures ${_target} OSX_ARCHITECTURES_${_upperConfig}) + if (NOT _architectures) + get_target_property(_architectures ${_target} OSX_ARCHITECTURES) + endif() + if (_architectures) + foreach (_arch ${_architectures}) + list (APPEND _compileFlags "-arch" "${_arch}") + endforeach() + endif() + if (CMAKE_OSX_SYSROOT) + if (CMAKE_${_language}_SYSROOT_FLAG) + list (APPEND _compileFlags "${CMAKE_${_language}_SYSROOT_FLAG}" "${CMAKE_OSX_SYSROOT}") + else() + list (APPEND _compileFlags "-isysroot" "${CMAKE_OSX_SYSROOT}") + endif() + endif() + if (CMAKE_OSX_DEPLOYMENT_TARGET) + if (CMAKE_${_language}_OSX_DEPLOYMENT_TARGET_FLAG) + list (APPEND _compileFlags "${CMAKE_${_language}_OSX_DEPLOYMENT_TARGET_FLAG}${CMAKE_OSX_DEPLOYMENT_TARGET}") + else() + list (APPEND _compileFlags "-mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") + endif() + endif() + endif() + if (COTIRE_DEBUG AND _compileFlags) + message (STATUS "Target ${_target} compile flags: ${_compileFlags}") + endif() + set (${_flagsVar} ${_compileFlags} PARENT_SCOPE) +endfunction() + +function (cotire_get_target_include_directories _config _language _target _includeDirsVar _systemIncludeDirsVar) + set (_includeDirs "") + set (_systemIncludeDirs "") + # default include dirs + if (CMAKE_INCLUDE_CURRENT_DIR) + list (APPEND _includeDirs "${CMAKE_CURRENT_BINARY_DIR}") + list (APPEND _includeDirs "${CMAKE_CURRENT_SOURCE_DIR}") + endif() + set (_targetFlags "") + cotire_get_target_compile_flags("${_config}" "${_language}" "${_target}" _targetFlags) + # parse additional include directories from target compile flags + if (CMAKE_INCLUDE_FLAG_${_language}) + string (STRIP "${CMAKE_INCLUDE_FLAG_${_language}}" _includeFlag) + string (REGEX REPLACE "^[-/]+" "" _includeFlag "${_includeFlag}") + if (_includeFlag) + set (_dirs "") + cotire_filter_compile_flags("${_language}" "${_includeFlag}" _dirs _ignore ${_targetFlags}) + if (_dirs) + list (APPEND _includeDirs ${_dirs}) + endif() + endif() + endif() + # parse additional system include directories from target compile flags + if (CMAKE_INCLUDE_SYSTEM_FLAG_${_language}) + string (STRIP "${CMAKE_INCLUDE_SYSTEM_FLAG_${_language}}" _includeFlag) + string (REGEX REPLACE "^[-/]+" "" _includeFlag "${_includeFlag}") + if (_includeFlag) + set (_dirs "") + cotire_filter_compile_flags("${_language}" "${_includeFlag}" _dirs _ignore ${_targetFlags}) + if (_dirs) + list (APPEND _systemIncludeDirs ${_dirs}) + endif() + endif() + endif() + # target include directories + get_directory_property(_dirs DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" INCLUDE_DIRECTORIES) + if (_target) + get_target_property(_targetDirs ${_target} INCLUDE_DIRECTORIES) + if (_targetDirs) + list (APPEND _dirs ${_targetDirs}) + endif() + get_target_property(_targetDirs ${_target} INTERFACE_SYSTEM_INCLUDE_DIRECTORIES) + if (_targetDirs) + list (APPEND _systemIncludeDirs ${_targetDirs}) + endif() + endif() + # interface include directories from linked library targets + if (_target) + set (_linkedTargets "") + cotire_get_target_usage_requirements(${_target} ${_config} _linkedTargets) + foreach (_linkedTarget ${_linkedTargets}) + get_target_property(_linkedTargetType ${_linkedTarget} TYPE) + if (CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE AND NOT CMAKE_VERSION VERSION_LESS "3.4.0" AND + _linkedTargetType MATCHES "(STATIC|SHARED|MODULE|OBJECT)_LIBRARY") + # CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE refers to CMAKE_CURRENT_BINARY_DIR and CMAKE_CURRENT_SOURCE_DIR + # at the time, when the target was created. These correspond to the target properties BINARY_DIR and SOURCE_DIR + # which are only available with CMake 3.4 or later. + get_target_property(_targetDirs ${_linkedTarget} BINARY_DIR) + if (_targetDirs) + list (APPEND _dirs ${_targetDirs}) + endif() + get_target_property(_targetDirs ${_linkedTarget} SOURCE_DIR) + if (_targetDirs) + list (APPEND _dirs ${_targetDirs}) + endif() + endif() + get_target_property(_targetDirs ${_linkedTarget} INTERFACE_INCLUDE_DIRECTORIES) + if (_targetDirs) + list (APPEND _dirs ${_targetDirs}) + endif() + get_target_property(_targetDirs ${_linkedTarget} INTERFACE_SYSTEM_INCLUDE_DIRECTORIES) + if (_targetDirs) + list (APPEND _systemIncludeDirs ${_targetDirs}) + endif() + endforeach() + endif() + if (dirs) + list (REMOVE_DUPLICATES _dirs) + endif() + list (LENGTH _includeDirs _projectInsertIndex) + foreach (_dir ${_dirs}) + if (CMAKE_INCLUDE_DIRECTORIES_PROJECT_BEFORE) + cotire_check_is_path_relative_to("${_dir}" _isRelative "${CMAKE_SOURCE_DIR}" "${CMAKE_BINARY_DIR}") + if (_isRelative) + list (LENGTH _includeDirs _len) + if (_len EQUAL _projectInsertIndex) + list (APPEND _includeDirs "${_dir}") + else() + list (INSERT _includeDirs _projectInsertIndex "${_dir}") + endif() + math (EXPR _projectInsertIndex "${_projectInsertIndex} + 1") + else() + list (APPEND _includeDirs "${_dir}") + endif() + else() + list (APPEND _includeDirs "${_dir}") + endif() + endforeach() + list (REMOVE_DUPLICATES _includeDirs) + list (REMOVE_DUPLICATES _systemIncludeDirs) + if (CMAKE_${_language}_IMPLICIT_INCLUDE_DIRECTORIES) + list (REMOVE_ITEM _includeDirs ${CMAKE_${_language}_IMPLICIT_INCLUDE_DIRECTORIES}) + endif() + if (WIN32 AND NOT MINGW) + # convert Windows paths in include directories to CMake paths + if (_includeDirs) + set (_paths "") + foreach (_dir ${_includeDirs}) + file (TO_CMAKE_PATH "${_dir}" _path) + list (APPEND _paths "${_path}") + endforeach() + set (_includeDirs ${_paths}) + endif() + if (_systemIncludeDirs) + set (_paths "") + foreach (_dir ${_systemIncludeDirs}) + file (TO_CMAKE_PATH "${_dir}" _path) + list (APPEND _paths "${_path}") + endforeach() + set (_systemIncludeDirs ${_paths}) + endif() + endif() + if (COTIRE_DEBUG AND _includeDirs) + message (STATUS "Target ${_target} include dirs: ${_includeDirs}") + endif() + set (${_includeDirsVar} ${_includeDirs} PARENT_SCOPE) + if (COTIRE_DEBUG AND _systemIncludeDirs) + message (STATUS "Target ${_target} system include dirs: ${_systemIncludeDirs}") + endif() + set (${_systemIncludeDirsVar} ${_systemIncludeDirs} PARENT_SCOPE) +endfunction() + +function (cotire_get_target_export_symbol _target _exportSymbolVar) + set (_exportSymbol "") + get_target_property(_targetType ${_target} TYPE) + get_target_property(_enableExports ${_target} ENABLE_EXPORTS) + if (_targetType MATCHES "(SHARED|MODULE)_LIBRARY" OR + (_targetType STREQUAL "EXECUTABLE" AND _enableExports)) + get_target_property(_exportSymbol ${_target} DEFINE_SYMBOL) + if (NOT _exportSymbol) + set (_exportSymbol "${_target}_EXPORTS") + endif() + string (MAKE_C_IDENTIFIER "${_exportSymbol}" _exportSymbol) + endif() + set (${_exportSymbolVar} ${_exportSymbol} PARENT_SCOPE) +endfunction() + +function (cotire_get_target_compile_definitions _config _language _target _definitionsVar) + string (TOUPPER "${_config}" _upperConfig) + set (_configDefinitions "") + # CMAKE_INTDIR for multi-configuration build systems + if (NOT "${CMAKE_CFG_INTDIR}" STREQUAL ".") + list (APPEND _configDefinitions "CMAKE_INTDIR=\"${_config}\"") + endif() + # target export define symbol + cotire_get_target_export_symbol("${_target}" _defineSymbol) + if (_defineSymbol) + list (APPEND _configDefinitions "${_defineSymbol}") + endif() + # directory compile definitions + get_directory_property(_definitions DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" COMPILE_DEFINITIONS) + if (_definitions) + list (APPEND _configDefinitions ${_definitions}) + endif() + get_directory_property(_definitions DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" COMPILE_DEFINITIONS_${_upperConfig}) + if (_definitions) + list (APPEND _configDefinitions ${_definitions}) + endif() + # target compile definitions + get_target_property(_definitions ${_target} COMPILE_DEFINITIONS) + if (_definitions) + list (APPEND _configDefinitions ${_definitions}) + endif() + get_target_property(_definitions ${_target} COMPILE_DEFINITIONS_${_upperConfig}) + if (_definitions) + list (APPEND _configDefinitions ${_definitions}) + endif() + # interface compile definitions from linked library targets + set (_linkedTargets "") + cotire_get_target_usage_requirements(${_target} ${_config} _linkedTargets) + foreach (_linkedTarget ${_linkedTargets}) + get_target_property(_definitions ${_linkedTarget} INTERFACE_COMPILE_DEFINITIONS) + if (_definitions) + list (APPEND _configDefinitions ${_definitions}) + endif() + endforeach() + # parse additional compile definitions from target compile flags + # and do not look at directory compile definitions, which we already handled + set (_targetFlags "") + cotire_get_target_compile_flags("${_config}" "${_language}" "${_target}" _targetFlags) + cotire_filter_compile_flags("${_language}" "D" _definitions _ignore ${_targetFlags}) + if (_definitions) + list (APPEND _configDefinitions ${_definitions}) + endif() + list (REMOVE_DUPLICATES _configDefinitions) + if (COTIRE_DEBUG AND _configDefinitions) + message (STATUS "Target ${_target} compile definitions: ${_configDefinitions}") + endif() + set (${_definitionsVar} ${_configDefinitions} PARENT_SCOPE) +endfunction() + +function (cotire_get_target_compiler_flags _config _language _target _compilerFlagsVar) + # parse target compile flags omitting compile definitions and include directives + set (_targetFlags "") + cotire_get_target_compile_flags("${_config}" "${_language}" "${_target}" _targetFlags) + set (_flagFilter "D") + if (CMAKE_INCLUDE_FLAG_${_language}) + string (STRIP "${CMAKE_INCLUDE_FLAG_${_language}}" _includeFlag) + string (REGEX REPLACE "^[-/]+" "" _includeFlag "${_includeFlag}") + if (_includeFlag) + set (_flagFilter "${_flagFilter}|${_includeFlag}") + endif() + endif() + if (CMAKE_INCLUDE_SYSTEM_FLAG_${_language}) + string (STRIP "${CMAKE_INCLUDE_SYSTEM_FLAG_${_language}}" _includeFlag) + string (REGEX REPLACE "^[-/]+" "" _includeFlag "${_includeFlag}") + if (_includeFlag) + set (_flagFilter "${_flagFilter}|${_includeFlag}") + endif() + endif() + set (_compilerFlags "") + cotire_filter_compile_flags("${_language}" "${_flagFilter}" _ignore _compilerFlags ${_targetFlags}) + if (COTIRE_DEBUG AND _compilerFlags) + message (STATUS "Target ${_target} compiler flags: ${_compilerFlags}") + endif() + set (${_compilerFlagsVar} ${_compilerFlags} PARENT_SCOPE) +endfunction() + +function (cotire_add_sys_root_paths _pathsVar) + if (APPLE) + if (CMAKE_OSX_SYSROOT AND CMAKE_${_language}_HAS_ISYSROOT) + foreach (_path IN LISTS ${_pathsVar}) + if (IS_ABSOLUTE "${_path}") + get_filename_component(_path "${CMAKE_OSX_SYSROOT}/${_path}" ABSOLUTE) + if (EXISTS "${_path}") + list (APPEND ${_pathsVar} "${_path}") + endif() + endif() + endforeach() + endif() + endif() + set (${_pathsVar} ${${_pathsVar}} PARENT_SCOPE) +endfunction() + +function (cotire_get_source_extra_properties _sourceFile _pattern _resultVar) + set (_extraProperties ${ARGN}) + set (_result "") + if (_extraProperties) + list (FIND _extraProperties "${_sourceFile}" _index) + if (_index GREATER -1) + math (EXPR _index "${_index} + 1") + list (LENGTH _extraProperties _len) + math (EXPR _len "${_len} - 1") + foreach (_index RANGE ${_index} ${_len}) + list (GET _extraProperties ${_index} _value) + if (_value MATCHES "${_pattern}") + list (APPEND _result "${_value}") + else() + break() + endif() + endforeach() + endif() + endif() + set (${_resultVar} ${_result} PARENT_SCOPE) +endfunction() + +function (cotire_get_source_compile_definitions _config _language _sourceFile _definitionsVar) + set (_compileDefinitions "") + if (NOT CMAKE_SCRIPT_MODE_FILE) + string (TOUPPER "${_config}" _upperConfig) + get_source_file_property(_definitions "${_sourceFile}" COMPILE_DEFINITIONS) + if (_definitions) + list (APPEND _compileDefinitions ${_definitions}) + endif() + get_source_file_property(_definitions "${_sourceFile}" COMPILE_DEFINITIONS_${_upperConfig}) + if (_definitions) + list (APPEND _compileDefinitions ${_definitions}) + endif() + endif() + cotire_get_source_extra_properties("${_sourceFile}" "^[a-zA-Z0-9_]+(=.*)?$" _definitions ${ARGN}) + if (_definitions) + list (APPEND _compileDefinitions ${_definitions}) + endif() + if (COTIRE_DEBUG AND _compileDefinitions) + message (STATUS "Source ${_sourceFile} compile definitions: ${_compileDefinitions}") + endif() + set (${_definitionsVar} ${_compileDefinitions} PARENT_SCOPE) +endfunction() + +function (cotire_get_source_files_compile_definitions _config _language _definitionsVar) + set (_configDefinitions "") + foreach (_sourceFile ${ARGN}) + cotire_get_source_compile_definitions("${_config}" "${_language}" "${_sourceFile}" _sourceDefinitions) + if (_sourceDefinitions) + list (APPEND _configDefinitions "${_sourceFile}" ${_sourceDefinitions} "-") + endif() + endforeach() + set (${_definitionsVar} ${_configDefinitions} PARENT_SCOPE) +endfunction() + +function (cotire_get_source_undefs _sourceFile _property _sourceUndefsVar) + set (_sourceUndefs "") + if (NOT CMAKE_SCRIPT_MODE_FILE) + get_source_file_property(_undefs "${_sourceFile}" ${_property}) + if (_undefs) + list (APPEND _sourceUndefs ${_undefs}) + endif() + endif() + cotire_get_source_extra_properties("${_sourceFile}" "^[a-zA-Z0-9_]+$" _undefs ${ARGN}) + if (_undefs) + list (APPEND _sourceUndefs ${_undefs}) + endif() + if (COTIRE_DEBUG AND _sourceUndefs) + message (STATUS "Source ${_sourceFile} ${_property} undefs: ${_sourceUndefs}") + endif() + set (${_sourceUndefsVar} ${_sourceUndefs} PARENT_SCOPE) +endfunction() + +function (cotire_get_source_files_undefs _property _sourceUndefsVar) + set (_sourceUndefs "") + foreach (_sourceFile ${ARGN}) + cotire_get_source_undefs("${_sourceFile}" ${_property} _undefs) + if (_undefs) + list (APPEND _sourceUndefs "${_sourceFile}" ${_undefs} "-") + endif() + endforeach() + set (${_sourceUndefsVar} ${_sourceUndefs} PARENT_SCOPE) +endfunction() + +macro (cotire_set_cmd_to_prologue _cmdVar) + set (${_cmdVar} "${CMAKE_COMMAND}") + if (COTIRE_DEBUG) + list (APPEND ${_cmdVar} "--warn-uninitialized") + endif() + list (APPEND ${_cmdVar} "-DCOTIRE_BUILD_TYPE:STRING=$") + if (XCODE) + list (APPEND ${_cmdVar} "-DXCODE:BOOL=TRUE") + endif() + if (COTIRE_VERBOSE) + list (APPEND ${_cmdVar} "-DCOTIRE_VERBOSE:BOOL=ON") + elseif("${CMAKE_GENERATOR}" MATCHES "Makefiles") + list (APPEND ${_cmdVar} "-DCOTIRE_VERBOSE:BOOL=$(VERBOSE)") + endif() +endmacro() + +function (cotire_init_compile_cmd _cmdVar _language _compilerLauncher _compilerExe _compilerArg1) + if (NOT _compilerLauncher) + set (_compilerLauncher ${CMAKE_${_language}_COMPILER_LAUNCHER}) + endif() + if (NOT _compilerExe) + set (_compilerExe "${CMAKE_${_language}_COMPILER}") + endif() + if (NOT _compilerArg1) + set (_compilerArg1 ${CMAKE_${_language}_COMPILER_ARG1}) + endif() + if (WIN32) + file (TO_NATIVE_PATH "${_compilerExe}" _compilerExe) + endif() + string (STRIP "${_compilerArg1}" _compilerArg1) + if ("${CMAKE_GENERATOR}" MATCHES "Make|Ninja") + # compiler launcher is only supported for Makefile and Ninja + set (${_cmdVar} ${_compilerLauncher} "${_compilerExe}" ${_compilerArg1} PARENT_SCOPE) + else() + set (${_cmdVar} "${_compilerExe}" ${_compilerArg1} PARENT_SCOPE) + endif() +endfunction() + +macro (cotire_add_definitions_to_cmd _cmdVar _language) + foreach (_definition ${ARGN}) + if (WIN32 AND CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel") + list (APPEND ${_cmdVar} "/D${_definition}") + else() + list (APPEND ${_cmdVar} "-D${_definition}") + endif() + endforeach() +endmacro() + +function (cotire_add_includes_to_cmd _cmdVar _language _includesVar _systemIncludesVar) + set (_includeDirs ${${_includesVar}} ${${_systemIncludesVar}}) + if (_includeDirs) + list (REMOVE_DUPLICATES _includeDirs) + foreach (_include ${_includeDirs}) + if (WIN32 AND CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel") + file (TO_NATIVE_PATH "${_include}" _include) + list (APPEND ${_cmdVar} "${CMAKE_INCLUDE_FLAG_${_language}}${CMAKE_INCLUDE_FLAG_SEP_${_language}}${_include}") + else() + set (_index -1) + if ("${CMAKE_INCLUDE_SYSTEM_FLAG_${_language}}" MATCHES ".+") + list (FIND ${_systemIncludesVar} "${_include}" _index) + endif() + if (_index GREATER -1) + list (APPEND ${_cmdVar} "${CMAKE_INCLUDE_SYSTEM_FLAG_${_language}}${CMAKE_INCLUDE_FLAG_SEP_${_language}}${_include}") + else() + list (APPEND ${_cmdVar} "${CMAKE_INCLUDE_FLAG_${_language}}${CMAKE_INCLUDE_FLAG_SEP_${_language}}${_include}") + endif() + endif() + endforeach() + endif() + set (${_cmdVar} ${${_cmdVar}} PARENT_SCOPE) +endfunction() + +function (cotire_add_frameworks_to_cmd _cmdVar _language _includesVar _systemIncludesVar) + if (APPLE) + set (_frameworkDirs "") + foreach (_include ${${_includesVar}}) + if (IS_ABSOLUTE "${_include}" AND _include MATCHES "\\.framework$") + get_filename_component(_frameworkDir "${_include}" DIRECTORY) + list (APPEND _frameworkDirs "${_frameworkDir}") + endif() + endforeach() + set (_systemFrameworkDirs "") + foreach (_include ${${_systemIncludesVar}}) + if (IS_ABSOLUTE "${_include}" AND _include MATCHES "\\.framework$") + get_filename_component(_frameworkDir "${_include}" DIRECTORY) + list (APPEND _systemFrameworkDirs "${_frameworkDir}") + endif() + endforeach() + if (_systemFrameworkDirs) + list (APPEND _frameworkDirs ${_systemFrameworkDirs}) + endif() + if (_frameworkDirs) + list (REMOVE_DUPLICATES _frameworkDirs) + foreach (_frameworkDir ${_frameworkDirs}) + set (_index -1) + if ("${CMAKE_${_language}_SYSTEM_FRAMEWORK_SEARCH_FLAG}" MATCHES ".+") + list (FIND _systemFrameworkDirs "${_frameworkDir}" _index) + endif() + if (_index GREATER -1) + list (APPEND ${_cmdVar} "${CMAKE_${_language}_SYSTEM_FRAMEWORK_SEARCH_FLAG}${_frameworkDir}") + else() + list (APPEND ${_cmdVar} "${CMAKE_${_language}_FRAMEWORK_SEARCH_FLAG}${_frameworkDir}") + endif() + endforeach() + endif() + endif() + set (${_cmdVar} ${${_cmdVar}} PARENT_SCOPE) +endfunction() + +macro (cotire_add_compile_flags_to_cmd _cmdVar) + foreach (_flag ${ARGN}) + list (APPEND ${_cmdVar} "${_flag}") + endforeach() +endmacro() + +function (cotire_check_file_up_to_date _fileIsUpToDateVar _file) + if (EXISTS "${_file}") + set (_triggerFile "") + foreach (_dependencyFile ${ARGN}) + if (EXISTS "${_dependencyFile}") + # IS_NEWER_THAN returns TRUE if both files have the same timestamp + # thus we do the comparison in both directions to exclude ties + if ("${_dependencyFile}" IS_NEWER_THAN "${_file}" AND + NOT "${_file}" IS_NEWER_THAN "${_dependencyFile}") + set (_triggerFile "${_dependencyFile}") + break() + endif() + endif() + endforeach() + if (_triggerFile) + if (COTIRE_VERBOSE) + get_filename_component(_fileName "${_file}" NAME) + message (STATUS "${_fileName} update triggered by ${_triggerFile} change.") + endif() + set (${_fileIsUpToDateVar} FALSE PARENT_SCOPE) + else() + if (COTIRE_VERBOSE) + get_filename_component(_fileName "${_file}" NAME) + message (STATUS "${_fileName} is up-to-date.") + endif() + set (${_fileIsUpToDateVar} TRUE PARENT_SCOPE) + endif() + else() + if (COTIRE_VERBOSE) + get_filename_component(_fileName "${_file}" NAME) + message (STATUS "${_fileName} does not exist yet.") + endif() + set (${_fileIsUpToDateVar} FALSE PARENT_SCOPE) + endif() +endfunction() + +macro (cotire_find_closest_relative_path _headerFile _includeDirs _relPathVar) + set (${_relPathVar} "") + foreach (_includeDir ${_includeDirs}) + if (IS_DIRECTORY "${_includeDir}") + file (RELATIVE_PATH _relPath "${_includeDir}" "${_headerFile}") + if (NOT IS_ABSOLUTE "${_relPath}" AND NOT "${_relPath}" MATCHES "^\\.\\.") + string (LENGTH "${${_relPathVar}}" _closestLen) + string (LENGTH "${_relPath}" _relLen) + if (_closestLen EQUAL 0 OR _relLen LESS _closestLen) + set (${_relPathVar} "${_relPath}") + endif() + endif() + elseif ("${_includeDir}" STREQUAL "${_headerFile}") + # if path matches exactly, return short non-empty string + set (${_relPathVar} "1") + break() + endif() + endforeach() +endmacro() + +macro (cotire_check_header_file_location _headerFile _insideIncludeDirs _outsideIncludeDirs _headerIsInside) + # check header path against ignored and honored include directories + cotire_find_closest_relative_path("${_headerFile}" "${_insideIncludeDirs}" _insideRelPath) + if (_insideRelPath) + # header is inside, but could be become outside if there is a shorter outside match + cotire_find_closest_relative_path("${_headerFile}" "${_outsideIncludeDirs}" _outsideRelPath) + if (_outsideRelPath) + string (LENGTH "${_insideRelPath}" _insideRelPathLen) + string (LENGTH "${_outsideRelPath}" _outsideRelPathLen) + if (_outsideRelPathLen LESS _insideRelPathLen) + set (${_headerIsInside} FALSE) + else() + set (${_headerIsInside} TRUE) + endif() + else() + set (${_headerIsInside} TRUE) + endif() + else() + # header is outside + set (${_headerIsInside} FALSE) + endif() +endmacro() + +macro (cotire_check_ignore_header_file_path _headerFile _headerIsIgnoredVar) + if (NOT EXISTS "${_headerFile}") + set (${_headerIsIgnoredVar} TRUE) + elseif (IS_DIRECTORY "${_headerFile}") + set (${_headerIsIgnoredVar} TRUE) + elseif ("${_headerFile}" MATCHES "\\.\\.|[_-]fixed") + # heuristic: ignore headers with embedded parent directory references or "-fixed" or "_fixed" in path + # these often stem from using GCC #include_next tricks, which may break the precompiled header compilation + # with the error message "error: no include path in which to search for header" + set (${_headerIsIgnoredVar} TRUE) + else() + set (${_headerIsIgnoredVar} FALSE) + endif() +endmacro() + +macro (cotire_check_ignore_header_file_ext _headerFile _ignoreExtensionsVar _headerIsIgnoredVar) + # check header file extension + cotire_get_source_file_extension("${_headerFile}" _headerFileExt) + set (${_headerIsIgnoredVar} FALSE) + if (_headerFileExt) + list (FIND ${_ignoreExtensionsVar} "${_headerFileExt}" _index) + if (_index GREATER -1) + set (${_headerIsIgnoredVar} TRUE) + endif() + endif() +endmacro() + +macro (cotire_parse_line _line _headerFileVar _headerDepthVar) + if (MSVC) + # cl.exe /showIncludes produces different output, depending on the language pack used, e.g.: + # English: "Note: including file: C:\directory\file" + # German: "Hinweis: Einlesen der Datei: C:\directory\file" + # We use a very general regular expression, relying on the presence of the : characters + if (_line MATCHES "( +)([a-zA-Z]:[^:]+)$") + string (LENGTH "${CMAKE_MATCH_1}" ${_headerDepthVar}) + get_filename_component(${_headerFileVar} "${CMAKE_MATCH_2}" ABSOLUTE) + else() + set (${_headerFileVar} "") + set (${_headerDepthVar} 0) + endif() + else() + if (_line MATCHES "^(\\.+) (.*)$") + # GCC like output + string (LENGTH "${CMAKE_MATCH_1}" ${_headerDepthVar}) + if (IS_ABSOLUTE "${CMAKE_MATCH_2}") + set (${_headerFileVar} "${CMAKE_MATCH_2}") + else() + get_filename_component(${_headerFileVar} "${CMAKE_MATCH_2}" REALPATH) + endif() + else() + set (${_headerFileVar} "") + set (${_headerDepthVar} 0) + endif() + endif() +endmacro() + +function (cotire_parse_includes _language _scanOutput _ignoredIncludeDirs _honoredIncludeDirs _ignoredExtensions _selectedIncludesVar _unparsedLinesVar) + if (WIN32) + # prevent CMake macro invocation errors due to backslash characters in Windows paths + string (REPLACE "\\" "/" _scanOutput "${_scanOutput}") + endif() + # canonize slashes + string (REPLACE "//" "/" _scanOutput "${_scanOutput}") + # prevent semicolon from being interpreted as a line separator + string (REPLACE ";" "\\;" _scanOutput "${_scanOutput}") + # then separate lines + string (REGEX REPLACE "\n" ";" _scanOutput "${_scanOutput}") + list (LENGTH _scanOutput _len) + # remove duplicate lines to speed up parsing + list (REMOVE_DUPLICATES _scanOutput) + list (LENGTH _scanOutput _uniqueLen) + if (COTIRE_VERBOSE OR COTIRE_DEBUG) + message (STATUS "Scanning ${_uniqueLen} unique lines of ${_len} for includes") + if (_ignoredExtensions) + message (STATUS "Ignored extensions: ${_ignoredExtensions}") + endif() + if (_ignoredIncludeDirs) + message (STATUS "Ignored paths: ${_ignoredIncludeDirs}") + endif() + if (_honoredIncludeDirs) + message (STATUS "Included paths: ${_honoredIncludeDirs}") + endif() + endif() + set (_sourceFiles ${ARGN}) + set (_selectedIncludes "") + set (_unparsedLines "") + # stack keeps track of inside/outside project status of processed header files + set (_headerIsInsideStack "") + foreach (_line IN LISTS _scanOutput) + if (_line) + cotire_parse_line("${_line}" _headerFile _headerDepth) + if (_headerFile) + cotire_check_header_file_location("${_headerFile}" "${_ignoredIncludeDirs}" "${_honoredIncludeDirs}" _headerIsInside) + if (COTIRE_DEBUG) + message (STATUS "${_headerDepth}: ${_headerFile} ${_headerIsInside}") + endif() + # update stack + list (LENGTH _headerIsInsideStack _stackLen) + if (_headerDepth GREATER _stackLen) + math (EXPR _stackLen "${_stackLen} + 1") + foreach (_index RANGE ${_stackLen} ${_headerDepth}) + list (APPEND _headerIsInsideStack ${_headerIsInside}) + endforeach() + else() + foreach (_index RANGE ${_headerDepth} ${_stackLen}) + list (REMOVE_AT _headerIsInsideStack -1) + endforeach() + list (APPEND _headerIsInsideStack ${_headerIsInside}) + endif() + if (COTIRE_DEBUG) + message (STATUS "${_headerIsInsideStack}") + endif() + # header is a candidate if it is outside project + if (NOT _headerIsInside) + # get parent header file's inside/outside status + if (_headerDepth GREATER 1) + math (EXPR _index "${_headerDepth} - 2") + list (GET _headerIsInsideStack ${_index} _parentHeaderIsInside) + else() + set (_parentHeaderIsInside TRUE) + endif() + # select header file if parent header file is inside project + # (e.g., a project header file that includes a standard header file) + if (_parentHeaderIsInside) + cotire_check_ignore_header_file_path("${_headerFile}" _headerIsIgnored) + if (NOT _headerIsIgnored) + cotire_check_ignore_header_file_ext("${_headerFile}" _ignoredExtensions _headerIsIgnored) + if (NOT _headerIsIgnored) + list (APPEND _selectedIncludes "${_headerFile}") + else() + # fix header's inside status on stack, it is ignored by extension now + list (REMOVE_AT _headerIsInsideStack -1) + list (APPEND _headerIsInsideStack TRUE) + endif() + endif() + if (COTIRE_DEBUG) + message (STATUS "${_headerFile} ${_ignoredExtensions} ${_headerIsIgnored}") + endif() + endif() + endif() + else() + if (MSVC) + # for cl.exe do not keep unparsed lines which solely consist of a source file name + string (FIND "${_sourceFiles}" "${_line}" _index) + if (_index LESS 0) + list (APPEND _unparsedLines "${_line}") + endif() + else() + list (APPEND _unparsedLines "${_line}") + endif() + endif() + endif() + endforeach() + list (REMOVE_DUPLICATES _selectedIncludes) + set (${_selectedIncludesVar} ${_selectedIncludes} PARENT_SCOPE) + set (${_unparsedLinesVar} ${_unparsedLines} PARENT_SCOPE) +endfunction() + +function (cotire_scan_includes _includesVar) + set(_options "") + set(_oneValueArgs COMPILER_ID COMPILER_EXECUTABLE COMPILER_ARG1 COMPILER_VERSION LANGUAGE UNPARSED_LINES SCAN_RESULT) + set(_multiValueArgs COMPILE_DEFINITIONS COMPILE_FLAGS INCLUDE_DIRECTORIES SYSTEM_INCLUDE_DIRECTORIES + IGNORE_PATH INCLUDE_PATH IGNORE_EXTENSIONS INCLUDE_PRIORITY_PATH COMPILER_LAUNCHER) + cmake_parse_arguments(_option "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN}) + set (_sourceFiles ${_option_UNPARSED_ARGUMENTS}) + if (NOT _option_LANGUAGE) + set (_option_LANGUAGE "CXX") + endif() + if (NOT _option_COMPILER_ID) + set (_option_COMPILER_ID "${CMAKE_${_option_LANGUAGE}_ID}") + endif() + if (NOT _option_COMPILER_VERSION) + set (_option_COMPILER_VERSION "${CMAKE_${_option_LANGUAGE}_COMPILER_VERSION}") + endif() + cotire_init_compile_cmd(_cmd "${_option_LANGUAGE}" "${_option_COMPILER_LAUNCHER}" "${_option_COMPILER_EXECUTABLE}" "${_option_COMPILER_ARG1}") + cotire_add_definitions_to_cmd(_cmd "${_option_LANGUAGE}" ${_option_COMPILE_DEFINITIONS}) + cotire_add_compile_flags_to_cmd(_cmd ${_option_COMPILE_FLAGS}) + cotire_add_includes_to_cmd(_cmd "${_option_LANGUAGE}" _option_INCLUDE_DIRECTORIES _option_SYSTEM_INCLUDE_DIRECTORIES) + cotire_add_frameworks_to_cmd(_cmd "${_option_LANGUAGE}" _option_INCLUDE_DIRECTORIES _option_SYSTEM_INCLUDE_DIRECTORIES) + cotire_add_makedep_flags("${_option_LANGUAGE}" "${_option_COMPILER_ID}" "${_option_COMPILER_VERSION}" _cmd) + # only consider existing source files for scanning + set (_existingSourceFiles "") + foreach (_sourceFile ${_sourceFiles}) + if (EXISTS "${_sourceFile}") + list (APPEND _existingSourceFiles "${_sourceFile}") + endif() + endforeach() + if (NOT _existingSourceFiles) + set (${_includesVar} "" PARENT_SCOPE) + return() + endif() + # add source files to be scanned + if (WIN32) + foreach (_sourceFile ${_existingSourceFiles}) + file (TO_NATIVE_PATH "${_sourceFile}" _sourceFileNative) + list (APPEND _cmd "${_sourceFileNative}") + endforeach() + else() + list (APPEND _cmd ${_existingSourceFiles}) + endif() + if (COTIRE_VERBOSE) + message (STATUS "execute_process: ${_cmd}") + endif() + if (MSVC_IDE OR _option_COMPILER_ID MATCHES "MSVC") + # cl.exe messes with the output streams unless the environment variable VS_UNICODE_OUTPUT is cleared + unset (ENV{VS_UNICODE_OUTPUT}) + endif() + execute_process( + COMMAND ${_cmd} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE _result + OUTPUT_QUIET + ERROR_VARIABLE _output) + if (_result) + message (STATUS "Result ${_result} scanning includes of ${_existingSourceFiles}.") + endif() + cotire_parse_includes( + "${_option_LANGUAGE}" "${_output}" + "${_option_IGNORE_PATH}" "${_option_INCLUDE_PATH}" + "${_option_IGNORE_EXTENSIONS}" + _includes _unparsedLines + ${_sourceFiles}) + if (_option_INCLUDE_PRIORITY_PATH) + set (_sortedIncludes "") + foreach (_priorityPath ${_option_INCLUDE_PRIORITY_PATH}) + foreach (_include ${_includes}) + string (FIND ${_include} ${_priorityPath} _position) + if (_position GREATER -1) + list (APPEND _sortedIncludes ${_include}) + endif() + endforeach() + endforeach() + if (_sortedIncludes) + list (INSERT _includes 0 ${_sortedIncludes}) + list (REMOVE_DUPLICATES _includes) + endif() + endif() + set (${_includesVar} ${_includes} PARENT_SCOPE) + if (_option_UNPARSED_LINES) + set (${_option_UNPARSED_LINES} ${_unparsedLines} PARENT_SCOPE) + endif() + if (_option_SCAN_RESULT) + set (${_option_SCAN_RESULT} ${_result} PARENT_SCOPE) + endif() +endfunction() + +macro (cotire_append_undefs _contentsVar) + set (_undefs ${ARGN}) + if (_undefs) + list (REMOVE_DUPLICATES _undefs) + foreach (_definition ${_undefs}) + list (APPEND ${_contentsVar} "#undef ${_definition}") + endforeach() + endif() +endmacro() + +macro (cotire_comment_str _language _commentText _commentVar) + if ("${_language}" STREQUAL "CMAKE") + set (${_commentVar} "# ${_commentText}") + else() + set (${_commentVar} "/* ${_commentText} */") + endif() +endmacro() + +function (cotire_write_file _language _file _contents _force) + get_filename_component(_moduleName "${COTIRE_CMAKE_MODULE_FILE}" NAME) + cotire_comment_str("${_language}" "${_moduleName} ${COTIRE_CMAKE_MODULE_VERSION} generated file" _header1) + cotire_comment_str("${_language}" "${_file}" _header2) + set (_contents "${_header1}\n${_header2}\n${_contents}") + if (COTIRE_DEBUG) + message (STATUS "${_contents}") + endif() + if (_force OR NOT EXISTS "${_file}") + file (WRITE "${_file}" "${_contents}") + else() + file (READ "${_file}" _oldContents) + if (NOT "${_oldContents}" STREQUAL "${_contents}") + file (WRITE "${_file}" "${_contents}") + else() + if (COTIRE_DEBUG) + message (STATUS "${_file} unchanged") + endif() + endif() + endif() +endfunction() + +function (cotire_generate_unity_source _unityFile) + set(_options "") + set(_oneValueArgs LANGUAGE) + set(_multiValueArgs + DEPENDS SOURCES_COMPILE_DEFINITIONS + PRE_UNDEFS SOURCES_PRE_UNDEFS POST_UNDEFS SOURCES_POST_UNDEFS PROLOGUE EPILOGUE) + cmake_parse_arguments(_option "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN}) + if (_option_DEPENDS) + cotire_check_file_up_to_date(_unityFileIsUpToDate "${_unityFile}" ${_option_DEPENDS}) + if (_unityFileIsUpToDate) + return() + endif() + endif() + set (_sourceFiles ${_option_UNPARSED_ARGUMENTS}) + if (NOT _option_PRE_UNDEFS) + set (_option_PRE_UNDEFS "") + endif() + if (NOT _option_SOURCES_PRE_UNDEFS) + set (_option_SOURCES_PRE_UNDEFS "") + endif() + if (NOT _option_POST_UNDEFS) + set (_option_POST_UNDEFS "") + endif() + if (NOT _option_SOURCES_POST_UNDEFS) + set (_option_SOURCES_POST_UNDEFS "") + endif() + set (_contents "") + if (_option_PROLOGUE) + list (APPEND _contents ${_option_PROLOGUE}) + endif() + if (_option_LANGUAGE AND _sourceFiles) + if ("${_option_LANGUAGE}" STREQUAL "CXX") + list (APPEND _contents "#ifdef __cplusplus") + elseif ("${_option_LANGUAGE}" STREQUAL "C") + list (APPEND _contents "#ifndef __cplusplus") + endif() + endif() + set (_compileUndefinitions "") + foreach (_sourceFile ${_sourceFiles}) + cotire_get_source_compile_definitions( + "${_option_CONFIGURATION}" "${_option_LANGUAGE}" "${_sourceFile}" _compileDefinitions + ${_option_SOURCES_COMPILE_DEFINITIONS}) + cotire_get_source_undefs("${_sourceFile}" COTIRE_UNITY_SOURCE_PRE_UNDEFS _sourcePreUndefs ${_option_SOURCES_PRE_UNDEFS}) + cotire_get_source_undefs("${_sourceFile}" COTIRE_UNITY_SOURCE_POST_UNDEFS _sourcePostUndefs ${_option_SOURCES_POST_UNDEFS}) + if (_option_PRE_UNDEFS) + list (APPEND _compileUndefinitions ${_option_PRE_UNDEFS}) + endif() + if (_sourcePreUndefs) + list (APPEND _compileUndefinitions ${_sourcePreUndefs}) + endif() + if (_compileUndefinitions) + cotire_append_undefs(_contents ${_compileUndefinitions}) + set (_compileUndefinitions "") + endif() + if (_sourcePostUndefs) + list (APPEND _compileUndefinitions ${_sourcePostUndefs}) + endif() + if (_option_POST_UNDEFS) + list (APPEND _compileUndefinitions ${_option_POST_UNDEFS}) + endif() + foreach (_definition ${_compileDefinitions}) + if (_definition MATCHES "^([a-zA-Z0-9_]+)=(.+)$") + list (APPEND _contents "#define ${CMAKE_MATCH_1} ${CMAKE_MATCH_2}") + list (INSERT _compileUndefinitions 0 "${CMAKE_MATCH_1}") + else() + list (APPEND _contents "#define ${_definition}") + list (INSERT _compileUndefinitions 0 "${_definition}") + endif() + endforeach() + # use absolute path as source file location + get_filename_component(_sourceFileLocation "${_sourceFile}" ABSOLUTE) + if (WIN32) + file (TO_NATIVE_PATH "${_sourceFileLocation}" _sourceFileLocation) + endif() + list (APPEND _contents "#include \"${_sourceFileLocation}\"") + endforeach() + if (_compileUndefinitions) + cotire_append_undefs(_contents ${_compileUndefinitions}) + set (_compileUndefinitions "") + endif() + if (_option_LANGUAGE AND _sourceFiles) + list (APPEND _contents "#endif") + endif() + if (_option_EPILOGUE) + list (APPEND _contents ${_option_EPILOGUE}) + endif() + list (APPEND _contents "") + string (REPLACE ";" "\n" _contents "${_contents}") + if (COTIRE_VERBOSE) + message ("${_contents}") + endif() + cotire_write_file("${_option_LANGUAGE}" "${_unityFile}" "${_contents}" TRUE) +endfunction() + +function (cotire_generate_prefix_header _prefixFile) + set(_options "") + set(_oneValueArgs LANGUAGE COMPILER_EXECUTABLE COMPILER_ARG1 COMPILER_ID COMPILER_VERSION) + set(_multiValueArgs DEPENDS COMPILE_DEFINITIONS COMPILE_FLAGS + INCLUDE_DIRECTORIES SYSTEM_INCLUDE_DIRECTORIES IGNORE_PATH INCLUDE_PATH + IGNORE_EXTENSIONS INCLUDE_PRIORITY_PATH COMPILER_LAUNCHER) + cmake_parse_arguments(_option "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN}) + if (NOT _option_COMPILER_ID) + set (_option_COMPILER_ID "${CMAKE_${_option_LANGUAGE}_ID}") + endif() + if (NOT _option_COMPILER_VERSION) + set (_option_COMPILER_VERSION "${CMAKE_${_option_LANGUAGE}_COMPILER_VERSION}") + endif() + if (_option_DEPENDS) + cotire_check_file_up_to_date(_prefixFileIsUpToDate "${_prefixFile}" ${_option_DEPENDS}) + if (_prefixFileIsUpToDate) + # create empty log file + set (_unparsedLinesFile "${_prefixFile}.log") + file (WRITE "${_unparsedLinesFile}" "") + return() + endif() + endif() + set (_prologue "") + set (_epilogue "") + if (_option_COMPILER_ID MATCHES "Clang") + set (_prologue "#pragma clang system_header") + elseif (_option_COMPILER_ID MATCHES "GNU") + set (_prologue "#pragma GCC system_header") + elseif (_option_COMPILER_ID MATCHES "MSVC") + set (_prologue "#pragma warning(push, 0)") + set (_epilogue "#pragma warning(pop)") + elseif (_option_COMPILER_ID MATCHES "Intel") + # Intel compiler requires hdrstop pragma to stop generating PCH file + set (_epilogue "#pragma hdrstop") + endif() + set (_sourceFiles ${_option_UNPARSED_ARGUMENTS}) + cotire_scan_includes(_selectedHeaders ${_sourceFiles} + LANGUAGE "${_option_LANGUAGE}" + COMPILER_LAUNCHER "${_option_COMPILER_LAUNCHER}" + COMPILER_EXECUTABLE "${_option_COMPILER_EXECUTABLE}" + COMPILER_ARG1 "${_option_COMPILER_ARG1}" + COMPILER_ID "${_option_COMPILER_ID}" + COMPILER_VERSION "${_option_COMPILER_VERSION}" + COMPILE_DEFINITIONS ${_option_COMPILE_DEFINITIONS} + COMPILE_FLAGS ${_option_COMPILE_FLAGS} + INCLUDE_DIRECTORIES ${_option_INCLUDE_DIRECTORIES} + SYSTEM_INCLUDE_DIRECTORIES ${_option_SYSTEM_INCLUDE_DIRECTORIES} + IGNORE_PATH ${_option_IGNORE_PATH} + INCLUDE_PATH ${_option_INCLUDE_PATH} + IGNORE_EXTENSIONS ${_option_IGNORE_EXTENSIONS} + INCLUDE_PRIORITY_PATH ${_option_INCLUDE_PRIORITY_PATH} + UNPARSED_LINES _unparsedLines + SCAN_RESULT _scanResult) + cotire_generate_unity_source("${_prefixFile}" + PROLOGUE ${_prologue} EPILOGUE ${_epilogue} LANGUAGE "${_option_LANGUAGE}" ${_selectedHeaders}) + set (_unparsedLinesFile "${_prefixFile}.log") + if (_unparsedLines) + if (COTIRE_VERBOSE OR _scanResult OR NOT _selectedHeaders) + list (LENGTH _unparsedLines _skippedLineCount) + if (WIN32) + file (TO_NATIVE_PATH "${_unparsedLinesFile}" _unparsedLinesLogPath) + else() + set (_unparsedLinesLogPath "${_unparsedLinesFile}") + endif() + message (STATUS "${_skippedLineCount} line(s) skipped, see ${_unparsedLinesLogPath}") + endif() + string (REPLACE ";" "\n" _unparsedLines "${_unparsedLines}") + endif() + file (WRITE "${_unparsedLinesFile}" "${_unparsedLines}\n") +endfunction() + +function (cotire_add_makedep_flags _language _compilerID _compilerVersion _flagsVar) + set (_flags ${${_flagsVar}}) + if (_compilerID MATCHES "MSVC") + # cl.exe options used + # /nologo suppresses display of sign-on banner + # /TC treat all files named on the command line as C source files + # /TP treat all files named on the command line as C++ source files + # /EP preprocess to stdout without #line directives + # /showIncludes list include files + set (_sourceFileTypeC "/TC") + set (_sourceFileTypeCXX "/TP") + if (_flags) + # append to list + list (APPEND _flags /nologo "${_sourceFileType${_language}}" /EP /showIncludes) + else() + # return as a flag string + set (_flags "${_sourceFileType${_language}} /EP /showIncludes") + endif() + elseif (_compilerID MATCHES "GNU") + # GCC options used + # -H print the name of each header file used + # -E invoke preprocessor + # -fdirectives-only do not expand macros, requires GCC >= 4.3 + if (_flags) + # append to list + list (APPEND _flags -H -E) + if (NOT "${_compilerVersion}" VERSION_LESS "4.3.0") + list (APPEND _flags -fdirectives-only) + endif() + else() + # return as a flag string + set (_flags "-H -E") + if (NOT "${_compilerVersion}" VERSION_LESS "4.3.0") + set (_flags "${_flags} -fdirectives-only") + endif() + endif() + elseif (_compilerID MATCHES "Clang") + if (UNIX) + # Clang options used + # -H print the name of each header file used + # -E invoke preprocessor + # -fno-color-diagnostics do not print diagnostics in color + # -Eonly just run preprocessor, no output + if (_flags) + # append to list + list (APPEND _flags -H -E -fno-color-diagnostics -Xclang -Eonly) + else() + # return as a flag string + set (_flags "-H -E -fno-color-diagnostics -Xclang -Eonly") + endif() + elseif (WIN32) + # Clang-cl.exe options used + # /TC treat all files named on the command line as C source files + # /TP treat all files named on the command line as C++ source files + # /EP preprocess to stdout without #line directives + # -H print the name of each header file used + # -fno-color-diagnostics do not print diagnostics in color + # -Eonly just run preprocessor, no output + set (_sourceFileTypeC "/TC") + set (_sourceFileTypeCXX "/TP") + if (_flags) + # append to list + list (APPEND _flags "${_sourceFileType${_language}}" /EP -fno-color-diagnostics -Xclang -H -Xclang -Eonly) + else() + # return as a flag string + set (_flags "${_sourceFileType${_language}} /EP -fno-color-diagnostics -Xclang -H -Xclang -Eonly") + endif() + endif() + elseif (_compilerID MATCHES "Intel") + if (WIN32) + # Windows Intel options used + # /nologo do not display compiler version information + # /QH display the include file order + # /EP preprocess to stdout, omitting #line directives + # /TC process all source or unrecognized file types as C source files + # /TP process all source or unrecognized file types as C++ source files + set (_sourceFileTypeC "/TC") + set (_sourceFileTypeCXX "/TP") + if (_flags) + # append to list + list (APPEND _flags /nologo "${_sourceFileType${_language}}" /EP /QH) + else() + # return as a flag string + set (_flags "${_sourceFileType${_language}} /EP /QH") + endif() + else() + # Linux / Mac OS X Intel options used + # -H print the name of each header file used + # -EP preprocess to stdout, omitting #line directives + # -Kc++ process all source or unrecognized file types as C++ source files + if (_flags) + # append to list + if ("${_language}" STREQUAL "CXX") + list (APPEND _flags -Kc++) + endif() + list (APPEND _flags -H -EP) + else() + # return as a flag string + if ("${_language}" STREQUAL "CXX") + set (_flags "-Kc++ ") + endif() + set (_flags "${_flags}-H -EP") + endif() + endif() + else() + message (FATAL_ERROR "cotire: unsupported ${_language} compiler ${_compilerID} version ${_compilerVersion}.") + endif() + set (${_flagsVar} ${_flags} PARENT_SCOPE) +endfunction() + +function (cotire_add_pch_compilation_flags _language _compilerID _compilerVersion _prefixFile _pchFile _hostFile _flagsVar) + set (_flags ${${_flagsVar}}) + if (_compilerID MATCHES "MSVC") + file (TO_NATIVE_PATH "${_prefixFile}" _prefixFileNative) + file (TO_NATIVE_PATH "${_pchFile}" _pchFileNative) + file (TO_NATIVE_PATH "${_hostFile}" _hostFileNative) + # cl.exe options used + # /Yc creates a precompiled header file + # /Fp specifies precompiled header binary file name + # /FI forces inclusion of file + # /TC treat all files named on the command line as C source files + # /TP treat all files named on the command line as C++ source files + # /Zs syntax check only + # /Zm precompiled header memory allocation scaling factor + set (_sourceFileTypeC "/TC") + set (_sourceFileTypeCXX "/TP") + if (_flags) + # append to list + list (APPEND _flags /nologo "${_sourceFileType${_language}}" + "/Yc${_prefixFileNative}" "/Fp${_pchFileNative}" "/FI${_prefixFileNative}" /Zs "${_hostFileNative}") + if (COTIRE_PCH_MEMORY_SCALING_FACTOR) + list (APPEND _flags "/Zm${COTIRE_PCH_MEMORY_SCALING_FACTOR}") + endif() + else() + # return as a flag string + set (_flags "/Yc\"${_prefixFileNative}\" /Fp\"${_pchFileNative}\" /FI\"${_prefixFileNative}\"") + if (COTIRE_PCH_MEMORY_SCALING_FACTOR) + set (_flags "${_flags} /Zm${COTIRE_PCH_MEMORY_SCALING_FACTOR}") + endif() + endif() + elseif (_compilerID MATCHES "GNU") + # GCC options used + # -x specify the source language + # -c compile but do not link + # -o place output in file + # note that we cannot use -w to suppress all warnings upon pre-compiling, because turning off a warning may + # alter compile flags as a side effect (e.g., -Wwrite-string implies -fconst-strings) + set (_xLanguage_C "c-header") + set (_xLanguage_CXX "c++-header") + if (_flags) + # append to list + list (APPEND _flags -x "${_xLanguage_${_language}}" -c "${_prefixFile}" -o "${_pchFile}") + else() + # return as a flag string + set (_flags "-x ${_xLanguage_${_language}} -c \"${_prefixFile}\" -o \"${_pchFile}\"") + endif() + elseif (_compilerID MATCHES "Clang") + if (UNIX) + # Clang options used + # -x specify the source language + # -c compile but do not link + # -o place output in file + # -fno-pch-timestamp disable inclusion of timestamp in precompiled headers (clang 4.0.0+) + set (_xLanguage_C "c-header") + set (_xLanguage_CXX "c++-header") + if (_flags) + # append to list + list (APPEND _flags -x "${_xLanguage_${_language}}" -c "${_prefixFile}" -o "${_pchFile}") + if (NOT "${_compilerVersion}" VERSION_LESS "4.0.0") + list (APPEND _flags -Xclang -fno-pch-timestamp) + endif() + else() + # return as a flag string + set (_flags "-x ${_xLanguage_${_language}} -c \"${_prefixFile}\" -o \"${_pchFile}\"") + if (NOT "${_compilerVersion}" VERSION_LESS "4.0.0") + set (_flags "${_flags} -Xclang -fno-pch-timestamp") + endif() + endif() + elseif (WIN32) + file (TO_NATIVE_PATH "${_prefixFile}" _prefixFileNative) + file (TO_NATIVE_PATH "${_pchFile}" _pchFileNative) + file (TO_NATIVE_PATH "${_hostFile}" _hostFileNative) + # Clang-cl.exe options used + # /Yc creates a precompiled header file + # /Fp specifies precompiled header binary file name + # /FI forces inclusion of file + # /Zs syntax check only + # /TC treat all files named on the command line as C source files + # /TP treat all files named on the command line as C++ source files + set (_sourceFileTypeC "/TC") + set (_sourceFileTypeCXX "/TP") + if (_flags) + # append to list + list (APPEND _flags "${_sourceFileType${_language}}" + "/Yc${_prefixFileNative}" "/Fp${_pchFileNative}" "/FI${_prefixFileNative}" /Zs "${_hostFileNative}") + else() + # return as a flag string + set (_flags "/Yc\"${_prefixFileNative}\" /Fp\"${_pchFileNative}\" /FI\"${_prefixFileNative}\"") + endif() + endif() + elseif (_compilerID MATCHES "Intel") + if (WIN32) + file (TO_NATIVE_PATH "${_prefixFile}" _prefixFileNative) + file (TO_NATIVE_PATH "${_pchFile}" _pchFileNative) + file (TO_NATIVE_PATH "${_hostFile}" _hostFileNative) + # Windows Intel options used + # /nologo do not display compiler version information + # /Yc create a precompiled header (PCH) file + # /Fp specify a path or file name for precompiled header files + # /FI tells the preprocessor to include a specified file name as the header file + # /TC process all source or unrecognized file types as C source files + # /TP process all source or unrecognized file types as C++ source files + # /Zs syntax check only + # /Wpch-messages enable diagnostics related to pre-compiled headers (requires Intel XE 2013 Update 2) + set (_sourceFileTypeC "/TC") + set (_sourceFileTypeCXX "/TP") + if (_flags) + # append to list + list (APPEND _flags /nologo "${_sourceFileType${_language}}" + "/Yc" "/Fp${_pchFileNative}" "/FI${_prefixFileNative}" /Zs "${_hostFileNative}") + if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0") + list (APPEND _flags "/Wpch-messages") + endif() + else() + # return as a flag string + set (_flags "/Yc /Fp\"${_pchFileNative}\" /FI\"${_prefixFileNative}\"") + if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0") + set (_flags "${_flags} /Wpch-messages") + endif() + endif() + else() + # Linux / Mac OS X Intel options used + # -pch-dir location for precompiled header files + # -pch-create name of the precompiled header (PCH) to create + # -Kc++ process all source or unrecognized file types as C++ source files + # -fsyntax-only check only for correct syntax + # -Wpch-messages enable diagnostics related to pre-compiled headers (requires Intel XE 2013 Update 2) + get_filename_component(_pchDir "${_pchFile}" DIRECTORY) + get_filename_component(_pchName "${_pchFile}" NAME) + set (_xLanguage_C "c-header") + set (_xLanguage_CXX "c++-header") + set (_pchSuppressMessages FALSE) + if ("${CMAKE_${_language}_FLAGS}" MATCHES ".*-Wno-pch-messages.*") + set(_pchSuppressMessages TRUE) + endif() + if (_flags) + # append to list + if ("${_language}" STREQUAL "CXX") + list (APPEND _flags -Kc++) + endif() + list (APPEND _flags -include "${_prefixFile}" -pch-dir "${_pchDir}" -pch-create "${_pchName}" -fsyntax-only "${_hostFile}") + if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0") + if (NOT _pchSuppressMessages) + list (APPEND _flags -Wpch-messages) + endif() + endif() + else() + # return as a flag string + set (_flags "-include \"${_prefixFile}\" -pch-dir \"${_pchDir}\" -pch-create \"${_pchName}\"") + if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0") + if (NOT _pchSuppressMessages) + set (_flags "${_flags} -Wpch-messages") + endif() + endif() + endif() + endif() + else() + message (FATAL_ERROR "cotire: unsupported ${_language} compiler ${_compilerID} version ${_compilerVersion}.") + endif() + set (${_flagsVar} ${_flags} PARENT_SCOPE) +endfunction() + +function (cotire_add_prefix_pch_inclusion_flags _language _compilerID _compilerVersion _prefixFile _pchFile _flagsVar) + set (_flags ${${_flagsVar}}) + if (_compilerID MATCHES "MSVC") + file (TO_NATIVE_PATH "${_prefixFile}" _prefixFileNative) + # cl.exe options used + # /Yu uses a precompiled header file during build + # /Fp specifies precompiled header binary file name + # /FI forces inclusion of file + # /Zm precompiled header memory allocation scaling factor + if (_pchFile) + file (TO_NATIVE_PATH "${_pchFile}" _pchFileNative) + if (_flags) + # append to list + list (APPEND _flags "/Yu${_prefixFileNative}" "/Fp${_pchFileNative}" "/FI${_prefixFileNative}") + if (COTIRE_PCH_MEMORY_SCALING_FACTOR) + list (APPEND _flags "/Zm${COTIRE_PCH_MEMORY_SCALING_FACTOR}") + endif() + else() + # return as a flag string + set (_flags "/Yu\"${_prefixFileNative}\" /Fp\"${_pchFileNative}\" /FI\"${_prefixFileNative}\"") + if (COTIRE_PCH_MEMORY_SCALING_FACTOR) + set (_flags "${_flags} /Zm${COTIRE_PCH_MEMORY_SCALING_FACTOR}") + endif() + endif() + else() + # no precompiled header, force inclusion of prefix header + if (_flags) + # append to list + list (APPEND _flags "/FI${_prefixFileNative}") + else() + # return as a flag string + set (_flags "/FI\"${_prefixFileNative}\"") + endif() + endif() + elseif (_compilerID MATCHES "GNU") + # GCC options used + # -include process include file as the first line of the primary source file + # -Winvalid-pch warns if precompiled header is found but cannot be used + # note: ccache requires the -include flag to be used in order to process precompiled header correctly + if (_flags) + # append to list + list (APPEND _flags -Winvalid-pch -include "${_prefixFile}") + else() + # return as a flag string + set (_flags "-Winvalid-pch -include \"${_prefixFile}\"") + endif() + elseif (_compilerID MATCHES "Clang") + if (UNIX) + # Clang options used + # -include process include file as the first line of the primary source file + # note: ccache requires the -include flag to be used in order to process precompiled header correctly + if (_flags) + # append to list + list (APPEND _flags -include "${_prefixFile}") + else() + # return as a flag string + set (_flags "-include \"${_prefixFile}\"") + endif() + elseif (WIN32) + file (TO_NATIVE_PATH "${_prefixFile}" _prefixFileNative) + # Clang-cl.exe options used + # /Yu uses a precompiled header file during build + # /Fp specifies precompiled header binary file name + # /FI forces inclusion of file + if (_pchFile) + file (TO_NATIVE_PATH "${_pchFile}" _pchFileNative) + if (_flags) + # append to list + list (APPEND _flags "/Yu${_prefixFileNative}" "/Fp${_pchFileNative}" "/FI${_prefixFileNative}") + else() + # return as a flag string + set (_flags "/Yu\"${_prefixFileNative}\" /Fp\"${_pchFileNative}\" /FI\"${_prefixFileNative}\"") + endif() + else() + # no precompiled header, force inclusion of prefix header + if (_flags) + # append to list + list (APPEND _flags "/FI${_prefixFileNative}") + else() + # return as a flag string + set (_flags "/FI\"${_prefixFileNative}\"") + endif() + endif() + endif() + elseif (_compilerID MATCHES "Intel") + if (WIN32) + file (TO_NATIVE_PATH "${_prefixFile}" _prefixFileNative) + # Windows Intel options used + # /Yu use a precompiled header (PCH) file + # /Fp specify a path or file name for precompiled header files + # /FI tells the preprocessor to include a specified file name as the header file + # /Wpch-messages enable diagnostics related to pre-compiled headers (requires Intel XE 2013 Update 2) + if (_pchFile) + file (TO_NATIVE_PATH "${_pchFile}" _pchFileNative) + if (_flags) + # append to list + list (APPEND _flags "/Yu" "/Fp${_pchFileNative}" "/FI${_prefixFileNative}") + if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0") + list (APPEND _flags "/Wpch-messages") + endif() + else() + # return as a flag string + set (_flags "/Yu /Fp\"${_pchFileNative}\" /FI\"${_prefixFileNative}\"") + if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0") + set (_flags "${_flags} /Wpch-messages") + endif() + endif() + else() + # no precompiled header, force inclusion of prefix header + if (_flags) + # append to list + list (APPEND _flags "/FI${_prefixFileNative}") + else() + # return as a flag string + set (_flags "/FI\"${_prefixFileNative}\"") + endif() + endif() + else() + # Linux / Mac OS X Intel options used + # -pch-dir location for precompiled header files + # -pch-use name of the precompiled header (PCH) to use + # -include process include file as the first line of the primary source file + # -Wpch-messages enable diagnostics related to pre-compiled headers (requires Intel XE 2013 Update 2) + if (_pchFile) + get_filename_component(_pchDir "${_pchFile}" DIRECTORY) + get_filename_component(_pchName "${_pchFile}" NAME) + set (_pchSuppressMessages FALSE) + if ("${CMAKE_${_language}_FLAGS}" MATCHES ".*-Wno-pch-messages.*") + set(_pchSuppressMessages TRUE) + endif() + if (_flags) + # append to list + list (APPEND _flags -include "${_prefixFile}" -pch-dir "${_pchDir}" -pch-use "${_pchName}") + if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0") + if (NOT _pchSuppressMessages) + list (APPEND _flags -Wpch-messages) + endif() + endif() + else() + # return as a flag string + set (_flags "-include \"${_prefixFile}\" -pch-dir \"${_pchDir}\" -pch-use \"${_pchName}\"") + if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0") + if (NOT _pchSuppressMessages) + set (_flags "${_flags} -Wpch-messages") + endif() + endif() + endif() + else() + # no precompiled header, force inclusion of prefix header + if (_flags) + # append to list + list (APPEND _flags -include "${_prefixFile}") + else() + # return as a flag string + set (_flags "-include \"${_prefixFile}\"") + endif() + endif() + endif() + else() + message (FATAL_ERROR "cotire: unsupported ${_language} compiler ${_compilerID} version ${_compilerVersion}.") + endif() + set (${_flagsVar} ${_flags} PARENT_SCOPE) +endfunction() + +function (cotire_precompile_prefix_header _prefixFile _pchFile _hostFile) + set(_options "") + set(_oneValueArgs COMPILER_EXECUTABLE COMPILER_ARG1 COMPILER_ID COMPILER_VERSION LANGUAGE) + set(_multiValueArgs COMPILE_DEFINITIONS COMPILE_FLAGS INCLUDE_DIRECTORIES SYSTEM_INCLUDE_DIRECTORIES SYS COMPILER_LAUNCHER) + cmake_parse_arguments(_option "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN}) + if (NOT _option_LANGUAGE) + set (_option_LANGUAGE "CXX") + endif() + if (NOT _option_COMPILER_ID) + set (_option_COMPILER_ID "${CMAKE_${_option_LANGUAGE}_ID}") + endif() + if (NOT _option_COMPILER_VERSION) + set (_option_COMPILER_VERSION "${CMAKE_${_option_LANGUAGE}_COMPILER_VERSION}") + endif() + cotire_init_compile_cmd(_cmd "${_option_LANGUAGE}" "${_option_COMPILER_LAUNCHER}" "${_option_COMPILER_EXECUTABLE}" "${_option_COMPILER_ARG1}") + cotire_add_definitions_to_cmd(_cmd "${_option_LANGUAGE}" ${_option_COMPILE_DEFINITIONS}) + cotire_add_compile_flags_to_cmd(_cmd ${_option_COMPILE_FLAGS}) + cotire_add_includes_to_cmd(_cmd "${_option_LANGUAGE}" _option_INCLUDE_DIRECTORIES _option_SYSTEM_INCLUDE_DIRECTORIES) + cotire_add_frameworks_to_cmd(_cmd "${_option_LANGUAGE}" _option_INCLUDE_DIRECTORIES _option_SYSTEM_INCLUDE_DIRECTORIES) + cotire_add_pch_compilation_flags( + "${_option_LANGUAGE}" "${_option_COMPILER_ID}" "${_option_COMPILER_VERSION}" + "${_prefixFile}" "${_pchFile}" "${_hostFile}" _cmd) + if (COTIRE_VERBOSE) + message (STATUS "execute_process: ${_cmd}") + endif() + if (MSVC_IDE OR _option_COMPILER_ID MATCHES "MSVC") + # cl.exe messes with the output streams unless the environment variable VS_UNICODE_OUTPUT is cleared + unset (ENV{VS_UNICODE_OUTPUT}) + elseif (_option_COMPILER_ID MATCHES "Clang" AND _option_COMPILER_VERSION VERSION_LESS "4.0.0") + if (_option_COMPILER_LAUNCHER MATCHES "ccache" OR + _option_COMPILER_EXECUTABLE MATCHES "ccache") + # Newer versions of Clang embed a compilation timestamp into the precompiled header binary, + # which results in "file has been modified since the precompiled header was built" errors if ccache is used. + # We work around the problem by disabling ccache upon pre-compiling the prefix header. + set (ENV{CCACHE_DISABLE} "true") + endif() + endif() + execute_process( + COMMAND ${_cmd} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE _result) + if (_result) + message (FATAL_ERROR "cotire: error ${_result} precompiling ${_prefixFile}.") + endif() +endfunction() + +function (cotire_check_precompiled_header_support _language _target _msgVar) + set (_unsupportedCompiler + "Precompiled headers not supported for ${_language} compiler ${CMAKE_${_language}_COMPILER_ID}") + if (CMAKE_${_language}_COMPILER_ID MATCHES "MSVC") + # PCH supported since Visual Studio C++ 6.0 + # and CMake does not support an earlier version + set (${_msgVar} "" PARENT_SCOPE) + elseif (CMAKE_${_language}_COMPILER_ID MATCHES "GNU") + # GCC PCH support requires version >= 3.4 + if ("${CMAKE_${_language}_COMPILER_VERSION}" VERSION_LESS "3.4.0") + set (${_msgVar} "${_unsupportedCompiler} version ${CMAKE_${_language}_COMPILER_VERSION}." PARENT_SCOPE) + else() + set (${_msgVar} "" PARENT_SCOPE) + endif() + elseif (CMAKE_${_language}_COMPILER_ID MATCHES "Clang") + if (UNIX) + # all Unix Clang versions have PCH support + set (${_msgVar} "" PARENT_SCOPE) + elseif (WIN32) + # only clang-cl is supported under Windows + get_filename_component(_compilerName "${CMAKE_${_language}_COMPILER}" NAME_WE) + if (NOT _compilerName MATCHES "cl$") + set (${_msgVar} "${_unsupportedCompiler} version ${CMAKE_${_language}_COMPILER_VERSION}. Use clang-cl instead." PARENT_SCOPE) + endif() + endif() + elseif (CMAKE_${_language}_COMPILER_ID MATCHES "Intel") + # Intel PCH support requires version >= 8.0.0 + if ("${CMAKE_${_language}_COMPILER_VERSION}" VERSION_LESS "8.0.0") + set (${_msgVar} "${_unsupportedCompiler} version ${CMAKE_${_language}_COMPILER_VERSION}." PARENT_SCOPE) + else() + set (${_msgVar} "" PARENT_SCOPE) + endif() + else() + set (${_msgVar} "${_unsupportedCompiler}." PARENT_SCOPE) + endif() + # check if ccache is used as a compiler launcher + get_target_property(_launcher ${_target} ${_language}_COMPILER_LAUNCHER) + get_filename_component(_realCompilerExe "${CMAKE_${_language}_COMPILER}" REALPATH) + if (_realCompilerExe MATCHES "ccache" OR _launcher MATCHES "ccache") + # verify that ccache configuration is compatible with precompiled headers + # always check environment variable CCACHE_SLOPPINESS, because earlier versions of ccache + # do not report the "sloppiness" setting correctly upon printing ccache configuration + if (DEFINED ENV{CCACHE_SLOPPINESS}) + if (NOT "$ENV{CCACHE_SLOPPINESS}" MATCHES "pch_defines" OR + NOT "$ENV{CCACHE_SLOPPINESS}" MATCHES "time_macros") + set (${_msgVar} + "ccache requires the environment variable CCACHE_SLOPPINESS to be set to \"pch_defines,time_macros\"." + PARENT_SCOPE) + endif() + else() + if (_realCompilerExe MATCHES "ccache") + set (_ccacheExe "${_realCompilerExe}") + else() + set (_ccacheExe "${_launcher}") + endif() + # ccache 3.7.0 replaced --print-config with --show-config + # use -p instead, which seems to work for all version for now, sigh + execute_process( + COMMAND "${_ccacheExe}" "-p" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + RESULT_VARIABLE _result + OUTPUT_VARIABLE _ccacheConfig OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if (_result) + set (${_msgVar} "ccache configuration cannot be determined." PARENT_SCOPE) + elseif (NOT _ccacheConfig MATCHES "sloppiness.*=.*time_macros" OR + NOT _ccacheConfig MATCHES "sloppiness.*=.*pch_defines") + set (${_msgVar} + "ccache requires configuration setting \"sloppiness\" to be set to \"pch_defines,time_macros\"." + PARENT_SCOPE) + endif() + endif() + endif() + if (APPLE) + # PCH compilation not supported by GCC / Clang for multi-architecture builds (e.g., i386, x86_64) + cotire_get_configuration_types(_configs) + foreach (_config ${_configs}) + set (_targetFlags "") + cotire_get_target_compile_flags("${_config}" "${_language}" "${_target}" _targetFlags) + cotire_filter_compile_flags("${_language}" "arch" _architectures _ignore ${_targetFlags}) + list (LENGTH _architectures _numberOfArchitectures) + if (_numberOfArchitectures GREATER 1) + string (REPLACE ";" ", " _architectureStr "${_architectures}") + set (${_msgVar} + "Precompiled headers not supported on Darwin for multi-architecture builds (${_architectureStr})." + PARENT_SCOPE) + break() + endif() + endforeach() + endif() +endfunction() + +macro (cotire_get_intermediate_dir _cotireDir) + # ${CMAKE_CFG_INTDIR} may reference a build-time variable when using a generator which supports configuration types + get_filename_component(${_cotireDir} "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${COTIRE_INTDIR}" ABSOLUTE) +endmacro() + +macro (cotire_setup_file_extension_variables) + set (_unityFileExt_C ".c") + set (_unityFileExt_CXX ".cxx") + set (_prefixFileExt_C ".h") + set (_prefixFileExt_CXX ".hxx") + set (_prefixSourceFileExt_C ".c") + set (_prefixSourceFileExt_CXX ".cxx") +endmacro() + +function (cotire_make_single_unity_source_file_path _language _target _unityFileVar) + cotire_setup_file_extension_variables() + if (NOT DEFINED _unityFileExt_${_language}) + set (${_unityFileVar} "" PARENT_SCOPE) + return() + endif() + set (_unityFileBaseName "${_target}_${_language}${COTIRE_UNITY_SOURCE_FILENAME_SUFFIX}") + set (_unityFileName "${_unityFileBaseName}${_unityFileExt_${_language}}") + cotire_get_intermediate_dir(_baseDir) + set (_unityFile "${_baseDir}/${_unityFileName}") + set (${_unityFileVar} "${_unityFile}" PARENT_SCOPE) +endfunction() + +function (cotire_make_unity_source_file_paths _language _target _maxIncludes _unityFilesVar) + cotire_setup_file_extension_variables() + if (NOT DEFINED _unityFileExt_${_language}) + set (${_unityFileVar} "" PARENT_SCOPE) + return() + endif() + set (_unityFileBaseName "${_target}_${_language}${COTIRE_UNITY_SOURCE_FILENAME_SUFFIX}") + cotire_get_intermediate_dir(_baseDir) + set (_startIndex 0) + set (_index 0) + set (_unityFiles "") + set (_sourceFiles ${ARGN}) + foreach (_sourceFile ${_sourceFiles}) + get_source_file_property(_startNew "${_sourceFile}" COTIRE_START_NEW_UNITY_SOURCE) + math (EXPR _unityFileCount "${_index} - ${_startIndex}") + if (_startNew OR (_maxIncludes GREATER 0 AND NOT _unityFileCount LESS _maxIncludes)) + if (_index GREATER 0) + # start new unity file segment + math (EXPR _endIndex "${_index} - 1") + set (_unityFileName "${_unityFileBaseName}_${_startIndex}_${_endIndex}${_unityFileExt_${_language}}") + list (APPEND _unityFiles "${_baseDir}/${_unityFileName}") + endif() + set (_startIndex ${_index}) + endif() + math (EXPR _index "${_index} + 1") + endforeach() + list (LENGTH _sourceFiles _numberOfSources) + if (_startIndex EQUAL 0) + # there is only a single unity file + cotire_make_single_unity_source_file_path(${_language} ${_target} _unityFiles) + elseif (_startIndex LESS _numberOfSources) + # end with final unity file segment + math (EXPR _endIndex "${_index} - 1") + set (_unityFileName "${_unityFileBaseName}_${_startIndex}_${_endIndex}${_unityFileExt_${_language}}") + list (APPEND _unityFiles "${_baseDir}/${_unityFileName}") + endif() + set (${_unityFilesVar} ${_unityFiles} PARENT_SCOPE) + if (COTIRE_DEBUG AND _unityFiles) + message (STATUS "unity files: ${_unityFiles}") + endif() +endfunction() + +function (cotire_unity_to_prefix_file_path _language _target _unityFile _prefixFileVar) + cotire_setup_file_extension_variables() + if (NOT DEFINED _unityFileExt_${_language}) + set (${_prefixFileVar} "" PARENT_SCOPE) + return() + endif() + set (_unityFileBaseName "${_target}_${_language}${COTIRE_UNITY_SOURCE_FILENAME_SUFFIX}") + set (_prefixFileBaseName "${_target}_${_language}${COTIRE_PREFIX_HEADER_FILENAME_SUFFIX}") + string (REPLACE "${_unityFileBaseName}" "${_prefixFileBaseName}" _prefixFile "${_unityFile}") + string (REGEX REPLACE "${_unityFileExt_${_language}}$" "${_prefixFileExt_${_language}}" _prefixFile "${_prefixFile}") + set (${_prefixFileVar} "${_prefixFile}" PARENT_SCOPE) +endfunction() + +function (cotire_prefix_header_to_source_file_path _language _prefixHeaderFile _prefixSourceFileVar) + cotire_setup_file_extension_variables() + if (NOT DEFINED _prefixSourceFileExt_${_language}) + set (${_prefixSourceFileVar} "" PARENT_SCOPE) + return() + endif() + string (REGEX REPLACE "${_prefixFileExt_${_language}}$" "${_prefixSourceFileExt_${_language}}" _prefixSourceFile "${_prefixHeaderFile}") + set (${_prefixSourceFileVar} "${_prefixSourceFile}" PARENT_SCOPE) +endfunction() + +function (cotire_make_prefix_file_name _language _target _prefixFileBaseNameVar _prefixFileNameVar) + cotire_setup_file_extension_variables() + if (NOT _language) + set (_prefixFileBaseName "${_target}${COTIRE_PREFIX_HEADER_FILENAME_SUFFIX}") + set (_prefixFileName "${_prefixFileBaseName}${_prefixFileExt_C}") + elseif (DEFINED _prefixFileExt_${_language}) + set (_prefixFileBaseName "${_target}_${_language}${COTIRE_PREFIX_HEADER_FILENAME_SUFFIX}") + set (_prefixFileName "${_prefixFileBaseName}${_prefixFileExt_${_language}}") + else() + set (_prefixFileBaseName "") + set (_prefixFileName "") + endif() + set (${_prefixFileBaseNameVar} "${_prefixFileBaseName}" PARENT_SCOPE) + set (${_prefixFileNameVar} "${_prefixFileName}" PARENT_SCOPE) +endfunction() + +function (cotire_make_prefix_file_path _language _target _prefixFileVar) + cotire_make_prefix_file_name("${_language}" "${_target}" _prefixFileBaseName _prefixFileName) + set (${_prefixFileVar} "" PARENT_SCOPE) + if (_prefixFileName) + if (NOT _language) + set (_language "C") + endif() + if (CMAKE_${_language}_COMPILER_ID MATCHES "GNU|Clang|Intel|MSVC") + cotire_get_intermediate_dir(_baseDir) + set (${_prefixFileVar} "${_baseDir}/${_prefixFileName}" PARENT_SCOPE) + endif() + endif() +endfunction() + +function (cotire_make_pch_file_path _language _target _pchFileVar) + cotire_make_prefix_file_name("${_language}" "${_target}" _prefixFileBaseName _prefixFileName) + set (${_pchFileVar} "" PARENT_SCOPE) + if (_prefixFileBaseName AND _prefixFileName) + cotire_check_precompiled_header_support("${_language}" "${_target}" _msg) + if (NOT _msg) + if (XCODE) + # For Xcode, we completely hand off the compilation of the prefix header to the IDE + return() + endif() + cotire_get_intermediate_dir(_baseDir) + if (CMAKE_${_language}_COMPILER_ID MATCHES "MSVC") + # MSVC uses the extension .pch added to the prefix header base name + set (${_pchFileVar} "${_baseDir}/${_prefixFileBaseName}.pch" PARENT_SCOPE) + elseif (CMAKE_${_language}_COMPILER_ID MATCHES "Clang") + # Clang looks for a precompiled header corresponding to the prefix header with the extension .pch appended + set (${_pchFileVar} "${_baseDir}/${_prefixFileName}.pch" PARENT_SCOPE) + elseif (CMAKE_${_language}_COMPILER_ID MATCHES "GNU") + # GCC looks for a precompiled header corresponding to the prefix header with the extension .gch appended + set (${_pchFileVar} "${_baseDir}/${_prefixFileName}.gch" PARENT_SCOPE) + elseif (CMAKE_${_language}_COMPILER_ID MATCHES "Intel") + # Intel uses the extension .pchi added to the prefix header base name + set (${_pchFileVar} "${_baseDir}/${_prefixFileBaseName}.pchi" PARENT_SCOPE) + endif() + endif() + endif() +endfunction() + +function (cotire_select_unity_source_files _unityFile _sourcesVar) + set (_sourceFiles ${ARGN}) + if (_sourceFiles AND "${_unityFile}" MATCHES "${COTIRE_UNITY_SOURCE_FILENAME_SUFFIX}_([0-9]+)_([0-9]+)") + set (_startIndex ${CMAKE_MATCH_1}) + set (_endIndex ${CMAKE_MATCH_2}) + list (LENGTH _sourceFiles _numberOfSources) + if (NOT _startIndex LESS _numberOfSources) + math (EXPR _startIndex "${_numberOfSources} - 1") + endif() + if (NOT _endIndex LESS _numberOfSources) + math (EXPR _endIndex "${_numberOfSources} - 1") + endif() + set (_files "") + foreach (_index RANGE ${_startIndex} ${_endIndex}) + list (GET _sourceFiles ${_index} _file) + list (APPEND _files "${_file}") + endforeach() + else() + set (_files ${_sourceFiles}) + endif() + set (${_sourcesVar} ${_files} PARENT_SCOPE) +endfunction() + +function (cotire_get_unity_source_dependencies _language _target _dependencySourcesVar) + set (_dependencySources "") + # depend on target's generated source files + get_target_property(_targetSourceFiles ${_target} SOURCES) + cotire_get_objects_with_property_on(_generatedSources GENERATED SOURCE ${_targetSourceFiles}) + if (_generatedSources) + # but omit all generated source files that have the COTIRE_EXCLUDED property set to true + cotire_get_objects_with_property_on(_excludedGeneratedSources COTIRE_EXCLUDED SOURCE ${_generatedSources}) + if (_excludedGeneratedSources) + list (REMOVE_ITEM _generatedSources ${_excludedGeneratedSources}) + endif() + # and omit all generated source files that have the COTIRE_DEPENDENCY property set to false explicitly + cotire_get_objects_with_property_off(_excludedNonDependencySources COTIRE_DEPENDENCY SOURCE ${_generatedSources}) + if (_excludedNonDependencySources) + list (REMOVE_ITEM _generatedSources ${_excludedNonDependencySources}) + endif() + if (_generatedSources) + list (APPEND _dependencySources ${_generatedSources}) + endif() + endif() + if (COTIRE_DEBUG AND _dependencySources) + message (STATUS "${_language} ${_target} unity source dependencies: ${_dependencySources}") + endif() + set (${_dependencySourcesVar} ${_dependencySources} PARENT_SCOPE) +endfunction() + +function (cotire_get_prefix_header_dependencies _language _target _dependencySourcesVar) + set (_dependencySources "") + # depend on target source files marked with custom COTIRE_DEPENDENCY property + get_target_property(_targetSourceFiles ${_target} SOURCES) + cotire_get_objects_with_property_on(_dependencySources COTIRE_DEPENDENCY SOURCE ${_targetSourceFiles}) + if (COTIRE_DEBUG AND _dependencySources) + message (STATUS "${_language} ${_target} prefix header dependencies: ${_dependencySources}") + endif() + set (${_dependencySourcesVar} ${_dependencySources} PARENT_SCOPE) +endfunction() + +function (cotire_generate_target_script _language _configurations _target _targetScriptVar _targetConfigScriptVar) + set (_targetSources ${ARGN}) + cotire_get_prefix_header_dependencies(${_language} ${_target} COTIRE_TARGET_PREFIX_DEPENDS ${_targetSources}) + cotire_get_unity_source_dependencies(${_language} ${_target} COTIRE_TARGET_UNITY_DEPENDS ${_targetSources}) + # set up variables to be configured + set (COTIRE_TARGET_LANGUAGE "${_language}") + get_target_property(COTIRE_TARGET_IGNORE_PATH ${_target} COTIRE_PREFIX_HEADER_IGNORE_PATH) + cotire_add_sys_root_paths(COTIRE_TARGET_IGNORE_PATH) + get_target_property(COTIRE_TARGET_INCLUDE_PATH ${_target} COTIRE_PREFIX_HEADER_INCLUDE_PATH) + cotire_add_sys_root_paths(COTIRE_TARGET_INCLUDE_PATH) + get_target_property(COTIRE_TARGET_PRE_UNDEFS ${_target} COTIRE_UNITY_SOURCE_PRE_UNDEFS) + get_target_property(COTIRE_TARGET_POST_UNDEFS ${_target} COTIRE_UNITY_SOURCE_POST_UNDEFS) + get_target_property(COTIRE_TARGET_MAXIMUM_NUMBER_OF_INCLUDES ${_target} COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES) + get_target_property(COTIRE_TARGET_INCLUDE_PRIORITY_PATH ${_target} COTIRE_PREFIX_HEADER_INCLUDE_PRIORITY_PATH) + cotire_get_source_files_undefs(COTIRE_UNITY_SOURCE_PRE_UNDEFS COTIRE_TARGET_SOURCES_PRE_UNDEFS ${_targetSources}) + cotire_get_source_files_undefs(COTIRE_UNITY_SOURCE_POST_UNDEFS COTIRE_TARGET_SOURCES_POST_UNDEFS ${_targetSources}) + set (COTIRE_TARGET_CONFIGURATION_TYPES "${_configurations}") + foreach (_config ${_configurations}) + string (TOUPPER "${_config}" _upperConfig) + cotire_get_target_include_directories( + "${_config}" "${_language}" "${_target}" COTIRE_TARGET_INCLUDE_DIRECTORIES_${_upperConfig} COTIRE_TARGET_SYSTEM_INCLUDE_DIRECTORIES_${_upperConfig}) + cotire_get_target_compile_definitions( + "${_config}" "${_language}" "${_target}" COTIRE_TARGET_COMPILE_DEFINITIONS_${_upperConfig}) + cotire_get_target_compiler_flags( + "${_config}" "${_language}" "${_target}" COTIRE_TARGET_COMPILE_FLAGS_${_upperConfig}) + cotire_get_source_files_compile_definitions( + "${_config}" "${_language}" COTIRE_TARGET_SOURCES_COMPILE_DEFINITIONS_${_upperConfig} ${_targetSources}) + endforeach() + get_target_property(COTIRE_TARGET_${_language}_COMPILER_LAUNCHER ${_target} ${_language}_COMPILER_LAUNCHER) + # set up COTIRE_TARGET_SOURCES + set (COTIRE_TARGET_SOURCES "") + foreach (_sourceFile ${_targetSources}) + get_source_file_property(_generated "${_sourceFile}" GENERATED) + if (_generated) + # use absolute paths for generated files only, retrieving the LOCATION property is an expensive operation + get_source_file_property(_sourceLocation "${_sourceFile}" LOCATION) + list (APPEND COTIRE_TARGET_SOURCES "${_sourceLocation}") + else() + list (APPEND COTIRE_TARGET_SOURCES "${_sourceFile}") + endif() + endforeach() + # copy variable definitions to cotire target script + get_cmake_property(_vars VARIABLES) + string (REGEX MATCHALL "COTIRE_[A-Za-z0-9_]+" _matchVars "${_vars}") + # omit COTIRE_*_INIT variables + string (REGEX MATCHALL "COTIRE_[A-Za-z0-9_]+_INIT" _initVars "${_matchVars}") + if (_initVars) + list (REMOVE_ITEM _matchVars ${_initVars}) + endif() + # omit COTIRE_VERBOSE which is passed as a CMake define on command line + list (REMOVE_ITEM _matchVars COTIRE_VERBOSE) + set (_contents "") + set (_contentsHasGeneratorExpressions FALSE) + foreach (_var IN LISTS _matchVars ITEMS + XCODE MSVC CMAKE_GENERATOR CMAKE_BUILD_TYPE CMAKE_CONFIGURATION_TYPES + CMAKE_${_language}_COMPILER_ID CMAKE_${_language}_COMPILER_VERSION + CMAKE_${_language}_COMPILER_LAUNCHER CMAKE_${_language}_COMPILER CMAKE_${_language}_COMPILER_ARG1 + CMAKE_INCLUDE_FLAG_${_language} CMAKE_INCLUDE_FLAG_SEP_${_language} + CMAKE_INCLUDE_SYSTEM_FLAG_${_language} + CMAKE_${_language}_FRAMEWORK_SEARCH_FLAG + CMAKE_${_language}_SYSTEM_FRAMEWORK_SEARCH_FLAG + CMAKE_${_language}_SOURCE_FILE_EXTENSIONS) + if (DEFINED ${_var}) + string (REPLACE "\"" "\\\"" _value "${${_var}}") + set (_contents "${_contents}set (${_var} \"${_value}\")\n") + if (NOT _contentsHasGeneratorExpressions) + if ("${_value}" MATCHES "\\$<.*>") + set (_contentsHasGeneratorExpressions TRUE) + endif() + endif() + endif() + endforeach() + # generate target script file + get_filename_component(_moduleName "${COTIRE_CMAKE_MODULE_FILE}" NAME) + set (_targetCotireScript "${CMAKE_CURRENT_BINARY_DIR}/${_target}_${_language}_${_moduleName}") + cotire_write_file("CMAKE" "${_targetCotireScript}" "${_contents}" FALSE) + if (_contentsHasGeneratorExpressions) + # use file(GENERATE ...) to expand generator expressions in the target script at CMake generate-time + set (_configNameOrNoneGeneratorExpression "$<$:None>$<$>:$>") + set (_targetCotireConfigScript "${CMAKE_CURRENT_BINARY_DIR}/${_target}_${_language}_${_configNameOrNoneGeneratorExpression}_${_moduleName}") + file (GENERATE OUTPUT "${_targetCotireConfigScript}" INPUT "${_targetCotireScript}") + else() + set (_targetCotireConfigScript "${_targetCotireScript}") + endif() + set (${_targetScriptVar} "${_targetCotireScript}" PARENT_SCOPE) + set (${_targetConfigScriptVar} "${_targetCotireConfigScript}" PARENT_SCOPE) +endfunction() + +function (cotire_setup_pch_file_compilation _language _target _targetScript _prefixFile _pchFile _hostFile) + set (_sourceFiles ${ARGN}) + if (CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel" OR + (WIN32 AND CMAKE_${_language}_COMPILER_ID MATCHES "Clang")) + # for MSVC, Intel and Clang-cl, we attach the precompiled header compilation to the host file + # the remaining files include the precompiled header, see cotire_setup_pch_file_inclusion + if (_sourceFiles) + set (_flags "") + cotire_add_pch_compilation_flags( + "${_language}" "${CMAKE_${_language}_COMPILER_ID}" "${CMAKE_${_language}_COMPILER_VERSION}" + "${_prefixFile}" "${_pchFile}" "${_hostFile}" _flags) + set_property (SOURCE ${_hostFile} APPEND_STRING PROPERTY COMPILE_FLAGS " ${_flags} ") + if (COTIRE_DEBUG) + message (STATUS "set_property: SOURCE ${_hostFile} APPEND_STRING COMPILE_FLAGS ${_flags}") + endif() + set_property (SOURCE ${_hostFile} APPEND PROPERTY OBJECT_OUTPUTS "${_pchFile}") + # make object file generated from host file depend on prefix header + set_property (SOURCE ${_hostFile} APPEND PROPERTY OBJECT_DEPENDS "${_prefixFile}") + # mark host file as cotired to prevent it from being used in another cotired target + set_property (SOURCE ${_hostFile} PROPERTY COTIRE_TARGET "${_target}") + endif() + elseif ("${CMAKE_GENERATOR}" MATCHES "Make|Ninja") + # for makefile based generator, we add a custom command to precompile the prefix header + if (_targetScript) + cotire_set_cmd_to_prologue(_cmds) + list (APPEND _cmds -P "${COTIRE_CMAKE_MODULE_FILE}" "precompile" "${_targetScript}" "${_prefixFile}" "${_pchFile}" "${_hostFile}") + if (MSVC_IDE) + file (TO_NATIVE_PATH "${_pchFile}" _pchFileLogPath) + else() + file (RELATIVE_PATH _pchFileLogPath "${CMAKE_BINARY_DIR}" "${_pchFile}") + endif() + # make precompiled header compilation depend on the actual compiler executable used to force + # re-compilation when the compiler executable is updated. This prevents "created by a different GCC executable" + # warnings when the precompiled header is included. + get_filename_component(_realCompilerExe "${CMAKE_${_language}_COMPILER}" ABSOLUTE) + if (COTIRE_DEBUG) + message (STATUS "add_custom_command: OUTPUT ${_pchFile} ${_cmds} DEPENDS ${_prefixFile} ${_realCompilerExe} IMPLICIT_DEPENDS ${_language} ${_prefixFile}") + endif() + set_property (SOURCE "${_pchFile}" PROPERTY GENERATED TRUE) + add_custom_command( + OUTPUT "${_pchFile}" + COMMAND ${_cmds} + DEPENDS "${_prefixFile}" "${_realCompilerExe}" + IMPLICIT_DEPENDS ${_language} "${_prefixFile}" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Building ${_language} precompiled header ${_pchFileLogPath}" + VERBATIM) + endif() + endif() +endfunction() + +function (cotire_setup_pch_file_inclusion _language _target _wholeTarget _prefixFile _pchFile _hostFile) + if (CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel" OR + (WIN32 AND CMAKE_${_language}_COMPILER_ID MATCHES "Clang")) + # for MSVC, Intel and clang-cl, we include the precompiled header in all but the host file + # the host file does the precompiled header compilation, see cotire_setup_pch_file_compilation + set (_sourceFiles ${ARGN}) + list (LENGTH _sourceFiles _numberOfSourceFiles) + if (_numberOfSourceFiles GREATER 0) + # mark sources as cotired to prevent them from being used in another cotired target + set_source_files_properties(${_sourceFiles} PROPERTIES COTIRE_TARGET "${_target}") + set (_flags "") + cotire_add_prefix_pch_inclusion_flags( + "${_language}" "${CMAKE_${_language}_COMPILER_ID}" "${CMAKE_${_language}_COMPILER_VERSION}" + "${_prefixFile}" "${_pchFile}" _flags) + set_property (SOURCE ${_sourceFiles} APPEND_STRING PROPERTY COMPILE_FLAGS " ${_flags} ") + if (COTIRE_DEBUG) + message (STATUS "set_property: SOURCE ${_sourceFiles} APPEND_STRING COMPILE_FLAGS ${_flags}") + endif() + # make object files generated from source files depend on precompiled header + set_property (SOURCE ${_sourceFiles} APPEND PROPERTY OBJECT_DEPENDS "${_pchFile}") + endif() + elseif ("${CMAKE_GENERATOR}" MATCHES "Make|Ninja") + set (_sourceFiles ${_hostFile} ${ARGN}) + if (NOT _wholeTarget) + # for makefile based generator, we force the inclusion of the prefix header for a subset + # of the source files, if this is a multi-language target or has excluded files + set (_flags "") + cotire_add_prefix_pch_inclusion_flags( + "${_language}" "${CMAKE_${_language}_COMPILER_ID}" "${CMAKE_${_language}_COMPILER_VERSION}" + "${_prefixFile}" "${_pchFile}" _flags) + set_property (SOURCE ${_sourceFiles} APPEND_STRING PROPERTY COMPILE_FLAGS " ${_flags} ") + if (COTIRE_DEBUG) + message (STATUS "set_property: SOURCE ${_sourceFiles} APPEND_STRING COMPILE_FLAGS ${_flags}") + endif() + # mark sources as cotired to prevent them from being used in another cotired target + set_source_files_properties(${_sourceFiles} PROPERTIES COTIRE_TARGET "${_target}") + endif() + # make object files generated from source files depend on precompiled header + set_property (SOURCE ${_sourceFiles} APPEND PROPERTY OBJECT_DEPENDS "${_pchFile}") + endif() +endfunction() + +function (cotire_setup_prefix_file_inclusion _language _target _prefixFile) + set (_sourceFiles ${ARGN}) + # force the inclusion of the prefix header for the given source files + set (_flags "") + set (_pchFile "") + cotire_add_prefix_pch_inclusion_flags( + "${_language}" "${CMAKE_${_language}_COMPILER_ID}" "${CMAKE_${_language}_COMPILER_VERSION}" + "${_prefixFile}" "${_pchFile}" _flags) + set_property (SOURCE ${_sourceFiles} APPEND_STRING PROPERTY COMPILE_FLAGS " ${_flags} ") + if (COTIRE_DEBUG) + message (STATUS "set_property: SOURCE ${_sourceFiles} APPEND_STRING COMPILE_FLAGS ${_flags}") + endif() + # mark sources as cotired to prevent them from being used in another cotired target + set_source_files_properties(${_sourceFiles} PROPERTIES COTIRE_TARGET "${_target}") + # make object files generated from source files depend on prefix header + set_property (SOURCE ${_sourceFiles} APPEND PROPERTY OBJECT_DEPENDS "${_prefixFile}") +endfunction() + +function (cotire_get_first_set_property_value _propertyValueVar _type _object) + set (_properties ${ARGN}) + foreach (_property ${_properties}) + get_property(_propertyValue ${_type} "${_object}" PROPERTY ${_property}) + if (_propertyValue) + set (${_propertyValueVar} ${_propertyValue} PARENT_SCOPE) + return() + endif() + endforeach() + set (${_propertyValueVar} "" PARENT_SCOPE) +endfunction() + +function (cotire_setup_combine_command _language _targetScript _joinedFile _cmdsVar) + set (_files ${ARGN}) + set (_filesPaths "") + foreach (_file ${_files}) + get_filename_component(_filePath "${_file}" ABSOLUTE) + list (APPEND _filesPaths "${_filePath}") + endforeach() + cotire_set_cmd_to_prologue(_prefixCmd) + list (APPEND _prefixCmd -P "${COTIRE_CMAKE_MODULE_FILE}" "combine") + if (_targetScript) + list (APPEND _prefixCmd "${_targetScript}") + endif() + list (APPEND _prefixCmd "${_joinedFile}" ${_filesPaths}) + if (COTIRE_DEBUG) + message (STATUS "add_custom_command: OUTPUT ${_joinedFile} COMMAND ${_prefixCmd} DEPENDS ${_files}") + endif() + set_property (SOURCE "${_joinedFile}" PROPERTY GENERATED TRUE) + if (MSVC_IDE) + file (TO_NATIVE_PATH "${_joinedFile}" _joinedFileLogPath) + else() + file (RELATIVE_PATH _joinedFileLogPath "${CMAKE_BINARY_DIR}" "${_joinedFile}") + endif() + get_filename_component(_joinedFileBaseName "${_joinedFile}" NAME_WE) + get_filename_component(_joinedFileExt "${_joinedFile}" EXT) + if (_language AND _joinedFileBaseName MATCHES "${COTIRE_UNITY_SOURCE_FILENAME_SUFFIX}$") + set (_comment "Generating ${_language} unity source ${_joinedFileLogPath}") + elseif (_language AND _joinedFileBaseName MATCHES "${COTIRE_PREFIX_HEADER_FILENAME_SUFFIX}$") + if (_joinedFileExt MATCHES "^\\.c") + set (_comment "Generating ${_language} prefix source ${_joinedFileLogPath}") + else() + set (_comment "Generating ${_language} prefix header ${_joinedFileLogPath}") + endif() + else() + set (_comment "Generating ${_joinedFileLogPath}") + endif() + add_custom_command( + OUTPUT "${_joinedFile}" + COMMAND ${_prefixCmd} + DEPENDS ${_files} + COMMENT "${_comment}" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + VERBATIM) + list (APPEND ${_cmdsVar} COMMAND ${_prefixCmd}) + set (${_cmdsVar} ${${_cmdsVar}} PARENT_SCOPE) +endfunction() + +function (cotire_setup_target_pch_usage _languages _target _wholeTarget) + if (XCODE) + # for Xcode, we attach a pre-build action to generate the unity sources and prefix headers + set (_prefixFiles "") + foreach (_language ${_languages}) + get_property(_prefixFile TARGET ${_target} PROPERTY COTIRE_${_language}_PREFIX_HEADER) + if (_prefixFile) + list (APPEND _prefixFiles "${_prefixFile}") + endif() + endforeach() + set (_cmds ${ARGN}) + list (LENGTH _prefixFiles _numberOfPrefixFiles) + if (_numberOfPrefixFiles GREATER 1) + # we also generate a generic, single prefix header which includes all language specific prefix headers + set (_language "") + set (_targetScript "") + cotire_make_prefix_file_path("${_language}" ${_target} _prefixHeader) + cotire_setup_combine_command("${_language}" "${_targetScript}" "${_prefixHeader}" _cmds ${_prefixFiles}) + else() + set (_prefixHeader "${_prefixFiles}") + endif() + if (COTIRE_DEBUG) + message (STATUS "add_custom_command: TARGET ${_target} PRE_BUILD ${_cmds}") + endif() + # because CMake PRE_BUILD command does not support dependencies, + # we check dependencies explicity in cotire script mode when the pre-build action is run + add_custom_command( + TARGET "${_target}" + PRE_BUILD ${_cmds} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Updating target ${_target} prefix headers" + VERBATIM) + # make Xcode precompile the generated prefix header with ProcessPCH and ProcessPCH++ + set_target_properties(${_target} PROPERTIES XCODE_ATTRIBUTE_GCC_PRECOMPILE_PREFIX_HEADER "YES") + set_target_properties(${_target} PROPERTIES XCODE_ATTRIBUTE_GCC_PREFIX_HEADER "${_prefixHeader}") + elseif ("${CMAKE_GENERATOR}" MATCHES "Make|Ninja") + # for makefile based generator, we force inclusion of the prefix header for all target source files + # if this is a single-language target without any excluded files + if (_wholeTarget) + set (_language "${_languages}") + # for MSVC, Intel and clang-cl, precompiled header inclusion is always done on the source file level + # see cotire_setup_pch_file_inclusion + if (NOT CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel" AND NOT + (WIN32 AND CMAKE_${_language}_COMPILER_ID MATCHES "Clang")) + get_property(_prefixFile TARGET ${_target} PROPERTY COTIRE_${_language}_PREFIX_HEADER) + if (_prefixFile) + get_property(_pchFile TARGET ${_target} PROPERTY COTIRE_${_language}_PRECOMPILED_HEADER) + set (_options COMPILE_OPTIONS) + cotire_add_prefix_pch_inclusion_flags( + "${_language}" "${CMAKE_${_language}_COMPILER_ID}" "${CMAKE_${_language}_COMPILER_VERSION}" + "${_prefixFile}" "${_pchFile}" _options) + set_property(TARGET ${_target} APPEND PROPERTY ${_options}) + if (COTIRE_DEBUG) + message (STATUS "set_property: TARGET ${_target} APPEND PROPERTY ${_options}") + endif() + endif() + endif() + endif() + endif() +endfunction() + +function (cotire_setup_unity_generation_commands _language _target _targetScript _targetConfigScript _unityFiles _cmdsVar) + set (_dependencySources "") + cotire_get_unity_source_dependencies(${_language} ${_target} _dependencySources ${ARGN}) + foreach (_unityFile ${_unityFiles}) + set_property (SOURCE "${_unityFile}" PROPERTY GENERATED TRUE) + # set up compiled unity source dependencies via OBJECT_DEPENDS + # this ensures that missing source files are generated before the unity file is compiled + if (COTIRE_DEBUG AND _dependencySources) + message (STATUS "${_unityFile} OBJECT_DEPENDS ${_dependencySources}") + endif() + if (_dependencySources) + # the OBJECT_DEPENDS property requires a list of full paths + set (_objectDependsPaths "") + foreach (_sourceFile ${_dependencySources}) + get_source_file_property(_sourceLocation "${_sourceFile}" LOCATION) + list (APPEND _objectDependsPaths "${_sourceLocation}") + endforeach() + set_property (SOURCE "${_unityFile}" PROPERTY OBJECT_DEPENDS ${_objectDependsPaths}) + endif() + if (WIN32 AND CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel") + # unity file compilation results in potentially huge object file, + # thus use /bigobj by default unter cl.exe and Windows Intel + set_property (SOURCE "${_unityFile}" APPEND_STRING PROPERTY COMPILE_FLAGS "/bigobj") + endif() + cotire_set_cmd_to_prologue(_unityCmd) + list (APPEND _unityCmd -P "${COTIRE_CMAKE_MODULE_FILE}" "unity" "${_targetConfigScript}" "${_unityFile}") + if (CMAKE_VERSION VERSION_LESS "3.1.0") + set (_unityCmdDepends "${_targetScript}") + else() + # CMake 3.1.0 supports generator expressions in arguments to DEPENDS + set (_unityCmdDepends "${_targetConfigScript}") + endif() + if (MSVC_IDE) + file (TO_NATIVE_PATH "${_unityFile}" _unityFileLogPath) + else() + file (RELATIVE_PATH _unityFileLogPath "${CMAKE_BINARY_DIR}" "${_unityFile}") + endif() + if (COTIRE_DEBUG) + message (STATUS "add_custom_command: OUTPUT ${_unityFile} COMMAND ${_unityCmd} DEPENDS ${_unityCmdDepends}") + endif() + add_custom_command( + OUTPUT "${_unityFile}" + COMMAND ${_unityCmd} + DEPENDS ${_unityCmdDepends} + COMMENT "Generating ${_language} unity source ${_unityFileLogPath}" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + VERBATIM) + list (APPEND ${_cmdsVar} COMMAND ${_unityCmd}) + endforeach() + set (${_cmdsVar} ${${_cmdsVar}} PARENT_SCOPE) +endfunction() + +function (cotire_setup_prefix_generation_command _language _target _targetScript _prefixFile _unityFiles _cmdsVar) + set (_sourceFiles ${ARGN}) + set (_dependencySources "") + cotire_get_prefix_header_dependencies(${_language} ${_target} _dependencySources ${_sourceFiles}) + cotire_set_cmd_to_prologue(_prefixCmd) + list (APPEND _prefixCmd -P "${COTIRE_CMAKE_MODULE_FILE}" "prefix" "${_targetScript}" "${_prefixFile}" ${_unityFiles}) + set_property (SOURCE "${_prefixFile}" PROPERTY GENERATED TRUE) + # make prefix header generation depend on the actual compiler executable used to force + # re-generation when the compiler executable is updated. This prevents "file not found" + # errors for compiler version specific system header files. + get_filename_component(_realCompilerExe "${CMAKE_${_language}_COMPILER}" ABSOLUTE) + if (COTIRE_DEBUG) + message (STATUS "add_custom_command: OUTPUT ${_prefixFile} COMMAND ${_prefixCmd} DEPENDS ${_unityFile} ${_dependencySources} ${_realCompilerExe}") + endif() + if (MSVC_IDE) + file (TO_NATIVE_PATH "${_prefixFile}" _prefixFileLogPath) + else() + file (RELATIVE_PATH _prefixFileLogPath "${CMAKE_BINARY_DIR}" "${_prefixFile}") + endif() + get_filename_component(_prefixFileExt "${_prefixFile}" EXT) + if (_prefixFileExt MATCHES "^\\.c") + set (_comment "Generating ${_language} prefix source ${_prefixFileLogPath}") + else() + set (_comment "Generating ${_language} prefix header ${_prefixFileLogPath}") + endif() + # prevent pre-processing errors upon generating the prefix header when a target's generated include file does not yet exist + # we do not add a file-level dependency for the target's generated files though, because we only want to depend on their existence + # thus we make the prefix header generation depend on a custom helper target which triggers the generation of the files + set (_preTargetName "${_target}${COTIRE_PCH_TARGET_SUFFIX}_pre") + if (TARGET ${_preTargetName}) + # custom helper target has already been generated while processing a different language + list (APPEND _dependencySources ${_preTargetName}) + else() + get_target_property(_targetSourceFiles ${_target} SOURCES) + cotire_get_objects_with_property_on(_generatedSources GENERATED SOURCE ${_targetSourceFiles}) + if (_generatedSources) + add_custom_target("${_preTargetName}" DEPENDS ${_generatedSources}) + cotire_init_target("${_preTargetName}") + list (APPEND _dependencySources ${_preTargetName}) + endif() + endif() + add_custom_command( + OUTPUT "${_prefixFile}" "${_prefixFile}.log" + COMMAND ${_prefixCmd} + DEPENDS ${_unityFiles} ${_dependencySources} "${_realCompilerExe}" + COMMENT "${_comment}" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + VERBATIM) + list (APPEND ${_cmdsVar} COMMAND ${_prefixCmd}) + set (${_cmdsVar} ${${_cmdsVar}} PARENT_SCOPE) +endfunction() + +function (cotire_setup_prefix_generation_from_unity_command _language _target _targetScript _prefixFile _unityFiles _cmdsVar) + set (_sourceFiles ${ARGN}) + if (CMAKE_${_language}_COMPILER_ID MATCHES "GNU|Clang") + # GNU and Clang require indirect compilation of the prefix header to make them honor the system_header pragma + cotire_prefix_header_to_source_file_path(${_language} "${_prefixFile}" _prefixSourceFile) + else() + set (_prefixSourceFile "${_prefixFile}") + endif() + cotire_setup_prefix_generation_command( + ${_language} ${_target} "${_targetScript}" + "${_prefixSourceFile}" "${_unityFiles}" ${_cmdsVar} ${_sourceFiles}) + if (CMAKE_${_language}_COMPILER_ID MATCHES "GNU|Clang") + # set up generation of a prefix source file which includes the prefix header + cotire_setup_combine_command(${_language} "${_targetScript}" "${_prefixFile}" _cmds ${_prefixSourceFile}) + endif() + set (${_cmdsVar} ${${_cmdsVar}} PARENT_SCOPE) +endfunction() + +function (cotire_setup_prefix_generation_from_provided_command _language _target _targetScript _prefixFile _cmdsVar) + set (_prefixHeaderFiles ${ARGN}) + if (CMAKE_${_language}_COMPILER_ID MATCHES "GNU|Clang") + # GNU and Clang require indirect compilation of the prefix header to make them honor the system_header pragma + cotire_prefix_header_to_source_file_path(${_language} "${_prefixFile}" _prefixSourceFile) + else() + set (_prefixSourceFile "${_prefixFile}") + endif() + cotire_setup_combine_command(${_language} "${_targetScript}" "${_prefixSourceFile}" _cmds ${_prefixHeaderFiles}) + if (CMAKE_${_language}_COMPILER_ID MATCHES "GNU|Clang") + # set up generation of a prefix source file which includes the prefix header + cotire_setup_combine_command(${_language} "${_targetScript}" "${_prefixFile}" _cmds ${_prefixSourceFile}) + endif() + set (${_cmdsVar} ${${_cmdsVar}} PARENT_SCOPE) +endfunction() + +function (cotire_init_cotire_target_properties _target) + get_property(_isSet TARGET ${_target} PROPERTY COTIRE_ENABLE_PRECOMPILED_HEADER SET) + if (NOT _isSet) + set_property(TARGET ${_target} PROPERTY COTIRE_ENABLE_PRECOMPILED_HEADER TRUE) + endif() + get_property(_isSet TARGET ${_target} PROPERTY COTIRE_ADD_UNITY_BUILD SET) + if (NOT _isSet) + set_property(TARGET ${_target} PROPERTY COTIRE_ADD_UNITY_BUILD TRUE) + endif() + get_property(_isSet TARGET ${_target} PROPERTY COTIRE_ADD_CLEAN SET) + if (NOT _isSet) + set_property(TARGET ${_target} PROPERTY COTIRE_ADD_CLEAN FALSE) + endif() + get_property(_isSet TARGET ${_target} PROPERTY COTIRE_PREFIX_HEADER_IGNORE_PATH SET) + if (NOT _isSet) + set_property(TARGET ${_target} PROPERTY COTIRE_PREFIX_HEADER_IGNORE_PATH "${CMAKE_SOURCE_DIR}") + cotire_check_is_path_relative_to("${CMAKE_BINARY_DIR}" _isRelative "${CMAKE_SOURCE_DIR}") + if (NOT _isRelative) + set_property(TARGET ${_target} APPEND PROPERTY COTIRE_PREFIX_HEADER_IGNORE_PATH "${CMAKE_BINARY_DIR}") + endif() + endif() + get_property(_isSet TARGET ${_target} PROPERTY COTIRE_PREFIX_HEADER_INCLUDE_PATH SET) + if (NOT _isSet) + set_property(TARGET ${_target} PROPERTY COTIRE_PREFIX_HEADER_INCLUDE_PATH "") + endif() + get_property(_isSet TARGET ${_target} PROPERTY COTIRE_PREFIX_HEADER_INCLUDE_PRIORITY_PATH SET) + if (NOT _isSet) + set_property(TARGET ${_target} PROPERTY COTIRE_PREFIX_HEADER_INCLUDE_PRIORITY_PATH "") + endif() + get_property(_isSet TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_PRE_UNDEFS SET) + if (NOT _isSet) + set_property(TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_PRE_UNDEFS "") + endif() + get_property(_isSet TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_POST_UNDEFS SET) + if (NOT _isSet) + set_property(TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_POST_UNDEFS "") + endif() + get_property(_isSet TARGET ${_target} PROPERTY COTIRE_UNITY_LINK_LIBRARIES_INIT SET) + if (NOT _isSet) + set_property(TARGET ${_target} PROPERTY COTIRE_UNITY_LINK_LIBRARIES_INIT "COPY_UNITY") + endif() + get_property(_isSet TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES SET) + if (NOT _isSet) + if (COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES) + set_property(TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES "${COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES}") + else() + set_property(TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES "") + endif() + endif() +endfunction() + +function (cotire_make_target_message _target _languages _disableMsg _targetMsgVar) + get_target_property(_targetUsePCH ${_target} COTIRE_ENABLE_PRECOMPILED_HEADER) + get_target_property(_targetAddSCU ${_target} COTIRE_ADD_UNITY_BUILD) + string (REPLACE ";" " " _languagesStr "${_languages}") + math (EXPR _numberOfExcludedFiles "${ARGC} - 4") + if (_numberOfExcludedFiles EQUAL 0) + set (_excludedStr "") + elseif (COTIRE_VERBOSE OR _numberOfExcludedFiles LESS 4) + string (REPLACE ";" ", " _excludedStr "excluding ${ARGN}") + else() + set (_excludedStr "excluding ${_numberOfExcludedFiles} files") + endif() + set (_targetMsg "") + if (NOT _languages) + set (_targetMsg "Target ${_target} cannot be cotired.") + if (_disableMsg) + set (_targetMsg "${_targetMsg} ${_disableMsg}") + endif() + elseif (NOT _targetUsePCH AND NOT _targetAddSCU) + set (_targetMsg "${_languagesStr} target ${_target} cotired without unity build and precompiled header.") + if (_disableMsg) + set (_targetMsg "${_targetMsg} ${_disableMsg}") + endif() + elseif (NOT _targetUsePCH) + if (_excludedStr) + set (_targetMsg "${_languagesStr} target ${_target} cotired without precompiled header ${_excludedStr}.") + else() + set (_targetMsg "${_languagesStr} target ${_target} cotired without precompiled header.") + endif() + if (_disableMsg) + set (_targetMsg "${_targetMsg} ${_disableMsg}") + endif() + elseif (NOT _targetAddSCU) + if (_excludedStr) + set (_targetMsg "${_languagesStr} target ${_target} cotired without unity build ${_excludedStr}.") + else() + set (_targetMsg "${_languagesStr} target ${_target} cotired without unity build.") + endif() + if (_disableMsg) + set (_targetMsg "${_targetMsg} ${_disableMsg}") + endif() + else() + if (_excludedStr) + set (_targetMsg "${_languagesStr} target ${_target} cotired ${_excludedStr}.") + else() + set (_targetMsg "${_languagesStr} target ${_target} cotired.") + endif() + endif() + set (${_targetMsgVar} "${_targetMsg}" PARENT_SCOPE) +endfunction() + +function (cotire_choose_target_languages _target _targetLanguagesVar _wholeTargetVar) + set (_languages ${ARGN}) + set (_allSourceFiles "") + set (_allExcludedSourceFiles "") + set (_allCotiredSourceFiles "") + set (_targetLanguages "") + set (_pchEligibleTargetLanguages "") + get_target_property(_targetType ${_target} TYPE) + get_target_property(_targetSourceFiles ${_target} SOURCES) + get_target_property(_targetUsePCH ${_target} COTIRE_ENABLE_PRECOMPILED_HEADER) + get_target_property(_targetAddSCU ${_target} COTIRE_ADD_UNITY_BUILD) + set (_disableMsg "") + foreach (_language ${_languages}) + get_target_property(_prefixHeader ${_target} COTIRE_${_language}_PREFIX_HEADER) + get_target_property(_unityBuildFile ${_target} COTIRE_${_language}_UNITY_SOURCE) + if (_prefixHeader OR _unityBuildFile) + message (STATUS "cotire: target ${_target} has already been cotired.") + set (${_targetLanguagesVar} "" PARENT_SCOPE) + return() + endif() + if (_targetUsePCH AND "${_language}" MATCHES "^C|CXX$" AND DEFINED CMAKE_${_language}_COMPILER_ID) + if (CMAKE_${_language}_COMPILER_ID) + cotire_check_precompiled_header_support("${_language}" "${_target}" _disableMsg) + if (_disableMsg) + set (_targetUsePCH FALSE) + endif() + endif() + endif() + set (_sourceFiles "") + set (_excludedSources "") + set (_cotiredSources "") + cotire_filter_language_source_files(${_language} ${_target} _sourceFiles _excludedSources _cotiredSources ${_targetSourceFiles}) + if (_sourceFiles OR _excludedSources OR _cotiredSources) + list (APPEND _targetLanguages ${_language}) + endif() + if (_sourceFiles) + list (APPEND _allSourceFiles ${_sourceFiles}) + endif() + list (LENGTH _sourceFiles _numberOfSources) + if (NOT _numberOfSources LESS ${COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES}) + list (APPEND _pchEligibleTargetLanguages ${_language}) + endif() + if (_excludedSources) + list (APPEND _allExcludedSourceFiles ${_excludedSources}) + endif() + if (_cotiredSources) + list (APPEND _allCotiredSourceFiles ${_cotiredSources}) + endif() + endforeach() + set (_targetMsgLevel STATUS) + if (NOT _targetLanguages) + string (REPLACE ";" " or " _languagesStr "${_languages}") + set (_disableMsg "No ${_languagesStr} source files.") + set (_targetUsePCH FALSE) + set (_targetAddSCU FALSE) + endif() + if (_targetUsePCH) + if (_allCotiredSourceFiles) + cotire_get_source_file_property_values(_cotireTargets COTIRE_TARGET ${_allCotiredSourceFiles}) + list (REMOVE_DUPLICATES _cotireTargets) + string (REPLACE ";" ", " _cotireTargetsStr "${_cotireTargets}") + set (_disableMsg "Target sources already include a precompiled header for target(s) ${_cotireTargets}.") + set (_disableMsg "${_disableMsg} Set target property COTIRE_ENABLE_PRECOMPILED_HEADER to FALSE for targets ${_target},") + set (_disableMsg "${_disableMsg} ${_cotireTargetsStr} to get a workable build system.") + set (_targetMsgLevel SEND_ERROR) + set (_targetUsePCH FALSE) + elseif (NOT _pchEligibleTargetLanguages) + set (_disableMsg "Too few applicable sources.") + set (_targetUsePCH FALSE) + elseif (XCODE AND _allExcludedSourceFiles) + # for Xcode, we cannot apply the precompiled header to individual sources, only to the whole target + set (_disableMsg "Exclusion of source files not supported for generator Xcode.") + set (_targetUsePCH FALSE) + elseif (XCODE AND "${_targetType}" STREQUAL "OBJECT_LIBRARY") + # for Xcode, we cannot apply the required PRE_BUILD action to generate the prefix header to an OBJECT_LIBRARY target + set (_disableMsg "Required PRE_BUILD action not supported for OBJECT_LIBRARY targets for generator Xcode.") + set (_targetUsePCH FALSE) + endif() + endif() + if (_targetAddSCU) + # disable unity builds if automatic Qt processing is used + get_target_property(_targetAutoMoc ${_target} AUTOMOC) + get_target_property(_targetAutoUic ${_target} AUTOUIC) + get_target_property(_targetAutoRcc ${_target} AUTORCC) + if (_targetAutoMoc OR _targetAutoUic OR _targetAutoRcc) + if (_disableMsg) + set (_disableMsg "${_disableMsg} Target uses automatic CMake Qt processing.") + else() + set (_disableMsg "Target uses automatic CMake Qt processing.") + endif() + set (_targetAddSCU FALSE) + endif() + endif() + set_property(TARGET ${_target} PROPERTY COTIRE_ENABLE_PRECOMPILED_HEADER ${_targetUsePCH}) + set_property(TARGET ${_target} PROPERTY COTIRE_ADD_UNITY_BUILD ${_targetAddSCU}) + cotire_make_target_message(${_target} "${_targetLanguages}" "${_disableMsg}" _targetMsg ${_allExcludedSourceFiles}) + if (_targetMsg) + if (NOT DEFINED COTIREMSG_${_target}) + set (COTIREMSG_${_target} "") + endif() + if (COTIRE_VERBOSE OR NOT "${_targetMsgLevel}" STREQUAL "STATUS" OR + NOT "${COTIREMSG_${_target}}" STREQUAL "${_targetMsg}") + # cache message to avoid redundant messages on re-configure + set (COTIREMSG_${_target} "${_targetMsg}" CACHE INTERNAL "${_target} cotire message.") + message (${_targetMsgLevel} "${_targetMsg}") + endif() + endif() + list (LENGTH _targetLanguages _numberOfLanguages) + if (_numberOfLanguages GREATER 1 OR _allExcludedSourceFiles) + set (${_wholeTargetVar} FALSE PARENT_SCOPE) + else() + set (${_wholeTargetVar} TRUE PARENT_SCOPE) + endif() + set (${_targetLanguagesVar} ${_targetLanguages} PARENT_SCOPE) +endfunction() + +function (cotire_compute_unity_max_number_of_includes _target _maxIncludesVar) + set (_sourceFiles ${ARGN}) + get_target_property(_maxIncludes ${_target} COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES) + if (_maxIncludes MATCHES "(-j|--parallel|--jobs) ?([0-9]*)") + if (DEFINED CMAKE_MATCH_2) + set (_numberOfThreads "${CMAKE_MATCH_2}") + else() + set (_numberOfThreads "") + endif() + if (NOT _numberOfThreads) + # use all available cores + ProcessorCount(_numberOfThreads) + endif() + list (LENGTH _sourceFiles _numberOfSources) + math (EXPR _maxIncludes "(${_numberOfSources} + ${_numberOfThreads} - 1) / ${_numberOfThreads}") + elseif (NOT _maxIncludes MATCHES "[0-9]+") + set (_maxIncludes 0) + endif() + if (COTIRE_DEBUG) + message (STATUS "${_target} unity source max includes: ${_maxIncludes}") + endif() + set (${_maxIncludesVar} ${_maxIncludes} PARENT_SCOPE) +endfunction() + +function (cotire_process_target_language _language _configurations _target _wholeTarget _cmdsVar) + set (${_cmdsVar} "" PARENT_SCOPE) + get_target_property(_targetSourceFiles ${_target} SOURCES) + set (_sourceFiles "") + set (_excludedSources "") + set (_cotiredSources "") + cotire_filter_language_source_files(${_language} ${_target} _sourceFiles _excludedSources _cotiredSources ${_targetSourceFiles}) + if (NOT _sourceFiles AND NOT _cotiredSources) + return() + endif() + set (_cmds "") + # check for user provided unity source file list + get_property(_unitySourceFiles TARGET ${_target} PROPERTY COTIRE_${_language}_UNITY_SOURCE_INIT) + if (NOT _unitySourceFiles) + set (_unitySourceFiles ${_sourceFiles} ${_cotiredSources}) + endif() + cotire_generate_target_script( + ${_language} "${_configurations}" ${_target} _targetScript _targetConfigScript ${_unitySourceFiles}) + # set up unity files for parallel compilation + cotire_compute_unity_max_number_of_includes(${_target} _maxIncludes ${_unitySourceFiles}) + cotire_make_unity_source_file_paths(${_language} ${_target} ${_maxIncludes} _unityFiles ${_unitySourceFiles}) + list (LENGTH _unityFiles _numberOfUnityFiles) + if (_numberOfUnityFiles EQUAL 0) + return() + elseif (_numberOfUnityFiles GREATER 1) + cotire_setup_unity_generation_commands( + ${_language} ${_target} "${_targetScript}" "${_targetConfigScript}" "${_unityFiles}" _cmds ${_unitySourceFiles}) + endif() + # set up single unity file for prefix header generation + cotire_make_single_unity_source_file_path(${_language} ${_target} _unityFile) + cotire_setup_unity_generation_commands( + ${_language} ${_target} "${_targetScript}" "${_targetConfigScript}" "${_unityFile}" _cmds ${_unitySourceFiles}) + cotire_make_prefix_file_path(${_language} ${_target} _prefixFile) + # set up prefix header + if (_prefixFile) + # check for user provided prefix header files + get_property(_prefixHeaderFiles TARGET ${_target} PROPERTY COTIRE_${_language}_PREFIX_HEADER_INIT) + if (_prefixHeaderFiles) + cotire_setup_prefix_generation_from_provided_command( + ${_language} ${_target} "${_targetConfigScript}" "${_prefixFile}" _cmds ${_prefixHeaderFiles}) + else() + cotire_setup_prefix_generation_from_unity_command( + ${_language} ${_target} "${_targetConfigScript}" "${_prefixFile}" "${_unityFile}" _cmds ${_unitySourceFiles}) + endif() + # check if selected language has enough sources at all + list (LENGTH _sourceFiles _numberOfSources) + if (_numberOfSources LESS ${COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES}) + set (_targetUsePCH FALSE) + else() + get_target_property(_targetUsePCH ${_target} COTIRE_ENABLE_PRECOMPILED_HEADER) + endif() + if (_targetUsePCH) + cotire_make_pch_file_path(${_language} ${_target} _pchFile) + if (_pchFile) + # first file in _sourceFiles is passed as the host file + cotire_setup_pch_file_compilation( + ${_language} ${_target} "${_targetConfigScript}" "${_prefixFile}" "${_pchFile}" ${_sourceFiles}) + cotire_setup_pch_file_inclusion( + ${_language} ${_target} ${_wholeTarget} "${_prefixFile}" "${_pchFile}" ${_sourceFiles}) + endif() + elseif (_prefixHeaderFiles) + # user provided prefix header must be included unconditionally + cotire_setup_prefix_file_inclusion(${_language} ${_target} "${_prefixFile}" ${_sourceFiles}) + endif() + endif() + # mark target as cotired for language + set_property(TARGET ${_target} PROPERTY COTIRE_${_language}_UNITY_SOURCE "${_unityFiles}") + if (_prefixFile) + set_property(TARGET ${_target} PROPERTY COTIRE_${_language}_PREFIX_HEADER "${_prefixFile}") + if (_targetUsePCH AND _pchFile) + set_property(TARGET ${_target} PROPERTY COTIRE_${_language}_PRECOMPILED_HEADER "${_pchFile}") + endif() + endif() + set (${_cmdsVar} ${_cmds} PARENT_SCOPE) +endfunction() + +function (cotire_setup_clean_target _target) + set (_cleanTargetName "${_target}${COTIRE_CLEAN_TARGET_SUFFIX}") + if (NOT TARGET "${_cleanTargetName}") + cotire_set_cmd_to_prologue(_cmds) + get_filename_component(_outputDir "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}" ABSOLUTE) + list (APPEND _cmds -P "${COTIRE_CMAKE_MODULE_FILE}" "cleanup" "${_outputDir}" "${COTIRE_INTDIR}" "${_target}") + add_custom_target(${_cleanTargetName} + COMMAND ${_cmds} + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + COMMENT "Cleaning up target ${_target} cotire generated files" + VERBATIM) + cotire_init_target("${_cleanTargetName}") + endif() +endfunction() + +function (cotire_setup_pch_target _languages _configurations _target) + if ("${CMAKE_GENERATOR}" MATCHES "Make|Ninja") + # for makefile based generators, we add a custom target to trigger the generation of the cotire related files + set (_dependsFiles "") + foreach (_language ${_languages}) + set (_props COTIRE_${_language}_PREFIX_HEADER COTIRE_${_language}_UNITY_SOURCE) + if (NOT CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel" AND NOT + (WIN32 AND CMAKE_${_language}_COMPILER_ID MATCHES "Clang")) + # MSVC, Intel and clang-cl only create precompiled header as a side effect + list (INSERT _props 0 COTIRE_${_language}_PRECOMPILED_HEADER) + endif() + cotire_get_first_set_property_value(_dependsFile TARGET ${_target} ${_props}) + if (_dependsFile) + list (APPEND _dependsFiles "${_dependsFile}") + endif() + endforeach() + if (_dependsFiles) + set (_pchTargetName "${_target}${COTIRE_PCH_TARGET_SUFFIX}") + add_custom_target("${_pchTargetName}" DEPENDS ${_dependsFiles}) + cotire_init_target("${_pchTargetName}") + cotire_add_to_pch_all_target(${_pchTargetName}) + endif() + else() + # for other generators, we add the "clean all" target to clean up the precompiled header + cotire_setup_clean_all_target() + endif() +endfunction() + +function (cotire_filter_object_libraries _target _objectLibrariesVar) + set (_objectLibraries "") + foreach (_source ${ARGN}) + if (_source MATCHES "^\\$$") + list (APPEND _objectLibraries "${_source}") + endif() + endforeach() + set (${_objectLibrariesVar} ${_objectLibraries} PARENT_SCOPE) +endfunction() + +function (cotire_collect_unity_target_sources _target _languages _unityTargetSourcesVar) + get_target_property(_targetSourceFiles ${_target} SOURCES) + set (_unityTargetSources ${_targetSourceFiles}) + foreach (_language ${_languages}) + get_property(_unityFiles TARGET ${_target} PROPERTY COTIRE_${_language}_UNITY_SOURCE) + if (_unityFiles) + # remove source files that are included in the unity source + set (_sourceFiles "") + set (_excludedSources "") + set (_cotiredSources "") + cotire_filter_language_source_files(${_language} ${_target} _sourceFiles _excludedSources _cotiredSources ${_targetSourceFiles}) + if (_sourceFiles OR _cotiredSources) + list (REMOVE_ITEM _unityTargetSources ${_sourceFiles} ${_cotiredSources}) + endif() + # add unity source files instead + list (APPEND _unityTargetSources ${_unityFiles}) + endif() + endforeach() + # handle object libraries which are part of the target's sources + get_target_property(_linkLibrariesStrategy ${_target} COTIRE_UNITY_LINK_LIBRARIES_INIT) + if ("${_linkLibrariesStrategy}" MATCHES "^COPY_UNITY$") + cotire_filter_object_libraries(${_target} _objectLibraries ${_targetSourceFiles}) + if (_objectLibraries) + cotire_map_libraries("${_linkLibrariesStrategy}" _unityObjectLibraries ${_objectLibraries}) + list (REMOVE_ITEM _unityTargetSources ${_objectLibraries}) + list (APPEND _unityTargetSources ${_unityObjectLibraries}) + endif() + endif() + set (${_unityTargetSourcesVar} ${_unityTargetSources} PARENT_SCOPE) +endfunction() + +function (cotire_setup_unity_target_pch_usage _languages _target) + foreach (_language ${_languages}) + get_property(_unityFiles TARGET ${_target} PROPERTY COTIRE_${_language}_UNITY_SOURCE) + if (_unityFiles) + get_property(_userPrefixFile TARGET ${_target} PROPERTY COTIRE_${_language}_PREFIX_HEADER_INIT) + get_property(_prefixFile TARGET ${_target} PROPERTY COTIRE_${_language}_PREFIX_HEADER) + if (_userPrefixFile AND _prefixFile) + # user provided prefix header must be included unconditionally by unity sources + cotire_setup_prefix_file_inclusion(${_language} ${_target} "${_prefixFile}" ${_unityFiles}) + endif() + endif() + endforeach() +endfunction() + +function (cotire_setup_unity_build_target _languages _configurations _target) + get_target_property(_unityTargetName ${_target} COTIRE_UNITY_TARGET_NAME) + if (NOT _unityTargetName) + set (_unityTargetName "${_target}${COTIRE_UNITY_BUILD_TARGET_SUFFIX}") + endif() + # determine unity target sub type + get_target_property(_targetType ${_target} TYPE) + if ("${_targetType}" STREQUAL "EXECUTABLE") + set (_unityTargetSubType "") + elseif (_targetType MATCHES "(STATIC|SHARED|MODULE|OBJECT)_LIBRARY") + set (_unityTargetSubType "${CMAKE_MATCH_1}") + else() + message (WARNING "cotire: target ${_target} has unknown target type ${_targetType}.") + return() + endif() + # determine unity target sources + set (_unityTargetSources "") + cotire_collect_unity_target_sources(${_target} "${_languages}" _unityTargetSources) + # prevent AUTOMOC, AUTOUIC and AUTORCC properties from being set when the unity target is created + set (CMAKE_AUTOMOC OFF) + set (CMAKE_AUTOUIC OFF) + set (CMAKE_AUTORCC OFF) + if (COTIRE_DEBUG) + message (STATUS "add target ${_targetType} ${_unityTargetName} ${_unityTargetSubType} EXCLUDE_FROM_ALL ${_unityTargetSources}") + endif() + # generate unity target + if ("${_targetType}" STREQUAL "EXECUTABLE") + add_executable(${_unityTargetName} ${_unityTargetSubType} EXCLUDE_FROM_ALL ${_unityTargetSources}) + else() + add_library(${_unityTargetName} ${_unityTargetSubType} EXCLUDE_FROM_ALL ${_unityTargetSources}) + endif() + # copy output location properties + set (_outputDirProperties + ARCHIVE_OUTPUT_DIRECTORY ARCHIVE_OUTPUT_DIRECTORY_ + LIBRARY_OUTPUT_DIRECTORY LIBRARY_OUTPUT_DIRECTORY_ + RUNTIME_OUTPUT_DIRECTORY RUNTIME_OUTPUT_DIRECTORY_) + if (COTIRE_UNITY_OUTPUT_DIRECTORY) + set (_setDefaultOutputDir TRUE) + if (IS_ABSOLUTE "${COTIRE_UNITY_OUTPUT_DIRECTORY}") + set (_outputDir "${COTIRE_UNITY_OUTPUT_DIRECTORY}") + else() + # append relative COTIRE_UNITY_OUTPUT_DIRECTORY to target's actual output directory + cotire_copy_set_properties("${_configurations}" TARGET ${_target} ${_unityTargetName} ${_outputDirProperties}) + cotire_resolve_config_properties("${_configurations}" _properties ${_outputDirProperties}) + foreach (_property ${_properties}) + get_property(_outputDir TARGET ${_target} PROPERTY ${_property}) + if (_outputDir) + get_filename_component(_outputDir "${_outputDir}/${COTIRE_UNITY_OUTPUT_DIRECTORY}" ABSOLUTE) + set_property(TARGET ${_unityTargetName} PROPERTY ${_property} "${_outputDir}") + set (_setDefaultOutputDir FALSE) + endif() + endforeach() + if (_setDefaultOutputDir) + get_filename_component(_outputDir "${CMAKE_CURRENT_BINARY_DIR}/${COTIRE_UNITY_OUTPUT_DIRECTORY}" ABSOLUTE) + endif() + endif() + if (_setDefaultOutputDir) + set_target_properties(${_unityTargetName} PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${_outputDir}" + LIBRARY_OUTPUT_DIRECTORY "${_outputDir}" + RUNTIME_OUTPUT_DIRECTORY "${_outputDir}") + endif() + else() + cotire_copy_set_properties("${_configurations}" TARGET ${_target} ${_unityTargetName} + ${_outputDirProperties}) + endif() + # copy output name + cotire_copy_set_properties("${_configurations}" TARGET ${_target} ${_unityTargetName} + ARCHIVE_OUTPUT_NAME ARCHIVE_OUTPUT_NAME_ + LIBRARY_OUTPUT_NAME LIBRARY_OUTPUT_NAME_ + OUTPUT_NAME OUTPUT_NAME_ + RUNTIME_OUTPUT_NAME RUNTIME_OUTPUT_NAME_ + PREFIX _POSTFIX SUFFIX + IMPORT_PREFIX IMPORT_SUFFIX) + # copy compile stuff + cotire_copy_set_properties("${_configurations}" TARGET ${_target} ${_unityTargetName} + COMPILE_DEFINITIONS COMPILE_DEFINITIONS_ + COMPILE_FLAGS COMPILE_OPTIONS + Fortran_FORMAT Fortran_MODULE_DIRECTORY + INCLUDE_DIRECTORIES + INTERPROCEDURAL_OPTIMIZATION INTERPROCEDURAL_OPTIMIZATION_ + POSITION_INDEPENDENT_CODE + C_COMPILER_LAUNCHER CXX_COMPILER_LAUNCHER + C_INCLUDE_WHAT_YOU_USE CXX_INCLUDE_WHAT_YOU_USE + C_VISIBILITY_PRESET CXX_VISIBILITY_PRESET VISIBILITY_INLINES_HIDDEN + C_CLANG_TIDY CXX_CLANG_TIDY) + # copy compile features + cotire_copy_set_properties("${_configurations}" TARGET ${_target} ${_unityTargetName} + C_EXTENSIONS C_STANDARD C_STANDARD_REQUIRED + CXX_EXTENSIONS CXX_STANDARD CXX_STANDARD_REQUIRED + COMPILE_FEATURES) + # copy interface stuff + cotire_copy_set_properties("${_configurations}" TARGET ${_target} ${_unityTargetName} + COMPATIBLE_INTERFACE_BOOL COMPATIBLE_INTERFACE_NUMBER_MAX COMPATIBLE_INTERFACE_NUMBER_MIN + COMPATIBLE_INTERFACE_STRING + INTERFACE_COMPILE_DEFINITIONS INTERFACE_COMPILE_FEATURES INTERFACE_COMPILE_OPTIONS + INTERFACE_INCLUDE_DIRECTORIES INTERFACE_SOURCES + INTERFACE_POSITION_INDEPENDENT_CODE INTERFACE_SYSTEM_INCLUDE_DIRECTORIES + INTERFACE_AUTOUIC_OPTIONS NO_SYSTEM_FROM_IMPORTED) + # copy link stuff + cotire_copy_set_properties("${_configurations}" TARGET ${_target} ${_unityTargetName} + BUILD_WITH_INSTALL_RPATH BUILD_WITH_INSTALL_NAME_DIR + INSTALL_RPATH INSTALL_RPATH_USE_LINK_PATH SKIP_BUILD_RPATH + LINKER_LANGUAGE LINK_DEPENDS LINK_DEPENDS_NO_SHARED + LINK_FLAGS LINK_FLAGS_ + LINK_INTERFACE_LIBRARIES LINK_INTERFACE_LIBRARIES_ + LINK_INTERFACE_MULTIPLICITY LINK_INTERFACE_MULTIPLICITY_ + LINK_SEARCH_START_STATIC LINK_SEARCH_END_STATIC + STATIC_LIBRARY_FLAGS STATIC_LIBRARY_FLAGS_ + NO_SONAME SOVERSION VERSION + LINK_WHAT_YOU_USE BUILD_RPATH) + # copy cmake stuff + cotire_copy_set_properties("${_configurations}" TARGET ${_target} ${_unityTargetName} + IMPLICIT_DEPENDS_INCLUDE_TRANSFORM RULE_LAUNCH_COMPILE RULE_LAUNCH_CUSTOM RULE_LAUNCH_LINK) + # copy Apple platform specific stuff + cotire_copy_set_properties("${_configurations}" TARGET ${_target} ${_unityTargetName} + BUNDLE BUNDLE_EXTENSION FRAMEWORK FRAMEWORK_VERSION INSTALL_NAME_DIR + MACOSX_BUNDLE MACOSX_BUNDLE_INFO_PLIST MACOSX_FRAMEWORK_INFO_PLIST MACOSX_RPATH + OSX_ARCHITECTURES OSX_ARCHITECTURES_ PRIVATE_HEADER PUBLIC_HEADER RESOURCE XCTEST + IOS_INSTALL_COMBINED XCODE_EXPLICIT_FILE_TYPE XCODE_PRODUCT_TYPE) + # copy Windows platform specific stuff + cotire_copy_set_properties("${_configurations}" TARGET ${_target} ${_unityTargetName} + GNUtoMS + COMPILE_PDB_NAME COMPILE_PDB_NAME_ + COMPILE_PDB_OUTPUT_DIRECTORY COMPILE_PDB_OUTPUT_DIRECTORY_ + PDB_NAME PDB_NAME_ PDB_OUTPUT_DIRECTORY PDB_OUTPUT_DIRECTORY_ + VS_DESKTOP_EXTENSIONS_VERSION VS_DOTNET_REFERENCES VS_DOTNET_TARGET_FRAMEWORK_VERSION + VS_GLOBAL_KEYWORD VS_GLOBAL_PROJECT_TYPES VS_GLOBAL_ROOTNAMESPACE + VS_IOT_EXTENSIONS_VERSION VS_IOT_STARTUP_TASK + VS_KEYWORD VS_MOBILE_EXTENSIONS_VERSION + VS_SCC_AUXPATH VS_SCC_LOCALPATH VS_SCC_PROJECTNAME VS_SCC_PROVIDER + VS_WINDOWS_TARGET_PLATFORM_MIN_VERSION + VS_WINRT_COMPONENT VS_WINRT_EXTENSIONS VS_WINRT_REFERENCES + WIN32_EXECUTABLE WINDOWS_EXPORT_ALL_SYMBOLS + DEPLOYMENT_REMOTE_DIRECTORY VS_CONFIGURATION_TYPE + VS_SDK_REFERENCES VS_USER_PROPS VS_DEBUGGER_WORKING_DIRECTORY) + # copy Android platform specific stuff + cotire_copy_set_properties("${_configurations}" TARGET ${_target} ${_unityTargetName} + ANDROID_API ANDROID_API_MIN ANDROID_GUI + ANDROID_ANT_ADDITIONAL_OPTIONS ANDROID_ARCH ANDROID_ASSETS_DIRECTORIES + ANDROID_JAR_DEPENDENCIES ANDROID_JAR_DIRECTORIES ANDROID_JAVA_SOURCE_DIR + ANDROID_NATIVE_LIB_DEPENDENCIES ANDROID_NATIVE_LIB_DIRECTORIES + ANDROID_PROCESS_MAX ANDROID_PROGUARD ANDROID_PROGUARD_CONFIG_PATH + ANDROID_SECURE_PROPS_PATH ANDROID_SKIP_ANT_STEP ANDROID_STL_TYPE) + # copy CUDA platform specific stuff + cotire_copy_set_properties("${_configurations}" TARGET ${_target} ${_unityTargetName} + CUDA_PTX_COMPILATION CUDA_SEPARABLE_COMPILATION CUDA_RESOLVE_DEVICE_SYMBOLS + CUDA_EXTENSIONS CUDA_STANDARD CUDA_STANDARD_REQUIRED) + # use output name from original target + get_target_property(_targetOutputName ${_unityTargetName} OUTPUT_NAME) + if (NOT _targetOutputName) + set_property(TARGET ${_unityTargetName} PROPERTY OUTPUT_NAME "${_target}") + endif() + # use export symbol from original target + cotire_get_target_export_symbol("${_target}" _defineSymbol) + if (_defineSymbol) + set_property(TARGET ${_unityTargetName} PROPERTY DEFINE_SYMBOL "${_defineSymbol}") + if ("${_targetType}" STREQUAL "EXECUTABLE") + set_property(TARGET ${_unityTargetName} PROPERTY ENABLE_EXPORTS TRUE) + endif() + endif() + # enable parallel compilation for MSVC + if (MSVC AND "${CMAKE_GENERATOR}" MATCHES "Visual Studio") + list (LENGTH _unityTargetSources _numberOfUnityTargetSources) + if (_numberOfUnityTargetSources GREATER 1) + set_property(TARGET ${_unityTargetName} APPEND PROPERTY COMPILE_OPTIONS "/MP") + endif() + endif() + cotire_init_target(${_unityTargetName}) + cotire_add_to_unity_all_target(${_unityTargetName}) + set_property(TARGET ${_target} PROPERTY COTIRE_UNITY_TARGET_NAME "${_unityTargetName}") +endfunction(cotire_setup_unity_build_target) + +function (cotire_target _target) + set(_options "") + set(_oneValueArgs "") + set(_multiValueArgs LANGUAGES CONFIGURATIONS) + cmake_parse_arguments(_option "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN}) + if (NOT _option_LANGUAGES) + get_property (_option_LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES) + endif() + if (NOT _option_CONFIGURATIONS) + cotire_get_configuration_types(_option_CONFIGURATIONS) + endif() + # check if cotire can be applied to target at all + cotire_is_target_supported(${_target} _isSupported) + if (NOT _isSupported) + get_target_property(_imported ${_target} IMPORTED) + get_target_property(_targetType ${_target} TYPE) + if (_imported) + message (WARNING "cotire: imported ${_targetType} target ${_target} cannot be cotired.") + else() + message (STATUS "cotire: ${_targetType} target ${_target} cannot be cotired.") + endif() + return() + endif() + # resolve alias + get_target_property(_aliasName ${_target} ALIASED_TARGET) + if (_aliasName) + if (COTIRE_DEBUG) + message (STATUS "${_target} is an alias. Applying cotire to aliased target ${_aliasName} instead.") + endif() + set (_target ${_aliasName}) + endif() + # check if target needs to be cotired for build type + # when using configuration types, the test is performed at build time + cotire_init_cotire_target_properties(${_target}) + if (NOT CMAKE_CONFIGURATION_TYPES) + if (CMAKE_BUILD_TYPE) + list (FIND _option_CONFIGURATIONS "${CMAKE_BUILD_TYPE}" _index) + else() + list (FIND _option_CONFIGURATIONS "None" _index) + endif() + if (_index EQUAL -1) + if (COTIRE_DEBUG) + message (STATUS "CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} not cotired (${_option_CONFIGURATIONS})") + endif() + return() + endif() + endif() + # when not using configuration types, immediately create cotire intermediate dir + if (NOT CMAKE_CONFIGURATION_TYPES) + cotire_get_intermediate_dir(_baseDir) + file (MAKE_DIRECTORY "${_baseDir}") + endif() + # choose languages that apply to the target + cotire_choose_target_languages("${_target}" _targetLanguages _wholeTarget ${_option_LANGUAGES}) + if (NOT _targetLanguages) + return() + endif() + set (_cmds "") + foreach (_language ${_targetLanguages}) + cotire_process_target_language("${_language}" "${_option_CONFIGURATIONS}" ${_target} ${_wholeTarget} _cmd) + if (_cmd) + list (APPEND _cmds ${_cmd}) + endif() + endforeach() + get_target_property(_targetAddSCU ${_target} COTIRE_ADD_UNITY_BUILD) + if (_targetAddSCU) + cotire_setup_unity_build_target("${_targetLanguages}" "${_option_CONFIGURATIONS}" ${_target}) + endif() + get_target_property(_targetUsePCH ${_target} COTIRE_ENABLE_PRECOMPILED_HEADER) + if (_targetUsePCH) + cotire_setup_target_pch_usage("${_targetLanguages}" ${_target} ${_wholeTarget} ${_cmds}) + cotire_setup_pch_target("${_targetLanguages}" "${_option_CONFIGURATIONS}" ${_target}) + if (_targetAddSCU) + cotire_setup_unity_target_pch_usage("${_targetLanguages}" ${_target}) + endif() + endif() + get_target_property(_targetAddCleanTarget ${_target} COTIRE_ADD_CLEAN) + if (_targetAddCleanTarget) + cotire_setup_clean_target(${_target}) + endif() +endfunction(cotire_target) + +function (cotire_map_libraries _strategy _mappedLibrariesVar) + set (_mappedLibraries "") + foreach (_library ${ARGN}) + if (_library MATCHES "^\\$$") + set (_libraryName "${CMAKE_MATCH_1}") + set (_linkOnly TRUE) + set (_objectLibrary FALSE) + elseif (_library MATCHES "^\\$$") + set (_libraryName "${CMAKE_MATCH_1}") + set (_linkOnly FALSE) + set (_objectLibrary TRUE) + else() + set (_libraryName "${_library}") + set (_linkOnly FALSE) + set (_objectLibrary FALSE) + endif() + if ("${_strategy}" MATCHES "COPY_UNITY") + cotire_is_target_supported(${_libraryName} _isSupported) + if (_isSupported) + # use target's corresponding unity target, if available + get_target_property(_libraryUnityTargetName ${_libraryName} COTIRE_UNITY_TARGET_NAME) + if (TARGET "${_libraryUnityTargetName}") + if (_linkOnly) + list (APPEND _mappedLibraries "$") + elseif (_objectLibrary) + list (APPEND _mappedLibraries "$") + else() + list (APPEND _mappedLibraries "${_libraryUnityTargetName}") + endif() + else() + list (APPEND _mappedLibraries "${_library}") + endif() + else() + list (APPEND _mappedLibraries "${_library}") + endif() + else() + list (APPEND _mappedLibraries "${_library}") + endif() + endforeach() + list (REMOVE_DUPLICATES _mappedLibraries) + set (${_mappedLibrariesVar} ${_mappedLibraries} PARENT_SCOPE) +endfunction() + +function (cotire_target_link_libraries _target) + cotire_is_target_supported(${_target} _isSupported) + if (NOT _isSupported) + return() + endif() + get_target_property(_unityTargetName ${_target} COTIRE_UNITY_TARGET_NAME) + if (TARGET "${_unityTargetName}") + get_target_property(_linkLibrariesStrategy ${_target} COTIRE_UNITY_LINK_LIBRARIES_INIT) + if (COTIRE_DEBUG) + message (STATUS "unity target ${_unityTargetName} link strategy: ${_linkLibrariesStrategy}") + endif() + if ("${_linkLibrariesStrategy}" MATCHES "^(COPY|COPY_UNITY)$") + get_target_property(_linkLibraries ${_target} LINK_LIBRARIES) + if (_linkLibraries) + cotire_map_libraries("${_linkLibrariesStrategy}" _unityLinkLibraries ${_linkLibraries}) + set_target_properties(${_unityTargetName} PROPERTIES LINK_LIBRARIES "${_unityLinkLibraries}") + if (COTIRE_DEBUG) + message (STATUS "unity target ${_unityTargetName} link libraries: ${_unityLinkLibraries}") + endif() + endif() + get_target_property(_interfaceLinkLibraries ${_target} INTERFACE_LINK_LIBRARIES) + if (_interfaceLinkLibraries) + cotire_map_libraries("${_linkLibrariesStrategy}" _unityLinkInterfaceLibraries ${_interfaceLinkLibraries}) + set_target_properties(${_unityTargetName} PROPERTIES INTERFACE_LINK_LIBRARIES "${_unityLinkInterfaceLibraries}") + if (COTIRE_DEBUG) + message (STATUS "unity target ${_unityTargetName} interface link libraries: ${_unityLinkInterfaceLibraries}") + endif() + endif() + get_target_property(_manualDependencies ${_target} MANUALLY_ADDED_DEPENDENCIES) + if (_manualDependencies) + cotire_map_libraries("${_linkLibrariesStrategy}" _unityManualDependencies ${_manualDependencies}) + if (_unityManualDependencies) + add_dependencies("${_unityTargetName}" ${_unityManualDependencies}) + endif() + endif() + endif() + endif() +endfunction(cotire_target_link_libraries) + +function (cotire_cleanup _binaryDir _cotireIntermediateDirName _targetName) + if (_targetName) + file (GLOB_RECURSE _cotireFiles "${_binaryDir}/${_targetName}*.*") + else() + file (GLOB_RECURSE _cotireFiles "${_binaryDir}/*.*") + endif() + # filter files in intermediate directory + set (_filesToRemove "") + foreach (_file ${_cotireFiles}) + get_filename_component(_dir "${_file}" DIRECTORY) + get_filename_component(_dirName "${_dir}" NAME) + if ("${_dirName}" STREQUAL "${_cotireIntermediateDirName}") + list (APPEND _filesToRemove "${_file}") + endif() + endforeach() + if (_filesToRemove) + if (COTIRE_VERBOSE) + message (STATUS "cleaning up ${_filesToRemove}") + endif() + file (REMOVE ${_filesToRemove}) + endif() +endfunction() + +function (cotire_init_target _targetName) + if (COTIRE_TARGETS_FOLDER) + set_target_properties(${_targetName} PROPERTIES FOLDER "${COTIRE_TARGETS_FOLDER}") + endif() + set_target_properties(${_targetName} PROPERTIES EXCLUDE_FROM_ALL TRUE) + if (MSVC_IDE) + set_target_properties(${_targetName} PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD TRUE) + endif() +endfunction() + +function (cotire_add_to_pch_all_target _pchTargetName) + set (_targetName "${COTIRE_PCH_ALL_TARGET_NAME}") + if (NOT TARGET "${_targetName}") + add_custom_target("${_targetName}" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + VERBATIM) + cotire_init_target("${_targetName}") + endif() + cotire_setup_clean_all_target() + add_dependencies(${_targetName} ${_pchTargetName}) +endfunction() + +function (cotire_add_to_unity_all_target _unityTargetName) + set (_targetName "${COTIRE_UNITY_BUILD_ALL_TARGET_NAME}") + if (NOT TARGET "${_targetName}") + add_custom_target("${_targetName}" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + VERBATIM) + cotire_init_target("${_targetName}") + endif() + cotire_setup_clean_all_target() + add_dependencies(${_targetName} ${_unityTargetName}) +endfunction() + +function (cotire_setup_clean_all_target) + set (_targetName "${COTIRE_CLEAN_ALL_TARGET_NAME}") + if (NOT TARGET "${_targetName}") + cotire_set_cmd_to_prologue(_cmds) + list (APPEND _cmds -P "${COTIRE_CMAKE_MODULE_FILE}" "cleanup" "${CMAKE_BINARY_DIR}" "${COTIRE_INTDIR}") + add_custom_target(${_targetName} + COMMAND ${_cmds} + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + COMMENT "Cleaning up all cotire generated files" + VERBATIM) + cotire_init_target("${_targetName}") + endif() +endfunction() + +function (cotire) + set(_options "") + set(_oneValueArgs "") + set(_multiValueArgs LANGUAGES CONFIGURATIONS) + cmake_parse_arguments(_option "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN}) + set (_targets ${_option_UNPARSED_ARGUMENTS}) + foreach (_target ${_targets}) + if (TARGET ${_target}) + cotire_target(${_target} LANGUAGES ${_option_LANGUAGES} CONFIGURATIONS ${_option_CONFIGURATIONS}) + else() + message (WARNING "cotire: ${_target} is not a target.") + endif() + endforeach() + foreach (_target ${_targets}) + if (TARGET ${_target}) + cotire_target_link_libraries(${_target}) + endif() + endforeach() +endfunction() + +if (CMAKE_SCRIPT_MODE_FILE) + + # cotire is being run in script mode + # locate -P on command args + set (COTIRE_ARGC -1) + foreach (_index RANGE ${CMAKE_ARGC}) + if (COTIRE_ARGC GREATER -1) + set (COTIRE_ARGV${COTIRE_ARGC} "${CMAKE_ARGV${_index}}") + math (EXPR COTIRE_ARGC "${COTIRE_ARGC} + 1") + elseif ("${CMAKE_ARGV${_index}}" STREQUAL "-P") + set (COTIRE_ARGC 0) + endif() + endforeach() + + # include target script if available + if ("${COTIRE_ARGV2}" MATCHES "\\.cmake$") + # the included target scripts sets up additional variables relating to the target (e.g., COTIRE_TARGET_SOURCES) + include("${COTIRE_ARGV2}") + endif() + + if (COTIRE_DEBUG) + message (STATUS "${COTIRE_ARGV0} ${COTIRE_ARGV1} ${COTIRE_ARGV2} ${COTIRE_ARGV3} ${COTIRE_ARGV4} ${COTIRE_ARGV5}") + endif() + + if (NOT COTIRE_BUILD_TYPE) + set (COTIRE_BUILD_TYPE "None") + endif() + string (TOUPPER "${COTIRE_BUILD_TYPE}" _upperConfig) + set (_includeDirs ${COTIRE_TARGET_INCLUDE_DIRECTORIES_${_upperConfig}}) + set (_systemIncludeDirs ${COTIRE_TARGET_SYSTEM_INCLUDE_DIRECTORIES_${_upperConfig}}) + set (_compileDefinitions ${COTIRE_TARGET_COMPILE_DEFINITIONS_${_upperConfig}}) + set (_compileFlags ${COTIRE_TARGET_COMPILE_FLAGS_${_upperConfig}}) + # check if target has been cotired for actual build type COTIRE_BUILD_TYPE + list (FIND COTIRE_TARGET_CONFIGURATION_TYPES "${COTIRE_BUILD_TYPE}" _index) + if (_index GREATER -1) + set (_sources ${COTIRE_TARGET_SOURCES}) + set (_sourcesDefinitions ${COTIRE_TARGET_SOURCES_COMPILE_DEFINITIONS_${_upperConfig}}) + else() + if (COTIRE_DEBUG) + message (STATUS "COTIRE_BUILD_TYPE=${COTIRE_BUILD_TYPE} not cotired (${COTIRE_TARGET_CONFIGURATION_TYPES})") + endif() + set (_sources "") + set (_sourcesDefinitions "") + endif() + set (_targetPreUndefs ${COTIRE_TARGET_PRE_UNDEFS}) + set (_targetPostUndefs ${COTIRE_TARGET_POST_UNDEFS}) + set (_sourcesPreUndefs ${COTIRE_TARGET_SOURCES_PRE_UNDEFS}) + set (_sourcesPostUndefs ${COTIRE_TARGET_SOURCES_POST_UNDEFS}) + + if ("${COTIRE_ARGV1}" STREQUAL "unity") + + if (XCODE) + # executing pre-build action under Xcode, check dependency on target script + set (_dependsOption DEPENDS "${COTIRE_ARGV2}") + else() + # executing custom command, no need to re-check for dependencies + set (_dependsOption "") + endif() + + cotire_select_unity_source_files("${COTIRE_ARGV3}" _sources ${_sources}) + + cotire_generate_unity_source( + "${COTIRE_ARGV3}" ${_sources} + LANGUAGE "${COTIRE_TARGET_LANGUAGE}" + SOURCES_COMPILE_DEFINITIONS ${_sourcesDefinitions} + PRE_UNDEFS ${_targetPreUndefs} + POST_UNDEFS ${_targetPostUndefs} + SOURCES_PRE_UNDEFS ${_sourcesPreUndefs} + SOURCES_POST_UNDEFS ${_sourcesPostUndefs} + ${_dependsOption}) + + elseif ("${COTIRE_ARGV1}" STREQUAL "prefix") + + if (XCODE) + # executing pre-build action under Xcode, check dependency on unity file and prefix dependencies + set (_dependsOption DEPENDS "${COTIRE_ARGV4}" ${COTIRE_TARGET_PREFIX_DEPENDS}) + else() + # executing custom command, no need to re-check for dependencies + set (_dependsOption "") + endif() + + set (_files "") + foreach (_index RANGE 4 ${COTIRE_ARGC}) + if (COTIRE_ARGV${_index}) + list (APPEND _files "${COTIRE_ARGV${_index}}") + endif() + endforeach() + + cotire_generate_prefix_header( + "${COTIRE_ARGV3}" ${_files} + COMPILER_LAUNCHER "${COTIRE_TARGET_${COTIRE_TARGET_LANGUAGE}_COMPILER_LAUNCHER}" + COMPILER_EXECUTABLE "${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER}" + COMPILER_ARG1 ${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER_ARG1} + COMPILER_ID "${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER_ID}" + COMPILER_VERSION "${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER_VERSION}" + LANGUAGE "${COTIRE_TARGET_LANGUAGE}" + IGNORE_PATH "${COTIRE_TARGET_IGNORE_PATH};${COTIRE_ADDITIONAL_PREFIX_HEADER_IGNORE_PATH}" + INCLUDE_PATH ${COTIRE_TARGET_INCLUDE_PATH} + IGNORE_EXTENSIONS "${CMAKE_${COTIRE_TARGET_LANGUAGE}_SOURCE_FILE_EXTENSIONS};${COTIRE_ADDITIONAL_PREFIX_HEADER_IGNORE_EXTENSIONS}" + INCLUDE_PRIORITY_PATH ${COTIRE_TARGET_INCLUDE_PRIORITY_PATH} + INCLUDE_DIRECTORIES ${_includeDirs} + SYSTEM_INCLUDE_DIRECTORIES ${_systemIncludeDirs} + COMPILE_DEFINITIONS ${_compileDefinitions} + COMPILE_FLAGS ${_compileFlags} + ${_dependsOption}) + + elseif ("${COTIRE_ARGV1}" STREQUAL "precompile") + + set (_files "") + foreach (_index RANGE 5 ${COTIRE_ARGC}) + if (COTIRE_ARGV${_index}) + list (APPEND _files "${COTIRE_ARGV${_index}}") + endif() + endforeach() + + cotire_precompile_prefix_header( + "${COTIRE_ARGV3}" "${COTIRE_ARGV4}" "${COTIRE_ARGV5}" + COMPILER_LAUNCHER "${COTIRE_TARGET_${COTIRE_TARGET_LANGUAGE}_COMPILER_LAUNCHER}" + COMPILER_EXECUTABLE "${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER}" + COMPILER_ARG1 ${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER_ARG1} + COMPILER_ID "${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER_ID}" + COMPILER_VERSION "${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER_VERSION}" + LANGUAGE "${COTIRE_TARGET_LANGUAGE}" + INCLUDE_DIRECTORIES ${_includeDirs} + SYSTEM_INCLUDE_DIRECTORIES ${_systemIncludeDirs} + COMPILE_DEFINITIONS ${_compileDefinitions} + COMPILE_FLAGS ${_compileFlags}) + + elseif ("${COTIRE_ARGV1}" STREQUAL "combine") + + if (COTIRE_TARGET_LANGUAGE) + set (_combinedFile "${COTIRE_ARGV3}") + set (_startIndex 4) + else() + set (_combinedFile "${COTIRE_ARGV2}") + set (_startIndex 3) + endif() + set (_files "") + foreach (_index RANGE ${_startIndex} ${COTIRE_ARGC}) + if (COTIRE_ARGV${_index}) + list (APPEND _files "${COTIRE_ARGV${_index}}") + endif() + endforeach() + + if (XCODE) + # executing pre-build action under Xcode, check dependency on files to be combined + set (_dependsOption DEPENDS ${_files}) + else() + # executing custom command, no need to re-check for dependencies + set (_dependsOption "") + endif() + + if (COTIRE_TARGET_LANGUAGE) + cotire_generate_unity_source( + "${_combinedFile}" ${_files} + LANGUAGE "${COTIRE_TARGET_LANGUAGE}" + ${_dependsOption}) + else() + cotire_generate_unity_source("${_combinedFile}" ${_files} ${_dependsOption}) + endif() + + elseif ("${COTIRE_ARGV1}" STREQUAL "cleanup") + + cotire_cleanup("${COTIRE_ARGV2}" "${COTIRE_ARGV3}" "${COTIRE_ARGV4}") + + else() + message (FATAL_ERROR "cotire: unknown command \"${COTIRE_ARGV1}\".") + endif() + +else() + + # cotire is being run in include mode + # set up all variable and property definitions + + if (NOT DEFINED COTIRE_DEBUG_INIT) + if (DEFINED COTIRE_DEBUG) + set (COTIRE_DEBUG_INIT ${COTIRE_DEBUG}) + else() + set (COTIRE_DEBUG_INIT FALSE) + endif() + endif() + option (COTIRE_DEBUG "Enable cotire debugging output?" ${COTIRE_DEBUG_INIT}) + + if (NOT DEFINED COTIRE_VERBOSE_INIT) + if (DEFINED COTIRE_VERBOSE) + set (COTIRE_VERBOSE_INIT ${COTIRE_VERBOSE}) + else() + set (COTIRE_VERBOSE_INIT FALSE) + endif() + endif() + option (COTIRE_VERBOSE "Enable cotire verbose output?" ${COTIRE_VERBOSE_INIT}) + + set (COTIRE_ADDITIONAL_PREFIX_HEADER_IGNORE_EXTENSIONS "inc;inl;ipp" CACHE STRING + "Ignore headers with the listed file extensions from the generated prefix header.") + + set (COTIRE_ADDITIONAL_PREFIX_HEADER_IGNORE_PATH "" CACHE STRING + "Ignore headers from these directories when generating the prefix header.") + + set (COTIRE_UNITY_SOURCE_EXCLUDE_EXTENSIONS "m;mm" CACHE STRING + "Ignore sources with the listed file extensions from the generated unity source.") + + set (COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES "2" CACHE STRING + "Minimum number of sources in target required to enable use of precompiled header.") + + if (NOT DEFINED COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES_INIT) + if (DEFINED COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES) + set (COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES_INIT ${COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES}) + elseif ("${CMAKE_GENERATOR}" MATCHES "JOM|Ninja|Visual Studio") + # enable parallelization for generators that run multiple jobs by default + set (COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES_INIT "-j") + else() + set (COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES_INIT "0") + endif() + endif() + set (COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES "${COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES_INIT}" CACHE STRING + "Maximum number of source files to include in a single unity source file.") + + if (NOT COTIRE_PREFIX_HEADER_FILENAME_SUFFIX) + set (COTIRE_PREFIX_HEADER_FILENAME_SUFFIX "_prefix") + endif() + if (NOT COTIRE_UNITY_SOURCE_FILENAME_SUFFIX) + set (COTIRE_UNITY_SOURCE_FILENAME_SUFFIX "_unity") + endif() + if (NOT COTIRE_INTDIR) + set (COTIRE_INTDIR "cotire") + endif() + if (NOT COTIRE_PCH_ALL_TARGET_NAME) + set (COTIRE_PCH_ALL_TARGET_NAME "all_pch") + endif() + if (NOT COTIRE_UNITY_BUILD_ALL_TARGET_NAME) + set (COTIRE_UNITY_BUILD_ALL_TARGET_NAME "all_unity") + endif() + if (NOT COTIRE_CLEAN_ALL_TARGET_NAME) + set (COTIRE_CLEAN_ALL_TARGET_NAME "clean_cotire") + endif() + if (NOT COTIRE_CLEAN_TARGET_SUFFIX) + set (COTIRE_CLEAN_TARGET_SUFFIX "_clean_cotire") + endif() + if (NOT COTIRE_PCH_TARGET_SUFFIX) + set (COTIRE_PCH_TARGET_SUFFIX "_pch") + endif() + if (MSVC) + # MSVC default PCH memory scaling factor of 100 percent (75 MB) is too small for template heavy C++ code + # use a bigger default factor of 170 percent (128 MB) + if (NOT DEFINED COTIRE_PCH_MEMORY_SCALING_FACTOR) + set (COTIRE_PCH_MEMORY_SCALING_FACTOR "170") + endif() + endif() + if (NOT COTIRE_UNITY_BUILD_TARGET_SUFFIX) + set (COTIRE_UNITY_BUILD_TARGET_SUFFIX "_unity") + endif() + if (NOT DEFINED COTIRE_TARGETS_FOLDER) + set (COTIRE_TARGETS_FOLDER "cotire") + endif() + if (NOT DEFINED COTIRE_UNITY_OUTPUT_DIRECTORY) + if ("${CMAKE_GENERATOR}" MATCHES "Ninja") + # generated Ninja build files do not work if the unity target produces the same output file as the cotired target + set (COTIRE_UNITY_OUTPUT_DIRECTORY "unity") + else() + set (COTIRE_UNITY_OUTPUT_DIRECTORY "") + endif() + endif() + + # define cotire cache variables + + define_property( + CACHED_VARIABLE PROPERTY "COTIRE_ADDITIONAL_PREFIX_HEADER_IGNORE_PATH" + BRIEF_DOCS "Ignore headers from these directories when generating the prefix header." + FULL_DOCS + "The variable can be set to a semicolon separated list of include directories." + "If a header file is found in one of these directories or sub-directories, it will be excluded from the generated prefix header." + "If not defined, defaults to empty list." + ) + + define_property( + CACHED_VARIABLE PROPERTY "COTIRE_ADDITIONAL_PREFIX_HEADER_IGNORE_EXTENSIONS" + BRIEF_DOCS "Ignore includes with the listed file extensions from the generated prefix header." + FULL_DOCS + "The variable can be set to a semicolon separated list of file extensions." + "If a header file extension matches one in the list, it will be excluded from the generated prefix header." + "Includes with an extension in CMAKE__SOURCE_FILE_EXTENSIONS are always ignored." + "If not defined, defaults to inc;inl;ipp." + ) + + define_property( + CACHED_VARIABLE PROPERTY "COTIRE_UNITY_SOURCE_EXCLUDE_EXTENSIONS" + BRIEF_DOCS "Exclude sources with the listed file extensions from the generated unity source." + FULL_DOCS + "The variable can be set to a semicolon separated list of file extensions." + "If a source file extension matches one in the list, it will be excluded from the generated unity source file." + "Source files with an extension in CMAKE__IGNORE_EXTENSIONS are always excluded." + "If not defined, defaults to m;mm." + ) + + define_property( + CACHED_VARIABLE PROPERTY "COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES" + BRIEF_DOCS "Minimum number of sources in target required to enable use of precompiled header." + FULL_DOCS + "The variable can be set to an integer > 0." + "If a target contains less than that number of source files, cotire will not enable the use of the precompiled header for the target." + "If not defined, defaults to 2." + ) + + define_property( + CACHED_VARIABLE PROPERTY "COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES" + BRIEF_DOCS "Maximum number of source files to include in a single unity source file." + FULL_DOCS + "This may be set to an integer >= 0." + "If 0, cotire will only create a single unity source file." + "If a target contains more than that number of source files, cotire will create multiple unity source files for it." + "Can be set to \"-j\" to optimize the count of unity source files for the number of available processor cores." + "Can be set to \"-j jobs\" to optimize the number of unity source files for the given number of simultaneous jobs." + "Is used to initialize the target property COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES." + "Defaults to \"-j\" for the generators Visual Studio, JOM or Ninja. Defaults to 0 otherwise." + ) + + # define cotire directory properties + + define_property( + DIRECTORY PROPERTY "COTIRE_ENABLE_PRECOMPILED_HEADER" + BRIEF_DOCS "Modify build command of cotired targets added in this directory to make use of the generated precompiled header." + FULL_DOCS + "See target property COTIRE_ENABLE_PRECOMPILED_HEADER." + ) + + define_property( + DIRECTORY PROPERTY "COTIRE_ADD_UNITY_BUILD" + BRIEF_DOCS "Add a new target that performs a unity build for cotired targets added in this directory." + FULL_DOCS + "See target property COTIRE_ADD_UNITY_BUILD." + ) + + define_property( + DIRECTORY PROPERTY "COTIRE_ADD_CLEAN" + BRIEF_DOCS "Add a new target that cleans all cotire generated files for cotired targets added in this directory." + FULL_DOCS + "See target property COTIRE_ADD_CLEAN." + ) + + define_property( + DIRECTORY PROPERTY "COTIRE_PREFIX_HEADER_IGNORE_PATH" + BRIEF_DOCS "Ignore headers from these directories when generating the prefix header." + FULL_DOCS + "See target property COTIRE_PREFIX_HEADER_IGNORE_PATH." + ) + + define_property( + DIRECTORY PROPERTY "COTIRE_PREFIX_HEADER_INCLUDE_PATH" + BRIEF_DOCS "Honor headers from these directories when generating the prefix header." + FULL_DOCS + "See target property COTIRE_PREFIX_HEADER_INCLUDE_PATH." + ) + + define_property( + DIRECTORY PROPERTY "COTIRE_PREFIX_HEADER_INCLUDE_PRIORITY_PATH" + BRIEF_DOCS "Header paths matching one of these directories are put at the top of the prefix header." + FULL_DOCS + "See target property COTIRE_PREFIX_HEADER_INCLUDE_PRIORITY_PATH." + ) + + define_property( + DIRECTORY PROPERTY "COTIRE_UNITY_SOURCE_PRE_UNDEFS" + BRIEF_DOCS "Preprocessor undefs to place in the generated unity source file before the inclusion of each source file." + FULL_DOCS + "See target property COTIRE_UNITY_SOURCE_PRE_UNDEFS." + ) + + define_property( + DIRECTORY PROPERTY "COTIRE_UNITY_SOURCE_POST_UNDEFS" + BRIEF_DOCS "Preprocessor undefs to place in the generated unity source file after the inclusion of each source file." + FULL_DOCS + "See target property COTIRE_UNITY_SOURCE_POST_UNDEFS." + ) + + define_property( + DIRECTORY PROPERTY "COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES" + BRIEF_DOCS "Maximum number of source files to include in a single unity source file." + FULL_DOCS + "See target property COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES." + ) + + define_property( + DIRECTORY PROPERTY "COTIRE_UNITY_LINK_LIBRARIES_INIT" + BRIEF_DOCS "Define strategy for setting up the unity target's link libraries." + FULL_DOCS + "See target property COTIRE_UNITY_LINK_LIBRARIES_INIT." + ) + + # define cotire target properties + + define_property( + TARGET PROPERTY "COTIRE_ENABLE_PRECOMPILED_HEADER" INHERITED + BRIEF_DOCS "Modify this target's build command to make use of the generated precompiled header." + FULL_DOCS + "If this property is set to TRUE, cotire will modify the build command to make use of the generated precompiled header." + "Irrespective of the value of this property, cotire will setup custom commands to generate the unity source and prefix header for the target." + "For makefile based generators cotire will also set up a custom target to manually invoke the generation of the precompiled header." + "The target name will be set to this target's name with the suffix _pch appended." + "Inherited from directory." + "Defaults to TRUE." + ) + + define_property( + TARGET PROPERTY "COTIRE_ADD_UNITY_BUILD" INHERITED + BRIEF_DOCS "Add a new target that performs a unity build for this target." + FULL_DOCS + "If this property is set to TRUE, cotire creates a new target of the same type that uses the generated unity source file instead of the target sources." + "Most of the relevant target properties will be copied from this target to the new unity build target." + "Target dependencies and linked libraries have to be manually set up for the new unity build target." + "The unity target name will be set to this target's name with the suffix _unity appended." + "Inherited from directory." + "Defaults to TRUE." + ) + + define_property( + TARGET PROPERTY "COTIRE_ADD_CLEAN" INHERITED + BRIEF_DOCS "Add a new target that cleans all cotire generated files for this target." + FULL_DOCS + "If this property is set to TRUE, cotire creates a new target that clean all files (unity source, prefix header, precompiled header)." + "The clean target name will be set to this target's name with the suffix _clean_cotire appended." + "Inherited from directory." + "Defaults to FALSE." + ) + + define_property( + TARGET PROPERTY "COTIRE_PREFIX_HEADER_IGNORE_PATH" INHERITED + BRIEF_DOCS "Ignore headers from these directories when generating the prefix header." + FULL_DOCS + "The property can be set to a list of directories." + "If a header file is found in one of these directories or sub-directories, it will be excluded from the generated prefix header." + "Inherited from directory." + "If not set, this property is initialized to \${CMAKE_SOURCE_DIR};\${CMAKE_BINARY_DIR}." + ) + + define_property( + TARGET PROPERTY "COTIRE_PREFIX_HEADER_INCLUDE_PATH" INHERITED + BRIEF_DOCS "Honor headers from these directories when generating the prefix header." + FULL_DOCS + "The property can be set to a list of directories." + "If a header file is found in one of these directories or sub-directories, it will be included in the generated prefix header." + "If a header file is both selected by COTIRE_PREFIX_HEADER_IGNORE_PATH and COTIRE_PREFIX_HEADER_INCLUDE_PATH," + "the option which yields the closer relative path match wins." + "Inherited from directory." + "If not set, this property is initialized to the empty list." + ) + + define_property( + TARGET PROPERTY "COTIRE_PREFIX_HEADER_INCLUDE_PRIORITY_PATH" INHERITED + BRIEF_DOCS "Header paths matching one of these directories are put at the top of prefix header." + FULL_DOCS + "The property can be set to a list of directories." + "Header file paths matching one of these directories will be inserted at the beginning of the generated prefix header." + "Header files are sorted according to the order of the directories in the property." + "If not set, this property is initialized to the empty list." + ) + + define_property( + TARGET PROPERTY "COTIRE_UNITY_SOURCE_PRE_UNDEFS" INHERITED + BRIEF_DOCS "Preprocessor undefs to place in the generated unity source file before the inclusion of each target source file." + FULL_DOCS + "This may be set to a semicolon-separated list of preprocessor symbols." + "cotire will add corresponding #undef directives to the generated unit source file before each target source file." + "Inherited from directory." + "Defaults to empty string." + ) + + define_property( + TARGET PROPERTY "COTIRE_UNITY_SOURCE_POST_UNDEFS" INHERITED + BRIEF_DOCS "Preprocessor undefs to place in the generated unity source file after the inclusion of each target source file." + FULL_DOCS + "This may be set to a semicolon-separated list of preprocessor symbols." + "cotire will add corresponding #undef directives to the generated unit source file after each target source file." + "Inherited from directory." + "Defaults to empty string." + ) + + define_property( + TARGET PROPERTY "COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES" INHERITED + BRIEF_DOCS "Maximum number of source files to include in a single unity source file." + FULL_DOCS + "This may be set to an integer > 0." + "If a target contains more than that number of source files, cotire will create multiple unity build files for it." + "If not set, cotire will only create a single unity source file." + "Inherited from directory." + "Defaults to empty." + ) + + define_property( + TARGET PROPERTY "COTIRE__UNITY_SOURCE_INIT" + BRIEF_DOCS "User provided unity source file to be used instead of the automatically generated one." + FULL_DOCS + "If set, cotire will only add the given file(s) to the generated unity source file." + "If not set, cotire will add all the target source files to the generated unity source file." + "The property can be set to a user provided unity source file." + "Defaults to empty." + ) + + define_property( + TARGET PROPERTY "COTIRE__PREFIX_HEADER_INIT" + BRIEF_DOCS "User provided prefix header file to be used instead of the automatically generated one." + FULL_DOCS + "If set, cotire will add the given header file(s) to the generated prefix header file." + "If not set, cotire will generate a prefix header by tracking the header files included by the unity source file." + "The property can be set to a user provided prefix header file (e.g., stdafx.h)." + "Defaults to empty." + ) + + define_property( + TARGET PROPERTY "COTIRE_UNITY_LINK_LIBRARIES_INIT" INHERITED + BRIEF_DOCS "Define strategy for setting up unity target's link libraries." + FULL_DOCS + "If this property is empty or set to NONE, the generated unity target's link libraries have to be set up manually." + "If this property is set to COPY, the unity target's link libraries will be copied from this target." + "If this property is set to COPY_UNITY, the unity target's link libraries will be copied from this target with considering existing unity targets." + "Inherited from directory." + "Defaults to empty." + ) + + define_property( + TARGET PROPERTY "COTIRE__UNITY_SOURCE" + BRIEF_DOCS "Read-only property. The generated unity source file(s)." + FULL_DOCS + "cotire sets this property to the path of the generated single computation unit source file for the target." + "Defaults to empty string." + ) + + define_property( + TARGET PROPERTY "COTIRE__PREFIX_HEADER" + BRIEF_DOCS "Read-only property. The generated prefix header file." + FULL_DOCS + "cotire sets this property to the full path of the generated language prefix header for the target." + "Defaults to empty string." + ) + + define_property( + TARGET PROPERTY "COTIRE__PRECOMPILED_HEADER" + BRIEF_DOCS "Read-only property. The generated precompiled header file." + FULL_DOCS + "cotire sets this property to the full path of the generated language precompiled header binary for the target." + "Defaults to empty string." + ) + + define_property( + TARGET PROPERTY "COTIRE_UNITY_TARGET_NAME" + BRIEF_DOCS "The name of the generated unity build target corresponding to this target." + FULL_DOCS + "This property can be set to the desired name of the unity target that will be created by cotire." + "If not set, the unity target name will be set to this target's name with the suffix _unity appended." + "After this target has been processed by cotire, the property is set to the actual name of the generated unity target." + "Defaults to empty string." + ) + + # define cotire source properties + + define_property( + SOURCE PROPERTY "COTIRE_EXCLUDED" + BRIEF_DOCS "Do not modify source file's build command." + FULL_DOCS + "If this property is set to TRUE, the source file's build command will not be modified to make use of the precompiled header." + "The source file will also be excluded from the generated unity source file." + "Source files that have their COMPILE_FLAGS property set will be excluded by default." + "Defaults to FALSE." + ) + + define_property( + SOURCE PROPERTY "COTIRE_DEPENDENCY" + BRIEF_DOCS "Add this source file to dependencies of the automatically generated prefix header file." + FULL_DOCS + "If this property is set to TRUE, the source file is added to dependencies of the generated prefix header file." + "If the file is modified, cotire will re-generate the prefix header source upon build." + "Defaults to FALSE." + ) + + define_property( + SOURCE PROPERTY "COTIRE_UNITY_SOURCE_PRE_UNDEFS" + BRIEF_DOCS "Preprocessor undefs to place in the generated unity source file before the inclusion of this source file." + FULL_DOCS + "This may be set to a semicolon-separated list of preprocessor symbols." + "cotire will add corresponding #undef directives to the generated unit source file before this file is included." + "Defaults to empty string." + ) + + define_property( + SOURCE PROPERTY "COTIRE_UNITY_SOURCE_POST_UNDEFS" + BRIEF_DOCS "Preprocessor undefs to place in the generated unity source file after the inclusion of this source file." + FULL_DOCS + "This may be set to a semicolon-separated list of preprocessor symbols." + "cotire will add corresponding #undef directives to the generated unit source file after this file is included." + "Defaults to empty string." + ) + + define_property( + SOURCE PROPERTY "COTIRE_START_NEW_UNITY_SOURCE" + BRIEF_DOCS "Start a new unity source file which includes this source file as the first one." + FULL_DOCS + "If this property is set to TRUE, cotire will complete the current unity file and start a new one." + "The new unity source file will include this source file as the first one." + "This property essentially works as a separator for unity source files." + "Defaults to FALSE." + ) + + define_property( + SOURCE PROPERTY "COTIRE_TARGET" + BRIEF_DOCS "Read-only property. Mark this source file as cotired for the given target." + FULL_DOCS + "cotire sets this property to the name of target, that the source file's build command has been altered for." + "Defaults to empty string." + ) + + message (STATUS "cotire ${COTIRE_CMAKE_MODULE_VERSION} loaded.") + +endif() diff --git a/cmake/git_watcher.cmake b/cmake/git_watcher.cmake new file mode 100644 index 0000000..60be06b --- /dev/null +++ b/cmake/git_watcher.cmake @@ -0,0 +1,295 @@ +# git_watcher.cmake +# https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/git_watcher.cmake +# +# Released under the MIT License. +# https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/LICENSE + + +# This file defines a target that monitors the state of a git repo. +# If the state changes (e.g. a commit is made), then a file gets reconfigured. +# Here are the primary variables that control script behavior: +# +# PRE_CONFIGURE_FILE (REQUIRED) +# -- The path to the file that'll be configured. +# +# POST_CONFIGURE_FILE (REQUIRED) +# -- The path to the configured PRE_CONFIGURE_FILE. +# +# GIT_STATE_FILE (OPTIONAL) +# -- The path to the file used to store the previous build's git state. +# Defaults to the current binary directory. +# +# GIT_WORKING_DIR (OPTIONAL) +# -- The directory from which git commands will be run. +# Defaults to the directory with the top level CMakeLists.txt. +# +# GIT_EXECUTABLE (OPTIONAL) +# -- The path to the git executable. It'll automatically be set if the +# user doesn't supply a path. +# +# DESIGN +# - This script was designed similar to a Python application +# with a Main() function. I wanted to keep it compact to +# simplify "copy + paste" usage. +# +# - This script is invoked under two CMake contexts: +# 1. Configure time (when build files are created). +# 2. Build time (called via CMake -P). +# The first invocation is what registers the script to +# be executed at build time. +# +# MODIFICATIONS +# You may wish to track other git properties like when the last +# commit was made. There are two sections you need to modify, +# and they're tagged with a ">>>" header. + +# Short hand for converting paths to absolute. +macro(PATH_TO_ABSOLUTE var_name) + get_filename_component(${var_name} "${${var_name}}" ABSOLUTE) +endmacro() + +# Check that a required variable is set. +macro(CHECK_REQUIRED_VARIABLE var_name) + if(NOT DEFINED ${var_name}) + message(FATAL_ERROR "The \"${var_name}\" variable must be defined.") + endif() + PATH_TO_ABSOLUTE(${var_name}) +endmacro() + +# Check that an optional variable is set, or, set it to a default value. +macro(CHECK_OPTIONAL_VARIABLE var_name default_value) + if(NOT DEFINED ${var_name}) + set(${var_name} ${default_value}) + endif() + PATH_TO_ABSOLUTE(${var_name}) +endmacro() + +CHECK_REQUIRED_VARIABLE(PRE_CONFIGURE_FILE) +CHECK_REQUIRED_VARIABLE(POST_CONFIGURE_FILE) +CHECK_OPTIONAL_VARIABLE(GIT_STATE_FILE "${CMAKE_BINARY_DIR}/git-state-hash") +CHECK_OPTIONAL_VARIABLE(GIT_WORKING_DIR "${CMAKE_SOURCE_DIR}") + +# Check the optional git variable. +# If it's not set, we'll try to find it using the CMake packaging system. +if(NOT DEFINED GIT_EXECUTABLE) + find_package(Git QUIET) + if (NOT Git_FOUND) + return() + endif() +endif() +CHECK_REQUIRED_VARIABLE(GIT_EXECUTABLE) + + +set(_state_variable_names + GIT_RETRIEVED_STATE + GIT_HEAD_SHA1 + GIT_IS_DIRTY + GIT_AUTHOR_NAME + GIT_AUTHOR_EMAIL + GIT_COMMIT_DATE_ISO8601 + GIT_COMMIT_SUBJECT + GIT_COMMIT_BODY + GIT_DESCRIBE + GIT_SHORT_SHA1 + # >>> + # 1. Add the name of the additional git variable you're interested in monitoring + # to this list. + ) + + + +# Macro: RunGitCommand +# Description: short-hand macro for calling a git function. Outputs are the +# "exit_code" and "output" variables. +macro(RunGitCommand) + execute_process(COMMAND + "${GIT_EXECUTABLE}" ${ARGV} + WORKING_DIRECTORY "${_working_dir}" + RESULT_VARIABLE exit_code + OUTPUT_VARIABLE output + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT exit_code EQUAL 0) + set(ENV{GIT_RETRIEVED_STATE} "false") + endif() +endmacro() + + + +# Function: GetGitState +# Description: gets the current state of the git repo. +# Args: +# _working_dir (in) string; the directory from which git commands will be executed. +function(GetGitState _working_dir) + + # This is an error code that'll be set to FALSE if the + # RunGitCommand ever returns a non-zero exit code. + set(ENV{GIT_RETRIEVED_STATE} "true") + + # Get whether or not the working tree is dirty. + RunGitCommand(status --porcelain) + if(NOT exit_code EQUAL 0) + set(ENV{GIT_IS_DIRTY} "false") + else() + if(NOT "${output}" STREQUAL "") + set(ENV{GIT_IS_DIRTY} "true") + else() + set(ENV{GIT_IS_DIRTY} "false") + endif() + endif() + + # There's a long list of attributes grabbed from git show. + set(object HEAD) + RunGitCommand(show -s "--format=%H" ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_HEAD_SHA1} ${output}) + endif() + + RunGitCommand(show -s "--format=%an" ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_AUTHOR_NAME} "${output}") + endif() + + RunGitCommand(show -s "--format=%ae" ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_AUTHOR_EMAIL} "${output}") + endif() + + RunGitCommand(show -s "--format=%cI" ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_COMMIT_DATE_ISO8601} "${output}") + endif() + + RunGitCommand(show -s "--format=%s" ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_COMMIT_SUBJECT} "${output}") + endif() + + RunGitCommand(show -s "--format=%b" ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_COMMIT_BODY} "${output}") + endif() + + RunGitCommand(describe --tags "--match=v*" ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_DESCRIBE} "${output}") + endif() + + RunGitCommand(rev-parse --short ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_SHORT_SHA1} ${output}) + endif() + + # >>> + # 2. Additional git properties can be added here via the + # "execute_process()" command. Be sure to set them in + # the environment using the same variable name you added + # to the "_state_variable_names" list. + +endfunction() + + + +# Function: GitStateChangedAction +# Description: this function is executed when the state of the git +# repository changes (e.g. a commit is made). +function(GitStateChangedAction) + foreach(var_name ${_state_variable_names}) + set(${var_name} $ENV{${var_name}}) + endforeach() + configure_file("${PRE_CONFIGURE_FILE}" "${POST_CONFIGURE_FILE}" @ONLY) +endfunction() + + + +# Function: HashGitState +# Description: loop through the git state variables and compute a unique hash. +# Args: +# _state (out) string; a hash computed from the current git state. +function(HashGitState _state) + set(ans "") + foreach(var_name ${_state_variable_names}) + string(SHA256 ans "${ans}$ENV{${var_name}}") + endforeach() + set(${_state} ${ans} PARENT_SCOPE) +endfunction() + + + +# Function: CheckGit +# Description: check if the git repo has changed. If so, update the state file. +# Args: +# _working_dir (in) string; the directory from which git commands will be ran. +# _state_changed (out) bool; whether or no the state of the repo has changed. +function(CheckGit _working_dir _state_changed) + + # Get the current state of the repo. + GetGitState("${_working_dir}") + + # Convert that state into a hash that we can compare against + # the hash stored on-disk. + HashGitState(state) + + # Check if the state has changed compared to the backup on disk. + if(EXISTS "${GIT_STATE_FILE}") + file(READ "${GIT_STATE_FILE}" OLD_HEAD_CONTENTS) + if(OLD_HEAD_CONTENTS STREQUAL "${state}") + # State didn't change. + set(${_state_changed} "false" PARENT_SCOPE) + return() + endif() + endif() + + # The state has changed. + # We need to update the state file on disk. + # Future builds will compare their state to this file. + file(WRITE "${GIT_STATE_FILE}" "${state}") + set(${_state_changed} "true" PARENT_SCOPE) +endfunction() + + + +# Function: SetupGitMonitoring +# Description: this function sets up custom commands that make the build system +# check the state of git before every build. If the state has +# changed, then a file is configured. +function(SetupGitMonitoring) + add_custom_target(check_git + ALL + DEPENDS ${PRE_CONFIGURE_FILE} + BYPRODUCTS + ${POST_CONFIGURE_FILE} + ${GIT_STATE_FILE} + COMMENT "Checking the git repository for changes..." + COMMAND + ${CMAKE_COMMAND} + -D_BUILD_TIME_CHECK_GIT=TRUE + -DGIT_WORKING_DIR=${GIT_WORKING_DIR} + -DGIT_EXECUTABLE=${GIT_EXECUTABLE} + -DGIT_STATE_FILE=${GIT_STATE_FILE} + -DPRE_CONFIGURE_FILE=${PRE_CONFIGURE_FILE} + -DPOST_CONFIGURE_FILE=${POST_CONFIGURE_FILE} + -P "${CMAKE_CURRENT_LIST_FILE}") +endfunction() + + + +# Function: Main +# Description: primary entry-point to the script. Functions are selected based +# on whether it's configure or build time. +function(Main) + if(_BUILD_TIME_CHECK_GIT) + # Check if the repo has changed. + # If so, run the change action. + CheckGit("${GIT_WORKING_DIR}" changed) + if(changed OR NOT EXISTS "${POST_CONFIGURE_FILE}") + GitStateChangedAction() + endif() + else() + # >> Executes at configure time. + SetupGitMonitoring() + endif() +endfunction() + +# And off we go... +Main() diff --git a/cmake/gitmetadata.h.in b/cmake/gitmetadata.h.in new file mode 100644 index 0000000..7e15a1e --- /dev/null +++ b/cmake/gitmetadata.h.in @@ -0,0 +1,20 @@ +// This file was created automatically by CMake. +#pragma once + +// Whether or not we retrieved the state of the repo. +#define GIT_RETRIEVED_STATE @GIT_RETRIEVED_STATE@ + +// git describe --tags --match 'v*' +#define GIT_DESCRIBE "@GIT_DESCRIBE@" + +// The SHA1 for the HEAD of the repo. +#define GIT_HEAD_SHA1 "@GIT_HEAD_SHA1@" + +// git rev-parse --short HEAD +#define GIT_SHORT_SHA1 "@GIT_SHORT_SHA1@" + +// Whether or not there were uncommited changes present. +#define GIT_IS_DIRTY @GIT_IS_DIRTY@ + +// When HEAD was committed. +#define GIT_COMMIT_DATE_ISO8601 "@GIT_COMMIT_DATE_ISO8601@" diff --git a/data/XML/groups.xml b/data/XML/groups.xml new file mode 100644 index 0000000..2266ae4 --- /dev/null +++ b/data/XML/groups.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/XML/mounts.xml b/data/XML/mounts.xml new file mode 100644 index 0000000..625190c --- /dev/null +++ b/data/XML/mounts.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/XML/outfits.xml b/data/XML/outfits.xml new file mode 100644 index 0000000..e6c4329 --- /dev/null +++ b/data/XML/outfits.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/XML/quests.xml b/data/XML/quests.xml new file mode 100644 index 0000000..38e5ca4 --- /dev/null +++ b/data/XML/quests.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/data/XML/stages.xml b/data/XML/stages.xml new file mode 100644 index 0000000..06b639c --- /dev/null +++ b/data/XML/stages.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/data/XML/vocations.xml b/data/XML/vocations.xml new file mode 100644 index 0000000..6ddebd0 --- /dev/null +++ b/data/XML/vocations.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/actions/actions.xml b/data/actions/actions.xml new file mode 100644 index 0000000..9f871e6 --- /dev/null +++ b/data/actions/actions.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/actions/lib/actions.lua b/data/actions/lib/actions.lua new file mode 100644 index 0000000..9c8d8e4 --- /dev/null +++ b/data/actions/lib/actions.lua @@ -0,0 +1,254 @@ +local wildGrowth = {1499, 11099} -- wild growth destroyable by machete +local jungleGrass = { -- grass destroyable by machete + [2782] = 2781, + [3985] = 3984, + [19433] = 19431 +} +local groundIds = {354, 355} -- pick usable ground +local sandIds = {231, 9059} -- desert sand +local holeId = { -- usable rope holes, for rope spots see global.lua + 294, 369, 370, 383, 392, 408, 409, 410, 427, 428, 429, 430, 462, 469, 470, 482, + 484, 485, 489, 924, 1369, 3135, 3136, 4835, 4837, 7933, 7938, 8170, 8249, 8250, + 8251, 8252, 8254, 8255, 8256, 8276, 8277, 8279, 8281, 8284, 8285, 8286, 8323, + 8567, 8585, 8595, 8596, 8972, 9606, 9625, 13190, 14461, 19519, 21536, 23713, + 26020 +} +local holes = {468, 481, 483, 7932, 23712} -- holes opened by shovel +local fruits = {2673, 2674, 2675, 2676, 2677, 2678, 2679, 2680, 2681, 2682, 2684, 2685, 5097, 8839, 8840, 8841} -- fruits to make decorated cake with knife + +function destroyItem(player, target, toPosition) + if type(target) ~= "userdata" or not target:isItem() then + return false + end + + if target:hasAttribute(ITEM_ATTRIBUTE_UNIQUEID) or target:hasAttribute(ITEM_ATTRIBUTE_ACTIONID) then + return false + end + + if toPosition.x == CONTAINER_POSITION then + player:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + return true + end + + local destroyId = ItemType(target.itemid):getDestroyId() + if destroyId == 0 then + return false + end + + if math.random(7) == 1 then + local item = Game.createItem(destroyId, 1, toPosition) + if item then + item:decay() + end + + -- Move items outside the container + if target:isContainer() then + for i = target:getSize() - 1, 0, -1 do + local containerItem = target:getItem(i) + if containerItem then + containerItem:moveTo(toPosition) + end + end + end + + target:remove(1) + end + + toPosition:sendMagicEffect(CONST_ME_POFF) + return true +end + +function onUseMachete(player, item, fromPosition, target, toPosition, isHotkey) + local targetId = target.itemid + if not targetId then + return true + end + + if table.contains(wildGrowth, targetId) then + toPosition:sendMagicEffect(CONST_ME_POFF) + target:remove() + return true + end + + local grass = jungleGrass[targetId] + if grass then + target:transform(grass) + target:decay() + player:addAchievementProgress("Nothing Can Stop Me", 100) + return true + end + + return destroyItem(player, target, toPosition) +end + +function onUsePick(player, item, fromPosition, target, toPosition, isHotkey) + if target.itemid == 11227 then -- shiny stone refining + local chance = math.random(1, 100) + if chance == 1 then + player:addItem(ITEM_CRYSTAL_COIN) -- 1% chance of getting crystal coin + elseif chance <= 6 then + player:addItem(ITEM_GOLD_COIN) -- 5% chance of getting gold coin + elseif chance <= 51 then + player:addItem(ITEM_PLATINUM_COIN) -- 45% chance of getting platinum coin + else + player:addItem(2145) -- 49% chance of getting small diamond + end + player:addAchievementProgress("Petrologist", 100) + target:getPosition():sendMagicEffect(CONST_ME_BLOCKHIT) + target:remove(1) + return true + end + + local tile = Tile(toPosition) + if not tile then + return false + end + + local ground = tile:getGround() + if not ground then + return false + end + + if table.contains(groundIds, ground.itemid) and ground.actionid == actionIds.pickHole then + ground:transform(392) + ground:decay() + + toPosition.z = toPosition.z + 1 + tile:relocateTo(toPosition) + end + + return true +end + +function onUseRope(player, item, fromPosition, target, toPosition, isHotkey) + local tile = Tile(toPosition) + if not tile then + return false + end + + if table.contains(ropeSpots, tile:getGround():getId()) or tile:getItemById(14435) then + if Tile(toPosition:moveUpstairs()):hasFlag(TILESTATE_PROTECTIONZONE) and player:isPzLocked() then + player:sendCancelMessage(RETURNVALUE_PLAYERISPZLOCKED) + return true + end + player:teleportTo(toPosition, false) + return true + elseif table.contains(holeId, target.itemid) then + toPosition.z = toPosition.z + 1 + tile = Tile(toPosition) + if tile then + local thing = tile:getTopVisibleThing() + if thing:isPlayer() then + if Tile(toPosition:moveUpstairs()):hasFlag(TILESTATE_PROTECTIONZONE) and thing:isPzLocked() then + return false + end + return thing:teleportTo(toPosition, false) + end + if thing:isItem() and thing:getType():isMovable() then + return thing:moveTo(toPosition:moveUpstairs()) + end + end + player:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + return true + end + return false +end + +function onUseShovel(player, item, fromPosition, target, toPosition, isHotkey) + local tile = Tile(toPosition) + if not tile then + return false + end + + local ground = tile:getGround() + if not ground then + return false + end + + local groundId = ground:getId() + if table.contains(holes, groundId) then + ground:transform(groundId + 1) + ground:decay() + + toPosition.z = toPosition.z + 1 + tile:relocateTo(toPosition) + player:addAchievementProgress("The Undertaker", 500) + elseif target.itemid == 20230 then -- swamp digging + if (player:getStorageValue(PlayerStorageKeys.swampDigging)) <= os.time() then + local chance = math.random(100) + if chance >= 1 and chance <= 42 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You dug up a dead snake.") + player:addItem(3077) + elseif chance >= 43 and chance <= 79 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You dug up a small diamond.") + player:addItem(2145) + elseif chance >= 80 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You dug up a leech.") + player:addItem(20138) + end + player:setStorageValue(PlayerStorageKeys.swampDigging, os.time() + 7 * 24 * 60 * 60) + player:getPosition():sendMagicEffect(CONST_ME_GREEN_RINGS) + end + elseif table.contains(sandIds, groundId) then + local randomValue = math.random(1, 100) + if target.actionid == actionIds.sandHole and randomValue <= 20 then + ground:transform(489) + ground:decay() + elseif randomValue == 1 then + Game.createItem(2159, 1, toPosition) + player:addAchievementProgress("Gold Digger", 100) + elseif randomValue > 95 then + Game.createMonster("Scarab", toPosition) + end + toPosition:sendMagicEffect(CONST_ME_POFF) + else + return false + end + + return true +end + +function onUseScythe(player, item, fromPosition, target, toPosition, isHotkey) + if not table.contains({2550, 10513}, item.itemid) then + return false + end + + if target.itemid == 2739 then -- wheat + target:transform(2737) + target:decay() + Game.createItem(2694, 1, toPosition) -- bunch of wheat + player:addAchievementProgress("Happy Farmer", 200) + return true + end + if target.itemid == 5465 then -- burning sugar cane + target:transform(5464) + target:decay() + Game.createItem(5467, 1, toPosition) -- bunch of sugar cane + player:addAchievementProgress("Natural Sweetener", 50) + return true + end + return destroyItem(player, target, toPosition) +end + +function onUseCrowbar(player, item, fromPosition, target, toPosition, isHotkey) + if not table.contains({2416, 10515}, item.itemid) then + return false + end + + return destroyItem(player, target, toPosition) +end + +function onUseKitchenKnife(player, item, fromPosition, target, toPosition, isHotkey) + if not table.contains({2566, 10511, 10515}, item.itemid) then + return false + end + + if table.contains(fruits, target.itemid) and player:removeItem(6278, 1) then + target:remove(1) + player:addItem(6279, 1) + player:getPosition():sendMagicEffect(CONST_ME_MAGIC_GREEN) + return true + end + + return false +end diff --git a/data/actions/scripts/other/bed_modification_kits.lua b/data/actions/scripts/other/bed_modification_kits.lua new file mode 100644 index 0000000..4b1f0a9 --- /dev/null +++ b/data/actions/scripts/other/bed_modification_kits.lua @@ -0,0 +1,50 @@ +local beds = { + [7904] = {{7811, 7812}, {7813, 7814}}, -- green kit + [7905] = {{7819, 7820}, {7821, 7822}}, -- yellow kit + [7906] = {{7815, 7816}, {7817, 7818}}, -- red kit + [7907] = {{1754, 1755}, {1760, 1761}}, -- removal kit + [20252] = {{20197, 20198}, {20199, 20200}} -- canopy kit +} + +local function internalBedTransform(item, targetItem, toPosition, itemArray) + targetItem:transform(itemArray[1]) + targetItem:getPosition():sendMagicEffect(CONST_ME_POFF) + + Tile(toPosition):getItemByType(ITEM_TYPE_BED):transform(itemArray[2]) + toPosition:sendMagicEffect(CONST_ME_POFF) + + item:remove() +end + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local newBed = beds[item:getId()] + if not newBed or type(target) ~= "userdata" or not target:isItem() then + return false + end + + local tile = Tile(toPosition) + if not tile or not tile:getHouse() then + return false + end + + local targetItemId = target:getId() + if targetItemId == newBed[1][1] or targetItemId == newBed[2][1] then + player:sendTextMessage(MESSAGE_STATUS_SMALL, "You already have this bed modification.") + return true + end + + for _, bed in pairs(beds) do + if bed[1][1] == targetItemId or table.contains({1758, 5502, 18027}, targetItemId) then + toPosition:sendMagicEffect(CONST_ME_POFF) + toPosition.y = toPosition.y + 1 + internalBedTransform(item, target, toPosition, newBed[1]) + break + elseif bed[2][1] == targetItemId or table.contains({1756, 5500, 18029}, targetItemId) then + toPosition:sendMagicEffect(CONST_ME_POFF) + toPosition.x = toPosition.x + 1 + internalBedTransform(item, target, toPosition, newBed[2]) + break + end + end + return true +end diff --git a/data/actions/scripts/other/birdcage.lua b/data/actions/scripts/other/birdcage.lua new file mode 100644 index 0000000..bec3614 --- /dev/null +++ b/data/actions/scripts/other/birdcage.lua @@ -0,0 +1,9 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + if math.random(100) == 1 then + item:transform(2094) + player:addAchievement("Oops") + else + item:getPosition():sendMagicEffect(CONST_ME_SOUND_YELLOW) + end + return true +end diff --git a/data/actions/scripts/other/blueberrybush.lua b/data/actions/scripts/other/blueberrybush.lua new file mode 100644 index 0000000..d158f67 --- /dev/null +++ b/data/actions/scripts/other/blueberrybush.lua @@ -0,0 +1,7 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + item:transform(2786) + item:decay() + Game.createItem(2677, 3, fromPosition) + player:addAchievementProgress("Bluebarian", 500) + return true +end diff --git a/data/actions/scripts/other/changegold.lua b/data/actions/scripts/other/changegold.lua new file mode 100644 index 0000000..6af6385 --- /dev/null +++ b/data/actions/scripts/other/changegold.lua @@ -0,0 +1,19 @@ +local config = { + [ITEM_GOLD_COIN] = {changeTo = ITEM_PLATINUM_COIN}, + [ITEM_PLATINUM_COIN] = {changeBack = ITEM_GOLD_COIN, changeTo = ITEM_CRYSTAL_COIN}, + [ITEM_CRYSTAL_COIN] = {changeBack = ITEM_PLATINUM_COIN} +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local coin = config[item:getId()] + if coin.changeTo and item.type == 100 then + item:remove() + player:addItem(coin.changeTo, 1) + elseif coin.changeBack then + item:remove(1) + player:addItem(coin.changeBack, 100) + else + return false + end + return true +end diff --git a/data/actions/scripts/other/constructionkits.lua b/data/actions/scripts/other/constructionkits.lua new file mode 100644 index 0000000..1e12d56 --- /dev/null +++ b/data/actions/scripts/other/constructionkits.lua @@ -0,0 +1,31 @@ +local constructionKits = { + [3901] = 1666, [3902] = 1670, [3903] = 1652, [3904] = 1674, [3905] = 1658, + [3906] = 3813, [3907] = 3817, [3908] = 1619, [3909] = 12799, [3910] = 2105, + [3911] = 1614, [3912] = 3806, [3913] = 3807, [3914] = 3809, [3915] = 1716, + [3916] = 1724, [3917] = 1732, [3918] = 1775, [3919] = 1774, [3920] = 1750, + [3921] = 3832, [3922] = 2095, [3923] = 2098, [3924] = 2064, [3925] = 2582, + [3926] = 2117, [3927] = 1728, [3928] = 1442, [3929] = 1446, [3930] = 1447, + [3931] = 2034, [3932] = 2604, [3933] = 2080, [3934] = 2084, [3935] = 3821, + [3936] = 3811, [3937] = 2101, [3938] = 3812, [5086] = 5046, [5087] = 5055, + [5088] = 5056, [6114] = 6111, [6115] = 6109, [6372] = 6356, [6373] = 6371, + [8692] = 8688, [9974] = 9975, [11126] = 11127, [11133] = 11129, [11124] = 11125, + [11205] = 11203, [14328] = 1616, [14329] = 1615, [16075] = 16020, [16099] = 16098, + [20254] = 20295, [20255] = 20297, [20257] = 20299 +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local kit = constructionKits[item.itemid] + if not kit then + return false + end + + if fromPosition.x == CONTAINER_POSITION then + player:sendTextMessage(MESSAGE_STATUS_SMALL, "Put the construction kit on the floor first.") + elseif not Tile(fromPosition):getHouse() then + player:sendTextMessage(MESSAGE_STATUS_SMALL, "You may construct this only inside a house.") + else + item:transform(kit) + fromPosition:sendMagicEffect(CONST_ME_POFF) + end + return true +end diff --git a/data/actions/scripts/other/createbread.lua b/data/actions/scripts/other/createbread.lua new file mode 100644 index 0000000..20be59f --- /dev/null +++ b/data/actions/scripts/other/createbread.lua @@ -0,0 +1,19 @@ +local liquidContainers = {1775, 2005, 2006, 2007, 2008, 2009, 2011, 2012, 2013, 2014, 2015, 2023, 2031, 2032, 2033} +local millstones = {1381, 1382, 1383, 1384} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local itemId = item:getId() + if itemId == 2692 then + if target.type == 1 and table.contains(liquidContainers, target.itemid) then + item:remove(1) + player:addItem(2693, 1) + target:transform(target.itemid, 0) + return true + end + elseif table.contains(millstones, target.itemid) then + item:remove(1) + player:addItem(2692, 1) + return true + end + return false +end diff --git a/data/actions/scripts/other/decayto.lua b/data/actions/scripts/other/decayto.lua new file mode 100644 index 0000000..fa0afd9 --- /dev/null +++ b/data/actions/scripts/other/decayto.lua @@ -0,0 +1,28 @@ +-- keep in sync with actions.xml +local decayItems = { + [1479] = 1480, [1480] = 1479, [1634] = 1635, [1635] = 1634, [1636] = 1637, + [1637] = 1636, [1638] = 1639, [1639] = 1638, [1640] = 1641, [1641] = 1640, + [1786] = 1787, [1787] = 1786, [1788] = 1789, [1789] = 1788, [1790] = 1791, + [1791] = 1790, [1792] = 1793, [1793] = 1792, [1873] = 1874, [1874] = 1873, + [1875] = 1876, [1876] = 1875, [1945] = 1946, [1946] = 1945, [2037] = 2038, + [2038] = 2037, [2039] = 2040, [2040] = 2039, [2041] = 2042, [2042] = 2041, + [2044] = 2045, [2045] = 2044, [2047] = 2048, [2048] = 2047, [2050] = 2051, + [2051] = 2050, [2052] = 2053, [2053] = 2052, [2054] = 2055, [2055] = 2054, + [2058] = 2059, [2059] = 2058, [2060] = 2061, [2061] = 2060, [2064] = 2065, + [2065] = 2064, [2066] = 2067, [2067] = 2066, [2068] = 2069, [2069] = 2068, + [2096] = 2097, [2097] = 2096, [2162] = 2163, [2163] = 2162, [2578] = 2579, + [3947] = 3948, [3948] = 3947, [5812] = 5813, [5813] = 5812, [6489] = 6490, + [6490] = 6489, [6572] = 6573, [7058] = 7059, [7059] = 7058, [7183] = 7184, + [8684] = 8685, [8685] = 8684, [8686] = 8687, [8687] = 8686, [8688] = 8689, + [8689] = 8688, [8690] = 8691, [8691] = 8690, [9575] = 9576, [9576] = 9575, + [9577] = 9578, [9578] = 9577, [9579] = 9580, [9580] = 9579, [9581] = 9582, + [9582] = 9581, [9747] = 9748, [9748] = 9747, [9749] = 9750, [9750] = 9749, + [10719] = 10720, [11401] = 11402, [19691] = 19692, [19692] = 19691, [25545] = 25546, + [26098] = 26099 +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + item:transform(decayItems[item.itemid]) + item:decay() + return true +end diff --git a/data/actions/scripts/other/destroy.lua b/data/actions/scripts/other/destroy.lua new file mode 100644 index 0000000..0499f62 --- /dev/null +++ b/data/actions/scripts/other/destroy.lua @@ -0,0 +1,3 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + return destroyItem(player, target, toPosition) +end diff --git a/data/actions/scripts/other/die.lua b/data/actions/scripts/other/die.lua new file mode 100644 index 0000000..86505a5 --- /dev/null +++ b/data/actions/scripts/other/die.lua @@ -0,0 +1,15 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local position = item:getPosition() + local value = math.random(1, 6) + local isInGhostMode = player:isInGhostMode() + + position:sendMagicEffect(CONST_ME_CRAPS, isInGhostMode and player) + + local spectators = Game.getSpectators(position, false, true, 3, 3) + for _, spectator in ipairs(spectators) do + player:say(player:getName() .. " rolled a " .. value .. ".", TALKTYPE_MONSTER_SAY, isInGhostMode, spectator, position) + end + + item:transform(5791 + value) + return true +end diff --git a/data/actions/scripts/other/doors.lua b/data/actions/scripts/other/doors.lua new file mode 100644 index 0000000..36ba6ec --- /dev/null +++ b/data/actions/scripts/other/doors.lua @@ -0,0 +1,65 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local itemId = item:getId() + if table.contains(questDoors, itemId) then + if player:getStorageValue(item.actionid) ~= -1 then + item:transform(itemId + 1) + player:teleportTo(toPosition, true) + else + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The door seems to be sealed against unwanted intruders.") + end + return true + elseif table.contains(levelDoors, itemId) then + if item.actionid > 0 and player:getLevel() >= item.actionid - actionIds.levelDoor then + item:transform(itemId + 1) + player:teleportTo(toPosition, true) + else + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Only the worthy may pass.") + end + return true + elseif table.contains(keys, itemId) then + if target.actionid > 0 then + if item.actionid == target.actionid and doors[target.itemid] then + target:transform(doors[target.itemid]) + return true + end + player:sendTextMessage(MESSAGE_STATUS_SMALL, "The key does not match.") + return true + end + return false + end + + if table.contains(horizontalOpenDoors, itemId) or table.contains(verticalOpenDoors, itemId) then + local doorCreature = Tile(toPosition):getTopCreature() + if doorCreature then + toPosition.x = toPosition.x + 1 + local query = Tile(toPosition):queryAdd(doorCreature, bit.bor(FLAG_IGNOREBLOCKCREATURE, FLAG_PATHFINDING)) + if query ~= RETURNVALUE_NOERROR then + toPosition.x = toPosition.x - 1 + toPosition.y = toPosition.y + 1 + query = Tile(toPosition):queryAdd(doorCreature, bit.bor(FLAG_IGNOREBLOCKCREATURE, FLAG_PATHFINDING)) + end + + if query ~= RETURNVALUE_NOERROR then + player:sendTextMessage(MESSAGE_STATUS_SMALL, Game.getReturnMessage(query)) + return true + end + + doorCreature:teleportTo(toPosition, true) + end + + if not table.contains(openSpecialDoors, itemId) then + item:transform(itemId - 1) + end + return true + end + + if doors[itemId] then + if item.actionid == 0 then + item:transform(doors[itemId]) + else + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "It is locked.") + end + return true + end + return false +end diff --git a/data/actions/scripts/other/enchanting.lua b/data/actions/scripts/other/enchanting.lua new file mode 100644 index 0000000..f302eb9 --- /dev/null +++ b/data/actions/scripts/other/enchanting.lua @@ -0,0 +1,210 @@ +local items = { + equipment = { + [2147] = { -- small ruby + [COMBAT_FIREDAMAGE] = {id = 2343, targetId = 2147} -- helmet of the ancients (enchanted) + }, + [2383] = { -- spike sword + [COMBAT_FIREDAMAGE] = {id = 7744}, [COMBAT_ICEDAMAGE] = {id = 7763}, + [COMBAT_EARTHDAMAGE] = {id = 7854}, [COMBAT_ENERGYDAMAGE] = {id = 7869} + }, + [2391] = { -- war hammer + [COMBAT_FIREDAMAGE] = {id = 7758}, [COMBAT_ICEDAMAGE] = {id = 7777}, + [COMBAT_EARTHDAMAGE] = {id = 7868}, [COMBAT_ENERGYDAMAGE] = {id = 7883} + }, + [2423] = { -- clerical mace + [COMBAT_FIREDAMAGE] = {id = 7754}, [COMBAT_ICEDAMAGE] = {id = 7773}, + [COMBAT_EARTHDAMAGE] = {id = 7864}, [COMBAT_ENERGYDAMAGE] = {id = 7879} + }, + [2429] = { -- barbarian axe + [COMBAT_FIREDAMAGE] = {id = 7749}, [COMBAT_ICEDAMAGE] = {id = 7768}, + [COMBAT_EARTHDAMAGE] = {id = 7859}, [COMBAT_ENERGYDAMAGE] = {id = 7874} + }, + [2430] = { -- knight axe + [COMBAT_FIREDAMAGE] = {id = 7750}, [COMBAT_ICEDAMAGE] = {id = 7769}, + [COMBAT_EARTHDAMAGE] = {id = 7860}, [COMBAT_ENERGYDAMAGE] = {id = 7875} + }, + [2445] = { -- crystal mace + [COMBAT_FIREDAMAGE] = {id = 7755}, [COMBAT_ICEDAMAGE] = {id = 7774}, + [COMBAT_EARTHDAMAGE] = {id = 7865}, [COMBAT_ENERGYDAMAGE] = {id = 7880} + }, + [2454] = { -- war axe + [COMBAT_FIREDAMAGE] = {id = 7753}, [COMBAT_ICEDAMAGE] = {id = 7772}, + [COMBAT_EARTHDAMAGE] = {id = 7863}, [COMBAT_ENERGYDAMAGE] = {id = 7878} + }, + [7380] = { -- headchopper + [COMBAT_FIREDAMAGE] = {id = 7752}, [COMBAT_ICEDAMAGE] = {id = 7771}, + [COMBAT_EARTHDAMAGE] = {id = 7862}, [COMBAT_ENERGYDAMAGE] = {id = 7877} + }, + [7383] = { -- relic sword + [COMBAT_FIREDAMAGE] = {id = 7745}, [COMBAT_ICEDAMAGE] = {id = 7764}, + [COMBAT_EARTHDAMAGE] = {id = 7855}, [COMBAT_ENERGYDAMAGE] = {id = 7870} + }, + [7384] = { -- mystic blade + [COMBAT_FIREDAMAGE] = {id = 7746}, [COMBAT_ICEDAMAGE] = {id = 7765}, + [COMBAT_EARTHDAMAGE] = {id = 7856}, [COMBAT_ENERGYDAMAGE] = {id = 7871} + }, + [7389] = { -- heroic axe + [COMBAT_FIREDAMAGE] = {id = 7751}, [COMBAT_ICEDAMAGE] = {id = 7770}, + [COMBAT_EARTHDAMAGE] = {id = 7861}, [COMBAT_ENERGYDAMAGE] = {id = 7876} + }, + [7392] = { -- orcish maul + [COMBAT_FIREDAMAGE] = {id = 7757}, [COMBAT_ICEDAMAGE] = {id = 7776}, + [COMBAT_EARTHDAMAGE] = {id = 7867}, [COMBAT_ENERGYDAMAGE] = {id = 7882} + }, + [7402] = { -- dragon slayer + [COMBAT_FIREDAMAGE] = {id = 7748}, [COMBAT_ICEDAMAGE] = {id = 7767}, + [COMBAT_EARTHDAMAGE] = {id = 7858}, [COMBAT_ENERGYDAMAGE] = {id = 7873} + }, + [7406] = { -- blacksteel sword + [COMBAT_FIREDAMAGE] = {id = 7747}, [COMBAT_ICEDAMAGE] = {id = 7766}, + [COMBAT_EARTHDAMAGE] = {id = 7857}, [COMBAT_ENERGYDAMAGE] = {id = 7872} + }, + [7415] = { -- cranial basher + [COMBAT_FIREDAMAGE] = {id = 7756}, [COMBAT_ICEDAMAGE] = {id = 7775}, + [COMBAT_EARTHDAMAGE] = {id = 7866}, [COMBAT_ENERGYDAMAGE] = {id = 7881} + }, + [8905] = { -- rainbow shield + [COMBAT_FIREDAMAGE] = {id = 8906}, [COMBAT_ICEDAMAGE] = {id = 8907}, + [COMBAT_EARTHDAMAGE] = {id = 8909}, [COMBAT_ENERGYDAMAGE] = {id = 8908} + }, + [9949] = { -- dracoyle statue + [COMBAT_EARTHDAMAGE] = {id = 9948} -- dracoyle statue (enchanted) + }, + [9954] = { -- dracoyle statue + [COMBAT_EARTHDAMAGE] = {id = 9953} -- dracoyle statue (enchanted) + }, + [10022] = { -- worn firewalker boots + [COMBAT_FIREDAMAGE] = {id = 9933, say = {text = "Take the boots off first."}}, + slot = {type = CONST_SLOT_FEET, check = true} + }, + [24716] = { -- werewolf amulet + [COMBAT_NONE] = { + id = 24717, + effects = {failure = CONST_ME_POFF, success = CONST_ME_THUNDER}, + message = {text = "The amulet cannot be enchanted while worn."} + }, + slot = {type = CONST_SLOT_NECKLACE, check = true} + }, + [24718] = { -- werewolf helmet + [COMBAT_NONE] = { + id = { + [SKILL_CLUB] = {id = 24783}, + [SKILL_SWORD] = {id = 24783}, + [SKILL_AXE] = {id = 24783}, + [SKILL_DISTANCE] = {id = 24783}, + [SKILL_MAGLEVEL] = {id = 24783} + }, + effects = {failure = CONST_ME_POFF, success = CONST_ME_THUNDER}, + message = {text = "The helmet cannot be enchanted while worn."}, + usesStorage = true + }, + slot = {type = CONST_SLOT_HEAD, check = true} + }, + charges = 1000, effect = CONST_ME_MAGIC_RED + }, + + valuables = { + [2146] = {id = 7759, shrine = {7508, 7509, 7510, 7511}}, -- small sapphire + [2147] = {id = 7760, shrine = {7504, 7505, 7506, 7507}}, -- small ruby + [2149] = {id = 7761, shrine = {7516, 7517, 7518, 7519}}, -- small emerald + [2150] = {id = 7762, shrine = {7512, 7513, 7514, 7515}}, -- small amethyst + soul = 2, mana = 300, effect = CONST_ME_HOLYDAMAGE + }, + + [2342] = {combatType = COMBAT_FIREDAMAGE, targetId = 2147}, -- helmet of the ancients + [7759] = {combatType = COMBAT_ICEDAMAGE}, -- small enchanted sapphire + [7760] = {combatType = COMBAT_FIREDAMAGE}, -- small enchanted ruby + [7761] = {combatType = COMBAT_EARTHDAMAGE}, -- small enchanted emerald + [7762] = {combatType = COMBAT_ENERGYDAMAGE}, -- small enchanted amethyst + [24739] = {combatType = COMBAT_NONE} -- moonlight crystals +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + if not target or not target:isItem() then + return false + end + + local itemId, targetId = item:getId(), target:getId() + local targetType = items.valuables[itemId] or items.equipment[items[itemId].targetId or targetId] + if not targetType then + return false + end + + if targetType.shrine then + if not table.contains(targetType.shrine, targetId) then + player:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + return true + end + + if player:getMana() < items.valuables.mana then + player:sendCancelMessage(RETURNVALUE_NOTENOUGHMANA) + return true + end + + if player:getSoul() < items.valuables.soul then + player:sendCancelMessage(RETURNVALUE_NOTENOUGHSOUL) + return true + end + player:addSoul(-items.valuables.soul) + player:addMana(-items.valuables.mana) + player:addManaSpent(items.valuables.mana) + player:addItem(targetType.id) + player:getPosition():sendMagicEffect(items.valuables.effect) + item:remove(1) + else + local targetItem = targetType[items[itemId].combatType] + if not targetItem or targetItem.targetId and targetItem.targetId ~= targetId then + return false + end + + local isInSlot = targetType.slot and targetType.slot.check and target:getType():usesSlot(targetType.slot.type) and Player(target:getParent()) + if isInSlot then + if targetItem.say then + player:say(targetItem.say.text, TALKTYPE_MONSTER_SAY) + return true + elseif targetItem.message then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, targetItem.message.text) + else + return false + end + else + if targetItem.targetId then + item:transform(targetItem.id) + item:decay() + target:remove(1) + else + if targetItem.usesStorage then + local vocationId = player:getVocation():getDemotion():getId() + local storage = storages[itemId] and storages[itemId][targetId] and storages[itemId][targetId][vocationId] + if not storage then + return false + end + + local storageValue = player:getStorageValue(storage.key) + if storageValue == -1 then + return false + end + + local transform = targetItem.id and targetItem.id[storageValue] + if not transform then + return false + end + target:transform(transform.id) + else + target:transform(targetItem.id) + end + + if target:hasAttribute(ITEM_ATTRIBUTE_DURATION) then + target:decay() + end + + if target:hasAttribute(ITEM_ATTRIBUTE_CHARGES) then + target:setAttribute(ITEM_ATTRIBUTE_CHARGES, items.equipment.charges) + end + item:remove(1) + end + end + player:getPosition():sendMagicEffect(targetItem.effects and (isInSlot and targetItem.effects.failure or targetItem.effects.success) or items.equipment.effect) + end + return true +end diff --git a/data/actions/scripts/other/fireworksrocket.lua b/data/actions/scripts/other/fireworksrocket.lua new file mode 100644 index 0000000..fc33c4a --- /dev/null +++ b/data/actions/scripts/other/fireworksrocket.lua @@ -0,0 +1,13 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + if fromPosition.x ~= CONTAINER_POSITION then + fromPosition:sendMagicEffect(math.random(CONST_ME_FIREWORK_YELLOW, CONST_ME_FIREWORK_BLUE)) + else + local position = player:getPosition() + position:sendMagicEffect(CONST_ME_FIREAREA) + player:say("Ouch! Rather place it on the ground next time.", TALKTYPE_MONSTER_SAY) + doTargetCombat(0, player, COMBAT_PHYSICALDAMAGE, -10, -10, CONST_ME_EXPLOSIONAREA) + end + player:addAchievementProgress("Fireworks in the Sky", 250) + item:remove() + return true +end diff --git a/data/actions/scripts/other/fluids.lua b/data/actions/scripts/other/fluids.lua new file mode 100644 index 0000000..015f390 --- /dev/null +++ b/data/actions/scripts/other/fluids.lua @@ -0,0 +1,85 @@ +local drunk = Condition(CONDITION_DRUNK) +drunk:setParameter(CONDITION_PARAM_TICKS, 60000) + +local poison = Condition(CONDITION_POISON) +poison:setParameter(CONDITION_PARAM_DELAYED, true) +poison:setParameter(CONDITION_PARAM_MINVALUE, -50) +poison:setParameter(CONDITION_PARAM_MAXVALUE, -120) +poison:setParameter(CONDITION_PARAM_STARTVALUE, -5) +poison:setParameter(CONDITION_PARAM_TICKINTERVAL, 4000) +poison:setParameter(CONDITION_PARAM_FORCEUPDATE, true) + +local fluidMessage = { + [3] = "Aah...", + [4] = "Urgh!", + [5] = "Mmmh.", + [7] = "Aaaah...", + [10] = "Aaaah...", + [11] = "Urgh!", + [13] = "Urgh!", + [15] = "Aah...", + [19] = "Urgh!", + [27] = "Aah...", + [43] = "Aaaah..." +} + +local distillery = {[5513] = 5469, [5514] = 5470} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local targetItemType = ItemType(target.itemid) + if targetItemType and targetItemType:isFluidContainer() then + if target.type == 0 and item.type ~= 0 then + target:transform(target:getId(), item.type) + item:transform(item:getId(), 0) + return true + elseif target.type ~= 0 and item.type == 0 then + target:transform(target:getId(), 0) + item:transform(item:getId(), target.type) + return true + end + end + + if target.itemid == 1 then + if item.type == 0 then + player:sendTextMessage(MESSAGE_STATUS_SMALL, "It is empty.") + elseif target.uid == player.uid then + if table.contains({3, 15, 43}, item.type) then + player:addCondition(drunk) + elseif item.type == 4 then + player:addCondition(poison) + elseif item.type == 7 then + player:addMana(math.random(50, 150)) + fromPosition:sendMagicEffect(CONST_ME_MAGIC_BLUE) + elseif item.type == 10 then + player:addHealth(60) + fromPosition:sendMagicEffect(CONST_ME_MAGIC_BLUE) + end + player:say(fluidMessage[item.type] or "Gulp.", TALKTYPE_MONSTER_SAY) + item:transform(item:getId(), 0) + else + Game.createItem(2016, item.type, toPosition):decay() + item:transform(item:getId(), 0) + end + else + local fluidSource = targetItemType and targetItemType:getFluidSource() or 0 + if fluidSource ~= 0 then + item:transform(item:getId(), fluidSource) + elseif table.contains(distillery, target.itemid) then + local tmp = distillery[target.itemid] + if tmp then + item:transform(item:getId(), 0) + else + player:sendCancelMessage("You have to process the bunch into the distillery to get rum.") + end + elseif item.type == 0 then + player:sendTextMessage(MESSAGE_STATUS_SMALL, "It is empty.") + else + if toPosition.x == CONTAINER_POSITION then + toPosition = player:getPosition() + end + Game.createItem(2016, item.type, toPosition):decay() + item:transform(item:getId(), 0) + end + end + return true +end diff --git a/data/actions/scripts/other/food.lua b/data/actions/scripts/other/food.lua new file mode 100644 index 0000000..6ac7c0c --- /dev/null +++ b/data/actions/scripts/other/food.lua @@ -0,0 +1,125 @@ +local foods = { + [2362] = {5, "Crunch."}, -- carrot + [2666] = {15, "Munch."}, -- meat + [2667] = {12, "Munch."}, -- fish + [2668] = {10, "Mmmm."}, -- salmon + [2669] = {17, "Munch."}, -- northern pike + [2670] = {4, "Gulp."}, -- shrimp + [2671] = {30, "Chomp."}, -- ham + [2672] = {60, "Chomp."}, -- dragon ham + [2673] = {5, "Yum."}, -- pear + [2674] = {6, "Yum."}, -- red apple + [2675] = {13, "Yum."}, -- orange + [2676] = {8, "Yum."}, -- banana + [2677] = {1, "Yum."}, -- blueberry + [2678] = {18, "Slurp."}, -- coconut + [2679] = {1, "Yum."}, -- cherry + [2680] = {2, "Yum."}, -- strawberry + [2681] = {9, "Yum."}, -- grapes + [2682] = {20, "Yum."}, -- melon + [2683] = {17, "Munch."}, -- pumpkin + [2684] = {5, "Crunch."}, -- carrot + [2685] = {6, "Munch."}, -- tomato + [2686] = {9, "Crunch."}, -- corncob + [2687] = {2, "Crunch."}, -- cookie + [2688] = {2, "Munch."}, -- candy cane + [2689] = {10, "Crunch."}, -- bread + [2690] = {3, "Crunch."}, -- roll + [2691] = {8, "Crunch."}, -- brown bread + [2695] = {6, "Gulp."}, -- egg + [2696] = {9, "Smack."}, -- cheese + [2787] = {9, "Munch."}, -- white mushroom + [2788] = {4, "Munch."}, -- red mushroom + [2789] = {22, "Munch."}, -- brown mushroom + [2790] = {30, "Munch."}, -- orange mushroom + [2791] = {9, "Munch."}, -- wood mushroom + [2792] = {6, "Munch."}, -- dark mushroom + [2793] = {12, "Munch."}, -- some mushrooms + [2794] = {3, "Munch."}, -- some mushrooms + [2795] = {36, "Munch."}, -- fire mushroom + [2796] = {5, "Munch."}, -- green mushroom + [5097] = {4, "Yum."}, -- mango + [5678] = {8, "Gulp."}, -- tortoise egg + [6125] = {8, "Gulp."}, -- tortoise egg from Nargor + [6278] = {10, "Mmmm."}, -- cake + [6279] = {15, "Mmmm."}, -- decorated cake + [6393] = {12, "Mmmm."}, -- valentine's cake + [6394] = {15, "Mmmm."}, -- cream cake + [6501] = {20, "Mmmm."}, -- gingerbread man + [6541] = {6, "Gulp."}, -- coloured egg (yellow) + [6542] = {6, "Gulp."}, -- coloured egg (red) + [6543] = {6, "Gulp."}, -- coloured egg (blue) + [6544] = {6, "Gulp."}, -- coloured egg (green) + [6545] = {6, "Gulp."}, -- coloured egg (purple) + [6569] = {1, "Mmmm."}, -- candy + [6574] = {5, "Mmmm."}, -- bar of chocolate + [7158] = {15, "Munch."}, -- rainbow trout + [7159] = {13, "Munch."}, -- green perch + [7372] = {2, "Yum."}, -- ice cream cone (crispy chocolate chips) + [7373] = {2, "Yum."}, -- ice cream cone (velvet vanilla) + [7374] = {2, "Yum."}, -- ice cream cone (sweet strawberry) + [7375] = {2, "Yum."}, -- ice cream cone (chilly cherry) + [7376] = {2, "Yum."}, -- ice cream cone (mellow melon) + [7377] = {2, "Yum."}, -- ice cream cone (blue-barian) + [7909] = {4, "Crunch."}, -- walnut + [7910] = {4, "Crunch."}, -- peanut + [7963] = {60, "Munch."}, -- marlin + [8112] = {9, "Urgh."}, -- scarab cheese + [8838] = {10, "Gulp."}, -- potato + [8839] = {5, "Yum."}, -- plum + [8840] = {1, "Yum."}, -- raspberry + [8841] = {1, "Urgh."}, -- lemon + [8842] = {7, "Munch."}, -- cucumber + [8843] = {5, "Crunch."}, -- onion + [8844] = {1, "Gulp."}, -- jalapeño pepper + [8845] = {5, "Munch."}, -- beetroot + [8847] = {11, "Yum."}, -- chocolate cake + [9005] = {7, "Slurp."}, -- yummy gummy worm + [9114] = {5, "Crunch."}, -- bulb of garlic + [9996] = {0, "Slurp."}, -- banana chocolate shake + [10454] = {0, "Your head begins to feel better."}, -- headache pill + [11246] = {15, "Yum."}, -- rice ball + [11370] = {3, "Urgh."}, -- terramite eggs + [11429] = {10, "Mmmm."}, -- crocodile steak + [12415] = {20, "Yum."}, -- pineapple + [12416] = {10, "Munch."}, -- aubergine + [12417] = {8, "Crunch."}, -- broccoli + [12418] = {9, "Crunch."}, -- cauliflower + [12637] = {55, "Gulp."}, -- ectoplasmic sushi + [12638] = {18, "Yum."}, -- dragonfruit + [12639] = {2, "Munch."}, -- peas + [13297] = {20, "Crunch."}, -- haunch of boar + [15405] = {55, "Munch."}, -- sandfish + [15487] = {14, "Urgh."}, -- larvae + [15488] = {15, "Munch."}, -- deepling filet + [16014] = {60, "Mmmm."}, -- anniversary cake + [18397] = {33, "Munch."}, -- mushroom pie + [19737] = {10, "Urgh."}, -- insectoid eggs + [20100] = {15, "Smack."}, -- soft cheese + [20101] = {12, "Smack."}, -- rat cheese + [23514] = {15, "Munch."}, -- glooth sandwich + [23515] = {7, "Slurp."}, -- bowl of glooth soup + [23516] = {6, "Burp."}, -- bottle of glooth wine + [23517] = {25, "Chomp."}, -- glooth steak + [24841] = {12, "Yum."}, -- prickly pear + [24843] = {60, "Chomp."}, -- roasted meat + [26191] = {25, "Mmmm."}, -- energy bar + [26201] = {15, "Mmmm."} -- energy drink +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local food = foods[item.itemid] + if not food then + return false + end + + local condition = player:getCondition(CONDITION_REGENERATION, CONDITIONID_DEFAULT) + if condition and math.floor(condition:getTicks() / 1000 + (food[1] * 12)) >= 1200 then + player:sendTextMessage(MESSAGE_STATUS_SMALL, "You are full.") + else + player:feed(food[1] * 12) + player:say(food[2], TALKTYPE_MONSTER_SAY) + item:remove(1) + end + return true +end diff --git a/data/actions/scripts/other/largeseashell.lua b/data/actions/scripts/other/largeseashell.lua new file mode 100644 index 0000000..5a70e76 --- /dev/null +++ b/data/actions/scripts/other/largeseashell.lua @@ -0,0 +1,24 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + if player:getStorageValue(PlayerStorageKeys.delayLargeSeaShell) <= os.time() then + local chance = math.random(100) + local msg = "" + if chance <= 16 then + doTargetCombat(0, player, COMBAT_PHYSICALDAMAGE, -200, -200, CONST_ME_NONE) + msg = "Ouch! You squeezed your fingers." + elseif chance > 16 and chance <= 64 then + Game.createItem(math.random(7632, 7633), 1, player:getPosition()) + msg = "You found a beautiful pearl." + player:addAchievementProgress("Shell Seeker", 100) + else + msg = "Nothing is inside." + end + player:say(msg, TALKTYPE_MONSTER_SAY, false, player, item:getPosition()) + item:transform(7553) + item:decay() + player:setStorageValue(PlayerStorageKeys.delayLargeSeaShell, os.time() + 20 * 60 * 60) + item:getPosition():sendMagicEffect(CONST_ME_BUBBLES) + else + player:say("You have already opened a shell today.", TALKTYPE_MONSTER_SAY, false, player, item:getPosition()) + end + return true +end diff --git a/data/actions/scripts/other/music.lua b/data/actions/scripts/other/music.lua new file mode 100644 index 0000000..3e3709e --- /dev/null +++ b/data/actions/scripts/other/music.lua @@ -0,0 +1,65 @@ +local instruments = { + [2070] = {effect = CONST_ME_SOUND_GREEN}, -- wooden flute + [2071] = {effect = CONST_ME_SOUND_GREEN}, -- lyre + [2072] = {effect = CONST_ME_SOUND_GREEN}, -- lute + [2073] = {effect = CONST_ME_SOUND_GREEN}, -- drum + [2074] = {effect = CONST_ME_SOUND_GREEN}, -- panpipes + [2075] = {effect = CONST_ME_SOUND_GREEN}, -- simple fanfare + [2076] = {effect = CONST_ME_SOUND_GREEN}, -- fanfare + [2077] = {effect = CONST_ME_SOUND_GREEN}, -- royal fanfare + [2078] = {effect = CONST_ME_SOUND_GREEN}, -- post horn + [2079] = {effect = CONST_ME_SOUND_GREEN}, -- war horn + [2080] = {effects = {failure = CONST_ME_SOUND_PURPLE, success = CONST_ME_SOUND_GREEN}, chance = 50}, -- piano + [2081] = {effects = {failure = CONST_ME_SOUND_PURPLE, success = CONST_ME_SOUND_GREEN}, chance = 50}, -- piano + [2082] = {effects = {failure = CONST_ME_SOUND_PURPLE, success = CONST_ME_SOUND_GREEN}, chance = 50}, -- piano + [2083] = {effects = {failure = CONST_ME_SOUND_PURPLE, success = CONST_ME_SOUND_GREEN}, chance = 50}, -- piano + [2084] = {effect = CONST_ME_SOUND_GREEN}, -- harp + [2085] = {effect = CONST_ME_SOUND_GREEN}, -- harp + [2332] = {effect = CONST_ME_SOUND_GREEN}, -- Waldo's post horn + [2367] = {effect = CONST_ME_SOUND_GREEN}, -- drum (immovable) + [2368] = {effect = CONST_ME_SOUND_GREEN}, -- simple fanfare (immovable) + [2369] = {effect = CONST_ME_SOUND_YELLOW, itemId = 2681, itemCount = 10, chance = 80, remove = true}, -- cornucopia (immovable) + [2370] = {effect = CONST_ME_SOUND_GREEN}, -- lute (immovable) + [2371] = {effect = CONST_ME_SOUND_BLUE}, -- the horn of sundering (actual effect is unknown; immovable) + [2372] = {effect = CONST_ME_SOUND_GREEN}, -- lyre (immovable) + [2373] = {effect = CONST_ME_SOUND_GREEN}, -- panpipes (immovable) + [3951] = {effect = CONST_ME_SOUND_BLUE}, -- bongo drum (actual effect is unknown) + [3952] = {effects = {failure = CONST_ME_POFF, success = CONST_ME_SOUND_GREEN}, chance = 20}, -- didgeridoo + [3953] = {effect = CONST_ME_SOUND_RED}, -- war drum + [3957] = {effect = CONST_ME_SOUND_YELLOW, itemId = 2681, itemCount = 10, chance = 80, remove = true}, -- cornucopia + [5786] = {effects = {failure = CONST_ME_SOUND_RED, success = CONST_ME_SOUND_YELLOW}, monster = "war wolf", chance = 60, remove = true}, -- wooden whistle + [6572] = {effect = CONST_ME_SOUND_GREEN, text = "TOOOOOOT", transformId = 13578, decayId = 6572}, -- party trumpet + [6573] = {effect = CONST_ME_SOUND_GREEN, text = "TOOOOOOT", transformId = 13578, decayId = 6573}, -- party trumpet + [13759] = {effect = CONST_ME_SOUND_BLUE}, -- small whistle (actual effect is unknown) + [23923] = {effect = CONST_ME_SOUND_WHITE} -- small crystal bell +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local instrument, chance = instruments[item:getId()] + if instrument.chance then + chance = instrument.chance >= math.random(1, 100) + + if instrument.monster and chance then + local monster = Game.createMonster(instrument.monster, player:getPosition(), true) + if monster then + player:addSummon(monster) + end + elseif instrument.itemId and chance then + player:addItem(instrument.itemId, instrument.itemCount) + end + end + + item:getPosition():sendMagicEffect(instrument.effect or instrument.effects and chance and instrument.effects.success or instrument.effects.failure) + + if instrument.transformId then + player:say(instrument.text, TALKTYPE_MONSTER_SAY, false, nil, item:getPosition()) + item:transform(instrument.transformId) + item:decay(instrument.decayId) + end + + if not chance and instrument.remove then + item:remove() + end + player:addAchievementProgress("Rockstar", 10000) + return true +end diff --git a/data/actions/scripts/other/partyhat.lua b/data/actions/scripts/other/partyhat.lua new file mode 100644 index 0000000..55dab6a --- /dev/null +++ b/data/actions/scripts/other/partyhat.lua @@ -0,0 +1,9 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local headSlotItem = player:getSlotItem(CONST_SLOT_HEAD) + if not headSlotItem or item.uid ~= headSlotItem:getUniqueId() then + return false + end + player:addAchievementProgress("Party Animal", 200) + player:getPosition():sendMagicEffect(CONST_ME_GIFT_WRAPS) + return true +end diff --git a/data/actions/scripts/other/piggybank.lua b/data/actions/scripts/other/piggybank.lua new file mode 100644 index 0000000..fbf2a05 --- /dev/null +++ b/data/actions/scripts/other/piggybank.lua @@ -0,0 +1,12 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + if math.random(6) == 1 then + item:getPosition():sendMagicEffect(CONST_ME_POFF) + player:addItem(ITEM_GOLD_COIN, 1) + item:transform(2115) + player:addAchievementProgress("Allowance Collector", 50) + else + item:getPosition():sendMagicEffect(CONST_ME_SOUND_YELLOW) + player:addItem(ITEM_PLATINUM_COIN, 1) + end + return true +end diff --git a/data/actions/scripts/other/potions.lua b/data/actions/scripts/other/potions.lua new file mode 100644 index 0000000..d0163e9 --- /dev/null +++ b/data/actions/scripts/other/potions.lua @@ -0,0 +1,82 @@ +local berserk = Condition(CONDITION_ATTRIBUTES) +berserk:setParameter(CONDITION_PARAM_TICKS, 10 * 60 * 1000) +berserk:setParameter(CONDITION_PARAM_SKILL_MELEE, 5) +berserk:setParameter(CONDITION_PARAM_SKILL_SHIELD, -10) +berserk:setParameter(CONDITION_PARAM_BUFF_SPELL, true) + +local mastermind = Condition(CONDITION_ATTRIBUTES) +mastermind:setParameter(CONDITION_PARAM_TICKS, 10 * 60 * 1000) +mastermind:setParameter(CONDITION_PARAM_STAT_MAGICPOINTS, 3) +mastermind:setParameter(CONDITION_PARAM_BUFF_SPELL, true) + +local bullseye = Condition(CONDITION_ATTRIBUTES) +bullseye:setParameter(CONDITION_PARAM_TICKS, 10 * 60 * 1000) +bullseye:setParameter(CONDITION_PARAM_SKILL_DISTANCE, 5) +bullseye:setParameter(CONDITION_PARAM_SKILL_SHIELD, -10) +bullseye:setParameter(CONDITION_PARAM_BUFF_SPELL, true) + +local potions = { + [6558] = {transform = {7588, 7589}, effect = CONST_ME_DRAWBLOOD}, + [7439] = {condition = berserk, vocations = {4, 8}, effect = CONST_ME_MAGIC_RED, description = "Only knights may drink this potion.", text = "You feel stronger."}, + [7440] = {condition = mastermind, vocations = {1, 2, 5, 6}, effect = CONST_ME_MAGIC_BLUE, description = "Only sorcerers and druids may drink this potion.", text = "You feel smarter."}, + [7443] = {condition = bullseye, vocations = {3, 7}, effect = CONST_ME_MAGIC_GREEN, description = "Only paladins may drink this potion.", text = "You feel more accurate."}, + [7588] = {health = {250, 350}, vocations = {3, 4, 7, 8}, level = 50, flask = 7634, description = "Only knights and paladins of level 50 or above may drink this fluid."}, + [7589] = {mana = {115, 185}, vocations = {1, 2, 3, 5, 6, 7}, level = 50, flask = 7634, description = "Only sorcerers, druids and paladins of level 50 or above may drink this fluid."}, + [7590] = {mana = {150, 250}, vocations = {1, 2, 5, 6}, level = 80, flask = 7635, description = "Only druids and sorcerers of level 80 or above may drink this fluid."}, + [7591] = {health = {425, 575}, vocations = {4, 8}, level = 80, flask = 7635, description = "Only knights of level 80 or above may drink this fluid."}, + [7618] = {health = {125, 175}, flask = 7636}, + [7620] = {mana = {75, 125}, flask = 7636}, + [8472] = {health = {250, 350}, mana = {100, 200}, vocations = {3, 7}, level = 80, flask = 7635, description = "Only paladins of level 80 or above may drink this fluid."}, + [8473] = {health = {650, 850}, vocations = {4, 8}, level = 130, flask = 7635, description = "Only knights of level 130 or above may drink this fluid."}, + [8474] = {antidote = true, flask = 7636}, + [8704] = {health = {60, 90}, flask = 7636}, + [26029] = {mana = {425, 575}, vocations = {1, 2, 5, 6}, level = 130, flask = 7635, description = "Only druids and sorcerers of level 130 or above may drink this fluid."}, + [26030] = {health = {420, 580}, mana = {200, 350}, vocations = {3, 7}, level = 130, flask = 7635, description = "Only paladins of level 130 or above may drink this fluid."}, + [26031] = {health = {875, 1125}, vocations = {4, 8}, level = 200, flask = 7635, description = "Only knights of level 200 or above may drink this fluid."} +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + if type(target) == "userdata" and not target:isPlayer() then + return false + end + + local potion = potions[item:getId()] + if potion.level and player:getLevel() < potion.level or potion.vocations and not table.contains(potion.vocations, player:getVocation():getId()) then + player:say(potion.description, TALKTYPE_MONSTER_SAY) + return true + end + + if potion.condition then + player:addCondition(potion.condition) + player:say(potion.text, TALKTYPE_MONSTER_SAY) + player:getPosition():sendMagicEffect(potion.effect) + elseif potion.transform then + item:transform(potion.transform[math.random(#potion.transform)]) + item:getPosition():sendMagicEffect(potion.effect) + return true + else + if potion.health then + doTargetCombat(0, target, COMBAT_HEALING, potion.health[1], potion.health[2]) + end + + if potion.mana then + doTargetCombat(0, target, COMBAT_MANADRAIN, potion.mana[1], potion.mana[2]) + end + + if potion.antidote then + target:removeCondition(CONDITION_POISON) + end + + player:addAchievementProgress("Potion Addict", 100000) + player:addItem(potion.flask) + target:say("Aaaah...", TALKTYPE_MONSTER_SAY) + target:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + end + + if not configManager.getBoolean(configKeys.REMOVE_POTION_CHARGES) then + return true + end + + item:remove(1) + return true +end diff --git a/data/actions/scripts/other/skilltrainer.lua b/data/actions/scripts/other/skilltrainer.lua new file mode 100644 index 0000000..cdefb49 --- /dev/null +++ b/data/actions/scripts/other/skilltrainer.lua @@ -0,0 +1,23 @@ +local statues = { + [18488] = SKILL_SWORD, + [18489] = SKILL_AXE, + [18490] = SKILL_CLUB, + [18491] = SKILL_DISTANCE, + [18492] = SKILL_MAGLEVEL +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local skill = statues[item:getId()] + if not player:isPremium() then + player:sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT) + return true + end + + if player:isPzLocked() then + return false + end + + player:setOfflineTrainingSkill(skill) + player:remove() + return true +end diff --git a/data/actions/scripts/other/snowheap.lua b/data/actions/scripts/other/snowheap.lua new file mode 100644 index 0000000..97739fb --- /dev/null +++ b/data/actions/scripts/other/snowheap.lua @@ -0,0 +1,4 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + Game.createItem(2111, 1, item:getPosition()) + return true +end diff --git a/data/actions/scripts/other/surprisebag.lua b/data/actions/scripts/other/surprisebag.lua new file mode 100644 index 0000000..4727e05 --- /dev/null +++ b/data/actions/scripts/other/surprisebag.lua @@ -0,0 +1,27 @@ +local presents = { + [6570] = { -- blue present + {2687, 10}, {6394, 3}, 6280, 6574, 6578, 6575, 6577, 6569, 6576, 6572, 2114 + }, + [6571] = { -- red present + {2152, 10}, {2152, 10}, {2152, 10}, 2153, 5944, 2112, 6568, 6566, 2492, 2520, 2195, 2114, 2114, 2114, 6394, 6394, 6576, 6576, 6578, 6578, 6574, 6574 + } +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local count = 1 + local targetItem = presents[item.itemid] + if not targetItem then + return true + end + + local gift = targetItem[math.random(#targetItem)] + if type(gift) == "table" then + gift = gift[1] + count = gift[2] + end + + player:addItem(gift, count) + item:remove(1) + fromPosition:sendMagicEffect(CONST_ME_GIFT_WRAPS) + return true +end diff --git a/data/actions/scripts/other/teleport.lua b/data/actions/scripts/other/teleport.lua new file mode 100644 index 0000000..43f11fd --- /dev/null +++ b/data/actions/scripts/other/teleport.lua @@ -0,0 +1,16 @@ +local upFloorIds = {1386, 3678, 5543, 22845, 22846} +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + if table.contains(upFloorIds, item.itemid) then + fromPosition:moveUpstairs() + else + fromPosition.z = fromPosition.z + 1 + end + + if player:isPzLocked() and Tile(fromPosition):hasFlag(TILESTATE_PROTECTIONZONE) then + player:sendCancelMessage(RETURNVALUE_PLAYERISPZLOCKED) + return true + end + + player:teleportTo(fromPosition, false) + return true +end diff --git a/data/actions/scripts/other/transforms.lua b/data/actions/scripts/other/transforms.lua new file mode 100644 index 0000000..f8b2346 --- /dev/null +++ b/data/actions/scripts/other/transforms.lua @@ -0,0 +1,13 @@ +local transformItems = { + [3743] = 4404, [4404] = 3743 +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local transformIds = transformItems[item:getId()] + if not transformIds then + return false + end + + item:transform(transformIds) + return true +end diff --git a/data/actions/scripts/other/trap.lua b/data/actions/scripts/other/trap.lua new file mode 100644 index 0000000..9d45547 --- /dev/null +++ b/data/actions/scripts/other/trap.lua @@ -0,0 +1,5 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + item:transform(item:getId() - 1) + fromPosition:sendMagicEffect(CONST_ME_POFF) + return true +end diff --git a/data/actions/scripts/other/wallmirror.lua b/data/actions/scripts/other/wallmirror.lua new file mode 100644 index 0000000..3e965b2 --- /dev/null +++ b/data/actions/scripts/other/wallmirror.lua @@ -0,0 +1,24 @@ +local messages = { + "You could win a beauty contest today!", + "You rarely looked better.", + "Well, you can't look good every day.", + "You should think about a makeover.", + "Is that the indication of a potbelly looming under your clothes?", + "You look irresistible.", + "You look tired.", + "You look awesome!", + "You nearly don't recognize yourself.", + "You look fabulous.", + "Surprise, surprise, you don't see yourself." +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + if player:getStorageValue(PlayerStorageKeys.delayWallMirror) <= os.time() then + player:say(messages[math.random(1, #messages)], TALKTYPE_MONSTER_SAY) + player:setStorageValue(PlayerStorageKeys.delayWallMirror, os.time() + 20 * 60 * 60) + player:addAchievementProgress("Vanity", 300) + else + player:say("Don't be so vain about your appearance.", TALKTYPE_MONSTER_SAY) + end + return true +end diff --git a/data/actions/scripts/other/watch.lua b/data/actions/scripts/other/watch.lua new file mode 100644 index 0000000..88b7df4 --- /dev/null +++ b/data/actions/scripts/other/watch.lua @@ -0,0 +1,4 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + player:sendTextMessage(MESSAGE_INFO_DESCR, "The time is " .. getFormattedWorldTime() .. ".") + return true +end diff --git a/data/actions/scripts/other/waterpipe.lua b/data/actions/scripts/other/waterpipe.lua new file mode 100644 index 0000000..41a37ed --- /dev/null +++ b/data/actions/scripts/other/waterpipe.lua @@ -0,0 +1,8 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + if math.random(3) == 1 then + item:getPosition():sendMagicEffect(CONST_ME_POFF) + else + player:getPosition():sendMagicEffect(CONST_ME_POFF) + end + return true +end diff --git a/data/actions/scripts/other/windows.lua b/data/actions/scripts/other/windows.lua new file mode 100644 index 0000000..7f46355 --- /dev/null +++ b/data/actions/scripts/other/windows.lua @@ -0,0 +1,52 @@ +local windows = { + [5303] = 6448, [5304] = 6449, [6438] = 6436, [6436] = 6438, + [6439] = 6437, [6437] = 6439, [6442] = 6440, [6440] = 6442, + [6443] = 6441, [6441] = 6443, [6446] = 6444, [6444] = 6446, + [6447] = 6445, [6445] = 6447, [6448] = 5303, [6449] = 5304, + [6452] = 6450, [6450] = 6452, [6453] = 6451, [6451] = 6453, + [6456] = 6454, [6454] = 6456, [6457] = 6455, [6455] = 6457, + [6460] = 6458, [6458] = 6460, [6461] = 6459, [6459] = 6461, + [6464] = 6462, [6462] = 6464, [6465] = 6463, [6463] = 6465, + [6468] = 6466, [6466] = 6468, [6469] = 6467, [6467] = 6469, + [6472] = 6470, [6470] = 6472, [6473] = 6471, [6471] = 6473, + [6790] = 6788, [6788] = 6790, [6791] = 6789, [6789] = 6791, + [7027] = 7025, [7025] = 7027, [7028] = 7026, [7026] = 7028, + [7031] = 7029, [7029] = 7031, [7032] = 7030, [7030] = 7032, + [10264] = 10266, [10266] = 10264, [10265] = 10267, [10267] = 10265, + [10488] = 10490, [10490] = 10488, [10489] = 10491, [10491] = 10489, + [19427] = 19447, [19428] = 19448, [19441] = 19450, [19440] = 19449, + [19443] = 20180, [19444] = 20181, [19445] = 20183, [19446] = 20184, + [19447] = 19427, [19448] = 19428, [19449] = 19440, [19450] = 19441, + [19974] = 20182, [19975] = 20185, [20180] = 19443, [20181] = 19444, + [20182] = 19974, [20183] = 19445, [20184] = 19446, [20185] = 19975 +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local window = windows[item:getId()] + if not window then + return false + end + + local tile = Tile(fromPosition) + local house = tile and tile:getHouse() + if not house then + fromPosition.y = fromPosition.y - 1 + tile = Tile(fromPosition) + house = tile and tile:getHouse() + if not house then + fromPosition.y = fromPosition.y + 1 + fromPosition.x = fromPosition.x - 1 + tile = Tile(fromPosition) + house = tile and tile:getHouse() + end + end + + if house and player:getTile():getHouse() ~= house and player:getAccountType() < ACCOUNT_TYPE_GAMEMASTER then + return false + end + + player:addAchievementProgress("Do Not Disturb", 100) + player:addAchievementProgress("Let the Sunshine In", 100) + item:transform(window) + return true +end diff --git a/data/actions/scripts/quests/annihilator.lua b/data/actions/scripts/quests/annihilator.lua new file mode 100644 index 0000000..77cba96 --- /dev/null +++ b/data/actions/scripts/quests/annihilator.lua @@ -0,0 +1,36 @@ +local playerPosition = { + {x = 247, y = 659, z = 13}, + {x = 247, y = 660, z = 13}, + {x = 247, y = 661, z = 13}, + {x = 247, y = 662, z = 13} +} +local newPosition = { + {x = 189, y = 650, z = 13}, + {x = 189, y = 651, z = 13}, + {x = 189, y = 652, z = 13}, + {x = 189, y = 653, z = 13} +} + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + if item.itemid == 1945 then + local players = {} + for _, position in ipairs(playerPosition) do + local topPlayer = Tile(position):getTopCreature() + if not topPlayer or not topPlayer:isPlayer() or topPlayer:getLevel() < 100 or topPlayer:getStorageValue(PlayerStorageKeys.annihilatorReward) ~= -1 then + player:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + return false + end + players[#players + 1] = topPlayer + end + + for i, targetPlayer in ipairs(players) do + Position(playerPosition[i]):sendMagicEffect(CONST_ME_POFF) + targetPlayer:teleportTo(newPosition[i], false) + targetPlayer:getPosition():sendMagicEffect(CONST_ME_ENERGYAREA) + end + item:transform(1946) + elseif item.itemid == 1946 then + player:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + end + return true +end diff --git a/data/actions/scripts/quests/quests.lua b/data/actions/scripts/quests/quests.lua new file mode 100644 index 0000000..94348ca --- /dev/null +++ b/data/actions/scripts/quests/quests.lua @@ -0,0 +1,43 @@ +local annihilatorReward = {1990, 2400, 2431, 2494} +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + if item.uid <= 1250 or item.uid >= 30000 then + return false + end + + local itemType = ItemType(item.uid) + if itemType:getId() == 0 then + return false + end + + local itemWeight = itemType:getWeight() + local playerCap = player:getFreeCapacity() + if table.contains(annihilatorReward, item.uid) then + if player:getStorageValue(PlayerStorageKeys.annihilatorReward) == -1 then + if playerCap >= itemWeight then + if item.uid == 1990 then + player:addItem(1990, 1):addItem(2326, 1) + else + player:addItem(item.uid, 1) + end + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, 'You have found a ' .. itemType:getName() .. '.') + player:setStorageValue(PlayerStorageKeys.annihilatorReward, 1) + player:addAchievement("Annihilator") + else + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, 'You have found a ' .. itemType:getName() .. ' weighing ' .. itemWeight .. ' oz it\'s too heavy.') + end + else + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "It is empty.") + end + elseif player:getStorageValue(item.uid) == -1 then + if playerCap >= itemWeight then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, 'You have found a ' .. itemType:getName() .. '.') + player:addItem(item.uid, 1) + player:setStorageValue(item.uid, 1) + else + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, 'You have found a ' .. itemType:getName() .. ' weighing ' .. itemWeight .. ' oz it\'s too heavy.') + end + else + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "It is empty.") + end + return true +end diff --git a/data/actions/scripts/tools/crowbar.lua b/data/actions/scripts/tools/crowbar.lua new file mode 100644 index 0000000..31b8aba --- /dev/null +++ b/data/actions/scripts/tools/crowbar.lua @@ -0,0 +1,3 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + return onUseCrowbar(player, item, fromPosition, target, toPosition, isHotkey) +end diff --git a/data/actions/scripts/tools/fishing.lua b/data/actions/scripts/tools/fishing.lua new file mode 100644 index 0000000..ea5de5c --- /dev/null +++ b/data/actions/scripts/tools/fishing.lua @@ -0,0 +1,82 @@ +local waterIds = {493, 4608, 4609, 4610, 4611, 4612, 4613, 4614, 4615, 4616, 4617, 4618, 4619, 4620, 4621, 4622, 4623, 4624, 4625, 7236, 10499, 15401, 15402} +local lootTrash = {2234, 2238, 2376, 2509, 2667} +local lootCommon = {2152, 2167, 2168, 2669, 7588, 7589} +local lootRare = {2143, 2146, 2149, 7158, 7159} +local lootVeryRare = {7632, 7633, 10220} +local useWorms = true + +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + local targetId = target.itemid + if not table.contains(waterIds, target.itemid) then + return false + end + + if targetId == 10499 then + local owner = target:getAttribute(ITEM_ATTRIBUTE_CORPSEOWNER) + if owner ~= 0 and owner ~= player:getId() then + player:sendTextMessage(MESSAGE_STATUS_SMALL, "You are not the owner.") + return true + end + + toPosition:sendMagicEffect(CONST_ME_WATERSPLASH) + target:transform(targetId + 1) + target:decay() + + local rareChance = math.random(1, 100) + if rareChance == 1 then + player:addItem(lootVeryRare[math.random(#lootVeryRare)], 1) + elseif rareChance <= 3 then + player:addItem(lootRare[math.random(#lootRare)], 1) + elseif rareChance <= 10 then + player:addItem(lootCommon[math.random(#lootCommon)], 1) + else + player:addItem(lootTrash[math.random(#lootTrash)], 1) + end + return true + end + + if targetId ~= 7236 then + toPosition:sendMagicEffect(CONST_ME_LOSEENERGY) + end + + if targetId == 493 or targetId == 15402 then + return true + end + + player:addSkillTries(SKILL_FISHING, 1) + if math.random(1, 100) <= math.min(math.max(10 + (player:getEffectiveSkillLevel(SKILL_FISHING) - 10) * 0.597, 10), 50) then + if useWorms and not player:removeItem(3976, 1) then + return true + end + + if targetId == 15401 then + target:transform(targetId + 1) + target:decay() + + if math.random(1, 100) >= 97 then + player:addItem(15405, 1) + player:addAchievement("Desert Fisher") + return true + end + elseif targetId == 7236 then + target:transform(targetId + 1) + target:decay() + player:addAchievementProgress("Exquisite Taste", 250) + + local rareChance = math.random(1, 100) + if rareChance == 1 then + player:addItem(7158, 1) + return true + elseif rareChance <= 4 then + player:addItem(2669, 1) + return true + elseif rareChance <= 10 then + player:addItem(7159, 1) + return true + end + end + player:addAchievementProgress("Here, Fishy Fishy!", 1000) + player:addItem(2667, 1) + end + return true +end diff --git a/data/actions/scripts/tools/kitchen_knife.lua b/data/actions/scripts/tools/kitchen_knife.lua new file mode 100644 index 0000000..3e276e0 --- /dev/null +++ b/data/actions/scripts/tools/kitchen_knife.lua @@ -0,0 +1,3 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + return onUseKitchenKnife(player, item, fromPosition, target, toPosition, isHotkey) +end diff --git a/data/actions/scripts/tools/machete.lua b/data/actions/scripts/tools/machete.lua new file mode 100644 index 0000000..f26fb8e --- /dev/null +++ b/data/actions/scripts/tools/machete.lua @@ -0,0 +1,3 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + return onUseMachete(player, item, fromPosition, target, toPosition, isHotkey) +end diff --git a/data/actions/scripts/tools/pick.lua b/data/actions/scripts/tools/pick.lua new file mode 100644 index 0000000..f379312 --- /dev/null +++ b/data/actions/scripts/tools/pick.lua @@ -0,0 +1,3 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + return onUsePick(player, item, fromPosition, target, toPosition, isHotkey) +end diff --git a/data/actions/scripts/tools/rope.lua b/data/actions/scripts/tools/rope.lua new file mode 100644 index 0000000..405e301 --- /dev/null +++ b/data/actions/scripts/tools/rope.lua @@ -0,0 +1,3 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + return onUseRope(player, item, fromPosition, target, toPosition, isHotkey) +end diff --git a/data/actions/scripts/tools/scythe.lua b/data/actions/scripts/tools/scythe.lua new file mode 100644 index 0000000..7ca273d --- /dev/null +++ b/data/actions/scripts/tools/scythe.lua @@ -0,0 +1,3 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + return onUseScythe(player, item, fromPosition, target, toPosition, isHotkey) +end diff --git a/data/actions/scripts/tools/shovel.lua b/data/actions/scripts/tools/shovel.lua new file mode 100644 index 0000000..bd809c0 --- /dev/null +++ b/data/actions/scripts/tools/shovel.lua @@ -0,0 +1,3 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + return onUseShovel(player, item, fromPosition, target, toPosition, isHotkey) +end diff --git a/data/actions/scripts/tools/toolgear.lua b/data/actions/scripts/tools/toolgear.lua new file mode 100644 index 0000000..3ee3636 --- /dev/null +++ b/data/actions/scripts/tools/toolgear.lua @@ -0,0 +1,17 @@ +function onUse(player, item, fromPosition, target, toPosition, isHotkey) + if math.random(100) > 5 then + return onUseRope(player, item, fromPosition, target, toPosition, isHotkey) + or onUseShovel(player, item, fromPosition, target, toPosition, isHotkey) + or onUsePick(player, item, fromPosition, target, toPosition, isHotkey) + or onUseMachete(player, item, fromPosition, target, toPosition, isHotkey) + or onUseCrowbar(player, item, fromPosition, target, toPosition, isHotkey) + or onUseScythe(player, item, fromPosition, target, toPosition, isHotkey) + or onUseKitchenKnife(player, item, fromPosition, target, toPosition, isHotkey) + else + player:say("Oh no! Your tool is jammed and can't be used for a minute.", TALKTYPE_MONSTER_SAY) + player:addAchievementProgress("Bad Timming", 10) + item:transform(item.itemid + 1) + item:decay() + end + return true +end diff --git a/data/chatchannels/chatchannels.xml b/data/chatchannels/chatchannels.xml new file mode 100644 index 0000000..dc79224 --- /dev/null +++ b/data/chatchannels/chatchannels.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/data/chatchannels/scripts/advertising-rook.lua b/data/chatchannels/scripts/advertising-rook.lua new file mode 100644 index 0000000..e652a24 --- /dev/null +++ b/data/chatchannels/scripts/advertising-rook.lua @@ -0,0 +1,40 @@ +function canJoin(player) + return player:getVocation():getId() == VOCATION_NONE or player:getAccountType() >= ACCOUNT_TYPE_SENIORTUTOR +end + +local CHANNEL_ADVERTISING_ROOK = 6 + +local muted = Condition(CONDITION_CHANNELMUTEDTICKS, CONDITIONID_DEFAULT) +muted:setParameter(CONDITION_PARAM_SUBID, CHANNEL_ADVERTISING_ROOK) +muted:setParameter(CONDITION_PARAM_TICKS, 120000) + +function onSpeak(player, type, message) + if player:getAccountType() >= ACCOUNT_TYPE_GAMEMASTER then + if type == TALKTYPE_CHANNEL_Y then + return TALKTYPE_CHANNEL_O + end + return true + end + + if player:getLevel() == 1 then + player:sendCancelMessage("You may not speak into channels as long as you are on level 1.") + return false + end + + if player:getCondition(CONDITION_CHANNELMUTEDTICKS, CONDITIONID_DEFAULT, CHANNEL_ADVERTISING_ROOK) then + player:sendCancelMessage("You may only place one offer in two minutes.") + return false + end + player:addCondition(muted) + + if type == TALKTYPE_CHANNEL_O then + if player:getAccountType() < ACCOUNT_TYPE_GAMEMASTER then + type = TALKTYPE_CHANNEL_Y + end + elseif type == TALKTYPE_CHANNEL_R1 then + if not player:hasFlag(PlayerFlag_CanTalkRedChannel) then + type = TALKTYPE_CHANNEL_Y + end + end + return type +end diff --git a/data/chatchannels/scripts/advertising.lua b/data/chatchannels/scripts/advertising.lua new file mode 100644 index 0000000..0998faa --- /dev/null +++ b/data/chatchannels/scripts/advertising.lua @@ -0,0 +1,40 @@ +function canJoin(player) + return player:getVocation():getId() ~= VOCATION_NONE or player:getAccountType() >= ACCOUNT_TYPE_SENIORTUTOR +end + +local CHANNEL_ADVERTISING = 5 + +local muted = Condition(CONDITION_CHANNELMUTEDTICKS, CONDITIONID_DEFAULT) +muted:setParameter(CONDITION_PARAM_SUBID, CHANNEL_ADVERTISING) +muted:setParameter(CONDITION_PARAM_TICKS, 120000) + +function onSpeak(player, type, message) + if player:getAccountType() >= ACCOUNT_TYPE_GAMEMASTER then + if type == TALKTYPE_CHANNEL_Y then + return TALKTYPE_CHANNEL_O + end + return true + end + + if player:getLevel() == 1 then + player:sendCancelMessage("You may not speak into channels as long as you are on level 1.") + return false + end + + if player:getCondition(CONDITION_CHANNELMUTEDTICKS, CONDITIONID_DEFAULT, CHANNEL_ADVERTISING) then + player:sendCancelMessage("You may only place one offer in two minutes.") + return false + end + player:addCondition(muted) + + if type == TALKTYPE_CHANNEL_O then + if player:getAccountType() < ACCOUNT_TYPE_GAMEMASTER then + type = TALKTYPE_CHANNEL_Y + end + elseif type == TALKTYPE_CHANNEL_R1 then + if not player:hasFlag(PlayerFlag_CanTalkRedChannel) then + type = TALKTYPE_CHANNEL_Y + end + end + return type +end diff --git a/data/chatchannels/scripts/englishchat.lua b/data/chatchannels/scripts/englishchat.lua new file mode 100644 index 0000000..42517d2 --- /dev/null +++ b/data/chatchannels/scripts/englishchat.lua @@ -0,0 +1,22 @@ +function onSpeak(player, type, message) + local playerAccountType = player:getAccountType() + if player:getLevel() == 1 and playerAccountType < ACCOUNT_TYPE_GAMEMASTER then + player:sendCancelMessage("You may not speak into channels as long as you are on level 1.") + return false + end + + if type == TALKTYPE_CHANNEL_Y then + if playerAccountType >= ACCOUNT_TYPE_GAMEMASTER then + type = TALKTYPE_CHANNEL_O + end + elseif type == TALKTYPE_CHANNEL_O then + if playerAccountType < ACCOUNT_TYPE_GAMEMASTER then + type = TALKTYPE_CHANNEL_Y + end + elseif type == TALKTYPE_CHANNEL_R1 then + if playerAccountType < ACCOUNT_TYPE_GAMEMASTER and not player:hasFlag(PlayerFlag_CanTalkRedChannel) then + type = TALKTYPE_CHANNEL_Y + end + end + return type +end diff --git a/data/chatchannels/scripts/gamemaster.lua b/data/chatchannels/scripts/gamemaster.lua new file mode 100644 index 0000000..19725ee --- /dev/null +++ b/data/chatchannels/scripts/gamemaster.lua @@ -0,0 +1,21 @@ +function canJoin(player) + return player:getAccountType() >= ACCOUNT_TYPE_GAMEMASTER +end + +function onSpeak(player, type, message) + local playerAccountType = player:getAccountType() + if type == TALKTYPE_CHANNEL_Y then + if playerAccountType == ACCOUNT_TYPE_GOD then + type = TALKTYPE_CHANNEL_O + end + elseif type == TALKTYPE_CHANNEL_O then + if playerAccountType ~= ACCOUNT_TYPE_GOD then + type = TALKTYPE_CHANNEL_Y + end + elseif type == TALKTYPE_CHANNEL_R1 then + if playerAccountType ~= ACCOUNT_TYPE_GOD and not player:hasFlag(PlayerFlag_CanTalkRedChannel) then + type = TALKTYPE_CHANNEL_Y + end + end + return type +end diff --git a/data/chatchannels/scripts/help.lua b/data/chatchannels/scripts/help.lua new file mode 100644 index 0000000..8e13a78 --- /dev/null +++ b/data/chatchannels/scripts/help.lua @@ -0,0 +1,77 @@ +local CHANNEL_HELP = 7 + +local muted = Condition(CONDITION_CHANNELMUTEDTICKS, CONDITIONID_DEFAULT) +muted:setParameter(CONDITION_PARAM_SUBID, CHANNEL_HELP) +muted:setParameter(CONDITION_PARAM_TICKS, 3600000) + +function onSpeak(player, type, message) + local playerAccountType = player:getAccountType() + if player:getLevel() == 1 and playerAccountType == ACCOUNT_TYPE_NORMAL then + player:sendCancelMessage("You may not speak into channels as long as you are on level 1.") + return false + end + + if player:getCondition(CONDITION_CHANNELMUTEDTICKS, CONDITIONID_DEFAULT, CHANNEL_HELP) then + player:sendCancelMessage("You are muted from the Help channel for using it inappropriately.") + return false + end + + if playerAccountType >= ACCOUNT_TYPE_TUTOR then + if string.sub(message, 1, 6) == "!mute " then + local targetName = string.sub(message, 7) + local target = Player(targetName) + if target then + if playerAccountType > target:getAccountType() then + if not target:getCondition(CONDITION_CHANNELMUTEDTICKS, CONDITIONID_DEFAULT, CHANNEL_HELP) then + target:addCondition(muted) + sendChannelMessage(CHANNEL_HELP, TALKTYPE_CHANNEL_R1, target:getName() .. " has been muted by " .. player:getName() .. " for using Help Channel inappropriately.") + else + player:sendCancelMessage("That player is already muted.") + end + else + player:sendCancelMessage("You are not authorized to mute that player.") + end + else + player:sendCancelMessage(RETURNVALUE_PLAYERWITHTHISNAMEISNOTONLINE) + end + return false + elseif string.sub(message, 1, 8) == "!unmute " then + local targetName = string.sub(message, 9) + local target = Player(targetName) + if target then + if playerAccountType > target:getAccountType() then + if target:getCondition(CONDITION_CHANNELMUTEDTICKS, CONDITIONID_DEFAULT, CHANNEL_HELP) then + target:removeCondition(CONDITION_CHANNELMUTEDTICKS, CONDITIONID_DEFAULT, CHANNEL_HELP) + sendChannelMessage(CHANNEL_HELP, TALKTYPE_CHANNEL_R1, target:getName() .. " has been unmuted by " .. player:getName() .. ".") + else + player:sendCancelMessage("That player is not muted.") + end + else + player:sendCancelMessage("You are not authorized to unmute that player.") + end + else + player:sendCancelMessage(RETURNVALUE_PLAYERWITHTHISNAMEISNOTONLINE) + end + return false + end + end + + if type == TALKTYPE_CHANNEL_Y then + if playerAccountType >= ACCOUNT_TYPE_TUTOR or player:hasFlag(PlayerFlag_TalkOrangeHelpChannel) then + type = TALKTYPE_CHANNEL_O + end + elseif type == TALKTYPE_CHANNEL_O then + if playerAccountType < ACCOUNT_TYPE_TUTOR and not player:hasFlag(PlayerFlag_TalkOrangeHelpChannel) then + type = TALKTYPE_CHANNEL_Y + end + elseif type == TALKTYPE_CHANNEL_R1 then + if playerAccountType < ACCOUNT_TYPE_GAMEMASTER and not player:hasFlag(PlayerFlag_CanTalkRedChannel) then + if playerAccountType >= ACCOUNT_TYPE_TUTOR or player:hasFlag(PlayerFlag_TalkOrangeHelpChannel) then + type = TALKTYPE_CHANNEL_O + else + type = TALKTYPE_CHANNEL_Y + end + end + end + return type +end diff --git a/data/chatchannels/scripts/tutor.lua b/data/chatchannels/scripts/tutor.lua new file mode 100644 index 0000000..9928fef --- /dev/null +++ b/data/chatchannels/scripts/tutor.lua @@ -0,0 +1,21 @@ +function canJoin(player) + return player:getAccountType() >= ACCOUNT_TYPE_TUTOR +end + +function onSpeak(player, type, message) + local playerAccountType = player:getAccountType() + if type == TALKTYPE_CHANNEL_Y then + if playerAccountType >= ACCOUNT_TYPE_SENIORTUTOR then + type = TALKTYPE_CHANNEL_O + end + elseif type == TALKTYPE_CHANNEL_O then + if playerAccountType < ACCOUNT_TYPE_SENIORTUTOR then + type = TALKTYPE_CHANNEL_Y + end + elseif type == TALKTYPE_CHANNEL_R1 then + if playerAccountType < ACCOUNT_TYPE_GAMEMASTER and not player:hasFlag(PlayerFlag_CanTalkRedChannel) then + type = TALKTYPE_CHANNEL_Y + end + end + return type +end diff --git a/data/chatchannels/scripts/worldchat.lua b/data/chatchannels/scripts/worldchat.lua new file mode 100644 index 0000000..42517d2 --- /dev/null +++ b/data/chatchannels/scripts/worldchat.lua @@ -0,0 +1,22 @@ +function onSpeak(player, type, message) + local playerAccountType = player:getAccountType() + if player:getLevel() == 1 and playerAccountType < ACCOUNT_TYPE_GAMEMASTER then + player:sendCancelMessage("You may not speak into channels as long as you are on level 1.") + return false + end + + if type == TALKTYPE_CHANNEL_Y then + if playerAccountType >= ACCOUNT_TYPE_GAMEMASTER then + type = TALKTYPE_CHANNEL_O + end + elseif type == TALKTYPE_CHANNEL_O then + if playerAccountType < ACCOUNT_TYPE_GAMEMASTER then + type = TALKTYPE_CHANNEL_Y + end + elseif type == TALKTYPE_CHANNEL_R1 then + if playerAccountType < ACCOUNT_TYPE_GAMEMASTER and not player:hasFlag(PlayerFlag_CanTalkRedChannel) then + type = TALKTYPE_CHANNEL_Y + end + end + return type +end diff --git a/data/creaturescripts/creaturescripts.xml b/data/creaturescripts/creaturescripts.xml new file mode 100644 index 0000000..86e2c8c --- /dev/null +++ b/data/creaturescripts/creaturescripts.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/data/creaturescripts/lib/creaturescripts.lua b/data/creaturescripts/lib/creaturescripts.lua new file mode 100644 index 0000000..6116bcc --- /dev/null +++ b/data/creaturescripts/lib/creaturescripts.lua @@ -0,0 +1 @@ +-- empty file -- diff --git a/data/creaturescripts/scripts/droploot.lua b/data/creaturescripts/scripts/droploot.lua new file mode 100644 index 0000000..50fe39f --- /dev/null +++ b/data/creaturescripts/scripts/droploot.lua @@ -0,0 +1,42 @@ +function onDeath(player, corpse, killer, mostDamage, unjustified, mostDamage_unjustified) + if player:hasFlag(PlayerFlag_NotGenerateLoot) or player:getVocation():getId() == VOCATION_NONE then + return true + end + + local amulet = player:getSlotItem(CONST_SLOT_NECKLACE) + local isRedOrBlack = table.contains({SKULL_RED, SKULL_BLACK}, player:getSkull()) + if amulet and amulet.itemid == ITEM_AMULETOFLOSS and not isRedOrBlack then + local isPlayer = false + if killer then + if killer:isPlayer() then + isPlayer = true + else + local master = killer:getMaster() + if master and master:isPlayer() then + isPlayer = true + end + end + end + + if not isPlayer or not player:hasBlessing(6) then + player:removeItem(ITEM_AMULETOFLOSS, 1, -1, false) + end + else + for i = CONST_SLOT_HEAD, CONST_SLOT_AMMO do + local item = player:getSlotItem(i) + local lossPercent = player:getLossPercent() + if item then + if isRedOrBlack or math.random(item:isContainer() and 100 or 1000) <= lossPercent then + if (isRedOrBlack or lossPercent ~= 0) and not item:moveTo(corpse) then + item:remove() + end + end + end + end + end + + if not player:getSlotItem(CONST_SLOT_BACKPACK) then + player:addItem(ITEM_BAG, 1, false, CONST_SLOT_BACKPACK) + end + return true +end diff --git a/data/creaturescripts/scripts/extendedopcode.lua b/data/creaturescripts/scripts/extendedopcode.lua new file mode 100644 index 0000000..7fa2d42 --- /dev/null +++ b/data/creaturescripts/scripts/extendedopcode.lua @@ -0,0 +1,13 @@ +local OPCODE_LANGUAGE = 1 + +function onExtendedOpcode(player, opcode, buffer) + if opcode == OPCODE_LANGUAGE then + -- otclient language + if buffer == 'en' or buffer == 'pt' then + -- example, setting player language, because otclient is multi-language... + -- player:setStorageValue(SOME_STORAGE_ID, SOME_VALUE) + end + else + -- other opcodes can be ignored, and the server will just work fine... + end +end diff --git a/data/creaturescripts/scripts/firstitems.lua b/data/creaturescripts/scripts/firstitems.lua new file mode 100644 index 0000000..69a3efa --- /dev/null +++ b/data/creaturescripts/scripts/firstitems.lua @@ -0,0 +1,12 @@ +local firstItems = {2050, 2382} + +function onLogin(player) + if player:getLastLoginSaved() == 0 then + for i = 1, #firstItems do + player:addItem(firstItems[i], 1) + end + player:addItem(player:getSex() == 0 and 2651 or 2650, 1) + player:addItem(ITEM_BAG, 1):addItem(2674, 1) + end + return true +end diff --git a/data/creaturescripts/scripts/login.lua b/data/creaturescripts/scripts/login.lua new file mode 100644 index 0000000..95456db --- /dev/null +++ b/data/creaturescripts/scripts/login.lua @@ -0,0 +1,36 @@ +function onLogin(player) + local loginStr = "Welcome to " .. configManager.getString(configKeys.SERVER_NAME) .. "!" + if player:getLastLoginSaved() <= 0 then + loginStr = loginStr .. " Please choose your outfit." + player:sendOutfitWindow() + else + if loginStr ~= "" then + player:sendTextMessage(MESSAGE_STATUS_DEFAULT, loginStr) + end + + loginStr = string.format("Your last visit was on %s.", os.date("%a %b %d %X %Y", player:getLastLoginSaved())) + end + player:sendTextMessage(MESSAGE_STATUS_DEFAULT, loginStr) + + -- Stamina + nextUseStaminaTime[player.uid] = 0 + + -- Promotion + local vocation = player:getVocation() + local promotion = vocation:getPromotion() + if player:isPremium() then + local value = player:getStorageValue(PlayerStorageKeys.promotion) + if not promotion and value ~= 1 then + player:setStorageValue(PlayerStorageKeys.promotion, 1) + elseif value == 1 then + player:setVocation(promotion) + end + elseif not promotion then + player:setVocation(vocation:getDemotion()) + end + + -- Events + player:registerEvent("PlayerDeath") + player:registerEvent("DropLoot") + return true +end diff --git a/data/creaturescripts/scripts/logout.lua b/data/creaturescripts/scripts/logout.lua new file mode 100644 index 0000000..7c83ff7 --- /dev/null +++ b/data/creaturescripts/scripts/logout.lua @@ -0,0 +1,7 @@ +function onLogout(player) + local playerId = player:getId() + if nextUseStaminaTime[playerId] then + nextUseStaminaTime[playerId] = nil + end + return true +end diff --git a/data/creaturescripts/scripts/offlinetraining.lua b/data/creaturescripts/scripts/offlinetraining.lua new file mode 100644 index 0000000..325f6dd --- /dev/null +++ b/data/creaturescripts/scripts/offlinetraining.lua @@ -0,0 +1,75 @@ +function onLogin(player) + local lastLogout = player:getLastLogout() + local offlineTime = lastLogout ~= 0 and math.min(os.time() - lastLogout, 86400 * 21) or 0 + local offlineTrainingSkill = player:getOfflineTrainingSkill() + if offlineTrainingSkill == -1 then + player:addOfflineTrainingTime(offlineTime * 1000) + return true + end + + player:setOfflineTrainingSkill(-1) + + if offlineTime < 600 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You must be logged out for more than 10 minutes to start offline training.") + return true + end + + local trainingTime = math.max(0, math.min(offlineTime, math.min(43200, player:getOfflineTrainingTime() / 1000))) + player:removeOfflineTrainingTime(trainingTime * 1000) + + local remainder = offlineTime - trainingTime + if remainder > 0 then + player:addOfflineTrainingTime(remainder * 1000) + end + + if trainingTime < 60 then + return true + end + + local text = "During your absence you trained for" + local hours = math.floor(trainingTime / 3600) + if hours > 1 then + text = string.format("%s %d hours", text, hours) + elseif hours == 1 then + text = string.format("%s 1 hour", text) + end + + local minutes = math.floor((trainingTime % 3600) / 60) + if minutes ~= 0 then + if hours ~= 0 then + text = string.format("%s and", text) + end + + if minutes > 1 then + text = string.format("%s %d minutes", text, minutes) + else + text = string.format("%s 1 minute", text) + end + end + + text = string.format("%s.", text) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, text) + + local vocation = player:getVocation() + local promotion = vocation:getPromotion() + local topVocation = not promotion and vocation or promotion + + local updateSkills = false + if table.contains({SKILL_CLUB, SKILL_SWORD, SKILL_AXE, SKILL_DISTANCE}, offlineTrainingSkill) then + local modifier = topVocation:getAttackSpeed() / 1000 + updateSkills = player:addOfflineTrainingTries(offlineTrainingSkill, (trainingTime / modifier) / (offlineTrainingSkill == SKILL_DISTANCE and 4 or 2)) + elseif offlineTrainingSkill == SKILL_MAGLEVEL then + local gainTicks = topVocation:getManaGainTicks() * 2 + if gainTicks == 0 then + gainTicks = 1 + end + + updateSkills = player:addOfflineTrainingTries(SKILL_MAGLEVEL, trainingTime * (vocation:getManaGainAmount() / gainTicks)) + end + + if updateSkills then + player:addOfflineTrainingTries(SKILL_SHIELD, trainingTime / 4) + end + + return true +end diff --git a/data/creaturescripts/scripts/playerdeath.lua b/data/creaturescripts/scripts/playerdeath.lua new file mode 100644 index 0000000..ec5096a --- /dev/null +++ b/data/creaturescripts/scripts/playerdeath.lua @@ -0,0 +1,89 @@ +local deathListEnabled = true +local maxDeathRecords = 5 + +function onDeath(player, corpse, killer, mostDamageKiller, unjustified, mostDamageUnjustified) + local playerId = player:getId() + if nextUseStaminaTime[playerId] then + nextUseStaminaTime[playerId] = nil + end + + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You are dead.") + if not deathListEnabled then + return + end + + local byPlayer = 0 + local killerName + if killer then + if killer:isPlayer() then + byPlayer = 1 + else + local master = killer:getMaster() + if master and master ~= killer and master:isPlayer() then + killer = master + byPlayer = 1 + end + end + killerName = killer:getName() + else + killerName = "field item" + end + + local byPlayerMostDamage = 0 + local mostDamageKillerName + if mostDamageKiller then + if mostDamageKiller:isPlayer() then + byPlayerMostDamage = 1 + else + local master = mostDamageKiller:getMaster() + if master and master ~= mostDamageKiller and master:isPlayer() then + mostDamageKiller = master + byPlayerMostDamage = 1 + end + end + mostDamageName = mostDamageKiller:getName() + else + mostDamageName = "field item" + end + + local playerGuid = player:getGuid() + db.query("INSERT INTO `player_deaths` (`player_id`, `time`, `level`, `killed_by`, `is_player`, `mostdamage_by`, `mostdamage_is_player`, `unjustified`, `mostdamage_unjustified`) VALUES (" .. playerGuid .. ", " .. os.time() .. ", " .. player:getLevel() .. ", " .. db.escapeString(killerName) .. ", " .. byPlayer .. ", " .. db.escapeString(mostDamageName) .. ", " .. byPlayerMostDamage .. ", " .. (unjustified and 1 or 0) .. ", " .. (mostDamageUnjustified and 1 or 0) .. ")") + local resultId = db.storeQuery("SELECT `player_id` FROM `player_deaths` WHERE `player_id` = " .. playerGuid) + + local deathRecords = 0 + local tmpResultId = resultId + while tmpResultId ~= false do + tmpResultId = result.next(resultId) + deathRecords = deathRecords + 1 + end + + if resultId ~= false then + result.free(resultId) + end + + local limit = deathRecords - maxDeathRecords + if limit > 0 then + db.asyncQuery("DELETE FROM `player_deaths` WHERE `player_id` = " .. playerGuid .. " ORDER BY `time` LIMIT " .. limit) + end + + if byPlayer == 1 then + local targetGuild = player:getGuild() + targetGuild = targetGuild and targetGuild:getId() or 0 + if targetGuild ~= 0 then + local killerGuild = killer:getGuild() + killerGuild = killerGuild and killerGuild:getId() or 0 + if killerGuild ~= 0 and targetGuild ~= killerGuild and isInWar(playerId, killer:getId()) then + local warId = false + resultId = db.storeQuery("SELECT `id` FROM `guild_wars` WHERE `status` = 1 AND ((`guild1` = " .. killerGuild .. " AND `guild2` = " .. targetGuild .. ") OR (`guild1` = " .. targetGuild .. " AND `guild2` = " .. killerGuild .. "))") + if resultId ~= false then + warId = result.getNumber(resultId, "id") + result.free(resultId) + end + + if warId ~= false then + db.asyncQuery("INSERT INTO `guildwar_kills` (`killer`, `target`, `killerguild`, `targetguild`, `time`, `warid`) VALUES (" .. db.escapeString(killerName) .. ", " .. db.escapeString(player:getName()) .. ", " .. killerGuild .. ", " .. targetGuild .. ", " .. os.time() .. ", " .. warId .. ")") + end + end + end + end +end diff --git a/data/creaturescripts/scripts/regeneratestamina.lua b/data/creaturescripts/scripts/regeneratestamina.lua new file mode 100644 index 0000000..0440905 --- /dev/null +++ b/data/creaturescripts/scripts/regeneratestamina.lua @@ -0,0 +1,27 @@ +function onLogin(player) + if not configManager.getBoolean(configKeys.STAMINA_SYSTEM) then + return true + end + + local lastLogout = player:getLastLogout() + local offlineTime = lastLogout ~= 0 and math.min(os.time() - lastLogout, 86400 * 21) or 0 + offlineTime = offlineTime - 600 + + if offlineTime < 180 then + return true + end + + local staminaMinutes = player:getStamina() + local maxNormalStaminaRegen = 2400 - math.min(2400, staminaMinutes) + + local regainStaminaMinutes = offlineTime / 180 + if regainStaminaMinutes > maxNormalStaminaRegen then + local happyHourStaminaRegen = (offlineTime - (maxNormalStaminaRegen * 180)) / 600 + staminaMinutes = math.min(2520, math.max(2400, staminaMinutes) + happyHourStaminaRegen) + else + staminaMinutes = staminaMinutes + regainStaminaMinutes + end + + player:setStamina(staminaMinutes) + return true +end diff --git a/data/events/events.xml b/data/events/events.xml new file mode 100644 index 0000000..4327754 --- /dev/null +++ b/data/events/events.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/events/scripts/creature.lua b/data/events/scripts/creature.lua new file mode 100644 index 0000000..1a00735 --- /dev/null +++ b/data/events/scripts/creature.lua @@ -0,0 +1,14 @@ +function Creature:onChangeOutfit(outfit) + return true +end + +function Creature:onAreaCombat(tile, isAggressive) + return RETURNVALUE_NOERROR +end + +function Creature:onTargetCombat(target) + return RETURNVALUE_NOERROR +end + +function Creature:onHear(speaker, words, type) +end diff --git a/data/events/scripts/monster.lua b/data/events/scripts/monster.lua new file mode 100644 index 0000000..3f5c5a9 --- /dev/null +++ b/data/events/scripts/monster.lua @@ -0,0 +1,39 @@ +function Monster:onDropLoot(corpse) + if configManager.getNumber(configKeys.RATE_LOOT) == 0 then + return + end + + local player = Player(corpse:getCorpseOwner()) + local mType = self:getType() + if not player or player:getStamina() > 840 then + local monsterLoot = mType:getLoot() + for i = 1, #monsterLoot do + local item = corpse:createLootItem(monsterLoot[i]) + if not item then + print('[Warning] DropLoot:', 'Could not add loot item to corpse.') + end + end + + if player then + local text = ("Loot of %s: %s"):format(mType:getNameDescription(), corpse:getContentDescription()) + local party = player:getParty() + if party then + party:broadcastPartyLoot(text) + else + player:sendTextMessage(MESSAGE_LOOT, text) + end + end + else + local text = ("Loot of %s: nothing (due to low stamina)"):format(mType:getNameDescription()) + local party = player:getParty() + if party then + party:broadcastPartyLoot(text) + else + player:sendTextMessage(MESSAGE_LOOT, text) + end + end +end + +function Monster:onSpawn(position, startup, artificial) + return true +end diff --git a/data/events/scripts/party.lua b/data/events/scripts/party.lua new file mode 100644 index 0000000..1a6b561 --- /dev/null +++ b/data/events/scripts/party.lua @@ -0,0 +1,35 @@ +function Party:onJoin(player) + return true +end + +function Party:onLeave(player) + return true +end + +function Party:onDisband() + return true +end + +function Party:onShareExperience(exp) + local sharedExperienceMultiplier = 1.20 --20% + local vocationsIds = {} + + local vocationId = self:getLeader():getVocation():getBase():getId() + if vocationId ~= VOCATION_NONE then + table.insert(vocationsIds, vocationId) + end + + for _, member in ipairs(self:getMembers()) do + vocationId = member:getVocation():getBase():getId() + if not table.contains(vocationsIds, vocationId) and vocationId ~= VOCATION_NONE then + table.insert(vocationsIds, vocationId) + end + end + + local size = #vocationsIds + if size > 1 then + sharedExperienceMultiplier = 1.0 + ((size * (5 * (size - 1) + 10)) / 100) + end + + return (exp * sharedExperienceMultiplier) / (#self:getMembers() + 1) +end diff --git a/data/events/scripts/player.lua b/data/events/scripts/player.lua new file mode 100644 index 0000000..cc2ecb1 --- /dev/null +++ b/data/events/scripts/player.lua @@ -0,0 +1,327 @@ +function Player:onBrowseField(position) + return true +end + +function Player:onLook(thing, position, distance) + local description = "You see " .. thing:getDescription(distance) + if self:getGroup():getAccess() then + if thing:isItem() then + description = string.format("%s\nItem ID: %d", description, thing:getId()) + + local actionId = thing:getActionId() + if actionId ~= 0 then + description = string.format("%s, Action ID: %d", description, actionId) + end + + local uniqueId = thing:getAttribute(ITEM_ATTRIBUTE_UNIQUEID) + if uniqueId > 0 and uniqueId < 65536 then + description = string.format("%s, Unique ID: %d", description, uniqueId) + end + + local itemType = thing:getType() + + local transformEquipId = itemType:getTransformEquipId() + local transformDeEquipId = itemType:getTransformDeEquipId() + if transformEquipId ~= 0 then + description = string.format("%s\nTransforms to: %d (onEquip)", description, transformEquipId) + elseif transformDeEquipId ~= 0 then + description = string.format("%s\nTransforms to: %d (onDeEquip)", description, transformDeEquipId) + end + + local decayId = itemType:getDecayId() + if decayId ~= -1 then + description = string.format("%s\nDecays to: %d", description, decayId) + end + elseif thing:isCreature() then + local str = "%s\nHealth: %d / %d" + if thing:isPlayer() and thing:getMaxMana() > 0 then + str = string.format("%s, Mana: %d / %d", str, thing:getMana(), thing:getMaxMana()) + end + description = string.format(str, description, thing:getHealth(), thing:getMaxHealth()) .. "." + end + + local position = thing:getPosition() + description = string.format( + "%s\nPosition: %d, %d, %d", + description, position.x, position.y, position.z + ) + + if thing:isCreature() then + if thing:isPlayer() then + description = string.format("%s\nIP: %s.", description, Game.convertIpToString(thing:getIp())) + end + end + end + self:sendTextMessage(MESSAGE_INFO_DESCR, description) +end + +function Player:onLookInBattleList(creature, distance) + local description = "You see " .. creature:getDescription(distance) + if self:getGroup():getAccess() then + local str = "%s\nHealth: %d / %d" + if creature:isPlayer() and creature:getMaxMana() > 0 then + str = string.format("%s, Mana: %d / %d", str, creature:getMana(), creature:getMaxMana()) + end + description = string.format(str, description, creature:getHealth(), creature:getMaxHealth()) .. "." + + local position = creature:getPosition() + description = string.format( + "%s\nPosition: %d, %d, %d", + description, position.x, position.y, position.z + ) + + if creature:isPlayer() then + description = string.format("%s\nIP: %s", description, Game.convertIpToString(creature:getIp())) + end + end + self:sendTextMessage(MESSAGE_INFO_DESCR, description) +end + +function Player:onLookInTrade(partner, item, distance) + self:sendTextMessage(MESSAGE_INFO_DESCR, "You see " .. item:getDescription(distance)) +end + +function Player:onLookInShop(itemType, count) + return true +end + +function Player:onMoveItem(item, count, fromPosition, toPosition, fromCylinder, toCylinder) + if item:getAttribute("wrapid") ~= 0 then + local tile = Tile(toPosition) + if (fromPosition.x ~= CONTAINER_POSITION and toPosition.x ~= CONTAINER_POSITION) or tile and not tile:getHouse() then + if tile and not tile:getHouse() then + self:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + return false + end + end + end + + if toPosition.x ~= CONTAINER_POSITION then + return true + end + + if item:getTopParent() == self and bit.band(toPosition.y, 0x40) == 0 then + local itemType, moveItem = ItemType(item:getId()) + if bit.band(itemType:getSlotPosition(), SLOTP_TWO_HAND) ~= 0 and toPosition.y == CONST_SLOT_LEFT then + moveItem = self:getSlotItem(CONST_SLOT_RIGHT) + elseif itemType:getWeaponType() == WEAPON_SHIELD and toPosition.y == CONST_SLOT_RIGHT then + moveItem = self:getSlotItem(CONST_SLOT_LEFT) + if moveItem and bit.band(ItemType(moveItem:getId()):getSlotPosition(), SLOTP_TWO_HAND) == 0 then + return true + end + end + + if moveItem then + local parent = item:getParent() + if parent:isContainer() and parent:getSize() == parent:getCapacity() then + self:sendTextMessage(MESSAGE_STATUS_SMALL, Game.getReturnMessage(RETURNVALUE_CONTAINERNOTENOUGHROOM)) + return false + else + return moveItem:moveTo(parent) + end + end + end + + return true +end + +function Player:onItemMoved(item, count, fromPosition, toPosition, fromCylinder, toCylinder) +end + +function Player:onMoveCreature(creature, fromPosition, toPosition) + return true +end + +local function hasPendingReport(name, targetName, reportType) + local f = io.open(string.format("data/reports/players/%s-%s-%d.txt", name, targetName, reportType), "r") + if f then + io.close(f) + return true + else + return false + end +end + +function Player:onReportRuleViolation(targetName, reportType, reportReason, comment, translation) + local name = self:getName() + if hasPendingReport(name, targetName, reportType) then + self:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Your report is being processed.") + return + end + + local file = io.open(string.format("data/reports/players/%s-%s-%d.txt", name, targetName, reportType), "a") + if not file then + self:sendTextMessage(MESSAGE_EVENT_ADVANCE, "There was an error when processing your report, please contact a gamemaster.") + return + end + + io.output(file) + io.write("------------------------------\n") + io.write("Reported by: " .. name .. "\n") + io.write("Target: " .. targetName .. "\n") + io.write("Type: " .. reportType .. "\n") + io.write("Reason: " .. reportReason .. "\n") + io.write("Comment: " .. comment .. "\n") + if reportType ~= REPORT_TYPE_BOT then + io.write("Translation: " .. translation .. "\n") + end + io.write("------------------------------\n") + io.close(file) + self:sendTextMessage(MESSAGE_EVENT_ADVANCE, string.format("Thank you for reporting %s. Your report will be processed by %s team as soon as possible.", targetName, configManager.getString(configKeys.SERVER_NAME))) + return +end + +function Player:onReportBug(message, position, category) + if self:getAccountType() == ACCOUNT_TYPE_NORMAL then + return false + end + + local name = self:getName() + local file = io.open("data/reports/bugs/" .. name .. " report.txt", "a") + + if not file then + self:sendTextMessage(MESSAGE_EVENT_DEFAULT, "There was an error when processing your report, please contact a gamemaster.") + return true + end + + io.output(file) + io.write("------------------------------\n") + io.write("Name: " .. name) + if category == BUG_CATEGORY_MAP then + io.write(" [Map position: " .. position.x .. ", " .. position.y .. ", " .. position.z .. "]") + end + local playerPosition = self:getPosition() + io.write(" [Player Position: " .. playerPosition.x .. ", " .. playerPosition.y .. ", " .. playerPosition.z .. "]\n") + io.write("Comment: " .. message .. "\n") + io.close(file) + + self:sendTextMessage(MESSAGE_EVENT_DEFAULT, "Your report has been sent to " .. configManager.getString(configKeys.SERVER_NAME) .. ".") + return true +end + +function Player:onTurn(direction) + return true +end + +function Player:onTradeRequest(target, item) + return true +end + +function Player:onTradeAccept(target, item, targetItem) + return true +end + +function Player:onTradeCompleted(target, item, targetItem, isSuccess) + return +end + +local soulCondition = Condition(CONDITION_SOUL, CONDITIONID_DEFAULT) +soulCondition:setTicks(4 * 60 * 1000) +soulCondition:setParameter(CONDITION_PARAM_SOULGAIN, 1) + +local function useStamina(player) + local staminaMinutes = player:getStamina() + if staminaMinutes == 0 then + return + end + + local playerId = player:getId() + local currentTime = os.time() + local timePassed = currentTime - nextUseStaminaTime[playerId] + if timePassed <= 0 then + return + end + + if timePassed > 60 then + if staminaMinutes > 2 then + staminaMinutes = staminaMinutes - 2 + else + staminaMinutes = 0 + end + nextUseStaminaTime[playerId] = currentTime + 120 + else + staminaMinutes = staminaMinutes - 1 + nextUseStaminaTime[playerId] = currentTime + 60 + end + player:setStamina(staminaMinutes) +end + +function Player:onGainExperience(source, exp, rawExp) + if not source or source:isPlayer() then + return exp + end + + -- Soul regeneration + local vocation = self:getVocation() + if self:getSoul() < vocation:getMaxSoul() and exp >= self:getLevel() then + soulCondition:setParameter(CONDITION_PARAM_SOULTICKS, vocation:getSoulGainTicks() * 1000) + self:addCondition(soulCondition) + end + + -- Apply experience stage multiplier + exp = exp * Game.getExperienceStage(self:getLevel()) + + -- Stamina modifier + if configManager.getBoolean(configKeys.STAMINA_SYSTEM) then + useStamina(self) + + local staminaMinutes = self:getStamina() + if staminaMinutes > 2400 and self:isPremium() then + exp = exp * 1.5 + elseif staminaMinutes <= 840 then + exp = exp * 0.5 + end + end + + return exp +end + +function Player:onLoseExperience(exp) + return exp +end + +function Player:onGainSkillTries(skill, tries) + if APPLY_SKILL_MULTIPLIER == false then + return tries + end + + if skill == SKILL_MAGLEVEL then + return tries * configManager.getNumber(configKeys.RATE_MAGIC) + end + return tries * configManager.getNumber(configKeys.RATE_SKILL) +end + +function Player:onWrapItem(item, position) + local topCylinder = item:getTopParent() + if not topCylinder then + return + end + + local tile = Tile(topCylinder:getPosition()) + if not tile then + return + end + + local house = tile:getHouse() + if not house then + self:sendCancelMessage("You can only wrap and unwrap this item inside a house.") + return + end + + if house ~= self:getHouse() and not string.find(house:getAccessList(SUBOWNER_LIST):lower(), "%f[%a]" .. self:getName():lower() .. "%f[%A]") then + self:sendCancelMessage("You cannot wrap or unwrap items from a house, which you are only guest to.") + return + end + + local wrapId = item:getAttribute("wrapid") + if wrapId == 0 then + return + end + + local oldId = item:getId() + item:remove(1) + local item = tile:addItem(wrapId) + if item then + item:setAttribute("wrapid", oldId) + end +end diff --git a/data/global.lua b/data/global.lua new file mode 100644 index 0000000..0c2e01e --- /dev/null +++ b/data/global.lua @@ -0,0 +1,70 @@ +math.randomseed(os.time()) +dofile('data/lib/lib.lua') + +ropeSpots = {384, 418, 8278, 8592, 13189, 14435, 14436, 14857, 15635, 19518, 24621, 24622, 24623, 24624, 26019} + +doors = {[1209] = 1211, [1210] = 1211, [1212] = 1214, [1213] = 1214, [1219] = 1220, [1221] = 1222, [1231] = 1233, [1232] = 1233, [1234] = 1236, [1235] = 1236, [1237] = 1238, [1239] = 1240, [1249] = 1251, [1250] = 1251, [1252] = 1254, [1253] = 1254, [1539] = 1540, [1541] = 1542, [3535] = 3537, [3536] = 3537, [3538] = 3539, [3544] = 3546, [3545] = 3546, [3547] = 3548, [4913] = 4915, [4914] = 4915, [4916] = 4918, [4917] = 4918, [5082] = 5083, [5084] = 5085, [5098] = 5100, [5099] = 5100, [5101] = 5102, [5107] = 5109, [5108] = 5109, [5110] = 5111, [5116] = 5118, [5117] = 5118, [5119] = 5120, [5125] = 5127, [5126] = 5127, [5128] = 5129, [5134] = 5136, [5135] = 5136, [5137] = 5139, [5138] = 5139, [5140] = 5142, [5141] = 5142, [5143] = 5145, [5144] = 5145, [5278] = 5280, [5279] = 5280, [5281] = 5283, [5282] = 5283, [5284] = 5285, [5286] = 5287, [5515] = 5516, [5517] = 5518, [5732] = 5734, [5733] = 5734, [5735] = 5737, [5736] = 5737, [6192] = 6194, [6193] = 6194, [6195] = 6197, [6196] = 6197, [6198] = 6199, [6200] = 6201, [6249] = 6251, [6250] = 6251, [6252] = 6254, [6253] = 6254, [6255] = 6256, [6257] = 6258, [6795] = 6796, [6797] = 6798, [6799] = 6800, [6801] = 6802, [6891] = 6893, [6892] = 6893, [6894] = 6895, [6900] = 6902, [6901] = 6902, [6903] = 6904, [7033] = 7035, [7034] = 7035, [7036] = 7037, [7042] = 7044, [7043] = 7044, [7045] = 7046, [7054] = 7055, [7056] = 7057, [8541] = 8543, [8542] = 8543, [8544] = 8546, [8545] = 8546, [8547] = 8548, [8549] = 8550, [9165] = 9167, [9166] = 9167, [9168] = 9170, [9169] = 9170, [9171] = 9172, [9173] = 9174, [9267] = 9269, [9268] = 9269, [9270] = 9272, [9271] = 9272, [9273] = 9274, [9275] = 9276, [10276] = 10277, [10274] = 10275, [10268] = 10270, [10269] = 10270, [10271] = 10273, [10272] = 10273, [10471] = 10472, [10480] = 10481, [10477] = 10479, [10478] = 10479, [10468] = 10470, [10469] = 10470, [10775] = 10777, [10776] = 10777, [12092] = 12094, [12093] = 12094, [12188] = 12190, [12189] = 12190, [19840] = 19842, [19841] = 19842, [19843] = 19844, [19980] = 19982, [19981] = 19982, [19983] = 19984, [20273] = 20275, [20274] = 20275, [20276] = 20277, [17235] = 17236, [18208] = 18209, [13022] = 13023, [10784] = 10786, [10785] = 10786, [12099] = 12101, [12100] = 12101, [12197] = 12199, [12198] = 12199, [19849] = 19851, [19850] = 19851, [19852] = 19853, [19989] = 19991, [19990] = 19991, [19992] = 19993, [20282] = 20284, [20283] = 20284, [20285] = 20286, [17237] = 17238, [13020] = 13021, [10780] = 10781, [12095] = 12096, [12195] = 12196, [19845] = 19846, [19985] = 19986, [20278] = 20279, [10789] = 10790, [12102] = 12103, [12204] = 12205, [19854] = 19855, [19994] = 19995, [20287] = 20288, [10782] = 10783, [12097] = 12098, [12193] = 12194, [19847] = 19848, [19987] = 19988, [20280] = 20281, [10791] = 10792, [12104] = 12105, [12202] = 12203, [19856] = 19857, [19996] = 19997, [20289] = 20290, [25158] = 25159, [25160] = 25161} +verticalOpenDoors = {1211, 1220, 1224, 1228, 1233, 1238, 1242, 1246, 1251, 1256, 1260, 1540, 3546, 3548, 3550, 3552, 4915, 5083, 5109, 5111, 5113, 5115, 5127, 5129, 5131, 5133, 5142, 5145, 5283, 5285, 5289, 5293, 5516, 5737, 5749, 6194, 6199, 6203, 6207, 6251, 6256, 6260, 6264, 6798, 6802, 6902, 6904, 6906, 6908, 7044, 7046, 7048, 7050, 7055, 8543, 8548, 8552, 8556, 9167, 9172, 9269, 9274, 9274, 9269, 9278, 9282, 10270, 10275, 10279, 10283, 10479, 10481, 10485, 10483, 10786, 12101, 12199, 19851, 19853, 19991, 19993, 20284, 20286, 17238, 13021, 10790, 12103, 12205, 19855, 19995, 20288, 10792, 12105, 12203, 19857, 19997, 20290} +horizontalOpenDoors = {1214, 1222, 1226, 1230, 1236, 1240, 1244, 1248, 1254, 1258, 1262, 1542, 3537, 3539, 3541, 3543, 4918, 5085, 5100, 5102, 5104, 5106, 5118, 5120, 5122, 5124, 5136, 5139, 5280, 5287, 5291, 5295, 5518, 5734, 5746, 6197, 6201, 6205, 6209, 6254, 6258, 6262, 6266, 6796, 6800, 6893, 6895, 6897, 6899, 7035, 7037, 7039, 7041, 7057, 8546, 8550, 8554, 8558, 9170, 9174, 9272, 9276, 9280, 9284, 10273, 10277, 10281, 10285, 10470, 10472, 10476, 10474, 10777, 12094, 12190, 19842, 19844, 19982, 19984, 20275, 20277, 17236, 18209, 13023, 10781, 12096, 12196, 19846, 19986, 20279, 10783, 12098, 12194, 19848, 19988, 20281} +openSpecialDoors = {1224, 1226, 1228, 1230, 1242, 1244, 1246, 1248, 1256, 1258, 1260, 1262, 3541, 3543, 3550, 3552, 5104, 5106, 5113, 5115, 5122, 5124, 5131, 5133, 5289, 5291, 5293, 5295, 6203, 6205, 6207, 6209, 6260, 6262, 6264, 6266, 6897, 6899, 6906, 6908, 7039, 7041, 7048, 7050, 8552, 8554, 8556, 8558, 9176, 9178, 9180, 9182, 9278, 9280, 9282, 9284, 10279, 10281, 10283, 10285, 10474, 10476, 10483, 10485, 10781, 12096, 12196, 19846, 19986, 20279, 10783, 12098, 12194, 19848, 19988, 20281, 10790, 12103, 12205, 19855, 19995, 20288, 10792, 12105, 12203, 19857, 19997, 20290, 25163, 25165} +questDoors = {1223, 1225, 1241, 1243, 1255, 1257, 3542, 3551, 5105, 5114, 5123, 5132, 5288, 5290, 5745, 5748, 6202, 6204, 6259, 6261, 6898, 6907, 7040, 7049, 8551, 8553, 9175, 9177, 9277, 9279, 10278, 10280, 10475, 10484, 10782, 10791, 12097, 12104, 12193, 12202, 19847, 19856, 19987, 19996, 20280, 20289, 25162, 25164} +levelDoors = {1227, 1229, 1245, 1247, 1259, 1261, 3540, 3549, 5103, 5112, 5121, 5130, 5292, 5294, 6206, 6208, 6263, 6265, 6896, 6905, 7038, 7047, 8555, 8557, 9179, 9181, 9281, 9283, 10282, 10284, 10473, 10482, 10780, 10789, 10780, 12095, 12102, 12204, 12195, 19845, 19854, 19985, 19994, 20278, 20287} +keys = {2086, 2087, 2088, 2089, 2090, 2091, 2092, 10032} + +function getDistanceBetween(firstPosition, secondPosition) + local xDif = math.abs(firstPosition.x - secondPosition.x) + local yDif = math.abs(firstPosition.y - secondPosition.y) + local posDif = math.max(xDif, yDif) + if firstPosition.z ~= secondPosition.z then + posDif = posDif + 15 + end + return posDif +end + +function getFormattedWorldTime() + local worldTime = getWorldTime() + local hours = math.floor(worldTime / 60) + + local minutes = worldTime % 60 + if minutes < 10 then + minutes = '0' .. minutes + end + return hours .. ':' .. minutes +end + +function getLootRandom() + return math.random(0, MAX_LOOTCHANCE) / configManager.getNumber(configKeys.RATE_LOOT) +end + +table.contains = function(array, value) + for _, targetColumn in pairs(array) do + if targetColumn == value then + return true + end + end + return false +end + +string.split = function(str, sep) + local res = {} + for v in str:gmatch("([^" .. sep .. "]+)") do + res[#res + 1] = v + end + return res +end + +string.splitTrimmed = function(str, sep) + local res = {} + for v in str:gmatch("([^" .. sep .. "]+)") do + res[#res + 1] = v:trim() + end + return res +end + +string.trim = function(str) + return str:match'^()%s*$' and '' or str:match'^%s*(.*%S)' +end + +if not nextUseStaminaTime then + nextUseStaminaTime = {} +end diff --git a/data/globalevents/globalevents.xml b/data/globalevents/globalevents.xml new file mode 100644 index 0000000..942d72a --- /dev/null +++ b/data/globalevents/globalevents.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/data/globalevents/lib/globalevents.lua b/data/globalevents/lib/globalevents.lua new file mode 100644 index 0000000..6116bcc --- /dev/null +++ b/data/globalevents/lib/globalevents.lua @@ -0,0 +1 @@ +-- empty file -- diff --git a/data/globalevents/scripts/record.lua b/data/globalevents/scripts/record.lua new file mode 100644 index 0000000..47d6b79 --- /dev/null +++ b/data/globalevents/scripts/record.lua @@ -0,0 +1,4 @@ +function onRecord(current, old) + addEvent(broadcastMessage, 150, "New record: " .. current .. " players are logged in.", MESSAGE_STATUS_DEFAULT) + return true +end diff --git a/data/globalevents/scripts/serversave.lua b/data/globalevents/scripts/serversave.lua new file mode 100644 index 0000000..f6210ab --- /dev/null +++ b/data/globalevents/scripts/serversave.lua @@ -0,0 +1,37 @@ +local function ServerSave() + if configManager.getBoolean(configKeys.SERVER_SAVE_CLEAN_MAP) then + cleanMap() + end + + if configManager.getBoolean(configKeys.SERVER_SAVE_CLOSE) then + Game.setGameState(GAME_STATE_CLOSED) + end + + if configManager.getBoolean(configKeys.SERVER_SAVE_SHUTDOWN) then + Game.setGameState(GAME_STATE_SHUTDOWN) + end +end + +local function ServerSaveWarning(time) + local remaningTime = tonumber(time) - 60000 + + if configManager.getBoolean(configKeys.SERVER_SAVE_NOTIFY_MESSAGE) then + Game.broadcastMessage("Server is saving game in " .. (remaningTime/60000) .." minute(s). Please logout.", MESSAGE_STATUS_WARNING) + end + + if remaningTime > 60000 then + addEvent(ServerSaveWarning, 60000, remaningTime) + else + addEvent(ServerSave, 60000) + end +end + +function onTime(interval) + local remaningTime = configManager.getNumber(configKeys.SERVER_SAVE_NOTIFY_DURATION) * 60000 + if configManager.getBoolean(configKeys.SERVER_SAVE_NOTIFY_MESSAGE) then + Game.broadcastMessage("Server is saving game in " .. (remaningTime/60000) .." minute(s). Please logout.", MESSAGE_STATUS_WARNING) + end + + addEvent(ServerSaveWarning, 60000, remaningTime) + return not configManager.getBoolean(configKeys.SERVER_SAVE_SHUTDOWN) +end diff --git a/data/globalevents/scripts/startup.lua b/data/globalevents/scripts/startup.lua new file mode 100644 index 0000000..c60a94d --- /dev/null +++ b/data/globalevents/scripts/startup.lua @@ -0,0 +1,45 @@ +function onStartup() + + db.query("TRUNCATE TABLE `players_online`") + db.asyncQuery("DELETE FROM `guild_wars` WHERE `status` = 0") + db.asyncQuery("DELETE FROM `players` WHERE `deletion` != 0 AND `deletion` < " .. os.time()) + db.asyncQuery("DELETE FROM `ip_bans` WHERE `expires_at` != 0 AND `expires_at` <= " .. os.time()) + db.asyncQuery("DELETE FROM `market_history` WHERE `inserted` <= " .. (os.time() - configManager.getNumber(configKeys.MARKET_OFFER_DURATION))) + + -- Move expired bans to ban history + local resultId = db.storeQuery("SELECT * FROM `account_bans` WHERE `expires_at` != 0 AND `expires_at` <= " .. os.time()) + if resultId ~= false then + repeat + local accountId = result.getNumber(resultId, "account_id") + db.asyncQuery("INSERT INTO `account_ban_history` (`account_id`, `reason`, `banned_at`, `expired_at`, `banned_by`) VALUES (" .. accountId .. ", " .. db.escapeString(result.getString(resultId, "reason")) .. ", " .. result.getNumber(resultId, "banned_at") .. ", " .. result.getNumber(resultId, "expires_at") .. ", " .. result.getNumber(resultId, "banned_by") .. ")") + db.asyncQuery("DELETE FROM `account_bans` WHERE `account_id` = " .. accountId) + until not result.next(resultId) + result.free(resultId) + end + + -- Check house auctions + local resultId = db.storeQuery("SELECT `id`, `highest_bidder`, `last_bid`, (SELECT `balance` FROM `players` WHERE `players`.`id` = `highest_bidder`) AS `balance` FROM `houses` WHERE `owner` = 0 AND `bid_end` != 0 AND `bid_end` < " .. os.time()) + if resultId ~= false then + repeat + local house = House(result.getNumber(resultId, "id")) + if house then + local highestBidder = result.getNumber(resultId, "highest_bidder") + local balance = result.getNumber(resultId, "balance") + local lastBid = result.getNumber(resultId, "last_bid") + if balance >= lastBid then + db.query("UPDATE `players` SET `balance` = " .. (balance - lastBid) .. " WHERE `id` = " .. highestBidder) + house:setOwnerGuid(highestBidder) + end + db.asyncQuery("UPDATE `houses` SET `last_bid` = 0, `bid_end` = 0, `highest_bidder` = 0, `bid` = 0 WHERE `id` = " .. house:getId()) + end + until not result.next(resultId) + result.free(resultId) + end + + -- store towns in database + db.query("TRUNCATE TABLE `towns`") + for i, town in ipairs(Game.getTowns()) do + local position = town:getTemplePosition() + db.query("INSERT INTO `towns` (`id`, `name`, `posx`, `posy`, `posz`) VALUES (" .. town:getId() .. ", " .. db.escapeString(town:getName()) .. ", " .. position.x .. ", " .. position.y .. ", " .. position.z .. ")") + end +end diff --git a/data/items/items.otb b/data/items/items.otb new file mode 100644 index 0000000..da589f2 Binary files /dev/null and b/data/items/items.otb differ diff --git a/data/items/items.xml b/data/items/items.xml new file mode 100644 index 0000000..b9d240c --- /dev/null +++ b/data/items/items.xml @@ -0,0 +1,36177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/lib/compat/compat.lua b/data/lib/compat/compat.lua new file mode 100644 index 0000000..0beaba1 --- /dev/null +++ b/data/lib/compat/compat.lua @@ -0,0 +1,1314 @@ +TRUE = true +FALSE = false + +result.getDataInt = result.getNumber +result.getDataLong = result.getNumber +result.getDataString = result.getString +result.getDataStream = result.getStream + +LUA_ERROR = false +LUA_NO_ERROR = true + +STACKPOS_GROUND = 0 +STACKPOS_FIRST_ITEM_ABOVE_GROUNDTILE = 1 +STACKPOS_SECOND_ITEM_ABOVE_GROUNDTILE = 2 +STACKPOS_THIRD_ITEM_ABOVE_GROUNDTILE = 3 +STACKPOS_FOURTH_ITEM_ABOVE_GROUNDTILE = 4 +STACKPOS_FIFTH_ITEM_ABOVE_GROUNDTILE = 5 +STACKPOS_TOP_CREATURE = 253 +STACKPOS_TOP_FIELD = 254 +STACKPOS_TOP_MOVEABLE_ITEM_OR_CREATURE = 255 + +THING_TYPE_PLAYER = CREATURETYPE_PLAYER + 1 +THING_TYPE_MONSTER = CREATURETYPE_MONSTER + 1 +THING_TYPE_NPC = CREATURETYPE_NPC + 1 + +COMBAT_POISONDAMAGE = COMBAT_EARTHDAMAGE +CONDITION_EXHAUST = CONDITION_EXHAUST_WEAPON +TALKTYPE_ORANGE_1 = TALKTYPE_MONSTER_SAY +TALKTYPE_ORANGE_2 = TALKTYPE_MONSTER_YELL + +NORTH = DIRECTION_NORTH +EAST = DIRECTION_EAST +SOUTH = DIRECTION_SOUTH +WEST = DIRECTION_WEST +SOUTHWEST = DIRECTION_SOUTHWEST +SOUTHEAST = DIRECTION_SOUTHEAST +NORTHWEST = DIRECTION_NORTHWEST +NORTHEAST = DIRECTION_NORTHEAST + +do + local function CreatureIndex(self, key) + local methods = getmetatable(self) + if key == "uid" then + return methods.getId(self) + elseif key == "type" then + local creatureType = 0 + if methods.isPlayer(self) then + creatureType = THING_TYPE_PLAYER + elseif methods.isMonster(self) then + creatureType = THING_TYPE_MONSTER + elseif methods.isNpc(self) then + creatureType = THING_TYPE_NPC + end + return creatureType + elseif key == "itemid" then + return 1 + elseif key == "actionid" then + return 0 + end + return methods[key] + end + rawgetmetatable("Player").__index = CreatureIndex + rawgetmetatable("Monster").__index = CreatureIndex + rawgetmetatable("Npc").__index = CreatureIndex +end + +do + local function ItemIndex(self, key) + local methods = getmetatable(self) + if key == "itemid" then + return methods.getId(self) + elseif key == "actionid" then + return methods.getActionId(self) + elseif key == "uid" then + return methods.getUniqueId(self) + elseif key == "type" then + return methods.getSubType(self) + end + return methods[key] + end + rawgetmetatable("Item").__index = ItemIndex + rawgetmetatable("Container").__index = ItemIndex + rawgetmetatable("Teleport").__index = ItemIndex +end + +do + local function ActionNewIndex(self, key, value) + if key == "onUse" then + self:onUse(value) + return + end + rawset(self, key, value) + end + rawgetmetatable("Action").__newindex = ActionNewIndex +end + +do + local function TalkActionNewIndex(self, key, value) + if key == "onSay" then + self:onSay(value) + return + end + rawset(self, key, value) + end + rawgetmetatable("TalkAction").__newindex = TalkActionNewIndex +end + +do + local function CreatureEventNewIndex(self, key, value) + if key == "onLogin" then + self:type("login") + self:onLogin(value) + return + elseif key == "onLogout" then + self:type("logout") + self:onLogout(value) + return + elseif key == "onThink" then + self:type("think") + self:onThink(value) + return + elseif key == "onPrepareDeath" then + self:type("preparedeath") + self:onPrepareDeath(value) + return + elseif key == "onDeath" then + self:type("death") + self:onDeath(value) + return + elseif key == "onKill" then + self:type("kill") + self:onKill(value) + return + elseif key == "onAdvance" then + self:type("advance") + self:onAdvance(value) + return + elseif key == "onModalWindow" then + self:type("modalwindow") + self:onModalWindow(value) + return + elseif key == "onTextEdit" then + self:type("textedit") + self:onTextEdit(value) + return + elseif key == "onHealthChange" then + self:type("healthchange") + self:onHealthChange(value) + return + elseif key == "onManaChange" then + self:type("manachange") + self:onManaChange(value) + return + elseif key == "onExtendedOpcode" then + self:type("extendedopcode") + self:onExtendedOpcode(value) + return + end + rawset(self, key, value) + end + rawgetmetatable("CreatureEvent").__newindex = CreatureEventNewIndex +end + +do + local function MoveEventNewIndex(self, key, value) + if key == "onEquip" then + self:type("equip") + self:onEquip(value) + return + elseif key == "onDeEquip" then + self:type("deequip") + self:onDeEquip(value) + return + elseif key == "onAddItem" then + self:type("additem") + self:onAddItem(value) + return + elseif key == "onRemoveItem" then + self:type("removeitem") + self:onRemoveItem(value) + return + elseif key == "onStepIn" then + self:type("stepin") + self:onStepIn(value) + return + elseif key == "onStepOut" then + self:type("stepout") + self:onStepOut(value) + return + end + rawset(self, key, value) + end + rawgetmetatable("MoveEvent").__newindex = MoveEventNewIndex +end + +do + local function GlobalEventNewIndex(self, key, value) + if key == "onThink" then + self:onThink(value) + return + elseif key == "onTime" then + self:onTime(value) + return + elseif key == "onStartup" then + self:type("startup") + self:onStartup(value) + return + elseif key == "onShutdown" then + self:type("shutdown") + self:onShutdown(value) + return + elseif key == "onRecord" then + self:type("record") + self:onRecord(value) + return + end + rawset(self, key, value) + end + rawgetmetatable("GlobalEvent").__newindex = GlobalEventNewIndex +end + +do + local function WeaponNewIndex(self, key, value) + if key == "onUseWeapon" then + self:onUseWeapon(value) + return + end + rawset(self, key, value) + end + rawgetmetatable("Weapon").__newindex = WeaponNewIndex +end + +do + local function SpellNewIndex(self, key, value) + if key == "onCastSpell" then + self:onCastSpell(value) + return + end + rawset(self, key, value) + end + rawgetmetatable("Spell").__newindex = SpellNewIndex +end + +do + local function MonsterTypeNewIndex(self, key, value) + if key == "onThink" then + self:eventType(MONSTERS_EVENT_THINK) + self:onThink(value) + return + elseif key == "onAppear" then + self:eventType(MONSTERS_EVENT_APPEAR) + self:onAppear(value) + return + elseif key == "onDisappear" then + self:eventType(MONSTERS_EVENT_DISAPPEAR) + self:onDisappear(value) + return + elseif key == "onMove" then + self:eventType(MONSTERS_EVENT_MOVE) + self:onMove(value) + return + elseif key == "onSay" then + self:eventType(MONSTERS_EVENT_SAY) + self:onSay(value) + return + end + rawset(self, key, value) + end + rawgetmetatable("MonsterType").__newindex = MonsterTypeNewIndex +end + +function pushThing(thing) + local t = {uid = 0, itemid = 0, type = 0, actionid = 0} + if thing then + if thing:isItem() then + t.uid = thing:getUniqueId() + t.itemid = thing:getId() + if ItemType(t.itemid):hasSubType() then + t.type = thing:getSubType() + end + t.actionid = thing:getActionId() + elseif thing:isCreature() then + t.uid = thing:getId() + t.itemid = 1 + if thing:isPlayer() then + t.type = THING_TYPE_PLAYER + elseif thing:isMonster() then + t.type = THING_TYPE_MONSTER + else + t.type = THING_TYPE_NPC + end + end + end + return t +end + +createCombatObject = Combat +addCombatCondition = Combat.addCondition +setCombatArea = Combat.setArea +setCombatCallback = Combat.setCallback +setCombatFormula = Combat.setFormula +setCombatParam = Combat.setParameter + +Combat.setCondition = function(...) + print("[Warning] Function Combat.setCondition was renamed to Combat.addCondition and will be removed in the future") + Combat.addCondition(...) +end + +setCombatCondition = function(...) + print("[Warning] Function setCombatCondition was renamed to addCombatCondition and will be removed in the future") + Combat.addCondition(...) +end + +function doTargetCombatHealth(...) return doTargetCombat(...) end +function doAreaCombatHealth(...) return doAreaCombat(...) end +function doTargetCombatMana(cid, target, min, max, effect) return doTargetCombat(cid, target, COMBAT_MANADRAIN, min, max, effect) end +function doAreaCombatMana(cid, pos, area, min, max, effect) return doAreaCombat(cid, COMBAT_MANADRAIN, pos, area, min, max, effect) end + +createConditionObject = Condition +setConditionParam = Condition.setParameter +setConditionFormula = Condition.setFormula +addDamageCondition = Condition.addDamage +addOutfitCondition = Condition.setOutfit + +function doCombat(cid, combat, var) return combat:execute(cid, var) end + +function isCreature(cid) return Creature(cid) end +function isPlayer(cid) return Player(cid) end +function isMonster(cid) return Monster(cid) end +function isSummon(cid) return Creature(cid):getMaster() end +function isNpc(cid) return Npc(cid) end +function isItem(uid) return Item(uid) end +function isContainer(uid) return Container(uid) end + +function getCreatureName(cid) local c = Creature(cid) return c and c:getName() or false end +function getCreatureHealth(cid) local c = Creature(cid) return c and c:getHealth() or false end +function getCreatureMaxHealth(cid) local c = Creature(cid) return c and c:getMaxHealth() or false end +function getCreaturePosition(cid) local c = Creature(cid) return c and c:getPosition() or false end +function getCreatureOutfit(cid) local c = Creature(cid) return c and c:getOutfit() or false end +function getCreatureSpeed(cid) local c = Creature(cid) return c and c:getSpeed() or false end +function getCreatureBaseSpeed(cid) local c = Creature(cid) return c and c:getBaseSpeed() or false end + +function getCreatureTarget(cid) + local c = Creature(cid) + if c then + local target = c:getTarget() + return target and target:getId() or 0 + end + return false +end + +function getCreatureMaster(cid) + local c = Creature(cid) + if c then + local master = c:getMaster() + return master and master:getId() or c:getId() + end + return false +end + +function getCreatureSummons(cid) + local c = Creature(cid) + if c == nil then + return false + end + + local result = {} + for _, summon in ipairs(c:getSummons()) do + result[#result + 1] = summon:getId() + end + return result +end + +getCreaturePos = getCreaturePosition + +function doCreatureAddHealth(cid, health) local c = Creature(cid) return c and c:addHealth(health) or false end +function doRemoveCreature(cid) local c = Creature(cid) return c and c:remove() or false end +function doCreatureSetLookDir(cid, direction) local c = Creature(cid) return c and c:setDirection(direction) or false end +function doCreatureSay(cid, text, type, ...) local c = Creature(cid) return c and c:say(text, type, ...) or false end +function doCreatureChangeOutfit(cid, outfit) local c = Creature(cid) return c and c:setOutfit(outfit) or false end +function doSetCreatureDropLoot(cid, doDrop) local c = Creature(cid) return c and c:setDropLoot(doDrop) or false end +function doChangeSpeed(cid, delta) local c = Creature(cid) return c and c:changeSpeed(delta) or false end +function doAddCondition(cid, conditionId) local c = Creature(cid) return c and c:addCondition(conditionId) or false end +function doRemoveCondition(cid, conditionType, subId) local c = Creature(cid) return c and (c:removeCondition(conditionType, CONDITIONID_COMBAT, subId) or c:removeCondition(conditionType, CONDITIONID_DEFAULT, subId) or true) end +function getCreatureCondition(cid, type, subId) local c = Creature(cid) return c and c:hasCondition(type, subId) or false end + +doSetCreatureDirection = doCreatureSetLookDir + +function registerCreatureEvent(cid, name) local c = Creature(cid) return c and c:registerEvent(name) or false end +function unregisterCreatureEvent(cid, name) local c = Creature(cid) return c and c:unregisterEvent(name) or false end + +function getPlayerByName(name) local p = Player(name) return p and p:getId() or false end +function getIPByPlayerName(name) local p = Player(name) return p and p:getIp() or false end +function getPlayerGUID(cid) local p = Player(cid) return p and p:getGuid() or false end +function getPlayerIp(cid) local p = Player(cid) return p and p:getIp() or false end +function getPlayerAccountType(cid) local p = Player(cid) return p and p:getAccountType() or false end +function getPlayerLastLoginSaved(cid) local p = Player(cid) return p and p:getLastLoginSaved() or false end +function getPlayerName(cid) local p = Player(cid) return p and p:getName() or false end +function getPlayerFreeCap(cid) local p = Player(cid) return p and (p:getFreeCapacity() / 100) or false end +function getPlayerPosition(cid) local p = Player(cid) return p and p:getPosition() or false end +function getPlayerMagLevel(cid) local p = Player(cid) return p and p:getMagicLevel() or false end +function getPlayerAccess(cid) + local player = Player(cid) + if player == nil then + return false + end + return player:getGroup():getAccess() and 1 or 0 +end +function getPlayerSkill(cid, skillId) local p = Player(cid) return p and p:getSkillLevel(skillId) or false end +function getPlayerMana(cid) local p = Player(cid) return p and p:getMana() or false end +function getPlayerMaxMana(cid) local p = Player(cid) return p and p:getMaxMana() or false end +function getPlayerLevel(cid) local p = Player(cid) return p and p:getLevel() or false end +function getPlayerTown(cid) local p = Player(cid) return p and p:getTown():getId() or false end +function getPlayerVocation(cid) local p = Player(cid) return p and p:getVocation():getId() or false end +function getPlayerSoul(cid) local p = Player(cid) return p and p:getSoul() or false end +function getPlayerSex(cid) local p = Player(cid) return p and p:getSex() or false end +function getPlayerStorageValue(cid, key) local p = Player(cid) return p and p:getStorageValue(key) or false end +function getPlayerBalance(cid) local p = Player(cid) return p and p:getBankBalance() or false end +function getPlayerMoney(cid) local p = Player(cid) return p and p:getMoney() or false end +function getPlayerGroupId(cid) local p = Player(cid) return p and p:getGroup():getId() or false end +function getPlayerLookDir(cid) local p = Player(cid) return p and p:getDirection() or false end +function getPlayerLight(cid) local p = Player(cid) return p and p:getLight() or false end +function getPlayerDepotItems(cid, depotId) local p = Player(cid) return p and p:getDepotItems(depotId) or false end +function getPlayerSkullType(cid) local p = Player(cid) return p and p:getSkull() or false end +function getPlayerLossPercent(cid) local p = Player(cid) return p and p:getDeathPenalty() or false end +function getPlayerMount(cid, mountId) local p = Player(cid) return p and p:hasMount(mountId) or false end +function getPlayerPremiumDays(cid) local p = Player(cid) return p and p:getPremiumDays() or false end +function getPlayerBlessing(cid, blessing) local p = Player(cid) return p and p:hasBlessing(blessing) or false end +function getPlayerFlagValue(cid, flag) local p = Player(cid) return p ~= nil and p:hasFlag(flag) or false end +function getPlayerParty(cid) + local player = Player(cid) + if player == nil then + return false + end + + local party = player:getParty() + if party == nil then + return nil + end + return party:getLeader():getId() +end +function getPlayerGuildId(cid) + local player = Player(cid) + if player == nil then + return false + end + + local guild = player:getGuild() + if guild == nil then + return false + end + return guild:getId() +end +function getPlayerGuildLevel(cid) local p = Player(cid) return p and p:getGuildLevel() or false end +function getPlayerGuildName(cid) + local player = Player(cid) + if player == nil then + return false + end + + local guild = player:getGuild() + if guild == nil then + return false + end + return guild:getName() +end +function getPlayerGuildRank(cid) + local player = Player(cid) + if player == nil then + return false + end + + local guild = player:getGuild() + if guild == nil then + return false + end + + local rank = guild:getRankByLevel(player:getGuildLevel()) + return rank and rank.name or false +end +function getPlayerGuildNick(cid) local p = Player(cid) return p and p:getGuildNick() or false end +function getPlayerMasterPos(cid) local p = Player(cid) return p and p:getTown():getTemplePosition() or false end +function getPlayerItemCount(cid, itemId, ...) local p = Player(cid) return p and p:getItemCount(itemId, ...) or false end +function getPlayerSlotItem(cid, slot) + local player = Player(cid) + if player == nil then + return pushThing(nil) + end + return pushThing(player:getSlotItem(slot)) +end +function getPlayerItemById(cid, deepSearch, itemId, ...) + local player = Player(cid) + if player == nil then + return pushThing(nil) + end + return pushThing(player:getItemById(itemId, deepSearch, ...)) +end +function getPlayerFood(cid) + local player = Player(cid) + if player == nil then + return false + end + local c = player:getCondition(CONDITION_REGENERATION, CONDITIONID_DEFAULT) return c and math.floor(c:getTicks() / 1000) or 0 +end +function canPlayerLearnInstantSpell(cid, name) local p = Player(cid) return p and p:canLearnSpell(name) or false end +function getPlayerLearnedInstantSpell(cid, name) local p = Player(cid) return p and p:hasLearnedSpell(name) or false end +function isPlayerGhost(cid) local p = Player(cid) return p and p:isInGhostMode() or false end +function isPlayerPzLocked(cid) local p = Player(cid) return p and p:isPzLocked() or false end +function isPremium(cid) local p = Player(cid) return p and p:isPremium() or false end +function getPlayersByIPAddress(ip, mask) + if mask == nil then mask = 0xFFFFFFFF end + local masked = bit.band(ip, mask) + local result = {} + for _, player in ipairs(Game.getPlayers()) do + if bit.band(player:getIp(), mask) == masked then + result[#result + 1] = player:getId() + end + end + return result +end +function getOnlinePlayers() + local result = {} + for _, player in ipairs(Game.getPlayers()) do + result[#result + 1] = player:getName() + end + return result +end +function getPlayersByAccountNumber(accountNumber) + local result = {} + for _, player in ipairs(Game.getPlayers()) do + if player:getAccountId() == accountNumber then + result[#result + 1] = player:getId() + end + end + return result +end +function getPlayerGUIDByName(name) + local player = Player(name) + if player then + return player:getGuid() + end + + local resultId = db.storeQuery("SELECT `id` FROM `players` WHERE `name` = " .. db.escapeString(name)) + if resultId ~= false then + local guid = result.getDataInt(resultId, "id") + result.free(resultId) + return guid + end + return 0 +end +function getAccountNumberByPlayerName(name) + local player = Player(name) + if player then + return player:getAccountId() + end + + local resultId = db.storeQuery("SELECT `account_id` FROM `players` WHERE `name` = " .. db.escapeString(name)) + if resultId ~= false then + local accountId = result.getDataInt(resultId, "account_id") + result.free(resultId) + return accountId + end + return 0 +end + +getPlayerAccountBalance = getPlayerBalance +getIpByName = getIPByPlayerName + +function setPlayerStorageValue(cid, key, value) local p = Player(cid) return p and p:setStorageValue(key, value) or false end +function doPlayerSetBalance(cid, balance) local p = Player(cid) return p and p:setBankBalance(balance) or false end +function doPlayerAddMoney(cid, money) local p = Player(cid) return p and p:addMoney(money) or false end +function doPlayerRemoveMoney(cid, money) local p = Player(cid) return p and p:removeMoney(money) or false end +function doPlayerAddSoul(cid, soul) local p = Player(cid) return p and p:addSoul(soul) or false end +function doPlayerSetVocation(cid, vocation) local p = Player(cid) return p and p:setVocation(Vocation(vocation)) or false end +function doPlayerSetTown(cid, town) local p = Player(cid) return p and p:setTown(Town(town)) or false end +function setPlayerGroupId(cid, groupId) local p = Player(cid) return p and p:setGroup(Group(groupId)) or false end +function doPlayerSetSex(cid, sex) local p = Player(cid) return p and p:setSex(sex) or false end +function doPlayerSetGuildLevel(cid, level) local p = Player(cid) return p and p:setGuildLevel(level) or false end +function doPlayerSetGuildNick(cid, nick) local p = Player(cid) return p and p:setGuildNick(nick) or false end +function doPlayerSetOfflineTrainingSkill(cid, skillId) local p = Player(cid) return p and p:setOfflineTrainingSkill(skillId) or false end +function doShowTextDialog(cid, itemId, text) local p = Player(cid) return p and p:showTextDialog(itemId, text) or false end +function doPlayerAddItemEx(cid, uid, ...) local p = Player(cid) return p and p:addItemEx(Item(uid), ...) or false end +function doPlayerRemoveItem(cid, itemid, count, ...) local p = Player(cid) return p and p:removeItem(itemid, count, ...) or false end +function doPlayerAddPremiumDays(cid, days) local p = Player(cid) return p and p:addPremiumDays(days) or false end +function doPlayerRemovePremiumDays(cid, days) local p = Player(cid) return p and p:removePremiumDays(days) or false end +function doPlayerAddBlessing(cid, blessing) local p = Player(cid) return p and p:addBlessing(blessing) or false end +function doPlayerAddOutfit(cid, lookType, addons) local p = Player(cid) return p and p:addOutfitAddon(lookType, addons) or false end +function doPlayerRemOutfit(cid, lookType, addons) + local player = Player(cid) + if player == nil then + return false + end + if addons == 255 then + return player:removeOutfit(lookType) + else + return player:removeOutfitAddon(lookType, addons) + end +end +function canPlayerWearOutfit(cid, lookType, addons) local p = Player(cid) return p and p:hasOutfit(lookType, addons) or false end +function doPlayerAddMount(cid, mountId) local p = Player(cid) return p and p:addMount(mountId) or false end +function doPlayerRemoveMount(cid, mountId) local p = Player(cid) return p and p:removeMount(mountId) or false end +function doPlayerSendCancel(cid, text) local p = Player(cid) return p and p:sendCancelMessage(text) or false end +function doPlayerFeed(cid, food) local p = Player(cid) return p and p:feed(food) or false end +function playerLearnInstantSpell(cid, name) local p = Player(cid) return p and p:learnSpell(name) or false end +function doPlayerPopupFYI(cid, message) local p = Player(cid) return p and p:popupFYI(message) or false end +function doSendTutorial(cid, tutorialId) local p = Player(cid) return p and p:sendTutorial(tutorialId) or false end +function doAddMapMark(cid, pos, type, description) local p = Player(cid) return p and p:addMapMark(pos, type, description or "") or false end +function doPlayerSendTextMessage(cid, type, text, ...) local p = Player(cid) return p and p:sendTextMessage(type, text, ...) or false end +function doSendAnimatedText() debugPrint("Deprecated function.") return true end +function doPlayerAddExp(cid, exp, useMult, ...) + local player = Player(cid) + if player == nil then + return false + end + + if useMult then + exp = exp * Game.getExperienceStage(player:getLevel()) + end + return player:addExperience(exp, ...) +end +function doPlayerAddManaSpent(cid, mana) local p = Player(cid) return p and p:addManaSpent(mana) or false end +function doPlayerAddSkillTry(cid, skillid, n) local p = Player(cid) return p and p:addSkillTries(skillid, n) or false end +function doPlayerAddMana(cid, mana, ...) local p = Player(cid) return p and p:addMana(mana, ...) or false end +function doPlayerJoinParty(cid, leaderId) + local player = Player(cid) + if player == nil then + return false + end + + if player:getParty() then + player:sendTextMessage(MESSAGE_INFO_DESCR, "You are already in a party.") + return true + end + + local leader = Player(leaderId) + if leader == nil then + return false + end + + local party = leader:getParty() + if party == nil or party:getLeader() ~= leader then + return true + end + + for _, invitee in ipairs(party:getInvitees()) do + if player ~= invitee then + return true + end + end + + party:addMember(player) + return true +end +function getPartyMembers(cid) + local player = Player(cid) + if player == nil then + return false + end + + local party = player:getParty() + if party == nil then + return false + end + + local result = {party:getLeader():getId()} + for _, member in ipairs(party:getMembers()) do + result[#result + 1] = member:getId() + end + return result +end + +doPlayerSendDefaultCancel = doPlayerSendCancel + +function getMonsterTargetList(cid) + local monster = Monster(cid) + if monster == nil then + return false + end + + local result = {} + for _, creature in ipairs(monster:getTargetList()) do + if monster:isTarget(creature) then + result[#result + 1] = creature:getId() + end + end + return result +end +function getMonsterFriendList(cid) + local monster = Monster(cid) + if monster == nil then + return false + end + + local z = monster:getPosition().z + + local result = {} + for _, creature in ipairs(monster:getFriendList()) do + if not creature:isRemoved() and creature:getPosition().z == z then + result[#result + 1] = creature:getId() + end + end + return result +end +function doSetMonsterTarget(cid, target) + local monster = Monster(cid) + if monster == nil then + return false + end + + if monster:getMaster() then + return true + end + + local target = Creature(cid) + if target == nil then + return false + end + + monster:selectTarget(target) + return true +end +function doMonsterChangeTarget(cid) + local monster = Monster(cid) + if monster == nil then + return false + end + + if monster:getMaster() then + return true + end + + monster:searchTarget(1) + return true +end +function doCreateNpc(name, pos, ...) + local npc = Game.createNpc(name, pos, ...) return npc and npc:setMasterPos(pos) or false +end +function doSummonCreature(name, pos, ...) + local m = Game.createMonster(name, pos, ...) return m and m:getId() or false +end +function doConvinceCreature(cid, target) + local creature = Creature(cid) + if creature == nil then + return false + end + + local targetCreature = Creature(target) + if targetCreature == nil then + return false + end + + creature:addSummon(targetCreature) + return true +end + +function getTownId(townName) local t = Town(townName) return t and t:getId() or false end +function getTownName(townId) local t = Town(townId) return t and t:getName() or false end +function getTownTemplePosition(townId) local t = Town(townId) return t and t:getTemplePosition() or false end + +function doSetItemActionId(uid, actionId) local i = Item(uid) return i and i:setActionId(actionId) or false end +function doTransformItem(uid, newItemId, ...) local i = Item(uid) return i and i:transform(newItemId, ...) or false end +function doChangeTypeItem(uid, newType) local i = Item(uid) return i and i:transform(i:getId(), newType) or false end +function doRemoveItem(uid, ...) local i = Item(uid) return i and i:remove(...) or false end + +function getContainerSize(uid) local c = Container(uid) return c and c:getSize() or false end +function getContainerCap(uid) local c = Container(uid) return c and c:getCapacity() or false end +function getContainerItem(uid, slot) + local container = Container(uid) + if container == nil then + return pushThing(nil) + end + return pushThing(container:getItem(slot)) +end + +function doAddContainerItemEx(uid, virtualId) + local container = Container(uid) + if container == nil then + return false + end + + local res = container:addItemEx(Item(virtualId)) + if res == nil then + return false + end + return res +end + +function doSendMagicEffect(pos, magicEffect, ...) return Position(pos):sendMagicEffect(magicEffect, ...) end +function doSendDistanceShoot(fromPos, toPos, distanceEffect, ...) return Position(fromPos):sendDistanceEffect(toPos, distanceEffect, ...) end +function isSightClear(fromPos, toPos, floorCheck) return Position(fromPos):isSightClear(toPos, floorCheck) end + +function getPromotedVocation(vocationId) + local vocation = Vocation(vocationId) + if vocation == nil then + return 0 + end + + local promotedVocation = vocation:getPromotion() + if promotedVocation == nil then + return 0 + end + return promotedVocation:getId() +end + +function getGuildId(guildName) + local resultId = db.storeQuery("SELECT `id` FROM `guilds` WHERE `name` = " .. db.escapeString(guildName)) + if resultId == false then + return false + end + + local guildId = result.getDataInt(resultId, "id") + result.free(resultId) + return guildId +end + +function getHouseName(houseId) local h = House(houseId) return h and h:getName() or false end +function getHouseOwner(houseId) local h = House(houseId) return h and h:getOwnerGuid() or false end +function getHouseEntry(houseId) local h = House(houseId) return h and h:getExitPosition() or false end +function getHouseTown(houseId) local h = House(houseId) if h == nil then return false end local t = h:getTown() return t and t:getId() or false end +function getHouseTilesSize(houseId) local h = House(houseId) return h and h:getTileCount() or false end + +function isItemStackable(itemId) return ItemType(itemId):isStackable() end +function isItemRune(itemId) return ItemType(itemId):isRune() end +function isItemDoor(itemId) return ItemType(itemId):isDoor() end +function isItemContainer(itemId) return ItemType(itemId):isContainer() end +function isItemFluidContainer(itemId) return ItemType(itemId):isFluidContainer() end +function isItemMovable(itemId) return ItemType(itemId):isMovable() end +function isCorpse(uid) local i = Item(uid) return i and ItemType(i:getId()):isCorpse() or false end + +isItemMoveable = isItemMovable +isMoveable = isMovable + +function getItemName(itemId) return ItemType(itemId):getName() end +function getItemWeight(itemId, ...) return ItemType(itemId):getWeight(...) / 100 end +function getItemDescriptions(itemId) + local itemType = ItemType(itemId) + return { + name = itemType:getName(), + plural = itemType:getPluralName(), + article = itemType:getArticle(), + description = itemType:getDescription() + } +end +function getItemIdByName(name) + local id = ItemType(name):getId() + if id == 0 then + return false + end + return id +end +function getItemWeightByUID(uid, ...) + local item = Item(uid) + if item == nil then + return false + end + + local itemType = ItemType(item:getId()) + return itemType:isStackable() and (itemType:getWeight(item:getCount(), ...) / 100) or (itemType:getWeight(1, ...) / 100) +end +function getItemRWInfo(uid) + local item = Item(uid) + if item == nil then + return false + end + + local rwFlags = 0 + local itemType = ItemType(item:getId()) + if itemType:isReadable() then + rwFlags = bit.bor(rwFlags, 1) + end + + if itemType:isWritable() then + rwFlags = bit.bor(rwFlags, 2) + end + return rwFlags +end +function getContainerCapById(itemId) return ItemType(itemId):getCapacity() end +function getFluidSourceType(itemId) local it = ItemType(itemId) return it.id ~= 0 and it:getFluidSource() or false end +function hasProperty(uid, prop) + local item = Item(uid) + if item == nil then + return false + end + + local parent = item:getParent() + if parent:isTile() and item == parent:getGround() then + return parent:hasProperty(prop) + else + return item:hasProperty(prop) + end +end + +function doSetItemText(uid, text) + local item = Item(uid) + if item == nil then + return false + end + + if text ~= "" then + item:setAttribute(ITEM_ATTRIBUTE_TEXT, text) + else + item:removeAttribute(ITEM_ATTRIBUTE_TEXT) + end + return true +end +function doSetItemSpecialDescription(uid, desc) + local item = Item(uid) + if item == nil then + return false + end + + if desc ~= "" then + item:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, desc) + else + item:removeAttribute(ITEM_ATTRIBUTE_DESCRIPTION) + end + return true +end +function doDecayItem(uid) local i = Item(uid) return i and i:decay() or false end + +function setHouseOwner(id, guid) local h = House(id) return h and h:setOwnerGuid(guid) or false end +function getHouseRent(id) local h = House(id) return h and h:getRent() or nil end +function getHouseAccessList(id, listId) local h = House(id) return h and h:getAccessList(listId) or nil end +function setHouseAccessList(id, listId, listText) local h = House(id) return h and h:setAccessList(listId, listText) or false end + +function getHouseByPlayerGUID(playerGUID) + for _, house in ipairs(Game.getHouses()) do + if house:getOwnerGuid() == playerGUID then + return house:getId() + end + end + return nil +end + +function getTileHouseInfo(pos) + local t = Tile(pos) + if t == nil then + return false + end + local h = t:getHouse() + return h and h:getId() or false +end + +function getTilePzInfo(position) + local t = Tile(position) + if t == nil then + return false + end + return t:hasFlag(TILESTATE_PROTECTIONZONE) +end + +function getTileInfo(position) + local t = Tile(position) + if t == nil then + return false + end + + local ret = pushThing(t:getGround()) + ret.protection = t:hasFlag(TILESTATE_PROTECTIONZONE) + ret.nopz = ret.protection + ret.nologout = t:hasFlag(TILESTATE_NOLOGOUT) + ret.refresh = t:hasFlag(TILESTATE_REFRESH) + ret.house = t:getHouse() ~= nil + ret.bed = t:hasFlag(TILESTATE_BED) + ret.depot = t:hasFlag(TILESTATE_DEPOT) + + ret.things = t:getThingCount() + ret.creatures = t:getCreatureCount() + ret.items = t:getItemCount() + ret.topItems = t:getTopItemCount() + ret.downItems = t:getDownItemCount() + return ret +end + +function getTileItemByType(position, itemType) + local t = Tile(position) + if t == nil then + return pushThing(nil) + end + return pushThing(t:getItemByType(itemType)) +end + +function getTileItemById(position, itemId, ...) + local t = Tile(position) + if t == nil then + return pushThing(nil) + end + return pushThing(t:getItemById(itemId, ...)) +end + +function getTileThingByPos(position) + local t = Tile(position) + if t == nil then + if position.stackpos == -1 then + return -1 + end + return pushThing(nil) + end + + if position.stackpos == -1 then + return t:getThingCount() + end + return pushThing(t:getThing(position.stackpos)) +end + +function getTileThingByTopOrder(position, topOrder) + local t = Tile(position) + if t == nil then + return pushThing(nil) + end + return pushThing(t:getItemByTopOrder(topOrder)) +end + +function getTopCreature(position) + local t = Tile(position) + if t == nil then + return pushThing(nil) + end + return pushThing(t:getTopCreature()) +end + +function queryTileAddThing(thing, position, ...) local t = Tile(position) return t and t:queryAdd(thing, ...) or false end + +function doTeleportThing(uid, dest, pushMovement) + if type(uid) == "userdata" then + if uid:isCreature() then + return uid:teleportTo(dest, pushMovement or false) + else + return uid:moveTo(dest) + end + else + if uid >= 0x10000000 then + local creature = Creature(uid) + if creature then + return creature:teleportTo(dest, pushMovement or false) + end + else + local item = Item(uid) + if item then + return item:moveTo(dest) + end + end + end + return false +end + +function getThingPos(uid) + local thing + if type(uid) ~= "userdata" then + if uid >= 0x10000000 then + thing = Creature(uid) + else + thing = Item(uid) + end + else + thing = uid + end + + if thing == nil then + return false + end + + local stackpos = 0 + local tile = thing:getTile() + if tile then + stackpos = tile:getThingIndex(thing) + end + + local position = thing:getPosition() + position.stackpos = stackpos + return position +end + +function getThingfromPos(pos) + local tile = Tile(pos) + if tile == nil then + return pushThing(nil) + end + + local thing + local stackpos = pos.stackpos or 0 + if stackpos == STACKPOS_TOP_MOVEABLE_ITEM_OR_CREATURE then + thing = tile:getTopCreature() + if thing == nil then + local item = tile:getTopDownItem() + if item and item:getType():isMovable() then + thing = item + end + end + elseif stackpos == STACKPOS_TOP_FIELD then + thing = tile:getFieldItem() + elseif stackpos == STACKPOS_TOP_CREATURE then + thing = tile:getTopCreature() + else + thing = tile:getThing(stackpos) + end + return pushThing(thing) +end + +function doRelocate(fromPos, toPos) + if fromPos == toPos then + return false + end + + local fromTile = Tile(fromPos) + if fromTile == nil then + return false + end + + if Tile(toPos) == nil then + return false + end + + for i = fromTile:getThingCount() - 1, 0, -1 do + local thing = fromTile:getThing(i) + if thing then + if thing:isItem() then + if ItemType(thing:getId()):isMovable() then + thing:moveTo(toPos) + end + elseif thing:isCreature() then + thing:teleportTo(toPos) + end + end + end + return true +end + +function getThing(uid) + return uid >= 0x10000000 and pushThing(Creature(uid)) or pushThing(Item(uid)) +end + +function getConfigInfo(info) + if type(info) ~= "string" then + return nil + end + dofile('config.lua') + return _G[info] +end + +function getWorldCreatures(type) + if type == 0 then + return Game.getPlayerCount() + elseif type == 1 then + return Game.getMonsterCount() + elseif type == 2 then + return Game.getNpcCount() + end + return Game.getPlayerCount() + Game.getMonsterCount() + Game.getNpcCount() +end + +saveData = saveServer + +function getGlobalStorageValue(key) + return Game.getStorageValue(key) or -1 +end + +function setGlobalStorageValue(key, value) + Game.setStorageValue(key, value) + return true +end + +getWorldType = Game.getWorldType + +numberToVariant = Variant +stringToVariant = Variant +positionToVariant = Variant + +function targetPositionToVariant(position) + local variant = Variant(position) + variant.type = VARIANT_TARGETPOSITION + return variant +end + +variantToNumber = Variant.getNumber +variantToString = Variant.getString +variantToPosition = Variant.getPosition + +function doCreateTeleport(itemId, destination, position) + local item = Game.createItem(itemId, 1, position) + if not item:isTeleport() then + item:remove() + return false + end + item:setDestination(destination) + return item:getUniqueId() +end + +function getSpectators(centerPos, rangex, rangey, multifloor, onlyPlayers) + local result = Game.getSpectators(centerPos, multifloor, onlyPlayers or false, rangex, rangex, rangey, rangey) + if #result == 0 then + return nil + end + + for index, spectator in ipairs(result) do + result[index] = spectator:getId() + end + return result +end + +function broadcastMessage(message, messageType) + Game.broadcastMessage(message, messageType) + print("> Broadcasted message: \"" .. message .. "\".") +end + +function Guild.addMember(self, player) + return player:setGuild(guild) +end +function Guild.removeMember(self, player) + return player:getGuild() == self and player:setGuild(nil) +end + +function getPlayerInstantSpellCount(cid) local p = Player(cid) return p and #p:getInstantSpells() end +function getPlayerInstantSpellInfo(cid, spellId) + local player = Player(cid) + if not player then + return false + end + + local spell = Spell(spellId) + if not spell or not player:canCast(spell) then + return false + end + + return spell +end + +function doSetItemOutfit(cid, item, time) local c = Creature(cid) return c and c:setItemOutfit(item, time) end +function doSetMonsterOutfit(cid, name, time) local c = Creature(cid) return c and c:setMonsterOutfit(name, time) end +function doSetCreatureOutfit(cid, outfit, time) + local creature = Creature(cid) + if not creature then + return false + end + + local condition = Condition(CONDITION_OUTFIT) + condition:setOutfit({ + lookTypeEx = itemType:getId() + }) + condition:setTicks(time) + creature:addCondition(condition) + + return true +end + +function doTileAddItemEx(pos, uid, flags) + local tile = Tile(pos) + if not tile then + return false + end + + local item = Item(uid) + if item then + return tile:addItemEx(item, flags) + end + + return false +end + +function isInArray(array, value) return table.contains(array, value) end + +function doCreateItem(itemid, count, pos) + local tile = Tile(pos) + if not tile then + return false + end + + local item = Game.createItem(itemid, count, pos) + if item then + return item:getUniqueId() + end + return false +end + +function doCreateItemEx(itemid, count) + local item = Game.createItem(itemid, count) + if item then + return item:getUniqueId() + end + return false +end + +function doMoveCreature(cid, direction) local c = Creature(cid) return c ~= nil and c:move(direction) end + +function createFunctions(class) + local exclude = {[2] = {"is"}, [3] = {"get", "set", "add", "can"}, [4] = {"need"}} + local temp = {} + for name, func in pairs(class) do + local add = true + for strLen, strTable in pairs(exclude) do + if table.contains(strTable, name:sub(1, strLen)) then + add = false + end + end + if add then + local str = name:sub(1, 1):upper() .. name:sub(2) + local getFunc = function(self) return func(self) end + local setFunc = function(self, ...) return func(self, ...) end + local get = "get" .. str + local set = "set" .. str + if not (rawget(class, get) and rawget(class, set)) then + table.insert(temp, {set, setFunc, get, getFunc}) + end + end + end + for _, func in ipairs(temp) do + rawset(class, func[1], func[2]) + rawset(class, func[3], func[4]) + end +end + +function doPlayerTakeItem(cid, itemid, count) + return Player(cid):removeItem(itemid, count) +end diff --git a/data/lib/core/achievements.lua b/data/lib/core/achievements.lua new file mode 100644 index 0000000..dc829b5 --- /dev/null +++ b/data/lib/core/achievements.lua @@ -0,0 +1,666 @@ +--[[ +Functions: + getAchievementInfoById(achievement_id) + getAchievementInfoByName(achievement_name) + getSecretAchievements() + getPublicAchievements() + getAchievements() + Player:addAchievement(achievement_id/name[, hideMsg]) + Player:removeAchievement(achievement_id/name) + Player:hasAchievement(achievement_id/name) + Player:addAllAchievements([hideMsg]) + Player:removeAllAchievements() + Player:getSecretAchievements() + Player:getPublicAchievements() + Player:getAchievements() + isAchievementSecret(achievement_id/name) + Player:getAchievementPoints() + Player:addAchievementProgress() +Storages: + PlayerStorageKeys.achievementsBase -- base storage + PlayerStorageKeys.achievementsCounter -- this storage will be used to save the process to obtain the certain achievement + (Ex: this storage + the id of achievement 'Allowance Collector' to save how many piggy banks has been broken +]] + +achievements = +{ + -- 8.6 + [1] = {name = "Allow Cookies?", grade = 1, points = 2, description = "With a perfectly harmless smile you fooled all of those wisecrackers into eating your exploding cookies. Consider a boy or girl scout outfit next time to make the trick even better."}, + [2] = {name = "Allowance Collector", grade = 1, points = 2, secret = true, description = "You certainly have your ways when it comes to acquiring money. Many of them are pink and paved with broken fragments of porcelain."}, + [3] = {name = "Amateur Actor", grade = 1, points = 2, description = "You helped bringing Princess Buttercup, Doctor Dumbness and Lucky the Wonder Dog to life - and will probably dream of them tonight, since you memorised your lines perfectly. What a .. special piece of.. screenplay."}, + [4] = {name = "Animal Activist", grade = 1, points = 2, description = "You have a soft spot for little, weak animals, and you do everything in your power to protect them - even if you probably eat dragons for breakfast."}, + [5] = {name = "Annihilator", grade = 2, points = 5, description = "You've daringly jumped into the infamous Annihilator and survived - taking home fame, glory and your reward."}, + [6] = {name = "Archpostman", grade = 1, points = 3, description = "Delivering letters and parcels has always been a secret passion of yours, and now you can officially put on your blue hat, blow your Post Horn and do what you like to do most. Beware of dogs!"}, + [7] = {name = "Backpack Tourist", grade = 1, points = 1, secret = true, description = "If someone lost a random thing in a random place, you're probably a good person to ask and go find it, even if you don't know what and where."}, + [8] = {name = "Beach Tamer", grade = 1, points = 2, description = "You re-enacted the Taming of the Shrew on a beach setting and proved that you can handle capricious girls quite well. With or without fish tails."}, + [9] = {name = "Bearhugger", grade = 1, points = 1, description = "Warm, furry and cuddly - though that same bear you just hugged would probably rip you into pieces if he had been conscious, he reminded you of that old teddy bear which always slept in your bed when you were still small."}, + [10] = {name = "Blessed!", grade = 1, points = 2, description = "You travelled the world for an almost meaningless prayer - but at least you don't have to do that again and can get a new blessed stake in the blink of an eye."}, + [11] = {name = "Bone Brother", grade = 1, points = 1, description = "You've joined the undead bone brothers - making death your enemy and your weapon as well. Devouring what's weak and leaving space for what's strong is your primary goal."}, + [12] = {name = "Castlemania", grade = 2, points = 5, secret = true, description = "You have an eye for suspicious places and love to read other people's diaries, especially those with vampire stories in it. You're also a dedicated token collector and explorer. Respect!"}, + [13] = {name = "Champion of Chazorai", grade = 2, points = 4, description = "You won the merciless 2 vs. 2 team tournament on the Isle of Strife and wiped out wave after wave of fearsome opponents. Death or victory - you certainly chose the latter."}, + [14] = {name = "Chorister", grade = 1, points = 1, description = "Lalalala... you now know the cult's hymn sung in Liberty Bay"}, + [15] = {name = "Clay Fighter", grade = 1, points = 3, secret = true, description = "You love getting your hands wet and dirty - and covered with clay. Your perfect sculpture of Brog, the raging Titan is your true masterpiece."}, + [16] = {name = "Clay to Fame", grade = 2, points = 6, secret = true, description = "Sculpting Brog, the raging Titan, is your secret passion. Numerous perfect little clay statues with your name on them can be found everywhere around the World."}, + [17] = {name = "Cold as Ice", grade = 2, points = 6, secret = true, description = "Take an ice cube and an obsidian knife and you'll very likely shape something really pretty from it. Mostly cute little mammoths, which are a hit with all the girls."}, + [18] = {name = "Culinary Master", grade = 2, points = 4, description = "Simple hams and bread merely make you laugh. You're the master of the extra-ordinaire, melter of cheese, fryer of bat wings and shaker of shakes. Delicious!"}, + [19] = {name = "Deep Sea Diver", grade = 2, points = 4, secret = true, description = "Under the sea - might not be your natural living space, but you're feeling quite comfortable on the ocean floor. Quara don't scare you anymore and sometimes you sleep with your helmet of the deep still equipped."}, + [20] = {name = "Dread Lord", grade = 3, points = 8, secret = true, description = "You don't care for rules that others set up and shape the world to your liking. Having left behind meaningless conventions and morals, you prize only the power you wield. You're a master of your fate and battle to cleanse the world."}, + [21] = {name = "Efreet Ally", grade = 1, points = 3, description = "Even though the welcomed you only reluctantly and viewed you as \"only a human\" for quite some time, you managed to impress Malor and gained his respect and trade options with the green djinns."}, + [22] = {name = "Elite Hunter", grade = 2, points = 5, description = "You jump at every opportunity for a hunting challenge that's offered to you and carry out those tasks with deadly precision. You're a hunter at heart and a valuable member of the Paw & Fur Society."}, + [23] = {name = "Explorer", grade = 2, points = 4, description = "You've been to places most people don't even know the names of. Collecting botanic, zoologic and ectoplasmic samples is your daily business and you're always prepared to discover new horizons."}, + [24] = {name = "Exquisite Taste", grade = 1, points = 2, secret = true, description = "You love fish - but preferably those caught in the cold north. Even though they're hard to come by you never get tired of picking holes in ice sheets and hanging your fishing rod in."}, + [25] = {name = "Firewalker", grade = 2, points = 4, secret = true, description = "Running barefoot across ember is not for you! You do it the elegant way. Yet, you're kind of drawn to fire and warm surroundings in general - you like it hot!"}, + [26] = {name = "Fireworks in the Sky", grade = 1, points = 2, secret = true, description = "You love the moment right before your rocket takes off and explodes into beautiful colours - not only on new year's eve!"}, + [27] = {name = "Follower of Azerus", grade = 2, points = 4, description = "When you do something, you do it right. You have an opinion and you stand by it - and no one will be able to convince you otherwise. On a sidenote, you're a bit on the brutal and war-oriented side, but that's not a bad thing, is it?"}, + [28] = {name = "Follower of Palimuth", grade = 2, points = 4, description = "You're a peacekeeper and listen to what the small people have to say. You've made up your mind and know who to help and for which reasons - and you do it consistently. Your war is fought with reason rather than weapons."}, + [29] = {name = "Fountain of Life", grade = 1, points = 1, secret = true, description = "You found and took a sip from the Fountain of Life. Thought it didn't grant you eternal life, you feel changed and somehow at peace."}, + [30] = {name = "Friend of the Apes", grade = 2, points = 4, description = "You know Banuta like the back of your hand and are good at destroying caskets and urns. The sight of giant footprints doesn't keep you from exploring unknown areas either."}, + [31] = {name = "Ghostwhisperer", grade = 1, points = 3, description = "You don't hunt them, you talk to them. You know that ghosts might keep secrets that have been long lost among the living, and you're skilled at talking them into revealing them to you."}, + [32] = {name = "Golem in the Gears", grade = 2, points = 4, description = "You're an aspiring mago-mechanic. Science and magic work well together in your eyes - and even though you probably delivered countless wrong charges while working for Telas, you might just have enough knowledge to build your own golem now."}, + [33] = {name = "Green Thumb", grade = 2, points = 4, secret = true, description = "If someone gives you seeds, you usually grow a beautiful plant from it within a few days. You like your house green and decorated with flowers. Probably you also talk to them."}, + [34] = {name = "Greenhorn", grade = 1, points = 2, description = "You wiped out Orcus the Cruel in the Arena of Svargrond. You're still a bit green behind the ears, but there's some great potential."}, + [35] = {name = "Herbicide", grade = 3, points = 8, secret = true, description = "You're one of the brave heroes to face and defeat the mysterious demon oak and all the critters it threw in your face. Wielding your blessed axe no tree dares stand in your way - demonic or not."}, + [36] = {name = "Here, Fishy Fishy!", grade = 1, points = 1, secret = true, description = "Ah, the smell of the sea! Standing at the shore and casting a line is one of your favourite activities. For you, fishingis relaxing - and at the same time, providing easy food. Perfect!"}, + [37] = {name = "High-Flyer", grade = 2, points = 4, secret = true, description = "The breeze in your hair, your fingers clutching the rim of your Carpet - that's how you like to travel. Faster! Higher! And a looping every now and then."}, + [38] = {name = "High Inquisitor", grade = 2, points = 5, description = "You're the one who poses the questions around here, and you know how to get the answers you want to hear. Besides, you're a famous exorcist and slay a few vampires and demons here and there. You and your stake are a perfect team."}, + [39] = {name = "His True Face", grade = 1, points = 3, secret = true, description = "You're one of the few citizens who Armenius chose to actually show his true face to - and he made you fight him. Either that means you're very lucky or very unlucky, but one thing's for sure - it's extremely rare."}, + [40] = {name = "Honorary Barbarian", grade = 1, points = 1, description = "You've hugged bears, pushed mammoths and proved your drinking skills. And even though you have a slight hangover, a partially fractured rib and some greasy hair on your tongue, you're quite proud to call yourself a honorary barbarian from now on."}, + [41] = {name = "Huntsman", grade = 1, points = 2, description = "You're familiar with hunting tasks and have carried out quite a few already. A bright career as hunter for the Paw & Fur society lies ahead!"}, + [42] = {name = "Ice Sculptor", grade = 1, points = 3, secret = true, description = "You love to hang out in cold surroundings and consider ice the best material to be shaped. What a waste to use ice cubes for drinks when you can create a beautiful mammoth statue from it!"}, + [43] = {name = "Interior Decorator", grade = 2, points = 4, secret = true, description = "Your home is your castle - and the furniture in it is just as important. Your friends ask for your advice when decorating their Houses and your probably own every statue, rack and bed there is."}, + [44] = {name = "Jamjam", grade = 2, points = 5, secret = true, description = "When it comes to interracial understanding, you're an expert. You've mastered the language of the Chakoya and made someone really happy with your generosity. Achuq!"}, + [45] = {name = "Jinx", grade = 1, points = 2, secret = true, description = "Sometimes you feel there's a gremlin in there. So many lottery tickets, so many blanks? That's just not fair! Share your misery with the world."}, + [46] = {name = "Just in Time", grade = 1, points = 1, description = "You're a fast runner and are good at delivering wares which are bound to decay just in the nick of time, even if you can't use any means of transportation or if your hands get cold or smelly in the process."}, + [47] = {name = "King Fan", grade = 1, points = 3, description = "You're not sure what it is, but you feel drawn to royalty. Your knees are always a bit grazed from crawling around in front of thrones and you love hanging out in castles. Maybe you should consider applying as a guard?"}, + [48] = {name = "Lord Protector", grade = 3, points = 8, secret = true, description = "You proved yourself - not only in your dreams - and possess a strong and spiritual mind. Your valorous fight against demons and the undead plague has granted you the highest and most respected rank among the Nightmare Knights."}, + [49] = {name = "Lord of the Elements", grade = 2, points = 5, description = "You travelled the surreal realm of the elemental spheres, summoned and slayed the Lord of the Elements, all in order to retrieve neutral matter. And as brave as you were, you couldn't have done it without your team!"}, + [50] = {name = "Lucid Dreamer", grade = 1, points = 2, description = "Dreams - are your reality? Strange visions, ticking clocks, going to bed and waking up somewhere completely else - that was some trip, but you're almost sure you actually did enjoy it."}, + [51] = {name = "Lucky Devil", grade = 2, points = 4, secret = true, description = "That's almost too much luck for one person. If something's really, really rare - it probably falls into your lap sooner or later. Congratulations!"}, + [52] = {name = "Marble Madness", grade = 2, points = 6, secret = true, description = "Your little statues of Godsula have become quite famous around the World and there's few people with similar skills when it comes to shaping marble."}, + [53] = {name = "Marblelous", grade = 1, points = 3, secret = true, description = "You're an aspiring marble sculptor with promising skills - proven by the perfect little Godsula statue you shaped. One day you'll be really famous!"}, + [54] = {name = "Marid Ally", grade = 1, points = 3, description = "You've proven to be a valuable ally to the Marid, and Gabel welcomed you to trade with Haroun and Nah'Bob whenever you want to. Though the Djinn war has still not ended, the Marid can't fail with you on their side."}, + [55] = {name = "Masquerader", grade = 1, points = 3, secret = true, description = "You probably don't know anymore how you really look like - usually when you look into a mirror, some kind of monster stares back at you. On the other hand - maybe that's an improvement?"}, + [56] = {name = "Master Thief", grade = 2, points = 4, description = "Robbing, inviting yourself to VIP parties, faking contracts and pretending to be someone else - you're a jack of all trades when it comes to illegal activities. You take no prisoners, except for the occasional goldfish now and then."}, + [57] = {name = "Master of the Nexus", grade = 2, points = 6, description = "You were able to fight your way through the countless hordes in the Demon Forge. Once more you proved that nothing is impossible."}, + [58] = {name = "Matchmaker", grade = 1, points = 1, description = "You don't believe in romance to be a coincidence or in love at first sight. In fact - love potions, bouquets of flowers and cheesy poems do the trick much better than ever could. Keep those hormones flowing!"}, + [59] = {name = "Mathemagician", grade = 1, points = 1, description = "Sometimes the biggest secrets of life can have a simple solution."}, + [60] = {name = "Ministrel", grade = 1, points = 2, secret = true, description = "You can handle any music instrument you're given - and actually manage to produce a pleasant sound with it. You're a welcome guest and entertainer in most taverns."}, + [61] = {name = "Nightmare Knight", grade = 1, points = 1, description = "You follow the path of dreams and that of responsibility without self-centered power. Free from greed and selfishness, you help others without expecting a reward."}, + [62] = {name = "Party Animal", grade = 1, points = 1, secret = true, description = "Oh my god, it's a paaaaaaaaaaaarty! You're always in for fun, friends and booze and love being the center of attention. There's endless reasons to celebrate! Woohoo!"}, + [63] = {name = "Passionate Kisser", grade = 1, points = 3, description = "For you, a kiss is more than a simple touch of lips. You kiss maidens and deadbeats alike with unmatched affection and faced death and rebirth through the kiss of the banshee queen. Lucky are those who get to share such an intimate moment with you!"}, + [64] = {name = "Perfect Fool", grade = 1, points = 3, description = "You love playing jokes on others and tricking them into looking a little silly. Wagging tongues say that the moment of realisation in your victims' eyes is the reward you feed on, but you're probably just kidding and having fun with them... right??"}, + [65] = {name = "Poet Laureate", grade = 1, points = 2, secret = true, description = "Poems, verses, songs and rhymes you've recited many times. You have passed the cryptic door, raconteur of ancient lore. Even elves you've left impressed, so it seems you're truly blessed."}, + [66] = {name = "Polisher", grade = 2, points = 4, secret = true, description = "If you see a rusty item, you can't resist polishing it. There's always a little flask of rust remover in your inventory - who knows, there might be a golden armor beneath all that dirt!"}, + [67] = {name = "Potion Addict", grade = 2, points = 4, secret = true, description = "Your local magic trader considers you one of his best customers - you usually buy large stocks of potions so you won't wake up in the middle of the night craving for more. Yet, you always seem to run out of them too fast. Cheers!"}, + [68] = {name = "Quick as a Turtle", grade = 1, points = 2, secret = true, description = "There... is... simply... no... better... way - than to travel on the back of a turtle. At least you get to enjoy the beautiful surroundings of Laguna."}, + [69] = {name = "Razing!", grade = 3, points = 7, secret = true, description = "People with sharp canine teeth better beware of you, especially at nighttime, or they might find a stake between their ribs. You're a merciless vampire hunter and have gathered numerous tokens as proof."}, + [70] = {name = "Recognised Trader", grade = 1, points = 3, description = "You're a talented merchant who's able to handle wares with care, finds good offers and digs up rares every now and then. Never late to complete an order, you're a reliable trader - at least in Rashid's eyes."}, + [71] = {name = "Rockstar", grade = 1, points = 3, secret = true, description = "Music just comes to you naturally. You feel comfortable on any stage, at any time, and secretly hope that someday you will be able to defeat your foes by playing music only. Rock on!"}, + [72] = {name = "Ruthless", grade = 2, points = 5, description = "You've touched all thrones of The Ruthless Seven and absorbed some of their evil spirit. It may have changed you forever."}, + [73] = {name = "Scrapper", grade = 1, points = 3, description = "You put out the Spirit of Fire's flames in the arena of Svargrond. Arena fights are for you - fair, square, with simple rules and one-on-one battles."}, + [74] = {name = "Sea Scout", grade = 1, points = 2, description = "Not even the hostile underwater environment stops you from doing your duty for the Explorer Society. Scouting the Quara realm is a piece of cake for you."}, + [75] = {name = "Secret Agent", grade = 1, points = 1, description = "Pack your spy gear and get ready for some dangerous missions in service of a secret agency. You've shown you want to - but can you really do it? Time will tell."}, + [76] = {name = "Shell Seeker", grade = 1, points = 3, secret = true, description = "You found a hundred beautiful pearls in large sea shells. By now that necklace should be finished - and hopefully you didn't get your fingers squeezed too often during the process."}, + [77] = {name = "Ship's Kobold", grade = 2, points = 4, secret = true, description = "You've probably never gotten seasick in your life - you love spending your free time on the ocean and covered quite a lot of miles with ships. Aren't you glad you didn't have to swim all that?"}, + [78] = {name = "Steampunked", grade = 1, points = 2, secret = true, description = "Travelling with the dwarven steamboats through the underground rivers is your preferred way of crossing the lands. No pesky seagulls, and good beer on board!"}, + [79] = {name = "Superstitious", grade = 1, points = 2, secret = true, description = "Fortune tellers and horoscopes guide you through your life. And you probably wouldn't dare going on a big game hunt without your trusty voodoo skull giving you his approval for the day."}, + [80] = {name = "Talented Dancer", grade = 1, points = 1, description = "You're a lord or lady of the dance - and not afraid to use your skills to impress tribal gods. One step to the left, one jump to the right, twist and shout!"}, + [81] = {name = "Territorial", grade = 1, points = 1, secret = true, description = "Your map is your friend - always in your back pocket and covered with countless marks of interesting and useful locations. One could say that you might be lost without it - but luckily there's no way to take it from you."}, + [82] = {name = "The Milkman", grade = 1, points = 2, description = "Who's the milkman? You are!"}, + [83] = {name = "Top AVIN Agent", grade = 2, points = 4, description = "You've proven yourself as a worthy member of the 'family' and successfully carried out numerous spy missions for your 'uncle' to support the Venorean traders and their goals."}, + [84] = {name = "Top CGB Agent", grade = 2, points = 4, description = "Girl power! Whether you're female or not, you've proven absolute loyalty and the willingness to put your life at stake for the girls brigade of Carlin."}, + [85] = {name = "Top TBI Agent", grade = 2, points = 4, description = "Conspiracies and open secrets are your daily bread. You've shown loyalty to the Thaian crown through your courage when facing enemies and completing spy missions. You're an excellent field agent of the TBI."}, + [86] = {name = "Turncoat", grade = 2, points = 4, secret = true, description = "You served Yalahar - but you didn't seem so sure whom to believe on the way. Both Azerus and Palimuth had good reasons for their actions, and thus you followed your gut instinct in the end, even if you helped either of them. May Yalahar prosper!"}, + [87] = {name = "Vanity", grade = 1, points = 3, secret = true, description = "Aren't you just perfectly, wonderfully, beautifully gorgeous? You can't pass a mirror without admiring your looks. Or maybe doing a quick check whether something's stuck in your teeth, perhaps?"}, + [88] = {name = "Vive la Resistance", grade = 1, points = 2, description = "You've always been a rebel - admit it! Supplying prisoners, caring for outcasts, stealing from the rich and giving to the poor - no wait, that was another story."}, + [89] = {name = "Warlord of Svargrond", grade = 2, points = 5, description = "You sent the Obliverator into oblivion in the arena of Svargrond and defeated nine other dangerous enemies on the way. All hail the Warlord of Svargrond!"}, + [90] = {name = "Waverider", grade = 1, points = 2, secret = true, description = "One thing's for sure: You definitely love swimming. Hanging out on the beach with your friends, having ice cream and playing beach ball is splashingly good fun!"}, + [91] = {name = "Wayfarer", grade = 1, points = 3, secret = true, description = "Dragon dreams are golden."}, + [92] = {name = "Worm Whacker", grade = 1, points = 1, secret = true, description = "Weehee! Whack those worms! You sure know how to handle a big hammer."}, + + -- 8.61 + [93] = {name = "Cocoon of Doom", grade = 1, points = 3, secret = true, description = "You helped bringing Devovorga's dangerous tentacles and her humongous cocoon down - not stopping her transformation, but ultimately completing a crucial step to her death."}, + [94] = {name = "Daring Trespasser", grade = 1, points = 3, secret = true, description = "You've entered the lair of Devovorga and joined the crew trying to take her down - whether crowned with success or not doesn't matter, but they can't blame you for not trying!"}, + [95] = {name = "Devovorga's Nemesis", grade = 2, points = 5, secret = true, description = "One special hero among many. This year - it was you. Devovorga withdrew in a darker realm because she could not withstand your power - and that of your comrades. Time will tell if the choice you made was good - but for now, it saved your world."}, + [96] = {name = "I Did My Part", grade = 1, points = 2, secret = true, description = "Your world is lucky to have you! You don't hesitate to jump in and help when brave heroes are called to save the world."}, + [97] = {name = "Notorious Worldsaver", grade = 3, points = 8, secret = true, description = "You're in the front line when it comes to saving your world or taking part in social events. Whether you do it noticed or unnoticed by the people, your world can rely on you to dutifully do your part to make it a better place for everyone."}, + [98] = {name = "Slayer of Anmothra", grade = 1, points = 2, secret = true, description = "Souls are like butterflies. The black soul of a living weapon yearning to strike lies shattered beneath your feet."}, + [99] = {name = "Slayer of Chikhaton", grade = 1, points = 2, secret = true, description = "Power lies in the will of her who commands it. You fought it with full force - and were stronger."}, + [100] = {name = "Slayer of Irahsae", grade = 1, points = 2, secret = true, description = "Few things equal the wild fury of a trapped and riven creature. You were a worthy opponent."}, + [101] = {name = "Slayer of Phrodomo", grade = 1, points = 2, secret = true, description = "Blind hatred took physical form, violently rebelling against the injustice it was born into. You were not able to bring justice - but at least temporary peace."}, + [102] = {name = "Slayer of Teneshpar", grade = 1, points = 2, secret = true, description = "The forbidden knowledge of aeons was never meant to invade this world. You silenced its voice before it could be made heard."}, + [103] = {name = "Teamplayer", grade = 1, points = 2, secret = true, description = "You don't consider yourself too good to do the dirty work while someone else might win the laurels for killing Devovorga. They couldn't do it without you!"}, + + -- 8.62 + [104] = {name = "Alumni", grade = 2, points = 6, description = "You're considered a first-rate graduate of the Magic Academy in Edron due to your pioneering discoveries and successful studies in the field of experimental magic and spell development. Ever considered teaching the Armageddon spell?"}, + [105] = {name = "Aristocrat", grade = 2, points = 4, description = "You begin your day by bathing in your pot of gold and you don't mind showing off your wealth while strolling the streets in your best clothes - after all it's your hard-earned money! You prefer to be addressed with 'Your Highness'."}, + [106] = {name = "Bad Timing", grade = 1, points = 2, secret = true, description = "Argh! Not now! How is it that those multifunctional tools never fail when you're using them for something completely trivial like squeezing juice, but mess up when you desperately need to climb up a rope spot with a fire-breathing dragon chasing you?"}, + [107] = {name = "Berserker", grade = 1, points = 3, description = "RAWR! Strength running through your body, your heart racing faster and adrenaline fueling your every weapon swing. All in a little bottle. No refund for destroyed furniture. For further questions consult your healer or potion dealer."}, + [108] = {name = "Bluebarian", grade = 1, points = 2, secret = true, description = "You live the life of hunters and gatherers. Well, especially that of a gatherer, and especially of one who gathers lots of blueberries. Have you checked the colour of your tongue lately?"}, + [109] = {name = "Brutal Politeness", grade = 2, points = 6, description = "What is best in life? To crush your enemies. To see them driven before you. And to maybe have a nice cup of tea afterwards."}, + [110] = {name = "Commitment Phobic", grade = 1, points = 2, secret = true, description = "Longterm relationships are just not for you. And each time you think you're in love, you're proven wrong shortly afterwards. Or maybe you just end up with the wrong lover each time - exploited and betrayed. Staying single might just be better."}, + [111] = {name = "Cookie Monster", grade = 1, points = 1, secret = true, description = "You can easily be found by anyone if they just follow the cookie crumb trail. And for you, true love means to give away your last cookie."}, + [112] = {name = "Cursed!", grade = 1, points = 3, secret = true, description = "The wrath of the Noxious Spawn - you accidentally managed to incur it. Your days are counted and your death inevitable. Sometime. Someplace."}, + [113] = {name = "Demonbane", grade = 2, points = 6, description = "You don't carry that stake just for decoration - you're prepared to use it. Usually you're seen hightailing through the deepest dungeons leaving a trail of slain demons. Whoever dares stand in your way should prepare to die."}, + [114] = {name = "Demonic Barkeeper", grade = 1, points = 3, description = "Thick, red - shaken, not stirred - and with a straw in it: that's the way you prefer your demon blood. Served with an onion ring, the subtle metallic aftertaste is almost not noticeable. Beneficial effects on health or mana are welcome."}, + [115] = {name = "The Snowman", grade = 1, points = 1, description = "You love the winter. Fully equipped with scarf and gloves, you like to have fun outside while building lots of snowmen with your friends. Snowball fight, anyone?"}, + [116] = {name = "Do Not Disturb", grade = 1, points = 1, secret = true, description = "Urgh! Close the windows! Shut out the sun rearing its ugly yellow head, shut out the earsplitting laughter of your neighbour's corpulent children. Ahhh. Embrace sweet darkness and silence."}, + [117] = {name = "Exemplary Citizen", grade = 2, points = 4, description = "Every city should be proud to call someone like you its inhabitant. You're keeping the streets clean and help settling the usual disputes in front of the depot. Also, you probably own a cat and like hiking."}, + [118] = {name = "Fool at Heart", grade = 1, points = 3, description = "And remember: Never try to teach a pig to sing. It wastes your time and annoys the pig."}, + [119] = {name = "Free Items!", grade = 1, points = 3, secret = true, description = "Yay! Finders keepers, losers weepers! Who cares where all that stuff came from and if you had to crawl through garbage piles to get it? It's FREE!"}, + [120] = {name = "Godslayer", grade = 2, points = 4, description = "You have defeated the Snake God's incarnations and, with a final powerful swing of the snake sceptre, cut off his life force supply. The story of power, deceit and corruption has come to an end - or... not?"}, + [121] = {name = "Gold Digger", grade = 2, points = 4, secret = true, description = "Hidden treasures below the sand dunes of the desert - you have a nose for finding them and you know where to dig. They might not make you filthy rich, but they're shiny and pretty anyhow."}, + [122] = {name = "Happy Farmer", grade = 1, points = 1, secret = true, description = "Scythe swung over your shoulder, sun burning down on your back - you are a farmer at heart and love working in the fields. Or then again maybe you just create fancy crop circles to scare your fellow men."}, + [123] = {name = "Heartbreaker", grade = 1, points = 1, secret = true, description = "Trust? Love? Faithfulness? Pah! Antiquated sentiments. As long as you have fun, you do not mind stepping on lots of hearts. Preferably while wearing combat boots."}, + [124] = {name = "Homebrewed", grade = 1, points = 1, secret = true, description = "Yo-ho-ho and a bottle of rum - homebrewed, of course, made from handpicked and personally harvested sugar cane plants. Now, let it age in an oak barrel and enjoy it in about 10 years. Or for the impatient ones: Let's have a paaaarty right now!"}, + [125] = {name = "Hunting with Style", grade = 2, points = 6, description = "At daytime you can be found camouflaged in the woods laying traps or chasing big game, at night you're sitting by the campfire and sharing your hunting stories. You eat what you hunted and wear what you skinned. Life could go on like that forever."}, + [126] = {name = "I Need a Hug", grade = 1, points = 2, description = "You and your stuffed furry friends are inseparable, and you're not ashamed to take them to bed with you - who knows when you will wake up in the middle of the night in dire need of a cuddle?"}, + [127] = {name = "In Shining Armor", grade = 2, points = 6, description = "With edged blade and fully equipped in a sturdy full plate armor, you charge at your enemies with both strength and valour. There's always a maiden to save and a dragon to slay for you."}, + [128] = {name = "Joke's on You", grade = 1, points = 1, secret = true, description = "Well - the contents of that present weren't quite what you expected. With friends like these, who needs enemies?"}, + [129] = {name = "Keeper of the Flame", grade = 1, points = 2, secret = true, description = "One of the Lightbearers. One of those who helped to keep the basins burning and worked together against the darkness. The demonic whispers behind the thin veil between the worlds - they were silenced again thanks to your help."}, + [130] = {name = "Let the Sunshine In", grade = 1, points = 1, secret = true, description = "Rise and shine! It's a beautiful new day - open your windows, feel the warm sunlight, watch the birds singing on your windowsill and care for your plants. What reason is there not to be happy?"}, + [131] = {name = "Life on the Streets", grade = 2, points = 4, description = "You're a beggar, homeless, wearing filthy and ragged clothes. But that doesn't mean you have to beg anyone for stuff - and you still kept your pride. Fine feathers do not necessarily make fine birds - what's under them is more important."}, + [132] = {name = "Make a Wish", grade = 1, points = 1, secret = true, description = "But close your eyes and don't tell anyone what you wished for, or it won't come true!"}, + [133] = {name = "Master of War", grade = 2, points = 6, description = "You're not afraid to show your colours in the heat of battle. Enemies fear your lethal lance and impenetrable armor. The list of the wars you've won is impressive. Hail and kill!"}, + [134] = {name = "Mastermind", grade = 1, points = 3, description = "You feel you could solve the hardest riddles within a minute or so. Plus, there's a nice boost on your spell damage. All in a little bottle. Aftereffects - feeling slightly stupid. For further questions consult your healer or potion dealer."}, + [135] = {name = "Mister Sandman", grade = 1, points = 2, secret = true, description = "Tired... so tired... curling up in a warm and cosy bed seems like the perfect thing to do right now. Sweet dreams!"}, + [136] = {name = "Modest Guest", grade = 1, points = 1, secret = true, description = "You don't need much to sleep comfortably. A pile of straw and a roof over your head - with the latter being completely optional - is quite enough to relax. You don't even mind the rats nibbling on your toes."}, + [137] = {name = "Mutated Presents", grade = 1, points = 1, secret = true, description = "Muahahaha it's a... mutated pumpkin! After helping to take it down - you DID help, didn't you? - you claimed your reward and got a more or less weird present. Happy Halloween!"}, + [138] = {name = "Natural Sweetener", grade = 1, points = 1, secret = true, description = "Liberty Bay is the perfect hangout for you and harvesting sugar cane quite a relaxing leisure activity. Would you like some tea with your sugar, hon?"}, + [139] = {name = "Nightmare Walker", grade = 2, points = 6, description = "You do not fear nightmares, you travel in them - facing countless horrors and fighting the fate they're about to bring. Few believe the dark prophecies you bring back from those dreams, but those who do fight alongside you as Nightmare Knights."}, + [140] = {name = "Nothing Can Stop Me", grade = 1, points = 1, secret = true, description = "You laugh at unprepared adventurers stuck in high grass or rush wood. Or maybe you actually do help them out. They call you... 'Machete'."}, + [141] = {name = "Number of the Beast", grade = 1, points = 2, description = "Six. Six. Six."}, + [142] = {name = "Of Wolves and Bears", grade = 2, points = 6, description = "One with nature, one with wildlife. Raw and animalistic power, sharpened senses, howling on the highest cliffs and roaring in the thickest forests - that's you."}, + [143] = {name = "One Thousand and One", grade = 2, points = 6, description = "You feel at home under the hot desert sun with sand between your toes, and your favourite means of travel is a flying carpet. Also, you can probably do that head isolation dance move."}, + [144] = {name = "Oops", grade = 1, points = 2, secret = true, description = "So much for your feathered little friend! Maybe standing in front of the birdcage, squeezing its neck and shouting 'Sing! Sing! Sing!' was a little too much for it?!"}, + [145] = {name = "Out in the Snowstorm", grade = 2, points = 4, description = "Snow heaps and hailstorms can't keep you from where you want to go. You're perfectly equipped for any expedition into the perpetual ice and know how to keep your feet warm. If you're a woman, that's quite an accomplishment, too."}, + [146] = {name = "Peazzekeeper", grade = 2, points = 6, description = "You're a humble warrior who doesn't need wealth or specialised equipment for travelling and fighting. You feel at home in the northern lands of Zao and did your part in fighting its corruption."}, + [147] = {name = "Piece of Cake", grade = 1, points = 1, description = "Life can be so easy with the right cake at the right time - and you mastered baking many different ones, so you should be prepared for almost everything life decides to throw at you."}, + [148] = {name = "Ritualist", grade = 2, points = 6, description = "You could be the author of the magnum opus 'How to Summon the Ultimate Beast from the Infernal Depths, Volume I'. Or, if your mind and heart are pure, you rather summon beings to help others. Or maybe just a little cat to have someone to cuddle."}, + [149] = {name = "Rock Me to Sleep", grade = 1, points = 1, secret = true, description = "Sleeping - you do it with style. You're chilling in your hammock, listening to the sound of the birds and crickets as you slowly drift away into the realm of dreams."}, + [150] = {name = "Rocket in Pocket", grade = 1, points = 1, secret = true, description = "Either you are not a fast learner or you find some pleasure in setting yourself on fire. Or you're just looking for a fancy title. In any case, you should know that passing gas during your little donkey experiments is not recommended."}, + [151] = {name = "Rollercoaster", grade = 1, points = 1, description = "Up and down and up and down... and then the big looping! Wait - they don't build loopings in Kazordoon. But ore wagon rides are still fun!"}, + [152] = {name = "Santa's Li'l Helper", grade = 1, points = 2, secret = true, description = "Christmas is your favourite time of the year, and boy, do you love presents. Buy some nice things for your friends, hide them away until - well, until you decide to actually unwrap them rather yourself."}, + [153] = {name = "Sharpshooter", grade = 1, points = 3, description = "Improved eyesight, arrows and bolts flying at the speed of light and pinning your enemies with extra damage. All in a little bottle. No consumption of carrots required. For further questions consult your healer or potion dealer."}, + [154] = {name = "Skull and Bones", grade = 2, points = 6, description = "Wearing the insignia and dark robes of the Brotherhood of Bones you roam the lands spreading fear and pain, creating new soldiers for the necromantic army which is about to rise soon. Hail the Brotherhood."}, + [155] = {name = "Slim Chance", grade = 1, points = 1, description = "Okay, let's face it - as long as you believe it could potentially lead you to the biggest treasure ever, you won't let go of that map, however fishy it might look. There must be a secret behind all of this!"}, + [156] = {name = "Swashbuckler", grade = 2, points = 6, description = "Ye be a gentleman o' fortune, fightin' and carousin' on the high seas, out fer booty and lassies! Ye no be answerin' to no man or blasted monarchy and yer life ain't fer the lily-livered. Aye, matey!"}, + [157] = {name = "Sweet Tooth", grade = 1, points = 2, secret = true, description = "The famous 'Ode to a Molten Chocolate Cake' was probably written by you. Spending a rainy afternoon in front of the chimney, wrapped in a blanket while indulging in cocoa delights sounds just like something you'd do. Enjoy!"}, + [158] = {name = "Swift Death", grade = 2, points = 6, description = "Stealth kills and backstabbing are you specialty. Your numerous victims are usually unaware of their imminent death, which you bring to them silently and swiftly. Everything is permitted."}, + [159] = {name = "The Cake's the Truth", grade = 1, points = 1, secret = true, description = "And anyone claiming otherwise is a liar."}, + [160] = {name = "The Day After", grade = 1, points = 2, secret = true, description = "Uhm... who's that person who you just woke up beside? Broken cocktail glasses on the floor, flowers all over the room, and why the heck are you wearing a ring? Yesterday must have been a long, weird day..."}, + [161] = {name = "The Undertaker", grade = 1, points = 2, secret = true, description = "You and your shovel - a match made in heaven. Or hell, for that matter. Somewhere down below in any case. You're magically attracted by stone piles and love to open them up and see where those holes lead you. Good biceps as well."}, + [162] = {name = "True Lightbearer", grade = 2, points = 5, secret = true, description = "You're one of the most dedicated Lightbearers - without you, the demons would have torn the veil between the worlds for sure. You've lit each and every basin, travelling high and low, pushing back the otherworldly forces. Let there be light!"}, + [163] = {name = "Warlock", grade = 2, points = 6, description = "You're proficient in the darker ways of magic and are usually found sitting inside a circle of candles and skulls muttering unspeakable words. Don't carry things too far or the demons might come get you."}, + [164] = {name = "Way of the Shaman", grade = 2, points = 6, description = "Shaking your rattle and dancing around the fire to jungle drums sounds like something you like doing. Besides, dreadlocks are a convenient way to wear your hair - no combing required!"}, + [165] = {name = "Wild Warrior", grade = 2, points = 6, description = "Valour is for weaklings - it doesn't matter how you win the battle, as long as you're victorious. Thick armor would just hinder your movements, thus you keep it light and rely on speed and skill instead of hiding in an uncomfortable shell."}, + [166] = {name = "With a Cherry on Top", grade = 1, points = 1, secret = true, description = "You like your cake soft, with fruity bits and a nice sugar icing. And you prefer to make them by yourself. Have you ever considered opening a bakery? You must be really good by now!"}, + [167] = {name = "Yalahari of Power", grade = 1, points = 3, description = "You defend Yalahar with brute force and are ready to lead it into a glorious battle, if necessary. Thanks to you, Yalahar will be powerful enough to stand up against any enemy."}, + [168] = {name = "Yalahari of Wisdom", grade = 1, points = 3, description = "Your deeds for Yalahar are usually characterised by deep insight and thoughtful actions. Thanks to you, Yalahar might have a chance to grow peacefully and with happy people living in it."}, + + -- 8.7 + [169] = {name = "Afraid of no Ghost!", grade = 1, points = 2, description = "You passed their test and helped the Spirithunters testing equipment, researching the supernatural and catching ghosts - it's you they're gonna call."}, + [170] = {name = "Ashes to Dust", grade = 2, points = 4, secret = true, description = "Staking vampires and demons has almost turned into your profession. You make sure to gather even the tiniest amount of evil dust particles. Beware of silicosis."}, + [171] = {name = "Baby Sitter", grade = 1, points = 1, secret = true, description = "You have cheered up a demon baby and returned it to its mother. A quick count of your fingers will reveal if you made it through unharmed."}, + [172] = {name = "Banebringers' Bane", grade = 1, points = 2, secret = true, description = "You sacrificed a lot of ingredients to create the protective brew of the witches and played a significant part in the efforts to repel the dreaded banebringers. The drawback is that even the banebringers may take notice of you ..."}, + [173] = {name = "Berry Picker", grade = 2, points = 4, secret = true, description = "The Combined Magical Winterberry Society hereby honours continued selfless dedication and extraordinary efforts in the Annual Autumn Vintage."}, + [174] = {name = "Bunny Slipped", grade = 1, points = 2, description = "Indeed, you have a soft spot for rabbits. Maybe the rabbits you saved today will be the rabbits that will save you tomorrow. When you are really hungry."}, + [175] = {name = "Cake Conqueror", grade = 1, points = 1, description = "You have bravely stepped onto the cake isle. Is there any more beautiful, tasty place to be in the whole world?"}, + [176] = {name = "Dark Voodoo Priest", grade = 1, points = 2, secret = true, description = "Sinister curses, evil magic - you don't shy away from punishing others by questionable means. Someone just gave you a strange look - now where's that needle again?"}, + [177] = {name = "Extreme Degustation", grade = 1, points = 2, secret = true, description = "Almost all the plants you tested for Chartan in Zao where inedible - you tasted them all, yet you're still standing! You should really get some fresh air now, though."}, + [178] = {name = "Fire Devil", grade = 1, points = 3, secret = true, description = "To keep the witches' fire burning, you trashed a lot of the wood the bane bringers animated. Some might find your fascination for fire ... disturbing."}, + [179] = {name = "Fire Lighter", grade = 1, points = 1, secret = true, description = "You have helped to keep the witches fire burning. Just watch your fingers, it's hot!"}, + [180] = {name = "Ghost Sailor", grade = 1, points = 1, secret = true, description = "You have sailed the nether seas with the Ghost Captain. Despite the perils, you and your fellow crewmen have braved the challenge."}, + [181] = {name = "Guinea Pig", grade = 1, points = 2, description = "True scientists know their equipment. Testing new inventions is essential daily work for any hard working researcher. You showed no fear and took all the new equipment from Spectulus and Sinclair for a spin."}, + [182] = {name = "Hidden Powers", grade = 1, points = 2, description = "You've discovered the Ancients' hidden powers - from now on, they will aid you in your adventures."}, + [183] = {name = "Honorary Witch", grade = 2, points = 4, secret = true, description = "Your efforts in fighting back the banebringers has not gone unnoticed. You are a legend amongst the witches and your name is whispered with awe and admiration."}, + [184] = {name = "I Like it Fancy", grade = 1, points = 1, secret = true, description = "You definitely know how to bring out the best in your furniture and decoration pieces. Beautiful."}, + [185] = {name = "Master Shapeshifter", grade = 1, points = 2, secret = true, description = "You have mastered Kuriks challenge in all possible shapes."}, + [186] = {name = "Merry Adventures", grade = 1, points = 2, description = "You went into the forest, met Rottin Wood and the Married Men and helped them out in their camp. Oh, and don't worry about those merchants. They won't dare mentioning the strangely large sums of gold they actually possessed which are missing now."}, + [187] = {name = "Nanny from Hell", grade = 1, points = 3, secret = true, description = "You have cheered up a bunch of demon babies and returned them to their mother. Don't bother the burn marks, don't bother the strains of grey hair, don't bother the nights you wake up screaming. It was worth it ... probably ... somehow."}, + [188] = {name = "Natural Born Cowboy", grade = 1, points = 1, secret = true, description = "Oh, the joy of riding! You've just got your very first own mount. Conveniently enough you don't even need stables, but can summon it any time you like."}, + [189] = {name = "Nether Pirate", grade = 1, points = 3, secret = true, description = "Not fearing death or ghosts you have traveled with the ghost captain several times and are a seasoned traveler of the netherworld. The dead and the living whisper about your exploits with appreciation."}, + [190] = {name = "Nomad Soul", grade = 1, points = 2, secret = true, description = "Home is where your current favourite hunting ground is, and though you might hold certain places more dear than others you never feel attached enough to really stay in one city for long. Pack all your stuff - it's time to move on again."}, + [191] = {name = "Petrologist", grade = 1, points = 2, secret = true, description = "Stones have always fascinated you. So has the chance of finding something really precious inside one of them. Statistically you should've discovered a few nice treasures by now. But then again, most statistics are overriden by Mother Disfortune."}, + [192] = {name = "Pyromaniac", grade = 2, points = 4, secret = true, description = "Love ... fire! So ... shiny! Must ... buuuurrrn!"}, + [193] = {name = "Safely Stored Away", grade = 1, points = 2, secret = true, description = "Don't worry, no one will be able to take it from you. Probably."}, + [194] = {name = "Scourge of Death", grade = 2, points = 5, secret = true, description = "You are a master of the nether sea and have traveled with the ghost captain so many times that you know his ship and the perils of the nether sea inside out. You laugh in the face of death and may return as a ghost pirate yourself in the afterlife!"}, + [195] = {name = "Silent Pet", grade = 1, points = 1, secret = true, description = "Awww. Your very own little goldfish friend - he's cute, he's shiny and he can't complain should you forget to feed him. He'll definitely brighten up your day!"}, + [196] = {name = "Skin-Deep", grade = 2, points = 4, secret = true, description = "You always carry your obsidian knife with you and won't hesitate to use it. You've skinned countless little - and bigger - critters and yeah: they usually don't get any more beautiful on the inside. It's rather blood and gore and all that..."}, + [197] = {name = "Snowbunny", grade = 1, points = 2, secret = true, description = "Hopping, hopping through the snow - that's the funnest way to go! Making footprints in a flurry - it's more fun the more you hurry! Licking icicles all day - Winter, never go away!"}, + [198] = {name = "Something's in There", grade = 1, points = 1, secret = true, description = "By the gods! What was that?"}, + [199] = {name = "Spectral Traveler", grade = 1, points = 2, secret = true, description = "You have sailed the nether seas with the Ghost Captain several times. The dangers of the nether have become familiar to you and unexperienced travelers turn to you for advice."}, + [200] = {name = "True Colours", grade = 1, points = 3, secret = true, description = "You and your friends showed the three wizards your loyalty three times - I am sure at least one of them is probably eternally thankful and exceedingly proud of you."}, + [201] = {name = "Truth Be Told", grade = 1, points = 2, secret = true, description = "You told Jack the truth by explaining you and Spectulus made a mistake when trying to convince him of being a completely different person."}, + [202] = {name = "Witches Lil' Helper", grade = 1, points = 1, secret = true, description = "You sacrificed ingredients to create the protective brew of the witches."}, + [203] = {name = "You Don't Know Jack", grade = 1, points = 2, secret = true, description = "You did not tell Jack the truth about the mistake you and Spectulus made when trying to convince him about being a completely different person. He will live in doubt until the end of his existence."}, + + -- 9.1 + [204] = {name = "Askarak Nemesis", grade = 1, points = 1, secret = true, description = "You are now the royal archfiend of the Askarak, prince slayer."}, + [205] = {name = "Beak Doctor", grade = 2, points = 4, description = "You significantly helped the afflicted citizens of Venore in times of dire need. Somehow you still feel close to the victims of the fever outbreak. Your clothes make you one of them, one poor soul amongst the countless afflicted."}, + [206] = {name = "Biodegradable", grade = 1, points = 1, secret = true, description = "You caught fifty rare shimmer swimmers. Getting rid of all those corpses by dumping them into the lake really was worth it, wasn't it? Wait, didn't something move in the water just now...?"}, + [207] = {name = "Deer Hunt", grade = 1, points = 1, secret = true, description = "You managed to kill more than four hundred white deer - it looks like you are one of the main reasons they will soon be considered extinct, way to go!"}, + [208] = {name = "Doctor! Doctor!", grade = 1, points = 2, secret = true, description = "Did someone call a doctor? You delivered 100 medicine bags to Ottokar of the Venore poor house in times of dire need, well done!"}, + [209] = {name = "Eye of the Deep", grade = 1, points = 1, secret = true, description = "You didn't look into it - at least not for too long... but Groam did. And you relieved him. Just don't tell his friend Dronk."}, + [210] = {name = "Firefighter", grade = 1, points = 2, secret = true, description = "You extinguished 500 thornfires! You were there when the Firestarters took over Shadowthorn. You saved the day - and the home of some elves which will try to kill you nonetheless. Isn't it nice to see everything restored just as it was before..?"}, + [211] = {name = "Invader of the Deep", grade = 1, points = 2, secret = true, description = "Many creatures of the deep have lost their lives by your hand. Three hundred have entered the depths of eternity. You should probably fear the revenge of the Eyes of the Deep."}, + [212] = {name = "Mageslayer", grade = 1, points = 1, secret = true, description = "You killed the raging mage in his tower south of Zao. Again. But this one just keeps coming back. The dimensional portal collapsed once more and you know he will eventually return but hey - a raging mage, it's like asking for it..."}, + [213] = {name = "Mystic Fabric Magic", grade = 2, points = 4, description = "You vanquished the mad mage, you subdued the raging mage - no spellweaving self-exposer can stand in your way. Yet you are quite absorbed in magical studies yourself. This very fabric reflects this personal approval of the magic arts."}, + [214] = {name = "Shaburak Nemesis", grade = 1, points = 1, secret = true, description = "You are now the public archenemy of the Shaburak, prince slayer."}, + [215] = {name = "Slimer", grade = 1, points = 1, secret = true, description = "With the assistance of your friendly little helper, you gobbled more than 500 chunks of slime. Well done, Slimer."}, + + -- 9.2 + [216] = {name = "Arachnoise", grade = 1, points = 1, description = "You've shattered each of Bloodweb's eight frozen legs. As they say: break a leg, and then some more."}, + [217] = {name = "Back into the Abyss", grade = 1, points = 1, description = "You've cut off a whole lot of tentacles today. Thul was driven back to where he belongs."}, + [218] = {name = "Beautiful Agony", grade = 1, points = 2, description = "Ethershreck's cry of agony kept ringing in your ear for hours after he had dissolved into thin air. He probably moved to another plane of existence... for a while."}, + [219] = {name = "Blood-Red Snapper", grade = 1, points = 1, description = "You've tainted the jungle floor with the Snapper's crimson blood."}, + [220] = {name = "Breaking the Ice", grade = 1, points = 1, description = "You almost made friends with Shardhead... before he died. Poor guy only seems to attract violence with his frosty attitude."}, + [221] = {name = "Choking on Her Venom", grade = 1, points = 1, description = "The Old Widow fell prey to your supreme hunting skills."}, + [222] = {name = "Crawling Death", grade = 1, points = 1, description = "You ripped the ancient scarab Fleshcrawler apart and made sure he didn't get under your skin."}, + [223] = {name = "Hissing Downfall", grade = 1, points = 2, description = "You've vansquished the Noxious Spawn and his serpentine heart."}, + [224] = {name = "Just Cracked Me Up!", grade = 1, points = 2, description = "Stonecracker's head was much softer than the stones he threw at you."}, + [225] = {name = "Meat Skewer", grade = 1, points = 1, description = "You've impaled the big mammoth Bloodtusk with his own tusks."}, + [226] = {name = "No More Hiding", grade = 1, points = 1, description = "You've found a well-hidden spider queen and caught her off guard in the middle of her meal."}, + [227] = {name = "One Less", grade = 1, points = 2, description = "The Many is no more, but how many more are there? One can never know."}, + [228] = {name = "Pwned a Lot of Fur", grade = 3, points = 8, secret = true, description = "You've faced and defeated a lot of the mighty bosses the Paw and Fur society sent you out to kill. All by yourself. What a hunt!"}, + [229] = {name = "Rootless Behaviour", grade = 1, points = 1, description = "You've descended into the swampy depths of Deathbine's lair and made quick work of it."}, + [230] = {name = "Scorched Flames", grade = 1, points = 1, description = "A mighty blaze went out today. It's Flameborn's turn to wait for his rebirth in the eternal cycle of life and death."}, + [231] = {name = "Something Smells", grade = 1, points = 1, description = "You've exinguished the Sulphur Scuttler's gas clouds and made the air in his cave a little better... at least for a while."}, + [232] = {name = "Spareribs for Dinner", grade = 1, points = 1, description = "Ribstride is striding no more. He had quite a few ribs to spare though."}, + [233] = {name = "The Drowned Sea God", grade = 1, points = 2, description = "As the killer of Leviathan, the giant sea serpent, his underwater kingdom is now under your reign."}, + [234] = {name = "The Gates of Hell", grade = 1, points = 2, description = "It seems the gates to the underworld have to remain unprotected for a while. Kerberos, the mighty hellhound, lost his head. All three of them."}, + [235] = {name = "The Serpent's Bride", grade = 1, points = 2, description = "You made a knot with Gorgo's living curls and took her scalp. You couldn't save her countless petrified victims, but at least you didn't become one."}, + [236] = {name = "Twisted Mutation", grade = 1, points = 1, description = "You've slain Esmeralda, the most hideous and aggressive of the mutated rats. No one will know that you almost lost a finger in the process."}, + + -- 9.4 + [237] = {name = "Bane of the Hive", grade = 1, points = 2, description = "Countless fights and never tiring effort in the war against the hive grant you the experience to finish your outfit with the last remaining part. Your chitin outfit is a testament of your skills and dedication for the cause."}, + [238] = {name = "Chest Robber", grade = 1, points = 1, description = "You've discovered three nomad camps and stole their supplies. Well, you can probably use them better then they can."}, + [239] = {name = "Chitin Bane", grade = 2, points = 4, description = "You have become competent and efficient in gathering the substance that is needed to fight the hive. You almost smell like dissolved chitin and the Hive Born would tell their children scary stories about you if they could speak."}, + [240] = {name = "Confusion", grade = 1, points = 3, description = "The destruction you have caused by now can be felt throughout the whole hive. The mayhem that follows your step caused significant confusion in the consciousness of the hive."}, + [241] = {name = "Dazzler", grade = 1, points = 3, description = "In the war against the hive, your efforts in blinding it begin to pay off. Your actions have blinded the hive severely and the entity seems to become aware that something dangerous is happening."}, + [242] = {name = "Death Song", grade = 1, points = 3, description = "You hushed the songs of war in the black depths by sliencing more than three hundred Deepling Spellsingers."}, + [243] = {name = "Depth Dwellers", grade = 1, points = 3, description = "By eliminating at least three hundred Deepling Warriors you delivered quite a blow to the amassing armies of the deep."}, + [244] = {name = "Desert Fisher", grade = 1, points = 1, description = "You managed to catch a fish in a surrounding that usually doesn't even carry water. Everything is subject to change, probably..."}, + [245] = {name = "Dog Sitter", grade = 1, points = 1, description = "You showed Noodles the way home. How long will it take this time until he's on the loose again? That dog must be really bored in the throne room by now."}, + [246] = {name = "Down the Drain", grade = 1, points = 2, description = "You've found a secret dungeon in the flooded plains and killed several of its inhabitants. And now you have wet feet."}, + [247] = {name = "Exterminator", grade = 2, points = 4, description = "Efficient and lethal, you have gained significant experience in fighting the elite forces of the hive. Almost single-handed, you have slain the best of the Hive Born and live to tell the tale."}, + [248] = {name = "Fire from the Earth", grade = 1, points = 2, description = "You've survived the Hellgorge eruption and found a way through the flames and lava. You've even managed to kill a few fireborn on the way."}, + [249] = {name = "Gatherer", grade = 1, points = 2, description = "By killing creatures of the hive and gaining weapons for further missions, you started a quite effective way of war. You gathered a lot of dissolved chitin to resupply the war effort."}, + [250] = {name = "Gem Cutter", grade = 1, points = 1, secret = true, description = "You cut your first gem - and it bears your own name! Now that would be a nice gift! This does not make it a \"true\" Heart of the Sea, however..."}, + [251] = {name = "Goldhunter", grade = 1, points = 2, secret = true, description = " If it wasn't for you, several banks in the World would've gotten bankrupt by now. Keep on chasing bank robbers and no one will have to worry about the World economy!"}, + [252] = {name = "Guard Killer", grade = 1, points = 2, description = "You have proven that you can beat the best of the hive. You have caused first promising breaches in the defence of the hive"}, + [253] = {name = "Guardian Downfall", grade = 2, points = 4, description = "You ended the life of over three hundred Deepling Guards. Not quite the guardian of the Deeplings, are you?"}, + [254] = {name = "Headache", grade = 1, points = 2, description = "Even in the deepest structures of the hive, you began to strike against the mighty foe. Your actions probably already gave the hive a headache."}, + [255] = {name = "Heartburn", grade = 1, points = 3, description = "Never-tiring, you attack the inner organs of the mighty hive. Your attacks on the hive's digestion system begin to cause some trouble."}, + [256] = {name = "Hickup", grade = 1, points = 2, description = "You have grown accustomed to frequenting the hive's stomach system. Your actions have caused the hive some first digestion problems."}, + [257] = {name = "Hive Blinder", grade = 2, points = 4, description = "You have put a lot of time and energy into keeping the hive unaware of what is happening on Quirefang. The hive learnt to fear your actions. It would surely crush you with all its might ... if it could only find you!"}, + [258] = {name = "Hive Fighter", grade = 1, points = 1, description = "You have participated that much in the hive war, that you are able to create some makeshift armor from the remains of dead hive born that can be found in the major hive, to show of your skill."}, + [259] = {name = "Hive Infiltrator", grade = 1, points = 3, description = "The most powerful warriors of the hive were killed by you by the dozens. The hive is not safe anymore because of your actions."}, + [260] = {name = "Hive War Veteran", grade = 1, points = 1, description = "Your invaluable experience in fighting the hive allows you to add another piece of armor to your chitin outfit to proove your dedication for the cause."}, + [261] = {name = "Honest Finder", grade = 1, points = 1, description = "You've stopped the bank robber and returned the bag full of gold. Good to know there are still lawful citizens like you around."}, + [262] = {name = "Ice Harvester", grade = 1, points = 1, description = "You witnessed the thawing of Svargrond and harvested rare seeds from some strange icy plants. They must be good for something."}, + [263] = {name = "Loyal Subject", grade = 1, points = 1, description = "You joined the Kingsday festivities and payed the King your respects. Now, off to party!"}, + [264] = {name = "Manic", grade = 2, points = 4, description = "You have destroyed a significant amount of the hive's vital nerve centres and caused massive destruction to the hive's awareness. You are probably causing the hive horrible nightmares."}, + [265] = {name = "Minor Disturbance", grade = 1, points = 2, description = "Your actions start to make a difference. You have blinded the antennae of the hive often enough to become an annoyance to it."}, + [266] = {name = "Navigational Error", grade = 2, points = 5, secret = true, description = "You confronted the Navigator."}, + [267] = {name = "Pimple", grade = 1, points = 3, description = "You are getting more and more experienced in destroying the supply of the enemy's forces. Your actions caused the hive some severe skin problems."}, + [268] = {name = "Planter", grade = 1, points = 2, description = "The hive has to be fought with might and main, hampering its soldiers is only the first step. You diligently stopped the pores of the hive to spread its warriors."}, + [269] = {name = "Preservationist", grade = 1, points = 1, secret = true, description = "You are a pretty smart thinker and managed to create everlasting flowers. They might become a big hit with all the people who aren't blessed with a green thumb or just forgetful."}, + [270] = {name = "Si, Ariki!", grade = 1, points = 1, description = "You've found the oriental traveller Yasir and were able to trade with him - even if you didn't really understand his language."}, + [271] = {name = "Someone's Bored", grade = 1, points = 1, secret = true, description = "That was NOT a giant spider. There's some witchcraft at work here."}, + [272] = {name = "Spolium Profundis", grade = 2, points = 4, description = "You travelled the depths of this very world. You entered the blackness of the deep sea to conquer the realm of the Deeplings. May this suit remind you of the strange beauty below."}, + [273] = {name = "Stomach Ulcer", grade = 2, points = 4, description = "You severely disrupted the digestion of the hive. The hive should for sure see a doctor. It seems you proved to be more than it can swallow."}, + [274] = {name = "Supplier", grade = 1, points = 3, description = "The need for supplies often decides over loss or victory. Your tireless efforts to resupply the resources keeps the war against the hive going."}, + [275] = {name = "Suppressor", grade = 2, points = 4, description = "A war is won by those who have the best supply of troops. The hive's troops have been dealt a significant blow by your actions. You interrupted the hive's replenishment of troops lastingly and severely."}, + [276] = {name = "Torn Treasures", grade = 1, points = 1, secret = true, description = "Wyda seems to be really, really bored. You also found out that she doesn't really need all those blood herbs that adventurers brought her. Still, she was nice enough to take one from you and gave you something quite cool in exchange."}, + [277] = {name = "Trail of the Ape God", grade = 1, points = 1, secret = true, description = "You've discovered a trail of giant footprints and Terrified Elephants running everywhere. Could it be that the mysterious Ape God is rambling in the jungle?"}, + [278] = {name = "Whistle-Blower", grade = 1, points = 1, secret = true, description = "You can't keep a secret, can you? Then again, you're just fulfilling your duty to the Queen of Carlin as a lawful citizen. That's a good thing, isn't it...?"}, + + -- 9.5 + [279] = {name = "Back from the Dead", grade = 1, points = 2, description = "You overcame the undead Zanakeph and sent him back into the darkness that spawned him."}, + [280] = {name = "Dream's Over", grade = 1, points = 1, description = "No more fear and bad dreams. You stabbed Tormentor to death with its scythe leg."}, + [281] = {name = "Enter zze Draken!", grade = 1, points = 2, description = "You gave zzze draken a tazte of your finizzzing move."}, + [282] = {name = "Howly Silence", grade = 1, points = 1, description = "You muted the everlasting howling of Hemming."}, + [283] = {name = "Kapow!", grade = 1, points = 1, description = "No joke, you murdered the bat."}, + [284] = {name = "King of the Ring", grade = 1, points = 2, description = "Bretzecutioner's body just got slammed away. You are a true king of the ring!"}, + [285] = {name = "Pwned All Fur", grade = 3, points = 8, secret = true, description = "You've faced and defeated each of the mighty bosses the Paw and Fur society sent you out to kill. All by yourself. What a hunt!"}, + [286] = {name = "Stepped on a Big Toe", grade = 1, points = 1, description = "This time you knocked out the big one."}, + [287] = {name = "Zzztill Zzztanding!", grade = 1, points = 1, description = "You wiped Fazzrah away - zzeemzz like now you're the captain."}, + + -- 9.6 + [288] = {name = "Becoming a Bigfoot", grade = 1, points = 1, description = "You did it! You convinced the reclusive gnomes to accept you as one of their Bigfoots. Now you are ready to help them. With big feet big missions seen to come."}, + [289] = {name = "Bibby's Bloodbath", grade = 1, points = 1, secret = true, description = "You lend a helping hand in defeating invading Orcs by destroying their warcamp along with their leader. Bibby's personal bloodbath..."}, + [290] = {name = "Call Me Sparky", grade = 1, points = 1, description = "Admittedly you enjoyed the killing as usual. But the part with the sparks still gives you shivers ... or is it that there is some charge left on you?"}, + [291] = {name = "Crystal Clear", grade = 1, points = 3, description = "If the gnomes had told you that crystal armor is see-through you had probably changed your underwear in time."}, + [292] = {name = "Crystal Keeper", grade = 1, points = 1, description = "So you repaired the light of some crystals for those gnomes. What's next? Sitting a week in a mushroom bed as a temporary mushroom?"}, + [293] = {name = "Crystals in Love", grade = 1, points = 1, description = "You brought two loving crystals together. Perhaps they might even name one of their children after you. To bad you forgot to leave your calling card."}, + [294] = {name = "Death from Below", grade = 1, points = 2, secret = true, description = "The face of the enemy is unmasked. You have encountered one of 'those below' and survived. More than that, you managed to kill the beast and prove once and for all that the enemy can be beaten."}, + [295] = {name = "Death on Strike", grade = 2, points = 4, secret = true, description = "Again and again Deathstrike has fallen to your prowess. Perhaps it's time for people calling YOU Deathstrike from now on."}, + [296] = {name = "Diplomatic Immunity", grade = 2, points = 4, secret = true, description = "You killed the ambassador of the abyss that often that they might consider sending another one. Perhaps that will one day stop further intrusions."}, + [297] = {name = "Dungeon Cleaner", grade = 1, points = 3, secret = true, description = "Seen it all. Done it all. Your unstoppable force swept through the dungeons and you vanquished their masters. Not to forget the precious loot you took! Now stop reading this and continue hunting! Time is money after all!"}, + [298] = {name = "Fall of the Fallen", grade = 2, points = 4, secret = true, description = "Have you ever wondered how he reappears again and again? You only care for the loot, do you? Gotcha!"}, + [299] = {name = "Final Strike", grade = 1, points = 2, secret = true, description = "The mighty Deathstrike is dead! One legend is dead and you're on your way to become one yourself."}, + [300] = {name = "Funghitastic", grade = 1, points = 3, description = "Finally your dream to become a walking mushroom has come true ... No, wait a minute!"}, + [301] = {name = "Gnome Friend", grade = 1, points = 2, description = "The gnomes are warming up to you. One or two of them might actually bother to remember your name. You're allowed to access their gnomebase alpha. You are prepared to boldly put your gib feet into areas few humans have walked before."}, + [302] = {name = "Gnome Little Helper", grade = 1, points = 1, description = "You think the gnomes start to like you. A little step for a Bigfoot but a big step for humanity."}, + [303] = {name = "Gnomebane's Bane", grade = 1, points = 2, secret = true, description = "The fallen gnome is dead and justice served. But what was it that the gnome whispered with his last breath? He's your father???"}, + [304] = {name = "Gnomelike", grade = 1, points = 3, description = "You have become a household name in gnomish society! Your name is mentioned by gnomes more than once. Of course usually by gnomish mothers whose children refuse to eat their mushroom soup, but you are certainly making some tremendous progress."}, + [305] = {name = "Gnomish Art Of War", grade = 1, points = 3, description = "You have unleashed your inner gnome and slain some of the most fearsome threats that gnomekind has ever faced. Now you can come and go to the warzones as it pleases you. The enemies of gnomekind will never be safe again."}, + [306] = {name = "Goo Goo Dancer", grade = 1, points = 1, secret = true, description = "Seeing a mucus plug makes your heart dance and you can't resist to see what it hides. Goo goo away!"}, + [307] = {name = "Grinding Again", grade = 1, points = 1, description = "Burnt fingers and itching lungs are a small price for bringing those gnomes some lousy stone and getting almost killed! Your mother warned you to better become a farmer."}, + [308] = {name = "Honorary Gnome", grade = 2, points = 4, description = "You accomplished what few humans ever will: you truly impressed the gnomes. This might not change their outlook on humanity as a whole, but at least you can bathe in gnomish respect! And don't forget you're now allowed to enter the warzones!"}, + [309] = {name = "Nestling", grade = 1, points = 1, description = "You cleansed the land from an eight legged nuisance by defeating Mamma Longlegs three times. She won't be back soon... or will she?"}, + [310] = {name = "One Foot Vs. Many", grade = 1, points = 1, description = "One Bigfoot won over thousands of tiny feet. Perhaps the gnomes are wrong and size matters?"}, + [311] = {name = "Spore Hunter", grade = 1, points = 1, description = "After hunting for the correct mushrooms and their spores you're starting to feel like a mushroom yourself. A few times more and you might start thinking like a mushroom, who knows?"}, + [312] = {name = "Substitute Tinker", grade = 1, points = 1, description = "Ring-a-ding! You have visited the golem workshop and lent a hand in repairing them. To know those golems are safe is worth all the bruises, isn't it?"}, + [313] = {name = "The Picky Pig", grade = 1, points = 1, description = "The gnomes decided their pigs need some exclusive diet and you had to do all the dirty work - but wasn't the piglet adorable?"}, + + -- 9.8 + [314] = {name = "Task Manager", grade = 1, points = 2, secret = true, description = "Helping a poor, stupid goblin to feed his starving children and wifes feels good ... if you'd only get rid of the strange feeling that you're missing something."}, + [315] = {name = "True Dedication", grade = 2, points = 5, secret = true, description = "You conquered the demon challenge and prevailed... now show off your success in style!"}, + + -- 10.1 + [316] = {name = "Gravedigger", grade = 1, points = 3, description = "Assisting Omrabas' sick plan to resurrect made you dig your way through the blood-soaked halls of Drefia. Maybe better he failed!"}, + [317] = {name = "Repenter", grade = 1, points = 1, secret = true, description = "You cleansed your soul in serving the Repenter enclave and purified thine self in completing all tasks in a single day of labour."}, + + -- 10.2 + [318] = {name = "Cave Completionist", grade = 1, points = 2, description = " You have helped the gnomes of the spike in securing the caves and explored enough of the lightles depths to earn you a complete cave explorers outfit. Well done!"}, + + -- 10.3 + [319] = {name = "Dream Warden", grade = 2, points = 5, description = "It doesn't matter what noise you would hear... dream, nightmare, illusion - there is nothing you can't vanquish. You are a true Dream Warden."}, + [320] = {name = "Dream Wright", grade = 1, points = 1, description = "You have mended many a broken dream and so, the dream of Roshamuul is safely being told over and over again."}, + [321] = {name = "Ending the Horror", grade = 1, points = 2, description = "You have cleansed the lands of many retching horrors. You sure know how to end a bad dream: forcefully, that's how!"}, + [322] = {name = "Luring Silence", grade = 1, points = 2, description = "What a scientific discovery - they really DO communicate! Using their own communication habits against them, you lured a large pack of silencers away from the walls of Roshamuul."}, + [323] = {name = "Never Surrender", grade = 1, points = 3, description = "You did not show any signs of surrender to any sight of... you get the picture. Even a hundred of them did not pose a threat to you."}, + [324] = {name = "Nevermending Story", grade = 1, points = 3, secret = true, description = "You collected all of the mysterious bottle messages around the island of Roshamuul and located the remains of the first mate. Time will tell if his tale of mending an evil ring holds true."}, + [325] = {name = "Noblesse Obliterated", grade = 2, points = 6, description = "After a battle like this you know who your friends are."}, + [326] = {name = "Prison Break", grade = 3, points = 8, description = "Gaz'haragoth... a day to remember! Your world accomplished someting really big - and you have been part of it!"}, + [327] = {name = "Sleepwalking", grade = 1, points = 1, description = "You know your way, in dream and waking. And how to make tea that transcends the boundaries of conscience."}, + [328] = {name = "Umbral Archer", grade = 2, points = 6, description = "You managed to transform, improve and sacrify your bow into a master state and have proven yourself worthy in a nightmarish world."}, + [329] = {name = "Umbral Berserker", grade = 2, points = 6, description = "You managed to transform, improve and sacrify your hammer into a master state and have proven yourself worthy in a nightmarish world."}, + [330] = {name = "Umbral Bladelord", grade = 2, points = 6, description = "You managed to transform, improve and sacrify your slayer into a master state and have proven yourself worthy in a nightmarish world."}, + [331] = {name = "Umbral Brawler", grade = 2, points = 6, description = "You managed to transform, improve and sacrify your mace into a master state and have proven yourself worthy in a nightmarish world."}, + [332] = {name = "Umbral Executioner", grade = 2, points = 6, description = "You managed to transform, improve and sacrify your chopper into a master state and have proven yourself worthy in a nightmarish world."}, + [333] = {name = "Umbral Harbringer", grade = 2, points = 6, description = "You managed to transform, improve and sacrify your spellbook into a master state and have proven yourself worthy in a nightmarish world."}, + [334] = {name = "Umbral Headsman", grade = 2, points = 6, description = "You managed to transform, improve and sacrify your axe into a master state and have proven yourself worthy in a nightmarish world."}, + [335] = {name = "Umbral Marksman", grade = 2, points = 6, description = "You managed to transform, improve and sacrify your crossbow into a master state and have proven yourself worthy in a nightmarish world."}, + [336] = {name = "Umbral Master", grade = 3, points = 8, description = "You managed to transform, improve and sacrify all kinds of weapons into a master state and have proven yourself worthy in a nightmarish world. Respect!"}, + [337] = {name = "Umbral Swordsman", grade = 2, points = 6, description = "You managed to transform, improve and sacrify your blade into a master state and have proven yourself worthy in a nightmarish world."}, + + -- 10.5 + [338] = {name = "Combo Master", grade = 1, points = 1, secret = true, description = "You accomplished 10 or more consecutive chains in a row! That's killing at least 39 creatures in the correct order - now that's combinatorics!"}, + [339] = {name = "Elementary, My Dear", grade = 1, points = 1, description = "Through the spirit of science and exploration, you have discovered how to enter the secret hideout of the renowned Dr Merlay."}, + [340] = {name = "Glooth Engineer", grade = 2, points = 5, description = "Though you might have averted a dire threat for Rathleton, this relative peace may only hold for a while. At least you've scavenged an outfit from some of the poor fellows that have fallen prey to death priest Shagron."}, + [341] = {name = "Rathleton Citizen", grade = 1, points = 1, description = "By having rendered numerous services to the city of Rathleton you have been promoted to the rank of Citizen."}, + [342] = {name = "Rathleton Commoner", grade = 1, points = 1, description = "By having rendered numerous services to the city of Rathleton you have been promoted to the rank of Commoner."}, + [343] = {name = "Rathleton Inhabitant", grade = 1, points = 1, description = "By having rendered numerous services to the city of Rathleton you have been promoted to the rank of Inhabitant."}, + + -- 10.56 + [344] = {name = "Seasoned Adventurer", grade = 1, points = 1, description = "Adventure is your middle name. You spent much time in dangerous lands and have seen things others only dream of. You know your way around in the World - you are a seasoned adventurer now. And your journey has only just begun!"}, + + -- 10.7 + [345] = {name = "Go with da Lava Flow", grade = 1, points = 1, secret = true, description = "You escaped the glowing hot lava death trap, Professor Maxxen has set for you - Captain Caveworm is indeed proud!"}, + [346] = {name = "Lion's Den Explorer", grade = 1, points = 1, secret = true, description = "You discovered the Lion's Rock, passed the tests to enter the inner sanctum and finally revealed the secrets of the buried temple. You literally put your head in the lion's mouth and survived."}, + [347] = {name = "Mind the Step!", grade = 1, points = 1, description = "You've got a mind ready to draw strange conclusions that defy the laws of logic and sidestep reality. Or maybe it's just a lucky guess - or adventurous recklessness?"}, + [348] = {name = "Plant vs. Minos", grade = 1, points = 4, secret = true, description = "You have defeated the wallbreaker and saved the glooth plant."}, + [349] = {name = "Publicity", grade = 1, points = 1, description = "You are a man of the public. Or of good publicity at least. Through your efforts in advertising the airtight cloth, Zeronex might yet be redeemed - and Rathleton might yet see its first working Gloud Ship."}, + [350] = {name = "Rathleton Squire", grade = 1, points = 1, description = "By having rendered numerous services to the city of Rathleton you have been promoted to the rank of Squire."}, + [351] = {name = "Robo Chop", grade = 1, points = 4, secret = true, description = "You have defeated the glooth bomb and chopped down a lot of metal monsters on your way."}, + [352] = {name = "Rumble in the Plant", grade = 2, points = 4, secret = true, description = "You have defeated the tremor worm - and wonder what kind of fish you'd be able to catch with such a bait."}, + [353] = {name = "Snake Charmer", grade = 1, points = 1, description = "By restoring the Everhungry Altar, you charmed the Fire-Feathered Sea Serpent back into its fitful sleep, twenty miles beneath the sea."}, + [354] = {name = "The Professor's Nut", grade = 1, points = 3, description = "He seriously stored away a wallnut? That was a nutty professor indeed."}, + [355] = {name = "Wail of the Banshee", grade = 1, points = 1, secret = true, description = "You saw the Crystal Gardens with all their stunning beauty and survived the equally impressive monsters there. In the end you discovered a great evil and destroyed it with the help of a banshee who was not even aware of her support."}, + + -- 10.8 + [356] = {name = "Bearbaiting", grade = 1, points = 1, description = "Hunter's greeting! Your skillful use of the slingshot actually stunned a large bear. The creature is slightly dazed, but seems susceptible to your commands. Let's declare open season on all our foes!"}, + [357] = {name = "Beneath the Sea", grade = 1, points = 3, description = "Not really twenty thousand miles, but you had to dive a fair way beneath the sea to find your personal Manta Ray."}, + [358] = {name = "Blacknailed", grade = 1, points = 1, description = "Well, you can rest your nailcase now. This gravedigger's fingernails are nice and clean. Though after the next hellride, you might not want to let it hand any food to you."}, + [359] = {name = "Cartography 101", grade = 1, points = 2, description = "You succeeded in finding and charting several previously unexplored landmarks and locations for the Adventurer's Guild, you probably never need to ask anyone for the way - do you?"}, + [360] = {name = "Chequered Teddy", grade = 1, points = 1, description = "Don't let its fluffy appearance deceive you. The panda is a creature of the wild. It will take you to the most distant regions of the World, always in hopes of a little bamboo to nibble on or to check on a possible mate."}, + [361] = {name = "Dragon Mimicry", grade = 1, points = 2, description = "It's not really a dragon, but rather a kind of chimera. Nonetheless a decent mount to impress any passer-by."}, + [362] = {name = "Fabled Construction", grade = 1, points = 3, description = "Finding all the pieces to this complicated vehicle was one kind of a challenge. However, what you built in the end is rather a fabled than a feeble construction."}, + [363] = {name = "Fata Morgana", grade = 1, points = 2, description = "There are many delusions and phantasms in the desert. You saw a false oasis with fruit-bearing palm trees. Instead of water and refreshment, however, you found a dromedary in the end. What a useful Fata Morgana!"}, + [364] = {name = "Fried Shrimp", grade = 1, points = 2, description = "This must be underwater love - this enormous crustacean now does thy bidding. Or maybe it's just in it for a little more of that shrimp barbecue, as that's a little hard to come by in the sea."}, + [365] = {name = "Friend of Elves", grade = 1, points = 1, description = "Kingly deer mostly prefer elves as friends and familiars. This one, however, decided to favour you as a confidant and rider. Well done!"}, + [366] = {name = "Gear Up", grade = 1, points = 3, description = "Installing that control unit was a no-brainer. Now you're in control to make it walk this way or that, or to change tack at any moment if required. Your faithful walker mount obeys your every command."}, + [367] = {name = "Golden Sands", grade = 1, points = 3, description = "Counting ten thousand grains of sand could not have been harder than gaining this impressive mount."}, + [368] = {name = "Hoard of the Dragon", grade = 1, points = 1, secret = true, description = "Your adventurous way through countless dragon lairs earned you a pretty treasure - and surely the enmity of many a dragon."}, + [369] = {name = "Icy Glare", grade = 1, points = 1, description = "Here's looking at you, kid. This ancient creature seems to size you up with its brilliant eyes and barely tolerates you riding it. Maybe it thinks you're the defrosted snack, after all?"}, + [370] = {name = "Knock on Wood", grade = 1, points = 3, description = "It's a wound-up wooden lizard! Well, stranger things have happened, or so you're told. Just hop on and let this wood-and-tin contraption take you anywhere you want to wind down a bit. And hope you don't get hit by lightning underway."}, + [371] = {name = "Lion King", grade = 1, points = 1, description = "By mastering the secrets of Lion's Rock, you proved yourself worthy to face the mighty lions there. One of them even chose to accompany you."}, + [372] = {name = "Little Ball of Wool", grade = 1, points = 1, description = "You found a lost sheep and thus a steady source of black wool. But careful: don't get entangled."}, + [373] = {name = "Lost Palace Raider", grade = 1, points = 2, secret = true, description = "Lifting the secrets of a fabulous palace and defeating a beautiful demon princess was a thrilling experience indeed. This site's marvels nearly matched its terrors. Nearly."}, + [374] = {name = "Lovely Dots", grade = 1, points = 3, description = "Finding a four-leaved clover is always a sign of luck. And as luck would have it, you even baited a lovely dotted ladybug. Lucky you!"}, + [375] = {name = "Loyal Lad", grade = 1, points = 1, description = "Having a loyal friend alongside is comforting to every adventurer. If only this lad was not so stubborn..."}, + [376] = {name = "Lucky Horseshoe", grade = 1, points = 1, description = "'Sweets for my steed' could be your motto. An impressive horse is eating out of your hand. Saddle up and be ready to find adventure, new friends, and maybe someone to shoe your horse now and then."}, + [377] = {name = "Luminous Kitty", grade = 1, points = 3, description = "You made some efforts to bring a little more light into the world. And what a nice present you got in return!"}, + [378] = {name = "Magnetised", grade = 1, points = 2, description = "This magnetic beast attracted you in a very literal way. Or was it attracted by your metal equipment? Anyway, you seem to be stuck together now."}, + [379] = {name = "Mind the Dog!", grade = 1, points = 2, description = "Barking dogs never bite, as the saying goes. But this one clearly tried. In the end, however, you were able to walk the dog - ahem, gnarlhound."}, + [380] = {name = "Out of the Stone Age", grade = 1, points = 3, description = "What a blast from the past! This thankful patient thinks you missed your dentist vocation. It's now ready to take a bite of the future and to carry you to your next adventure, or your next patient."}, + [381] = {name = "Pecking Order", grade = 1, points = 1, description = "Ah, the old carrot-on-a-stick trick. Well done! You've made the racing bird accept you as a rider and provider. Just don't feed it your fingers."}, + [382] = {name = "Personal Nightmare", grade = 1, points = 3, description = "It might come as a shock to you, but this is the mount of your dreams. Not exactly the white steed of Prince Charming, but maybe the ladies will still scream and faint at the sight of you."}, + [383] = {name = "Pig-Headed", grade = 1, points = 2, description = "Whoa, sow long! This boar is like a force of nature, breaking through the undergrowth of all the forests and all records of speed. Hang on!"}, + [384] = {name = "Scales and Tail", grade = 1, points = 2, description = "The Muggy Plains are a dangerous place, often raided by dragons. But that was your luck: thus you found this scaly little guy."}, + [385] = {name = "Slugging Around", grade = 1, points = 2, description = "Drugging a snail can have some beneficial side effects. You're now the proud owner of a snarling, speed-crazy slug. Maybe it'll purr if you stroke it. Anyway, life should be one slick ride from now on."}, + [386] = {name = "Spin-Off", grade = 1, points = 1, description = "Seems like this spider has got a sweet tooth. As a result, eight hairy legs are now at your disposal to crawl and weave at your whim, and strike fear into the hearts of men."}, + [387] = {name = "Starless Night", grade = 1, points = 3, description = "By many it is considered a myth like the Yeti. But you came, saw and tamed it. Now you're the proud rider of a midnight panther, black as a starless night."}, + [388] = {name = "Stuntman", grade = 1, points = 3, description = "A drop of oil and you're good to go. This unique mount will roll merrily in and out of any strange place you want to visit. If you see no exit, you probably ended up in a circus ring. Ah well, the show must go on!"}, + [389] = {name = "Swamp Beast", grade = 1, points = 1, description = "By cleverly using a leech to cool that raging bull's blood, you managed not to get swamped or trampled in a water buffalo stampede. The creature is now docile and follows your every command."}, + [390] = {name = "The Right Tone", grade = 1, points = 1, description = "By setting the right tone you convinced a crystal wolf to accompany you. Remember it is made of crystal, though, so be careful in a banshee's presence."}, + [391] = {name = "Thick-Skinned", grade = 1, points = 2, description = "It's unstoppable! Walls? Fortresses? Obstacles? Objections? Pah! Nothing will stand before the stampor. Arrows and spears bounce off its hide, enemies are trampled by the dozen. Just don't go for the subtle approach or a date on this thing."}, + [392] = {name = "Way to Hell", grade = 1, points = 2, description = "This fiery beast really tried to give you hell. But not even a magma crawler can resist a mug of spicy, hot glow wine. Skol!"}, + + -- 10.9 + [393] = {name = "Hat Hunter", grade = 2, points = 5, description = "You sucessfully fought against all odds to protect your world from an ascending god! – You weren't there for the hat only after all?"}, + [394] = {name = "Ogre Chef", grade = 1, points = 1, description = "You didn't manage to become an ogre chief. But at least you are, beyond doubt, a worthy ogre chef."}, + [395] = {name = "Rift Warrior", grade = 1, points = 3, description = "You went through hell. Seven times. You defeated the demons. Countless times. You put an end to Ferumbras claims to ascendancy. Once and for all."}, + [396] = {name = "The Call of the Wild", grade = 1, points = 2, description = "You opposed man-eating ogres and clumsy clomps. You grappled with hungry chieftains, desperate goblins and angry spirits. So you truly overcame the wild vastness of Krailos."}, + + -- 10.94 + [397] = {name = "Ender of the End", grade = 2, points = 5, description = "You have entered the heart of destruction and valiantly defeated the world devourer. By your actions you have postponed the end of the world — at least for a while."}, + [398] = {name = "Vortex Tamer", grade = 2, points = 5, description = "After a long journey and dedication you were favoured by fortune and have tamed all three elusive beasts of the vortex. Unless the Vortexion decides you're a tasty morsel you can enjoy your small stable of ravaging beasts from beyond."}, +} + +ACHIEVEMENT_FIRST = 1 +ACHIEVEMENT_LAST = #achievements + +function getAchievementInfoById(id) + for k, v in pairs(achievements) do + if k == id then + local targetAchievement = {} + targetAchievement.id = k + targetAchievement.actionStorage = PlayerStorageKeys.achievementsCounter + k + for inf, it in pairs(v) do + targetAchievement[inf] = it + end + return targetAchievement + end + end + return false +end + +function getAchievementInfoByName(name) + for k, v in pairs(achievements) do + if v.name:lower() == name:lower() then + local targetAchievement = {} + targetAchievement.id = k + targetAchievement.actionStorage = PlayerStorageKeys.achievementsCounter + k + for inf, it in pairs(v) do + targetAchievement[inf] = it + end + return targetAchievement + end + end + return false +end + +function getSecretAchievements() + local targetAchievement = {} + for k, v in pairs(achievements) do + if v.secret then + targetAchievement[#targetAchievement + 1] = k + end + end + return targetAchievement +end + +function getPublicAchievements() + local targetAchievement = {} + for k, v in pairs(achievements) do + if not v.secret then + targetAchievement[#targetAchievement + 1] = k + end + end + return targetAchievement +end + +function getAchievements() + return achievements +end + +function isAchievementSecret(ach) + local achievement + if tonumber(ach) ~= nil then + achievement = getAchievementInfoById(ach) + else + achievement = getAchievementInfoByName(ach) + end + if not achievement then + print("[!] -> Invalid achievement \"" .. ach .. "\".") + return false + end + + return achievement.secret +end + +function Player.hasAchievement(self, ach) + local achievement + if tonumber(ach) ~= nil then + achievement = getAchievementInfoById(ach) + else + achievement = getAchievementInfoByName(ach) + end + if not achievement then + print("[!] -> Invalid achievement \"" .. ach .. "\".") + return false + end + + return self:getStorageValue(PlayerStorageKeys.achievementsBase + achievement.id) > 0 +end + +function Player.getAchievements(self) + local targetAchievement = {} + for k = 1, #achievements do + if self:hasAchievement(k) then + targetAchievement[#targetAchievement + 1] = k + end + end + return targetAchievement +end + +function Player.addAchievement(self, ach, hideMsg) + local achievement + if tonumber(ach) ~= nil then + achievement = getAchievementInfoById(ach) + else + achievement = getAchievementInfoByName(ach) + end + if not achievement then + print("[!] -> Invalid achievement \"" .. ach .. "\".") + return false + end + + if not self:hasAchievement(achievement.id) then + self:setStorageValue(PlayerStorageKeys.achievementsBase + achievement.id, 1) + if not hideMsg then + self:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Congratulations! You earned the achievement \"" .. achievement.name .. "\".") + end + end + return true +end + +function Player.removeAchievement(self, ach) + local achievement + if tonumber(ach) ~= nil then + achievement = getAchievementInfoById(ach) + else + achievement = getAchievementInfoByName(ach) + end + if not achievement then + print("[!] -> Invalid achievement \"" .. ach .. "\".") + return false + end + + if self:hasAchievement(achievement.id) then + self:setStorageValue(PlayerStorageKeys.achievementsBase + achievement.id, -1) + end + return true +end + +function Player.addAllAchievements(self, hideMsg) + for i = ACHIEVEMENT_FIRST, ACHIEVEMENT_LAST do + self:addAchievement(i, hideMsg) + end + return true +end + +function Player.removeAllAchievements(self) + for k = 1, #achievements do + if self:hasAchievement(k) then + self:removeAchievement(k) + end + end + return true +end + +function Player.getSecretAchievements(self) + local targetAchievement = {} + for k, v in pairs(achievements) do + if self:hasAchievement(k) and v.secret then + targetAchievement[#targetAchievement + 1] = k + end + end + return targetAchievement +end + +function Player.getPublicAchievements(self) + local targetAchievement = {} + for k, v in pairs(achievements) do + if self:hasAchievement(k) and not v.secret then + targetAchievement[#targetAchievement + 1] = k + end + end + return targetAchievement +end + +function Player.getAchievementPoints(self) + local points = 0 + local list = self:getAchievements() + if #list > 0 then -- has achievements + for i = 1, #list do + local targetAchievement = getAchievementInfoById(list[i]) + if targetAchievement.points > 0 then -- avoid achievements with unknow points + points = points + targetAchievement.points + end + end + end + return points +end + +function Player.addAchievementProgress(self, ach, value) + local achievement = tonumber(ach) ~= nil and getAchievementInfoById(ach) or getAchievementInfoByName(ach) + if not achievement then + print('[!] -> Invalid achievement "' .. ach .. '".') + return true + end + + local storage = PlayerStorageKeys.achievementsCounter + achievement.id + local progress = self:getStorageValue(storage) + if progress < value then + self:setStorageValue(storage, math.max(1, progress) + 1) + elseif progress == value then + self:setStorageValue(storage, value + 1) + self:addAchievement(achievement.id) + end + return true +end diff --git a/data/lib/core/actionids.lua b/data/lib/core/actionids.lua new file mode 100644 index 0000000..d4092e4 --- /dev/null +++ b/data/lib/core/actionids.lua @@ -0,0 +1,7 @@ +actionIds = { + sandHole = 100, -- hidden sand hole + pickHole = 105, -- hidden mud hole + levelDoor = 1000, -- level door + citizenship = 30020, -- citizenship teleport + citizenshipLast = 30050, -- citizenship teleport last +} diff --git a/data/lib/core/combat.lua b/data/lib/core/combat.lua new file mode 100644 index 0000000..236ab74 --- /dev/null +++ b/data/lib/core/combat.lua @@ -0,0 +1,21 @@ +function Combat:getPositions(creature, variant) + local positions = {} + function onTargetTile(creature, position) + positions[#positions + 1] = position + end + + self:setCallback(CALLBACK_PARAM_TARGETTILE, "onTargetTile") + self:execute(creature, variant) + return positions +end + +function Combat:getTargets(creature, variant) + local targets = {} + function onTargetCreature(creature, target) + targets[#targets + 1] = target + end + + self:setCallback(CALLBACK_PARAM_TARGETCREATURE, "onTargetCreature") + self:execute(creature, variant) + return targets +end diff --git a/data/lib/core/constants.lua b/data/lib/core/constants.lua new file mode 100644 index 0000000..1083b2b --- /dev/null +++ b/data/lib/core/constants.lua @@ -0,0 +1,6 @@ +CONTAINER_POSITION = 0xFFFF + +DAMAGELIST_EXPONENTIAL_DAMAGE = 0 +DAMAGELIST_LOGARITHMIC_DAMAGE = 1 +DAMAGELIST_VARYING_PERIOD = 2 +DAMAGELIST_CONSTANT_PERIOD = 3 diff --git a/data/lib/core/container.lua b/data/lib/core/container.lua new file mode 100644 index 0000000..b8fe9b5 --- /dev/null +++ b/data/lib/core/container.lua @@ -0,0 +1,48 @@ +function Container.isContainer(self) + return true +end + +function Container.createLootItem(self, item) + if self:getEmptySlots() == 0 then + return true + end + + local itemCount = 0 + local randvalue = getLootRandom() + if randvalue < item.chance then + if ItemType(item.itemId):isStackable() then + itemCount = randvalue % item.maxCount + 1 + else + itemCount = 1 + end + end + + if itemCount > 0 then + local tmpItem = self:addItem(item.itemId, math.min(itemCount, 100)) + if not tmpItem then + return false + end + + if tmpItem:isContainer() then + for i = 1, #item.childLoot do + if not tmpItem:createLootItem(item.childLoot[i]) then + tmpItem:remove() + return false + end + end + end + + if item.subType ~= -1 then + tmpItem:setAttribute(ITEM_ATTRIBUTE_CHARGES, item.subType) + end + + if item.actionId ~= -1 then + tmpItem:setActionId(item.actionId) + end + + if item.text and item.text ~= "" then + tmpItem:setText(item.text) + end + end + return true +end diff --git a/data/lib/core/core.lua b/data/lib/core/core.lua new file mode 100644 index 0000000..ef4d6e2 --- /dev/null +++ b/data/lib/core/core.lua @@ -0,0 +1,18 @@ +-- Note: The library of storages must be loaded previously to the other libraries. +dofile('data/lib/core/storages.lua') + +dofile('data/lib/core/achievements.lua') +dofile('data/lib/core/actionids.lua') +dofile('data/lib/core/combat.lua') +dofile('data/lib/core/constants.lua') +dofile('data/lib/core/container.lua') +dofile('data/lib/core/creature.lua') +dofile('data/lib/core/game.lua') +dofile('data/lib/core/item.lua') +dofile('data/lib/core/itemtype.lua') +dofile('data/lib/core/party.lua') +dofile('data/lib/core/player.lua') +dofile('data/lib/core/position.lua') +dofile('data/lib/core/teleport.lua') +dofile('data/lib/core/tile.lua') +dofile('data/lib/core/vocation.lua') diff --git a/data/lib/core/creature.lua b/data/lib/core/creature.lua new file mode 100644 index 0000000..97084f8 --- /dev/null +++ b/data/lib/core/creature.lua @@ -0,0 +1,169 @@ +function Creature.getClosestFreePosition(self, position, maxRadius, mustBeReachable) + maxRadius = maxRadius or 1 + + -- backward compatability (extended) + if maxRadius == true then + maxRadius = 2 + end + + local checkPosition = Position(position) + for radius = 0, maxRadius do + checkPosition.x = checkPosition.x - math.min(1, radius) + checkPosition.y = checkPosition.y + math.min(1, radius) + + local total = math.max(1, radius * 8) + for i = 1, total do + if radius > 0 then + local direction = math.floor((i - 1) / (radius * 2)) + checkPosition:getNextPosition(direction) + end + + local tile = Tile(checkPosition) + if tile:getCreatureCount() == 0 and not tile:hasProperty(CONST_PROP_IMMOVABLEBLOCKSOLID) and + (not mustBeReachable or self:getPathTo(checkPosition)) then + return checkPosition + end + end + end + return Position() +end + +function Creature.getPlayer(self) + return self:isPlayer() and self or nil +end + +function Creature.isContainer(self) + return false +end + +function Creature.isItem(self) + return false +end + +function Creature.isMonster(self) + return false +end + +function Creature.isNpc(self) + return false +end + +function Creature.isPlayer(self) + return false +end + +function Creature.isTeleport(self) + return false +end + +function Creature.isTile(self) + return false +end + +function Creature:setMonsterOutfit(monster, time) + local monsterType = MonsterType(monster) + if not monsterType then + return false + end + + if self:isPlayer() and not (self:hasFlag(PlayerFlag_CanIllusionAll) or monsterType:isIllusionable()) then + return false + end + + local condition = Condition(CONDITION_OUTFIT) + condition:setOutfit(monsterType:getOutfit()) + condition:setTicks(time) + self:addCondition(condition) + + return true +end + +function Creature:setItemOutfit(item, time) + local itemType = ItemType(item) + if not itemType then + return false + end + + local condition = Condition(CONDITION_OUTFIT) + condition:setOutfit({ + lookTypeEx = itemType:getId() + }) + condition:setTicks(time) + self:addCondition(condition) + + return true +end + +function Creature:addSummon(monster) + local summon = Monster(monster) + if not summon then + return false + end + + summon:setTarget(nil) + summon:setFollowCreature(nil) + summon:setDropLoot(false) + summon:setSkillLoss(false) + summon:setMaster(self) + summon:getPosition():notifySummonAppear(summon) + + return true +end + +function Creature:removeSummon(monster) + local summon = Monster(monster) + if not summon or summon:getMaster() ~= self then + return false + end + + summon:setTarget(nil) + summon:setFollowCreature(nil) + summon:setDropLoot(true) + summon:setSkillLoss(true) + summon:setMaster(nil) + + return true +end + +function Creature:addDamageCondition(target, type, list, damage, period, rounds) + if damage <= 0 or not target or target:isImmune(type) then + return false + end + + local condition = Condition(type) + condition:setParameter(CONDITION_PARAM_OWNER, self:getId()) + condition:setParameter(CONDITION_PARAM_DELAYED, true) + + if list == DAMAGELIST_EXPONENTIAL_DAMAGE then + local exponent, value = -10, 0 + while value < damage do + value = math.floor(10 * math.pow(1.2, exponent) + 0.5) + condition:addDamage(1, period or 4000, -value) + + if value >= damage then + local permille = math.random(10, 1200) / 1000 + condition:addDamage(1, period or 4000, -math.max(1, math.floor(value * permille + 0.5))) + else + exponent = exponent + 1 + end + end + elseif list == DAMAGELIST_LOGARITHMIC_DAMAGE then + local n, value = 0, damage + while value > 0 do + value = math.floor(damage * math.pow(2.718281828459, -0.05 * n) + 0.5) + if value ~= 0 then + condition:addDamage(1, period or 4000, -value) + n = n + 1 + end + end + elseif list == DAMAGELIST_VARYING_PERIOD then + for _ = 1, rounds do + condition:addDamage(1, math.random(period[1], period[2]) * 1000, -damage) + end + elseif list == DAMAGELIST_CONSTANT_PERIOD then + condition:addDamage(rounds, period * 1000, -damage) + end + + target:addCondition(condition) + return true +end diff --git a/data/lib/core/game.lua b/data/lib/core/game.lua new file mode 100644 index 0000000..7d6497e --- /dev/null +++ b/data/lib/core/game.lua @@ -0,0 +1,68 @@ +function Game.broadcastMessage(message, messageType) + if not messageType then + messageType = MESSAGE_STATUS_WARNING + end + + for _, player in ipairs(Game.getPlayers()) do + player:sendTextMessage(messageType, message) + end +end + +function Game.convertIpToString(ip) + local band = bit.band + local rshift = bit.rshift + return string.format("%d.%d.%d.%d", + band(ip, 0xFF), + band(rshift(ip, 8), 0xFF), + band(rshift(ip, 16), 0xFF), + rshift(ip, 24) + ) +end + +function Game.getReverseDirection(direction) + if direction == WEST then + return EAST + elseif direction == EAST then + return WEST + elseif direction == NORTH then + return SOUTH + elseif direction == SOUTH then + return NORTH + elseif direction == NORTHWEST then + return SOUTHEAST + elseif direction == NORTHEAST then + return SOUTHWEST + elseif direction == SOUTHWEST then + return NORTHEAST + elseif direction == SOUTHEAST then + return NORTHWEST + end + return NORTH +end + +function Game.getSkillType(weaponType) + if weaponType == WEAPON_CLUB then + return SKILL_CLUB + elseif weaponType == WEAPON_SWORD then + return SKILL_SWORD + elseif weaponType == WEAPON_AXE then + return SKILL_AXE + elseif weaponType == WEAPON_DISTANCE then + return SKILL_DISTANCE + elseif weaponType == WEAPON_SHIELD then + return SKILL_SHIELD + end + return SKILL_FIST +end + +if not globalStorageTable then + globalStorageTable = {} +end + +function Game.getStorageValue(key) + return globalStorageTable[key] +end + +function Game.setStorageValue(key, value) + globalStorageTable[key] = value +end diff --git a/data/lib/core/item.lua b/data/lib/core/item.lua new file mode 100644 index 0000000..f036872 --- /dev/null +++ b/data/lib/core/item.lua @@ -0,0 +1,31 @@ +function Item.getType(self) + return ItemType(self:getId()) +end + +function Item.isContainer(self) + return false +end + +function Item.isCreature(self) + return false +end + +function Item.isMonster(self) + return false +end + +function Item.isNpc(self) + return false +end + +function Item.isPlayer(self) + return false +end + +function Item.isTeleport(self) + return false +end + +function Item.isTile(self) + return false +end diff --git a/data/lib/core/itemtype.lua b/data/lib/core/itemtype.lua new file mode 100644 index 0000000..c94ba7f --- /dev/null +++ b/data/lib/core/itemtype.lua @@ -0,0 +1,16 @@ +local slotBits = { + [CONST_SLOT_HEAD] = SLOTP_HEAD, + [CONST_SLOT_NECKLACE] = SLOTP_NECKLACE, + [CONST_SLOT_BACKPACK] = SLOTP_BACKPACK, + [CONST_SLOT_ARMOR] = SLOTP_ARMOR, + [CONST_SLOT_RIGHT] = SLOTP_RIGHT, + [CONST_SLOT_LEFT] = SLOTP_LEFT, + [CONST_SLOT_LEGS] = SLOTP_LEGS, + [CONST_SLOT_FEET] = SLOTP_FEET, + [CONST_SLOT_RING] = SLOTP_RING, + [CONST_SLOT_AMMO] = SLOTP_AMMO +} + +function ItemType.usesSlot(self, slot) + return bit.band(self:getSlotPosition(), slotBits[slot] or 0) ~= 0 +end diff --git a/data/lib/core/party.lua b/data/lib/core/party.lua new file mode 100644 index 0000000..64fef7f --- /dev/null +++ b/data/lib/core/party.lua @@ -0,0 +1,10 @@ +function Party.broadcastPartyLoot(self, text) + self:getLeader():sendTextMessage(MESSAGE_INFO_DESCR, text) + local membersList = self:getMembers() + for i = 1, #membersList do + local player = membersList[i] + if player then + player:sendTextMessage(MESSAGE_INFO_DESCR, text) + end + end +end diff --git a/data/lib/core/player.lua b/data/lib/core/player.lua new file mode 100644 index 0000000..9ceae51 --- /dev/null +++ b/data/lib/core/player.lua @@ -0,0 +1,102 @@ +local foodCondition = Condition(CONDITION_REGENERATION, CONDITIONID_DEFAULT) + +function Player.feed(self, food) + local condition = self:getCondition(CONDITION_REGENERATION, CONDITIONID_DEFAULT) + if condition then + condition:setTicks(condition:getTicks() + (food * 1000)) + else + local vocation = self:getVocation() + if not vocation then + return nil + end + + foodCondition:setTicks(food * 1000) + foodCondition:setParameter(CONDITION_PARAM_HEALTHGAIN, vocation:getHealthGainAmount()) + foodCondition:setParameter(CONDITION_PARAM_HEALTHTICKS, vocation:getHealthGainTicks() * 1000) + foodCondition:setParameter(CONDITION_PARAM_MANAGAIN, vocation:getManaGainAmount()) + foodCondition:setParameter(CONDITION_PARAM_MANATICKS, vocation:getManaGainTicks() * 1000) + + self:addCondition(foodCondition) + end + return true +end + +function Player.getClosestFreePosition(self, position, extended) + if self:getGroup():getAccess() and self:getAccountType() >= ACCOUNT_TYPE_GOD then + return position + end + return Creature.getClosestFreePosition(self, position, extended) +end + +function Player.getDepotItems(self, depotId) + return self:getDepotChest(depotId, true):getItemHoldingCount() +end + +function Player.hasFlag(self, flag) + return self:getGroup():hasFlag(flag) +end + +function Player.getLossPercent(self) + local blessings = 0 + local lossPercent = { + [0] = 100, + [1] = 70, + [2] = 45, + [3] = 25, + [4] = 10, + [5] = 0 + } + + for i = 1, 5 do + if self:hasBlessing(i) then + blessings = blessings + 1 + end + end + return lossPercent[blessings] +end + +function Player.isPremium(self) + return self:getPremiumDays() > 0 or configManager.getBoolean(configKeys.FREE_PREMIUM) +end + +function Player.sendCancelMessage(self, message) + if type(message) == "number" then + message = Game.getReturnMessage(message) + end + return self:sendTextMessage(MESSAGE_STATUS_SMALL, message) +end + +function Player.isUsingOtClient(self) + return self:getClient().os >= CLIENTOS_OTCLIENT_LINUX +end + +function Player.sendExtendedOpcode(self, opcode, buffer) + if not self:isUsingOtClient() then + return false + end + + local networkMessage = NetworkMessage() + networkMessage:addByte(0x32) + networkMessage:addByte(opcode) + networkMessage:addString(buffer) + networkMessage:sendToPlayer(self, false) + networkMessage:delete() + return true +end + +APPLY_SKILL_MULTIPLIER = true +local addSkillTriesFunc = Player.addSkillTries +function Player.addSkillTries(...) + APPLY_SKILL_MULTIPLIER = false + local ret = addSkillTriesFunc(...) + APPLY_SKILL_MULTIPLIER = true + return ret +end + +local addManaSpentFunc = Player.addManaSpent +function Player.addManaSpent(...) + APPLY_SKILL_MULTIPLIER = false + local ret = addManaSpentFunc(...) + APPLY_SKILL_MULTIPLIER = true + return ret +end diff --git a/data/lib/core/position.lua b/data/lib/core/position.lua new file mode 100644 index 0000000..a40f8ea --- /dev/null +++ b/data/lib/core/position.lua @@ -0,0 +1,81 @@ +Position.directionOffset = { + [DIRECTION_NORTH] = {x = 0, y = -1}, + [DIRECTION_EAST] = {x = 1, y = 0}, + [DIRECTION_SOUTH] = {x = 0, y = 1}, + [DIRECTION_WEST] = {x = -1, y = 0}, + [DIRECTION_SOUTHWEST] = {x = -1, y = 1}, + [DIRECTION_SOUTHEAST] = {x = 1, y = 1}, + [DIRECTION_NORTHWEST] = {x = -1, y = -1}, + [DIRECTION_NORTHEAST] = {x = 1, y = -1} +} + +function Position:getNextPosition(direction, steps) + local offset = Position.directionOffset[direction] + if offset then + steps = steps or 1 + self.x = self.x + offset.x * steps + self.y = self.y + offset.y * steps + end +end + +function Position:moveUpstairs() + local swap = function (lhs, rhs) + lhs.x, rhs.x = rhs.x, lhs.x + lhs.y, rhs.y = rhs.y, lhs.y + lhs.z, rhs.z = rhs.z, lhs.z + end + + self.z = self.z - 1 + + local defaultPosition = self + Position.directionOffset[DIRECTION_SOUTH] + local toTile = Tile(defaultPosition) + if not toTile or not toTile:isWalkable() then + for direction = DIRECTION_NORTH, DIRECTION_NORTHEAST do + if direction == DIRECTION_SOUTH then + direction = DIRECTION_WEST + end + + local position = self + Position.directionOffset[direction] + toTile = Tile(position) + if toTile and toTile:isWalkable() then + swap(self, position) + return self + end + end + end + swap(self, defaultPosition) + return self +end + +function Position:isInRange(from, to) + -- No matter what corner from and to is, we want to make + -- life easier by calculating north-west and south-east + local zone = { + nW = { + x = (from.x < to.x and from.x or to.x), + y = (from.y < to.y and from.y or to.y), + z = (from.z < to.z and from.z or to.z) + }, + sE = { + x = (to.x > from.x and to.x or from.x), + y = (to.y > from.y and to.y or from.y), + z = (to.z > from.z and to.z or from.z) + } + } + + if self.x >= zone.nW.x and self.x <= zone.sE.x + and self.y >= zone.nW.y and self.y <= zone.sE.y + and self.z >= zone.nW.z and self.z <= zone.sE.z then + return true + end + return false +end + +function Position:notifySummonAppear(summon) + local spectators = Game.getSpectators(self) + for _, spectator in ipairs(spectators) do + if spectator:isMonster() and spectator ~= summon then + spectator:addTarget(summon) + end + end +end diff --git a/data/lib/core/storages.lua b/data/lib/core/storages.lua new file mode 100644 index 0000000..23d966a --- /dev/null +++ b/data/lib/core/storages.lua @@ -0,0 +1,19 @@ +PlayerStorageKeys = { + annihilatorReward = 30015, + promotion = 30018, + delayLargeSeaShell = 30019, + firstRod = 30020, + delayWallMirror = 30021, + madSheepSummon = 30023, + crateUsable = 30024, + afflictedOutfit = 30026, + afflictedPlagueMask = 30027, + afflictedPlagueBell = 30028, + nailCaseUseCount = 30031, + swampDigging = 30032, + achievementsBase = 300000, -- range 300000 to 301000+ reserved for achievements + achievementsCounter = 20000, -- range 20000 to 21000+ reserved for achievement progress +} + +GlobalStorageKeys = { +} diff --git a/data/lib/core/teleport.lua b/data/lib/core/teleport.lua new file mode 100644 index 0000000..8930268 --- /dev/null +++ b/data/lib/core/teleport.lua @@ -0,0 +1,3 @@ +function Teleport.isTeleport(self) + return true +end diff --git a/data/lib/core/tile.lua b/data/lib/core/tile.lua new file mode 100644 index 0000000..528c347 --- /dev/null +++ b/data/lib/core/tile.lua @@ -0,0 +1,55 @@ +function Tile.isCreature(self) + return false +end + +function Tile.isItem(self) + return false +end + +function Tile.isTile(self) + return true +end + +function Tile.isContainer(self) + return false +end + +function Tile.relocateTo(self, toPosition) + if self:getPosition() == toPosition or not Tile(toPosition) then + return false + end + + for i = self:getThingCount() - 1, 0, -1 do + local thing = self:getThing(i) + if thing then + if thing:isItem() then + if thing:getFluidType() ~= 0 then + thing:remove() + elseif ItemType(thing:getId()):isMovable() then + thing:moveTo(toPosition) + end + elseif thing:isCreature() then + thing:teleportTo(toPosition) + end + end + end + + return true +end + +function Tile.isWalkable(self) + local ground = self:getGround() + if not ground or ground:hasProperty(CONST_PROP_BLOCKSOLID) then + return false + end + + local items = self:getItems() + for i = 1, self:getItemCount() do + local item = items[i] + local itemType = item:getType() + if itemType:getType() ~= ITEM_TYPE_MAGICFIELD and not itemType:isMovable() and item:hasProperty(CONST_PROP_BLOCKSOLID) then + return false + end + end + return true +end diff --git a/data/lib/core/vocation.lua b/data/lib/core/vocation.lua new file mode 100644 index 0000000..acf31af --- /dev/null +++ b/data/lib/core/vocation.lua @@ -0,0 +1,7 @@ +function Vocation.getBase(self) + local base = self + while base:getDemotion() do + base = base:getDemotion() + end + return base +end diff --git a/data/lib/debugging/dump.lua b/data/lib/debugging/dump.lua new file mode 100644 index 0000000..4f9ccf3 --- /dev/null +++ b/data/lib/debugging/dump.lua @@ -0,0 +1,59 @@ +-- recursive dump function +function dumpLevel(input, level) + local indent = '' + + for i = 1, level do + indent = indent .. ' ' + end + + if type(input) == 'table' then + local str = '{ \n' + local lines = {} + + for k, v in pairs(input) do + if type(k) ~= 'number' then + k = '"' .. k .. '"' + end + + if type(v) == 'string' then + v = '"' .. v .. '"' + end + + table.insert(lines, indent .. ' [' .. k .. '] = ' .. dumpLevel(v, level + 1)) + end + return str .. table.concat(lines, ',\n') .. '\n' .. indent .. '}' + end + + return tostring(input) +end + +-- Return a string representation of input for debugging purposes +function dump(input) + return dumpLevel(input, 0) +end + +-- Call the dump function and print it to console +function pdump(input) + local dump_str = dump(input) + print(dump_str) + return dump_str +end + +-- Call the dump function with a title and print it beautifully to the console +function tdump(title, input) + local title_fill = '' + for i = 1, title:len() do + title_fill = title_fill .. '=' + end + + local header_str = '\n====' .. title_fill .. '====\n' + header_str = header_str .. '=== ' .. title .. ' ===\n' + header_str = header_str .. '====' .. title_fill .. '====\n' + + local dump_str = dump(input) + local footer_str = '\n====' .. title_fill .. '====\n' + + print(header_str .. dump_str .. footer_str) + + return dump_str +end diff --git a/data/lib/debugging/lua_version.lua b/data/lib/debugging/lua_version.lua new file mode 100644 index 0000000..18294c1 --- /dev/null +++ b/data/lib/debugging/lua_version.lua @@ -0,0 +1,5 @@ +if type(jit) == 'table' then + print('>> Using ' .. jit.version) --LuaJIT 2.0.2 +else + print('>> Using ' .. _VERSION) +end diff --git a/data/lib/lib.lua b/data/lib/lib.lua new file mode 100644 index 0000000..98ce93c --- /dev/null +++ b/data/lib/lib.lua @@ -0,0 +1,9 @@ +-- Core API functions implemented in Lua +dofile('data/lib/core/core.lua') + +-- Compatibility library for our old Lua API +dofile('data/lib/compat/compat.lua') + +-- Debugging helper function for Lua developers +dofile('data/lib/debugging/dump.lua') +dofile('data/lib/debugging/lua_version.lua') diff --git a/data/logs/.gitignore b/data/logs/.gitignore new file mode 100644 index 0000000..5e7d273 --- /dev/null +++ b/data/logs/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/data/migrations/0.lua b/data/migrations/0.lua new file mode 100644 index 0000000..0fb3ff9 --- /dev/null +++ b/data/migrations/0.lua @@ -0,0 +1,7 @@ +function onUpdateDatabase() + print("> Updating database to version 1 (account names)") + db.query("ALTER TABLE `accounts` ADD `name` VARCHAR(32) NOT NULL AFTER `id`") + db.query("UPDATE `accounts` SET `name` = `id`") + db.query("ALTER TABLE `accounts` ADD UNIQUE (`name`)") + return true +end diff --git a/data/migrations/1.lua b/data/migrations/1.lua new file mode 100644 index 0000000..c3e6f22 --- /dev/null +++ b/data/migrations/1.lua @@ -0,0 +1,5 @@ +function onUpdateDatabase() + print("> Updating database to version 2 (market offers)") + db.query("CREATE TABLE `market_offers` (`id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `player_id` INT NOT NULL, `sale` TINYINT(1) NOT NULL DEFAULT 0, `itemtype` INT UNSIGNED NOT NULL, `amount` SMALLINT UNSIGNED NOT NULL, `created` BIGINT UNSIGNED NOT NULL, `anonymous` TINYINT(1) NOT NULL DEFAULT 0, `price` INT UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY(`sale`, `itemtype`), KEY(`created`), FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE) ENGINE = InnoDB") + return true +end diff --git a/data/migrations/10.lua b/data/migrations/10.lua new file mode 100644 index 0000000..79efeca --- /dev/null +++ b/data/migrations/10.lua @@ -0,0 +1,28 @@ +function onUpdateDatabase() + print("> Updating database to version 11 (improved guild and players online structure)") + db.query("CREATE TABLE IF NOT EXISTS `guild_membership` (`player_id` int(11) NOT NULL, `guild_id` int(11) NOT NULL, `rank_id` int(11) NOT NULL, `nick` varchar(15) NOT NULL DEFAULT '', PRIMARY KEY (`player_id`), KEY `guild_id` (`guild_id`), KEY `rank_id` (`rank_id`)) ENGINE=InnoDB") + db.query("ALTER TABLE `guild_membership` ADD CONSTRAINT `guild_membership_ibfk_3` FOREIGN KEY (`rank_id`) REFERENCES `guild_ranks` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, ADD CONSTRAINT `guild_membership_ibfk_1` FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, ADD CONSTRAINT `guild_membership_ibfk_2` FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`id`) ON DELETE CASCADE ON UPDATE CASCADE") + db.query("ALTER TABLE `guild_invites` ADD PRIMARY KEY (`player_id`, `guild_id`)") + db.query("ALTER TABLE `player_skills` ADD PRIMARY KEY (`player_id`, `skillid`)") + db.query("ALTER TABLE `player_storage` ADD PRIMARY KEY (`player_id`, `key`)") + + local resultId = db.storeQuery("SELECT `players`.`id` AS `player_id`, `players`.`rank_id` AS `rank_id`, `players`.`guildnick` AS `guild_nick`, `guild_ranks`.`guild_id` AS `guild_id` FROM `guild_ranks` INNER JOIN `players` ON `guild_ranks`.`id` = `players`.`rank_id`") + if resultId ~= false then + local stmt = "INSERT INTO `guild_membership` (`player_id`, `guild_id`, `rank_id`, `nick`) VALUES " + repeat + stmt = stmt .. "(" .. result.getNumber(resultId, "player_id") .. "," .. result.getNumber(resultId, "guild_id") .. "," .. result.getNumber(resultId, "rank_id") .. "," .. db.escapeString(result.getString(resultId, "guild_nick")) .. ")," + until not result.next(resultId) + result.free(resultId) + + local stmtLen = string.len(stmt) + if stmtLen > 83 then + stmt = string.sub(stmt, 1, stmtLen - 1) + db.query(stmt) + end + end + + db.query("ALTER TABLE `players` DROP `rank_id`, DROP `guildnick`, DROP `direction`, DROP `loss_experience`, DROP `loss_mana`, DROP `loss_skills`, DROP `premend`, DROP `online`") + db.query("DROP TRIGGER IF EXISTS `ondelete_guilds`") + db.query("CREATE TABLE IF NOT EXISTS `players_online` (`player_id` int(11) NOT NULL, PRIMARY KEY (`player_id`)) ENGINE=MEMORY") + return true +end diff --git a/data/migrations/11.lua b/data/migrations/11.lua new file mode 100644 index 0000000..f9b1ef4 --- /dev/null +++ b/data/migrations/11.lua @@ -0,0 +1,24 @@ +function onUpdateDatabase() + print("> Updating database to version 12 (storing players record and message of the day in database)") + + local motdNum = "" + local motd = "" + + local lastMotdFile = io.open("lastMotd.txt", "r") + if lastMotdFile then + motdNum = lastMotdFile:read() + motd = lastMotdFile:read() + lastMotdFile:close() + end + + local record = 0 + + local playersRecordFile = io.open("playersRecord.txt", "r") + if playersRecordFile then + record = playersRecordFile:read("*n") + playersRecordFile:close() + end + + db.query("INSERT INTO `server_config` (`config`, `value`) VALUES ('players_record', '" .. record .. "'), ('motd_hash', SHA1(" .. db.escapeString(motd) .. ")), ('motd_num', " .. db.escapeString(motdNum) .. ")") + return true +end diff --git a/data/migrations/12.lua b/data/migrations/12.lua new file mode 100644 index 0000000..6e16858 --- /dev/null +++ b/data/migrations/12.lua @@ -0,0 +1,9 @@ +function onUpdateDatabase() + print("> Updating database to version 13 (house bidding system and additional columns to other tables)") + + db.query("ALTER TABLE `player_deaths` ADD `mostdamage_by` varchar(100) NOT NULL, ADD `mostdamage_is_player` tinyint(1) NOT NULL DEFAULT '0', ADD `unjustified` tinyint(1) NOT NULL DEFAULT '0', ADD `mostdamage_unjustified` tinyint(1) NOT NULL DEFAULT '0', ADD KEY `killed_by` (`killed_by`), ADD KEY `mostdamage_by` (`mostdamage_by`)") + db.query("ALTER TABLE `houses` ADD `name` varchar(255) NOT NULL, ADD `rent` int(11) NOT NULL DEFAULT '0', ADD `town_id` int(11) NOT NULL DEFAULT '0', ADD `bid` int(11) NOT NULL DEFAULT '0', ADD `bid_end` int(11) NOT NULL DEFAULT '0', ADD `last_bid` int(11) NOT NULL DEFAULT '0', ADD `highest_bidder` int(11) NOT NULL DEFAULT '0', ADD `size` int(11) NOT NULL DEFAULT '0', ADD `beds` int(11) NOT NULL DEFAULT '0', ADD KEY `owner` (`owner`), ADD KEY `town_id` (`town_id`)") + db.query("ALTER TABLE `players` ADD `onlinetime` int(11) NOT NULL DEFAULT '0', ADD `deletion` bigint(15) NOT NULL DEFAULT '0'") + db.query("ALTER TABLE `accounts` ADD `points` int(11) NOT NULL DEFAULT '0', ADD `creation` int(11) NOT NULL DEFAULT '0'") + return true +end diff --git a/data/migrations/13.lua b/data/migrations/13.lua new file mode 100644 index 0000000..4cbd9ff --- /dev/null +++ b/data/migrations/13.lua @@ -0,0 +1,37 @@ +function onUpdateDatabase() + print("> Updating database to version 14 (account_bans, ip_bans and player_bans)") + + db.query("CREATE TABLE IF NOT EXISTS `account_bans` (`account_id` int(11) NOT NULL, `reason` varchar(255) NOT NULL, `banned_at` bigint(20) NOT NULL, `expires_at` bigint(20) NOT NULL, `banned_by` int(11) NOT NULL, PRIMARY KEY (`account_id`), KEY `banned_by` (`banned_by`), FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (`banned_by`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB") + db.query("CREATE TABLE IF NOT EXISTS `account_ban_history` (`account_id` int(11) NOT NULL, `reason` varchar(255) NOT NULL, `banned_at` bigint(20) NOT NULL, `expired_at` bigint(20) NOT NULL, `banned_by` int(11) NOT NULL, PRIMARY KEY (`account_id`), KEY `banned_by` (`banned_by`), FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (`banned_by`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB") + db.query("CREATE TABLE IF NOT EXISTS `ip_bans` (`ip` int(10) unsigned NOT NULL, `reason` varchar(255) NOT NULL, `banned_at` bigint(20) NOT NULL, `expires_at` bigint(20) NOT NULL, `banned_by` int(11) NOT NULL, PRIMARY KEY (`ip`), KEY `banned_by` (`banned_by`), FOREIGN KEY (`banned_by`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB") + db.query("CREATE TABLE IF NOT EXISTS `player_namelocks` (`player_id` int(11) NOT NULL, `reason` varchar(255) NOT NULL, `namelocked_at` bigint(20) NOT NULL, `namelocked_by` int(11) NOT NULL, PRIMARY KEY (`player_id`), KEY `namelocked_by` (`namelocked_by`), FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (`namelocked_by`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB") + + local resultId = db.storeQuery("SELECT `player`, `time` FROM `bans` WHERE `type` = 2") + if resultId ~= false then + local stmt = "INSERT INTO `player_namelocks` (`player_id`, `namelocked_at`, `namelocked_by`) VALUES " + repeat + stmt = stmt .. "(" .. result.getNumber(resultId, "player") .. "," .. result.getNumber(resultId, "time") .. "," .. result.getNumber(resultId, "player") .. ")," + until not result.next(resultId) + result.free(resultId) + + local stmtLen = string.len(stmt) + if stmtLen > 86 then + stmt = string.sub(stmt, 1, stmtLen - 1) + db.query(stmt) + end + end + + db.query("DROP TRIGGER `ondelete_accounts`") + db.query("DROP TRIGGER `ondelete_players`") + db.query("ALTER TABLE `accounts` DROP `warnings`") + + db.query("DROP TABLE `bans`") + + print("Run this query in your database to create the ondelete_players trigger:") + print("DELIMITER //") + print("CREATE TRIGGER `ondelete_players` BEFORE DELETE ON `players`") + print(" FOR EACH ROW BEGIN") + print(" UPDATE `houses` SET `owner` = 0 WHERE `owner` = OLD.`id`;") + print("END //") + return true +end diff --git a/data/migrations/14.lua b/data/migrations/14.lua new file mode 100644 index 0000000..84e2e74 --- /dev/null +++ b/data/migrations/14.lua @@ -0,0 +1,30 @@ +function onUpdateDatabase() + print("> Updating database to version 15 (moving groups to data/XML/groups.xml)") + + db.query("ALTER TABLE players DROP FOREIGN KEY players_ibfk_2") + db.query("DROP INDEX group_id ON players") + + db.query("ALTER TABLE accounts DROP FOREIGN KEY accounts_ibfk_1") + db.query("DROP INDEX group_id ON accounts") + db.query("ALTER TABLE `accounts` DROP `group_id`") + + local groupsFile = io.open("data/XML/groups.xml", "w") + if groupsFile then + groupsFile:write("\r\n") + groupsFile:write("\r\n") + + local resultId = db.storeQuery("SELECT `id`, `name`, `flags`, `access`, `maxdepotitems`, `maxviplist` FROM `groups` ORDER BY `id` ASC") + if resultId ~= false then + repeat + groupsFile:write("\t\r\n") + until not result.next(resultId) + result.free(resultId) + end + + groupsFile:write("\r\n") + groupsFile:close() + + db.query("DROP TABLE `groups`") + end + return true +end diff --git a/data/migrations/15.lua b/data/migrations/15.lua new file mode 100644 index 0000000..f04796b --- /dev/null +++ b/data/migrations/15.lua @@ -0,0 +1,8 @@ +function onUpdateDatabase() + print("> Updating database to version 16 (moving skills into players table)") + db.query("ALTER TABLE `players` ADD `skill_fist` int(10) unsigned NOT NULL DEFAULT 10, ADD `skill_fist_tries` bigint(20) unsigned NOT NULL DEFAULT 0, ADD `skill_club` int(10) unsigned NOT NULL DEFAULT 10, ADD `skill_club_tries` bigint(20) unsigned NOT NULL DEFAULT 0, ADD `skill_sword` int(10) unsigned NOT NULL DEFAULT 10, ADD `skill_sword_tries` bigint(20) unsigned NOT NULL DEFAULT 0, ADD `skill_axe` int(10) unsigned NOT NULL DEFAULT 10, ADD `skill_axe_tries` bigint(20) unsigned NOT NULL DEFAULT 0, ADD `skill_dist` int(10) unsigned NOT NULL DEFAULT 10, ADD `skill_dist_tries` bigint(20) unsigned NOT NULL DEFAULT 0, ADD `skill_shielding` int(10) unsigned NOT NULL DEFAULT 10, ADD `skill_shielding_tries` bigint(20) unsigned NOT NULL DEFAULT 0, ADD `skill_fishing` int(10) unsigned NOT NULL DEFAULT 10, ADD `skill_fishing_tries` bigint(20) unsigned NOT NULL DEFAULT 0") + db.query("UPDATE `players` SET `skill_fist` = (SELECT `value` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 0), `skill_fist_tries` = (SELECT `count` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 0), `skill_club` = (SELECT `value` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 1), `skill_club_tries` = (SELECT `count` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 1), `skill_sword` = (SELECT `value` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 2), `skill_sword_tries` = (SELECT `count` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 2), `skill_axe` = (SELECT `value` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 3), `skill_axe_tries` = (SELECT `count` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 3), `skill_dist` = (SELECT `value` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 4), `skill_dist_tries` = (SELECT `count` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 4), `skill_shielding` = (SELECT `value` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 5), `skill_shielding_tries` = (SELECT `count` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 5), `skill_fishing` = (SELECT `value` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 6), `skill_fishing_tries` = (SELECT `count` FROM `player_skills` WHERE `player_id` = `id` AND `skillid` = 6)") + db.query("DROP TRIGGER `oncreate_players`") + db.query("DROP TABLE `player_skills`") + return true +end diff --git a/data/migrations/16.lua b/data/migrations/16.lua new file mode 100644 index 0000000..45e7422 --- /dev/null +++ b/data/migrations/16.lua @@ -0,0 +1,9 @@ +function onUpdateDatabase() + print("> Updating database to version 17 (fixing primary key in account ban history)") + db.query("ALTER TABLE `account_ban_history` DROP FOREIGN KEY `account_ban_history_ibfk_1`") + db.query("ALTER TABLE `account_ban_history` DROP PRIMARY KEY") + db.query("ALTER TABLE `account_ban_history` ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST") + db.query("ALTER TABLE `account_ban_history` ADD INDEX (`account_id`)") + db.query("ALTER TABLE `account_ban_history` ADD FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE") + return true +end diff --git a/data/migrations/17.lua b/data/migrations/17.lua new file mode 100644 index 0000000..e26bdd4 --- /dev/null +++ b/data/migrations/17.lua @@ -0,0 +1,6 @@ +function onUpdateDatabase() + print("> Updating database to version 18 (optimize account password field)") + db.query("DELETE FROM `server_config` WHERE `config` = 'encryption'") + db.query("ALTER TABLE `accounts` CHANGE `password` `password` CHAR(40) NOT NULL") + return true +end diff --git a/data/migrations/18.lua b/data/migrations/18.lua new file mode 100644 index 0000000..d84c3b3 --- /dev/null +++ b/data/migrations/18.lua @@ -0,0 +1,5 @@ +function onUpdateDatabase() + print("> Updating database to version 19 (authenticator token support)") + db.query("ALTER TABLE `accounts` ADD COLUMN `secret` CHAR(16) NULL AFTER `password`") + return true +end diff --git a/data/migrations/19.lua b/data/migrations/19.lua new file mode 100644 index 0000000..1e7e1ae --- /dev/null +++ b/data/migrations/19.lua @@ -0,0 +1,5 @@ +function onUpdateDatabase() + print("> Updating database to version 20 (setting default cap to 400)") + db.query("ALTER TABLE `players` CHANGE `cap` `cap` int(11) NOT NULL DEFAULT '400'") + return true +end diff --git a/data/migrations/2.lua b/data/migrations/2.lua new file mode 100644 index 0000000..942a1cd --- /dev/null +++ b/data/migrations/2.lua @@ -0,0 +1,5 @@ +function onUpdateDatabase() + print("> Updating database to version 3 (bank balance)") + db.query("ALTER TABLE `players` ADD `balance` BIGINT UNSIGNED NOT NULL DEFAULT 0") + return true +end diff --git a/data/migrations/20.lua b/data/migrations/20.lua new file mode 100644 index 0000000..135905d --- /dev/null +++ b/data/migrations/20.lua @@ -0,0 +1,6 @@ +function onUpdateDatabase() + print("> Updating database to version 21 (store towns in database)") + db.query("CREATE TABLE IF NOT EXISTS `towns` (`id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `posx` int(11) NOT NULL DEFAULT '0', `posy` int(11) NOT NULL DEFAULT '0', `posz` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`)) ENGINE=InnoDB") + + return true +end diff --git a/data/migrations/21.lua b/data/migrations/21.lua new file mode 100644 index 0000000..060cb19 --- /dev/null +++ b/data/migrations/21.lua @@ -0,0 +1,32 @@ +function onUpdateDatabase() + print("> Updating database to version 22 (UTF-8 encoding)") + db.query("ALTER TABLE account_ban_history CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE account_bans CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE account_viplist CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE accounts CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE guild_invites CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE guild_membership CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE guild_ranks CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE guild_wars CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE guilds CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE guildwar_kills CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE house_lists CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE houses CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE ip_bans CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE market_history CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE market_offers CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE player_deaths CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE player_depotitems CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE player_inboxitems CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE player_items CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE player_namelocks CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE player_spells CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE player_storage CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE players CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE players_online CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE server_config CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE tile_store CONVERT TO CHARACTER SET utf8") + db.query("ALTER TABLE towns CONVERT TO CHARACTER SET utf8") + + return true +end diff --git a/data/migrations/22.lua b/data/migrations/22.lua new file mode 100644 index 0000000..703aa0b --- /dev/null +++ b/data/migrations/22.lua @@ -0,0 +1,5 @@ +function onUpdateDatabase() + print("> Updating database to version 23 (Fix skulltime)") + db.query("ALTER TABLE players MODIFY skulltime BIGINT(20) NOT NULL DEFAULT 0") + return true +end diff --git a/data/migrations/23.lua b/data/migrations/23.lua new file mode 100644 index 0000000..27daa57 --- /dev/null +++ b/data/migrations/23.lua @@ -0,0 +1,5 @@ +function onUpdateDatabase() + print("> Updating database to version 24 (Add player direction)") + db.query("ALTER TABLE `players` ADD `direction` int(1) unsigned NOT NULL DEFAULT 2") + return true +end diff --git a/data/migrations/24.lua b/data/migrations/24.lua new file mode 100644 index 0000000..ff10db3 --- /dev/null +++ b/data/migrations/24.lua @@ -0,0 +1,5 @@ +function onUpdateDatabase() + print("> Updating database to version 25 (Optimize player direction 4 -> 1 byte)") + db.query("ALTER TABLE `players` MODIFY COLUMN `direction` tinyint(1) unsigned NOT NULL DEFAULT 2") + return true +end \ No newline at end of file diff --git a/data/migrations/25.lua b/data/migrations/25.lua new file mode 100644 index 0000000..d0ffd9c --- /dev/null +++ b/data/migrations/25.lua @@ -0,0 +1,3 @@ +function onUpdateDatabase() + return false +end diff --git a/data/migrations/3.lua b/data/migrations/3.lua new file mode 100644 index 0000000..c93e34d --- /dev/null +++ b/data/migrations/3.lua @@ -0,0 +1,5 @@ +function onUpdateDatabase() + print("> Updating database to version 4 (market history)") + db.query("CREATE TABLE `market_history` (`id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `player_id` INT NOT NULL, `sale` TINYINT(1) NOT NULL DEFAULT 0, `itemtype` INT UNSIGNED NOT NULL, `amount` SMALLINT UNSIGNED NOT NULL, `price` INT UNSIGNED NOT NULL DEFAULT 0, `expires_at` BIGINT UNSIGNED NOT NULL, `inserted` BIGINT UNSIGNED NOT NULL, `state` TINYINT(1) UNSIGNED NOT NULL, PRIMARY KEY(`id`), KEY(`player_id`, `sale`), FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE) ENGINE = InnoDB") + return true +end diff --git a/data/migrations/4.lua b/data/migrations/4.lua new file mode 100644 index 0000000..069a643 --- /dev/null +++ b/data/migrations/4.lua @@ -0,0 +1,7 @@ +function onUpdateDatabase() + print("> Updating database to version 5 (black skull & guild wars)") + db.query("ALTER TABLE `players` CHANGE `redskull` `skull` TINYINT(1) NOT NULL DEFAULT '0', CHANGE `redskulltime` `skulltime` INT(11) NOT NULL DEFAULT '0'") + db.query("CREATE TABLE IF NOT EXISTS `guild_wars` ( `id` int(11) NOT NULL AUTO_INCREMENT, `guild1` int(11) NOT NULL DEFAULT '0', `guild2` int(11) NOT NULL DEFAULT '0', `name1` varchar(255) NOT NULL, `name2` varchar(255) NOT NULL, `status` tinyint(2) NOT NULL DEFAULT '0', `started` bigint(15) NOT NULL DEFAULT '0', `ended` bigint(15) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `guild1` (`guild1`), KEY `guild2` (`guild2`)) ENGINE=InnoDB") + db.query("CREATE TABLE IF NOT EXISTS `guildwar_kills` (`id` int(11) NOT NULL AUTO_INCREMENT, `killer` varchar(50) NOT NULL, `target` varchar(50) NOT NULL, `killerguild` int(11) NOT NULL DEFAULT '0', `targetguild` int(11) NOT NULL DEFAULT '0', `warid` int(11) NOT NULL DEFAULT '0', `time` bigint(15) NOT NULL, PRIMARY KEY (`id`), KEY `warid` (`warid`), FOREIGN KEY (`warid`) REFERENCES `guild_wars`(`id`) ON DELETE CASCADE) ENGINE=InnoDB") + return true +end diff --git a/data/migrations/5.lua b/data/migrations/5.lua new file mode 100644 index 0000000..82ba621 --- /dev/null +++ b/data/migrations/5.lua @@ -0,0 +1,5 @@ +function onUpdateDatabase() + print("> Updating database to version 6 (market bug fix)") + db.query("DELETE FROM `market_offers` WHERE `amount` = 0") + return true +end diff --git a/data/migrations/6.lua b/data/migrations/6.lua new file mode 100644 index 0000000..f989467 --- /dev/null +++ b/data/migrations/6.lua @@ -0,0 +1,6 @@ +function onUpdateDatabase() + print("> Updating database to version 7 (offline training)") + db.query("ALTER TABLE `players` ADD `offlinetraining_time` SMALLINT UNSIGNED NOT NULL DEFAULT 43200") + db.query("ALTER TABLE `players` ADD `offlinetraining_skill` INT NOT NULL DEFAULT -1") + return true +end diff --git a/data/migrations/7.lua b/data/migrations/7.lua new file mode 100644 index 0000000..793f695 --- /dev/null +++ b/data/migrations/7.lua @@ -0,0 +1,35 @@ +function onUpdateDatabase() + print("> Updating database to version 8 (account viplist with description, icon and notify server side)") + db.query("RENAME TABLE `player_viplist` TO `account_viplist`") + db.query("ALTER TABLE `account_viplist` DROP FOREIGN KEY `account_viplist_ibfk_1`") + db.query("UPDATE `account_viplist` SET `player_id` = (SELECT `account_id` FROM `players` WHERE `id` = `player_id`)") + db.query("ALTER TABLE `account_viplist` CHANGE `player_id` `account_id` INT( 11 ) NOT NULL COMMENT 'id of account whose viplist entry it is'") + db.query("ALTER TABLE `account_viplist` DROP FOREIGN KEY `account_viplist_ibfk_2`") + db.query("ALTER TABLE `account_viplist` CHANGE `vip_id` `player_id` INT( 11 ) NOT NULL COMMENT 'id of target player of viplist entry'") + db.query("ALTER TABLE `account_viplist` DROP INDEX `player_id`, ADD INDEX `account_id` (`account_id`)") + db.query("ALTER TABLE `account_viplist` DROP INDEX `vip_id`, ADD INDEX `player_id` (`player_id`)") + db.query("ALTER TABLE `account_viplist` ADD FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE") + db.query("ALTER TABLE `account_viplist` ADD FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ON DELETE CASCADE") + db.query("ALTER TABLE `account_viplist` ADD `description` VARCHAR(128) NOT NULL DEFAULT '', ADD `icon` TINYINT( 2 ) UNSIGNED NOT NULL DEFAULT '0', ADD `notify` TINYINT( 1 ) NOT NULL DEFAULT '0'") + + -- Remove duplicates + local resultId = db.storeQuery("SELECT `account_id`, `player_id`, COUNT(*) AS `count` FROM `account_viplist` GROUP BY `account_id`, `player_id` HAVING COUNT(*) > 1") + if resultId ~= false then + repeat + db.query("DELETE FROM `account_viplist` WHERE `account_id` = " .. result.getNumber(resultId, "account_id") .. " AND `player_id` = " .. result.getNumber(resultId, "player_id") .. " LIMIT " .. (result.getNumber(resultId, "count") - 1)) + until not result.next(resultId) + result.free(resultId) + end + + -- Remove if an account has over 200 entries + resultId = db.storeQuery("SELECT `account_id`, COUNT(*) AS `count` FROM `account_viplist` GROUP BY `account_id` HAVING COUNT(*) > 200") + if resultId ~= false then + repeat + db.query("DELETE FROM `account_viplist` WHERE `account_id` = " .. result.getNumber(resultId, "account_id") .. " LIMIT " .. (result.getNumber(resultId, "count") - 200)) + until not result.next(resultId) + result.free(resultId) + end + + db.query("ALTER TABLE `account_viplist` ADD UNIQUE `account_player_index` (`account_id`, `player_id`)") + return true +end diff --git a/data/migrations/8.lua b/data/migrations/8.lua new file mode 100644 index 0000000..10aa660 --- /dev/null +++ b/data/migrations/8.lua @@ -0,0 +1,70 @@ +function onUpdateDatabase() + print("> Updating database to version 9 (global inbox)") + db.query("CREATE TABLE IF NOT EXISTS `player_inboxitems` (`player_id` int(11) NOT NULL, `sid` int(11) NOT NULL, `pid` int(11) NOT NULL DEFAULT '0', `itemtype` smallint(6) NOT NULL, `count` smallint(5) NOT NULL DEFAULT '0', `attributes` blob NOT NULL, UNIQUE KEY `player_id_2` (`player_id`,`sid`), KEY `player_id` (`player_id`), FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=latin1") + + -- Delete "market" item + db.query("DELETE FROM `player_depotitems` WHERE `itemtype` = 14405") + + -- Move up items in depot chests + local resultId = db.storeQuery("SELECT `player_id`, `pid`, (SELECT `dp2`.`sid` FROM `player_depotitems` AS `dp2` WHERE `dp2`.`player_id` = `dp1`.`player_id` AND `dp2`.`pid` = `dp1`.`sid` AND `itemtype` = 2594) AS `sid` FROM `player_depotitems` AS `dp1` WHERE `itemtype` = 2589") + if resultId ~= false then + repeat + db.query("UPDATE `player_depotitems` SET `pid` = " .. result.getNumber(resultId, "pid") .. " WHERE `player_id` = " .. result.getNumber(resultId, "player_id") .. " AND `pid` = " .. result.getNumber(resultId, "sid")) + until not result.next(resultId) + result.free(resultId) + end + + -- Delete the depot lockers + db.query("DELETE FROM `player_depotitems` WHERE `itemtype` = 2589") + + -- Delete the depot chests + db.query("DELETE FROM `player_depotitems` WHERE `itemtype` = 2594") + + resultId = db.storeQuery("SELECT DISTINCT `player_id` FROM `player_depotitems` WHERE `itemtype` = 14404") + if resultId ~= false then + repeat + local playerId = result.getNumber(resultId, "player_id") + + local runningId = 100 + + local stmt = "INSERT INTO `player_inboxitems` (`player_id`, `sid`, `pid`, `itemtype`, `count`, `attributes`) VALUES " + + local resultId2 = db.storeQuery("SELECT `sid` FROM `player_depotitems` WHERE `player_id` = " .. playerId .. " AND `itemtype` = 14404") + if resultId2 ~= false then + repeat + local sids = {} + sids[#sids + 1] = result.getNumber(resultId2, "sid") + while #sids > 0 do + local sid = sids[#sids] + sids[#sids] = nil + + local resultId3 = db.storeQuery("SELECT * FROM `player_depotitems` WHERE `player_id` = " .. playerId .. " AND `pid` = " .. sid) + if resultId3 ~= false then + repeat + local attr, attrSize = result.getStream(resultId3, "attributes") + runningId = runningId + 1 + stmt = stmt .. "(" .. playerId .. "," .. runningId .. ",0," .. result.getNumber(resultId3, "itemtype") .. "," .. result.getNumber(resultId3, "count") .. "," .. db.escapeBlob(attr, attrSize) .. ")," + sids[#sids + 1] = result.getNumber(resultId3, "sid") + + db.query("DELETE FROM `player_depotitems` WHERE `player_id` = " .. result.getNumber(resultId, "player_id") .. " AND `sid` = " .. result.getNumber(resultId3, "sid")) + until not result.next(resultId3) + result.free(resultId3) + end + end + until not result.next(resultId2) + result.free(resultId2) + end + + local stmtLen = string.len(stmt) + if stmtLen > 102 then + stmt = string.sub(stmt, 1, stmtLen - 1) + db.query(stmt) + end + until not result.next(resultId) + result.free(resultId) + end + + -- Delete the inboxes + db.query("DELETE FROM `player_depotitems` WHERE `itemtype` = 14404") + return true +end diff --git a/data/migrations/9.lua b/data/migrations/9.lua new file mode 100644 index 0000000..33022ee --- /dev/null +++ b/data/migrations/9.lua @@ -0,0 +1,5 @@ +function onUpdateDatabase() + print("> Updating database to version 10 (stamina)") + db.query("ALTER TABLE `players` ADD `stamina` SMALLINT UNSIGNED NOT NULL DEFAULT 2520") + return true +end diff --git a/data/monster/Amazons/amazon.xml b/data/monster/Amazons/amazon.xml new file mode 100644 index 0000000..02e0bc8 --- /dev/null +++ b/data/monster/Amazons/amazon.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Amazons/valkyrie.xml b/data/monster/Amazons/valkyrie.xml new file mode 100644 index 0000000..5627425 --- /dev/null +++ b/data/monster/Amazons/valkyrie.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Annelids/carrion_worm.xml b/data/monster/Annelids/carrion_worm.xml new file mode 100644 index 0000000..17e0405 --- /dev/null +++ b/data/monster/Annelids/carrion_worm.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Annelids/drillworm.xml b/data/monster/Annelids/drillworm.xml new file mode 100644 index 0000000..a182938 --- /dev/null +++ b/data/monster/Annelids/drillworm.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Annelids/rift_worm.xml b/data/monster/Annelids/rift_worm.xml new file mode 100644 index 0000000..57db1c8 --- /dev/null +++ b/data/monster/Annelids/rift_worm.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Annelids/rotworm.xml b/data/monster/Annelids/rotworm.xml new file mode 100644 index 0000000..6f203e0 --- /dev/null +++ b/data/monster/Annelids/rotworm.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Apes/kongra.xml b/data/monster/Apes/kongra.xml new file mode 100644 index 0000000..f82d85a --- /dev/null +++ b/data/monster/Apes/kongra.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Apes/merlkin.xml b/data/monster/Apes/merlkin.xml new file mode 100644 index 0000000..0a74035 --- /dev/null +++ b/data/monster/Apes/merlkin.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Apes/sibang.xml b/data/monster/Apes/sibang.xml new file mode 100644 index 0000000..a97d657 --- /dev/null +++ b/data/monster/Apes/sibang.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Apes/yeti.xml b/data/monster/Apes/yeti.xml new file mode 100644 index 0000000..ef5f4d9 --- /dev/null +++ b/data/monster/Apes/yeti.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arachnids/crystal_spider.xml b/data/monster/Arachnids/crystal_spider.xml new file mode 100644 index 0000000..29487b6 --- /dev/null +++ b/data/monster/Arachnids/crystal_spider.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arachnids/giant_spider.xml b/data/monster/Arachnids/giant_spider.xml new file mode 100644 index 0000000..c462260 --- /dev/null +++ b/data/monster/Arachnids/giant_spider.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arachnids/giant_spider_wyda.xml b/data/monster/Arachnids/giant_spider_wyda.xml new file mode 100644 index 0000000..885ea57 --- /dev/null +++ b/data/monster/Arachnids/giant_spider_wyda.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arachnids/poison_spider.xml b/data/monster/Arachnids/poison_spider.xml new file mode 100644 index 0000000..0a98c8e --- /dev/null +++ b/data/monster/Arachnids/poison_spider.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arachnids/sacred_spider.xml b/data/monster/Arachnids/sacred_spider.xml new file mode 100644 index 0000000..8e13b02 --- /dev/null +++ b/data/monster/Arachnids/sacred_spider.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arachnids/sandstone_scorpion.xml b/data/monster/Arachnids/sandstone_scorpion.xml new file mode 100644 index 0000000..7870166 --- /dev/null +++ b/data/monster/Arachnids/sandstone_scorpion.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arachnids/scorpion.xml b/data/monster/Arachnids/scorpion.xml new file mode 100644 index 0000000..040e916 --- /dev/null +++ b/data/monster/Arachnids/scorpion.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arachnids/spider.xml b/data/monster/Arachnids/spider.xml new file mode 100644 index 0000000..c94f75a --- /dev/null +++ b/data/monster/Arachnids/spider.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arachnids/tarantula.xml b/data/monster/Arachnids/tarantula.xml new file mode 100644 index 0000000..dbe2e3b --- /dev/null +++ b/data/monster/Arachnids/tarantula.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arachnids/wailing_widow.xml b/data/monster/Arachnids/wailing_widow.xml new file mode 100644 index 0000000..7a1c131 --- /dev/null +++ b/data/monster/Arachnids/wailing_widow.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/greenhorn/achad.xml b/data/monster/Arena/greenhorn/achad.xml new file mode 100644 index 0000000..efa2de4 --- /dev/null +++ b/data/monster/Arena/greenhorn/achad.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/greenhorn/axeitus_headbanger.xml b/data/monster/Arena/greenhorn/axeitus_headbanger.xml new file mode 100644 index 0000000..a339639 --- /dev/null +++ b/data/monster/Arena/greenhorn/axeitus_headbanger.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/greenhorn/bloodpaw.xml b/data/monster/Arena/greenhorn/bloodpaw.xml new file mode 100644 index 0000000..c878fcf --- /dev/null +++ b/data/monster/Arena/greenhorn/bloodpaw.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/greenhorn/bovinus.xml b/data/monster/Arena/greenhorn/bovinus.xml new file mode 100644 index 0000000..d15770f --- /dev/null +++ b/data/monster/Arena/greenhorn/bovinus.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/greenhorn/colerian_the_barbarian.xml b/data/monster/Arena/greenhorn/colerian_the_barbarian.xml new file mode 100644 index 0000000..e576657 --- /dev/null +++ b/data/monster/Arena/greenhorn/colerian_the_barbarian.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/greenhorn/cursed_gladiator.xml b/data/monster/Arena/greenhorn/cursed_gladiator.xml new file mode 100644 index 0000000..ab438e6 --- /dev/null +++ b/data/monster/Arena/greenhorn/cursed_gladiator.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/greenhorn/frostfur.xml b/data/monster/Arena/greenhorn/frostfur.xml new file mode 100644 index 0000000..b33154e --- /dev/null +++ b/data/monster/Arena/greenhorn/frostfur.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/greenhorn/orcus_the_cruel.xml b/data/monster/Arena/greenhorn/orcus_the_cruel.xml new file mode 100644 index 0000000..bbf790c --- /dev/null +++ b/data/monster/Arena/greenhorn/orcus_the_cruel.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/greenhorn/rocky.xml b/data/monster/Arena/greenhorn/rocky.xml new file mode 100644 index 0000000..9a4e75b --- /dev/null +++ b/data/monster/Arena/greenhorn/rocky.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/greenhorn/the_hairy_one.xml b/data/monster/Arena/greenhorn/the_hairy_one.xml new file mode 100644 index 0000000..ab05686 --- /dev/null +++ b/data/monster/Arena/greenhorn/the_hairy_one.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/scrapper/avalanche.xml b/data/monster/Arena/scrapper/avalanche.xml new file mode 100644 index 0000000..ce0f3b0 --- /dev/null +++ b/data/monster/Arena/scrapper/avalanche.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/scrapper/drasilla.xml b/data/monster/Arena/scrapper/drasilla.xml new file mode 100644 index 0000000..ec1798e --- /dev/null +++ b/data/monster/Arena/scrapper/drasilla.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/scrapper/grimgor_guteater.xml b/data/monster/Arena/scrapper/grimgor_guteater.xml new file mode 100644 index 0000000..e48100b --- /dev/null +++ b/data/monster/Arena/scrapper/grimgor_guteater.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/scrapper/kreebosh_the_exile.xml b/data/monster/Arena/scrapper/kreebosh_the_exile.xml new file mode 100644 index 0000000..0f27f83 --- /dev/null +++ b/data/monster/Arena/scrapper/kreebosh_the_exile.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/scrapper/slim.xml b/data/monster/Arena/scrapper/slim.xml new file mode 100644 index 0000000..3cf480e --- /dev/null +++ b/data/monster/Arena/scrapper/slim.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/scrapper/spirit_of_earth.xml b/data/monster/Arena/scrapper/spirit_of_earth.xml new file mode 100644 index 0000000..2eefb1d --- /dev/null +++ b/data/monster/Arena/scrapper/spirit_of_earth.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/scrapper/spirit_of_fire.xml b/data/monster/Arena/scrapper/spirit_of_fire.xml new file mode 100644 index 0000000..c7751b8 --- /dev/null +++ b/data/monster/Arena/scrapper/spirit_of_fire.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/scrapper/spirit_of_water.xml b/data/monster/Arena/scrapper/spirit_of_water.xml new file mode 100644 index 0000000..1ad640b --- /dev/null +++ b/data/monster/Arena/scrapper/spirit_of_water.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/scrapper/the_dark_dancer.xml b/data/monster/Arena/scrapper/the_dark_dancer.xml new file mode 100644 index 0000000..2519497 --- /dev/null +++ b/data/monster/Arena/scrapper/the_dark_dancer.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/scrapper/the_hag.xml b/data/monster/Arena/scrapper/the_hag.xml new file mode 100644 index 0000000..3c56802 --- /dev/null +++ b/data/monster/Arena/scrapper/the_hag.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/warlord/darakan_the_executioner.xml b/data/monster/Arena/warlord/darakan_the_executioner.xml new file mode 100644 index 0000000..dc04c00 --- /dev/null +++ b/data/monster/Arena/warlord/darakan_the_executioner.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/warlord/deathbringer.xml b/data/monster/Arena/warlord/deathbringer.xml new file mode 100644 index 0000000..1bcaa2f --- /dev/null +++ b/data/monster/Arena/warlord/deathbringer.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/warlord/fallen_mooh'tah_master_ghar.xml b/data/monster/Arena/warlord/fallen_mooh'tah_master_ghar.xml new file mode 100644 index 0000000..2be7e09 --- /dev/null +++ b/data/monster/Arena/warlord/fallen_mooh'tah_master_ghar.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/warlord/gnorre_chyllson.xml b/data/monster/Arena/warlord/gnorre_chyllson.xml new file mode 100644 index 0000000..71a8fc2 --- /dev/null +++ b/data/monster/Arena/warlord/gnorre_chyllson.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/warlord/norgle_glacierbeard.xml b/data/monster/Arena/warlord/norgle_glacierbeard.xml new file mode 100644 index 0000000..528d848 --- /dev/null +++ b/data/monster/Arena/warlord/norgle_glacierbeard.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/warlord/svoren_the_mad.xml b/data/monster/Arena/warlord/svoren_the_mad.xml new file mode 100644 index 0000000..4aceed2 --- /dev/null +++ b/data/monster/Arena/warlord/svoren_the_mad.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/warlord/the_masked_marauder.xml b/data/monster/Arena/warlord/the_masked_marauder.xml new file mode 100644 index 0000000..e29c894 --- /dev/null +++ b/data/monster/Arena/warlord/the_masked_marauder.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/warlord/the_obliverator.xml b/data/monster/Arena/warlord/the_obliverator.xml new file mode 100644 index 0000000..ad0d507 --- /dev/null +++ b/data/monster/Arena/warlord/the_obliverator.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/warlord/the_pit_lord.xml b/data/monster/Arena/warlord/the_pit_lord.xml new file mode 100644 index 0000000..fb746a3 --- /dev/null +++ b/data/monster/Arena/warlord/the_pit_lord.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Arena/warlord/webster.xml b/data/monster/Arena/warlord/webster.xml new file mode 100644 index 0000000..fde9125 --- /dev/null +++ b/data/monster/Arena/warlord/webster.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Barbarians/barbarian_bloodwalker.xml b/data/monster/Barbarians/barbarian_bloodwalker.xml new file mode 100644 index 0000000..547195e --- /dev/null +++ b/data/monster/Barbarians/barbarian_bloodwalker.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Barbarians/barbarian_brutetamer.xml b/data/monster/Barbarians/barbarian_brutetamer.xml new file mode 100644 index 0000000..f2a7ce9 --- /dev/null +++ b/data/monster/Barbarians/barbarian_brutetamer.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Barbarians/barbarian_headsplitter.xml b/data/monster/Barbarians/barbarian_headsplitter.xml new file mode 100644 index 0000000..fe6d581 --- /dev/null +++ b/data/monster/Barbarians/barbarian_headsplitter.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Barbarians/barbarian_skullhunter.xml b/data/monster/Barbarians/barbarian_skullhunter.xml new file mode 100644 index 0000000..63c2f8c --- /dev/null +++ b/data/monster/Barbarians/barbarian_skullhunter.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bats/bat.xml b/data/monster/Bats/bat.xml new file mode 100644 index 0000000..81482a7 --- /dev/null +++ b/data/monster/Bats/bat.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bats/mutated_bat.xml b/data/monster/Bats/mutated_bat.xml new file mode 100644 index 0000000..b6cef40 --- /dev/null +++ b/data/monster/Bats/mutated_bat.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bats/nightfiend.xml b/data/monster/Bats/nightfiend.xml new file mode 100644 index 0000000..536b181 --- /dev/null +++ b/data/monster/Bats/nightfiend.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bats/vicious_manbat.xml b/data/monster/Bats/vicious_manbat.xml new file mode 100644 index 0000000..e3b49ec --- /dev/null +++ b/data/monster/Bats/vicious_manbat.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bears/bear.xml b/data/monster/Bears/bear.xml new file mode 100644 index 0000000..d2582a4 --- /dev/null +++ b/data/monster/Bears/bear.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bears/panda.xml b/data/monster/Bears/panda.xml new file mode 100644 index 0000000..288c20b --- /dev/null +++ b/data/monster/Bears/panda.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bears/polar_bear.xml b/data/monster/Bears/polar_bear.xml new file mode 100644 index 0000000..0d5c8e2 --- /dev/null +++ b/data/monster/Bears/polar_bear.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bears/undead_cavebear.xml b/data/monster/Bears/undead_cavebear.xml new file mode 100644 index 0000000..339e2ee --- /dev/null +++ b/data/monster/Bears/undead_cavebear.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/bog_raider.xml b/data/monster/Bio-Elementals/bog_raider.xml new file mode 100644 index 0000000..b0f1334 --- /dev/null +++ b/data/monster/Bio-Elementals/bog_raider.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/carniphila.xml b/data/monster/Bio-Elementals/carniphila.xml new file mode 100644 index 0000000..ae97a19 --- /dev/null +++ b/data/monster/Bio-Elementals/carniphila.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/defiler.xml b/data/monster/Bio-Elementals/defiler.xml new file mode 100644 index 0000000..8365cf8 --- /dev/null +++ b/data/monster/Bio-Elementals/defiler.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/haunted_treeling.xml b/data/monster/Bio-Elementals/haunted_treeling.xml new file mode 100644 index 0000000..66b175c --- /dev/null +++ b/data/monster/Bio-Elementals/haunted_treeling.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/hideous_fungus.xml b/data/monster/Bio-Elementals/hideous_fungus.xml new file mode 100644 index 0000000..2c66f9f --- /dev/null +++ b/data/monster/Bio-Elementals/hideous_fungus.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/humongous_fungus.xml b/data/monster/Bio-Elementals/humongous_fungus.xml new file mode 100644 index 0000000..4a91e14 --- /dev/null +++ b/data/monster/Bio-Elementals/humongous_fungus.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/humorless_fungus.xml b/data/monster/Bio-Elementals/humorless_fungus.xml new file mode 100644 index 0000000..5d3223b --- /dev/null +++ b/data/monster/Bio-Elementals/humorless_fungus.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/leaf_golem.xml b/data/monster/Bio-Elementals/leaf_golem.xml new file mode 100644 index 0000000..38576b3 --- /dev/null +++ b/data/monster/Bio-Elementals/leaf_golem.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/mechanical_fighter.xml b/data/monster/Bio-Elementals/mechanical_fighter.xml new file mode 100644 index 0000000..d416329 --- /dev/null +++ b/data/monster/Bio-Elementals/mechanical_fighter.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/slime.xml b/data/monster/Bio-Elementals/slime.xml new file mode 100644 index 0000000..99ebef3 --- /dev/null +++ b/data/monster/Bio-Elementals/slime.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/son_of_verminor.xml b/data/monster/Bio-Elementals/son_of_verminor.xml new file mode 100644 index 0000000..738e4a6 --- /dev/null +++ b/data/monster/Bio-Elementals/son_of_verminor.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/spit_nettle.xml b/data/monster/Bio-Elementals/spit_nettle.xml new file mode 100644 index 0000000..13b2bd0 --- /dev/null +++ b/data/monster/Bio-Elementals/spit_nettle.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/squidgy_slime.xml b/data/monster/Bio-Elementals/squidgy_slime.xml new file mode 100644 index 0000000..eef8c48 --- /dev/null +++ b/data/monster/Bio-Elementals/squidgy_slime.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/strange_slime.xml b/data/monster/Bio-Elementals/strange_slime.xml new file mode 100644 index 0000000..af68f11 --- /dev/null +++ b/data/monster/Bio-Elementals/strange_slime.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/swampling.xml b/data/monster/Bio-Elementals/swampling.xml new file mode 100644 index 0000000..b2bb25e --- /dev/null +++ b/data/monster/Bio-Elementals/swampling.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bio-Elementals/wilting_leaf_golem.xml b/data/monster/Bio-Elementals/wilting_leaf_golem.xml new file mode 100644 index 0000000..aac142a --- /dev/null +++ b/data/monster/Bio-Elementals/wilting_leaf_golem.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Birds/berserker_chicken.xml b/data/monster/Birds/berserker_chicken.xml new file mode 100644 index 0000000..b8c4192 --- /dev/null +++ b/data/monster/Birds/berserker_chicken.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Birds/chicken.xml b/data/monster/Birds/chicken.xml new file mode 100644 index 0000000..cfb8e4f --- /dev/null +++ b/data/monster/Birds/chicken.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Birds/demon_parrot.xml b/data/monster/Birds/demon_parrot.xml new file mode 100644 index 0000000..15267cf --- /dev/null +++ b/data/monster/Birds/demon_parrot.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Birds/dire_penguin.xml b/data/monster/Birds/dire_penguin.xml new file mode 100644 index 0000000..70332c6 --- /dev/null +++ b/data/monster/Birds/dire_penguin.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Birds/flamingo.xml b/data/monster/Birds/flamingo.xml new file mode 100644 index 0000000..3814654 --- /dev/null +++ b/data/monster/Birds/flamingo.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Birds/marsh_stalker.xml b/data/monster/Birds/marsh_stalker.xml new file mode 100644 index 0000000..6d7615f --- /dev/null +++ b/data/monster/Birds/marsh_stalker.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Birds/parrot.xml b/data/monster/Birds/parrot.xml new file mode 100644 index 0000000..e970deb --- /dev/null +++ b/data/monster/Birds/parrot.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Birds/penguin.xml b/data/monster/Birds/penguin.xml new file mode 100644 index 0000000..39f5a23 --- /dev/null +++ b/data/monster/Birds/penguin.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Birds/pigeon.xml b/data/monster/Birds/pigeon.xml new file mode 100644 index 0000000..9776606 --- /dev/null +++ b/data/monster/Birds/pigeon.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Birds/seagull.xml b/data/monster/Birds/seagull.xml new file mode 100644 index 0000000..5b57124 --- /dev/null +++ b/data/monster/Birds/seagull.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Birds/terror_bird.xml b/data/monster/Birds/terror_bird.xml new file mode 100644 index 0000000..3826686 --- /dev/null +++ b/data/monster/Birds/terror_bird.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Blobs/acid_blob.xml b/data/monster/Blobs/acid_blob.xml new file mode 100644 index 0000000..7d43935 --- /dev/null +++ b/data/monster/Blobs/acid_blob.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Blobs/death_blob.xml b/data/monster/Blobs/death_blob.xml new file mode 100644 index 0000000..2cc5a68 --- /dev/null +++ b/data/monster/Blobs/death_blob.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Blobs/floor_blob.xml b/data/monster/Blobs/floor_blob.xml new file mode 100644 index 0000000..2f30d7a --- /dev/null +++ b/data/monster/Blobs/floor_blob.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Blobs/mercury_blob.xml b/data/monster/Blobs/mercury_blob.xml new file mode 100644 index 0000000..70ace57 --- /dev/null +++ b/data/monster/Blobs/mercury_blob.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bonelords/bonelord.xml b/data/monster/Bonelords/bonelord.xml new file mode 100644 index 0000000..26d0824 --- /dev/null +++ b/data/monster/Bonelords/bonelord.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bonelords/braindeath.xml b/data/monster/Bonelords/braindeath.xml new file mode 100644 index 0000000..1e20af7 --- /dev/null +++ b/data/monster/Bonelords/braindeath.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bonelords/elder_bonelord.xml b/data/monster/Bonelords/elder_bonelord.xml new file mode 100644 index 0000000..6c24dd1 --- /dev/null +++ b/data/monster/Bonelords/elder_bonelord.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bonelords/eye_of_the_seven.xml b/data/monster/Bonelords/eye_of_the_seven.xml new file mode 100644 index 0000000..78da72c --- /dev/null +++ b/data/monster/Bonelords/eye_of_the_seven.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bonelords/gazer.xml b/data/monster/Bonelords/gazer.xml new file mode 100644 index 0000000..f1f675f --- /dev/null +++ b/data/monster/Bonelords/gazer.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/abyssador.xml b/data/monster/Bosses/abyssador.xml new file mode 100644 index 0000000..6e04ab2 --- /dev/null +++ b/data/monster/Bosses/abyssador.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/annihilon.xml b/data/monster/Bosses/annihilon.xml new file mode 100644 index 0000000..c2e3ff0 --- /dev/null +++ b/data/monster/Bosses/annihilon.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/apocalypse.xml b/data/monster/Bosses/apocalypse.xml new file mode 100644 index 0000000..cd204b6 --- /dev/null +++ b/data/monster/Bosses/apocalypse.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/apprentice_sheng.xml b/data/monster/Bosses/apprentice_sheng.xml new file mode 100644 index 0000000..b47c138 --- /dev/null +++ b/data/monster/Bosses/apprentice_sheng.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/arachir_the_ancient_one.xml b/data/monster/Bosses/arachir_the_ancient_one.xml new file mode 100644 index 0000000..e3e138b --- /dev/null +++ b/data/monster/Bosses/arachir_the_ancient_one.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/armenius.xml b/data/monster/Bosses/armenius.xml new file mode 100644 index 0000000..3e1b768 --- /dev/null +++ b/data/monster/Bosses/armenius.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/azerus.xml b/data/monster/Bosses/azerus.xml new file mode 100644 index 0000000..6d27784 --- /dev/null +++ b/data/monster/Bosses/azerus.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/barbaria.xml b/data/monster/Bosses/barbaria.xml new file mode 100644 index 0000000..e08c684 --- /dev/null +++ b/data/monster/Bosses/barbaria.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/baron_brute.xml b/data/monster/Bosses/baron_brute.xml new file mode 100644 index 0000000..f3cbfd7 --- /dev/null +++ b/data/monster/Bosses/baron_brute.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/battlemaster_zunzu.xml b/data/monster/Bosses/battlemaster_zunzu.xml new file mode 100644 index 0000000..a44fbd0 --- /dev/null +++ b/data/monster/Bosses/battlemaster_zunzu.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/bazir.xml b/data/monster/Bosses/bazir.xml new file mode 100644 index 0000000..5cbbce0 --- /dev/null +++ b/data/monster/Bosses/bazir.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/big_boss_trolliver.xml b/data/monster/Bosses/big_boss_trolliver.xml new file mode 100644 index 0000000..4e27071 --- /dev/null +++ b/data/monster/Bosses/big_boss_trolliver.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/bones.xml b/data/monster/Bosses/bones.xml new file mode 100644 index 0000000..0643202 --- /dev/null +++ b/data/monster/Bosses/bones.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/boogey.xml b/data/monster/Bosses/boogey.xml new file mode 100644 index 0000000..93c5f56 --- /dev/null +++ b/data/monster/Bosses/boogey.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/brutus_bloodbeard.xml b/data/monster/Bosses/brutus_bloodbeard.xml new file mode 100644 index 0000000..8c13d76 --- /dev/null +++ b/data/monster/Bosses/brutus_bloodbeard.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/chizzoron_the_distorter.xml b/data/monster/Bosses/chizzoron_the_distorter.xml new file mode 100644 index 0000000..339bf6b --- /dev/null +++ b/data/monster/Bosses/chizzoron_the_distorter.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/coldheart.xml b/data/monster/Bosses/coldheart.xml new file mode 100644 index 0000000..627e411 --- /dev/null +++ b/data/monster/Bosses/coldheart.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/countess_sorrow.xml b/data/monster/Bosses/countess_sorrow.xml new file mode 100644 index 0000000..48b366b --- /dev/null +++ b/data/monster/Bosses/countess_sorrow.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/deadeye_devious.xml b/data/monster/Bosses/deadeye_devious.xml new file mode 100644 index 0000000..6bd4642 --- /dev/null +++ b/data/monster/Bosses/deadeye_devious.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/deathbine.xml b/data/monster/Bosses/deathbine.xml new file mode 100644 index 0000000..66f6cb7 --- /dev/null +++ b/data/monster/Bosses/deathbine.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/deathstrike.xml b/data/monster/Bosses/deathstrike.xml new file mode 100644 index 0000000..3789cc9 --- /dev/null +++ b/data/monster/Bosses/deathstrike.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/demodras.xml b/data/monster/Bosses/demodras.xml new file mode 100644 index 0000000..f038880 --- /dev/null +++ b/data/monster/Bosses/demodras.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/dharalion.xml b/data/monster/Bosses/dharalion.xml new file mode 100644 index 0000000..1b5ffcb --- /dev/null +++ b/data/monster/Bosses/dharalion.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/diblis_the_fair.xml b/data/monster/Bosses/diblis_the_fair.xml new file mode 100644 index 0000000..4598f2a --- /dev/null +++ b/data/monster/Bosses/diblis_the_fair.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/dirtbeard.xml b/data/monster/Bosses/dirtbeard.xml new file mode 100644 index 0000000..f74646d --- /dev/null +++ b/data/monster/Bosses/dirtbeard.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/diseased_bill.xml b/data/monster/Bosses/diseased_bill.xml new file mode 100644 index 0000000..32cce11 --- /dev/null +++ b/data/monster/Bosses/diseased_bill.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/diseased_dan.xml b/data/monster/Bosses/diseased_dan.xml new file mode 100644 index 0000000..b32dffd --- /dev/null +++ b/data/monster/Bosses/diseased_dan.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/diseased_fred.xml b/data/monster/Bosses/diseased_fred.xml new file mode 100644 index 0000000..02efea7 --- /dev/null +++ b/data/monster/Bosses/diseased_fred.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/doomhowl.xml b/data/monster/Bosses/doomhowl.xml new file mode 100644 index 0000000..a202758 --- /dev/null +++ b/data/monster/Bosses/doomhowl.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/dracola.xml b/data/monster/Bosses/dracola.xml new file mode 100644 index 0000000..6be3658 --- /dev/null +++ b/data/monster/Bosses/dracola.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/dreadwing.xml b/data/monster/Bosses/dreadwing.xml new file mode 100644 index 0000000..0ae8fe2 --- /dev/null +++ b/data/monster/Bosses/dreadwing.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/energized_raging_mage.xml b/data/monster/Bosses/energized_raging_mage.xml new file mode 100644 index 0000000..f188e70 --- /dev/null +++ b/data/monster/Bosses/energized_raging_mage.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/esmeralda.xml b/data/monster/Bosses/esmeralda.xml new file mode 100644 index 0000000..d33015a --- /dev/null +++ b/data/monster/Bosses/esmeralda.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/evil_mastermind.xml b/data/monster/Bosses/evil_mastermind.xml new file mode 100644 index 0000000..93195bf --- /dev/null +++ b/data/monster/Bosses/evil_mastermind.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/fatality.xml b/data/monster/Bosses/fatality.xml new file mode 100644 index 0000000..d18c007 --- /dev/null +++ b/data/monster/Bosses/fatality.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/fernfang.xml b/data/monster/Bosses/fernfang.xml new file mode 100644 index 0000000..caa5d8d --- /dev/null +++ b/data/monster/Bosses/fernfang.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/ferumbras.xml b/data/monster/Bosses/ferumbras.xml new file mode 100644 index 0000000..9b61239 --- /dev/null +++ b/data/monster/Bosses/ferumbras.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/fleshslicer.xml b/data/monster/Bosses/fleshslicer.xml new file mode 100644 index 0000000..fb28a99 --- /dev/null +++ b/data/monster/Bosses/fleshslicer.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/fluffy.xml b/data/monster/Bosses/fluffy.xml new file mode 100644 index 0000000..a2c3492 --- /dev/null +++ b/data/monster/Bosses/fluffy.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/foreman_kneebiter.xml b/data/monster/Bosses/foreman_kneebiter.xml new file mode 100644 index 0000000..194c95d --- /dev/null +++ b/data/monster/Bosses/foreman_kneebiter.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/freegoiz.xml b/data/monster/Bosses/freegoiz.xml new file mode 100644 index 0000000..d5678cd --- /dev/null +++ b/data/monster/Bosses/freegoiz.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/fury_of_the_emperor.xml b/data/monster/Bosses/fury_of_the_emperor.xml new file mode 100644 index 0000000..c8c681f --- /dev/null +++ b/data/monster/Bosses/fury_of_the_emperor.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/gaz'haragoth.xml b/data/monster/Bosses/gaz'haragoth.xml new file mode 100644 index 0000000..01c0330 --- /dev/null +++ b/data/monster/Bosses/gaz'haragoth.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/general_murius.xml b/data/monster/Bosses/general_murius.xml new file mode 100644 index 0000000..f26305a --- /dev/null +++ b/data/monster/Bosses/general_murius.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/ghazbaran.xml b/data/monster/Bosses/ghazbaran.xml new file mode 100644 index 0000000..8851195 --- /dev/null +++ b/data/monster/Bosses/ghazbaran.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/glitterscale.xml b/data/monster/Bosses/glitterscale.xml new file mode 100644 index 0000000..b2aee45 --- /dev/null +++ b/data/monster/Bosses/glitterscale.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/gnomevil.xml b/data/monster/Bosses/gnomevil.xml new file mode 100644 index 0000000..90868b4 --- /dev/null +++ b/data/monster/Bosses/gnomevil.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/golgordan.xml b/data/monster/Bosses/golgordan.xml new file mode 100644 index 0000000..ba7802e --- /dev/null +++ b/data/monster/Bosses/golgordan.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/grand_mother_foulscale.xml b/data/monster/Bosses/grand_mother_foulscale.xml new file mode 100644 index 0000000..d2d24e8 --- /dev/null +++ b/data/monster/Bosses/grand_mother_foulscale.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/groam.xml b/data/monster/Bosses/groam.xml new file mode 100644 index 0000000..8eea57f --- /dev/null +++ b/data/monster/Bosses/groam.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/grorlam.xml b/data/monster/Bosses/grorlam.xml new file mode 100644 index 0000000..afa2b65 --- /dev/null +++ b/data/monster/Bosses/grorlam.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/hairman_the_huge.xml b/data/monster/Bosses/hairman_the_huge.xml new file mode 100644 index 0000000..30ebbda --- /dev/null +++ b/data/monster/Bosses/hairman_the_huge.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/haunter.xml b/data/monster/Bosses/haunter.xml new file mode 100644 index 0000000..99cd722 --- /dev/null +++ b/data/monster/Bosses/haunter.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/hellgorak.xml b/data/monster/Bosses/hellgorak.xml new file mode 100644 index 0000000..8303283 --- /dev/null +++ b/data/monster/Bosses/hellgorak.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/heoni.xml b/data/monster/Bosses/heoni.xml new file mode 100644 index 0000000..f7dd34f --- /dev/null +++ b/data/monster/Bosses/heoni.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/hide.xml b/data/monster/Bosses/hide.xml new file mode 100644 index 0000000..0f46c5f --- /dev/null +++ b/data/monster/Bosses/hide.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/horestis.xml b/data/monster/Bosses/horestis.xml new file mode 100644 index 0000000..7f57fbb --- /dev/null +++ b/data/monster/Bosses/horestis.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/incineron.xml b/data/monster/Bosses/incineron.xml new file mode 100644 index 0000000..c57a1d6 --- /dev/null +++ b/data/monster/Bosses/incineron.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/infernatil.xml b/data/monster/Bosses/infernatil.xml new file mode 100644 index 0000000..6677a48 --- /dev/null +++ b/data/monster/Bosses/infernatil.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/inky.xml b/data/monster/Bosses/inky.xml new file mode 100644 index 0000000..f3b6501 --- /dev/null +++ b/data/monster/Bosses/inky.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/jaul.xml b/data/monster/Bosses/jaul.xml new file mode 100644 index 0000000..7c2a2fb --- /dev/null +++ b/data/monster/Bosses/jaul.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/koshei_the_deathless.xml b/data/monster/Bosses/koshei_the_deathless.xml new file mode 100644 index 0000000..433f8d7 --- /dev/null +++ b/data/monster/Bosses/koshei_the_deathless.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/kraknaknork's_demon.xml b/data/monster/Bosses/kraknaknork's_demon.xml new file mode 100644 index 0000000..9b1d6cf --- /dev/null +++ b/data/monster/Bosses/kraknaknork's_demon.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/kraknaknork.xml b/data/monster/Bosses/kraknaknork.xml new file mode 100644 index 0000000..aa9a130 --- /dev/null +++ b/data/monster/Bosses/kraknaknork.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/latrivan.xml b/data/monster/Bosses/latrivan.xml new file mode 100644 index 0000000..1dda187 --- /dev/null +++ b/data/monster/Bosses/latrivan.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/lethal_lissy.xml b/data/monster/Bosses/lethal_lissy.xml new file mode 100644 index 0000000..479ac01 --- /dev/null +++ b/data/monster/Bosses/lethal_lissy.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/leviathan.xml b/data/monster/Bosses/leviathan.xml new file mode 100644 index 0000000..15ac50e --- /dev/null +++ b/data/monster/Bosses/leviathan.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/lizard_abomination.xml b/data/monster/Bosses/lizard_abomination.xml new file mode 100644 index 0000000..7d21f2a --- /dev/null +++ b/data/monster/Bosses/lizard_abomination.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/lord_of_the_elements.xml b/data/monster/Bosses/lord_of_the_elements.xml new file mode 100644 index 0000000..c966867 --- /dev/null +++ b/data/monster/Bosses/lord_of_the_elements.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/mad_mage.xml b/data/monster/Bosses/mad_mage.xml new file mode 100644 index 0000000..4de508c --- /dev/null +++ b/data/monster/Bosses/mad_mage.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/mad_technomancer.xml b/data/monster/Bosses/mad_technomancer.xml new file mode 100644 index 0000000..696881c --- /dev/null +++ b/data/monster/Bosses/mad_technomancer.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/madareth.xml b/data/monster/Bosses/madareth.xml new file mode 100644 index 0000000..fb6f059 --- /dev/null +++ b/data/monster/Bosses/madareth.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/man_in_the_cave.xml b/data/monster/Bosses/man_in_the_cave.xml new file mode 100644 index 0000000..8d8bd03 --- /dev/null +++ b/data/monster/Bosses/man_in_the_cave.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/massacre.xml b/data/monster/Bosses/massacre.xml new file mode 100644 index 0000000..218d3dd --- /dev/null +++ b/data/monster/Bosses/massacre.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/mawhawk.xml b/data/monster/Bosses/mawhawk.xml new file mode 100644 index 0000000..4bef5ff --- /dev/null +++ b/data/monster/Bosses/mawhawk.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/menace.xml b/data/monster/Bosses/menace.xml new file mode 100644 index 0000000..76470e6 --- /dev/null +++ b/data/monster/Bosses/menace.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/mephiles.xml b/data/monster/Bosses/mephiles.xml new file mode 100644 index 0000000..dca2375 --- /dev/null +++ b/data/monster/Bosses/mephiles.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/minishabaal.xml b/data/monster/Bosses/minishabaal.xml new file mode 100644 index 0000000..3b202b0 --- /dev/null +++ b/data/monster/Bosses/minishabaal.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/monstor.xml b/data/monster/Bosses/monstor.xml new file mode 100644 index 0000000..10510d0 --- /dev/null +++ b/data/monster/Bosses/monstor.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/morgaroth.xml b/data/monster/Bosses/morgaroth.xml new file mode 100644 index 0000000..e7578ad --- /dev/null +++ b/data/monster/Bosses/morgaroth.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/morik_the_gladiator.xml b/data/monster/Bosses/morik_the_gladiator.xml new file mode 100644 index 0000000..c79039b --- /dev/null +++ b/data/monster/Bosses/morik_the_gladiator.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/mr._punish.xml b/data/monster/Bosses/mr._punish.xml new file mode 100644 index 0000000..9033eb6 --- /dev/null +++ b/data/monster/Bosses/mr._punish.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/munster.xml b/data/monster/Bosses/munster.xml new file mode 100644 index 0000000..fdd6923 --- /dev/null +++ b/data/monster/Bosses/munster.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/necropharus.xml b/data/monster/Bosses/necropharus.xml new file mode 100644 index 0000000..76ccc66 --- /dev/null +++ b/data/monster/Bosses/necropharus.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/obujos.xml b/data/monster/Bosses/obujos.xml new file mode 100644 index 0000000..1a497eb --- /dev/null +++ b/data/monster/Bosses/obujos.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/orshabaal.xml b/data/monster/Bosses/orshabaal.xml new file mode 100644 index 0000000..3ec81be --- /dev/null +++ b/data/monster/Bosses/orshabaal.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/pythius_the_rotten.xml b/data/monster/Bosses/pythius_the_rotten.xml new file mode 100644 index 0000000..be42718 --- /dev/null +++ b/data/monster/Bosses/pythius_the_rotten.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/raging_mage.xml b/data/monster/Bosses/raging_mage.xml new file mode 100644 index 0000000..1b298a8 --- /dev/null +++ b/data/monster/Bosses/raging_mage.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/rocko.xml b/data/monster/Bosses/rocko.xml new file mode 100644 index 0000000..b58fc06 --- /dev/null +++ b/data/monster/Bosses/rocko.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/ron_the_ripper.xml b/data/monster/Bosses/ron_the_ripper.xml new file mode 100644 index 0000000..dbd376a --- /dev/null +++ b/data/monster/Bosses/ron_the_ripper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/rottie_the_rotworm.xml b/data/monster/Bosses/rottie_the_rotworm.xml new file mode 100644 index 0000000..3938ba6 --- /dev/null +++ b/data/monster/Bosses/rottie_the_rotworm.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/rotworm_queen.xml b/data/monster/Bosses/rotworm_queen.xml new file mode 100644 index 0000000..be8e24f --- /dev/null +++ b/data/monster/Bosses/rotworm_queen.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/scorn_of_the_emperor.xml b/data/monster/Bosses/scorn_of_the_emperor.xml new file mode 100644 index 0000000..c7d0bf4 --- /dev/null +++ b/data/monster/Bosses/scorn_of_the_emperor.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/shardhead.xml b/data/monster/Bosses/shardhead.xml new file mode 100644 index 0000000..3616ee1 --- /dev/null +++ b/data/monster/Bosses/shardhead.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/sharptooth.xml b/data/monster/Bosses/sharptooth.xml new file mode 100644 index 0000000..da8f021 --- /dev/null +++ b/data/monster/Bosses/sharptooth.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/sir_valorcrest.xml b/data/monster/Bosses/sir_valorcrest.xml new file mode 100644 index 0000000..e8e2340 --- /dev/null +++ b/data/monster/Bosses/sir_valorcrest.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/snake_god_essence.xml b/data/monster/Bosses/snake_god_essence.xml new file mode 100644 index 0000000..4c00b40 --- /dev/null +++ b/data/monster/Bosses/snake_god_essence.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/snake_thing.xml b/data/monster/Bosses/snake_thing.xml new file mode 100644 index 0000000..027ce6a --- /dev/null +++ b/data/monster/Bosses/snake_thing.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/spider_queen.xml b/data/monster/Bosses/spider_queen.xml new file mode 100644 index 0000000..be6b5d4 --- /dev/null +++ b/data/monster/Bosses/spider_queen.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/spite_of_the_emperor.xml b/data/monster/Bosses/spite_of_the_emperor.xml new file mode 100644 index 0000000..f46e59b --- /dev/null +++ b/data/monster/Bosses/spite_of_the_emperor.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/splasher.xml b/data/monster/Bosses/splasher.xml new file mode 100644 index 0000000..62c9ee9 --- /dev/null +++ b/data/monster/Bosses/splasher.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/stonecracker.xml b/data/monster/Bosses/stonecracker.xml new file mode 100644 index 0000000..ad90719 --- /dev/null +++ b/data/monster/Bosses/stonecracker.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/tanjis.xml b/data/monster/Bosses/tanjis.xml new file mode 100644 index 0000000..0af5b87 --- /dev/null +++ b/data/monster/Bosses/tanjis.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/terofar.xml b/data/monster/Bosses/terofar.xml new file mode 100644 index 0000000..fdcb060 --- /dev/null +++ b/data/monster/Bosses/terofar.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_abomination.xml b/data/monster/Bosses/the_abomination.xml new file mode 100644 index 0000000..29b5447 --- /dev/null +++ b/data/monster/Bosses/the_abomination.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_axeorcist.xml b/data/monster/Bosses/the_axeorcist.xml new file mode 100644 index 0000000..9a94024 --- /dev/null +++ b/data/monster/Bosses/the_axeorcist.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_blightfather.xml b/data/monster/Bosses/the_blightfather.xml new file mode 100644 index 0000000..7593d86 --- /dev/null +++ b/data/monster/Bosses/the_blightfather.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_bloodtusk.xml b/data/monster/Bosses/the_bloodtusk.xml new file mode 100644 index 0000000..a13b5c4 --- /dev/null +++ b/data/monster/Bosses/the_bloodtusk.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_bloodweb.xml b/data/monster/Bosses/the_bloodweb.xml new file mode 100644 index 0000000..05902a4 --- /dev/null +++ b/data/monster/Bosses/the_bloodweb.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_collector.xml b/data/monster/Bosses/the_collector.xml new file mode 100644 index 0000000..39dac1c --- /dev/null +++ b/data/monster/Bosses/the_collector.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_count.xml b/data/monster/Bosses/the_count.xml new file mode 100644 index 0000000..c023d68 --- /dev/null +++ b/data/monster/Bosses/the_count.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_dreadorian.xml b/data/monster/Bosses/the_dreadorian.xml new file mode 100644 index 0000000..3abfda4 --- /dev/null +++ b/data/monster/Bosses/the_dreadorian.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_evil_eye.xml b/data/monster/Bosses/the_evil_eye.xml new file mode 100644 index 0000000..6c6b0e5 --- /dev/null +++ b/data/monster/Bosses/the_evil_eye.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_frog_prince.xml b/data/monster/Bosses/the_frog_prince.xml new file mode 100644 index 0000000..45eb985 --- /dev/null +++ b/data/monster/Bosses/the_frog_prince.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_handmaiden.xml b/data/monster/Bosses/the_handmaiden.xml new file mode 100644 index 0000000..5fb3f53 --- /dev/null +++ b/data/monster/Bosses/the_handmaiden.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_horned_fox.xml b/data/monster/Bosses/the_horned_fox.xml new file mode 100644 index 0000000..7c86ca7 --- /dev/null +++ b/data/monster/Bosses/the_horned_fox.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_imperor.xml b/data/monster/Bosses/the_imperor.xml new file mode 100644 index 0000000..faa6168 --- /dev/null +++ b/data/monster/Bosses/the_imperor.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_many.xml b/data/monster/Bosses/the_many.xml new file mode 100644 index 0000000..7c31503 --- /dev/null +++ b/data/monster/Bosses/the_many.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_noxious_spawn.xml b/data/monster/Bosses/the_noxious_spawn.xml new file mode 100644 index 0000000..682a961 --- /dev/null +++ b/data/monster/Bosses/the_noxious_spawn.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_old_widow.xml b/data/monster/Bosses/the_old_widow.xml new file mode 100644 index 0000000..e826b05 --- /dev/null +++ b/data/monster/Bosses/the_old_widow.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_pale_count.xml b/data/monster/Bosses/the_pale_count.xml new file mode 100644 index 0000000..0d4d7d9 --- /dev/null +++ b/data/monster/Bosses/the_pale_count.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_plasmother.xml b/data/monster/Bosses/the_plasmother.xml new file mode 100644 index 0000000..3526446 --- /dev/null +++ b/data/monster/Bosses/the_plasmother.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/the_snapper.xml b/data/monster/Bosses/the_snapper.xml new file mode 100644 index 0000000..64a0e15 --- /dev/null +++ b/data/monster/Bosses/the_snapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/thul.xml b/data/monster/Bosses/thul.xml new file mode 100644 index 0000000..0f990c5 --- /dev/null +++ b/data/monster/Bosses/thul.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/tiquandas_revenge.xml b/data/monster/Bosses/tiquandas_revenge.xml new file mode 100644 index 0000000..8c54200 --- /dev/null +++ b/data/monster/Bosses/tiquandas_revenge.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/tirecz.xml b/data/monster/Bosses/tirecz.xml new file mode 100644 index 0000000..82f54a0 --- /dev/null +++ b/data/monster/Bosses/tirecz.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/tremorak.xml b/data/monster/Bosses/tremorak.xml new file mode 100644 index 0000000..489b4d6 --- /dev/null +++ b/data/monster/Bosses/tremorak.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/ungreez.xml b/data/monster/Bosses/ungreez.xml new file mode 100644 index 0000000..35be776 --- /dev/null +++ b/data/monster/Bosses/ungreez.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/ushuriel.xml b/data/monster/Bosses/ushuriel.xml new file mode 100644 index 0000000..259bd39 --- /dev/null +++ b/data/monster/Bosses/ushuriel.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/verminor.xml b/data/monster/Bosses/verminor.xml new file mode 100644 index 0000000..a6a93fd --- /dev/null +++ b/data/monster/Bosses/verminor.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/versperoth.xml b/data/monster/Bosses/versperoth.xml new file mode 100644 index 0000000..9301842 --- /dev/null +++ b/data/monster/Bosses/versperoth.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/warlord_ruzad.xml b/data/monster/Bosses/warlord_ruzad.xml new file mode 100644 index 0000000..8b1d634 --- /dev/null +++ b/data/monster/Bosses/warlord_ruzad.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/wrath_of_the_emperor.xml b/data/monster/Bosses/wrath_of_the_emperor.xml new file mode 100644 index 0000000..7e634ea --- /dev/null +++ b/data/monster/Bosses/wrath_of_the_emperor.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/xenia.xml b/data/monster/Bosses/xenia.xml new file mode 100644 index 0000000..47eb896 --- /dev/null +++ b/data/monster/Bosses/xenia.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/yaga_the_crone.xml b/data/monster/Bosses/yaga_the_crone.xml new file mode 100644 index 0000000..06ab53c --- /dev/null +++ b/data/monster/Bosses/yaga_the_crone.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/yakchal.xml b/data/monster/Bosses/yakchal.xml new file mode 100644 index 0000000..f49896e --- /dev/null +++ b/data/monster/Bosses/yakchal.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/zavarash.xml b/data/monster/Bosses/zavarash.xml new file mode 100644 index 0000000..908aa7e --- /dev/null +++ b/data/monster/Bosses/zavarash.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/zevelon_duskbringer.xml b/data/monster/Bosses/zevelon_duskbringer.xml new file mode 100644 index 0000000..bb85c5b --- /dev/null +++ b/data/monster/Bosses/zevelon_duskbringer.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/zomba.xml b/data/monster/Bosses/zomba.xml new file mode 100644 index 0000000..cd75a1f --- /dev/null +++ b/data/monster/Bosses/zomba.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/zoralurk.xml b/data/monster/Bosses/zoralurk.xml new file mode 100644 index 0000000..63c522d --- /dev/null +++ b/data/monster/Bosses/zoralurk.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/zugurosh.xml b/data/monster/Bosses/zugurosh.xml new file mode 100644 index 0000000..95b09c8 --- /dev/null +++ b/data/monster/Bosses/zugurosh.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Bosses/zulazza_the_corruptor.xml b/data/monster/Bosses/zulazza_the_corruptor.xml new file mode 100644 index 0000000..bbc06e1 --- /dev/null +++ b/data/monster/Bosses/zulazza_the_corruptor.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/crystal_wolf.xml b/data/monster/Canines/crystal_wolf.xml new file mode 100644 index 0000000..301064b --- /dev/null +++ b/data/monster/Canines/crystal_wolf.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/dog.xml b/data/monster/Canines/dog.xml new file mode 100644 index 0000000..abbb1b9 --- /dev/null +++ b/data/monster/Canines/dog.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/gnarlhound.xml b/data/monster/Canines/gnarlhound.xml new file mode 100644 index 0000000..cbbb81e --- /dev/null +++ b/data/monster/Canines/gnarlhound.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/hot_dog.xml b/data/monster/Canines/hot_dog.xml new file mode 100644 index 0000000..1fdad96 --- /dev/null +++ b/data/monster/Canines/hot_dog.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/husky.xml b/data/monster/Canines/husky.xml new file mode 100644 index 0000000..a565a3b --- /dev/null +++ b/data/monster/Canines/husky.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/poodle.xml b/data/monster/Canines/poodle.xml new file mode 100644 index 0000000..530d6f4 --- /dev/null +++ b/data/monster/Canines/poodle.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/starving_wolf.xml b/data/monster/Canines/starving_wolf.xml new file mode 100644 index 0000000..c65777a --- /dev/null +++ b/data/monster/Canines/starving_wolf.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/thornfire_wolf.xml b/data/monster/Canines/thornfire_wolf.xml new file mode 100644 index 0000000..fd7dbd8 --- /dev/null +++ b/data/monster/Canines/thornfire_wolf.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/war_wolf.xml b/data/monster/Canines/war_wolf.xml new file mode 100644 index 0000000..38752c5 --- /dev/null +++ b/data/monster/Canines/war_wolf.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/werewolf.xml b/data/monster/Canines/werewolf.xml new file mode 100644 index 0000000..9fb26f9 --- /dev/null +++ b/data/monster/Canines/werewolf.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/wild_dog.xml b/data/monster/Canines/wild_dog.xml new file mode 100644 index 0000000..e933a69 --- /dev/null +++ b/data/monster/Canines/wild_dog.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/winter_wolf.xml b/data/monster/Canines/winter_wolf.xml new file mode 100644 index 0000000..ca6dbd3 --- /dev/null +++ b/data/monster/Canines/winter_wolf.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Canines/wolf.xml b/data/monster/Canines/wolf.xml new file mode 100644 index 0000000..74c4880 --- /dev/null +++ b/data/monster/Canines/wolf.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Chakoyas/chakoya_toolshaper.xml b/data/monster/Chakoyas/chakoya_toolshaper.xml new file mode 100644 index 0000000..7713296 --- /dev/null +++ b/data/monster/Chakoyas/chakoya_toolshaper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Chakoyas/chakoya_tribewarden.xml b/data/monster/Chakoyas/chakoya_tribewarden.xml new file mode 100644 index 0000000..658da65 --- /dev/null +++ b/data/monster/Chakoyas/chakoya_tribewarden.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Chakoyas/chakoya_windcaller.xml b/data/monster/Chakoyas/chakoya_windcaller.xml new file mode 100644 index 0000000..398b234 --- /dev/null +++ b/data/monster/Chakoyas/chakoya_windcaller.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Cnidarians/jellyfish.xml b/data/monster/Cnidarians/jellyfish.xml new file mode 100644 index 0000000..8e051f2 --- /dev/null +++ b/data/monster/Cnidarians/jellyfish.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Corym/corym_charlatan.xml b/data/monster/Corym/corym_charlatan.xml new file mode 100644 index 0000000..b4f9958 --- /dev/null +++ b/data/monster/Corym/corym_charlatan.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Corym/corym_skirmisher.xml b/data/monster/Corym/corym_skirmisher.xml new file mode 100644 index 0000000..1912a4f --- /dev/null +++ b/data/monster/Corym/corym_skirmisher.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Corym/corym_vanguard.xml b/data/monster/Corym/corym_vanguard.xml new file mode 100644 index 0000000..f962ff5 --- /dev/null +++ b/data/monster/Corym/corym_vanguard.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Corym/little_corym_charlatan.xml b/data/monster/Corym/little_corym_charlatan.xml new file mode 100644 index 0000000..a7c87bb --- /dev/null +++ b/data/monster/Corym/little_corym_charlatan.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Crustaceans/blood_crab.xml b/data/monster/Crustaceans/blood_crab.xml new file mode 100644 index 0000000..6b54a82 --- /dev/null +++ b/data/monster/Crustaceans/blood_crab.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Crustaceans/crab.xml b/data/monster/Crustaceans/crab.xml new file mode 100644 index 0000000..d1795d7 --- /dev/null +++ b/data/monster/Crustaceans/crab.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Crustaceans/crustacea_gigantica.xml b/data/monster/Crustaceans/crustacea_gigantica.xml new file mode 100644 index 0000000..3557c7a --- /dev/null +++ b/data/monster/Crustaceans/crustacea_gigantica.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Crustaceans/deepsea_blood_crab.xml b/data/monster/Crustaceans/deepsea_blood_crab.xml new file mode 100644 index 0000000..903c687 --- /dev/null +++ b/data/monster/Crustaceans/deepsea_blood_crab.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Cryo-Elementals/ice_golem.xml b/data/monster/Cryo-Elementals/ice_golem.xml new file mode 100644 index 0000000..8b1f50a --- /dev/null +++ b/data/monster/Cryo-Elementals/ice_golem.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Deeplings/deepling_brawler.xml b/data/monster/Deeplings/deepling_brawler.xml new file mode 100644 index 0000000..44f3e5d --- /dev/null +++ b/data/monster/Deeplings/deepling_brawler.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Deeplings/deepling_elite.xml b/data/monster/Deeplings/deepling_elite.xml new file mode 100644 index 0000000..eb6c49e --- /dev/null +++ b/data/monster/Deeplings/deepling_elite.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Deeplings/deepling_guard.xml b/data/monster/Deeplings/deepling_guard.xml new file mode 100644 index 0000000..1b25163 --- /dev/null +++ b/data/monster/Deeplings/deepling_guard.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Deeplings/deepling_master_librarian.xml b/data/monster/Deeplings/deepling_master_librarian.xml new file mode 100644 index 0000000..a16ac05 --- /dev/null +++ b/data/monster/Deeplings/deepling_master_librarian.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Deeplings/deepling_scout.xml b/data/monster/Deeplings/deepling_scout.xml new file mode 100644 index 0000000..c6c23f5 --- /dev/null +++ b/data/monster/Deeplings/deepling_scout.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Deeplings/deepling_spellsinger.xml b/data/monster/Deeplings/deepling_spellsinger.xml new file mode 100644 index 0000000..acf587b --- /dev/null +++ b/data/monster/Deeplings/deepling_spellsinger.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Deeplings/deepling_tyrant.xml b/data/monster/Deeplings/deepling_tyrant.xml new file mode 100644 index 0000000..53f561c --- /dev/null +++ b/data/monster/Deeplings/deepling_tyrant.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Deeplings/deepling_warrior.xml b/data/monster/Deeplings/deepling_warrior.xml new file mode 100644 index 0000000..eae0b3e --- /dev/null +++ b/data/monster/Deeplings/deepling_warrior.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Deeplings/deepling_worker.xml b/data/monster/Deeplings/deepling_worker.xml new file mode 100644 index 0000000..a26ca6b --- /dev/null +++ b/data/monster/Deeplings/deepling_worker.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/askarak_demon.xml b/data/monster/Demons/askarak_demon.xml new file mode 100644 index 0000000..2437630 --- /dev/null +++ b/data/monster/Demons/askarak_demon.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/askarak_lord.xml b/data/monster/Demons/askarak_lord.xml new file mode 100644 index 0000000..ff4bc91 --- /dev/null +++ b/data/monster/Demons/askarak_lord.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/askarak_prince.xml b/data/monster/Demons/askarak_prince.xml new file mode 100644 index 0000000..0c8e716 --- /dev/null +++ b/data/monster/Demons/askarak_prince.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/dark_torturer.xml b/data/monster/Demons/dark_torturer.xml new file mode 100644 index 0000000..3f8af09 --- /dev/null +++ b/data/monster/Demons/dark_torturer.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/demon.xml b/data/monster/Demons/demon.xml new file mode 100644 index 0000000..5648c90 --- /dev/null +++ b/data/monster/Demons/demon.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/destroyer.xml b/data/monster/Demons/destroyer.xml new file mode 100644 index 0000000..feda3b0 --- /dev/null +++ b/data/monster/Demons/destroyer.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/diabolic_imp.xml b/data/monster/Demons/diabolic_imp.xml new file mode 100644 index 0000000..e0d9e0a --- /dev/null +++ b/data/monster/Demons/diabolic_imp.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/fire_devil.xml b/data/monster/Demons/fire_devil.xml new file mode 100644 index 0000000..604904d --- /dev/null +++ b/data/monster/Demons/fire_devil.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/gozzler.xml b/data/monster/Demons/gozzler.xml new file mode 100644 index 0000000..f56ffeb --- /dev/null +++ b/data/monster/Demons/gozzler.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/grave_guard.xml b/data/monster/Demons/grave_guard.xml new file mode 100644 index 0000000..0baac3a --- /dev/null +++ b/data/monster/Demons/grave_guard.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/gravedigger.xml b/data/monster/Demons/gravedigger.xml new file mode 100644 index 0000000..bdb5cd2 --- /dev/null +++ b/data/monster/Demons/gravedigger.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/hand_of_cursed_fate.xml b/data/monster/Demons/hand_of_cursed_fate.xml new file mode 100644 index 0000000..ee6e2d3 --- /dev/null +++ b/data/monster/Demons/hand_of_cursed_fate.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/hellhound.xml b/data/monster/Demons/hellhound.xml new file mode 100644 index 0000000..2ec41df --- /dev/null +++ b/data/monster/Demons/hellhound.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/hellspawn.xml b/data/monster/Demons/hellspawn.xml new file mode 100644 index 0000000..5441eb6 --- /dev/null +++ b/data/monster/Demons/hellspawn.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/juggernaut.xml b/data/monster/Demons/juggernaut.xml new file mode 100644 index 0000000..14f05f5 --- /dev/null +++ b/data/monster/Demons/juggernaut.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/nightmare.xml b/data/monster/Demons/nightmare.xml new file mode 100644 index 0000000..29a5076 --- /dev/null +++ b/data/monster/Demons/nightmare.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/nightmare_scion.xml b/data/monster/Demons/nightmare_scion.xml new file mode 100644 index 0000000..deeec41 --- /dev/null +++ b/data/monster/Demons/nightmare_scion.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/nightstalker.xml b/data/monster/Demons/nightstalker.xml new file mode 100644 index 0000000..09eb561 --- /dev/null +++ b/data/monster/Demons/nightstalker.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/plaguesmith.xml b/data/monster/Demons/plaguesmith.xml new file mode 100644 index 0000000..2749492 --- /dev/null +++ b/data/monster/Demons/plaguesmith.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/rift_brood.xml b/data/monster/Demons/rift_brood.xml new file mode 100644 index 0000000..32e8564 --- /dev/null +++ b/data/monster/Demons/rift_brood.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/rift_scythe.xml b/data/monster/Demons/rift_scythe.xml new file mode 100644 index 0000000..046da92 --- /dev/null +++ b/data/monster/Demons/rift_scythe.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/shaburak_demon.xml b/data/monster/Demons/shaburak_demon.xml new file mode 100644 index 0000000..4302048 --- /dev/null +++ b/data/monster/Demons/shaburak_demon.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/shaburak_lord.xml b/data/monster/Demons/shaburak_lord.xml new file mode 100644 index 0000000..49fce81 --- /dev/null +++ b/data/monster/Demons/shaburak_lord.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/shaburak_prince.xml b/data/monster/Demons/shaburak_prince.xml new file mode 100644 index 0000000..1b62cea --- /dev/null +++ b/data/monster/Demons/shaburak_prince.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Demons/yielothax.xml b/data/monster/Demons/yielothax.xml new file mode 100644 index 0000000..4a6ef70 --- /dev/null +++ b/data/monster/Demons/yielothax.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Djinn/blue_djinn.xml b/data/monster/Djinn/blue_djinn.xml new file mode 100644 index 0000000..69ff768 --- /dev/null +++ b/data/monster/Djinn/blue_djinn.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Djinn/efreet.xml b/data/monster/Djinn/efreet.xml new file mode 100644 index 0000000..c3b71ff --- /dev/null +++ b/data/monster/Djinn/efreet.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Djinn/green_djinn.xml b/data/monster/Djinn/green_djinn.xml new file mode 100644 index 0000000..ab3d6be --- /dev/null +++ b/data/monster/Djinn/green_djinn.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Djinn/marid.xml b/data/monster/Djinn/marid.xml new file mode 100644 index 0000000..e79647e --- /dev/null +++ b/data/monster/Djinn/marid.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dragons/dragon.xml b/data/monster/Dragons/dragon.xml new file mode 100644 index 0000000..3d79b8a --- /dev/null +++ b/data/monster/Dragons/dragon.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dragons/dragon_hatchling.xml b/data/monster/Dragons/dragon_hatchling.xml new file mode 100644 index 0000000..865ceaa --- /dev/null +++ b/data/monster/Dragons/dragon_hatchling.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dragons/dragon_lord.xml b/data/monster/Dragons/dragon_lord.xml new file mode 100644 index 0000000..b0775a0 --- /dev/null +++ b/data/monster/Dragons/dragon_lord.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dragons/dragon_lord_hatchling.xml b/data/monster/Dragons/dragon_lord_hatchling.xml new file mode 100644 index 0000000..3afbad7 --- /dev/null +++ b/data/monster/Dragons/dragon_lord_hatchling.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dragons/dragonling.xml b/data/monster/Dragons/dragonling.xml new file mode 100644 index 0000000..db56deb --- /dev/null +++ b/data/monster/Dragons/dragonling.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dragons/draptor.xml b/data/monster/Dragons/draptor.xml new file mode 100644 index 0000000..b27501c --- /dev/null +++ b/data/monster/Dragons/draptor.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dragons/elder_wyrm.xml b/data/monster/Dragons/elder_wyrm.xml new file mode 100644 index 0000000..b7e128c --- /dev/null +++ b/data/monster/Dragons/elder_wyrm.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dragons/frost_dragon.xml b/data/monster/Dragons/frost_dragon.xml new file mode 100644 index 0000000..c38375d --- /dev/null +++ b/data/monster/Dragons/frost_dragon.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dragons/frost_dragon_hatchling.xml b/data/monster/Dragons/frost_dragon_hatchling.xml new file mode 100644 index 0000000..e9ec78f --- /dev/null +++ b/data/monster/Dragons/frost_dragon_hatchling.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dragons/ghastly_dragon.xml b/data/monster/Dragons/ghastly_dragon.xml new file mode 100644 index 0000000..880f36d --- /dev/null +++ b/data/monster/Dragons/ghastly_dragon.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dragons/undead_dragon.xml b/data/monster/Dragons/undead_dragon.xml new file mode 100644 index 0000000..663069d --- /dev/null +++ b/data/monster/Dragons/undead_dragon.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dragons/wyrm.xml b/data/monster/Dragons/wyrm.xml new file mode 100644 index 0000000..9c335ef --- /dev/null +++ b/data/monster/Dragons/wyrm.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/bad_dream.xml b/data/monster/Dreamhaunters/bad_dream.xml new file mode 100644 index 0000000..bc2025a --- /dev/null +++ b/data/monster/Dreamhaunters/bad_dream.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/choking_fear.xml b/data/monster/Dreamhaunters/choking_fear.xml new file mode 100644 index 0000000..dbab739 --- /dev/null +++ b/data/monster/Dreamhaunters/choking_fear.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/demon_outcast.xml b/data/monster/Dreamhaunters/demon_outcast.xml new file mode 100644 index 0000000..849dd62 --- /dev/null +++ b/data/monster/Dreamhaunters/demon_outcast.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/feversleep.xml b/data/monster/Dreamhaunters/feversleep.xml new file mode 100644 index 0000000..0f99fe7 --- /dev/null +++ b/data/monster/Dreamhaunters/feversleep.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/frazzlemaw.xml b/data/monster/Dreamhaunters/frazzlemaw.xml new file mode 100644 index 0000000..38e5b70 --- /dev/null +++ b/data/monster/Dreamhaunters/frazzlemaw.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/guzzlemaw.xml b/data/monster/Dreamhaunters/guzzlemaw.xml new file mode 100644 index 0000000..b860c8e --- /dev/null +++ b/data/monster/Dreamhaunters/guzzlemaw.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/minion_of_gaz'haragoth.xml b/data/monster/Dreamhaunters/minion_of_gaz'haragoth.xml new file mode 100644 index 0000000..8cd335a --- /dev/null +++ b/data/monster/Dreamhaunters/minion_of_gaz'haragoth.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/nightmare_of_gaz'haragoth.xml b/data/monster/Dreamhaunters/nightmare_of_gaz'haragoth.xml new file mode 100644 index 0000000..33bea4f --- /dev/null +++ b/data/monster/Dreamhaunters/nightmare_of_gaz'haragoth.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/retching_horror.xml b/data/monster/Dreamhaunters/retching_horror.xml new file mode 100644 index 0000000..9b16607 --- /dev/null +++ b/data/monster/Dreamhaunters/retching_horror.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/shiversleep.xml b/data/monster/Dreamhaunters/shiversleep.xml new file mode 100644 index 0000000..e334578 --- /dev/null +++ b/data/monster/Dreamhaunters/shiversleep.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/shock_head.xml b/data/monster/Dreamhaunters/shock_head.xml new file mode 100644 index 0000000..4068909 --- /dev/null +++ b/data/monster/Dreamhaunters/shock_head.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/sight_of_surrender.xml b/data/monster/Dreamhaunters/sight_of_surrender.xml new file mode 100644 index 0000000..257fc23 --- /dev/null +++ b/data/monster/Dreamhaunters/sight_of_surrender.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/silencer.xml b/data/monster/Dreamhaunters/silencer.xml new file mode 100644 index 0000000..dd4cfb5 --- /dev/null +++ b/data/monster/Dreamhaunters/silencer.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dreamhaunters/terrorsleep.xml b/data/monster/Dreamhaunters/terrorsleep.xml new file mode 100644 index 0000000..82a192b --- /dev/null +++ b/data/monster/Dreamhaunters/terrorsleep.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dwarves/dwarf.xml b/data/monster/Dwarves/dwarf.xml new file mode 100644 index 0000000..095be32 --- /dev/null +++ b/data/monster/Dwarves/dwarf.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dwarves/dwarf_geomancer.xml b/data/monster/Dwarves/dwarf_geomancer.xml new file mode 100644 index 0000000..a6cd633 --- /dev/null +++ b/data/monster/Dwarves/dwarf_geomancer.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dwarves/dwarf_guard.xml b/data/monster/Dwarves/dwarf_guard.xml new file mode 100644 index 0000000..72f0783 --- /dev/null +++ b/data/monster/Dwarves/dwarf_guard.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dwarves/dwarf_miner.xml b/data/monster/Dwarves/dwarf_miner.xml new file mode 100644 index 0000000..6c912cf --- /dev/null +++ b/data/monster/Dwarves/dwarf_miner.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dwarves/dwarf_soldier.xml b/data/monster/Dwarves/dwarf_soldier.xml new file mode 100644 index 0000000..cdf1ede --- /dev/null +++ b/data/monster/Dwarves/dwarf_soldier.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dwarves/enslaved_dwarf.xml b/data/monster/Dwarves/enslaved_dwarf.xml new file mode 100644 index 0000000..9f3cfbd --- /dev/null +++ b/data/monster/Dwarves/enslaved_dwarf.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dwarves/lost_basher.xml b/data/monster/Dwarves/lost_basher.xml new file mode 100644 index 0000000..1753269 --- /dev/null +++ b/data/monster/Dwarves/lost_basher.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dwarves/lost_berserker.xml b/data/monster/Dwarves/lost_berserker.xml new file mode 100644 index 0000000..74f71f3 --- /dev/null +++ b/data/monster/Dwarves/lost_berserker.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dwarves/lost_husher.xml b/data/monster/Dwarves/lost_husher.xml new file mode 100644 index 0000000..935e98f --- /dev/null +++ b/data/monster/Dwarves/lost_husher.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dwarves/lost_thrower.xml b/data/monster/Dwarves/lost_thrower.xml new file mode 100644 index 0000000..ff3f2da --- /dev/null +++ b/data/monster/Dwarves/lost_thrower.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dworcs/dworc_fleshhunter.xml b/data/monster/Dworcs/dworc_fleshhunter.xml new file mode 100644 index 0000000..6e1c12f --- /dev/null +++ b/data/monster/Dworcs/dworc_fleshhunter.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dworcs/dworc_venomsniper.xml b/data/monster/Dworcs/dworc_venomsniper.xml new file mode 100644 index 0000000..8bbe0ab --- /dev/null +++ b/data/monster/Dworcs/dworc_venomsniper.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Dworcs/dworc_voodoomaster.xml b/data/monster/Dworcs/dworc_voodoomaster.xml new file mode 100644 index 0000000..6425c14 --- /dev/null +++ b/data/monster/Dworcs/dworc_voodoomaster.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Electro-Elementals/charged_energy_elemental.xml b/data/monster/Electro-Elementals/charged_energy_elemental.xml new file mode 100644 index 0000000..924dbd3 --- /dev/null +++ b/data/monster/Electro-Elementals/charged_energy_elemental.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Electro-Elementals/energy_elemental.xml b/data/monster/Electro-Elementals/energy_elemental.xml new file mode 100644 index 0000000..aeabf4b --- /dev/null +++ b/data/monster/Electro-Elementals/energy_elemental.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Electro-Elementals/massive_energy_elemental.xml b/data/monster/Electro-Elementals/massive_energy_elemental.xml new file mode 100644 index 0000000..5c9e72a --- /dev/null +++ b/data/monster/Electro-Elementals/massive_energy_elemental.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Electro-Elementals/overcharged_energy_element.xml b/data/monster/Electro-Elementals/overcharged_energy_element.xml new file mode 100644 index 0000000..d447667 --- /dev/null +++ b/data/monster/Electro-Elementals/overcharged_energy_element.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Elemental_Lords/earth_overlord.xml b/data/monster/Elemental_Lords/earth_overlord.xml new file mode 100644 index 0000000..09b4b6f --- /dev/null +++ b/data/monster/Elemental_Lords/earth_overlord.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Elemental_Lords/energy_overlord.xml b/data/monster/Elemental_Lords/energy_overlord.xml new file mode 100644 index 0000000..6da38d3 --- /dev/null +++ b/data/monster/Elemental_Lords/energy_overlord.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Elemental_Lords/fire_overlord.xml b/data/monster/Elemental_Lords/fire_overlord.xml new file mode 100644 index 0000000..c1e6236 --- /dev/null +++ b/data/monster/Elemental_Lords/fire_overlord.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Elemental_Lords/ice_overlord.xml b/data/monster/Elemental_Lords/ice_overlord.xml new file mode 100644 index 0000000..5ec5c93 --- /dev/null +++ b/data/monster/Elemental_Lords/ice_overlord.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Elves/elf.xml b/data/monster/Elves/elf.xml new file mode 100644 index 0000000..4ad81e6 --- /dev/null +++ b/data/monster/Elves/elf.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Elves/elf_arcanist.xml b/data/monster/Elves/elf_arcanist.xml new file mode 100644 index 0000000..e14b8ae --- /dev/null +++ b/data/monster/Elves/elf_arcanist.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Elves/elf_scout.xml b/data/monster/Elves/elf_scout.xml new file mode 100644 index 0000000..905afde --- /dev/null +++ b/data/monster/Elves/elf_scout.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Elves/firestarter.xml b/data/monster/Elves/firestarter.xml new file mode 100644 index 0000000..676b9ab --- /dev/null +++ b/data/monster/Elves/firestarter.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Event_Creatures/bane_bringer.xml b/data/monster/Event_Creatures/bane_bringer.xml new file mode 100644 index 0000000..ee7b003 --- /dev/null +++ b/data/monster/Event_Creatures/bane_bringer.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Event_Creatures/essence_of_darkness.xml b/data/monster/Event_Creatures/essence_of_darkness.xml new file mode 100644 index 0000000..39e8ce3 --- /dev/null +++ b/data/monster/Event_Creatures/essence_of_darkness.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Event_Creatures/grynch_clan_goblin.xml b/data/monster/Event_Creatures/grynch_clan_goblin.xml new file mode 100644 index 0000000..4f9a105 --- /dev/null +++ b/data/monster/Event_Creatures/grynch_clan_goblin.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Event_Creatures/hacker.xml b/data/monster/Event_Creatures/hacker.xml new file mode 100644 index 0000000..cb81199 --- /dev/null +++ b/data/monster/Event_Creatures/hacker.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Event_Creatures/herald_of_gloom.xml b/data/monster/Event_Creatures/herald_of_gloom.xml new file mode 100644 index 0000000..019b8cb --- /dev/null +++ b/data/monster/Event_Creatures/herald_of_gloom.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Event_Creatures/midnight_spawn.xml b/data/monster/Event_Creatures/midnight_spawn.xml new file mode 100644 index 0000000..e009e5e --- /dev/null +++ b/data/monster/Event_Creatures/midnight_spawn.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Event_Creatures/primitive.xml b/data/monster/Event_Creatures/primitive.xml new file mode 100644 index 0000000..b3246c2 --- /dev/null +++ b/data/monster/Event_Creatures/primitive.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Event_Creatures/shadow_hound.xml b/data/monster/Event_Creatures/shadow_hound.xml new file mode 100644 index 0000000..2dd60ba --- /dev/null +++ b/data/monster/Event_Creatures/shadow_hound.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Event_Creatures/spectral_scum.xml b/data/monster/Event_Creatures/spectral_scum.xml new file mode 100644 index 0000000..e552ccb --- /dev/null +++ b/data/monster/Event_Creatures/spectral_scum.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Event_Creatures/the_halloween_hare.xml b/data/monster/Event_Creatures/the_halloween_hare.xml new file mode 100644 index 0000000..43299b9 --- /dev/null +++ b/data/monster/Event_Creatures/the_halloween_hare.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Event_Creatures/undead_minion.xml b/data/monster/Event_Creatures/undead_minion.xml new file mode 100644 index 0000000..f474f44 --- /dev/null +++ b/data/monster/Event_Creatures/undead_minion.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Felines/cat.xml b/data/monster/Felines/cat.xml new file mode 100644 index 0000000..40aa744 --- /dev/null +++ b/data/monster/Felines/cat.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Felines/lion.xml b/data/monster/Felines/lion.xml new file mode 100644 index 0000000..897c73c --- /dev/null +++ b/data/monster/Felines/lion.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Felines/midnight_panther.xml b/data/monster/Felines/midnight_panther.xml new file mode 100644 index 0000000..53a193f --- /dev/null +++ b/data/monster/Felines/midnight_panther.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Felines/tiger.xml b/data/monster/Felines/tiger.xml new file mode 100644 index 0000000..30154cb --- /dev/null +++ b/data/monster/Felines/tiger.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Fish/fish.xml b/data/monster/Fish/fish.xml new file mode 100644 index 0000000..d6ddf14 --- /dev/null +++ b/data/monster/Fish/fish.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Fish/manta_ray.xml b/data/monster/Fish/manta_ray.xml new file mode 100644 index 0000000..3a32026 --- /dev/null +++ b/data/monster/Fish/manta_ray.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Fish/northern_pike.xml b/data/monster/Fish/northern_pike.xml new file mode 100644 index 0000000..0a9dc3d --- /dev/null +++ b/data/monster/Fish/northern_pike.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Fish/shark.xml b/data/monster/Fish/shark.xml new file mode 100644 index 0000000..2fd8c7a --- /dev/null +++ b/data/monster/Fish/shark.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Fish/slippery_northern_pike.xml b/data/monster/Fish/slippery_northern_pike.xml new file mode 100644 index 0000000..b87da53 --- /dev/null +++ b/data/monster/Fish/slippery_northern_pike.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Frogs/azure_frog.xml b/data/monster/Frogs/azure_frog.xml new file mode 100644 index 0000000..19ffd89 --- /dev/null +++ b/data/monster/Frogs/azure_frog.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Frogs/bog_frog.xml b/data/monster/Frogs/bog_frog.xml new file mode 100644 index 0000000..3bd2156 --- /dev/null +++ b/data/monster/Frogs/bog_frog.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Frogs/coral_frog.xml b/data/monster/Frogs/coral_frog.xml new file mode 100644 index 0000000..0b1049b --- /dev/null +++ b/data/monster/Frogs/coral_frog.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Frogs/crimson_frog.xml b/data/monster/Frogs/crimson_frog.xml new file mode 100644 index 0000000..753f5d3 --- /dev/null +++ b/data/monster/Frogs/crimson_frog.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Frogs/deathspawn.xml b/data/monster/Frogs/deathspawn.xml new file mode 100644 index 0000000..dc640b5 --- /dev/null +++ b/data/monster/Frogs/deathspawn.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Frogs/filth_toad.xml b/data/monster/Frogs/filth_toad.xml new file mode 100644 index 0000000..52f0c19 --- /dev/null +++ b/data/monster/Frogs/filth_toad.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Frogs/green_frog.xml b/data/monster/Frogs/green_frog.xml new file mode 100644 index 0000000..3dd38b9 --- /dev/null +++ b/data/monster/Frogs/green_frog.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Frogs/infernal_frog.xml b/data/monster/Frogs/infernal_frog.xml new file mode 100644 index 0000000..66ec379 --- /dev/null +++ b/data/monster/Frogs/infernal_frog.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Frogs/orchid_frog.xml b/data/monster/Frogs/orchid_frog.xml new file mode 100644 index 0000000..356ad3b --- /dev/null +++ b/data/monster/Frogs/orchid_frog.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Frogs/toad.xml b/data/monster/Frogs/toad.xml new file mode 100644 index 0000000..3393665 --- /dev/null +++ b/data/monster/Frogs/toad.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/armadile.xml b/data/monster/Geo-Elementals/armadile.xml new file mode 100644 index 0000000..b45df7e --- /dev/null +++ b/data/monster/Geo-Elementals/armadile.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/clay_guardian.xml b/data/monster/Geo-Elementals/clay_guardian.xml new file mode 100644 index 0000000..7f99496 --- /dev/null +++ b/data/monster/Geo-Elementals/clay_guardian.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/cliff_strider.xml b/data/monster/Geo-Elementals/cliff_strider.xml new file mode 100644 index 0000000..0979a6d --- /dev/null +++ b/data/monster/Geo-Elementals/cliff_strider.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/crystalcrusher.xml b/data/monster/Geo-Elementals/crystalcrusher.xml new file mode 100644 index 0000000..9de5626 --- /dev/null +++ b/data/monster/Geo-Elementals/crystalcrusher.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/damaged_crystal_golem.xml b/data/monster/Geo-Elementals/damaged_crystal_golem.xml new file mode 100644 index 0000000..83f5311 --- /dev/null +++ b/data/monster/Geo-Elementals/damaged_crystal_golem.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/damaged_worker_golem.xml b/data/monster/Geo-Elementals/damaged_worker_golem.xml new file mode 100644 index 0000000..252fa78 --- /dev/null +++ b/data/monster/Geo-Elementals/damaged_worker_golem.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/diamond_servant.xml b/data/monster/Geo-Elementals/diamond_servant.xml new file mode 100644 index 0000000..fbd9bd9 --- /dev/null +++ b/data/monster/Geo-Elementals/diamond_servant.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/earth_elemental.xml b/data/monster/Geo-Elementals/earth_elemental.xml new file mode 100644 index 0000000..c0df895 --- /dev/null +++ b/data/monster/Geo-Elementals/earth_elemental.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/enraged_crystal_golem.xml b/data/monster/Geo-Elementals/enraged_crystal_golem.xml new file mode 100644 index 0000000..cfa2a52 --- /dev/null +++ b/data/monster/Geo-Elementals/enraged_crystal_golem.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/eternal_guardian.xml b/data/monster/Geo-Elementals/eternal_guardian.xml new file mode 100644 index 0000000..148b4a6 --- /dev/null +++ b/data/monster/Geo-Elementals/eternal_guardian.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/forest_fury.xml b/data/monster/Geo-Elementals/forest_fury.xml new file mode 100644 index 0000000..eb3cafe --- /dev/null +++ b/data/monster/Geo-Elementals/forest_fury.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/gargoyle.xml b/data/monster/Geo-Elementals/gargoyle.xml new file mode 100644 index 0000000..d18c00f --- /dev/null +++ b/data/monster/Geo-Elementals/gargoyle.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/golden_servant.xml b/data/monster/Geo-Elementals/golden_servant.xml new file mode 100644 index 0000000..b36a4fd --- /dev/null +++ b/data/monster/Geo-Elementals/golden_servant.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/iron_servant.xml b/data/monster/Geo-Elementals/iron_servant.xml new file mode 100644 index 0000000..070d653 --- /dev/null +++ b/data/monster/Geo-Elementals/iron_servant.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/ironblight.xml b/data/monster/Geo-Elementals/ironblight.xml new file mode 100644 index 0000000..7dfe56a --- /dev/null +++ b/data/monster/Geo-Elementals/ironblight.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/jagged_earth_elemental.xml b/data/monster/Geo-Elementals/jagged_earth_elemental.xml new file mode 100644 index 0000000..beda073 --- /dev/null +++ b/data/monster/Geo-Elementals/jagged_earth_elemental.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/massive_earth_elemental.xml b/data/monster/Geo-Elementals/massive_earth_elemental.xml new file mode 100644 index 0000000..00579a6 --- /dev/null +++ b/data/monster/Geo-Elementals/massive_earth_elemental.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/medusa.xml b/data/monster/Geo-Elementals/medusa.xml new file mode 100644 index 0000000..7f1de5d --- /dev/null +++ b/data/monster/Geo-Elementals/medusa.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/muddy_earth_elemental.xml b/data/monster/Geo-Elementals/muddy_earth_elemental.xml new file mode 100644 index 0000000..e21f323 --- /dev/null +++ b/data/monster/Geo-Elementals/muddy_earth_elemental.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/stone_devourer.xml b/data/monster/Geo-Elementals/stone_devourer.xml new file mode 100644 index 0000000..8545604 --- /dev/null +++ b/data/monster/Geo-Elementals/stone_devourer.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/stone_golem.xml b/data/monster/Geo-Elementals/stone_golem.xml new file mode 100644 index 0000000..e87e7b9 --- /dev/null +++ b/data/monster/Geo-Elementals/stone_golem.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/vulcongra.xml b/data/monster/Geo-Elementals/vulcongra.xml new file mode 100644 index 0000000..8762f2d --- /dev/null +++ b/data/monster/Geo-Elementals/vulcongra.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/war_golem.xml b/data/monster/Geo-Elementals/war_golem.xml new file mode 100644 index 0000000..12ed69c --- /dev/null +++ b/data/monster/Geo-Elementals/war_golem.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Geo-Elementals/worker_golem.xml b/data/monster/Geo-Elementals/worker_golem.xml new file mode 100644 index 0000000..b60c5ca --- /dev/null +++ b/data/monster/Geo-Elementals/worker_golem.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ghosts/ghost.xml b/data/monster/Ghosts/ghost.xml new file mode 100644 index 0000000..546de60 --- /dev/null +++ b/data/monster/Ghosts/ghost.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ghosts/phantasm.xml b/data/monster/Ghosts/phantasm.xml new file mode 100644 index 0000000..731393a --- /dev/null +++ b/data/monster/Ghosts/phantasm.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ghosts/phantasm_summon.xml b/data/monster/Ghosts/phantasm_summon.xml new file mode 100644 index 0000000..af1db89 --- /dev/null +++ b/data/monster/Ghosts/phantasm_summon.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ghosts/souleater.xml b/data/monster/Ghosts/souleater.xml new file mode 100644 index 0000000..927f3cf --- /dev/null +++ b/data/monster/Ghosts/souleater.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ghosts/spectre.xml b/data/monster/Ghosts/spectre.xml new file mode 100644 index 0000000..aa62c2c --- /dev/null +++ b/data/monster/Ghosts/spectre.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ghosts/tarnished_spirit.xml b/data/monster/Ghosts/tarnished_spirit.xml new file mode 100644 index 0000000..4192921 --- /dev/null +++ b/data/monster/Ghosts/tarnished_spirit.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ghosts/tormented_ghost.xml b/data/monster/Ghosts/tormented_ghost.xml new file mode 100644 index 0000000..fd8cfbe --- /dev/null +++ b/data/monster/Ghosts/tormented_ghost.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ghosts/white_shade.xml b/data/monster/Ghosts/white_shade.xml new file mode 100644 index 0000000..4c3fa27 --- /dev/null +++ b/data/monster/Ghosts/white_shade.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ghosts/wisp.xml b/data/monster/Ghosts/wisp.xml new file mode 100644 index 0000000..0796239 --- /dev/null +++ b/data/monster/Ghosts/wisp.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Giants/behemoth.xml b/data/monster/Giants/behemoth.xml new file mode 100644 index 0000000..785d1c2 --- /dev/null +++ b/data/monster/Giants/behemoth.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Giants/cyclops.xml b/data/monster/Giants/cyclops.xml new file mode 100644 index 0000000..9482692 --- /dev/null +++ b/data/monster/Giants/cyclops.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Giants/cyclops_drone.xml b/data/monster/Giants/cyclops_drone.xml new file mode 100644 index 0000000..87dac1a --- /dev/null +++ b/data/monster/Giants/cyclops_drone.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Giants/cyclops_smith.xml b/data/monster/Giants/cyclops_smith.xml new file mode 100644 index 0000000..9e5867d --- /dev/null +++ b/data/monster/Giants/cyclops_smith.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Giants/frost_giant.xml b/data/monster/Giants/frost_giant.xml new file mode 100644 index 0000000..f96236b --- /dev/null +++ b/data/monster/Giants/frost_giant.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Giants/frost_giantess.xml b/data/monster/Giants/frost_giantess.xml new file mode 100644 index 0000000..06d59d6 --- /dev/null +++ b/data/monster/Giants/frost_giantess.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Glires/cave_rat.xml b/data/monster/Glires/cave_rat.xml new file mode 100644 index 0000000..1e97192 --- /dev/null +++ b/data/monster/Glires/cave_rat.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Glires/killer_rabbit.xml b/data/monster/Glires/killer_rabbit.xml new file mode 100644 index 0000000..2c713dc --- /dev/null +++ b/data/monster/Glires/killer_rabbit.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Glires/rabbit.xml b/data/monster/Glires/rabbit.xml new file mode 100644 index 0000000..8184d1f --- /dev/null +++ b/data/monster/Glires/rabbit.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Glires/rat.xml b/data/monster/Glires/rat.xml new file mode 100644 index 0000000..17c38e0 --- /dev/null +++ b/data/monster/Glires/rat.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Glires/silver_rabbit.xml b/data/monster/Glires/silver_rabbit.xml new file mode 100644 index 0000000..e56099f --- /dev/null +++ b/data/monster/Glires/silver_rabbit.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Glires/squirrel.xml b/data/monster/Glires/squirrel.xml new file mode 100644 index 0000000..b72d591 --- /dev/null +++ b/data/monster/Glires/squirrel.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Goblins/goblin.xml b/data/monster/Goblins/goblin.xml new file mode 100644 index 0000000..fcc9dcb --- /dev/null +++ b/data/monster/Goblins/goblin.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Goblins/goblin_assassin.xml b/data/monster/Goblins/goblin_assassin.xml new file mode 100644 index 0000000..8a50465 --- /dev/null +++ b/data/monster/Goblins/goblin_assassin.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Goblins/goblin_leader.xml b/data/monster/Goblins/goblin_leader.xml new file mode 100644 index 0000000..ab6b590 --- /dev/null +++ b/data/monster/Goblins/goblin_leader.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Goblins/goblin_scavenger.xml b/data/monster/Goblins/goblin_scavenger.xml new file mode 100644 index 0000000..85bbdc5 --- /dev/null +++ b/data/monster/Goblins/goblin_scavenger.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Hydro-Elementals/massive_water_elemental.xml b/data/monster/Hydro-Elementals/massive_water_elemental.xml new file mode 100644 index 0000000..89876b7 --- /dev/null +++ b/data/monster/Hydro-Elementals/massive_water_elemental.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Hydro-Elementals/roaring_water_elemental.xml b/data/monster/Hydro-Elementals/roaring_water_elemental.xml new file mode 100644 index 0000000..61cb113 --- /dev/null +++ b/data/monster/Hydro-Elementals/roaring_water_elemental.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Hydro-Elementals/slick_water_elemental.xml b/data/monster/Hydro-Elementals/slick_water_elemental.xml new file mode 100644 index 0000000..f7b4022 --- /dev/null +++ b/data/monster/Hydro-Elementals/slick_water_elemental.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Hydro-Elementals/water_elemental.xml b/data/monster/Hydro-Elementals/water_elemental.xml new file mode 100644 index 0000000..2c48d38 --- /dev/null +++ b/data/monster/Hydro-Elementals/water_elemental.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/ancient_scarab.xml b/data/monster/Insects/ancient_scarab.xml new file mode 100644 index 0000000..160b227 --- /dev/null +++ b/data/monster/Insects/ancient_scarab.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/blue_butterfly.xml b/data/monster/Insects/blue_butterfly.xml new file mode 100644 index 0000000..e10641b --- /dev/null +++ b/data/monster/Insects/blue_butterfly.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/brimstone_bug.xml b/data/monster/Insects/brimstone_bug.xml new file mode 100644 index 0000000..c737498 --- /dev/null +++ b/data/monster/Insects/brimstone_bug.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/bug.xml b/data/monster/Insects/bug.xml new file mode 100644 index 0000000..c80d3cc --- /dev/null +++ b/data/monster/Insects/bug.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/cockroach.xml b/data/monster/Insects/cockroach.xml new file mode 100644 index 0000000..8a2dcc8 --- /dev/null +++ b/data/monster/Insects/cockroach.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/emerald_damselfly.xml b/data/monster/Insects/emerald_damselfly.xml new file mode 100644 index 0000000..a33330f --- /dev/null +++ b/data/monster/Insects/emerald_damselfly.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/hive_overseer.xml b/data/monster/Insects/hive_overseer.xml new file mode 100644 index 0000000..213d570 --- /dev/null +++ b/data/monster/Insects/hive_overseer.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/insect_swarm.xml b/data/monster/Insects/insect_swarm.xml new file mode 100644 index 0000000..eed5d03 --- /dev/null +++ b/data/monster/Insects/insect_swarm.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/insectoid_scout.xml b/data/monster/Insects/insectoid_scout.xml new file mode 100644 index 0000000..3eab531 --- /dev/null +++ b/data/monster/Insects/insectoid_scout.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/insectoid_worker.xml b/data/monster/Insects/insectoid_worker.xml new file mode 100644 index 0000000..4d1d177 --- /dev/null +++ b/data/monster/Insects/insectoid_worker.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/kollos.xml b/data/monster/Insects/kollos.xml new file mode 100644 index 0000000..da982df --- /dev/null +++ b/data/monster/Insects/kollos.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/ladybug.xml b/data/monster/Insects/ladybug.xml new file mode 100644 index 0000000..6cbbaf3 --- /dev/null +++ b/data/monster/Insects/ladybug.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/lancer_beetle.xml b/data/monster/Insects/lancer_beetle.xml new file mode 100644 index 0000000..3e008f5 --- /dev/null +++ b/data/monster/Insects/lancer_beetle.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/larva.xml b/data/monster/Insects/larva.xml new file mode 100644 index 0000000..d25d962 --- /dev/null +++ b/data/monster/Insects/larva.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/lesser_swarmer.xml b/data/monster/Insects/lesser_swarmer.xml new file mode 100644 index 0000000..2381a0e --- /dev/null +++ b/data/monster/Insects/lesser_swarmer.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/parasite.xml b/data/monster/Insects/parasite.xml new file mode 100644 index 0000000..3f24223 --- /dev/null +++ b/data/monster/Insects/parasite.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/purple_butterfly.xml b/data/monster/Insects/purple_butterfly.xml new file mode 100644 index 0000000..c09c32b --- /dev/null +++ b/data/monster/Insects/purple_butterfly.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/red_butterfly.xml b/data/monster/Insects/red_butterfly.xml new file mode 100644 index 0000000..8c6be71 --- /dev/null +++ b/data/monster/Insects/red_butterfly.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/sandcrawler.xml b/data/monster/Insects/sandcrawler.xml new file mode 100644 index 0000000..51b1b9e --- /dev/null +++ b/data/monster/Insects/sandcrawler.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/scarab.xml b/data/monster/Insects/scarab.xml new file mode 100644 index 0000000..63b654b --- /dev/null +++ b/data/monster/Insects/scarab.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/spidris.xml b/data/monster/Insects/spidris.xml new file mode 100644 index 0000000..c721d0b --- /dev/null +++ b/data/monster/Insects/spidris.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/spidris_elite.xml b/data/monster/Insects/spidris_elite.xml new file mode 100644 index 0000000..ce89cc8 --- /dev/null +++ b/data/monster/Insects/spidris_elite.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/spitter.xml b/data/monster/Insects/spitter.xml new file mode 100644 index 0000000..a3eb4e9 --- /dev/null +++ b/data/monster/Insects/spitter.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/swarmer.xml b/data/monster/Insects/swarmer.xml new file mode 100644 index 0000000..0c8d00c --- /dev/null +++ b/data/monster/Insects/swarmer.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/swarmer_hatchling.xml b/data/monster/Insects/swarmer_hatchling.xml new file mode 100644 index 0000000..6d024d6 --- /dev/null +++ b/data/monster/Insects/swarmer_hatchling.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/terramite.xml b/data/monster/Insects/terramite.xml new file mode 100644 index 0000000..3f93c04 --- /dev/null +++ b/data/monster/Insects/terramite.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/wasp.xml b/data/monster/Insects/wasp.xml new file mode 100644 index 0000000..c8ee4c6 --- /dev/null +++ b/data/monster/Insects/wasp.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/waspoid.xml b/data/monster/Insects/waspoid.xml new file mode 100644 index 0000000..7f3bdc6 --- /dev/null +++ b/data/monster/Insects/waspoid.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Insects/yellow_butterfly.xml b/data/monster/Insects/yellow_butterfly.xml new file mode 100644 index 0000000..a0c42c5 --- /dev/null +++ b/data/monster/Insects/yellow_butterfly.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/draken_abomination.xml b/data/monster/Lizards/draken_abomination.xml new file mode 100644 index 0000000..f40d5de --- /dev/null +++ b/data/monster/Lizards/draken_abomination.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/draken_elite.xml b/data/monster/Lizards/draken_elite.xml new file mode 100644 index 0000000..d3fe168 --- /dev/null +++ b/data/monster/Lizards/draken_elite.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/draken_spellweaver.xml b/data/monster/Lizards/draken_spellweaver.xml new file mode 100644 index 0000000..90d3705 --- /dev/null +++ b/data/monster/Lizards/draken_spellweaver.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/draken_warmaster.xml b/data/monster/Lizards/draken_warmaster.xml new file mode 100644 index 0000000..d106512 --- /dev/null +++ b/data/monster/Lizards/draken_warmaster.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/lizard_chosen.xml b/data/monster/Lizards/lizard_chosen.xml new file mode 100644 index 0000000..d0e94dd --- /dev/null +++ b/data/monster/Lizards/lizard_chosen.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/lizard_dragon_priest.xml b/data/monster/Lizards/lizard_dragon_priest.xml new file mode 100644 index 0000000..1e0dcb6 --- /dev/null +++ b/data/monster/Lizards/lizard_dragon_priest.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/lizard_high_guard.xml b/data/monster/Lizards/lizard_high_guard.xml new file mode 100644 index 0000000..22068b6 --- /dev/null +++ b/data/monster/Lizards/lizard_high_guard.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/lizard_legionnaire.xml b/data/monster/Lizards/lizard_legionnaire.xml new file mode 100644 index 0000000..2eaa220 --- /dev/null +++ b/data/monster/Lizards/lizard_legionnaire.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/lizard_magistratus.xml b/data/monster/Lizards/lizard_magistratus.xml new file mode 100644 index 0000000..b0de4ad --- /dev/null +++ b/data/monster/Lizards/lizard_magistratus.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/lizard_noble.xml b/data/monster/Lizards/lizard_noble.xml new file mode 100644 index 0000000..eedb644 --- /dev/null +++ b/data/monster/Lizards/lizard_noble.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/lizard_sentinel.xml b/data/monster/Lizards/lizard_sentinel.xml new file mode 100644 index 0000000..7674516 --- /dev/null +++ b/data/monster/Lizards/lizard_sentinel.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/lizard_snakecharmer.xml b/data/monster/Lizards/lizard_snakecharmer.xml new file mode 100644 index 0000000..d3ff1b1 --- /dev/null +++ b/data/monster/Lizards/lizard_snakecharmer.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/lizard_templar.xml b/data/monster/Lizards/lizard_templar.xml new file mode 100644 index 0000000..2a3541a --- /dev/null +++ b/data/monster/Lizards/lizard_templar.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/lizard_zaogun.xml b/data/monster/Lizards/lizard_zaogun.xml new file mode 100644 index 0000000..a10f504 --- /dev/null +++ b/data/monster/Lizards/lizard_zaogun.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Lizards/wyvern.xml b/data/monster/Lizards/wyvern.xml new file mode 100644 index 0000000..8e41492 --- /dev/null +++ b/data/monster/Lizards/wyvern.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Minotaurs/minotaur.xml b/data/monster/Minotaurs/minotaur.xml new file mode 100644 index 0000000..654d9f7 --- /dev/null +++ b/data/monster/Minotaurs/minotaur.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Minotaurs/minotaur_archer.xml b/data/monster/Minotaurs/minotaur_archer.xml new file mode 100644 index 0000000..e522994 --- /dev/null +++ b/data/monster/Minotaurs/minotaur_archer.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Minotaurs/minotaur_guard.xml b/data/monster/Minotaurs/minotaur_guard.xml new file mode 100644 index 0000000..386149a --- /dev/null +++ b/data/monster/Minotaurs/minotaur_guard.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Minotaurs/minotaur_mage.xml b/data/monster/Minotaurs/minotaur_mage.xml new file mode 100644 index 0000000..9876dda --- /dev/null +++ b/data/monster/Minotaurs/minotaur_mage.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Misc/Misc Mammals/badger.xml b/data/monster/Misc/Misc Mammals/badger.xml new file mode 100644 index 0000000..8508265 --- /dev/null +++ b/data/monster/Misc/Misc Mammals/badger.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Misc/Misc Mammals/ghoulish_hyaena.xml b/data/monster/Misc/Misc Mammals/ghoulish_hyaena.xml new file mode 100644 index 0000000..56aa8a8 --- /dev/null +++ b/data/monster/Misc/Misc Mammals/ghoulish_hyaena.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Misc/Misc Mammals/hyaena.xml b/data/monster/Misc/Misc Mammals/hyaena.xml new file mode 100644 index 0000000..9d50d9b --- /dev/null +++ b/data/monster/Misc/Misc Mammals/hyaena.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Misc/Misc Mammals/skunk.xml b/data/monster/Misc/Misc Mammals/skunk.xml new file mode 100644 index 0000000..3b6af0d --- /dev/null +++ b/data/monster/Misc/Misc Mammals/skunk.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Misc/Misc Mammals/water_buffalo.xml b/data/monster/Misc/Misc Mammals/water_buffalo.xml new file mode 100644 index 0000000..0e3397a --- /dev/null +++ b/data/monster/Misc/Misc Mammals/water_buffalo.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Misc/Misc Mounts/donkey.xml b/data/monster/Misc/Misc Mounts/donkey.xml new file mode 100644 index 0000000..6af4eda --- /dev/null +++ b/data/monster/Misc/Misc Mounts/donkey.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Misc/Misc Mounts/modified_gnarlhound.xml b/data/monster/Misc/Misc Mounts/modified_gnarlhound.xml new file mode 100644 index 0000000..31f55f4 --- /dev/null +++ b/data/monster/Misc/Misc Mounts/modified_gnarlhound.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Misc/Misc Reptiles/crocodile.xml b/data/monster/Misc/Misc Reptiles/crocodile.xml new file mode 100644 index 0000000..0b003b3 --- /dev/null +++ b/data/monster/Misc/Misc Reptiles/crocodile.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Misc/Misc Reptiles/hydra.xml b/data/monster/Misc/Misc Reptiles/hydra.xml new file mode 100644 index 0000000..0e486ec --- /dev/null +++ b/data/monster/Misc/Misc Reptiles/hydra.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Misc/Misc Reptiles/killer_caiman.xml b/data/monster/Misc/Misc Reptiles/killer_caiman.xml new file mode 100644 index 0000000..0576905 --- /dev/null +++ b/data/monster/Misc/Misc Reptiles/killer_caiman.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Misc/Misc Reptiles/thornback_tortoise.xml b/data/monster/Misc/Misc Reptiles/thornback_tortoise.xml new file mode 100644 index 0000000..8b65547 --- /dev/null +++ b/data/monster/Misc/Misc Reptiles/thornback_tortoise.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Misc/Misc Reptiles/tortoise.xml b/data/monster/Misc/Misc Reptiles/tortoise.xml new file mode 100644 index 0000000..920aa9f --- /dev/null +++ b/data/monster/Misc/Misc Reptiles/tortoise.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Mollusks/calamary.xml b/data/monster/Mollusks/calamary.xml new file mode 100644 index 0000000..13cad3a --- /dev/null +++ b/data/monster/Mollusks/calamary.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Mollusks/slug.xml b/data/monster/Mollusks/slug.xml new file mode 100644 index 0000000..a826c11 --- /dev/null +++ b/data/monster/Mollusks/slug.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Monks/dark_monk.xml b/data/monster/Monks/dark_monk.xml new file mode 100644 index 0000000..84e4f82 --- /dev/null +++ b/data/monster/Monks/dark_monk.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Monks/monk.xml b/data/monster/Monks/monk.xml new file mode 100644 index 0000000..fb2acbe --- /dev/null +++ b/data/monster/Monks/monk.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Mutated/mutated_human.xml b/data/monster/Mutated/mutated_human.xml new file mode 100644 index 0000000..2646325 --- /dev/null +++ b/data/monster/Mutated/mutated_human.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Mutated/mutated_rat.xml b/data/monster/Mutated/mutated_rat.xml new file mode 100644 index 0000000..748aeac --- /dev/null +++ b/data/monster/Mutated/mutated_rat.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Mutated/mutated_tiger.xml b/data/monster/Mutated/mutated_tiger.xml new file mode 100644 index 0000000..766018f --- /dev/null +++ b/data/monster/Mutated/mutated_tiger.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Myriapods/centipede.xml b/data/monster/Myriapods/centipede.xml new file mode 100644 index 0000000..1804f86 --- /dev/null +++ b/data/monster/Myriapods/centipede.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Myriapods/crawler.xml b/data/monster/Myriapods/crawler.xml new file mode 100644 index 0000000..d674534 --- /dev/null +++ b/data/monster/Myriapods/crawler.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Myriapods/wiggler.xml b/data/monster/Myriapods/wiggler.xml new file mode 100644 index 0000000..d10759d --- /dev/null +++ b/data/monster/Myriapods/wiggler.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Necromancers/blood_hand.xml b/data/monster/Necromancers/blood_hand.xml new file mode 100644 index 0000000..55865c7 --- /dev/null +++ b/data/monster/Necromancers/blood_hand.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Necromancers/blood_priest.xml b/data/monster/Necromancers/blood_priest.xml new file mode 100644 index 0000000..c62984a --- /dev/null +++ b/data/monster/Necromancers/blood_priest.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Necromancers/necromancer.xml b/data/monster/Necromancers/necromancer.xml new file mode 100644 index 0000000..e4dea0e --- /dev/null +++ b/data/monster/Necromancers/necromancer.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Necromancers/necromancer_servant.xml b/data/monster/Necromancers/necromancer_servant.xml new file mode 100644 index 0000000..1e91230 --- /dev/null +++ b/data/monster/Necromancers/necromancer_servant.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Necromancers/priestess.xml b/data/monster/Necromancers/priestess.xml new file mode 100644 index 0000000..5791e92 --- /dev/null +++ b/data/monster/Necromancers/priestess.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Necromancers/shadow_pupil.xml b/data/monster/Necromancers/shadow_pupil.xml new file mode 100644 index 0000000..d02346f --- /dev/null +++ b/data/monster/Necromancers/shadow_pupil.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Orcs/furious_orc_berserker.xml b/data/monster/Orcs/furious_orc_berserker.xml new file mode 100644 index 0000000..623030b --- /dev/null +++ b/data/monster/Orcs/furious_orc_berserker.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Orcs/orc.xml b/data/monster/Orcs/orc.xml new file mode 100644 index 0000000..cbc77be --- /dev/null +++ b/data/monster/Orcs/orc.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Orcs/orc_berserker.xml b/data/monster/Orcs/orc_berserker.xml new file mode 100644 index 0000000..1eb752d --- /dev/null +++ b/data/monster/Orcs/orc_berserker.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Orcs/orc_leader.xml b/data/monster/Orcs/orc_leader.xml new file mode 100644 index 0000000..c250241 --- /dev/null +++ b/data/monster/Orcs/orc_leader.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Orcs/orc_marauder.xml b/data/monster/Orcs/orc_marauder.xml new file mode 100644 index 0000000..9d0c989 --- /dev/null +++ b/data/monster/Orcs/orc_marauder.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Orcs/orc_rider.xml b/data/monster/Orcs/orc_rider.xml new file mode 100644 index 0000000..a875770 --- /dev/null +++ b/data/monster/Orcs/orc_rider.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Orcs/orc_shaman.xml b/data/monster/Orcs/orc_shaman.xml new file mode 100644 index 0000000..1e5f718 --- /dev/null +++ b/data/monster/Orcs/orc_shaman.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Orcs/orc_spearman.xml b/data/monster/Orcs/orc_spearman.xml new file mode 100644 index 0000000..be70c68 --- /dev/null +++ b/data/monster/Orcs/orc_spearman.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Orcs/orc_warlord.xml b/data/monster/Orcs/orc_warlord.xml new file mode 100644 index 0000000..e566171 --- /dev/null +++ b/data/monster/Orcs/orc_warlord.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Orcs/orc_warrior.xml b/data/monster/Orcs/orc_warrior.xml new file mode 100644 index 0000000..d44716c --- /dev/null +++ b/data/monster/Orcs/orc_warrior.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Orcs/rorc.xml b/data/monster/Orcs/rorc.xml new file mode 100644 index 0000000..0b124bd --- /dev/null +++ b/data/monster/Orcs/rorc.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/adventurer.xml b/data/monster/Outlaws/adventurer.xml new file mode 100644 index 0000000..1ce2f17 --- /dev/null +++ b/data/monster/Outlaws/adventurer.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/angry_adventurer.xml b/data/monster/Outlaws/angry_adventurer.xml new file mode 100644 index 0000000..66ff7af --- /dev/null +++ b/data/monster/Outlaws/angry_adventurer.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/assassin.xml b/data/monster/Outlaws/assassin.xml new file mode 100644 index 0000000..50bf054 --- /dev/null +++ b/data/monster/Outlaws/assassin.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/bandit.xml b/data/monster/Outlaws/bandit.xml new file mode 100644 index 0000000..828ea41 --- /dev/null +++ b/data/monster/Outlaws/bandit.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/black_knight.xml b/data/monster/Outlaws/black_knight.xml new file mode 100644 index 0000000..9845dfe --- /dev/null +++ b/data/monster/Outlaws/black_knight.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/crazed_beggar.xml b/data/monster/Outlaws/crazed_beggar.xml new file mode 100644 index 0000000..f5ba2b7 --- /dev/null +++ b/data/monster/Outlaws/crazed_beggar.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/crypt_defiler.xml b/data/monster/Outlaws/crypt_defiler.xml new file mode 100644 index 0000000..3afdcc2 --- /dev/null +++ b/data/monster/Outlaws/crypt_defiler.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/feverish_citizen.xml b/data/monster/Outlaws/feverish_citizen.xml new file mode 100644 index 0000000..00159ee --- /dev/null +++ b/data/monster/Outlaws/feverish_citizen.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/gang_member.xml b/data/monster/Outlaws/gang_member.xml new file mode 100644 index 0000000..b26cd54 --- /dev/null +++ b/data/monster/Outlaws/gang_member.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/gladiator.xml b/data/monster/Outlaws/gladiator.xml new file mode 100644 index 0000000..8834198 --- /dev/null +++ b/data/monster/Outlaws/gladiator.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/grave_robber.xml b/data/monster/Outlaws/grave_robber.xml new file mode 100644 index 0000000..144e835 --- /dev/null +++ b/data/monster/Outlaws/grave_robber.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/hero.xml b/data/monster/Outlaws/hero.xml new file mode 100644 index 0000000..9aba5f9 --- /dev/null +++ b/data/monster/Outlaws/hero.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/hunter.xml b/data/monster/Outlaws/hunter.xml new file mode 100644 index 0000000..a70d97e --- /dev/null +++ b/data/monster/Outlaws/hunter.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/nomad.xml b/data/monster/Outlaws/nomad.xml new file mode 100644 index 0000000..26f63d1 --- /dev/null +++ b/data/monster/Outlaws/nomad.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/poacher.xml b/data/monster/Outlaws/poacher.xml new file mode 100644 index 0000000..59e8ded --- /dev/null +++ b/data/monster/Outlaws/poacher.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/smuggler.xml b/data/monster/Outlaws/smuggler.xml new file mode 100644 index 0000000..0d6ec64 --- /dev/null +++ b/data/monster/Outlaws/smuggler.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/stalker.xml b/data/monster/Outlaws/stalker.xml new file mode 100644 index 0000000..a02e776 --- /dev/null +++ b/data/monster/Outlaws/stalker.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Outlaws/wild_warrior.xml b/data/monster/Outlaws/wild_warrior.xml new file mode 100644 index 0000000..1587c49 --- /dev/null +++ b/data/monster/Outlaws/wild_warrior.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pharaohs/ashmunrah.xml b/data/monster/Pharaohs/ashmunrah.xml new file mode 100644 index 0000000..f5ec4cc --- /dev/null +++ b/data/monster/Pharaohs/ashmunrah.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pharaohs/dipthrah.xml b/data/monster/Pharaohs/dipthrah.xml new file mode 100644 index 0000000..0214851 --- /dev/null +++ b/data/monster/Pharaohs/dipthrah.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pharaohs/mahrdis.xml b/data/monster/Pharaohs/mahrdis.xml new file mode 100644 index 0000000..f76cee6 --- /dev/null +++ b/data/monster/Pharaohs/mahrdis.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pharaohs/morguthis.xml b/data/monster/Pharaohs/morguthis.xml new file mode 100644 index 0000000..9259c2a --- /dev/null +++ b/data/monster/Pharaohs/morguthis.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pharaohs/omruc.xml b/data/monster/Pharaohs/omruc.xml new file mode 100644 index 0000000..fc065dd --- /dev/null +++ b/data/monster/Pharaohs/omruc.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pharaohs/rahemos.xml b/data/monster/Pharaohs/rahemos.xml new file mode 100644 index 0000000..b4f8094 --- /dev/null +++ b/data/monster/Pharaohs/rahemos.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pharaohs/thalas.xml b/data/monster/Pharaohs/thalas.xml new file mode 100644 index 0000000..934335f --- /dev/null +++ b/data/monster/Pharaohs/thalas.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pharaohs/vashresamun.xml b/data/monster/Pharaohs/vashresamun.xml new file mode 100644 index 0000000..50a1163 --- /dev/null +++ b/data/monster/Pharaohs/vashresamun.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pirates/pirate_buccaneer.xml b/data/monster/Pirates/pirate_buccaneer.xml new file mode 100644 index 0000000..d356da3 --- /dev/null +++ b/data/monster/Pirates/pirate_buccaneer.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pirates/pirate_corsair.xml b/data/monster/Pirates/pirate_corsair.xml new file mode 100644 index 0000000..764870d --- /dev/null +++ b/data/monster/Pirates/pirate_corsair.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pirates/pirate_cutthroat.xml b/data/monster/Pirates/pirate_cutthroat.xml new file mode 100644 index 0000000..a2e48a8 --- /dev/null +++ b/data/monster/Pirates/pirate_cutthroat.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pirates/pirate_ghost.xml b/data/monster/Pirates/pirate_ghost.xml new file mode 100644 index 0000000..5034b0f --- /dev/null +++ b/data/monster/Pirates/pirate_ghost.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pirates/pirate_marauder.xml b/data/monster/Pirates/pirate_marauder.xml new file mode 100644 index 0000000..bc543b9 --- /dev/null +++ b/data/monster/Pirates/pirate_marauder.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pirates/pirate_skeleton.xml b/data/monster/Pirates/pirate_skeleton.xml new file mode 100644 index 0000000..fea2637 --- /dev/null +++ b/data/monster/Pirates/pirate_skeleton.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pyro-Elementals/blazing_fire_elemental.xml b/data/monster/Pyro-Elementals/blazing_fire_elemental.xml new file mode 100644 index 0000000..ddc87a8 --- /dev/null +++ b/data/monster/Pyro-Elementals/blazing_fire_elemental.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pyro-Elementals/blistering_fire_elemental.xml b/data/monster/Pyro-Elementals/blistering_fire_elemental.xml new file mode 100644 index 0000000..2d59deb --- /dev/null +++ b/data/monster/Pyro-Elementals/blistering_fire_elemental.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pyro-Elementals/fire_elemental.xml b/data/monster/Pyro-Elementals/fire_elemental.xml new file mode 100644 index 0000000..ea6c6b7 --- /dev/null +++ b/data/monster/Pyro-Elementals/fire_elemental.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pyro-Elementals/hellfire_fighter.xml b/data/monster/Pyro-Elementals/hellfire_fighter.xml new file mode 100644 index 0000000..a8380ea --- /dev/null +++ b/data/monster/Pyro-Elementals/hellfire_fighter.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pyro-Elementals/infected_weeper.xml b/data/monster/Pyro-Elementals/infected_weeper.xml new file mode 100644 index 0000000..bd70b8f --- /dev/null +++ b/data/monster/Pyro-Elementals/infected_weeper.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pyro-Elementals/lava_golem.xml b/data/monster/Pyro-Elementals/lava_golem.xml new file mode 100644 index 0000000..6946b77 --- /dev/null +++ b/data/monster/Pyro-Elementals/lava_golem.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pyro-Elementals/magma_crawler.xml b/data/monster/Pyro-Elementals/magma_crawler.xml new file mode 100644 index 0000000..e953c9f --- /dev/null +++ b/data/monster/Pyro-Elementals/magma_crawler.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pyro-Elementals/massive_fire_elemental.xml b/data/monster/Pyro-Elementals/massive_fire_elemental.xml new file mode 100644 index 0000000..f18729d --- /dev/null +++ b/data/monster/Pyro-Elementals/massive_fire_elemental.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pyro-Elementals/orewalker.xml b/data/monster/Pyro-Elementals/orewalker.xml new file mode 100644 index 0000000..6e0431c --- /dev/null +++ b/data/monster/Pyro-Elementals/orewalker.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Pyro-Elementals/weeper.xml b/data/monster/Pyro-Elementals/weeper.xml new file mode 100644 index 0000000..9d495b5 --- /dev/null +++ b/data/monster/Pyro-Elementals/weeper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Quaras/quara_constrictor.xml b/data/monster/Quaras/quara_constrictor.xml new file mode 100644 index 0000000..7376831 --- /dev/null +++ b/data/monster/Quaras/quara_constrictor.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Quaras/quara_constrictor_scout.xml b/data/monster/Quaras/quara_constrictor_scout.xml new file mode 100644 index 0000000..7defefc --- /dev/null +++ b/data/monster/Quaras/quara_constrictor_scout.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Quaras/quara_hydromancer.xml b/data/monster/Quaras/quara_hydromancer.xml new file mode 100644 index 0000000..4719b39 --- /dev/null +++ b/data/monster/Quaras/quara_hydromancer.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Quaras/quara_hydromancer_scout.xml b/data/monster/Quaras/quara_hydromancer_scout.xml new file mode 100644 index 0000000..0702128 --- /dev/null +++ b/data/monster/Quaras/quara_hydromancer_scout.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Quaras/quara_mantassin.xml b/data/monster/Quaras/quara_mantassin.xml new file mode 100644 index 0000000..ab85d47 --- /dev/null +++ b/data/monster/Quaras/quara_mantassin.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Quaras/quara_mantassin_scout.xml b/data/monster/Quaras/quara_mantassin_scout.xml new file mode 100644 index 0000000..b5f52d1 --- /dev/null +++ b/data/monster/Quaras/quara_mantassin_scout.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Quaras/quara_pincher.xml b/data/monster/Quaras/quara_pincher.xml new file mode 100644 index 0000000..7494fbd --- /dev/null +++ b/data/monster/Quaras/quara_pincher.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Quaras/quara_pincher_scout.xml b/data/monster/Quaras/quara_pincher_scout.xml new file mode 100644 index 0000000..ed0d419 --- /dev/null +++ b/data/monster/Quaras/quara_pincher_scout.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Quaras/quara_predator.xml b/data/monster/Quaras/quara_predator.xml new file mode 100644 index 0000000..9367a3e --- /dev/null +++ b/data/monster/Quaras/quara_predator.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Quaras/quara_predator_scout.xml b/data/monster/Quaras/quara_predator_scout.xml new file mode 100644 index 0000000..c5f4b8b --- /dev/null +++ b/data/monster/Quaras/quara_predator_scout.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Salamanders/salamander.xml b/data/monster/Salamanders/salamander.xml new file mode 100644 index 0000000..8b5c04f --- /dev/null +++ b/data/monster/Salamanders/salamander.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Serpents/cobra.xml b/data/monster/Serpents/cobra.xml new file mode 100644 index 0000000..84abfd6 --- /dev/null +++ b/data/monster/Serpents/cobra.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Serpents/sea_serpent.xml b/data/monster/Serpents/sea_serpent.xml new file mode 100644 index 0000000..dc77198 --- /dev/null +++ b/data/monster/Serpents/sea_serpent.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Serpents/serpent_spawn.xml b/data/monster/Serpents/serpent_spawn.xml new file mode 100644 index 0000000..ec38de7 --- /dev/null +++ b/data/monster/Serpents/serpent_spawn.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Serpents/snake.xml b/data/monster/Serpents/snake.xml new file mode 100644 index 0000000..2537ec5 --- /dev/null +++ b/data/monster/Serpents/snake.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Serpents/young_sea_serpent.xml b/data/monster/Serpents/young_sea_serpent.xml new file mode 100644 index 0000000..88709df --- /dev/null +++ b/data/monster/Serpents/young_sea_serpent.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Shapeshifters/demon_(goblin).xml b/data/monster/Shapeshifters/demon_(goblin).xml new file mode 100644 index 0000000..bbcd2ce --- /dev/null +++ b/data/monster/Shapeshifters/demon_(goblin).xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Shapeshifters/mimic.xml b/data/monster/Shapeshifters/mimic.xml new file mode 100644 index 0000000..52ac748 --- /dev/null +++ b/data/monster/Shapeshifters/mimic.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Skeletons/betrayed_wraith.xml b/data/monster/Skeletons/betrayed_wraith.xml new file mode 100644 index 0000000..ebfef68 --- /dev/null +++ b/data/monster/Skeletons/betrayed_wraith.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Skeletons/bonebeast.xml b/data/monster/Skeletons/bonebeast.xml new file mode 100644 index 0000000..99de2f9 --- /dev/null +++ b/data/monster/Skeletons/bonebeast.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Skeletons/demon_skeleton.xml b/data/monster/Skeletons/demon_skeleton.xml new file mode 100644 index 0000000..3910afd --- /dev/null +++ b/data/monster/Skeletons/demon_skeleton.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Skeletons/dreadbeast.xml b/data/monster/Skeletons/dreadbeast.xml new file mode 100644 index 0000000..f68c8c0 --- /dev/null +++ b/data/monster/Skeletons/dreadbeast.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Skeletons/honour_guard.xml b/data/monster/Skeletons/honour_guard.xml new file mode 100644 index 0000000..843ec0b --- /dev/null +++ b/data/monster/Skeletons/honour_guard.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Skeletons/lost_soul.xml b/data/monster/Skeletons/lost_soul.xml new file mode 100644 index 0000000..e0232c0 --- /dev/null +++ b/data/monster/Skeletons/lost_soul.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Skeletons/party_skeleton.xml b/data/monster/Skeletons/party_skeleton.xml new file mode 100644 index 0000000..e3fb99d --- /dev/null +++ b/data/monster/Skeletons/party_skeleton.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Skeletons/skeleton.xml b/data/monster/Skeletons/skeleton.xml new file mode 100644 index 0000000..90f5484 --- /dev/null +++ b/data/monster/Skeletons/skeleton.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Skeletons/skeleton_warrior.xml b/data/monster/Skeletons/skeleton_warrior.xml new file mode 100644 index 0000000..c6fb6f4 --- /dev/null +++ b/data/monster/Skeletons/skeleton_warrior.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Skeletons/undead_gladiator.xml b/data/monster/Skeletons/undead_gladiator.xml new file mode 100644 index 0000000..d86677c --- /dev/null +++ b/data/monster/Skeletons/undead_gladiator.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Skeletons/undead_mine_worker.xml b/data/monster/Skeletons/undead_mine_worker.xml new file mode 100644 index 0000000..1f1fa47 --- /dev/null +++ b/data/monster/Skeletons/undead_mine_worker.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Sorcerers/dark_apprentice.xml b/data/monster/Sorcerers/dark_apprentice.xml new file mode 100644 index 0000000..a3e366a --- /dev/null +++ b/data/monster/Sorcerers/dark_apprentice.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Sorcerers/dark_magician.xml b/data/monster/Sorcerers/dark_magician.xml new file mode 100644 index 0000000..1ee3a02 --- /dev/null +++ b/data/monster/Sorcerers/dark_magician.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Sorcerers/fury.xml b/data/monster/Sorcerers/fury.xml new file mode 100644 index 0000000..65f3d9c --- /dev/null +++ b/data/monster/Sorcerers/fury.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Sorcerers/ice_witch.xml b/data/monster/Sorcerers/ice_witch.xml new file mode 100644 index 0000000..eb5da0e --- /dev/null +++ b/data/monster/Sorcerers/ice_witch.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Sorcerers/incredibly_old_witch.xml b/data/monster/Sorcerers/incredibly_old_witch.xml new file mode 100644 index 0000000..a6224b6 --- /dev/null +++ b/data/monster/Sorcerers/incredibly_old_witch.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Sorcerers/infernalist.xml b/data/monster/Sorcerers/infernalist.xml new file mode 100644 index 0000000..8e25c38 --- /dev/null +++ b/data/monster/Sorcerers/infernalist.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Sorcerers/mad_scientist.xml b/data/monster/Sorcerers/mad_scientist.xml new file mode 100644 index 0000000..d673434 --- /dev/null +++ b/data/monster/Sorcerers/mad_scientist.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Sorcerers/warlock.xml b/data/monster/Sorcerers/warlock.xml new file mode 100644 index 0000000..625ef83 --- /dev/null +++ b/data/monster/Sorcerers/warlock.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Sorcerers/witch.xml b/data/monster/Sorcerers/witch.xml new file mode 100644 index 0000000..dd2340d --- /dev/null +++ b/data/monster/Sorcerers/witch.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Sorcerers/yalahari.xml b/data/monster/Sorcerers/yalahari.xml new file mode 100644 index 0000000..720ad89 --- /dev/null +++ b/data/monster/Sorcerers/yalahari.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Traps/deathslicer.xml b/data/monster/Traps/deathslicer.xml new file mode 100644 index 0000000..1b3a5d5 --- /dev/null +++ b/data/monster/Traps/deathslicer.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Traps/flamethrower.xml b/data/monster/Traps/flamethrower.xml new file mode 100644 index 0000000..ee74e27 --- /dev/null +++ b/data/monster/Traps/flamethrower.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Traps/hive_pore.xml b/data/monster/Traps/hive_pore.xml new file mode 100644 index 0000000..bb3e04b --- /dev/null +++ b/data/monster/Traps/hive_pore.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Traps/lavahole.xml b/data/monster/Traps/lavahole.xml new file mode 100644 index 0000000..37cb069 --- /dev/null +++ b/data/monster/Traps/lavahole.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Traps/magic_pillar.xml b/data/monster/Traps/magic_pillar.xml new file mode 100644 index 0000000..bc38e46 --- /dev/null +++ b/data/monster/Traps/magic_pillar.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Traps/magicthrower.xml b/data/monster/Traps/magicthrower.xml new file mode 100644 index 0000000..9b7e38e --- /dev/null +++ b/data/monster/Traps/magicthrower.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Traps/plaguethrower.xml b/data/monster/Traps/plaguethrower.xml new file mode 100644 index 0000000..11773f2 --- /dev/null +++ b/data/monster/Traps/plaguethrower.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Traps/shredderthrower.xml b/data/monster/Traps/shredderthrower.xml new file mode 100644 index 0000000..3670dd4 --- /dev/null +++ b/data/monster/Traps/shredderthrower.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Trolls/frost_troll.xml b/data/monster/Trolls/frost_troll.xml new file mode 100644 index 0000000..e4c8746 --- /dev/null +++ b/data/monster/Trolls/frost_troll.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Trolls/furious_troll.xml b/data/monster/Trolls/furious_troll.xml new file mode 100644 index 0000000..5d428b0 --- /dev/null +++ b/data/monster/Trolls/furious_troll.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Trolls/island_troll.xml b/data/monster/Trolls/island_troll.xml new file mode 100644 index 0000000..25b95ff --- /dev/null +++ b/data/monster/Trolls/island_troll.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Trolls/swamp_troll.xml b/data/monster/Trolls/swamp_troll.xml new file mode 100644 index 0000000..3c4d266 --- /dev/null +++ b/data/monster/Trolls/swamp_troll.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Trolls/troll.xml b/data/monster/Trolls/troll.xml new file mode 100644 index 0000000..4bc16d6 --- /dev/null +++ b/data/monster/Trolls/troll.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Trolls/troll_champion.xml b/data/monster/Trolls/troll_champion.xml new file mode 100644 index 0000000..8281abf --- /dev/null +++ b/data/monster/Trolls/troll_champion.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Trolls/troll_guard.xml b/data/monster/Trolls/troll_guard.xml new file mode 100644 index 0000000..80ccc1e --- /dev/null +++ b/data/monster/Trolls/troll_guard.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Trolls/troll_legionnaire.xml b/data/monster/Trolls/troll_legionnaire.xml new file mode 100644 index 0000000..5a08791 --- /dev/null +++ b/data/monster/Trolls/troll_legionnaire.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Trolls/young_troll.xml b/data/monster/Trolls/young_troll.xml new file mode 100644 index 0000000..2b0b240 --- /dev/null +++ b/data/monster/Trolls/young_troll.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Undead_Humanoids/banshee.xml b/data/monster/Undead_Humanoids/banshee.xml new file mode 100644 index 0000000..91c2b5b --- /dev/null +++ b/data/monster/Undead_Humanoids/banshee.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Undead_Humanoids/blightwalker.xml b/data/monster/Undead_Humanoids/blightwalker.xml new file mode 100644 index 0000000..e06002f --- /dev/null +++ b/data/monster/Undead_Humanoids/blightwalker.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Undead_Humanoids/crypt_shambler.xml b/data/monster/Undead_Humanoids/crypt_shambler.xml new file mode 100644 index 0000000..0ec7774 --- /dev/null +++ b/data/monster/Undead_Humanoids/crypt_shambler.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Undead_Humanoids/death_priest.xml b/data/monster/Undead_Humanoids/death_priest.xml new file mode 100644 index 0000000..c21180f --- /dev/null +++ b/data/monster/Undead_Humanoids/death_priest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Undead_Humanoids/elder_mummy.xml b/data/monster/Undead_Humanoids/elder_mummy.xml new file mode 100644 index 0000000..a5e6f4b --- /dev/null +++ b/data/monster/Undead_Humanoids/elder_mummy.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Undead_Humanoids/ghoul.xml b/data/monster/Undead_Humanoids/ghoul.xml new file mode 100644 index 0000000..28b28c3 --- /dev/null +++ b/data/monster/Undead_Humanoids/ghoul.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Undead_Humanoids/grim_reaper.xml b/data/monster/Undead_Humanoids/grim_reaper.xml new file mode 100644 index 0000000..adfcbc4 --- /dev/null +++ b/data/monster/Undead_Humanoids/grim_reaper.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Undead_Humanoids/lich.xml b/data/monster/Undead_Humanoids/lich.xml new file mode 100644 index 0000000..c6e34f5 --- /dev/null +++ b/data/monster/Undead_Humanoids/lich.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Undead_Humanoids/mummy.xml b/data/monster/Undead_Humanoids/mummy.xml new file mode 100644 index 0000000..d209200 --- /dev/null +++ b/data/monster/Undead_Humanoids/mummy.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Undead_Humanoids/tomb_servant.xml b/data/monster/Undead_Humanoids/tomb_servant.xml new file mode 100644 index 0000000..14bcc5b --- /dev/null +++ b/data/monster/Undead_Humanoids/tomb_servant.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Undead_Humanoids/undead_prospector.xml b/data/monster/Undead_Humanoids/undead_prospector.xml new file mode 100644 index 0000000..bbd2d63 --- /dev/null +++ b/data/monster/Undead_Humanoids/undead_prospector.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Undead_Humanoids/zombie.xml b/data/monster/Undead_Humanoids/zombie.xml new file mode 100644 index 0000000..b9df359 --- /dev/null +++ b/data/monster/Undead_Humanoids/zombie.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/black_sheep.xml b/data/monster/Ungulates/black_sheep.xml new file mode 100644 index 0000000..2846b95 --- /dev/null +++ b/data/monster/Ungulates/black_sheep.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/boar.xml b/data/monster/Ungulates/boar.xml new file mode 100644 index 0000000..70e88dc --- /dev/null +++ b/data/monster/Ungulates/boar.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/deer.xml b/data/monster/Ungulates/deer.xml new file mode 100644 index 0000000..506d5f3 --- /dev/null +++ b/data/monster/Ungulates/deer.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/desperate_white_deer.xml b/data/monster/Ungulates/desperate_white_deer.xml new file mode 100644 index 0000000..8cef6d0 --- /dev/null +++ b/data/monster/Ungulates/desperate_white_deer.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/doom_deer.xml b/data/monster/Ungulates/doom_deer.xml new file mode 100644 index 0000000..c035a75 --- /dev/null +++ b/data/monster/Ungulates/doom_deer.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/dromedary.xml b/data/monster/Ungulates/dromedary.xml new file mode 100644 index 0000000..f83b817 --- /dev/null +++ b/data/monster/Ungulates/dromedary.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/elephant.xml b/data/monster/Ungulates/elephant.xml new file mode 100644 index 0000000..72870cb --- /dev/null +++ b/data/monster/Ungulates/elephant.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/enraged_white_deer.xml b/data/monster/Ungulates/enraged_white_deer.xml new file mode 100644 index 0000000..b581277 --- /dev/null +++ b/data/monster/Ungulates/enraged_white_deer.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/evil_sheep.xml b/data/monster/Ungulates/evil_sheep.xml new file mode 100644 index 0000000..ebbdd0e --- /dev/null +++ b/data/monster/Ungulates/evil_sheep.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/evil_sheep_lord.xml b/data/monster/Ungulates/evil_sheep_lord.xml new file mode 100644 index 0000000..c0cb5d8 --- /dev/null +++ b/data/monster/Ungulates/evil_sheep_lord.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/horse(brown).xml b/data/monster/Ungulates/horse(brown).xml new file mode 100644 index 0000000..5e7cdd2 --- /dev/null +++ b/data/monster/Ungulates/horse(brown).xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/horse(fire).xml b/data/monster/Ungulates/horse(fire).xml new file mode 100644 index 0000000..e1ec715 --- /dev/null +++ b/data/monster/Ungulates/horse(fire).xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/horse(grey).xml b/data/monster/Ungulates/horse(grey).xml new file mode 100644 index 0000000..2616459 --- /dev/null +++ b/data/monster/Ungulates/horse(grey).xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/horse.xml b/data/monster/Ungulates/horse.xml new file mode 100644 index 0000000..0757d32 --- /dev/null +++ b/data/monster/Ungulates/horse.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/mad_sheep.xml b/data/monster/Ungulates/mad_sheep.xml new file mode 100644 index 0000000..5e55530 --- /dev/null +++ b/data/monster/Ungulates/mad_sheep.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/mammoth.xml b/data/monster/Ungulates/mammoth.xml new file mode 100644 index 0000000..9b4d0b6 --- /dev/null +++ b/data/monster/Ungulates/mammoth.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/mushroom_sniffer.xml b/data/monster/Ungulates/mushroom_sniffer.xml new file mode 100644 index 0000000..5228f7b --- /dev/null +++ b/data/monster/Ungulates/mushroom_sniffer.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/pig.xml b/data/monster/Ungulates/pig.xml new file mode 100644 index 0000000..cb36f85 --- /dev/null +++ b/data/monster/Ungulates/pig.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/sheep.xml b/data/monster/Ungulates/sheep.xml new file mode 100644 index 0000000..3d1318f --- /dev/null +++ b/data/monster/Ungulates/sheep.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/stampor.xml b/data/monster/Ungulates/stampor.xml new file mode 100644 index 0000000..0bc033c --- /dev/null +++ b/data/monster/Ungulates/stampor.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/terrified_elephant.xml b/data/monster/Ungulates/terrified_elephant.xml new file mode 100644 index 0000000..946430a --- /dev/null +++ b/data/monster/Ungulates/terrified_elephant.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/vampire_pig.xml b/data/monster/Ungulates/vampire_pig.xml new file mode 100644 index 0000000..d16db28 --- /dev/null +++ b/data/monster/Ungulates/vampire_pig.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/white_deer.xml b/data/monster/Ungulates/white_deer.xml new file mode 100644 index 0000000..4044b0a --- /dev/null +++ b/data/monster/Ungulates/white_deer.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Ungulates/wild_horse.xml b/data/monster/Ungulates/wild_horse.xml new file mode 100644 index 0000000..4068938 --- /dev/null +++ b/data/monster/Ungulates/wild_horse.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Vampires/vampire.xml b/data/monster/Vampires/vampire.xml new file mode 100644 index 0000000..169291e --- /dev/null +++ b/data/monster/Vampires/vampire.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Vampires/vampire_bride.xml b/data/monster/Vampires/vampire_bride.xml new file mode 100644 index 0000000..9769190 --- /dev/null +++ b/data/monster/Vampires/vampire_bride.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Vampires/vampire_viscount.xml b/data/monster/Vampires/vampire_viscount.xml new file mode 100644 index 0000000..85c54aa --- /dev/null +++ b/data/monster/Vampires/vampire_viscount.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Voodoo_Cultists/acolyte_of_the_cult.xml b/data/monster/Voodoo_Cultists/acolyte_of_the_cult.xml new file mode 100644 index 0000000..1840c9b --- /dev/null +++ b/data/monster/Voodoo_Cultists/acolyte_of_the_cult.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Voodoo_Cultists/adept_of_the_cult.xml b/data/monster/Voodoo_Cultists/adept_of_the_cult.xml new file mode 100644 index 0000000..e56077a --- /dev/null +++ b/data/monster/Voodoo_Cultists/adept_of_the_cult.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Voodoo_Cultists/enlightened_of_the_cult.xml b/data/monster/Voodoo_Cultists/enlightened_of_the_cult.xml new file mode 100644 index 0000000..239bbcd --- /dev/null +++ b/data/monster/Voodoo_Cultists/enlightened_of_the_cult.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Voodoo_Cultists/novice_of_the_cult.xml b/data/monster/Voodoo_Cultists/novice_of_the_cult.xml new file mode 100644 index 0000000..e923646 --- /dev/null +++ b/data/monster/Voodoo_Cultists/novice_of_the_cult.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Wild_Magics/wild_fire_magic.xml b/data/monster/Wild_Magics/wild_fire_magic.xml new file mode 100644 index 0000000..10e3aa2 --- /dev/null +++ b/data/monster/Wild_Magics/wild_fire_magic.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Wild_Magics/wild_fury_magic.xml b/data/monster/Wild_Magics/wild_fury_magic.xml new file mode 100644 index 0000000..a0b5172 --- /dev/null +++ b/data/monster/Wild_Magics/wild_fury_magic.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Wild_Magics/wild_nature_magic.xml b/data/monster/Wild_Magics/wild_nature_magic.xml new file mode 100644 index 0000000..62ded51 --- /dev/null +++ b/data/monster/Wild_Magics/wild_nature_magic.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/Wild_Magics/wild_water_magic.xml b/data/monster/Wild_Magics/wild_water_magic.xml new file mode 100644 index 0000000..da8542d --- /dev/null +++ b/data/monster/Wild_Magics/wild_water_magic.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/monster/lua/#example.lua b/data/monster/lua/#example.lua new file mode 100644 index 0000000..f0bee8d --- /dev/null +++ b/data/monster/lua/#example.lua @@ -0,0 +1,114 @@ +local mType = Game.createMonsterType("example") +local monster = {} +monster.description = "an example" +monster.experience = 1 +monster.outfit = { + lookType = 37 +} + +monster.health = 99200 +monster.maxHealth = monster.health +monster.race = "fire" +monster.corpse = 5995 +monster.speed = 280 +monster.maxSummons = 2 + +monster.changeTarget = { + interval = 4*1000, + chance = 20 +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + targetDistance = 1, + staticAttackChance = 70 +} + +monster.summons = { + {name = "demon", chance = 10, interval = 2*1000} +} + +monster.voices = { + interval = 5000, + chance = 10, + {text = "I'm an example", yell = false}, + {text = "You shall bow", yell = false} +} + +monster.loot = { + {id = "gold coin", chance = 60000, maxCount = 100}, + {id = 1987, chance = 60000, -- bag + child = { + {id = "platinum coin", chance = 60000, maxCount = 100}, + {id = "crystal coin", chance = 60000, maxCount = 100} + } + } +} + +monster.attacks = { + {name = "melee", attack = 130, skill = 70, effect = CONST_ME_DRAWBLOOD, interval = 2*1000}, + {name = "energy strike", range = 1, chance = 10, interval = 2*1000, minDamage = -210, maxDamage = -300, target = true}, + {name = "combat", type = COMBAT_MANADRAIN, chance = 10, interval = 2*1000, minDamage = 0, maxDamage = -120, target = true, range = 7, effect = CONST_ME_MAGIC_BLUE}, + {name = "combat", type = COMBAT_FIREDAMAGE, chance = 20, interval = 2*1000, minDamage = -150, maxDamage = -250, radius = 1, target = true, effect = CONST_ME_FIREAREA, shootEffect = CONST_ANI_FIRE}, + {name = "speed", chance = 15, interval = 2*1000, speed = -700, radius = 1, target = true, duration = 30*1000, effect = CONST_ME_MAGIC_RED}, + {name = "firefield", chance = 10, interval = 2*1000, range = 7, radius = 1, target = true, shootEffect = CONST_ANI_FIRE}, + {name = "combat", type = COMBAT_LIFEDRAIN, chance = 10, interval = 2*1000, length = 8, spread = 0, minDamage = -300, maxDamage = -490, effect = CONST_ME_PURPLEENERGY} +} + +monster.defenses = { + defense = 55, + armor = 55, + {name = "combat", type = COMBAT_HEALING, chance = 15, interval = 2*1000, minDamage = 180, maxDamage = 250, effect = CONST_ME_MAGIC_BLUE}, + {name = "speed", chance = 15, interval = 2*1000, speed = 320, effect = CONST_ME_MAGIC_RED} +} + +monster.elements = { + {type = COMBAT_PHYSICALDAMAGE, percent = 30}, + {type = COMBAT_DEATHDAMAGE, percent = 30}, + {type = COMBAT_ENERGYDAMAGE, percent = 50}, + {type = COMBAT_EARTHDAMAGE, percent = 40}, + {type = COMBAT_ICEDAMAGE, percent = -10}, + {type = COMBAT_HOLYDAMAGE, percent = -10} +} + +monster.immunities = { + {type = "fire", combat = true, condition = true}, + {type = "drown", condition = true}, + {type = "lifedrain", combat = true}, + {type = "paralyze", condition = true}, + {type = "invisible", condition = true} +} + +mType.onThink = function(monster, interval) + print("I'm thinking") +end + +mType.onAppear = function(monster, creature) + if monster:getId() == creature:getId() then + print(monster:getId(), creature:getId()) + end +end + +mType.onDisappear = function(monster, creature) + if monster:getId() == creature:getId() then + print(monster:getId(), creature:getId()) + end +end + +mType.onMove = function(monster, creature, fromPosition, toPosition) + if monster:getId() == creature:getId() then + print(monster:getId(), creature:getId(), fromPosition, toPosition) + end +end + +mType.onSay = function(monster, creature, type, message) + print(monster:getId(), creature:getId(), type, message) +end + +mType:register(monster) diff --git a/data/monster/monsters.xml b/data/monster/monsters.xml new file mode 100644 index 0000000..7bb645b --- /dev/null +++ b/data/monster/monsters.xml @@ -0,0 +1,818 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/movements/lib/movements.lua b/data/movements/lib/movements.lua new file mode 100644 index 0000000..b081a0f --- /dev/null +++ b/data/movements/lib/movements.lua @@ -0,0 +1,2 @@ +-- Nothing -- + diff --git a/data/movements/movements.xml b/data/movements/movements.xml new file mode 100644 index 0000000..0430546 --- /dev/null +++ b/data/movements/movements.xml @@ -0,0 +1,1422 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/movements/scripts/citizen.lua b/data/movements/scripts/citizen.lua new file mode 100644 index 0000000..e1bf300 --- /dev/null +++ b/data/movements/scripts/citizen.lua @@ -0,0 +1,10 @@ +function onStepIn(creature, item, position, fromPosition) + if item.actionid > actionIds.citizenship and item.actionid < actionIds.citizenshipLast then + if not creature:isPlayer() then + return false + end + + creature:setTown(Town(item.actionid - actionIds.citizenship)) + end + return true +end diff --git a/data/movements/scripts/closingdoor.lua b/data/movements/scripts/closingdoor.lua new file mode 100644 index 0000000..83904ae --- /dev/null +++ b/data/movements/scripts/closingdoor.lua @@ -0,0 +1,31 @@ +function onStepOut(creature, item, position, fromPosition) + local tile = Tile(position) + if tile:getCreatureCount() > 0 then + return true + end + + local newPosition = {x = position.x + 1, y = position.y, z = position.z} + local query = Tile(newPosition):queryAdd(creature) + if query ~= RETURNVALUE_NOERROR or query == RETURNVALUE_NOTENOUGHROOM then + newPosition.x = newPosition.x - 1 + newPosition.y = newPosition.y + 1 + query = Tile(newPosition):queryAdd(creature) + end + + if query == RETURNVALUE_NOERROR or query ~= RETURNVALUE_NOTENOUGHROOM then + doRelocate(position, newPosition) + end + + local i, tileItem, tileCount = 1, true, tile:getThingCount() + while tileItem and i < tileCount do + tileItem = tile:getThing(i) + if tileItem and tileItem:getUniqueId() ~= item.uid and tileItem:getType():isMovable() then + tileItem:remove() + else + i = i + 1 + end + end + + item:transform(item.itemid - 1) + return true +end diff --git a/data/movements/scripts/decay.lua b/data/movements/scripts/decay.lua new file mode 100644 index 0000000..9a48822 --- /dev/null +++ b/data/movements/scripts/decay.lua @@ -0,0 +1,5 @@ +function onStepIn(creature, item, position, fromPosition) + item:transform(item.itemid + 1) + item:decay() + return true +end diff --git a/data/movements/scripts/dough.lua b/data/movements/scripts/dough.lua new file mode 100644 index 0000000..a0b371f --- /dev/null +++ b/data/movements/scripts/dough.lua @@ -0,0 +1,10 @@ +function onAddItem(moveitem, tileitem, position) + if moveitem:getId() == 2693 then + moveitem:transform(2689) + position:sendMagicEffect(CONST_ME_HITBYFIRE) + elseif moveitem:getId() == 6277 then + moveitem:transform(2687, 12) + position:sendMagicEffect(CONST_ME_HITBYFIRE) + end + return true +end diff --git a/data/movements/scripts/drowning.lua b/data/movements/scripts/drowning.lua new file mode 100644 index 0000000..8f6525f --- /dev/null +++ b/data/movements/scripts/drowning.lua @@ -0,0 +1,21 @@ +local condition = Condition(CONDITION_DROWN) +condition:setParameter(CONDITION_PARAM_PERIODICDAMAGE, -20) +condition:setParameter(CONDITION_PARAM_TICKS, -1) +condition:setParameter(CONDITION_PARAM_TICKINTERVAL, 2000) + +function onStepIn(creature, item, position, fromPosition) + if creature:isPlayer() then + if math.random(1, 10) == 1 then + position:sendMagicEffect(CONST_ME_BUBBLES) + end + creature:addCondition(condition) + end + return true +end + +function onStepOut(creature, item, position, fromPosition) + if not creature:isPlayer() then + creature:removeCondition(CONDITION_DROWN) + end + return true +end diff --git a/data/movements/scripts/level_door.lua b/data/movements/scripts/level_door.lua new file mode 100644 index 0000000..a6e938b --- /dev/null +++ b/data/movements/scripts/level_door.lua @@ -0,0 +1,12 @@ +function onStepIn(creature, item, position, fromPosition) + if not creature:isPlayer() then + return false + end + + if creature:getLevel() < item.actionid - actionIds.levelDoor then + creature:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Only the worthy may pass.") + creature:teleportTo(fromPosition, true) + return false + end + return true +end diff --git a/data/movements/scripts/quest_door.lua b/data/movements/scripts/quest_door.lua new file mode 100644 index 0000000..455795d --- /dev/null +++ b/data/movements/scripts/quest_door.lua @@ -0,0 +1,12 @@ +function onStepIn(creature, item, position, fromPosition) + if not creature:isPlayer() then + return false + end + + if creature:getStorageValue(item.actionid) == -1 then + creature:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The door seems to be sealed against unwanted intruders.") + creature:teleportTo(fromPosition, true) + return false + end + return true +end diff --git a/data/movements/scripts/snow.lua b/data/movements/scripts/snow.lua new file mode 100644 index 0000000..8a07bbf --- /dev/null +++ b/data/movements/scripts/snow.lua @@ -0,0 +1,13 @@ +function onStepOut(creature, item, position, fromPosition) + if creature:isPlayer() and creature:isInGhostMode() then + return true + end + + if item:getId() == 670 then + item:transform(6594) + else + item:transform(item.itemid + 15) + end + item:decay() + return true +end diff --git a/data/movements/scripts/swimming.lua b/data/movements/scripts/swimming.lua new file mode 100644 index 0000000..8964f44 --- /dev/null +++ b/data/movements/scripts/swimming.lua @@ -0,0 +1,21 @@ +local condition = Condition(CONDITION_OUTFIT) +condition:setOutfit({lookType = 267}) +condition:setTicks(-1) + +function onStepIn(creature, item, position, fromPosition) + if not creature:isPlayer() then + return false + end + + creature:addCondition(condition) + return true +end + +function onStepOut(creature, item, position, fromPosition) + if not creature:isPlayer() then + return false + end + + creature:removeCondition(CONDITION_OUTFIT) + return true +end diff --git a/data/movements/scripts/tiles.lua b/data/movements/scripts/tiles.lua new file mode 100644 index 0000000..c9c0ae0 --- /dev/null +++ b/data/movements/scripts/tiles.lua @@ -0,0 +1,56 @@ +local increasing = {[416] = 417, [426] = 425, [446] = 447, [3216] = 3217, [3202] = 3215, [11062] = 11063} +local decreasing = {[417] = 416, [425] = 426, [447] = 446, [3217] = 3216, [3215] = 3202, [11063] = 11062} + +function onStepIn(creature, item, position, fromPosition) + if not increasing[item.itemid] then + return true + end + + if not creature:isPlayer() or creature:isInGhostMode() then + return true + end + + item:transform(increasing[item.itemid]) + + if item.actionid >= actionIds.levelDoor then + if creature:getLevel() < item.actionid - actionIds.levelDoor then + creature:teleportTo(fromPosition, false) + position:sendMagicEffect(CONST_ME_MAGIC_BLUE) + creature:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The tile seems to be protected against unwanted intruders.") + end + return true + end + + if Tile(position):hasFlag(TILESTATE_PROTECTIONZONE) then + local lookPosition = creature:getPosition() + lookPosition:getNextPosition(creature:getDirection()) + local depotItem = Tile(lookPosition):getItemByType(ITEM_TYPE_DEPOT) + if depotItem then + local depotItems = creature:getDepotChest(getDepotId(depotItem:getUniqueId()), true):getItemHoldingCount() + creature:sendTextMessage(MESSAGE_STATUS_DEFAULT, "Your depot contains " .. depotItems .. " item" .. (depotItems > 1 and "s." or ".")) + creature:addAchievementProgress("Safely Stored Away", 1000) + return true + end + end + + if item.actionid ~= 0 and creature:getStorageValue(item.actionid) <= 0 then + creature:teleportTo(fromPosition, false) + position:sendMagicEffect(CONST_ME_MAGIC_BLUE) + creature:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The tile seems to be protected against unwanted intruders.") + return true + end + return true +end + +function onStepOut(creature, item, position, fromPosition) + if not decreasing[item.itemid] then + return true + end + + if creature:isPlayer() and creature:isInGhostMode() then + return true + end + + item:transform(decreasing[item.itemid]) + return true +end diff --git a/data/movements/scripts/trap.lua b/data/movements/scripts/trap.lua new file mode 100644 index 0000000..0969d88 --- /dev/null +++ b/data/movements/scripts/trap.lua @@ -0,0 +1,36 @@ +local traps = { + [1510] = {transformTo = 1511, damage = {-50, -100}}, + [1513] = {damage = {-50, -100}}, + [2579] = {transformTo = 2578, damage = {-15, -30}}, + [4208] = {transformTo = 4209, damage = {-15, -30}, type = COMBAT_EARTHDAMAGE} +} + +function onStepIn(creature, item, position, fromPosition) + local trap = traps[item.itemid] + if not trap then + return true + end + + if creature:isMonster() then + doTargetCombat(0, creature, trap.type or COMBAT_PHYSICALDAMAGE, trap.damage[1], trap.damage[2], CONST_ME_NONE) + end + + if trap.transformTo then + item:transform(trap.transformTo) + end + return true +end + +function onStepOut(creature, item, position, fromPosition) + item:transform(item.itemid - 1) + return true +end + +function onRemoveItem(item, tile, position) + local itemPosition = item:getPosition() + if itemPosition:getDistance(position) > 0 then + item:transform(item.itemid - 1) + itemPosition:sendMagicEffect(CONST_ME_POFF) + end + return true +end diff --git a/data/movements/scripts/walkback.lua b/data/movements/scripts/walkback.lua new file mode 100644 index 0000000..30b00f2 --- /dev/null +++ b/data/movements/scripts/walkback.lua @@ -0,0 +1,6 @@ +function onStepIn(creature, item, position, fromPosition) + if item.uid > 0 and item.uid <= 65535 then + creature:teleportTo(fromPosition, false) + end + return true +end diff --git a/data/npc/Alice.xml b/data/npc/Alice.xml new file mode 100644 index 0000000..5f02ca5 --- /dev/null +++ b/data/npc/Alice.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/data/npc/Deruno.xml b/data/npc/Deruno.xml new file mode 100644 index 0000000..3015d5f --- /dev/null +++ b/data/npc/Deruno.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/data/npc/Eryn.xml b/data/npc/Eryn.xml new file mode 100644 index 0000000..93622cd --- /dev/null +++ b/data/npc/Eryn.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/data/npc/Riona.xml b/data/npc/Riona.xml new file mode 100644 index 0000000..13a1df4 --- /dev/null +++ b/data/npc/Riona.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/data/npc/The Forgotten King.xml b/data/npc/The Forgotten King.xml new file mode 100644 index 0000000..5f34703 --- /dev/null +++ b/data/npc/The Forgotten King.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/data/npc/The Oracle.xml b/data/npc/The Oracle.xml new file mode 100644 index 0000000..8aec9e7 --- /dev/null +++ b/data/npc/The Oracle.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/data/npc/Tyoric.xml b/data/npc/Tyoric.xml new file mode 100644 index 0000000..38f6114 --- /dev/null +++ b/data/npc/Tyoric.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/data/npc/lib/npc.lua b/data/npc/lib/npc.lua new file mode 100644 index 0000000..87d64be --- /dev/null +++ b/data/npc/lib/npc.lua @@ -0,0 +1,134 @@ +-- Including the Advanced NPC System +dofile('data/npc/lib/npcsystem/npcsystem.lua') + +function msgcontains(message, keyword) + local message, keyword = message:lower(), keyword:lower() + if message == keyword then + return true + end + + return message:find(keyword) and not message:find('(%w+)' .. keyword) +end + +function doNpcSellItem(cid, itemid, amount, subType, ignoreCap, inBackpacks, backpack) + local amount = amount or 1 + local subType = subType or 0 + local item = 0 + if ItemType(itemid):isStackable() then + if inBackpacks then + stuff = Game.createItem(backpack, 1) + item = stuff:addItem(itemid, math.min(100, amount)) + else + stuff = Game.createItem(itemid, math.min(100, amount)) + end + return Player(cid):addItemEx(stuff, ignoreCap) ~= RETURNVALUE_NOERROR and 0 or amount, 0 + end + + local a = 0 + if inBackpacks then + local container, b = Game.createItem(backpack, 1), 1 + for i = 1, amount do + local item = container:addItem(itemid, subType) + if table.contains({(ItemType(backpack):getCapacity() * b), amount}, i) then + if Player(cid):addItemEx(container, ignoreCap) ~= RETURNVALUE_NOERROR then + b = b - 1 + break + end + + a = i + if amount > i then + container = Game.createItem(backpack, 1) + b = b + 1 + end + end + end + return a, b + end + + for i = 1, amount do -- normal method for non-stackable items + local item = Game.createItem(itemid, subType) + if Player(cid):addItemEx(item, ignoreCap) ~= RETURNVALUE_NOERROR then + break + end + a = i + end + return a, 0 +end + +local func = function(cid, text, type, e, pcid) + if Player(pcid):isPlayer() then + local creature = Creature(cid) + creature:say(text, type, false, pcid, creature:getPosition()) + e.done = true + end +end + +function doCreatureSayWithDelay(cid, text, type, delay, e, pcid) + if Player(pcid):isPlayer() then + e.done = false + e.event = addEvent(func, delay < 1 and 1000 or delay, cid, text, type, e, pcid) + end +end + +function doPlayerSellItem(cid, itemid, count, cost) + local player = Player(cid) + if player:removeItem(itemid, count) then + if not player:addMoney(cost) then + error('Could not add money to ' .. player:getName() .. '(' .. cost .. 'gp)') + end + return true + end + return false +end + +function doPlayerBuyItemContainer(cid, containerid, itemid, count, cost, charges) + local player = Player(cid) + if not player:removeTotalMoney(cost) then + return false + end + + for i = 1, count do + local container = Game.createItem(containerid, 1) + for x = 1, ItemType(containerid):getCapacity() do + container:addItem(itemid, charges) + end + + if player:addItemEx(container, true) ~= RETURNVALUE_NOERROR then + return false + end + end + return true +end + +function getCount(string) + local b, e = string:find("%d+") + return b and e and tonumber(string:sub(b, e)) or -1 +end + +function Player.removeTotalMoney(self, amount) + local moneyCount = self:getMoney() + local bankCount = self:getBankBalance() + + if amount <= moneyCount then + self:removeMoney(amount) + return true + + elseif amount <= (moneyCount + bankCount) then + if moneyCount ~= 0 then + self:removeMoney(moneyCount) + local remains = amount - moneyCount + self:setBankBalance(bankCount - remains) + self:sendTextMessage(MESSAGE_INFO_DESCR, ("Paid %d from inventory and %d gold from bank account. Your account balance is now %d gold."):format(moneyCount, amount - moneyCount, self:getBankBalance())) + return true + else + self:setBankBalance(bankCount - amount) + self:sendTextMessage(MESSAGE_INFO_DESCR, ("Paid %d gold from bank account. Your account balance is now %d gold."):format(amount, self:getBankBalance())) + return true + end + end + return false +end + +function Player.getTotalMoney(self) + return self:getMoney() + self:getBankBalance() +end diff --git a/data/npc/lib/npcsystem/keywordhandler.lua b/data/npc/lib/npcsystem/keywordhandler.lua new file mode 100644 index 0000000..de84f49 --- /dev/null +++ b/data/npc/lib/npcsystem/keywordhandler.lua @@ -0,0 +1,174 @@ +-- Advanced NPC System by Jiddo + +if not KeywordHandler then + KeywordNode = { + keywords = nil, + callback = nil, + parameters = nil, + children = nil, + parent = nil + } + + -- Created a new keywordnode with the given keywords, callback function and parameters and without any childNodes. + function KeywordNode:new(keys, func, param) + local obj = {} + obj.keywords = keys + obj.callback = func + obj.parameters = param + obj.children = {} + setmetatable(obj, self) + self.__index = self + return obj + end + + -- Calls the underlying callback function if it is not nil. + function KeywordNode:processMessage(cid, message) + return (not self.callback or self.callback(cid, message, self.keywords, self.parameters, self)) + end + + -- Returns true if message contains all patterns/strings found in keywords. + function KeywordNode:checkMessage(message) + if self.keywords.callback then + return self.keywords.callback(self.keywords, message) + end + + for _, v in ipairs(self.keywords) do + if type(v) == 'string' then + local a, b = string.find(message, v) + if not a or not b then + return false + end + end + end + return true + end + + -- Returns the parent of this node or nil if no such node exists. + function KeywordNode:getParent() + return self.parent + end + + -- Returns an array of the callback function parameters assosiated with this node. + function KeywordNode:getParameters() + return self.parameters + end + + -- Returns an array of the triggering keywords assosiated with this node. + function KeywordNode:getKeywords() + return self.keywords + end + + -- Adds a childNode to this node. Creates the childNode based on the parameters (k = keywords, c = callback, p = parameters) + function KeywordNode:addChildKeyword(keywords, callback, parameters) + local new = KeywordNode:new(keywords, callback, parameters) + return self:addChildKeywordNode(new) + end + + -- Adds a pre-created childNode to this node. Should be used for example if several nodes should have a common child. + function KeywordNode:addChildKeywordNode(childNode) + self.children[#self.children + 1] = childNode + childNode.parent = self + return childNode + end + + KeywordHandler = { + root = nil, + lastNode = nil + } + + -- Creates a new keywordhandler with an empty rootnode. + function KeywordHandler:new() + local obj = {} + obj.root = KeywordNode:new(nil, nil, nil) + obj.lastNode = {} + setmetatable(obj, self) + self.__index = self + return obj + end + + -- Resets the lastNode field, and this resetting the current position in the node hierarchy to root. + function KeywordHandler:reset(cid) + if self.lastNode[cid] then + self.lastNode[cid] = nil + end + end + + -- Makes sure the correct childNode of lastNode gets a chance to process the message. + function KeywordHandler:processMessage(cid, message) + local node = self:getLastNode(cid) + if not node then + error('No root node found.') + return false + end + + local ret = self:processNodeMessage(node, cid, message) + if ret then + return true + end + + if node:getParent() then + node = node:getParent() -- Search through the parent. + local ret = self:processNodeMessage(node, cid, message) + if ret then + return true + end + end + + if node ~= self:getRoot() then + node = self:getRoot() -- Search through the root. + local ret = self:processNodeMessage(node, cid, message) + if ret then + return true + end + end + return false + end + + -- Tries to process the given message using the node parameter's children and calls the node's callback function if found. + -- Returns the childNode which processed the message or nil if no such node was found. + function KeywordHandler:processNodeMessage(node, cid, message) + local messageLower = string.lower(message) + for i, childNode in pairs(node.children) do + if childNode:checkMessage(messageLower) then + local oldLast = self.lastNode[cid] + self.lastNode[cid] = childNode + childNode.parent = node -- Make sure node is the parent of childNode (as one node can be parent to several nodes). + if childNode:processMessage(cid, message) then + return true + end + self.lastNode[cid] = oldLast + end + end + return false + end + + -- Returns the root keywordnode + function KeywordHandler:getRoot() + return self.root + end + + -- Returns the last processed keywordnode or root if no last node is found. + function KeywordHandler:getLastNode(cid) + return self.lastNode[cid] or self:getRoot() + end + + -- Adds a new keyword to the root keywordnode. Returns the new node. + function KeywordHandler:addKeyword(keys, callback, parameters) + return self:getRoot():addChildKeyword(keys, callback, parameters) + end + + -- Moves the current position in the keyword hierarchy steps upwards. Steps defalut value = 1. + function KeywordHandler:moveUp(cid, steps) + if not steps or type(steps) ~= "number" then + steps = 1 + end + + for i = 1, steps do + if not self.lastNode[cid] then + return nil + end + self.lastNode[cid] = self.lastNode[cid]:getParent() or self:getRoot() + end + return self.lastNode[cid] + end +end diff --git a/data/npc/lib/npcsystem/modules.lua b/data/npc/lib/npcsystem/modules.lua new file mode 100644 index 0000000..96307c5 --- /dev/null +++ b/data/npc/lib/npcsystem/modules.lua @@ -0,0 +1,1252 @@ +-- Advanced NPC System by Jiddo + +if Modules == nil then + -- default words for greeting and ungreeting the npc. Should be a table containing all such words. + FOCUS_GREETWORDS = {"hi", "hello"} + FOCUS_FAREWELLWORDS = {"bye", "farewell"} + + -- The words for requesting trade window. + SHOP_TRADEREQUEST = {"trade"} + + -- The word for accepting/declining an offer. CAN ONLY CONTAIN ONE FIELD! Should be a table with a single string value. + SHOP_YESWORD = {"yes"} + SHOP_NOWORD = {"no"} + + -- Pattern used to get the amount of an item a player wants to buy/sell. + PATTERN_COUNT = "%d+" + + -- Constants used to separate buying from selling. + SHOPMODULE_SELL_ITEM = 1 + SHOPMODULE_BUY_ITEM = 2 + SHOPMODULE_BUY_ITEM_CONTAINER = 3 + + -- Constants used for shop mode. Notice: addBuyableItemContainer is working on all modes + SHOPMODULE_MODE_TALK = 1 -- Old system used before client version 8.2: sell/buy item name + SHOPMODULE_MODE_TRADE = 2 -- Trade window system introduced in client version 8.2 + SHOPMODULE_MODE_BOTH = 3 -- Both working at one time + + -- Used shop mode + SHOPMODULE_MODE = SHOPMODULE_MODE_BOTH + + Modules = { + parseableModules = {} + } + + StdModule = {} + + -- These callback function must be called with parameters.npcHandler = npcHandler in the parameters table or they will not work correctly. + -- Notice: The members of StdModule have not yet been tested. If you find any bugs, please report them to me. + -- Usage: + -- keywordHandler:addKeyword({"offer"}, StdModule.say, {npcHandler = npcHandler, text = "I sell many powerful melee weapons."}) + function StdModule.say(cid, message, keywords, parameters, node) + local npcHandler = parameters.npcHandler + if npcHandler == nil then + error("StdModule.say called without any npcHandler instance.") + end + local onlyFocus = (parameters.onlyFocus == nil or parameters.onlyFocus == true) + if not npcHandler:isFocused(cid) and onlyFocus then + return false + end + + local parseInfo = {[TAG_PLAYERNAME] = Player(cid):getName()} + npcHandler:say(npcHandler:parseMessage(parameters.text or parameters.message, parseInfo), cid, parameters.publicize and true) + if parameters.reset then + npcHandler:resetNpc(cid) + elseif parameters.moveup then + npcHandler.keywordHandler:moveUp(cid, parameters.moveup) + end + + return true + end + + --Usage: + -- local node1 = keywordHandler:addKeyword({"promot"}, StdModule.say, {npcHandler = npcHandler, text = "I can promote you for 20000 gold coins. Do you want me to promote you?"}) + -- node1:addChildKeyword({"yes"}, StdModule.promotePlayer, {npcHandler = npcHandler, cost = 20000, level = 20}, text = "Congratulations! You are now promoted.") + -- node1:addChildKeyword({"no"}, StdModule.say, {npcHandler = npcHandler, text = "Allright then. Come back when you are ready."}, reset = true) + function StdModule.promotePlayer(cid, message, keywords, parameters, node) + local npcHandler = parameters.npcHandler + if npcHandler == nil then + error("StdModule.promotePlayer called without any npcHandler instance.") + end + + if not npcHandler:isFocused(cid) then + return false + end + + local player = Player(cid) + if player:isPremium() or not parameters.premium then + local promotion = player:getVocation():getPromotion() + if player:getStorageValue(PlayerStorageKeys.promotion) == 1 then + npcHandler:say("You are already promoted!", cid) + elseif player:getLevel() < parameters.level then + npcHandler:say("I am sorry, but I can only promote you once you have reached level " .. parameters.level .. ".", cid) + elseif not player:removeTotalMoney(parameters.cost) then + npcHandler:say("You do not have enough money!", cid) + else + npcHandler:say(parameters.text, cid) + player:setVocation(promotion) + player:setStorageValue(PlayerStorageKeys.promotion, 1) + end + else + npcHandler:say("You need a premium account in order to get promoted.", cid) + end + npcHandler:resetNpc(cid) + return true + end + + function StdModule.learnSpell(cid, message, keywords, parameters, node) + local npcHandler = parameters.npcHandler + if npcHandler == nil then + error("StdModule.learnSpell called without any npcHandler instance.") + end + + if not npcHandler:isFocused(cid) then + return false + end + + local player = Player(cid) + if player:isPremium() or not parameters.premium then + if player:hasLearnedSpell(parameters.spellName) then + npcHandler:say("You already know this spell.", cid) + elseif not player:canLearnSpell(parameters.spellName) then + npcHandler:say("You cannot learn this spell.", cid) + elseif not player:removeTotalMoney(parameters.price) then + npcHandler:say("You do not have enough money, this spell costs " .. parameters.price .. " gold.", cid) + else + npcHandler:say("You have learned " .. parameters.spellName .. ".", cid) + player:learnSpell(parameters.spellName) + end + else + npcHandler:say("You need a premium account in order to buy " .. parameters.spellName .. ".", cid) + end + npcHandler:resetNpc(cid) + return true + end + + function StdModule.bless(cid, message, keywords, parameters, node) + local npcHandler = parameters.npcHandler + if npcHandler == nil then + error("StdModule.bless called without any npcHandler instance.") + end + + if not npcHandler:isFocused(cid) or Game.getWorldType() == WORLD_TYPE_PVP_ENFORCED then + return false + end + + local player = Player(cid) + if player:isPremium() or not parameters.premium then + if player:hasBlessing(parameters.bless) then + npcHandler:say("Gods have already blessed you with this blessing!", cid) + elseif not player:removeTotalMoney(parameters.cost) then + npcHandler:say("You don't have enough money for blessing.", cid) + else + player:addBlessing(parameters.bless) + npcHandler:say("You have been blessed by one of the five gods!", cid) + end + else + npcHandler:say("You need a premium account in order to be blessed.", cid) + end + npcHandler:resetNpc(cid) + return true + end + + function StdModule.travel(cid, message, keywords, parameters, node) + local npcHandler = parameters.npcHandler + if npcHandler == nil then + error("StdModule.travel called without any npcHandler instance.") + end + + if not npcHandler:isFocused(cid) then + return false + end + + local player = Player(cid) + if player:isPremium() or not parameters.premium then + if player:isPzLocked() then + npcHandler:say("First get rid of those blood stains! You are not going to ruin my vehicle!", cid) + elseif parameters.level and player:getLevel() < parameters.level then + npcHandler:say("You must reach level " .. parameters.level .. " before I can let you go there.", cid) + elseif not player:removeTotalMoney(parameters.cost) then + npcHandler:say("You don't have enough money.", cid) + else + npcHandler:say(parameters.msg or "Set the sails!", cid) + npcHandler:releaseFocus(cid) + + local destination = Position(parameters.destination) + local position = player:getPosition() + player:teleportTo(destination) + + position:sendMagicEffect(CONST_ME_TELEPORT) + destination:sendMagicEffect(CONST_ME_TELEPORT) + end + else + npcHandler:say("I'm sorry, but you need a premium account in order to travel onboard our ships.", cid) + end + npcHandler:resetNpc(cid) + return true + end + + FocusModule = { + npcHandler = nil + } + + -- Creates a new instance of FocusModule without an associated NpcHandler. + function FocusModule:new() + local obj = {} + setmetatable(obj, self) + self.__index = self + return obj + end + + -- Inits the module and associates handler to it. + function FocusModule:init(handler) + self.npcHandler = handler + for i, word in pairs(FOCUS_GREETWORDS) do + local obj = {} + obj[#obj + 1] = word + obj.callback = FOCUS_GREETWORDS.callback or FocusModule.messageMatcher + handler.keywordHandler:addKeyword(obj, FocusModule.onGreet, {module = self}) + end + + for i, word in pairs(FOCUS_FAREWELLWORDS) do + local obj = {} + obj[#obj + 1] = word + obj.callback = FOCUS_FAREWELLWORDS.callback or FocusModule.messageMatcher + handler.keywordHandler:addKeyword(obj, FocusModule.onFarewell, {module = self}) + end + + return true + end + + -- Greeting callback function. + function FocusModule.onGreet(cid, message, keywords, parameters) + parameters.module.npcHandler:onGreet(cid) + return true + end + + -- UnGreeting callback function. + function FocusModule.onFarewell(cid, message, keywords, parameters) + if parameters.module.npcHandler:isFocused(cid) then + parameters.module.npcHandler:onFarewell(cid) + return true + else + return false + end + end + + -- Custom message matching callback function for greeting messages. + function FocusModule.messageMatcher(keywords, message) + for i, word in pairs(keywords) do + if type(word) == "string" then + if string.find(message, word) and not string.find(message, "[%w+]" .. word) and not string.find(message, word .. "[%w+]") then + return true + end + end + end + return false + end + + KeywordModule = { + npcHandler = nil + } + -- Add it to the parseable module list. + Modules.parseableModules["module_keywords"] = KeywordModule + + function KeywordModule:new() + local obj = {} + setmetatable(obj, self) + self.__index = self + return obj + end + + function KeywordModule:init(handler) + self.npcHandler = handler + return true + end + + -- Parses all known parameters. + function KeywordModule:parseParameters() + local ret = NpcSystem.getParameter("keywords") + if ret then + self:parseKeywords(ret) + end + end + + function KeywordModule:parseKeywords(data) + local n = 1 + for keys in string.gmatch(data, "[^;]+") do + local i = 1 + + local keywords = {} + for temp in string.gmatch(keys, "[^,]+") do + keywords[#keywords + 1] = temp + i = i + 1 + end + + if i ~= 1 then + local reply = NpcSystem.getParameter("keyword_reply" .. n) + if reply then + self:addKeyword(keywords, reply) + else + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "Parameter '" .. "keyword_reply" .. n .. "' missing. Skipping...") + end + else + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "No keywords found for keyword set #" .. n .. ". Skipping...") + end + + n = n + 1 + end + end + + function KeywordModule:addKeyword(keywords, reply) + self.npcHandler.keywordHandler:addKeyword(keywords, StdModule.say, {npcHandler = self.npcHandler, onlyFocus = true, text = reply, reset = true}) + end + + TravelModule = { + npcHandler = nil, + destinations = nil, + yesNode = nil, + noNode = nil, + } + + -- Add it to the parseable module list. + Modules.parseableModules["module_travel"] = TravelModule + + function TravelModule:new() + local obj = {} + setmetatable(obj, self) + self.__index = self + return obj + end + + function TravelModule:init(handler) + self.npcHandler = handler + self.yesNode = KeywordNode:new(SHOP_YESWORD, TravelModule.onConfirm, {module = self}) + self.noNode = KeywordNode:new(SHOP_NOWORD, TravelModule.onDecline, {module = self}) + self.destinations = {} + return true + end + + -- Parses all known parameters. + function TravelModule:parseParameters() + local ret = NpcSystem.getParameter("travel_destinations") + if ret then + self:parseDestinations(ret) + + self.npcHandler.keywordHandler:addKeyword({"destination"}, TravelModule.listDestinations, {module = self}) + self.npcHandler.keywordHandler:addKeyword({"where"}, TravelModule.listDestinations, {module = self}) + self.npcHandler.keywordHandler:addKeyword({"travel"}, TravelModule.listDestinations, {module = self}) + + end + end + + function TravelModule:parseDestinations(data) + for destination in string.gmatch(data, "[^;]+") do + local i = 1 + + local name = nil + local x = nil + local y = nil + local z = nil + local cost = nil + local premium = false + + for temp in string.gmatch(destination, "[^,]+") do + if i == 1 then + name = temp + elseif i == 2 then + x = tonumber(temp) + elseif i == 3 then + y = tonumber(temp) + elseif i == 4 then + z = tonumber(temp) + elseif i == 5 then + cost = tonumber(temp) + elseif i == 6 then + premium = temp == "true" + else + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "Unknown parameter found in travel destination parameter.", temp, destination) + end + i = i + 1 + end + + if name and x and y and z and cost then + self:addDestination(name, {x=x, y=y, z=z}, cost, premium) + else + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "Parameter(s) missing for travel destination:", name, x, y, z, cost, premium) + end + end + end + + function TravelModule:addDestination(name, position, price, premium) + self.destinations[#self.destinations + 1] = name + + local parameters = { + cost = price, + destination = position, + premium = premium, + module = self + } + local keywords = {} + keywords[#keywords + 1] = name + + local keywords2 = {} + keywords2[#keywords2 + 1] = "bring me to " .. name + local node = self.npcHandler.keywordHandler:addKeyword(keywords, TravelModule.travel, parameters) + self.npcHandler.keywordHandler:addKeyword(keywords2, TravelModule.bringMeTo, parameters) + node:addChildKeywordNode(self.yesNode) + node:addChildKeywordNode(self.noNode) + + if npcs_loaded_travel[getNpcCid()] == nil then + npcs_loaded_travel[getNpcCid()] = getNpcCid() + self.npcHandler.keywordHandler:addKeyword({'yes'}, TravelModule.onConfirm, {module = self}) + self.npcHandler.keywordHandler:addKeyword({'no'}, TravelModule.onDecline, {module = self}) + end + end + + function TravelModule.travel(cid, message, keywords, parameters, node) + local module = parameters.module + if not module.npcHandler:isFocused(cid) then + return false + end + + local npcHandler = module.npcHandler + + shop_destination[cid] = parameters.destination + shop_cost[cid] = parameters.cost + shop_premium[cid] = parameters.premium + shop_npcuid[cid] = getNpcCid() + + local cost = parameters.cost + local destination = parameters.destination + local premium = parameters.premium + + module.npcHandler:say("Do you want to travel to " .. keywords[1] .. " for " .. cost .. " gold coins?", cid) + return true + end + + function TravelModule.onConfirm(cid, message, keywords, parameters, node) + local module = parameters.module + if not module.npcHandler:isFocused(cid) then + return false + end + + if shop_npcuid[cid] ~= Npc().uid then + return false + end + + local npcHandler = module.npcHandler + + local cost = shop_cost[cid] + local destination = Position(shop_destination[cid]) + + local player = Player(cid) + if player:isPremium() or not shop_premium[cid] then + if not player:removeTotalMoney(cost) then + npcHandler:say("You do not have enough money!", cid) + elseif player:isPzLocked(cid) then + npcHandler:say("Get out of there with this blood.", cid) + else + npcHandler:say("It was a pleasure doing business with you.", cid) + npcHandler:releaseFocus(cid) + + local position = player:getPosition() + player:teleportTo(destination) + + position:sendMagicEffect(CONST_ME_TELEPORT) + destination:sendMagicEffect(CONST_ME_TELEPORT) + end + else + npcHandler:say("I can only allow premium players to travel there.", cid) + end + + npcHandler:resetNpc(cid) + return true + end + + -- onDecline keyword callback function. Generally called when the player sais "no" after wanting to buy an item. + function TravelModule.onDecline(cid, message, keywords, parameters, node) + local module = parameters.module + if not module.npcHandler:isFocused(cid) or shop_npcuid[cid] ~= getNpcCid() then + return false + end + local parentParameters = node:getParent():getParameters() + local parseInfo = { [TAG_PLAYERNAME] = Player(cid):getName() } + local msg = module.npcHandler:parseMessage(module.npcHandler:getMessage(MESSAGE_DECLINE), parseInfo) + module.npcHandler:say(msg, cid) + module.npcHandler:resetNpc(cid) + return true + end + + function TravelModule.bringMeTo(cid, message, keywords, parameters, node) + local module = parameters.module + if not module.npcHandler:isFocused(cid) then + return false + end + + local cost = parameters.cost + local destination = Position(parameters.destination) + + local player = Player(cid) + if player:isPremium() or not parameters.premium then + if player:removeTotalMoney(cost) then + local position = player:getPosition() + player:teleportTo(destination) + + position:sendMagicEffect(CONST_ME_TELEPORT) + destination:sendMagicEffect(CONST_ME_TELEPORT) + end + end + return true + end + + function TravelModule.listDestinations(cid, message, keywords, parameters, node) + local module = parameters.module + if not module.npcHandler:isFocused(cid) then + return false + end + + local msg = "I can bring you to " + --local i = 1 + local maxn = #module.destinations + for i, destination in pairs(module.destinations) do + msg = msg .. destination + if i == maxn - 1 then + msg = msg .. " and " + elseif i == maxn then + msg = msg .. "." + else + msg = msg .. ", " + end + i = i + 1 + end + + module.npcHandler:say(msg, cid) + module.npcHandler:resetNpc(cid) + return true + end + + ShopModule = { + npcHandler = nil, + yesNode = nil, + noNode = nil, + noText = "", + maxCount = 100, + amount = 0 + } + + -- Add it to the parseable module list. + Modules.parseableModules["module_shop"] = ShopModule + + -- Creates a new instance of ShopModule + function ShopModule:new() + local obj = {} + setmetatable(obj, self) + self.__index = self + return obj + end + + -- Parses all known parameters. + function ShopModule:parseParameters() + local ret = NpcSystem.getParameter("shop_buyable") + if ret then + self:parseBuyable(ret) + end + + local ret = NpcSystem.getParameter("shop_sellable") + if ret then + self:parseSellable(ret) + end + + local ret = NpcSystem.getParameter("shop_buyable_containers") + if ret then + self:parseBuyableContainers(ret) + end + end + + -- Parse a string contaning a set of buyable items. + function ShopModule:parseBuyable(data) + for item in string.gmatch(data, "[^;]+") do + local i = 1 + + local name = nil + local itemid = nil + local cost = nil + local subType = nil + local realName = nil + + for temp in string.gmatch(item, "[^,]+") do + if i == 1 then + name = temp + elseif i == 2 then + itemid = tonumber(temp) + elseif i == 3 then + cost = tonumber(temp) + elseif i == 4 then + subType = tonumber(temp) + elseif i == 5 then + realName = temp + else + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "Unknown parameter found in buyable items parameter.", temp, item) + end + i = i + 1 + end + + local it = ItemType(itemid) + if subType == nil and it:getCharges() ~= 0 then + subType = it:getCharges() + end + + if SHOPMODULE_MODE == SHOPMODULE_MODE_TRADE then + if itemid and cost then + if subType == nil and it:isFluidContainer() then + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "SubType missing for parameter item:", item) + else + self:addBuyableItem(nil, itemid, cost, subType, realName) + end + else + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "Parameter(s) missing for item:", itemid, cost) + end + else + if name and itemid and cost then + if subType == nil and it:isFluidContainer() then + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "SubType missing for parameter item:", item) + else + local names = {} + names[#names + 1] = name + self:addBuyableItem(names, itemid, cost, subType, realName) + end + else + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "Parameter(s) missing for item:", name, itemid, cost) + end + end + end + end + + -- Parse a string contaning a set of sellable items. + function ShopModule:parseSellable(data) + for item in string.gmatch(data, "[^;]+") do + local i = 1 + + local name = nil + local itemid = nil + local cost = nil + local realName = nil + local subType = nil + + for temp in string.gmatch(item, "[^,]+") do + if i == 1 then + name = temp + elseif i == 2 then + itemid = tonumber(temp) + elseif i == 3 then + cost = tonumber(temp) + elseif i == 4 then + realName = temp + elseif i == 5 then + subType = tonumber(temp) + else + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "Unknown parameter found in sellable items parameter.", temp, item) + end + i = i + 1 + end + + if SHOPMODULE_MODE == SHOPMODULE_MODE_TRADE then + if itemid and cost then + self:addSellableItem(nil, itemid, cost, realName, subType) + else + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "Parameter(s) missing for item:", itemid, cost) + end + else + if name and itemid and cost then + local names = {} + names[#names + 1] = name + self:addSellableItem(names, itemid, cost, realName, subType) + else + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "Parameter(s) missing for item:", name, itemid, cost) + end + end + end + end + + -- Parse a string contaning a set of buyable items. + function ShopModule:parseBuyableContainers(data) + for item in string.gmatch(data, "[^;]+") do + local i = 1 + + local name = nil + local container = nil + local itemid = nil + local cost = nil + local subType = nil + local realName = nil + + for temp in string.gmatch(item, "[^,]+") do + if i == 1 then + name = temp + elseif i == 2 then + itemid = tonumber(temp) + elseif i == 3 then + itemid = tonumber(temp) + elseif i == 4 then + cost = tonumber(temp) + elseif i == 5 then + subType = tonumber(temp) + elseif i == 6 then + realName = temp + else + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "Unknown parameter found in buyable items parameter.", temp, item) + end + i = i + 1 + end + + if name and container and itemid and cost then + if subType == nil and ItemType(itemid):isFluidContainer() then + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "SubType missing for parameter item:", item) + else + local names = {} + names[#names + 1] = name + self:addBuyableItemContainer(names, container, itemid, cost, subType, realName) + end + else + print("[Warning : " .. Npc():getName() .. "] NpcSystem:", "Parameter(s) missing for item:", name, container, itemid, cost) + end + end + end + + -- Initializes the module and associates handler to it. + function ShopModule:init(handler) + self.npcHandler = handler + self.yesNode = KeywordNode:new(SHOP_YESWORD, ShopModule.onConfirm, {module = self}) + self.noNode = KeywordNode:new(SHOP_NOWORD, ShopModule.onDecline, {module = self}) + self.noText = handler:getMessage(MESSAGE_DECLINE) + + if SHOPMODULE_MODE ~= SHOPMODULE_MODE_TALK then + for i, word in pairs(SHOP_TRADEREQUEST) do + local obj = {} + obj[#obj + 1] = word + obj.callback = SHOP_TRADEREQUEST.callback or ShopModule.messageMatcher + handler.keywordHandler:addKeyword(obj, ShopModule.requestTrade, {module = self}) + end + end + + return true + end + + -- Custom message matching callback function for requesting trade messages. + function ShopModule.messageMatcher(keywords, message) + for i, word in pairs(keywords) do + if type(word) == "string" then + if string.find(message, word) and not string.find(message, "[%w+]" .. word) and not string.find(message, word .. "[%w+]") then + return true + end + end + end + + return false + end + + -- Resets the module-specific variables. + function ShopModule:reset() + self.amount = 0 + end + + -- Function used to match a number value from a string. + function ShopModule:getCount(message) + local ret = 1 + local b, e = string.find(message, PATTERN_COUNT) + if b and e then + ret = tonumber(string.sub(message, b, e)) + end + + if ret <= 0 then + ret = 1 + elseif ret > self.maxCount then + ret = self.maxCount + end + + return ret + end + + -- Adds a new buyable item. + -- names = A table containing one or more strings of alternative names to this item. Used only for old buy/sell system. + -- itemid = The itemid of the buyable item + -- cost = The price of one single item + -- subType - The subType of each rune or fluidcontainer item. Can be left out if it is not a rune/fluidcontainer. Default value is 1. + -- realName - The real, full name for the item. Will be used as ITEMNAME in MESSAGE_ONBUY and MESSAGE_ONSELL if defined. Default value is nil (ItemType(itemId):getName() will be used) + function ShopModule:addBuyableItem(names, itemid, cost, itemSubType, realName) + if SHOPMODULE_MODE ~= SHOPMODULE_MODE_TALK then + if itemSubType == nil then + itemSubType = 1 + end + + local shopItem = self:getShopItem(itemid, itemSubType) + if shopItem == nil then + self.npcHandler.shopItems[#self.npcHandler.shopItems + 1] = {id = itemid, buy = cost, sell = -1, subType = itemSubType, name = realName or ItemType(itemid):getName()} + else + shopItem.buy = cost + end + end + + if names and SHOPMODULE_MODE ~= SHOPMODULE_MODE_TRADE then + for i, name in pairs(names) do + local parameters = { + itemid = itemid, + cost = cost, + eventType = SHOPMODULE_BUY_ITEM, + module = self, + realName = realName or ItemType(itemid):getName(), + subType = itemSubType or 1 + } + + keywords = {} + keywords[#keywords + 1] = "buy" + keywords[#keywords + 1] = name + local node = self.npcHandler.keywordHandler:addKeyword(keywords, ShopModule.tradeItem, parameters) + node:addChildKeywordNode(self.yesNode) + node:addChildKeywordNode(self.noNode) + end + end + + if npcs_loaded_shop[getNpcCid()] == nil then + npcs_loaded_shop[getNpcCid()] = getNpcCid() + self.npcHandler.keywordHandler:addKeyword({'yes'}, ShopModule.onConfirm, {module = self}) + self.npcHandler.keywordHandler:addKeyword({'no'}, ShopModule.onDecline, {module = self}) + end + end + + function ShopModule:getShopItem(itemId, itemSubType) + if ItemType(itemId):isFluidContainer() then + for i = 1, #self.npcHandler.shopItems do + local shopItem = self.npcHandler.shopItems[i] + if shopItem.id == itemId and shopItem.subType == itemSubType then + return shopItem + end + end + else + for i = 1, #self.npcHandler.shopItems do + local shopItem = self.npcHandler.shopItems[i] + if shopItem.id == itemId then + return shopItem + end + end + end + return nil + end + + -- Adds a new buyable container of items. + -- names = A table containing one or more strings of alternative names to this item. + -- container = Backpack, bag or any other itemid of container where bought items will be stored + -- itemid = The itemid of the buyable item + -- cost = The price of one single item + -- subType - The subType of each rune or fluidcontainer item. Can be left out if it is not a rune/fluidcontainer. Default value is 1. + -- realName - The real, full name for the item. Will be used as ITEMNAME in MESSAGE_ONBUY and MESSAGE_ONSELL if defined. Default value is nil (ItemType(itemId):getName() will be used) + function ShopModule:addBuyableItemContainer(names, container, itemid, cost, subType, realName) + if names then + for i, name in pairs(names) do + local parameters = { + container = container, + itemid = itemid, + cost = cost, + eventType = SHOPMODULE_BUY_ITEM_CONTAINER, + module = self, + realName = realName or ItemType(itemid):getName(), + subType = subType or 1 + } + + keywords = {} + keywords[#keywords + 1] = "buy" + keywords[#keywords + 1] = name + local node = self.npcHandler.keywordHandler:addKeyword(keywords, ShopModule.tradeItem, parameters) + node:addChildKeywordNode(self.yesNode) + node:addChildKeywordNode(self.noNode) + end + end + end + + -- Adds a new sellable item. + -- names = A table containing one or more strings of alternative names to this item. Used only by old buy/sell system. + -- itemid = The itemid of the sellable item + -- cost = The price of one single item + -- realName - The real, full name for the item. Will be used as ITEMNAME in MESSAGE_ONBUY and MESSAGE_ONSELL if defined. Default value is nil (ItemType(itemId):getName() will be used) + function ShopModule:addSellableItem(names, itemid, cost, realName, itemSubType) + if SHOPMODULE_MODE ~= SHOPMODULE_MODE_TALK then + if itemSubType == nil then + itemSubType = 0 + end + + local shopItem = self:getShopItem(itemid, itemSubType) + if shopItem == nil then + self.npcHandler.shopItems[#self.npcHandler.shopItems + 1] = {id = itemid, buy = -1, sell = cost, subType = itemSubType, name = realName or ItemType(itemid):getName()} + else + shopItem.sell = cost + end + end + + if names and SHOPMODULE_MODE ~= SHOPMODULE_MODE_TRADE then + for i, name in pairs(names) do + local parameters = { + itemid = itemid, + cost = cost, + eventType = SHOPMODULE_SELL_ITEM, + module = self, + realName = realName or ItemType(itemid):getName() + } + + keywords = {} + keywords[#keywords + 1] = "sell" + keywords[#keywords + 1] = name + + local node = self.npcHandler.keywordHandler:addKeyword(keywords, ShopModule.tradeItem, parameters) + node:addChildKeywordNode(self.yesNode) + node:addChildKeywordNode(self.noNode) + end + end + end + + -- onModuleReset callback function. Calls ShopModule:reset() + function ShopModule:callbackOnModuleReset() + self:reset() + return true + end + + -- Callback onBuy() function. If you wish, you can change certain Npc to use your onBuy(). + function ShopModule:callbackOnBuy(cid, itemid, subType, amount, ignoreCap, inBackpacks) + local shopItem = self:getShopItem(itemid, subType) + if shopItem == nil then + error("[ShopModule.onBuy] shopItem == nil") + return false + end + + if shopItem.buy == -1 then + error("[ShopModule.onSell] attempt to buy a non-buyable item") + return false + end + + local totalCost = amount * shopItem.buy + if inBackpacks then + totalCost = ItemType(itemid):isStackable() and totalCost + 20 or totalCost + (math.max(1, math.floor(amount / ItemType(ITEM_SHOPPING_BAG):getCapacity())) * 20) + end + + local player = Player(cid) + local parseInfo = { + [TAG_PLAYERNAME] = player:getName(), + [TAG_ITEMCOUNT] = amount, + [TAG_TOTALCOST] = totalCost, + [TAG_ITEMNAME] = shopItem.name + } + + if player:getTotalMoney() < totalCost then + local msg = self.npcHandler:getMessage(MESSAGE_NEEDMONEY) + msg = self.npcHandler:parseMessage(msg, parseInfo) + player:sendCancelMessage(msg) + return false + end + + local subType = shopItem.subType or 1 + local a, b = doNpcSellItem(cid, itemid, amount, subType, ignoreCap, inBackpacks, ITEM_SHOPPING_BAG) + if a < amount then + local msgId = MESSAGE_NEEDMORESPACE + if a == 0 then + msgId = MESSAGE_NEEDSPACE + end + + local msg = self.npcHandler:getMessage(msgId) + parseInfo[TAG_ITEMCOUNT] = a + msg = self.npcHandler:parseMessage(msg, parseInfo) + player:sendCancelMessage(msg) + self.npcHandler.talkStart[cid] = os.time() + + if a > 0 then + if not player:removeTotalMoney((a * shopItem.buy) + (b * 20)) then + return false + end + return true + end + + return false + else + local msg = self.npcHandler:getMessage(MESSAGE_BOUGHT) + msg = self.npcHandler:parseMessage(msg, parseInfo) + player:sendTextMessage(MESSAGE_INFO_DESCR, msg) + if not player:removeTotalMoney(totalCost) then + return false + end + self.npcHandler.talkStart[cid] = os.time() + return true + end + end + + -- Callback onSell() function. If you wish, you can change certain Npc to use your onSell(). + function ShopModule:callbackOnSell(cid, itemid, subType, amount, ignoreEquipped, _) + local shopItem = self:getShopItem(itemid, subType) + if shopItem == nil then + error("[ShopModule.onSell] items[itemid] == nil") + return false + end + + if shopItem.sell == -1 then + error("[ShopModule.onSell] attempt to sell a non-sellable item") + return false + end + + local player = Player(cid) + local parseInfo = { + [TAG_PLAYERNAME] = player:getName(), + [TAG_ITEMCOUNT] = amount, + [TAG_TOTALCOST] = amount * shopItem.sell, + [TAG_ITEMNAME] = shopItem.name + } + + if not isItemFluidContainer(itemid) then + subType = -1 + end + + if player:removeItem(itemid, amount, subType, ignoreEquipped) then + local msg = self.npcHandler:getMessage(MESSAGE_SOLD) + msg = self.npcHandler:parseMessage(msg, parseInfo) + player:sendTextMessage(MESSAGE_INFO_DESCR, msg) + player:addMoney(amount * shopItem.sell) + self.npcHandler.talkStart[cid] = os.time() + return true + else + local msg = self.npcHandler:getMessage(MESSAGE_NEEDITEM) + msg = self.npcHandler:parseMessage(msg, parseInfo) + player:sendCancelMessage(msg) + self.npcHandler.talkStart[cid] = os.time() + return false + end + end + + -- Callback for requesting a trade window with the NPC. + function ShopModule.requestTrade(cid, message, keywords, parameters, node) + local module = parameters.module + if not module.npcHandler:isFocused(cid) then + return false + end + + if not module.npcHandler:onTradeRequest(cid) then + return false + end + + local itemWindow = {} + for i = 1, #module.npcHandler.shopItems do + itemWindow[#itemWindow + 1] = module.npcHandler.shopItems[i] + end + + if itemWindow[1] == nil then + local parseInfo = { [TAG_PLAYERNAME] = Player(cid):getName() } + local msg = module.npcHandler:parseMessage(module.npcHandler:getMessage(MESSAGE_NOSHOP), parseInfo) + module.npcHandler:say(msg, cid) + return true + end + + local parseInfo = { [TAG_PLAYERNAME] = Player(cid):getName() } + local msg = module.npcHandler:parseMessage(module.npcHandler:getMessage(MESSAGE_SENDTRADE), parseInfo) + openShopWindow(cid, itemWindow, + function(cid, itemid, subType, amount, ignoreCap, inBackpacks) module.npcHandler:onBuy(cid, itemid, subType, amount, ignoreCap, inBackpacks) end, + function(cid, itemid, subType, amount, ignoreCap, inBackpacks) module.npcHandler:onSell(cid, itemid, subType, amount, ignoreCap, inBackpacks) end) + module.npcHandler:say(msg, cid) + return true + end + + -- onConfirm keyword callback function. Sells/buys the actual item. + function ShopModule.onConfirm(cid, message, keywords, parameters, node) + local module = parameters.module + if not module.npcHandler:isFocused(cid) or shop_npcuid[cid] ~= getNpcCid() then + return false + end + shop_npcuid[cid] = 0 + + local parentParameters = node:getParent():getParameters() + local player = Player(cid) + local parseInfo = { + [TAG_PLAYERNAME] = player:getName(), + [TAG_ITEMCOUNT] = shop_amount[cid], + [TAG_TOTALCOST] = shop_cost[cid] * shop_amount[cid], + [TAG_ITEMNAME] = shop_rlname[cid] + } + + if shop_eventtype[cid] == SHOPMODULE_SELL_ITEM then + local ret = doPlayerSellItem(cid, shop_itemid[cid], shop_amount[cid], shop_cost[cid] * shop_amount[cid]) + if ret == true then + local msg = module.npcHandler:getMessage(MESSAGE_ONSELL) + msg = module.npcHandler:parseMessage(msg, parseInfo) + module.npcHandler:say(msg, cid) + else + local msg = module.npcHandler:getMessage(MESSAGE_MISSINGITEM) + msg = module.npcHandler:parseMessage(msg, parseInfo) + module.npcHandler:say(msg, cid) + end + elseif shop_eventtype[cid] == SHOPMODULE_BUY_ITEM then + local cost = shop_cost[cid] * shop_amount[cid] + if player:getTotalMoney() < cost then + local msg = module.npcHandler:getMessage(MESSAGE_MISSINGMONEY) + msg = module.npcHandler:parseMessage(msg, parseInfo) + module.npcHandler:say(msg, cid) + return false + end + + local a, b = doNpcSellItem(cid, shop_itemid[cid], shop_amount[cid], shop_subtype[cid], false, false, ITEM_SHOPPING_BAG) + if a < shop_amount[cid] then + local msgId = MESSAGE_NEEDMORESPACE + if a == 0 then + msgId = MESSAGE_NEEDSPACE + end + + local msg = module.npcHandler:getMessage(msgId) + msg = module.npcHandler:parseMessage(msg, parseInfo) + module.npcHandler:say(msg, cid) + if a > 0 then + if not player:removeTotalMoney(a * shop_cost[cid]) then + return false + end + if shop_itemid[cid] == ITEM_PARCEL then + doNpcSellItem(cid, ITEM_LABEL, shop_amount[cid], shop_subtype[cid], true, false, ITEM_SHOPPING_BAG) + end + return true + end + return false + else + local msg = module.npcHandler:getMessage(MESSAGE_ONBUY) + msg = module.npcHandler:parseMessage(msg, parseInfo) + module.npcHandler:say(msg, cid) + if not player:removeTotalMoney(cost) then + return false + end + if shop_itemid[cid] == ITEM_PARCEL then + doNpcSellItem(cid, ITEM_LABEL, shop_amount[cid], shop_subtype[cid], true, false, ITEM_SHOPPING_BAG) + end + return true + end + elseif shop_eventtype[cid] == SHOPMODULE_BUY_ITEM_CONTAINER then + local ret = doPlayerBuyItemContainer(cid, shop_container[cid], shop_itemid[cid], shop_amount[cid], shop_cost[cid] * shop_amount[cid], shop_subtype[cid]) + if ret == true then + local msg = module.npcHandler:getMessage(MESSAGE_ONBUY) + msg = module.npcHandler:parseMessage(msg, parseInfo) + module.npcHandler:say(msg, cid) + else + local msg = module.npcHandler:getMessage(MESSAGE_MISSINGMONEY) + msg = module.npcHandler:parseMessage(msg, parseInfo) + module.npcHandler:say(msg, cid) + end + end + + module.npcHandler:resetNpc(cid) + return true + end + + -- onDecline keyword callback function. Generally called when the player sais "no" after wanting to buy an item. + function ShopModule.onDecline(cid, message, keywords, parameters, node) + local module = parameters.module + if not module.npcHandler:isFocused(cid) or shop_npcuid[cid] ~= getNpcCid() then + return false + end + shop_npcuid[cid] = 0 + + local parentParameters = node:getParent():getParameters() + local parseInfo = { + [TAG_PLAYERNAME] = Player(cid):getName(), + [TAG_ITEMCOUNT] = shop_amount[cid], + [TAG_TOTALCOST] = shop_cost[cid] * shop_amount[cid], + [TAG_ITEMNAME] = shop_rlname[cid] + } + + local msg = module.npcHandler:parseMessage(module.noText, parseInfo) + module.npcHandler:say(msg, cid) + module.npcHandler:resetNpc(cid) + return true + end + + -- tradeItem callback function. Makes the npc say the message defined by MESSAGE_BUY or MESSAGE_SELL + function ShopModule.tradeItem(cid, message, keywords, parameters, node) + local module = parameters.module + if not module.npcHandler:isFocused(cid) then + return false + end + + if not module.npcHandler:onTradeRequest(cid) then + return true + end + + local count = module:getCount(message) + module.amount = count + + shop_amount[cid] = module.amount + shop_cost[cid] = parameters.cost + shop_rlname[cid] = parameters.realName + shop_itemid[cid] = parameters.itemid + shop_container[cid] = parameters.container + shop_npcuid[cid] = getNpcCid() + shop_eventtype[cid] = parameters.eventType + shop_subtype[cid] = parameters.subType + + local parseInfo = { + [TAG_PLAYERNAME] = Player(cid):getName(), + [TAG_ITEMCOUNT] = shop_amount[cid], + [TAG_TOTALCOST] = shop_cost[cid] * shop_amount[cid], + [TAG_ITEMNAME] = shop_rlname[cid] + } + + if shop_eventtype[cid] == SHOPMODULE_SELL_ITEM then + local msg = module.npcHandler:getMessage(MESSAGE_SELL) + msg = module.npcHandler:parseMessage(msg, parseInfo) + module.npcHandler:say(msg, cid) + elseif shop_eventtype[cid] == SHOPMODULE_BUY_ITEM then + local msg = module.npcHandler:getMessage(MESSAGE_BUY) + msg = module.npcHandler:parseMessage(msg, parseInfo) + module.npcHandler:say(msg, cid) + elseif shop_eventtype[cid] == SHOPMODULE_BUY_ITEM_CONTAINER then + local msg = module.npcHandler:getMessage(MESSAGE_BUY) + msg = module.npcHandler:parseMessage(msg, parseInfo) + module.npcHandler:say(msg, cid) + end + return true + end + + VoiceModule = { + voices = nil, + voiceCount = 0, + lastVoice = 0, + timeout = nil, + chance = nil + } + + -- VoiceModule: makes the NPC say/yell random lines from a table, with delay, chance and yell optional + function VoiceModule:new(voices, timeout, chance) + local obj = {} + setmetatable(obj, self) + self.__index = self + + obj.voices = voices + for i = 1, #obj.voices do + local voice = obj.voices[i] + if voice.yell then + voice.yell = nil + voice.talktype = TALKTYPE_YELL + else + voice.talktype = TALKTYPE_SAY + end + end + + obj.voiceCount = #voices + obj.timeout = timeout or 10 + obj.chance = chance or 10 + return obj + end + + function VoiceModule:init(handler) + return true + end + + function VoiceModule:callbackOnThink() + if self.lastVoice < os.time() then + self.lastVoice = os.time() + self.timeout + if math.random(100) <= self.chance then + local voice = self.voices[math.random(self.voiceCount)] + Npc():say(voice.text, voice.talktype) + end + end + return true + end +end diff --git a/data/npc/lib/npcsystem/npchandler.lua b/data/npc/lib/npcsystem/npchandler.lua new file mode 100644 index 0000000..9e193e9 --- /dev/null +++ b/data/npc/lib/npcsystem/npchandler.lua @@ -0,0 +1,635 @@ +-- Advanced NPC System by Jiddo + +if NpcHandler == nil then + -- Constant talkdelay behaviors. + TALKDELAY_NONE = 0 -- No talkdelay. Npc will reply immedeatly. + TALKDELAY_ONTHINK = 1 -- Talkdelay handled through the onThink callback function. (Default) + TALKDELAY_EVENT = 2 -- Not yet implemented + + -- Currently applied talkdelay behavior. TALKDELAY_ONTHINK is default. + NPCHANDLER_TALKDELAY = TALKDELAY_ONTHINK + + -- Constant indexes for defining default messages. + MESSAGE_GREET = 1 -- When the player greets the npc. + MESSAGE_FAREWELL = 2 -- When the player unGreets the npc. + MESSAGE_BUY = 3 -- When the npc asks the player if he wants to buy something. + MESSAGE_ONBUY = 4 -- When the player successfully buys something via talk. + MESSAGE_BOUGHT = 5 -- When the player bought something through the shop window. + MESSAGE_SELL = 6 -- When the npc asks the player if he wants to sell something. + MESSAGE_ONSELL = 7 -- When the player successfully sells something via talk. + MESSAGE_SOLD = 8 -- When the player sold something through the shop window. + MESSAGE_MISSINGMONEY = 9 -- When the player does not have enough money. + MESSAGE_NEEDMONEY = 10 -- Same as above, used for shop window. + MESSAGE_MISSINGITEM = 11 -- When the player is trying to sell an item he does not have. + MESSAGE_NEEDITEM = 12 -- Same as above, used for shop window. + MESSAGE_NEEDSPACE = 13 -- When the player don't have any space to buy an item + MESSAGE_NEEDMORESPACE = 14 -- When the player has some space to buy an item, but not enough space + MESSAGE_IDLETIMEOUT = 15 -- When the player has been idle for longer then idleTime allows. + MESSAGE_WALKAWAY = 16 -- When the player walks out of the talkRadius of the npc. + MESSAGE_DECLINE = 17 -- When the player says no to something. + MESSAGE_SENDTRADE = 18 -- When the npc sends the trade window to the player + MESSAGE_NOSHOP = 19 -- When the npc's shop is requested but he doesn't have any + MESSAGE_ONCLOSESHOP = 20 -- When the player closes the npc's shop window + MESSAGE_ALREADYFOCUSED = 21 -- When the player already has the focus of this npc. + MESSAGE_WALKAWAY_MALE = 22 -- When a male player walks out of the talkRadius of the npc. + MESSAGE_WALKAWAY_FEMALE = 23 -- When a female player walks out of the talkRadius of the npc. + + -- Constant indexes for callback functions. These are also used for module callback ids. + CALLBACK_CREATURE_APPEAR = 1 + CALLBACK_CREATURE_DISAPPEAR = 2 + CALLBACK_CREATURE_SAY = 3 + CALLBACK_ONTHINK = 4 + CALLBACK_GREET = 5 + CALLBACK_FAREWELL = 6 + CALLBACK_MESSAGE_DEFAULT = 7 + CALLBACK_PLAYER_ENDTRADE = 8 + CALLBACK_PLAYER_CLOSECHANNEL = 9 + CALLBACK_ONBUY = 10 + CALLBACK_ONSELL = 11 + CALLBACK_ONADDFOCUS = 18 + CALLBACK_ONRELEASEFOCUS = 19 + CALLBACK_ONTRADEREQUEST = 20 + + -- Addidional module callback ids + CALLBACK_MODULE_INIT = 12 + CALLBACK_MODULE_RESET = 13 + + -- Constant strings defining the keywords to replace in the default messages. + TAG_PLAYERNAME = "|PLAYERNAME|" + TAG_ITEMCOUNT = "|ITEMCOUNT|" + TAG_TOTALCOST = "|TOTALCOST|" + TAG_ITEMNAME = "|ITEMNAME|" + + NpcHandler = { + keywordHandler = nil, + focuses = nil, + talkStart = nil, + idleTime = 120, + talkRadius = 3, + talkDelayTime = 1, -- Seconds to delay outgoing messages. + talkDelay = nil, + callbackFunctions = nil, + modules = nil, + shopItems = nil, -- They must be here since ShopModule uses 'static' functions + eventSay = nil, + eventDelayedSay = nil, + topic = nil, + messages = { + -- These are the default replies of all npcs. They can/should be changed individually for each npc. + [MESSAGE_GREET] = "Greetings, |PLAYERNAME|.", + [MESSAGE_FAREWELL] = "Good bye, |PLAYERNAME|.", + [MESSAGE_BUY] = "Do you want to buy |ITEMCOUNT| |ITEMNAME| for |TOTALCOST| gold coins?", + [MESSAGE_ONBUY] = "Here you are.", + [MESSAGE_BOUGHT] = "Bought |ITEMCOUNT|x |ITEMNAME| for |TOTALCOST| gold.", + [MESSAGE_SELL] = "Do you want to sell |ITEMCOUNT| |ITEMNAME| for |TOTALCOST| gold coins?", + [MESSAGE_ONSELL] = "Here you are, |TOTALCOST| gold.", + [MESSAGE_SOLD] = "Sold |ITEMCOUNT|x |ITEMNAME| for |TOTALCOST| gold.", + [MESSAGE_MISSINGMONEY] = "You don't have enough money.", + [MESSAGE_NEEDMONEY] = "You don't have enough money.", + [MESSAGE_MISSINGITEM] = "You don't have so many.", + [MESSAGE_NEEDITEM] = "You do not have this object.", + [MESSAGE_NEEDSPACE] = "You do not have enough capacity.", + [MESSAGE_NEEDMORESPACE] = "You do not have enough capacity for all items.", + [MESSAGE_IDLETIMEOUT] = "Good bye.", + [MESSAGE_WALKAWAY] = "Good bye.", + [MESSAGE_DECLINE] = "Then not.", + [MESSAGE_SENDTRADE] = "Of course, just browse through my wares.", + [MESSAGE_NOSHOP] = "Sorry, I'm not offering anything.", + [MESSAGE_ONCLOSESHOP] = "Thank you, come back whenever you're in need of something else.", + [MESSAGE_ALREADYFOCUSED] = "|PLAYERNAME|, I am already talking to you.", + [MESSAGE_WALKAWAY_MALE] = "Good bye.", + [MESSAGE_WALKAWAY_FEMALE] = "Good bye." + } + } + + -- Creates a new NpcHandler with an empty callbackFunction stack. + function NpcHandler:new(keywordHandler) + local obj = {} + obj.callbackFunctions = {} + obj.modules = {} + obj.eventSay = {} + obj.eventDelayedSay = {} + obj.topic = {} + obj.focuses = {} + obj.talkStart = {} + obj.talkDelay = {} + obj.keywordHandler = keywordHandler + obj.messages = {} + obj.shopItems = {} + + setmetatable(obj.messages, self.messages) + self.messages.__index = self.messages + + setmetatable(obj, self) + self.__index = self + return obj + end + + -- Re-defines the maximum idle time allowed for a player when talking to this npc. + function NpcHandler:setMaxIdleTime(newTime) + self.idleTime = newTime + end + + -- Attaches a new keyword handler to this npchandler + function NpcHandler:setKeywordHandler(newHandler) + self.keywordHandler = newHandler + end + + -- Function used to change the focus of this npc. + function NpcHandler:addFocus(newFocus) + if self:isFocused(newFocus) then + return + end + + self.focuses[#self.focuses + 1] = newFocus + self.topic[newFocus] = 0 + local callback = self:getCallback(CALLBACK_ONADDFOCUS) + if callback == nil or callback(newFocus) then + self:processModuleCallback(CALLBACK_ONADDFOCUS, newFocus) + end + self:updateFocus() + end + + -- Function used to verify if npc is focused to certain player + function NpcHandler:isFocused(focus) + for k,v in pairs(self.focuses) do + if v == focus then + return true + end + end + return false + end + + -- This function should be called on each onThink and makes sure the npc faces the player it is talking to. + -- Should also be called whenever a new player is focused. + function NpcHandler:updateFocus() + for pos, focus in pairs(self.focuses) do + if focus then + doNpcSetCreatureFocus(focus) + return + end + end + doNpcSetCreatureFocus(0) + end + + -- Used when the npc should un-focus the player. + function NpcHandler:releaseFocus(focus) + if shop_cost[focus] then + shop_amount[focus] = nil + shop_cost[focus] = nil + shop_rlname[focus] = nil + shop_itemid[focus] = nil + shop_container[focus] = nil + shop_npcuid[focus] = nil + shop_eventtype[focus] = nil + shop_subtype[focus] = nil + shop_destination[focus] = nil + shop_premium[focus] = nil + end + + if self.eventDelayedSay[focus] then + self:cancelNPCTalk(self.eventDelayedSay[focus]) + end + + if not self:isFocused(focus) then + return + end + + local pos = nil + for k,v in pairs(self.focuses) do + if v == focus then + pos = k + end + end + self.focuses[pos] = nil + + self.eventSay[focus] = nil + self.eventDelayedSay[focus] = nil + self.talkStart[focus] = nil + self.topic[focus] = nil + + local callback = self:getCallback(CALLBACK_ONRELEASEFOCUS) + if callback == nil or callback(focus) then + self:processModuleCallback(CALLBACK_ONRELEASEFOCUS, focus) + end + + if Player(focus) then + closeShopWindow(focus) --Even if it can not exist, we need to prevent it. + self:updateFocus() + end + end + + -- Returns the callback function with the specified id or nil if no such callback function exists. + function NpcHandler:getCallback(id) + local ret = nil + if self.callbackFunctions then + ret = self.callbackFunctions[id] + end + return ret + end + + -- Changes the callback function for the given id to callback. + function NpcHandler:setCallback(id, callback) + if self.callbackFunctions then + self.callbackFunctions[id] = callback + end + end + + -- Adds a module to this npchandler and inits it. + function NpcHandler:addModule(module) + if self.modules then + self.modules[#self.modules + 1] = module + module:init(self) + end + end + + -- Calls the callback function represented by id for all modules added to this npchandler with the given arguments. + function NpcHandler:processModuleCallback(id, ...) + local ret = true + for i, module in pairs(self.modules) do + local tmpRet = true + if id == CALLBACK_CREATURE_APPEAR and module.callbackOnCreatureAppear then + tmpRet = module:callbackOnCreatureAppear(...) + elseif id == CALLBACK_CREATURE_DISAPPEAR and module.callbackOnCreatureDisappear then + tmpRet = module:callbackOnCreatureDisappear(...) + elseif id == CALLBACK_CREATURE_SAY and module.callbackOnCreatureSay then + tmpRet = module:callbackOnCreatureSay(...) + elseif id == CALLBACK_PLAYER_ENDTRADE and module.callbackOnPlayerEndTrade then + tmpRet = module:callbackOnPlayerEndTrade(...) + elseif id == CALLBACK_PLAYER_CLOSECHANNEL and module.callbackOnPlayerCloseChannel then + tmpRet = module:callbackOnPlayerCloseChannel(...) + elseif id == CALLBACK_ONBUY and module.callbackOnBuy then + tmpRet = module:callbackOnBuy(...) + elseif id == CALLBACK_ONSELL and module.callbackOnSell then + tmpRet = module:callbackOnSell(...) + elseif id == CALLBACK_ONTRADEREQUEST and module.callbackOnTradeRequest then + tmpRet = module:callbackOnTradeRequest(...) + elseif id == CALLBACK_ONADDFOCUS and module.callbackOnAddFocus then + tmpRet = module:callbackOnAddFocus(...) + elseif id == CALLBACK_ONRELEASEFOCUS and module.callbackOnReleaseFocus then + tmpRet = module:callbackOnReleaseFocus(...) + elseif id == CALLBACK_ONTHINK and module.callbackOnThink then + tmpRet = module:callbackOnThink(...) + elseif id == CALLBACK_GREET and module.callbackOnGreet then + tmpRet = module:callbackOnGreet(...) + elseif id == CALLBACK_FAREWELL and module.callbackOnFarewell then + tmpRet = module:callbackOnFarewell(...) + elseif id == CALLBACK_MESSAGE_DEFAULT and module.callbackOnMessageDefault then + tmpRet = module:callbackOnMessageDefault(...) + elseif id == CALLBACK_MODULE_RESET and module.callbackOnModuleReset then + tmpRet = module:callbackOnModuleReset(...) + end + if not tmpRet then + ret = false + break + end + end + return ret + end + + -- Returns the message represented by id. + function NpcHandler:getMessage(id) + local ret = nil + if self.messages then + ret = self.messages[id] + end + return ret + end + + -- Changes the default response message with the specified id to newMessage. + function NpcHandler:setMessage(id, newMessage) + if self.messages then + self.messages[id] = newMessage + end + end + + -- Translates all message tags found in msg using parseInfo + function NpcHandler:parseMessage(msg, parseInfo) + local ret = msg + for search, replace in pairs(parseInfo) do + ret = string.gsub(ret, search, replace) + end + return ret + end + + -- Makes sure the npc un-focuses the currently focused player + function NpcHandler:unGreet(cid) + if not self:isFocused(cid) then + return + end + + local callback = self:getCallback(CALLBACK_FAREWELL) + if callback == nil or callback() then + if self:processModuleCallback(CALLBACK_FAREWELL) then + local msg = self:getMessage(MESSAGE_FAREWELL) + local player = Player(cid) + local playerName = player and player:getName() or -1 + local parseInfo = { [TAG_PLAYERNAME] = playerName } + self:resetNpc(cid) + msg = self:parseMessage(msg, parseInfo) + self:say(msg, cid, true) + self:releaseFocus(cid) + end + end + end + + -- Greets a new player. + function NpcHandler:greet(cid) + if cid ~= 0 then + local callback = self:getCallback(CALLBACK_GREET) + if callback == nil or callback(cid) then + if self:processModuleCallback(CALLBACK_GREET, cid) then + local msg = self:getMessage(MESSAGE_GREET) + local player = Player(cid) + local playerName = player and player:getName() or -1 + local parseInfo = { [TAG_PLAYERNAME] = playerName } + msg = self:parseMessage(msg, parseInfo) + self:say(msg, cid, true) + else + return + end + else + return + end + end + self:addFocus(cid) + end + + -- Handles onCreatureAppear events. If you with to handle this yourself, please use the CALLBACK_CREATURE_APPEAR callback. + function NpcHandler:onCreatureAppear(creature) + local cid = creature:getId() + if cid == getNpcCid() and next(self.shopItems) then + local npc = Npc() + local speechBubble = npc:getSpeechBubble() + if speechBubble == SPEECHBUBBLE_QUEST then + npc:setSpeechBubble(SPEECHBUBBLE_QUESTTRADER) + else + npc:setSpeechBubble(SPEECHBUBBLE_TRADE) + end + end + + local callback = self:getCallback(CALLBACK_CREATURE_APPEAR) + if callback == nil or callback(cid) then + if self:processModuleCallback(CALLBACK_CREATURE_APPEAR, cid) then + -- + end + end + end + + -- Handles onCreatureDisappear events. If you with to handle this yourself, please use the CALLBACK_CREATURE_DISAPPEAR callback. + function NpcHandler:onCreatureDisappear(creature) + local cid = creature:getId() + if getNpcCid() == cid then + return + end + + local callback = self:getCallback(CALLBACK_CREATURE_DISAPPEAR) + if callback == nil or callback(cid) then + if self:processModuleCallback(CALLBACK_CREATURE_DISAPPEAR, cid) then + if self:isFocused(cid) then + self:unGreet(cid) + end + end + end + end + + -- Handles onCreatureSay events. If you with to handle this yourself, please use the CALLBACK_CREATURE_SAY callback. + function NpcHandler:onCreatureSay(creature, msgtype, msg) + local cid = creature:getId() + local callback = self:getCallback(CALLBACK_CREATURE_SAY) + if callback == nil or callback(cid, msgtype, msg) then + if self:processModuleCallback(CALLBACK_CREATURE_SAY, cid, msgtype, msg) then + if not self:isInRange(cid) then + return + end + + if self.keywordHandler then + if self:isFocused(cid) and msgtype == TALKTYPE_PRIVATE_PN or not self:isFocused(cid) then + local ret = self.keywordHandler:processMessage(cid, msg) + if not ret then + local callback = self:getCallback(CALLBACK_MESSAGE_DEFAULT) + if callback and callback(cid, msgtype, msg) then + self.talkStart[cid] = os.time() + end + else + self.talkStart[cid] = os.time() + end + end + end + end + end + end + + -- Handles onPlayerEndTrade events. If you wish to handle this yourself, use the CALLBACK_PLAYER_ENDTRADE callback. + function NpcHandler:onPlayerEndTrade(creature) + local cid = creature:getId() + local callback = self:getCallback(CALLBACK_PLAYER_ENDTRADE) + if callback == nil or callback(cid) then + if self:processModuleCallback(CALLBACK_PLAYER_ENDTRADE, cid, msgtype, msg) then + if self:isFocused(cid) then + local player = Player(cid) + local playerName = player and player:getName() or -1 + local parseInfo = { [TAG_PLAYERNAME] = playerName } + local msg = self:parseMessage(self:getMessage(MESSAGE_ONCLOSESHOP), parseInfo) + self:say(msg, cid) + end + end + end + end + + -- Handles onPlayerCloseChannel events. If you wish to handle this yourself, use the CALLBACK_PLAYER_CLOSECHANNEL callback. + function NpcHandler:onPlayerCloseChannel(creature) + local cid = creature:getId() + local callback = self:getCallback(CALLBACK_PLAYER_CLOSECHANNEL) + if callback == nil or callback(cid) then + if self:processModuleCallback(CALLBACK_PLAYER_CLOSECHANNEL, cid, msgtype, msg) then + if self:isFocused(cid) then + self:unGreet(cid) + end + end + end + end + + -- Handles onBuy events. If you wish to handle this yourself, use the CALLBACK_ONBUY callback. + function NpcHandler:onBuy(creature, itemid, subType, amount, ignoreCap, inBackpacks) + local cid = creature:getId() + local callback = self:getCallback(CALLBACK_ONBUY) + if callback == nil or callback(cid, itemid, subType, amount, ignoreCap, inBackpacks) then + if self:processModuleCallback(CALLBACK_ONBUY, cid, itemid, subType, amount, ignoreCap, inBackpacks) then + -- + end + end + end + + -- Handles onSell events. If you wish to handle this yourself, use the CALLBACK_ONSELL callback. + function NpcHandler:onSell(creature, itemid, subType, amount, ignoreCap, inBackpacks) + local cid = creature:getId() + local callback = self:getCallback(CALLBACK_ONSELL) + if callback == nil or callback(cid, itemid, subType, amount, ignoreCap, inBackpacks) then + if self:processModuleCallback(CALLBACK_ONSELL, cid, itemid, subType, amount, ignoreCap, inBackpacks) then + -- + end + end + end + + -- Handles onTradeRequest events. If you wish to handle this yourself, use the CALLBACK_ONTRADEREQUEST callback. + function NpcHandler:onTradeRequest(cid) + local callback = self:getCallback(CALLBACK_ONTRADEREQUEST) + if callback == nil or callback(cid) then + if self:processModuleCallback(CALLBACK_ONTRADEREQUEST, cid) then + return true + end + end + return false + end + + -- Handles onThink events. If you wish to handle this yourself, please use the CALLBACK_ONTHINK callback. + function NpcHandler:onThink() + local callback = self:getCallback(CALLBACK_ONTHINK) + if callback == nil or callback() then + if NPCHANDLER_TALKDELAY == TALKDELAY_ONTHINK then + for cid, talkDelay in pairs(self.talkDelay) do + if talkDelay.time and talkDelay.message and os.time() >= talkDelay.time then + selfSay(talkDelay.message, cid, talkDelay.publicize and true or false) + self.talkDelay[cid] = nil + end + end + end + + if self:processModuleCallback(CALLBACK_ONTHINK) then + for pos, focus in pairs(self.focuses) do + if focus then + if not self:isInRange(focus) then + self:onWalkAway(focus) + elseif self.talkStart[focus] and (os.time() - self.talkStart[focus]) > self.idleTime then + self:unGreet(focus) + else + self:updateFocus() + end + end + end + end + end + end + + -- Tries to greet the player with the given cid. + function NpcHandler:onGreet(cid) + if self:isInRange(cid) then + if not self:isFocused(cid) then + self:greet(cid) + return + end + end + end + + -- Simply calls the underlying unGreet function. + function NpcHandler:onFarewell(cid) + self:unGreet(cid) + end + + -- Should be called on this npc's focus if the distance to focus is greater then talkRadius. + function NpcHandler:onWalkAway(cid) + if self:isFocused(cid) then + local callback = self:getCallback(CALLBACK_CREATURE_DISAPPEAR) + if callback == nil or callback() then + if self:processModuleCallback(CALLBACK_CREATURE_DISAPPEAR, cid) then + local msg = self:getMessage(MESSAGE_WALKAWAY) + + local player = Player(cid) + local playerName = player and player:getName() or -1 + local playerSex = player and player:getSex() or 0 + + local parseInfo = { [TAG_PLAYERNAME] = playerName } + local message = self:parseMessage(msg, parseInfo) + + local msg_male = self:getMessage(MESSAGE_WALKAWAY_MALE) + local message_male = self:parseMessage(msg_male, parseInfo) + local msg_female = self:getMessage(MESSAGE_WALKAWAY_FEMALE) + local message_female = self:parseMessage(msg_female, parseInfo) + if message_female ~= message_male then + if playerSex == PLAYERSEX_FEMALE then + selfSay(message_female) + else + selfSay(message_male) + end + elseif message ~= "" then + selfSay(message) + end + self:resetNpc(cid) + self:releaseFocus(cid) + end + end + end + end + + -- Returns true if cid is within the talkRadius of this npc. + function NpcHandler:isInRange(cid) + local distance = Player(cid) and getDistanceTo(cid) or -1 + if distance == -1 then + return false + end + + return distance <= self.talkRadius + end + + -- Resets the npc into its initial state (in regard of the keywordhandler). + -- All modules are also receiving a reset call through their callbackOnModuleReset function. + function NpcHandler:resetNpc(cid) + if self:processModuleCallback(CALLBACK_MODULE_RESET) then + self.keywordHandler:reset(cid) + end + end + + function NpcHandler:cancelNPCTalk(events) + for aux = 1, #events do + stopEvent(events[aux].event) + end + events = nil + end + + function NpcHandler:doNPCTalkALot(msgs, interval, pcid) + if self.eventDelayedSay[pcid] then + self:cancelNPCTalk(self.eventDelayedSay[pcid]) + end + + self.eventDelayedSay[pcid] = {} + local ret = {} + for aux = 1, #msgs do + self.eventDelayedSay[pcid][aux] = {} + doCreatureSayWithDelay(getNpcCid(), msgs[aux], TALKTYPE_PRIVATE_NP, ((aux-1) * (interval or 4000)) + 700, self.eventDelayedSay[pcid][aux], pcid) + ret[#ret + 1] = self.eventDelayedSay[pcid][aux] + end + return(ret) + end + + -- Makes the npc represented by this instance of NpcHandler say something. + -- This implements the currently set type of talkdelay. + -- shallDelay is a boolean value. If it is false, the message is not delayed. Default value is true. + function NpcHandler:say(message, focus, publicize, shallDelay, delay) + if type(message) == "table" then + return self:doNPCTalkALot(message, delay or 6000, focus) + end + + if self.eventDelayedSay[focus] then + self:cancelNPCTalk(self.eventDelayedSay[focus]) + end + + local shallDelay = not shallDelay and true or shallDelay + if NPCHANDLER_TALKDELAY == TALKDELAY_NONE or shallDelay == false then + selfSay(message, focus, publicize and true or false) + return + end + + stopEvent(self.eventSay[focus]) + self.eventSay[focus] = addEvent(function(npcId, message, focusId) + local npc = Npc(npcId) + if npc == nil then + return + end + local player = Player(focusId) + if player then + npc:say(message, TALKTYPE_PRIVATE_NP, false, player, npc:getPosition()) + end + end, self.talkDelayTime * 1000, Npc():getId(), message, focus) + end +end diff --git a/data/npc/lib/npcsystem/npcsystem.lua b/data/npc/lib/npcsystem/npcsystem.lua new file mode 100644 index 0000000..c7d7b61 --- /dev/null +++ b/data/npc/lib/npcsystem/npcsystem.lua @@ -0,0 +1,177 @@ +-- Advanced NPC System by Jiddo + +shop_amount = {} +shop_cost = {} +shop_rlname = {} +shop_itemid = {} +shop_container = {} +shop_npcuid = {} +shop_eventtype = {} +shop_subtype = {} +shop_destination = {} +shop_premium = {} + +npcs_loaded_shop = {} +npcs_loaded_travel = {} + +if not NpcSystem then + -- Loads the underlying classes of the npcsystem. + dofile('data/npc/lib/npcsystem/keywordhandler.lua') + dofile('data/npc/lib/npcsystem/npchandler.lua') + dofile('data/npc/lib/npcsystem/modules.lua') + + -- Global npc constants: + + -- Greeting and unGreeting keywords. For more information look at the top of modules.lua + FOCUS_GREETWORDS = {'hi', 'hello'} + FOCUS_FAREWELLWORDS = {'bye', 'farewell'} + + -- The word for requesting trade window. For more information look at the top of modules.lua + SHOP_TRADEREQUEST = {'trade'} + + -- The word for accepting/declining an offer. CAN ONLY CONTAIN ONE FIELD! For more information look at the top of modules.lua + SHOP_YESWORD = {'yes'} + SHOP_NOWORD = {'no'} + + -- Pattern used to get the amount of an item a player wants to buy/sell. + PATTERN_COUNT = '%d+' + + -- Talkdelay behavior. For more information, look at the top of npchandler.lua. + NPCHANDLER_TALKDELAY = TALKDELAY_ONTHINK + + -- Constant strings defining the keywords to replace in the default messages. + -- For more information, look at the top of npchandler.lua... + TAG_PLAYERNAME = '|PLAYERNAME|' + TAG_ITEMCOUNT = '|ITEMCOUNT|' + TAG_TOTALCOST = '|TOTALCOST|' + TAG_ITEMNAME = '|ITEMNAME|' + + NpcSystem = {} + + -- Gets an npcparameter with the specified key. Returns nil if no such parameter is found. + function NpcSystem.getParameter(key) + local ret = getNpcParameter(tostring(key)) + if (type(ret) == 'number' and ret == 0) then + return nil + else + return ret + end + end + + -- Parses all known parameters for the npc. Also parses parseable modules. + function NpcSystem.parseParameters(npcHandler) + local ret = NpcSystem.getParameter('idletime') + if ret then + npcHandler.idleTime = tonumber(ret) + end + local ret = NpcSystem.getParameter('talkradius') + if ret then + npcHandler.talkRadius = tonumber(ret) + end + local ret = NpcSystem.getParameter('message_greet') + if ret then + npcHandler:setMessage(MESSAGE_GREET, ret) + end + local ret = NpcSystem.getParameter('message_farewell') + if ret then + npcHandler:setMessage(MESSAGE_FAREWELL, ret) + end + local ret = NpcSystem.getParameter('message_decline') + if ret then + npcHandler:setMessage(MESSAGE_DECLINE, ret) + end + local ret = NpcSystem.getParameter('message_needmorespace') + if ret then + npcHandler:setMessage(MESSAGE_NEEDMORESPACE, ret) + end + local ret = NpcSystem.getParameter('message_needspace') + if ret then + npcHandler:setMessage(MESSAGE_NEEDSPACE, ret) + end + local ret = NpcSystem.getParameter('message_sendtrade') + if ret then + npcHandler:setMessage(MESSAGE_SENDTRADE, ret) + end + local ret = NpcSystem.getParameter('message_noshop') + if ret then + npcHandler:setMessage(MESSAGE_NOSHOP, ret) + end + local ret = NpcSystem.getParameter('message_oncloseshop') + if ret then + npcHandler:setMessage(MESSAGE_ONCLOSESHOP, ret) + end + local ret = NpcSystem.getParameter('message_onbuy') + if ret then + npcHandler:setMessage(MESSAGE_ONBUY, ret) + end + local ret = NpcSystem.getParameter('message_onsell') + if ret then + npcHandler:setMessage(MESSAGE_ONSELL, ret) + end + local ret = NpcSystem.getParameter('message_missingmoney') + if ret then + npcHandler:setMessage(MESSAGE_MISSINGMONEY, ret) + end + local ret = NpcSystem.getParameter('message_needmoney') + if ret then + npcHandler:setMessage(MESSAGE_NEEDMONEY, ret) + end + local ret = NpcSystem.getParameter('message_missingitem') + if ret then + npcHandler:setMessage(MESSAGE_MISSINGITEM, ret) + end + local ret = NpcSystem.getParameter('message_needitem') + if ret then + npcHandler:setMessage(MESSAGE_NEEDITEM, ret) + end + local ret = NpcSystem.getParameter('message_idletimeout') + if ret then + npcHandler:setMessage(MESSAGE_IDLETIMEOUT, ret) + end + local ret = NpcSystem.getParameter('message_walkaway') + if ret then + npcHandler:setMessage(MESSAGE_WALKAWAY, ret) + end + local ret = NpcSystem.getParameter('message_alreadyfocused') + if ret then + npcHandler:setMessage(MESSAGE_ALREADYFOCUSED, ret) + end + local ret = NpcSystem.getParameter('message_buy') + if ret then + npcHandler:setMessage(MESSAGE_BUY, ret) + end + local ret = NpcSystem.getParameter('message_sell') + if ret then + npcHandler:setMessage(MESSAGE_SELL, ret) + end + local ret = NpcSystem.getParameter('message_bought') + if ret then + npcHandler:setMessage(MESSAGE_BOUGHT, ret) + end + local ret = NpcSystem.getParameter('message_sold') + if ret then + npcHandler:setMessage(MESSAGE_SOLD, ret) + end + local ret = NpcSystem.getParameter('message_walkaway_male') + if ret then + npcHandler:setMessage(MESSAGE_WALKAWAY_MALE, ret) + end + local ret = NpcSystem.getParameter('message_walkaway_female') + if ret then + npcHandler:setMessage(MESSAGE_WALKAWAY_FEMALE, ret) + end + + -- Parse modules. + for parameter, module in pairs(Modules.parseableModules) do + local ret = NpcSystem.getParameter(parameter) + if ret then + local number = tonumber(ret) + if number ~= 0 and module.parseParameters then + local instance = module:new() + npcHandler:addModule(instance) + instance:parseParameters() + end + end + end + end +end diff --git a/data/npc/scripts/The Oracle.lua b/data/npc/scripts/The Oracle.lua new file mode 100644 index 0000000..6619add --- /dev/null +++ b/data/npc/scripts/The Oracle.lua @@ -0,0 +1,104 @@ +local keywordHandler = KeywordHandler:new() +local npcHandler = NpcHandler:new(keywordHandler) +NpcSystem.parseParameters(npcHandler) + +local vocation = {} +local town = {} +local destination = {} + +function onCreatureAppear(cid) npcHandler:onCreatureAppear(cid) end +function onCreatureDisappear(cid) npcHandler:onCreatureDisappear(cid) end +function onCreatureSay(cid, type, msg) npcHandler:onCreatureSay(cid, type, msg) end +function onThink() npcHandler:onThink() end + +local function greetCallback(cid) + local player = Player(cid) + local level = player:getLevel() + if level < 8 then + npcHandler:say("CHILD! COME BACK WHEN YOU HAVE GROWN UP!", cid) + return false + elseif level > 9 then + npcHandler:say(player:getName() .. ", I CAN'T LET YOU LEAVE - YOU ARE TOO STRONG ALREADY! YOU CAN ONLY LEAVE WITH LEVEL 9 OR LOWER.", cid) + return false + elseif player:getVocation():getId() > 0 then + npcHandler:say("YOU ALREADY HAVE A VOCATION!", cid) + return false + end + return true +end + +local function creatureSayCallback(cid, type, msg) + if not npcHandler:isFocused(cid) then + return false + end + + if msgcontains(msg, "yes") and npcHandler.topic[cid] == 0 then + npcHandler:say("IN WHICH TOWN DO YOU WANT TO LIVE: {RHYVES}?", cid) + npcHandler.topic[cid] = 1 + elseif npcHandler.topic[cid] == 1 then + if msgcontains(msg, "rhyves") then + town[cid] = 2 + destination[cid] = Position(159, 387, 6) + npcHandler:say("IN RHYVES! AND WHAT PROFESSION HAVE YOU CHOSEN: {KNIGHT}, {PALADIN}, {SORCERER}, OR {DRUID}?", cid) + npcHandler.topic[cid] = 2 + else + npcHandler:say("IN WHICH TOWN DO YOU WANT TO LIVE: {RHYVES}?", cid) + end + elseif npcHandler.topic[cid] == 2 then + if msgcontains(msg, "sorcerer") then + npcHandler:say("A SORCERER! ARE YOU SURE? THIS DECISION IS IRREVERSIBLE!", cid) + npcHandler.topic[cid] = 3 + vocation[cid] = 1 + elseif msgcontains(msg, "druid") then + npcHandler:say("A DRUID! ARE YOU SURE? THIS DECISION IS IRREVERSIBLE!", cid) + npcHandler.topic[cid] = 3 + vocation[cid] = 2 + elseif msgcontains(msg, "paladin") then + npcHandler:say("A PALADIN! ARE YOU SURE? THIS DECISION IS IRREVERSIBLE!", cid) + npcHandler.topic[cid] = 3 + vocation[cid] = 3 + elseif msgcontains(msg, "knight") then + npcHandler:say("A KNIGHT! ARE YOU SURE? THIS DECISION IS IRREVERSIBLE!", cid) + npcHandler.topic[cid] = 3 + vocation[cid] = 4 + else + npcHandler:say("{KNIGHT}, {PALADIN}, {SORCERER}, OR {DRUID}?", cid) + end + elseif npcHandler.topic[cid] == 3 then + if msgcontains(msg, "yes") then + local player = Player(cid) + npcHandler:say("SO BE IT!", cid) + player:setVocation(Vocation(vocation[cid])) + player:setTown(Town(town[cid])) + + local destination = destination[cid] + npcHandler:releaseFocus(cid) + player:teleportTo(destination) + player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + destination:sendMagicEffect(CONST_ME_TELEPORT) + else + npcHandler:say("THEN WHAT? {KNIGHT}, {PALADIN}, {SORCERER}, OR {DRUID}?", cid) + npcHandler.topic[cid] = 2 + end + end + return true +end + +local function onAddFocus(cid) + town[cid] = 0 + vocation[cid] = 0 + destination[cid] = 0 +end + +local function onReleaseFocus(cid) + town[cid] = nil + vocation[cid] = nil + destination[cid] = nil +end + +npcHandler:setCallback(CALLBACK_ONADDFOCUS, onAddFocus) +npcHandler:setCallback(CALLBACK_ONRELEASEFOCUS, onReleaseFocus) + +npcHandler:setCallback(CALLBACK_GREET, greetCallback) +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:addModule(FocusModule:new()) diff --git a/data/npc/scripts/bless.lua b/data/npc/scripts/bless.lua new file mode 100644 index 0000000..eb3fc5a --- /dev/null +++ b/data/npc/scripts/bless.lua @@ -0,0 +1,30 @@ +local keywordHandler = KeywordHandler:new() +local npcHandler = NpcHandler:new(keywordHandler) +NpcSystem.parseParameters(npcHandler) + +function onCreatureAppear(cid) npcHandler:onCreatureAppear(cid) end +function onCreatureDisappear(cid) npcHandler:onCreatureDisappear(cid) end +function onCreatureSay(cid, type, msg) npcHandler:onCreatureSay(cid, type, msg) end +function onThink() npcHandler:onThink() end + +local node1 = keywordHandler:addKeyword({'first bless'}, StdModule.say, {npcHandler = npcHandler, onlyFocus = true, text = 'Do you want to buy the first blessing for 10000 gold?'}) + node1:addChildKeyword({'yes'}, StdModule.bless, {npcHandler = npcHandler, bless = 1, premium = true, cost = 10000}) + node1:addChildKeyword({'no'}, StdModule.say, {npcHandler = npcHandler, onlyFocus = true, reset = true, text = 'Too expensive, eh?'}) + +local node2 = keywordHandler:addKeyword({'second bless'}, StdModule.say, {npcHandler = npcHandler, onlyFocus = true, text = 'Do you want to buy the second blessing for 10000 gold?'}) + node2:addChildKeyword({'yes'}, StdModule.bless, {npcHandler = npcHandler, bless = 2, premium = true, cost = 10000}) + node2:addChildKeyword({'no'}, StdModule.say, {npcHandler = npcHandler, onlyFocus = true, reset = true, text = 'Too expensive, eh?'}) + +local node3 = keywordHandler:addKeyword({'third bless'}, StdModule.say, {npcHandler = npcHandler, onlyFocus = true, text = 'Do you want to buy the third blessing for 10000 gold?'}) + node3:addChildKeyword({'yes'}, StdModule.bless, {npcHandler = npcHandler, bless = 3, premium = true, cost = 10000}) + node3:addChildKeyword({'no'}, StdModule.say, {npcHandler = npcHandler, onlyFocus = true, reset = true, text = 'Too expensive, eh?'}) + +local node4 = keywordHandler:addKeyword({'fourth bless'}, StdModule.say, {npcHandler = npcHandler, onlyFocus = true, text = 'Do you want to buy the fourth blessing for 10000 gold?'}) + node4:addChildKeyword({'yes'}, StdModule.bless, {npcHandler = npcHandler, bless = 4, premium = true, cost = 10000}) + node4:addChildKeyword({'no'}, StdModule.say, {npcHandler = npcHandler, onlyFocus = true, reset = true, text = 'Too expensive, eh?'}) + +local node5 = keywordHandler:addKeyword({'fifth bless'}, StdModule.say, {npcHandler = npcHandler, onlyFocus = true, text = 'Do you want to buy the fifth blessing for 10000 gold?'}) + node5:addChildKeyword({'yes'}, StdModule.bless, {npcHandler = npcHandler, bless = 5, premium = true, cost = 10000}) + node5:addChildKeyword({'no'}, StdModule.say, {npcHandler = npcHandler, onlyFocus = true, reset = true, text = 'Too expensive, eh?'}) + +npcHandler:addModule(FocusModule:new()) diff --git a/data/npc/scripts/default.lua b/data/npc/scripts/default.lua new file mode 100644 index 0000000..36d042d --- /dev/null +++ b/data/npc/scripts/default.lua @@ -0,0 +1,10 @@ +local keywordHandler = KeywordHandler:new() +local npcHandler = NpcHandler:new(keywordHandler) +NpcSystem.parseParameters(npcHandler) + +function onCreatureAppear(cid) npcHandler:onCreatureAppear(cid) end +function onCreatureDisappear(cid) npcHandler:onCreatureDisappear(cid) end +function onCreatureSay(cid, type, msg) npcHandler:onCreatureSay(cid, type, msg) end +function onThink() npcHandler:onThink() end + +npcHandler:addModule(FocusModule:new()) diff --git a/data/npc/scripts/promotion.lua b/data/npc/scripts/promotion.lua new file mode 100644 index 0000000..e565d57 --- /dev/null +++ b/data/npc/scripts/promotion.lua @@ -0,0 +1,14 @@ +local keywordHandler = KeywordHandler:new() +local npcHandler = NpcHandler:new(keywordHandler) +NpcSystem.parseParameters(npcHandler) + +function onCreatureAppear(cid) npcHandler:onCreatureAppear(cid) end +function onCreatureDisappear(cid) npcHandler:onCreatureDisappear(cid) end +function onCreatureSay(cid, type, msg) npcHandler:onCreatureSay(cid, type, msg) end +function onThink() npcHandler:onThink() end + +local node1 = keywordHandler:addKeyword({'promot'}, StdModule.say, {npcHandler = npcHandler, onlyFocus = true, text = 'I can promote you for 20000 gold coins. Do you want me to promote you?'}) + node1:addChildKeyword({'yes'}, StdModule.promotePlayer, {npcHandler = npcHandler, cost = 20000, level = 20, text = 'Congratulations! You are now promoted.'}) + node1:addChildKeyword({'no'}, StdModule.say, {npcHandler = npcHandler, onlyFocus = true, text = 'Alright then, come back when you are ready.', reset = true}) + +npcHandler:addModule(FocusModule:new()) diff --git a/data/npc/scripts/runes.lua b/data/npc/scripts/runes.lua new file mode 100644 index 0000000..386c43a --- /dev/null +++ b/data/npc/scripts/runes.lua @@ -0,0 +1,138 @@ +local keywordHandler = KeywordHandler:new() +local npcHandler = NpcHandler:new(keywordHandler) +NpcSystem.parseParameters(npcHandler) + +function onCreatureAppear(cid) npcHandler:onCreatureAppear(cid) end +function onCreatureDisappear(cid) npcHandler:onCreatureDisappear(cid) end +function onCreatureSay(cid, type, msg) npcHandler:onCreatureSay(cid, type, msg) end +function onThink() npcHandler:onThink() end + +local voices = { {text = "Runes, wands, rods, health and mana potions! Have a look!"} } +npcHandler:addModule(VoiceModule:new(voices)) + +local shopModule = ShopModule:new() +npcHandler:addModule(shopModule) + +shopModule:addBuyableItem({'spellbook'}, 2175, 150, 'spellbook') +shopModule:addBuyableItem({'magic lightwand'}, 2163, 400, 'magic lightwand') + +shopModule:addBuyableItem({'small health'}, 8704, 20, 1, 'small health potion') +shopModule:addBuyableItem({'health potion'}, 7618, 45, 1, 'health potion') +shopModule:addBuyableItem({'mana potion'}, 7620, 50, 1, 'mana potion') +shopModule:addBuyableItem({'strong health'}, 7588, 100, 1, 'strong health potion') +shopModule:addBuyableItem({'strong mana'}, 7589, 80, 1, 'strong mana potion') +shopModule:addBuyableItem({'great health'}, 7591, 190, 1, 'great health potion') +shopModule:addBuyableItem({'great mana'}, 7590, 120, 1, 'great mana potion') +shopModule:addBuyableItem({'great spirit'}, 8472, 190, 1, 'great spirit potion') +shopModule:addBuyableItem({'ultimate health'}, 8473, 310, 1, 'ultimate health potion') +shopModule:addBuyableItem({'antidote potion'}, 8474, 50, 1, 'antidote potion') + +shopModule:addSellableItem({'normal potion flask', 'normal flask'}, 7636, 5, 'empty small potion flask') +shopModule:addSellableItem({'strong potion flask', 'strong flask'}, 7634, 10, 'empty strong potion flask') +shopModule:addSellableItem({'great potion flask', 'great flask'}, 7635, 15, 'empty great potion flask') + +shopModule:addBuyableItem({'intense healing'}, 2265, 95, 1, 'intense healing rune') +shopModule:addBuyableItem({'ultimate healing'}, 2273, 175, 1, 'ultimate healing rune') +shopModule:addBuyableItem({'magic wall'}, 2293, 350, 3, 'magic wall rune') +shopModule:addBuyableItem({'destroy field'}, 2261, 45, 3, 'destroy field rune') +shopModule:addBuyableItem({'light magic missile'}, 2287, 40, 10, 'light magic missile rune') +shopModule:addBuyableItem({'heavy magic missile'}, 2311, 120, 10, 'heavy magic missile rune') +shopModule:addBuyableItem({'great fireball'}, 2304, 180, 4, 'great fireball rune') +shopModule:addBuyableItem({'explosion'}, 2313, 250, 6, 'explosion rune') +shopModule:addBuyableItem({'sudden death'}, 2268, 350, 3, 'sudden death rune') +shopModule:addBuyableItem({'paralyze'}, 2278, 700, 1, 'paralyze rune') +shopModule:addBuyableItem({'animate dead'}, 2316, 375, 1, 'animate dead rune') +shopModule:addBuyableItem({'convince creature'}, 2290, 80, 1, 'convince creature rune') +shopModule:addBuyableItem({'chameleon'}, 2291, 210, 1, 'chameleon rune') +shopModule:addBuyableItem({'disintegrate'}, 2310, 80, 3, 'disintegrate rune') + +shopModule:addBuyableItemContainer({'bp ap'}, 2002, 8378, 2000, 1, 'backpack of antidote potions') +shopModule:addBuyableItemContainer({'bp slhp'}, 2000, 8610, 400, 1, 'backpack of small health potions') +shopModule:addBuyableItemContainer({'bp hp'}, 2000, 7618, 900, 1, 'backpack of health potions') +shopModule:addBuyableItemContainer({'bp mp'}, 2001, 7620, 1000, 1, 'backpack of mana potions') +shopModule:addBuyableItemContainer({'bp shp'}, 2000, 7588, 2000, 1, 'backpack of strong health potions') +shopModule:addBuyableItemContainer({'bp smp'}, 2001, 7589, 1600, 1, 'backpack of strong mana potions') +shopModule:addBuyableItemContainer({'bp ghp'}, 2000, 7591, 3800, 1, 'backpack of great health potions') +shopModule:addBuyableItemContainer({'bp gmp'}, 2001, 7590, 2400, 1, 'backpack of great mana potions') +shopModule:addBuyableItemContainer({'bp gsp'}, 1999, 8376, 3800, 1, 'backpack of great spirit potions') +shopModule:addBuyableItemContainer({'bp uhp'}, 2000, 8377, 6200, 1, 'backpack of ultimate health potions') + +shopModule:addBuyableItem({'wand of vortex', 'vortex'}, 2190, 500, 'wand of vortex') +shopModule:addBuyableItem({'wand of dragonbreath', 'dragonbreath'}, 2191, 1000, 'wand of dragonbreath') +shopModule:addBuyableItem({'wand of decay', 'decay'}, 2188, 5000, 'wand of decay') +shopModule:addBuyableItem({'wand of draconia', 'draconia'}, 8921, 7500, 'wand of draconia') +shopModule:addBuyableItem({'wand of cosmic energy', 'cosmic energy'}, 2189, 10000, 'wand of cosmic energy') +shopModule:addBuyableItem({'wand of inferno', 'inferno'}, 2187, 15000, 'wand of inferno') +shopModule:addBuyableItem({'wand of starstorm', 'starstorm'}, 8920, 18000, 'wand of starstorm') +shopModule:addBuyableItem({'wand of voodoo', 'voodoo'}, 8922, 22000, 'wand of voodoo') + +shopModule:addBuyableItem({'snakebite rod', 'snakebite'}, 2182, 500, 'snakebite rod') +shopModule:addBuyableItem({'moonlight rod', 'moonlight'}, 2186, 1000, 'moonlight rod') +shopModule:addBuyableItem({'necrotic rod', 'necrotic'}, 2185, 5000, 'necrotic rod') +shopModule:addBuyableItem({'northwind rod', 'northwind'}, 8911, 7500, 'northwind rod') +shopModule:addBuyableItem({'terra rod', 'terra'}, 2181, 10000, 'terra rod') +shopModule:addBuyableItem({'hailstorm rod', 'hailstorm'}, 2183, 15000, 'hailstorm rod') +shopModule:addBuyableItem({'springsprout rod', 'springsprout'}, 8912, 18000, 'springsprout rod') +shopModule:addBuyableItem({'underworld rod', 'underworld'}, 8910, 22000, 'underworld rod') + +shopModule:addSellableItem({'wand of vortex', 'vortex'}, 2190, 250, 'wand of vortex') +shopModule:addSellableItem({'wand of dragonbreath', 'dragonbreath'}, 2191, 500, 'wand of dragonbreath') +shopModule:addSellableItem({'wand of decay', 'decay'}, 2188, 2500, 'wand of decay') +shopModule:addSellableItem({'wand of draconia', 'draconia'}, 8921, 3750, 'wand of draconia') +shopModule:addSellableItem({'wand of cosmic energy', 'cosmic energy'}, 2189, 5000, 'wand of cosmic energy') +shopModule:addSellableItem({'wand of inferno', 'inferno'},2187, 7500, 'wand of inferno') +shopModule:addSellableItem({'wand of starstorm', 'starstorm'}, 8920, 9000, 'wand of starstorm') +shopModule:addSellableItem({'wand of voodoo', 'voodoo'}, 8922, 11000, 'wand of voodoo') + +shopModule:addSellableItem({'snakebite rod', 'snakebite'}, 2182, 250,'snakebite rod') +shopModule:addSellableItem({'moonlight rod', 'moonlight'}, 2186, 500, 'moonlight rod') +shopModule:addSellableItem({'necrotic rod', 'necrotic'}, 2185, 2500, 'necrotic rod') +shopModule:addSellableItem({'northwind rod', 'northwind'}, 8911, 3750, 'northwind rod') +shopModule:addSellableItem({'terra rod', 'terra'}, 2181, 5000, 'terra rod') +shopModule:addSellableItem({'hailstorm rod', 'hailstorm'}, 2183, 7500, 'hailstorm rod') +shopModule:addSellableItem({'springsprout rod', 'springsprout'}, 8912, 9000, 'springsprout rod') +shopModule:addSellableItem({'underworld rod', 'underworld'}, 8910, 11000, 'underworld rod') + + +function creatureSayCallback(cid, type, msg) + if not npcHandler:isFocused(cid) then + return false + end + + local player = Player(cid) + local vocationId = player:getVocation():getId() + local items = { + [1] = 2190, + [2] = 2182, + [5] = 2190, + [6] = 2182 + } + + if msgcontains(msg, 'first rod') or msgcontains(msg, 'first wand') then + if table.contains({1, 2, 5, 6}, vocationId) then + if player:getStorageValue(PlayerStorageKeys.firstRod) == -1 then + selfSay('So you ask me for a {' .. ItemType(items[vocationId]):getName() .. '} to begin your adventure?', cid) + npcHandler.topic[cid] = 1 + else + selfSay('What? I have already gave you one {' .. ItemType(items[vocationId]):getName() .. '}!', cid) + end + else + selfSay('Sorry, you aren\'t a druid either a sorcerer.', cid) + end + elseif msgcontains(msg, 'yes') then + if npcHandler.topic[cid] == 1 then + player:addItem(items[vocationId], 1) + selfSay('Here you are young adept, take care yourself.', cid) + player:setStorageValue(PlayerStorageKeys.firstRod, 1) + end + npcHandler.topic[cid] = 0 + elseif msgcontains(msg, 'no') and npcHandler.topic[cid] == 1 then + selfSay('Ok then.', cid) + npcHandler.topic[cid] = 0 + end + + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:addModule(FocusModule:new()) diff --git a/data/raids/raids.xml b/data/raids/raids.xml new file mode 100644 index 0000000..e61a5a6 --- /dev/null +++ b/data/raids/raids.xml @@ -0,0 +1,7 @@ + + + + diff --git a/data/raids/testraid.xml b/data/raids/testraid.xml new file mode 100644 index 0000000..ebfe9d0 --- /dev/null +++ b/data/raids/testraid.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/data/reports/bugs/.gitignore b/data/reports/bugs/.gitignore new file mode 100644 index 0000000..eb67fe0 --- /dev/null +++ b/data/reports/bugs/.gitignore @@ -0,0 +1,3 @@ +# Ignore user generated reports +* +!.gitignore diff --git a/data/reports/players/.gitignore b/data/reports/players/.gitignore new file mode 100644 index 0000000..eb67fe0 --- /dev/null +++ b/data/reports/players/.gitignore @@ -0,0 +1,3 @@ +# Ignore user generated reports +* +!.gitignore diff --git a/data/scripts/actions/others/afflicted_outfit.lua b/data/scripts/actions/others/afflicted_outfit.lua new file mode 100644 index 0000000..8be2b74 --- /dev/null +++ b/data/scripts/actions/others/afflicted_outfit.lua @@ -0,0 +1,61 @@ +local afflictedOutfit = Action() + +function afflictedOutfit.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if not player:isPremium() then + player:sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT) + return true + end + local hasOutfit = player:getStorageValue(PlayerStorageKeys.afflictedOutfit) == 1 + if item.itemid == 13925 then -- plague mask + if not hasOutfit then + return false + end + if player:getStorageValue(PlayerStorageKeys.afflictedPlagueMask) == 1 then + return false + end + player:addOutfitAddon(430, 2) + player:addOutfitAddon(431, 2) + player:setStorageValue(PlayerStorageKeys.afflictedPlagueMask, 1) + player:say("You gained a plague mask for your outfit.", TALKTYPE_MONSTER_SAY, false, player) + if player:hasOutfit(looktype, 3) then + player:addAchievement("Beak Doctor") + end + item:remove(1) + elseif item.itemid == 13926 then -- plague bell + if not hasOutfit then + return false + end + if player:getStorageValue(PlayerStorageKeys.addonPlagueBell) == 1 then + return false + end + player:addOutfitAddon(430, 1) + player:addOutfitAddon(431, 1) + player:setStorageValue(PlayerStorageKeys.addonPlagueBell, 1) + player:say("You gained a plague bell for your outfit.", TALKTYPE_MONSTER_SAY, false, player) + if player:hasOutfit(looktype, 3) then + player:addAchievement("Beak Doctor") + end + item:remove(1) + else -- outfit + if hasOutfit then + return false + end + for id = 13540, 13545 do + if player:getItemCount(id) < 1 then + return false + end + end + for id = 13540, 13545 do + player:removeItem(id, 1) + end + player:addOutfit(430) + player:addOutfit(431) + player:getPosition():sendMagicEffect(CONST_ME_GREEN_RINGS) + player:setStorageValue(PlayerStorageKeys.afflictedOutfit, 1) + player:say("You have restored an outfit.", TALKTYPE_MONSTER_SAY, false, player) + end + return true +end + +afflictedOutfit:id(13540, 13541, 13542, 13543, 13544, 13545, 13925, 13926) +afflictedOutfit:register() diff --git a/data/scripts/actions/others/blessing_charms.lua b/data/scripts/actions/others/blessing_charms.lua new file mode 100644 index 0000000..650283a --- /dev/null +++ b/data/scripts/actions/others/blessing_charms.lua @@ -0,0 +1,29 @@ +local items = { + [11260] = {text = "The Spiritual Shielding protects you.", id = 1, effect = CONST_ME_LOSEENERGY}, + [11259] = {text = "The Embrace of Tibia surrounds you.", id = 2, effect = CONST_ME_MAGIC_BLUE}, + [11261] = {text = "The Fire of the Suns engulfs you.", id = 3, effect = CONST_ME_MAGIC_RED}, + [11262] = {text = "The Wisdom of Solitude inspires you.", id = 4, effect = CONST_ME_MAGIC_GREEN}, + [11258] = {text = "The Spark of the Phoenix emblazes you.", id = 5, effect = CONST_ME_FIREATTACK} +} + +local blessingCharms = Action() + +function blessingCharms.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local blessItem = items[item.itemid] + if blessItem then + if player:hasBlessing(blessItem.id) then + player:say("You already possess this blessing.", TALKTYPE_MONSTER_SAY) + return true + end + player:addBlessing(blessItem.id) + player:say(blessItem.text, TALKTYPE_MONSTER_SAY) + player:getPosition():sendMagicEffect(blessItem.effect) + item:remove() + end + return true +end + +for k, v in pairs(items) do + blessingCharms:id(k) +end +blessingCharms:register() diff --git a/data/scripts/actions/others/carpets.lua b/data/scripts/actions/others/carpets.lua new file mode 100644 index 0000000..d5c5ac3 --- /dev/null +++ b/data/scripts/actions/others/carpets.lua @@ -0,0 +1,50 @@ +local transformID = { + [25393] = 25392, [25392] = 25393, -- rift carpet + [26193] = 26192, [26192] = 26193, -- void carpet + [26087] = 26109, [26109] = 26087, -- yalaharian carpet + [26088] = 26110, [26110] = 26088, -- white fur carpet + [26089] = 26111, [26111] = 26089, -- bamboo mat carpet + [26371] = 26363, [26363] = 26371, -- crimson carpet + [26366] = 26372, [26372] = 26366, -- azure carpet + [26367] = 26373, [26373] = 26367, -- emerald carpet + [26368] = 26374, [26374] = 26368, -- light parquet carpet + [26369] = 26375, [26375] = 26369, -- dark parquet carpet + [26370] = 26376, [26376] = 26370 -- marble floor +} + +local carpets = Action() + +function carpets.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local carpet = transformID[item.itemid] + if not carpet then + return false + end + if fromPosition.x == CONTAINER_POSITION then + player:sendTextMessage(MESSAGE_STATUS_SMALL, "Put the item on the floor first.") + return true + end + local tile = Tile(item:getPosition()) + if not tile:getHouse() then + player:sendTextMessage(MESSAGE_STATUS_SMALL, "You may use this only inside a house.") + return true + end + if tile:getItemByType(ITEM_TYPE_DOOR) then + player:sendCancelMessage("You cannot use this item on house doors.") + return true + end + local carpetStack = 0 + for _, carpetId in pairs(transformID) do + carpetStack = carpetStack + tile:getItemCountById(carpetId) + end + if carpetStack > 1 then + player:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + return true + end + item:transform(carpet) + return true +end + +for k, v in pairs(transformID) do + carpets:id(k) +end +carpets:register() diff --git a/data/scripts/actions/others/christmas_bundle.lua b/data/scripts/actions/others/christmas_bundle.lua new file mode 100644 index 0000000..d858d7c --- /dev/null +++ b/data/scripts/actions/others/christmas_bundle.lua @@ -0,0 +1,70 @@ +local christmasBundle = Action() + +function christmasBundle.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local presents = { -- [christmas bundle item id] = {{reward item id, count}, ...} + [6507] = { -- red bundle + {6569, 15}, -- candy + {2687, 20}, -- cookie + {2688, 10}, -- candy cane + {2675, 10}, -- orange + {2674, 5}, -- red apple + 6501, -- gingerbreadman + 6502, -- christmas wreath + 6490, -- christmas branch + 6504, -- red christmas garland + 6388 -- christmas card + }, + [6508] = { -- blue bundle + {6569, 15}, -- candy + {2687, 20}, -- cookie + {2688, 10}, -- candy cane + {2675, 10}, -- orange + {2674, 5}, -- red apple + 6501, -- gingerbreadman + 6502, -- christmas wreath + 6490, -- christmas branch + 6505, -- blue christmas garland + 6388 -- christmas card + }, + [6509] = { -- green bundle + {6569, 15}, -- candy + {2687, 20}, -- cookie + {2688, 10}, -- candy cane + {2675, 10}, -- orange + {2674, 5}, -- red apple + 6501, -- gingerbreadman + 6502, -- christmas wreath + 6490, -- christmas branch + 6503, -- christmas garland + 6388 -- christmas card + } + } + + local targetItem = presents[item.itemid] + if not targetItem then + return true + end + + local rewards = {} + while #rewards < 7 do + local count = 1 + local rand = math.random(#targetItem) + local gift = targetItem[rand] + if type(gift) == "table" then + gift, count = unpack(gift) + end + rewards[#rewards + 1] = {gift, count} + table.remove(targetItem, rand) + end + + for i = 1, #rewards do + player:addItem(unpack(rewards[i])) + end + item:remove(1) + player:getPosition():sendMagicEffect(CONST_ME_GIFT_WRAPS) + player:addAchievementProgress("Santa's Li'l Helper", 25) + return true +end + +christmasBundle:id(6507,6508,6509) +christmasBundle:register() diff --git a/data/scripts/actions/others/clay_lump.lua b/data/scripts/actions/others/clay_lump.lua new file mode 100644 index 0000000..20d66fb --- /dev/null +++ b/data/scripts/actions/others/clay_lump.lua @@ -0,0 +1,43 @@ +local config = { + {chance = {0.0, 1.54}, transformId = 11342, description = "This little figurine of Brog, the raging Titan, was skillfully made by |PLAYERNAME|.", achievement = true}, + {chance = {1.54, 9.16}, transformId = 11341, description = "It was made by |PLAYERNAME| and is clearly a little figurine of.. hm, one does not recognise that yet."}, + {chance = {9.16, 25.48}, transformId = 11340, description = "It was made by |PLAYERNAME|, whose potter skills could use some serious improvement."}, + {chance = {25.48, 100.0}, remove = true, sound = "Aw man. That did not work out too well."} +} + +local clayLump = Action() + +function clayLump.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local random, tmpItem = math.random(0, 10000) * 0.01 + for i = 1, #config do + tmpItem = config[i] + if random >= tmpItem.chance[1] and random < tmpItem.chance[2] then + item:getPosition():sendMagicEffect(CONST_ME_POFF) + + if tmpItem.remove then + item:remove() + else + item:transform(tmpItem.transformId) + end + + if tmpItem.sound then + player:say(tmpItem.sound, TALKTYPE_MONSTER_SAY, false, player) + end + + if tmpItem.description then + item:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, tmpItem.description:gsub('|PLAYERNAME|', player:getName())) + end + + if tmpItem.achievement then + player:addAchievement("Clay Fighter") + player:addAchievementProgress("Clay to Fame", 5) + end + + break + end + end + return true +end + +clayLump:id(11339) +clayLump:register() diff --git a/data/scripts/actions/others/costume_bag.lua b/data/scripts/actions/others/costume_bag.lua new file mode 100644 index 0000000..6aa8667 --- /dev/null +++ b/data/scripts/actions/others/costume_bag.lua @@ -0,0 +1,24 @@ +local config = { + [7737] = {"orc warrior", "pirate cutthroat", "dworc voodoomaster", "dwarf guard", "minotaur mage"}, -- common + [7739] = {"serpent spawn", "demon", "juggernaut", "behemoth", "ashmunrah"}, -- deluxe + [9076] = {"quara hydromancer", "diabolic imp", "banshee", "frost giant", "lich"} -- uncommon +} + +local costumeBag = Action() + +function costumeBag.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local creatures = config[item.itemid] + if not creatures then + return true + end + player:setMonsterOutfit(creatures[math.random(#creatures)], 5 * 60 * 10 * 1000) + player:addAchievementProgress("Masquerader", 100) + item:getPosition():sendMagicEffect(CONST_ME_MAGIC_RED) + item:remove() + return true +end + +for k,v in pairs(config) do + costumeBag:id(k) +end +costumeBag:register() diff --git a/data/scripts/actions/others/crate_usable.lua b/data/scripts/actions/others/crate_usable.lua new file mode 100644 index 0000000..e3f2401 --- /dev/null +++ b/data/scripts/actions/others/crate_usable.lua @@ -0,0 +1,43 @@ +local rewards = { -- chanceMin, chanceMax, itemID, count + {1, 36}, -- nothing + {37, 46, 2148, 80}, -- gold coin + {47, 55, 2148, 50}, -- gold coin + {56, 64, 2671, 5}, -- ham + {65, 73, 2789, 5}, -- brown mushroom + {74, 81, 7620}, -- mana potion + {82, 87, 7618}, -- health potion + {88, 92, 9811}, -- rusty legs (common) + {93, 96, 9808}, -- rusty armor (common) + {97, 100, 2213} -- dwarven ring +} + +local crateUsable = Action() + +function crateUsable.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if (player:getStorageValue(PlayerStorageKeys.crateUsable)) <= os.time() then + local totalChance = math.random(100) + for i = 1, #rewards do + local reward = rewards[i] + if totalChance >= reward[1] and totalChance <= reward[2] then + if reward[3] then + local item = ItemType(reward[3]) + local count = reward[4] or 1 + player:addItem(reward[3], count) + local str = ("You found %s %s!"):format(count > 1 and count or item:getArticle(), count > 1 and item:getPluralName() or item:getName()) + player:say(str, TALKTYPE_MONSTER_SAY, false, player, toPosition) + player:setStorageValue(PlayerStorageKeys.crateUsable, os.time() + 20 * 60 * 60) + player:addAchievementProgress("Free Items!", 50) + else + player:say("You found nothing useful.", TALKTYPE_MONSTER_SAY, false, player, toPosition) + end + break + end + end + else + player:say("You found nothing useful.", TALKTYPE_MONSTER_SAY, false, player, toPosition) + end + return true +end + +crateUsable:id(9661) +crateUsable:register() diff --git a/data/scripts/actions/others/cup_of_molten_gold.lua b/data/scripts/actions/others/cup_of_molten_gold.lua new file mode 100644 index 0000000..72e2946 --- /dev/null +++ b/data/scripts/actions/others/cup_of_molten_gold.lua @@ -0,0 +1,25 @@ +local cupOfMoltenGold = Action() + +function cupOfMoltenGold.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local targetId = target.itemid + if not table.contains({2700, 21428}, targetId) then -- fir tree or fir cone + return false + end + if math.random(100) <= 10 then + if targetId == 21428 then -- fir cone + item:transform(13539) -- golden fir cone + else + player:addItem(13539, 1) + end + else + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Drizzling all over a fir cone you picked from the tree, the molten gold only covers about half of it - not enough.") + if targetId == 21428 then -- fir cone + target:remove(1) + end + item:remove(1) + end + return true +end + +cupOfMoltenGold:id(13941) -- cup of molten gold +cupOfMoltenGold:register() diff --git a/data/scripts/actions/others/dolls.lua b/data/scripts/actions/others/dolls.lua new file mode 100644 index 0000000..4878ebd --- /dev/null +++ b/data/scripts/actions/others/dolls.lua @@ -0,0 +1,159 @@ +local dollsTable = { + [5080] = {"Hug me!"}, + [5669] = { + "It's not winning that matters, but winning in style.", + "Today's your lucky day. Probably.", + "Do not meddle in the affairs of dragons, for you are crunchy and taste good with ketchup.", + "That is one stupid question.", + "You'll need more rum for that.", + "Do or do not. There is no try.", + "You should do something you always wanted to.", + "If you walk under a ladder and it falls down on you it probably means bad luck.", + "Never say 'oops'. Always say 'Ah, interesting!'", + "Five steps east, fourteen steps south, two steps north and seventeen steps west!" + }, + [6566] = { + "Fchhhhhh!", + "Zchhhhhh!", + "Grooaaaaar*cough*", + "Aaa... CHOO!", + "You... will.... burn!!" + }, + [6388] = {"Merry Christmas |PLAYERNAME|."}, + [6512] = { + "Ho ho ho", + "Jingle bells, jingle bells...", + "Have you been naughty?", + "Have you been nice?", + "Merry Christmas!", + "Can you stop squeezing me now... I'm starting to feel a little sick." + }, + [8974] = {"ARE YOU PREPARED TO FACE YOUR DESTINY?"}, + [8977] = { + "Weirdo, you're a weirdo! Actually all of you are!", + "Pie for breakfast, pie for lunch and pie for dinner!", + "All hail the control panel!", + "I own, god owns, perfect match!", + "Hug me! Feed me! Hail me!" + }, + [8981] = { + "It's news to me.", + "News, updated as infrequently as possible!", + "Extra! Extra! Read all about it!", + "Fresh off the press!" + }, + [8982] = { + "Hail!", + "So cold.", + "Run, mammoth!" + }, + [23806] = { + "I can hear their whisperings... Revenge!", + "You shall feel pain and terror, |PLAYERNAME|", + "I do not need a sword to slaughter you", + "My sword is broken, but my spirit is not dead", + "I can say 469 and more...", + "My dark magic lies on the world" + }, + [24331] = { + "Hail! (União&Força)", + "Hail |PLAYERNAME|! (União&Força)", + "Only the real killers can touch me!", + "The path of assassin is found in death, DIE!", + " Ahhh... silent and deadly..." + }, + [20624] = { + "Hail!", + "Shhhhhh, please be quiet!", + "Books are great!! Aren't they?" + }, + [16107] = { + "My powers are limitless!", + "Hail!" + }, + [13030] = { + "For zze emperor!", + "Hail!", + "Hail |PLAYERNAME|!" + }, + [13559] = { + "Mhausheausheu! What a FAIL! Mwahaha!", + "Hail |PLAYERNAME|! You are wearing old socks!", + "You are so unpopular even your own shadow refuses to follow you.", + "Have fun!" + }, + [10063] = { + "Hail |PLAYERNAME|! Hail!", + "Hauopa!", + "WHERE IS MY HYDROMEL?!", + "Yala Boom" + }, + [24776] = { + "Silence! I smell something!", + "Let me guide you, |PLAYERNAME|!", + "I have a bad feeling about this.", + "Watch your steps - we found the pit latrine." + }, + [24316] = { + "Hail!", + "Don't be afraid of the darkness!", + "Feel lucky, |PLAYERNAME|!", + "Purrrrrrr!" + } +} + +local dolls = Action() + +function dolls.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local sounds = dollsTable[item.itemid] + if not sounds then + return false + end + + if fromPosition.x == CONTAINER_POSITION then + fromPosition = player:getPosition() + end + + local chance = math.random(#sounds) + local sound = sounds[chance] + if item.itemid == 6566 then + if chance == 3 then + fromPosition:sendMagicEffect(CONST_ME_POFF) + elseif chance == 4 then + fromPosition:sendMagicEffect(CONST_ME_FIREAREA) + elseif chance == 5 then + doTargetCombat(0, player, COMBAT_PHYSICALDAMAGE, -1, -1, CONST_ME_EXPLOSIONHIT) + end + elseif item.itemid == 5669 then + player:addAchievementProgress("Superstitious", 100) + fromPosition:sendMagicEffect(CONST_ME_MAGIC_RED) + item:transform(item.itemid + 1) + item:decay() + elseif item.itemid == 6388 then + fromPosition:sendMagicEffect(CONST_ME_SOUND_YELLOW) + elseif item.itemid == 23806 then + item:transform(item.itemid + 1) + item:decay() + elseif item.itemid == 16107 then + item:transform(item.itemid + 1) + item:decay() + elseif item.itemid == 13559 then + item:transform(13581) + item:decay() + elseif item.itemid == 10063 then + item:transform(item.itemid + 1) + item:decay() + elseif item.itemid == 24776 then + item:transform(item.itemid + 1) + item:decay() + end + + sound = sound:gsub('|PLAYERNAME|', player:getName()) + player:say(sound, TALKTYPE_MONSTER_SAY, false, 0, fromPosition) + return true +end + +for k, v in pairs(dollsTable) do + dolls:id(k) +end +dolls:register() diff --git a/data/scripts/actions/others/explosive_present.lua b/data/scripts/actions/others/explosive_present.lua new file mode 100644 index 0000000..fbaee2a --- /dev/null +++ b/data/scripts/actions/others/explosive_present.lua @@ -0,0 +1,11 @@ +local explosivePresent = Action() + +function explosivePresent.onUse(player, item, fromPosition, target, toPosition, isHotkey) + player:say("KABOOOOOOOOOOM!", TALKTYPE_MONSTER_SAY) + player:getPosition():sendMagicEffect(CONST_ME_FIREAREA) + item:remove() + return true +end + +explosivePresent:id(8110) +explosivePresent:register() diff --git a/data/scripts/actions/others/ferumbras_amulet.lua b/data/scripts/actions/others/ferumbras_amulet.lua new file mode 100644 index 0000000..075bc0f --- /dev/null +++ b/data/scripts/actions/others/ferumbras_amulet.lua @@ -0,0 +1,17 @@ +local ferumbrasAmulet = Action() + +function ferumbrasAmulet.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if math.random(2) == 1 then + player:addMana(1000) + else + player:addHealth(1000) + end + item:transform(25424) + item:getPosition():sendMagicEffect(CONST_ME_MAGIC_RED) + item:decay() + player:say("Magical sparks whirl around the amulet and you suddenly feel refreshed.", TALKTYPE_MONSTER_SAY) + return true +end + +ferumbrasAmulet:id(25423) +ferumbrasAmulet:register() diff --git a/data/scripts/actions/others/ferumbras_mana_keg.lua b/data/scripts/actions/others/ferumbras_mana_keg.lua new file mode 100644 index 0000000..c52de33 --- /dev/null +++ b/data/scripts/actions/others/ferumbras_mana_keg.lua @@ -0,0 +1,13 @@ +local ferumbrasManaKeg = Action() + +function ferumbrasManaKeg.onUse(player, item, fromPosition, target, toPosition, isHotkey) + player:addItem(7590, 10) + item:transform(25426) + item:getPosition():sendMagicEffect(CONST_ME_MAGIC_RED) + item:decay() + player:say("Magical sparks whirl around the keg as you open the spigot and you fill ten empty vials with mana fluid.", TALKTYPE_MONSTER_SAY) + return true +end + +ferumbrasManaKeg:id(25425) +ferumbrasManaKeg:register() diff --git a/data/scripts/actions/others/ferumbras_staff.lua b/data/scripts/actions/others/ferumbras_staff.lua new file mode 100644 index 0000000..5d045e7 --- /dev/null +++ b/data/scripts/actions/others/ferumbras_staff.lua @@ -0,0 +1,16 @@ +local ferumbrasStaff = Action() + +function ferumbrasStaff.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if math.random(100) > 20 then + item:transform(25423) + else + item:transform(25422) + end + item:getPosition():sendMagicEffect(CONST_ME_MAGIC_RED) + item:decay() + player:say("Magical sparks whirl around the staff and suddenly it changes its appearance.", TALKTYPE_MONSTER_SAY) + return true +end + +ferumbrasStaff:id(25420) +ferumbrasStaff:register() diff --git a/data/scripts/actions/others/flower_pot.lua b/data/scripts/actions/others/flower_pot.lua new file mode 100644 index 0000000..efe800b --- /dev/null +++ b/data/scripts/actions/others/flower_pot.lua @@ -0,0 +1,83 @@ +local flowers = { + {itemid = 7655, watered = false, advance = false, msg = "You should plant some seeds first."}, + {itemid = 7665, watered = true, advance = false, msg = "You watered your plant.", after = 7673}, + {itemid = 7673, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 7670, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 7680, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 7682, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 7684, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 7686, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 7688, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 7690, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 7992, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 7994, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 9982, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 9990, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 7692, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 7694, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 9986, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 9988, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 7689, watered = true, advance = false, msg = "You watered your plant.", after = 7688}, + {itemid = 7691, watered = true, advance = false, msg = "You watered your plant.", after = 7690}, + {itemid = 7693, watered = true, advance = false, msg = "You watered your plant.", after = 7692}, + {itemid = 7695, watered = true, advance = false, msg = "You watered your plant.", after = 7694}, + {itemid = 9991, watered = true, advance = false, msg = "You watered your plant.", after = 9990}, + {itemid = 9989, watered = true, advance = false, msg = "You watered your plant.", after = 9988}, + {itemid = 7674, watered = true, advance = false, msg = "You finally remembered to water your plant and it recovered.", after = 7688}, + {itemid = 7675, watered = true, advance = false, msg = "You finally remembered to water your plant and it recovered.", after = 7690}, + {itemid = 7676, watered = true, advance = false, msg = "You finally remembered to water your plant and it recovered.", after = 7692}, + {itemid = 7677, watered = true, advance = false, msg = "You finally remembered to water your plant and it recovered.", after = 7694}, + {itemid = 9984, watered = true, advance = false, msg = "You finally remembered to water your plant and it recovered.", after = 9990}, + {itemid = 9985, watered = true, advance = false, msg = "You finally remembered to water your plant and it recovered.", after = 9988}, + {itemid = 7679, watered = true, advance = true, msg = {"You watered your plant.","Your plant has grown to the next stage!"}, after = {7673, 7670}, chance = 80}, + {itemid = 7681, watered = true, advance = true, msg = {"You watered your plant.","Your plant has grown to the next stage!"}, after = {7680, 7688}, chance = 80}, + {itemid = 7683, watered = true, advance = true, msg = {"You watered your plant.","Your plant has grown to the next stage!"}, after = {7682, 7690}, chance = 80}, + {itemid = 7685, watered = true, advance = true, msg = {"You watered your plant.","Your plant has grown to the next stage!"}, after = {7684, 7692}, chance = 80}, + {itemid = 7687, watered = true, advance = true, msg = {"You watered your plant.","Your plant has grown to the next stage!"}, after = {7686, 7694}, chance = 80}, + {itemid = 9983, watered = true, advance = true, msg = {"You watered your plant.","Your plant has grown to the next stage!"}, after = {9982, 9990}, chance = 80}, + {itemid = 9987, watered = true, advance = true, msg = {"You watered your plant.","Your plant has grown to the next stage!"}, after = {9986, 9988}, chance = 80}, + {itemid = 7678, watered = true, advance = true, msg = {"You watered your plant.","Your plant has grown to the next stage!"}, after = {7670, 7680, 7682, 7684, 7686, 9982, 9986}, chance = 80}, + {itemid = 15444, watered = true, advance = false, msg = "You finally remembered to water your plant and it recovered.", after = 15443}, + {itemid = 15442, watered = true, advance = true, msg = {"You watered your plant.","Your plant has grown to the next stage!"}, after = {15443, 15441}, chance = 80}, + {itemid = 15443, watered = false, advance = false, msg = "Your plant doesn't need water."}, + {itemid = 15441, watered = false, advance = false, msg = "This plant can't wither anymore."}, + {itemid = 15445, watered = false, advance = false, msg = "This plant can't wither anymore."}, + {itemid = 15446, watered = false, advance = false, msg = "This plant can't wither anymore."}, + {itemid = 15447, watered = false, advance = false, msg = "This plant can't wither anymore."}, + {itemid = 15448, watered = false, advance = false, msg = "This plant can't wither anymore."}, + {itemid = 15449, watered = false, advance = false, msg = "This plant can't wither anymore."}, + {itemid = 15450, watered = false, advance = false, msg = "This plant can't wither anymore."} +} + +local flowerPot = Action() + +function flowerPot.onUse(player, item, fromPosition, target, toPosition, isHotkey) + for _, flower in pairs(flowers) do + if target.itemid == flower.itemid then + if (flower.watered == false and flower.advance == false) then + player:say(flower.msg, TALKTYPE_MONSTER_SAY) + elseif (flower.watered == true and flower.advance == false) then + target:transform(flower.after) + player:say(flower.msg, TALKTYPE_MONSTER_SAY) + toPosition:sendMagicEffect(CONST_ME_LOSEENERGY) + target:decay() + elseif (flower.watered == true and flower.advance == true) then + local i = 1 + if (math.random(100) <= flower.chance) then + i = 2 + target:transform(flower.after[math.random(2, #flower.after)]) + else + target:transform(flower.after[i]) + end + toPosition:sendMagicEffect(CONST_ME_LOSEENERGY) + player:say(flower.msg[i], TALKTYPE_MONSTER_SAY) + target:decay() + end + break + end + end + return true +end + +flowerPot:id(7734) -- watering can +flowerPot:register() diff --git a/data/scripts/actions/others/gnomish_voucher_types.lua b/data/scripts/actions/others/gnomish_voucher_types.lua new file mode 100644 index 0000000..a3e3a81 --- /dev/null +++ b/data/scripts/actions/others/gnomish_voucher_types.lua @@ -0,0 +1,49 @@ +local config = { + [18517] = {female = 514, male = 516, effect = CONST_ME_GREEN_RINGS}, -- gnomish voucher type MB + [18518] = {female = 514, male = 516, addon = 1, effect = CONST_ME_GREEN_RINGS, achievement = "Funghitastic"}, -- gnomish voucher type MA1 + [18519] = {female = 514, male = 516, addon = 2, effect = CONST_ME_GREEN_RINGS, achievement = "Funghitastic"}, -- gnomish voucher type MA2 + [18520] = {female = 513, male = 512, effect = CONST_ME_GIANTICE}, -- gnomish voucher type CB + [18521] = {female = 513, male = 512, addon = 1, effect = CONST_ME_GIANTICE, achievement = "Crystal Clear"}, -- gnomish voucher type CA1 + [18522] = {female = 513, male = 512, addon = 2, effect = CONST_ME_GIANTICE, achievement = "Crystal Clear"} -- gnomish voucher type CA2 +} + +local gnomishVoucher = Action() + +function gnomishVoucher.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if not player:isPremium() then + player:sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT) + return true + end + local useItem = config[item.itemid] + local looktype = player:getSex() == PLAYERSEX_FEMALE and useItem.female or useItem.male + if useItem.addon then + if not player:isPremium() + or not player:hasOutfit(looktype) + or player:hasOutfit(looktype, useItem.addon) then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You own no premium account, lack the base outfit or already own this outfit part.") + return true + end + player:addOutfitAddon(useItem.female, useItem.addon) + player:addOutfitAddon(useItem.male, useItem.addon) + player:getPosition():sendMagicEffect(useItem.effect) + if player:hasOutfit(looktype, 3) then + player:addAchievement(useItem.achievement) + end + item:remove(1) + else + if not player:isPremium() or player:hasOutfit(looktype) then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You own no premium account or already own this outfit part.") + return true + end + player:addOutfit(useItem.female) + player:addOutfit(useItem.male) + player:getPosition():sendMagicEffect(useItem.effect) + item:remove(1) + end + return true +end + +for k, v in pairs(config) do + gnomishVoucher:id(k) +end +gnomishVoucher:register() diff --git a/data/scripts/actions/others/goldfish_bowl.lua b/data/scripts/actions/others/goldfish_bowl.lua new file mode 100644 index 0000000..dbe8955 --- /dev/null +++ b/data/scripts/actions/others/goldfish_bowl.lua @@ -0,0 +1,15 @@ +local goldfishBowl = Action() + +function goldfishBowl.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if target.itemid ~= 5554 then + return false + end + + target:remove(1) + item:transform(5929) + player:addAchievement("Silent Pet") + return true +end + +goldfishBowl:id(5928) +goldfishBowl:register() diff --git a/data/scripts/actions/others/hive_gate.lua b/data/scripts/actions/others/hive_gate.lua new file mode 100644 index 0000000..5a14730 --- /dev/null +++ b/data/scripts/actions/others/hive_gate.lua @@ -0,0 +1,34 @@ +local hiveGateHorizontal = Action() + +function hiveGateHorizontal.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local position = player:getPosition() + if position.y == toPosition.y then + return false + end + + toPosition.y = position.y > toPosition.y and toPosition.y - 1 or toPosition.y + 1 + player:teleportTo(toPosition) + toPosition:sendMagicEffect(CONST_ME_TELEPORT) + return true +end + +hiveGateHorizontal:id(14755, 14756, 14757, 14758, 14759, 14760) +hiveGateHorizontal:register() + + +local hiveGateVertical = Action() + +function hiveGateVertical.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local position = player:getPosition() + if position.x == toPosition.x then + return false + end + + toPosition.x = position.x > toPosition.x and toPosition.x - 1 or toPosition.x + 1 + player:teleportTo(toPosition) + toPosition:sendMagicEffect(CONST_ME_TELEPORT) + return true +end + +hiveGateVertical:id(14767, 14768, 14769, 14770, 14771) +hiveGateVertical:register() diff --git a/data/scripts/actions/others/ice_flower.lua b/data/scripts/actions/others/ice_flower.lua new file mode 100644 index 0000000..6a20e85 --- /dev/null +++ b/data/scripts/actions/others/ice_flower.lua @@ -0,0 +1,14 @@ +local iceFlower = Action() + +function iceFlower.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if math.random(5) == 1 then + player:addItem(15271, 1) -- ice flower seeds + player:addAchievementProgress("Ice Harvester", 10) + end + item:transform(15270) -- harvested ice flower + item:decay() + return true +end + +iceFlower:id(15269) +iceFlower:register() diff --git a/data/scripts/actions/others/lottery_ticket.lua b/data/scripts/actions/others/lottery_ticket.lua new file mode 100644 index 0000000..0c7ebba --- /dev/null +++ b/data/scripts/actions/others/lottery_ticket.lua @@ -0,0 +1,16 @@ +local lotteryTicket = Action() + +function lotteryTicket.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if math.random(98) == 1 then + player:getPosition():sendMagicEffect(CONST_ME_SOUND_YELLOW) + item:transform(5958) + else + player:addAchievementProgress("Jinx", 500) + item:getPosition():sendMagicEffect(CONST_ME_POFF) + item:remove(1) + end + return true +end + +lotteryTicket:id(5957) +lotteryTicket:register() diff --git a/data/scripts/actions/others/melting_horn.lua b/data/scripts/actions/others/melting_horn.lua new file mode 100644 index 0000000..450eae2 --- /dev/null +++ b/data/scripts/actions/others/melting_horn.lua @@ -0,0 +1,38 @@ +local meltingHorn = Action() + +function meltingHorn.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if not player:isPremium() then + player:sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT) + return true + end + if player:hasMount(38) then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You already have the obedience of the ursagrodon.") + return true + end + + if target.itemid == 22729 or target.itemid == 22730 or target.itemid == 22731 then + if math.random(100) > 32 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The ice cracked and the frozen creature with it - be more careful next time!") + item:remove(1) + target:transform(22732) + else + if target.itemid == 22729 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You managed to melt about half of the ice blook. Quickly now, it's ice cold here and the ice block could freeze over again.") + elseif target.itemid == 22730 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You managed to melt almost the whole block, only the feet of the creature are still stuck in the ice. Finish the job!") + elseif target.itemid == 22731 then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "The freed ursagrodon look at you with glowing, obedient eyes.") + item:remove(1) + player:addMount(38) + player:addAchievement("Natural Born Cowboy") + player:addAchievement("Icy Glare") + end + target:transform(target.itemid + 1) + end + target:decay() + end + return true +end + +meltingHorn:id(22726) +meltingHorn:register() diff --git a/data/scripts/actions/others/muck_remover.lua b/data/scripts/actions/others/muck_remover.lua new file mode 100644 index 0000000..a24976b --- /dev/null +++ b/data/scripts/actions/others/muck_remover.lua @@ -0,0 +1,40 @@ +local config = { -- chance1, chance2, itemID, count + {from = 1, to = 1644, itemId = 18394}, -- crystal backpack + {from = 1645, to = 3189, itemId = 2158}, -- blue gem + {from = 3190, to = 4725, itemId = 18391}, -- glowing mushroom + {from = 4726, to = 6225, itemId = 18414, count = 5}, -- violet crystal shard + {from = 6226, to = 7672, itemId = 18418, count = 10}, -- blue crystal splinter + {from = 7673, to = 9083, itemId = 18413, count = 10}, -- blue crystal shard + {from = 9084, to = 9577, itemId = 2445}, -- crystal mace + {from = 9578, to = 9873, itemId = 8878}, -- crystalline armor + {from = 9874, to = 9999, itemId = 18450} -- crystalline sword +} + +local muckRemover = Action() + +function muckRemover.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if target.itemid ~= 18396 then -- mucus plug + return false + end + + local chance, randomItem = math.random(9999) + for i = 1, #config do + randomItem = config[i] + if chance >= randomItem.from and chance <= randomItem.to then + if toPosition.x == CONTAINER_POSITION then + player:addItem(randomItem.itemId, randomItem.count or 1) + else + Game.createItem(randomItem.itemId, randomItem.count or 1, toPosition) + end + player:addAchievementProgress("Goo Goo Dancer", 100) + target:getPosition():sendMagicEffect(CONST_ME_GREEN_RINGS) + target:remove(1) + item:remove(1) + break + end + end + return true +end + +muckRemover:id(18395) +muckRemover:register() diff --git a/data/scripts/actions/others/music_box.lua b/data/scripts/actions/others/music_box.lua new file mode 100644 index 0000000..0fd958d --- /dev/null +++ b/data/scripts/actions/others/music_box.lua @@ -0,0 +1,88 @@ +local config = { + ["dragonling"] = { + mountId = 31, + achievement = "Dragon Mimicry", + tameMessage = "The wild dragonling will accompany you as a friend from now on.", + sound = "FI?" + }, + ["draptor"] = { + mountId = 6, + achievement = "Scales and Tail", + tameMessage = "The wild draptor will accompany you as a friend from now on.", + sound = "Screeeeeeeeech" + }, + ["enraged white deer"] = { + mountId = 18, + achievement = "Friend of Elves", + tameMessage = "The wild deer will accompany you as a friend from now on.", + sound = "*bell*" + }, + ["ironblight"] = { + mountId = 29, + achievement = "Magnetised", + tameMessage = "The ironblight will accompany you as a friend from now on.", + sound = "Plinngggg" + }, + ["magma crawler"] = { + mountId = 30, + achievement = "Way to Hell", + tameMessage = "The magma crawler will accompany you as a friend from now on.", + sound = "ZzzZzzZzzzZz" + }, + ["midnight panther"] = { + mountId = 5, + achievement = "Starless Night", + tameMessage = "The wild panther will accompany you as a friend from now on.", + sound = "Purrrrrrr" + }, + ["wailing widow"] = { + mountId = 1, + achievement = "Spin-Off", + tameMessage = "You have tamed the wailing widow.", + sound = "Sssssssss" + }, + ["wild horse"] = { + mountId = 17, + achievement = "Lucky Horseshoe", + tameMessage = "The horse accepts you as its new master.", + sound = "*snort*" + }, + ["panda"] = { + mountId = 19, + achievement = "Chequered Teddy", + tameMessage = "The panda will accompany you as a friend from now on.", + sound = "Rrrrr... Grrrr" + } +} + +local musicBox = Action() + +function musicBox.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if not target:isCreature() or not target:isMonster() or target:getMaster() then + return false + end + + local monsterConfig = config[target:getName():lower()] + if not monsterConfig then + return false + end + + if player:hasMount(monsterConfig.mountId) then + return false + end + + player:addMount(monsterConfig.mountId) + player:addAchievement("Natural Born Cowboy") + player:addAchievement(monsterConfig.achievement) + player:say(monsterConfig.tameMessage, TALKTYPE_MONSTER_SAY) + toPosition:sendMagicEffect(CONST_ME_SOUND_RED) + + target:say(monsterConfig.sound, TALKTYPE_MONSTER_SAY) + target:remove() + + item:remove(1) + return true +end + +musicBox:id(18511) +musicBox:register() diff --git a/data/scripts/actions/others/nail_case.lua b/data/scripts/actions/others/nail_case.lua new file mode 100644 index 0000000..2e9cc54 --- /dev/null +++ b/data/scripts/actions/others/nail_case.lua @@ -0,0 +1,54 @@ +local messages = { + [1] = "You carefully cut the nail of the gravedigger's thumb.", + [2] = "You successfully manicured the forefinger of the gravedigger.", + [3] = "You successfully manicured the middlefinger of the gravedigger.", + [4] = "You actually manicured the ring finger of the gravedigger without a problem." +} + +local nailCase = Action() + +function nailCase.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if not target:isCreature() or not target:isMonster() or target:getMaster() then + return false + end + + if not player:isPremium() then + player:sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT) + return true + end + + if target:getName():lower() ~= "gravedigger" then + return true + end + + if player:hasMount(39) then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You already have the obedience of the hellgrip.") + return true + end + + if math.random(100) >= 80 then + target:remove() + player:say("You failed to manicure the nails of the sensitive gravedigger. Before you can make amends, the creature runs away in agony.", TALKTYPE_MONSTER_SAY) + player:setStorageValue(PlayerStorageKeys.nailCaseUseCount, 0) + return true + end + + player:setStorageValue(PlayerStorageKeys.nailCaseUseCount, player:getStorageValue(PlayerStorageKeys.nailCaseUseCount) + 1) + local count = player:getStorageValue(PlayerStorageKeys.nailCaseUseCount) + local message = messages[count] + if message then + player:say(message, TALKTYPE_MONSTER_SAY) + return true + end + + player:say("You did it! A completely manicured gravedigger is ready to follow you as your own personal trusty mount until the bitter end!", TALKTYPE_MONSTER_SAY) + item:remove(1) + target:remove() + player:addMount(39) + player:addAchievement("Natural Born Cowboy") + player:addAchievement("Blacknailed") + return true +end + +nailCase:id(21452) +nailCase:register() diff --git a/data/scripts/actions/others/premium_scroll.lua b/data/scripts/actions/others/premium_scroll.lua new file mode 100644 index 0000000..b3bc0c5 --- /dev/null +++ b/data/scripts/actions/others/premium_scroll.lua @@ -0,0 +1,12 @@ +local premiumScroll = Action() + +function premiumScroll.onUse(player, item, fromPosition, target, toPosition, isHotkey) + item:remove(1) + player:addPremiumDays(30) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have activated your 30 day premium time, relog to make it effective.") + player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return true +end + +premiumScroll:id(16101) +premiumScroll:register() diff --git a/data/scripts/actions/others/roasted_meat.lua b/data/scripts/actions/others/roasted_meat.lua new file mode 100644 index 0000000..276105b --- /dev/null +++ b/data/scripts/actions/others/roasted_meat.lua @@ -0,0 +1,12 @@ +local roastedMeat = Action() + +function roastedMeat.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if target.itemid == 1423 then -- campfire + item:transform(24843) -- roasted meat + toPosition:sendMagicEffect(CONST_ME_HITBYFIRE) + end + return true +end + +roastedMeat:id(24842) +roastedMeat:register() diff --git a/data/scripts/actions/others/scroll_of_ascension.lua b/data/scripts/actions/others/scroll_of_ascension.lua new file mode 100644 index 0000000..f69da4e --- /dev/null +++ b/data/scripts/actions/others/scroll_of_ascension.lua @@ -0,0 +1,17 @@ +local scrollOfAscencion = Action() + +function scrollOfAscencion.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if math.random(10) > 1 then + player:setMonsterOutfit("Demon", 30 * 10 * 1000) + else + player:setMonsterOutfit("Ferumbras", 30 * 10 * 1000) + end + item:transform(25428) + item:getPosition():sendMagicEffect(CONST_ME_MAGIC_RED) + item:decay() + player:say("Magical sparks whirl around the scroll as you read it and then your appearance is changing.", TALKTYPE_MONSTER_SAY) + return true +end + +scrollOfAscencion:id(25427) +scrollOfAscencion:register() diff --git a/data/scripts/actions/others/spellbook.lua b/data/scripts/actions/others/spellbook.lua new file mode 100644 index 0000000..5f10d83 --- /dev/null +++ b/data/scripts/actions/others/spellbook.lua @@ -0,0 +1,36 @@ +local spellbook = Action() + +function spellbook.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local text = {} + local spells = {} + for _, spell in ipairs(player:getInstantSpells()) do + if spell.level ~= 0 then + if spell.manapercent > 0 then + spell.mana = spell.manapercent .. "%" + end + spells[#spells + 1] = spell + end + end + + table.sort(spells, function(a, b) return a.level < b.level end) + + local prevLevel = -1 + for i, spell in ipairs(spells) do + if prevLevel ~= spell.level then + if i == 1 then + text[#text == nil and 1 or #text+1] = "Spells for Level " + else + text[#text+1] = "\nSpells for Level " + end + text[#text+1] = spell.level .. "\n" + prevLevel = spell.level + end + text[#text+1] = spell.words .. " - " .. spell.name .. " : " .. spell.mana .. "\n" + end + + player:showTextDialog(item:getId(), table.concat(text)) + return true +end + +spellbook:id(2175, 6120, 8900, 8901, 8902, 8903, 8904, 8918, 16112, 18401, 22422, 22423, 22424, 23771) +spellbook:register() diff --git a/data/scripts/actions/others/spellwand.lua b/data/scripts/actions/others/spellwand.lua new file mode 100644 index 0000000..f74062b --- /dev/null +++ b/data/scripts/actions/others/spellwand.lua @@ -0,0 +1,30 @@ +local monsters = {"Rat", "Green Frog", "Chicken"} + +local spellwand = Action() + +function spellwand.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if player:getTile():hasFlag(TILESTATE_PROTECTIONZONE) then + player:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + return true + end + if not target:isPlayer() then + player:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + return true + end + if math.random(100) <= 33 then + item:remove() + player:say("The spellwand broke.", TALKTYPE_MONSTER_SAY) + if math.random(100) <= 75 and player:getStorageValue(PlayerStorageKeys.madSheepSummon) <= os.time() then + Game.createMonster("Mad Sheep", fromPosition) + player:setStorageValue(PlayerStorageKeys.madSheepSummon, os.time() + 12 * 60 * 60) + end + else + target:setMonsterOutfit(monsters[math.random(#monsters)], 60 * 1000) + target:getPosition():sendMagicEffect(CONST_ME_MAGIC_GREEN) + return true + end + return true +end + +spellwand:id(7735) +spellwand:register() diff --git a/data/scripts/actions/others/spider_egg.lua b/data/scripts/actions/others/spider_egg.lua new file mode 100644 index 0000000..b340ea3 --- /dev/null +++ b/data/scripts/actions/others/spider_egg.lua @@ -0,0 +1,20 @@ +local spiderEgg = Action() + +function spiderEgg.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local chance = math.random(100) + if chance >= 50 and chance < 83 then + Game.createMonster("Spider", fromPosition) + elseif chance >= 83 and chance < 97 then + Game.createMonster("Poison Spider", fromPosition) + elseif chance >= 97 and chance < 100 then + Game.createMonster("Tarantula", fromPosition) + else + item:getPosition():sendMagicEffect(CONST_ME_POFF) + end + item:transform(7536) + item:decay() + return true +end + +spiderEgg:id(7537) +spiderEgg:register() diff --git a/data/scripts/actions/others/sugar_oat.lua b/data/scripts/actions/others/sugar_oat.lua new file mode 100644 index 0000000..10f6065 --- /dev/null +++ b/data/scripts/actions/others/sugar_oat.lua @@ -0,0 +1,34 @@ +local machines = { + [5469] = 5513, + [5470] = 5514 +} + +local sugarOat = Action() + +function sugarOat.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local targetId = target.itemid + if targetId == 2694 then + if toPosition.x ~= CONTAINER_POSITION then + Game.createItem(13939, 1, toPosition) + else + player:addItem(13939, 1) + toPosition = player:getPosition() + end + toPosition:sendMagicEffect(CONST_ME_MAGIC_BLUE) + item:remove(1) + target:remove(1) + else + local machine = machines[targetId] + if machine then + target:transform(machine) + target:decay() + item:remove(1) + player:getPosition():sendMagicEffect(CONST_ME_MAGIC_RED) + player:addAchievementProgress("Homebrewed", 50) + end + end + return true +end + +sugarOat:id(5467) +sugarOat:register() diff --git a/data/scripts/actions/others/sweetheart_ring.lua b/data/scripts/actions/others/sweetheart_ring.lua new file mode 100644 index 0000000..e3b917a --- /dev/null +++ b/data/scripts/actions/others/sweetheart_ring.lua @@ -0,0 +1,12 @@ +local sweetheartRing = Action() + +function sweetheartRing.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if item == player:getSlotItem(CONST_SLOT_RING) then + player:getPosition():sendMagicEffect(CONST_ME_HEARTS) + return true + end + return false +end + +sweetheartRing:id(24324) +sweetheartRing:register() diff --git a/data/scripts/actions/others/taming.lua b/data/scripts/actions/others/taming.lua new file mode 100644 index 0000000..122ac7b --- /dev/null +++ b/data/scripts/actions/others/taming.lua @@ -0,0 +1,475 @@ +local TYPE_ITEM, TYPE_MONSTER = 1, 2 + +local config = { + [5907] = { -- slingshot + name = "bear", + id = 3, + type = TYPE_MONSTER, + achievement = "Bearbaiting", + chance = 20, + fail = { + {run = true, text = "The bear ran away."}, + {broke = true, text = "Oh no! The slingshot broke."}, + {sound = "GRRRRRRRRRRRR", text = "The bear is trying to hit you with its claws."} + }, + success = {sound = "Grrrrrrr", text = "You tamed the wild bear."} + }, + [13247] = { -- hunting horn + name = "boar", + id = 10, + type = TYPE_MONSTER, + achievement = "Pig-Headed", + chance = 20, + fail = { + {run = true, text = "The boar ran away."}, + {broke = true, text = "Oh no! The hunting horn broke."}, + {sound = "Grunt! Grunt!", text = "The boar is refusing to obey the hunting horn."} + }, + success = {sound = "Oink", text = "You tamed the wild boar."} + }, + [13291] = { -- maxilla maximus + name = "undead cavebear", + id = 12, + type = TYPE_MONSTER, + achievement = "Out of the Stone Age", + chance = 20, + fail = { + {run = true, text = "The undead cavebear ran away."}, + {sound = "GRRRRRRRRRR", text = "The undead cavebear is growling at you."} + }, + success = {sound = "Grrrrrrr", text = "You tamed the undead cavebear."} + }, + [13292] = { -- tin key + name = "inoperative tin lizzard", + mountName = "tin lizzard", + id = 8, + type = TYPE_ITEM, + achievement = "Knock on Wood", + chance = 20, + fail = { + {destroyObject = true, sound = "Krr... kch.", text = "The tin lizzard broke apart."} + }, + success = {sound = "Krkrkrkrk", text = "You wind up the tin lizzard."} + }, + [13293] = { -- leather whip + name = "midnight panther", + id = 5, + type = TYPE_MONSTER, + achievement = "Starless Night", + chance = 20, + fail = { + {run = true, text = "The midnight panther ran away."}, + {sound = "Groarrrrrrrr", text = "The midnight panther is growling at you."} + }, + success = {sound = "Purrrrrrr", text = "You tamed the wild panther."} + }, + [13294] = { -- harness + name = "draptor", + id = 6, + type = TYPE_MONSTER, + achievement = "Scales and Tail", + chance = 20, + fail = { + {run = true, text = "The wild draptor ran away."}, + {sound = "Screeeeeeeeech", text = "The wild draptor is struggling."} + }, + success = {sound = "Screeeeeeeeech", text = "You tamed the wild draptor."} + }, + [13295] = { -- reins + name = "black sheep", + id = 4, + type = TYPE_MONSTER, + achievement = "Little Ball of Wool", + chance = 20, + fail = { + {run = true, sound = "Baaaah", text = "The black sheep ran away."}, + {broke = true, text = "Oh no! The reins were torn."}, + {sound = "Baaaah", text = "The black sheep is trying to run away."} + }, + success = {sound = "Baaaaaah", text = "You tamed the black sheep."} + }, + [13298] = { -- carrot on a stick + name = "terror bird", + id = 2, + type = TYPE_MONSTER, + achievement = "Pecking Order", + chance = 15, + fail = { + {run = true, text = "The terror bird ran away."}, + {broke = true, text = "Oh no, the bird ate the carrot."}, + {sound = "CARRRRAAAH!", text = "The terror bird is pecking you."} + }, + success = {sound = "Guruuuuh", text = "You tamed the bird."} + }, + [13305] = { -- giant shrimp + name = "crustacea gigantica", + id = 7, + type = TYPE_MONSTER, + achievement = "Fried Shrimp", + chance = 20, + fail = { + {run = true, text = "The gigantic creature ran away."}, + {sound = "CHRRRR", text = "The gigantic creature is trying to pinch you."} + }, + success = {sound = "Chrrrrr", text = "You tamed the gigantic creature."} + }, + [13307] = { -- sweet smelling bait + name = "wailing widow", + id = 1, + type = TYPE_MONSTER, + achievement = "Spin-Off", + chance = 20, + fail = { + {run = true, sound = "SSSSSSSSSSSSS", text = "The wailing widow ran away."}, + {broke = true, text = "Oh no! The wailing widow ate the bait."}, + {sound = "SSSSSSSSSSSSS", text = "The wailing widow is hissing at you."} + }, + success = {sound = "Sssssssss", text = "You tamed the wailing widow."} + }, + [13498] = { -- scorpion sceptre + name = "sandstone scorpion", + id = 21, + type = TYPE_MONSTER, + achievement = "Golden Sands", + chance = 20, + fail = { + {run = true, sound = "*rattle-rattle*", text = "The sandstone scorpion flees."}, + {broke = true, text = "Using the sceptre on the stone surface of the scorpion, it breaks in two halves."}, + {sound = "*tak tak tak*", text = "The sandstone scorpion eludes the influence of the scepter."} + }, + success = {sound = "*rattle*", text = "You tamed the sandstone scorpion."} + }, + [13508] = { -- slug drug + name = "slug", + id = 14, + type = TYPE_MONSTER, + achievement = "Slugging Around", + chance = 20, + fail = { + {run = true, sound = "Slurp!", text = "The slug flees."}, + {broke = true, text = "This slug drug didn't seem to have any effect."}, + {sound = "*shlorp*", text = "The slug slips through your grasp."} + }, + success = {sound = "Sluuuuurp!", text = "You drugged the slug."} + }, + [13535] = { -- fist on a stick + name = "dromedary", + id = 20, + type = TYPE_MONSTER, + achievement = "Fata Morgana", + chance = 20, + fail = { + {run = true, sound = "Gruuuuunt!", text = "The dromedary flees."}, + {sound = "Grunt!", text = "The dromedary remains stubborn."} + }, + success = {sound = "*blaaammm*", text = "You tamed the dromedary."} + }, + [13536] = { -- diapason + name = "crystal wolf", + id = 16, + type = TYPE_MONSTER, + achievement = "The Right Tone", + chance = 20, + fail = { + {run = true, sound = "*zwiiiish*", text = "The crystal wolf vanished into thin air."}, + {sound = "*klaaaaaang* Rrrrooooaaaarrrgh!", text = "The crystal wolf is startled by the wrong sound of the diapason."} + }, + success = {sound = "*kliiiiiiiiiiing* Aooooouuuuu!!", text = "The smooth sound of the diapason tamed the crystal wolf."} + }, + [13537] = { -- bag of apple slices + mountName = "donkey", + lookType = 399, + id = 13, + type = TYPE_MONSTER, + achievement = "Loyal Lad", + chance = 20, + fail = { + {removeTransformation = true, text = "The donkey transformation suddenly wears off."}, + {broke = true, sound = "Heeee-haaa-haaa-haaw!", text = "You did not manage to feed the donkey enough apple slices."} + }, + success = {sound = "Heeee-haaaaw!", text = "Munching a large pile of apple slices tamed the donkey."} + }, + [13538] = { -- bamboo leaves + name = "panda", + id = 19, + type = TYPE_MONSTER, + achievement = "Chequered Teddy", + chance = 20, + fail = { + {run = true, sound = "Grrrrr!", text = "The panda flees."}, + {broke = true, text = "While you were trying to soothe the panda, it ate all the remaining bamboo behind your back."}, + {sound = "Grrrroaaar!!", text = "The panda refuses to follow any of your orders."} + }, + success = {sound = "Rrrrr...", text = "You tamed the panda."} + }, + [13539] = { -- golden fir cone + name = "enraged white deer", + mountName = "white deer", + id = 18, + type = TYPE_MONSTER, + achievement = "Friend of Elves", + chance = 20, + fail = { + {run = true, sound = "*sniff*", text = "The white deer flees."}, + {broke = true, sound = "ROOOAAARR!!", text = "Oh no... the enraged deer angrily ripped the fir cone from your hands!"}, + {sound = "*wheeze*", text = "The white deer sniffs and wheezes trying to withstand the taming."} + }, + success = {sound = "*bell*", text = "You tamed the white deer."} + }, + [13938] = { -- golden can of oil + name = "inoperative uniwheel", + mountName = "uniwheel", + id = 15, + type = TYPE_ITEM, + achievement = "Stuntman", + chance = 20, + fail = { + {broke = true, sound = "Splosh!", text = "It looks like most of the special oil this can was holding was spilt without any effect."} + }, + success = {sound = "Vroooomratatatatatatat.", text = "The strange wheel seems to vibrate and slowly starts turning continuously."} + }, + [13939] = { -- sugar oat + name = "wild horse", + id = 17, + type = TYPE_MONSTER, + achievement = "Lucky Horseshoe", + chance = 5, + fail = { + {run = true, text = "With its last strength the horse the horse runs to safety."}, + {broke = true, sound = "Weeeheeeehee", text = "The wild horse happily munches the sugar oat and runs on."}, + {sound = "Weeheheheehaaa!!", text = "Weeeheeeehee."} + }, + success = {sound = "*snort*", text = "The horse eats the sugar oat and accepts you as its new master."} + }, + [15545] = { -- foxtail + name = "manta ray", + id = 28, + type = TYPE_MONSTER, + achievement = "Beneath the Sea", + chance = 20, + fail = { + {run = true, sound = "Swooooosh", text = "The manta ray fled."}, + {sound = "Shhhhhh", text = "The manta ray is trying to escape."} + }, + success = {sound = "~~~", text = "You tamed the manta ray."} + }, + [15546] = { -- four-leaf clover + name = "ladybug", + id = 27, + type = TYPE_MONSTER, + achievement = "Lovely Dots", + chance = 20, + fail = { + {run = true, text = "The bug got scared and ran away."}, + {sound = "Chrk chrk!", text = "The ladybug is trying to nibble you."} + }, + success = {sound = "Chhrk...", text = "You tamed the lady bug."} + }, + [18447] = { -- iron loadstone + name = "ironblight", + id = 29, + type = TYPE_MONSTER, + achievement = "Magnetised", + chance = 20, + fail = { + {run = true, sound = "Pling", text = "The ironblight managed to run away."}, + {broke = true, text = "Oh no! The magnet lost its power!"}, + {sound = "Plinngggg", text = "The ironblight is fighting against the magnetic force."} + }, + success = {sound = "Plinnnggggggg", text = "You tamed the ironblight."} + }, + [18448] = { -- glow wine + name = "magma crawler", + id = 30, + type = TYPE_MONSTER, + achievement = "Way to Hell", + chance = 20, + fail = { + {run = true, sound = "Charrrrrr", text = "The magma crawler refused to drink wine and vanishes into thin air."}, + {broke = true, text = "Argh! The magma crawler pushed you and you spilled the glow wine!"}, + {sound = " ", text = "The magma crawler is smelling the glow wine suspiciously."} + }, + success = {sound = "ZzzZzzZzzzZz", text = "The magma crawler will accompany you as a friend from now on."} + }, + [18449] = { -- decorative ribbon + name = "dragonling", + id = 31, + type = TYPE_MONSTER, + achievement = "Dragon Mimicry", + chance = 20, + fail = { + {sound = "FCHHHHHHHHHHHHHHHH", text = "The dragonling doesn't seem to impressed with your ribbon."} + }, + success = {sound = "FI?", text = "The wild dragonling has accepted you as its master."} + }, + [18516] = { -- golem wrench + name = "modified gnarlhound", + id = 32, + type = TYPE_MONSTER, + achievement = "Mind the Dog!", + chance = 100, + success = {sound = "Gnarl!", text = "You now own a modified gnarlhound."} + }, + [20138] = { -- leech + name = "water buffalo", + id = 35, + type = TYPE_MONSTER, + achievement = "Swamp Beast", + chance = 20, + fail = { + {run = true, sound = "Baaaah", text = "The water buffalo flees."}, + {broke = true, text = "The leech slipped through your fingers and is now following the call of nature."}, + {sound = "Bellow!", text = "The water buffalo ignores you."} + }, + success = {sound = "Looooow!", text = "The leech appeased the water buffalo and your taming was successful."} + }, + [22608] = { -- nightmare horn + name = "shock head", + id = 42, + type = TYPE_MONSTER, + achievement = "Personal Nightmare", + chance = 20, + fail = { + {run = true, text = "The shock head ran away."}, + {sound = "GRRRRRRRRRRR", text = "The shock head is growling at you."} + }, + success = {sound = "Grrrrrrr", text = "You tamed the shock head."} + }, + [23557] = { -- control unit + name = "walker", + id = 43, + type = TYPE_MONSTER, + achievement = "Gear Up", + chance = 20, + fail = { + {run = true, sound = "*click clack*", text = "The walker disappeared."}, + {broke = true, text = "Aah no! The glooth batteries of this unit must have leaked."}, + {sound = "*pling*", text = "The walker seems incompatible with this control unit."} + }, + success = {sound = "*brzzz*", text = "You tamed the walker."} + }, + [23810] = { -- the lion's heart + name = "noble lion", + id = 40, + type = TYPE_MONSTER, + achievement = "Lion King", + chance = 20, + fail = { + {run = true, text = "The noble lion fled."}, + {sound = "GRRRRRRRRR", text = "The noble lion majestically rejects your amulet."} + }, + success = {sound = "Grrr", text = "The noble lion will now accompany you as a friend and ally."} + }, + [26194] = { -- vibrant egg + name = "sparkion", + id = 94, + type = TYPE_MONSTER, + achievement = "Vortex Tamer", + chance = 100, + success = {text = "You receive the permission to ride a sparkion."} + }, + [26340] = { -- crackling egg + name = "neon sparkid", + id = 98, + type = TYPE_MONSTER, + achievement = "Vortex Tamer", + chance = 100, + success = {text = "You receive the permission to ride a neon sparkid."} + }, + [26341] = { -- menacing egg + name = "vortexion", + id = 99, + type = TYPE_MONSTER, + achievement = "Vortex Tamer", + chance = 100, + success = {text = "You receive the permission to ride a vortexion."} + } +} + +local taming = Action() + +function taming.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local mount = config[item.itemid] + if not mount then + return false + end + + if not player:isPremium() then + player:sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT) + return true + end + + if player:hasMount(mount.id) then + return false + end + + if target:getName():lower() == "horse" and item.itemid == 13939 then + player:say("The horse happily munches the sugar oat and runs on. You shouldn't steal one of the horse station's horses anyway.", TALKTYPE_MONSTER_SAY) + item:remove(1) + return true + end + + if target:getName():lower() == "white deer" and item.itemid == 13539 then + player:say("You should try to enrage this deer before your taming attempt. That way you make sure it's strong enough to carry you.", TALKTYPE_MONSTER_SAY) + return true + end + + if target:getName():lower() == "desperate white deer" and item.itemid == 13539 then + player:say("This deer doesn't show enough strength and is too desperate already. Only enraged deer have the necessary power to carry you.", TALKTYPE_MONSTER_SAY) + return true + end + + if mount.type ~= target.type then + return false + end + + if mount.name and mount.name ~= target:getName():lower() then + return false + end + + if target.type ~= TYPE_ITEM then + if mount.lookType and mount.lookType ~= target:getOutfit().lookType then + return false + end + end + + if target.type == TYPE_MONSTER then + if target:getMaster() then + return false + end + end + + if math.random(100) > mount.chance then + local action = mount.fail[math.random(#mount.fail)] + player:say(action.text, TALKTYPE_MONSTER_SAY) + if action.run then + target:getPosition():sendMagicEffect(CONST_ME_POFF) + target:remove() + elseif action.broke then + item:remove(1) + elseif action.destroyObject then + addEvent(Game.createItem, math.random(3) * 60 * 60 * 1000, target.itemid, 1, toPosition) + target:remove() + elseif action.removeTransformation then + target:removeCondition(CONDITION_OUTFIT) + end + if action.sound then + player:say(action.sound, TALKTYPE_MONSTER_SAY, false, 0, toPosition) + end + return true + end + + player:addMount(mount.id) + player:addAchievement("Natural Born Cowboy") + player:addAchievement(mount.achievement) + player:say(mount.success.text, TALKTYPE_MONSTER_SAY) + player:say(mount.success.sound, TALKTYPE_MONSTER_SAY, false, 0, toPosition) + target:remove() + item:remove(1) + return true +end + +for k, v in pairs(config) do + taming:id(k) +end +taming:register() diff --git a/data/scripts/actions/others/tinder_box.lua b/data/scripts/actions/others/tinder_box.lua new file mode 100644 index 0000000..81701bd --- /dev/null +++ b/data/scripts/actions/others/tinder_box.lua @@ -0,0 +1,13 @@ +local tinderBox = Action() + +function tinderBox.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if target.itemid == 22727 then -- fireproof horn + item:remove(1) + target:transform(22726) -- melting horn + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You ignite the tinder in the fireproof horn to create an effective device to melt just about anything.") + end + return true +end + +tinderBox:id(22728) +tinderBox:register() diff --git a/data/scripts/actions/others/voodoo_doll.lua b/data/scripts/actions/others/voodoo_doll.lua new file mode 100644 index 0000000..fd190b6 --- /dev/null +++ b/data/scripts/actions/others/voodoo_doll.lua @@ -0,0 +1,22 @@ +local voodooDoll = Action() + +function voodooDoll.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if target.itemid ~= 1 or not target:isPlayer() then + return false + end + + local text = "" + if math.random(100) <= 5 then + text = "You concentrate on your victim and hit the needle in the doll." + player:addAchievement("Dark Voodoo Priest") + toPosition:sendMagicEffect(CONST_ME_DRAWBLOOD, player) + else + text = "You concentrate on your victim, hit the needle in the doll.......but nothing happens." + end + + player:say(text, TALKTYPE_MONSTER_SAY, false, player) + return true +end + +voodooDoll:id(3955) +voodooDoll:register() diff --git a/data/scripts/actions/tools/check_bless.lua b/data/scripts/actions/tools/check_bless.lua new file mode 100644 index 0000000..59b57c8 --- /dev/null +++ b/data/scripts/actions/tools/check_bless.lua @@ -0,0 +1,24 @@ +local blessings = { + "Spiritual Shielding", + "Embrace of Tibia", + "Fire of the Suns", + "Spark of the Phoenix", + "Wisdom of Solitude", + "Twist of Fate" +} + +local checkBless = Action() + +function checkBless.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local result, bless = "Received blessings:" + for i = 1, #blessings do + bless = blessings[i] + result = player:hasBlessing(i) and result .. "\n" .. bless or result + end + + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, 20 > result:len() and "No blessings received." or result) + return true +end + +checkBless:id(12424, 6561) +checkBless:register() diff --git a/data/scripts/actions/tools/claw_of_the_noxious_spawn.lua b/data/scripts/actions/tools/claw_of_the_noxious_spawn.lua new file mode 100644 index 0000000..bce81b7 --- /dev/null +++ b/data/scripts/actions/tools/claw_of_the_noxious_spawn.lua @@ -0,0 +1,31 @@ +local cursed = Condition(CONDITION_CURSED) +cursed:setParameter(CONDITION_PARAM_DELAYED, true) -- condition will delay the first damage from when it's added +cursed:setParameter(CONDITION_PARAM_MINVALUE, -800) -- minimum damage the condition can do at total +cursed:setParameter(CONDITION_PARAM_MAXVALUE, -1200) -- maximum damage +cursed:setParameter(CONDITION_PARAM_STARTVALUE, -1) -- the damage the condition will do on the first hit +cursed:setParameter(CONDITION_PARAM_TICKINTERVAL, 4000) -- delay between damages +cursed:setParameter(CONDITION_PARAM_FORCEUPDATE, true) -- re-update condition when adding it(ie. min/max value) + +local clawOfTheNoxiousSpawn = Action() + +function clawOfTheNoxiousSpawn.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if item == player:getSlotItem(CONST_SLOT_RING) then + if math.random(100) <= 5 then + player:addCondition(cursed) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You are cursed by The Noxious Spawn!") + item:transform(10312) + item:decay() + player:getPosition():sendMagicEffect(CONST_ME_MAGIC_RED) + else + player:removeCondition(CONDITION_POISON) + item:transform(10311) + item:decay() + player:getPosition():sendMagicEffect(CONST_ME_MAGIC_GREEN) + end + return true + end + return false +end + +clawOfTheNoxiousSpawn:id(10309) +clawOfTheNoxiousSpawn:register() diff --git a/data/scripts/actions/tools/firebug.lua b/data/scripts/actions/tools/firebug.lua new file mode 100644 index 0000000..abd15e8 --- /dev/null +++ b/data/scripts/actions/tools/firebug.lua @@ -0,0 +1,36 @@ +local fireBug = Action() + +function fireBug.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local chance = math.random(10) + if chance > 4 then -- Success 6% chance + if target.itemid == 7538 then -- Destroy spider webs/North - South + toPosition:sendMagicEffect(CONST_ME_HITBYFIRE) + target:transform(7544) + target:decay() + elseif target.itemid == 7539 then -- Destroy spider webs/East - West + toPosition:sendMagicEffect(CONST_ME_HITBYFIRE) + target:transform(7545) + target:decay() + elseif target.itemid == 5466 then -- Burn Sugar Cane + toPosition:sendMagicEffect(CONST_ME_FIREAREA) + target:transform(5465) + target:decay() + elseif target.itemid == 1485 then -- Light up empty coal basins + toPosition:sendMagicEffect(CONST_ME_HITBYFIRE) + target:transform(1484) + end + elseif chance == 2 then -- It removes the firebug 1% chance + item:remove(1) + toPosition:sendMagicEffect(CONST_ME_POFF) + elseif chance == 1 then -- It explodes on the user 1% chance + doTargetCombat(0, player, COMBAT_FIREDAMAGE, -5, -5, CONST_ME_HITBYFIRE) + player:say('OUCH!', TALKTYPE_MONSTER_SAY) + item:remove(1) + else + toPosition:sendMagicEffect(CONST_ME_POFF) -- It fails, but don't get removed 3% chance + end + return true +end + +fireBug:id(5468) +fireBug:register() diff --git a/data/scripts/actions/tools/gold_converter.lua b/data/scripts/actions/tools/gold_converter.lua new file mode 100644 index 0000000..b366536 --- /dev/null +++ b/data/scripts/actions/tools/gold_converter.lua @@ -0,0 +1,36 @@ +local config = { + [ITEM_GOLD_COIN] = {changeTo = ITEM_PLATINUM_COIN}, + [ITEM_PLATINUM_COIN] = {changeBack = ITEM_GOLD_COIN, changeTo = ITEM_CRYSTAL_COIN}, + [ITEM_CRYSTAL_COIN] = {changeBack = ITEM_PLATINUM_COIN} +} + +local goldConverter = Action() + +function goldConverter.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local coin = config[target.itemid] + + if not coin then + return false + end + + local charges = item:getCharges() + if coin.changeTo and target.type == 100 then + target:remove() + player:addItem(coin.changeTo, 1) + item:transform(item:getId(), charges -1) + elseif coin.changeBack then + target:transform(target.itemid, target.type - 1) + player:addItem(coin.changeBack, 100) + item:transform(item:getId(), charges -1) + else + return false + end + + if charges == 0 then + item:remove() + end + return true +end + +goldConverter:id(26378) +goldConverter:register() diff --git a/data/scripts/actions/tools/juice_squeezer.lua b/data/scripts/actions/tools/juice_squeezer.lua new file mode 100644 index 0000000..689bf37 --- /dev/null +++ b/data/scripts/actions/tools/juice_squeezer.lua @@ -0,0 +1,28 @@ +local juiceSquizer = Action() + +function juiceSquizer.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local fruits = { + 2673, -- pear + 2674, -- red apple + 2675, -- orange + 2676, -- banana + 2677, -- blueberry + 2678, -- coconut + 2679, -- cherry + 2680, -- strawberry + 2681, -- grapes + 2682, -- melon + 5097, -- mango + 8839, -- plum + 8840, -- raspberry + 8841 -- lemon + } + if table.contains(fruits, target.itemid) and player:removeItem(2006, 1, 0) then + target:remove(1) + player:addItem(2006, target.itemid == 2678 and 14 or 21) -- if target is a coconut, create coconut milk, otherwise create fruit juice + return true + end +end + +juiceSquizer:id(5865) +juiceSquizer:register() diff --git a/data/scripts/actions/tools/rust_remover.lua b/data/scripts/actions/tools/rust_remover.lua new file mode 100644 index 0000000..2350d24 --- /dev/null +++ b/data/scripts/actions/tools/rust_remover.lua @@ -0,0 +1,129 @@ +local config = { + [9808] = { -- Rusty Armor (Common) + [1] = {id = 2464, chance = 6994}, -- Chain Armor + [2] = {id = 2483, chance = 3952}, -- Scale Armor + [3] = {id = 2465, chance = 1502}, -- Brass Armor + [4] = {id = 2463, chance = 197} -- Plate Armor + }, + [9809] = { -- Rusty Armor (Semi-rare) + [1] = {id = 2483, chance = 6437}, -- Scale Armor + [2] = {id = 2464, chance = 4606}, -- Chain Armor + [3] = {id = 2465, chance = 3029}, -- Brass Armor + [4] = {id = 2463, chance = 1559}, -- Plate Armor + [5] = {id = 2476, chance = 595}, -- Knight Armor + [6] = {id = 8891, chance = 283}, -- Paladin Armor + [7] = {id = 2487, chance = 49} -- Crown Armor + }, + [9810] = { -- Rusty Armor (Rare) + [1] = {id = 2465, chance = 6681}, -- Brass Armor + [2] = {id = 2463, chance = 3767}, -- Plate Armor + [3] = {id = 2476, chance = 1832}, -- Knight Armor + [4] = {id = 2487, chance = 177}, -- Crown Armor + [5] = {id = 8891, chance = 31}, -- Paladin Armor + [6] = {id = 2466, chance = 10} -- Golden Armor + }, + [9811] = { -- Rusty Legs (Common) + [1] = {id = 2648, chance = 6949}, -- Chain Legs + [2] = {id = 2468, chance = 3692}, -- Studded Legs + [3] = {id = 2478, chance = 1307}, -- Brass Legs + [4] = {id = 2647, chance = 133} -- Plate Legs + }, + [9812] = { -- Rusty Legs (Semi-Rare) + [1] = {id = 2468, chance = 5962}, -- Studded Legs + [2] = {id = 2648, chance = 4037}, -- Chain Legs + [3] = {id = 2478, chance = 2174}, -- Brass Legs + [4] = {id = 2647, chance = 1242}, -- Plate Legs + [5] = {id = 2477, chance = 186}, -- Knight Legs + }, + [9813] = { -- Rusty Legs (Rare) + [1] = {id = 2478, chance = 6500}, -- Brass Legs + [2] = {id = 2647, chance = 3800}, -- Plate Legs + [3] = {id = 2477, chance = 200}, -- Knight Legs + [4] = {id = 2488, chance = 52}, -- Crown Legs + [5] = {id = 2470, chance = 30} -- Golden Legs + }, + [9814] = { -- Heavily Rusted Shield + }, + [9815] = { -- Rusted Shield + }, + [9816] = { -- Slightly Rusted Shield + [1] = {id = 2510, chance = 3137}, -- Plate Shield + [2] = {id = 2532, chance = 2887}, -- Ancient Shield + [3] = {id = 7460, chance = 929}, -- Norse Shield + [4] = {id = 2519, chance = 23}, -- Crown Shield + [5] = {id = 2534, chance = 10} -- Vampire Shield + }, + [9820] = { -- Heavily Rusted Helmet + }, + [9821] = { -- Rusted Helmet + [1] = {id = 2460, chance = 2200}, -- Brass Helmet + [2] = {id = 2482, chance = 1870}, -- Studded Helmet + [3] = {id = 2459, chance = 1490}, -- Iron Helmet + [4] = {id = 2457, chance = 1010}, -- Steel Helmet + [5] = {id = 2491, chance = 190}, -- Crown Helmet + [6] = {id = 2497, chance = 10} -- Crusader Helmet + }, + [9822] = { -- Slightly Rusted Helmet + [1] = {id = 2459, chance = 3156}, -- Iron Helmet + [2] = {id = 2457, chance = 2976}, -- Steel Helmet + [3] = {id = 2491, chance = 963}, -- Crown Helmet + [4] = {id = 2497, chance = 210}, -- Crusader Helmet + [5] = {id = 2498, chance = 7} -- Royal Helmet + }, + [9817] = { -- Heavily Rusted Boots + }, + [9818] = { -- Rusted Boots + }, + [9819] = { -- Slightly Rusted Boots + }, +} + +local rustRemover = Action() + +function rustRemover.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local targetItem = config[target.itemid] + if not targetItem then + return true + end + + local randomChance = math.random(10000) + local index = false + + if targetItem[1].chance >= randomChance then -- implying first item in the table index always has the highest chance. + while not index do + randomIndex = math.random(#targetItem) + if targetItem[randomIndex].chance >= randomChance then + index = randomIndex + end + end + end + + if not index then + if table.contains({9808, 9809, 9810}, target.itemid) then + msg = "The armor was already damaged so badly that it broke when you tried to clean it." + end + if table.contains({9811, 9812, 9813}, target.itemid) then + msg = "The legs were already damaged so badly that they broke when you tried to clean them." + end + if table.contains({9814, 9815, 9816}, target.itemid) then + msg = "The shield was already damaged so badly that it broke when you tried to clean it." + end + if table.contains({9817, 9818, 9819}, target.itemid) then + msg = "The boots were already damaged so badly that they broke when you tried to clean them." + end + if table.contains({9820, 9821, 9822}, target.itemid) then + msg = "The helmet was already damaged so badly that it broke when you tried to clean it." + end + player:say(msg, TALKTYPE_MONSTER_SAY) + target:getPosition():sendMagicEffect(CONST_ME_BLOCKHIT) + target:remove() + else + target:transform(targetItem[index].id) + target:getPosition():sendMagicEffect(CONST_ME_MAGIC_GREEN) + player:addAchievementProgress("Polisher", 1000) + end + return item:remove(1) +end + +rustRemover:id(9930) +rustRemover:register() diff --git a/data/scripts/actions/tools/saw.lua b/data/scripts/actions/tools/saw.lua new file mode 100644 index 0000000..c9ed57c --- /dev/null +++ b/data/scripts/actions/tools/saw.lua @@ -0,0 +1,13 @@ +local saw = Action() + +function saw.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if target.itemid ~= 5901 then -- wood + return false + end + + target:transform(10033) -- wooden ties + return true +end + +saw:id(2558) +saw:register() diff --git a/data/scripts/actions/tools/skinning.lua b/data/scripts/actions/tools/skinning.lua new file mode 100644 index 0000000..a79d8e5 --- /dev/null +++ b/data/scripts/actions/tools/skinning.lua @@ -0,0 +1,173 @@ +local config = { + [5908] = { + -- Minotaurs + [3090] = {chance = 7000, newItem = 5878, after = 2831}, -- minotaur + [5969] = {chance = 7000, newItem = 5878, after = 2831}, -- minotaur, after being killed + [2871] = {chance = 7000, newItem = 5878, after = 2872}, -- minotaur archer + [5982] = {chance = 7000, newItem = 5878, after = 2872}, -- minotaur archer, after being killed + [2866] = {chance = 7000, newItem = 5878, after = 2867}, -- minotaur mage + [5981] = {chance = 7000, newItem = 5878, after = 2867}, -- minotaur mage, after being killed + [2876] = {chance = 7000, newItem = 5878, after = 2877}, -- minotaur guard + [5983] = {chance = 7000, newItem = 5878, after = 2877}, -- minotaur guard, after being killed + [23463] = {chance = 7000, newItem = 5878, after = 23464}, -- mooh'tah warrior + [23462] = {chance = 7000, newItem = 5878, after = 23464}, -- mooh'tah warrior, after being killed + [23467] = {chance = 7000, newItem = 5878, after = 23468}, -- minotaur hunter + [23466] = {chance = 7000, newItem = 5878, after = 23468}, -- minotaur hunter, after being killed + [23471] = {chance = 7000, newItem = 5878, after = 23472}, -- worm priestess + [23470] = {chance = 7000, newItem = 5878, after = 23472}, -- worm priestess, after being killed + [23371] = {chance = 7000, newItem = 5878, after = 23373}, -- minotaur amazon + [23372] = {chance = 7000, newItem = 5878, after = 23373}, -- minotaur amazon, after being killed + [23375] = {chance = 7000, newItem = 5878, after = 23377}, -- execowtioner + [23376] = {chance = 7000, newItem = 5878, after = 23377}, -- execowtioner, after being killed + [23367] = {chance = 7000, newItem = 5878, after = 23369}, -- moohtant + [23368] = {chance = 7000, newItem = 5878, after = 23369}, -- moohtant, after being killed + + -- Low Class Lizards + [4259] = {chance = 6000, newItem = 5876, after = 4260}, -- lizard sentinel + [6040] = {chance = 6000, newItem = 5876, after = 4260}, -- lizard sentinel, after being killed + [4262] = {chance = 6000, newItem = 5876, after = 4263}, -- lizard snakecharmer + [6041] = {chance = 6000, newItem = 5876, after = 4263}, -- lizard snakecharmer, after being killed + [4256] = {chance = 6000, newItem = 5876, after = 4257}, -- lizard templar + [4251] = {chance = 6000, newItem = 5876, after = 4257}, -- lizard templar, after being killed + + -- High Class Lizards + [11285] = {chance = 10000, newItem = 5876, after = 11286}, -- lizard chosen, + [11288] = {chance = 10000, newItem = 5876, after = 11286}, -- lizard chosen, after being killed + [11277] = {chance = 10000, newItem = 5876, after = 11278}, -- lizard dragon priest + [11280] = {chance = 10000, newItem = 5876, after = 11278}, -- lizard dragon priest, after being killed + [11269] = {chance = 10000, newItem = 5876, after = 11270}, -- lizard high guard + [11272] = {chance = 10000, newItem = 5876, after = 11270}, -- lizard high guard, after being killed + [11281] = {chance = 10000, newItem = 5876, after = 11282}, -- lizard zaogun + [11284] = {chance = 10000, newItem = 5876, after = 11282}, -- lizard zaogun, after being killed + + -- Dragon + [3104] = {chance = 5000, newItem = 5877, after = 3105}, + [5973] = {chance = 5000, newItem = 5877, after = 3105}, -- after being killed + + -- Dragon Lord + [2881] = {chance = 5000, newItem = 5948, after = 2882}, + [5984] = {chance = 5000, newItem = 5948, after = 2882}, -- after being killed + + -- Behemoth + [2931] = {chance = 10000, newItem = 5893, after = 2932}, + [5999] = {chance = 10000, newItem = 5893, after = 2932}, -- after being killed + + -- Bone Beast + [3031] = {chance = 6000, newItem = 5925, after = 3032}, + [6030] = {chance = 6000, newItem = 5925, after = 3032}, -- after being killed + + -- Clomp + [25399] = {chance = 50000, newItem = 24842, after = 25400}, + [25398] = {chance = 50000, newItem = 24842, after = 25400}, -- after being killed + + -- Piece of Marble Rock + [11343] = { + {chance = 530, newItem = 11346, desc = "This little figurine of a goddess was masterfully sculpted by |PLAYERNAME|."}, + {chance = 9600, newItem = 11345, desc = "This little figurine made by |PLAYERNAME| has some room for improvement."}, + {chance = 24000, newItem = 11344, desc = "This shoddy work was made by |PLAYERNAME|."} + }, + + -- Ice Cube + [7441] = {chance = 22000, newItem = 7442}, + [7442] = {chance = 4800, newItem = 7444}, + [7444] = {chance = 900, newItem = 7445}, + [7445] = {chance = 40, newItem = 7446} + }, + [5942] = { + -- Demon + [2916] = {chance = 3000, newItem = 5906, after = 2917}, + [5995] = {chance = 3000, newItem = 5906, after = 2917}, -- after being killed + + -- Vampires + [2956] = {chance = 6000, newItem = 5905, after = 2957}, -- vampire + [6006] = {chance = 6000, newItem = 5905, after = 2957}, -- vampire, after being killed + [9654] = {chance = 6000, newItem = 5905, after = 9658}, -- vampire bride + [9660] = {chance = 6000, newItem = 5905, after = 9658}, -- vampire bride, after being killed + [21275] = {chance = 6000, newItem = 5905, after = 21276}, -- vampire viscount + [21278] = {chance = 6000, newItem = 5905, after = 21276} -- vampire viscount, after being killed + } +} + +local skinning = Action() + +function skinning.onUse(player, item, fromPosition, target, toPosition, isHotkey) + local skin = config[item.itemid][target.itemid] + if not skin then + player:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + return true + end + + local randomChance = math.random(1, 100000) + local effect = CONST_ME_MAGIC_GREEN + local transform = true + if type(skin[1]) == "table" then + local added = false + for _, skinChild in ipairs(skin) do + if randomChance <= skinChild.chance then + if target.itemid == 11343 then + local marble = player:addItem(skinChild.newItem, skinChild.amount or 1) + if marble then + marble:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, skinChild.desc:gsub("|PLAYERNAME|", player:getName())) + end + if skinChild.newItem == 11346 then + player:addAchievement("Marblelous") + player:addAchievementProgress("Marble Madness", 5) + end + effect = CONST_ME_HITAREA + target:remove() + added = true + else + target:transform(skinChild.newItem, skinChild.amount or 1) + effect = CONST_ME_HITAREA + added = true + end + break + end + end + + if not added and target.itemid == 11343 then + effect = CONST_ME_HITAREA + player:say("Your attempt at shaping that marble rock failed miserably.", TALKTYPE_MONSTER_SAY) + transform = false + target:remove() + end + elseif randomChance <= skin.chance then + if table.contains({7441, 7442, 7444, 7445}, target.itemid) then + if skin.newItem == 7446 then + player:addAchievement("Ice Sculptor") + player:addAchievementProgress("Cold as Ice", 10) + end + target:transform(skin.newItem, 1) + effect = CONST_ME_HITAREA + else + if table.contains({5906, 5905}, skin.newItem) then + player:addAchievementProgress("Ashes to Dust", 500) + else + player:addAchievementProgress("Skin-Deep", 500) + end + player:addItem(skin.newItem, skin.amount or 1) + end + else + if table.contains({7441, 7442, 7444, 7445}, target.itemid) then + player:say("The attempt of sculpting failed miserably.", TALKTYPE_MONSTER_SAY) + effect = CONST_ME_HITAREA + target:remove() + else + effect = CONST_ME_BLOCKHIT + end + end + if toPosition.x == CONTAINER_POSITION then + toPosition = player:getPosition() + end + toPosition:sendMagicEffect(effect) + if transform then + target:transform(skin.after or target:getType():getDecayId() or target.itemid + 1) + else + target:remove() + end + + return true +end + +skinning:id(5908, 5942) +skinning:register() diff --git a/data/scripts/creaturescripts/gs_wyda_death.lua b/data/scripts/creaturescripts/gs_wyda_death.lua new file mode 100644 index 0000000..fd8ba6d --- /dev/null +++ b/data/scripts/creaturescripts/gs_wyda_death.lua @@ -0,0 +1,11 @@ +local creatureevent = CreatureEvent("GiantSpiderWyda") + +function creatureevent.onDeath(creature, corpse, lasthitkiller, mostdamagekiller, lasthitunjustified, mostdamageunjustified) + creature:say("It seems this was just an illusion.", TALKTYPE_MONSTER_SAY) + if mostdamagekiller:isPlayer() then + mostdamagekiller:addAchievement("Someone's Bored") + end + return true +end + +creatureevent:register() diff --git a/data/scripts/creaturescripts/northern_pike_death.lua b/data/scripts/creaturescripts/northern_pike_death.lua new file mode 100644 index 0000000..96226e8 --- /dev/null +++ b/data/scripts/creaturescripts/northern_pike_death.lua @@ -0,0 +1,10 @@ +local creatureevent = CreatureEvent("NorthernPikeDeath") + +function creatureevent.onDeath(creature, corpse, lasthitkiller, mostdamagekiller, lasthitunjustified, mostdamageunjustified) + if math.random(100) < 11 then + Game.createMonster("Slippery Northern Pike", creature:getPosition(), false, true) + end + return true +end + +creatureevent:register() diff --git a/data/scripts/creaturescripts/scarab_death.lua b/data/scripts/creaturescripts/scarab_death.lua new file mode 100644 index 0000000..2e0b8bf --- /dev/null +++ b/data/scripts/creaturescripts/scarab_death.lua @@ -0,0 +1,11 @@ +local creatureevent = CreatureEvent("ScarabDeath") + +function creatureevent.onDeath(creature, corpse, lasthitkiller, mostdamagekiller, lasthitunjustified, mostdamageunjustified) + if math.random(100) < 10 then + Game.createMonster("Scorpion", creature:getPosition()) + creature:say("Horestis' curse spawns a vengeful scorpion from the body!", TALKTYPE_MONSTER_SAY) + end + return true +end + +creatureevent:register() diff --git a/data/scripts/creaturescripts/white_deer_death.lua b/data/scripts/creaturescripts/white_deer_death.lua new file mode 100644 index 0000000..a868911 --- /dev/null +++ b/data/scripts/creaturescripts/white_deer_death.lua @@ -0,0 +1,28 @@ +local config = { + {chance = 30, monster = "Enraged White Deer", message = "The white deer summons all his strength and turns to fight!"}, + {chance = 100, monster = "Desperate White Deer", message = "The white deer desperately tries to escape!"} +} + +local creatureevent = CreatureEvent("WhiteDeerDeath") + +function creatureevent.onDeath(creature, corpse, lasthitkiller, mostdamagekiller, lasthitunjustified, mostdamageunjustified) + local targetMonster = creature:getMonster() + if not targetMonster or targetMonster:getMaster() then + return true + end + + local chance = math.random(100) + for i = 1, #config do + if chance <= config[i].chance then + local spawnMonster = Game.createMonster(config[i].monster, targetMonster:getPosition(), true, true) + if spawnMonster then + spawnMonster:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + targetMonster:say(config[i].message, TALKTYPE_MONSTER_SAY) + end + break + end + end + return true +end + +creatureevent:register() diff --git a/data/scripts/creaturescripts/whitedeer_scouts.lua b/data/scripts/creaturescripts/whitedeer_scouts.lua new file mode 100644 index 0000000..ce36670 --- /dev/null +++ b/data/scripts/creaturescripts/whitedeer_scouts.lua @@ -0,0 +1,22 @@ +local creatureevent = CreatureEvent("WhiteDeerScouts") + +function creatureevent.onDeath(creature, corpse, lasthitkiller, mostdamagekiller, lasthitunjustified, mostdamageunjustified) + local targetMonster = creature:getMonster() + if not targetMonster or targetMonster:getMaster() then + return true + end + + local chance = math.random(100) + if chance <= 10 then + for i = 1, 2 do + local spawnMonster = Game.createMonster("Elf Scout", targetMonster:getPosition(), true, true) + if spawnMonster then + spawnMonster:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + end + end + targetMonster:say("The elves came too late to save the deer, however they might avenge it.", TALKTYPE_MONSTER_SAY) + end + return true +end + +creatureevent:register() diff --git a/data/scripts/creaturescripts/wild_horse_death.lua b/data/scripts/creaturescripts/wild_horse_death.lua new file mode 100644 index 0000000..ca0e8cb --- /dev/null +++ b/data/scripts/creaturescripts/wild_horse_death.lua @@ -0,0 +1,8 @@ +local creatureevent = CreatureEvent("WildHorseDeath") + +function creatureevent.onDeath(creature, corpse, lasthitkiller, mostdamagekiller, lasthitunjustified, mostdamageunjustified) + creature:say("With its last strength the horse runs to safety.", TALKTYPE_MONSTER_SAY) + return true +end + +creatureevent:register() diff --git a/data/scripts/lib/create_functions.lua b/data/scripts/lib/create_functions.lua new file mode 100644 index 0000000..f34cc3b --- /dev/null +++ b/data/scripts/lib/create_functions.lua @@ -0,0 +1,2 @@ +createFunctions(MonsterType) -- creates get/set functions for MonsterType +createFunctions(Spell) -- creates get/set functions for Spell diff --git a/data/scripts/lib/defaults_move_event.lua b/data/scripts/lib/defaults_move_event.lua new file mode 100644 index 0000000..75191a6 --- /dev/null +++ b/data/scripts/lib/defaults_move_event.lua @@ -0,0 +1,25 @@ +-- default callbacks + +function defaultStepIn(creature, item, position, fromPosition) + return true +end + +function defaultStepOut(creature, item, position, fromPosition) + return true +end + +function defaultAddItem(moveitem, tileitem, pos) + return true +end + +function defaultRemoveItem(moveitem, tileitem, pos) + return true +end + +function defaultEquip(player, item, slot, isCheck) + return true +end + +function defaultDeEquip(player, item, slot, isCheck) + return true +end diff --git a/data/scripts/lib/helper_constructors.lua b/data/scripts/lib/helper_constructors.lua new file mode 100644 index 0000000..1ff606f --- /dev/null +++ b/data/scripts/lib/helper_constructors.lua @@ -0,0 +1,52 @@ +local classes = {Action, CreatureEvent, Spell, TalkAction, MoveEvent, GlobalEvent, Weapon} + +for _, class in ipairs(classes) do + local MT = getmetatable(class) + local DefaultConstructor = MT.__call + + MT.__call = function(self, def, ...) + -- Backwards compatibility for default obj() constructor + if type(def) ~= "table" then + return DefaultConstructor(self, def, ...) + end + + local obj = nil + if def.init then + obj = DefaultConstructor(self, unpack(def.init)) + else + obj = DefaultConstructor(self) + end + + -- Call each method from definition table with the value as params + local hasCallback = false + + for methodName, value in pairs(def) do + -- Strictly check if a correct callback is passed + if methodName:sub(1, 2) == "on" and type(value) == "function" and rawget(class, methodName) then + hasCallback = true + end + + if methodName ~= "register" then + local method = rawget(self, methodName) + if method then + if type(value) == "table" then + method(obj, unpack(value)) + else + method(obj, value) + end + end + end + end + + -- Only register if callback has already been defined, otherwise defining afterwards will not work + if def.register then + if not hasCallback then + print("Warning: Event not registered due to there being no callback.") + else + obj:register() + end + end + + return obj + end +end diff --git a/data/scripts/lib/register_monster_type.lua b/data/scripts/lib/register_monster_type.lua new file mode 100644 index 0000000..11faa63 --- /dev/null +++ b/data/scripts/lib/register_monster_type.lua @@ -0,0 +1,471 @@ +registerMonsterType = {} +setmetatable(registerMonsterType, +{ + __call = + function(self, mtype, mask) + for _,parse in pairs(self) do + parse(mtype, mask) + end + end +}) + +MonsterType.register = function(self, mask) + return registerMonsterType(self, mask) +end + +registerMonsterType.description = function(mtype, mask) + if mask.description then + mtype:nameDescription(mask.description) + end +end +registerMonsterType.experience = function(mtype, mask) + if mask.experience then + mtype:experience(mask.experience) + end +end +registerMonsterType.skull = function(mtype, mask) + if mask.skull then + mtype:skull(mask.skull) + end +end +registerMonsterType.outfit = function(mtype, mask) + if mask.outfit then + mtype:outfit(mask.outfit) + end +end +registerMonsterType.maxHealth = function(mtype, mask) + if mask.maxHealth then + mtype:maxHealth(mask.maxHealth) + end +end +registerMonsterType.health = function(mtype, mask) + if mask.health then + mtype:health(mask.health) + end +end +registerMonsterType.runHealth = function(mtype, mask) + if mask.runHealth then + mtype:runHealth(mask.runHealth) + end +end +registerMonsterType.maxSummons = function(mtype, mask) + if mask.maxSummons then + mtype:maxSummons(mask.maxSummons) + end +end +registerMonsterType.race = function(mtype, mask) + if mask.race then + mtype:race(mask.race) + end +end +registerMonsterType.manaCost = function(mtype, mask) + if mask.manaCost then + mtype:manaCost(mask.manaCost) + end +end +registerMonsterType.speed = function(mtype, mask) + if mask.speed then + mtype:baseSpeed(mask.speed) + end +end +registerMonsterType.corpse = function(mtype, mask) + if mask.corpse then + mtype:corpseId(mask.corpse) + end +end +registerMonsterType.flags = function(mtype, mask) + if mask.flags then + if mask.flags.attackable ~= nil then + mtype:isAttackable(mask.flags.attackable) + end + if mask.flags.healthHidden ~= nil then + mtype:isHealthHidden(mask.flags.healthHidden) + end + if mask.flags.boss ~= nil then + mtype:isBoss(mask.flags.boss) + end + if mask.flags.convinceable ~= nil then + mtype:isConvinceable(mask.flags.convinceable) + end + if mask.flags.illusionable ~= nil then + mtype:isIllusionable(mask.flags.illusionable) + end + if mask.flags.hostile ~= nil then + mtype:isHostile(mask.flags.hostile) + end + if mask.flags.pushable ~= nil then + mtype:isPushable(mask.flags.pushable) + end + if mask.flags.canPushItems ~= nil then + mtype:canPushItems(mask.flags.canPushItems) + end + if mask.flags.canPushCreatures ~= nil then + mtype:canPushCreatures(mask.flags.canPushCreatures) + end + if mask.flags.targetDistance then + mtype:targetDistance(mask.flags.targetDistance) + end + if mask.flags.staticAttackChance then + mtype:staticAttackChance(mask.flags.staticAttackChance) + end + end +end +registerMonsterType.light = function(mtype, mask) + if mask.light then + if mask.light.color then + local color = mask.light.color + end + if mask.light.level then + mtype:light(color, mask.light.level) + end + end +end +registerMonsterType.changeTarget = function(mtype, mask) + if mask.changeTarget then + if mask.changeTarget.chance then + mtype:changeTargetChance(mask.changeTarget.chance) + end + if mask.changeTarget.interval then + mtype:changeTargetSpeed(mask.changeTarget.interval) + end + end +end +registerMonsterType.voices = function(mtype, mask) + if type(mask.voices) == "table" then + local interval, chance + if mask.voices.interval then + interval = mask.voices.interval + end + if mask.voices.chance then + chance = mask.voices.chance + end + for k, v in pairs(mask.voices) do + if type(v) == "table" then + mtype:addVoice(v.text, interval, chance, v.yell) + end + end + end +end +registerMonsterType.summons = function(mtype, mask) + if type(mask.summons) == "table" then + for k, v in pairs(mask.summons) do + mtype:addSummon(v.name, v.interval, v.chance) + end + end +end +registerMonsterType.events = function(mtype, mask) + if type(mask.events) == "table" then + for k, v in pairs(mask.events) do + mtype:registerEvent(v) + end + end +end +registerMonsterType.loot = function(mtype, mask) + if type(mask.loot) == "table" then + local lootError = false + for _, loot in pairs(mask.loot) do + local parent = Loot() + if not parent:setId(loot.id) then + lootError = true + end + if loot.chance then + parent:setChance(loot.chance) + end + if loot.maxCount then + parent:setMaxCount(loot.maxCount) + end + if loot.aid or loot.actionId then + parent:setActionId(loot.aid or loot.actionId) + end + if loot.subType or loot.charges then + parent:setSubType(loot.subType or loot.charges) + end + if loot.text or loot.description then + parent:setDescription(loot.text or loot.description) + end + if loot.child then + for _, children in pairs(loot.child) do + local child = Loot() + if not child:setId(children.id) then + lootError = true + end + if children.chance then + child:setChance(children.chance) + end + if children.maxCount then + child:setMaxCount(children.maxCount) + end + if children.aid or children.actionId then + child:setActionId(children.aid or children.actionId) + end + if children.subType or children.charges then + child:setSubType(children.subType or children.charges) + end + if children.text or children.description then + child:setDescription(children.text or children.description) + end + parent:addChildLoot(child) + end + end + mtype:addLoot(parent) + end + if lootError then + print("[Warning - end] Monster: \"".. mtype:name() .. "\" loot could not correctly be load.") + end + end +end +registerMonsterType.elements = function(mtype, mask) + if type(mask.elements) == "table" then + for _, element in pairs(mask.elements) do + if element.type and element.percent then + mtype:addElement(element.type, element.percent) + end + end + end +end +registerMonsterType.immunities = function(mtype, mask) + if type(mask.immunities) == "table" then + for _, immunity in pairs(mask.immunities) do + if immunity.type and immunity.combat then + mtype:combatImmunities(immunity.type) + end + if immunity.type and immunity.condition then + mtype:conditionImmunities(immunity.type) + end + end + end +end +registerMonsterType.attacks = function(mtype, mask) + if type(mask.attacks) == "table" then + for _, attack in pairs(mask.attacks) do + local spell = MonsterSpell() + if attack.name then + if attack.name == "melee" then + spell:setType("melee") + if attack.attack and attack.skill then + spell:setAttackValue(attack.attack, attack.skill) + end + if attack.interval then + spell:setInterval(attack.interval) + end + if attack.effect then + spell:setCombatEffect(attack.effect) + end + if attack.condition then + if attack.condition.type then + spell:setConditionType(attack.condition.type) + end + local startDamnage = 0 + if attack.condition.startDamage then + startDamage = attack.condition.startDamage + end + if attack.condition.minDamage and attack.condition.maxDamage then + spell:setConditionDamage(attack.condition.minDamage, attack.condition.maxDamage, startDamage) + end + if attack.condition.duration then + spell:setConditionDuration(attack.condition.duration) + end + if attack.condition.interval then + spell:setConditionTickInterval(attack.condition.interval) + end + end + else + spell:setType(attack.name) + if attack.type then + if attack.name == "combat" then + spell:setCombatType(attack.type) + else + spell:setConditionType(attack.type) + end + end + if attack.interval then + spell:setInterval(attack.interval) + end + if attack.chance then + spell:setChance(attack.chance) + end + if attack.range then + spell:setRange(attack.range) + end + if attack.duration then + spell:setConditionDuration(attack.duration) + end + if attack.speed then + if type(attack.speed) ~= "table" then + spell:setConditionSpeedChange(attack.speed) + elseif type(attack.speed) == "table" then + if attack.speed.min and attack.speed.max then + spell:setConditionSpeedChange(attack.speed.min, attack.speed.max) + end + end + end + if attack.target then + spell:setNeedTarget(attack.target) + end + if attack.length then + spell:setCombatLength(attack.length) + end + if attack.spread then + spell:setCombatSpread(attack.spread) + end + if attack.radius then + spell:setCombatRadius(attack.radius) + end + if attack.minDamage and attack.maxDamage then + if attack.name == "combat" then + spell:setCombatValue(attack.minDamage, attack.maxDamage) + else + local startDamage = 0 + if attack.startDamage then + startDamage = attack.startDamage + end + spell:setConditionDamage(attack.minDamage, attack.maxDamage, startDamage) + end + end + if attack.effect then + spell:setCombatEffect(attack.effect) + end + if attack.shootEffect then + spell:setCombatShootEffect(attack.shootEffect) + end + end + elseif attack.script then + spell:setScriptName(attack.script) + if attack.interval then + spell:setInterval(attack.interval) + end + if attack.chance then + spell:setChance(attack.chance) + end + if attack.minDamage and attack.maxDamage then + spell:setCombatValue(attack.minDamage, attack.maxDamage) + end + if attack.target then + spell:setNeedTarget(attack.target) + end + end + mtype:addAttack(spell) + end + end +end +registerMonsterType.defenses = function(mtype, mask) + if type(mask.defenses) == "table" then + if mask.defenses.defense then + mtype:defense(mask.defenses.defense) + end + if mask.defenses.armor then + mtype:armor(mask.defenses.armor) + end + for _, defense in pairs(mask.defenses) do + if type(defense) == "table" then + local spell = MonsterSpell() + if defense.name then + if defense.name == "melee" then + spell:setType("melee") + if defense.attack and defense.skill then + spell:setAttackValue(defense.attack, defense.skill) + end + if defense.interval then + spell:setInterval(defense.interval) + end + if defense.effect then + spell:setCombatEffect(defense.effect) + end + if defense.condition then + if defense.condition.type then + spell:setConditionType(defense.condition.type) + end + local startDamnage = 0 + if defense.condition.startDamage then + startDamage = defense.condition.startDamage + end + if defense.condition.minDamage and defense.condition.maxDamage then + spell:setConditionDamage(defense.condition.minDamage, defense.condition.maxDamage, startDamage) + end + if defense.condition.duration then + spell:setConditionDuration(defense.condition.duration) + end + if defense.condition.interval then + spell:setConditionTickInterval(defense.condition.interval) + end + end + else + spell:setType(defense.name) + if defense.type then + if defense.name == "combat" then + spell:setCombatType(defense.type) + else + spell:setConditionType(defense.type) + end + end + if defense.interval then + spell:setInterval(defense.interval) + end + if defense.chance then + spell:setChance(defense.chance) + end + if defense.range then + spell:setRange(defense.range) + end + if defense.duration then + spell:setConditionDuration(defense.duration) + end + if defense.speed then + if type(defense.speed) ~= "table" then + spell:setConditionSpeedChange(defense.speed) + elseif type(defense.speed) == "table" then + if defense.speed.min and defense.speed.max then + spell:setConditionSpeedChange(defense.speed.min, defense.speed.max) + end + end + end + if defense.target then + spell:setNeedTarget(defense.target) + end + if defense.length then + spell:setCombatLength(defense.length) + end + if defense.spread then + spell:setCombatSpread(defense.spread) + end + if defense.radius then + spell:setCombatRadius(defense.radius) + end + if defense.minDamage and defense.maxDamage then + if defense.name == "combat" then + spell:setCombatValue(defense.minDamage, defense.maxDamage) + else + local startDamage = 0 + if defense.startDamage then + startDamage = defense.startDamage + end + spell:setConditionDamage(defense.minDamage, defense.maxDamage, startDamage) + end + end + if defense.effect then + spell:setCombatEffect(defense.effect) + end + if defense.shootEffect then + spell:setCombatShootEffect(defense.shootEffect) + end + end + elseif defense.script then + spell:setScriptName(defense.script) + if defense.interval then + spell:setInterval(defense.interval) + end + if defense.chance then + spell:setChance(defense.chance) + end + if defense.minDamage and defense.maxDamage then + spell:setCombatValue(defense.minDamage, defense.maxDamage) + end + if defense.target then + spell:setNeedTarget(defense.target) + end + end + mtype:addDefense(spell) + end + end + end +end diff --git a/data/scripts/movements/claw_of_the_noxious_spawn.lua b/data/scripts/movements/claw_of_the_noxious_spawn.lua new file mode 100644 index 0000000..0cdfd4f --- /dev/null +++ b/data/scripts/movements/claw_of_the_noxious_spawn.lua @@ -0,0 +1,17 @@ +local clawOfTheNoxiousSpawn = MoveEvent() + +function clawOfTheNoxiousSpawn.onEquip(player, item, slot, isCheck) + if isCheck == false then + if not Tile(player:getPosition()):hasFlag(TILESTATE_PROTECTIONZONE) then + doTargetCombat(0, player, COMBAT_PHYSICALDAMAGE, -150, -200, CONST_ME_DRAWBLOOD) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Ouch! The serpent claw stabbed you.") + return true + end + end + return true +end + +clawOfTheNoxiousSpawn:type("equip") +clawOfTheNoxiousSpawn:slot("ring") +clawOfTheNoxiousSpawn:id(10310) +clawOfTheNoxiousSpawn:register() diff --git a/data/scripts/movements/seeds.lua b/data/scripts/movements/seeds.lua new file mode 100644 index 0000000..c4d0a44 --- /dev/null +++ b/data/scripts/movements/seeds.lua @@ -0,0 +1,19 @@ +local moveevent = MoveEvent() + +function moveevent.onAddItem(moveitem, tileitem, position) + if moveitem:getId() == 7732 then -- seeds + tileitem:transform(7665) -- flower pot + tileitem:decay() + moveitem:remove(1) + position:sendMagicEffect(CONST_ME_MAGIC_GREEN) + elseif moveitem:getId() == 15271 then -- ice flower seeds + tileitem:transform(15442) -- flower pot + tileitem:decay() + moveitem:remove(1) + position:sendMagicEffect(CONST_ME_MAGIC_BLUE) + end + return true +end + +moveevent:id(7655) -- empty flower pot +moveevent:register() diff --git a/data/scripts/movements/yellow_pillow.lua b/data/scripts/movements/yellow_pillow.lua new file mode 100644 index 0000000..6d98144 --- /dev/null +++ b/data/scripts/movements/yellow_pillow.lua @@ -0,0 +1,14 @@ +local yellowPillow = MoveEvent() +yellowPillow:type("stepin") + +function yellowPillow.onStepIn(player, item, position, fromPosition) + if not player or player:isInGhostMode() then + return true + end + player:say("Faaart!", TALKTYPE_MONSTER_SAY) + item:getPosition():sendMagicEffect(CONST_ME_POFF) + return true +end + +yellowPillow:id(8072) +yellowPillow:register() diff --git a/data/scripts/spells/#example.lua b/data/scripts/spells/#example.lua new file mode 100644 index 0000000..9ba6a17 --- /dev/null +++ b/data/scripts/spells/#example.lua @@ -0,0 +1,52 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_TARGETCASTERORTOPMOST, true) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 3.2) + 20 + local max = (level / 5) + (magicLevel * 5.4) + 40 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +local spell = Spell(SPELL_RUNE) + +function spell.onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end + +spell:name("test rune") +spell:runeId(2275) +spell:id(220) +spell:level(20) +spell:magicLevel(5) +spell:needTarget(true) +spell:isAggressive(false) +spell:allowFarUse(true) +spell:charges(25) +spell:vocation("sorcerer;true", "master sorcerer") +spell:register() + +local conjureRune = Spell(SPELL_INSTANT) + +function conjureRune.onCastSpell(creature, variant) + return creature:conjureItem(2260, 2275, 25) +end + +conjureRune:name("Test") +conjureRune:id(221) +conjureRune:words("adori mas test") +conjureRune:level(30) +conjureRune:mana(530) +conjureRune:group("support") +conjureRune:soul(3) +conjureRune:isAggressive(false) +conjureRune:cooldown(2000) +conjureRune:groupCooldown(2000) +conjureRune:needLearn(false) +conjureRune:vocation("sorcerer", "master sorcerer") +conjureRune:register() diff --git a/data/scripts/talkactions/position.lua b/data/scripts/talkactions/position.lua new file mode 100644 index 0000000..d6a65f6 --- /dev/null +++ b/data/scripts/talkactions/position.lua @@ -0,0 +1,15 @@ +local talk = TalkAction("/pos") + +function talk.onSay(player, words, param) + if player:getGroup():getAccess() and param ~= "" then + local split = param:split(",") + player:teleportTo(Position(split[1], split[2], split[3])) + else + local position = player:getPosition() + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Your current position is: " .. position.x .. ", " .. position.y .. ", " .. position.z .. ".") + end + return false +end + +talk:separator(" ") +talk:register() diff --git a/data/scripts/weapons/#example.lua b/data/scripts/weapons/#example.lua new file mode 100644 index 0000000..91ce910 --- /dev/null +++ b/data/scripts/weapons/#example.lua @@ -0,0 +1,80 @@ +--[[ + + Burst Arrow example + +]] +local area = createCombatArea({ + {1, 1, 1}, + {1, 3, 1}, + {1, 1, 1} +}) + +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_EXPLOSIONAREA) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) +combat:setFormula(COMBAT_FORMULA_SKILL, 0, 0, 1, 0) +combat:setArea(area) + +local burstarrow = Weapon(WEAPON_AMMO) + +burstarrow.onUseWeapon = function(player, variant) + if player:getSkull() == SKULL_BLACK then + return false + end + + return combat:execute(player, variant) +end + +burstarrow:id(2546) +burstarrow:attack(27) +burstarrow:shootType(CONST_ANI_BURSTARROW) +burstarrow:ammoType("arrow") +burstarrow:maxHitChance(100) +burstarrow:register() + +--[[ + + Wand of Vortex example + +]] +local wov = Weapon(WEAPON_WAND) +wov:id(2190) +wov:damage(8, 18) +wov:element("energy") +wov:level(7) +wov:mana(2) +wov:vocation("sorcerer", true, true) +wov:vocation("master sorcerer") +wov:register() + +--[[ + + Arbalest example + +]] +local arbalest = Weapon(WEAPON_DISTANCE) +arbalest:id(5803) +arbalest:slotType("two-handed") -- it's now a 2h weapon +arbalest:ammoType("bolt") +arbalest:range(6) +arbalest:attack(2) +arbalest:hitChance(2) +arbalest:level(75) +arbalest:wieldedUnproperly(true) +arbalest:register() + +--[[ + + Earth Barbarian Axe example + +]] +local eba = Weapon(WEAPON_AXE) +eba:id(7859) +eba:attack(23) +eba:defense(18, 1) +eba:extraElement(5, COMBAT_EARTHDAMAGE) +eba:charges(1000, true) -- showCharges = true +eba:action("removecharge") +eba:decayTo(2429) +eba:register() diff --git a/data/spells/lib/spells.lua b/data/spells/lib/spells.lua new file mode 100644 index 0000000..0c7caa6 --- /dev/null +++ b/data/spells/lib/spells.lua @@ -0,0 +1,343 @@ +--Pre-made areas + +--Waves +AREA_WAVE3 = { +{1, 1, 1}, +{1, 1, 1}, +{0, 3, 0} +} + +AREA_WAVE4 = { +{1, 1, 1, 1, 1}, +{0, 1, 1, 1, 0}, +{0, 1, 1, 1, 0}, +{0, 0, 3, 0, 0} +} + +AREA_WAVE6 = { +{0, 0, 0, 0, 0}, +{0, 1, 3, 1, 0}, +{0, 0, 0, 0, 0} +} + +AREA_SQUAREWAVE5 = { +{1, 1, 1}, +{1, 1, 1}, +{1, 1, 1}, +{0, 1, 0}, +{0, 3, 0} +} + +AREA_SQUAREWAVE6 = { +{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, +{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, +{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, +{0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0}, +{0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0}, +{0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0}, +{0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0}, +{0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0}, +{0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0}, +{0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0}, +{0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0}, +{0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0} +} + +AREA_SQUAREWAVE7 = { +{0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0}, +{0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0}, +{0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0}, +{0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0}, +{0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0}, +{0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0}, +{0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0}, +{0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0}, +{0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0}, +{0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0}, +{0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0} +} + +--Diagonal waves +AREADIAGONAL_WAVE4 = { +{0, 0, 0, 0, 1, 0}, +{0, 0, 0, 1, 1, 0}, +{0, 0, 1, 1, 1, 0}, +{0, 1, 1, 1, 1, 0}, +{1, 1, 1, 1, 1, 0}, +{0, 0, 0, 0, 0, 3} +} + +AREADIAGONAL_SQUAREWAVE5 = { +{1, 1, 1, 0, 0}, +{1, 1, 1, 0, 0}, +{1, 1, 1, 0, 0}, +{0, 0, 0, 1, 0}, +{0, 0, 0, 0, 3} +} + +AREADIAGONAL_WAVE6 = { +{0, 0, 1}, +{0, 3, 0}, +{1, 0, 0} +} + +--Beams +AREA_BEAM1 = { +{3} +} + +AREA_BEAM5 = { +{1}, +{1}, +{1}, +{1}, +{3} +} + +AREA_BEAM7 = { +{1}, +{1}, +{1}, +{1}, +{1}, +{1}, +{3} +} + +AREA_BEAM8 = { +{1}, +{1}, +{1}, +{1}, +{1}, +{1}, +{1}, +{3} +} + +--Diagonal Beams +AREADIAGONAL_BEAM5 = { +{1, 0, 0, 0, 0}, +{0, 1, 0, 0, 0}, +{0, 0, 1, 0, 0}, +{0, 0, 0, 1, 0}, +{0, 0, 0, 0, 3} +} + +AREADIAGONAL_BEAM7 = { +{1, 0, 0, 0, 0, 0, 0}, +{0, 1, 0, 0, 0, 0, 0}, +{0, 0, 1, 0, 0, 0, 0}, +{0, 0, 0, 1, 0, 0, 0}, +{0, 0, 0, 0, 1, 0, 0}, +{0, 0, 0, 0, 0, 1, 0}, +{0, 0, 0, 0, 0, 0, 3} +} + +--Circles +AREA_CIRCLE2X2 = { +{0, 1, 1, 1, 0}, +{1, 1, 1, 1, 1}, +{1, 1, 3, 1, 1}, +{1, 1, 1, 1, 1}, +{0, 1, 1, 1, 0} +} + +AREA_CIRCLE3X3 = { +{0, 0, 1, 1, 1, 0, 0}, +{0, 1, 1, 1, 1, 1, 0}, +{1, 1, 1, 1, 1, 1, 1}, +{1, 1, 1, 3, 1, 1, 1}, +{1, 1, 1, 1, 1, 1, 1}, +{0, 1, 1, 1, 1, 1, 0}, +{0, 0, 1, 1, 1, 0, 0} +} + +-- Crosses +AREA_CROSS1X1 = { +{0, 1, 0}, +{1, 3, 1}, +{0, 1, 0} +} + +AREA_CIRCLE5X5 = { +{0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0}, +{0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0}, +{0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0}, +{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, +{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, +{1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1}, +{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, +{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, +{0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0}, +{0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0}, +{0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0} +} + +AREA_CIRCLE6X6 = { +{0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}, +{0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0}, +{0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0}, +{0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0}, +{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, +{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, +{1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1}, +{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, +{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, +{0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0}, +{0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0}, +{0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0}, +{0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0} +} + +--Squares +AREA_SQUARE1X1 = { +{1, 1, 1}, +{1, 3, 1}, +{1, 1, 1} +} + +-- Walls +AREA_WALLFIELD = { +{1, 1, 3, 1, 1} +} + +AREADIAGONAL_WALLFIELD = { +{0, 0, 0, 0, 1}, +{0, 0, 0, 1, 1}, +{0, 1, 3, 1, 0}, +{1, 1, 0, 0, 0}, +{1, 0, 0, 0, 0}, +} + +-- Spells-only arrays + +--This HUGE array contains all corpses of the game, until protocol 8.0 +-- It is used on animate dead rune and on undead legion spell. No unmoveable corpses are there. +CORPSES = { +2806,2807,2808,2809,2810,2811,2812,2813,2814,2815,2816,2817,2818,2819,2820,2821,2822,2823, +2824,2825,2826,2827,2828,2829,2830,2831,2832,2833,2834,2835,2836,2837,2838,2839,2840,2841, +2842,2843,2844,2845,2846,2847,2848,2849,2850,2851,2852,2853,2854,2855,2856,2857,2858,2859, +2879,2880,2881,2882,2883,2884,2885,2886,2887,2888,2889,2890,2891,2892,2893,2894,2895,2896, +2897,2898,2899,2900,2901,2902,2903,2904,2905,2906,2907,2908,2909,2910,2911,2912,2913,2914, +2915,2916,2917,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2928,2929,2930,2931,2932, +2933,2934,2935,2936,2937,2938,2939,2940,2941,2942,2943,2944,2945,2946,2947,2948,2949,2950, +2951,2952,2953,2954,2955,2956,2957,2958,2959,2960,2961,2962,2963,2964,2965,2966,2967,2968, +2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986, +2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004, +3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022, +3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040, +3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058, +3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076, +3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094, +3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112, +3113,3114,3115,3116,3117,3118,3119,3120,3121,3128,3129,3130,3131,3132,3133,3134,4252,4253, +4254,4255,4256,4257,4258,4259,4260,4261,4262,4263,4264,4265,4266,4267,4268,4269,4270,4271, +4272,4273,4274,4275,4276,4277,4278,4279,4280,4281,4282,4283,4284,4285,4286,4287,4288,4289, +4290,4291,4292,4293,4294,4295,4296,4297,4298,4299,4300,4301,4302,4303,4304,4305,4306,4307, +4308,4309,4310,4311,4312,4313,4314,4315,4316,4317,4318,4319,4320,4321,4322,4323,4324,4325, +4326,4327,5522,5523,5524,5525,5526,5527,5528,5529,5530,5531,5532,5533,5534,5535,5536,5537, +5538,5540,5541,5542,5565,5566,5567,5568,5625,5626,5627,5628,5629,5630,5666,5667,5668,5688, +5689,5690,5727,5728,5729,5762,5765,5766,5767,5931,5932,5933,5934,5935,5936,5965,6022,6082, +6083,6084,6303,6304,6305,6307,6308,6309,6310,6313,6314,6315,6317,6318,6319,6321,6322,6323, +6325,6326,6327,6328,6329,6330,6333,6334,6335,6337,6338,6339,6341,6342,6343,6345,6346,6347, +6349,6350,6351,6355,6365,6366,6367,6520,6521,6522,6560,7092,7093,7094,7256,7257,7258,7283, +7284,7285,7317,7318,7319,7321,7322,7323,7325,7326,7328,7329,7331,7332,7333,7335,7336,7337, +7339,7340,7341,7345,7346,7347,7623,7624,7625,7626,7627,7629,7630,7631,7638,7639,7640,7741, +7742,7743,7848,7849,7908,7927,7928,7929,7931,7970,7971,8272} + +-- This array contains all destroyable field items +FIELDS = {1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1500,1501,1502,1503,1504} + +function Player:addPartyCondition(combat, variant, condition, baseMana) + local party = self:getParty() + if not party then + self:sendCancelMessage(RETURNVALUE_NOPARTYMEMBERSINRANGE) + self:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + local positions = combat:getPositions(self, variant) + local members = party:getMembers() + members[#members + 1] = party:getLeader() + + local affectedMembers = {} + for _, member in ipairs(members) do + local memberPosition = member:getPosition() + for _, position in ipairs(positions) do + if memberPosition == position then + affectedMembers[#affectedMembers + 1] = member + end + end + end + + if #affectedMembers <= 1 then + self:sendCancelMessage(RETURNVALUE_NOPARTYMEMBERSINRANGE) + self:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + local mana = math.ceil(#affectedMembers * math.pow(0.9, #affectedMembers - 1) * baseMana) + if self:getMana() < mana then + self:sendCancelMessage(RETURNVALUE_NOTENOUGHMANA) + self:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + self:addMana(-mana) + self:addManaSpent(mana) + + for _, member in ipairs(affectedMembers) do + member:addCondition(condition) + end + + for _, position in ipairs(positions) do + position:sendMagicEffect(CONST_ME_MAGIC_BLUE) + end + return true +end + +function Player:conjureItem(reagentId, conjureId, conjureCount, effect) + if not conjureCount and conjureId ~= 0 then + local itemType = ItemType(conjureId) + if itemType:getId() == 0 then + return false + end + + local charges = itemType:getCharges() + if charges ~= 0 then + conjureCount = charges + end + end + + if reagentId ~= 0 and not self:removeItem(reagentId, 1, -1) then + self:sendCancelMessage(RETURNVALUE_YOUNEEDAMAGICITEMTOCASTSPELL) + self:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + local item = self:addItem(conjureId, conjureCount) + if not item then + self:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + self:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + if item:hasAttribute(ITEM_ATTRIBUTE_DURATION) then + item:decay() + end + + self:getPosition():sendMagicEffect(item:getType():isRune() and CONST_ME_MAGIC_RED or effect) + return true +end + +function Creature:addAttributeCondition(parameters) + local condition = Condition(CONDITION_ATTRIBUTES) + for _, parameter in ipairs(parameters) do + if parameter.key and parameter.value then + condition:setParameter(parameter.key, parameter.value) + end + end + + self:addCondition(condition) +end diff --git a/data/spells/scripts/attack/annihilation.lua b/data/spells/scripts/attack/annihilation.lua new file mode 100644 index 0000000..bcdf5bf --- /dev/null +++ b/data/spells/scripts/attack/annihilation.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_WEAPONTYPE) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) +combat:setParameter(COMBAT_PARAM_USECHARGES, true) + +function onGetFormulaValues(player, skill, attack, factor) + local min = (player:getLevel() / 5) + (skill * attack * 0.06) + 13 + local max = (player:getLevel() / 5) + (skill * attack * 0.14) + 34 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_SKILLVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/apprentice's_strike.lua b/data/spells/scripts/attack/apprentice's_strike.lua new file mode 100644 index 0000000..fedb80e --- /dev/null +++ b/data/spells/scripts/attack/apprentice's_strike.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREATTACK) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) + +function onGetFormulaValues(player, level, magicLevel) + return -10, -20 +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/avalanche_rune.lua b/data/spells/scripts/attack/avalanche_rune.lua new file mode 100644 index 0000000..6148c2e --- /dev/null +++ b/data/spells/scripts/attack/avalanche_rune.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ICEAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ICE) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.2) + 7 + local max = (level / 5) + (magicLevel * 2.85) + 16 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/berserk.lua b/data/spells/scripts/attack/berserk.lua new file mode 100644 index 0000000..9afe602 --- /dev/null +++ b/data/spells/scripts/attack/berserk.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITAREA) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) +combat:setParameter(COMBAT_PARAM_USECHARGES, true) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) + +function onGetFormulaValues(player, skill, attack, factor) + local min = (player:getLevel() / 5) + (skill * attack * 0.03) + 7 + local max = (player:getLevel() / 5) + (skill * attack * 0.05) + 11 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_SKILLVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/brutal_strike.lua b/data/spells/scripts/attack/brutal_strike.lua new file mode 100644 index 0000000..e7bf8be --- /dev/null +++ b/data/spells/scripts/attack/brutal_strike.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_WEAPONTYPE) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) +combat:setParameter(COMBAT_PARAM_USECHARGES, true) + +function onGetFormulaValues(player, skill, attack, factor) + local min = (player:getLevel() / 5) + (skill * attack * 0.02) + 4 + local max = (player:getLevel() / 5) + (skill * attack * 0.04) + 9 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_SKILLVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/buzz.lua b/data/spells/scripts/attack/buzz.lua new file mode 100644 index 0000000..3518c88 --- /dev/null +++ b/data/spells/scripts/attack/buzz.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) + +function onGetFormulaValues(player, level, magicLevel) + level = math.min(level, 20) + magicLevel = math.min(magicLevel, 20) + local min = (level / 5) + (magicLevel * 0.4) + 2 + local max = (level / 5) + (magicLevel * 0.8) + 5 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/chill_out.lua b/data/spells/scripts/attack/chill_out.lua new file mode 100644 index 0000000..d89b1dd --- /dev/null +++ b/data/spells/scripts/attack/chill_out.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ICEAREA) +combat:setArea(createCombatArea(AREA_WAVE4)) + +function onGetFormulaValues(player, level, magicLevel) + level = math.min(level, 20) + magicLevel = math.min(magicLevel, 20) + local min = (level / 5) + (magicLevel * 0.3) + 2 + local max = (level / 5) + (magicLevel * 0.45) + 3 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/curse.lua b/data/spells/scripts/attack/curse.lua new file mode 100644 index 0000000..368c888 --- /dev/null +++ b/data/spells/scripts/attack/curse.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DEATHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MORTAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_DEATH) + +function onCastSpell(creature, variant) + local min = (creature:getLevel() / 80) + (creature:getMagicLevel() * 0.5) + 7 + local max = (creature:getLevel() / 80) + (creature:getMagicLevel() * 0.9) + 8 + local damage = math.random(math.floor(min), math.floor(max)) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_CURSED, DAMAGELIST_EXPONENTIAL_DAMAGE, damage) + end + return true +end diff --git a/data/spells/scripts/attack/death_strike.lua b/data/spells/scripts/attack/death_strike.lua new file mode 100644 index 0000000..80d7659 --- /dev/null +++ b/data/spells/scripts/attack/death_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DEATHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MORTAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_DEATH) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.4) + 8 + local max = (level / 5) + (magicLevel * 2.2) + 14 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/divine_caldera.lua b/data/spells/scripts/attack/divine_caldera.lua new file mode 100644 index 0000000..f174b20 --- /dev/null +++ b/data/spells/scripts/attack/divine_caldera.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HOLYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HOLYAREA) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 5) + 25 + local max = (level / 5) + (magicLevel * 6.2) + 45 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/divine_missile.lua b/data/spells/scripts/attack/divine_missile.lua new file mode 100644 index 0000000..cdcb055 --- /dev/null +++ b/data/spells/scripts/attack/divine_missile.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HOLYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HOLYDAMAGE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SMALLHOLY) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.9) + 8 + local max = (level / 5) + (magicLevel * 3) + 18 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/electrify.lua b/data/spells/scripts/attack/electrify.lua new file mode 100644 index 0000000..368bb48 --- /dev/null +++ b/data/spells/scripts/attack/electrify.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) + +function onCastSpell(creature, variant) + local min = (creature:getLevel() / 80) + (creature:getMagicLevel() * 0.15) + 1 + local max = (creature:getLevel() / 80) + (creature:getMagicLevel() * 0.25) + 1 + local rounds = math.random(math.floor(min), math.floor(max)) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_ENERGY, DAMAGELIST_VARYING_PERIOD, target:isPlayer() and 13 or 25, {10, 12}, rounds) + end + return true +end diff --git a/data/spells/scripts/attack/energy_beam.lua b/data/spells/scripts/attack/energy_beam.lua new file mode 100644 index 0000000..ea59575 --- /dev/null +++ b/data/spells/scripts/attack/energy_beam.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) +combat:setArea(createCombatArea(AREA_BEAM5, AREADIAGONAL_BEAM5)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.8) + 11 + local max = (level / 5) + (magicLevel * 3) + 19 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/energy_bomb_rune.lua b/data/spells/scripts/attack/energy_bomb_rune.lua new file mode 100644 index 0000000..7872610 --- /dev/null +++ b/data/spells/scripts/attack/energy_bomb_rune.lua @@ -0,0 +1,10 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGYBALL) +combat:setParameter(COMBAT_PARAM_CREATEITEM, ITEM_ENERGYFIELD_PVP) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/energy_field_rune.lua b/data/spells/scripts/attack/energy_field_rune.lua new file mode 100644 index 0000000..f026a05 --- /dev/null +++ b/data/spells/scripts/attack/energy_field_rune.lua @@ -0,0 +1,9 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGYBALL) +combat:setParameter(COMBAT_PARAM_CREATEITEM, ITEM_ENERGYFIELD_PVP) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/energy_strike.lua b/data/spells/scripts/attack/energy_strike.lua new file mode 100644 index 0000000..75cc8af --- /dev/null +++ b/data/spells/scripts/attack/energy_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.4) + 8 + local max = (level / 5) + (magicLevel * 2.2) + 14 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/energy_wall_rune.lua b/data/spells/scripts/attack/energy_wall_rune.lua new file mode 100644 index 0000000..9888b87 --- /dev/null +++ b/data/spells/scripts/attack/energy_wall_rune.lua @@ -0,0 +1,10 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGYBALL) +combat:setParameter(COMBAT_PARAM_CREATEITEM, ITEM_ENERGYFIELD_PVP) +combat:setArea(createCombatArea(AREA_WALLFIELD, AREADIAGONAL_WALLFIELD)) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/energy_wave.lua b/data/spells/scripts/attack/energy_wave.lua new file mode 100644 index 0000000..76ddb34 --- /dev/null +++ b/data/spells/scripts/attack/energy_wave.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) +combat:setArea(createCombatArea(AREA_SQUAREWAVE5, AREADIAGONAL_SQUAREWAVE5)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 4.5) + 20 + local max = (level / 5) + (magicLevel * 7.6) + 48 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/envenom.lua b/data/spells/scripts/attack/envenom.lua new file mode 100644 index 0000000..e9d4f68 --- /dev/null +++ b/data/spells/scripts/attack/envenom.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_CARNIPHILA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_EARTH) + +function onCastSpell(creature, variant) + local min = (creature:getLevel() / 80) + (creature:getMagicLevel() * 0.55) + 6 + local max = (creature:getLevel() / 80) + (creature:getMagicLevel() * 0.75) + 7 + local damage = math.random(math.floor(min) * 1000, math.floor(max) * 1000) / 1000 + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_POISON, DAMAGELIST_LOGARITHMIC_DAMAGE, target:isPlayer() and damage / 2 or damage) + end + return true +end diff --git a/data/spells/scripts/attack/eternal_winter.lua b/data/spells/scripts/attack/eternal_winter.lua new file mode 100644 index 0000000..c1b8ffe --- /dev/null +++ b/data/spells/scripts/attack/eternal_winter.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ICETORNADO) +combat:setArea(createCombatArea(AREA_CIRCLE5X5)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 5.5) + 25 + local max = (level / 5) + (magicLevel * 11) + 50 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/ethereal_spear.lua b/data/spells/scripts/attack/ethereal_spear.lua new file mode 100644 index 0000000..164a9c8 --- /dev/null +++ b/data/spells/scripts/attack/ethereal_spear.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ETHEREALSPEAR) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) + +function onGetFormulaValues(player, skill, attack, factor) + local distanceSkill = player:getEffectiveSkillLevel(SKILL_DISTANCE) + local min = (player:getLevel() / 5) + distanceSkill * 0.7 + local max = (player:getLevel() / 5) + distanceSkill + 5 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_SKILLVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/explosion_rune.lua b/data/spells/scripts/attack/explosion_rune.lua new file mode 100644 index 0000000..8c5b420 --- /dev/null +++ b/data/spells/scripts/attack/explosion_rune.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_EXPLOSIONAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_EXPLOSION) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) +combat:setArea(createCombatArea(AREA_CROSS1X1)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.6) + 9 + local max = (level / 5) + (magicLevel * 3.2) + 19 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/fierce_berserk.lua b/data/spells/scripts/attack/fierce_berserk.lua new file mode 100644 index 0000000..1f051ba --- /dev/null +++ b/data/spells/scripts/attack/fierce_berserk.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITAREA) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) +combat:setParameter(COMBAT_PARAM_USECHARGES, true) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) + +function onGetFormulaValues(player, skill, attack, factor) + local min = (player:getLevel() / 5) + (skill * attack * 0.06) + 13 + local max = (player:getLevel() / 5) + (skill * attack * 0.11) + 27 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_SKILLVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/fire_bomb_rune.lua b/data/spells/scripts/attack/fire_bomb_rune.lua new file mode 100644 index 0000000..aabd9a7 --- /dev/null +++ b/data/spells/scripts/attack/fire_bomb_rune.lua @@ -0,0 +1,9 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) +combat:setParameter(COMBAT_PARAM_CREATEITEM, ITEM_FIREFIELD_PVP_FULL) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/fire_field_rune.lua b/data/spells/scripts/attack/fire_field_rune.lua new file mode 100644 index 0000000..3a5f3db --- /dev/null +++ b/data/spells/scripts/attack/fire_field_rune.lua @@ -0,0 +1,8 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) +combat:setParameter(COMBAT_PARAM_CREATEITEM, ITEM_FIREFIELD_PVP_FULL) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/fire_wall_rune.lua b/data/spells/scripts/attack/fire_wall_rune.lua new file mode 100644 index 0000000..a74fe64 --- /dev/null +++ b/data/spells/scripts/attack/fire_wall_rune.lua @@ -0,0 +1,9 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) +combat:setParameter(COMBAT_PARAM_CREATEITEM, ITEM_FIREFIELD_PVP_FULL) +combat:setArea(createCombatArea(AREA_WALLFIELD, AREADIAGONAL_WALLFIELD)) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/fire_wave.lua b/data/spells/scripts/attack/fire_wave.lua new file mode 100644 index 0000000..56b1966 --- /dev/null +++ b/data/spells/scripts/attack/fire_wave.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITBYFIRE) +combat:setArea(createCombatArea(AREA_WAVE4, AREADIAGONAL_WAVE4)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.2) + 7 + local max = (level / 5) + (magicLevel * 2) + 12 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/fireball_rune.lua b/data/spells/scripts/attack/fireball_rune.lua new file mode 100644 index 0000000..903217f --- /dev/null +++ b/data/spells/scripts/attack/fireball_rune.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREATTACK) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.8) + 12 + local max = (level / 5) + (magicLevel * 3) + 17 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/flame_strike.lua b/data/spells/scripts/attack/flame_strike.lua new file mode 100644 index 0000000..1f5850b --- /dev/null +++ b/data/spells/scripts/attack/flame_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREATTACK) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.4) + 8 + local max = (level / 5) + (magicLevel * 2.2) + 14 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/front_sweep.lua b/data/spells/scripts/attack/front_sweep.lua new file mode 100644 index 0000000..f88136c --- /dev/null +++ b/data/spells/scripts/attack/front_sweep.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITAREA) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) +combat:setParameter(COMBAT_PARAM_USECHARGES, true) +combat:setArea(createCombatArea(AREA_WAVE6, AREADIAGONAL_WAVE6)) + +function onGetFormulaValues(player, skill, attack, factor) + local min = (player:getLevel() / 5) + (skill * attack * 0.04) + 11 + local max = (player:getLevel() / 5) + (skill * attack * 0.08) + 21 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_SKILLVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/great_energy_beam.lua b/data/spells/scripts/attack/great_energy_beam.lua new file mode 100644 index 0000000..2b71c0f --- /dev/null +++ b/data/spells/scripts/attack/great_energy_beam.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYAREA) +combat:setArea(createCombatArea(AREA_BEAM8)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 3.6) + 22 + local max = (level / 5) + (magicLevel * 6) + 37 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/great_fireball_rune.lua b/data/spells/scripts/attack/great_fireball_rune.lua new file mode 100644 index 0000000..d048751 --- /dev/null +++ b/data/spells/scripts/attack/great_fireball_rune.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.2) + 7 + local max = (level / 5) + (magicLevel * 2.85) + 16 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/groundshaker.lua b/data/spells/scripts/attack/groundshaker.lua new file mode 100644 index 0000000..bb99954 --- /dev/null +++ b/data/spells/scripts/attack/groundshaker.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_GROUNDSHAKER) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) +combat:setParameter(COMBAT_PARAM_USECHARGES, true) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +function onGetFormulaValues(player, skill, attack, factor) + local min = (player:getLevel() / 5) + (skill * attack * 0.02) + 4 + local max = (player:getLevel() / 5) + (skill * attack * 0.03) + 6 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_SKILLVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/heavy_magic_missile_rune.lua b/data/spells/scripts/attack/heavy_magic_missile_rune.lua new file mode 100644 index 0000000..b05030e --- /dev/null +++ b/data/spells/scripts/attack/heavy_magic_missile_rune.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 0.8) + 5 + local max = (level / 5) + (magicLevel * 1.6) + 10 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/hell's_core.lua b/data/spells/scripts/attack/hell's_core.lua new file mode 100644 index 0000000..31fb914 --- /dev/null +++ b/data/spells/scripts/attack/hell's_core.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREAREA) +combat:setArea(createCombatArea(AREA_CIRCLE5X5)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 8) + 50 + local max = (level / 5) + (magicLevel * 12) + 75 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/holy_flash.lua b/data/spells/scripts/attack/holy_flash.lua new file mode 100644 index 0000000..ee9c049 --- /dev/null +++ b/data/spells/scripts/attack/holy_flash.lua @@ -0,0 +1,15 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HOLYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HOLYDAMAGE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SMALLHOLY) + +function onCastSpell(creature, variant) + local min = (creature:getLevel() / 80) + (creature:getMagicLevel() * 0.3) + 2 + local max = (creature:getLevel() / 80) + (creature:getMagicLevel() * 0.5) + 3 + local rounds = math.random(math.floor(min), math.floor(max)) + local period = math.random(10, 12) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_DAZZLED, DAMAGELIST_CONSTANT_PERIOD, target:isPlayer() and 10 or 20, period, rounds) + end + return true +end diff --git a/data/spells/scripts/attack/holy_missile_rune.lua b/data/spells/scripts/attack/holy_missile_rune.lua new file mode 100644 index 0000000..9764865 --- /dev/null +++ b/data/spells/scripts/attack/holy_missile_rune.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HOLYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HOLYAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_HOLY) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.8) + 11 + local max = (level / 5) + (magicLevel * 3.8) + 23 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/ice_strike.lua b/data/spells/scripts/attack/ice_strike.lua new file mode 100644 index 0000000..deab64c --- /dev/null +++ b/data/spells/scripts/attack/ice_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ICEATTACK) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SMALLICE) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.4) + 8 + local max = (level / 5) + (magicLevel * 2.2) + 14 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/ice_wave.lua b/data/spells/scripts/attack/ice_wave.lua new file mode 100644 index 0000000..c3cc4c3 --- /dev/null +++ b/data/spells/scripts/attack/ice_wave.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ICEAREA) +combat:setArea(createCombatArea(AREA_WAVE4, AREADIAGONAL_WAVE4)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 0.8) + 5 + local max = (level / 5) + (magicLevel * 2) + 12 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/icicle_rune.lua b/data/spells/scripts/attack/icicle_rune.lua new file mode 100644 index 0000000..f5961b9 --- /dev/null +++ b/data/spells/scripts/attack/icicle_rune.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ICEATTACK) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SMALLICE) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.8) + 12 + local max = (level / 5) + (magicLevel * 3) + 17 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/ignite.lua b/data/spells/scripts/attack/ignite.lua new file mode 100644 index 0000000..7d048bf --- /dev/null +++ b/data/spells/scripts/attack/ignite.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITBYFIRE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) + +function onCastSpell(creature, variant) + local min = (creature:getLevel() / 80) + (creature:getMagicLevel() * 0.3) + 2 + local max = (creature:getLevel() / 80) + (creature:getMagicLevel() * 0.6) + 4 + local rounds = math.random(math.floor(min), math.floor(max)) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_FIRE, DAMAGELIST_VARYING_PERIOD, target:isPlayer() and 5 or 10, {8, 10}, rounds) + end + return true +end diff --git a/data/spells/scripts/attack/inflict_wound.lua b/data/spells/scripts/attack/inflict_wound.lua new file mode 100644 index 0000000..229c4e7 --- /dev/null +++ b/data/spells/scripts/attack/inflict_wound.lua @@ -0,0 +1,25 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_DRAWBLOOD) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_WEAPONTYPE) + +local function getHighestSkillLevel(creature) + local skillLevel = -1 + for skillType = SKILL_CLUB, SKILL_AXE do + if skillLevel < creature:getEffectiveSkillLevel(skillType) then + skillLevel = creature:getEffectiveSkillLevel(skillType) + end + end + return skillLevel +end + +function onCastSpell(creature, variant) + local skill = getHighestSkillLevel(creature) + local min = (creature:getLevel() / 80) + (skill * 0.2) + 2 + local max = (creature:getLevel() / 80) + (skill * 0.4) + 2 + local damage = math.random(math.floor(min) * 1000, math.floor(max) * 1000) / 1000 + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_BLEEDING, DAMAGELIST_LOGARITHMIC_DAMAGE, target:isPlayer() and damage / 4 or damage) + end + return true +end diff --git a/data/spells/scripts/attack/light_magic_missile_rune.lua b/data/spells/scripts/attack/light_magic_missile_rune.lua new file mode 100644 index 0000000..798ace2 --- /dev/null +++ b/data/spells/scripts/attack/light_magic_missile_rune.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 0.4) + 3 + local max = (level / 5) + (magicLevel * 0.8) + 5 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/light_stone_shower_rune.lua b/data/spells/scripts/attack/light_stone_shower_rune.lua new file mode 100644 index 0000000..280ef3b --- /dev/null +++ b/data/spells/scripts/attack/light_stone_shower_rune.lua @@ -0,0 +1,19 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_STONES) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_EARTH) +combat:setArea(createCombatArea(AREA_CROSS1X1)) + +function onGetFormulaValues(player, level, magicLevel) + level = math.min(level, 20) + magicLevel = math.min(magicLevel, 20) + local min = (level / 5) + (magicLevel * 0.3) + 2 + local max = (level / 5) + (magicLevel * 0.45) + 3 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/lightest_magic_missile_rune.lua b/data/spells/scripts/attack/lightest_magic_missile_rune.lua new file mode 100644 index 0000000..abb26ca --- /dev/null +++ b/data/spells/scripts/attack/lightest_magic_missile_rune.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) + +function onGetFormulaValues(player, level, magicLevel) + return -3, -7 +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/lightest_missile_rune.lua b/data/spells/scripts/attack/lightest_missile_rune.lua new file mode 100644 index 0000000..546f823 --- /dev/null +++ b/data/spells/scripts/attack/lightest_missile_rune.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) + +function onGetFormulaValues(player, level, magicLevel) + level = math.min(level, 20) + magicLevel = math.min(magicLevel, 20) + local min = (level / 5) + (magicLevel * 0.45) + 3 + local max = (level / 5) + (magicLevel * 0.7) + 4 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/lightning.lua b/data/spells/scripts/attack/lightning.lua new file mode 100644 index 0000000..62dabea --- /dev/null +++ b/data/spells/scripts/attack/lightning.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 2.2) + 12 + local max = (level / 5) + (magicLevel * 3.4) + 21 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/magic_wall_rune.lua b/data/spells/scripts/attack/magic_wall_rune.lua new file mode 100644 index 0000000..79c26ee --- /dev/null +++ b/data/spells/scripts/attack/magic_wall_rune.lua @@ -0,0 +1,7 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) +combat:setParameter(COMBAT_PARAM_CREATEITEM, ITEM_MAGICWALL) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/mud_attack.lua b/data/spells/scripts/attack/mud_attack.lua new file mode 100644 index 0000000..0583a99 --- /dev/null +++ b/data/spells/scripts/attack/mud_attack.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_CARNIPHILA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SMALLEARTH) + +function onGetFormulaValues(player, level, magicLevel) + level = math.min(level, 20) + magicLevel = math.min(magicLevel, 20) + local min = (level / 5) + (magicLevel * 0.4) + 2 + local max = (level / 5) + (magicLevel * 0.8) + 5 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/paralyze_rune.lua b/data/spells/scripts/attack/paralyze_rune.lua new file mode 100644 index 0000000..419d431 --- /dev/null +++ b/data/spells/scripts/attack/paralyze_rune.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_RED) + +local condition = Condition(CONDITION_PARALYZE) +condition:setParameter(CONDITION_PARAM_TICKS, 20000) +condition:setFormula(-1, 80, -1, 80) +combat:addCondition(condition) + +function onCastSpell(creature, variant, isHotkey) + if not combat:execute(creature, variant) then + return false + end + + creature:getPosition():sendMagicEffect(CONST_ME_MAGIC_GREEN) + return true +end diff --git a/data/spells/scripts/attack/physical_strike.lua b/data/spells/scripts/attack/physical_strike.lua new file mode 100644 index 0000000..f6ed9b0 --- /dev/null +++ b/data/spells/scripts/attack/physical_strike.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_EXPLOSIONAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_EXPLOSION) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.6) + 9 + local max = (level / 5) + (magicLevel * 2.4) + 14 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/poison_bomb_rune.lua b/data/spells/scripts/attack/poison_bomb_rune.lua new file mode 100644 index 0000000..d0a0db9 --- /dev/null +++ b/data/spells/scripts/attack/poison_bomb_rune.lua @@ -0,0 +1,9 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) +combat:setParameter(COMBAT_PARAM_CREATEITEM, ITEM_POISONFIELD_PVP) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/poison_field_rune.lua b/data/spells/scripts/attack/poison_field_rune.lua new file mode 100644 index 0000000..fdac58e --- /dev/null +++ b/data/spells/scripts/attack/poison_field_rune.lua @@ -0,0 +1,8 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) +combat:setParameter(COMBAT_PARAM_CREATEITEM, ITEM_POISONFIELD_PVP) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/poison_wall_rune.lua b/data/spells/scripts/attack/poison_wall_rune.lua new file mode 100644 index 0000000..4cf2f42 --- /dev/null +++ b/data/spells/scripts/attack/poison_wall_rune.lua @@ -0,0 +1,9 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) +combat:setParameter(COMBAT_PARAM_CREATEITEM, ITEM_POISONFIELD_PVP) +combat:setArea(createCombatArea(AREA_WALLFIELD, AREADIAGONAL_WALLFIELD)) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/practice_fire_wave.lua b/data/spells/scripts/attack/practice_fire_wave.lua new file mode 100644 index 0000000..77f0a48 --- /dev/null +++ b/data/spells/scripts/attack/practice_fire_wave.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITBYFIRE) +combat:setArea(createCombatArea(AREA_WAVE4, AREADIAGONAL_WAVE4)) + +function onGetFormulaValues(player, level, magicLevel) + return -11, -14 +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/rage_of_the_skies.lua b/data/spells/scripts/attack/rage_of_the_skies.lua new file mode 100644 index 0000000..c91ec0f --- /dev/null +++ b/data/spells/scripts/attack/rage_of_the_skies.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_BIGCLOUDS) +combat:setArea(createCombatArea(AREA_CIRCLE6X6)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 4) + 75 + local max = (level / 5) + (magicLevel * 10) + 150 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/scorch.lua b/data/spells/scripts/attack/scorch.lua new file mode 100644 index 0000000..6cd2b79 --- /dev/null +++ b/data/spells/scripts/attack/scorch.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITBYFIRE) +combat:setArea(createCombatArea(AREA_WAVE4)) + +function onGetFormulaValues(player, level, magicLevel) + level = math.min(level, 20) + magicLevel = math.min(magicLevel, 20) + local min = (level / 5) + (magicLevel * 0.3) + 2 + local max = (level / 5) + (magicLevel * 0.45) + 3 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/soulfire_rune.lua b/data/spells/scripts/attack/soulfire_rune.lua new file mode 100644 index 0000000..dbec631 --- /dev/null +++ b/data/spells/scripts/attack/soulfire_rune.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITBYFIRE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) + +function onCastSpell(creature, variant, isHotkey) + local min = (creature:getLevel() / 80) + (creature:getMagicLevel() * 0.3) + 2 + local max = (creature:getLevel() / 80) + (creature:getMagicLevel() * 0.6) + 4 + local rounds = math.random(math.floor(min), math.floor(max)) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_FIRE, DAMAGELIST_VARYING_PERIOD, target:isPlayer() and 5 or 10, {8, 10}, rounds) + end + return true +end diff --git a/data/spells/scripts/attack/stalagmite_rune.lua b/data/spells/scripts/attack/stalagmite_rune.lua new file mode 100644 index 0000000..9053633 --- /dev/null +++ b/data/spells/scripts/attack/stalagmite_rune.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_STONES) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_EARTH) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 0.8) + 5 + local max = (level / 5) + (magicLevel * 1.6) + 10 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/stone_shower_rune.lua b/data/spells/scripts/attack/stone_shower_rune.lua new file mode 100644 index 0000000..9a9f709 --- /dev/null +++ b/data/spells/scripts/attack/stone_shower_rune.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_STONES) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_EARTH) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + magicLevel + 6 + local max = (level / 5) + (magicLevel * 2.6) + 16 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/strong_energy_strike.lua b/data/spells/scripts/attack/strong_energy_strike.lua new file mode 100644 index 0000000..cd723bb --- /dev/null +++ b/data/spells/scripts/attack/strong_energy_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 2.8) + 16 + local max = (level / 5) + (magicLevel * 4.4) + 28 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/strong_ethereal_spear.lua b/data/spells/scripts/attack/strong_ethereal_spear.lua new file mode 100644 index 0000000..2b457a5 --- /dev/null +++ b/data/spells/scripts/attack/strong_ethereal_spear.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ETHEREALSPEAR) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) + +function onGetFormulaValues(player, skill, attack, factor) + local distanceSkill = player:getEffectiveSkillLevel(SKILL_DISTANCE) + local min = (player:getLevel() / 5) + distanceSkill + 7 + local max = (player:getLevel() / 5) + (distanceSkill * 1.5) + 13 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_SKILLVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/strong_flame_strike.lua b/data/spells/scripts/attack/strong_flame_strike.lua new file mode 100644 index 0000000..1c0f75e --- /dev/null +++ b/data/spells/scripts/attack/strong_flame_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREATTACK) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 2.8) + 16 + local max = (level / 5) + (magicLevel * 4.4) + 28 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/strong_ice_strike.lua b/data/spells/scripts/attack/strong_ice_strike.lua new file mode 100644 index 0000000..5e2830b --- /dev/null +++ b/data/spells/scripts/attack/strong_ice_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ICEATTACK) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SMALLICE) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 2.8) + 16 + local max = (level / 5) + (magicLevel * 4.4) + 28 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/strong_ice_wave.lua b/data/spells/scripts/attack/strong_ice_wave.lua new file mode 100644 index 0000000..4b4a721 --- /dev/null +++ b/data/spells/scripts/attack/strong_ice_wave.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ICEAREA) +combat:setArea(createCombatArea(AREA_WAVE3)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 4.5) + 20 + local max = (level / 5) + (magicLevel * 7.6) + 48 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/strong_terra_strike.lua b/data/spells/scripts/attack/strong_terra_strike.lua new file mode 100644 index 0000000..7a7e74f --- /dev/null +++ b/data/spells/scripts/attack/strong_terra_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_CARNIPHILA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SMALLEARTH) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 2.8) + 16 + local max = (level / 5) + (magicLevel * 4.4) + 28 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/sudden_death_rune.lua b/data/spells/scripts/attack/sudden_death_rune.lua new file mode 100644 index 0000000..532c2ea --- /dev/null +++ b/data/spells/scripts/attack/sudden_death_rune.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DEATHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MORTAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SUDDENDEATH) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 4.3) + 32 + local max = (level / 5) + (magicLevel * 7.4) + 48 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/terra_strike.lua b/data/spells/scripts/attack/terra_strike.lua new file mode 100644 index 0000000..19154c9 --- /dev/null +++ b/data/spells/scripts/attack/terra_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_CARNIPHILA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SMALLEARTH) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.4) + 8 + local max = (level / 5) + (magicLevel * 2.2) + 14 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/terra_wave.lua b/data/spells/scripts/attack/terra_wave.lua new file mode 100644 index 0000000..d50097f --- /dev/null +++ b/data/spells/scripts/attack/terra_wave.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SMALLPLANTS) +combat:setArea(createCombatArea(AREA_SQUAREWAVE5, AREADIAGONAL_SQUAREWAVE5)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 3.25) + 5 + local max = (level / 5) + (magicLevel * 6.75) + 30 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/thunderstorm_rune.lua b/data/spells/scripts/attack/thunderstorm_rune.lua new file mode 100644 index 0000000..798c6e9 --- /dev/null +++ b/data/spells/scripts/attack/thunderstorm_rune.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGYBALL) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + magicLevel + 6 + local max = (level / 5) + (magicLevel * 2.6) + 16 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/ultimate_energy_strike.lua b/data/spells/scripts/attack/ultimate_energy_strike.lua new file mode 100644 index 0000000..2aef4b9 --- /dev/null +++ b/data/spells/scripts/attack/ultimate_energy_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 4.5) + 35 + local max = (level / 5) + (magicLevel * 7.3) + 55 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/ultimate_flame_strike.lua b/data/spells/scripts/attack/ultimate_flame_strike.lua new file mode 100644 index 0000000..a60738a --- /dev/null +++ b/data/spells/scripts/attack/ultimate_flame_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREATTACK) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 4.5) + 35 + local max = (level / 5) + (magicLevel * 7.3) + 55 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/ultimate_ice_strike.lua b/data/spells/scripts/attack/ultimate_ice_strike.lua new file mode 100644 index 0000000..10f3e27 --- /dev/null +++ b/data/spells/scripts/attack/ultimate_ice_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ICEATTACK) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SMALLICE) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 4.5) + 35 + local max = (level / 5) + (magicLevel * 7.3) + 55 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/ultimate_terra_strike.lua b/data/spells/scripts/attack/ultimate_terra_strike.lua new file mode 100644 index 0000000..c7b863e --- /dev/null +++ b/data/spells/scripts/attack/ultimate_terra_strike.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_CARNIPHILA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SMALLEARTH) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 4.5) + 35 + local max = (level / 5) + (magicLevel * 7.3) + 55 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/whirlwind_throw.lua b/data/spells/scripts/attack/whirlwind_throw.lua new file mode 100644 index 0000000..5080621 --- /dev/null +++ b/data/spells/scripts/attack/whirlwind_throw.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_WEAPONTYPE) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) +combat:setParameter(COMBAT_PARAM_USECHARGES, true) + +function onGetFormulaValues(player, skill, attack, factor) + local min = (player:getLevel() / 5) + (skill * attack * 0.01) + 1 + local max = (player:getLevel() / 5) + (skill * attack * 0.03) + 6 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_SKILLVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/wild_growth_rune.lua b/data/spells/scripts/attack/wild_growth_rune.lua new file mode 100644 index 0000000..1ab7d60 --- /dev/null +++ b/data/spells/scripts/attack/wild_growth_rune.lua @@ -0,0 +1,7 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) +combat:setParameter(COMBAT_PARAM_CREATEITEM, ITEM_WILDGROWTH) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/attack/wrath_of_nature.lua b/data/spells/scripts/attack/wrath_of_nature.lua new file mode 100644 index 0000000..80f1c72 --- /dev/null +++ b/data/spells/scripts/attack/wrath_of_nature.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SMALLPLANTS) +combat:setArea(createCombatArea(AREA_CIRCLE6X6)) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 3) + 32 + local max = (level / 5) + (magicLevel * 9) + 40 + return -min, -max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/conjuring/animate_dead_rune.lua b/data/spells/scripts/conjuring/animate_dead_rune.lua new file mode 100644 index 0000000..d377f8a --- /dev/null +++ b/data/spells/scripts/conjuring/animate_dead_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2316, 1) +end diff --git a/data/spells/scripts/conjuring/arrow_call.lua b/data/spells/scripts/conjuring/arrow_call.lua new file mode 100644 index 0000000..061a908 --- /dev/null +++ b/data/spells/scripts/conjuring/arrow_call.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(0, 23839, 3, CONST_ME_MAGIC_BLUE) +end diff --git a/data/spells/scripts/conjuring/avalanche_rune.lua b/data/spells/scripts/conjuring/avalanche_rune.lua new file mode 100644 index 0000000..551e669 --- /dev/null +++ b/data/spells/scripts/conjuring/avalanche_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2274, 4) +end diff --git a/data/spells/scripts/conjuring/blank_rune.lua b/data/spells/scripts/conjuring/blank_rune.lua new file mode 100644 index 0000000..6edf7f2 --- /dev/null +++ b/data/spells/scripts/conjuring/blank_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(0, 2260, 1) +end diff --git a/data/spells/scripts/conjuring/chameleon_rune.lua b/data/spells/scripts/conjuring/chameleon_rune.lua new file mode 100644 index 0000000..eeaef43 --- /dev/null +++ b/data/spells/scripts/conjuring/chameleon_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2291, 1) +end diff --git a/data/spells/scripts/conjuring/conjure_arrow.lua b/data/spells/scripts/conjuring/conjure_arrow.lua new file mode 100644 index 0000000..c930588 --- /dev/null +++ b/data/spells/scripts/conjuring/conjure_arrow.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(0, 2544, 10, CONST_ME_MAGIC_BLUE) +end diff --git a/data/spells/scripts/conjuring/conjure_bolt.lua b/data/spells/scripts/conjuring/conjure_bolt.lua new file mode 100644 index 0000000..13acd10 --- /dev/null +++ b/data/spells/scripts/conjuring/conjure_bolt.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(0, 2543, 5, CONST_ME_MAGIC_BLUE) +end diff --git a/data/spells/scripts/conjuring/conjure_explosive_arrow.lua b/data/spells/scripts/conjuring/conjure_explosive_arrow.lua new file mode 100644 index 0000000..fda0294 --- /dev/null +++ b/data/spells/scripts/conjuring/conjure_explosive_arrow.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(0, 2546, 8, CONST_ME_MAGIC_BLUE) +end diff --git a/data/spells/scripts/conjuring/conjure_piercing_bolt.lua b/data/spells/scripts/conjuring/conjure_piercing_bolt.lua new file mode 100644 index 0000000..b9c1180 --- /dev/null +++ b/data/spells/scripts/conjuring/conjure_piercing_bolt.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(0, 7363, 5, CONST_ME_MAGIC_BLUE) +end diff --git a/data/spells/scripts/conjuring/conjure_poisoned_arrow.lua b/data/spells/scripts/conjuring/conjure_poisoned_arrow.lua new file mode 100644 index 0000000..cf3ab9e --- /dev/null +++ b/data/spells/scripts/conjuring/conjure_poisoned_arrow.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(0, 2545, 7, CONST_ME_MAGIC_BLUE) +end diff --git a/data/spells/scripts/conjuring/conjure_power_bolt.lua b/data/spells/scripts/conjuring/conjure_power_bolt.lua new file mode 100644 index 0000000..bd24485 --- /dev/null +++ b/data/spells/scripts/conjuring/conjure_power_bolt.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(0, 2547, 10, CONST_ME_MAGIC_BLUE) +end diff --git a/data/spells/scripts/conjuring/conjure_sniper_arrow.lua b/data/spells/scripts/conjuring/conjure_sniper_arrow.lua new file mode 100644 index 0000000..a286d52 --- /dev/null +++ b/data/spells/scripts/conjuring/conjure_sniper_arrow.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(0, 7364, 5, CONST_ME_MAGIC_BLUE) +end diff --git a/data/spells/scripts/conjuring/convince_creature_rune.lua b/data/spells/scripts/conjuring/convince_creature_rune.lua new file mode 100644 index 0000000..b250a7d --- /dev/null +++ b/data/spells/scripts/conjuring/convince_creature_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2290, 1) +end diff --git a/data/spells/scripts/conjuring/cure_poison_rune.lua b/data/spells/scripts/conjuring/cure_poison_rune.lua new file mode 100644 index 0000000..2456b88 --- /dev/null +++ b/data/spells/scripts/conjuring/cure_poison_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2266, 1) +end diff --git a/data/spells/scripts/conjuring/destroy_field_rune.lua b/data/spells/scripts/conjuring/destroy_field_rune.lua new file mode 100644 index 0000000..4ce3fd3 --- /dev/null +++ b/data/spells/scripts/conjuring/destroy_field_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2261, 3) +end diff --git a/data/spells/scripts/conjuring/disintegrate_rune.lua b/data/spells/scripts/conjuring/disintegrate_rune.lua new file mode 100644 index 0000000..ec5ec67 --- /dev/null +++ b/data/spells/scripts/conjuring/disintegrate_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2310, 3) +end diff --git a/data/spells/scripts/conjuring/enchant_spear.lua b/data/spells/scripts/conjuring/enchant_spear.lua new file mode 100644 index 0000000..b534cef --- /dev/null +++ b/data/spells/scripts/conjuring/enchant_spear.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2389, 7367, 1, CONST_ME_MAGIC_GREEN) +end diff --git a/data/spells/scripts/conjuring/enchant_staff.lua b/data/spells/scripts/conjuring/enchant_staff.lua new file mode 100644 index 0000000..cd79482 --- /dev/null +++ b/data/spells/scripts/conjuring/enchant_staff.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2401, 2433, 1, CONST_ME_MAGIC_GREEN) +end diff --git a/data/spells/scripts/conjuring/energy_bomb_rune.lua b/data/spells/scripts/conjuring/energy_bomb_rune.lua new file mode 100644 index 0000000..470977f --- /dev/null +++ b/data/spells/scripts/conjuring/energy_bomb_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2262, 2) +end diff --git a/data/spells/scripts/conjuring/energy_field_rune.lua b/data/spells/scripts/conjuring/energy_field_rune.lua new file mode 100644 index 0000000..77a9b02 --- /dev/null +++ b/data/spells/scripts/conjuring/energy_field_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2277, 3) +end diff --git a/data/spells/scripts/conjuring/energy_wall_rune.lua b/data/spells/scripts/conjuring/energy_wall_rune.lua new file mode 100644 index 0000000..fa6d968 --- /dev/null +++ b/data/spells/scripts/conjuring/energy_wall_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2279, 4) +end diff --git a/data/spells/scripts/conjuring/explosion_rune.lua b/data/spells/scripts/conjuring/explosion_rune.lua new file mode 100644 index 0000000..483f619 --- /dev/null +++ b/data/spells/scripts/conjuring/explosion_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2313, 6) +end diff --git a/data/spells/scripts/conjuring/fire_bomb_rune.lua b/data/spells/scripts/conjuring/fire_bomb_rune.lua new file mode 100644 index 0000000..4beda35 --- /dev/null +++ b/data/spells/scripts/conjuring/fire_bomb_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2305, 2) +end diff --git a/data/spells/scripts/conjuring/fire_field_rune.lua b/data/spells/scripts/conjuring/fire_field_rune.lua new file mode 100644 index 0000000..96c1fe3 --- /dev/null +++ b/data/spells/scripts/conjuring/fire_field_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2301, 3) +end diff --git a/data/spells/scripts/conjuring/fire_wall_rune.lua b/data/spells/scripts/conjuring/fire_wall_rune.lua new file mode 100644 index 0000000..2186343 --- /dev/null +++ b/data/spells/scripts/conjuring/fire_wall_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2303, 4) +end diff --git a/data/spells/scripts/conjuring/fireball_rune.lua b/data/spells/scripts/conjuring/fireball_rune.lua new file mode 100644 index 0000000..bdcb120 --- /dev/null +++ b/data/spells/scripts/conjuring/fireball_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2302, 5) +end diff --git a/data/spells/scripts/conjuring/great_fireball_rune.lua b/data/spells/scripts/conjuring/great_fireball_rune.lua new file mode 100644 index 0000000..1842809 --- /dev/null +++ b/data/spells/scripts/conjuring/great_fireball_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2304, 4) +end diff --git a/data/spells/scripts/conjuring/heavy_magic_missile_rune.lua b/data/spells/scripts/conjuring/heavy_magic_missile_rune.lua new file mode 100644 index 0000000..786c3b1 --- /dev/null +++ b/data/spells/scripts/conjuring/heavy_magic_missile_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2311, 10) +end diff --git a/data/spells/scripts/conjuring/holy_missile_rune.lua b/data/spells/scripts/conjuring/holy_missile_rune.lua new file mode 100644 index 0000000..b4931f0 --- /dev/null +++ b/data/spells/scripts/conjuring/holy_missile_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2295, 5) +end diff --git a/data/spells/scripts/conjuring/icicle_rune.lua b/data/spells/scripts/conjuring/icicle_rune.lua new file mode 100644 index 0000000..3ff841c --- /dev/null +++ b/data/spells/scripts/conjuring/icicle_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2271, 5) +end diff --git a/data/spells/scripts/conjuring/intense_healing_rune.lua b/data/spells/scripts/conjuring/intense_healing_rune.lua new file mode 100644 index 0000000..9080b9a --- /dev/null +++ b/data/spells/scripts/conjuring/intense_healing_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2265, 1) +end diff --git a/data/spells/scripts/conjuring/light_magic_missile_rune.lua b/data/spells/scripts/conjuring/light_magic_missile_rune.lua new file mode 100644 index 0000000..6018747 --- /dev/null +++ b/data/spells/scripts/conjuring/light_magic_missile_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2287, 10) +end diff --git a/data/spells/scripts/conjuring/light_stone_shower_rune.lua b/data/spells/scripts/conjuring/light_stone_shower_rune.lua new file mode 100644 index 0000000..5f27585 --- /dev/null +++ b/data/spells/scripts/conjuring/light_stone_shower_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 23722, 4) +end diff --git a/data/spells/scripts/conjuring/lightest_missile_rune.lua b/data/spells/scripts/conjuring/lightest_missile_rune.lua new file mode 100644 index 0000000..3da0653 --- /dev/null +++ b/data/spells/scripts/conjuring/lightest_missile_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 23723, 10) +end diff --git a/data/spells/scripts/conjuring/magic_wall_rune.lua b/data/spells/scripts/conjuring/magic_wall_rune.lua new file mode 100644 index 0000000..d0964d6 --- /dev/null +++ b/data/spells/scripts/conjuring/magic_wall_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2293, 3) +end diff --git a/data/spells/scripts/conjuring/paralyze_rune.lua b/data/spells/scripts/conjuring/paralyze_rune.lua new file mode 100644 index 0000000..88c1b9f --- /dev/null +++ b/data/spells/scripts/conjuring/paralyze_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2278, 1) +end diff --git a/data/spells/scripts/conjuring/poison_bomb_rune.lua b/data/spells/scripts/conjuring/poison_bomb_rune.lua new file mode 100644 index 0000000..2bfd335 --- /dev/null +++ b/data/spells/scripts/conjuring/poison_bomb_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2286, 2) +end diff --git a/data/spells/scripts/conjuring/poison_field_rune.lua b/data/spells/scripts/conjuring/poison_field_rune.lua new file mode 100644 index 0000000..3c82f91 --- /dev/null +++ b/data/spells/scripts/conjuring/poison_field_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2285, 3) +end diff --git a/data/spells/scripts/conjuring/poison_wall_rune.lua b/data/spells/scripts/conjuring/poison_wall_rune.lua new file mode 100644 index 0000000..841903b --- /dev/null +++ b/data/spells/scripts/conjuring/poison_wall_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2289, 4) +end diff --git a/data/spells/scripts/conjuring/practice_magic_missile_rune.lua b/data/spells/scripts/conjuring/practice_magic_missile_rune.lua new file mode 100644 index 0000000..91b7083 --- /dev/null +++ b/data/spells/scripts/conjuring/practice_magic_missile_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 19392, 10) +end diff --git a/data/spells/scripts/conjuring/soulfire_rune.lua b/data/spells/scripts/conjuring/soulfire_rune.lua new file mode 100644 index 0000000..95b1978 --- /dev/null +++ b/data/spells/scripts/conjuring/soulfire_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2308, 3) +end diff --git a/data/spells/scripts/conjuring/stalagmite_rune.lua b/data/spells/scripts/conjuring/stalagmite_rune.lua new file mode 100644 index 0000000..4b8ee41 --- /dev/null +++ b/data/spells/scripts/conjuring/stalagmite_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2292, 10) +end diff --git a/data/spells/scripts/conjuring/stone_shower_rune.lua b/data/spells/scripts/conjuring/stone_shower_rune.lua new file mode 100644 index 0000000..094f74a --- /dev/null +++ b/data/spells/scripts/conjuring/stone_shower_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2288, 4) +end diff --git a/data/spells/scripts/conjuring/sudden_death_rune.lua b/data/spells/scripts/conjuring/sudden_death_rune.lua new file mode 100644 index 0000000..6e23bb3 --- /dev/null +++ b/data/spells/scripts/conjuring/sudden_death_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2268, 3) +end diff --git a/data/spells/scripts/conjuring/thunderstorm_rune.lua b/data/spells/scripts/conjuring/thunderstorm_rune.lua new file mode 100644 index 0000000..4a2e6e9 --- /dev/null +++ b/data/spells/scripts/conjuring/thunderstorm_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2315, 4) +end diff --git a/data/spells/scripts/conjuring/ultimate_healing_rune.lua b/data/spells/scripts/conjuring/ultimate_healing_rune.lua new file mode 100644 index 0000000..7481442 --- /dev/null +++ b/data/spells/scripts/conjuring/ultimate_healing_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2273, 1) +end diff --git a/data/spells/scripts/conjuring/wild_growth_rune.lua b/data/spells/scripts/conjuring/wild_growth_rune.lua new file mode 100644 index 0000000..af73e35 --- /dev/null +++ b/data/spells/scripts/conjuring/wild_growth_rune.lua @@ -0,0 +1,3 @@ +function onCastSpell(creature, variant) + return creature:conjureItem(2260, 2269, 2) +end diff --git a/data/spells/scripts/custom/apocalypse.lua b/data/spells/scripts/custom/apocalypse.lua new file mode 100644 index 0000000..b0248b3 --- /dev/null +++ b/data/spells/scripts/custom/apocalypse.lua @@ -0,0 +1,25 @@ +function spellCallback(cid, position, count) + if Creature(cid) then + if count > 0 or math.random(0, 1) == 1 then + position:sendMagicEffect(CONST_ME_HITBYFIRE) + doAreaCombat(cid, COMBAT_FIREDAMAGE, position, 0, -100, -100, CONST_ME_EXPLOSIONHIT) + end + + if count < 5 then + count = count + 1 + addEvent(spellCallback, math.random(1000, 4000), cid, position, count) + end + end +end + +function onTargetTile(creature, position) + spellCallback(creature:getId(), position, 0) +end + +local combat = Combat() +combat:setArea(createCombatArea(AREA_CIRCLE5X5)) +combat:setCallback(CALLBACK_PARAM_TARGETTILE, "onTargetTile") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/custom/combustion.lua b/data/spells/scripts/custom/combustion.lua new file mode 100644 index 0000000..f86ca58 --- /dev/null +++ b/data/spells/scripts/custom/combustion.lua @@ -0,0 +1,15 @@ +local condition = Condition(CONDITION_FIRE) +condition:setParameter(CONDITION_PARAM_DELAYED, true) +condition:addDamage(5, 3000, -25) +condition:addDamage(1, 5000, -666) + +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) +combat:setFormula(COMBAT_FORMULA_LEVELMAGIC, -1.3, -30, -1.7, 0) +combat:addCondition(condition) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/custom/drunk.lua b/data/spells/scripts/custom/drunk.lua new file mode 100644 index 0000000..fcbae56 --- /dev/null +++ b/data/spells/scripts/custom/drunk.lua @@ -0,0 +1,11 @@ +local condition = Condition(CONDITION_DRUNK) +condition:setParameter(CONDITION_PARAM_TICKS, 20000) + +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_RED) +combat:addCondition(condition) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/custom/magic prison.lua b/data/spells/scripts/custom/magic prison.lua new file mode 100644 index 0000000..ee5bfba --- /dev/null +++ b/data/spells/scripts/custom/magic prison.lua @@ -0,0 +1,8 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) +combat:setParameter(COMBAT_PARAM_CREATEITEM, ITEM_MAGICWALL) +combat:setArea(createCombatArea(AREA_CROSS1X1)) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/custom/polymorph.lua b/data/spells/scripts/custom/polymorph.lua new file mode 100644 index 0000000..e807554 --- /dev/null +++ b/data/spells/scripts/custom/polymorph.lua @@ -0,0 +1,29 @@ +local condition = Condition(CONDITION_OUTFIT) +condition:setParameter(CONDITION_PARAM_TICKS, 20000) +condition:setOutfit(0, 230, 0, 0, 0, 0) +condition:setOutfit(0, 231, 0, 0, 0, 0) +condition:setOutfit(0, 232, 0, 0, 0, 0) +condition:setOutfit(0, 233, 0, 0, 0, 0) +condition:setOutfit(0, 234, 0, 0, 0, 0) +condition:setOutfit(0, 235, 0, 0, 0, 0) +condition:setOutfit(0, 236, 0, 0, 0, 0) +condition:setOutfit(0, 237, 0, 0, 0, 0) +condition:setOutfit(0, 238, 0, 0, 0, 0) +condition:setOutfit(0, 239, 0, 0, 0, 0) +condition:setOutfit(0, 240, 0, 0, 0, 0) +condition:setOutfit(0, 241, 0, 0, 0, 0) +condition:setOutfit(0, 242, 0, 0, 0, 0) +condition:setOutfit(0, 243, 0, 0, 0, 0) +condition:setOutfit(0, 244, 0, 0, 0, 0) +condition:setOutfit(0, 245, 0, 0, 0, 0) +condition:setOutfit(0, 246, 0, 0, 0, 0) +condition:setOutfit(0, 247, 0, 0, 0, 0) + +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_GREEN) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) +combat:addCondition(condition) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/bruise_bane.lua b/data/spells/scripts/healing/bruise_bane.lua new file mode 100644 index 0000000..425c81f --- /dev/null +++ b/data/spells/scripts/healing/bruise_bane.lua @@ -0,0 +1,19 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + level = math.min(level, 20) + magicLevel = math.min(magicLevel, 20) + local min = (level / 5) + (magicLevel * 1.4) + 8 + local max = (level / 5) + (magicLevel * 1.8) + 11 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/cure_bleeding.lua b/data/spells/scripts/healing/cure_bleeding.lua new file mode 100644 index 0000000..b416385 --- /dev/null +++ b/data/spells/scripts/healing/cure_bleeding.lua @@ -0,0 +1,8 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_BLEEDING) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/cure_burning.lua b/data/spells/scripts/healing/cure_burning.lua new file mode 100644 index 0000000..7813858 --- /dev/null +++ b/data/spells/scripts/healing/cure_burning.lua @@ -0,0 +1,8 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_FIRE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/cure_curse.lua b/data/spells/scripts/healing/cure_curse.lua new file mode 100644 index 0000000..1d74c77 --- /dev/null +++ b/data/spells/scripts/healing/cure_curse.lua @@ -0,0 +1,8 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_CURSED) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/cure_electrification.lua b/data/spells/scripts/healing/cure_electrification.lua new file mode 100644 index 0000000..14f8862 --- /dev/null +++ b/data/spells/scripts/healing/cure_electrification.lua @@ -0,0 +1,8 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_ENERGY) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/cure_poison.lua b/data/spells/scripts/healing/cure_poison.lua new file mode 100644 index 0000000..b50548e --- /dev/null +++ b/data/spells/scripts/healing/cure_poison.lua @@ -0,0 +1,8 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_POISON) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/cure_poison_rune.lua b/data/spells/scripts/healing/cure_poison_rune.lua new file mode 100644 index 0000000..f272fb9 --- /dev/null +++ b/data/spells/scripts/healing/cure_poison_rune.lua @@ -0,0 +1,9 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_POISON) +combat:setParameter(COMBAT_PARAM_TARGETCASTERORTOPMOST, true) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/divine_healing.lua b/data/spells/scripts/healing/divine_healing.lua new file mode 100644 index 0000000..6cc3847 --- /dev/null +++ b/data/spells/scripts/healing/divine_healing.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 6.9) + 40 + local max = (level / 5) + (magicLevel * 13.2) + 82 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/heal_friend.lua b/data/spells/scripts/healing/heal_friend.lua new file mode 100644 index 0000000..a13bfc8 --- /dev/null +++ b/data/spells/scripts/healing/heal_friend.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_GREEN) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 6.3) + 45 + local max = (level / 5) + (magicLevel * 14.4) + 90 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + creature:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/intense_healing.lua b/data/spells/scripts/healing/intense_healing.lua new file mode 100644 index 0000000..b5bf4c4 --- /dev/null +++ b/data/spells/scripts/healing/intense_healing.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 3.2) + 20 + local max = (level / 5) + (magicLevel * 5.4) + 40 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/intense_healing_rune.lua b/data/spells/scripts/healing/intense_healing_rune.lua new file mode 100644 index 0000000..82fe568 --- /dev/null +++ b/data/spells/scripts/healing/intense_healing_rune.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_TARGETCASTERORTOPMOST, true) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 3.2) + 20 + local max = (level / 5) + (magicLevel * 5.4) + 40 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/intense_recovery.lua b/data/spells/scripts/healing/intense_recovery.lua new file mode 100644 index 0000000..f626cff --- /dev/null +++ b/data/spells/scripts/healing/intense_recovery.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_GREEN) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local condition = Condition(CONDITION_REGENERATION) +condition:setParameter(CONDITION_PARAM_TICKS, 1 * 60 * 1000) +condition:setParameter(CONDITION_PARAM_HEALTHGAIN, 40) +condition:setParameter(CONDITION_PARAM_HEALTHTICKS, 3000) +condition:setParameter(CONDITION_PARAM_BUFF_SPELL, true) +combat:addCondition(condition) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/intense_wound_cleansing.lua b/data/spells/scripts/healing/intense_wound_cleansing.lua new file mode 100644 index 0000000..cee08e3 --- /dev/null +++ b/data/spells/scripts/healing/intense_wound_cleansing.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 70) + 438 + local max = (level / 5) + (magicLevel * 92) + 544 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/light_healing.lua b/data/spells/scripts/healing/light_healing.lua new file mode 100644 index 0000000..ce66e0e --- /dev/null +++ b/data/spells/scripts/healing/light_healing.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 1.4) + 8 + local max = (level / 5) + (magicLevel * 1.8) + 11 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/magic_patch.lua b/data/spells/scripts/healing/magic_patch.lua new file mode 100644 index 0000000..8c60b46 --- /dev/null +++ b/data/spells/scripts/healing/magic_patch.lua @@ -0,0 +1,19 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + level = math.min(level, 20) + magicLevel = math.min(magicLevel, 20) + local min = (level / 5) + (magicLevel * 0.6) + 3 + local max = (level / 5) + magicLevel + 6 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/mass_healing.lua b/data/spells/scripts/healing/mass_healing.lua new file mode 100644 index 0000000..3ac122a --- /dev/null +++ b/data/spells/scripts/healing/mass_healing.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +function onCastSpell(creature, variant) + local min = (creature:getLevel() / 5) + (creature:getMagicLevel() * 4.6) + 100 + local max = (creature:getLevel() / 5) + (creature:getMagicLevel() * 9.6) + 125 + for _, target in ipairs(combat:getTargets(creature, variant)) do + local master = target:getMaster() + if target:isPlayer() or master and master:isPlayer() then + doTargetCombat(0, target, COMBAT_HEALING, min, max) + end + end + return true +end diff --git a/data/spells/scripts/healing/practice_healing.lua b/data/spells/scripts/healing/practice_healing.lua new file mode 100644 index 0000000..b18ec02 --- /dev/null +++ b/data/spells/scripts/healing/practice_healing.lua @@ -0,0 +1,15 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + return 5, 9 +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/recovery.lua b/data/spells/scripts/healing/recovery.lua new file mode 100644 index 0000000..63e1f8f --- /dev/null +++ b/data/spells/scripts/healing/recovery.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_GREEN) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local condition = Condition(CONDITION_REGENERATION) +condition:setParameter(CONDITION_PARAM_TICKS, 1 * 60 * 1000) +condition:setParameter(CONDITION_PARAM_HEALTHGAIN, 20) +condition:setParameter(CONDITION_PARAM_HEALTHTICKS, 3000) +condition:setParameter(CONDITION_PARAM_BUFF_SPELL, true) +combat:addCondition(condition) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/salvation.lua b/data/spells/scripts/healing/salvation.lua new file mode 100644 index 0000000..58e2766 --- /dev/null +++ b/data/spells/scripts/healing/salvation.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 12) + 75 + local max = (level / 5) + (magicLevel * 20) + 125 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/ultimate_healing.lua b/data/spells/scripts/healing/ultimate_healing.lua new file mode 100644 index 0000000..a5a8cf8 --- /dev/null +++ b/data/spells/scripts/healing/ultimate_healing.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 6.8) + 42 + local max = (level / 5) + (magicLevel * 12.9) + 90 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/ultimate_healing_rune.lua b/data/spells/scripts/healing/ultimate_healing_rune.lua new file mode 100644 index 0000000..f5d5815 --- /dev/null +++ b/data/spells/scripts/healing/ultimate_healing_rune.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_TARGETCASTERORTOPMOST, true) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 7.3) + 42 + local max = (level / 5) + (magicLevel * 12.4) + 90 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant, isHotkey) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/healing/wound_cleansing.lua b/data/spells/scripts/healing/wound_cleansing.lua new file mode 100644 index 0000000..004f51b --- /dev/null +++ b/data/spells/scripts/healing/wound_cleansing.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_HEALING) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +function onGetFormulaValues(player, level, magicLevel) + local min = (level / 5) + (magicLevel * 4) + 25 + local max = (level / 5) + (magicLevel * 8) + 50 + return min, max +end + +combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/house/edit_door_list.lua b/data/spells/scripts/house/edit_door_list.lua new file mode 100644 index 0000000..495dbbd --- /dev/null +++ b/data/spells/scripts/house/edit_door_list.lua @@ -0,0 +1,16 @@ +function onCastSpell(creature, variant) + local creaturePos = creature:getPosition() + creaturePos:getNextPosition(creature:getDirection()) + local tile = Tile(creaturePos) + local house = tile and tile:getHouse() + local doorId = house and house:getDoorIdByPosition(creaturePos) + if not doorId or not house:canEditAccessList(doorId, creature) then + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + creature:setEditHouse(house, doorId) + creature:sendHouseWindow(house, doorId) + return true +end diff --git a/data/spells/scripts/house/edit_guest_list.lua b/data/spells/scripts/house/edit_guest_list.lua new file mode 100644 index 0000000..7b62f80 --- /dev/null +++ b/data/spells/scripts/house/edit_guest_list.lua @@ -0,0 +1,12 @@ +function onCastSpell(creature, variant) + local house = creature:getTile():getHouse() + if not house or not house:canEditAccessList(GUEST_LIST, creature) then + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + creature:setEditHouse(house, GUEST_LIST) + creature:sendHouseWindow(house, GUEST_LIST) + return true +end diff --git a/data/spells/scripts/house/edit_subowner_list.lua b/data/spells/scripts/house/edit_subowner_list.lua new file mode 100644 index 0000000..97d3ed0 --- /dev/null +++ b/data/spells/scripts/house/edit_subowner_list.lua @@ -0,0 +1,12 @@ +function onCastSpell(creature, variant) + local house = creature:getTile():getHouse() + if not house or not house:canEditAccessList(SUBOWNER_LIST, creature) then + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + creature:setEditHouse(house, SUBOWNER_LIST) + creature:sendHouseWindow(house, SUBOWNER_LIST) + return true +end diff --git a/data/spells/scripts/house/kick.lua b/data/spells/scripts/house/kick.lua new file mode 100644 index 0000000..0235037 --- /dev/null +++ b/data/spells/scripts/house/kick.lua @@ -0,0 +1,10 @@ +function onCastSpell(creature, variant) + local target = Player(variant:getString()) or creature + local house = target:getTile():getHouse() + if not house or not house:kickPlayer(creature, target) then + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + return true +end diff --git a/data/spells/scripts/monster/barbarian_brutetamer_skill_reducer.lua b/data/spells/scripts/monster/barbarian_brutetamer_skill_reducer.lua new file mode 100644 index 0000000..8f4f569 --- /dev/null +++ b/data/spells/scripts/monster/barbarian_brutetamer_skill_reducer.lua @@ -0,0 +1,23 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SNOWBALL) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_POFF) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 5 * 1000}, + {key = CONDITION_PARAM_SKILL_FIST, value = nil}, + {key = CONDITION_PARAM_SKILL_CLUB, value = nil}, + {key = CONDITION_PARAM_SKILL_SWORD, value = nil}, + {key = CONDITION_PARAM_SKILL_AXE, value = nil}, + {key = CONDITION_PARAM_SKILL_DISTANCE, value = nil}, + {key = CONDITION_PARAM_SKILL_SHIELD, value = nil} +} + +function onCastSpell(creature, variant) + local index = math.random(2, #parameters) + parameters[index].value = -math.random(1, 6) + + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition({parameters[1], parameters[index]}) + end + return true +end diff --git a/data/spells/scripts/monster/betrayed_wraith_skill_reducer.lua b/data/spells/scripts/monster/betrayed_wraith_skill_reducer.lua new file mode 100644 index 0000000..cd0ddbd --- /dev/null +++ b/data/spells/scripts/monster/betrayed_wraith_skill_reducer.lua @@ -0,0 +1,15 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_YELLOW_RINGS) +combat:setArea(createCombatArea(AREA_SQUAREWAVE5)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 5 * 1000}, + {key = CONDITION_PARAM_SKILL_DISTANCEPERCENT, value = 80} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/blightwalker_curse.lua b/data/spells/scripts/monster/blightwalker_curse.lua new file mode 100644 index 0000000..7be900b --- /dev/null +++ b/data/spells/scripts/monster/blightwalker_curse.lua @@ -0,0 +1,12 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DEATHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SMALLCLOUDS) +combat:setArea(createCombatArea(AREA_CIRCLE6X6)) + +function onCastSpell(creature, variant) + local damage = math.random(52, 154) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_CURSED, DAMAGELIST_EXPONENTIAL_DAMAGE, damage) + end + return true +end diff --git a/data/spells/scripts/monster/choking_fear_drown.lua b/data/spells/scripts/monster/choking_fear_drown.lua new file mode 100644 index 0000000..ad23766 --- /dev/null +++ b/data/spells/scripts/monster/choking_fear_drown.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DROWNDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_BUBBLES) +combat:setArea(createCombatArea(AREA_CIRCLE6X6)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_DROWN, DAMAGELIST_CONSTANT_PERIOD, 20, 5, 50) + end + return true +end diff --git a/data/spells/scripts/monster/cliff_strider_electrify.lua b/data/spells/scripts/monster/cliff_strider_electrify.lua new file mode 100644 index 0000000..3bf4a89 --- /dev/null +++ b/data/spells/scripts/monster/cliff_strider_electrify.lua @@ -0,0 +1,10 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_ENERGY, DAMAGELIST_VARYING_PERIOD, 25, {10, 12}, 20) + end + return true +end diff --git a/data/spells/scripts/monster/cliff_strider_skill_reducer.lua b/data/spells/scripts/monster/cliff_strider_skill_reducer.lua new file mode 100644 index 0000000..d13f0cc --- /dev/null +++ b/data/spells/scripts/monster/cliff_strider_skill_reducer.lua @@ -0,0 +1,15 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_RED) +combat:setArea(createCombatArea(AREA_CIRCLE2X2)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 6 * 1000}, + {key = CONDITION_PARAM_SKILL_DISTANCEPERCENT, value = 50} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/dark_torturer_skill_reducer.lua b/data/spells/scripts/monster/dark_torturer_skill_reducer.lua new file mode 100644 index 0000000..9fc95a8 --- /dev/null +++ b/data/spells/scripts/monster/dark_torturer_skill_reducer.lua @@ -0,0 +1,15 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SOUND_PURPLE) +combat:setArea(createCombatArea(AREA_SQUAREWAVE6)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 8 * 1000}, + {key = CONDITION_PARAM_SKILL_SHIELDPERCENT, value = 85} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/death_blob_curse.lua b/data/spells/scripts/monster/death_blob_curse.lua new file mode 100644 index 0000000..9f42fe1 --- /dev/null +++ b/data/spells/scripts/monster/death_blob_curse.lua @@ -0,0 +1,12 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DEATHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SMALLCLOUDS) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_DEATH) + +function onCastSpell(creature, variant) + local damage = math.random(17, 36) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_CURSED, DAMAGELIST_EXPONENTIAL_DAMAGE, damage) + end + return true +end diff --git a/data/spells/scripts/monster/deepling_spellsinger_skill_reducer.lua b/data/spells/scripts/monster/deepling_spellsinger_skill_reducer.lua new file mode 100644 index 0000000..b4f2a33 --- /dev/null +++ b/data/spells/scripts/monster/deepling_spellsinger_skill_reducer.lua @@ -0,0 +1,20 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_STUN) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_EXPLOSION) +combat:setArea(createCombatArea(AREA_BEAM1)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 8 * 1000}, + {key = CONDITION_PARAM_SKILL_MELEEPERCENT, value = nil}, + {key = CONDITION_PARAM_SKILL_DISTANCEPERCENT, value = nil} +} + +function onCastSpell(creature, variant) + parameters[2].value = math.random(45, 65) + parameters[3].value = parameters[2].value + + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/demon_outcast_skill_reducer.lua b/data/spells/scripts/monster/demon_outcast_skill_reducer.lua new file mode 100644 index 0000000..6f459ce --- /dev/null +++ b/data/spells/scripts/monster/demon_outcast_skill_reducer.lua @@ -0,0 +1,15 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FLASHARROW) +combat:setArea(createCombatArea(AREA_BEAM1)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 6 * 1000}, + {key = CONDITION_PARAM_SKILL_DISTANCEPERCENT, value = 25} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/diabolic_imp_skill_reducer.lua b/data/spells/scripts/monster/diabolic_imp_skill_reducer.lua new file mode 100644 index 0000000..bdc5951 --- /dev/null +++ b/data/spells/scripts/monster/diabolic_imp_skill_reducer.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) +combat:setArea(createCombatArea(AREA_BEAM1)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 6 * 1000}, + {key = CONDITION_PARAM_SKILL_MELEEPERCENT, value = nil} +} + +function onCastSpell(creature, variant) + parameters[2].value = math.random(70, 80) + + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/dipthrah_skill_reducer.lua b/data/spells/scripts/monster/dipthrah_skill_reducer.lua new file mode 100644 index 0000000..9a79c36 --- /dev/null +++ b/data/spells/scripts/monster/dipthrah_skill_reducer.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HOLYAREA) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 4 * 1000}, + {key = CONDITION_PARAM_SKILL_SHIELDPERCENT, value = 50}, + {key = CONDITION_PARAM_SKILL_MELEEPERCENT, value = 50}, + {key = CONDITION_PARAM_SKILL_DISTANCEPERCENT, value = 50} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/djinn_cancel_invisibility.lua b/data/spells/scripts/monster/djinn_cancel_invisibility.lua new file mode 100644 index 0000000..61128bc --- /dev/null +++ b/data/spells/scripts/monster/djinn_cancel_invisibility.lua @@ -0,0 +1,7 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_INVISIBLE) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/monster/djinn_electrify.lua b/data/spells/scripts/monster/djinn_electrify.lua new file mode 100644 index 0000000..8fa49f4 --- /dev/null +++ b/data/spells/scripts/monster/djinn_electrify.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_ENERGY, DAMAGELIST_VARYING_PERIOD, 25, {10, 12}, 3) + end + return true +end diff --git a/data/spells/scripts/monster/draken_abomination_curse.lua b/data/spells/scripts/monster/draken_abomination_curse.lua new file mode 100644 index 0000000..ad3333f --- /dev/null +++ b/data/spells/scripts/monster/draken_abomination_curse.lua @@ -0,0 +1,12 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DEATHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SMALLCLOUDS) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_DEATH) + +function onCastSpell(creature, variant) + local damage = math.random(62, 128) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_CURSED, DAMAGELIST_EXPONENTIAL_DAMAGE, damage) + end + return true +end diff --git a/data/spells/scripts/monster/energy_elemental_electrify.lua b/data/spells/scripts/monster/energy_elemental_electrify.lua new file mode 100644 index 0000000..6eef60d --- /dev/null +++ b/data/spells/scripts/monster/energy_elemental_electrify.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_BLOCKHIT) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_ENERGY, DAMAGELIST_VARYING_PERIOD, 25, {10, 12}, 7) + end + return true +end diff --git a/data/spells/scripts/monster/enslaved_dwarf_skill_reducer_1.lua b/data/spells/scripts/monster/enslaved_dwarf_skill_reducer_1.lua new file mode 100644 index 0000000..3132fbe --- /dev/null +++ b/data/spells/scripts/monster/enslaved_dwarf_skill_reducer_1.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_RED) +combat:setArea(createCombatArea(AREA_CIRCLE2X2)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 6 * 1000}, + {key = CONDITION_PARAM_SKILL_SHIELDPERCENT, value = nil} +} + +function onCastSpell(creature, variant) + parameters[2].value = math.random(13, 50) + + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/enslaved_dwarf_skill_reducer_2.lua b/data/spells/scripts/monster/enslaved_dwarf_skill_reducer_2.lua new file mode 100644 index 0000000..d477f32 --- /dev/null +++ b/data/spells/scripts/monster/enslaved_dwarf_skill_reducer_2.lua @@ -0,0 +1,15 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITAREA) +combat:setArea(createCombatArea(AREA_CROSS1X1)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 6 * 1000}, + {key = CONDITION_PARAM_SKILL_MELEEPERCENT, value = 45} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/feversleep_skill_reducer.lua b/data/spells/scripts/monster/feversleep_skill_reducer.lua new file mode 100644 index 0000000..003f0e2 --- /dev/null +++ b/data/spells/scripts/monster/feversleep_skill_reducer.lua @@ -0,0 +1,15 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_STUN) +combat:setArea(createCombatArea(AREA_CIRCLE6X6)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 4 * 1000}, + {key = CONDITION_PARAM_SKILL_DISTANCEPERCENT, value = 50} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/forest_fury_skill_reducer.lua b/data/spells/scripts/monster/forest_fury_skill_reducer.lua new file mode 100644 index 0000000..edb104d --- /dev/null +++ b/data/spells/scripts/monster/forest_fury_skill_reducer.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_LARGEROCK) +combat:setArea(createCombatArea(AREA_CIRCLE2X2)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 4 * 1000}, + {key = CONDITION_PARAM_SKILL_SHIELDPERCENT, value = 50}, + {key = CONDITION_PARAM_SKILL_DISTANCEPERCENT, value = 50} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/fury_skill_reducer.lua b/data/spells/scripts/monster/fury_skill_reducer.lua new file mode 100644 index 0000000..4849297 --- /dev/null +++ b/data/spells/scripts/monster/fury_skill_reducer.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SOUND_YELLOW) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 5 * 1000}, + {key = CONDITION_PARAM_SKILL_SHIELDPERCENT, value = 60}, + {key = CONDITION_PARAM_SKILL_MELEEPERCENT, value = 70} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/ghastly_dragon_curse.lua b/data/spells/scripts/monster/ghastly_dragon_curse.lua new file mode 100644 index 0000000..a063645 --- /dev/null +++ b/data/spells/scripts/monster/ghastly_dragon_curse.lua @@ -0,0 +1,12 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DEATHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SMALLCLOUDS) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_DEATH) + +function onCastSpell(creature, variant) + local damage = math.random(128, 954) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_CURSED, DAMAGELIST_EXPONENTIAL_DAMAGE, damage) + end + return true +end diff --git a/data/spells/scripts/monster/hellfire_fighter_soulfire.lua b/data/spells/scripts/monster/hellfire_fighter_soulfire.lua new file mode 100644 index 0000000..84474ba --- /dev/null +++ b/data/spells/scripts/monster/hellfire_fighter_soulfire.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREATTACK) +combat:setArea(createCombatArea(AREA_CIRCLE6X6)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_FIRE, DAMAGELIST_VARYING_PERIOD, 10, {8, 10}, 20) + end + return true +end diff --git a/data/spells/scripts/monster/hellspawn_soulfire.lua b/data/spells/scripts/monster/hellspawn_soulfire.lua new file mode 100644 index 0000000..b85ee6b --- /dev/null +++ b/data/spells/scripts/monster/hellspawn_soulfire.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITBYFIRE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_FIRE, DAMAGELIST_VARYING_PERIOD, 10, {8, 10}, 26) + end + return true +end diff --git a/data/spells/scripts/monster/ice_golem_skill_reducer.lua b/data/spells/scripts/monster/ice_golem_skill_reducer.lua new file mode 100644 index 0000000..2fa92b7 --- /dev/null +++ b/data/spells/scripts/monster/ice_golem_skill_reducer.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITAREA) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 4 * 1000}, + {key = CONDITION_PARAM_SKILL_SHIELDPERCENT, value = 85}, + {key = CONDITION_PARAM_SKILL_MELEEPERCENT, value = 85} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/lancer_beetle_curse.lua b/data/spells/scripts/monster/lancer_beetle_curse.lua new file mode 100644 index 0000000..d34d798 --- /dev/null +++ b/data/spells/scripts/monster/lancer_beetle_curse.lua @@ -0,0 +1,12 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DEATHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SMALLCLOUDS) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_DEATH) + +function onCastSpell(creature, variant) + local damage = math.random(17, 43) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_CURSED, DAMAGELIST_EXPONENTIAL_DAMAGE, damage) + end + return true +end diff --git a/data/spells/scripts/monster/lava_golem_soulfire.lua b/data/spells/scripts/monster/lava_golem_soulfire.lua new file mode 100644 index 0000000..b3329c8 --- /dev/null +++ b/data/spells/scripts/monster/lava_golem_soulfire.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_EXPLOSIONHIT) +combat:setArea(createCombatArea(AREA_CIRCLE2X2)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_FIRE, DAMAGELIST_VARYING_PERIOD, 10, {8, 10}, 40) + end + return true +end diff --git a/data/spells/scripts/monster/lizard_magistratus_curse.lua b/data/spells/scripts/monster/lizard_magistratus_curse.lua new file mode 100644 index 0000000..b716c87 --- /dev/null +++ b/data/spells/scripts/monster/lizard_magistratus_curse.lua @@ -0,0 +1,12 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DEATHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SMALLCLOUDS) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_DEATH) + +function onCastSpell(creature, variant) + local damage = math.random(74, 107) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_CURSED, DAMAGELIST_EXPONENTIAL_DAMAGE, damage) + end + return true +end diff --git a/data/spells/scripts/monster/magma_crawler_soulfire.lua b/data/spells/scripts/monster/magma_crawler_soulfire.lua new file mode 100644 index 0000000..99248fa --- /dev/null +++ b/data/spells/scripts/monster/magma_crawler_soulfire.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_EXPLOSIONHIT) +combat:setArea(createCombatArea(AREA_CIRCLE2X2)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_FIRE, DAMAGELIST_VARYING_PERIOD, 10, {8, 10}, 20) + end + return true +end diff --git a/data/spells/scripts/monster/massive_energy_elemental_electrify.lua b/data/spells/scripts/monster/massive_energy_elemental_electrify.lua new file mode 100644 index 0000000..1605e1c --- /dev/null +++ b/data/spells/scripts/monster/massive_energy_elemental_electrify.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_BLOCKHIT) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_ENERGY, DAMAGELIST_VARYING_PERIOD, 25, {10, 12}, 10) + end + return true +end diff --git a/data/spells/scripts/monster/massive_fire_elemental_soulfire.lua b/data/spells/scripts/monster/massive_fire_elemental_soulfire.lua new file mode 100644 index 0000000..27b9bed --- /dev/null +++ b/data/spells/scripts/monster/massive_fire_elemental_soulfire.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_RED) +combat:setArea(createCombatArea(AREA_CIRCLE6X6)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_FIRE, DAMAGELIST_VARYING_PERIOD, 10, {8, 10}, 20) + end + return true +end diff --git a/data/spells/scripts/monster/monster_soulfire.lua b/data/spells/scripts/monster/monster_soulfire.lua new file mode 100644 index 0000000..d9299d8 --- /dev/null +++ b/data/spells/scripts/monster/monster_soulfire.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_FIRE) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_FIRE, DAMAGELIST_VARYING_PERIOD, 10, {8, 10}, 20) + end + return true +end diff --git a/data/spells/scripts/monster/mutated_bat_curse.lua b/data/spells/scripts/monster/mutated_bat_curse.lua new file mode 100644 index 0000000..07e0b2a --- /dev/null +++ b/data/spells/scripts/monster/mutated_bat_curse.lua @@ -0,0 +1,13 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DEATHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SMALLCLOUDS) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_DEATH) +combat:setArea(createCombatArea(AREA_SQUAREWAVE7)) + +function onCastSpell(creature, variant) + local damage = math.random(10, 21) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_CURSED, DAMAGELIST_EXPONENTIAL_DAMAGE, damage) + end + return true +end diff --git a/data/spells/scripts/monster/phantasm_drown.lua b/data/spells/scripts/monster/phantasm_drown.lua new file mode 100644 index 0000000..8febf4b --- /dev/null +++ b/data/spells/scripts/monster/phantasm_drown.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DROWNDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setArea(createCombatArea(AREA_SQUAREWAVE7)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_DROWN, DAMAGELIST_CONSTANT_PERIOD, 20, 5, 20) + end + return true +end diff --git a/data/spells/scripts/monster/pirate_corsair_skill_reducer.lua b/data/spells/scripts/monster/pirate_corsair_skill_reducer.lua new file mode 100644 index 0000000..a174cc9 --- /dev/null +++ b/data/spells/scripts/monster/pirate_corsair_skill_reducer.lua @@ -0,0 +1,15 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SOUND_PURPLE) +combat:setArea(createCombatArea(AREA_BEAM1)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 4 * 1000}, + {key = CONDITION_PARAM_SKILL_MELEEPERCENT, value = 25} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/quara_constrictor_electrify.lua b/data/spells/scripts/monster/quara_constrictor_electrify.lua new file mode 100644 index 0000000..8fa49f4 --- /dev/null +++ b/data/spells/scripts/monster/quara_constrictor_electrify.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_ENERGY, DAMAGELIST_VARYING_PERIOD, 25, {10, 12}, 3) + end + return true +end diff --git a/data/spells/scripts/monster/quara_constrictor_freeze.lua b/data/spells/scripts/monster/quara_constrictor_freeze.lua new file mode 100644 index 0000000..702fcd6 --- /dev/null +++ b/data/spells/scripts/monster/quara_constrictor_freeze.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_GREEN_RINGS) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_FREEZING, DAMAGELIST_VARYING_PERIOD, 8, {8, 10}, 10) + end + return true +end diff --git a/data/spells/scripts/monster/sea_serpent_drown.lua b/data/spells/scripts/monster/sea_serpent_drown.lua new file mode 100644 index 0000000..b98396e --- /dev/null +++ b/data/spells/scripts/monster/sea_serpent_drown.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DROWNDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_WATERSPLASH) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_DROWN, DAMAGELIST_CONSTANT_PERIOD, 20, 5, 12) + end + return true +end diff --git a/data/spells/scripts/monster/shock_head_skill_reducer_1.lua b/data/spells/scripts/monster/shock_head_skill_reducer_1.lua new file mode 100644 index 0000000..4f5adcf --- /dev/null +++ b/data/spells/scripts/monster/shock_head_skill_reducer_1.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_GROUNDSHAKER) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_EXPLOSION) +combat:setArea(createCombatArea(AREA_CIRCLE2X2)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 6 * 1000}, + {key = CONDITION_PARAM_SKILL_SHIELDPERCENT, value = 65} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/shock_head_skill_reducer_2.lua b/data/spells/scripts/monster/shock_head_skill_reducer_2.lua new file mode 100644 index 0000000..88a457a --- /dev/null +++ b/data/spells/scripts/monster/shock_head_skill_reducer_2.lua @@ -0,0 +1,15 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_STUN) +combat:setArea(createCombatArea(AREA_CIRCLE6X6)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 4 * 1000}, + {key = CONDITION_PARAM_STAT_MAGICPOINTSPERCENT, value = 70} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/silencer_skill_reducer.lua b/data/spells/scripts/monster/silencer_skill_reducer.lua new file mode 100644 index 0000000..7173abc --- /dev/null +++ b/data/spells/scripts/monster/silencer_skill_reducer.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ENERGY) +combat:setArea(createCombatArea(AREA_CIRCLE2X2)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 6 * 1000}, + {key = CONDITION_PARAM_STAT_MAGICPOINTSPERCENT, value = nil} +} + +function onCastSpell(creature, variant) + parameters[2].value = math.random(20, 70) + + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/souleater_drown.lua b/data/spells/scripts/monster/souleater_drown.lua new file mode 100644 index 0000000..0f64d97 --- /dev/null +++ b/data/spells/scripts/monster/souleater_drown.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DROWNDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MORTAREA) +combat:setArea(createCombatArea(AREA_CIRCLE2X2)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_DROWN, DAMAGELIST_CONSTANT_PERIOD, 20, 5, 10) + end + return true +end diff --git a/data/spells/scripts/monster/spectre_drown.lua b/data/spells/scripts/monster/spectre_drown.lua new file mode 100644 index 0000000..c305b93 --- /dev/null +++ b/data/spells/scripts/monster/spectre_drown.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DROWNDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_GREEN) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_DROWN, DAMAGELIST_CONSTANT_PERIOD, 20, 5, 20) + end + return true +end diff --git a/data/spells/scripts/monster/stampor_skill_reducer.lua b/data/spells/scripts/monster/stampor_skill_reducer.lua new file mode 100644 index 0000000..ac80b1e --- /dev/null +++ b/data/spells/scripts/monster/stampor_skill_reducer.lua @@ -0,0 +1,20 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SMALLPLANTS) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_SMALLEARTH) +combat:setArea(createCombatArea(AREA_BEAM1)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 4 * 1000}, + {key = CONDITION_PARAM_SKILL_SHIELDPERCENT, value = nil}, + {key = CONDITION_PARAM_SKILL_MELEEPERCENT, value = nil} +} + +function onCastSpell(creature, variant) + parameters[2].value = math.random(60, 85) + parameters[3].value = parameters[2].value + + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/undead_dragon_curse.lua b/data/spells/scripts/monster/undead_dragon_curse.lua new file mode 100644 index 0000000..970ff42 --- /dev/null +++ b/data/spells/scripts/monster/undead_dragon_curse.lua @@ -0,0 +1,13 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DEATHDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_SMALLCLOUDS) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_DEATH) +combat:setArea(createCombatArea(AREA_SQUAREWAVE7)) + +function onCastSpell(creature, variant) + local damage = math.random(154, 266) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_CURSED, DAMAGELIST_EXPONENTIAL_DAMAGE, damage) + end + return true +end diff --git a/data/spells/scripts/monster/vulcongra_soulfire.lua b/data/spells/scripts/monster/vulcongra_soulfire.lua new file mode 100644 index 0000000..b01ff32 --- /dev/null +++ b/data/spells/scripts/monster/vulcongra_soulfire.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITBYFIRE) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_FIRE, DAMAGELIST_VARYING_PERIOD, 10, {8, 10}, 20) + end + return true +end diff --git a/data/spells/scripts/monster/war_golem_electrify.lua b/data/spells/scripts/monster/war_golem_electrify.lua new file mode 100644 index 0000000..8f895bf --- /dev/null +++ b/data/spells/scripts/monster/war_golem_electrify.lua @@ -0,0 +1,10 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ENERGYHIT) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_ENERGY, DAMAGELIST_VARYING_PERIOD, 25, {10, 12}, 8) + end + return true +end diff --git a/data/spells/scripts/monster/war_golem_skill_reducer.lua b/data/spells/scripts/monster/war_golem_skill_reducer.lua new file mode 100644 index 0000000..c7a0997 --- /dev/null +++ b/data/spells/scripts/monster/war_golem_skill_reducer.lua @@ -0,0 +1,15 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_STUN) +combat:setArea(createCombatArea(AREA_BEAM8)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 3 * 1000}, + {key = CONDITION_PARAM_SKILL_SHIELDPERCENT, value = 70} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/warlock_skill_reducer.lua b/data/spells/scripts/monster/warlock_skill_reducer.lua new file mode 100644 index 0000000..874a278 --- /dev/null +++ b/data/spells/scripts/monster/warlock_skill_reducer.lua @@ -0,0 +1,17 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_ICEAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_ICE) +combat:setArea(createCombatArea(AREA_BEAM1)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 5 * 1000}, + {key = CONDITION_PARAM_SKILL_SHIELDPERCENT, value = 50}, + {key = CONDITION_PARAM_SKILL_MELEEPERCENT, value = 50} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/werewolf_skill_reducer.lua b/data/spells/scripts/monster/werewolf_skill_reducer.lua new file mode 100644 index 0000000..c8609d2 --- /dev/null +++ b/data/spells/scripts/monster/werewolf_skill_reducer.lua @@ -0,0 +1,16 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_DRAWBLOOD) +combat:setArea(createCombatArea(AREA_BEAM1)) + +local parameters = { + {key = CONDITION_PARAM_TICKS, value = 4 * 1000}, + {key = CONDITION_PARAM_SKILL_SHIELDPERCENT, value = 65}, + {key = CONDITION_PARAM_SKILL_MELEEPERCENT, value = 65} +} + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + target:addAttributeCondition(parameters) + end + return true +end diff --git a/data/spells/scripts/monster/young_sea_serpent_drown.lua b/data/spells/scripts/monster/young_sea_serpent_drown.lua new file mode 100644 index 0000000..855d1f4 --- /dev/null +++ b/data/spells/scripts/monster/young_sea_serpent_drown.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_DROWNDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_WATERSPLASH) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) + +function onCastSpell(creature, variant) + for _, target in ipairs(combat:getTargets(creature, variant)) do + creature:addDamageCondition(target, CONDITION_DROWN, DAMAGELIST_CONSTANT_PERIOD, 20, 5, 6) + end + return true +end diff --git a/data/spells/scripts/party/enchant_party.lua b/data/spells/scripts/party/enchant_party.lua new file mode 100644 index 0000000..eeb060c --- /dev/null +++ b/data/spells/scripts/party/enchant_party.lua @@ -0,0 +1,12 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +local condition = Condition(CONDITION_ATTRIBUTES) +condition:setParameter(CONDITION_PARAM_TICKS, 2 * 60 * 1000) +condition:setParameter(CONDITION_PARAM_STAT_MAGICPOINTS, 1) +condition:setParameter(CONDITION_PARAM_BUFF_SPELL, true) + +function onCastSpell(creature, variant) + return creature:addPartyCondition(combat, variant, condition, 120) +end diff --git a/data/spells/scripts/party/heal_party.lua b/data/spells/scripts/party/heal_party.lua new file mode 100644 index 0000000..b5634a4 --- /dev/null +++ b/data/spells/scripts/party/heal_party.lua @@ -0,0 +1,13 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +local condition = Condition(CONDITION_REGENERATION) +condition:setParameter(CONDITION_PARAM_TICKS, 2 * 60 * 1000) +condition:setParameter(CONDITION_PARAM_HEALTHGAIN, 20) +condition:setParameter(CONDITION_PARAM_HEALTHTICKS, 2000) +condition:setParameter(CONDITION_PARAM_BUFF_SPELL, true) + +function onCastSpell(creature, variant) + return creature:addPartyCondition(combat, variant, condition, 120) +end diff --git a/data/spells/scripts/party/protect_party.lua b/data/spells/scripts/party/protect_party.lua new file mode 100644 index 0000000..a347e01 --- /dev/null +++ b/data/spells/scripts/party/protect_party.lua @@ -0,0 +1,12 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +local condition = Condition(CONDITION_ATTRIBUTES) +condition:setParameter(CONDITION_PARAM_TICKS, 2 * 60 * 1000) +condition:setParameter(CONDITION_PARAM_SKILL_SHIELD, 3) +condition:setParameter(CONDITION_PARAM_BUFF_SPELL, true) + +function onCastSpell(creature, variant) + return creature:addPartyCondition(combat, variant, condition, 90) +end diff --git a/data/spells/scripts/party/train_party.lua b/data/spells/scripts/party/train_party.lua new file mode 100644 index 0000000..5acb0cc --- /dev/null +++ b/data/spells/scripts/party/train_party.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +local condition = Condition(CONDITION_ATTRIBUTES) +condition:setParameter(CONDITION_PARAM_TICKS, 2 * 60 * 1000) +condition:setParameter(CONDITION_PARAM_SKILL_FIST, 3) +condition:setParameter(CONDITION_PARAM_SKILL_MELEE, 3) +condition:setParameter(CONDITION_PARAM_SKILL_DISTANCE, 3) +condition:setParameter(CONDITION_PARAM_BUFF_SPELL, true) + +function onCastSpell(creature, variant) + return creature:addPartyCondition(combat, variant, condition, 60) +end diff --git a/data/spells/scripts/support/animate_dead_rune.lua b/data/spells/scripts/support/animate_dead_rune.lua new file mode 100644 index 0000000..675cb71 --- /dev/null +++ b/data/spells/scripts/support/animate_dead_rune.lua @@ -0,0 +1,23 @@ +function onCastSpell(creature, variant, isHotkey) + local position = variant:getPosition() + local tile = Tile(position) + if tile and creature:getSkull() ~= SKULL_BLACK then + local corpse = tile:getTopDownItem() + if corpse then + local itemType = corpse:getType() + if itemType:isCorpse() and itemType:isMovable() then + local monster = Game.createMonster("Skeleton", position) + if monster then + corpse:remove() + creature:addSummon(monster) + position:sendMagicEffect(CONST_ME_MAGIC_BLUE) + return true + end + end + end + end + + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + return false +end diff --git a/data/spells/scripts/support/blood_rage.lua b/data/spells/scripts/support/blood_rage.lua new file mode 100644 index 0000000..139a0cf --- /dev/null +++ b/data/spells/scripts/support/blood_rage.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_GREEN) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local condition = Condition(CONDITION_ATTRIBUTES) +condition:setParameter(CONDITION_PARAM_TICKS, 10000) +condition:setParameter(CONDITION_PARAM_SKILL_MELEEPERCENT, 135) +condition:setParameter(CONDITION_PARAM_DISABLE_DEFENSE, true) +condition:setParameter(CONDITION_PARAM_BUFF_SPELL, true) +combat:addCondition(condition) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/cancel_invisibility.lua b/data/spells/scripts/support/cancel_invisibility.lua new file mode 100644 index 0000000..1137dce --- /dev/null +++ b/data/spells/scripts/support/cancel_invisibility.lua @@ -0,0 +1,8 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_INVISIBLE) +combat:setArea(createCombatArea(AREA_CIRCLE3X3)) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/challenge.lua b/data/spells/scripts/support/challenge.lua new file mode 100644 index 0000000..348a3a7 --- /dev/null +++ b/data/spells/scripts/support/challenge.lua @@ -0,0 +1,13 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setArea(createCombatArea(AREA_SQUARE1X1)) + +function onTargetCreature(creature, target) + return doChallengeCreature(creature, target) +end + +combat:setCallback(CALLBACK_PARAM_TARGETCREATURE, "onTargetCreature") + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/chameleon.lua b/data/spells/scripts/support/chameleon.lua new file mode 100644 index 0000000..e157d1a --- /dev/null +++ b/data/spells/scripts/support/chameleon.lua @@ -0,0 +1,27 @@ +local condition = Condition(CONDITION_OUTFIT) +condition:setTicks(200000) + +function onCastSpell(creature, variant, isHotkey) + local position, item = variant:getPosition() + if position.x == CONTAINER_POSITION then + local container = creature:getContainerById(position.y - 64) + if container then + item = container:getItem(position.z) + else + item = creature:getSlotItem(position.y) + end + else + item = Tile(position):getTopDownItem() + end + + if not item or item.itemid == 0 or not isMoveable(item.uid) then + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + condition:setOutfit({lookTypeEx = item.itemid}) + creature:addCondition(condition) + creature:getPosition():sendMagicEffect(CONST_ME_MAGIC_RED) + return true +end diff --git a/data/spells/scripts/support/charge.lua b/data/spells/scripts/support/charge.lua new file mode 100644 index 0000000..32e9aba --- /dev/null +++ b/data/spells/scripts/support/charge.lua @@ -0,0 +1,12 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_GREEN) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local condition = Condition(CONDITION_HASTE) +condition:setParameter(CONDITION_PARAM_TICKS, 5000) +condition:setFormula(0.9, -72, 0.9, -72) +combat:addCondition(condition) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/convince_creature.lua b/data/spells/scripts/support/convince_creature.lua new file mode 100644 index 0000000..c466f57 --- /dev/null +++ b/data/spells/scripts/support/convince_creature.lua @@ -0,0 +1,36 @@ +function onCastSpell(creature, variant, isHotkey) + local target = Creature(variant:getNumber()) + if not target or not target:isMonster() then + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + local monsterType = target:getType() + if not creature:hasFlag(PlayerFlag_CanConvinceAll) then + if not monsterType:isConvinceable() then + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + if #creature:getSummons() >= 2 then + creature:sendCancelMessage("You cannot control more creatures.") + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + end + + local manaCost = target:getType():getManaCost() + if creature:getMana() < manaCost and not creature:hasFlag(PlayerFlag_HasInfiniteMana) then + creature:sendCancelMessage(RETURNVALUE_NOTENOUGHMANA) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + creature:addMana(-manaCost) + creature:addManaSpent(manaCost) + creature:addSummon(target) + creature:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + return true +end diff --git a/data/spells/scripts/support/creature_illusion.lua b/data/spells/scripts/support/creature_illusion.lua new file mode 100644 index 0000000..848cfc7 --- /dev/null +++ b/data/spells/scripts/support/creature_illusion.lua @@ -0,0 +1,23 @@ +local condition = Condition(CONDITION_OUTFIT) +condition:setTicks(180000) + +function onCastSpell(creature, variant) + local returnValue = RETURNVALUE_NOERROR + local monsterType = MonsterType(variant:getString()) + if not monsterType then + returnValue = RETURNVALUE_CREATUREDOESNOTEXIST + elseif not creature:hasFlag(PlayerFlag_CanIllusionAll) and not monsterType:isIllusionable() then + returnValue = RETURNVALUE_NOTPOSSIBLE + end + + if returnValue ~= RETURNVALUE_NOERROR then + creature:sendCancelMessage(returnValue) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + condition:setOutfit(monsterType:getOutfit()) + creature:addCondition(condition) + creature:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + return true +end diff --git a/data/spells/scripts/support/destroy_field_rune.lua b/data/spells/scripts/support/destroy_field_rune.lua new file mode 100644 index 0000000..4d8374f --- /dev/null +++ b/data/spells/scripts/support/destroy_field_rune.lua @@ -0,0 +1,14 @@ +function onCastSpell(creature, variant, isHotkey) + local position = variant:getPosition() + local tile = Tile(position) + local field = tile and tile:getItemByType(ITEM_TYPE_MAGICFIELD) + if field and table.contains(FIELDS, field:getId()) then + field:remove() + position:sendMagicEffect(CONST_ME_POFF) + return true + end + + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false +end diff --git a/data/spells/scripts/support/disintegrate_rune.lua b/data/spells/scripts/support/disintegrate_rune.lua new file mode 100644 index 0000000..e54ae11 --- /dev/null +++ b/data/spells/scripts/support/disintegrate_rune.lua @@ -0,0 +1,24 @@ +local corpseIds = {3058, 3059, 3060, 3061, 3064, 3065, 3066} +local removalLimit = 500 + +function onCastSpell(creature, variant, isHotkey) + local position = variant:getPosition() + local tile = Tile(position) + if tile then + local items = tile:getItems() + if items then + for i, item in ipairs(items) do + if item:getType():isMovable() and item:getUniqueId() > 65535 and item:getActionId() == 0 and not table.contains(corpseIds, item:getId()) then + item:remove() + end + + if i == removalLimit then + break + end + end + end + end + + position:sendMagicEffect(CONST_ME_POFF) + return true +end diff --git a/data/spells/scripts/support/find_person.lua b/data/spells/scripts/support/find_person.lua new file mode 100644 index 0000000..f2984c0 --- /dev/null +++ b/data/spells/scripts/support/find_person.lua @@ -0,0 +1,74 @@ +local LEVEL_LOWER = 1 +local LEVEL_SAME = 2 +local LEVEL_HIGHER = 3 + +local DISTANCE_BESIDE = 1 +local DISTANCE_CLOSE = 2 +local DISTANCE_FAR = 3 +local DISTANCE_VERYFAR = 4 + +local directions = { + [DIRECTION_NORTH] = "north", + [DIRECTION_SOUTH] = "south", + [DIRECTION_EAST] = "east", + [DIRECTION_WEST] = "west", + [DIRECTION_NORTHEAST] = "north-east", + [DIRECTION_NORTHWEST] = "north-west", + [DIRECTION_SOUTHEAST] = "south-east", + [DIRECTION_SOUTHWEST] = "south-west" +} + +local descriptions = { + [DISTANCE_BESIDE] = { + [LEVEL_LOWER] = "is below you", + [LEVEL_SAME] = "is standing next to you", + [LEVEL_HIGHER] = "is above you" + }, + [DISTANCE_CLOSE] = { + [LEVEL_LOWER] = "is on a lower level to the", + [LEVEL_SAME] = "is to the", + [LEVEL_HIGHER] = "is on a higher level to the" + }, + [DISTANCE_FAR] = "is far to the", + [DISTANCE_VERYFAR] = "is very far to the" +} + +function onCastSpell(creature, variant) + local target = Player(variant:getString()) + if not target or target:getGroup():getAccess() and not creature:getGroup():getAccess() then + creature:sendCancelMessage(RETURNVALUE_PLAYERWITHTHISNAMEISNOTONLINE) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + local targetPosition = target:getPosition() + local creaturePosition = creature:getPosition() + local positionDifference = { + x = creaturePosition.x - targetPosition.x, + y = creaturePosition.y - targetPosition.y, + z = creaturePosition.z - targetPosition.z + } + + local maxPositionDifference, direction = math.max(math.abs(positionDifference.x), math.abs(positionDifference.y)) + if maxPositionDifference >= 5 then + local positionTangent = positionDifference.x ~= 0 and positionDifference.y / positionDifference.x or 10 + if math.abs(positionTangent) < 0.4142 then + direction = positionDifference.x > 0 and DIRECTION_WEST or DIRECTION_EAST + elseif math.abs(positionTangent) < 2.4142 then + direction = positionTangent > 0 and (positionDifference.y > 0 and DIRECTION_NORTHWEST or DIRECTION_SOUTHEAST) or positionDifference.x > 0 and DIRECTION_SOUTHWEST or DIRECTION_NORTHEAST + else + direction = positionDifference.y > 0 and DIRECTION_NORTH or DIRECTION_SOUTH + end + end + + local level = positionDifference.z > 0 and LEVEL_HIGHER or positionDifference.z < 0 and LEVEL_LOWER or LEVEL_SAME + local distance = maxPositionDifference < 5 and DISTANCE_BESIDE or maxPositionDifference < 101 and DISTANCE_CLOSE or maxPositionDifference < 275 and DISTANCE_FAR or DISTANCE_VERYFAR + local description = descriptions[distance][level] or descriptions[distance] + if distance ~= DISTANCE_BESIDE then + description = description .. " " .. directions[direction] + end + + creature:sendTextMessage(MESSAGE_INFO_DESCR, target:getName() .. " " .. description .. ".") + creaturePosition:sendMagicEffect(CONST_ME_MAGIC_BLUE) + return true +end diff --git a/data/spells/scripts/support/food.lua b/data/spells/scripts/support/food.lua new file mode 100644 index 0000000..b1c81d0 --- /dev/null +++ b/data/spells/scripts/support/food.lua @@ -0,0 +1,19 @@ +local foods = { + 2666, -- meat + 2671, -- ham + 2681, -- grape + 2674, -- apple + 2689, -- bread + 2690, -- roll + 2696 -- cheese +} + +function onCastSpell(creature, variant) + if math.random(0, 1) == 1 then + creature:addItem(foods[math.random(#foods)]) + end + + creature:addItem(foods[math.random(#foods)]) + creature:getPosition():sendMagicEffect(CONST_ME_MAGIC_GREEN) + return true +end diff --git a/data/spells/scripts/support/great_light.lua b/data/spells/scripts/support/great_light.lua new file mode 100644 index 0000000..766ba6b --- /dev/null +++ b/data/spells/scripts/support/great_light.lua @@ -0,0 +1,13 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local condition = Condition(CONDITION_LIGHT) +condition:setParameter(CONDITION_PARAM_LIGHT_LEVEL, 8) +condition:setParameter(CONDITION_PARAM_LIGHT_COLOR, 215) +condition:setParameter(CONDITION_PARAM_TICKS, (11 * 60 + 35) * 1000) +combat:addCondition(condition) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/haste.lua b/data/spells/scripts/support/haste.lua new file mode 100644 index 0000000..645795a --- /dev/null +++ b/data/spells/scripts/support/haste.lua @@ -0,0 +1,12 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_GREEN) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local condition = Condition(CONDITION_HASTE) +condition:setParameter(CONDITION_PARAM_TICKS, 33000) +condition:setFormula(0.3, -24, 0.3, -24) +combat:addCondition(condition) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/invisible.lua b/data/spells/scripts/support/invisible.lua new file mode 100644 index 0000000..e6a9557 --- /dev/null +++ b/data/spells/scripts/support/invisible.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local condition = Condition(CONDITION_INVISIBLE) +condition:setParameter(CONDITION_PARAM_TICKS, 200000) +combat:addCondition(condition) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/levitate.lua b/data/spells/scripts/support/levitate.lua new file mode 100644 index 0000000..6cf401f --- /dev/null +++ b/data/spells/scripts/support/levitate.lua @@ -0,0 +1,30 @@ +local function levitate(creature, parameter) + local fromPosition = creature:getPosition() + + if parameter == "up" and fromPosition.z ~= 8 or parameter == "down" and fromPosition.z ~= 7 then + local toPosition = creature:getPosition() + toPosition:getNextPosition(creature:getDirection()) + + local tile = Tile(parameter == "up" and Position(fromPosition.x, fromPosition.y, fromPosition.z - 1) or toPosition) + if not tile or not tile:getGround() and not tile:hasFlag(parameter == "up" and TILESTATE_IMMOVABLEBLOCKSOLID or TILESTATE_BLOCKSOLID) then + tile = Tile(toPosition.x, toPosition.y, toPosition.z + (parameter == "up" and -1 or 1)) + + if tile and tile:getGround() and not tile:hasFlag(bit.bor(TILESTATE_IMMOVABLEBLOCKSOLID, TILESTATE_FLOORCHANGE)) then + return creature:move(tile, bit.bor(FLAG_IGNOREBLOCKITEM, FLAG_IGNOREBLOCKCREATURE)) + end + end + end + return RETURNVALUE_NOTPOSSIBLE +end + +function onCastSpell(creature, variant) + local returnValue = levitate(creature, variant:getString()) + if returnValue ~= RETURNVALUE_NOERROR then + creature:sendCancelMessage(returnValue) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + creature:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return true +end diff --git a/data/spells/scripts/support/light.lua b/data/spells/scripts/support/light.lua new file mode 100644 index 0000000..c69728d --- /dev/null +++ b/data/spells/scripts/support/light.lua @@ -0,0 +1,13 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local condition = Condition(CONDITION_LIGHT) +condition:setParameter(CONDITION_PARAM_LIGHT_LEVEL, 6) +condition:setParameter(CONDITION_PARAM_LIGHT_COLOR, 215) +condition:setParameter(CONDITION_PARAM_TICKS, (6 * 60 + 10) * 1000) +combat:addCondition(condition) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/magic_rope.lua b/data/spells/scripts/support/magic_rope.lua new file mode 100644 index 0000000..92f0acb --- /dev/null +++ b/data/spells/scripts/support/magic_rope.lua @@ -0,0 +1,20 @@ +function onCastSpell(creature, variant) + local position = creature:getPosition() + position:sendMagicEffect(CONST_ME_POFF) + + local tile = Tile(position) + if not table.contains(ropeSpots, tile:getGround():getId()) and not tile:getItemById(14435) then + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + return false + end + + tile = Tile(position:moveUpstairs()) + if not tile then + creature:sendCancelMessage(RETURNVALUE_NOTENOUGHROOM) + return false + end + + creature:teleportTo(position, false) + position:sendMagicEffect(CONST_ME_TELEPORT) + return true +end diff --git a/data/spells/scripts/support/magic_shield.lua b/data/spells/scripts/support/magic_shield.lua new file mode 100644 index 0000000..e762a2d --- /dev/null +++ b/data/spells/scripts/support/magic_shield.lua @@ -0,0 +1,11 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local condition = Condition(CONDITION_MANASHIELD) +condition:setParameter(CONDITION_PARAM_TICKS, 200000) +combat:addCondition(condition) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/protector.lua b/data/spells/scripts/support/protector.lua new file mode 100644 index 0000000..09eea9e --- /dev/null +++ b/data/spells/scripts/support/protector.lua @@ -0,0 +1,22 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_GREEN) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local skill = Condition(CONDITION_ATTRIBUTES) +skill:setParameter(CONDITION_PARAM_TICKS, 13000) +skill:setParameter(CONDITION_PARAM_SKILL_SHIELDPERCENT, 220) +skill:setParameter(CONDITION_PARAM_BUFF_SPELL, true) +combat:addCondition(skill) + +local cooldownAttackGroup = Condition(CONDITION_SPELLGROUPCOOLDOWN) +cooldownAttackGroup:setParameter(CONDITION_PARAM_SUBID, 1) +cooldownAttackGroup:setParameter(CONDITION_PARAM_TICKS, 10000) +combat:addCondition(cooldownAttackGroup) + +local pacified = Condition(CONDITION_PACIFIED) +pacified:setParameter(CONDITION_PARAM_TICKS, 10000) +combat:addCondition(pacified) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/sharpshooter.lua b/data/spells/scripts/support/sharpshooter.lua new file mode 100644 index 0000000..ce05f5e --- /dev/null +++ b/data/spells/scripts/support/sharpshooter.lua @@ -0,0 +1,19 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_GREEN) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local skill = Condition(CONDITION_ATTRIBUTES) +skill:setParameter(CONDITION_PARAM_TICKS, 10000) +skill:setParameter(CONDITION_PARAM_SKILL_DISTANCEPERCENT, 150) +skill:setParameter(CONDITION_PARAM_DISABLE_DEFENSE, true) +skill:setParameter(CONDITION_PARAM_BUFF_SPELL, true) +combat:addCondition(skill) + +local speed = Condition(CONDITION_PARALYZE) +speed:setParameter(CONDITION_PARAM_TICKS, 10000) +speed:setFormula(-0.7, 56, -0.7, 56) +combat:addCondition(speed) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/strong_haste.lua b/data/spells/scripts/support/strong_haste.lua new file mode 100644 index 0000000..48e0dbb --- /dev/null +++ b/data/spells/scripts/support/strong_haste.lua @@ -0,0 +1,12 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_GREEN) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local condition = Condition(CONDITION_HASTE) +condition:setParameter(CONDITION_PARAM_TICKS, 22000) +condition:setFormula(0.7, -56, 0.7, -56) +combat:addCondition(condition) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/summon_creature.lua b/data/spells/scripts/support/summon_creature.lua new file mode 100644 index 0000000..85ffd50 --- /dev/null +++ b/data/spells/scripts/support/summon_creature.lua @@ -0,0 +1,51 @@ +function onCastSpell(creature, variant) + if creature:getSkull() == SKULL_BLACK then + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + return false + end + + local monsterName = variant:getString() + local monsterType = MonsterType(monsterName) + + if not monsterType then + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + if not creature:hasFlag(PlayerFlag_CanSummonAll) then + if not monsterType:isSummonable() then + creature:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + if #creature:getSummons() >= 2 then + creature:sendCancelMessage("You cannot summon more creatures.") + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + end + + local manaCost = monsterType:getManaCost() + if creature:getMana() < manaCost and not creature:hasFlag(PlayerFlag_HasInfiniteMana) then + creature:sendCancelMessage(RETURNVALUE_NOTENOUGHMANA) + creature:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end + + local position = creature:getPosition() + local summon = Game.createMonster(monsterName, position, true) + if not summon then + creature:sendCancelMessage(RETURNVALUE_NOTENOUGHROOM) + position:sendMagicEffect(CONST_ME_POFF) + return false + end + + creature:addMana(-manaCost) + creature:addManaSpent(manaCost) + creature:addSummon(summon) + position:sendMagicEffect(CONST_ME_MAGIC_BLUE) + summon:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + return true +end diff --git a/data/spells/scripts/support/swift_foot.lua b/data/spells/scripts/support/swift_foot.lua new file mode 100644 index 0000000..85640f8 --- /dev/null +++ b/data/spells/scripts/support/swift_foot.lua @@ -0,0 +1,21 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_GREEN) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local speed = Condition(CONDITION_HASTE) +speed:setParameter(CONDITION_PARAM_TICKS, 10000) +speed:setFormula(0.8, -64, 0.8, -64) +combat:addCondition(speed) + +local cooldownAttackGroup = Condition(CONDITION_SPELLGROUPCOOLDOWN) +cooldownAttackGroup:setParameter(CONDITION_PARAM_SUBID, 1) +cooldownAttackGroup:setParameter(CONDITION_PARAM_TICKS, 10000) +combat:addCondition(cooldownAttackGroup) + +local pacified = Condition(CONDITION_PACIFIED) +pacified:setParameter(CONDITION_PARAM_TICKS, 10000) +combat:addCondition(pacified) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/scripts/support/ultimate_light.lua b/data/spells/scripts/support/ultimate_light.lua new file mode 100644 index 0000000..d5611d3 --- /dev/null +++ b/data/spells/scripts/support/ultimate_light.lua @@ -0,0 +1,13 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_MAGIC_BLUE) +combat:setParameter(COMBAT_PARAM_AGGRESSIVE, false) + +local condition = Condition(CONDITION_LIGHT) +condition:setParameter(CONDITION_PARAM_LIGHT_LEVEL, 8) +condition:setParameter(CONDITION_PARAM_LIGHT_COLOR, 215) +condition:setParameter(CONDITION_PARAM_TICKS, (60 * 33 + 10) * 1000) +combat:addCondition(condition) + +function onCastSpell(creature, variant) + return combat:execute(creature, variant) +end diff --git a/data/spells/spells.xml b/data/spells/spells.xml new file mode 100644 index 0000000..f997ecd --- /dev/null +++ b/data/spells/spells.xml @@ -0,0 +1,804 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/talkactions/lib/talkactions.lua b/data/talkactions/lib/talkactions.lua new file mode 100644 index 0000000..c962dd4 --- /dev/null +++ b/data/talkactions/lib/talkactions.lua @@ -0,0 +1,12 @@ +local logFormat = "[%s] %s %s" + +function logCommand(player, words, param) + local file = io.open("data/logs/" .. player:getName() .. " commands.log", "a") + if not file then + return + end + + io.output(file) + io.write(logFormat:format(os.date("%d/%m/%Y %H:%M"), words, param):trim() .. "\n") + io.close(file) +end diff --git a/data/talkactions/scripts/add_skill.lua b/data/talkactions/scripts/add_skill.lua new file mode 100644 index 0000000..daca4aa --- /dev/null +++ b/data/talkactions/scripts/add_skill.lua @@ -0,0 +1,62 @@ +local function getSkillId(skillName) + if skillName == "club" then + return SKILL_CLUB + elseif skillName == "sword" then + return SKILL_SWORD + elseif skillName == "axe" then + return SKILL_AXE + elseif skillName:sub(1, 4) == "dist" then + return SKILL_DISTANCE + elseif skillName:sub(1, 6) == "shield" then + return SKILL_SHIELD + elseif skillName:sub(1, 4) == "fish" then + return SKILL_FISHING + else + return SKILL_FIST + end +end + +local function getExpForLevel(level) + level = level - 1 + return ((50 * level * level * level) - (150 * level * level) + (400 * level)) / 3 +end + +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + local split = param:splitTrimmed(",") + if not split[2] then + player:sendCancelMessage("Insufficient parameters.") + return false + end + + local target = Player(split[1]) + if not target then + player:sendCancelMessage("A player with that name is not online.") + return false + end + + local count = 1 + if split[3] then + count = tonumber(split[3]) + end + + local ch = split[2]:sub(1, 1) + for i = 1, count do + if ch == "l" or ch == "e" then + target:addExperience(getExpForLevel(target:getLevel() + 1) - target:getExperience(), false) + elseif ch == "m" then + target:addManaSpent(target:getVocation():getRequiredManaSpent(target:getBaseMagicLevel() + 1) - target:getManaSpent()) + else + local skillId = getSkillId(split[2]) + target:addSkillTries(skillId, target:getVocation():getRequiredSkillTries(skillId, target:getSkillLevel(skillId) + 1) - target:getSkillTries(skillId)) + end + end + return false +end diff --git a/data/talkactions/scripts/add_tutor.lua b/data/talkactions/scripts/add_tutor.lua new file mode 100644 index 0000000..c2dcde7 --- /dev/null +++ b/data/talkactions/scripts/add_tutor.lua @@ -0,0 +1,21 @@ +function onSay(player, words, param) + if player:getAccountType() <= ACCOUNT_TYPE_TUTOR then + return true + end + + local target = Player(param) + if not target then + player:sendCancelMessage("A player with that name is not online.") + return false + end + + if target:getAccountType() ~= ACCOUNT_TYPE_NORMAL then + player:sendCancelMessage("You can only promote a normal player to a tutor.") + return false + end + + target:setAccountType(ACCOUNT_TYPE_TUTOR) + target:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have been promoted to a tutor by " .. player:getName() .. ".") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have promoted " .. target:getName() .. " to a tutor.") + return false +end diff --git a/data/talkactions/scripts/animationeffect.lua b/data/talkactions/scripts/animationeffect.lua new file mode 100644 index 0000000..ba55558 --- /dev/null +++ b/data/talkactions/scripts/animationeffect.lua @@ -0,0 +1,29 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local effect = tonumber(param) + local position = player:getPosition() + local toPositionLow = {z = position.z} + local toPositionHigh = {z = position.z} + + toPositionLow.x = position.x - 7 + toPositionHigh.x = position.x + 7 + for i = -5, 5 do + toPositionLow.y = position.y + i + toPositionHigh.y = toPositionLow.y + position:sendDistanceEffect(toPositionLow, effect) + position:sendDistanceEffect(toPositionHigh, effect) + end + + toPositionLow.y = position.y - 5 + toPositionHigh.y = position.y + 5 + for i = -6, 6 do + toPositionLow.x = position.x + i + toPositionHigh.x = toPositionLow.x + position:sendDistanceEffect(toPositionLow, effect) + position:sendDistanceEffect(toPositionHigh, effect) + end + return false +end diff --git a/data/talkactions/scripts/ban.lua b/data/talkactions/scripts/ban.lua new file mode 100644 index 0000000..1b4283f --- /dev/null +++ b/data/talkactions/scripts/ban.lua @@ -0,0 +1,39 @@ +local banDays = 7 + +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local name = param + local reason = '' + + local separatorPos = param:find(',') + if separatorPos then + name = param:sub(0, separatorPos - 1) + reason = string.trim(param:sub(separatorPos + 1)) + end + + local accountId = getAccountNumberByPlayerName(name) + if accountId == 0 then + return false + end + + local resultId = db.storeQuery("SELECT 1 FROM `account_bans` WHERE `account_id` = " .. accountId) + if resultId ~= false then + result.free(resultId) + return false + end + + local timeNow = os.time() + db.query("INSERT INTO `account_bans` (`account_id`, `reason`, `banned_at`, `expires_at`, `banned_by`) VALUES (" .. + accountId .. ", " .. db.escapeString(reason) .. ", " .. timeNow .. ", " .. timeNow + (banDays * 86400) .. ", " .. player:getGuid() .. ")") + + local target = Player(name) + if target then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, target:getName() .. " has been banned.") + target:remove() + else + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, name .. " has been banned.") + end +end diff --git a/data/talkactions/scripts/broadcast.lua b/data/talkactions/scripts/broadcast.lua new file mode 100644 index 0000000..d2ba260 --- /dev/null +++ b/data/talkactions/scripts/broadcast.lua @@ -0,0 +1,11 @@ +function onSay(player, words, param) + if not player:hasFlag(PlayerFlag_CanBroadcast) then + return true + end + + print("> " .. player:getName() .. " broadcasted: \"" .. param .. "\".") + for _, targetPlayer in ipairs(Game.getPlayers()) do + targetPlayer:sendPrivateMessage(player, param, TALKTYPE_BROADCAST) + end + return false +end diff --git a/data/talkactions/scripts/buyhouse.lua b/data/talkactions/scripts/buyhouse.lua new file mode 100644 index 0000000..c934439 --- /dev/null +++ b/data/talkactions/scripts/buyhouse.lua @@ -0,0 +1,41 @@ +function onSay(player, words, param) + local housePrice = configManager.getNumber(configKeys.HOUSE_PRICE) + if housePrice == -1 then + return true + end + + if not player:isPremium() then + player:sendCancelMessage("You need a premium account.") + return false + end + + local position = player:getPosition() + position:getNextPosition(player:getDirection()) + + local tile = Tile(position) + local house = tile and tile:getHouse() + if not house then + player:sendCancelMessage("You have to be looking at the door of the house you would like to buy.") + return false + end + + if house:getOwnerGuid() > 0 then + player:sendCancelMessage("This house already has an owner.") + return false + end + + if player:getHouse() then + player:sendCancelMessage("You are already the owner of a house.") + return false + end + + local price = house:getTileCount() * housePrice + if not player:removeMoney(price) then + player:sendCancelMessage("You do not have enough money.") + return false + end + + house:setOwnerGuid(player:getGuid()) + player:sendTextMessage(MESSAGE_INFO_DESCR, "You have successfully bought this house, be sure to have the money for the rent in the bank.") + return false +end diff --git a/data/talkactions/scripts/buyprem.lua b/data/talkactions/scripts/buyprem.lua new file mode 100644 index 0000000..1aa4f5f --- /dev/null +++ b/data/talkactions/scripts/buyprem.lua @@ -0,0 +1,25 @@ +local config = { + days = 90, + maxDays = 365, + price = 10000 +} + +function onSay(player, words, param) + if configManager.getBoolean(configKeys.FREE_PREMIUM) then + return true + end + + if player:getPremiumDays() <= config.maxDays then + if player:removeMoney(config.price) then + player:addPremiumDays(config.days) + player:sendTextMessage(MESSAGE_INFO_DESCR, "You have bought " .. config.days .." days of premium account.") + else + player:sendCancelMessage("You don't have enough money, " .. config.maxDays .. " days premium account costs " .. config.price .. " gold coins.") + player:getPosition():sendMagicEffect(CONST_ME_POFF) + end + else + player:sendCancelMessage("You can not buy more than " .. config.maxDays .. " days of premium account.") + player:getPosition():sendMagicEffect(CONST_ME_POFF) + end + return false +end diff --git a/data/talkactions/scripts/castsystem.lua b/data/talkactions/scripts/castsystem.lua new file mode 100644 index 0000000..f541db5 --- /dev/null +++ b/data/talkactions/scripts/castsystem.lua @@ -0,0 +1,28 @@ +function onSay(player, words, param) + if words == "!cast" then + local t = param:split(",") + local status = t[1] + local password = t[2] + + if status == "on" or status == "start" or status == "begin" then + if password then + if player:startLiveCast(password) then + player:sendTextMessage(MESSAGE_INFO_DESCR, "You have started casting your gameplay.") + else + player:sendCancelMessage("You're already casting your gameplay.") + end + else + if player:startLiveCast() then + player:sendTextMessage(MESSAGE_INFO_DESCR, "You have started casting your gameplay.") + end + end + elseif status == "stop" or status == "end" or status == "close" or status == "off" then + if player:stopLiveCast() then + player:sendTextMessage(MESSAGE_INFO_DESCR, "You have stopped casting your gameplay.") + else + player:sendCancelMessage("You're not casting your gameplay.") + end + end + end + return false +end diff --git a/data/talkactions/scripts/chameleon.lua b/data/talkactions/scripts/chameleon.lua new file mode 100644 index 0000000..d3fcbe2 --- /dev/null +++ b/data/talkactions/scripts/chameleon.lua @@ -0,0 +1,25 @@ +local condition = Condition(CONDITION_OUTFIT, CONDITIONID_COMBAT) +condition:setTicks(-1) + +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + local itemType = ItemType(param) + if itemType:getId() == 0 then + itemType = ItemType(tonumber(param)) + if itemType:getId() == 0 then + player:sendCancelMessage("There is no item with that id or name.") + return false + end + end + + condition:setOutfit(itemType:getId()) + player:addCondition(condition) + return false +end diff --git a/data/talkactions/scripts/changesex.lua b/data/talkactions/scripts/changesex.lua new file mode 100644 index 0000000..3b71fe0 --- /dev/null +++ b/data/talkactions/scripts/changesex.lua @@ -0,0 +1,19 @@ +local premiumDaysCost = 3 + +function onSay(player, words, param) + if player:getGroup():getAccess() then + player:setSex(player:getSex() == PLAYERSEX_FEMALE and PLAYERSEX_MALE or PLAYERSEX_FEMALE) + player:sendTextMessage(MESSAGE_INFO_DESCR, "You have changed your sex.") + return false + end + + if player:getPremiumDays() >= premiumDaysCost then + player:removePremiumDays(premiumDaysCost) + player:setSex(player:getSex() == PLAYERSEX_FEMALE and PLAYERSEX_MALE or PLAYERSEX_FEMALE) + player:sendTextMessage(MESSAGE_INFO_DESCR, "You have changed your sex for " .. premiumDaysCost .. " days of your premium account.") + else + player:sendCancelMessage("You do not have enough premium days, changing sex costs " .. premiumDaysCost .. " days of your premium account.") + player:getPosition():sendMagicEffect(CONST_ME_POFF) + end + return false +end diff --git a/data/talkactions/scripts/clean.lua b/data/talkactions/scripts/clean.lua new file mode 100644 index 0000000..0d5f7aa --- /dev/null +++ b/data/talkactions/scripts/clean.lua @@ -0,0 +1,15 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + local itemCount = cleanMap() + if itemCount > 0 then + player:sendTextMessage(MESSAGE_STATUS_WARNING, "Cleaned " .. itemCount .. " item" .. (itemCount > 1 and "s" or "") .. " from the map.") + end + return false +end diff --git a/data/talkactions/scripts/closeserver.lua b/data/talkactions/scripts/closeserver.lua new file mode 100644 index 0000000..2f7c95e --- /dev/null +++ b/data/talkactions/scripts/closeserver.lua @@ -0,0 +1,17 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + if param == "shutdown" then + Game.setGameState(GAME_STATE_SHUTDOWN) + else + Game.setGameState(GAME_STATE_CLOSED) + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Server is now closed.") + end + return false +end diff --git a/data/talkactions/scripts/create_item.lua b/data/talkactions/scripts/create_item.lua new file mode 100644 index 0000000..4b9a39e --- /dev/null +++ b/data/talkactions/scripts/create_item.lua @@ -0,0 +1,60 @@ +local invalidIds = { + 1, 2, 3, 4, 5, 6, 7, 10, 11, 13, 14, 15, 19, 21, 26, 27, 28, 35, 43 +} + +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + local split = param:splitTrimmed(",") + + local itemType = ItemType(split[1]) + if itemType:getId() == 0 then + itemType = ItemType(tonumber(split[1])) + if not tonumber(split[1]) or itemType:getId() == 0 then + player:sendCancelMessage("There is no item with that id or name.") + return false + end + end + + if table.contains(invalidIds, itemType:getId()) then + return false + end + + local count = tonumber(split[2]) + if count then + if itemType:isStackable() then + count = math.min(10000, math.max(1, count)) + elseif not itemType:isFluidContainer() then + count = math.min(100, math.max(1, count)) + else + count = math.max(0, count) + end + else + if not itemType:isFluidContainer() then + count = 1 + else + count = 0 + end + end + + local result = player:addItem(itemType:getId(), count) + if result then + if not itemType:isStackable() then + if type(result) == "table" then + for _, item in ipairs(result) do + item:decay() + end + else + result:decay() + end + end + player:getPosition():sendMagicEffect(CONST_ME_MAGIC_GREEN) + end + return false +end diff --git a/data/talkactions/scripts/deathlist.lua b/data/talkactions/scripts/deathlist.lua new file mode 100644 index 0000000..3996b44 --- /dev/null +++ b/data/talkactions/scripts/deathlist.lua @@ -0,0 +1,62 @@ +local function getArticle(str) + return str:find("[AaEeIiOoUuYy]") == 1 and "an" or "a" +end + +local function getMonthDayEnding(day) + if day == "01" or day == "21" or day == "31" then + return "st" + elseif day == "02" or day == "22" then + return "nd" + elseif day == "03" or day == "23" then + return "rd" + else + return "th" + end +end + +local function getMonthString(m) + return os.date("%B", os.time{year = 1970, month = m, day = 1}) +end + +function onSay(player, words, param) + local resultId = db.storeQuery("SELECT `id`, `name` FROM `players` WHERE `name` = " .. db.escapeString(param)) + if resultId ~= false then + local targetGUID = result.getNumber(resultId, "id") + local targetName = result.getString(resultId, "name") + result.free(resultId) + local str = "" + local breakline = "" + + local resultId = db.storeQuery("SELECT `time`, `level`, `killed_by`, `is_player` FROM `player_deaths` WHERE `player_id` = " .. targetGUID .. " ORDER BY `time` DESC") + if resultId ~= false then + repeat + if str ~= "" then + breakline = "\n" + end + local date = os.date("*t", result.getNumber(resultId, "time")) + + local article = "" + local killed_by = result.getString(resultId, "killed_by") + if result.getNumber(resultId, "is_player") == 0 then + article = getArticle(killed_by) .. " " + killed_by = string.lower(killed_by) + end + + if date.day < 10 then date.day = "0" .. date.day end + if date.hour < 10 then date.hour = "0" .. date.hour end + if date.min < 10 then date.min = "0" .. date.min end + if date.sec < 10 then date.sec = "0" .. date.sec end + str = str .. breakline .. " " .. date.day .. getMonthDayEnding(date.day) .. " " .. getMonthString(date.month) .. " " .. date.year .. " " .. date.hour .. ":" .. date.min .. ":" .. date.sec .. " Died at Level " .. result.getNumber(resultId, "level") .. " by " .. article .. killed_by .. "." + until not result.next(resultId) + result.free(resultId) + end + + if str == "" then + str = "No deaths." + end + player:popupFYI("Deathlist for player, " .. targetName .. ".\n\n" .. str) + else + player:sendCancelMessage("A player with that name does not exist.") + end + return false +end diff --git a/data/talkactions/scripts/down.lua b/data/talkactions/scripts/down.lua new file mode 100644 index 0000000..460e0c4 --- /dev/null +++ b/data/talkactions/scripts/down.lua @@ -0,0 +1,10 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local position = player:getPosition() + position.z = position.z + 1 + player:teleportTo(position) + return false +end diff --git a/data/talkactions/scripts/force_raid.lua b/data/talkactions/scripts/force_raid.lua new file mode 100644 index 0000000..ebdf162 --- /dev/null +++ b/data/talkactions/scripts/force_raid.lua @@ -0,0 +1,20 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GAMEMASTER then + return false + end + + logCommand(player, words, param) + + local returnValue = Game.startRaid(param) + if returnValue ~= RETURNVALUE_NOERROR then + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, Game.getReturnMessage(returnValue)) + else + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Raid started.") + end + + return false +end diff --git a/data/talkactions/scripts/ghost.lua b/data/talkactions/scripts/ghost.lua new file mode 100644 index 0000000..fa1fe48 --- /dev/null +++ b/data/talkactions/scripts/ghost.lua @@ -0,0 +1,23 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + local position = player:getPosition() + local isGhost = not player:isInGhostMode() + + player:setGhostMode(isGhost) + if isGhost then + player:sendTextMessage(MESSAGE_INFO_DESCR, "You are now invisible.") + position:sendMagicEffect(CONST_ME_YALAHARIGHOST) + else + player:sendTextMessage(MESSAGE_INFO_DESCR, "You are visible again.") + position.x = position.x + 1 + position:sendMagicEffect(CONST_ME_SMOKE) + end + return false +end diff --git a/data/talkactions/scripts/hide.lua b/data/talkactions/scripts/hide.lua new file mode 100644 index 0000000..1e84352 --- /dev/null +++ b/data/talkactions/scripts/hide.lua @@ -0,0 +1,12 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + player:setHiddenHealth(not player:isHealthHidden()) + return false +end diff --git a/data/talkactions/scripts/info.lua b/data/talkactions/scripts/info.lua new file mode 100644 index 0000000..8969b1c --- /dev/null +++ b/data/talkactions/scripts/info.lua @@ -0,0 +1,37 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local target = Player(param) + if not target then + player:sendCancelMessage("Player not found.") + return false + end + + if target:getAccountType() > player:getAccountType() then + player:sendCancelMessage("You can not get info about this player.") + return false + end + + local targetIp = target:getIp() + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Name: " .. target:getName()) + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Access: " .. (target:getGroup():getAccess() and "1" or "0")) + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Level: " .. target:getLevel()) + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Magic Level: " .. target:getMagicLevel()) + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Speed: " .. target:getSpeed()) + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Position: " .. string.format("(%0.5d / %0.5d / %0.3d)", target:getPosition().x, target:getPosition().y, target:getPosition().z)) + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "IP: " .. Game.convertIpToString(targetIp)) + + local players = {} + for _, targetPlayer in ipairs(Game.getPlayers()) do + if targetPlayer:getIp() == targetIp and targetPlayer ~= target then + players[#players + 1] = targetPlayer:getName() .. " [" .. targetPlayer:getLevel() .. "]" + end + end + + if #players > 0 then + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Other players on same IP: " .. table.concat(players, ", ") .. ".") + end + return false +end diff --git a/data/talkactions/scripts/ipban.lua b/data/talkactions/scripts/ipban.lua new file mode 100644 index 0000000..42a78fc --- /dev/null +++ b/data/talkactions/scripts/ipban.lua @@ -0,0 +1,39 @@ +local ipBanDays = 7 + +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local resultId = db.storeQuery("SELECT `name`, `lastip` FROM `players` WHERE `name` = " .. db.escapeString(param)) + if resultId == false then + return false + end + + local targetName = result.getString(resultId, "name") + local targetIp = result.getNumber(resultId, "lastip") + result.free(resultId) + + local targetPlayer = Player(param) + if targetPlayer then + targetIp = targetPlayer:getIp() + targetPlayer:remove() + end + + if targetIp == 0 then + return false + end + + resultId = db.storeQuery("SELECT 1 FROM `ip_bans` WHERE `ip` = " .. targetIp) + if resultId ~= false then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, targetName .. " is already IP banned.") + result.free(resultId) + return false + end + + local timeNow = os.time() + db.query("INSERT INTO `ip_bans` (`ip`, `reason`, `banned_at`, `expires_at`, `banned_by`) VALUES (" .. + targetIp .. ", '', " .. timeNow .. ", " .. timeNow + (ipBanDays * 86400) .. ", " .. player:getGuid() .. ")") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, targetName .. " has been IP banned.") + return false +end diff --git a/data/talkactions/scripts/kick.lua b/data/talkactions/scripts/kick.lua new file mode 100644 index 0000000..ceb92d9 --- /dev/null +++ b/data/talkactions/scripts/kick.lua @@ -0,0 +1,19 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local target = Player(param) + if not target then + player:sendCancelMessage("Player not found.") + return false + end + + if target:getGroup():getAccess() then + player:sendCancelMessage("You cannot kick this player.") + return false + end + + target:remove() + return false +end diff --git a/data/talkactions/scripts/kills.lua b/data/talkactions/scripts/kills.lua new file mode 100644 index 0000000..360bf62 --- /dev/null +++ b/data/talkactions/scripts/kills.lua @@ -0,0 +1,46 @@ +function onSay(player, words, param) + local fragTime = configManager.getNumber(configKeys.FRAG_TIME) + if fragTime <= 0 then + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "You do not have any unjustified kill.") + return false + end + + local skullTime = player:getSkullTime() + if skullTime <= 0 then + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "You do not have any unjustified kill.") + return false + end + + local kills = math.ceil(skullTime / fragTime) + local remainingSeconds = math.floor(skullTime % fragTime) + + local hours = math.floor(remainingSeconds / 3600) + local minutes = math.floor((remainingSeconds % 3600) / 60) + local seconds = remainingSeconds % 60 + + local message = "You have " .. kills .. " unjustified kill" .. (kills > 1 and "s" or "") .. ". The amount of unjustified kills will decrease after: " + if hours ~= 0 then + if hours == 1 then + message = message .. hours .. " hour, " + else + message = message .. hours .. " hours, " + end + end + + if hours ~= 0 or minutes ~= 0 then + if minutes == 1 then + message = message .. minutes .. " minute and " + else + message = message .. minutes .. " minutes and " + end + end + + if seconds == 1 then + message = message .. seconds .. " second." + else + message = message .. seconds .. " seconds." + end + + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, message) + return false +end diff --git a/data/talkactions/scripts/leavehouse.lua b/data/talkactions/scripts/leavehouse.lua new file mode 100644 index 0000000..ea5fdb5 --- /dev/null +++ b/data/talkactions/scripts/leavehouse.lua @@ -0,0 +1,21 @@ +function onSay(player, words, param) + local position = player:getPosition() + local tile = Tile(position) + local house = tile and tile:getHouse() + if not house then + player:sendCancelMessage("You are not inside a house.") + position:sendMagicEffect(CONST_ME_POFF) + return false + end + + if house:getOwnerGuid() ~= player:getGuid() then + player:sendCancelMessage("You are not the owner of this house.") + position:sendMagicEffect(CONST_ME_POFF) + return false + end + + house:setOwnerGuid(0) + player:sendTextMessage(MESSAGE_INFO_DESCR, "You have successfully left your house.") + position:sendMagicEffect(CONST_ME_POFF) + return false +end diff --git a/data/talkactions/scripts/looktype.lua b/data/talkactions/scripts/looktype.lua new file mode 100644 index 0000000..05d677d --- /dev/null +++ b/data/talkactions/scripts/looktype.lua @@ -0,0 +1,34 @@ +-- keep it ordered +local invalidTypes = { + 1, 135, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, + 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, + 189, 190, 191, 411, 415, 424, 439, 440, 468, 469, 474, 475, 476, 477, 478, + 479, 480, 481, 482, 483, 484, 485, 501, 518, 519, 520, 524, 525, 536, 543, + 549, 576, 581, 582, 597, 616, 623, 625, 638, 639, 640, 641, 642, 643, 645, + 646, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 678, 700, + 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 713, 715, 718, 719, + 722, 723, 737, 741, 742, 743, 744, 748, 751, 752, 753, 754, 755, 756, 757, + 758, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, + 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, + 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, + 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, + 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, + 838, 839, 840, 841, 847, 864, 865, 866, 867, 871, 872, 880, 891, 892, 893, + 894, 895, 896, 897, 898 +} + +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local lookType = tonumber(param) + if lookType >= 0 and lookType < 903 and not table.contains(invalidTypes, lookType) then + local playerOutfit = player:getOutfit() + playerOutfit.lookType = lookType + player:setOutfit(playerOutfit) + else + player:sendCancelMessage("A look type with that id does not exist.") + end + return false +end diff --git a/data/talkactions/scripts/magiceffect.lua b/data/talkactions/scripts/magiceffect.lua new file mode 100644 index 0000000..51972c2 --- /dev/null +++ b/data/talkactions/scripts/magiceffect.lua @@ -0,0 +1,12 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local effect = tonumber(param) + if(effect ~= nil and effect > 0) then + player:getPosition():sendMagicEffect(effect) + end + + return false +end diff --git a/data/talkactions/scripts/mccheck.lua b/data/talkactions/scripts/mccheck.lua new file mode 100644 index 0000000..13ffbc9 --- /dev/null +++ b/data/talkactions/scripts/mccheck.lua @@ -0,0 +1,40 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Multiclient Check List:") + + local ipList = {} + local players = Game.getPlayers() + for i = 1, #players do + local tmpPlayer = players[i] + local ip = tmpPlayer:getIp() + if ip ~= 0 then + local list = ipList[ip] + if not list then + ipList[ip] = {} + list = ipList[ip] + end + list[#list + 1] = tmpPlayer + end + end + + for ip, list in pairs(ipList) do + local listLength = #list + if listLength > 1 then + local tmpPlayer = list[1] + local message = ("%s: %s [%d]"):format(Game.convertIpToString(ip), tmpPlayer:getName(), tmpPlayer:getLevel()) + for i = 2, listLength do + tmpPlayer = list[i] + message = ("%s, %s [%d]"):format(message, tmpPlayer:getName(), tmpPlayer:getLevel()) + end + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, message .. ".") + end + end + return false +end diff --git a/data/talkactions/scripts/online.lua b/data/talkactions/scripts/online.lua new file mode 100644 index 0000000..56879fa --- /dev/null +++ b/data/talkactions/scripts/online.lua @@ -0,0 +1,23 @@ +local maxPlayersPerMessage = 10 + +function onSay(player, words, param) + local hasAccess = player:getGroup():getAccess() + local players = Game.getPlayers() + local onlineList = {} + + for _, targetPlayer in ipairs(players) do + if hasAccess or not targetPlayer:isInGhostMode() then + table.insert(onlineList, ("%s [%d]"):format(targetPlayer:getName(), targetPlayer:getLevel())) + end + end + + local playersOnline = #onlineList + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, ("%d players online."):format(playersOnline)) + + for i = 1, playersOnline, maxPlayersPerMessage do + local j = math.min(i + maxPlayersPerMessage - 1, playersOnline) + local msg = table.concat(onlineList, ", ", i, j) .. "." + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, msg) + end + return false +end diff --git a/data/talkactions/scripts/openserver.lua b/data/talkactions/scripts/openserver.lua new file mode 100644 index 0000000..c3896e7 --- /dev/null +++ b/data/talkactions/scripts/openserver.lua @@ -0,0 +1,13 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + Game.setGameState(GAME_STATE_NORMAL) + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Server is now open.") + return false +end diff --git a/data/talkactions/scripts/owner.lua b/data/talkactions/scripts/owner.lua new file mode 100644 index 0000000..ce4dd80 --- /dev/null +++ b/data/talkactions/scripts/owner.lua @@ -0,0 +1,30 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + local tile = Tile(player:getPosition()) + local house = tile and tile:getHouse() + if not house then + player:sendCancelMessage("You are not inside a house.") + return false + end + + if param == "" or param == "none" then + house:setOwnerGuid(0) + return false + end + + local targetPlayer = Player(param) + if not targetPlayer then + player:sendCancelMessage("Player not found.") + return false + end + + house:setOwnerGuid(targetPlayer:getGuid()) + return false +end diff --git a/data/talkactions/scripts/place_monster.lua b/data/talkactions/scripts/place_monster.lua new file mode 100644 index 0000000..b83aadd --- /dev/null +++ b/data/talkactions/scripts/place_monster.lua @@ -0,0 +1,20 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + local position = player:getPosition() + local monster = Game.createMonster(param, position) + if monster then + monster:getPosition():sendMagicEffect(CONST_ME_TELEPORT) + position:sendMagicEffect(CONST_ME_MAGIC_RED) + else + player:sendCancelMessage("There is not enough room.") + position:sendMagicEffect(CONST_ME_POFF) + end + return false +end diff --git a/data/talkactions/scripts/place_npc.lua b/data/talkactions/scripts/place_npc.lua new file mode 100644 index 0000000..7270c9b --- /dev/null +++ b/data/talkactions/scripts/place_npc.lua @@ -0,0 +1,20 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + local position = player:getPosition() + local npc = Game.createNpc(param, position) + if npc then + npc:setMasterPos(position) + position:sendMagicEffect(CONST_ME_MAGIC_RED) + else + player:sendCancelMessage("There is not enough room.") + position:sendMagicEffect(CONST_ME_POFF) + end + return false +end diff --git a/data/talkactions/scripts/place_summon.lua b/data/talkactions/scripts/place_summon.lua new file mode 100644 index 0000000..d5562e0 --- /dev/null +++ b/data/talkactions/scripts/place_summon.lua @@ -0,0 +1,20 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + local position = player:getPosition() + local monster = Game.createMonster(param, position) + if monster then + player:addSummon(monster) + position:sendMagicEffect(CONST_ME_MAGIC_RED) + else + player:sendCancelMessage("There is not enough room.") + position:sendMagicEffect(CONST_ME_POFF) + end + return false +end diff --git a/data/talkactions/scripts/position.lua b/data/talkactions/scripts/position.lua new file mode 100644 index 0000000..299ce6e --- /dev/null +++ b/data/talkactions/scripts/position.lua @@ -0,0 +1,10 @@ +function onSay(player, words, param) + if player:getGroup():getAccess() and param ~= "" then + local split = param:split(",") + player:teleportTo(Position(split[1], split[2], split[3])) + else + local position = player:getPosition() + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Your current position is: " .. position.x .. ", " .. position.y .. ", " .. position.z .. ".") + end + return false +end diff --git a/data/talkactions/scripts/reload.lua b/data/talkactions/scripts/reload.lua new file mode 100644 index 0000000..7e98385 --- /dev/null +++ b/data/talkactions/scripts/reload.lua @@ -0,0 +1,79 @@ +local reloadTypes = { + ["all"] = RELOAD_TYPE_ALL, + + ["action"] = RELOAD_TYPE_ACTIONS, + ["actions"] = RELOAD_TYPE_ACTIONS, + + ["chat"] = RELOAD_TYPE_CHAT, + ["channel"] = RELOAD_TYPE_CHAT, + ["chatchannels"] = RELOAD_TYPE_CHAT, + + ["config"] = RELOAD_TYPE_CONFIG, + ["configuration"] = RELOAD_TYPE_CONFIG, + + ["creaturescript"] = RELOAD_TYPE_CREATURESCRIPTS, + ["creaturescripts"] = RELOAD_TYPE_CREATURESCRIPTS, + + ["events"] = RELOAD_TYPE_EVENTS, + + ["global"] = RELOAD_TYPE_GLOBAL, + + ["globalevent"] = RELOAD_TYPE_GLOBALEVENTS, + ["globalevents"] = RELOAD_TYPE_GLOBALEVENTS, + + ["items"] = RELOAD_TYPE_ITEMS, + + ["monster"] = RELOAD_TYPE_MONSTERS, + ["monsters"] = RELOAD_TYPE_MONSTERS, + + ["mount"] = RELOAD_TYPE_MOUNTS, + ["mounts"] = RELOAD_TYPE_MOUNTS, + + ["move"] = RELOAD_TYPE_MOVEMENTS, + ["movement"] = RELOAD_TYPE_MOVEMENTS, + ["movements"] = RELOAD_TYPE_MOVEMENTS, + + ["npc"] = RELOAD_TYPE_NPCS, + ["npcs"] = RELOAD_TYPE_NPCS, + + ["quest"] = RELOAD_TYPE_QUESTS, + ["quests"] = RELOAD_TYPE_QUESTS, + + ["raid"] = RELOAD_TYPE_RAIDS, + ["raids"] = RELOAD_TYPE_RAIDS, + + ["spell"] = RELOAD_TYPE_SPELLS, + ["spells"] = RELOAD_TYPE_SPELLS, + + ["talk"] = RELOAD_TYPE_TALKACTIONS, + ["talkaction"] = RELOAD_TYPE_TALKACTIONS, + ["talkactions"] = RELOAD_TYPE_TALKACTIONS, + + ["weapon"] = RELOAD_TYPE_WEAPONS, + ["weapons"] = RELOAD_TYPE_WEAPONS, + + ["scripts"] = RELOAD_TYPE_SCRIPTS, + ["libs"] = RELOAD_TYPE_GLOBAL +} + +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + logCommand(player, words, param) + + local reloadType = reloadTypes[param:lower()] + if not reloadType then + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Reload type not found.") + return false + end + + Game.reload(reloadType) + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, string.format("Reloaded %s.", param:lower())) + return false +end diff --git a/data/talkactions/scripts/remove_tutor.lua b/data/talkactions/scripts/remove_tutor.lua new file mode 100644 index 0000000..c233fbc --- /dev/null +++ b/data/talkactions/scripts/remove_tutor.lua @@ -0,0 +1,28 @@ +function onSay(player, words, param) + if player:getAccountType() <= ACCOUNT_TYPE_TUTOR then + return true + end + + local resultId = db.storeQuery("SELECT `name`, `account_id`, (SELECT `type` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `account_type` FROM `players` WHERE `name` = " .. db.escapeString(param)) + if resultId == false then + player:sendCancelMessage("A player with that name does not exist.") + return false + end + + if result.getNumber(resultId, "account_type") ~= ACCOUNT_TYPE_TUTOR then + player:sendCancelMessage("You can only demote a tutor to a normal player.") + result.free(resultId) + return false + end + + local target = Player(param) + if target then + target:setAccountType(ACCOUNT_TYPE_NORMAL) + else + db.query("UPDATE `accounts` SET `type` = " .. ACCOUNT_TYPE_NORMAL .. " WHERE `id` = " .. result.getNumber(resultId, "account_id")) + end + + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have demoted " .. result.getString(resultId, "name") .. " to a normal player.") + result.free(resultId) + return false +end diff --git a/data/talkactions/scripts/removething.lua b/data/talkactions/scripts/removething.lua new file mode 100644 index 0000000..8216e03 --- /dev/null +++ b/data/talkactions/scripts/removething.lua @@ -0,0 +1,33 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local position = player:getPosition() + position:getNextPosition(player:getDirection()) + + local tile = Tile(position) + if not tile then + player:sendCancelMessage("Object not found.") + return false + end + + local thing = tile:getTopVisibleThing(player) + if not thing then + player:sendCancelMessage("Thing not found.") + return false + end + + if thing:isCreature() then + thing:remove() + elseif thing:isItem() then + if thing == tile:getGround() then + player:sendCancelMessage("You may not remove a ground tile.") + return false + end + thing:remove(tonumber(param) or -1) + end + + position:sendMagicEffect(CONST_ME_MAGIC_RED) + return false +end diff --git a/data/talkactions/scripts/sellhouse.lua b/data/talkactions/scripts/sellhouse.lua new file mode 100644 index 0000000..525d71c --- /dev/null +++ b/data/talkactions/scripts/sellhouse.lua @@ -0,0 +1,19 @@ +function onSay(player, words, param) + local tradePartner = Player(param) + if not tradePartner or tradePartner == player then + player:sendCancelMessage("Trade player not found.") + return false + end + + local house = player:getTile():getHouse() + if not house then + player:sendCancelMessage("You must stand in your house to initiate the trade.") + return false + end + + local returnValue = house:startTrade(player, tradePartner) + if returnValue ~= RETURNVALUE_NOERROR then + player:sendCancelMessage(returnValue) + end + return false +end diff --git a/data/talkactions/scripts/serverinfo.lua b/data/talkactions/scripts/serverinfo.lua new file mode 100644 index 0000000..6b26b98 --- /dev/null +++ b/data/talkactions/scripts/serverinfo.lua @@ -0,0 +1,8 @@ +function onSay(player, words, param) + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Server Info:" + .. "\nExp rate: " .. Game.getExperienceStage(player:getLevel()) + .. "\nSkill rate: " .. configManager.getNumber(configKeys.RATE_SKILL) + .. "\nMagic rate: " .. configManager.getNumber(configKeys.RATE_MAGIC) + .. "\nLoot rate: " .. configManager.getNumber(configKeys.RATE_LOOT)) + return false +end diff --git a/data/talkactions/scripts/teleport_creature_here.lua b/data/talkactions/scripts/teleport_creature_here.lua new file mode 100644 index 0000000..ea36919 --- /dev/null +++ b/data/talkactions/scripts/teleport_creature_here.lua @@ -0,0 +1,24 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local creature = Creature(param) + if not creature then + player:sendCancelMessage("A creature with that name could not be found.") + return false + end + + local oldPosition = creature:getPosition() + local newPosition = creature:getClosestFreePosition(player:getPosition(), false) + if newPosition.x == 0 then + player:sendCancelMessage("You can not teleport " .. creature:getName() .. ".") + return false + elseif creature:teleportTo(newPosition) then + if not creature:isInGhostMode() then + oldPosition:sendMagicEffect(CONST_ME_POFF) + newPosition:sendMagicEffect(CONST_ME_TELEPORT) + end + end + return false +end diff --git a/data/talkactions/scripts/teleport_home.lua b/data/talkactions/scripts/teleport_home.lua new file mode 100644 index 0000000..6485230 --- /dev/null +++ b/data/talkactions/scripts/teleport_home.lua @@ -0,0 +1,8 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + player:teleportTo(player:getTown():getTemplePosition()) + return false +end diff --git a/data/talkactions/scripts/teleport_ntiles.lua b/data/talkactions/scripts/teleport_ntiles.lua new file mode 100644 index 0000000..28cbccb --- /dev/null +++ b/data/talkactions/scripts/teleport_ntiles.lua @@ -0,0 +1,22 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local steps = tonumber(param) + if not steps then + return false + end + + local position = player:getPosition() + position:getNextPosition(player:getDirection(), steps) + + position = player:getClosestFreePosition(position, false) + if position.x == 0 then + player:sendCancelMessage("You cannot teleport there.") + return false + end + + player:teleportTo(position) + return false +end diff --git a/data/talkactions/scripts/teleport_to_creature.lua b/data/talkactions/scripts/teleport_to_creature.lua new file mode 100644 index 0000000..d8f7996 --- /dev/null +++ b/data/talkactions/scripts/teleport_to_creature.lua @@ -0,0 +1,13 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local target = Creature(param) + if target then + player:teleportTo(target:getPosition()) + else + player:sendCancelMessage("Creature not found.") + end + return false +end diff --git a/data/talkactions/scripts/teleport_to_town.lua b/data/talkactions/scripts/teleport_to_town.lua new file mode 100644 index 0000000..ab0664f --- /dev/null +++ b/data/talkactions/scripts/teleport_to_town.lua @@ -0,0 +1,13 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local town = Town(param) or Town(tonumber(param)) + if town then + player:teleportTo(town:getTemplePosition()) + else + player:sendCancelMessage("Town not found.") + end + return false +end diff --git a/data/talkactions/scripts/unban.lua b/data/talkactions/scripts/unban.lua new file mode 100644 index 0000000..f0cbffa --- /dev/null +++ b/data/talkactions/scripts/unban.lua @@ -0,0 +1,16 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local resultId = db.storeQuery("SELECT `account_id`, `lastip` FROM `players` WHERE `name` = " .. db.escapeString(param)) + if resultId == false then + return false + end + + db.asyncQuery("DELETE FROM `account_bans` WHERE `account_id` = " .. result.getNumber(resultId, "account_id")) + db.asyncQuery("DELETE FROM `ip_bans` WHERE `ip` = " .. result.getNumber(resultId, "lastip")) + result.free(resultId) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, param .. " has been unbanned.") + return false +end diff --git a/data/talkactions/scripts/up.lua b/data/talkactions/scripts/up.lua new file mode 100644 index 0000000..c48bfd7 --- /dev/null +++ b/data/talkactions/scripts/up.lua @@ -0,0 +1,10 @@ +function onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + local position = player:getPosition() + position.z = position.z - 1 + player:teleportTo(position) + return false +end diff --git a/data/talkactions/scripts/uptime.lua b/data/talkactions/scripts/uptime.lua new file mode 100644 index 0000000..7c0e291 --- /dev/null +++ b/data/talkactions/scripts/uptime.lua @@ -0,0 +1,8 @@ +function onSay(player, words, param) + local uptime = getWorldUpTime() + + local hours = math.floor(uptime / 3600) + local minutes = math.floor((uptime - (3600 * hours)) / 60) + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Uptime: " .. hours .. " hours and " .. minutes .. " minutes.") + return false +end diff --git a/data/talkactions/talkactions.xml b/data/talkactions/talkactions.xml new file mode 100644 index 0000000..00f9ef9 --- /dev/null +++ b/data/talkactions/talkactions.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/weapons/lib/weapons.lua b/data/weapons/lib/weapons.lua new file mode 100644 index 0000000..e69de29 diff --git a/data/weapons/scripts/burst_arrow.lua b/data/weapons/scripts/burst_arrow.lua new file mode 100644 index 0000000..290aa19 --- /dev/null +++ b/data/weapons/scripts/burst_arrow.lua @@ -0,0 +1,21 @@ +local area = createCombatArea({ + {1, 1, 1}, + {1, 3, 1}, + {1, 1, 1} +}) + +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_EXPLOSIONAREA) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_BURSTARROW) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) +combat:setFormula(COMBAT_FORMULA_SKILL, 0, 0, 1, 0) +combat:setArea(area) + +function onUseWeapon(player, variant) + if player:getSkull() == SKULL_BLACK then + return false + end + + return combat:execute(player, variant) +end diff --git a/data/weapons/scripts/poison_arrow.lua b/data/weapons/scripts/poison_arrow.lua new file mode 100644 index 0000000..fa49730 --- /dev/null +++ b/data/weapons/scripts/poison_arrow.lua @@ -0,0 +1,14 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_POISONARROW) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) +combat:setFormula(COMBAT_FORMULA_SKILL, 0, 0, 1, 0) + +function onUseWeapon(player, variant) + if not combat:execute(player, variant) then + return false + end + + player:addDamageCondition(Creature(variant:getNumber()), CONDITION_POISON, DAMAGELIST_LOGARITHMIC_DAMAGE, 3) + return true +end diff --git a/data/weapons/scripts/viper_star.lua b/data/weapons/scripts/viper_star.lua new file mode 100644 index 0000000..9733fb1 --- /dev/null +++ b/data/weapons/scripts/viper_star.lua @@ -0,0 +1,18 @@ +local combat = Combat() +combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE) +combat:setParameter(COMBAT_PARAM_DISTANCEEFFECT, CONST_ANI_GREENSTAR) +combat:setParameter(COMBAT_PARAM_BLOCKARMOR, true) +combat:setFormula(COMBAT_FORMULA_SKILL, 0, 0, 1, 0) + +function onUseWeapon(player, variant) + if not combat:execute(player, variant) then + return false + end + + if math.random(1, 100) <= 90 then + return false + end + + player:addDamageCondition(Creature(variant:getNumber()), CONDITION_POISON, DAMAGELIST_LOGARITHMIC_DAMAGE, 2) + return true +end diff --git a/data/weapons/weapons.xml b/data/weapons/weapons.xml new file mode 100644 index 0000000..eaea815 --- /dev/null +++ b/data/weapons/weapons.xml @@ -0,0 +1,623 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/world/forgotten-house.xml b/data/world/forgotten-house.xml new file mode 100644 index 0000000..4279d56 --- /dev/null +++ b/data/world/forgotten-house.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/world/forgotten-spawn.xml b/data/world/forgotten-spawn.xml new file mode 100644 index 0000000..948ec78 --- /dev/null +++ b/data/world/forgotten-spawn.xml @@ -0,0 +1,1351 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/world/forgotten.otbm b/data/world/forgotten.otbm new file mode 100644 index 0000000..84b4290 Binary files /dev/null and b/data/world/forgotten.otbm differ diff --git a/key.pem b/key.pem new file mode 100644 index 0000000..ec19bb4 --- /dev/null +++ b/key.pem @@ -0,0 +1,13 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCbZGkDtFsHrJVlaNhzU71xZROd15QHA7A+bdB5OZZhtKg3qmBWHXzLlFL6AIBZ +SQmIKrW8pYoaGzX4sQWbcrEhJhHGFSrT27PPvuetwUKnXT11lxUJwyHFwkpb1R/UYPAbThW+sN4Z +MFKKXT8VwePL9cQB1nd+EKyqsz2+jVt/9QIDAQABAoGAQovTtTRtr3GnYRBvcaQxAvjIV9ZUnFRm +C7Y3i1KwJhOZ3ozmSLrEEOLqTgoc7R+sJ1YzEiDKbbete11EC3gohlhW56ptj0WDf+7ptKOgqiEy +Kh4qt1sYJeeGz4GiiooJoeKFGdtk/5uvMR6FDCv6H7ewigVswzf330Q3Ya7+jYECQQERBxsga6+5 +x6IofXyNF6QuMqvuiN/pUgaStUOdlnWBf/T4yUpKvNS1+I4iDzqGWOOSR6RsaYPYVhj9iRABoKyx +AkEAkbNzB6vhLAWht4dUdGzaREF3p4SwNcu5bJRa/9wCLSHaS9JaTq4lljgVPp1zyXyJCSCWpFnl +0WvK3Qf6nVBIhQJBANS7rK8+ONWQbxENdZaZ7Rrx8HUTwSOS/fwhsGWBbl1Qzhdq/6/sIfEHkfeH +1hoH+IlpuPuf21MdAqvJt+cMwoECQF1LyBOYduYGcSgg6u5mKVldhm3pJCA+ZGxnjuGZEnet3qeA +eb05++112fyvO85ABUun524z9lokKNFh45NKLjUCQGshzV43P+RioiBhtEpB/QFzijiS4L2HKNu1 +tdhudnUjWkaf6jJmQS/ppln0hhRMHlk9Vus/bPx7LtuDuo6VQDo= +-----END RSA PRIVATE KEY----- diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..0cc7be8 --- /dev/null +++ b/schema.sql @@ -0,0 +1,370 @@ +CREATE TABLE IF NOT EXISTS `accounts` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL, + `password` char(40) NOT NULL, + `secret` char(16) DEFAULT NULL, + `type` int(11) NOT NULL DEFAULT '1', + `premdays` int(11) NOT NULL DEFAULT '0', + `lastday` int(10) unsigned NOT NULL DEFAULT '0', + `email` varchar(255) NOT NULL DEFAULT '', + `creation` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `players` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `group_id` int(11) NOT NULL DEFAULT '1', + `account_id` int(11) NOT NULL DEFAULT '0', + `level` int(11) NOT NULL DEFAULT '1', + `vocation` int(11) NOT NULL DEFAULT '0', + `health` int(11) NOT NULL DEFAULT '150', + `healthmax` int(11) NOT NULL DEFAULT '150', + `experience` bigint(20) NOT NULL DEFAULT '0', + `lookbody` int(11) NOT NULL DEFAULT '0', + `lookfeet` int(11) NOT NULL DEFAULT '0', + `lookhead` int(11) NOT NULL DEFAULT '0', + `looklegs` int(11) NOT NULL DEFAULT '0', + `looktype` int(11) NOT NULL DEFAULT '136', + `lookaddons` int(11) NOT NULL DEFAULT '0', + `direction` tinyint(1) unsigned NOT NULL DEFAULT '2', + `maglevel` int(11) NOT NULL DEFAULT '0', + `mana` int(11) NOT NULL DEFAULT '0', + `manamax` int(11) NOT NULL DEFAULT '0', + `manaspent` int(11) unsigned NOT NULL DEFAULT '0', + `soul` int(10) unsigned NOT NULL DEFAULT '0', + `town_id` int(11) NOT NULL DEFAULT '1', + `posx` int(11) NOT NULL DEFAULT '0', + `posy` int(11) NOT NULL DEFAULT '0', + `posz` int(11) NOT NULL DEFAULT '0', + `conditions` blob NOT NULL, + `cap` int(11) NOT NULL DEFAULT '400', + `sex` int(11) NOT NULL DEFAULT '0', + `lastlogin` bigint(20) unsigned NOT NULL DEFAULT '0', + `lastip` int(10) unsigned NOT NULL DEFAULT '0', + `save` tinyint(1) NOT NULL DEFAULT '1', + `skull` tinyint(1) NOT NULL DEFAULT '0', + `skulltime` bigint(20) NOT NULL DEFAULT '0', + `lastlogout` bigint(20) unsigned NOT NULL DEFAULT '0', + `blessings` tinyint(2) NOT NULL DEFAULT '0', + `onlinetime` int(11) NOT NULL DEFAULT '0', + `deletion` bigint(15) NOT NULL DEFAULT '0', + `balance` bigint(20) unsigned NOT NULL DEFAULT '0', + `offlinetraining_time` smallint(5) unsigned NOT NULL DEFAULT '43200', + `offlinetraining_skill` int(11) NOT NULL DEFAULT '-1', + `stamina` smallint(5) unsigned NOT NULL DEFAULT '2520', + `skill_fist` int(10) unsigned NOT NULL DEFAULT 10, + `skill_fist_tries` bigint(20) unsigned NOT NULL DEFAULT 0, + `skill_club` int(10) unsigned NOT NULL DEFAULT 10, + `skill_club_tries` bigint(20) unsigned NOT NULL DEFAULT 0, + `skill_sword` int(10) unsigned NOT NULL DEFAULT 10, + `skill_sword_tries` bigint(20) unsigned NOT NULL DEFAULT 0, + `skill_axe` int(10) unsigned NOT NULL DEFAULT 10, + `skill_axe_tries` bigint(20) unsigned NOT NULL DEFAULT 0, + `skill_dist` int(10) unsigned NOT NULL DEFAULT 10, + `skill_dist_tries` bigint(20) unsigned NOT NULL DEFAULT 0, + `skill_shielding` int(10) unsigned NOT NULL DEFAULT 10, + `skill_shielding_tries` bigint(20) unsigned NOT NULL DEFAULT 0, + `skill_fishing` int(10) unsigned NOT NULL DEFAULT 10, + `skill_fishing_tries` bigint(20) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`), + FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + KEY `vocation` (`vocation`) +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `account_bans` ( + `account_id` int(11) NOT NULL, + `reason` varchar(255) NOT NULL, + `banned_at` bigint(20) NOT NULL, + `expires_at` bigint(20) NOT NULL, + `banned_by` int(11) NOT NULL, + PRIMARY KEY (`account_id`), + FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (`banned_by`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `account_ban_history` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `account_id` int(11) NOT NULL, + `reason` varchar(255) NOT NULL, + `banned_at` bigint(20) NOT NULL, + `expired_at` bigint(20) NOT NULL, + `banned_by` int(11) NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (`banned_by`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `ip_bans` ( + `ip` int(10) unsigned NOT NULL, + `reason` varchar(255) NOT NULL, + `banned_at` bigint(20) NOT NULL, + `expires_at` bigint(20) NOT NULL, + `banned_by` int(11) NOT NULL, + PRIMARY KEY (`ip`), + FOREIGN KEY (`banned_by`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `player_namelocks` ( + `player_id` int(11) NOT NULL, + `reason` varchar(255) NOT NULL, + `namelocked_at` bigint(20) NOT NULL, + `namelocked_by` int(11) NOT NULL, + PRIMARY KEY (`player_id`), + FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (`namelocked_by`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `account_viplist` ( + `account_id` int(11) NOT NULL COMMENT 'id of account whose viplist entry it is', + `player_id` int(11) NOT NULL COMMENT 'id of target player of viplist entry', + `description` varchar(128) NOT NULL DEFAULT '', + `icon` tinyint(2) unsigned NOT NULL DEFAULT '0', + `notify` tinyint(1) NOT NULL DEFAULT '0', + UNIQUE KEY `account_player_index` (`account_id`,`player_id`), + FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `guilds` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `ownerid` int(11) NOT NULL, + `creationdata` int(11) NOT NULL, + `motd` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + UNIQUE KEY (`name`), + UNIQUE KEY (`ownerid`), + FOREIGN KEY (`ownerid`) REFERENCES `players`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `guild_invites` ( + `player_id` int(11) NOT NULL DEFAULT '0', + `guild_id` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`player_id`,`guild_id`), + FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `guild_ranks` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `guild_id` int(11) NOT NULL COMMENT 'guild', + `name` varchar(255) NOT NULL COMMENT 'rank name', + `level` int(11) NOT NULL COMMENT 'rank level - leader, vice, member, maybe something else', + PRIMARY KEY (`id`), + FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `guild_membership` ( + `player_id` int(11) NOT NULL, + `guild_id` int(11) NOT NULL, + `rank_id` int(11) NOT NULL, + `nick` varchar(15) NOT NULL DEFAULT '', + PRIMARY KEY (`player_id`), + FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (`rank_id`) REFERENCES `guild_ranks` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `guild_wars` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `guild1` int(11) NOT NULL DEFAULT '0', + `guild2` int(11) NOT NULL DEFAULT '0', + `name1` varchar(255) NOT NULL, + `name2` varchar(255) NOT NULL, + `status` tinyint(2) NOT NULL DEFAULT '0', + `started` bigint(15) NOT NULL DEFAULT '0', + `ended` bigint(15) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `guild1` (`guild1`), + KEY `guild2` (`guild2`) +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `guildwar_kills` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `killer` varchar(50) NOT NULL, + `target` varchar(50) NOT NULL, + `killerguild` int(11) NOT NULL DEFAULT '0', + `targetguild` int(11) NOT NULL DEFAULT '0', + `warid` int(11) NOT NULL DEFAULT '0', + `time` bigint(15) NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY (`warid`) REFERENCES `guild_wars` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `houses` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `owner` int(11) NOT NULL, + `paid` int(10) unsigned NOT NULL DEFAULT '0', + `warnings` int(11) NOT NULL DEFAULT '0', + `name` varchar(255) NOT NULL, + `rent` int(11) NOT NULL DEFAULT '0', + `town_id` int(11) NOT NULL DEFAULT '0', + `bid` int(11) NOT NULL DEFAULT '0', + `bid_end` int(11) NOT NULL DEFAULT '0', + `last_bid` int(11) NOT NULL DEFAULT '0', + `highest_bidder` int(11) NOT NULL DEFAULT '0', + `size` int(11) NOT NULL DEFAULT '0', + `beds` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `owner` (`owner`), + KEY `town_id` (`town_id`) +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `house_lists` ( + `house_id` int(11) NOT NULL, + `listid` int(11) NOT NULL, + `list` text NOT NULL, + FOREIGN KEY (`house_id`) REFERENCES `houses` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `market_history` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `player_id` int(11) NOT NULL, + `sale` tinyint(1) NOT NULL DEFAULT '0', + `itemtype` int(10) unsigned NOT NULL, + `amount` smallint(5) unsigned NOT NULL, + `price` int(10) unsigned NOT NULL DEFAULT '0', + `expires_at` bigint(20) unsigned NOT NULL, + `inserted` bigint(20) unsigned NOT NULL, + `state` tinyint(1) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `player_id` (`player_id`, `sale`), + FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `market_offers` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `player_id` int(11) NOT NULL, + `sale` tinyint(1) NOT NULL DEFAULT '0', + `itemtype` int(10) unsigned NOT NULL, + `amount` smallint(5) unsigned NOT NULL, + `created` bigint(20) unsigned NOT NULL, + `anonymous` tinyint(1) NOT NULL DEFAULT '0', + `price` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `sale` (`sale`,`itemtype`), + KEY `created` (`created`), + FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `players_online` ( + `player_id` int(11) NOT NULL, + PRIMARY KEY (`player_id`) +) ENGINE=MEMORY DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `player_deaths` ( + `player_id` int(11) NOT NULL, + `time` bigint(20) unsigned NOT NULL DEFAULT '0', + `level` int(11) NOT NULL DEFAULT '1', + `killed_by` varchar(255) NOT NULL, + `is_player` tinyint(1) NOT NULL DEFAULT '1', + `mostdamage_by` varchar(100) NOT NULL, + `mostdamage_is_player` tinyint(1) NOT NULL DEFAULT '0', + `unjustified` tinyint(1) NOT NULL DEFAULT '0', + `mostdamage_unjustified` tinyint(1) NOT NULL DEFAULT '0', + FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE, + KEY `killed_by` (`killed_by`), + KEY `mostdamage_by` (`mostdamage_by`) +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `player_depotitems` ( + `player_id` int(11) NOT NULL, + `sid` int(11) NOT NULL COMMENT 'any given range eg 0-100 will be reserved for depot lockers and all > 100 will be then normal items inside depots', + `pid` int(11) NOT NULL DEFAULT '0', + `itemtype` smallint(6) NOT NULL, + `count` smallint(5) NOT NULL DEFAULT '0', + `attributes` blob NOT NULL, + UNIQUE KEY `player_id_2` (`player_id`, `sid`), + FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `player_inboxitems` ( + `player_id` int(11) NOT NULL, + `sid` int(11) NOT NULL, + `pid` int(11) NOT NULL DEFAULT '0', + `itemtype` smallint(6) NOT NULL, + `count` smallint(5) NOT NULL DEFAULT '0', + `attributes` blob NOT NULL, + UNIQUE KEY `player_id_2` (`player_id`, `sid`), + FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `player_items` ( + `player_id` int(11) NOT NULL DEFAULT '0', + `pid` int(11) NOT NULL DEFAULT '0', + `sid` int(11) NOT NULL DEFAULT '0', + `itemtype` smallint(6) NOT NULL DEFAULT '0', + `count` smallint(5) NOT NULL DEFAULT '0', + `attributes` blob NOT NULL, + FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE, + KEY `sid` (`sid`) +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `player_spells` ( + `player_id` int(11) NOT NULL, + `name` varchar(255) NOT NULL, + FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `player_storage` ( + `player_id` int(11) NOT NULL DEFAULT '0', + `key` int(10) unsigned NOT NULL DEFAULT '0', + `value` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`player_id`,`key`), + FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `server_config` ( + `config` varchar(50) NOT NULL, + `value` varchar(256) NOT NULL DEFAULT '', + PRIMARY KEY `config` (`config`) +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `tile_store` ( + `house_id` int(11) NOT NULL, + `data` longblob NOT NULL, + FOREIGN KEY (`house_id`) REFERENCES `houses` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `towns` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `posx` int(11) NOT NULL DEFAULT '0', + `posy` int(11) NOT NULL DEFAULT '0', + `posz` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `live_casts` ( + `player_id` int(11) NOT NULL, + `cast_name` varchar(255) NOT NULL, + `password` boolean NOT NULL DEFAULT false, + `description` varchar(255), + `spectators` smallint(5) DEFAULT 0, + UNIQUE KEY `player_id_2` (`player_id`), + FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB; + +INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '24'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); + +DROP TRIGGER IF EXISTS `ondelete_players`; +DROP TRIGGER IF EXISTS `oncreate_guilds`; + +DELIMITER // +CREATE TRIGGER `ondelete_players` BEFORE DELETE ON `players` + FOR EACH ROW BEGIN + UPDATE `houses` SET `owner` = 0 WHERE `owner` = OLD.`id`; +END +// +CREATE TRIGGER `oncreate_guilds` AFTER INSERT ON `guilds` + FOR EACH ROW BEGIN + INSERT INTO `guild_ranks` (`name`, `level`, `guild_id`) VALUES ('the Leader', 3, NEW.`id`); + INSERT INTO `guild_ranks` (`name`, `level`, `guild_id`) VALUES ('a Vice-Leader', 2, NEW.`id`); + INSERT INTO `guild_ranks` (`name`, `level`, `guild_id`) VALUES ('a Member', 1, NEW.`id`); +END +// +DELIMITER ; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..d30e6ed --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,82 @@ +set(tfs_SRC + ${CMAKE_CURRENT_LIST_DIR}/otpch.cpp + ${CMAKE_CURRENT_LIST_DIR}/actions.cpp + ${CMAKE_CURRENT_LIST_DIR}/ban.cpp + ${CMAKE_CURRENT_LIST_DIR}/baseevents.cpp + ${CMAKE_CURRENT_LIST_DIR}/bed.cpp + ${CMAKE_CURRENT_LIST_DIR}/chat.cpp + ${CMAKE_CURRENT_LIST_DIR}/combat.cpp + ${CMAKE_CURRENT_LIST_DIR}/condition.cpp + ${CMAKE_CURRENT_LIST_DIR}/configmanager.cpp + ${CMAKE_CURRENT_LIST_DIR}/connection.cpp + ${CMAKE_CURRENT_LIST_DIR}/container.cpp + ${CMAKE_CURRENT_LIST_DIR}/creature.cpp + ${CMAKE_CURRENT_LIST_DIR}/creatureevent.cpp + ${CMAKE_CURRENT_LIST_DIR}/cylinder.cpp + ${CMAKE_CURRENT_LIST_DIR}/database.cpp + ${CMAKE_CURRENT_LIST_DIR}/databasemanager.cpp + ${CMAKE_CURRENT_LIST_DIR}/databasetasks.cpp + ${CMAKE_CURRENT_LIST_DIR}/depotchest.cpp + ${CMAKE_CURRENT_LIST_DIR}/depotlocker.cpp + ${CMAKE_CURRENT_LIST_DIR}/events.cpp + ${CMAKE_CURRENT_LIST_DIR}/fileloader.cpp + ${CMAKE_CURRENT_LIST_DIR}/game.cpp + ${CMAKE_CURRENT_LIST_DIR}/globalevent.cpp + ${CMAKE_CURRENT_LIST_DIR}/guild.cpp + ${CMAKE_CURRENT_LIST_DIR}/groups.cpp + ${CMAKE_CURRENT_LIST_DIR}/house.cpp + ${CMAKE_CURRENT_LIST_DIR}/housetile.cpp + ${CMAKE_CURRENT_LIST_DIR}/inbox.cpp + ${CMAKE_CURRENT_LIST_DIR}/ioguild.cpp + ${CMAKE_CURRENT_LIST_DIR}/iologindata.cpp + ${CMAKE_CURRENT_LIST_DIR}/iomap.cpp + ${CMAKE_CURRENT_LIST_DIR}/iomapserialize.cpp + ${CMAKE_CURRENT_LIST_DIR}/iomarket.cpp + ${CMAKE_CURRENT_LIST_DIR}/item.cpp + ${CMAKE_CURRENT_LIST_DIR}/items.cpp + ${CMAKE_CURRENT_LIST_DIR}/luascript.cpp + ${CMAKE_CURRENT_LIST_DIR}/mailbox.cpp + ${CMAKE_CURRENT_LIST_DIR}/map.cpp + ${CMAKE_CURRENT_LIST_DIR}/monster.cpp + ${CMAKE_CURRENT_LIST_DIR}/monsters.cpp + ${CMAKE_CURRENT_LIST_DIR}/mounts.cpp + ${CMAKE_CURRENT_LIST_DIR}/movement.cpp + ${CMAKE_CURRENT_LIST_DIR}/networkmessage.cpp + ${CMAKE_CURRENT_LIST_DIR}/npc.cpp + ${CMAKE_CURRENT_LIST_DIR}/otserv.cpp + ${CMAKE_CURRENT_LIST_DIR}/outfit.cpp + ${CMAKE_CURRENT_LIST_DIR}/outputmessage.cpp + ${CMAKE_CURRENT_LIST_DIR}/party.cpp + ${CMAKE_CURRENT_LIST_DIR}/player.cpp + ${CMAKE_CURRENT_LIST_DIR}/position.cpp + ${CMAKE_CURRENT_LIST_DIR}/protocol.cpp + ${CMAKE_CURRENT_LIST_DIR}/protocolspectator.cpp + ${CMAKE_CURRENT_LIST_DIR}/protocolgamebase.cpp + ${CMAKE_CURRENT_LIST_DIR}/protocolgame.cpp + ${CMAKE_CURRENT_LIST_DIR}/protocollogin.cpp + ${CMAKE_CURRENT_LIST_DIR}/protocolold.cpp + ${CMAKE_CURRENT_LIST_DIR}/protocolstatus.cpp + ${CMAKE_CURRENT_LIST_DIR}/quests.cpp + ${CMAKE_CURRENT_LIST_DIR}/raids.cpp + ${CMAKE_CURRENT_LIST_DIR}/rsa.cpp + ${CMAKE_CURRENT_LIST_DIR}/scheduler.cpp + ${CMAKE_CURRENT_LIST_DIR}/scriptmanager.cpp + ${CMAKE_CURRENT_LIST_DIR}/script.cpp + ${CMAKE_CURRENT_LIST_DIR}/server.cpp + ${CMAKE_CURRENT_LIST_DIR}/signals.cpp + ${CMAKE_CURRENT_LIST_DIR}/spawn.cpp + ${CMAKE_CURRENT_LIST_DIR}/spells.cpp + ${CMAKE_CURRENT_LIST_DIR}/talkaction.cpp + ${CMAKE_CURRENT_LIST_DIR}/tasks.cpp + ${CMAKE_CURRENT_LIST_DIR}/teleport.cpp + ${CMAKE_CURRENT_LIST_DIR}/thing.cpp + ${CMAKE_CURRENT_LIST_DIR}/tile.cpp + ${CMAKE_CURRENT_LIST_DIR}/tools.cpp + ${CMAKE_CURRENT_LIST_DIR}/trashholder.cpp + ${CMAKE_CURRENT_LIST_DIR}/vocation.cpp + ${CMAKE_CURRENT_LIST_DIR}/waitlist.cpp + ${CMAKE_CURRENT_LIST_DIR}/weapons.cpp + ${CMAKE_CURRENT_LIST_DIR}/wildcardtree.cpp + ${CMAKE_CURRENT_LIST_DIR}/xtea.cpp + PARENT_SCOPE) + diff --git a/src/account.h b/src/account.h new file mode 100644 index 0000000..7e14962 --- /dev/null +++ b/src/account.h @@ -0,0 +1,37 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_ACCOUNT_H_34817537BA2B4CB7B71AA562AFBB118F +#define FS_ACCOUNT_H_34817537BA2B4CB7B71AA562AFBB118F + +#include "enums.h" + +struct Account { + std::vector characters; + std::string name; + std::string key; + time_t lastDay = 0; + uint32_t id = 0; + uint16_t premiumDays = 0; + AccountType_t accountType = ACCOUNT_TYPE_NORMAL; + + Account() = default; +}; + +#endif diff --git a/src/actions.cpp b/src/actions.cpp new file mode 100644 index 0000000..80b230a --- /dev/null +++ b/src/actions.cpp @@ -0,0 +1,585 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "actions.h" +#include "bed.h" +#include "configmanager.h" +#include "container.h" +#include "game.h" +#include "pugicast.h" +#include "spells.h" + +extern Game g_game; +extern Spells* g_spells; +extern Actions* g_actions; +extern ConfigManager g_config; + +Actions::Actions() : + scriptInterface("Action Interface") +{ + scriptInterface.initState(); +} + +Actions::~Actions() +{ + clear(false); +} + +void Actions::clearMap(ActionUseMap& map, bool fromLua) +{ + for (auto it = map.begin(); it != map.end(); ) { + if (fromLua == it->second.fromLua) { + it = map.erase(it); + } else { + ++it; + } + } +} + +void Actions::clear(bool fromLua) +{ + clearMap(useItemMap, fromLua); + clearMap(uniqueItemMap, fromLua); + clearMap(actionItemMap, fromLua); + + reInitState(fromLua); +} + +LuaScriptInterface& Actions::getScriptInterface() +{ + return scriptInterface; +} + +std::string Actions::getScriptBaseName() const +{ + return "actions"; +} + +Event_ptr Actions::getEvent(const std::string& nodeName) +{ + if (strcasecmp(nodeName.c_str(), "action") != 0) { + return nullptr; + } + return Event_ptr(new Action(&scriptInterface)); +} + +bool Actions::registerEvent(Event_ptr event, const pugi::xml_node& node) +{ + Action_ptr action{static_cast(event.release())}; //event is guaranteed to be an Action + + pugi::xml_attribute attr; + if ((attr = node.attribute("itemid"))) { + uint16_t id = pugi::cast(attr.value()); + + auto result = useItemMap.emplace(id, std::move(*action)); + if (!result.second) { + std::cout << "[Warning - Actions::registerEvent] Duplicate registered item with id: " << id << std::endl; + } + return result.second; + } else if ((attr = node.attribute("fromid"))) { + pugi::xml_attribute toIdAttribute = node.attribute("toid"); + if (!toIdAttribute) { + std::cout << "[Warning - Actions::registerEvent] Missing toid in fromid: " << attr.as_string() << std::endl; + return false; + } + + uint16_t fromId = pugi::cast(attr.value()); + uint16_t iterId = fromId; + uint16_t toId = pugi::cast(toIdAttribute.value()); + + auto result = useItemMap.emplace(iterId, *action); + if (!result.second) { + std::cout << "[Warning - Actions::registerEvent] Duplicate registered item with id: " << iterId << " in fromid: " << fromId << ", toid: " << toId << std::endl; + } + + bool success = result.second; + while (++iterId <= toId) { + result = useItemMap.emplace(iterId, *action); + if (!result.second) { + std::cout << "[Warning - Actions::registerEvent] Duplicate registered item with id: " << iterId << " in fromid: " << fromId << ", toid: " << toId << std::endl; + continue; + } + success = true; + } + return success; + } else if ((attr = node.attribute("uniqueid"))) { + uint16_t uid = pugi::cast(attr.value()); + + auto result = uniqueItemMap.emplace(uid, std::move(*action)); + if (!result.second) { + std::cout << "[Warning - Actions::registerEvent] Duplicate registered item with uniqueid: " << uid << std::endl; + } + return result.second; + } else if ((attr = node.attribute("fromuid"))) { + pugi::xml_attribute toUidAttribute = node.attribute("touid"); + if (!toUidAttribute) { + std::cout << "[Warning - Actions::registerEvent] Missing touid in fromuid: " << attr.as_string() << std::endl; + return false; + } + + uint16_t fromUid = pugi::cast(attr.value()); + uint16_t iterUid = fromUid; + uint16_t toUid = pugi::cast(toUidAttribute.value()); + + auto result = uniqueItemMap.emplace(iterUid, *action); + if (!result.second) { + std::cout << "[Warning - Actions::registerEvent] Duplicate registered item with unique id: " << iterUid << " in fromuid: " << fromUid << ", touid: " << toUid << std::endl; + } + + bool success = result.second; + while (++iterUid <= toUid) { + result = uniqueItemMap.emplace(iterUid, *action); + if (!result.second) { + std::cout << "[Warning - Actions::registerEvent] Duplicate registered item with unique id: " << iterUid << " in fromuid: " << fromUid << ", touid: " << toUid << std::endl; + continue; + } + success = true; + } + return success; + } else if ((attr = node.attribute("actionid"))) { + uint16_t aid = pugi::cast(attr.value()); + + auto result = actionItemMap.emplace(aid, std::move(*action)); + if (!result.second) { + std::cout << "[Warning - Actions::registerEvent] Duplicate registered item with actionid: " << aid << std::endl; + } + return result.second; + } else if ((attr = node.attribute("fromaid"))) { + pugi::xml_attribute toAidAttribute = node.attribute("toaid"); + if (!toAidAttribute) { + std::cout << "[Warning - Actions::registerEvent] Missing toaid in fromaid: " << attr.as_string() << std::endl; + return false; + } + + uint16_t fromAid = pugi::cast(attr.value()); + uint16_t iterAid = fromAid; + uint16_t toAid = pugi::cast(toAidAttribute.value()); + + auto result = actionItemMap.emplace(iterAid, *action); + if (!result.second) { + std::cout << "[Warning - Actions::registerEvent] Duplicate registered item with action id: " << iterAid << " in fromaid: " << fromAid << ", toaid: " << toAid << std::endl; + } + + bool success = result.second; + while (++iterAid <= toAid) { + result = actionItemMap.emplace(iterAid, *action); + if (!result.second) { + std::cout << "[Warning - Actions::registerEvent] Duplicate registered item with action id: " << iterAid << " in fromaid: " << fromAid << ", toaid: " << toAid << std::endl; + continue; + } + success = true; + } + return success; + } + return false; +} + +bool Actions::registerLuaEvent(Action* event) +{ + Action_ptr action{ event }; + if (action->getItemIdRange().size() > 0) { + if (action->getItemIdRange().size() == 1) { + auto id = action->getItemIdRange().front(); + auto result = useItemMap.emplace(id, std::move(*action)); + if (!result.second) { + std::cout << "[Warning - Actions::registerLuaEvent] Duplicate registered item with id: " << id << std::endl; + } + return result.second; + } else { + auto v = action->getItemIdRange(); + for (auto i = v.begin(); i != v.end(); i++) { + auto result = useItemMap.emplace(*i, std::move(*action)); + if (!result.second) { + std::cout << "[Warning - Actions::registerLuaEvent] Duplicate registered item with id: " << *i << " in range from id: " << v.front() << ", to id: " << v.at(v.size() - 1) << std::endl; + continue; + } + } + return true; + } + } else if (action->getUniqueIdRange().size() > 0) { + if (action->getUniqueIdRange().size() == 1) { + auto uid = action->getUniqueIdRange().front(); + auto result = uniqueItemMap.emplace(uid, std::move(*action)); + if (!result.second) { + std::cout << "[Warning - Actions::registerLuaEvent] Duplicate registered item with uid: " << uid << std::endl; + } + return result.second; + } else { + auto v = action->getUniqueIdRange(); + for (auto i = v.begin(); i != v.end(); i++) { + auto result = uniqueItemMap.emplace(*i, std::move(*action)); + if (!result.second) { + std::cout << "[Warning - Actions::registerLuaEvent] Duplicate registered item with uid: " << *i << " in range from uid: " << v.front() << ", to uid: " << v.at(v.size() - 1) << std::endl; + continue; + } + } + return true; + } + } else if (action->getActionIdRange().size() > 0) { + if (action->getActionIdRange().size() == 1) { + auto aid = action->getActionIdRange().front(); + auto result = actionItemMap.emplace(aid, std::move(*action)); + if (!result.second) { + std::cout << "[Warning - Actions::registerLuaEvent] Duplicate registered item with aid: " << aid << std::endl; + } + return result.second; + } else { + auto v = action->getActionIdRange(); + for (auto i = v.begin(); i != v.end(); i++) { + auto result = actionItemMap.emplace(*i, std::move(*action)); + if (!result.second) { + std::cout << "[Warning - Actions::registerLuaEvent] Duplicate registered item with aid: " << *i << " in range from aid: " << v.front() << ", to aid: " << v.at(v.size() - 1) << std::endl; + continue; + } + } + return true; + } + } else { + std::cout << "[Warning - Actions::registerLuaEvent] There is no id / aid / uid set for this event" << std::endl; + return false; + } +} + +ReturnValue Actions::canUse(const Player* player, const Position& pos) +{ + if (pos.x != 0xFFFF) { + const Position& playerPos = player->getPosition(); + if (playerPos.z != pos.z) { + return playerPos.z > pos.z ? RETURNVALUE_FIRSTGOUPSTAIRS : RETURNVALUE_FIRSTGODOWNSTAIRS; + } + + if (!Position::areInRange<1, 1>(playerPos, pos)) { + return RETURNVALUE_TOOFARAWAY; + } + } + return RETURNVALUE_NOERROR; +} + +ReturnValue Actions::canUse(const Player* player, const Position& pos, const Item* item) +{ + Action* action = getAction(item); + if (action) { + return action->canExecuteAction(player, pos); + } + return RETURNVALUE_NOERROR; +} + +ReturnValue Actions::canUseFar(const Creature* creature, const Position& toPos, bool checkLineOfSight, bool checkFloor) +{ + if (toPos.x == 0xFFFF) { + return RETURNVALUE_NOERROR; + } + + const Position& creaturePos = creature->getPosition(); + if (checkFloor && creaturePos.z != toPos.z) { + return creaturePos.z > toPos.z ? RETURNVALUE_FIRSTGOUPSTAIRS : RETURNVALUE_FIRSTGODOWNSTAIRS; + } + + if (!Position::areInRange<7, 5>(toPos, creaturePos)) { + return RETURNVALUE_TOOFARAWAY; + } + + if (checkLineOfSight && !g_game.canThrowObjectTo(creaturePos, toPos)) { + return RETURNVALUE_CANNOTTHROW; + } + + return RETURNVALUE_NOERROR; +} + +Action* Actions::getAction(const Item* item) +{ + if (item->hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)) { + auto it = uniqueItemMap.find(item->getUniqueId()); + if (it != uniqueItemMap.end()) { + return &it->second; + } + } + + if (item->hasAttribute(ITEM_ATTRIBUTE_ACTIONID)) { + auto it = actionItemMap.find(item->getActionId()); + if (it != actionItemMap.end()) { + return &it->second; + } + } + + auto it = useItemMap.find(item->getID()); + if (it != useItemMap.end()) { + return &it->second; + } + + //rune items + return g_spells->getRuneSpell(item->getID()); +} + +ReturnValue Actions::internalUseItem(Player* player, const Position& pos, uint8_t index, Item* item, bool isHotkey) +{ + if (Door* door = item->getDoor()) { + if (!door->canUse(player)) { + return RETURNVALUE_CANNOTUSETHISOBJECT; + } + } + + Action* action = getAction(item); + if (action) { + if (action->isScripted()) { + if (action->executeUse(player, item, pos, nullptr, pos, isHotkey)) { + return RETURNVALUE_NOERROR; + } + + if (item->isRemoved()) { + return RETURNVALUE_CANNOTUSETHISOBJECT; + } + } else if (action->function) { + if (action->function(player, item, pos, nullptr, pos, isHotkey)) { + return RETURNVALUE_NOERROR; + } + } + } + + if (BedItem* bed = item->getBed()) { + if (!bed->canUse(player)) { + return RETURNVALUE_CANNOTUSETHISOBJECT; + } + + if (bed->trySleep(player)) { + player->setBedItem(bed); + g_game.sendOfflineTrainingDialog(player); + } + + return RETURNVALUE_NOERROR; + } + + if (Container* container = item->getContainer()) { + Container* openContainer; + + //depot container + if (DepotLocker* depot = container->getDepotLocker()) { + DepotLocker* myDepotLocker = player->getDepotLocker(depot->getDepotId()); + myDepotLocker->setParent(depot->getParent()->getTile()); + openContainer = myDepotLocker; + player->setLastDepotId(depot->getDepotId()); + } else { + openContainer = container; + } + + uint32_t corpseOwner = container->getCorpseOwner(); + if (corpseOwner != 0 && !player->canOpenCorpse(corpseOwner)) { + return RETURNVALUE_YOUARENOTTHEOWNER; + } + + //open/close container + int32_t oldContainerId = player->getContainerID(openContainer); + if (oldContainerId != -1) { + player->onCloseContainer(openContainer); + player->closeContainer(oldContainerId); + } else { + player->addContainer(index, openContainer); + player->onSendContainer(openContainer); + } + + return RETURNVALUE_NOERROR; + } + + const ItemType& it = Item::items[item->getID()]; + if (it.canReadText) { + if (it.canWriteText) { + player->setWriteItem(item, it.maxTextLen); + player->sendTextWindow(item, it.maxTextLen, true); + } else { + player->setWriteItem(nullptr); + player->sendTextWindow(item, 0, false); + } + + return RETURNVALUE_NOERROR; + } + + return RETURNVALUE_CANNOTUSETHISOBJECT; +} + +bool Actions::useItem(Player* player, const Position& pos, uint8_t index, Item* item, bool isHotkey) +{ + player->setNextAction(OTSYS_TIME() + g_config.getNumber(ConfigManager::ACTIONS_DELAY_INTERVAL)); + player->stopWalk(); + + if (isHotkey) { + uint16_t subType = item->getSubType(); + showUseHotkeyMessage(player, item, player->getItemTypeCount(item->getID(), subType != item->getItemCount() ? subType : -1)); + } + + ReturnValue ret = internalUseItem(player, pos, index, item, isHotkey); + if (ret != RETURNVALUE_NOERROR) { + player->sendCancelMessage(ret); + return false; + } + return true; +} + +bool Actions::useItemEx(Player* player, const Position& fromPos, const Position& toPos, + uint8_t toStackPos, Item* item, bool isHotkey, Creature* creature/* = nullptr*/) +{ + player->setNextAction(OTSYS_TIME() + g_config.getNumber(ConfigManager::EX_ACTIONS_DELAY_INTERVAL)); + player->stopWalk(); + + Action* action = getAction(item); + if (!action) { + player->sendCancelMessage(RETURNVALUE_CANNOTUSETHISOBJECT); + return false; + } + + ReturnValue ret = action->canExecuteAction(player, toPos); + if (ret != RETURNVALUE_NOERROR) { + player->sendCancelMessage(ret); + return false; + } + + if (isHotkey) { + uint16_t subType = item->getSubType(); + showUseHotkeyMessage(player, item, player->getItemTypeCount(item->getID(), subType != item->getItemCount() ? subType : -1)); + } + + if (!action->executeUse(player, item, fromPos, action->getTarget(player, creature, toPos, toStackPos), toPos, isHotkey)) { + if (!action->hasOwnErrorHandler()) { + player->sendCancelMessage(RETURNVALUE_CANNOTUSETHISOBJECT); + } + return false; + } + return true; +} + +void Actions::showUseHotkeyMessage(Player* player, const Item* item, uint32_t count) +{ + std::ostringstream ss; + + const ItemType& it = Item::items[item->getID()]; + if (!it.showCount) { + ss << "Using one of " << item->getName() << "..."; + } else if (count == 1) { + ss << "Using the last " << item->getName() << "..."; + } else { + ss << "Using one of " << count << ' ' << item->getPluralName() << "..."; + } + player->sendTextMessage(MESSAGE_INFO_DESCR, ss.str()); +} + +Action::Action(LuaScriptInterface* interface) : + Event(interface), function(nullptr), allowFarUse(false), checkFloor(true), checkLineOfSight(true) {} + +bool Action::configureEvent(const pugi::xml_node& node) +{ + pugi::xml_attribute allowFarUseAttr = node.attribute("allowfaruse"); + if (allowFarUseAttr) { + allowFarUse = allowFarUseAttr.as_bool(); + } + + pugi::xml_attribute blockWallsAttr = node.attribute("blockwalls"); + if (blockWallsAttr) { + checkLineOfSight = blockWallsAttr.as_bool(); + } + + pugi::xml_attribute checkFloorAttr = node.attribute("checkfloor"); + if (checkFloorAttr) { + checkFloor = checkFloorAttr.as_bool(); + } + + return true; +} + +namespace { + +bool enterMarket(Player* player, Item*, const Position&, Thing*, const Position&, bool) +{ + if (player->getLastDepotId() == -1) { + return false; + } + + player->sendMarketEnter(player->getLastDepotId()); + return true; +} + +} + +bool Action::loadFunction(const pugi::xml_attribute& attr, bool isScripted) +{ + const char* functionName = attr.as_string(); + if (strcasecmp(functionName, "market") == 0) { + function = enterMarket; + } else { + if (!isScripted) { + std::cout << "[Warning - Action::loadFunction] Function \"" << functionName << "\" does not exist." << std::endl; + return false; + } + } + + if (!isScripted) { + scripted = false; + } + return true; +} + +std::string Action::getScriptEventName() const +{ + return "onUse"; +} + +ReturnValue Action::canExecuteAction(const Player* player, const Position& toPos) +{ + if (!allowFarUse) { + return g_actions->canUse(player, toPos); + } else { + return g_actions->canUseFar(player, toPos, checkLineOfSight, checkFloor); + } +} + +Thing* Action::getTarget(Player* player, Creature* targetCreature, const Position& toPosition, uint8_t toStackPos) const +{ + if (targetCreature) { + return targetCreature; + } + return g_game.internalGetThing(player, toPosition, toStackPos, 0, STACKPOS_USETARGET); +} + +bool Action::executeUse(Player* player, Item* item, const Position& fromPosition, Thing* target, const Position& toPosition, bool isHotkey) +{ + //onUse(player, item, fromPosition, target, toPosition, isHotkey) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - Action::executeUse] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushThing(L, item); + LuaScriptInterface::pushPosition(L, fromPosition); + + LuaScriptInterface::pushThing(L, target); + LuaScriptInterface::pushPosition(L, toPosition); + + LuaScriptInterface::pushBoolean(L, isHotkey); + return scriptInterface->callFunction(6); +} diff --git a/src/actions.h b/src/actions.h new file mode 100644 index 0000000..69ca1a9 --- /dev/null +++ b/src/actions.h @@ -0,0 +1,144 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_ACTIONS_H_87F60C5F587E4B84948F304A6451E6E6 +#define FS_ACTIONS_H_87F60C5F587E4B84948F304A6451E6E6 + +#include "baseevents.h" +#include "enums.h" +#include "luascript.h" + +class Action; +using Action_ptr = std::unique_ptr; +using ActionFunction = std::function; + +class Action : public Event +{ + public: + explicit Action(LuaScriptInterface* interface); + + bool configureEvent(const pugi::xml_node& node) override; + bool loadFunction(const pugi::xml_attribute& attr, bool isScripted) override; + + //scripting + virtual bool executeUse(Player* player, Item* item, const Position& fromPosition, + Thing* target, const Position& toPosition, bool isHotkey); + + bool getAllowFarUse() const { + return allowFarUse; + } + void setAllowFarUse(bool v) { + allowFarUse = v; + } + + bool getCheckLineOfSight() const { + return checkLineOfSight; + } + void setCheckLineOfSight(bool v) { + checkLineOfSight = v; + } + + bool getCheckFloor() const { + return checkFloor; + } + void setCheckFloor(bool v) { + checkFloor = v; + } + + std::vector getItemIdRange() { + return ids; + } + void addItemId(uint16_t id) { + ids.emplace_back(id); + } + + std::vector getUniqueIdRange() { + return uids; + } + void addUniqueId(uint16_t id) { + uids.emplace_back(id); + } + + std::vector getActionIdRange() { + return aids; + } + void addActionId(uint16_t id) { + aids.emplace_back(id); + } + + virtual ReturnValue canExecuteAction(const Player* player, const Position& toPos); + virtual bool hasOwnErrorHandler() { + return false; + } + virtual Thing* getTarget(Player* player, Creature* targetCreature, const Position& toPosition, uint8_t toStackPos) const; + + ActionFunction function; + + private: + std::string getScriptEventName() const override; + + bool allowFarUse = false; + bool checkFloor = true; + bool checkLineOfSight = true; + std::vector ids; + std::vector uids; + std::vector aids; +}; + +class Actions final : public BaseEvents +{ + public: + Actions(); + ~Actions(); + + // non-copyable + Actions(const Actions&) = delete; + Actions& operator=(const Actions&) = delete; + + bool useItem(Player* player, const Position& pos, uint8_t index, Item* item, bool isHotkey); + bool useItemEx(Player* player, const Position& fromPos, const Position& toPos, uint8_t toStackPos, Item* item, bool isHotkey, Creature* creature = nullptr); + + ReturnValue canUse(const Player* player, const Position& pos); + ReturnValue canUse(const Player* player, const Position& pos, const Item* item); + ReturnValue canUseFar(const Creature* creature, const Position& toPos, bool checkLineOfSight, bool checkFloor); + + bool registerLuaEvent(Action* event); + void clear(bool fromLua) override final; + + private: + ReturnValue internalUseItem(Player* player, const Position& pos, uint8_t index, Item* item, bool isHotkey); + static void showUseHotkeyMessage(Player* player, const Item* item, uint32_t count); + + LuaScriptInterface& getScriptInterface() override; + std::string getScriptBaseName() const override; + Event_ptr getEvent(const std::string& nodeName) override; + bool registerEvent(Event_ptr event, const pugi::xml_node& node) override; + + using ActionUseMap = std::map; + ActionUseMap useItemMap; + ActionUseMap uniqueItemMap; + ActionUseMap actionItemMap; + + Action* getAction(const Item* item); + void clearMap(ActionUseMap& map, bool fromLua); + + LuaScriptInterface scriptInterface; +}; + +#endif diff --git a/src/ban.cpp b/src/ban.cpp new file mode 100644 index 0000000..0b75ceb --- /dev/null +++ b/src/ban.cpp @@ -0,0 +1,127 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "ban.h" +#include "database.h" +#include "databasetasks.h" +#include "tools.h" + +bool Ban::acceptConnection(uint32_t clientIP) +{ + std::lock_guard lockClass(lock); + + uint64_t currentTime = OTSYS_TIME(); + + auto it = ipConnectMap.find(clientIP); + if (it == ipConnectMap.end()) { + ipConnectMap.emplace(clientIP, ConnectBlock(currentTime, 0, 1)); + return true; + } + + ConnectBlock& connectBlock = it->second; + if (connectBlock.blockTime > currentTime) { + connectBlock.blockTime += 250; + return false; + } + + int64_t timeDiff = currentTime - connectBlock.lastAttempt; + connectBlock.lastAttempt = currentTime; + if (timeDiff <= 5000) { + if (++connectBlock.count > 5) { + connectBlock.count = 0; + if (timeDiff <= 500) { + connectBlock.blockTime = currentTime + 3000; + return false; + } + } + } else { + connectBlock.count = 1; + } + return true; +} + +bool IOBan::isAccountBanned(uint32_t accountId, BanInfo& banInfo) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `reason`, `expires_at`, `banned_at`, `banned_by`, (SELECT `name` FROM `players` WHERE `id` = `banned_by`) AS `name` FROM `account_bans` WHERE `account_id` = " << accountId; + + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return false; + } + + int64_t expiresAt = result->getNumber("expires_at"); + if (expiresAt != 0 && time(nullptr) > expiresAt) { + // Move the ban to history if it has expired + query.str(std::string()); + query << "INSERT INTO `account_ban_history` (`account_id`, `reason`, `banned_at`, `expired_at`, `banned_by`) VALUES (" << accountId << ',' << db.escapeString(result->getString("reason")) << ',' << result->getNumber("banned_at") << ',' << expiresAt << ',' << result->getNumber("banned_by") << ')'; + g_databaseTasks.addTask(query.str()); + + query.str(std::string()); + query << "DELETE FROM `account_bans` WHERE `account_id` = " << accountId; + g_databaseTasks.addTask(query.str()); + return false; + } + + banInfo.expiresAt = expiresAt; + banInfo.reason = result->getString("reason"); + banInfo.bannedBy = result->getString("name"); + return true; +} + +bool IOBan::isIpBanned(uint32_t clientIP, BanInfo& banInfo) +{ + if (clientIP == 0) { + return false; + } + + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `reason`, `expires_at`, (SELECT `name` FROM `players` WHERE `id` = `banned_by`) AS `name` FROM `ip_bans` WHERE `ip` = " << clientIP; + + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return false; + } + + int64_t expiresAt = result->getNumber("expires_at"); + if (expiresAt != 0 && time(nullptr) > expiresAt) { + query.str(std::string()); + query << "DELETE FROM `ip_bans` WHERE `ip` = " << clientIP; + g_databaseTasks.addTask(query.str()); + return false; + } + + banInfo.expiresAt = expiresAt; + banInfo.reason = result->getString("reason"); + banInfo.bannedBy = result->getString("name"); + return true; +} + +bool IOBan::isPlayerNamelocked(uint32_t playerId) +{ + std::ostringstream query; + query << "SELECT 1 FROM `player_namelocks` WHERE `player_id` = " << playerId; + return Database::getInstance().storeQuery(query.str()).get() != nullptr; +} diff --git a/src/ban.h b/src/ban.h new file mode 100644 index 0000000..850d7cc --- /dev/null +++ b/src/ban.h @@ -0,0 +1,58 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_BAN_H_CADB975222D745F0BDA12D982F1006E3 +#define FS_BAN_H_CADB975222D745F0BDA12D982F1006E3 + +struct BanInfo { + std::string bannedBy; + std::string reason; + time_t expiresAt; +}; + +struct ConnectBlock { + constexpr ConnectBlock(uint64_t lastAttempt, uint64_t blockTime, uint32_t count) : + lastAttempt(lastAttempt), blockTime(blockTime), count(count) {} + + uint64_t lastAttempt; + uint64_t blockTime; + uint32_t count; +}; + +using IpConnectMap = std::map; + +class Ban +{ + public: + bool acceptConnection(uint32_t clientIP); + + private: + IpConnectMap ipConnectMap; + std::recursive_mutex lock; +}; + +class IOBan +{ + public: + static bool isAccountBanned(uint32_t accountId, BanInfo& banInfo); + static bool isIpBanned(uint32_t clientIP, BanInfo& banInfo); + static bool isPlayerNamelocked(uint32_t playerId); +}; + +#endif diff --git a/src/baseevents.cpp b/src/baseevents.cpp new file mode 100644 index 0000000..4dc648c --- /dev/null +++ b/src/baseevents.cpp @@ -0,0 +1,188 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "baseevents.h" + +#include "pugicast.h" +#include "tools.h" + +extern LuaEnvironment g_luaEnvironment; + +bool BaseEvents::loadFromXml() +{ + if (loaded) { + std::cout << "[Error - BaseEvents::loadFromXml] It's already loaded." << std::endl; + return false; + } + + std::string scriptsName = getScriptBaseName(); + std::string basePath = "data/" + scriptsName + "/"; + if (getScriptInterface().loadFile(basePath + "lib/" + scriptsName + ".lua") == -1) { + std::cout << "[Warning - BaseEvents::loadFromXml] Can not load " << scriptsName << " lib/" << scriptsName << ".lua" << std::endl; + } + + std::string filename = basePath + scriptsName + ".xml"; + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(filename.c_str()); + if (!result) { + printXMLError("Error - BaseEvents::loadFromXml", filename, result); + return false; + } + + loaded = true; + + for (auto node : doc.child(scriptsName.c_str()).children()) { + Event_ptr event = getEvent(node.name()); + if (!event) { + continue; + } + + if (!event->configureEvent(node)) { + std::cout << "[Warning - BaseEvents::loadFromXml] Failed to configure event" << std::endl; + continue; + } + + bool success; + + pugi::xml_attribute scriptAttribute = node.attribute("script"); + if (scriptAttribute) { + std::string scriptFile = "scripts/" + std::string(scriptAttribute.as_string()); + success = event->checkScript(basePath, scriptsName, scriptFile) && event->loadScript(basePath + scriptFile); + if (node.attribute("function")) { + event->loadFunction(node.attribute("function"), true); + } + } else { + success = event->loadFunction(node.attribute("function"), false); + } + + if (success) { + registerEvent(std::move(event), node); + } + } + return true; +} + +bool BaseEvents::reload() +{ + loaded = false; + clear(false); + return loadFromXml(); +} + +void BaseEvents::reInitState(bool fromLua) +{ + if (!fromLua) { + getScriptInterface().reInitState(); + } +} + +Event::Event(LuaScriptInterface* interface) : scriptInterface(interface) {} + +bool Event::checkScript(const std::string& basePath, const std::string& scriptsName, const std::string& scriptFile) const +{ + LuaScriptInterface* testInterface = g_luaEnvironment.getTestInterface(); + testInterface->reInitState(); + + if (testInterface->loadFile(std::string(basePath + "lib/" + scriptsName + ".lua")) == -1) { + std::cout << "[Warning - Event::checkScript] Can not load " << scriptsName << " lib/" << scriptsName << ".lua" << std::endl; + } + + if (scriptId != 0) { + std::cout << "[Failure - Event::checkScript] scriptid = " << scriptId << std::endl; + return false; + } + + if (testInterface->loadFile(basePath + scriptFile) == -1) { + std::cout << "[Warning - Event::checkScript] Can not load script: " << scriptFile << std::endl; + std::cout << testInterface->getLastLuaError() << std::endl; + return false; + } + + int32_t id = testInterface->getEvent(getScriptEventName()); + if (id == -1) { + std::cout << "[Warning - Event::checkScript] Event " << getScriptEventName() << " not found. " << scriptFile << std::endl; + return false; + } + return true; +} + +bool Event::loadScript(const std::string& scriptFile) +{ + if (!scriptInterface || scriptId != 0) { + std::cout << "Failure: [Event::loadScript] scriptInterface == nullptr. scriptid = " << scriptId << std::endl; + return false; + } + + if (scriptInterface->loadFile(scriptFile) == -1) { + std::cout << "[Warning - Event::loadScript] Can not load script. " << scriptFile << std::endl; + std::cout << scriptInterface->getLastLuaError() << std::endl; + return false; + } + + int32_t id = scriptInterface->getEvent(getScriptEventName()); + if (id == -1) { + std::cout << "[Warning - Event::loadScript] Event " << getScriptEventName() << " not found. " << scriptFile << std::endl; + return false; + } + + scripted = true; + scriptId = id; + return true; +} + +bool Event::loadCallback() +{ + if (!scriptInterface || scriptId != 0) { + std::cout << "Failure: [Event::loadCallback] scriptInterface == nullptr. scriptid = " << scriptId << std::endl; + return false; + } + + int32_t id = scriptInterface->getEvent(); + if (id == -1) { + std::cout << "[Warning - Event::loadCallback] Event " << getScriptEventName() << " not found. " << std::endl; + return false; + } + + scripted = true; + scriptId = id; + return true; +} + +bool CallBack::loadCallBack(LuaScriptInterface* interface, const std::string& name) +{ + if (!interface) { + std::cout << "Failure: [CallBack::loadCallBack] scriptInterface == nullptr" << std::endl; + return false; + } + + scriptInterface = interface; + + int32_t id = scriptInterface->getEvent(name.c_str()); + if (id == -1) { + std::cout << "[Warning - CallBack::loadCallBack] Event " << name << " not found." << std::endl; + return false; + } + + scriptId = id; + loaded = true; + return true; +} diff --git a/src/baseevents.h b/src/baseevents.h new file mode 100644 index 0000000..451bf06 --- /dev/null +++ b/src/baseevents.h @@ -0,0 +1,99 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_BASEEVENTS_H_9994E32C91CE4D95912A5FDD1F41884A +#define FS_BASEEVENTS_H_9994E32C91CE4D95912A5FDD1F41884A + +#include "luascript.h" + +class Event; +using Event_ptr = std::unique_ptr; + +class Event +{ + public: + explicit Event(LuaScriptInterface* interface); + virtual ~Event() = default; + + virtual bool configureEvent(const pugi::xml_node& node) = 0; + + bool checkScript(const std::string& basePath, const std::string& scriptsName, const std::string& scriptFile) const; + bool loadScript(const std::string& scriptFile); + bool loadCallback(); + virtual bool loadFunction(const pugi::xml_attribute&, bool) { + return false; + } + + bool isScripted() const { + return scripted; + } + + bool scripted = false; + bool fromLua = false; + + int32_t getScriptId() { + return scriptId; + } + + protected: + virtual std::string getScriptEventName() const = 0; + + int32_t scriptId = 0; + LuaScriptInterface* scriptInterface = nullptr; +}; + +class BaseEvents +{ + public: + constexpr BaseEvents() = default; + virtual ~BaseEvents() = default; + + bool loadFromXml(); + bool reload(); + bool isLoaded() const { + return loaded; + } + void reInitState(bool fromLua); + + private: + virtual LuaScriptInterface& getScriptInterface() = 0; + virtual std::string getScriptBaseName() const = 0; + virtual Event_ptr getEvent(const std::string& nodeName) = 0; + virtual bool registerEvent(Event_ptr event, const pugi::xml_node& node) = 0; + virtual void clear(bool) = 0; + + bool loaded = false; +}; + +class CallBack +{ + public: + CallBack() = default; + + bool loadCallBack(LuaScriptInterface* interface, const std::string& name); + + protected: + int32_t scriptId = 0; + LuaScriptInterface* scriptInterface = nullptr; + + private: + bool loaded = false; +}; + +#endif diff --git a/src/bed.cpp b/src/bed.cpp new file mode 100644 index 0000000..a66a573 --- /dev/null +++ b/src/bed.cpp @@ -0,0 +1,277 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "bed.h" +#include "game.h" +#include "iologindata.h" +#include "scheduler.h" + +extern Game g_game; + +BedItem::BedItem(uint16_t id) : Item(id) +{ + internalRemoveSleeper(); +} + +Attr_ReadValue BedItem::readAttr(AttrTypes_t attr, PropStream& propStream) +{ + switch (attr) { + case ATTR_SLEEPERGUID: { + uint32_t guid; + if (!propStream.read(guid)) { + return ATTR_READ_ERROR; + } + + if (guid != 0) { + std::string name = IOLoginData::getNameByGuid(guid); + if (!name.empty()) { + setSpecialDescription(name + " is sleeping there."); + g_game.setBedSleeper(this, guid); + sleeperGUID = guid; + } + } + return ATTR_READ_CONTINUE; + } + + case ATTR_SLEEPSTART: { + uint32_t sleep_start; + if (!propStream.read(sleep_start)) { + return ATTR_READ_ERROR; + } + + sleepStart = static_cast(sleep_start); + return ATTR_READ_CONTINUE; + } + + default: + break; + } + return Item::readAttr(attr, propStream); +} + +void BedItem::serializeAttr(PropWriteStream& propWriteStream) const +{ + if (sleeperGUID != 0) { + propWriteStream.write(ATTR_SLEEPERGUID); + propWriteStream.write(sleeperGUID); + } + + if (sleepStart != 0) { + propWriteStream.write(ATTR_SLEEPSTART); + // FIXME: should be stored as 64-bit, but we need to retain backwards compatibility + propWriteStream.write(static_cast(sleepStart)); + } +} + +BedItem* BedItem::getNextBedItem() const +{ + Direction dir = Item::items[id].bedPartnerDir; + Position targetPos = getNextPosition(dir, getPosition()); + + Tile* tile = g_game.map.getTile(targetPos); + if (!tile) { + return nullptr; + } + return tile->getBedItem(); +} + +bool BedItem::canUse(Player* player) +{ + if (!player || !house || !player->isPremium()) { + return false; + } + + if (sleeperGUID == 0) { + return true; + } + + if (house->getHouseAccessLevel(player) == HOUSE_OWNER) { + return true; + } + + Player sleeper(nullptr); + if (!IOLoginData::loadPlayerById(&sleeper, sleeperGUID)) { + return false; + } + + if (house->getHouseAccessLevel(&sleeper) > house->getHouseAccessLevel(player)) { + return false; + } + return true; +} + +bool BedItem::trySleep(Player* player) +{ + if (!house || player->isRemoved()) { + return false; + } + + if (sleeperGUID != 0) { + if (Item::items[id].transformToFree != 0 && house->getOwner() == player->getGUID()) { + wakeUp(nullptr); + } + + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + return true; +} + +bool BedItem::sleep(Player* player) +{ + if (!house) { + return false; + } + + if (sleeperGUID != 0) { + return false; + } + + BedItem* nextBedItem = getNextBedItem(); + + internalSetSleeper(player); + + if (nextBedItem) { + nextBedItem->internalSetSleeper(player); + } + + // update the bedSleepersMap + g_game.setBedSleeper(this, player->getGUID()); + + // make the player walk onto the bed + g_game.map.moveCreature(*player, *getTile()); + + // display 'Zzzz'/sleep effect + g_game.addMagicEffect(player->getPosition(), CONST_ME_SLEEP); + + // kick player after he sees himself walk onto the bed and it change id + uint32_t playerId = player->getID(); + g_scheduler.addEvent(createSchedulerTask(SCHEDULER_MINTICKS, std::bind(&Game::kickPlayer, &g_game, playerId, false))); + + // change self and partner's appearance + updateAppearance(player); + + if (nextBedItem) { + nextBedItem->updateAppearance(player); + } + + return true; +} + +void BedItem::wakeUp(Player* player) +{ + if (!house) { + return; + } + + if (sleeperGUID != 0) { + if (!player) { + Player regenPlayer(nullptr); + if (IOLoginData::loadPlayerById(®enPlayer, sleeperGUID)) { + regeneratePlayer(®enPlayer); + IOLoginData::savePlayer(®enPlayer); + } + } else { + regeneratePlayer(player); + g_game.addCreatureHealth(player); + } + } + + // update the bedSleepersMap + g_game.removeBedSleeper(sleeperGUID); + + BedItem* nextBedItem = getNextBedItem(); + + // unset sleep info + internalRemoveSleeper(); + + if (nextBedItem) { + nextBedItem->internalRemoveSleeper(); + } + + // change self and partner's appearance + updateAppearance(nullptr); + + if (nextBedItem) { + nextBedItem->updateAppearance(nullptr); + } +} + +void BedItem::regeneratePlayer(Player* player) const +{ + const uint32_t sleptTime = time(nullptr) - sleepStart; + + Condition* condition = player->getCondition(CONDITION_REGENERATION, CONDITIONID_DEFAULT); + if (condition) { + uint32_t regen; + if (condition->getTicks() != -1) { + regen = std::min((condition->getTicks() / 1000), sleptTime) / 30; + const int32_t newRegenTicks = condition->getTicks() - (regen * 30000); + if (newRegenTicks <= 0) { + player->removeCondition(condition); + } else { + condition->setTicks(newRegenTicks); + } + } else { + regen = sleptTime / 30; + } + + player->changeHealth(regen, false); + player->changeMana(regen); + } + + const int32_t soulRegen = sleptTime / (60 * 15); + player->changeSoul(soulRegen); +} + +void BedItem::updateAppearance(const Player* player) +{ + const ItemType& it = Item::items[id]; + if (it.type == ITEM_TYPE_BED) { + if (player && it.transformToOnUse[player->getSex()] != 0) { + const ItemType& newType = Item::items[it.transformToOnUse[player->getSex()]]; + if (newType.type == ITEM_TYPE_BED) { + g_game.transformItem(this, it.transformToOnUse[player->getSex()]); + } + } else if (it.transformToFree != 0) { + const ItemType& newType = Item::items[it.transformToFree]; + if (newType.type == ITEM_TYPE_BED) { + g_game.transformItem(this, it.transformToFree); + } + } + } +} + +void BedItem::internalSetSleeper(const Player* player) +{ + std::string desc_str = player->getName() + " is sleeping there."; + + sleeperGUID = player->getGUID(); + sleepStart = time(nullptr); + setSpecialDescription(desc_str); +} + +void BedItem::internalRemoveSleeper() +{ + sleeperGUID = 0; + sleepStart = 0; + setSpecialDescription("Nobody is sleeping there."); +} diff --git a/src/bed.h b/src/bed.h new file mode 100644 index 0000000..ccc37cc --- /dev/null +++ b/src/bed.h @@ -0,0 +1,74 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_BED_H_84DE19758D424C6C9789189231946BFF +#define FS_BED_H_84DE19758D424C6C9789189231946BFF + +#include "item.h" + +class House; +class Player; + +class BedItem final : public Item +{ + public: + explicit BedItem(uint16_t id); + + BedItem* getBed() override { + return this; + } + const BedItem* getBed() const override { + return this; + } + + Attr_ReadValue readAttr(AttrTypes_t attr, PropStream& propStream) override; + void serializeAttr(PropWriteStream& propWriteStream) const override; + + bool canRemove() const override { + return house == nullptr; + } + + uint32_t getSleeper() const { + return sleeperGUID; + } + + void setHouse(House* h) { + house = h; + } + + bool canUse(Player* player); + + bool trySleep(Player* player); + bool sleep(Player* player); + void wakeUp(Player* player); + + BedItem* getNextBedItem() const; + + private: + void updateAppearance(const Player* player); + void regeneratePlayer(Player* player) const; + void internalSetSleeper(const Player* player); + void internalRemoveSleeper(); + + House* house = nullptr; + uint64_t sleepStart; + uint32_t sleeperGUID; +}; + +#endif diff --git a/src/chat.cpp b/src/chat.cpp new file mode 100644 index 0000000..3ec62d7 --- /dev/null +++ b/src/chat.cpp @@ -0,0 +1,643 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "chat.h" +#include "game.h" +#include "pugicast.h" +#include "scheduler.h" + +extern Chat* g_chat; +extern Game g_game; + +bool PrivateChatChannel::isInvited(uint32_t guid) const +{ + if (guid == getOwner()) { + return true; + } + return invites.find(guid) != invites.end(); +} + +bool PrivateChatChannel::removeInvite(uint32_t guid) +{ + return invites.erase(guid) != 0; +} + +void PrivateChatChannel::invitePlayer(const Player& player, Player& invitePlayer) +{ + auto result = invites.emplace(invitePlayer.getGUID(), &invitePlayer); + if (!result.second) { + return; + } + + std::ostringstream ss; + ss << player.getName() << " invites you to " << (player.getSex() == PLAYERSEX_FEMALE ? "her" : "his") << " private chat channel."; + invitePlayer.sendTextMessage(MESSAGE_INFO_DESCR, ss.str()); + + ss.str(std::string()); + ss << invitePlayer.getName() << " has been invited."; + player.sendTextMessage(MESSAGE_INFO_DESCR, ss.str()); + + for (const auto& it : users) { + it.second->sendChannelEvent(id, invitePlayer.getName(), CHANNELEVENT_INVITE); + } +} + +void PrivateChatChannel::excludePlayer(const Player& player, Player& excludePlayer) +{ + if (!removeInvite(excludePlayer.getGUID())) { + return; + } + + removeUser(excludePlayer); + + std::ostringstream ss; + ss << excludePlayer.getName() << " has been excluded."; + player.sendTextMessage(MESSAGE_INFO_DESCR, ss.str()); + + excludePlayer.sendClosePrivate(id); + + for (const auto& it : users) { + it.second->sendChannelEvent(id, excludePlayer.getName(), CHANNELEVENT_EXCLUDE); + } +} + +void PrivateChatChannel::closeChannel() const +{ + for (const auto& it : users) { + it.second->sendClosePrivate(id); + } +} + +bool ChatChannel::addUser(Player& player) +{ + if (users.find(player.getID()) != users.end()) { + return false; + } + + if (!executeOnJoinEvent(player)) { + return false; + } + + // TODO: Move to script when guild channels can be scripted + if (id == CHANNEL_GUILD) { + Guild* guild = player.getGuild(); + if (guild && !guild->getMotd().empty()) { + g_scheduler.addEvent(createSchedulerTask(150, std::bind(&Game::sendGuildMotd, &g_game, player.getID()))); + } + } + + if (!publicChannel) { + for (const auto& it : users) { + it.second->sendChannelEvent(id, player.getName(), CHANNELEVENT_JOIN); + } + } + + users[player.getID()] = &player; + return true; +} + +bool ChatChannel::removeUser(const Player& player) +{ + auto iter = users.find(player.getID()); + if (iter == users.end()) { + return false; + } + + users.erase(iter); + + if (!publicChannel) { + for (const auto& it : users) { + it.second->sendChannelEvent(id, player.getName(), CHANNELEVENT_LEAVE); + } + } + + executeOnLeaveEvent(player); + return true; +} + +bool ChatChannel::hasUser(const Player& player) { + return users.find(player.getID()) != users.end(); +} + +void ChatChannel::sendToAll(const std::string& message, SpeakClasses type) const +{ + for (const auto& it : users) { + it.second->sendChannelMessage("", message, type, id); + } +} + +bool ChatChannel::talk(const Player& fromPlayer, SpeakClasses type, const std::string& text) +{ + if (users.find(fromPlayer.getID()) == users.end()) { + return false; + } + + for (const auto& it : users) { + it.second->sendToChannel(&fromPlayer, type, text, id); + } + return true; +} + +bool ChatChannel::executeCanJoinEvent(const Player& player) +{ + if (canJoinEvent == -1) { + return true; + } + + //canJoin(player) + LuaScriptInterface* scriptInterface = g_chat->getScriptInterface(); + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CanJoinChannelEvent::execute] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(canJoinEvent, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(canJoinEvent); + LuaScriptInterface::pushUserdata(L, &player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + return scriptInterface->callFunction(1); +} + +bool ChatChannel::executeOnJoinEvent(const Player& player) +{ + if (onJoinEvent == -1) { + return true; + } + + //onJoin(player) + LuaScriptInterface* scriptInterface = g_chat->getScriptInterface(); + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - OnJoinChannelEvent::execute] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(onJoinEvent, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(onJoinEvent); + LuaScriptInterface::pushUserdata(L, &player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + return scriptInterface->callFunction(1); +} + +bool ChatChannel::executeOnLeaveEvent(const Player& player) +{ + if (onLeaveEvent == -1) { + return true; + } + + //onLeave(player) + LuaScriptInterface* scriptInterface = g_chat->getScriptInterface(); + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - OnLeaveChannelEvent::execute] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(onLeaveEvent, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(onLeaveEvent); + LuaScriptInterface::pushUserdata(L, &player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + return scriptInterface->callFunction(1); +} + +bool ChatChannel::executeOnSpeakEvent(const Player& player, SpeakClasses& type, const std::string& message) +{ + if (onSpeakEvent == -1) { + return true; + } + + //onSpeak(player, type, message) + LuaScriptInterface* scriptInterface = g_chat->getScriptInterface(); + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - OnSpeakChannelEvent::execute] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(onSpeakEvent, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(onSpeakEvent); + LuaScriptInterface::pushUserdata(L, &player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + lua_pushnumber(L, type); + LuaScriptInterface::pushString(L, message); + + bool result = false; + int size0 = lua_gettop(L); + int ret = scriptInterface->protectedCall(L, 3, 1); + if (ret != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } else if (lua_gettop(L) > 0) { + if (lua_isboolean(L, -1)) { + result = LuaScriptInterface::getBoolean(L, -1); + } else if (lua_isnumber(L, -1)) { + result = true; + type = LuaScriptInterface::getNumber(L, -1); + } + lua_pop(L, 1); + } + + if ((lua_gettop(L) + 4) != size0) { + LuaScriptInterface::reportError(nullptr, "Stack size changed!"); + } + scriptInterface->resetScriptEnv(); + return result; +} + +Chat::Chat(): + scriptInterface("Chat Interface"), + dummyPrivate(CHANNEL_PRIVATE, "Private Chat Channel") +{ + scriptInterface.initState(); +} + +bool Chat::load() +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/chatchannels/chatchannels.xml"); + if (!result) { + printXMLError("Error - Chat::load", "data/chatchannels/chatchannels.xml", result); + return false; + } + + for (auto channelNode : doc.child("channels").children()) { + uint16_t channelId = pugi::cast(channelNode.attribute("id").value()); + std::string channelName = channelNode.attribute("name").as_string(); + bool isPublic = channelNode.attribute("public").as_bool(); + pugi::xml_attribute scriptAttribute = channelNode.attribute("script"); + + auto it = normalChannels.find(channelId); + if (it != normalChannels.end()) { + ChatChannel& channel = it->second; + channel.publicChannel = isPublic; + channel.name = channelName; + + if (scriptAttribute) { + if (scriptInterface.loadFile("data/chatchannels/scripts/" + std::string(scriptAttribute.as_string())) == 0) { + channel.onSpeakEvent = scriptInterface.getEvent("onSpeak"); + channel.canJoinEvent = scriptInterface.getEvent("canJoin"); + channel.onJoinEvent = scriptInterface.getEvent("onJoin"); + channel.onLeaveEvent = scriptInterface.getEvent("onLeave"); + } else { + std::cout << "[Warning - Chat::load] Can not load script: " << scriptAttribute.as_string() << std::endl; + } + } + + UsersMap tempUserMap = std::move(channel.users); + for (const auto& pair : tempUserMap) { + channel.addUser(*pair.second); + } + continue; + } + + ChatChannel channel(channelId, channelName); + channel.publicChannel = isPublic; + + if (scriptAttribute) { + if (scriptInterface.loadFile("data/chatchannels/scripts/" + std::string(scriptAttribute.as_string())) == 0) { + channel.onSpeakEvent = scriptInterface.getEvent("onSpeak"); + channel.canJoinEvent = scriptInterface.getEvent("canJoin"); + channel.onJoinEvent = scriptInterface.getEvent("onJoin"); + channel.onLeaveEvent = scriptInterface.getEvent("onLeave"); + } else { + std::cout << "[Warning - Chat::load] Can not load script: " << scriptAttribute.as_string() << std::endl; + } + } + + normalChannels[channel.id] = channel; + } + return true; +} + +ChatChannel* Chat::createChannel(const Player& player, uint16_t channelId) +{ + if (getChannel(player, channelId)) { + return nullptr; + } + + switch (channelId) { + case CHANNEL_GUILD: { + Guild* guild = player.getGuild(); + if (guild) { + auto ret = guildChannels.emplace(std::make_pair(guild->getId(), ChatChannel(channelId, guild->getName()))); + return &ret.first->second; + } + break; + } + + case CHANNEL_PARTY: { + Party* party = player.getParty(); + if (party) { + auto ret = partyChannels.emplace(std::make_pair(party, ChatChannel(channelId, "Party"))); + return &ret.first->second; + } + break; + } + + case CHANNEL_PRIVATE: { + //only 1 private channel for each premium player + if (!player.isPremium() || getPrivateChannel(player)) { + return nullptr; + } + + //find a free private channel slot + for (uint16_t i = 100; i < 10000; ++i) { + auto ret = privateChannels.emplace(std::make_pair(i, PrivateChatChannel(i, player.getName() + "'s Channel"))); + if (ret.second) { //second is a bool that indicates that a new channel has been placed in the map + auto& newChannel = (*ret.first).second; + newChannel.setOwner(player.getGUID()); + return &newChannel; + } + } + break; + } + + default: + break; + } + return nullptr; +} + +bool Chat::deleteChannel(const Player& player, uint16_t channelId) +{ + switch (channelId) { + case CHANNEL_GUILD: { + Guild* guild = player.getGuild(); + if (!guild) { + return false; + } + + auto it = guildChannels.find(guild->getId()); + if (it == guildChannels.end()) { + return false; + } + + guildChannels.erase(it); + break; + } + + case CHANNEL_PARTY: { + Party* party = player.getParty(); + if (!party) { + return false; + } + + auto it = partyChannels.find(party); + if (it == partyChannels.end()) { + return false; + } + + partyChannels.erase(it); + break; + } + + default: { + auto it = privateChannels.find(channelId); + if (it == privateChannels.end()) { + return false; + } + + it->second.closeChannel(); + + privateChannels.erase(it); + break; + } + } + return true; +} + +ChatChannel* Chat::addUserToChannel(Player& player, uint16_t channelId) +{ + ChatChannel* channel = getChannel(player, channelId); + if (channel && channel->addUser(player)) { + return channel; + } + return nullptr; +} + +bool Chat::removeUserFromChannel(const Player& player, uint16_t channelId) +{ + ChatChannel* channel = getChannel(player, channelId); + if (!channel || !channel->removeUser(player)) { + return false; + } + + if (channel->getOwner() == player.getGUID()) { + deleteChannel(player, channelId); + } + return true; +} + +void Chat::removeUserFromAllChannels(const Player& player) +{ + for (auto& it : normalChannels) { + it.second.removeUser(player); + } + + for (auto& it : partyChannels) { + it.second.removeUser(player); + } + + for (auto& it : guildChannels) { + it.second.removeUser(player); + } + + auto it = privateChannels.begin(); + while (it != privateChannels.end()) { + PrivateChatChannel* channel = &it->second; + channel->removeInvite(player.getGUID()); + channel->removeUser(player); + if (channel->getOwner() == player.getGUID()) { + channel->closeChannel(); + it = privateChannels.erase(it); + } else { + ++it; + } + } +} + +bool Chat::talkToChannel(const Player& player, SpeakClasses type, const std::string& text, uint16_t channelId) +{ + ChatChannel* channel = getChannel(player, channelId); + if (!channel) { + return false; + } + + if (channelId == CHANNEL_GUILD) { + GuildRank_ptr rank = player.getGuildRank(); + if (rank && rank->level > 1) { + type = TALKTYPE_CHANNEL_O; + } else if (type != TALKTYPE_CHANNEL_Y) { + type = TALKTYPE_CHANNEL_Y; + } + } else if (type != TALKTYPE_CHANNEL_Y && (channelId == CHANNEL_PRIVATE || channelId == CHANNEL_PARTY)) { + type = TALKTYPE_CHANNEL_Y; + } + + if (!channel->executeOnSpeakEvent(player, type, text)) { + return false; + } + + return channel->talk(player, type, text); +} + +ChannelList Chat::getChannelList(const Player& player) +{ + ChannelList list; + if (player.getGuild()) { + ChatChannel* channel = getChannel(player, CHANNEL_GUILD); + if (channel) { + list.push_back(channel); + } else { + channel = createChannel(player, CHANNEL_GUILD); + if (channel) { + list.push_back(channel); + } + } + } + + if (player.getParty()) { + ChatChannel* channel = getChannel(player, CHANNEL_PARTY); + if (channel) { + list.push_back(channel); + } else { + channel = createChannel(player, CHANNEL_PARTY); + if (channel) { + list.push_back(channel); + } + } + } + + for (const auto& it : normalChannels) { + ChatChannel* channel = getChannel(player, it.first); + if (channel) { + list.push_back(channel); + } + } + + bool hasPrivate = false; + for (auto& it : privateChannels) { + if (PrivateChatChannel* channel = &it.second) { + uint32_t guid = player.getGUID(); + if (channel->isInvited(guid)) { + list.push_back(channel); + } + + if (channel->getOwner() == guid) { + hasPrivate = true; + } + } + } + + if (!hasPrivate && player.isPremium()) { + list.push_front(&dummyPrivate); + } + return list; +} + +ChatChannel* Chat::getChannel(const Player& player, uint16_t channelId) +{ + switch (channelId) { + case CHANNEL_GUILD: { + Guild* guild = player.getGuild(); + if (guild) { + auto it = guildChannels.find(guild->getId()); + if (it != guildChannels.end()) { + return &it->second; + } + } + break; + } + + case CHANNEL_PARTY: { + Party* party = player.getParty(); + if (party) { + auto it = partyChannels.find(party); + if (it != partyChannels.end()) { + return &it->second; + } + } + break; + } + + default: { + auto it = normalChannels.find(channelId); + if (it != normalChannels.end()) { + ChatChannel& channel = it->second; + if (!channel.executeCanJoinEvent(player)) { + return nullptr; + } + return &channel; + } else { + auto it2 = privateChannels.find(channelId); + if (it2 != privateChannels.end() && it2->second.isInvited(player.getGUID())) { + return &it2->second; + } + } + break; + } + } + return nullptr; +} + +ChatChannel* Chat::getGuildChannelById(uint32_t guildId) +{ + auto it = guildChannels.find(guildId); + if (it == guildChannels.end()) { + return nullptr; + } + return &it->second; +} + +ChatChannel* Chat::getChannelById(uint16_t channelId) +{ + auto it = normalChannels.find(channelId); + if (it == normalChannels.end()) { + return nullptr; + } + return &it->second; +} + +PrivateChatChannel* Chat::getPrivateChannel(const Player& player) +{ + for (auto& it : privateChannels) { + if (it.second.getOwner() == player.getGUID()) { + return &it.second; + } + } + return nullptr; +} diff --git a/src/chat.h b/src/chat.h new file mode 100644 index 0000000..2f4c603 --- /dev/null +++ b/src/chat.h @@ -0,0 +1,164 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_CHAT_H_F1574642D0384ABFAB52B7ED906E5628 +#define FS_CHAT_H_F1574642D0384ABFAB52B7ED906E5628 + +#include "const.h" +#include "luascript.h" + +class Party; +class Player; + +using UsersMap = std::map; +using InvitedMap = std::map; + +class ChatChannel +{ + public: + ChatChannel() = default; + ChatChannel(uint16_t channelId, std::string channelName): + id{channelId}, name{std::move(channelName)} {} + + virtual ~ChatChannel() = default; + + bool addUser(Player& player); + bool removeUser(const Player& player); + bool hasUser(const Player& player); + + bool talk(const Player& fromPlayer, SpeakClasses type, const std::string& text); + void sendToAll(const std::string& message, SpeakClasses type) const; + + const std::string& getName() const { + return name; + } + uint16_t getId() const { + return id; + } + const UsersMap& getUsers() const { + return users; + } + virtual const InvitedMap* getInvitedUsers() const { + return nullptr; + } + + virtual uint32_t getOwner() const { + return 0; + } + + bool isPublicChannel() const { return publicChannel; } + + bool executeOnJoinEvent(const Player& player); + bool executeCanJoinEvent(const Player& player); + bool executeOnLeaveEvent(const Player& player); + bool executeOnSpeakEvent(const Player& player, SpeakClasses& type, const std::string& message); + + protected: + UsersMap users; + + uint16_t id; + + private: + std::string name; + + int32_t canJoinEvent = -1; + int32_t onJoinEvent = -1; + int32_t onLeaveEvent = -1; + int32_t onSpeakEvent = -1; + + bool publicChannel = false; + + friend class Chat; +}; + +class PrivateChatChannel final : public ChatChannel +{ + public: + PrivateChatChannel(uint16_t channelId, std::string channelName) : ChatChannel(channelId, channelName) {} + + uint32_t getOwner() const override { + return owner; + } + void setOwner(uint32_t owner) { + this->owner = owner; + } + + bool isInvited(uint32_t guid) const; + + void invitePlayer(const Player& player, Player& invitePlayer); + void excludePlayer(const Player& player, Player& excludePlayer); + + bool removeInvite(uint32_t guid); + + void closeChannel() const; + + const InvitedMap* getInvitedUsers() const override { + return &invites; + } + + private: + InvitedMap invites; + uint32_t owner = 0; +}; + +using ChannelList = std::list; + +class Chat +{ + public: + Chat(); + + // non-copyable + Chat(const Chat&) = delete; + Chat& operator=(const Chat&) = delete; + + bool load(); + + ChatChannel* createChannel(const Player& player, uint16_t channelId); + bool deleteChannel(const Player& player, uint16_t channelId); + + ChatChannel* addUserToChannel(Player& player, uint16_t channelId); + bool removeUserFromChannel(const Player& player, uint16_t channelId); + void removeUserFromAllChannels(const Player& player); + + bool talkToChannel(const Player& player, SpeakClasses type, const std::string& text, uint16_t channelId); + + ChannelList getChannelList(const Player& player); + + ChatChannel* getChannel(const Player& player, uint16_t channelId); + ChatChannel* getChannelById(uint16_t channelId); + ChatChannel* getGuildChannelById(uint32_t guildId); + PrivateChatChannel* getPrivateChannel(const Player& player); + + LuaScriptInterface* getScriptInterface() { + return &scriptInterface; + } + + private: + std::map normalChannels; + std::map privateChannels; + std::map partyChannels; + std::map guildChannels; + + LuaScriptInterface scriptInterface; + + PrivateChatChannel dummyPrivate; +}; + +#endif diff --git a/src/combat.cpp b/src/combat.cpp new file mode 100644 index 0000000..bd75d0d --- /dev/null +++ b/src/combat.cpp @@ -0,0 +1,1490 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "combat.h" + +#include "game.h" +#include "weapons.h" +#include "configmanager.h" +#include "events.h" + +extern Game g_game; +extern Weapons* g_weapons; +extern ConfigManager g_config; +extern Events* g_events; + +CombatDamage Combat::getCombatDamage(Creature* creature, Creature* target) const +{ + CombatDamage damage; + damage.origin = params.origin; + damage.primary.type = params.combatType; + if (formulaType == COMBAT_FORMULA_DAMAGE) { + damage.primary.value = normal_random( + static_cast(mina), + static_cast(maxa) + ); + } else if (creature) { + int32_t min, max; + if (creature->getCombatValues(min, max)) { + damage.primary.value = normal_random(min, max); + } else if (Player* player = creature->getPlayer()) { + if (params.valueCallback) { + params.valueCallback->getMinMaxValues(player, damage); + } else if (formulaType == COMBAT_FORMULA_LEVELMAGIC) { + int32_t levelFormula = player->getLevel() * 2 + player->getMagicLevel() * 3; + damage.primary.value = normal_random( + static_cast(levelFormula * mina + minb), + static_cast(levelFormula * maxa + maxb) + ); + } else if (formulaType == COMBAT_FORMULA_SKILL) { + Item* tool = player->getWeapon(); + const Weapon* weapon = g_weapons->getWeapon(tool); + if (weapon) { + damage.primary.value = normal_random( + static_cast(minb), + static_cast(weapon->getWeaponDamage(player, target, tool, true) * maxa + maxb) + ); + + damage.secondary.type = weapon->getElementType(); + damage.secondary.value = weapon->getElementDamage(player, target, tool); + } else { + damage.primary.value = normal_random( + static_cast(minb), + static_cast(maxb) + ); + } + } + } + } + return damage; +} + +void Combat::getCombatArea(const Position& centerPos, const Position& targetPos, const AreaCombat* area, std::forward_list& list) +{ + if (targetPos.z >= MAP_MAX_LAYERS) { + return; + } + + if (area) { + area->getList(centerPos, targetPos, list); + } else { + Tile* tile = g_game.map.getTile(targetPos); + if (!tile) { + tile = new StaticTile(targetPos.x, targetPos.y, targetPos.z); + g_game.map.setTile(targetPos, tile); + } + list.push_front(tile); + } +} + +CombatType_t Combat::ConditionToDamageType(ConditionType_t type) +{ + switch (type) { + case CONDITION_FIRE: + return COMBAT_FIREDAMAGE; + + case CONDITION_ENERGY: + return COMBAT_ENERGYDAMAGE; + + case CONDITION_BLEEDING: + return COMBAT_PHYSICALDAMAGE; + + case CONDITION_DROWN: + return COMBAT_DROWNDAMAGE; + + case CONDITION_POISON: + return COMBAT_EARTHDAMAGE; + + case CONDITION_FREEZING: + return COMBAT_ICEDAMAGE; + + case CONDITION_DAZZLED: + return COMBAT_HOLYDAMAGE; + + case CONDITION_CURSED: + return COMBAT_DEATHDAMAGE; + + default: + break; + } + + return COMBAT_NONE; +} + +ConditionType_t Combat::DamageToConditionType(CombatType_t type) +{ + switch (type) { + case COMBAT_FIREDAMAGE: + return CONDITION_FIRE; + + case COMBAT_ENERGYDAMAGE: + return CONDITION_ENERGY; + + case COMBAT_DROWNDAMAGE: + return CONDITION_DROWN; + + case COMBAT_EARTHDAMAGE: + return CONDITION_POISON; + + case COMBAT_ICEDAMAGE: + return CONDITION_FREEZING; + + case COMBAT_HOLYDAMAGE: + return CONDITION_DAZZLED; + + case COMBAT_DEATHDAMAGE: + return CONDITION_CURSED; + + case COMBAT_PHYSICALDAMAGE: + return CONDITION_BLEEDING; + + default: + return CONDITION_NONE; + } +} + +bool Combat::isPlayerCombat(const Creature* target) +{ + if (target->getPlayer()) { + return true; + } + + if (target->isSummon() && target->getMaster()->getPlayer()) { + return true; + } + + return false; +} + +ReturnValue Combat::canTargetCreature(Player* attacker, Creature* target) +{ + if (attacker == target) { + return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; + } + + if (!attacker->hasFlag(PlayerFlag_IgnoreProtectionZone)) { + //pz-zone + if (attacker->getZone() == ZONE_PROTECTION) { + return RETURNVALUE_YOUMAYNOTATTACKAPERSONWHILEINPROTECTIONZONE; + } + + if (target->getZone() == ZONE_PROTECTION) { + return RETURNVALUE_YOUMAYNOTATTACKAPERSONINPROTECTIONZONE; + } + + //nopvp-zone + if (isPlayerCombat(target)) { + if (attacker->getZone() == ZONE_NOPVP) { + return RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE; + } + + if (target->getZone() == ZONE_NOPVP) { + return RETURNVALUE_YOUMAYNOTATTACKAPERSONINPROTECTIONZONE; + } + } + } + + if (attacker->hasFlag(PlayerFlag_CannotUseCombat) || !target->isAttackable()) { + if (target->getPlayer()) { + return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; + } else { + return RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE; + } + } + + if (target->getPlayer()) { + if (isProtected(attacker, target->getPlayer())) { + return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; + } + + if (attacker->hasSecureMode() && !Combat::isInPvpZone(attacker, target) && attacker->getSkullClient(target->getPlayer()) == SKULL_NONE) { + return RETURNVALUE_TURNSECUREMODETOATTACKUNMARKEDPLAYERS; + } + } + + return Combat::canDoCombat(attacker, target); +} + +ReturnValue Combat::canDoCombat(Creature* caster, Tile* tile, bool aggressive) +{ + if (tile->hasProperty(CONST_PROP_BLOCKPROJECTILE)) { + return RETURNVALUE_NOTENOUGHROOM; + } + + if (tile->hasFlag(TILESTATE_FLOORCHANGE)) { + return RETURNVALUE_NOTENOUGHROOM; + } + + if (tile->getTeleportItem()) { + return RETURNVALUE_NOTENOUGHROOM; + } + + if (caster) { + const Position& casterPosition = caster->getPosition(); + const Position& tilePosition = tile->getPosition(); + if (casterPosition.z < tilePosition.z) { + return RETURNVALUE_FIRSTGODOWNSTAIRS; + } else if (casterPosition.z > tilePosition.z) { + return RETURNVALUE_FIRSTGOUPSTAIRS; + } + + if (const Player* player = caster->getPlayer()) { + if (player->hasFlag(PlayerFlag_IgnoreProtectionZone)) { + return RETURNVALUE_NOERROR; + } + } + } + + //pz-zone + if (aggressive && tile->hasFlag(TILESTATE_PROTECTIONZONE)) { + return RETURNVALUE_ACTIONNOTPERMITTEDINPROTECTIONZONE; + } + + return g_events->eventCreatureOnAreaCombat(caster, tile, aggressive); +} + +bool Combat::isInPvpZone(const Creature* attacker, const Creature* target) +{ + return attacker->getZone() == ZONE_PVP && target->getZone() == ZONE_PVP; +} + +bool Combat::isProtected(const Player* attacker, const Player* target) +{ + uint32_t protectionLevel = g_config.getNumber(ConfigManager::PROTECTION_LEVEL); + if (target->getLevel() < protectionLevel || attacker->getLevel() < protectionLevel) { + return true; + } + + if (attacker->getVocationId() == VOCATION_NONE || target->getVocationId() == VOCATION_NONE) { + return true; + } + + if (attacker->getSkull() == SKULL_BLACK && attacker->getSkullClient(target) == SKULL_NONE) { + return true; + } + + return false; +} + +ReturnValue Combat::canDoCombat(Creature* attacker, Creature* target) +{ + if (!attacker) { + return g_events->eventCreatureOnTargetCombat(attacker, target); + } + + if (const Player* targetPlayer = target->getPlayer()) { + if (targetPlayer->hasFlag(PlayerFlag_CannotBeAttacked)) { + return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; + } + + if (const Player* attackerPlayer = attacker->getPlayer()) { + if (attackerPlayer->hasFlag(PlayerFlag_CannotAttackPlayer)) { + return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; + } + + if (isProtected(attackerPlayer, targetPlayer)) { + return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; + } + + //nopvp-zone + const Tile* targetPlayerTile = targetPlayer->getTile(); + if (targetPlayerTile->hasFlag(TILESTATE_NOPVPZONE)) { + return RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE; + } else if (attackerPlayer->getTile()->hasFlag(TILESTATE_NOPVPZONE) && !targetPlayerTile->hasFlag(TILESTATE_NOPVPZONE | TILESTATE_PROTECTIONZONE)) { + return RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE; + } + } + + if (attacker->isSummon()) { + if (const Player* masterAttackerPlayer = attacker->getMaster()->getPlayer()) { + if (masterAttackerPlayer->hasFlag(PlayerFlag_CannotAttackPlayer)) { + return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; + } + + if (targetPlayer->getTile()->hasFlag(TILESTATE_NOPVPZONE)) { + return RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE; + } + + if (isProtected(masterAttackerPlayer, targetPlayer)) { + return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; + } + } + } + } else if (target->getMonster()) { + if (const Player* attackerPlayer = attacker->getPlayer()) { + if (attackerPlayer->hasFlag(PlayerFlag_CannotAttackMonster)) { + return RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE; + } + + if (target->isSummon() && target->getMaster()->getPlayer() && target->getZone() == ZONE_NOPVP) { + return RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE; + } + } else if (attacker->getMonster()) { + const Creature* targetMaster = target->getMaster(); + + if (!targetMaster || !targetMaster->getPlayer()) { + const Creature* attackerMaster = attacker->getMaster(); + + if (!attackerMaster || !attackerMaster->getPlayer()) { + return RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE; + } + } + } + } + + if (g_game.getWorldType() == WORLD_TYPE_NO_PVP) { + if (attacker->getPlayer() || (attacker->isSummon() && attacker->getMaster()->getPlayer())) { + if (target->getPlayer()) { + if (!isInPvpZone(attacker, target)) { + return RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER; + } + } + + if (target->isSummon() && target->getMaster()->getPlayer()) { + if (!isInPvpZone(attacker, target)) { + return RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE; + } + } + } + } + return g_events->eventCreatureOnTargetCombat(attacker, target); +} + +void Combat::setPlayerCombatValues(formulaType_t formulaType, double mina, double minb, double maxa, double maxb) +{ + this->formulaType = formulaType; + this->mina = mina; + this->minb = minb; + this->maxa = maxa; + this->maxb = maxb; +} + +bool Combat::setParam(CombatParam_t param, uint32_t value) +{ + switch (param) { + case COMBAT_PARAM_TYPE: { + params.combatType = static_cast(value); + return true; + } + + case COMBAT_PARAM_EFFECT: { + params.impactEffect = static_cast(value); + return true; + } + + case COMBAT_PARAM_DISTANCEEFFECT: { + params.distanceEffect = static_cast(value); + return true; + } + + case COMBAT_PARAM_BLOCKARMOR: { + params.blockedByArmor = (value != 0); + return true; + } + + case COMBAT_PARAM_BLOCKSHIELD: { + params.blockedByShield = (value != 0); + return true; + } + + case COMBAT_PARAM_TARGETCASTERORTOPMOST: { + params.targetCasterOrTopMost = (value != 0); + return true; + } + + case COMBAT_PARAM_CREATEITEM: { + params.itemId = value; + return true; + } + + case COMBAT_PARAM_AGGRESSIVE: { + params.aggressive = (value != 0); + return true; + } + + case COMBAT_PARAM_DISPEL: { + params.dispelType = static_cast(value); + return true; + } + + case COMBAT_PARAM_USECHARGES: { + params.useCharges = (value != 0); + return true; + } + } + return false; +} + +bool Combat::setCallback(CallBackParam_t key) +{ + switch (key) { + case CALLBACK_PARAM_LEVELMAGICVALUE: { + params.valueCallback.reset(new ValueCallback(COMBAT_FORMULA_LEVELMAGIC)); + return true; + } + + case CALLBACK_PARAM_SKILLVALUE: { + params.valueCallback.reset(new ValueCallback(COMBAT_FORMULA_SKILL)); + return true; + } + + case CALLBACK_PARAM_TARGETTILE: { + params.tileCallback.reset(new TileCallback()); + return true; + } + + case CALLBACK_PARAM_TARGETCREATURE: { + params.targetCallback.reset(new TargetCallback()); + return true; + } + } + return false; +} + +CallBack* Combat::getCallback(CallBackParam_t key) +{ + switch (key) { + case CALLBACK_PARAM_LEVELMAGICVALUE: + case CALLBACK_PARAM_SKILLVALUE: { + return params.valueCallback.get(); + } + + case CALLBACK_PARAM_TARGETTILE: { + return params.tileCallback.get(); + } + + case CALLBACK_PARAM_TARGETCREATURE: { + return params.targetCallback.get(); + } + } + return nullptr; +} + +void Combat::combatTileEffects(const SpectatorVec& spectators, Creature* caster, Tile* tile, const CombatParams& params) +{ + if (params.itemId != 0) { + uint16_t itemId = params.itemId; + switch (itemId) { + case ITEM_FIREFIELD_PERSISTENT_FULL: + itemId = ITEM_FIREFIELD_PVP_FULL; + break; + + case ITEM_FIREFIELD_PERSISTENT_MEDIUM: + itemId = ITEM_FIREFIELD_PVP_MEDIUM; + break; + + case ITEM_FIREFIELD_PERSISTENT_SMALL: + itemId = ITEM_FIREFIELD_PVP_SMALL; + break; + + case ITEM_ENERGYFIELD_PERSISTENT: + itemId = ITEM_ENERGYFIELD_PVP; + break; + + case ITEM_POISONFIELD_PERSISTENT: + itemId = ITEM_POISONFIELD_PVP; + break; + + case ITEM_MAGICWALL_PERSISTENT: + itemId = ITEM_MAGICWALL; + break; + + case ITEM_WILDGROWTH_PERSISTENT: + itemId = ITEM_WILDGROWTH; + break; + + default: + break; + } + + if (caster) { + Player* casterPlayer; + if (caster->isSummon()) { + casterPlayer = caster->getMaster()->getPlayer(); + } else { + casterPlayer = caster->getPlayer(); + } + + if (casterPlayer) { + if (g_game.getWorldType() == WORLD_TYPE_NO_PVP || tile->hasFlag(TILESTATE_NOPVPZONE)) { + if (itemId == ITEM_FIREFIELD_PVP_FULL) { + itemId = ITEM_FIREFIELD_NOPVP; + } else if (itemId == ITEM_POISONFIELD_PVP) { + itemId = ITEM_POISONFIELD_NOPVP; + } else if (itemId == ITEM_ENERGYFIELD_PVP) { + itemId = ITEM_ENERGYFIELD_NOPVP; + } + } else if (itemId == ITEM_FIREFIELD_PVP_FULL || itemId == ITEM_POISONFIELD_PVP || itemId == ITEM_ENERGYFIELD_PVP) { + casterPlayer->addInFightTicks(); + } + } + } + + Item* item = Item::CreateItem(itemId); + if (caster) { + item->setOwner(caster->getID()); + } + + ReturnValue ret = g_game.internalAddItem(tile, item); + if (ret == RETURNVALUE_NOERROR) { + g_game.startDecay(item); + } else { + delete item; + } + } + + if (params.tileCallback) { + params.tileCallback->onTileCombat(caster, tile); + } + + if (params.impactEffect != CONST_ME_NONE) { + Game::addMagicEffect(spectators, tile->getPosition(), params.impactEffect); + } +} + +void Combat::postCombatEffects(Creature* caster, const Position& pos, const CombatParams& params) +{ + if (caster && params.distanceEffect != CONST_ANI_NONE) { + addDistanceEffect(caster, caster->getPosition(), pos, params.distanceEffect); + } +} + +void Combat::addDistanceEffect(Creature* caster, const Position& fromPos, const Position& toPos, uint8_t effect) +{ + if (effect == CONST_ANI_WEAPONTYPE) { + if (!caster) { + return; + } + + Player* player = caster->getPlayer(); + if (!player) { + return; + } + + switch (player->getWeaponType()) { + case WEAPON_AXE: + effect = CONST_ANI_WHIRLWINDAXE; + break; + case WEAPON_SWORD: + effect = CONST_ANI_WHIRLWINDSWORD; + break; + case WEAPON_CLUB: + effect = CONST_ANI_WHIRLWINDCLUB; + break; + default: + effect = CONST_ANI_NONE; + break; + } + } + + if (effect != CONST_ANI_NONE) { + g_game.addDistanceEffect(fromPos, toPos, effect); + } +} + +void Combat::doCombat(Creature* caster, Creature* target) const +{ + //target combat callback function + if (params.combatType != COMBAT_NONE) { + CombatDamage damage = getCombatDamage(caster, target); + + bool canCombat = !params.aggressive || (caster != target && Combat::canDoCombat(caster, target) == RETURNVALUE_NOERROR); + if ((caster == target || canCombat) && params.impactEffect != CONST_ME_NONE) { + g_game.addMagicEffect(target->getPosition(), params.impactEffect); + } + + if (canCombat) { + doTargetCombat(caster, target, damage, params); + } + } else { + if (!params.aggressive || (caster != target && Combat::canDoCombat(caster, target) == RETURNVALUE_NOERROR)) { + SpectatorVec spectators; + g_game.map.getSpectators(spectators, target->getPosition(), true, true); + + if (params.origin != ORIGIN_MELEE) { + for (const auto& condition : params.conditionList) { + if (caster == target || !target->isImmune(condition->getType())) { + Condition* conditionCopy = condition->clone(); + conditionCopy->setParam(CONDITION_PARAM_OWNER, caster->getID()); + target->addCombatCondition(conditionCopy); + } + } + } + + if (params.dispelType == CONDITION_PARALYZE) { + target->removeCondition(CONDITION_PARALYZE); + } else { + target->removeCombatCondition(params.dispelType); + } + + combatTileEffects(spectators, caster, target->getTile(), params); + + if (params.targetCallback) { + params.targetCallback->onTargetCombat(caster, target); + } + + /* + if (params.impactEffect != CONST_ME_NONE) { + g_game.addMagicEffect(target->getPosition(), params.impactEffect); + } + */ + + if (caster && params.distanceEffect != CONST_ANI_NONE) { + addDistanceEffect(caster, caster->getPosition(), target->getPosition(), params.distanceEffect); + } + } + } +} + +void Combat::doCombat(Creature* caster, const Position& position) const +{ + //area combat callback function + if (params.combatType != COMBAT_NONE) { + CombatDamage damage = getCombatDamage(caster, nullptr); + doAreaCombat(caster, position, area.get(), damage, params); + } else { + std::forward_list tileList; + + if (caster) { + getCombatArea(caster->getPosition(), position, area.get(), tileList); + } else { + getCombatArea(position, position, area.get(), tileList); + } + + SpectatorVec spectators; + uint32_t maxX = 0; + uint32_t maxY = 0; + + //calculate the max viewable range + for (Tile* tile : tileList) { + const Position& tilePos = tile->getPosition(); + + uint32_t diff = Position::getDistanceX(tilePos, position); + if (diff > maxX) { + maxX = diff; + } + + diff = Position::getDistanceY(tilePos, position); + if (diff > maxY) { + maxY = diff; + } + } + + const int32_t rangeX = maxX + Map::maxViewportX; + const int32_t rangeY = maxY + Map::maxViewportY; + g_game.map.getSpectators(spectators, position, true, true, rangeX, rangeX, rangeY, rangeY); + + postCombatEffects(caster, position, params); + + for (Tile* tile : tileList) { + if (canDoCombat(caster, tile, params.aggressive) != RETURNVALUE_NOERROR) { + continue; + } + + combatTileEffects(spectators, caster, tile, params); + + if (CreatureVector* creatures = tile->getCreatures()) { + const Creature* topCreature = tile->getTopCreature(); + for (Creature* creature : *creatures) { + if (params.targetCasterOrTopMost) { + if (caster && caster->getTile() == tile) { + if (creature != caster) { + continue; + } + } else if (creature != topCreature) { + continue; + } + } + + if (params.origin != ORIGIN_MELEE) { + for (const auto& condition : params.conditionList) { + if (caster == creature || !creature->isImmune(condition->getType())) { + Condition* conditionCopy = condition->clone(); + if (caster) { + conditionCopy->setParam(CONDITION_PARAM_OWNER, caster->getID()); + } + + //TODO: infight condition until all aggressive conditions has ended + creature->addCombatCondition(conditionCopy); + } + } + } + + if (params.dispelType == CONDITION_PARALYZE) { + creature->removeCondition(CONDITION_PARALYZE); + } else { + creature->removeCombatCondition(params.dispelType); + } + } + } + } + } +} + +void Combat::doTargetCombat(Creature* caster, Creature* target, CombatDamage& damage, const CombatParams& params) +{ + Player* casterPlayer = caster ? caster->getPlayer() : nullptr; + if (casterPlayer) { + if (damage.primary.value < 0 || damage.secondary.value < 0) { + Player* targetPlayer = target ? target->getPlayer() : nullptr; + if (targetPlayer && targetPlayer->getSkull() != SKULL_BLACK) { + damage.primary.value /= 2; + damage.secondary.value /= 2; + } + + Combat::checkCriticalHit(casterPlayer, damage); + Combat::checkLeech(casterPlayer, damage); + } + } + + if (caster && target && params.distanceEffect != CONST_ANI_NONE) { + addDistanceEffect(caster, caster->getPosition(), target->getPosition(), params.distanceEffect); + } + + if (damage.critical && target) { + g_game.addMagicEffect(target->getPosition(), CONST_ME_CRITICAL_DAMAGE); + } + + bool success = false; + if (damage.primary.type != COMBAT_MANADRAIN) { + if (g_game.combatBlockHit(damage, caster, target, params.blockedByShield, params.blockedByArmor, params.itemId != 0)) { + return; + } + + success = g_game.combatChangeHealth(caster, target, damage); + } else { + success = g_game.combatChangeMana(caster, target, damage); + } + + if (success) { + if (damage.blockType == BLOCK_NONE || damage.blockType == BLOCK_ARMOR) { + for (const auto& condition : params.conditionList) { + if (caster == target || !target->isImmune(condition->getType())) { + Condition* conditionCopy = condition->clone(); + if (caster) { + conditionCopy->setParam(CONDITION_PARAM_OWNER, caster->getID()); + } + + //TODO: infight condition until all aggressive conditions has ended + target->addCombatCondition(conditionCopy); + } + } + } + + if (params.dispelType == CONDITION_PARALYZE) { + target->removeCondition(CONDITION_PARALYZE); + } else { + target->removeCombatCondition(params.dispelType); + } + } + + if (params.targetCallback) { + params.targetCallback->onTargetCombat(caster, target); + } +} + +void Combat::doAreaCombat(Creature* caster, const Position& position, const AreaCombat* area, CombatDamage& damage, const CombatParams& params) +{ + std::forward_list tileList; + + if (caster) { + getCombatArea(caster->getPosition(), position, area, tileList); + } else { + getCombatArea(position, position, area, tileList); + } + + Player* casterPlayer = caster ? caster->getPlayer() : nullptr; + int32_t criticalPrimary = 0; + int32_t criticalSecondary = 0; + if (casterPlayer) { + Combat::checkLeech(casterPlayer, damage); + if (!damage.critical && damage.origin != ORIGIN_CONDITION && (damage.primary.value < 0 || damage.secondary.value < 0)) { + uint16_t chance = casterPlayer->getSpecialSkill(SPECIALSKILL_CRITICALHITCHANCE); + if (chance != 0 && uniform_random(1, 100) <= chance) { + uint16_t criticalHit = casterPlayer->getSpecialSkill(SPECIALSKILL_CRITICALHITAMOUNT); + criticalPrimary = std::round(damage.primary.value * (criticalHit / 100.)); + criticalSecondary = std::round(damage.secondary.value * (criticalHit / 100.)); + damage.critical = true; + } + } + } + + uint32_t maxX = 0; + uint32_t maxY = 0; + + //calculate the max viewable range + for (Tile* tile : tileList) { + const Position& tilePos = tile->getPosition(); + + uint32_t diff = Position::getDistanceX(tilePos, position); + if (diff > maxX) { + maxX = diff; + } + + diff = Position::getDistanceY(tilePos, position); + if (diff > maxY) { + maxY = diff; + } + } + + const int32_t rangeX = maxX + Map::maxViewportX; + const int32_t rangeY = maxY + Map::maxViewportY; + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, position, true, true, rangeX, rangeX, rangeY, rangeY); + + postCombatEffects(caster, position, params); + + for (Tile* tile : tileList) { + if (canDoCombat(caster, tile, params.aggressive) != RETURNVALUE_NOERROR) { + continue; + } + + combatTileEffects(spectators, caster, tile, params); + + if (CreatureVector* creatures = tile->getCreatures()) { + const Creature* topCreature = tile->getTopCreature(); + for (Creature* creature : *creatures) { + if (params.targetCasterOrTopMost) { + if (caster && caster->getTile() == tile) { + if (creature != caster) { + continue; + } + } else if (creature != topCreature) { + continue; + } + } + + if (!params.aggressive || (caster != creature && Combat::canDoCombat(caster, creature) == RETURNVALUE_NOERROR)) { + CombatDamage damageCopy = damage; // we cannot avoid copying here, because we don't know if it's player combat or not, so we can't modify the initial damage. + bool playerCombatReduced = false; + if ((damageCopy.primary.value < 0 || damageCopy.secondary.value < 0) && caster) { + Player* targetPlayer = creature->getPlayer(); + if (targetPlayer && caster->getPlayer() && targetPlayer->getSkull() != SKULL_BLACK) { + damageCopy.primary.value /= 2; + damageCopy.secondary.value /= 2; + playerCombatReduced = true; + } + } + + damageCopy.primary.value += playerCombatReduced ? criticalPrimary / 2 : criticalPrimary; + damageCopy.secondary.value += playerCombatReduced ? criticalSecondary / 2 : criticalSecondary; + + if (damageCopy.critical) { + g_game.addMagicEffect(creature->getPosition(), CONST_ME_CRITICAL_DAMAGE); + } + + bool success = false; + if (damageCopy.primary.type != COMBAT_MANADRAIN) { + if (g_game.combatBlockHit(damageCopy, caster, creature, params.blockedByShield, params.blockedByArmor, params.itemId != 0)) { + continue; + } + success = g_game.combatChangeHealth(caster, creature, damageCopy); + } else { + success = g_game.combatChangeMana(caster, creature, damageCopy); + } + + if (success) { + if (damage.blockType == BLOCK_NONE || damage.blockType == BLOCK_ARMOR) { + for (const auto& condition : params.conditionList) { + if (caster == creature || !creature->isImmune(condition->getType())) { + Condition* conditionCopy = condition->clone(); + if (caster) { + conditionCopy->setParam(CONDITION_PARAM_OWNER, caster->getID()); + } + + //TODO: infight condition until all aggressive conditions has ended + creature->addCombatCondition(conditionCopy); + } + } + } + + if (params.dispelType == CONDITION_PARALYZE) { + creature->removeCondition(CONDITION_PARALYZE); + } else { + creature->removeCombatCondition(params.dispelType); + } + } + + if (params.targetCallback) { + params.targetCallback->onTargetCombat(caster, creature); + } + + if (params.targetCasterOrTopMost) { + break; + } + } + } + } + } +} + +void Combat::checkCriticalHit(Player* caster, CombatDamage& damage) +{ + if (damage.critical || damage.origin == ORIGIN_CONDITION) { + return; + } + + if (damage.primary.value > 0 || damage.secondary.value > 0) { + return; + } + + uint16_t chance = caster->getSpecialSkill(SPECIALSKILL_CRITICALHITCHANCE); + uint16_t criticalHit = caster->getSpecialSkill(SPECIALSKILL_CRITICALHITAMOUNT); + if (criticalHit != 0 && chance != 0 && normal_random(1, 100) <= chance) { + damage.primary.value += std::round(damage.primary.value * (criticalHit / 100.)); + damage.secondary.value += std::round(damage.secondary.value * (criticalHit / 100.)); + damage.critical = true; + } +} + +void Combat::checkLeech(Player* caster, CombatDamage& damage) +{ + if (damage.origin == ORIGIN_CONDITION) { + return; + } + + if (damage.primary.value > 0 || damage.secondary.value > 0) { + return; + } + + if (caster->getHealth() != caster->getMaxHealth()) { + uint16_t chance = caster->getSpecialSkill(SPECIALSKILL_LIFELEECHCHANCE); + uint16_t skill = caster->getSpecialSkill(SPECIALSKILL_LIFELEECHAMOUNT); + if (skill != 0 && chance != 0 && normal_random(1, 100) <= chance) { + CombatDamage healAmount; + healAmount.primary.value += std::round(std::abs(damage.primary.value) * (skill / 100.)); + healAmount.secondary.value += std::round(std::abs(damage.secondary.value) * (skill / 100.)); + g_game.combatChangeHealth(nullptr, caster, healAmount); + caster->sendMagicEffect(caster->getPosition(), CONST_ME_MAGIC_RED); + } + } + + if (caster->getMana() != caster->getMaxMana()) { + uint16_t chance = caster->getSpecialSkill(SPECIALSKILL_MANALEECHCHANCE); + uint16_t skill = caster->getSpecialSkill(SPECIALSKILL_MANALEECHAMOUNT); + if (skill != 0 && chance != 0 && normal_random(1, 100) <= chance) { + CombatDamage manaAmount; + manaAmount.primary.value += std::round(std::abs(damage.primary.value) * (skill / 100.)); + manaAmount.secondary.value += std::round(std::abs(damage.secondary.value) * (skill / 100.)); + g_game.combatChangeMana(nullptr, caster, manaAmount); + caster->sendMagicEffect(caster->getPosition(), CONST_ME_MAGIC_BLUE); + } + } +} + +//**********************************************************// + +void ValueCallback::getMinMaxValues(Player* player, CombatDamage& damage) const +{ + //onGetPlayerMinMaxValues(...) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - ValueCallback::getMinMaxValues] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + if (!env->setCallbackId(scriptId, scriptInterface)) { + scriptInterface->resetScriptEnv(); + return; + } + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + int parameters = 1; + switch (type) { + case COMBAT_FORMULA_LEVELMAGIC: { + //onGetPlayerMinMaxValues(player, level, maglevel) + lua_pushnumber(L, player->getLevel()); + lua_pushnumber(L, player->getMagicLevel()); + parameters += 2; + break; + } + + case COMBAT_FORMULA_SKILL: { + //onGetPlayerMinMaxValues(player, attackSkill, attackValue, attackFactor) + Item* tool = player->getWeapon(); + const Weapon* weapon = g_weapons->getWeapon(tool); + + int32_t attackValue = 7; + if (weapon) { + attackValue = tool->getAttack(); + if (tool->getWeaponType() == WEAPON_AMMO) { + Item* item = player->getWeapon(true); + if (item) { + attackValue += item->getAttack(); + } + } + + damage.secondary.type = weapon->getElementType(); + damage.secondary.value = weapon->getElementDamage(player, nullptr, tool); + } + + lua_pushnumber(L, player->getWeaponSkill(tool)); + lua_pushnumber(L, attackValue); + lua_pushnumber(L, player->getAttackFactor()); + parameters += 3; + break; + } + + default: { + std::cout << "ValueCallback::getMinMaxValues - unknown callback type" << std::endl; + scriptInterface->resetScriptEnv(); + return; + } + } + + int size0 = lua_gettop(L); + if (lua_pcall(L, parameters, 2, 0) != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } else { + damage.primary.value = normal_random( + LuaScriptInterface::getNumber(L, -2), + LuaScriptInterface::getNumber(L, -1) + ); + lua_pop(L, 2); + } + + if ((lua_gettop(L) + parameters + 1) != size0) { + LuaScriptInterface::reportError(nullptr, "Stack size changed!"); + } + + scriptInterface->resetScriptEnv(); +} + +//**********************************************************// + +void TileCallback::onTileCombat(Creature* creature, Tile* tile) const +{ + //onTileCombat(creature, pos) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - TileCallback::onTileCombat] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + if (!env->setCallbackId(scriptId, scriptInterface)) { + scriptInterface->resetScriptEnv(); + return; + } + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + if (creature) { + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + } else { + lua_pushnil(L); + } + LuaScriptInterface::pushPosition(L, tile->getPosition()); + + scriptInterface->callFunction(2); +} + +//**********************************************************// + +void TargetCallback::onTargetCombat(Creature* creature, Creature* target) const +{ + //onTargetCombat(creature, target) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - TargetCallback::onTargetCombat] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + if (!env->setCallbackId(scriptId, scriptInterface)) { + scriptInterface->resetScriptEnv(); + return; + } + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + + if (creature) { + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + } else { + lua_pushnil(L); + } + + if (target) { + LuaScriptInterface::pushUserdata(L, target); + LuaScriptInterface::setCreatureMetatable(L, -1, target); + } else { + lua_pushnil(L); + } + + int size0 = lua_gettop(L); + + if (lua_pcall(L, 2, 0 /*nReturnValues*/, 0) != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } + + if ((lua_gettop(L) + 2 /*nParams*/ + 1) != size0) { + LuaScriptInterface::reportError(nullptr, "Stack size changed!"); + } + + scriptInterface->resetScriptEnv(); +} + +//**********************************************************// + +void AreaCombat::clear() +{ + for (const auto& it : areas) { + delete it.second; + } + areas.clear(); +} + +AreaCombat::AreaCombat(const AreaCombat& rhs) +{ + hasExtArea = rhs.hasExtArea; + for (const auto& it : rhs.areas) { + areas[it.first] = new MatrixArea(*it.second); + } +} + +void AreaCombat::getList(const Position& centerPos, const Position& targetPos, std::forward_list& list) const +{ + const MatrixArea* area = getArea(centerPos, targetPos); + if (!area) { + return; + } + + uint32_t centerY, centerX; + area->getCenter(centerY, centerX); + + Position tmpPos(targetPos.x - centerX, targetPos.y - centerY, targetPos.z); + uint32_t cols = area->getCols(); + for (uint32_t y = 0, rows = area->getRows(); y < rows; ++y) { + for (uint32_t x = 0; x < cols; ++x) { + if (area->getValue(y, x) != 0) { + if (g_game.isSightClear(targetPos, tmpPos, true)) { + Tile* tile = g_game.map.getTile(tmpPos); + if (!tile) { + tile = new StaticTile(tmpPos.x, tmpPos.y, tmpPos.z); + g_game.map.setTile(tmpPos, tile); + } + list.push_front(tile); + } + } + tmpPos.x++; + } + tmpPos.x -= cols; + tmpPos.y++; + } +} + +void AreaCombat::copyArea(const MatrixArea* input, MatrixArea* output, MatrixOperation_t op) +{ + uint32_t centerY, centerX; + input->getCenter(centerY, centerX); + + if (op == MATRIXOPERATION_COPY) { + for (uint32_t y = 0; y < input->getRows(); ++y) { + for (uint32_t x = 0; x < input->getCols(); ++x) { + (*output)[y][x] = (*input)[y][x]; + } + } + + output->setCenter(centerY, centerX); + } else if (op == MATRIXOPERATION_MIRROR) { + for (uint32_t y = 0; y < input->getRows(); ++y) { + uint32_t rx = 0; + for (int32_t x = input->getCols(); --x >= 0;) { + (*output)[y][rx++] = (*input)[y][x]; + } + } + + output->setCenter(centerY, (input->getRows() - 1) - centerX); + } else if (op == MATRIXOPERATION_FLIP) { + for (uint32_t x = 0; x < input->getCols(); ++x) { + uint32_t ry = 0; + for (int32_t y = input->getRows(); --y >= 0;) { + (*output)[ry++][x] = (*input)[y][x]; + } + } + + output->setCenter((input->getCols() - 1) - centerY, centerX); + } else { + // rotation + int32_t rotateCenterX = (output->getCols() / 2) - 1; + int32_t rotateCenterY = (output->getRows() / 2) - 1; + int32_t angle; + + switch (op) { + case MATRIXOPERATION_ROTATE90: + angle = 90; + break; + + case MATRIXOPERATION_ROTATE180: + angle = 180; + break; + + case MATRIXOPERATION_ROTATE270: + angle = 270; + break; + + default: + angle = 0; + break; + } + + double angleRad = M_PI * angle / 180.0; + + double a = std::cos(angleRad); + double b = -std::sin(angleRad); + double c = std::sin(angleRad); + double d = std::cos(angleRad); + + const uint32_t rows = input->getRows(); + for (uint32_t x = 0, cols = input->getCols(); x < cols; ++x) { + for (uint32_t y = 0; y < rows; ++y) { + //calculate new coordinates using rotation center + int32_t newX = x - centerX; + int32_t newY = y - centerY; + + //perform rotation + int32_t rotatedX = static_cast(round(newX * a + newY * b)); + int32_t rotatedY = static_cast(round(newX * c + newY * d)); + + //write in the output matrix using rotated coordinates + (*output)[rotatedY + rotateCenterY][rotatedX + rotateCenterX] = (*input)[y][x]; + } + } + + output->setCenter(rotateCenterY, rotateCenterX); + } +} + +MatrixArea* AreaCombat::createArea(const std::list& list, uint32_t rows) +{ + uint32_t cols; + if (rows == 0) { + cols = 0; + } else { + cols = list.size() / rows; + } + + MatrixArea* area = new MatrixArea(rows, cols); + + uint32_t x = 0; + uint32_t y = 0; + + for (uint32_t value : list) { + if (value == 1 || value == 3) { + area->setValue(y, x, true); + } + + if (value == 2 || value == 3) { + area->setCenter(y, x); + } + + ++x; + + if (cols == x) { + x = 0; + ++y; + } + } + return area; +} + +void AreaCombat::setupArea(const std::list& list, uint32_t rows) +{ + MatrixArea* area = createArea(list, rows); + + //NORTH + areas[DIRECTION_NORTH] = area; + + uint32_t maxOutput = std::max(area->getCols(), area->getRows()) * 2; + + //SOUTH + MatrixArea* southArea = new MatrixArea(maxOutput, maxOutput); + AreaCombat::copyArea(area, southArea, MATRIXOPERATION_ROTATE180); + areas[DIRECTION_SOUTH] = southArea; + + //EAST + MatrixArea* eastArea = new MatrixArea(maxOutput, maxOutput); + AreaCombat::copyArea(area, eastArea, MATRIXOPERATION_ROTATE90); + areas[DIRECTION_EAST] = eastArea; + + //WEST + MatrixArea* westArea = new MatrixArea(maxOutput, maxOutput); + AreaCombat::copyArea(area, westArea, MATRIXOPERATION_ROTATE270); + areas[DIRECTION_WEST] = westArea; +} + +void AreaCombat::setupArea(int32_t length, int32_t spread) +{ + std::list list; + + uint32_t rows = length; + int32_t cols = 1; + + if (spread != 0) { + cols = ((length - (length % spread)) / spread) * 2 + 1; + } + + int32_t colSpread = cols; + + for (uint32_t y = 1; y <= rows; ++y) { + int32_t mincol = cols - colSpread + 1; + int32_t maxcol = cols - (cols - colSpread); + + for (int32_t x = 1; x <= cols; ++x) { + if (y == rows && x == ((cols - (cols % 2)) / 2) + 1) { + list.push_back(3); + } else if (x >= mincol && x <= maxcol) { + list.push_back(1); + } else { + list.push_back(0); + } + } + + if (spread > 0 && y % spread == 0) { + --colSpread; + } + } + + setupArea(list, rows); +} + +void AreaCombat::setupArea(int32_t radius) +{ + int32_t area[13][13] = { + {0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 8, 8, 7, 8, 8, 0, 0, 0, 0}, + {0, 0, 0, 8, 7, 6, 6, 6, 7, 8, 0, 0, 0}, + {0, 0, 8, 7, 6, 5, 5, 5, 6, 7, 8, 0, 0}, + {0, 8, 7, 6, 5, 4, 4, 4, 5, 6, 7, 8, 0}, + {0, 8, 6, 5, 4, 3, 2, 3, 4, 5, 6, 8, 0}, + {8, 7, 6, 5, 4, 2, 1, 2, 4, 5, 6, 7, 8}, + {0, 8, 6, 5, 4, 3, 2, 3, 4, 5, 6, 8, 0}, + {0, 8, 7, 6, 5, 4, 4, 4, 5, 6, 7, 8, 0}, + {0, 0, 8, 7, 6, 5, 5, 5, 6, 7, 8, 0, 0}, + {0, 0, 0, 8, 7, 6, 6, 6, 7, 8, 0, 0, 0}, + {0, 0, 0, 0, 8, 8, 7, 8, 8, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0} + }; + + std::list list; + + for (auto& row : area) { + for (int cell : row) { + if (cell == 1) { + list.push_back(3); + } else if (cell > 0 && cell <= radius) { + list.push_back(1); + } else { + list.push_back(0); + } + } + } + + setupArea(list, 13); +} + +void AreaCombat::setupExtArea(const std::list& list, uint32_t rows) +{ + if (list.empty()) { + return; + } + + hasExtArea = true; + MatrixArea* area = createArea(list, rows); + + //NORTH-WEST + areas[DIRECTION_NORTHWEST] = area; + + uint32_t maxOutput = std::max(area->getCols(), area->getRows()) * 2; + + //NORTH-EAST + MatrixArea* neArea = new MatrixArea(maxOutput, maxOutput); + AreaCombat::copyArea(area, neArea, MATRIXOPERATION_MIRROR); + areas[DIRECTION_NORTHEAST] = neArea; + + //SOUTH-WEST + MatrixArea* swArea = new MatrixArea(maxOutput, maxOutput); + AreaCombat::copyArea(area, swArea, MATRIXOPERATION_FLIP); + areas[DIRECTION_SOUTHWEST] = swArea; + + //SOUTH-EAST + MatrixArea* seArea = new MatrixArea(maxOutput, maxOutput); + AreaCombat::copyArea(swArea, seArea, MATRIXOPERATION_MIRROR); + areas[DIRECTION_SOUTHEAST] = seArea; +} + +//**********************************************************// + +void MagicField::onStepInField(Creature* creature) +{ + //remove magic walls/wild growth + if (id == ITEM_MAGICWALL || id == ITEM_WILDGROWTH || id == ITEM_MAGICWALL_SAFE || id == ITEM_WILDGROWTH_SAFE || isBlocking()) { + if (!creature->isInGhostMode()) { + g_game.internalRemoveItem(this, 1); + } + + return; + } + + const ItemType& it = items[getID()]; + if (it.conditionDamage) { + Condition* conditionCopy = it.conditionDamage->clone(); + uint32_t ownerId = getOwner(); + if (ownerId) { + bool harmfulField = true; + + if (g_game.getWorldType() == WORLD_TYPE_NO_PVP || getTile()->hasFlag(TILESTATE_NOPVPZONE)) { + Creature* owner = g_game.getCreatureByID(ownerId); + if (owner) { + if (owner->getPlayer() || (owner->isSummon() && owner->getMaster()->getPlayer())) { + harmfulField = false; + } + } + } + + Player* targetPlayer = creature->getPlayer(); + if (targetPlayer) { + Player* attackerPlayer = g_game.getPlayerByID(ownerId); + if (attackerPlayer) { + if (Combat::isProtected(attackerPlayer, targetPlayer)) { + harmfulField = false; + } + } + } + + if (!harmfulField || (OTSYS_TIME() - createTime <= 5000) || creature->hasBeenAttacked(ownerId)) { + conditionCopy->setParam(CONDITION_PARAM_OWNER, ownerId); + } + } + + creature->addCondition(conditionCopy); + } +} diff --git a/src/combat.h b/src/combat.h new file mode 100644 index 0000000..92fef82 --- /dev/null +++ b/src/combat.h @@ -0,0 +1,340 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_COMBAT_H_B02CE79230FC43708699EE91FCC8F7CC +#define FS_COMBAT_H_B02CE79230FC43708699EE91FCC8F7CC + +#include "thing.h" +#include "condition.h" +#include "map.h" +#include "baseevents.h" + +class Condition; +class Creature; +class Item; + +struct Position; + +//for luascript callback +class ValueCallback final : public CallBack +{ + public: + explicit ValueCallback(formulaType_t type): type(type) {} + void getMinMaxValues(Player* player, CombatDamage& damage) const; + + private: + formulaType_t type; +}; + +class TileCallback final : public CallBack +{ + public: + void onTileCombat(Creature* creature, Tile* tile) const; +}; + +class TargetCallback final : public CallBack +{ + public: + void onTargetCombat(Creature* creature, Creature* target) const; +}; + +struct CombatParams { + std::forward_list> conditionList; + + std::unique_ptr valueCallback; + std::unique_ptr tileCallback; + std::unique_ptr targetCallback; + + uint16_t itemId = 0; + + ConditionType_t dispelType = CONDITION_NONE; + CombatType_t combatType = COMBAT_NONE; + CombatOrigin origin = ORIGIN_SPELL; + + uint8_t impactEffect = CONST_ME_NONE; + uint8_t distanceEffect = CONST_ANI_NONE; + + bool blockedByArmor = false; + bool blockedByShield = false; + bool targetCasterOrTopMost = false; + bool aggressive = true; + bool useCharges = false; +}; + +class MatrixArea +{ + public: + MatrixArea(uint32_t rows, uint32_t cols): centerX(0), centerY(0), rows(rows), cols(cols) { + data_ = new bool*[rows]; + + for (uint32_t row = 0; row < rows; ++row) { + data_[row] = new bool[cols]; + + for (uint32_t col = 0; col < cols; ++col) { + data_[row][col] = 0; + } + } + } + + MatrixArea(const MatrixArea& rhs) { + centerX = rhs.centerX; + centerY = rhs.centerY; + rows = rhs.rows; + cols = rhs.cols; + + data_ = new bool*[rows]; + + for (uint32_t row = 0; row < rows; ++row) { + data_[row] = new bool[cols]; + + for (uint32_t col = 0; col < cols; ++col) { + data_[row][col] = rhs.data_[row][col]; + } + } + } + + ~MatrixArea() { + for (uint32_t row = 0; row < rows; ++row) { + delete[] data_[row]; + } + + delete[] data_; + } + + // non-assignable + MatrixArea& operator=(const MatrixArea&) = delete; + + void setValue(uint32_t row, uint32_t col, bool value) const { + data_[row][col] = value; + } + bool getValue(uint32_t row, uint32_t col) const { + return data_[row][col]; + } + + void setCenter(uint32_t y, uint32_t x) { + centerX = x; + centerY = y; + } + void getCenter(uint32_t& y, uint32_t& x) const { + x = centerX; + y = centerY; + } + + uint32_t getRows() const { + return rows; + } + uint32_t getCols() const { + return cols; + } + + const bool* operator[](uint32_t i) const { + return data_[i]; + } + bool* operator[](uint32_t i) { + return data_[i]; + } + + private: + uint32_t centerX; + uint32_t centerY; + + uint32_t rows; + uint32_t cols; + bool** data_; +}; + +class AreaCombat +{ + public: + AreaCombat() = default; + + AreaCombat(const AreaCombat& rhs); + ~AreaCombat() { + clear(); + } + + // non-assignable + AreaCombat& operator=(const AreaCombat&) = delete; + + void getList(const Position& centerPos, const Position& targetPos, std::forward_list& list) const; + + void setupArea(const std::list& list, uint32_t rows); + void setupArea(int32_t length, int32_t spread); + void setupArea(int32_t radius); + void setupExtArea(const std::list& list, uint32_t rows); + void clear(); + + private: + enum MatrixOperation_t { + MATRIXOPERATION_COPY, + MATRIXOPERATION_MIRROR, + MATRIXOPERATION_FLIP, + MATRIXOPERATION_ROTATE90, + MATRIXOPERATION_ROTATE180, + MATRIXOPERATION_ROTATE270, + }; + + MatrixArea* createArea(const std::list& list, uint32_t rows); + static void copyArea(const MatrixArea* input, MatrixArea* output, MatrixOperation_t op); + + MatrixArea* getArea(const Position& centerPos, const Position& targetPos) const { + int32_t dx = Position::getOffsetX(targetPos, centerPos); + int32_t dy = Position::getOffsetY(targetPos, centerPos); + + Direction dir; + if (dx < 0) { + dir = DIRECTION_WEST; + } else if (dx > 0) { + dir = DIRECTION_EAST; + } else if (dy < 0) { + dir = DIRECTION_NORTH; + } else { + dir = DIRECTION_SOUTH; + } + + if (hasExtArea) { + if (dx < 0 && dy < 0) { + dir = DIRECTION_NORTHWEST; + } else if (dx > 0 && dy < 0) { + dir = DIRECTION_NORTHEAST; + } else if (dx < 0 && dy > 0) { + dir = DIRECTION_SOUTHWEST; + } else if (dx > 0 && dy > 0) { + dir = DIRECTION_SOUTHEAST; + } + } + + auto it = areas.find(dir); + if (it == areas.end()) { + return nullptr; + } + return it->second; + } + + std::map areas; + bool hasExtArea = false; +}; + +class Combat +{ + public: + Combat() = default; + + // non-copyable + Combat(const Combat&) = delete; + Combat& operator=(const Combat&) = delete; + + static void getCombatArea(const Position& centerPos, const Position& targetPos, const AreaCombat* area, std::forward_list& list); + + static bool isInPvpZone(const Creature* attacker, const Creature* target); + static bool isProtected(const Player* attacker, const Player* target); + static bool isPlayerCombat(const Creature* target); + static CombatType_t ConditionToDamageType(ConditionType_t type); + static ConditionType_t DamageToConditionType(CombatType_t type); + static ReturnValue canTargetCreature(Player* attacker, Creature* target); + static ReturnValue canDoCombat(Creature* caster, Tile* tile, bool aggressive); + static ReturnValue canDoCombat(Creature* attacker, Creature* target); + static void postCombatEffects(Creature* caster, const Position& pos, const CombatParams& params); + + static void addDistanceEffect(Creature* caster, const Position& fromPos, const Position& toPos, uint8_t effect); + + void doCombat(Creature* caster, Creature* target) const; + void doCombat(Creature* caster, const Position& position) const; + + static void doTargetCombat(Creature* caster, Creature* target, CombatDamage& damage, const CombatParams& params); + static void doAreaCombat(Creature* caster, const Position& position, const AreaCombat* area, CombatDamage& damage, const CombatParams& params); + + static void checkCriticalHit(Player* caster, CombatDamage& damage); + static void checkLeech(Player* caster, CombatDamage& damage); + + bool setCallback(CallBackParam_t key); + CallBack* getCallback(CallBackParam_t key); + + bool setParam(CombatParam_t param, uint32_t value); + void setArea(AreaCombat* area) { + this->area.reset(area); + } + bool hasArea() const { + return area != nullptr; + } + void addCondition(const Condition* condition) { + params.conditionList.emplace_front(condition); + } + void clearConditions() { + params.conditionList.clear(); + } + void setPlayerCombatValues(formulaType_t formulaType, double mina, double minb, double maxa, double maxb); + void postCombatEffects(Creature* caster, const Position& pos) const { + postCombatEffects(caster, pos, params); + } + + void setOrigin(CombatOrigin origin) { + params.origin = origin; + } + + private: + static void combatTileEffects(const SpectatorVec& spectators, Creature* caster, Tile* tile, const CombatParams& params); + CombatDamage getCombatDamage(Creature* creature, Creature* target) const; + + //configureable + CombatParams params; + + //formula variables + formulaType_t formulaType = COMBAT_FORMULA_UNDEFINED; + double mina = 0.0; + double minb = 0.0; + double maxa = 0.0; + double maxb = 0.0; + + std::unique_ptr area; +}; + +class MagicField final : public Item +{ + public: + explicit MagicField(uint16_t type) : Item(type), createTime(OTSYS_TIME()) {} + + MagicField* getMagicField() override { + return this; + } + const MagicField* getMagicField() const override { + return this; + } + + bool isReplaceable() const { + return Item::items[getID()].replaceable; + } + CombatType_t getCombatType() const { + const ItemType& it = items[getID()]; + return it.combatType; + } + int32_t getDamage() const { + const ItemType& it = items[getID()]; + if (it.conditionDamage) { + return it.conditionDamage->getTotalDamage(); + } + return 0; + } + void onStepInField(Creature* creature); + + private: + int64_t createTime; +}; + +#endif diff --git a/src/condition.cpp b/src/condition.cpp new file mode 100644 index 0000000..6b17e75 --- /dev/null +++ b/src/condition.cpp @@ -0,0 +1,1729 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "condition.h" +#include "game.h" + +extern Game g_game; + +bool Condition::setParam(ConditionParam_t param, int32_t value) +{ + switch (param) { + case CONDITION_PARAM_TICKS: { + ticks = value; + return true; + } + + case CONDITION_PARAM_BUFF_SPELL: { + isBuff = (value != 0); + return true; + } + + case CONDITION_PARAM_SUBID: { + subId = value; + return true; + } + + case CONDITION_PARAM_AGGRESSIVE: { + aggressive = (value != 0); + return true; + } + + default: { + return false; + } + } +} + +bool Condition::unserialize(PropStream& propStream) +{ + uint8_t attr_type; + while (propStream.read(attr_type) && attr_type != CONDITIONATTR_END) { + if (!unserializeProp(static_cast(attr_type), propStream)) { + return false; + } + } + return true; +} + +bool Condition::unserializeProp(ConditionAttr_t attr, PropStream& propStream) +{ + switch (attr) { + case CONDITIONATTR_TYPE: { + int32_t value; + if (!propStream.read(value)) { + return false; + } + + conditionType = static_cast(value); + return true; + } + + case CONDITIONATTR_ID: { + int32_t value; + if (!propStream.read(value)) { + return false; + } + + id = static_cast(value); + return true; + } + + case CONDITIONATTR_TICKS: { + return propStream.read(ticks); + } + + case CONDITIONATTR_ISBUFF: { + uint8_t value; + if (!propStream.read(value)) { + return false; + } + + isBuff = (value != 0); + return true; + } + + case CONDITIONATTR_SUBID: { + return propStream.read(subId); + } + + case CONDITIONATTR_ISAGGRESSIVE: { + uint8_t value; + if (!propStream.read(value)) { + return false; + } + + aggressive = (value != 0); + return true; + } + + case CONDITIONATTR_END: + return true; + + default: + return false; + } +} + +void Condition::serialize(PropWriteStream& propWriteStream) +{ + propWriteStream.write(CONDITIONATTR_TYPE); + propWriteStream.write(conditionType); + + propWriteStream.write(CONDITIONATTR_ID); + propWriteStream.write(id); + + propWriteStream.write(CONDITIONATTR_TICKS); + propWriteStream.write(ticks); + + propWriteStream.write(CONDITIONATTR_ISBUFF); + propWriteStream.write(isBuff); + + propWriteStream.write(CONDITIONATTR_SUBID); + propWriteStream.write(subId); + + propWriteStream.write(CONDITIONATTR_ISAGGRESSIVE); + propWriteStream.write(aggressive); +} + +void Condition::setTicks(int32_t newTicks) +{ + ticks = newTicks; + endTime = ticks + OTSYS_TIME(); +} + +bool Condition::executeCondition(Creature*, int32_t interval) +{ + if (ticks == -1) { + return true; + } + + //Not using set ticks here since it would reset endTime + ticks = std::max(0, ticks - interval); + return getEndTime() >= OTSYS_TIME(); +} + +Condition* Condition::createCondition(ConditionId_t id, ConditionType_t type, int32_t ticks, int32_t param/* = 0*/, bool buff/* = false*/, uint32_t subId/* = 0*/, bool aggressive/* = false */) +{ + switch (type) { + case CONDITION_POISON: + case CONDITION_FIRE: + case CONDITION_ENERGY: + case CONDITION_DROWN: + case CONDITION_FREEZING: + case CONDITION_DAZZLED: + case CONDITION_CURSED: + case CONDITION_BLEEDING: + return new ConditionDamage(id, type, buff, subId, aggressive); + + case CONDITION_HASTE: + case CONDITION_PARALYZE: + return new ConditionSpeed(id, type, ticks, buff, subId, param, aggressive); + + case CONDITION_INVISIBLE: + return new ConditionInvisible(id, type, ticks, buff, subId, aggressive); + + case CONDITION_OUTFIT: + return new ConditionOutfit(id, type, ticks, buff, subId, aggressive); + + case CONDITION_LIGHT: + return new ConditionLight(id, type, ticks, buff, subId, param & 0xFF, (param & 0xFF00) >> 8, aggressive); + + case CONDITION_REGENERATION: + return new ConditionRegeneration(id, type, ticks, buff, subId, aggressive); + + case CONDITION_SOUL: + return new ConditionSoul(id, type, ticks, buff, subId, aggressive); + + case CONDITION_ATTRIBUTES: + return new ConditionAttributes(id, type, ticks, buff, subId, aggressive); + + case CONDITION_SPELLCOOLDOWN: + return new ConditionSpellCooldown(id, type, ticks, buff, subId, aggressive); + + case CONDITION_SPELLGROUPCOOLDOWN: + return new ConditionSpellGroupCooldown(id, type, ticks, buff, subId, aggressive); + + case CONDITION_INFIGHT: + case CONDITION_DRUNK: + case CONDITION_EXHAUST_WEAPON: + case CONDITION_EXHAUST_COMBAT: + case CONDITION_EXHAUST_HEAL: + case CONDITION_MUTED: + case CONDITION_CHANNELMUTEDTICKS: + case CONDITION_YELLTICKS: + case CONDITION_PACIFIED: + case CONDITION_MANASHIELD: + return new ConditionGeneric(id, type, ticks, buff, subId, aggressive); + + default: + return nullptr; + } +} + +Condition* Condition::createCondition(PropStream& propStream) +{ + uint8_t attr; + if (!propStream.read(attr) || attr != CONDITIONATTR_TYPE) { + return nullptr; + } + + uint32_t type; + if (!propStream.read(type)) { + return nullptr; + } + + if (!propStream.read(attr) || attr != CONDITIONATTR_ID) { + return nullptr; + } + + uint32_t id; + if (!propStream.read(id)) { + return nullptr; + } + + if (!propStream.read(attr) || attr != CONDITIONATTR_TICKS) { + return nullptr; + } + + uint32_t ticks; + if (!propStream.read(ticks)) { + return nullptr; + } + + if (!propStream.read(attr) || attr != CONDITIONATTR_ISBUFF) { + return nullptr; + } + + uint8_t buff; + if (!propStream.read(buff)) { + return nullptr; + } + + if (!propStream.read(attr) || attr != CONDITIONATTR_SUBID) { + return nullptr; + } + + uint32_t subId; + if (!propStream.read(subId)) { + return nullptr; + } + + uint8_t aggressive; + if (!propStream.read(aggressive)) { + return nullptr; + } + + return createCondition(static_cast(id), static_cast(type), ticks, 0, buff != 0, subId, aggressive); +} + +bool Condition::startCondition(Creature*) +{ + if (ticks > 0) { + endTime = ticks + OTSYS_TIME(); + } + return true; +} + +bool Condition::isPersistent() const +{ + if (ticks == -1) { + return false; + } + + if (!(id == CONDITIONID_DEFAULT || id == CONDITIONID_COMBAT || conditionType == CONDITION_MUTED)) { + return false; + } + + return true; +} + +uint32_t Condition::getIcons() const +{ + return isBuff ? ICON_PARTY_BUFF : 0; +} + +bool Condition::updateCondition(const Condition* addCondition) +{ + if (conditionType != addCondition->getType()) { + return false; + } + + if (ticks == -1 && addCondition->getTicks() > 0) { + return false; + } + + if (addCondition->getTicks() >= 0 && getEndTime() > (OTSYS_TIME() + addCondition->getTicks())) { + return false; + } + + return true; +} + +bool ConditionGeneric::startCondition(Creature* creature) +{ + return Condition::startCondition(creature); +} + +bool ConditionGeneric::executeCondition(Creature* creature, int32_t interval) +{ + return Condition::executeCondition(creature, interval); +} + +void ConditionGeneric::endCondition(Creature*) +{ + // +} + +void ConditionGeneric::addCondition(Creature*, const Condition* condition) +{ + if (updateCondition(condition)) { + setTicks(condition->getTicks()); + } +} + +uint32_t ConditionGeneric::getIcons() const +{ + uint32_t icons = Condition::getIcons(); + + switch (conditionType) { + case CONDITION_MANASHIELD: + icons |= ICON_MANASHIELD; + break; + + case CONDITION_INFIGHT: + icons |= ICON_SWORDS; + break; + + case CONDITION_DRUNK: + icons |= ICON_DRUNK; + break; + + default: + break; + } + + return icons; +} + +void ConditionAttributes::addCondition(Creature* creature, const Condition* condition) +{ + if (updateCondition(condition)) { + setTicks(condition->getTicks()); + + const ConditionAttributes& conditionAttrs = static_cast(*condition); + //Remove the old condition + endCondition(creature); + + //Apply the new one + memcpy(skills, conditionAttrs.skills, sizeof(skills)); + memcpy(specialSkills, conditionAttrs.specialSkills, sizeof(specialSkills)); + memcpy(skillsPercent, conditionAttrs.skillsPercent, sizeof(skillsPercent)); + memcpy(stats, conditionAttrs.stats, sizeof(stats)); + memcpy(statsPercent, conditionAttrs.statsPercent, sizeof(statsPercent)); + disableDefense = conditionAttrs.disableDefense; + + if (Player* player = creature->getPlayer()) { + updatePercentSkills(player); + updateSkills(player); + updatePercentStats(player); + updateStats(player); + } + } +} + +bool ConditionAttributes::unserializeProp(ConditionAttr_t attr, PropStream& propStream) +{ + if (attr == CONDITIONATTR_SKILLS) { + return propStream.read(skills[currentSkill++]); + } else if (attr == CONDITIONATTR_STATS) { + return propStream.read(stats[currentStat++]); + } else if (attr == CONDITIONATTR_DISABLEDEFENSE) { + return propStream.read(disableDefense); + } + return Condition::unserializeProp(attr, propStream); +} + +void ConditionAttributes::serialize(PropWriteStream& propWriteStream) +{ + Condition::serialize(propWriteStream); + + for (int32_t i = SKILL_FIRST; i <= SKILL_LAST; ++i) { + propWriteStream.write(CONDITIONATTR_SKILLS); + propWriteStream.write(skills[i]); + } + + for (int32_t i = STAT_FIRST; i <= STAT_LAST; ++i) { + propWriteStream.write(CONDITIONATTR_STATS); + propWriteStream.write(stats[i]); + } + + propWriteStream.write(CONDITIONATTR_DISABLEDEFENSE); + propWriteStream.write(disableDefense); +} + +bool ConditionAttributes::startCondition(Creature* creature) +{ + if (!Condition::startCondition(creature)) { + return false; + } + + creature->setUseDefense(!disableDefense); + + if (Player* player = creature->getPlayer()) { + updatePercentSkills(player); + updateSkills(player); + updatePercentStats(player); + updateStats(player); + } + + return true; +} + +void ConditionAttributes::updatePercentStats(Player* player) +{ + for (int32_t i = STAT_FIRST; i <= STAT_LAST; ++i) { + if (statsPercent[i] == 0) { + continue; + } + + switch (i) { + case STAT_MAXHITPOINTS: + stats[i] = static_cast(player->getMaxHealth() * ((statsPercent[i] - 100) / 100.f)); + break; + + case STAT_MAXMANAPOINTS: + stats[i] = static_cast(player->getMaxMana() * ((statsPercent[i] - 100) / 100.f)); + break; + + case STAT_MAGICPOINTS: + stats[i] = static_cast(player->getBaseMagicLevel() * ((statsPercent[i] - 100) / 100.f)); + break; + } + } +} + +void ConditionAttributes::updateStats(Player* player) +{ + bool needUpdateStats = false; + + for (int32_t i = STAT_FIRST; i <= STAT_LAST; ++i) { + if (stats[i]) { + needUpdateStats = true; + player->setVarStats(static_cast(i), stats[i]); + } + } + + if (needUpdateStats) { + player->sendStats(); + } +} + +void ConditionAttributes::updatePercentSkills(Player* player) +{ + for (uint8_t i = SKILL_FIRST; i <= SKILL_LAST; ++i) { + if (skillsPercent[i] == 0) { + continue; + } + + int32_t unmodifiedSkill = player->getBaseSkill(i); + skills[i] = static_cast(unmodifiedSkill * ((skillsPercent[i] - 100) / 100.f)); + } +} + +void ConditionAttributes::updateSkills(Player* player) +{ + bool needUpdateSkills = false; + + for (int32_t i = SKILL_FIRST; i <= SKILL_LAST; ++i) { + if (skills[i]) { + needUpdateSkills = true; + player->setVarSkill(static_cast(i), skills[i]); + } + } + + for (int32_t i = SPECIALSKILL_FIRST; i <= SPECIALSKILL_LAST; ++i) { + if (specialSkills[i]) { + needUpdateSkills = true; + player->setVarSpecialSkill(static_cast(i), specialSkills[i]); + } + } + + if (needUpdateSkills) { + player->sendSkills(); + } +} + +bool ConditionAttributes::executeCondition(Creature* creature, int32_t interval) +{ + return ConditionGeneric::executeCondition(creature, interval); +} + +void ConditionAttributes::endCondition(Creature* creature) +{ + Player* player = creature->getPlayer(); + if (player) { + bool needUpdateSkills = false; + + for (int32_t i = SKILL_FIRST; i <= SKILL_LAST; ++i) { + if (skills[i] || skillsPercent[i]) { + needUpdateSkills = true; + player->setVarSkill(static_cast(i), -skills[i]); + } + } + + for (int32_t i = SPECIALSKILL_FIRST; i <= SPECIALSKILL_LAST; ++i) { + if (specialSkills[i]) { + needUpdateSkills = true; + player->setVarSpecialSkill(static_cast(i), -specialSkills[i]); + } + } + + if (needUpdateSkills) { + player->sendSkills(); + } + + bool needUpdateStats = false; + + for (int32_t i = STAT_FIRST; i <= STAT_LAST; ++i) { + if (stats[i]) { + needUpdateStats = true; + player->setVarStats(static_cast(i), -stats[i]); + } + } + + if (needUpdateStats) { + player->sendStats(); + } + } + + if (disableDefense) { + creature->setUseDefense(true); + } +} + +bool ConditionAttributes::setParam(ConditionParam_t param, int32_t value) +{ + bool ret = ConditionGeneric::setParam(param, value); + + switch (param) { + case CONDITION_PARAM_SKILL_MELEE: { + skills[SKILL_CLUB] = value; + skills[SKILL_AXE] = value; + skills[SKILL_SWORD] = value; + return true; + } + + case CONDITION_PARAM_SKILL_MELEEPERCENT: { + skillsPercent[SKILL_CLUB] = value; + skillsPercent[SKILL_AXE] = value; + skillsPercent[SKILL_SWORD] = value; + return true; + } + + case CONDITION_PARAM_SKILL_FIST: { + skills[SKILL_FIST] = value; + return true; + } + + case CONDITION_PARAM_SKILL_FISTPERCENT: { + skillsPercent[SKILL_FIST] = value; + return true; + } + + case CONDITION_PARAM_SKILL_CLUB: { + skills[SKILL_CLUB] = value; + return true; + } + + case CONDITION_PARAM_SKILL_CLUBPERCENT: { + skillsPercent[SKILL_CLUB] = value; + return true; + } + + case CONDITION_PARAM_SKILL_SWORD: { + skills[SKILL_SWORD] = value; + return true; + } + + case CONDITION_PARAM_SKILL_SWORDPERCENT: { + skillsPercent[SKILL_SWORD] = value; + return true; + } + + case CONDITION_PARAM_SKILL_AXE: { + skills[SKILL_AXE] = value; + return true; + } + + case CONDITION_PARAM_SKILL_AXEPERCENT: { + skillsPercent[SKILL_AXE] = value; + return true; + } + + case CONDITION_PARAM_SKILL_DISTANCE: { + skills[SKILL_DISTANCE] = value; + return true; + } + + case CONDITION_PARAM_SKILL_DISTANCEPERCENT: { + skillsPercent[SKILL_DISTANCE] = value; + return true; + } + + case CONDITION_PARAM_SKILL_SHIELD: { + skills[SKILL_SHIELD] = value; + return true; + } + + case CONDITION_PARAM_SKILL_SHIELDPERCENT: { + skillsPercent[SKILL_SHIELD] = value; + return true; + } + + case CONDITION_PARAM_SKILL_FISHING: { + skills[SKILL_FISHING] = value; + return true; + } + + case CONDITION_PARAM_SKILL_FISHINGPERCENT: { + skillsPercent[SKILL_FISHING] = value; + return true; + } + + case CONDITION_PARAM_STAT_MAXHITPOINTS: { + stats[STAT_MAXHITPOINTS] = value; + return true; + } + + case CONDITION_PARAM_STAT_MAXMANAPOINTS: { + stats[STAT_MAXMANAPOINTS] = value; + return true; + } + + case CONDITION_PARAM_STAT_MAGICPOINTS: { + stats[STAT_MAGICPOINTS] = value; + return true; + } + + case CONDITION_PARAM_STAT_MAXHITPOINTSPERCENT: { + statsPercent[STAT_MAXHITPOINTS] = std::max(0, value); + return true; + } + + case CONDITION_PARAM_STAT_MAXMANAPOINTSPERCENT: { + statsPercent[STAT_MAXMANAPOINTS] = std::max(0, value); + return true; + } + + case CONDITION_PARAM_STAT_MAGICPOINTSPERCENT: { + statsPercent[STAT_MAGICPOINTS] = std::max(0, value); + return true; + } + + case CONDITION_PARAM_DISABLE_DEFENSE: { + disableDefense = (value != 0); + return true; + } + + case CONDITION_PARAM_SPECIALSKILL_CRITICALHITCHANCE: { + specialSkills[SPECIALSKILL_CRITICALHITCHANCE] = value; + return true; + } + + case CONDITION_PARAM_SPECIALSKILL_CRITICALHITAMOUNT: { + specialSkills[SPECIALSKILL_CRITICALHITAMOUNT] = value; + return true; + } + + case CONDITION_PARAM_SPECIALSKILL_LIFELEECHCHANCE: { + specialSkills[SPECIALSKILL_LIFELEECHCHANCE] = value; + return true; + } + + case CONDITION_PARAM_SPECIALSKILL_LIFELEECHAMOUNT: { + specialSkills[SPECIALSKILL_LIFELEECHAMOUNT] = value; + return true; + } + + case CONDITION_PARAM_SPECIALSKILL_MANALEECHCHANCE: { + specialSkills[SPECIALSKILL_MANALEECHCHANCE] = value; + return true; + } + + case CONDITION_PARAM_SPECIALSKILL_MANALEECHAMOUNT: { + specialSkills[SPECIALSKILL_MANALEECHAMOUNT] = value; + return true; + } + + case CONDITION_PARAM_AGGRESSIVE: { + aggressive = (value != 0); + return true; + } + + default: + return ret; + } +} + +void ConditionRegeneration::addCondition(Creature*, const Condition* condition) +{ + if (updateCondition(condition)) { + setTicks(condition->getTicks()); + + const ConditionRegeneration& conditionRegen = static_cast(*condition); + + healthTicks = conditionRegen.healthTicks; + manaTicks = conditionRegen.manaTicks; + + healthGain = conditionRegen.healthGain; + manaGain = conditionRegen.manaGain; + } +} + +bool ConditionRegeneration::unserializeProp(ConditionAttr_t attr, PropStream& propStream) +{ + if (attr == CONDITIONATTR_HEALTHTICKS) { + return propStream.read(healthTicks); + } else if (attr == CONDITIONATTR_HEALTHGAIN) { + return propStream.read(healthGain); + } else if (attr == CONDITIONATTR_MANATICKS) { + return propStream.read(manaTicks); + } else if (attr == CONDITIONATTR_MANAGAIN) { + return propStream.read(manaGain); + } + return Condition::unserializeProp(attr, propStream); +} + +void ConditionRegeneration::serialize(PropWriteStream& propWriteStream) +{ + Condition::serialize(propWriteStream); + + propWriteStream.write(CONDITIONATTR_HEALTHTICKS); + propWriteStream.write(healthTicks); + + propWriteStream.write(CONDITIONATTR_HEALTHGAIN); + propWriteStream.write(healthGain); + + propWriteStream.write(CONDITIONATTR_MANATICKS); + propWriteStream.write(manaTicks); + + propWriteStream.write(CONDITIONATTR_MANAGAIN); + propWriteStream.write(manaGain); +} + +bool ConditionRegeneration::executeCondition(Creature* creature, int32_t interval) +{ + internalHealthTicks += interval; + internalManaTicks += interval; + + if (creature->getZone() == ZONE_PROTECTION) { + return ConditionGeneric::executeCondition(creature, interval); + } + + if (internalHealthTicks >= healthTicks) { + internalHealthTicks = 0; + + int32_t realHealthGain = creature->getHealth(); + creature->changeHealth(healthGain); + realHealthGain = creature->getHealth() - realHealthGain; + + if (isBuff && realHealthGain > 0) { + Player* player = creature->getPlayer(); + if (player) { + std::string healString = std::to_string(realHealthGain) + (realHealthGain != 1 ? " hitpoints." : " hitpoint."); + + TextMessage message(MESSAGE_HEALED, "You were healed for " + healString); + message.position = player->getPosition(); + message.primary.value = realHealthGain; + message.primary.color = TEXTCOLOR_MAYABLUE; + player->sendTextMessage(message); + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, player->getPosition(), false, true); + spectators.erase(player); + if (!spectators.empty()) { + message.type = MESSAGE_HEALED_OTHERS; + message.text = player->getName() + " was healed for " + healString; + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendTextMessage(message); + } + } + } + } + } + + if (internalManaTicks >= manaTicks) { + internalManaTicks = 0; + + if (Player* player = creature->getPlayer()) { + int32_t realManaGain = player->getMana(); + player->changeMana(manaGain); + realManaGain = player->getMana() - realManaGain; + + if (isBuff && realManaGain > 0) { + std::string manaGainString = std::to_string(realManaGain); + + TextMessage message(MESSAGE_HEALED, "You gained " + manaGainString + " mana."); + message.position = player->getPosition(); + message.primary.value = realManaGain; + message.primary.color = TEXTCOLOR_MAYABLUE; + player->sendTextMessage(message); + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, player->getPosition(), false, true); + spectators.erase(player); + if (!spectators.empty()) { + message.type = MESSAGE_HEALED_OTHERS; + message.text = player->getName() + " gained " + manaGainString + " mana."; + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendTextMessage(message); + } + } + } + } + } + + return ConditionGeneric::executeCondition(creature, interval); +} + +bool ConditionRegeneration::setParam(ConditionParam_t param, int32_t value) +{ + bool ret = ConditionGeneric::setParam(param, value); + + switch (param) { + case CONDITION_PARAM_HEALTHGAIN: + healthGain = value; + return true; + + case CONDITION_PARAM_HEALTHTICKS: + healthTicks = value; + return true; + + case CONDITION_PARAM_MANAGAIN: + manaGain = value; + return true; + + case CONDITION_PARAM_MANATICKS: + manaTicks = value; + return true; + + default: + return ret; + } +} + +void ConditionSoul::addCondition(Creature*, const Condition* condition) +{ + if (updateCondition(condition)) { + setTicks(condition->getTicks()); + + const ConditionSoul& conditionSoul = static_cast(*condition); + + soulTicks = conditionSoul.soulTicks; + soulGain = conditionSoul.soulGain; + } +} + +bool ConditionSoul::unserializeProp(ConditionAttr_t attr, PropStream& propStream) +{ + if (attr == CONDITIONATTR_SOULGAIN) { + return propStream.read(soulGain); + } else if (attr == CONDITIONATTR_SOULTICKS) { + return propStream.read(soulTicks); + } + return Condition::unserializeProp(attr, propStream); +} + +void ConditionSoul::serialize(PropWriteStream& propWriteStream) +{ + Condition::serialize(propWriteStream); + + propWriteStream.write(CONDITIONATTR_SOULGAIN); + propWriteStream.write(soulGain); + + propWriteStream.write(CONDITIONATTR_SOULTICKS); + propWriteStream.write(soulTicks); +} + +bool ConditionSoul::executeCondition(Creature* creature, int32_t interval) +{ + internalSoulTicks += interval; + + if (Player* player = creature->getPlayer()) { + if (player->getZone() != ZONE_PROTECTION) { + if (internalSoulTicks >= soulTicks) { + internalSoulTicks = 0; + player->changeSoul(soulGain); + } + } + } + + return ConditionGeneric::executeCondition(creature, interval); +} + +bool ConditionSoul::setParam(ConditionParam_t param, int32_t value) +{ + bool ret = ConditionGeneric::setParam(param, value); + switch (param) { + case CONDITION_PARAM_SOULGAIN: + soulGain = value; + return true; + + case CONDITION_PARAM_SOULTICKS: + soulTicks = value; + return true; + + default: + return ret; + } +} + +bool ConditionDamage::setParam(ConditionParam_t param, int32_t value) +{ + bool ret = Condition::setParam(param, value); + + switch (param) { + case CONDITION_PARAM_OWNER: + owner = value; + return true; + + case CONDITION_PARAM_FORCEUPDATE: + forceUpdate = (value != 0); + return true; + + case CONDITION_PARAM_DELAYED: + delayed = (value != 0); + return true; + + case CONDITION_PARAM_MAXVALUE: + maxDamage = std::abs(value); + break; + + case CONDITION_PARAM_MINVALUE: + minDamage = std::abs(value); + break; + + case CONDITION_PARAM_STARTVALUE: + startDamage = std::abs(value); + break; + + case CONDITION_PARAM_TICKINTERVAL: + tickInterval = std::abs(value); + break; + + case CONDITION_PARAM_PERIODICDAMAGE: + periodDamage = value; + break; + + case CONDITION_PARAM_FIELD: + field = (value != 0); + break; + + default: + return false; + } + + return ret; +} + +bool ConditionDamage::unserializeProp(ConditionAttr_t attr, PropStream& propStream) +{ + if (attr == CONDITIONATTR_DELAYED) { + uint8_t value; + if (!propStream.read(value)) { + return false; + } + + delayed = (value != 0); + return true; + } else if (attr == CONDITIONATTR_PERIODDAMAGE) { + return propStream.read(periodDamage); + } else if (attr == CONDITIONATTR_OWNER) { + return propStream.skip(4); + } else if (attr == CONDITIONATTR_INTERVALDATA) { + IntervalInfo damageInfo; + if (!propStream.read(damageInfo)) { + return false; + } + + damageList.push_back(damageInfo); + if (ticks != -1) { + setTicks(ticks + damageInfo.interval); + } + return true; + } + return Condition::unserializeProp(attr, propStream); +} + +void ConditionDamage::serialize(PropWriteStream& propWriteStream) +{ + Condition::serialize(propWriteStream); + + propWriteStream.write(CONDITIONATTR_DELAYED); + propWriteStream.write(delayed); + + propWriteStream.write(CONDITIONATTR_PERIODDAMAGE); + propWriteStream.write(periodDamage); + + for (const IntervalInfo& intervalInfo : damageList) { + propWriteStream.write(CONDITIONATTR_INTERVALDATA); + propWriteStream.write(intervalInfo); + } +} + +bool ConditionDamage::updateCondition(const Condition* addCondition) +{ + const ConditionDamage& conditionDamage = static_cast(*addCondition); + if (conditionDamage.doForceUpdate()) { + return true; + } + + if (ticks == -1 && conditionDamage.ticks > 0) { + return false; + } + + return conditionDamage.getTotalDamage() > getTotalDamage(); +} + +bool ConditionDamage::addDamage(int32_t rounds, int32_t time, int32_t value) +{ + time = std::max(time, EVENT_CREATURE_THINK_INTERVAL); + if (rounds == -1) { + //periodic damage + periodDamage = value; + setParam(CONDITION_PARAM_TICKINTERVAL, time); + setParam(CONDITION_PARAM_TICKS, -1); + return true; + } + + if (periodDamage > 0) { + return false; + } + + //rounds, time, damage + for (int32_t i = 0; i < rounds; ++i) { + IntervalInfo damageInfo; + damageInfo.interval = time; + damageInfo.timeLeft = time; + damageInfo.value = value; + + damageList.push_back(damageInfo); + + if (ticks != -1) { + setTicks(ticks + damageInfo.interval); + } + } + + return true; +} + +bool ConditionDamage::init() +{ + if (periodDamage != 0) { + return true; + } + + if (!damageList.empty()) { + return true; + } + + setTicks(0); + + int32_t amount = uniform_random(minDamage, maxDamage); + if (amount != 0) { + if (startDamage > maxDamage) { + startDamage = maxDamage; + } else if (startDamage == 0) { + startDamage = std::max(1, std::ceil(amount / 20.0)); + } + + std::list list; + ConditionDamage::generateDamageList(amount, startDamage, list); + for (int32_t value : list) { + addDamage(1, tickInterval, -value); + } + } + return !damageList.empty(); +} + +bool ConditionDamage::startCondition(Creature* creature) +{ + if (!Condition::startCondition(creature)) { + return false; + } + + if (!delayed) { + // delayed condition does no initial damage + if (!doDamage(creature, initDamage)) { + return false; + } + } + + if (!init()) { + return false; + } + return true; +} + +bool ConditionDamage::executeCondition(Creature* creature, int32_t interval) +{ + if (periodDamage != 0) { + periodDamageTick += interval; + + if (periodDamageTick >= tickInterval) { + periodDamageTick = 0; + doDamage(creature, periodDamage); + } + } else if (!damageList.empty()) { + IntervalInfo& damageInfo = damageList.front(); + + bool bRemove = (ticks != -1); + creature->onTickCondition(getType(), bRemove); + damageInfo.timeLeft -= interval; + + if (damageInfo.timeLeft <= 0) { + int32_t damage = damageInfo.value; + + if (bRemove) { + damageList.pop_front(); + } else { + damageInfo.timeLeft = damageInfo.interval; + } + + doDamage(creature, damage); + } + + if (!bRemove) { + if (ticks > 0) { + endTime += interval; + } + + interval = 0; + } + } + + return Condition::executeCondition(creature, interval); +} + +bool ConditionDamage::getNextDamage(int32_t& damage) +{ + if (periodDamage != 0) { + damage = periodDamage; + return true; + } else if (!damageList.empty()) { + IntervalInfo& damageInfo = damageList.front(); + damage = damageInfo.value; + if (ticks != -1) { + damageList.pop_front(); + } + return true; + } + return false; +} + +bool ConditionDamage::doDamage(Creature* creature, int32_t healthChange) +{ + if (creature->isSuppress(getType()) || creature->isImmune(getType())) { + return false; + } + + CombatDamage damage; + damage.origin = ORIGIN_CONDITION; + damage.primary.value = healthChange; + damage.primary.type = Combat::ConditionToDamageType(conditionType); + + Creature* attacker = g_game.getCreatureByID(owner); + if (field && creature->getPlayer() && attacker && attacker->getPlayer()) { + damage.primary.value = static_cast(std::round(damage.primary.value / 2.)); + } + + if (!creature->isAttackable() || Combat::canDoCombat(attacker, creature) != RETURNVALUE_NOERROR) { + if (!creature->isInGhostMode()) { + g_game.addMagicEffect(creature->getPosition(), CONST_ME_POFF); + } + return false; + } + + if (g_game.combatBlockHit(damage, attacker, creature, false, false, field)) { + return false; + } + + return g_game.combatChangeHealth(attacker, creature, damage); +} + +void ConditionDamage::endCondition(Creature*) +{ + // +} + +void ConditionDamage::addCondition(Creature* creature, const Condition* condition) +{ + if (condition->getType() != conditionType) { + return; + } + + if (!updateCondition(condition)) { + return; + } + + const ConditionDamage& conditionDamage = static_cast(*condition); + + setTicks(condition->getTicks()); + owner = conditionDamage.owner; + maxDamage = conditionDamage.maxDamage; + minDamage = conditionDamage.minDamage; + startDamage = conditionDamage.startDamage; + tickInterval = conditionDamage.tickInterval; + periodDamage = conditionDamage.periodDamage; + int32_t nextTimeLeft = tickInterval; + + if (!damageList.empty()) { + //save previous timeLeft + IntervalInfo& damageInfo = damageList.front(); + nextTimeLeft = damageInfo.timeLeft; + damageList.clear(); + } + + damageList = conditionDamage.damageList; + + if (init()) { + if (!damageList.empty()) { + //restore last timeLeft + IntervalInfo& damageInfo = damageList.front(); + damageInfo.timeLeft = nextTimeLeft; + } + + if (!delayed) { + int32_t damage; + if (getNextDamage(damage)) { + doDamage(creature, damage); + } + } + } +} + +int32_t ConditionDamage::getTotalDamage() const +{ + int32_t result; + if (!damageList.empty()) { + result = 0; + for (const IntervalInfo& intervalInfo : damageList) { + result += intervalInfo.value; + } + } else { + result = minDamage + (maxDamage - minDamage) / 2; + } + return std::abs(result); +} + +uint32_t ConditionDamage::getIcons() const +{ + uint32_t icons = Condition::getIcons(); + switch (conditionType) { + case CONDITION_FIRE: + icons |= ICON_BURN; + break; + + case CONDITION_ENERGY: + icons |= ICON_ENERGY; + break; + + case CONDITION_DROWN: + icons |= ICON_DROWNING; + break; + + case CONDITION_POISON: + icons |= ICON_POISON; + break; + + case CONDITION_FREEZING: + icons |= ICON_FREEZING; + break; + + case CONDITION_DAZZLED: + icons |= ICON_DAZZLED; + break; + + case CONDITION_CURSED: + icons |= ICON_CURSED; + break; + + case CONDITION_BLEEDING: + icons |= ICON_BLEEDING; + break; + + default: + break; + } + return icons; +} + +void ConditionDamage::generateDamageList(int32_t amount, int32_t start, std::list& list) +{ + amount = std::abs(amount); + int32_t sum = 0; + double x1, x2; + + for (int32_t i = start; i > 0; --i) { + int32_t n = start + 1 - i; + int32_t med = (n * amount) / start; + + do { + sum += i; + list.push_back(i); + + x1 = std::fabs(1.0 - ((static_cast(sum)) + i) / med); + x2 = std::fabs(1.0 - (static_cast(sum) / med)); + } while (x1 < x2); + } +} + +void ConditionSpeed::setFormulaVars(float mina, float minb, float maxa, float maxb) +{ + this->mina = mina; + this->minb = minb; + this->maxa = maxa; + this->maxb = maxb; +} + +void ConditionSpeed::getFormulaValues(int32_t var, int32_t& min, int32_t& max) const +{ + min = (var * mina) + minb; + max = (var * maxa) + maxb; +} + +bool ConditionSpeed::setParam(ConditionParam_t param, int32_t value) +{ + Condition::setParam(param, value); + if (param != CONDITION_PARAM_SPEED) { + return false; + } + + speedDelta = value; + + if (value > 0) { + conditionType = CONDITION_HASTE; + } else { + conditionType = CONDITION_PARALYZE; + } + return true; +} + +bool ConditionSpeed::unserializeProp(ConditionAttr_t attr, PropStream& propStream) +{ + if (attr == CONDITIONATTR_SPEEDDELTA) { + return propStream.read(speedDelta); + } else if (attr == CONDITIONATTR_FORMULA_MINA) { + return propStream.read(mina); + } else if (attr == CONDITIONATTR_FORMULA_MINB) { + return propStream.read(minb); + } else if (attr == CONDITIONATTR_FORMULA_MAXA) { + return propStream.read(maxa); + } else if (attr == CONDITIONATTR_FORMULA_MAXB) { + return propStream.read(maxb); + } + return Condition::unserializeProp(attr, propStream); +} + +void ConditionSpeed::serialize(PropWriteStream& propWriteStream) +{ + Condition::serialize(propWriteStream); + + propWriteStream.write(CONDITIONATTR_SPEEDDELTA); + propWriteStream.write(speedDelta); + + propWriteStream.write(CONDITIONATTR_FORMULA_MINA); + propWriteStream.write(mina); + + propWriteStream.write(CONDITIONATTR_FORMULA_MINB); + propWriteStream.write(minb); + + propWriteStream.write(CONDITIONATTR_FORMULA_MAXA); + propWriteStream.write(maxa); + + propWriteStream.write(CONDITIONATTR_FORMULA_MAXB); + propWriteStream.write(maxb); +} + +bool ConditionSpeed::startCondition(Creature* creature) +{ + if (!Condition::startCondition(creature)) { + return false; + } + + if (speedDelta == 0) { + int32_t min, max; + getFormulaValues(creature->getBaseSpeed(), min, max); + speedDelta = uniform_random(min, max); + } + + g_game.changeSpeed(creature, speedDelta); + return true; +} + +bool ConditionSpeed::executeCondition(Creature* creature, int32_t interval) +{ + return Condition::executeCondition(creature, interval); +} + +void ConditionSpeed::endCondition(Creature* creature) +{ + g_game.changeSpeed(creature, -speedDelta); +} + +void ConditionSpeed::addCondition(Creature* creature, const Condition* condition) +{ + if (conditionType != condition->getType()) { + return; + } + + if (ticks == -1 && condition->getTicks() > 0) { + return; + } + + setTicks(condition->getTicks()); + + const ConditionSpeed& conditionSpeed = static_cast(*condition); + int32_t oldSpeedDelta = speedDelta; + speedDelta = conditionSpeed.speedDelta; + mina = conditionSpeed.mina; + maxa = conditionSpeed.maxa; + minb = conditionSpeed.minb; + maxb = conditionSpeed.maxb; + + if (speedDelta == 0) { + int32_t min; + int32_t max; + getFormulaValues(creature->getBaseSpeed(), min, max); + speedDelta = uniform_random(min, max); + } + + int32_t newSpeedChange = (speedDelta - oldSpeedDelta); + if (newSpeedChange != 0) { + g_game.changeSpeed(creature, newSpeedChange); + } +} + +uint32_t ConditionSpeed::getIcons() const +{ + uint32_t icons = Condition::getIcons(); + switch (conditionType) { + case CONDITION_HASTE: + icons |= ICON_HASTE; + break; + + case CONDITION_PARALYZE: + icons |= ICON_PARALYZE; + break; + + default: + break; + } + return icons; +} + +bool ConditionInvisible::startCondition(Creature* creature) +{ + if (!Condition::startCondition(creature)) { + return false; + } + + g_game.internalCreatureChangeVisible(creature, false); + return true; +} + +void ConditionInvisible::endCondition(Creature* creature) +{ + if (!creature->isInvisible()) { + g_game.internalCreatureChangeVisible(creature, true); + } +} + +void ConditionOutfit::setOutfit(const Outfit_t& outfit) +{ + this->outfit = outfit; +} + +bool ConditionOutfit::unserializeProp(ConditionAttr_t attr, PropStream& propStream) +{ + if (attr == CONDITIONATTR_OUTFIT) { + return propStream.read(outfit); + } + return Condition::unserializeProp(attr, propStream); +} + +void ConditionOutfit::serialize(PropWriteStream& propWriteStream) +{ + Condition::serialize(propWriteStream); + + propWriteStream.write(CONDITIONATTR_OUTFIT); + propWriteStream.write(outfit); +} + +bool ConditionOutfit::startCondition(Creature* creature) +{ + if (!Condition::startCondition(creature)) { + return false; + } + + g_game.internalCreatureChangeOutfit(creature, outfit); + return true; +} + +bool ConditionOutfit::executeCondition(Creature* creature, int32_t interval) +{ + return Condition::executeCondition(creature, interval); +} + +void ConditionOutfit::endCondition(Creature* creature) +{ + g_game.internalCreatureChangeOutfit(creature, creature->getDefaultOutfit()); +} + +void ConditionOutfit::addCondition(Creature* creature, const Condition* condition) +{ + if (updateCondition(condition)) { + setTicks(condition->getTicks()); + + const ConditionOutfit& conditionOutfit = static_cast(*condition); + outfit = conditionOutfit.outfit; + + g_game.internalCreatureChangeOutfit(creature, outfit); + } +} + +bool ConditionLight::startCondition(Creature* creature) +{ + if (!Condition::startCondition(creature)) { + return false; + } + + internalLightTicks = 0; + lightChangeInterval = ticks / lightInfo.level; + creature->setCreatureLight(lightInfo); + g_game.changeLight(creature); + return true; +} + +bool ConditionLight::executeCondition(Creature* creature, int32_t interval) +{ + internalLightTicks += interval; + + if (internalLightTicks >= lightChangeInterval) { + internalLightTicks = 0; + LightInfo lightInfo = creature->getCreatureLight(); + + if (lightInfo.level > 0) { + --lightInfo.level; + creature->setCreatureLight(lightInfo); + g_game.changeLight(creature); + } + } + + return Condition::executeCondition(creature, interval); +} + +void ConditionLight::endCondition(Creature* creature) +{ + creature->setNormalCreatureLight(); + g_game.changeLight(creature); +} + +void ConditionLight::addCondition(Creature* creature, const Condition* condition) +{ + if (updateCondition(condition)) { + setTicks(condition->getTicks()); + + const ConditionLight& conditionLight = static_cast(*condition); + lightInfo.level = conditionLight.lightInfo.level; + lightInfo.color = conditionLight.lightInfo.color; + lightChangeInterval = ticks / lightInfo.level; + internalLightTicks = 0; + creature->setCreatureLight(lightInfo); + g_game.changeLight(creature); + } +} + +bool ConditionLight::setParam(ConditionParam_t param, int32_t value) +{ + bool ret = Condition::setParam(param, value); + if (ret) { + return false; + } + + switch (param) { + case CONDITION_PARAM_LIGHT_LEVEL: + lightInfo.level = value; + return true; + + case CONDITION_PARAM_LIGHT_COLOR: + lightInfo.color = value; + return true; + + default: + return false; + } +} + +bool ConditionLight::unserializeProp(ConditionAttr_t attr, PropStream& propStream) +{ + if (attr == CONDITIONATTR_LIGHTCOLOR) { + uint32_t value; + if (!propStream.read(value)) { + return false; + } + + lightInfo.color = value; + return true; + } else if (attr == CONDITIONATTR_LIGHTLEVEL) { + uint32_t value; + if (!propStream.read(value)) { + return false; + } + + lightInfo.level = value; + return true; + } else if (attr == CONDITIONATTR_LIGHTTICKS) { + return propStream.read(internalLightTicks); + } else if (attr == CONDITIONATTR_LIGHTINTERVAL) { + return propStream.read(lightChangeInterval); + } + return Condition::unserializeProp(attr, propStream); +} + +void ConditionLight::serialize(PropWriteStream& propWriteStream) +{ + Condition::serialize(propWriteStream); + + // TODO: color and level could be serialized as 8-bit if we can retain backwards + // compatibility, but perhaps we should keep it like this in case they increase + // in the future... + propWriteStream.write(CONDITIONATTR_LIGHTCOLOR); + propWriteStream.write(lightInfo.color); + + propWriteStream.write(CONDITIONATTR_LIGHTLEVEL); + propWriteStream.write(lightInfo.level); + + propWriteStream.write(CONDITIONATTR_LIGHTTICKS); + propWriteStream.write(internalLightTicks); + + propWriteStream.write(CONDITIONATTR_LIGHTINTERVAL); + propWriteStream.write(lightChangeInterval); +} + +void ConditionSpellCooldown::addCondition(Creature* creature, const Condition* condition) +{ + if (updateCondition(condition)) { + setTicks(condition->getTicks()); + + if (subId != 0 && ticks > 0) { + Player* player = creature->getPlayer(); + if (player) { + player->sendSpellCooldown(subId, ticks); + } + } + } +} + +bool ConditionSpellCooldown::startCondition(Creature* creature) +{ + if (!Condition::startCondition(creature)) { + return false; + } + + if (subId != 0 && ticks > 0) { + Player* player = creature->getPlayer(); + if (player) { + player->sendSpellCooldown(subId, ticks); + } + } + return true; +} + +void ConditionSpellGroupCooldown::addCondition(Creature* creature, const Condition* condition) +{ + if (updateCondition(condition)) { + setTicks(condition->getTicks()); + + if (subId != 0 && ticks > 0) { + Player* player = creature->getPlayer(); + if (player) { + player->sendSpellGroupCooldown(static_cast(subId), ticks); + } + } + } +} + +bool ConditionSpellGroupCooldown::startCondition(Creature* creature) +{ + if (!Condition::startCondition(creature)) { + return false; + } + + if (subId != 0 && ticks > 0) { + Player* player = creature->getPlayer(); + if (player) { + player->sendSpellGroupCooldown(static_cast(subId), ticks); + } + } + return true; +} diff --git a/src/condition.h b/src/condition.h new file mode 100644 index 0000000..5cf0df5 --- /dev/null +++ b/src/condition.h @@ -0,0 +1,432 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_CONDITION_H_F92FF8BDDD5B4EA59E2B1BB5C9C0A086 +#define FS_CONDITION_H_F92FF8BDDD5B4EA59E2B1BB5C9C0A086 + +#include "fileloader.h" +#include "enums.h" + +class Creature; +class Player; +class PropStream; + +enum ConditionAttr_t { + CONDITIONATTR_TYPE = 1, + CONDITIONATTR_ID, + CONDITIONATTR_TICKS, + CONDITIONATTR_HEALTHTICKS, + CONDITIONATTR_HEALTHGAIN, + CONDITIONATTR_MANATICKS, + CONDITIONATTR_MANAGAIN, + CONDITIONATTR_DELAYED, + CONDITIONATTR_OWNER, + CONDITIONATTR_INTERVALDATA, + CONDITIONATTR_SPEEDDELTA, + CONDITIONATTR_FORMULA_MINA, + CONDITIONATTR_FORMULA_MINB, + CONDITIONATTR_FORMULA_MAXA, + CONDITIONATTR_FORMULA_MAXB, + CONDITIONATTR_LIGHTCOLOR, + CONDITIONATTR_LIGHTLEVEL, + CONDITIONATTR_LIGHTTICKS, + CONDITIONATTR_LIGHTINTERVAL, + CONDITIONATTR_SOULTICKS, + CONDITIONATTR_SOULGAIN, + CONDITIONATTR_SKILLS, + CONDITIONATTR_STATS, + CONDITIONATTR_OUTFIT, + CONDITIONATTR_PERIODDAMAGE, + CONDITIONATTR_ISBUFF, + CONDITIONATTR_SUBID, + CONDITIONATTR_ISAGGRESSIVE, + CONDITIONATTR_DISABLEDEFENSE, + + //reserved for serialization + CONDITIONATTR_END = 254, +}; + +struct IntervalInfo { + int32_t timeLeft; + int32_t value; + int32_t interval; +}; + +class Condition +{ + public: + Condition() = default; + Condition(ConditionId_t id, ConditionType_t type, int32_t ticks, bool buff = false, uint32_t subId = 0, bool aggressive = false) : + endTime(ticks == -1 ? std::numeric_limits::max() : 0), + subId(subId), ticks(ticks), conditionType(type), isBuff(buff), aggressive(aggressive), id(id) {} + virtual ~Condition() = default; + + virtual bool startCondition(Creature* creature); + virtual bool executeCondition(Creature* creature, int32_t interval); + virtual void endCondition(Creature* creature) = 0; + virtual void addCondition(Creature* creature, const Condition* condition) = 0; + virtual uint32_t getIcons() const; + ConditionId_t getId() const { + return id; + } + uint32_t getSubId() const { + return subId; + } + + virtual Condition* clone() const = 0; + + ConditionType_t getType() const { + return conditionType; + } + int64_t getEndTime() const { + return endTime; + } + int32_t getTicks() const { + return ticks; + } + void setTicks(int32_t newTicks); + bool isAggressive() const { + return aggressive; + } + + static Condition* createCondition(ConditionId_t id, ConditionType_t type, int32_t ticks, int32_t param = 0, bool buff = false, uint32_t subId = 0, bool aggressive = false); + static Condition* createCondition(PropStream& propStream); + + virtual bool setParam(ConditionParam_t param, int32_t value); + + //serialization + bool unserialize(PropStream& propStream); + virtual void serialize(PropWriteStream& propWriteStream); + virtual bool unserializeProp(ConditionAttr_t attr, PropStream& propStream); + + bool isPersistent() const; + + protected: + virtual bool updateCondition(const Condition* addCondition); + + int64_t endTime; + uint32_t subId; + int32_t ticks; + ConditionType_t conditionType; + bool isBuff; + bool aggressive; + + private: + ConditionId_t id; +}; + +class ConditionGeneric : public Condition +{ + public: + ConditionGeneric(ConditionId_t id, ConditionType_t type, int32_t ticks, bool buff = false, uint32_t subId = 0, bool aggressive = false): + Condition(id, type, ticks, buff, subId, aggressive) {} + + bool startCondition(Creature* creature) override; + bool executeCondition(Creature* creature, int32_t interval) override; + void endCondition(Creature* creature) override; + void addCondition(Creature* creature, const Condition* condition) override; + uint32_t getIcons() const override; + + ConditionGeneric* clone() const override { + return new ConditionGeneric(*this); + } +}; + +class ConditionAttributes final : public ConditionGeneric +{ + public: + ConditionAttributes(ConditionId_t id, ConditionType_t type, int32_t ticks, bool buff = false, uint32_t subId = 0, bool aggressive = false) : + ConditionGeneric(id, type, ticks, buff, subId, aggressive) {} + + bool startCondition(Creature* creature) override; + bool executeCondition(Creature* creature, int32_t interval) override; + void endCondition(Creature* creature) override; + void addCondition(Creature* creature, const Condition* condition) override; + + bool setParam(ConditionParam_t param, int32_t value) override; + + ConditionAttributes* clone() const override { + return new ConditionAttributes(*this); + } + + //serialization + void serialize(PropWriteStream& propWriteStream) override; + bool unserializeProp(ConditionAttr_t attr, PropStream& propStream) override; + + private: + int32_t skills[SKILL_LAST + 1] = {}; + int32_t skillsPercent[SKILL_LAST + 1] = {}; + int32_t specialSkills[SPECIALSKILL_LAST + 1] = {}; + int32_t stats[STAT_LAST + 1] = {}; + int32_t statsPercent[STAT_LAST + 1] = {}; + int32_t currentSkill = 0; + int32_t currentStat = 0; + + bool disableDefense = false; + + void updatePercentStats(Player* player); + void updateStats(Player* player); + void updatePercentSkills(Player* player); + void updateSkills(Player* player); +}; + +class ConditionRegeneration final : public ConditionGeneric +{ + public: + ConditionRegeneration(ConditionId_t id, ConditionType_t type, int32_t ticks, bool buff = false, uint32_t subId = 0, bool aggressive = false): + ConditionGeneric(id, type, ticks, buff, subId, aggressive) {} + + void addCondition(Creature* creature, const Condition* condition) override; + bool executeCondition(Creature* creature, int32_t interval) override; + + bool setParam(ConditionParam_t param, int32_t value) override; + + ConditionRegeneration* clone() const override { + return new ConditionRegeneration(*this); + } + + //serialization + void serialize(PropWriteStream& propWriteStream) override; + bool unserializeProp(ConditionAttr_t attr, PropStream& propStream) override; + + private: + uint32_t internalHealthTicks = 0; + uint32_t internalManaTicks = 0; + + uint32_t healthTicks = 1000; + uint32_t manaTicks = 1000; + uint32_t healthGain = 0; + uint32_t manaGain = 0; +}; + +class ConditionSoul final : public ConditionGeneric +{ + public: + ConditionSoul(ConditionId_t id, ConditionType_t type, int32_t ticks, bool buff = false, uint32_t subId = 0, bool aggressive = false) : + ConditionGeneric(id, type, ticks, buff, subId, aggressive) {} + + void addCondition(Creature* creature, const Condition* condition) override; + bool executeCondition(Creature* creature, int32_t interval) override; + + bool setParam(ConditionParam_t param, int32_t value) override; + + ConditionSoul* clone() const override { + return new ConditionSoul(*this); + } + + //serialization + void serialize(PropWriteStream& propWriteStream) override; + bool unserializeProp(ConditionAttr_t attr, PropStream& propStream) override; + + private: + uint32_t internalSoulTicks = 0; + uint32_t soulTicks = 0; + uint32_t soulGain = 0; +}; + +class ConditionInvisible final : public ConditionGeneric +{ + public: + ConditionInvisible(ConditionId_t id, ConditionType_t type, int32_t ticks, bool buff = false, uint32_t subId = 0, bool aggressive = false) : + ConditionGeneric(id, type, ticks, buff, subId, aggressive) {} + + bool startCondition(Creature* creature) override; + void endCondition(Creature* creature) override; + + ConditionInvisible* clone() const override { + return new ConditionInvisible(*this); + } +}; + +class ConditionDamage final : public Condition +{ + public: + ConditionDamage() = default; + ConditionDamage(ConditionId_t id, ConditionType_t type, bool buff = false, uint32_t subId = 0, bool aggressive = true) : + Condition(id, type, 0, buff, subId, aggressive) {} + + static void generateDamageList(int32_t amount, int32_t start, std::list& list); + + bool startCondition(Creature* creature) override; + bool executeCondition(Creature* creature, int32_t interval) override; + void endCondition(Creature* creature) override; + void addCondition(Creature* creature, const Condition* condition) override; + uint32_t getIcons() const override; + + ConditionDamage* clone() const override { + return new ConditionDamage(*this); + } + + bool setParam(ConditionParam_t param, int32_t value) override; + + bool addDamage(int32_t rounds, int32_t time, int32_t value); + bool doForceUpdate() const { + return forceUpdate; + } + int32_t getTotalDamage() const; + + void setInitDamage(int32_t initDamage) { + this->initDamage = initDamage; + } + + //serialization + void serialize(PropWriteStream& propWriteStream) override; + bool unserializeProp(ConditionAttr_t attr, PropStream& propStream) override; + + private: + int32_t maxDamage = 0; + int32_t minDamage = 0; + int32_t startDamage = 0; + int32_t periodDamage = 0; + int32_t periodDamageTick = 0; + int32_t tickInterval = 2000; + int32_t initDamage = 0; + + bool forceUpdate = false; + bool delayed = false; + bool field = false; + uint32_t owner = 0; + + bool init(); + + std::list damageList; + + bool getNextDamage(int32_t& damage); + bool doDamage(Creature* creature, int32_t healthChange); + + bool updateCondition(const Condition* addCondition) override; +}; + +class ConditionSpeed final : public Condition +{ + public: + ConditionSpeed(ConditionId_t id, ConditionType_t type, int32_t ticks, bool buff, uint32_t subId, int32_t changeSpeed, bool aggressive = false) : + Condition(id, type, ticks, buff, subId, aggressive), speedDelta(changeSpeed) {} + + bool startCondition(Creature* creature) override; + bool executeCondition(Creature* creature, int32_t interval) override; + void endCondition(Creature* creature) override; + void addCondition(Creature* creature, const Condition* condition) override; + uint32_t getIcons() const override; + + ConditionSpeed* clone() const override { + return new ConditionSpeed(*this); + } + + bool setParam(ConditionParam_t param, int32_t value) override; + + void setFormulaVars(float mina, float minb, float maxa, float maxb); + + //serialization + void serialize(PropWriteStream& propWriteStream) override; + bool unserializeProp(ConditionAttr_t attr, PropStream& propStream) override; + + private: + void getFormulaValues(int32_t var, int32_t& min, int32_t& max) const; + + int32_t speedDelta; + + //formula variables + float mina = 0.0f; + float minb = 0.0f; + float maxa = 0.0f; + float maxb = 0.0f; +}; + +class ConditionOutfit final : public Condition +{ + public: + ConditionOutfit(ConditionId_t id, ConditionType_t type, int32_t ticks, bool buff = false, uint32_t subId = 0, bool aggressive = false) : + Condition(id, type, ticks, buff, subId, aggressive) {} + + bool startCondition(Creature* creature) override; + bool executeCondition(Creature* creature, int32_t interval) override; + void endCondition(Creature* creature) override; + void addCondition(Creature* creature, const Condition* condition) override; + + ConditionOutfit* clone() const override { + return new ConditionOutfit(*this); + } + + void setOutfit(const Outfit_t& outfit); + + //serialization + void serialize(PropWriteStream& propWriteStream) override; + bool unserializeProp(ConditionAttr_t attr, PropStream& propStream) override; + + private: + Outfit_t outfit; +}; + +class ConditionLight final : public Condition +{ + public: + ConditionLight(ConditionId_t id, ConditionType_t type, int32_t ticks, bool buff, uint32_t subId, uint8_t lightlevel, uint8_t lightcolor, bool aggressive = false) : + Condition(id, type, ticks, buff, subId, aggressive), lightInfo(lightlevel, lightcolor) {} + + bool startCondition(Creature* creature) override; + bool executeCondition(Creature* creature, int32_t interval) override; + void endCondition(Creature* creature) override; + void addCondition(Creature* creature, const Condition* condition) override; + + ConditionLight* clone() const override { + return new ConditionLight(*this); + } + + bool setParam(ConditionParam_t param, int32_t value) override; + + //serialization + void serialize(PropWriteStream& propWriteStream) override; + bool unserializeProp(ConditionAttr_t attr, PropStream& propStream) override; + + private: + LightInfo lightInfo; + uint32_t internalLightTicks = 0; + uint32_t lightChangeInterval = 0; +}; + +class ConditionSpellCooldown final : public ConditionGeneric +{ + public: + ConditionSpellCooldown(ConditionId_t id, ConditionType_t type, int32_t ticks, bool buff = false, uint32_t subId = 0, bool aggressive = false) : + ConditionGeneric(id, type, ticks, buff, subId, aggressive) {} + + bool startCondition(Creature* creature) override; + void addCondition(Creature* creature, const Condition* condition) override; + + ConditionSpellCooldown* clone() const override { + return new ConditionSpellCooldown(*this); + } +}; + +class ConditionSpellGroupCooldown final : public ConditionGeneric +{ + public: + ConditionSpellGroupCooldown(ConditionId_t id, ConditionType_t type, int32_t ticks, bool buff = false, uint32_t subId = 0, bool aggressive = false) : + ConditionGeneric(id, type, ticks, buff, subId, aggressive) {} + + bool startCondition(Creature* creature) override; + void addCondition(Creature* creature, const Condition* condition) override; + + ConditionSpellGroupCooldown* clone() const override { + return new ConditionSpellGroupCooldown(*this); + } +}; + +#endif diff --git a/src/configmanager.cpp b/src/configmanager.cpp new file mode 100644 index 0000000..c90b4ad --- /dev/null +++ b/src/configmanager.cpp @@ -0,0 +1,233 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include + +#include "configmanager.h" +#include "game.h" + +#if LUA_VERSION_NUM >= 502 +#undef lua_strlen +#define lua_strlen lua_rawlen +#endif + +extern Game g_game; + +namespace { + +std::string getGlobalString(lua_State* L, const char* identifier, const char* defaultValue) +{ + lua_getglobal(L, identifier); + if (!lua_isstring(L, -1)) { + lua_pop(L, 1); + return defaultValue; + } + + size_t len = lua_strlen(L, -1); + std::string ret(lua_tostring(L, -1), len); + lua_pop(L, 1); + return ret; +} + +int32_t getGlobalNumber(lua_State* L, const char* identifier, const int32_t defaultValue = 0) +{ + lua_getglobal(L, identifier); + if (!lua_isnumber(L, -1)) { + lua_pop(L, 1); + return defaultValue; + } + + int32_t val = lua_tonumber(L, -1); + lua_pop(L, 1); + return val; +} + +bool getGlobalBoolean(lua_State* L, const char* identifier, const bool defaultValue) +{ + lua_getglobal(L, identifier); + if (!lua_isboolean(L, -1)) { + if (!lua_isstring(L, -1)) { + lua_pop(L, 1); + return defaultValue; + } + + size_t len = lua_strlen(L, -1); + std::string ret(lua_tostring(L, -1), len); + lua_pop(L, 1); + return booleanString(ret); + } + + int val = lua_toboolean(L, -1); + lua_pop(L, 1); + return val != 0; +} + +} + +bool ConfigManager::load() +{ + lua_State* L = luaL_newstate(); + if (!L) { + throw std::runtime_error("Failed to allocate memory"); + } + + luaL_openlibs(L); + + if (luaL_dofile(L, "config.lua")) { + std::cout << "[Error - ConfigManager::load] " << lua_tostring(L, -1) << std::endl; + lua_close(L); + return false; + } + + //parse config + if (!loaded) { //info that must be loaded one time (unless we reset the modules involved) + boolean[BIND_ONLY_GLOBAL_ADDRESS] = getGlobalBoolean(L, "bindOnlyGlobalAddress", false); + boolean[OPTIMIZE_DATABASE] = getGlobalBoolean(L, "startupDatabaseOptimization", true); + + string[IP] = getGlobalString(L, "ip", "127.0.0.1"); + string[MAP_NAME] = getGlobalString(L, "mapName", "forgotten"); + string[MAP_AUTHOR] = getGlobalString(L, "mapAuthor", "Unknown"); + string[HOUSE_RENT_PERIOD] = getGlobalString(L, "houseRentPeriod", "never"); + string[MYSQL_HOST] = getGlobalString(L, "mysqlHost", "127.0.0.1"); + string[MYSQL_USER] = getGlobalString(L, "mysqlUser", "forgottenserver"); + string[MYSQL_PASS] = getGlobalString(L, "mysqlPass", ""); + string[MYSQL_DB] = getGlobalString(L, "mysqlDatabase", "forgottenserver"); + string[MYSQL_SOCK] = getGlobalString(L, "mysqlSock", ""); + + integer[SQL_PORT] = getGlobalNumber(L, "mysqlPort", 3306); + integer[GAME_PORT] = getGlobalNumber(L, "gameProtocolPort", 7172); + integer[LOGIN_PORT] = getGlobalNumber(L, "loginProtocolPort", 7171); + integer[STATUS_PORT] = getGlobalNumber(L, "statusProtocolPort", 7171); + + integer[MARKET_OFFER_DURATION] = getGlobalNumber(L, "marketOfferDuration", 30 * 24 * 60 * 60); + } + + boolean[ALLOW_CHANGEOUTFIT] = getGlobalBoolean(L, "allowChangeOutfit", true); + boolean[ONE_PLAYER_ON_ACCOUNT] = getGlobalBoolean(L, "onePlayerOnlinePerAccount", true); + boolean[AIMBOT_HOTKEY_ENABLED] = getGlobalBoolean(L, "hotkeyAimbotEnabled", true); + boolean[REMOVE_RUNE_CHARGES] = getGlobalBoolean(L, "removeChargesFromRunes", true); + boolean[REMOVE_WEAPON_AMMO] = getGlobalBoolean(L, "removeWeaponAmmunition", true); + boolean[REMOVE_WEAPON_CHARGES] = getGlobalBoolean(L, "removeWeaponCharges", true); + boolean[REMOVE_POTION_CHARGES] = getGlobalBoolean(L, "removeChargesFromPotions", true); + boolean[EXPERIENCE_FROM_PLAYERS] = getGlobalBoolean(L, "experienceByKillingPlayers", false); + boolean[FREE_PREMIUM] = getGlobalBoolean(L, "freePremium", false); + boolean[REPLACE_KICK_ON_LOGIN] = getGlobalBoolean(L, "replaceKickOnLogin", true); + boolean[ALLOW_CLONES] = getGlobalBoolean(L, "allowClones", false); + boolean[MARKET_PREMIUM] = getGlobalBoolean(L, "premiumToCreateMarketOffer", true); + boolean[EMOTE_SPELLS] = getGlobalBoolean(L, "emoteSpells", false); + boolean[STAMINA_SYSTEM] = getGlobalBoolean(L, "staminaSystem", true); + boolean[WARN_UNSAFE_SCRIPTS] = getGlobalBoolean(L, "warnUnsafeScripts", true); + boolean[CONVERT_UNSAFE_SCRIPTS] = getGlobalBoolean(L, "convertUnsafeScripts", true); + boolean[CLASSIC_EQUIPMENT_SLOTS] = getGlobalBoolean(L, "classicEquipmentSlots", false); + boolean[ENABLE_LIVE_CASTING] = getGlobalBoolean(L, "enableLiveCasting", false); + boolean[CLASSIC_ATTACK_SPEED] = getGlobalBoolean(L, "classicAttackSpeed", false); + boolean[SCRIPTS_CONSOLE_LOGS] = getGlobalBoolean(L, "showScriptsLogInConsole", true); + boolean[SERVER_SAVE_NOTIFY_MESSAGE] = getGlobalBoolean(L, "serverSaveNotifyMessage", true); + boolean[SERVER_SAVE_CLEAN_MAP] = getGlobalBoolean(L, "serverSaveCleanMap", false); + boolean[SERVER_SAVE_CLOSE] = getGlobalBoolean(L, "serverSaveClose", false); + boolean[SERVER_SAVE_SHUTDOWN] = getGlobalBoolean(L, "serverSaveShutdown", true); + boolean[ONLINE_OFFLINE_CHARLIST] = getGlobalBoolean(L, "showOnlineStatusInCharlist", false); + boolean[YELL_ALLOW_PREMIUM] = getGlobalBoolean(L, "yellAlwaysAllowPremium", false); + boolean[FORCE_MONSTERTYPE_LOAD] = getGlobalBoolean(L, "forceMonsterTypesOnLoad", true); + boolean[HOUSE_OWNED_BY_ACCOUNT] = getGlobalBoolean(L, "houseOwnedByAccount", false); + boolean[HOUSE_DOOR_SHOW_PRICE] = getGlobalBoolean(L, "houseDoorShowPrice", true); + + string[DEFAULT_PRIORITY] = getGlobalString(L, "defaultPriority", "high"); + string[SERVER_NAME] = getGlobalString(L, "serverName", ""); + string[OWNER_NAME] = getGlobalString(L, "ownerName", ""); + string[OWNER_EMAIL] = getGlobalString(L, "ownerEmail", ""); + string[URL] = getGlobalString(L, "url", ""); + string[LOCATION] = getGlobalString(L, "location", ""); + string[MOTD] = getGlobalString(L, "motd", ""); + string[WORLD_TYPE] = getGlobalString(L, "worldType", "pvp"); + + integer[MAX_PLAYERS] = getGlobalNumber(L, "maxPlayers"); + integer[PZ_LOCKED] = getGlobalNumber(L, "pzLocked", 60000); + integer[DEFAULT_DESPAWNRANGE] = getGlobalNumber(L, "deSpawnRange", 2); + integer[DEFAULT_DESPAWNRADIUS] = getGlobalNumber(L, "deSpawnRadius", 50); + integer[RATE_EXPERIENCE] = getGlobalNumber(L, "rateExp", 5); + integer[RATE_SKILL] = getGlobalNumber(L, "rateSkill", 3); + integer[RATE_LOOT] = getGlobalNumber(L, "rateLoot", 2); + integer[RATE_MAGIC] = getGlobalNumber(L, "rateMagic", 3); + integer[RATE_SPAWN] = getGlobalNumber(L, "rateSpawn", 1); + integer[HOUSE_PRICE] = getGlobalNumber(L, "housePriceEachSQM", 1000); + integer[KILLS_TO_RED] = getGlobalNumber(L, "killsToRedSkull", 3); + integer[KILLS_TO_BLACK] = getGlobalNumber(L, "killsToBlackSkull", 6); + integer[ACTIONS_DELAY_INTERVAL] = getGlobalNumber(L, "timeBetweenActions", 200); + integer[EX_ACTIONS_DELAY_INTERVAL] = getGlobalNumber(L, "timeBetweenExActions", 1000); + integer[MAX_MESSAGEBUFFER] = getGlobalNumber(L, "maxMessageBuffer", 4); + integer[KICK_AFTER_MINUTES] = getGlobalNumber(L, "kickIdlePlayerAfterMinutes", 15); + integer[PROTECTION_LEVEL] = getGlobalNumber(L, "protectionLevel", 1); + integer[DEATH_LOSE_PERCENT] = getGlobalNumber(L, "deathLosePercent", -1); + integer[STATUSQUERY_TIMEOUT] = getGlobalNumber(L, "statusTimeout", 5000); + integer[FRAG_TIME] = getGlobalNumber(L, "timeToDecreaseFrags", 24 * 60 * 60); + integer[WHITE_SKULL_TIME] = getGlobalNumber(L, "whiteSkullTime", 15 * 60); + integer[STAIRHOP_DELAY] = getGlobalNumber(L, "stairJumpExhaustion", 2000); + integer[EXP_FROM_PLAYERS_LEVEL_RANGE] = getGlobalNumber(L, "expFromPlayersLevelRange", 75); + integer[CHECK_EXPIRED_MARKET_OFFERS_EACH_MINUTES] = getGlobalNumber(L, "checkExpiredMarketOffersEachMinutes", 60); + integer[MAX_MARKET_OFFERS_AT_A_TIME_PER_PLAYER] = getGlobalNumber(L, "maxMarketOffersAtATimePerPlayer", 100); + integer[MAX_PACKETS_PER_SECOND] = getGlobalNumber(L, "maxPacketsPerSecond", 25); + integer[LIVE_CAST_PORT] = getGlobalNumber(L, "liveCastPort", 7173); + integer[SERVER_SAVE_NOTIFY_DURATION] = getGlobalNumber(L, "serverSaveNotifyDuration", 5); + integer[YELL_MINIMUM_LEVEL] = getGlobalNumber(L, "yellMinimumLevel", 2); + + loaded = true; + lua_close(L); + return true; +} + +bool ConfigManager::reload() +{ + bool result = load(); + if (transformToSHA1(getString(ConfigManager::MOTD)) != g_game.getMotdHash()) { + g_game.incrementMotdNum(); + } + return result; +} + +static std::string dummyStr; + +const std::string& ConfigManager::getString(string_config_t what) const +{ + if (what >= LAST_STRING_CONFIG) { + std::cout << "[Warning - ConfigManager::getString] Accessing invalid index: " << what << std::endl; + return dummyStr; + } + return string[what]; +} + +int32_t ConfigManager::getNumber(integer_config_t what) const +{ + if (what >= LAST_INTEGER_CONFIG) { + std::cout << "[Warning - ConfigManager::getNumber] Accessing invalid index: " << what << std::endl; + return 0; + } + return integer[what]; +} + +bool ConfigManager::getBoolean(boolean_config_t what) const +{ + if (what >= LAST_BOOLEAN_CONFIG) { + std::cout << "[Warning - ConfigManager::getBoolean] Accessing invalid index: " << what << std::endl; + return false; + } + return boolean[what]; +} diff --git a/src/configmanager.h b/src/configmanager.h new file mode 100644 index 0000000..8218121 --- /dev/null +++ b/src/configmanager.h @@ -0,0 +1,138 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_CONFIGMANAGER_H_6BDD23BD0B8344F4B7C40E8BE6AF6F39 +#define FS_CONFIGMANAGER_H_6BDD23BD0B8344F4B7C40E8BE6AF6F39 + +class ConfigManager +{ + public: + enum boolean_config_t { + ALLOW_CHANGEOUTFIT, + ONE_PLAYER_ON_ACCOUNT, + AIMBOT_HOTKEY_ENABLED, + REMOVE_RUNE_CHARGES, + REMOVE_WEAPON_AMMO, + REMOVE_WEAPON_CHARGES, + REMOVE_POTION_CHARGES, + EXPERIENCE_FROM_PLAYERS, + FREE_PREMIUM, + REPLACE_KICK_ON_LOGIN, + ALLOW_CLONES, + BIND_ONLY_GLOBAL_ADDRESS, + OPTIMIZE_DATABASE, + MARKET_PREMIUM, + EMOTE_SPELLS, + STAMINA_SYSTEM, + WARN_UNSAFE_SCRIPTS, + CONVERT_UNSAFE_SCRIPTS, + CLASSIC_EQUIPMENT_SLOTS, + ENABLE_LIVE_CASTING, + CLASSIC_ATTACK_SPEED, + SCRIPTS_CONSOLE_LOGS, + SERVER_SAVE_NOTIFY_MESSAGE, + SERVER_SAVE_CLEAN_MAP, + SERVER_SAVE_CLOSE, + SERVER_SAVE_SHUTDOWN, + ONLINE_OFFLINE_CHARLIST, + YELL_ALLOW_PREMIUM, + FORCE_MONSTERTYPE_LOAD, + HOUSE_OWNED_BY_ACCOUNT, + HOUSE_DOOR_SHOW_PRICE, + + LAST_BOOLEAN_CONFIG /* this must be the last one */ + }; + + enum string_config_t { + MAP_NAME, + HOUSE_RENT_PERIOD, + SERVER_NAME, + OWNER_NAME, + OWNER_EMAIL, + URL, + LOCATION, + IP, + MOTD, + WORLD_TYPE, + MYSQL_HOST, + MYSQL_USER, + MYSQL_PASS, + MYSQL_DB, + MYSQL_SOCK, + DEFAULT_PRIORITY, + MAP_AUTHOR, + + LAST_STRING_CONFIG /* this must be the last one */ + }; + + enum integer_config_t { + SQL_PORT, + MAX_PLAYERS, + PZ_LOCKED, + DEFAULT_DESPAWNRANGE, + DEFAULT_DESPAWNRADIUS, + RATE_EXPERIENCE, + RATE_SKILL, + RATE_LOOT, + RATE_MAGIC, + RATE_SPAWN, + HOUSE_PRICE, + KILLS_TO_RED, + KILLS_TO_BLACK, + MAX_MESSAGEBUFFER, + ACTIONS_DELAY_INTERVAL, + EX_ACTIONS_DELAY_INTERVAL, + KICK_AFTER_MINUTES, + PROTECTION_LEVEL, + DEATH_LOSE_PERCENT, + STATUSQUERY_TIMEOUT, + FRAG_TIME, + WHITE_SKULL_TIME, + GAME_PORT, + LOGIN_PORT, + STATUS_PORT, + STAIRHOP_DELAY, + MARKET_OFFER_DURATION, + CHECK_EXPIRED_MARKET_OFFERS_EACH_MINUTES, + MAX_MARKET_OFFERS_AT_A_TIME_PER_PLAYER, + EXP_FROM_PLAYERS_LEVEL_RANGE, + MAX_PACKETS_PER_SECOND, + LIVE_CAST_PORT, + SERVER_SAVE_NOTIFY_DURATION, + YELL_MINIMUM_LEVEL, + + LAST_INTEGER_CONFIG /* this must be the last one */ + }; + + bool load(); + bool reload(); + + const std::string& getString(string_config_t what) const; + int32_t getNumber(integer_config_t what) const; + bool getBoolean(boolean_config_t what) const; + + private: + std::string string[LAST_STRING_CONFIG] = {}; + int32_t integer[LAST_INTEGER_CONFIG] = {}; + bool boolean[LAST_BOOLEAN_CONFIG] = {}; + + bool loaded = false; +}; + +#endif diff --git a/src/connection.cpp b/src/connection.cpp new file mode 100644 index 0000000..2b41721 --- /dev/null +++ b/src/connection.cpp @@ -0,0 +1,341 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2016 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "configmanager.h" +#include "connection.h" +#include "outputmessage.h" +#include "protocol.h" +#include "protocolgame.h" +#include "scheduler.h" +#include "server.h" + +extern ConfigManager g_config; + +Connection_ptr ConnectionManager::createConnection(boost::asio::io_service& io_service, ConstServicePort_ptr servicePort) +{ + std::lock_guard lockClass(connectionManagerLock); + + auto connection = std::make_shared(io_service, servicePort); + connections.insert(connection); + return connection; +} + +void ConnectionManager::releaseConnection(const Connection_ptr& connection) +{ + std::lock_guard lockClass(connectionManagerLock); + + connections.erase(connection); +} + +void ConnectionManager::closeAll() +{ + std::lock_guard lockClass(connectionManagerLock); + + for (const auto& connection : connections) { + try { + boost::system::error_code error; + connection->socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, error); + connection->socket.close(error); + } catch (boost::system::system_error&) { + } + } + connections.clear(); +} + +// Connection + +void Connection::close(bool force) +{ + //any thread + ConnectionManager::getInstance().releaseConnection(shared_from_this()); + + std::lock_guard lockClass(connectionLock); + if (connectionState != CONNECTION_STATE_OPEN) { + return; + } + connectionState = CONNECTION_STATE_CLOSED; + + if (protocol) { + g_dispatcher.addTask( + createTask(std::bind(&Protocol::release, protocol))); + } + + if (messageQueue.empty() || force) { + closeSocket(); + } else { + //will be closed by the destructor or onWriteOperation + } +} + +void Connection::closeSocket() +{ + if (socket.is_open()) { + try { + readTimer.cancel(); + writeTimer.cancel(); + boost::system::error_code error; + socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, error); + socket.close(error); + } catch (boost::system::system_error& e) { + std::cout << "[Network error - Connection::closeSocket] " << e.what() << std::endl; + } + } +} + +Connection::~Connection() +{ + closeSocket(); +} + +void Connection::accept(Protocol_ptr protocol) +{ + this->protocol = protocol; + g_dispatcher.addTask(createTask(std::bind(&Protocol::onConnect, protocol))); + + accept(); +} + +void Connection::accept() +{ + std::lock_guard lockClass(connectionLock); + try { + readTimer.expires_from_now(boost::posix_time::seconds(CONNECTION_READ_TIMEOUT)); + readTimer.async_wait(std::bind(&Connection::handleTimeout, std::weak_ptr(shared_from_this()), std::placeholders::_1)); + + // Read size of the first packet + boost::asio::async_read(socket, + boost::asio::buffer(msg.getBuffer(), NetworkMessage::HEADER_LENGTH), + std::bind(&Connection::parseHeader, shared_from_this(), std::placeholders::_1)); + } catch (boost::system::system_error& e) { + std::cout << "[Network error - Connection::accept] " << e.what() << std::endl; + close(FORCE_CLOSE); + } +} + +void Connection::parseHeader(const boost::system::error_code& error) +{ + std::lock_guard lockClass(connectionLock); + readTimer.cancel(); + + if (error) { + close(FORCE_CLOSE); + return; + } else if (connectionState != CONNECTION_STATE_OPEN) { + return; + } + + uint32_t timePassed = std::max(1, (time(nullptr) - timeConnected) + 1); + if ((++packetsSent / timePassed) > static_cast(g_config.getNumber(ConfigManager::MAX_PACKETS_PER_SECOND))) { + std::cout << convertIPToString(getIP()) << " disconnected for exceeding packet per second limit." << std::endl; + close(); + return; + } + + if (timePassed > 2) { + timeConnected = time(nullptr); + packetsSent = 0; + } + + uint16_t size = msg.getLengthHeader(); + if (size == 0 || size >= NETWORKMESSAGE_MAXSIZE - 16) { + close(FORCE_CLOSE); + return; + } + + try { + readTimer.expires_from_now(boost::posix_time::seconds(CONNECTION_READ_TIMEOUT)); + readTimer.async_wait(std::bind(&Connection::handleTimeout, std::weak_ptr(shared_from_this()), + std::placeholders::_1)); + + // Read packet content + msg.setLength(size + NetworkMessage::HEADER_LENGTH); + boost::asio::async_read(socket, boost::asio::buffer(msg.getBodyBuffer(), size), + std::bind(&Connection::parsePacket, shared_from_this(), std::placeholders::_1)); + } catch (boost::system::system_error& e) { + std::cout << "[Network error - Connection::parseHeader] " << e.what() << std::endl; + close(FORCE_CLOSE); + } +} + +void Connection::parsePacket(const boost::system::error_code& error) +{ + std::lock_guard lockClass(connectionLock); + readTimer.cancel(); + + if (error) { + close(FORCE_CLOSE); + return; + } else if (connectionState != CONNECTION_STATE_OPEN) { + return; + } + + //Check packet checksum + uint32_t checksum; + int32_t len = msg.getLength() - msg.getBufferPosition() - NetworkMessage::CHECKSUM_LENGTH; + if (len > 0) { + checksum = adlerChecksum(msg.getBuffer() + msg.getBufferPosition() + NetworkMessage::CHECKSUM_LENGTH, len); + } else { + checksum = 0; + } + + uint32_t recvChecksum = msg.get(); + if (recvChecksum != checksum) { + // it might not have been the checksum, step back + msg.skipBytes(-NetworkMessage::CHECKSUM_LENGTH); + } + + if (!receivedFirst) { + // First message received + receivedFirst = true; + + if (!protocol) { + // Game protocol has already been created at this point + protocol = service_port->make_protocol(recvChecksum == checksum, msg, shared_from_this()); + if (!protocol) { + close(FORCE_CLOSE); + return; + } + } else { + msg.skipBytes(1); // Skip protocol ID + } + + protocol->onRecvFirstMessage(msg); + } else { + protocol->onRecvMessage(msg); // Send the packet to the current protocol + } + + try { + readTimer.expires_from_now(boost::posix_time::seconds(CONNECTION_READ_TIMEOUT)); + readTimer.async_wait(std::bind(&Connection::handleTimeout, std::weak_ptr(shared_from_this()), + std::placeholders::_1)); + + // Wait to the next packet + boost::asio::async_read(socket, + boost::asio::buffer(msg.getBuffer(), NetworkMessage::HEADER_LENGTH), + std::bind(&Connection::parseHeader, shared_from_this(), std::placeholders::_1)); + } catch (boost::system::system_error& e) { + std::cout << "[Network error - Connection::parsePacket] " << e.what() << std::endl; + close(FORCE_CLOSE); + } +} + +void Connection::send(const OutputMessage_ptr& msg) +{ + std::lock_guard lockClass(connectionLock); + if (connectionState != CONNECTION_STATE_OPEN) { + return; + } + + bool noPendingWrite = messageQueue.empty(); + messageQueue.emplace_back(msg); + if (noPendingWrite) { + internalSend(msg); + } +} + +void Connection::internalSend(const OutputMessage_ptr& msg) +{ + if (msg->isBroadcastMsg()) { + dispatchBroadcastMessage(msg); + } + protocol->onSendMessage(msg); + + try { + writeTimer.expires_from_now(boost::posix_time::seconds(CONNECTION_WRITE_TIMEOUT)); + writeTimer.async_wait(std::bind(&Connection::handleTimeout, std::weak_ptr(shared_from_this()), + std::placeholders::_1)); + + boost::asio::async_write(socket, + boost::asio::buffer(msg->getOutputBuffer(), msg->getLength()), + std::bind(&Connection::onWriteOperation, shared_from_this(), std::placeholders::_1)); + } catch (boost::system::system_error& e) { + std::cout << "[Network error - Connection::internalSend] " << e.what() << std::endl; + close(FORCE_CLOSE); + } +} + +uint32_t Connection::getIP() +{ + std::lock_guard lockClass(connectionLock); + + // IP-address is expressed in network byte order + boost::system::error_code error; + const boost::asio::ip::tcp::endpoint endpoint = socket.remote_endpoint(error); + if (error) { + return 0; + } + + return htonl(endpoint.address().to_v4().to_ulong()); +} + +void Connection::dispatchBroadcastMessage(const OutputMessage_ptr& msg) +{ + auto msgCopy = OutputMessagePool::getOutputMessage(); + msgCopy->append(msg); + g_dispatcher.addTask(createTask(std::bind(&Connection::broadcastMessage, shared_from_this(), msgCopy))); +} + +void Connection::broadcastMessage(OutputMessage_ptr msg) +{ + std::lock_guard lockClass(connectionLock); + const auto client = std::dynamic_pointer_cast(protocol); + if (client) { + std::lock_guardliveCastLock)> lockGuard(client->liveCastLock); + + const auto& spectators = client->getLiveCastSpectators(); + for (const ProtocolSpectator_ptr& spectator : spectators) { + auto newMsg = OutputMessagePool::getOutputMessage(); + newMsg->append(msg); + spectator->send(std::move(newMsg)); + } + } +} + +void Connection::onWriteOperation(const boost::system::error_code& error) +{ + std::lock_guard lockClass(connectionLock); + writeTimer.cancel(); + messageQueue.pop_front(); + + if (error) { + messageQueue.clear(); + close(FORCE_CLOSE); + return; + } + + if (!messageQueue.empty()) { + internalSend(messageQueue.front()); + } else if (connectionState == CONNECTION_STATE_CLOSED) { + closeSocket(); + } +} + +void Connection::handleTimeout(ConnectionWeak_ptr connectionWeak, const boost::system::error_code& error) +{ + if (error == boost::asio::error::operation_aborted) { + //The timer has been manually cancelled + return; + } + + if (auto connection = connectionWeak.lock()) { + connection->close(FORCE_CLOSE); + } +} diff --git a/src/connection.h b/src/connection.h new file mode 100644 index 0000000..52b2a33 --- /dev/null +++ b/src/connection.h @@ -0,0 +1,136 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_CONNECTION_H_FC8E1B4392D24D27A2F129D8B93A6348 +#define FS_CONNECTION_H_FC8E1B4392D24D27A2F129D8B93A6348 + +#include + +#include "networkmessage.h" + +static constexpr int32_t CONNECTION_WRITE_TIMEOUT = 30; +static constexpr int32_t CONNECTION_READ_TIMEOUT = 30; + +class Protocol; +using Protocol_ptr = std::shared_ptr; +class OutputMessage; +using OutputMessage_ptr = std::shared_ptr; +class Connection; +using Connection_ptr = std::shared_ptr ; +using ConnectionWeak_ptr = std::weak_ptr; +class ServiceBase; +using Service_ptr = std::shared_ptr; +class ServicePort; +using ServicePort_ptr = std::shared_ptr; +using ConstServicePort_ptr = std::shared_ptr; + +class ConnectionManager +{ + public: + static ConnectionManager& getInstance() { + static ConnectionManager instance; + return instance; + } + + Connection_ptr createConnection(boost::asio::io_service& io_service, ConstServicePort_ptr servicePort); + void releaseConnection(const Connection_ptr& connection); + void closeAll(); + + private: + ConnectionManager() = default; + + std::unordered_set connections; + std::mutex connectionManagerLock; +}; + +class Connection : public std::enable_shared_from_this +{ + public: + // non-copyable + Connection(const Connection&) = delete; + Connection& operator=(const Connection&) = delete; + + enum ConnectionState_t { + CONNECTION_STATE_OPEN, + CONNECTION_STATE_CLOSED, + }; + + enum { FORCE_CLOSE = true }; + + Connection(boost::asio::io_service& io_service, + ConstServicePort_ptr service_port) : + readTimer(io_service), + writeTimer(io_service), + service_port(std::move(service_port)), + socket(io_service), + timeConnected(time(nullptr)) {} + ~Connection(); + + friend class ConnectionManager; + + void close(bool force = false); + // Used by protocols that require server to send first + void accept(Protocol_ptr protocol); + void accept(); + + void send(const OutputMessage_ptr& msg); + + uint32_t getIP(); + + private: + void parseHeader(const boost::system::error_code& error); + void parsePacket(const boost::system::error_code& error); + + void onWriteOperation(const boost::system::error_code& error); + + static void handleTimeout(ConnectionWeak_ptr connectionWeak, const boost::system::error_code& error); + + void closeSocket(); + void internalSend(const OutputMessage_ptr& msg); + + boost::asio::ip::tcp::socket& getSocket() { + return socket; + } + friend class ServicePort; + + NetworkMessage msg; + + void broadcastMessage(OutputMessage_ptr msg); + void dispatchBroadcastMessage(const OutputMessage_ptr& msg); + + boost::asio::deadline_timer readTimer; + boost::asio::deadline_timer writeTimer; + + std::recursive_mutex connectionLock; + + std::list messageQueue; + + ConstServicePort_ptr service_port; + Protocol_ptr protocol; + + boost::asio::ip::tcp::socket socket; + + time_t timeConnected; + uint32_t packetsSent = 0; + + bool connectionState = CONNECTION_STATE_OPEN; + bool receivedFirst = false; +}; + +#endif diff --git a/src/const.h b/src/const.h new file mode 100644 index 0000000..a17fbb4 --- /dev/null +++ b/src/const.h @@ -0,0 +1,597 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_CONST_H_0A49B5996F074465BF44B90F4F780E8B +#define FS_CONST_H_0A49B5996F074465BF44B90F4F780E8B + +static constexpr int32_t NETWORKMESSAGE_MAXSIZE = 24590; + +enum MagicEffectClasses : uint8_t { + CONST_ME_NONE, + + CONST_ME_DRAWBLOOD = 1, + CONST_ME_LOSEENERGY = 2, + CONST_ME_POFF = 3, + CONST_ME_BLOCKHIT = 4, + CONST_ME_EXPLOSIONAREA = 5, + CONST_ME_EXPLOSIONHIT = 6, + CONST_ME_FIREAREA = 7, + CONST_ME_YELLOW_RINGS = 8, + CONST_ME_GREEN_RINGS = 9, + CONST_ME_HITAREA = 10, + CONST_ME_TELEPORT = 11, + CONST_ME_ENERGYHIT = 12, + CONST_ME_MAGIC_BLUE = 13, + CONST_ME_MAGIC_RED = 14, + CONST_ME_MAGIC_GREEN = 15, + CONST_ME_HITBYFIRE = 16, + CONST_ME_HITBYPOISON = 17, + CONST_ME_MORTAREA = 18, + CONST_ME_SOUND_GREEN = 19, + CONST_ME_SOUND_RED = 20, + CONST_ME_POISONAREA = 21, + CONST_ME_SOUND_YELLOW = 22, + CONST_ME_SOUND_PURPLE = 23, + CONST_ME_SOUND_BLUE = 24, + CONST_ME_SOUND_WHITE = 25, + CONST_ME_BUBBLES = 26, + CONST_ME_CRAPS = 27, + CONST_ME_GIFT_WRAPS = 28, + CONST_ME_FIREWORK_YELLOW = 29, + CONST_ME_FIREWORK_RED = 30, + CONST_ME_FIREWORK_BLUE = 31, + CONST_ME_STUN = 32, + CONST_ME_SLEEP = 33, + CONST_ME_WATERCREATURE = 34, + CONST_ME_GROUNDSHAKER = 35, + CONST_ME_HEARTS = 36, + CONST_ME_FIREATTACK = 37, + CONST_ME_ENERGYAREA = 38, + CONST_ME_SMALLCLOUDS = 39, + CONST_ME_HOLYDAMAGE = 40, + CONST_ME_BIGCLOUDS = 41, + CONST_ME_ICEAREA = 42, + CONST_ME_ICETORNADO = 43, + CONST_ME_ICEATTACK = 44, + CONST_ME_STONES = 45, + CONST_ME_SMALLPLANTS = 46, + CONST_ME_CARNIPHILA = 47, + CONST_ME_PURPLEENERGY = 48, + CONST_ME_YELLOWENERGY = 49, + CONST_ME_HOLYAREA = 50, + CONST_ME_BIGPLANTS = 51, + CONST_ME_CAKE = 52, + CONST_ME_GIANTICE = 53, + CONST_ME_WATERSPLASH = 54, + CONST_ME_PLANTATTACK = 55, + CONST_ME_TUTORIALARROW = 56, + CONST_ME_TUTORIALSQUARE = 57, + CONST_ME_MIRRORHORIZONTAL = 58, + CONST_ME_MIRRORVERTICAL = 59, + CONST_ME_SKULLHORIZONTAL = 60, + CONST_ME_SKULLVERTICAL = 61, + CONST_ME_ASSASSIN = 62, + CONST_ME_STEPSHORIZONTAL = 63, + CONST_ME_BLOODYSTEPS = 64, + CONST_ME_STEPSVERTICAL = 65, + CONST_ME_YALAHARIGHOST = 66, + CONST_ME_BATS = 67, + CONST_ME_SMOKE = 68, + CONST_ME_INSECTS = 69, + CONST_ME_DRAGONHEAD = 70, + CONST_ME_ORCSHAMAN = 71, + CONST_ME_ORCSHAMAN_FIRE = 72, + CONST_ME_THUNDER = 73, + CONST_ME_FERUMBRAS = 74, + CONST_ME_CONFETTI_HORIZONTAL = 75, + CONST_ME_CONFETTI_VERTICAL = 76, + // 77-157 are empty + CONST_ME_BLACKSMOKE = 158, + // 159-166 are empty + CONST_ME_REDSMOKE = 167, + CONST_ME_YELLOWSMOKE = 168, + CONST_ME_GREENSMOKE = 169, + CONST_ME_PURPLESMOKE = 170, + CONST_ME_EARLY_THUNDER = 171, + CONST_ME_RAGIAZ_BONECAPSULE = 172, + CONST_ME_CRITICAL_DAMAGE = 173, + // 174 is empty + CONST_ME_PLUNGING_FISH = 175, +}; + +enum ShootType_t : uint8_t { + CONST_ANI_NONE, + + CONST_ANI_SPEAR = 1, + CONST_ANI_BOLT = 2, + CONST_ANI_ARROW = 3, + CONST_ANI_FIRE = 4, + CONST_ANI_ENERGY = 5, + CONST_ANI_POISONARROW = 6, + CONST_ANI_BURSTARROW = 7, + CONST_ANI_THROWINGSTAR = 8, + CONST_ANI_THROWINGKNIFE = 9, + CONST_ANI_SMALLSTONE = 10, + CONST_ANI_DEATH = 11, + CONST_ANI_LARGEROCK = 12, + CONST_ANI_SNOWBALL = 13, + CONST_ANI_POWERBOLT = 14, + CONST_ANI_POISON = 15, + CONST_ANI_INFERNALBOLT = 16, + CONST_ANI_HUNTINGSPEAR = 17, + CONST_ANI_ENCHANTEDSPEAR = 18, + CONST_ANI_REDSTAR = 19, + CONST_ANI_GREENSTAR = 20, + CONST_ANI_ROYALSPEAR = 21, + CONST_ANI_SNIPERARROW = 22, + CONST_ANI_ONYXARROW = 23, + CONST_ANI_PIERCINGBOLT = 24, + CONST_ANI_WHIRLWINDSWORD = 25, + CONST_ANI_WHIRLWINDAXE = 26, + CONST_ANI_WHIRLWINDCLUB = 27, + CONST_ANI_ETHEREALSPEAR = 28, + CONST_ANI_ICE = 29, + CONST_ANI_EARTH = 30, + CONST_ANI_HOLY = 31, + CONST_ANI_SUDDENDEATH = 32, + CONST_ANI_FLASHARROW = 33, + CONST_ANI_FLAMMINGARROW = 34, + CONST_ANI_SHIVERARROW = 35, + CONST_ANI_ENERGYBALL = 36, + CONST_ANI_SMALLICE = 37, + CONST_ANI_SMALLHOLY = 38, + CONST_ANI_SMALLEARTH = 39, + CONST_ANI_EARTHARROW = 40, + CONST_ANI_EXPLOSION = 41, + CONST_ANI_CAKE = 42, + + CONST_ANI_TARSALARROW = 44, + CONST_ANI_VORTEXBOLT = 45, + + CONST_ANI_PRISMATICBOLT = 48, + CONST_ANI_CRYSTALLINEARROW = 49, + CONST_ANI_DRILLBOLT = 50, + CONST_ANI_ENVENOMEDARROW = 51, + + CONST_ANI_GLOOTHSPEAR = 53, + CONST_ANI_SIMPLEARROW = 54, + + // for internal use, don't send to client + CONST_ANI_WEAPONTYPE = 0xFE, // 254 +}; + +enum SpeakClasses : uint8_t { + TALKTYPE_SAY = 1, + TALKTYPE_WHISPER = 2, + TALKTYPE_YELL = 3, + TALKTYPE_PRIVATE_FROM = 4, + TALKTYPE_PRIVATE_TO = 5, + TALKTYPE_CHANNEL_Y = 7, + TALKTYPE_CHANNEL_O = 8, + TALKTYPE_PRIVATE_NP = 10, + TALKTYPE_PRIVATE_PN = 12, + TALKTYPE_BROADCAST = 13, + TALKTYPE_CHANNEL_R1 = 14, //red - #c text + TALKTYPE_PRIVATE_RED_FROM = 15, //@name@text + TALKTYPE_PRIVATE_RED_TO = 16, //@name@text + TALKTYPE_MONSTER_SAY = 36, + TALKTYPE_MONSTER_YELL = 37, + + TALKTYPE_CHANNEL_R2 = 0xFF, //#d +}; + +enum MessageClasses : uint8_t { + MESSAGE_STATUS_CONSOLE_BLUE = 4, /*FIXME Blue message in the console*/ + + MESSAGE_STATUS_CONSOLE_RED = 13, /*Red message in the console*/ + + MESSAGE_STATUS_DEFAULT = 17, /*White message at the bottom of the game window and in the console*/ + MESSAGE_STATUS_WARNING = 18, /*Red message in game window and in the console*/ + MESSAGE_EVENT_ADVANCE = 19, /*White message in game window and in the console*/ + + MESSAGE_STATUS_SMALL = 21, /*White message at the bottom of the game window"*/ + MESSAGE_INFO_DESCR = 22, /*Green message in game window and in the console*/ + MESSAGE_DAMAGE_DEALT = 23, + MESSAGE_DAMAGE_RECEIVED = 24, + MESSAGE_HEALED = 25, + MESSAGE_EXPERIENCE = 26, + MESSAGE_DAMAGE_OTHERS = 27, + MESSAGE_HEALED_OTHERS = 28, + MESSAGE_EXPERIENCE_OTHERS = 29, + MESSAGE_EVENT_DEFAULT = 30, /*White message at the bottom of the game window and in the console*/ + MESSAGE_LOOT = 31, + + MESSAGE_GUILD = 33, /*White message in channel (+ channelId)*/ + MESSAGE_PARTY_MANAGEMENT = 34, /*White message in channel (+ channelId)*/ + MESSAGE_PARTY = 35, /*White message in channel (+ channelId)*/ + MESSAGE_EVENT_ORANGE = 36, /*Orange message in the console*/ + MESSAGE_STATUS_CONSOLE_ORANGE = 37, /*Orange message in the console*/ +}; + +enum FluidColors_t : uint8_t { + FLUID_EMPTY, + FLUID_BLUE, + FLUID_RED, + FLUID_BROWN, + FLUID_GREEN, + FLUID_YELLOW, + FLUID_WHITE, + FLUID_PURPLE, +}; + +enum FluidTypes_t : uint8_t { + FLUID_NONE = FLUID_EMPTY, + FLUID_WATER = FLUID_BLUE, + FLUID_BLOOD = FLUID_RED, + FLUID_BEER = FLUID_BROWN, + FLUID_SLIME = FLUID_GREEN, + FLUID_LEMONADE = FLUID_YELLOW, + FLUID_MILK = FLUID_WHITE, + FLUID_MANA = FLUID_PURPLE, + + FLUID_LIFE = FLUID_RED + 8, + FLUID_OIL = FLUID_BROWN + 8, + FLUID_URINE = FLUID_YELLOW + 8, + FLUID_COCONUTMILK = FLUID_WHITE + 8, + FLUID_WINE = FLUID_PURPLE + 8, + + FLUID_MUD = FLUID_BROWN + 16, + FLUID_FRUITJUICE = FLUID_YELLOW + 16, + + FLUID_LAVA = FLUID_RED + 24, + FLUID_RUM = FLUID_BROWN + 24, + FLUID_SWAMP = FLUID_GREEN + 24, + + FLUID_TEA = FLUID_BROWN + 32, + + FLUID_MEAD = FLUID_BROWN + 40, +}; + +const uint8_t reverseFluidMap[] = { + FLUID_EMPTY, + FLUID_WATER, + FLUID_MANA, + FLUID_BEER, + FLUID_EMPTY, + FLUID_BLOOD, + FLUID_SLIME, + FLUID_EMPTY, + FLUID_LEMONADE, + FLUID_MILK, +}; + +const uint8_t clientToServerFluidMap[] = { + FLUID_EMPTY, + FLUID_WATER, + FLUID_MANA, + FLUID_BEER, + FLUID_MUD, + FLUID_BLOOD, + FLUID_SLIME, + FLUID_RUM, + FLUID_LEMONADE, + FLUID_MILK, + FLUID_WINE, + FLUID_LIFE, + FLUID_URINE, + FLUID_OIL, + FLUID_FRUITJUICE, + FLUID_COCONUTMILK, + FLUID_TEA, + FLUID_MEAD, +}; + +enum ClientFluidTypes_t : uint8_t { + CLIENTFLUID_EMPTY = 0, + CLIENTFLUID_BLUE = 1, + CLIENTFLUID_PURPLE = 2, + CLIENTFLUID_BROWN_1 = 3, + CLIENTFLUID_BROWN_2 = 4, + CLIENTFLUID_RED = 5, + CLIENTFLUID_GREEN = 6, + CLIENTFLUID_BROWN = 7, + CLIENTFLUID_YELLOW = 8, + CLIENTFLUID_WHITE = 9, +}; + +const uint8_t fluidMap[] = { + CLIENTFLUID_EMPTY, + CLIENTFLUID_BLUE, + CLIENTFLUID_RED, + CLIENTFLUID_BROWN_1, + CLIENTFLUID_GREEN, + CLIENTFLUID_YELLOW, + CLIENTFLUID_WHITE, + CLIENTFLUID_PURPLE, +}; + +enum SquareColor_t : uint8_t { + SQ_COLOR_BLACK = 0, +}; + +enum TextColor_t : uint8_t { + TEXTCOLOR_BLUE = 5, + TEXTCOLOR_LIGHTGREEN = 30, + TEXTCOLOR_LIGHTBLUE = 35, + TEXTCOLOR_MAYABLUE = 95, + TEXTCOLOR_DARKRED = 108, + TEXTCOLOR_LIGHTGREY = 129, + TEXTCOLOR_SKYBLUE = 143, + TEXTCOLOR_PURPLE = 154, + TEXTCOLOR_ELECTRICPURPLE = 155, + TEXTCOLOR_RED = 180, + TEXTCOLOR_PASTELRED = 194, + TEXTCOLOR_ORANGE = 198, + TEXTCOLOR_YELLOW = 210, + TEXTCOLOR_WHITE_EXP = 215, + TEXTCOLOR_NONE = 255, +}; + +enum Icons_t { + ICON_POISON = 1 << 0, + ICON_BURN = 1 << 1, + ICON_ENERGY = 1 << 2, + ICON_DRUNK = 1 << 3, + ICON_MANASHIELD = 1 << 4, + ICON_PARALYZE = 1 << 5, + ICON_HASTE = 1 << 6, + ICON_SWORDS = 1 << 7, + ICON_DROWNING = 1 << 8, + ICON_FREEZING = 1 << 9, + ICON_DAZZLED = 1 << 10, + ICON_CURSED = 1 << 11, + ICON_PARTY_BUFF = 1 << 12, + ICON_REDSWORDS = 1 << 13, + ICON_PIGEON = 1 << 14, + ICON_BLEEDING = 1 << 15, +}; + +enum WeaponType_t : uint8_t { + WEAPON_NONE, + WEAPON_SWORD, + WEAPON_CLUB, + WEAPON_AXE, + WEAPON_SHIELD, + WEAPON_DISTANCE, + WEAPON_WAND, + WEAPON_AMMO, +}; + +enum Ammo_t : uint8_t { + AMMO_NONE, + AMMO_BOLT, + AMMO_ARROW, + AMMO_SPEAR, + AMMO_THROWINGSTAR, + AMMO_THROWINGKNIFE, + AMMO_STONE, + AMMO_SNOWBALL, +}; + +enum WeaponAction_t : uint8_t { + WEAPONACTION_NONE, + WEAPONACTION_REMOVECOUNT, + WEAPONACTION_REMOVECHARGE, + WEAPONACTION_MOVE, +}; + +enum WieldInfo_t { + WIELDINFO_NONE = 0 << 0, + WIELDINFO_LEVEL = 1 << 0, + WIELDINFO_MAGLV = 1 << 1, + WIELDINFO_VOCREQ = 1 << 2, + WIELDINFO_PREMIUM = 1 << 3, +}; + +enum Skulls_t : uint8_t { + SKULL_NONE = 0, + SKULL_YELLOW = 1, + SKULL_GREEN = 2, + SKULL_WHITE = 3, + SKULL_RED = 4, + SKULL_BLACK = 5, + SKULL_ORANGE = 6, +}; + +enum PartyShields_t : uint8_t { + SHIELD_NONE = 0, + SHIELD_WHITEYELLOW = 1, + SHIELD_WHITEBLUE = 2, + SHIELD_BLUE = 3, + SHIELD_YELLOW = 4, + SHIELD_BLUE_SHAREDEXP = 5, + SHIELD_YELLOW_SHAREDEXP = 6, + SHIELD_BLUE_NOSHAREDEXP_BLINK = 7, + SHIELD_YELLOW_NOSHAREDEXP_BLINK = 8, + SHIELD_BLUE_NOSHAREDEXP = 9, + SHIELD_YELLOW_NOSHAREDEXP = 10, + SHIELD_GRAY = 11, +}; + +enum GuildEmblems_t : uint8_t { + GUILDEMBLEM_NONE = 0, + GUILDEMBLEM_ALLY = 1, + GUILDEMBLEM_ENEMY = 2, + GUILDEMBLEM_NEUTRAL = 3, + GUILDEMBLEM_MEMBER = 4, + GUILDEMBLEM_OTHER = 5, +}; + +enum item_t : uint16_t { + ITEM_BROWSEFIELD = 460, // for internal use + + ITEM_FIREFIELD_PVP_FULL = 1487, + ITEM_FIREFIELD_PVP_MEDIUM = 1488, + ITEM_FIREFIELD_PVP_SMALL = 1489, + ITEM_FIREFIELD_PERSISTENT_FULL = 1492, + ITEM_FIREFIELD_PERSISTENT_MEDIUM = 1493, + ITEM_FIREFIELD_PERSISTENT_SMALL = 1494, + ITEM_FIREFIELD_NOPVP = 1500, + + ITEM_POISONFIELD_PVP = 1490, + ITEM_POISONFIELD_PERSISTENT = 1496, + ITEM_POISONFIELD_NOPVP = 1503, + + ITEM_ENERGYFIELD_PVP = 1491, + ITEM_ENERGYFIELD_PERSISTENT = 1495, + ITEM_ENERGYFIELD_NOPVP = 1504, + + ITEM_MAGICWALL = 1497, + ITEM_MAGICWALL_PERSISTENT = 1498, + ITEM_MAGICWALL_SAFE = 11098, + ITEM_MAGICWALL_NOPVP = 20669, + + ITEM_WILDGROWTH = 1499, + ITEM_WILDGROWTH_PERSISTENT = 2721, + ITEM_WILDGROWTH_SAFE = 11099, + ITEM_WILDGROWTH_NOPVP = 20670, + + ITEM_BAG = 1987, + ITEM_SHOPPING_BAG = 23782, + + ITEM_GOLD_COIN = 2148, + ITEM_PLATINUM_COIN = 2152, + ITEM_CRYSTAL_COIN = 2160, + ITEM_STORE_COIN = 24774, // in-game store currency + + ITEM_DEPOT = 2594, + ITEM_LOCKER1 = 2589, + ITEM_INBOX = 14404, + ITEM_MARKET = 14405, + ITEM_STORE_INBOX = 26052, + ITEM_DEPOT_BOX_I = 25453, + ITEM_DEPOT_BOX_II = 25454, + ITEM_DEPOT_BOX_III = 25455, + ITEM_DEPOT_BOX_IV = 25456, + ITEM_DEPOT_BOX_V = 25457, + ITEM_DEPOT_BOX_VI = 25458, + ITEM_DEPOT_BOX_VII = 25459, + ITEM_DEPOT_BOX_VIII = 25460, + ITEM_DEPOT_BOX_IX = 25461, + ITEM_DEPOT_BOX_X = 25462, + ITEM_DEPOT_BOX_XI = 25463, + ITEM_DEPOT_BOX_XII = 25464, + ITEM_DEPOT_BOX_XIII = 25465, + ITEM_DEPOT_BOX_XIV = 25466, + ITEM_DEPOT_BOX_XV = 25467, + ITEM_DEPOT_BOX_XVI = 25468, + ITEM_DEPOT_BOX_XVII = 25469, + + ITEM_MALE_CORPSE = 3058, + ITEM_FEMALE_CORPSE = 3065, + + ITEM_FULLSPLASH = 2016, + ITEM_SMALLSPLASH = 2019, + + ITEM_PARCEL = 2595, + ITEM_LETTER = 2597, + ITEM_LETTER_STAMPED = 2598, + ITEM_LABEL = 2599, + + ITEM_AMULETOFLOSS = 2173, + + ITEM_DOCUMENT_RO = 1968, //read-only +}; + +enum PlayerFlags : uint64_t { + PlayerFlag_CannotUseCombat = 1 << 0, + PlayerFlag_CannotAttackPlayer = 1 << 1, + PlayerFlag_CannotAttackMonster = 1 << 2, + PlayerFlag_CannotBeAttacked = 1 << 3, + PlayerFlag_CanConvinceAll = 1 << 4, + PlayerFlag_CanSummonAll = 1 << 5, + PlayerFlag_CanIllusionAll = 1 << 6, + PlayerFlag_CanSenseInvisibility = 1 << 7, + PlayerFlag_IgnoredByMonsters = 1 << 8, + PlayerFlag_NotGainInFight = 1 << 9, + PlayerFlag_HasInfiniteMana = 1 << 10, + PlayerFlag_HasInfiniteSoul = 1 << 11, + PlayerFlag_HasNoExhaustion = 1 << 12, + PlayerFlag_CannotUseSpells = 1 << 13, + PlayerFlag_CannotPickupItem = 1 << 14, + PlayerFlag_CanAlwaysLogin = 1 << 15, + PlayerFlag_CanBroadcast = 1 << 16, + PlayerFlag_CanEditHouses = 1 << 17, + PlayerFlag_CannotBeBanned = 1 << 18, + PlayerFlag_CannotBePushed = 1 << 19, + PlayerFlag_HasInfiniteCapacity = 1 << 20, + PlayerFlag_CanPushAllCreatures = 1 << 21, + PlayerFlag_CanTalkRedPrivate = 1 << 22, + PlayerFlag_CanTalkRedChannel = 1 << 23, + PlayerFlag_TalkOrangeHelpChannel = 1 << 24, + PlayerFlag_NotGainExperience = 1 << 25, + PlayerFlag_NotGainMana = 1 << 26, + PlayerFlag_NotGainHealth = 1 << 27, + PlayerFlag_NotGainSkill = 1 << 28, + PlayerFlag_SetMaxSpeed = 1 << 29, + PlayerFlag_SpecialVIP = 1 << 30, + PlayerFlag_NotGenerateLoot = static_cast(1) << 31, + PlayerFlag_CanTalkRedChannelAnonymous = static_cast(1) << 32, + PlayerFlag_IgnoreProtectionZone = static_cast(1) << 33, + PlayerFlag_IgnoreSpellCheck = static_cast(1) << 34, + PlayerFlag_IgnoreWeaponCheck = static_cast(1) << 35, + PlayerFlag_CannotBeMuted = static_cast(1) << 36, + PlayerFlag_IsAlwaysPremium = static_cast(1) << 37, +}; + +enum ReloadTypes_t : uint8_t { + RELOAD_TYPE_ALL, + RELOAD_TYPE_ACTIONS, + RELOAD_TYPE_CHAT, + RELOAD_TYPE_CONFIG, + RELOAD_TYPE_CREATURESCRIPTS, + RELOAD_TYPE_EVENTS, + RELOAD_TYPE_GLOBAL, + RELOAD_TYPE_GLOBALEVENTS, + RELOAD_TYPE_ITEMS, + RELOAD_TYPE_MONSTERS, + RELOAD_TYPE_MOUNTS, + RELOAD_TYPE_MOVEMENTS, + RELOAD_TYPE_NPCS, + RELOAD_TYPE_QUESTS, + RELOAD_TYPE_RAIDS, + RELOAD_TYPE_SCRIPTS, + RELOAD_TYPE_SPELLS, + RELOAD_TYPE_TALKACTIONS, + RELOAD_TYPE_WEAPONS, +}; + +static constexpr int32_t CHANNEL_GUILD = 0x00; +static constexpr int32_t CHANNEL_PARTY = 0x01; +static constexpr int32_t CHANNEL_CAST = 0xFFFE; +static constexpr int32_t CHANNEL_PRIVATE = 0xFFFF; + +const std::string LIVE_CAST_CHAT_NAME = "Live Cast Chat"; + +//Reserved player storage key ranges; +//[10000000 - 20000000]; +static constexpr int32_t PSTRG_RESERVED_RANGE_START = 10000000; +static constexpr int32_t PSTRG_RESERVED_RANGE_SIZE = 10000000; +//[1000 - 1500]; +static constexpr int32_t PSTRG_OUTFITS_RANGE_START = (PSTRG_RESERVED_RANGE_START + 1000); +static constexpr int32_t PSTRG_OUTFITS_RANGE_SIZE = 500; +//[2001 - 2011]; +static constexpr int32_t PSTRG_MOUNTS_RANGE_START = (PSTRG_RESERVED_RANGE_START + 2001); +static constexpr int32_t PSTRG_MOUNTS_RANGE_SIZE = 10; +static constexpr int32_t PSTRG_MOUNTS_CURRENTMOUNT = (PSTRG_MOUNTS_RANGE_START + 10); + + +#define IS_IN_KEYRANGE(key, range) (key >= PSTRG_##range##_START && ((key - PSTRG_##range##_START) <= PSTRG_##range##_SIZE)) + +#endif diff --git a/src/container.cpp b/src/container.cpp new file mode 100644 index 0000000..74c6619 --- /dev/null +++ b/src/container.cpp @@ -0,0 +1,729 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "container.h" +#include "iomap.h" +#include "game.h" + +extern Game g_game; + +Container::Container(uint16_t type) : + Container(type, items[type].maxItems) {} + +Container::Container(uint16_t type, uint16_t size, bool unlocked /*= true*/, bool pagination /*= false*/) : + Item(type), + maxSize(size), + unlocked(unlocked), + pagination(pagination) +{} + +Container::Container(Tile* tile) : Container(ITEM_BROWSEFIELD, 30, false, true) +{ + TileItemVector* itemVector = tile->getItemList(); + if (itemVector) { + for (Item* item : *itemVector) { + if ((item->getContainer() || item->hasProperty(CONST_PROP_MOVEABLE)) && !item->hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)) { + itemlist.push_front(item); + item->setParent(this); + } + } + } + + setParent(tile); +} + +Container::~Container() +{ + if (getID() == ITEM_BROWSEFIELD) { + g_game.browseFields.erase(getTile()); + + for (Item* item : itemlist) { + item->setParent(parent); + } + } else { + for (Item* item : itemlist) { + item->setParent(nullptr); + item->decrementReferenceCounter(); + } + } +} + +Item* Container::clone() const +{ + Container* clone = static_cast(Item::clone()); + for (Item* item : itemlist) { + clone->addItem(item->clone()); + } + clone->totalWeight = totalWeight; + return clone; +} + +Container* Container::getParentContainer() +{ + Thing* thing = getParent(); + if (!thing) { + return nullptr; + } + return thing->getContainer(); +} + +std::string Container::getName(bool addArticle /* = false*/) const { + const ItemType& it = items[id]; + return getNameDescription(it, this, -1, addArticle); +} + +bool Container::hasParent() const +{ + return getID() != ITEM_BROWSEFIELD && dynamic_cast(getParent()) == nullptr; +} + +void Container::addItem(Item* item) +{ + itemlist.push_back(item); + item->setParent(this); +} + +Attr_ReadValue Container::readAttr(AttrTypes_t attr, PropStream& propStream) +{ + if (attr == ATTR_CONTAINER_ITEMS) { + if (!propStream.read(serializationCount)) { + return ATTR_READ_ERROR; + } + return ATTR_READ_END; + } + return Item::readAttr(attr, propStream); +} + +bool Container::unserializeItemNode(OTB::Loader& loader, const OTB::Node& node, PropStream& propStream) +{ + bool ret = Item::unserializeItemNode(loader, node, propStream); + if (!ret) { + return false; + } + + for (auto& itemNode : node.children) { + //load container items + if (itemNode.type != OTBM_ITEM) { + // unknown type + return false; + } + + PropStream itemPropStream; + if (!loader.getProps(itemNode, itemPropStream)) { + return false; + } + + Item* item = Item::CreateItem(itemPropStream); + if (!item) { + return false; + } + + if (!item->unserializeItemNode(loader, itemNode, itemPropStream)) { + return false; + } + + addItem(item); + updateItemWeight(item->getWeight()); + } + return true; +} + +void Container::updateItemWeight(int32_t diff) +{ + totalWeight += diff; + if (Container* parentContainer = getParentContainer()) { + parentContainer->updateItemWeight(diff); + } +} + +uint32_t Container::getWeight() const +{ + return Item::getWeight() + totalWeight; +} + +std::string Container::getContentDescription() const +{ + std::ostringstream os; + return getContentDescription(os).str(); +} + +std::ostringstream& Container::getContentDescription(std::ostringstream& os) const +{ + bool firstitem = true; + for (ContainerIterator it = iterator(); it.hasNext(); it.advance()) { + Item* item = *it; + + Container* container = item->getContainer(); + if (container && !container->empty()) { + continue; + } + + if (firstitem) { + firstitem = false; + } else { + os << ", "; + } + + os << item->getNameDescription(); + } + + if (firstitem) { + os << "nothing"; + } + return os; +} + +Item* Container::getItemByIndex(size_t index) const +{ + if (index >= size()) { + return nullptr; + } + return itemlist[index]; +} + +uint32_t Container::getItemHoldingCount() const +{ + uint32_t counter = 0; + for (ContainerIterator it = iterator(); it.hasNext(); it.advance()) { + ++counter; + } + return counter; +} + +bool Container::isHoldingItem(const Item* item) const +{ + for (ContainerIterator it = iterator(); it.hasNext(); it.advance()) { + if (*it == item) { + return true; + } + } + return false; +} + +void Container::onAddContainerItem(Item* item) +{ + SpectatorVec spectators; + g_game.map.getSpectators(spectators, getPosition(), false, true, 2, 2, 2, 2); + + //send to client + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendAddContainerItem(this, item); + } + + //event methods + for (Creature* spectator : spectators) { + spectator->getPlayer()->onAddContainerItem(item); + } +} + +void Container::onUpdateContainerItem(uint32_t index, Item* oldItem, Item* newItem) +{ + SpectatorVec spectators; + g_game.map.getSpectators(spectators, getPosition(), false, true, 2, 2, 2, 2); + + //send to client + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendUpdateContainerItem(this, index, newItem); + } + + //event methods + for (Creature* spectator : spectators) { + spectator->getPlayer()->onUpdateContainerItem(this, oldItem, newItem); + } +} + +void Container::onRemoveContainerItem(uint32_t index, Item* item) +{ + SpectatorVec spectators; + g_game.map.getSpectators(spectators, getPosition(), false, true, 2, 2, 2, 2); + + //send change to client + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendRemoveContainerItem(this, index); + } + + //event methods + for (Creature* spectator : spectators) { + spectator->getPlayer()->onRemoveContainerItem(this, item); + } +} + +ReturnValue Container::queryAdd(int32_t index, const Thing& thing, uint32_t count, + uint32_t flags, Creature* actor/* = nullptr*/) const +{ + bool childIsOwner = hasBitSet(FLAG_CHILDISOWNER, flags); + if (childIsOwner) { + //a child container is querying, since we are the top container (not carried by a player) + //just return with no error. + return RETURNVALUE_NOERROR; + } + + if (!unlocked) { + return RETURNVALUE_NOTPOSSIBLE; + } + + const Item* item = thing.getItem(); + if (item == nullptr) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (!item->isPickupable()) { + return RETURNVALUE_CANNOTPICKUP; + } + + if (item == this) { + return RETURNVALUE_THISISIMPOSSIBLE; + } + + const Cylinder* cylinder = getParent(); + if (!hasBitSet(FLAG_NOLIMIT, flags)) { + while (cylinder) { + if (cylinder == &thing) { + return RETURNVALUE_THISISIMPOSSIBLE; + } + + if (dynamic_cast(cylinder)) { + return RETURNVALUE_CONTAINERNOTENOUGHROOM; + } + + cylinder = cylinder->getParent(); + } + + if (index == INDEX_WHEREEVER && size() >= capacity()) { + return RETURNVALUE_CONTAINERNOTENOUGHROOM; + } + } else { + while (cylinder) { + if (cylinder == &thing) { + return RETURNVALUE_THISISIMPOSSIBLE; + } + + cylinder = cylinder->getParent(); + } + } + + const Cylinder* topParent = getTopParent(); + if (topParent != this) { + return topParent->queryAdd(INDEX_WHEREEVER, *item, count, flags | FLAG_CHILDISOWNER, actor); + } else { + return RETURNVALUE_NOERROR; + } +} + +ReturnValue Container::queryMaxCount(int32_t index, const Thing& thing, uint32_t count, + uint32_t& maxQueryCount, uint32_t flags) const +{ + const Item* item = thing.getItem(); + if (item == nullptr) { + maxQueryCount = 0; + return RETURNVALUE_NOTPOSSIBLE; + } + + if (hasBitSet(FLAG_NOLIMIT, flags)) { + maxQueryCount = std::max(1, count); + return RETURNVALUE_NOERROR; + } + + int32_t freeSlots = std::max(capacity() - size(), 0); + + if (item->isStackable()) { + uint32_t n = 0; + + if (index == INDEX_WHEREEVER) { + //Iterate through every item and check how much free stackable slots there is. + uint32_t slotIndex = 0; + for (Item* containerItem : itemlist) { + if (containerItem != item && containerItem->equals(item) && containerItem->getItemCount() < 100) { + if (queryAdd(slotIndex++, *item, count, flags) == RETURNVALUE_NOERROR) { + n += 100 - containerItem->getItemCount(); + } + } + } + } else { + const Item* destItem = getItemByIndex(index); + if (item->equals(destItem) && destItem->getItemCount() < 100) { + if (queryAdd(index, *item, count, flags) == RETURNVALUE_NOERROR) { + n = 100 - destItem->getItemCount(); + } + } + } + + maxQueryCount = freeSlots * 100 + n; + if (maxQueryCount < count) { + return RETURNVALUE_CONTAINERNOTENOUGHROOM; + } + } else { + maxQueryCount = freeSlots; + if (maxQueryCount == 0) { + return RETURNVALUE_CONTAINERNOTENOUGHROOM; + } + } + return RETURNVALUE_NOERROR; +} + +ReturnValue Container::queryRemove(const Thing& thing, uint32_t count, uint32_t flags) const +{ + int32_t index = getThingIndex(&thing); + if (index == -1) { + return RETURNVALUE_NOTPOSSIBLE; + } + + const Item* item = thing.getItem(); + if (item == nullptr) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (count == 0 || (item->isStackable() && count > item->getItemCount())) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (!item->isMoveable() && !hasBitSet(FLAG_IGNORENOTMOVEABLE, flags)) { + return RETURNVALUE_NOTMOVEABLE; + } + return RETURNVALUE_NOERROR; +} + +Cylinder* Container::queryDestination(int32_t& index, const Thing& thing, Item** destItem, + uint32_t& flags) +{ + if (!unlocked) { + *destItem = nullptr; + return this; + } + + if (index == 254 /*move up*/) { + index = INDEX_WHEREEVER; + *destItem = nullptr; + + Container* parentContainer = dynamic_cast(getParent()); + if (parentContainer) { + return parentContainer; + } + return this; + } + + if (index == 255 /*add wherever*/) { + index = INDEX_WHEREEVER; + *destItem = nullptr; + } else if (index >= static_cast(capacity())) { + /* + if you have a container, maximize it to show all 20 slots + then you open a bag that is inside the container you will have a bag with 8 slots + and a "grey" area where the other 12 slots where from the container + if you drop the item on that grey area + the client calculates the slot position as if the bag has 20 slots + */ + index = INDEX_WHEREEVER; + *destItem = nullptr; + } + + const Item* item = thing.getItem(); + if (!item) { + return this; + } + + if (index != INDEX_WHEREEVER) { + Item* itemFromIndex = getItemByIndex(index); + if (itemFromIndex) { + *destItem = itemFromIndex; + } + + Cylinder* subCylinder = dynamic_cast(*destItem); + if (subCylinder) { + index = INDEX_WHEREEVER; + *destItem = nullptr; + return subCylinder; + } + } + + bool autoStack = !hasBitSet(FLAG_IGNOREAUTOSTACK, flags); + if (autoStack && item->isStackable() && item->getParent() != this) { + if (*destItem && (*destItem)->equals(item) && (*destItem)->getItemCount() < 100) { + return this; + } + + //try find a suitable item to stack with + uint32_t n = 0; + for (Item* listItem : itemlist) { + if (listItem != item && listItem->equals(item) && listItem->getItemCount() < 100) { + *destItem = listItem; + index = n; + return this; + } + ++n; + } + } + return this; +} + +void Container::addThing(Thing* thing) +{ + return addThing(0, thing); +} + +void Container::addThing(int32_t index, Thing* thing) +{ + if (index >= static_cast(capacity())) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + Item* item = thing->getItem(); + if (item == nullptr) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + item->setParent(this); + itemlist.push_front(item); + updateItemWeight(item->getWeight()); + + //send change to client + if (getParent() && (getParent() != VirtualCylinder::virtualCylinder)) { + onAddContainerItem(item); + } +} + +void Container::addItemBack(Item* item) +{ + addItem(item); + updateItemWeight(item->getWeight()); + + //send change to client + if (getParent() && (getParent() != VirtualCylinder::virtualCylinder)) { + onAddContainerItem(item); + } +} + +void Container::updateThing(Thing* thing, uint16_t itemId, uint32_t count) +{ + int32_t index = getThingIndex(thing); + if (index == -1) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + Item* item = thing->getItem(); + if (item == nullptr) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + const int32_t oldWeight = item->getWeight(); + item->setID(itemId); + item->setSubType(count); + updateItemWeight(-oldWeight + item->getWeight()); + + //send change to client + if (getParent()) { + onUpdateContainerItem(index, item, item); + } +} + +void Container::replaceThing(uint32_t index, Thing* thing) +{ + Item* item = thing->getItem(); + if (!item) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + Item* replacedItem = getItemByIndex(index); + if (!replacedItem) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + itemlist[index] = item; + item->setParent(this); + updateItemWeight(-static_cast(replacedItem->getWeight()) + item->getWeight()); + + //send change to client + if (getParent()) { + onUpdateContainerItem(index, replacedItem, item); + } + + replacedItem->setParent(nullptr); +} + +void Container::removeThing(Thing* thing, uint32_t count) +{ + Item* item = thing->getItem(); + if (item == nullptr) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + int32_t index = getThingIndex(thing); + if (index == -1) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + if (item->isStackable() && count != item->getItemCount()) { + uint8_t newCount = static_cast(std::max(0, item->getItemCount() - count)); + const int32_t oldWeight = item->getWeight(); + item->setItemCount(newCount); + updateItemWeight(-oldWeight + item->getWeight()); + + //send change to client + if (getParent()) { + onUpdateContainerItem(index, item, item); + } + } else { + updateItemWeight(-static_cast(item->getWeight())); + + //send change to client + if (getParent()) { + onRemoveContainerItem(index, item); + } + + item->setParent(nullptr); + itemlist.erase(itemlist.begin() + index); + } +} + +int32_t Container::getThingIndex(const Thing* thing) const +{ + int32_t index = 0; + for (Item* item : itemlist) { + if (item == thing) { + return index; + } + ++index; + } + return -1; +} + +size_t Container::getFirstIndex() const +{ + return 0; +} + +size_t Container::getLastIndex() const +{ + return size(); +} + +uint32_t Container::getItemTypeCount(uint16_t itemId, int32_t subType/* = -1*/) const +{ + uint32_t count = 0; + for (Item* item : itemlist) { + if (item->getID() == itemId) { + count += countByType(item, subType); + } + } + return count; +} + +std::map& Container::getAllItemTypeCount(std::map& countMap) const +{ + for (Item* item : itemlist) { + countMap[item->getID()] += item->getItemCount(); + } + return countMap; +} + +Thing* Container::getThing(size_t index) const +{ + return getItemByIndex(index); +} + +void Container::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t) +{ + Cylinder* topParent = getTopParent(); + if (topParent->getCreature()) { + topParent->postAddNotification(thing, oldParent, index, LINK_TOPPARENT); + } else if (topParent == this) { + //let the tile class notify surrounding players + if (topParent->getParent()) { + topParent->getParent()->postAddNotification(thing, oldParent, index, LINK_NEAR); + } + } else { + topParent->postAddNotification(thing, oldParent, index, LINK_PARENT); + } +} + +void Container::postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t) +{ + Cylinder* topParent = getTopParent(); + if (topParent->getCreature()) { + topParent->postRemoveNotification(thing, newParent, index, LINK_TOPPARENT); + } else if (topParent == this) { + //let the tile class notify surrounding players + if (topParent->getParent()) { + topParent->getParent()->postRemoveNotification(thing, newParent, index, LINK_NEAR); + } + } else { + topParent->postRemoveNotification(thing, newParent, index, LINK_PARENT); + } +} + +void Container::internalAddThing(Thing* thing) +{ + internalAddThing(0, thing); +} + +void Container::internalAddThing(uint32_t, Thing* thing) +{ + Item* item = thing->getItem(); + if (item == nullptr) { + return; + } + + item->setParent(this); + itemlist.push_front(item); + updateItemWeight(item->getWeight()); +} + +void Container::startDecaying() +{ + for (Item* item : itemlist) { + item->startDecaying(); + } +} + +ContainerIterator Container::iterator() const +{ + ContainerIterator cit; + if (!itemlist.empty()) { + cit.over.push_back(this); + cit.cur = itemlist.begin(); + } + return cit; +} + +Item* ContainerIterator::operator*() +{ + return *cur; +} + +void ContainerIterator::advance() +{ + if (Item* i = *cur) { + if (Container* c = i->getContainer()) { + if (!c->empty()) { + over.push_back(c); + } + } + } + + ++cur; + + if (cur == over.front()->itemlist.end()) { + over.pop_front(); + if (!over.empty()) { + cur = over.front()->itemlist.begin(); + } + } +} diff --git a/src/container.h b/src/container.h new file mode 100644 index 0000000..386f782 --- /dev/null +++ b/src/container.h @@ -0,0 +1,177 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_CONTAINER_H_5590165FD8A2451B98D71F13CD3ED8DC +#define FS_CONTAINER_H_5590165FD8A2451B98D71F13CD3ED8DC + +#include + +#include "cylinder.h" +#include "item.h" + +class Container; +class DepotChest; +class DepotLocker; + +class ContainerIterator +{ + public: + bool hasNext() const { + return !over.empty(); + } + + void advance(); + Item* operator*(); + + private: + std::list over; + ItemDeque::const_iterator cur; + + friend class Container; +}; + +class Container : public Item, public Cylinder +{ + public: + explicit Container(uint16_t type); + Container(uint16_t type, uint16_t size, bool unlocked = true, bool pagination = false); + explicit Container(Tile* tile); + ~Container(); + + // non-copyable + Container(const Container&) = delete; + Container& operator=(const Container&) = delete; + + Item* clone() const override final; + + Container* getContainer() override final { + return this; + } + const Container* getContainer() const override final { + return this; + } + + virtual DepotLocker* getDepotLocker() { + return nullptr; + } + virtual const DepotLocker* getDepotLocker() const { + return nullptr; + } + + Attr_ReadValue readAttr(AttrTypes_t attr, PropStream& propStream) override; + bool unserializeItemNode(OTB::Loader& loader, const OTB::Node& node, PropStream& propStream) override; + std::string getContentDescription() const; + + size_t size() const { + return itemlist.size(); + } + bool empty() const { + return itemlist.empty(); + } + uint32_t capacity() const { + return maxSize; + } + + ContainerIterator iterator() const; + + const ItemDeque& getItemList() const { + return itemlist; + } + + ItemDeque::const_reverse_iterator getReversedItems() const { + return itemlist.rbegin(); + } + ItemDeque::const_reverse_iterator getReversedEnd() const { + return itemlist.rend(); + } + + std::string getName(bool addArticle = false) const; + + bool hasParent() const; + void addItem(Item* item); + Item* getItemByIndex(size_t index) const; + bool isHoldingItem(const Item* item) const; + + uint32_t getItemHoldingCount() const; + uint32_t getWeight() const override final; + + bool isUnlocked() const { + return unlocked; + } + bool hasPagination() const { + return pagination; + } + + //cylinder implementations + virtual ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count, + uint32_t flags, Creature* actor = nullptr) const override; + ReturnValue queryMaxCount(int32_t index, const Thing& thing, uint32_t count, uint32_t& maxQueryCount, + uint32_t flags) const override final; + ReturnValue queryRemove(const Thing& thing, uint32_t count, uint32_t flags) const override final; + Cylinder* queryDestination(int32_t& index, const Thing& thing, Item** destItem, + uint32_t& flags) override final; + + void addThing(Thing* thing) override final; + void addThing(int32_t index, Thing* thing) override final; + void addItemBack(Item* item); + + void updateThing(Thing* thing, uint16_t itemId, uint32_t count) override final; + void replaceThing(uint32_t index, Thing* thing) override final; + + void removeThing(Thing* thing, uint32_t count) override final; + + int32_t getThingIndex(const Thing* thing) const override final; + size_t getFirstIndex() const override final; + size_t getLastIndex() const override final; + uint32_t getItemTypeCount(uint16_t itemId, int32_t subType = -1) const override final; + std::map& getAllItemTypeCount(std::map& countMap) const override final; + Thing* getThing(size_t index) const override final; + + void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + + void internalAddThing(Thing* thing) override final; + void internalAddThing(uint32_t index, Thing* thing) override final; + void startDecaying() override final; + + protected: + ItemDeque itemlist; + + private: + std::ostringstream& getContentDescription(std::ostringstream& os) const; + + uint32_t maxSize; + uint32_t totalWeight = 0; + uint32_t serializationCount = 0; + + bool unlocked; + bool pagination; + + void onAddContainerItem(Item* item); + void onUpdateContainerItem(uint32_t index, Item* oldItem, Item* newItem); + void onRemoveContainerItem(uint32_t index, Item* item); + + Container* getParentContainer(); + void updateItemWeight(int32_t diff); + + friend class ContainerIterator; + friend class IOMapSerialize; +}; + +#endif diff --git a/src/creature.cpp b/src/creature.cpp new file mode 100644 index 0000000..f378d1d --- /dev/null +++ b/src/creature.cpp @@ -0,0 +1,1593 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "creature.h" +#include "game.h" +#include "monster.h" +#include "configmanager.h" +#include "scheduler.h" + +double Creature::speedA = 857.36; +double Creature::speedB = 261.29; +double Creature::speedC = -4795.01; + +extern Game g_game; +extern ConfigManager g_config; +extern CreatureEvents* g_creatureEvents; + +Creature::Creature() +{ + onIdleStatus(); +} + +Creature::~Creature() +{ + for (Creature* summon : summons) { + summon->setAttackedCreature(nullptr); + summon->removeMaster(); + } + + for (Condition* condition : conditions) { + condition->endCondition(this); + delete condition; + } +} + +bool Creature::canSee(const Position& myPos, const Position& pos, int32_t viewRangeX, int32_t viewRangeY) +{ + if (myPos.z <= 7) { + //we are on ground level or above (7 -> 0) + //view is from 7 -> 0 + if (pos.z > 7) { + return false; + } + } else if (myPos.z >= 8) { + //we are underground (8 -> 15) + //view is +/- 2 from the floor we stand on + if (Position::getDistanceZ(myPos, pos) > 2) { + return false; + } + } + + const int_fast32_t offsetz = myPos.getZ() - pos.getZ(); + return (pos.getX() >= myPos.getX() - viewRangeX + offsetz) && (pos.getX() <= myPos.getX() + viewRangeX + offsetz) + && (pos.getY() >= myPos.getY() - viewRangeY + offsetz) && (pos.getY() <= myPos.getY() + viewRangeY + offsetz); +} + +bool Creature::canSee(const Position& pos) const +{ + return canSee(getPosition(), pos, Map::maxViewportX, Map::maxViewportY); +} + +bool Creature::canSeeCreature(const Creature* creature) const +{ + if (!canSeeInvisibility() && creature->isInvisible()) { + return false; + } + return true; +} + +void Creature::setSkull(Skulls_t newSkull) +{ + skull = newSkull; + g_game.updateCreatureSkull(this); +} + +int64_t Creature::getTimeSinceLastMove() const +{ + if (lastStep) { + return OTSYS_TIME() - lastStep; + } + return std::numeric_limits::max(); +} + +int32_t Creature::getWalkDelay(Direction dir) const +{ + if (lastStep == 0) { + return 0; + } + + int64_t ct = OTSYS_TIME(); + int64_t stepDuration = getStepDuration(dir); + return stepDuration - (ct - lastStep); +} + +int32_t Creature::getWalkDelay() const +{ + //Used for auto-walking + if (lastStep == 0) { + return 0; + } + + int64_t ct = OTSYS_TIME(); + int64_t stepDuration = getStepDuration() * lastStepCost; + return stepDuration - (ct - lastStep); +} + +void Creature::onThink(uint32_t interval) +{ + if (!isMapLoaded && useCacheMap()) { + isMapLoaded = true; + updateMapCache(); + } + + if (followCreature && master != followCreature && !canSeeCreature(followCreature)) { + onCreatureDisappear(followCreature, false); + } + + if (attackedCreature && master != attackedCreature && !canSeeCreature(attackedCreature)) { + onCreatureDisappear(attackedCreature, false); + } + + blockTicks += interval; + if (blockTicks >= 1000) { + blockCount = std::min(blockCount + 1, 2); + blockTicks = 0; + } + + if (followCreature) { + walkUpdateTicks += interval; + if (forceUpdateFollowPath || walkUpdateTicks >= 2000) { + walkUpdateTicks = 0; + forceUpdateFollowPath = false; + isUpdatingPath = true; + } + } + + if (isUpdatingPath) { + isUpdatingPath = false; + goToFollowCreature(); + } + + //scripting event - onThink + const CreatureEventList& thinkEvents = getCreatureEvents(CREATURE_EVENT_THINK); + for (CreatureEvent* thinkEvent : thinkEvents) { + thinkEvent->executeOnThink(this, interval); + } +} + +void Creature::onAttacking(uint32_t interval) +{ + if (!attackedCreature) { + return; + } + + onAttacked(); + attackedCreature->onAttacked(); + + if (g_game.isSightClear(getPosition(), attackedCreature->getPosition(), true)) { + doAttacking(interval); + } +} + +void Creature::onIdleStatus() +{ + if (getHealth() > 0) { + damageMap.clear(); + lastHitCreatureId = 0; + } +} + +void Creature::onWalk() +{ + if (getWalkDelay() <= 0) { + Direction dir; + uint32_t flags = FLAG_IGNOREFIELDDAMAGE; + if (getNextStep(dir, flags)) { + ReturnValue ret = g_game.internalMoveCreature(this, dir, flags); + if (ret != RETURNVALUE_NOERROR) { + if (Player* player = getPlayer()) { + player->sendCancelMessage(ret); + player->sendCancelWalk(); + } + + forceUpdateFollowPath = true; + } + } else { + if (listWalkDir.empty()) { + onWalkComplete(); + } + + stopEventWalk(); + } + } + + if (cancelNextWalk) { + listWalkDir.clear(); + onWalkAborted(); + cancelNextWalk = false; + } + + if (eventWalk != 0) { + eventWalk = 0; + addEventWalk(); + } +} + +void Creature::onWalk(Direction& dir) +{ + if (hasCondition(CONDITION_DRUNK)) { + uint32_t r = uniform_random(0, 20); + if (r <= DIRECTION_DIAGONAL_MASK) { + if (r < DIRECTION_DIAGONAL_MASK) { + dir = static_cast(r); + } + g_game.internalCreatureSay(this, TALKTYPE_MONSTER_SAY, "Hicks!", false); + } + } +} + +bool Creature::getNextStep(Direction& dir, uint32_t&) +{ + if (listWalkDir.empty()) { + return false; + } + + dir = listWalkDir.front(); + listWalkDir.pop_front(); + onWalk(dir); + return true; +} + +void Creature::startAutoWalk(const std::forward_list& listDir) +{ + listWalkDir = listDir; + + size_t size = 0; + for (auto it = listDir.begin(); it != listDir.end() && size <= 1; ++it) { + size++; + } + addEventWalk(size == 1); +} + +void Creature::addEventWalk(bool firstStep) +{ + cancelNextWalk = false; + + if (getStepSpeed() <= 0) { + return; + } + + if (eventWalk != 0) { + return; + } + + int64_t ticks = getEventStepTicks(firstStep); + if (ticks <= 0) { + return; + } + + // Take first step right away, but still queue the next + if (ticks == 1) { + g_game.checkCreatureWalk(getID()); + } + + eventWalk = g_scheduler.addEvent(createSchedulerTask(ticks, std::bind(&Game::checkCreatureWalk, &g_game, getID()))); +} + +void Creature::stopEventWalk() +{ + if (eventWalk != 0) { + g_scheduler.stopEvent(eventWalk); + eventWalk = 0; + } +} + +void Creature::updateMapCache() +{ + Tile* tile; + const Position& myPos = getPosition(); + Position pos(0, 0, myPos.z); + + for (int32_t y = -maxWalkCacheHeight; y <= maxWalkCacheHeight; ++y) { + for (int32_t x = -maxWalkCacheWidth; x <= maxWalkCacheWidth; ++x) { + pos.x = myPos.getX() + x; + pos.y = myPos.getY() + y; + tile = g_game.map.getTile(pos); + updateTileCache(tile, pos); + } + } +} + +void Creature::updateTileCache(const Tile* tile, int32_t dx, int32_t dy) +{ + if (std::abs(dx) <= maxWalkCacheWidth && std::abs(dy) <= maxWalkCacheHeight) { + localMapCache[maxWalkCacheHeight + dy][maxWalkCacheWidth + dx] = tile && tile->queryAdd(0, *this, 1, FLAG_PATHFINDING | FLAG_IGNOREFIELDDAMAGE) == RETURNVALUE_NOERROR; + } +} + +void Creature::updateTileCache(const Tile* tile, const Position& pos) +{ + const Position& myPos = getPosition(); + if (pos.z == myPos.z) { + int32_t dx = Position::getOffsetX(pos, myPos); + int32_t dy = Position::getOffsetY(pos, myPos); + updateTileCache(tile, dx, dy); + } +} + +int32_t Creature::getWalkCache(const Position& pos) const +{ + if (!useCacheMap()) { + return 2; + } + + const Position& myPos = getPosition(); + if (myPos.z != pos.z) { + return 0; + } + + if (pos == myPos) { + return 1; + } + + int32_t dx = Position::getOffsetX(pos, myPos); + if (std::abs(dx) <= maxWalkCacheWidth) { + int32_t dy = Position::getOffsetY(pos, myPos); + if (std::abs(dy) <= maxWalkCacheHeight) { + if (localMapCache[maxWalkCacheHeight + dy][maxWalkCacheWidth + dx]) { + return 1; + } else { + return 0; + } + } + } + + //out of range + return 2; +} + +void Creature::onAddTileItem(const Tile* tile, const Position& pos) +{ + if (isMapLoaded && pos.z == getPosition().z) { + updateTileCache(tile, pos); + } +} + +void Creature::onUpdateTileItem(const Tile* tile, const Position& pos, const Item*, + const ItemType& oldType, const Item*, const ItemType& newType) +{ + if (!isMapLoaded) { + return; + } + + if (oldType.blockSolid || oldType.blockPathFind || newType.blockPathFind || newType.blockSolid) { + if (pos.z == getPosition().z) { + updateTileCache(tile, pos); + } + } +} + +void Creature::onRemoveTileItem(const Tile* tile, const Position& pos, const ItemType& iType, const Item*) +{ + if (!isMapLoaded) { + return; + } + + if (iType.blockSolid || iType.blockPathFind || iType.isGroundTile()) { + if (pos.z == getPosition().z) { + updateTileCache(tile, pos); + } + } +} + +void Creature::onCreatureAppear(Creature* creature, bool isLogin) +{ + if (creature == this) { + if (useCacheMap()) { + isMapLoaded = true; + updateMapCache(); + } + + if (isLogin) { + setLastPosition(getPosition()); + } + } else if (isMapLoaded) { + if (creature->getPosition().z == getPosition().z) { + updateTileCache(creature->getTile(), creature->getPosition()); + } + } +} + +void Creature::onRemoveCreature(Creature* creature, bool) +{ + onCreatureDisappear(creature, true); + if (creature == this) { + if (master && !master->isRemoved()) { + setMaster(nullptr); + } + } else if (isMapLoaded) { + if (creature->getPosition().z == getPosition().z) { + updateTileCache(creature->getTile(), creature->getPosition()); + } + } +} + +void Creature::onCreatureDisappear(const Creature* creature, bool isLogout) +{ + if (attackedCreature == creature) { + setAttackedCreature(nullptr); + onAttackedCreatureDisappear(isLogout); + } + + if (followCreature == creature) { + setFollowCreature(nullptr); + onFollowCreatureDisappear(isLogout); + } +} + +void Creature::onChangeZone(ZoneType_t zone) +{ + if (attackedCreature && zone == ZONE_PROTECTION) { + onCreatureDisappear(attackedCreature, false); + } +} + +void Creature::onAttackedCreatureChangeZone(ZoneType_t zone) +{ + if (zone == ZONE_PROTECTION) { + onCreatureDisappear(attackedCreature, false); + } +} + +void Creature::onCreatureMove(Creature* creature, const Tile* newTile, const Position& newPos, + const Tile* oldTile, const Position& oldPos, bool teleport) +{ + if (creature == this) { + lastStep = OTSYS_TIME(); + lastStepCost = 1; + + if (!teleport) { + if (oldPos.z != newPos.z) { + //floor change extra cost + lastStepCost = 2; + } else if (Position::getDistanceX(newPos, oldPos) >= 1 && Position::getDistanceY(newPos, oldPos) >= 1) { + //diagonal extra cost + lastStepCost = 3; + } + } else { + stopEventWalk(); + } + + if (!summons.empty()) { + //check if any of our summons is out of range (+/- 2 floors or 30 tiles away) + std::forward_list despawnList; + for (Creature* summon : summons) { + const Position& pos = summon->getPosition(); + if (Position::getDistanceZ(newPos, pos) > 2 || (std::max(Position::getDistanceX(newPos, pos), Position::getDistanceY(newPos, pos)) > 30)) { + despawnList.push_front(summon); + } + } + + for (Creature* despawnCreature : despawnList) { + g_game.removeCreature(despawnCreature, true); + } + } + + if (newTile->getZone() != oldTile->getZone()) { + onChangeZone(getZone()); + } + + //update map cache + if (isMapLoaded) { + if (teleport || oldPos.z != newPos.z) { + updateMapCache(); + } else { + const Position& myPos = getPosition(); + + if (oldPos.y > newPos.y) { //north + //shift y south + for (int32_t y = mapWalkHeight - 1; --y >= 0;) { + memcpy(localMapCache[y + 1], localMapCache[y], sizeof(localMapCache[y])); + } + + //update 0 + for (int32_t x = -maxWalkCacheWidth; x <= maxWalkCacheWidth; ++x) { + Tile* cacheTile = g_game.map.getTile(myPos.getX() + x, myPos.getY() - maxWalkCacheHeight, myPos.z); + updateTileCache(cacheTile, x, -maxWalkCacheHeight); + } + } else if (oldPos.y < newPos.y) { // south + //shift y north + for (int32_t y = 0; y <= mapWalkHeight - 2; ++y) { + memcpy(localMapCache[y], localMapCache[y + 1], sizeof(localMapCache[y])); + } + + //update mapWalkHeight - 1 + for (int32_t x = -maxWalkCacheWidth; x <= maxWalkCacheWidth; ++x) { + Tile* cacheTile = g_game.map.getTile(myPos.getX() + x, myPos.getY() + maxWalkCacheHeight, myPos.z); + updateTileCache(cacheTile, x, maxWalkCacheHeight); + } + } + + if (oldPos.x < newPos.x) { // east + //shift y west + int32_t starty = 0; + int32_t endy = mapWalkHeight - 1; + int32_t dy = Position::getDistanceY(oldPos, newPos); + + if (dy < 0) { + endy += dy; + } else if (dy > 0) { + starty = dy; + } + + for (int32_t y = starty; y <= endy; ++y) { + for (int32_t x = 0; x <= mapWalkWidth - 2; ++x) { + localMapCache[y][x] = localMapCache[y][x + 1]; + } + } + + //update mapWalkWidth - 1 + for (int32_t y = -maxWalkCacheHeight; y <= maxWalkCacheHeight; ++y) { + Tile* cacheTile = g_game.map.getTile(myPos.x + maxWalkCacheWidth, myPos.y + y, myPos.z); + updateTileCache(cacheTile, maxWalkCacheWidth, y); + } + } else if (oldPos.x > newPos.x) { // west + //shift y east + int32_t starty = 0; + int32_t endy = mapWalkHeight - 1; + int32_t dy = Position::getDistanceY(oldPos, newPos); + + if (dy < 0) { + endy += dy; + } else if (dy > 0) { + starty = dy; + } + + for (int32_t y = starty; y <= endy; ++y) { + for (int32_t x = mapWalkWidth - 1; --x >= 0;) { + localMapCache[y][x + 1] = localMapCache[y][x]; + } + } + + //update 0 + for (int32_t y = -maxWalkCacheHeight; y <= maxWalkCacheHeight; ++y) { + Tile* cacheTile = g_game.map.getTile(myPos.x - maxWalkCacheWidth, myPos.y + y, myPos.z); + updateTileCache(cacheTile, -maxWalkCacheWidth, y); + } + } + + updateTileCache(oldTile, oldPos); + } + } + } else { + if (isMapLoaded) { + const Position& myPos = getPosition(); + + if (newPos.z == myPos.z) { + updateTileCache(newTile, newPos); + } + + if (oldPos.z == myPos.z) { + updateTileCache(oldTile, oldPos); + } + } + } + + if (creature == followCreature || (creature == this && followCreature)) { + if (hasFollowPath) { + isUpdatingPath = true; + } + + if (newPos.z != oldPos.z || !canSee(followCreature->getPosition())) { + onCreatureDisappear(followCreature, false); + } + } + + if (creature == attackedCreature || (creature == this && attackedCreature)) { + if (newPos.z != oldPos.z || !canSee(attackedCreature->getPosition())) { + onCreatureDisappear(attackedCreature, false); + } else { + if (hasExtraSwing()) { + //our target is moving lets see if we can get in hit + g_dispatcher.addTask(createTask(std::bind(&Game::checkCreatureAttack, &g_game, getID()))); + } + + if (newTile->getZone() != oldTile->getZone()) { + onAttackedCreatureChangeZone(attackedCreature->getZone()); + } + } + } +} + +void Creature::onDeath() +{ + bool lastHitUnjustified = false; + bool mostDamageUnjustified = false; + Creature* lastHitCreature = g_game.getCreatureByID(lastHitCreatureId); + Creature* lastHitCreatureMaster; + if (lastHitCreature) { + lastHitUnjustified = lastHitCreature->onKilledCreature(this); + lastHitCreatureMaster = lastHitCreature->getMaster(); + } else { + lastHitCreatureMaster = nullptr; + } + + Creature* mostDamageCreature = nullptr; + + const int64_t timeNow = OTSYS_TIME(); + const uint32_t inFightTicks = g_config.getNumber(ConfigManager::PZ_LOCKED); + int32_t mostDamage = 0; + std::map experienceMap; + for (const auto& it : damageMap) { + if (Creature* attacker = g_game.getCreatureByID(it.first)) { + CountBlock_t cb = it.second; + if ((cb.total > mostDamage && (timeNow - cb.ticks <= inFightTicks))) { + mostDamage = cb.total; + mostDamageCreature = attacker; + } + + if (attacker != this) { + uint64_t gainExp = getGainedExperience(attacker); + if (Player* attackerPlayer = attacker->getPlayer()) { + attackerPlayer->removeAttacked(getPlayer()); + + Party* party = attackerPlayer->getParty(); + if (party && party->getLeader() && party->isSharedExperienceActive() && party->isSharedExperienceEnabled()) { + attacker = party->getLeader(); + } + } + + auto tmpIt = experienceMap.find(attacker); + if (tmpIt == experienceMap.end()) { + experienceMap[attacker] = gainExp; + } else { + tmpIt->second += gainExp; + } + } + } + } + + for (const auto& it : experienceMap) { + it.first->onGainExperience(it.second, this); + } + + if (mostDamageCreature) { + if (mostDamageCreature != lastHitCreature && mostDamageCreature != lastHitCreatureMaster) { + Creature* mostDamageCreatureMaster = mostDamageCreature->getMaster(); + if (lastHitCreature != mostDamageCreatureMaster && (lastHitCreatureMaster == nullptr || mostDamageCreatureMaster != lastHitCreatureMaster)) { + mostDamageUnjustified = mostDamageCreature->onKilledCreature(this, false); + } + } + } + + bool droppedCorpse = dropCorpse(lastHitCreature, mostDamageCreature, lastHitUnjustified, mostDamageUnjustified); + death(lastHitCreature); + + if (master) { + setMaster(nullptr); + } + + if (droppedCorpse) { + g_game.removeCreature(this, false); + } +} + +bool Creature::dropCorpse(Creature* lastHitCreature, Creature* mostDamageCreature, bool lastHitUnjustified, bool mostDamageUnjustified) +{ + if (!lootDrop && getMonster()) { + if (master) { + //scripting event - onDeath + const CreatureEventList& deathEvents = getCreatureEvents(CREATURE_EVENT_DEATH); + for (CreatureEvent* deathEvent : deathEvents) { + deathEvent->executeOnDeath(this, nullptr, lastHitCreature, mostDamageCreature, lastHitUnjustified, mostDamageUnjustified); + } + } + + g_game.addMagicEffect(getPosition(), CONST_ME_POFF); + } else { + Item* splash; + switch (getRace()) { + case RACE_VENOM: + splash = Item::CreateItem(ITEM_FULLSPLASH, FLUID_SLIME); + break; + + case RACE_BLOOD: + splash = Item::CreateItem(ITEM_FULLSPLASH, FLUID_BLOOD); + break; + + default: + splash = nullptr; + break; + } + + Tile* tile = getTile(); + + if (splash) { + g_game.internalAddItem(tile, splash, INDEX_WHEREEVER, FLAG_NOLIMIT); + g_game.startDecay(splash); + } + + Item* corpse = getCorpse(lastHitCreature, mostDamageCreature); + if (corpse) { + g_game.internalAddItem(tile, corpse, INDEX_WHEREEVER, FLAG_NOLIMIT); + g_game.startDecay(corpse); + } + + //scripting event - onDeath + for (CreatureEvent* deathEvent : getCreatureEvents(CREATURE_EVENT_DEATH)) { + deathEvent->executeOnDeath(this, corpse, lastHitCreature, mostDamageCreature, lastHitUnjustified, mostDamageUnjustified); + } + + if (corpse) { + dropLoot(corpse->getContainer(), lastHitCreature); + } + } + + return true; +} + +bool Creature::hasBeenAttacked(uint32_t attackerId) +{ + auto it = damageMap.find(attackerId); + if (it == damageMap.end()) { + return false; + } + return (OTSYS_TIME() - it->second.ticks) <= g_config.getNumber(ConfigManager::PZ_LOCKED); +} + +Item* Creature::getCorpse(Creature*, Creature*) +{ + return Item::CreateItem(getLookCorpse()); +} + +void Creature::changeHealth(int32_t healthChange, bool sendHealthChange/* = true*/) +{ + int32_t oldHealth = health; + + if (healthChange > 0) { + health += std::min(healthChange, getMaxHealth() - health); + } else { + health = std::max(0, health + healthChange); + } + + if (sendHealthChange && oldHealth != health) { + g_game.addCreatureHealth(this); + } +} + +void Creature::gainHealth(Creature* healer, int32_t healthGain) +{ + changeHealth(healthGain); + if (healer) { + healer->onTargetCreatureGainHealth(this, healthGain); + } +} + +void Creature::drainHealth(Creature* attacker, int32_t damage) +{ + changeHealth(-damage, false); + + if (attacker) { + attacker->onAttackedCreatureDrainHealth(this, damage); + } +} + +BlockType_t Creature::blockHit(Creature* attacker, CombatType_t combatType, int32_t& damage, + bool checkDefense /* = false */, bool checkArmor /* = false */, bool /* field = false */) +{ + BlockType_t blockType = BLOCK_NONE; + + if (isImmune(combatType)) { + damage = 0; + blockType = BLOCK_IMMUNITY; + } else if (checkDefense || checkArmor) { + bool hasDefense = false; + + if (blockCount > 0) { + --blockCount; + hasDefense = true; + } + + if (checkDefense && hasDefense && canUseDefense) { + int32_t defense = getDefense(); + damage -= uniform_random(defense / 2, defense); + if (damage <= 0) { + damage = 0; + blockType = BLOCK_DEFENSE; + checkArmor = false; + } + } + + if (checkArmor) { + int32_t armor = getArmor(); + if (armor > 3) { + damage -= uniform_random(armor / 2, armor - (armor % 2 + 1)); + } else if (armor > 0) { + --damage; + } + + if (damage <= 0) { + damage = 0; + blockType = BLOCK_ARMOR; + } + } + + if (hasDefense && blockType != BLOCK_NONE) { + onBlockHit(); + } + } + + if (attacker) { + attacker->onAttackedCreature(this); + attacker->onAttackedCreatureBlockHit(blockType); + } + + onAttacked(); + return blockType; +} + +bool Creature::setAttackedCreature(Creature* creature) +{ + if (creature) { + const Position& creaturePos = creature->getPosition(); + if (creaturePos.z != getPosition().z || !canSee(creaturePos)) { + attackedCreature = nullptr; + return false; + } + + attackedCreature = creature; + onAttackedCreature(attackedCreature); + attackedCreature->onAttacked(); + } else { + attackedCreature = nullptr; + } + + for (Creature* summon : summons) { + summon->setAttackedCreature(creature); + } + return true; +} + +void Creature::getPathSearchParams(const Creature*, FindPathParams& fpp) const +{ + fpp.fullPathSearch = !hasFollowPath; + fpp.clearSight = true; + fpp.maxSearchDist = 12; + fpp.minTargetDist = 1; + fpp.maxTargetDist = 1; +} + +void Creature::goToFollowCreature() +{ + if (followCreature) { + FindPathParams fpp; + getPathSearchParams(followCreature, fpp); + + Monster* monster = getMonster(); + if (monster && !monster->getMaster() && (monster->isFleeing() || fpp.maxTargetDist > 1)) { + Direction dir = DIRECTION_NONE; + + if (monster->isFleeing()) { + monster->getDistanceStep(followCreature->getPosition(), dir, true); + } else { //maxTargetDist > 1 + if (!monster->getDistanceStep(followCreature->getPosition(), dir)) { + // if we can't get anything then let the A* calculate + listWalkDir.clear(); + if (getPathTo(followCreature->getPosition(), listWalkDir, fpp)) { + hasFollowPath = true; + startAutoWalk(listWalkDir); + } else { + hasFollowPath = false; + } + return; + } + } + + if (dir != DIRECTION_NONE) { + listWalkDir.clear(); + listWalkDir.push_front(dir); + + hasFollowPath = true; + startAutoWalk(listWalkDir); + } + } else { + listWalkDir.clear(); + if (getPathTo(followCreature->getPosition(), listWalkDir, fpp)) { + hasFollowPath = true; + startAutoWalk(listWalkDir); + } else { + hasFollowPath = false; + } + } + } + + onFollowCreatureComplete(followCreature); +} + +bool Creature::setFollowCreature(Creature* creature) +{ + if (creature) { + if (followCreature == creature) { + return true; + } + + const Position& creaturePos = creature->getPosition(); + if (creaturePos.z != getPosition().z || !canSee(creaturePos)) { + followCreature = nullptr; + return false; + } + + if (!listWalkDir.empty()) { + listWalkDir.clear(); + onWalkAborted(); + } + + hasFollowPath = false; + forceUpdateFollowPath = false; + followCreature = creature; + isUpdatingPath = true; + } else { + isUpdatingPath = false; + followCreature = nullptr; + } + + onFollowCreature(creature); + return true; +} + +double Creature::getDamageRatio(Creature* attacker) const +{ + uint32_t totalDamage = 0; + uint32_t attackerDamage = 0; + + for (const auto& it : damageMap) { + const CountBlock_t& cb = it.second; + totalDamage += cb.total; + if (it.first == attacker->getID()) { + attackerDamage += cb.total; + } + } + + if (totalDamage == 0) { + return 0; + } + + return (static_cast(attackerDamage) / totalDamage); +} + +uint64_t Creature::getGainedExperience(Creature* attacker) const +{ + return std::floor(getDamageRatio(attacker) * getLostExperience()); +} + +void Creature::addDamagePoints(Creature* attacker, int32_t damagePoints) +{ + if (damagePoints <= 0) { + return; + } + + uint32_t attackerId = attacker->id; + + auto it = damageMap.find(attackerId); + if (it == damageMap.end()) { + CountBlock_t cb; + cb.ticks = OTSYS_TIME(); + cb.total = damagePoints; + damageMap[attackerId] = cb; + } else { + it->second.total += damagePoints; + it->second.ticks = OTSYS_TIME(); + } + + lastHitCreatureId = attackerId; +} + +void Creature::onAddCondition(ConditionType_t type) +{ + if (type == CONDITION_PARALYZE && hasCondition(CONDITION_HASTE)) { + removeCondition(CONDITION_HASTE); + } else if (type == CONDITION_HASTE && hasCondition(CONDITION_PARALYZE)) { + removeCondition(CONDITION_PARALYZE); + } +} + +void Creature::onAddCombatCondition(ConditionType_t) +{ + // +} + +void Creature::onEndCondition(ConditionType_t) +{ + // +} + +void Creature::onTickCondition(ConditionType_t type, bool& bRemove) +{ + const MagicField* field = getTile()->getFieldItem(); + if (!field) { + return; + } + + switch (type) { + case CONDITION_FIRE: + bRemove = (field->getCombatType() != COMBAT_FIREDAMAGE); + break; + case CONDITION_ENERGY: + bRemove = (field->getCombatType() != COMBAT_ENERGYDAMAGE); + break; + case CONDITION_POISON: + bRemove = (field->getCombatType() != COMBAT_EARTHDAMAGE); + break; + case CONDITION_FREEZING: + bRemove = (field->getCombatType() != COMBAT_ICEDAMAGE); + break; + case CONDITION_DAZZLED: + bRemove = (field->getCombatType() != COMBAT_HOLYDAMAGE); + break; + case CONDITION_CURSED: + bRemove = (field->getCombatType() != COMBAT_DEATHDAMAGE); + break; + case CONDITION_DROWN: + bRemove = (field->getCombatType() != COMBAT_DROWNDAMAGE); + break; + case CONDITION_BLEEDING: + bRemove = (field->getCombatType() != COMBAT_PHYSICALDAMAGE); + break; + default: + break; + } +} + +void Creature::onCombatRemoveCondition(Condition* condition) +{ + removeCondition(condition); +} + +void Creature::onAttacked() +{ + // +} + +void Creature::onAttackedCreatureDrainHealth(Creature* target, int32_t points) +{ + target->addDamagePoints(this, points); +} + +bool Creature::onKilledCreature(Creature* target, bool) +{ + if (master) { + master->onKilledCreature(target); + } + + //scripting event - onKill + const CreatureEventList& killEvents = getCreatureEvents(CREATURE_EVENT_KILL); + for (CreatureEvent* killEvent : killEvents) { + killEvent->executeOnKill(this, target); + } + return false; +} + +void Creature::onGainExperience(uint64_t gainExp, Creature* target) +{ + if (gainExp == 0 || !master) { + return; + } + + gainExp /= 2; + master->onGainExperience(gainExp, target); + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, position, false, true); + if (spectators.empty()) { + return; + } + + TextMessage message(MESSAGE_EXPERIENCE_OTHERS, ucfirst(getNameDescription()) + " gained " + std::to_string(gainExp) + (gainExp != 1 ? " experience points." : " experience point.")); + message.position = position; + message.primary.color = TEXTCOLOR_WHITE_EXP; + message.primary.value = gainExp; + + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendTextMessage(message); + } +} + +bool Creature::setMaster(Creature* newMaster) { + if (!newMaster && !master) { + return false; + } + + if (newMaster) { + incrementReferenceCounter(); + newMaster->summons.push_back(this); + } + + Creature* oldMaster = master; + master = newMaster; + + if (oldMaster) { + auto summon = std::find(oldMaster->summons.begin(), oldMaster->summons.end(), this); + if (summon != oldMaster->summons.end()) { + oldMaster->summons.erase(summon); + decrementReferenceCounter(); + } + } + return true; +} + +bool Creature::addCondition(Condition* condition, bool force/* = false*/) +{ + if (condition == nullptr) { + return false; + } + + if (!force && condition->getType() == CONDITION_HASTE && hasCondition(CONDITION_PARALYZE)) { + int64_t walkDelay = getWalkDelay(); + if (walkDelay > 0) { + g_scheduler.addEvent(createSchedulerTask(walkDelay, std::bind(&Game::forceAddCondition, &g_game, getID(), condition))); + return false; + } + } + + Condition* prevCond = getCondition(condition->getType(), condition->getId(), condition->getSubId()); + if (prevCond) { + prevCond->addCondition(this, condition); + delete condition; + return true; + } + + if (condition->startCondition(this)) { + conditions.push_back(condition); + onAddCondition(condition->getType()); + return true; + } + + delete condition; + return false; +} + +bool Creature::addCombatCondition(Condition* condition) +{ + //Caution: condition variable could be deleted after the call to addCondition + ConditionType_t type = condition->getType(); + + if (!addCondition(condition)) { + return false; + } + + onAddCombatCondition(type); + return true; +} + +void Creature::removeCondition(ConditionType_t type, bool force/* = false*/) +{ + auto it = conditions.begin(), end = conditions.end(); + while (it != end) { + Condition* condition = *it; + if (condition->getType() != type) { + ++it; + continue; + } + + if (!force && type == CONDITION_PARALYZE) { + int64_t walkDelay = getWalkDelay(); + if (walkDelay > 0) { + g_scheduler.addEvent(createSchedulerTask(walkDelay, std::bind(&Game::forceRemoveCondition, &g_game, getID(), type))); + return; + } + } + + it = conditions.erase(it); + + condition->endCondition(this); + delete condition; + + onEndCondition(type); + } +} + +void Creature::removeCondition(ConditionType_t type, ConditionId_t conditionId, bool force/* = false*/) +{ + auto it = conditions.begin(), end = conditions.end(); + while (it != end) { + Condition* condition = *it; + if (condition->getType() != type || condition->getId() != conditionId) { + ++it; + continue; + } + + if (!force && type == CONDITION_PARALYZE) { + int64_t walkDelay = getWalkDelay(); + if (walkDelay > 0) { + g_scheduler.addEvent(createSchedulerTask(walkDelay, std::bind(&Game::forceRemoveCondition, &g_game, getID(), type))); + return; + } + } + + it = conditions.erase(it); + + condition->endCondition(this); + delete condition; + + onEndCondition(type); + } +} + +void Creature::removeCombatCondition(ConditionType_t type) +{ + std::vector removeConditions; + for (Condition* condition : conditions) { + if (condition->getType() == type) { + removeConditions.push_back(condition); + } + } + + for (Condition* condition : removeConditions) { + onCombatRemoveCondition(condition); + } +} + +void Creature::removeCondition(Condition* condition, bool force/* = false*/) +{ + auto it = std::find(conditions.begin(), conditions.end(), condition); + if (it == conditions.end()) { + return; + } + + if (!force && condition->getType() == CONDITION_PARALYZE) { + int64_t walkDelay = getWalkDelay(); + if (walkDelay > 0) { + g_scheduler.addEvent(createSchedulerTask(walkDelay, std::bind(&Game::forceRemoveCondition, &g_game, getID(), condition->getType()))); + return; + } + } + + conditions.erase(it); + + condition->endCondition(this); + onEndCondition(condition->getType()); + delete condition; +} + +Condition* Creature::getCondition(ConditionType_t type) const +{ + for (Condition* condition : conditions) { + if (condition->getType() == type) { + return condition; + } + } + return nullptr; +} + +Condition* Creature::getCondition(ConditionType_t type, ConditionId_t conditionId, uint32_t subId/* = 0*/) const +{ + for (Condition* condition : conditions) { + if (condition->getType() == type && condition->getId() == conditionId && condition->getSubId() == subId) { + return condition; + } + } + return nullptr; +} + +void Creature::executeConditions(uint32_t interval) +{ + ConditionList tempConditions{ conditions }; + for (Condition* condition : tempConditions) { + auto it = std::find(conditions.begin(), conditions.end(), condition); + if (it == conditions.end()) { + continue; + } + + if (!condition->executeCondition(this, interval)) { + it = std::find(conditions.begin(), conditions.end(), condition); + if (it != conditions.end()) { + conditions.erase(it); + condition->endCondition(this); + onEndCondition(condition->getType()); + delete condition; + } + } + } +} + +bool Creature::hasCondition(ConditionType_t type, uint32_t subId/* = 0*/) const +{ + if (isSuppress(type)) { + return false; + } + + int64_t timeNow = OTSYS_TIME(); + for (Condition* condition : conditions) { + if (condition->getType() != type || condition->getSubId() != subId) { + continue; + } + + if (condition->getEndTime() >= timeNow || condition->getTicks() == -1) { + return true; + } + } + return false; +} + +bool Creature::isImmune(CombatType_t type) const +{ + return hasBitSet(static_cast(type), getDamageImmunities()); +} + +bool Creature::isImmune(ConditionType_t type) const +{ + return hasBitSet(static_cast(type), getConditionImmunities()); +} + +bool Creature::isSuppress(ConditionType_t type) const +{ + return hasBitSet(static_cast(type), getConditionSuppressions()); +} + +int64_t Creature::getStepDuration(Direction dir) const +{ + int64_t stepDuration = getStepDuration(); + if ((dir & DIRECTION_DIAGONAL_MASK) != 0) { + stepDuration *= 3; + } + return stepDuration; +} + +int64_t Creature::getStepDuration() const +{ + if (isRemoved()) { + return 0; + } + + uint32_t calculatedStepSpeed; + uint32_t groundSpeed; + + int32_t stepSpeed = getStepSpeed(); + if (stepSpeed > -Creature::speedB) { + calculatedStepSpeed = floor((Creature::speedA * log((stepSpeed / 2) + Creature::speedB) + Creature::speedC) + 0.5); + if (calculatedStepSpeed == 0) { + calculatedStepSpeed = 1; + } + } else { + calculatedStepSpeed = 1; + } + + Item* ground = tile->getGround(); + if (ground) { + groundSpeed = Item::items[ground->getID()].speed; + if (groundSpeed == 0) { + groundSpeed = 150; + } + } else { + groundSpeed = 150; + } + + double duration = std::floor(1000 * groundSpeed / calculatedStepSpeed); + int64_t stepDuration = std::ceil(duration / 50) * 50; + + const Monster* monster = getMonster(); + if (monster && monster->isTargetNearby() && !monster->isFleeing() && !monster->getMaster()) { + stepDuration *= 2; + } + + return stepDuration; +} + +int64_t Creature::getEventStepTicks(bool onlyDelay) const +{ + int64_t ret = getWalkDelay(); + if (ret <= 0) { + int64_t stepDuration = getStepDuration(); + if (onlyDelay && stepDuration > 0) { + ret = 1; + } else { + ret = stepDuration * lastStepCost; + } + } + return ret; +} + +LightInfo Creature::getCreatureLight() const +{ + return internalLight; +} + +void Creature::setCreatureLight(LightInfo lightInfo) { + internalLight = std::move(lightInfo); +} + +void Creature::setNormalCreatureLight() +{ + internalLight = {}; +} + +bool Creature::registerCreatureEvent(const std::string& name) +{ + CreatureEvent* event = g_creatureEvents->getEventByName(name); + if (!event) { + return false; + } + + CreatureEventType_t type = event->getEventType(); + if (hasEventRegistered(type)) { + for (CreatureEvent* creatureEvent : eventsList) { + if (creatureEvent == event) { + return false; + } + } + } else { + scriptEventsBitField |= static_cast(1) << type; + } + + eventsList.push_back(event); + return true; +} + +bool Creature::unregisterCreatureEvent(const std::string& name) +{ + CreatureEvent* event = g_creatureEvents->getEventByName(name); + if (!event) { + return false; + } + + CreatureEventType_t type = event->getEventType(); + if (!hasEventRegistered(type)) { + return false; + } + + bool resetTypeBit = true; + + auto it = eventsList.begin(), end = eventsList.end(); + while (it != end) { + CreatureEvent* curEvent = *it; + if (curEvent == event) { + it = eventsList.erase(it); + continue; + } + + if (curEvent->getEventType() == type) { + resetTypeBit = false; + } + ++it; + } + + if (resetTypeBit) { + scriptEventsBitField &= ~(static_cast(1) << type); + } + return true; +} + +CreatureEventList Creature::getCreatureEvents(CreatureEventType_t type) +{ + CreatureEventList tmpEventList; + + if (!hasEventRegistered(type)) { + return tmpEventList; + } + + for (CreatureEvent* creatureEvent : eventsList) { + if (!creatureEvent->isLoaded()) { + continue; + } + + if (creatureEvent->getEventType() == type) { + tmpEventList.push_back(creatureEvent); + } + } + + return tmpEventList; +} + +bool FrozenPathingConditionCall::isInRange(const Position& startPos, const Position& testPos, + const FindPathParams& fpp) const +{ + if (fpp.fullPathSearch) { + if (testPos.x > targetPos.x + fpp.maxTargetDist) { + return false; + } + + if (testPos.x < targetPos.x - fpp.maxTargetDist) { + return false; + } + + if (testPos.y > targetPos.y + fpp.maxTargetDist) { + return false; + } + + if (testPos.y < targetPos.y - fpp.maxTargetDist) { + return false; + } + } else { + int_fast32_t dx = Position::getOffsetX(startPos, targetPos); + + int32_t dxMax = (dx >= 0 ? fpp.maxTargetDist : 0); + if (testPos.x > targetPos.x + dxMax) { + return false; + } + + int32_t dxMin = (dx <= 0 ? fpp.maxTargetDist : 0); + if (testPos.x < targetPos.x - dxMin) { + return false; + } + + int_fast32_t dy = Position::getOffsetY(startPos, targetPos); + + int32_t dyMax = (dy >= 0 ? fpp.maxTargetDist : 0); + if (testPos.y > targetPos.y + dyMax) { + return false; + } + + int32_t dyMin = (dy <= 0 ? fpp.maxTargetDist : 0); + if (testPos.y < targetPos.y - dyMin) { + return false; + } + } + return true; +} + +bool FrozenPathingConditionCall::operator()(const Position& startPos, const Position& testPos, + const FindPathParams& fpp, int32_t& bestMatchDist) const +{ + if (!isInRange(startPos, testPos, fpp)) { + return false; + } + + if (fpp.clearSight && !g_game.isSightClear(testPos, targetPos, true)) { + return false; + } + + int32_t testDist = std::max(Position::getDistanceX(targetPos, testPos), Position::getDistanceY(targetPos, testPos)); + if (fpp.maxTargetDist == 1) { + if (testDist < fpp.minTargetDist || testDist > fpp.maxTargetDist) { + return false; + } + + return true; + } else if (testDist <= fpp.maxTargetDist) { + if (testDist < fpp.minTargetDist) { + return false; + } + + if (testDist == fpp.maxTargetDist) { + bestMatchDist = 0; + return true; + } else if (testDist > bestMatchDist) { + //not quite what we want, but the best so far + bestMatchDist = testDist; + return true; + } + } + return false; +} + +bool Creature::isInvisible() const +{ + return std::find_if(conditions.begin(), conditions.end(), [] (const Condition* condition) { + return condition->getType() == CONDITION_INVISIBLE; + }) != conditions.end(); +} + +bool Creature::getPathTo(const Position& targetPos, std::forward_list& dirList, const FindPathParams& fpp) const +{ + return g_game.map.getPathMatching(*this, dirList, FrozenPathingConditionCall(targetPos), fpp); +} + +bool Creature::getPathTo(const Position& targetPos, std::forward_list& dirList, int32_t minTargetDist, int32_t maxTargetDist, bool fullPathSearch /*= true*/, bool clearSight /*= true*/, int32_t maxSearchDist /*= 0*/) const +{ + FindPathParams fpp; + fpp.fullPathSearch = fullPathSearch; + fpp.maxSearchDist = maxSearchDist; + fpp.clearSight = clearSight; + fpp.minTargetDist = minTargetDist; + fpp.maxTargetDist = maxTargetDist; + return getPathTo(targetPos, dirList, fpp); +} diff --git a/src/creature.h b/src/creature.h new file mode 100644 index 0000000..122baf8 --- /dev/null +++ b/src/creature.h @@ -0,0 +1,561 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_CREATURE_H_5363C04015254E298F84E6D59A139508 +#define FS_CREATURE_H_5363C04015254E298F84E6D59A139508 + +#include "map.h" +#include "position.h" +#include "condition.h" +#include "const.h" +#include "tile.h" +#include "enums.h" +#include "creatureevent.h" + +using ConditionList = std::list; +using CreatureEventList = std::list; + +enum slots_t : uint8_t { + CONST_SLOT_WHEREEVER = 0, + CONST_SLOT_HEAD = 1, + CONST_SLOT_NECKLACE = 2, + CONST_SLOT_BACKPACK = 3, + CONST_SLOT_ARMOR = 4, + CONST_SLOT_RIGHT = 5, + CONST_SLOT_LEFT = 6, + CONST_SLOT_LEGS = 7, + CONST_SLOT_FEET = 8, + CONST_SLOT_RING = 9, + CONST_SLOT_AMMO = 10, + + CONST_SLOT_FIRST = CONST_SLOT_HEAD, + CONST_SLOT_LAST = CONST_SLOT_AMMO, +}; + +struct FindPathParams { + bool fullPathSearch = true; + bool clearSight = true; + bool allowDiagonal = true; + bool keepDistance = false; + int32_t maxSearchDist = 0; + int32_t minTargetDist = -1; + int32_t maxTargetDist = -1; +}; + +class Map; +class Thing; +class Container; +class Player; +class Monster; +class Npc; +class Item; +class Tile; + +static constexpr int32_t EVENT_CREATURECOUNT = 10; +static constexpr int32_t EVENT_CREATURE_THINK_INTERVAL = 1000; +static constexpr int32_t EVENT_CHECK_CREATURE_INTERVAL = (EVENT_CREATURE_THINK_INTERVAL / EVENT_CREATURECOUNT); + +class FrozenPathingConditionCall +{ + public: + explicit FrozenPathingConditionCall(Position targetPos) : targetPos(std::move(targetPos)) {} + + bool operator()(const Position& startPos, const Position& testPos, + const FindPathParams& fpp, int32_t& bestMatchDist) const; + + bool isInRange(const Position& startPos, const Position& testPos, + const FindPathParams& fpp) const; + + private: + Position targetPos; +}; + +////////////////////////////////////////////////////////////////////// +// Defines the Base class for all creatures and base functions which +// every creature has + +class Creature : virtual public Thing +{ + protected: + Creature(); + + public: + static double speedA, speedB, speedC; + + virtual ~Creature(); + + // non-copyable + Creature(const Creature&) = delete; + Creature& operator=(const Creature&) = delete; + + Creature* getCreature() override final { + return this; + } + const Creature* getCreature() const override final { + return this; + } + virtual Player* getPlayer() { + return nullptr; + } + virtual const Player* getPlayer() const { + return nullptr; + } + virtual Npc* getNpc() { + return nullptr; + } + virtual const Npc* getNpc() const { + return nullptr; + } + virtual Monster* getMonster() { + return nullptr; + } + virtual const Monster* getMonster() const { + return nullptr; + } + + virtual const std::string& getName() const = 0; + virtual const std::string& getNameDescription() const = 0; + + virtual CreatureType_t getType() const = 0; + + virtual void setID() = 0; + void setRemoved() { + isInternalRemoved = true; + } + + uint32_t getID() const { + return id; + } + virtual void removeList() = 0; + virtual void addList() = 0; + + virtual bool canSee(const Position& pos) const; + virtual bool canSeeCreature(const Creature* creature) const; + + virtual RaceType_t getRace() const { + return RACE_NONE; + } + virtual Skulls_t getSkull() const { + return skull; + } + virtual Skulls_t getSkullClient(const Creature* creature) const { + return creature->getSkull(); + } + void setSkull(Skulls_t newSkull); + Direction getDirection() const { + return direction; + } + void setDirection(Direction dir) { + direction = dir; + } + + bool isHealthHidden() const { + return hiddenHealth; + } + void setHiddenHealth(bool b) { + hiddenHealth = b; + } + + int32_t getThrowRange() const override final { + return 1; + } + bool isPushable() const override { + return getWalkDelay() <= 0; + } + bool isRemoved() const override final { + return isInternalRemoved; + } + virtual bool canSeeInvisibility() const { + return false; + } + virtual bool isInGhostMode() const { + return false; + } + + int32_t getWalkDelay(Direction dir) const; + int32_t getWalkDelay() const; + int64_t getTimeSinceLastMove() const; + + int64_t getEventStepTicks(bool onlyDelay = false) const; + int64_t getStepDuration(Direction dir) const; + int64_t getStepDuration() const; + virtual int32_t getStepSpeed() const { + return getSpeed(); + } + int32_t getSpeed() const { + return baseSpeed + varSpeed; + } + void setSpeed(int32_t varSpeedDelta) { + int32_t oldSpeed = getSpeed(); + varSpeed = varSpeedDelta; + + if (getSpeed() <= 0) { + stopEventWalk(); + cancelNextWalk = true; + } else if (oldSpeed <= 0 && !listWalkDir.empty()) { + addEventWalk(); + } + } + + void setBaseSpeed(uint32_t newBaseSpeed) { + baseSpeed = newBaseSpeed; + } + uint32_t getBaseSpeed() const { + return baseSpeed; + } + + int32_t getHealth() const { + return health; + } + virtual int32_t getMaxHealth() const { + return healthMax; + } + + const Outfit_t getCurrentOutfit() const { + return currentOutfit; + } + void setCurrentOutfit(Outfit_t outfit) { + currentOutfit = outfit; + } + const Outfit_t getDefaultOutfit() const { + return defaultOutfit; + } + bool isInvisible() const; + ZoneType_t getZone() const { + return getTile()->getZone(); + } + + //walk functions + void startAutoWalk(const std::forward_list& listDir); + void addEventWalk(bool firstStep = false); + void stopEventWalk(); + virtual void goToFollowCreature(); + + //walk events + virtual void onWalk(Direction& dir); + virtual void onWalkAborted() {} + virtual void onWalkComplete() {} + + //follow functions + Creature* getFollowCreature() const { + return followCreature; + } + virtual bool setFollowCreature(Creature* creature); + + //follow events + virtual void onFollowCreature(const Creature*) {} + virtual void onFollowCreatureComplete(const Creature*) {} + + //combat functions + Creature* getAttackedCreature() { + return attackedCreature; + } + virtual bool setAttackedCreature(Creature* creature); + virtual BlockType_t blockHit(Creature* attacker, CombatType_t combatType, int32_t& damage, + bool checkDefense = false, bool checkArmor = false, bool field = false); + + bool setMaster(Creature* newMaster); + + void removeMaster() { + if (master) { + master = nullptr; + decrementReferenceCounter(); + } + } + + bool isSummon() const { + return master != nullptr; + } + Creature* getMaster() const { + return master; + } + + const std::list& getSummons() const { + return summons; + } + + virtual int32_t getArmor() const { + return 0; + } + virtual int32_t getDefense() const { + return 0; + } + virtual float getAttackFactor() const { + return 1.0f; + } + virtual float getDefenseFactor() const { + return 1.0f; + } + + virtual uint8_t getSpeechBubble() const { + return SPEECHBUBBLE_NONE; + } + + bool addCondition(Condition* condition, bool force = false); + bool addCombatCondition(Condition* condition); + void removeCondition(ConditionType_t type, ConditionId_t conditionId, bool force = false); + void removeCondition(ConditionType_t type, bool force = false); + void removeCondition(Condition* condition, bool force = false); + void removeCombatCondition(ConditionType_t type); + Condition* getCondition(ConditionType_t type) const; + Condition* getCondition(ConditionType_t type, ConditionId_t conditionId, uint32_t subId = 0) const; + void executeConditions(uint32_t interval); + bool hasCondition(ConditionType_t type, uint32_t subId = 0) const; + virtual bool isImmune(ConditionType_t type) const; + virtual bool isImmune(CombatType_t type) const; + virtual bool isSuppress(ConditionType_t type) const; + virtual uint32_t getDamageImmunities() const { + return 0; + } + virtual uint32_t getConditionImmunities() const { + return 0; + } + virtual uint32_t getConditionSuppressions() const { + return 0; + } + virtual bool isAttackable() const { + return true; + } + + virtual void changeHealth(int32_t healthChange, bool sendHealthChange = true); + + void gainHealth(Creature* healer, int32_t healthGain); + virtual void drainHealth(Creature* attacker, int32_t damage); + + virtual bool challengeCreature(Creature*) { + return false; + } + + void onDeath(); + virtual uint64_t getGainedExperience(Creature* attacker) const; + void addDamagePoints(Creature* attacker, int32_t damagePoints); + bool hasBeenAttacked(uint32_t attackerId); + + //combat event functions + virtual void onAddCondition(ConditionType_t type); + virtual void onAddCombatCondition(ConditionType_t type); + virtual void onEndCondition(ConditionType_t type); + void onTickCondition(ConditionType_t type, bool& bRemove); + virtual void onCombatRemoveCondition(Condition* condition); + virtual void onAttackedCreature(Creature*, bool = true) {} + virtual void onAttacked(); + virtual void onAttackedCreatureDrainHealth(Creature* target, int32_t points); + virtual void onTargetCreatureGainHealth(Creature*, int32_t) {} + virtual bool onKilledCreature(Creature* target, bool lastHit = true); + virtual void onGainExperience(uint64_t gainExp, Creature* target); + virtual void onAttackedCreatureBlockHit(BlockType_t) {} + virtual void onBlockHit() {} + virtual void onChangeZone(ZoneType_t zone); + virtual void onAttackedCreatureChangeZone(ZoneType_t zone); + virtual void onIdleStatus(); + + virtual LightInfo getCreatureLight() const; + virtual void setNormalCreatureLight(); + void setCreatureLight(LightInfo lightInfo); + + virtual void onThink(uint32_t interval); + void onAttacking(uint32_t interval); + virtual void onWalk(); + virtual bool getNextStep(Direction& dir, uint32_t& flags); + + void onAddTileItem(const Tile* tile, const Position& pos); + virtual void onUpdateTileItem(const Tile* tile, const Position& pos, const Item* oldItem, + const ItemType& oldType, const Item* newItem, const ItemType& newType); + virtual void onRemoveTileItem(const Tile* tile, const Position& pos, const ItemType& iType, + const Item* item); + + virtual void onCreatureAppear(Creature* creature, bool isLogin); + virtual void onRemoveCreature(Creature* creature, bool isLogout); + virtual void onCreatureMove(Creature* creature, const Tile* newTile, const Position& newPos, + const Tile* oldTile, const Position& oldPos, bool teleport); + + virtual void onAttackedCreatureDisappear(bool) {} + virtual void onFollowCreatureDisappear(bool) {} + + virtual void onCreatureSay(Creature*, SpeakClasses, const std::string&) {} + + virtual void onPlacedCreature() {} + + virtual bool getCombatValues(int32_t&, int32_t&) { + return false; + } + + size_t getSummonCount() const { + return summons.size(); + } + void setDropLoot(bool lootDrop) { + this->lootDrop = lootDrop; + } + void setSkillLoss(bool skillLoss) { + this->skillLoss = skillLoss; + } + void setUseDefense(bool useDefense) { + canUseDefense = useDefense; + } + + //creature script events + bool registerCreatureEvent(const std::string& name); + bool unregisterCreatureEvent(const std::string& name); + + Cylinder* getParent() const override final { + return tile; + } + void setParent(Cylinder* cylinder) override final { + tile = static_cast(cylinder); + position = tile->getPosition(); + } + + const Position& getPosition() const override final { + return position; + } + + Tile* getTile() override final { + return tile; + } + const Tile* getTile() const override final { + return tile; + } + + int32_t getWalkCache(const Position& pos) const; + + const Position& getLastPosition() const { + return lastPosition; + } + void setLastPosition(Position newLastPos) { + lastPosition = newLastPos; + } + + static bool canSee(const Position& myPos, const Position& pos, int32_t viewRangeX, int32_t viewRangeY); + + double getDamageRatio(Creature* attacker) const; + + bool getPathTo(const Position& targetPos, std::forward_list& dirList, const FindPathParams& fpp) const; + bool getPathTo(const Position& targetPos, std::forward_list& dirList, int32_t minTargetDist, int32_t maxTargetDist, bool fullPathSearch = true, bool clearSight = true, int32_t maxSearchDist = 0) const; + + void incrementReferenceCounter() { + ++referenceCounter; + } + void decrementReferenceCounter() { + if (--referenceCounter == 0) { + delete this; + } + } + + protected: + virtual bool useCacheMap() const { + return false; + } + + struct CountBlock_t { + int32_t total; + int64_t ticks; + }; + + static constexpr int32_t mapWalkWidth = Map::maxViewportX * 2 + 1; + static constexpr int32_t mapWalkHeight = Map::maxViewportY * 2 + 1; + static constexpr int32_t maxWalkCacheWidth = (mapWalkWidth - 1) / 2; + static constexpr int32_t maxWalkCacheHeight = (mapWalkHeight - 1) / 2; + + Position position; + + using CountMap = std::map; + CountMap damageMap; + + std::list summons; + CreatureEventList eventsList; + ConditionList conditions; + + std::forward_list listWalkDir; + + Tile* tile = nullptr; + Creature* attackedCreature = nullptr; + Creature* master = nullptr; + Creature* followCreature = nullptr; + + uint64_t lastStep = 0; + uint32_t referenceCounter = 0; + uint32_t id = 0; + uint32_t scriptEventsBitField = 0; + uint32_t eventWalk = 0; + uint32_t walkUpdateTicks = 0; + uint32_t lastHitCreatureId = 0; + uint32_t blockCount = 0; + uint32_t blockTicks = 0; + uint32_t lastStepCost = 1; + uint32_t baseSpeed = 220; + int32_t varSpeed = 0; + int32_t health = 1000; + int32_t healthMax = 1000; + + Outfit_t currentOutfit; + Outfit_t defaultOutfit; + + Position lastPosition; + LightInfo internalLight; + + Direction direction = DIRECTION_SOUTH; + Skulls_t skull = SKULL_NONE; + + bool localMapCache[mapWalkHeight][mapWalkWidth] = {{ false }}; + bool isInternalRemoved = false; + bool isMapLoaded = false; + bool isUpdatingPath = false; + bool creatureCheck = false; + bool inCheckCreaturesVector = false; + bool skillLoss = true; + bool lootDrop = true; + bool cancelNextWalk = false; + bool hasFollowPath = false; + bool forceUpdateFollowPath = false; + bool hiddenHealth = false; + bool canUseDefense = true; + + //creature script events + bool hasEventRegistered(CreatureEventType_t event) const { + return (0 != (scriptEventsBitField & (static_cast(1) << event))); + } + CreatureEventList getCreatureEvents(CreatureEventType_t type); + + void updateMapCache(); + void updateTileCache(const Tile* tile, int32_t dx, int32_t dy); + void updateTileCache(const Tile* tile, const Position& pos); + void onCreatureDisappear(const Creature* creature, bool isLogout); + virtual void doAttacking(uint32_t) {} + virtual bool hasExtraSwing() { + return false; + } + + virtual uint64_t getLostExperience() const { + return 0; + } + virtual void dropLoot(Container*, Creature*) {} + virtual uint16_t getLookCorpse() const { + return 0; + } + virtual void getPathSearchParams(const Creature* creature, FindPathParams& fpp) const; + virtual void death(Creature*) {} + virtual bool dropCorpse(Creature* lastHitCreature, Creature* mostDamageCreature, bool lastHitUnjustified, bool mostDamageUnjustified); + virtual Item* getCorpse(Creature* lastHitCreature, Creature* mostDamageCreature); + + friend class Game; + friend class Map; + friend class LuaScriptInterface; +}; + +#endif diff --git a/src/creatureevent.cpp b/src/creatureevent.cpp new file mode 100644 index 0000000..f2d2050 --- /dev/null +++ b/src/creatureevent.cpp @@ -0,0 +1,607 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "creatureevent.h" +#include "tools.h" +#include "player.h" + +CreatureEvents::CreatureEvents() : + scriptInterface("CreatureScript Interface") +{ + scriptInterface.initState(); +} + +void CreatureEvents::clear(bool fromLua) +{ + for (auto it = creatureEvents.begin(); it != creatureEvents.end(); ++it) { + if (fromLua == it->second.fromLua) { + it->second.clearEvent(); + } + } + + reInitState(fromLua); +} + +void CreatureEvents::removeInvalidEvents() +{ + for (auto it = creatureEvents.begin(); it != creatureEvents.end(); ++it) { + if (it->second.getScriptId() == 0) { + creatureEvents.erase(it->second.getName()); + } + } +} + +LuaScriptInterface& CreatureEvents::getScriptInterface() +{ + return scriptInterface; +} + +std::string CreatureEvents::getScriptBaseName() const +{ + return "creaturescripts"; +} + +Event_ptr CreatureEvents::getEvent(const std::string& nodeName) +{ + if (strcasecmp(nodeName.c_str(), "event") != 0) { + return nullptr; + } + return Event_ptr(new CreatureEvent(&scriptInterface)); +} + +bool CreatureEvents::registerEvent(Event_ptr event, const pugi::xml_node&) +{ + CreatureEvent_ptr creatureEvent{static_cast(event.release())}; //event is guaranteed to be a CreatureEvent + if (creatureEvent->getEventType() == CREATURE_EVENT_NONE) { + std::cout << "Error: [CreatureEvents::registerEvent] Trying to register event without type!" << std::endl; + return false; + } + + CreatureEvent* oldEvent = getEventByName(creatureEvent->getName(), false); + if (oldEvent) { + //if there was an event with the same that is not loaded + //(happens when realoading), it is reused + if (!oldEvent->isLoaded() && oldEvent->getEventType() == creatureEvent->getEventType()) { + oldEvent->copyEvent(creatureEvent.get()); + } + + return false; + } else { + //if not, register it normally + creatureEvents.emplace(creatureEvent->getName(), std::move(*creatureEvent)); + return true; + } +} + +bool CreatureEvents::registerLuaEvent(CreatureEvent* event) +{ + CreatureEvent_ptr creatureEvent{ event }; + if (creatureEvent->getEventType() == CREATURE_EVENT_NONE) { + std::cout << "Error: [CreatureEvents::registerLuaEvent] Trying to register event without type!" << std::endl; + return false; + } + + CreatureEvent* oldEvent = getEventByName(creatureEvent->getName(), false); + if (oldEvent) { + //if there was an event with the same that is not loaded + //(happens when realoading), it is reused + if (!oldEvent->isLoaded() && oldEvent->getEventType() == creatureEvent->getEventType()) { + oldEvent->copyEvent(creatureEvent.get()); + } + + return false; + } else { + //if not, register it normally + creatureEvents.emplace(creatureEvent->getName(), std::move(*creatureEvent)); + return true; + } +} + +CreatureEvent* CreatureEvents::getEventByName(const std::string& name, bool forceLoaded /*= true*/) +{ + auto it = creatureEvents.find(name); + if (it != creatureEvents.end()) { + if (!forceLoaded || it->second.isLoaded()) { + return &it->second; + } + } + return nullptr; +} + +bool CreatureEvents::playerLogin(Player* player) const +{ + //fire global event if is registered + for (const auto& it : creatureEvents) { + if (it.second.getEventType() == CREATURE_EVENT_LOGIN) { + if (!it.second.executeOnLogin(player)) { + return false; + } + } + } + return true; +} + +bool CreatureEvents::playerLogout(Player* player) const +{ + //fire global event if is registered + for (const auto& it : creatureEvents) { + if (it.second.getEventType() == CREATURE_EVENT_LOGOUT) { + if (!it.second.executeOnLogout(player)) { + return false; + } + } + } + return true; +} + +bool CreatureEvents::playerAdvance(Player* player, skills_t skill, uint32_t oldLevel, + uint32_t newLevel) +{ + for (auto& it : creatureEvents) { + if (it.second.getEventType() == CREATURE_EVENT_ADVANCE) { + if (!it.second.executeAdvance(player, skill, oldLevel, newLevel)) { + return false; + } + } + } + return true; +} + +///////////////////////////////////// + +CreatureEvent::CreatureEvent(LuaScriptInterface* interface) : + Event(interface), type(CREATURE_EVENT_NONE), loaded(false) {} + +bool CreatureEvent::configureEvent(const pugi::xml_node& node) +{ + // Name that will be used in monster xml files and + // lua function to register events to reference this event + pugi::xml_attribute nameAttribute = node.attribute("name"); + if (!nameAttribute) { + std::cout << "[Error - CreatureEvent::configureEvent] Missing name for creature event" << std::endl; + return false; + } + + eventName = nameAttribute.as_string(); + + pugi::xml_attribute typeAttribute = node.attribute("type"); + if (!typeAttribute) { + std::cout << "[Error - CreatureEvent::configureEvent] Missing type for creature event: " << eventName << std::endl; + return false; + } + + std::string tmpStr = asLowerCaseString(typeAttribute.as_string()); + if (tmpStr == "login") { + type = CREATURE_EVENT_LOGIN; + } else if (tmpStr == "logout") { + type = CREATURE_EVENT_LOGOUT; + } else if (tmpStr == "think") { + type = CREATURE_EVENT_THINK; + } else if (tmpStr == "preparedeath") { + type = CREATURE_EVENT_PREPAREDEATH; + } else if (tmpStr == "death") { + type = CREATURE_EVENT_DEATH; + } else if (tmpStr == "kill") { + type = CREATURE_EVENT_KILL; + } else if (tmpStr == "advance") { + type = CREATURE_EVENT_ADVANCE; + } else if (tmpStr == "modalwindow") { + type = CREATURE_EVENT_MODALWINDOW; + } else if (tmpStr == "textedit") { + type = CREATURE_EVENT_TEXTEDIT; + } else if (tmpStr == "healthchange") { + type = CREATURE_EVENT_HEALTHCHANGE; + } else if (tmpStr == "manachange") { + type = CREATURE_EVENT_MANACHANGE; + } else if (tmpStr == "extendedopcode") { + type = CREATURE_EVENT_EXTENDED_OPCODE; + } else { + std::cout << "[Error - CreatureEvent::configureEvent] Invalid type for creature event: " << eventName << std::endl; + return false; + } + + loaded = true; + return true; +} + +std::string CreatureEvent::getScriptEventName() const +{ + //Depending on the type script event name is different + switch (type) { + case CREATURE_EVENT_LOGIN: + return "onLogin"; + + case CREATURE_EVENT_LOGOUT: + return "onLogout"; + + case CREATURE_EVENT_THINK: + return "onThink"; + + case CREATURE_EVENT_PREPAREDEATH: + return "onPrepareDeath"; + + case CREATURE_EVENT_DEATH: + return "onDeath"; + + case CREATURE_EVENT_KILL: + return "onKill"; + + case CREATURE_EVENT_ADVANCE: + return "onAdvance"; + + case CREATURE_EVENT_MODALWINDOW: + return "onModalWindow"; + + case CREATURE_EVENT_TEXTEDIT: + return "onTextEdit"; + + case CREATURE_EVENT_HEALTHCHANGE: + return "onHealthChange"; + + case CREATURE_EVENT_MANACHANGE: + return "onManaChange"; + + case CREATURE_EVENT_EXTENDED_OPCODE: + return "onExtendedOpcode"; + + case CREATURE_EVENT_NONE: + default: + return std::string(); + } +} + +void CreatureEvent::copyEvent(CreatureEvent* creatureEvent) +{ + scriptId = creatureEvent->scriptId; + scriptInterface = creatureEvent->scriptInterface; + scripted = creatureEvent->scripted; + loaded = creatureEvent->loaded; +} + +void CreatureEvent::clearEvent() +{ + scriptId = 0; + scriptInterface = nullptr; + scripted = false; + loaded = false; +} + +bool CreatureEvent::executeOnLogin(Player* player) const +{ + //onLogin(player) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CreatureEvent::executeOnLogin] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + return scriptInterface->callFunction(1); +} + +bool CreatureEvent::executeOnLogout(Player* player) const +{ + //onLogout(player) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CreatureEvent::executeOnLogout] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + return scriptInterface->callFunction(1); +} + +bool CreatureEvent::executeOnThink(Creature* creature, uint32_t interval) +{ + //onThink(creature, interval) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CreatureEvent::executeOnThink] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + lua_pushnumber(L, interval); + + return scriptInterface->callFunction(2); +} + +bool CreatureEvent::executeOnPrepareDeath(Creature* creature, Creature* killer) +{ + //onPrepareDeath(creature, killer) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CreatureEvent::executeOnPrepareDeath] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + if (killer) { + LuaScriptInterface::pushUserdata(L, killer); + LuaScriptInterface::setCreatureMetatable(L, -1, killer); + } else { + lua_pushnil(L); + } + + return scriptInterface->callFunction(2); +} + +bool CreatureEvent::executeOnDeath(Creature* creature, Item* corpse, Creature* killer, Creature* mostDamageKiller, bool lastHitUnjustified, bool mostDamageUnjustified) +{ + //onDeath(creature, corpse, killer, mostDamageKiller, lastHitUnjustified, mostDamageUnjustified) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CreatureEvent::executeOnDeath] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + LuaScriptInterface::pushThing(L, corpse); + + if (killer) { + LuaScriptInterface::pushUserdata(L, killer); + LuaScriptInterface::setCreatureMetatable(L, -1, killer); + } else { + lua_pushnil(L); + } + + if (mostDamageKiller) { + LuaScriptInterface::pushUserdata(L, mostDamageKiller); + LuaScriptInterface::setCreatureMetatable(L, -1, mostDamageKiller); + } else { + lua_pushnil(L); + } + + LuaScriptInterface::pushBoolean(L, lastHitUnjustified); + LuaScriptInterface::pushBoolean(L, mostDamageUnjustified); + + return scriptInterface->callFunction(6); +} + +bool CreatureEvent::executeAdvance(Player* player, skills_t skill, uint32_t oldLevel, + uint32_t newLevel) +{ + //onAdvance(player, skill, oldLevel, newLevel) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CreatureEvent::executeAdvance] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + lua_pushnumber(L, static_cast(skill)); + lua_pushnumber(L, oldLevel); + lua_pushnumber(L, newLevel); + + return scriptInterface->callFunction(4); +} + +void CreatureEvent::executeOnKill(Creature* creature, Creature* target) +{ + //onKill(creature, target) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CreatureEvent::executeOnKill] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + LuaScriptInterface::pushUserdata(L, target); + LuaScriptInterface::setCreatureMetatable(L, -1, target); + scriptInterface->callVoidFunction(2); +} + +void CreatureEvent::executeModalWindow(Player* player, uint32_t modalWindowId, uint8_t buttonId, uint8_t choiceId) +{ + //onModalWindow(player, modalWindowId, buttonId, choiceId) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CreatureEvent::executeModalWindow] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(scriptId); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + lua_pushnumber(L, modalWindowId); + lua_pushnumber(L, buttonId); + lua_pushnumber(L, choiceId); + + scriptInterface->callVoidFunction(4); +} + +bool CreatureEvent::executeTextEdit(Player* player, Item* item, const std::string& text) +{ + //onTextEdit(player, item, text) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CreatureEvent::executeTextEdit] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(scriptId); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushThing(L, item); + LuaScriptInterface::pushString(L, text); + + return scriptInterface->callFunction(3); +} + +void CreatureEvent::executeHealthChange(Creature* creature, Creature* attacker, CombatDamage& damage) +{ + //onHealthChange(creature, attacker, primaryDamage, primaryType, secondaryDamage, secondaryType, origin) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CreatureEvent::executeHealthChange] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(scriptId); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + if (attacker) { + LuaScriptInterface::pushUserdata(L, attacker); + LuaScriptInterface::setCreatureMetatable(L, -1, attacker); + } else { + lua_pushnil(L); + } + + LuaScriptInterface::pushCombatDamage(L, damage); + + if (scriptInterface->protectedCall(L, 7, 4) != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } else { + damage.primary.value = std::abs(LuaScriptInterface::getNumber(L, -4)); + damage.primary.type = LuaScriptInterface::getNumber(L, -3); + damage.secondary.value = std::abs(LuaScriptInterface::getNumber(L, -2)); + damage.secondary.type = LuaScriptInterface::getNumber(L, -1); + + lua_pop(L, 4); + if (damage.primary.type != COMBAT_HEALING) { + damage.primary.value = -damage.primary.value; + damage.secondary.value = -damage.secondary.value; + } + } + + scriptInterface->resetScriptEnv(); +} + +void CreatureEvent::executeManaChange(Creature* creature, Creature* attacker, CombatDamage& damage) { + //onManaChange(creature, attacker, primaryDamage, primaryType, secondaryDamage, secondaryType, origin) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CreatureEvent::executeManaChange] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(scriptId); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + if (attacker) { + LuaScriptInterface::pushUserdata(L, attacker); + LuaScriptInterface::setCreatureMetatable(L, -1, attacker); + } else { + lua_pushnil(L); + } + + LuaScriptInterface::pushCombatDamage(L, damage); + + if (scriptInterface->protectedCall(L, 7, 4) != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } else { + damage.primary.value = LuaScriptInterface::getNumber(L, -4); + damage.primary.type = LuaScriptInterface::getNumber(L, -3); + damage.secondary.value = LuaScriptInterface::getNumber(L, -2); + damage.secondary.type = LuaScriptInterface::getNumber(L, -1); + lua_pop(L, 4); + } + + scriptInterface->resetScriptEnv(); +} + +void CreatureEvent::executeExtendedOpcode(Player* player, uint8_t opcode, const std::string& buffer) +{ + //onExtendedOpcode(player, opcode, buffer) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CreatureEvent::executeExtendedOpcode] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + lua_pushnumber(L, opcode); + LuaScriptInterface::pushString(L, buffer); + + scriptInterface->callVoidFunction(3); +} diff --git a/src/creatureevent.h b/src/creatureevent.h new file mode 100644 index 0000000..6b2c9dd --- /dev/null +++ b/src/creatureevent.h @@ -0,0 +1,132 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_CREATUREEVENT_H_73FCAF4608CB41399D53C919316646A9 +#define FS_CREATUREEVENT_H_73FCAF4608CB41399D53C919316646A9 + +#include "luascript.h" +#include "baseevents.h" +#include "enums.h" + +class CreatureEvent; +using CreatureEvent_ptr = std::unique_ptr; + +enum CreatureEventType_t { + CREATURE_EVENT_NONE, + CREATURE_EVENT_LOGIN, + CREATURE_EVENT_LOGOUT, + CREATURE_EVENT_THINK, + CREATURE_EVENT_PREPAREDEATH, + CREATURE_EVENT_DEATH, + CREATURE_EVENT_KILL, + CREATURE_EVENT_ADVANCE, + CREATURE_EVENT_MODALWINDOW, + CREATURE_EVENT_TEXTEDIT, + CREATURE_EVENT_HEALTHCHANGE, + CREATURE_EVENT_MANACHANGE, + CREATURE_EVENT_EXTENDED_OPCODE, // otclient additional network opcodes +}; + +class CreatureEvent final : public Event +{ + public: + explicit CreatureEvent(LuaScriptInterface* interface); + + bool configureEvent(const pugi::xml_node& node) override; + + CreatureEventType_t getEventType() const { + return type; + } + void setEventType(CreatureEventType_t eventType) { + type = eventType; + } + const std::string& getName() const { + return eventName; + } + void setName(const std::string& name) { + eventName = name; + } + bool isLoaded() const { + return loaded; + } + void setLoaded(bool b) { + loaded = b; + } + + void clearEvent(); + void copyEvent(CreatureEvent* creatureEvent); + + //scripting + bool executeOnLogin(Player* player) const; + bool executeOnLogout(Player* player) const; + bool executeOnThink(Creature* creature, uint32_t interval); + bool executeOnPrepareDeath(Creature* creature, Creature* killer); + bool executeOnDeath(Creature* creature, Item* corpse, Creature* killer, Creature* mostDamageKiller, bool lastHitUnjustified, bool mostDamageUnjustified); + void executeOnKill(Creature* creature, Creature* target); + bool executeAdvance(Player* player, skills_t, uint32_t, uint32_t); + void executeModalWindow(Player* player, uint32_t modalWindowId, uint8_t buttonId, uint8_t choiceId); + bool executeTextEdit(Player* player, Item* item, const std::string& text); + void executeHealthChange(Creature* creature, Creature* attacker, CombatDamage& damage); + void executeManaChange(Creature* creature, Creature* attacker, CombatDamage& damage); + void executeExtendedOpcode(Player* player, uint8_t opcode, const std::string& buffer); + // + + private: + std::string getScriptEventName() const override; + + std::string eventName; + CreatureEventType_t type; + bool loaded; +}; + +class CreatureEvents final : public BaseEvents +{ + public: + CreatureEvents(); + + // non-copyable + CreatureEvents(const CreatureEvents&) = delete; + CreatureEvents& operator=(const CreatureEvents&) = delete; + + // global events + bool playerLogin(Player* player) const; + bool playerLogout(Player* player) const; + bool playerAdvance(Player* player, skills_t, uint32_t, uint32_t); + + CreatureEvent* getEventByName(const std::string& name, bool forceLoaded = true); + + bool registerLuaEvent(CreatureEvent* event); + void clear(bool fromLua) override final; + + void removeInvalidEvents(); + + private: + LuaScriptInterface& getScriptInterface() override; + std::string getScriptBaseName() const override; + Event_ptr getEvent(const std::string& nodeName) override; + bool registerEvent(Event_ptr event, const pugi::xml_node& node) override; + + //creature events + using CreatureEventMap = std::map; + CreatureEventMap creatureEvents; + + LuaScriptInterface scriptInterface; +}; + +#endif diff --git a/src/cylinder.cpp b/src/cylinder.cpp new file mode 100644 index 0000000..4032343 --- /dev/null +++ b/src/cylinder.cpp @@ -0,0 +1,69 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "cylinder.h" + +VirtualCylinder* VirtualCylinder::virtualCylinder = new VirtualCylinder; + +int32_t Cylinder::getThingIndex(const Thing*) const +{ + return -1; +} + +size_t Cylinder::getFirstIndex() const +{ + return 0; +} + +size_t Cylinder::getLastIndex() const +{ + return 0; +} + +uint32_t Cylinder::getItemTypeCount(uint16_t, int32_t) const +{ + return 0; +} + +std::map& Cylinder::getAllItemTypeCount(std::map& countMap) const +{ + return countMap; +} + +Thing* Cylinder::getThing(size_t) const +{ + return nullptr; +} + +void Cylinder::internalAddThing(Thing*) +{ + // +} + +void Cylinder::internalAddThing(uint32_t, Thing*) +{ + // +} + +void Cylinder::startDecaying() +{ + // +} diff --git a/src/cylinder.h b/src/cylinder.h new file mode 100644 index 0000000..63a794e --- /dev/null +++ b/src/cylinder.h @@ -0,0 +1,249 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_CYLINDER_H_54BBCEB2A5B7415DAD837E4D58115150 +#define FS_CYLINDER_H_54BBCEB2A5B7415DAD837E4D58115150 + +#include "enums.h" +#include "thing.h" + +class Item; +class Creature; + +static constexpr int32_t INDEX_WHEREEVER = -1; + +enum cylinderflags_t { + FLAG_NOLIMIT = 1 << 0, //Bypass limits like capacity/container limits, blocking items/creatures etc. + FLAG_IGNOREBLOCKITEM = 1 << 1, //Bypass movable blocking item checks + FLAG_IGNOREBLOCKCREATURE = 1 << 2, //Bypass creature checks + FLAG_CHILDISOWNER = 1 << 3, //Used by containers to query capacity of the carrier (player) + FLAG_PATHFINDING = 1 << 4, //An additional check is done for floor changing/teleport items + FLAG_IGNOREFIELDDAMAGE = 1 << 5, //Bypass field damage checks + FLAG_IGNORENOTMOVEABLE = 1 << 6, //Bypass check for mobility + FLAG_IGNOREAUTOSTACK = 1 << 7, //queryDestination will not try to stack items together +}; + +enum cylinderlink_t { + LINK_OWNER, + LINK_PARENT, + LINK_TOPPARENT, + LINK_NEAR, +}; + +class Cylinder : virtual public Thing +{ + public: + /** + * Query if the cylinder can add an object + * \param index points to the destination index (inventory slot/container position) + * -1 is a internal value and means add to a empty position, with no destItem + * \param thing the object to move/add + * \param count is the amount that we want to move/add + * \param flags if FLAG_CHILDISOWNER if set the query is from a child-cylinder (check cap etc.) + * if FLAG_NOLIMIT is set blocking items/container limits is ignored + * \param actor the creature trying to add the thing + * \returns ReturnValue holds the return value + */ + virtual ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count, + uint32_t flags, Creature* actor = nullptr) const = 0; + + /** + * Query the cylinder how much it can accept + * \param index points to the destination index (inventory slot/container position) + * -1 is a internal value and means add to a empty position, with no destItem + * \param thing the object to move/add + * \param count is the amount that we want to move/add + * \param maxQueryCount is the max amount that the cylinder can accept + * \param flags optional flags to modify the default behaviour + * \returns ReturnValue holds the return value + */ + virtual ReturnValue queryMaxCount(int32_t index, const Thing& thing, uint32_t count, uint32_t& maxQueryCount, + uint32_t flags) const = 0; + + /** + * Query if the cylinder can remove an object + * \param thing the object to move/remove + * \param count is the amount that we want to remove + * \param flags optional flags to modify the default behaviour + * \returns ReturnValue holds the return value + */ + virtual ReturnValue queryRemove(const Thing& thing, uint32_t count, uint32_t flags) const = 0; + + /** + * Query the destination cylinder + * \param index points to the destination index (inventory slot/container position), + * -1 is a internal value and means add to a empty position, with no destItem + * this method can change the index to point to the new cylinder index + * \param destItem is the destination object + * \param flags optional flags to modify the default behaviour + * this method can modify the flags + * \returns Cylinder returns the destination cylinder + */ + virtual Cylinder* queryDestination(int32_t& index, const Thing& thing, Item** destItem, + uint32_t& flags) = 0; + + /** + * Add the object to the cylinder + * \param thing is the object to add + */ + virtual void addThing(Thing* thing) = 0; + + /** + * Add the object to the cylinder + * \param index points to the destination index (inventory slot/container position) + * \param thing is the object to add + */ + virtual void addThing(int32_t index, Thing* thing) = 0; + + /** + * Update the item count or type for an object + * \param thing is the object to update + * \param itemId is the new item id + * \param count is the new count value + */ + virtual void updateThing(Thing* thing, uint16_t itemId, uint32_t count) = 0; + + /** + * Replace an object with a new + * \param index is the position to change (inventory slot/container position) + * \param thing is the object to update + */ + virtual void replaceThing(uint32_t index, Thing* thing) = 0; + + /** + * Remove an object + * \param thing is the object to delete + * \param count is the new count value + */ + virtual void removeThing(Thing* thing, uint32_t count) = 0; + + /** + * Is sent after an operation (move/add) to update internal values + * \param thing is the object that has been added + * \param index is the objects new index value + * \param link holds the relation the object has to the cylinder + */ + virtual void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) = 0; + + /** + * Is sent after an operation (move/remove) to update internal values + * \param thing is the object that has been removed + * \param index is the previous index of the removed object + * \param link holds the relation the object has to the cylinder + */ + virtual void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) = 0; + + /** + * Gets the index of an object + * \param thing the object to get the index value from + * \returns the index of the object, returns -1 if not found + */ + virtual int32_t getThingIndex(const Thing* thing) const; + + /** + * Returns the first index + * \returns the first index, if not implemented 0 is returned + */ + virtual size_t getFirstIndex() const; + + /** + * Returns the last index + * \returns the last index, if not implemented 0 is returned + */ + virtual size_t getLastIndex() const; + + /** + * Gets the object based on index + * \returns the object, returns nullptr if not found + */ + virtual Thing* getThing(size_t index) const; + + /** + * Get the amount of items of a certain type + * \param itemId is the item type to the get the count of + * \param subType is the extra type an item can have such as charges/fluidtype, -1 means not used + * \returns the amount of items of the asked item type + */ + virtual uint32_t getItemTypeCount(uint16_t itemId, int32_t subType = -1) const; + + /** + * Get the amount of items of a all types + * \param countMap a map to put the itemID:count mapping in + * \returns a map mapping item id to count (same as first argument) + */ + virtual std::map& getAllItemTypeCount(std::map& countMap) const; + + /** + * Adds an object to the cylinder without sending to the client(s) + * \param thing is the object to add + */ + virtual void internalAddThing(Thing* thing); + + /** + * Adds an object to the cylinder without sending to the client(s) + * \param thing is the object to add + * \param index points to the destination index (inventory slot/container position) + */ + virtual void internalAddThing(uint32_t index, Thing* thing); + + virtual void startDecaying(); +}; + +class VirtualCylinder final : public Cylinder +{ + public: + static VirtualCylinder* virtualCylinder; + + virtual ReturnValue queryAdd(int32_t, const Thing&, uint32_t, uint32_t, Creature* = nullptr) const override { + return RETURNVALUE_NOTPOSSIBLE; + } + virtual ReturnValue queryMaxCount(int32_t, const Thing&, uint32_t, uint32_t&, uint32_t) const override { + return RETURNVALUE_NOTPOSSIBLE; + } + virtual ReturnValue queryRemove(const Thing&, uint32_t, uint32_t) const override { + return RETURNVALUE_NOTPOSSIBLE; + } + virtual Cylinder* queryDestination(int32_t&, const Thing&, Item**, uint32_t&) override { + return nullptr; + } + + virtual void addThing(Thing*) override {} + virtual void addThing(int32_t, Thing*) override {} + virtual void updateThing(Thing*, uint16_t, uint32_t) override {} + virtual void replaceThing(uint32_t, Thing*) override {} + virtual void removeThing(Thing*, uint32_t) override {} + + virtual void postAddNotification(Thing*, const Cylinder*, int32_t, cylinderlink_t = LINK_OWNER) override {} + virtual void postRemoveNotification(Thing*, const Cylinder*, int32_t, cylinderlink_t = LINK_OWNER) override {} + + bool isPushable() const override { + return false; + } + int32_t getThrowRange() const override { + return 1; + } + std::string getDescription(int32_t) const override { + return {}; + } + bool isRemoved() const override { + return false; + } +}; + +#endif diff --git a/src/database.cpp b/src/database.cpp new file mode 100644 index 0000000..8e74801 --- /dev/null +++ b/src/database.cpp @@ -0,0 +1,295 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "configmanager.h" +#include "database.h" + +#include + +extern ConfigManager g_config; + +Database::~Database() +{ + if (handle != nullptr) { + mysql_close(handle); + } +} + +bool Database::connect() +{ + // connection handle initialization + handle = mysql_init(nullptr); + if (!handle) { + std::cout << std::endl << "Failed to initialize MySQL connection handle." << std::endl; + return false; + } + + // automatic reconnect + bool reconnect = true; + mysql_options(handle, MYSQL_OPT_RECONNECT, &reconnect); + + // connects to database + if (!mysql_real_connect(handle, g_config.getString(ConfigManager::MYSQL_HOST).c_str(), g_config.getString(ConfigManager::MYSQL_USER).c_str(), g_config.getString(ConfigManager::MYSQL_PASS).c_str(), g_config.getString(ConfigManager::MYSQL_DB).c_str(), g_config.getNumber(ConfigManager::SQL_PORT), g_config.getString(ConfigManager::MYSQL_SOCK).c_str(), 0)) { + std::cout << std::endl << "MySQL Error Message: " << mysql_error(handle) << std::endl; + return false; + } + + DBResult_ptr result = storeQuery("SHOW VARIABLES LIKE 'max_allowed_packet'"); + if (result) { + maxPacketSize = result->getNumber("Value"); + } + return true; +} + +bool Database::beginTransaction() +{ + if (!executeQuery("BEGIN")) { + return false; + } + + databaseLock.lock(); + return true; +} + +bool Database::rollback() +{ + if (mysql_rollback(handle) != 0) { + std::cout << "[Error - mysql_rollback] Message: " << mysql_error(handle) << std::endl; + databaseLock.unlock(); + return false; + } + + databaseLock.unlock(); + return true; +} + +bool Database::commit() +{ + if (mysql_commit(handle) != 0) { + std::cout << "[Error - mysql_commit] Message: " << mysql_error(handle) << std::endl; + databaseLock.unlock(); + return false; + } + + databaseLock.unlock(); + return true; +} + +bool Database::executeQuery(const std::string& query) +{ + bool success = true; + + // executes the query + databaseLock.lock(); + + while (mysql_real_query(handle, query.c_str(), query.length()) != 0) { + std::cout << "[Error - mysql_real_query] Query: " << query.substr(0, 256) << std::endl << "Message: " << mysql_error(handle) << std::endl; + auto error = mysql_errno(handle); + if (error != CR_SERVER_LOST && error != CR_SERVER_GONE_ERROR && error != CR_CONN_HOST_ERROR && error != 1053/*ER_SERVER_SHUTDOWN*/ && error != CR_CONNECTION_ERROR) { + success = false; + break; + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + MYSQL_RES* m_res = mysql_store_result(handle); + databaseLock.unlock(); + + if (m_res) { + mysql_free_result(m_res); + } + + return success; +} + +DBResult_ptr Database::storeQuery(const std::string& query) +{ + databaseLock.lock(); + + retry: + while (mysql_real_query(handle, query.c_str(), query.length()) != 0) { + std::cout << "[Error - mysql_real_query] Query: " << query << std::endl << "Message: " << mysql_error(handle) << std::endl; + auto error = mysql_errno(handle); + if (error != CR_SERVER_LOST && error != CR_SERVER_GONE_ERROR && error != CR_CONN_HOST_ERROR && error != 1053/*ER_SERVER_SHUTDOWN*/ && error != CR_CONNECTION_ERROR) { + break; + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + // we should call that every time as someone would call executeQuery('SELECT...') + // as it is described in MySQL manual: "it doesn't hurt" :P + MYSQL_RES* res = mysql_store_result(handle); + if (res == nullptr) { + std::cout << "[Error - mysql_store_result] Query: " << query << std::endl << "Message: " << mysql_error(handle) << std::endl; + auto error = mysql_errno(handle); + if (error != CR_SERVER_LOST && error != CR_SERVER_GONE_ERROR && error != CR_CONN_HOST_ERROR && error != 1053/*ER_SERVER_SHUTDOWN*/ && error != CR_CONNECTION_ERROR) { + databaseLock.unlock(); + return nullptr; + } + goto retry; + } + databaseLock.unlock(); + + // retrieving results of query + DBResult_ptr result = std::make_shared(res); + if (!result->hasNext()) { + return nullptr; + } + return result; +} + +std::string Database::escapeString(const std::string& s) const +{ + return escapeBlob(s.c_str(), s.length()); +} + +std::string Database::escapeBlob(const char* s, uint32_t length) const +{ + // the worst case is 2n + 1 + size_t maxLength = (length * 2) + 1; + + std::string escaped; + escaped.reserve(maxLength + 2); + escaped.push_back('\''); + + if (length != 0) { + char* output = new char[maxLength]; + mysql_real_escape_string(handle, output, s, length); + escaped.append(output); + delete[] output; + } + + escaped.push_back('\''); + return escaped; +} + +DBResult::DBResult(MYSQL_RES* res) +{ + handle = res; + + size_t i = 0; + + MYSQL_FIELD* field = mysql_fetch_field(handle); + while (field) { + listNames[field->name] = i++; + field = mysql_fetch_field(handle); + } + + row = mysql_fetch_row(handle); +} + +DBResult::~DBResult() +{ + mysql_free_result(handle); +} + +std::string DBResult::getString(const std::string& s) const +{ + auto it = listNames.find(s); + if (it == listNames.end()) { + std::cout << "[Error - DBResult::getString] Column '" << s << "' does not exist in result set." << std::endl; + return std::string(); + } + + if (row[it->second] == nullptr) { + return std::string(); + } + + return std::string(row[it->second]); +} + +const char* DBResult::getStream(const std::string& s, unsigned long& size) const +{ + auto it = listNames.find(s); + if (it == listNames.end()) { + std::cout << "[Error - DBResult::getStream] Column '" << s << "' doesn't exist in the result set" << std::endl; + size = 0; + return nullptr; + } + + if (row[it->second] == nullptr) { + size = 0; + return nullptr; + } + + size = mysql_fetch_lengths(handle)[it->second]; + return row[it->second]; +} + +bool DBResult::hasNext() const +{ + return row != nullptr; +} + +bool DBResult::next() +{ + row = mysql_fetch_row(handle); + return row != nullptr; +} + +DBInsert::DBInsert(std::string query) : query(std::move(query)) +{ + this->length = this->query.length(); +} + +bool DBInsert::addRow(const std::string& row) +{ + // adds new row to buffer + const size_t rowLength = row.length(); + length += rowLength; + if (length > Database::getInstance().getMaxPacketSize() && !execute()) { + return false; + } + + if (values.empty()) { + values.reserve(rowLength + 2); + values.push_back('('); + values.append(row); + values.push_back(')'); + } else { + values.reserve(values.length() + rowLength + 3); + values.push_back(','); + values.push_back('('); + values.append(row); + values.push_back(')'); + } + return true; +} + +bool DBInsert::addRow(std::ostringstream& row) +{ + bool ret = addRow(row.str()); + row.str(std::string()); + return ret; +} + +bool DBInsert::execute() +{ + if (values.empty()) { + return true; + } + + // executes buffer + bool res = Database::getInstance().executeQuery(query + values); + values.clear(); + length = query.length(); + return res; +} diff --git a/src/database.h b/src/database.h new file mode 100644 index 0000000..51c3d78 --- /dev/null +++ b/src/database.h @@ -0,0 +1,242 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_DATABASE_H_A484B0CDFDE542838F506DCE3D40C693 +#define FS_DATABASE_H_A484B0CDFDE542838F506DCE3D40C693 + +#include + +#include + +class DBResult; +using DBResult_ptr = std::shared_ptr; + +class Database +{ + public: + Database() = default; + ~Database(); + + // non-copyable + Database(const Database&) = delete; + Database& operator=(const Database&) = delete; + + /** + * Singleton implementation. + * + * @return database connection handler singleton + */ + static Database& getInstance() + { + static Database instance; + return instance; + } + + /** + * Connects to the database + * + * @return true on successful connection, false on error + */ + bool connect(); + + /** + * Executes command. + * + * Executes query which doesn't generates results (eg. INSERT, UPDATE, DELETE...). + * + * @param query command + * @return true on success, false on error + */ + bool executeQuery(const std::string& query); + + /** + * Queries database. + * + * Executes query which generates results (mostly SELECT). + * + * @return results object (nullptr on error) + */ + DBResult_ptr storeQuery(const std::string& query); + + /** + * Escapes string for query. + * + * Prepares string to fit SQL queries including quoting it. + * + * @param s string to be escaped + * @return quoted string + */ + std::string escapeString(const std::string& s) const; + + /** + * Escapes binary stream for query. + * + * Prepares binary stream to fit SQL queries. + * + * @param s binary stream + * @param length stream length + * @return quoted string + */ + std::string escapeBlob(const char* s, uint32_t length) const; + + /** + * Retrieve id of last inserted row + * + * @return id on success, 0 if last query did not result on any rows with auto_increment keys + */ + uint64_t getLastInsertId() const { + return static_cast(mysql_insert_id(handle)); + } + + /** + * Get database engine version + * + * @return the database engine version + */ + static const char* getClientVersion() { + return mysql_get_client_info(); + } + + uint64_t getMaxPacketSize() const { + return maxPacketSize; + } + + private: + /** + * Transaction related methods. + * + * Methods for starting, commiting and rolling back transaction. Each of the returns boolean value. + * + * @return true on success, false on error + */ + bool beginTransaction(); + bool rollback(); + bool commit(); + + MYSQL* handle = nullptr; + std::recursive_mutex databaseLock; + uint64_t maxPacketSize = 1048576; + + friend class DBTransaction; +}; + +class DBResult +{ + public: + explicit DBResult(MYSQL_RES* res); + ~DBResult(); + + // non-copyable + DBResult(const DBResult&) = delete; + DBResult& operator=(const DBResult&) = delete; + + template + T getNumber(const std::string& s) const + { + auto it = listNames.find(s); + if (it == listNames.end()) { + std::cout << "[Error - DBResult::getNumber] Column '" << s << "' doesn't exist in the result set" << std::endl; + return static_cast(0); + } + + if (row[it->second] == nullptr) { + return static_cast(0); + } + + T data; + try { + data = boost::lexical_cast(row[it->second]); + } catch (boost::bad_lexical_cast&) { + data = 0; + } + return data; + } + + std::string getString(const std::string& s) const; + const char* getStream(const std::string& s, unsigned long& size) const; + + bool hasNext() const; + bool next(); + + private: + MYSQL_RES* handle; + MYSQL_ROW row; + + std::map listNames; + + friend class Database; +}; + +/** + * INSERT statement. + */ +class DBInsert +{ + public: + explicit DBInsert(std::string query); + bool addRow(const std::string& row); + bool addRow(std::ostringstream& row); + bool execute(); + + private: + std::string query; + std::string values; + size_t length; +}; + +class DBTransaction +{ + public: + constexpr DBTransaction() = default; + + ~DBTransaction() { + if (state == STATE_START) { + Database::getInstance().rollback(); + } + } + + // non-copyable + DBTransaction(const DBTransaction&) = delete; + DBTransaction& operator=(const DBTransaction&) = delete; + + bool begin() { + state = STATE_START; + return Database::getInstance().beginTransaction(); + } + + bool commit() { + if (state != STATE_START) { + return false; + } + + state = STATE_COMMIT; + return Database::getInstance().commit(); + } + + private: + enum TransactionStates_t { + STATE_NO_START, + STATE_START, + STATE_COMMIT, + }; + + TransactionStates_t state = STATE_NO_START; +}; + +#endif diff --git a/src/databasemanager.cpp b/src/databasemanager.cpp new file mode 100644 index 0000000..6a57652 --- /dev/null +++ b/src/databasemanager.cpp @@ -0,0 +1,172 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "configmanager.h" +#include "databasemanager.h" +#include "luascript.h" + +extern ConfigManager g_config; + +bool DatabaseManager::optimizeTables() +{ + Database& db = Database::getInstance(); + std::ostringstream query; + + query << "SELECT `TABLE_NAME` FROM `information_schema`.`TABLES` WHERE `TABLE_SCHEMA` = " << db.escapeString(g_config.getString(ConfigManager::MYSQL_DB)) << " AND `DATA_FREE` > 0"; + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return false; + } + + do { + std::string tableName = result->getString("TABLE_NAME"); + std::cout << "> Optimizing table " << tableName << "..." << std::flush; + + query.str(std::string()); + query << "OPTIMIZE TABLE `" << tableName << '`'; + + if (db.executeQuery(query.str())) { + std::cout << " [success]" << std::endl; + } else { + std::cout << " [failed]" << std::endl; + } + } while (result->next()); + return true; +} + +bool DatabaseManager::tableExists(const std::string& tableName) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `TABLE_NAME` FROM `information_schema`.`tables` WHERE `TABLE_SCHEMA` = " << db.escapeString(g_config.getString(ConfigManager::MYSQL_DB)) << " AND `TABLE_NAME` = " << db.escapeString(tableName) << " LIMIT 1"; + return db.storeQuery(query.str()).get() != nullptr; +} + +bool DatabaseManager::isDatabaseSetup() +{ + Database& db = Database::getInstance(); + std::ostringstream query; + query << "SELECT `TABLE_NAME` FROM `information_schema`.`tables` WHERE `TABLE_SCHEMA` = " << db.escapeString(g_config.getString(ConfigManager::MYSQL_DB)); + return db.storeQuery(query.str()).get() != nullptr; +} + +int32_t DatabaseManager::getDatabaseVersion() +{ + if (!tableExists("server_config")) { + Database& db = Database::getInstance(); + db.executeQuery("CREATE TABLE `server_config` (`config` VARCHAR(50) NOT NULL, `value` VARCHAR(256) NOT NULL DEFAULT '', UNIQUE(`config`)) ENGINE = InnoDB"); + db.executeQuery("INSERT INTO `server_config` VALUES ('db_version', 0)"); + return 0; + } + + int32_t version = 0; + if (getDatabaseConfig("db_version", version)) { + return version; + } + return -1; +} + +void DatabaseManager::updateDatabase() +{ + lua_State* L = luaL_newstate(); + if (!L) { + return; + } + + luaL_openlibs(L); + +#ifndef LUAJIT_VERSION + //bit operations for Lua, based on bitlib project release 24 + //bit.bnot, bit.band, bit.bor, bit.bxor, bit.lshift, bit.rshift + luaL_register(L, "bit", LuaScriptInterface::luaBitReg); +#endif + + //db table + luaL_register(L, "db", LuaScriptInterface::luaDatabaseTable); + + //result table + luaL_register(L, "result", LuaScriptInterface::luaResultTable); + + int32_t version = getDatabaseVersion(); + do { + std::ostringstream ss; + ss << "data/migrations/" << version << ".lua"; + if (luaL_dofile(L, ss.str().c_str()) != 0) { + std::cout << "[Error - DatabaseManager::updateDatabase - Version: " << version << "] " << lua_tostring(L, -1) << std::endl; + break; + } + + if (!LuaScriptInterface::reserveScriptEnv()) { + break; + } + + lua_getglobal(L, "onUpdateDatabase"); + if (lua_pcall(L, 0, 1, 0) != 0) { + LuaScriptInterface::resetScriptEnv(); + std::cout << "[Error - DatabaseManager::updateDatabase - Version: " << version << "] " << lua_tostring(L, -1) << std::endl; + break; + } + + if (!LuaScriptInterface::getBoolean(L, -1, false)) { + LuaScriptInterface::resetScriptEnv(); + break; + } + + version++; + std::cout << "> Database has been updated to version " << version << '.' << std::endl; + registerDatabaseConfig("db_version", version); + + LuaScriptInterface::resetScriptEnv(); + } while (true); + lua_close(L); +} + +bool DatabaseManager::getDatabaseConfig(const std::string& config, int32_t& value) +{ + Database& db = Database::getInstance(); + std::ostringstream query; + query << "SELECT `value` FROM `server_config` WHERE `config` = " << db.escapeString(config); + + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return false; + } + + value = result->getNumber("value"); + return true; +} + +void DatabaseManager::registerDatabaseConfig(const std::string& config, int32_t value) +{ + Database& db = Database::getInstance(); + std::ostringstream query; + + int32_t tmp; + + if (!getDatabaseConfig(config, tmp)) { + query << "INSERT INTO `server_config` VALUES (" << db.escapeString(config) << ", '" << value << "')"; + } else { + query << "UPDATE `server_config` SET `value` = '" << value << "' WHERE `config` = " << db.escapeString(config); + } + + db.executeQuery(query.str()); +} diff --git a/src/databasemanager.h b/src/databasemanager.h new file mode 100644 index 0000000..2e60c44 --- /dev/null +++ b/src/databasemanager.h @@ -0,0 +1,38 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_DATABASEMANAGER_H_2B75821C555E4D1D83E32B20D683217C +#define FS_DATABASEMANAGER_H_2B75821C555E4D1D83E32B20D683217C +#include "database.h" + +class DatabaseManager +{ + public: + static bool tableExists(const std::string& tableName); + + static int32_t getDatabaseVersion(); + static bool isDatabaseSetup(); + + static bool optimizeTables(); + static void updateDatabase(); + + static bool getDatabaseConfig(const std::string& config, int32_t& value); + static void registerDatabaseConfig(const std::string& config, int32_t value); +}; +#endif diff --git a/src/databasetasks.cpp b/src/databasetasks.cpp new file mode 100644 index 0000000..60969b1 --- /dev/null +++ b/src/databasetasks.cpp @@ -0,0 +1,105 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "databasetasks.h" +#include "tasks.h" + +extern Dispatcher g_dispatcher; + + +void DatabaseTasks::start() +{ + db.connect(); + ThreadHolder::start(); +} + +void DatabaseTasks::threadMain() +{ + std::unique_lock taskLockUnique(taskLock, std::defer_lock); + while (getState() != THREAD_STATE_TERMINATED) { + taskLockUnique.lock(); + if (tasks.empty()) { + taskSignal.wait(taskLockUnique); + } + + if (!tasks.empty()) { + DatabaseTask task = std::move(tasks.front()); + tasks.pop_front(); + taskLockUnique.unlock(); + runTask(task); + } else { + taskLockUnique.unlock(); + } + } +} + +void DatabaseTasks::addTask(std::string query, std::function callback/* = nullptr*/, bool store/* = false*/) +{ + bool signal = false; + taskLock.lock(); + if (getState() == THREAD_STATE_RUNNING) { + signal = tasks.empty(); + tasks.emplace_back(std::move(query), std::move(callback), store); + } + taskLock.unlock(); + + if (signal) { + taskSignal.notify_one(); + } +} + +void DatabaseTasks::runTask(const DatabaseTask& task) +{ + bool success; + DBResult_ptr result; + if (task.store) { + result = db.storeQuery(task.query); + success = true; + } else { + result = nullptr; + success = db.executeQuery(task.query); + } + + if (task.callback) { + g_dispatcher.addTask(createTask(std::bind(task.callback, result, success))); + } +} + +void DatabaseTasks::flush() +{ + std::unique_lock guard{ taskLock }; + while (!tasks.empty()) { + auto task = std::move(tasks.front()); + tasks.pop_front(); + guard.unlock(); + runTask(task); + guard.lock(); + } +} + +void DatabaseTasks::shutdown() +{ + taskLock.lock(); + setState(THREAD_STATE_TERMINATED); + taskLock.unlock(); + flush(); + taskSignal.notify_one(); +} diff --git a/src/databasetasks.h b/src/databasetasks.h new file mode 100644 index 0000000..2d19442 --- /dev/null +++ b/src/databasetasks.h @@ -0,0 +1,60 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_DATABASETASKS_H_9CBA08E9F5FEBA7275CCEE6560059576 +#define FS_DATABASETASKS_H_9CBA08E9F5FEBA7275CCEE6560059576 + +#include +#include "thread_holder_base.h" +#include "database.h" +#include "enums.h" + +struct DatabaseTask { + DatabaseTask(std::string&& query, std::function&& callback, bool store) : + query(std::move(query)), callback(std::move(callback)), store(store) {} + + std::string query; + std::function callback; + bool store; +}; + +class DatabaseTasks : public ThreadHolder +{ + public: + DatabaseTasks() = default; + void start(); + void flush(); + void shutdown(); + + void addTask(std::string query, std::function callback = nullptr, bool store = false); + + void threadMain(); + private: + void runTask(const DatabaseTask& task); + + Database db; + std::thread thread; + std::list tasks; + std::mutex taskLock; + std::condition_variable taskSignal; +}; + +extern DatabaseTasks g_databaseTasks; + +#endif diff --git a/src/definitions.h b/src/definitions.h new file mode 100644 index 0000000..93492b7 --- /dev/null +++ b/src/definitions.h @@ -0,0 +1,83 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_DEFINITIONS_H_877452FEC245450C9F96B8FD268D8963 +#define FS_DEFINITIONS_H_877452FEC245450C9F96B8FD268D8963 + +static constexpr auto STATUS_SERVER_NAME = "The Forgotten Server"; +static constexpr auto STATUS_SERVER_VERSION = "1.3"; +static constexpr auto STATUS_SERVER_DEVELOPERS = "Mark Samman"; + +static constexpr auto CLIENT_VERSION_MIN = 1097; +static constexpr auto CLIENT_VERSION_MAX = 1098; +static constexpr auto CLIENT_VERSION_STR = "10.98"; + +static constexpr auto AUTHENTICATOR_DIGITS = 6U; +static constexpr auto AUTHENTICATOR_PERIOD = 30U; + +#ifndef __FUNCTION__ +#define __FUNCTION__ __func__ +#endif + +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS +#endif + +#ifndef _USE_MATH_DEFINES +#define _USE_MATH_DEFINES +#endif + +#include + +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#define WIN32_LEAN_AND_MEAN + +#ifdef _MSC_VER +#ifdef NDEBUG +#define _SECURE_SCL 0 +#define HAS_ITERATOR_DEBUGGING 0 +#endif + +#pragma warning(disable:4127) // conditional expression is constant +#pragma warning(disable:4244) // 'argument' : conversion from 'type1' to 'type2', possible loss of data +#pragma warning(disable:4250) // 'class1' : inherits 'class2::member' via dominance +#pragma warning(disable:4267) // 'var' : conversion from 'size_t' to 'type', possible loss of data +#pragma warning(disable:4319) // '~': zero extending 'unsigned int' to 'lua_Number' of greater size +#pragma warning(disable:4351) // new behavior: elements of array will be default initialized +#pragma warning(disable:4458) // declaration hides class member +#endif + +#define strcasecmp _stricmp +#define strncasecmp _strnicmp + +#ifndef _WIN32_WINNT +// 0x0602: Windows 7 +#define _WIN32_WINNT 0x0602 +#endif +#endif + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#endif diff --git a/src/depotchest.cpp b/src/depotchest.cpp new file mode 100644 index 0000000..8d28e22 --- /dev/null +++ b/src/depotchest.cpp @@ -0,0 +1,82 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "depotchest.h" +#include "tools.h" + +DepotChest::DepotChest(uint16_t type) : + Container(type), maxDepotItems(1500) {} + +ReturnValue DepotChest::queryAdd(int32_t index, const Thing& thing, uint32_t count, + uint32_t flags, Creature* actor/* = nullptr*/) const +{ + const Item* item = thing.getItem(); + if (item == nullptr) { + return RETURNVALUE_NOTPOSSIBLE; + } + + bool skipLimit = hasBitSet(FLAG_NOLIMIT, flags); + if (!skipLimit) { + int32_t addCount = 0; + + if ((item->isStackable() && item->getItemCount() != count)) { + addCount = 1; + } + + if (item->getTopParent() != this) { + if (const Container* container = item->getContainer()) { + addCount = container->getItemHoldingCount() + 1; + } else { + addCount = 1; + } + } + + if (getItemHoldingCount() + addCount > maxDepotItems) { + return RETURNVALUE_DEPOTISFULL; + } + } + + return Container::queryAdd(index, thing, count, flags, actor); +} + +void DepotChest::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t) +{ + Cylinder* parent = getParent(); + if (parent != nullptr) { + parent->postAddNotification(thing, oldParent, index, LINK_PARENT); + } +} + +void DepotChest::postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t) +{ + Cylinder* parent = getParent(); + if (parent != nullptr) { + parent->postRemoveNotification(thing, newParent, index, LINK_PARENT); + } +} + +Cylinder* DepotChest::getParent() const +{ + if (parent) { + return parent->getParent(); + } + return nullptr; +} diff --git a/src/depotchest.h b/src/depotchest.h new file mode 100644 index 0000000..b1db4ea --- /dev/null +++ b/src/depotchest.h @@ -0,0 +1,57 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_DEPOTCHEST_H_6538526014684E3DBC92CC12815B6766 +#define FS_DEPOTCHEST_H_6538526014684E3DBC92CC12815B6766 + +#include "container.h" + +class DepotChest final : public Container +{ + public: + explicit DepotChest(uint16_t type); + + //serialization + void setMaxDepotItems(uint32_t maxitems) { + maxDepotItems = maxitems; + } + + //cylinder implementations + ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count, + uint32_t flags, Creature* actor = nullptr) const override; + + void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + + //overrides + bool canRemove() const override { + return false; + } + + Cylinder* getParent() const override; + Cylinder* getRealParent() const override { + return parent; + } + + private: + uint32_t maxDepotItems; +}; + +#endif + diff --git a/src/depotlocker.cpp b/src/depotlocker.cpp new file mode 100644 index 0000000..6cce856 --- /dev/null +++ b/src/depotlocker.cpp @@ -0,0 +1,64 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "depotlocker.h" + +DepotLocker::DepotLocker(uint16_t type) : + Container(type, 3), depotId(0) {} + +Attr_ReadValue DepotLocker::readAttr(AttrTypes_t attr, PropStream& propStream) +{ + if (attr == ATTR_DEPOT_ID) { + if (!propStream.read(depotId)) { + return ATTR_READ_ERROR; + } + return ATTR_READ_CONTINUE; + } + return Item::readAttr(attr, propStream); +} + +ReturnValue DepotLocker::queryAdd(int32_t, const Thing&, uint32_t, uint32_t, Creature*) const +{ + return RETURNVALUE_NOTENOUGHROOM; +} + +void DepotLocker::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t) +{ + if (parent != nullptr) { + parent->postAddNotification(thing, oldParent, index, LINK_PARENT); + } +} + +void DepotLocker::postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t) +{ + if (parent != nullptr) { + parent->postRemoveNotification(thing, newParent, index, LINK_PARENT); + } +} + +void DepotLocker::removeInbox(Inbox* inbox) +{ + auto cit = std::find(itemlist.begin(), itemlist.end(), inbox); + if (cit == itemlist.end()) { + return; + } + itemlist.erase(cit); +} diff --git a/src/depotlocker.h b/src/depotlocker.h new file mode 100644 index 0000000..750ec42 --- /dev/null +++ b/src/depotlocker.h @@ -0,0 +1,66 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_DEPOTLOCKER_H_53AD8E0606A34070B87F792611F4F3F8 +#define FS_DEPOTLOCKER_H_53AD8E0606A34070B87F792611F4F3F8 + +#include "container.h" +#include "inbox.h" + +class DepotLocker final : public Container +{ + public: + explicit DepotLocker(uint16_t type); + + DepotLocker* getDepotLocker() override { + return this; + } + const DepotLocker* getDepotLocker() const override { + return this; + } + + void removeInbox(Inbox* inbox); + + //serialization + Attr_ReadValue readAttr(AttrTypes_t attr, PropStream& propStream) override; + + uint16_t getDepotId() const { + return depotId; + } + void setDepotId(uint16_t depotId) { + this->depotId = depotId; + } + + //cylinder implementations + ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count, + uint32_t flags, Creature* actor = nullptr) const override; + + void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + + bool canRemove() const override { + return false; + } + + private: + uint16_t depotId; +}; + +#endif + diff --git a/src/enums.h b/src/enums.h new file mode 100644 index 0000000..669af52 --- /dev/null +++ b/src/enums.h @@ -0,0 +1,635 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_ENUMS_H_003445999FEE4A67BCECBE918B0124CE +#define FS_ENUMS_H_003445999FEE4A67BCECBE918B0124CE + +enum RuleViolationType_t : uint8_t { + REPORT_TYPE_NAME = 0, + REPORT_TYPE_STATEMENT = 1, + REPORT_TYPE_BOT = 2 +}; + +enum RuleViolationReasons_t : uint8_t { + REPORT_REASON_NAMEINAPPROPRIATE = 0, + REPORT_REASON_NAMEPOORFORMATTED = 1, + REPORT_REASON_NAMEADVERTISING = 2, + REPORT_REASON_NAMEUNFITTING = 3, + REPORT_REASON_NAMERULEVIOLATION = 4, + REPORT_REASON_INSULTINGSTATEMENT = 5, + REPORT_REASON_SPAMMING = 6, + REPORT_REASON_ADVERTISINGSTATEMENT = 7, + REPORT_REASON_UNFITTINGSTATEMENT = 8, + REPORT_REASON_LANGUAGESTATEMENT = 9, + REPORT_REASON_DISCLOSURE = 10, + REPORT_REASON_RULEVIOLATION = 11, + REPORT_REASON_STATEMENT_BUGABUSE = 12, + REPORT_REASON_UNOFFICIALSOFTWARE = 13, + REPORT_REASON_PRETENDING = 14, + REPORT_REASON_HARASSINGOWNERS = 15, + REPORT_REASON_FALSEINFO = 16, + REPORT_REASON_ACCOUNTSHARING = 17, + REPORT_REASON_STEALINGDATA = 18, + REPORT_REASON_SERVICEATTACKING = 19, + REPORT_REASON_SERVICEAGREEMENT = 20 +}; + +enum BugReportType_t : uint8_t { + BUG_CATEGORY_MAP = 0, + BUG_CATEGORY_TYPO = 1, + BUG_CATEGORY_TECHNICAL = 2, + BUG_CATEGORY_OTHER = 3 +}; + +enum ThreadState { + THREAD_STATE_RUNNING, + THREAD_STATE_CLOSING, + THREAD_STATE_TERMINATED, +}; + +enum itemAttrTypes : uint32_t { + ITEM_ATTRIBUTE_NONE, + + ITEM_ATTRIBUTE_ACTIONID = 1 << 0, + ITEM_ATTRIBUTE_UNIQUEID = 1 << 1, + ITEM_ATTRIBUTE_DESCRIPTION = 1 << 2, + ITEM_ATTRIBUTE_TEXT = 1 << 3, + ITEM_ATTRIBUTE_DATE = 1 << 4, + ITEM_ATTRIBUTE_WRITER = 1 << 5, + ITEM_ATTRIBUTE_NAME = 1 << 6, + ITEM_ATTRIBUTE_ARTICLE = 1 << 7, + ITEM_ATTRIBUTE_PLURALNAME = 1 << 8, + ITEM_ATTRIBUTE_WEIGHT = 1 << 9, + ITEM_ATTRIBUTE_ATTACK = 1 << 10, + ITEM_ATTRIBUTE_DEFENSE = 1 << 11, + ITEM_ATTRIBUTE_EXTRADEFENSE = 1 << 12, + ITEM_ATTRIBUTE_ARMOR = 1 << 13, + ITEM_ATTRIBUTE_HITCHANCE = 1 << 14, + ITEM_ATTRIBUTE_SHOOTRANGE = 1 << 15, + ITEM_ATTRIBUTE_OWNER = 1 << 16, + ITEM_ATTRIBUTE_DURATION = 1 << 17, + ITEM_ATTRIBUTE_DECAYSTATE = 1 << 18, + ITEM_ATTRIBUTE_CORPSEOWNER = 1 << 19, + ITEM_ATTRIBUTE_CHARGES = 1 << 20, + ITEM_ATTRIBUTE_FLUIDTYPE = 1 << 21, + ITEM_ATTRIBUTE_DOORID = 1 << 22, + ITEM_ATTRIBUTE_DECAYTO = 1 << 23, + ITEM_ATTRIBUTE_WRAPID = 1 << 24, + + ITEM_ATTRIBUTE_CUSTOM = 1U << 31 +}; + +enum VipStatus_t : uint8_t { + VIPSTATUS_OFFLINE = 0, + VIPSTATUS_ONLINE = 1, + VIPSTATUS_PENDING = 2 +}; + +enum MarketAction_t { + MARKETACTION_BUY = 0, + MARKETACTION_SELL = 1, +}; + +enum MarketRequest_t { + MARKETREQUEST_OWN_OFFERS = 0xFFFE, + MARKETREQUEST_OWN_HISTORY = 0xFFFF, +}; + +enum MarketOfferState_t { + OFFERSTATE_ACTIVE = 0, + OFFERSTATE_CANCELLED = 1, + OFFERSTATE_EXPIRED = 2, + OFFERSTATE_ACCEPTED = 3, + + OFFERSTATE_ACCEPTEDEX = 255, +}; + +enum ChannelEvent_t : uint8_t { + CHANNELEVENT_JOIN = 0, + CHANNELEVENT_LEAVE = 1, + CHANNELEVENT_INVITE = 2, + CHANNELEVENT_EXCLUDE = 3, +}; + +enum CreatureType_t : uint8_t { + CREATURETYPE_PLAYER = 0, + CREATURETYPE_MONSTER = 1, + CREATURETYPE_NPC = 2, + CREATURETYPE_SUMMON_OWN = 3, + CREATURETYPE_SUMMON_OTHERS = 4, +}; + +enum OperatingSystem_t : uint8_t { + CLIENTOS_NONE = 0, + + CLIENTOS_LINUX = 1, + CLIENTOS_WINDOWS = 2, + CLIENTOS_FLASH = 3, + + CLIENTOS_OTCLIENT_LINUX = 10, + CLIENTOS_OTCLIENT_WINDOWS = 11, + CLIENTOS_OTCLIENT_MAC = 12, +}; + +enum SpellGroup_t : uint8_t { + SPELLGROUP_NONE = 0, + SPELLGROUP_ATTACK = 1, + SPELLGROUP_HEALING = 2, + SPELLGROUP_SUPPORT = 3, + SPELLGROUP_SPECIAL = 4, +}; + +enum SpellType_t : uint8_t { + SPELL_UNDEFINED = 0, + SPELL_INSTANT = 1, + SPELL_RUNE = 2, +}; + +enum AccountType_t : uint8_t { + ACCOUNT_TYPE_NORMAL = 1, + ACCOUNT_TYPE_TUTOR = 2, + ACCOUNT_TYPE_SENIORTUTOR = 3, + ACCOUNT_TYPE_GAMEMASTER = 4, + ACCOUNT_TYPE_GOD = 5 +}; + +enum RaceType_t : uint8_t { + RACE_NONE, + RACE_VENOM, + RACE_BLOOD, + RACE_UNDEAD, + RACE_FIRE, + RACE_ENERGY, +}; + +enum CombatType_t : uint16_t { + COMBAT_NONE = 0, + + COMBAT_PHYSICALDAMAGE = 1 << 0, + COMBAT_ENERGYDAMAGE = 1 << 1, + COMBAT_EARTHDAMAGE = 1 << 2, + COMBAT_FIREDAMAGE = 1 << 3, + COMBAT_UNDEFINEDDAMAGE = 1 << 4, + COMBAT_LIFEDRAIN = 1 << 5, + COMBAT_MANADRAIN = 1 << 6, + COMBAT_HEALING = 1 << 7, + COMBAT_DROWNDAMAGE = 1 << 8, + COMBAT_ICEDAMAGE = 1 << 9, + COMBAT_HOLYDAMAGE = 1 << 10, + COMBAT_DEATHDAMAGE = 1 << 11, + + COMBAT_COUNT = 12 +}; + +enum CombatParam_t { + COMBAT_PARAM_TYPE, + COMBAT_PARAM_EFFECT, + COMBAT_PARAM_DISTANCEEFFECT, + COMBAT_PARAM_BLOCKSHIELD, + COMBAT_PARAM_BLOCKARMOR, + COMBAT_PARAM_TARGETCASTERORTOPMOST, + COMBAT_PARAM_CREATEITEM, + COMBAT_PARAM_AGGRESSIVE, + COMBAT_PARAM_DISPEL, + COMBAT_PARAM_USECHARGES, +}; + +enum CallBackParam_t { + CALLBACK_PARAM_LEVELMAGICVALUE, + CALLBACK_PARAM_SKILLVALUE, + CALLBACK_PARAM_TARGETTILE, + CALLBACK_PARAM_TARGETCREATURE, +}; + +enum ConditionParam_t { + CONDITION_PARAM_OWNER = 1, + CONDITION_PARAM_TICKS = 2, + //CONDITION_PARAM_OUTFIT = 3, + CONDITION_PARAM_HEALTHGAIN = 4, + CONDITION_PARAM_HEALTHTICKS = 5, + CONDITION_PARAM_MANAGAIN = 6, + CONDITION_PARAM_MANATICKS = 7, + CONDITION_PARAM_DELAYED = 8, + CONDITION_PARAM_SPEED = 9, + CONDITION_PARAM_LIGHT_LEVEL = 10, + CONDITION_PARAM_LIGHT_COLOR = 11, + CONDITION_PARAM_SOULGAIN = 12, + CONDITION_PARAM_SOULTICKS = 13, + CONDITION_PARAM_MINVALUE = 14, + CONDITION_PARAM_MAXVALUE = 15, + CONDITION_PARAM_STARTVALUE = 16, + CONDITION_PARAM_TICKINTERVAL = 17, + CONDITION_PARAM_FORCEUPDATE = 18, + CONDITION_PARAM_SKILL_MELEE = 19, + CONDITION_PARAM_SKILL_FIST = 20, + CONDITION_PARAM_SKILL_CLUB = 21, + CONDITION_PARAM_SKILL_SWORD = 22, + CONDITION_PARAM_SKILL_AXE = 23, + CONDITION_PARAM_SKILL_DISTANCE = 24, + CONDITION_PARAM_SKILL_SHIELD = 25, + CONDITION_PARAM_SKILL_FISHING = 26, + CONDITION_PARAM_STAT_MAXHITPOINTS = 27, + CONDITION_PARAM_STAT_MAXMANAPOINTS = 28, + // CONDITION_PARAM_STAT_SOULPOINTS = 29, + CONDITION_PARAM_STAT_MAGICPOINTS = 30, + CONDITION_PARAM_STAT_MAXHITPOINTSPERCENT = 31, + CONDITION_PARAM_STAT_MAXMANAPOINTSPERCENT = 32, + // CONDITION_PARAM_STAT_SOULPOINTSPERCENT = 33, + CONDITION_PARAM_STAT_MAGICPOINTSPERCENT = 34, + CONDITION_PARAM_PERIODICDAMAGE = 35, + CONDITION_PARAM_SKILL_MELEEPERCENT = 36, + CONDITION_PARAM_SKILL_FISTPERCENT = 37, + CONDITION_PARAM_SKILL_CLUBPERCENT = 38, + CONDITION_PARAM_SKILL_SWORDPERCENT = 39, + CONDITION_PARAM_SKILL_AXEPERCENT = 40, + CONDITION_PARAM_SKILL_DISTANCEPERCENT = 41, + CONDITION_PARAM_SKILL_SHIELDPERCENT = 42, + CONDITION_PARAM_SKILL_FISHINGPERCENT = 43, + CONDITION_PARAM_BUFF_SPELL = 44, + CONDITION_PARAM_SUBID = 45, + CONDITION_PARAM_FIELD = 46, + CONDITION_PARAM_DISABLE_DEFENSE = 47, + CONDITION_PARAM_SPECIALSKILL_CRITICALHITCHANCE = 48, + CONDITION_PARAM_SPECIALSKILL_CRITICALHITAMOUNT = 49, + CONDITION_PARAM_SPECIALSKILL_LIFELEECHCHANCE = 50, + CONDITION_PARAM_SPECIALSKILL_LIFELEECHAMOUNT = 51, + CONDITION_PARAM_SPECIALSKILL_MANALEECHCHANCE = 52, + CONDITION_PARAM_SPECIALSKILL_MANALEECHAMOUNT = 53, + CONDITION_PARAM_AGGRESSIVE = 54, +}; + +enum BlockType_t : uint8_t { + BLOCK_NONE, + BLOCK_DEFENSE, + BLOCK_ARMOR, + BLOCK_IMMUNITY +}; + +enum skills_t : uint8_t { + SKILL_FIST = 0, + SKILL_CLUB = 1, + SKILL_SWORD = 2, + SKILL_AXE = 3, + SKILL_DISTANCE = 4, + SKILL_SHIELD = 5, + SKILL_FISHING = 6, + + SKILL_MAGLEVEL = 7, + SKILL_LEVEL = 8, + + SKILL_FIRST = SKILL_FIST, + SKILL_LAST = SKILL_FISHING +}; + +enum stats_t { + STAT_MAXHITPOINTS, + STAT_MAXMANAPOINTS, + STAT_SOULPOINTS, // unused + STAT_MAGICPOINTS, + + STAT_FIRST = STAT_MAXHITPOINTS, + STAT_LAST = STAT_MAGICPOINTS +}; + +enum SpecialSkills_t { + SPECIALSKILL_CRITICALHITCHANCE, + SPECIALSKILL_CRITICALHITAMOUNT, + SPECIALSKILL_LIFELEECHCHANCE, + SPECIALSKILL_LIFELEECHAMOUNT, + SPECIALSKILL_MANALEECHCHANCE, + SPECIALSKILL_MANALEECHAMOUNT, + + SPECIALSKILL_FIRST = SPECIALSKILL_CRITICALHITCHANCE, + SPECIALSKILL_LAST = SPECIALSKILL_MANALEECHAMOUNT +}; + +enum formulaType_t { + COMBAT_FORMULA_UNDEFINED, + COMBAT_FORMULA_LEVELMAGIC, + COMBAT_FORMULA_SKILL, + COMBAT_FORMULA_DAMAGE, +}; + +enum ConditionType_t { + CONDITION_NONE, + + CONDITION_POISON = 1 << 0, + CONDITION_FIRE = 1 << 1, + CONDITION_ENERGY = 1 << 2, + CONDITION_BLEEDING = 1 << 3, + CONDITION_HASTE = 1 << 4, + CONDITION_PARALYZE = 1 << 5, + CONDITION_OUTFIT = 1 << 6, + CONDITION_INVISIBLE = 1 << 7, + CONDITION_LIGHT = 1 << 8, + CONDITION_MANASHIELD = 1 << 9, + CONDITION_INFIGHT = 1 << 10, + CONDITION_DRUNK = 1 << 11, + CONDITION_EXHAUST_WEAPON = 1 << 12, // unused + CONDITION_REGENERATION = 1 << 13, + CONDITION_SOUL = 1 << 14, + CONDITION_DROWN = 1 << 15, + CONDITION_MUTED = 1 << 16, + CONDITION_CHANNELMUTEDTICKS = 1 << 17, + CONDITION_YELLTICKS = 1 << 18, + CONDITION_ATTRIBUTES = 1 << 19, + CONDITION_FREEZING = 1 << 20, + CONDITION_DAZZLED = 1 << 21, + CONDITION_CURSED = 1 << 22, + CONDITION_EXHAUST_COMBAT = 1 << 23, // unused + CONDITION_EXHAUST_HEAL = 1 << 24, // unused + CONDITION_PACIFIED = 1 << 25, + CONDITION_SPELLCOOLDOWN = 1 << 26, + CONDITION_SPELLGROUPCOOLDOWN = 1 << 27, +}; + +enum ConditionId_t : int8_t { + CONDITIONID_DEFAULT = -1, + CONDITIONID_COMBAT, + CONDITIONID_HEAD, + CONDITIONID_NECKLACE, + CONDITIONID_BACKPACK, + CONDITIONID_ARMOR, + CONDITIONID_RIGHT, + CONDITIONID_LEFT, + CONDITIONID_LEGS, + CONDITIONID_FEET, + CONDITIONID_RING, + CONDITIONID_AMMO, +}; + +enum PlayerSex_t : uint8_t { + PLAYERSEX_FEMALE = 0, + PLAYERSEX_MALE = 1, + + PLAYERSEX_LAST = PLAYERSEX_MALE +}; + +enum Vocation_t : uint16_t { + VOCATION_NONE = 0 +}; + +enum ReturnValue { + RETURNVALUE_NOERROR, + RETURNVALUE_NOTPOSSIBLE, + RETURNVALUE_NOTENOUGHROOM, + RETURNVALUE_PLAYERISPZLOCKED, + RETURNVALUE_PLAYERISNOTINVITED, + RETURNVALUE_CANNOTTHROW, + RETURNVALUE_THEREISNOWAY, + RETURNVALUE_DESTINATIONOUTOFREACH, + RETURNVALUE_CREATUREBLOCK, + RETURNVALUE_NOTMOVEABLE, + RETURNVALUE_DROPTWOHANDEDITEM, + RETURNVALUE_BOTHHANDSNEEDTOBEFREE, + RETURNVALUE_CANONLYUSEONEWEAPON, + RETURNVALUE_NEEDEXCHANGE, + RETURNVALUE_CANNOTBEDRESSED, + RETURNVALUE_PUTTHISOBJECTINYOURHAND, + RETURNVALUE_PUTTHISOBJECTINBOTHHANDS, + RETURNVALUE_TOOFARAWAY, + RETURNVALUE_FIRSTGODOWNSTAIRS, + RETURNVALUE_FIRSTGOUPSTAIRS, + RETURNVALUE_CONTAINERNOTENOUGHROOM, + RETURNVALUE_NOTENOUGHCAPACITY, + RETURNVALUE_CANNOTPICKUP, + RETURNVALUE_THISISIMPOSSIBLE, + RETURNVALUE_DEPOTISFULL, + RETURNVALUE_CREATUREDOESNOTEXIST, + RETURNVALUE_CANNOTUSETHISOBJECT, + RETURNVALUE_PLAYERWITHTHISNAMEISNOTONLINE, + RETURNVALUE_NOTREQUIREDLEVELTOUSERUNE, + RETURNVALUE_YOUAREALREADYTRADING, + RETURNVALUE_THISPLAYERISALREADYTRADING, + RETURNVALUE_YOUMAYNOTLOGOUTDURINGAFIGHT, + RETURNVALUE_DIRECTPLAYERSHOOT, + RETURNVALUE_NOTENOUGHLEVEL, + RETURNVALUE_NOTENOUGHMAGICLEVEL, + RETURNVALUE_NOTENOUGHMANA, + RETURNVALUE_NOTENOUGHSOUL, + RETURNVALUE_YOUAREEXHAUSTED, + RETURNVALUE_YOUCANNOTUSEOBJECTSTHATFAST, + RETURNVALUE_PLAYERISNOTREACHABLE, + RETURNVALUE_CANONLYUSETHISRUNEONCREATURES, + RETURNVALUE_ACTIONNOTPERMITTEDINPROTECTIONZONE, + RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER, + RETURNVALUE_YOUMAYNOTATTACKAPERSONINPROTECTIONZONE, + RETURNVALUE_YOUMAYNOTATTACKAPERSONWHILEINPROTECTIONZONE, + RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE, + RETURNVALUE_YOUCANONLYUSEITONCREATURES, + RETURNVALUE_CREATUREISNOTREACHABLE, + RETURNVALUE_TURNSECUREMODETOATTACKUNMARKEDPLAYERS, + RETURNVALUE_YOUNEEDPREMIUMACCOUNT, + RETURNVALUE_YOUNEEDTOLEARNTHISSPELL, + RETURNVALUE_YOURVOCATIONCANNOTUSETHISSPELL, + RETURNVALUE_YOUNEEDAWEAPONTOUSETHISSPELL, + RETURNVALUE_PLAYERISPZLOCKEDLEAVEPVPZONE, + RETURNVALUE_PLAYERISPZLOCKEDENTERPVPZONE, + RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE, + RETURNVALUE_YOUCANNOTLOGOUTHERE, + RETURNVALUE_YOUNEEDAMAGICITEMTOCASTSPELL, + RETURNVALUE_CANNOTCONJUREITEMHERE, + RETURNVALUE_YOUNEEDTOSPLITYOURSPEARS, + RETURNVALUE_NAMEISTOOAMBIGUOUS, + RETURNVALUE_CANONLYUSEONESHIELD, + RETURNVALUE_NOPARTYMEMBERSINRANGE, + RETURNVALUE_YOUARENOTTHEOWNER, + RETURNVALUE_NOSUCHRAIDEXISTS, + RETURNVALUE_ANOTHERRAIDISALREADYEXECUTING, + RETURNVALUE_TRADEPLAYERFARAWAY, + RETURNVALUE_YOUDONTOWNTHISHOUSE, + RETURNVALUE_TRADEPLAYERALREADYOWNSAHOUSE, + RETURNVALUE_TRADEPLAYERHIGHESTBIDDER, + RETURNVALUE_YOUCANNOTTRADETHISHOUSE, + RETURNVALUE_YOUDONTHAVEREQUIREDPROFESSION, +}; + +enum SpeechBubble_t +{ + SPEECHBUBBLE_NONE = 0, + SPEECHBUBBLE_NORMAL = 1, + SPEECHBUBBLE_TRADE = 2, + SPEECHBUBBLE_QUEST = 3, + SPEECHBUBBLE_QUESTTRADER = 4, +}; + +enum MapMark_t +{ + MAPMARK_TICK = 0, + MAPMARK_QUESTION = 1, + MAPMARK_EXCLAMATION = 2, + MAPMARK_STAR = 3, + MAPMARK_CROSS = 4, + MAPMARK_TEMPLE = 5, + MAPMARK_KISS = 6, + MAPMARK_SHOVEL = 7, + MAPMARK_SWORD = 8, + MAPMARK_FLAG = 9, + MAPMARK_LOCK = 10, + MAPMARK_BAG = 11, + MAPMARK_SKULL = 12, + MAPMARK_DOLLAR = 13, + MAPMARK_REDNORTH = 14, + MAPMARK_REDSOUTH = 15, + MAPMARK_REDEAST = 16, + MAPMARK_REDWEST = 17, + MAPMARK_GREENNORTH = 18, + MAPMARK_GREENSOUTH = 19, +}; + +struct Outfit_t { + uint16_t lookType = 0; + uint16_t lookTypeEx = 0; + uint16_t lookMount = 0; + uint8_t lookHead = 0; + uint8_t lookBody = 0; + uint8_t lookLegs = 0; + uint8_t lookFeet = 0; + uint8_t lookAddons = 0; +}; + +struct LightInfo { + uint8_t level = 0; + uint8_t color = 0; + constexpr LightInfo() = default; + constexpr LightInfo(uint8_t level, uint8_t color) : level(level), color(color) {} +}; + +struct ShopInfo { + uint16_t itemId; + int32_t subType; + uint32_t buyPrice; + uint32_t sellPrice; + std::string realName; + + ShopInfo() { + itemId = 0; + subType = 1; + buyPrice = 0; + sellPrice = 0; + } + + ShopInfo(uint16_t itemId, int32_t subType = 0, uint32_t buyPrice = 0, uint32_t sellPrice = 0, std::string realName = "") + : itemId(itemId), subType(subType), buyPrice(buyPrice), sellPrice(sellPrice), realName(std::move(realName)) {} +}; + +struct MarketOffer { + uint32_t price; + uint32_t timestamp; + uint16_t amount; + uint16_t counter; + uint16_t itemId; + std::string playerName; +}; + +struct MarketOfferEx { + MarketOfferEx() = default; + MarketOfferEx(MarketOfferEx&& other) : + id(other.id), playerId(other.playerId), timestamp(other.timestamp), price(other.price), + amount(other.amount), counter(other.counter), itemId(other.itemId), type(other.type), + playerName(std::move(other.playerName)) {} + + uint32_t id; + uint32_t playerId; + uint32_t timestamp; + uint32_t price; + uint16_t amount; + uint16_t counter; + uint16_t itemId; + MarketAction_t type; + std::string playerName; +}; + +struct HistoryMarketOffer { + uint32_t timestamp; + uint32_t price; + uint16_t itemId; + uint16_t amount; + MarketOfferState_t state; +}; + +struct MarketStatistics { + MarketStatistics() { + numTransactions = 0; + highestPrice = 0; + totalPrice = 0; + lowestPrice = 0; + } + + uint32_t numTransactions; + uint32_t highestPrice; + uint64_t totalPrice; + uint32_t lowestPrice; +}; + +struct ModalWindow +{ + std::list> buttons, choices; + std::string title, message; + uint32_t id; + uint8_t defaultEnterButton, defaultEscapeButton; + bool priority; + + ModalWindow(uint32_t id, std::string title, std::string message) + : title(std::move(title)), message(std::move(message)), id(id), defaultEnterButton(0xFF), defaultEscapeButton(0xFF), priority(false) {} +}; + +enum CombatOrigin +{ + ORIGIN_NONE, + ORIGIN_CONDITION, + ORIGIN_SPELL, + ORIGIN_MELEE, + ORIGIN_RANGED, +}; + +struct CombatDamage +{ + struct { + CombatType_t type; + int32_t value; + } primary, secondary; + + CombatOrigin origin; + BlockType_t blockType; + bool critical; + CombatDamage() + { + origin = ORIGIN_NONE; + blockType = BLOCK_NONE; + primary.type = secondary.type = COMBAT_NONE; + primary.value = secondary.value = 0; + critical = false; + } +}; + +using MarketOfferList = std::list; +using HistoryMarketOfferList = std::list; +using ShopInfoList = std::list; + +enum MonstersEvent_t : uint8_t { + MONSTERS_EVENT_NONE = 0, + MONSTERS_EVENT_THINK = 1, + MONSTERS_EVENT_APPEAR = 2, + MONSTERS_EVENT_DISAPPEAR = 3, + MONSTERS_EVENT_MOVE = 4, + MONSTERS_EVENT_SAY = 5, +}; + +#endif diff --git a/src/events.cpp b/src/events.cpp new file mode 100644 index 0000000..bd46afe --- /dev/null +++ b/src/events.cpp @@ -0,0 +1,1022 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "events.h" +#include "tools.h" +#include "item.h" +#include "player.h" + +#include + +Events::Events() : + scriptInterface("Event Interface") +{ + scriptInterface.initState(); +} + +bool Events::load() +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/events/events.xml"); + if (!result) { + printXMLError("Error - Events::load", "data/events/events.xml", result); + return false; + } + + info = {}; + + std::set classes; + for (auto eventNode : doc.child("events").children()) { + if (!eventNode.attribute("enabled").as_bool()) { + continue; + } + + const std::string& className = eventNode.attribute("class").as_string(); + auto res = classes.insert(className); + if (res.second) { + const std::string& lowercase = asLowerCaseString(className); + if (scriptInterface.loadFile("data/events/scripts/" + lowercase + ".lua") != 0) { + std::cout << "[Warning - Events::load] Can not load script: " << lowercase << ".lua" << std::endl; + std::cout << scriptInterface.getLastLuaError() << std::endl; + } + } + + const std::string& methodName = eventNode.attribute("method").as_string(); + const int32_t event = scriptInterface.getMetaEvent(className, methodName); + if (className == "Creature") { + if (methodName == "onChangeOutfit") { + info.creatureOnChangeOutfit = event; + } else if (methodName == "onAreaCombat") { + info.creatureOnAreaCombat = event; + } else if (methodName == "onTargetCombat") { + info.creatureOnTargetCombat = event; + } else if (methodName == "onHear") { + info.creatureOnHear = event; + } else { + std::cout << "[Warning - Events::load] Unknown creature method: " << methodName << std::endl; + } + } else if (className == "Party") { + if (methodName == "onJoin") { + info.partyOnJoin = event; + } else if (methodName == "onLeave") { + info.partyOnLeave = event; + } else if (methodName == "onDisband") { + info.partyOnDisband = event; + } else if (methodName == "onShareExperience") { + info.partyOnShareExperience = event; + } else { + std::cout << "[Warning - Events::load] Unknown party method: " << methodName << std::endl; + } + } else if (className == "Player") { + if (methodName == "onBrowseField") { + info.playerOnBrowseField = event; + } else if (methodName == "onLook") { + info.playerOnLook = event; + } else if (methodName == "onLookInBattleList") { + info.playerOnLookInBattleList = event; + } else if (methodName == "onLookInTrade") { + info.playerOnLookInTrade = event; + } else if (methodName == "onLookInShop") { + info.playerOnLookInShop = event; + } else if (methodName == "onTradeRequest") { + info.playerOnTradeRequest = event; + } else if (methodName == "onTradeAccept") { + info.playerOnTradeAccept = event; + } else if (methodName == "onTradeCompleted") { + info.playerOnTradeCompleted = event; + } else if (methodName == "onMoveItem") { + info.playerOnMoveItem = event; + } else if (methodName == "onItemMoved") { + info.playerOnItemMoved = event; + } else if (methodName == "onMoveCreature") { + info.playerOnMoveCreature = event; + } else if (methodName == "onReportRuleViolation") { + info.playerOnReportRuleViolation = event; + } else if (methodName == "onReportBug") { + info.playerOnReportBug = event; + } else if (methodName == "onTurn") { + info.playerOnTurn = event; + } else if (methodName == "onGainExperience") { + info.playerOnGainExperience = event; + } else if (methodName == "onLoseExperience") { + info.playerOnLoseExperience = event; + } else if (methodName == "onGainSkillTries") { + info.playerOnGainSkillTries = event; + } else if (methodName == "onWrapItem") { + info.playerOnWrapItem = event; + } else { + std::cout << "[Warning - Events::load] Unknown player method: " << methodName << std::endl; + } + } else if (className == "Monster") { + if (methodName == "onDropLoot") { + info.monsterOnDropLoot = event; + } else if (methodName == "onSpawn") { + info.monsterOnSpawn = event; + } else { + std::cout << "[Warning - Events::load] Unknown monster method: " << methodName << std::endl; + } + } else { + std::cout << "[Warning - Events::load] Unknown class: " << className << std::endl; + } + } + return true; +} + +// Monster +bool Events::eventMonsterOnSpawn(Monster* monster, const Position& position, bool startup, bool artificial) +{ + // Monster:onSpawn(position, startup, artificial) + if (info.monsterOnSpawn == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::monsterOnSpawn] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.monsterOnSpawn, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.monsterOnSpawn); + + LuaScriptInterface::pushUserdata(L, monster); + LuaScriptInterface::setMetatable(L, -1, "Monster"); + LuaScriptInterface::pushPosition(L, position); + LuaScriptInterface::pushBoolean(L, startup); + LuaScriptInterface::pushBoolean(L, artificial); + + return scriptInterface.callFunction(4); +} + +// Creature +bool Events::eventCreatureOnChangeOutfit(Creature* creature, const Outfit_t& outfit) +{ + // Creature:onChangeOutfit(outfit) or Creature.onChangeOutfit(self, outfit) + if (info.creatureOnChangeOutfit == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventCreatureOnChangeOutfit] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.creatureOnChangeOutfit, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.creatureOnChangeOutfit); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + LuaScriptInterface::pushOutfit(L, outfit); + + return scriptInterface.callFunction(2); +} + +ReturnValue Events::eventCreatureOnAreaCombat(Creature* creature, Tile* tile, bool aggressive) +{ + // Creature:onAreaCombat(tile, aggressive) or Creature.onAreaCombat(self, tile, aggressive) + if (info.creatureOnAreaCombat == -1) { + return RETURNVALUE_NOERROR; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventCreatureOnAreaCombat] Call stack overflow" << std::endl; + return RETURNVALUE_NOTPOSSIBLE; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.creatureOnAreaCombat, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.creatureOnAreaCombat); + + if (creature) { + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + } else { + lua_pushnil(L); + } + + LuaScriptInterface::pushUserdata(L, tile); + LuaScriptInterface::setMetatable(L, -1, "Tile"); + + LuaScriptInterface::pushBoolean(L, aggressive); + + ReturnValue returnValue; + if (scriptInterface.protectedCall(L, 3, 1) != 0) { + returnValue = RETURNVALUE_NOTPOSSIBLE; + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } else { + returnValue = LuaScriptInterface::getNumber(L, -1); + lua_pop(L, 1); + } + + scriptInterface.resetScriptEnv(); + return returnValue; +} + +ReturnValue Events::eventCreatureOnTargetCombat(Creature* creature, Creature* target) +{ + // Creature:onTargetCombat(target) or Creature.onTargetCombat(self, target) + if (info.creatureOnTargetCombat == -1) { + return RETURNVALUE_NOERROR; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventCreatureOnTargetCombat] Call stack overflow" << std::endl; + return RETURNVALUE_NOTPOSSIBLE; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.creatureOnTargetCombat, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.creatureOnTargetCombat); + + if (creature) { + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + } else { + lua_pushnil(L); + } + + LuaScriptInterface::pushUserdata(L, target); + LuaScriptInterface::setCreatureMetatable(L, -1, target); + + ReturnValue returnValue; + if (scriptInterface.protectedCall(L, 2, 1) != 0) { + returnValue = RETURNVALUE_NOTPOSSIBLE; + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } else { + returnValue = LuaScriptInterface::getNumber(L, -1); + lua_pop(L, 1); + } + + scriptInterface.resetScriptEnv(); + return returnValue; +} + +void Events::eventCreatureOnHear(Creature* creature, Creature* speaker, const std::string& words, SpeakClasses type) +{ + // Creature:onHear(speaker, words, type) + if (info.creatureOnHear == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventCreatureOnHear] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.creatureOnHear, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.creatureOnHear); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + LuaScriptInterface::pushUserdata(L, speaker); + LuaScriptInterface::setCreatureMetatable(L, -1, speaker); + + LuaScriptInterface::pushString(L, words); + lua_pushnumber(L, type); + + scriptInterface.callVoidFunction(4); +} + +// Party +bool Events::eventPartyOnJoin(Party* party, Player* player) +{ + // Party:onJoin(player) or Party.onJoin(self, player) + if (info.partyOnJoin == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPartyOnJoin] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.partyOnJoin, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.partyOnJoin); + + LuaScriptInterface::pushUserdata(L, party); + LuaScriptInterface::setMetatable(L, -1, "Party"); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + return scriptInterface.callFunction(2); +} + +bool Events::eventPartyOnLeave(Party* party, Player* player) +{ + // Party:onLeave(player) or Party.onLeave(self, player) + if (info.partyOnLeave == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPartyOnLeave] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.partyOnLeave, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.partyOnLeave); + + LuaScriptInterface::pushUserdata(L, party); + LuaScriptInterface::setMetatable(L, -1, "Party"); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + return scriptInterface.callFunction(2); +} + +bool Events::eventPartyOnDisband(Party* party) +{ + // Party:onDisband() or Party.onDisband(self) + if (info.partyOnDisband == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPartyOnDisband] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.partyOnDisband, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.partyOnDisband); + + LuaScriptInterface::pushUserdata(L, party); + LuaScriptInterface::setMetatable(L, -1, "Party"); + + return scriptInterface.callFunction(1); +} + +void Events::eventPartyOnShareExperience(Party* party, uint64_t& exp) +{ + // Party:onShareExperience(exp) or Party.onShareExperience(self, exp) + if (info.partyOnShareExperience == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPartyOnShareExperience] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.partyOnShareExperience, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.partyOnShareExperience); + + LuaScriptInterface::pushUserdata(L, party); + LuaScriptInterface::setMetatable(L, -1, "Party"); + + lua_pushnumber(L, exp); + + if (scriptInterface.protectedCall(L, 2, 1) != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } else { + exp = LuaScriptInterface::getNumber(L, -1); + lua_pop(L, 1); + } + + scriptInterface.resetScriptEnv(); +} + +// Player +bool Events::eventPlayerOnBrowseField(Player* player, const Position& position) +{ + // Player:onBrowseField(position) or Player.onBrowseField(self, position) + if (info.playerOnBrowseField == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnBrowseField] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnBrowseField, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnBrowseField); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushPosition(L, position); + + return scriptInterface.callFunction(2); +} + +void Events::eventPlayerOnLook(Player* player, const Position& position, Thing* thing, uint8_t stackpos, int32_t lookDistance) +{ + // Player:onLook(thing, position, distance) or Player.onLook(self, thing, position, distance) + if (info.playerOnLook == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnLook] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnLook, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnLook); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + if (Creature* creature = thing->getCreature()) { + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + } else if (Item* item = thing->getItem()) { + LuaScriptInterface::pushUserdata(L, item); + LuaScriptInterface::setItemMetatable(L, -1, item); + } else { + lua_pushnil(L); + } + + LuaScriptInterface::pushPosition(L, position, stackpos); + lua_pushnumber(L, lookDistance); + + scriptInterface.callVoidFunction(4); +} + +void Events::eventPlayerOnLookInBattleList(Player* player, Creature* creature, int32_t lookDistance) +{ + // Player:onLookInBattleList(creature, position, distance) or Player.onLookInBattleList(self, creature, position, distance) + if (info.playerOnLookInBattleList == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnLookInBattleList] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnLookInBattleList, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnLookInBattleList); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + lua_pushnumber(L, lookDistance); + + scriptInterface.callVoidFunction(3); +} + +void Events::eventPlayerOnLookInTrade(Player* player, Player* partner, Item* item, int32_t lookDistance) +{ + // Player:onLookInTrade(partner, item, distance) or Player.onLookInTrade(self, partner, item, distance) + if (info.playerOnLookInTrade == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnLookInTrade] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnLookInTrade, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnLookInTrade); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, partner); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, item); + LuaScriptInterface::setItemMetatable(L, -1, item); + + lua_pushnumber(L, lookDistance); + + scriptInterface.callVoidFunction(4); +} + +bool Events::eventPlayerOnLookInShop(Player* player, const ItemType* itemType, uint8_t count) +{ + // Player:onLookInShop(itemType, count) or Player.onLookInShop(self, itemType, count) + if (info.playerOnLookInShop == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnLookInShop] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnLookInShop, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnLookInShop); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, itemType); + LuaScriptInterface::setMetatable(L, -1, "ItemType"); + + lua_pushnumber(L, count); + + return scriptInterface.callFunction(3); +} + +bool Events::eventPlayerOnMoveItem(Player* player, Item* item, uint16_t count, const Position& fromPosition, const Position& toPosition, Cylinder* fromCylinder, Cylinder* toCylinder) +{ + // Player:onMoveItem(item, count, fromPosition, toPosition) or Player.onMoveItem(self, item, count, fromPosition, toPosition, fromCylinder, toCylinder) + if (info.playerOnMoveItem == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnMoveItem] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnMoveItem, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnMoveItem); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, item); + LuaScriptInterface::setItemMetatable(L, -1, item); + + lua_pushnumber(L, count); + LuaScriptInterface::pushPosition(L, fromPosition); + LuaScriptInterface::pushPosition(L, toPosition); + + LuaScriptInterface::pushCylinder(L, fromCylinder); + LuaScriptInterface::pushCylinder(L, toCylinder); + + return scriptInterface.callFunction(7); +} + +void Events::eventPlayerOnItemMoved(Player* player, Item* item, uint16_t count, const Position& fromPosition, const Position& toPosition, Cylinder* fromCylinder, Cylinder* toCylinder) +{ + // Player:onItemMoved(item, count, fromPosition, toPosition) or Player.onItemMoved(self, item, count, fromPosition, toPosition, fromCylinder, toCylinder) + if (info.playerOnItemMoved == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnItemMoved] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnItemMoved, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnItemMoved); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, item); + LuaScriptInterface::setItemMetatable(L, -1, item); + + lua_pushnumber(L, count); + LuaScriptInterface::pushPosition(L, fromPosition); + LuaScriptInterface::pushPosition(L, toPosition); + + LuaScriptInterface::pushCylinder(L, fromCylinder); + LuaScriptInterface::pushCylinder(L, toCylinder); + + scriptInterface.callVoidFunction(7); +} + +bool Events::eventPlayerOnMoveCreature(Player* player, Creature* creature, const Position& fromPosition, const Position& toPosition) +{ + // Player:onMoveCreature(creature, fromPosition, toPosition) or Player.onMoveCreature(self, creature, fromPosition, toPosition) + if (info.playerOnMoveCreature == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnMoveCreature] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnMoveCreature, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnMoveCreature); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + LuaScriptInterface::pushPosition(L, fromPosition); + LuaScriptInterface::pushPosition(L, toPosition); + + return scriptInterface.callFunction(4); +} + +void Events::eventPlayerOnReportRuleViolation(Player* player, const std::string& targetName, uint8_t reportType, uint8_t reportReason, const std::string& comment, const std::string& translation) +{ + // Player:onReportRuleViolation(targetName, reportType, reportReason, comment, translation) + if (info.playerOnReportRuleViolation == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnReportRuleViolation] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnReportRuleViolation, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnReportRuleViolation); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushString(L, targetName); + + lua_pushnumber(L, reportType); + lua_pushnumber(L, reportReason); + + LuaScriptInterface::pushString(L, comment); + LuaScriptInterface::pushString(L, translation); + + scriptInterface.callVoidFunction(6); +} + +bool Events::eventPlayerOnReportBug(Player* player, const std::string& message, const Position& position, uint8_t category) +{ + // Player:onReportBug(message, position, category) + if (info.playerOnReportBug == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnReportBug] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnReportBug, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnReportBug); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushString(L, message); + LuaScriptInterface::pushPosition(L, position); + lua_pushnumber(L, category); + + return scriptInterface.callFunction(4); +} + +bool Events::eventPlayerOnTurn(Player* player, Direction direction) +{ + // Player:onTurn(direction) or Player.onTurn(self, direction) + if (info.playerOnTurn == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnTurn] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnTurn, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnTurn); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + lua_pushnumber(L, direction); + + return scriptInterface.callFunction(2); +} + +bool Events::eventPlayerOnTradeRequest(Player* player, Player* target, Item* item) +{ + // Player:onTradeRequest(target, item) + if (info.playerOnTradeRequest == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnTradeRequest] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnTradeRequest, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnTradeRequest); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, target); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, item); + LuaScriptInterface::setItemMetatable(L, -1, item); + + return scriptInterface.callFunction(3); +} + +bool Events::eventPlayerOnTradeAccept(Player* player, Player* target, Item* item, Item* targetItem) +{ + // Player:onTradeAccept(target, item, targetItem) + if (info.playerOnTradeAccept == -1) { + return true; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnTradeAccept] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnTradeAccept, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnTradeAccept); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, target); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, item); + LuaScriptInterface::setItemMetatable(L, -1, item); + + LuaScriptInterface::pushUserdata(L, targetItem); + LuaScriptInterface::setItemMetatable(L, -1, targetItem); + + return scriptInterface.callFunction(4); +} + +void Events::eventPlayerOnTradeCompleted(Player* player, Player* target, Item* item, Item* targetItem, bool isSuccess) +{ + // Player:onTradeCompleted(target, item, targetItem, isSuccess) + if (info.playerOnTradeCompleted == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnTradeCompleted] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnTradeCompleted, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnTradeCompleted); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, target); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, item); + LuaScriptInterface::setItemMetatable(L, -1, item); + + LuaScriptInterface::pushUserdata(L, targetItem); + LuaScriptInterface::setItemMetatable(L, -1, targetItem); + + LuaScriptInterface::pushBoolean(L, isSuccess); + + return scriptInterface.callVoidFunction(5); +} + +void Events::eventPlayerOnGainExperience(Player* player, Creature* source, uint64_t& exp, uint64_t rawExp) +{ + // Player:onGainExperience(source, exp, rawExp) + // rawExp gives the original exp which is not multiplied + if (info.playerOnGainExperience == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnGainExperience] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnGainExperience, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnGainExperience); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + if (source) { + LuaScriptInterface::pushUserdata(L, source); + LuaScriptInterface::setCreatureMetatable(L, -1, source); + } else { + lua_pushnil(L); + } + + lua_pushnumber(L, exp); + lua_pushnumber(L, rawExp); + + if (scriptInterface.protectedCall(L, 4, 1) != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } else { + exp = LuaScriptInterface::getNumber(L, -1); + lua_pop(L, 1); + } + + scriptInterface.resetScriptEnv(); +} + +void Events::eventPlayerOnLoseExperience(Player* player, uint64_t& exp) +{ + // Player:onLoseExperience(exp) + if (info.playerOnLoseExperience == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnLoseExperience] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnLoseExperience, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnLoseExperience); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + lua_pushnumber(L, exp); + + if (scriptInterface.protectedCall(L, 2, 1) != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } else { + exp = LuaScriptInterface::getNumber(L, -1); + lua_pop(L, 1); + } + + scriptInterface.resetScriptEnv(); +} + +void Events::eventPlayerOnGainSkillTries(Player* player, skills_t skill, uint64_t& tries) +{ + // Player:onGainSkillTries(skill, tries) + if (info.playerOnGainSkillTries == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnGainSkillTries] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnGainSkillTries, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnGainSkillTries); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + lua_pushnumber(L, skill); + lua_pushnumber(L, tries); + + if (scriptInterface.protectedCall(L, 3, 1) != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(L)); + } else { + tries = LuaScriptInterface::getNumber(L, -1); + lua_pop(L, 1); + } + + scriptInterface.resetScriptEnv(); +} + +void Events::eventPlayerOnWrapItem(Player* player, Item* item) +{ + // Player:onWrapItem(item) + if (info.playerOnWrapItem == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventPlayerOnWrapItem] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.playerOnWrapItem, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.playerOnWrapItem); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushUserdata(L, item); + LuaScriptInterface::setItemMetatable(L, -1, item); + + scriptInterface.callVoidFunction(2); +} + +void Events::eventMonsterOnDropLoot(Monster* monster, Container* corpse) +{ + // Monster:onDropLoot(corpse) + if (info.monsterOnDropLoot == -1) { + return; + } + + if (!scriptInterface.reserveScriptEnv()) { + std::cout << "[Error - Events::eventMonsterOnDropLoot] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface.getScriptEnv(); + env->setScriptId(info.monsterOnDropLoot, &scriptInterface); + + lua_State* L = scriptInterface.getLuaState(); + scriptInterface.pushFunction(info.monsterOnDropLoot); + + LuaScriptInterface::pushUserdata(L, monster); + LuaScriptInterface::setMetatable(L, -1, "Monster"); + + LuaScriptInterface::pushUserdata(L, corpse); + LuaScriptInterface::setMetatable(L, -1, "Container"); + + return scriptInterface.callVoidFunction(2); +} + diff --git a/src/events.h b/src/events.h new file mode 100644 index 0000000..f6ca8d0 --- /dev/null +++ b/src/events.h @@ -0,0 +1,116 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_EVENTS_H_BD444CC0EE167E5777E4C90C766B36DC +#define FS_EVENTS_H_BD444CC0EE167E5777E4C90C766B36DC + +#include "luascript.h" +#include "const.h" + +class Party; +class ItemType; +class Tile; + +class Events +{ + struct EventsInfo { + // Creature + int32_t creatureOnChangeOutfit = -1; + int32_t creatureOnAreaCombat = -1; + int32_t creatureOnTargetCombat = -1; + int32_t creatureOnHear = -1; + + // Party + int32_t partyOnJoin = -1; + int32_t partyOnLeave = -1; + int32_t partyOnDisband = -1; + int32_t partyOnShareExperience = -1; + + // Player + int32_t playerOnBrowseField = -1; + int32_t playerOnLook = -1; + int32_t playerOnLookInBattleList = -1; + int32_t playerOnLookInTrade = -1; + int32_t playerOnLookInShop = -1; + int32_t playerOnMoveItem = -1; + int32_t playerOnItemMoved = -1; + int32_t playerOnMoveCreature = -1; + int32_t playerOnReportRuleViolation = -1; + int32_t playerOnReportBug = -1; + int32_t playerOnTurn = -1; + int32_t playerOnTradeRequest = -1; + int32_t playerOnTradeAccept = -1; + int32_t playerOnTradeCompleted = -1; + int32_t playerOnGainExperience = -1; + int32_t playerOnLoseExperience = -1; + int32_t playerOnGainSkillTries = -1; + int32_t playerOnWrapItem = -1; + + // Monster + int32_t monsterOnDropLoot = -1; + int32_t monsterOnSpawn = -1; + }; + + public: + Events(); + + bool load(); + + // Creature + bool eventCreatureOnChangeOutfit(Creature* creature, const Outfit_t& outfit); + ReturnValue eventCreatureOnAreaCombat(Creature* creature, Tile* tile, bool aggressive); + ReturnValue eventCreatureOnTargetCombat(Creature* creature, Creature* target); + void eventCreatureOnHear(Creature* creature, Creature* speaker, const std::string& words, SpeakClasses type); + + // Party + bool eventPartyOnJoin(Party* party, Player* player); + bool eventPartyOnLeave(Party* party, Player* player); + bool eventPartyOnDisband(Party* party); + void eventPartyOnShareExperience(Party* party, uint64_t& exp); + + // Player + bool eventPlayerOnBrowseField(Player* player, const Position& position); + void eventPlayerOnLook(Player* player, const Position& position, Thing* thing, uint8_t stackpos, int32_t lookDistance); + void eventPlayerOnLookInBattleList(Player* player, Creature* creature, int32_t lookDistance); + void eventPlayerOnLookInTrade(Player* player, Player* partner, Item* item, int32_t lookDistance); + bool eventPlayerOnLookInShop(Player* player, const ItemType* itemType, uint8_t count); + bool eventPlayerOnMoveItem(Player* player, Item* item, uint16_t count, const Position& fromPosition, const Position& toPosition, Cylinder* fromCylinder, Cylinder* toCylinder); + void eventPlayerOnItemMoved(Player* player, Item* item, uint16_t count, const Position& fromPosition, const Position& toPosition, Cylinder* fromCylinder, Cylinder* toCylinder); + bool eventPlayerOnMoveCreature(Player* player, Creature* creature, const Position& fromPosition, const Position& toPosition); + void eventPlayerOnReportRuleViolation(Player* player, const std::string& targetName, uint8_t reportType, uint8_t reportReason, const std::string& comment, const std::string& translation); + bool eventPlayerOnReportBug(Player* player, const std::string& message, const Position& position, uint8_t category); + bool eventPlayerOnTurn(Player* player, Direction direction); + bool eventPlayerOnTradeRequest(Player* player, Player* target, Item* item); + bool eventPlayerOnTradeAccept(Player* player, Player* target, Item* item, Item* targetItem); + void eventPlayerOnTradeCompleted(Player* player, Player* target, Item* item, Item* targetItem, bool isSuccess); + void eventPlayerOnGainExperience(Player* player, Creature* source, uint64_t& exp, uint64_t rawExp); + void eventPlayerOnLoseExperience(Player* player, uint64_t& exp); + void eventPlayerOnGainSkillTries(Player* player, skills_t skill, uint64_t& tries); + void eventPlayerOnWrapItem(Player* player, Item* item); + + // Monster + void eventMonsterOnDropLoot(Monster* monster, Container* corpse); + bool eventMonsterOnSpawn(Monster* monster, const Position& position, bool startup, bool artificial); + + private: + LuaScriptInterface scriptInterface; + EventsInfo info; +}; + +#endif diff --git a/src/fileloader.cpp b/src/fileloader.cpp new file mode 100644 index 0000000..5c754fd --- /dev/null +++ b/src/fileloader.cpp @@ -0,0 +1,124 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include +#include "fileloader.h" + + +namespace OTB { + +constexpr Identifier wildcard = {{'\0', '\0', '\0', '\0'}}; + +Loader::Loader(const std::string& fileName, const Identifier& acceptedIdentifier): + fileContents(fileName) +{ + constexpr auto minimalSize = sizeof(Identifier) + sizeof(Node::START) + sizeof(Node::type) + sizeof(Node::END); + if (fileContents.size() <= minimalSize) { + throw InvalidOTBFormat{}; + } + + Identifier fileIdentifier; + std::copy(fileContents.begin(), fileContents.begin() + fileIdentifier.size(), fileIdentifier.begin()); + if (fileIdentifier != acceptedIdentifier && fileIdentifier != wildcard) { + throw InvalidOTBFormat{}; + } +} + +using NodeStack = std::stack>; +static Node& getCurrentNode(const NodeStack& nodeStack) { + if (nodeStack.empty()) { + throw InvalidOTBFormat{}; + } + return *nodeStack.top(); +} + +const Node& Loader::parseTree() +{ + auto it = fileContents.begin() + sizeof(Identifier); + if (static_cast(*it) != Node::START) { + throw InvalidOTBFormat{}; + } + root.type = *(++it); + root.propsBegin = ++it; + NodeStack parseStack; + parseStack.push(&root); + + for (; it != fileContents.end(); ++it) { + switch(static_cast(*it)) { + case Node::START: { + auto& currentNode = getCurrentNode(parseStack); + if (currentNode.children.empty()) { + currentNode.propsEnd = it; + } + currentNode.children.emplace_back(); + auto& child = currentNode.children.back(); + if (++it == fileContents.end()) { + throw InvalidOTBFormat{}; + } + child.type = *it; + child.propsBegin = it + sizeof(Node::type); + parseStack.push(&child); + break; + } + case Node::END: { + auto& currentNode = getCurrentNode(parseStack); + if (currentNode.children.empty()) { + currentNode.propsEnd = it; + } + parseStack.pop(); + break; + } + case Node::ESCAPE: { + if (++it == fileContents.end()) { + throw InvalidOTBFormat{}; + } + break; + } + default: { + break; + } + } + } + if (!parseStack.empty()) { + throw InvalidOTBFormat{}; + } + + return root; +} + +bool Loader::getProps(const Node& node, PropStream& props) +{ + auto size = std::distance(node.propsBegin, node.propsEnd); + if (size == 0) { + return false; + } + propBuffer.resize(size); + bool lastEscaped = false; + + auto escapedPropEnd = std::copy_if(node.propsBegin, node.propsEnd, propBuffer.begin(), [&lastEscaped](const char& byte) { + lastEscaped = byte == static_cast(Node::ESCAPE) && !lastEscaped; + return !lastEscaped; + }); + props.init(&propBuffer[0], std::distance(propBuffer.begin(), escapedPropEnd)); + return true; +} + +} //namespace OTB diff --git a/src/fileloader.h b/src/fileloader.h new file mode 100644 index 0000000..c55f624 --- /dev/null +++ b/src/fileloader.h @@ -0,0 +1,167 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_FILELOADER_H_9B663D19E58D42E6BFACFE5B09D7A05E +#define FS_FILELOADER_H_9B663D19E58D42E6BFACFE5B09D7A05E + +#include +#include +#include + +class PropStream; + +namespace OTB { +using MappedFile = boost::iostreams::mapped_file_source; +using ContentIt = MappedFile::iterator; +using Identifier = std::array; + +struct Node +{ + using ChildrenVector = std::vector; + + ChildrenVector children; + ContentIt propsBegin; + ContentIt propsEnd; + uint8_t type; + enum NodeChar: uint8_t + { + ESCAPE = 0xFD, + START = 0xFE, + END = 0xFF, + }; +}; + +struct LoadError : std::exception { + const char* what() const noexcept override = 0; +}; + +struct InvalidOTBFormat final : LoadError { + const char* what() const noexcept override { + return "Invalid OTBM file format"; + } +}; + +class Loader { + MappedFile fileContents; + Node root; + std::vector propBuffer; +public: + Loader(const std::string& fileName, const Identifier& acceptedIdentifier); + bool getProps(const Node& node, PropStream& props); + const Node& parseTree(); +}; + +} //namespace OTB + +class PropStream +{ + public: + void init(const char* a, size_t size) { + p = a; + end = a + size; + } + + size_t size() const { + return end - p; + } + + template + bool read(T& ret) { + if (size() < sizeof(T)) { + return false; + } + + memcpy(&ret, p, sizeof(T)); + p += sizeof(T); + return true; + } + + bool readString(std::string& ret) { + uint16_t strLen; + if (!read(strLen)) { + return false; + } + + if (size() < strLen) { + return false; + } + + char* str = new char[strLen + 1]; + memcpy(str, p, strLen); + str[strLen] = 0; + ret.assign(str, strLen); + delete[] str; + p += strLen; + return true; + } + + bool skip(size_t n) { + if (size() < n) { + return false; + } + + p += n; + return true; + } + + private: + const char* p = nullptr; + const char* end = nullptr; +}; + +class PropWriteStream +{ + public: + PropWriteStream() = default; + + // non-copyable + PropWriteStream(const PropWriteStream&) = delete; + PropWriteStream& operator=(const PropWriteStream&) = delete; + + const char* getStream(size_t& size) const { + size = buffer.size(); + return buffer.data(); + } + + void clear() { + buffer.clear(); + } + + template + void write(T add) { + char* addr = reinterpret_cast(&add); + std::copy(addr, addr + sizeof(T), std::back_inserter(buffer)); + } + + void writeString(const std::string& str) { + size_t strLength = str.size(); + if (strLength > std::numeric_limits::max()) { + write(0); + return; + } + + write(static_cast(strLength)); + std::copy(str.begin(), str.end(), std::back_inserter(buffer)); + } + + private: + std::vector buffer; +}; + +#endif diff --git a/src/game.cpp b/src/game.cpp new file mode 100644 index 0000000..9be25fb --- /dev/null +++ b/src/game.cpp @@ -0,0 +1,5857 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "pugicast.h" + +#include "actions.h" +#include "bed.h" +#include "configmanager.h" +#include "creature.h" +#include "creatureevent.h" +#include "databasetasks.h" +#include "events.h" +#include "game.h" +#include "globalevent.h" +#include "iologindata.h" +#include "iomarket.h" +#include "items.h" +#include "monster.h" +#include "movement.h" +#include "scheduler.h" +#include "server.h" +#include "spells.h" +#include "talkaction.h" +#include "weapons.h" +#include "script.h" + +extern ConfigManager g_config; +extern Actions* g_actions; +extern Chat* g_chat; +extern TalkActions* g_talkActions; +extern Spells* g_spells; +extern Vocations g_vocations; +extern GlobalEvents* g_globalEvents; +extern CreatureEvents* g_creatureEvents; +extern Events* g_events; +extern Monsters g_monsters; +extern MoveEvents* g_moveEvents; +extern Weapons* g_weapons; +extern Scripts* g_scripts; + +Game::Game() +{ + offlineTrainingWindow.choices.emplace_back("Sword Fighting and Shielding", SKILL_SWORD); + offlineTrainingWindow.choices.emplace_back("Axe Fighting and Shielding", SKILL_AXE); + offlineTrainingWindow.choices.emplace_back("Club Fighting and Shielding", SKILL_CLUB); + offlineTrainingWindow.choices.emplace_back("Distance Fighting and Shielding", SKILL_DISTANCE); + offlineTrainingWindow.choices.emplace_back("Magic Level and Shielding", SKILL_MAGLEVEL); + offlineTrainingWindow.buttons.emplace_back("Okay", 1); + offlineTrainingWindow.buttons.emplace_back("Cancel", 0); + offlineTrainingWindow.defaultEnterButton = 1; + offlineTrainingWindow.defaultEscapeButton = 0; + offlineTrainingWindow.priority = true; +} + +Game::~Game() +{ + for (const auto& it : guilds) { + delete it.second; + } +} + +void Game::start(ServiceManager* manager) +{ + serviceManager = manager; + + g_scheduler.addEvent(createSchedulerTask(EVENT_LIGHTINTERVAL, std::bind(&Game::checkLight, this))); + g_scheduler.addEvent(createSchedulerTask(EVENT_CREATURE_THINK_INTERVAL, std::bind(&Game::checkCreatures, this, 0))); + g_scheduler.addEvent(createSchedulerTask(EVENT_DECAYINTERVAL, std::bind(&Game::checkDecay, this))); +} + +GameState_t Game::getGameState() const +{ + return gameState; +} + +void Game::setWorldType(WorldType_t type) +{ + worldType = type; +} + +void Game::setGameState(GameState_t newState) +{ + if (gameState == GAME_STATE_SHUTDOWN) { + return; //this cannot be stopped + } + + if (gameState == newState) { + return; + } + + gameState = newState; + switch (newState) { + case GAME_STATE_INIT: { + loadExperienceStages(); + + groups.load(); + g_chat->load(); + + map.spawns.startup(); + + raids.loadFromXml(); + raids.startup(); + + quests.loadFromXml(); + mounts.loadFromXml(); + + loadMotdNum(); + loadPlayersRecord(); + + g_globalEvents->startup(); + break; + } + + case GAME_STATE_SHUTDOWN: { + g_globalEvents->execute(GLOBALEVENT_SHUTDOWN); + + //kick all players that are still online + auto it = players.begin(); + while (it != players.end()) { + it->second->kickPlayer(true); + it = players.begin(); + } + + saveMotdNum(); + saveGameState(); + + g_dispatcher.addTask( + createTask(std::bind(&Game::shutdown, this))); + + g_scheduler.stop(); + g_databaseTasks.stop(); + g_dispatcher.stop(); + break; + } + + case GAME_STATE_CLOSED: { + /* kick all players without the CanAlwaysLogin flag */ + auto it = players.begin(); + while (it != players.end()) { + if (!it->second->hasFlag(PlayerFlag_CanAlwaysLogin)) { + it->second->kickPlayer(true); + it = players.begin(); + } else { + ++it; + } + } + + saveGameState(); + break; + } + + default: + break; + } +} + +void Game::saveGameState() +{ + if (gameState == GAME_STATE_NORMAL) { + setGameState(GAME_STATE_MAINTAIN); + } + + std::cout << "Saving server..." << std::endl; + + for (const auto& it : players) { + it.second->loginPosition = it.second->getPosition(); + IOLoginData::savePlayer(it.second); + } + + Map::save(); + + g_databaseTasks.flush(); + + if (gameState == GAME_STATE_MAINTAIN) { + setGameState(GAME_STATE_NORMAL); + } +} + +bool Game::loadMainMap(const std::string& filename) +{ + Monster::despawnRange = g_config.getNumber(ConfigManager::DEFAULT_DESPAWNRANGE); + Monster::despawnRadius = g_config.getNumber(ConfigManager::DEFAULT_DESPAWNRADIUS); + return map.loadMap("data/world/" + filename + ".otbm", true); +} + +void Game::loadMap(const std::string& path) +{ + map.loadMap(path, false); +} + +Cylinder* Game::internalGetCylinder(Player* player, const Position& pos) const +{ + if (pos.x != 0xFFFF) { + return map.getTile(pos); + } + + //container + if (pos.y & 0x40) { + uint8_t from_cid = pos.y & 0x0F; + return player->getContainerByID(from_cid); + } + + //inventory + return player; +} + +Thing* Game::internalGetThing(Player* player, const Position& pos, int32_t index, uint32_t spriteId, stackPosType_t type) const +{ + if (pos.x != 0xFFFF) { + Tile* tile = map.getTile(pos); + if (!tile) { + return nullptr; + } + + Thing* thing; + switch (type) { + case STACKPOS_LOOK: { + return tile->getTopVisibleThing(player); + } + + case STACKPOS_MOVE: { + Item* item = tile->getTopDownItem(); + if (item && item->isMoveable()) { + thing = item; + } else { + thing = tile->getTopVisibleCreature(player); + } + break; + } + + case STACKPOS_USEITEM: { + thing = tile->getUseItem(index); + break; + } + + case STACKPOS_TOPDOWN_ITEM: { + thing = tile->getTopDownItem(); + break; + } + + case STACKPOS_USETARGET: { + thing = tile->getTopVisibleCreature(player); + if (!thing) { + thing = tile->getUseItem(index); + } + break; + } + + default: { + thing = nullptr; + break; + } + } + + if (player && tile->hasFlag(TILESTATE_SUPPORTS_HANGABLE)) { + //do extra checks here if the thing is accessable + if (thing && thing->getItem()) { + if (tile->hasProperty(CONST_PROP_ISVERTICAL)) { + if (player->getPosition().x + 1 == tile->getPosition().x) { + thing = nullptr; + } + } else { // horizontal + if (player->getPosition().y + 1 == tile->getPosition().y) { + thing = nullptr; + } + } + } + } + return thing; + } + + //container + if (pos.y & 0x40) { + uint8_t fromCid = pos.y & 0x0F; + + Container* parentContainer = player->getContainerByID(fromCid); + if (!parentContainer) { + return nullptr; + } + + if (parentContainer->getID() == ITEM_BROWSEFIELD) { + Tile* tile = parentContainer->getTile(); + if (tile && tile->hasFlag(TILESTATE_SUPPORTS_HANGABLE)) { + if (tile->hasProperty(CONST_PROP_ISVERTICAL)) { + if (player->getPosition().x + 1 == tile->getPosition().x) { + return nullptr; + } + } else { // horizontal + if (player->getPosition().y + 1 == tile->getPosition().y) { + return nullptr; + } + } + } + } + + uint8_t slot = pos.z; + return parentContainer->getItemByIndex(player->getContainerIndex(fromCid) + slot); + } else if (pos.y == 0 && pos.z == 0) { + const ItemType& it = Item::items.getItemIdByClientId(spriteId); + if (it.id == 0) { + return nullptr; + } + + int32_t subType; + if (it.isFluidContainer() && index < static_cast(sizeof(reverseFluidMap) / sizeof(uint8_t))) { + subType = reverseFluidMap[index]; + } else { + subType = -1; + } + + return findItemOfType(player, it.id, true, subType); + } + + //inventory + slots_t slot = static_cast(pos.y); + return player->getInventoryItem(slot); +} + +void Game::internalGetPosition(Item* item, Position& pos, uint8_t& stackpos) +{ + pos.x = 0; + pos.y = 0; + pos.z = 0; + stackpos = 0; + + Cylinder* topParent = item->getTopParent(); + if (topParent) { + if (Player* player = dynamic_cast(topParent)) { + pos.x = 0xFFFF; + + Container* container = dynamic_cast(item->getParent()); + if (container) { + pos.y = static_cast(0x40) | static_cast(player->getContainerID(container)); + pos.z = container->getThingIndex(item); + stackpos = pos.z; + } else { + pos.y = player->getThingIndex(item); + stackpos = pos.y; + } + } else if (Tile* tile = topParent->getTile()) { + pos = tile->getPosition(); + stackpos = tile->getThingIndex(item); + } + } +} + +Creature* Game::getCreatureByID(uint32_t id) +{ + if (id <= Player::playerAutoID) { + return getPlayerByID(id); + } else if (id <= Monster::monsterAutoID) { + return getMonsterByID(id); + } else if (id <= Npc::npcAutoID) { + return getNpcByID(id); + } + return nullptr; +} + +Monster* Game::getMonsterByID(uint32_t id) +{ + if (id == 0) { + return nullptr; + } + + auto it = monsters.find(id); + if (it == monsters.end()) { + return nullptr; + } + return it->second; +} + +Npc* Game::getNpcByID(uint32_t id) +{ + if (id == 0) { + return nullptr; + } + + auto it = npcs.find(id); + if (it == npcs.end()) { + return nullptr; + } + return it->second; +} + +Player* Game::getPlayerByID(uint32_t id) +{ + if (id == 0) { + return nullptr; + } + + auto it = players.find(id); + if (it == players.end()) { + return nullptr; + } + return it->second; +} + +Creature* Game::getCreatureByName(const std::string& s) +{ + if (s.empty()) { + return nullptr; + } + + const std::string& lowerCaseName = asLowerCaseString(s); + + auto m_it = mappedPlayerNames.find(lowerCaseName); + if (m_it != mappedPlayerNames.end()) { + return m_it->second; + } + + for (const auto& it : npcs) { + if (lowerCaseName == asLowerCaseString(it.second->getName())) { + return it.second; + } + } + + for (const auto& it : monsters) { + if (lowerCaseName == asLowerCaseString(it.second->getName())) { + return it.second; + } + } + return nullptr; +} + +Npc* Game::getNpcByName(const std::string& s) +{ + if (s.empty()) { + return nullptr; + } + + const char* npcName = s.c_str(); + for (const auto& it : npcs) { + if (strcasecmp(npcName, it.second->getName().c_str()) == 0) { + return it.second; + } + } + return nullptr; +} + +Player* Game::getPlayerByName(const std::string& s) +{ + if (s.empty()) { + return nullptr; + } + + auto it = mappedPlayerNames.find(asLowerCaseString(s)); + if (it == mappedPlayerNames.end()) { + return nullptr; + } + return it->second; +} + +Player* Game::getPlayerByGUID(const uint32_t& guid) +{ + if (guid == 0) { + return nullptr; + } + + auto it = mappedPlayerGuids.find(guid); + if (it == mappedPlayerGuids.end()) { + return nullptr; + } + return it->second; +} + +ReturnValue Game::getPlayerByNameWildcard(const std::string& s, Player*& player) +{ + size_t strlen = s.length(); + if (strlen == 0 || strlen > 20) { + return RETURNVALUE_PLAYERWITHTHISNAMEISNOTONLINE; + } + + if (s.back() == '~') { + const std::string& query = asLowerCaseString(s.substr(0, strlen - 1)); + std::string result; + ReturnValue ret = wildcardTree.findOne(query, result); + if (ret != RETURNVALUE_NOERROR) { + return ret; + } + + player = getPlayerByName(result); + } else { + player = getPlayerByName(s); + } + + if (!player) { + return RETURNVALUE_PLAYERWITHTHISNAMEISNOTONLINE; + } + + return RETURNVALUE_NOERROR; +} + +Player* Game::getPlayerByAccount(uint32_t acc) +{ + for (const auto& it : players) { + if (it.second->getAccount() == acc) { + return it.second; + } + } + return nullptr; +} + +bool Game::internalPlaceCreature(Creature* creature, const Position& pos, bool extendedPos /*=false*/, bool forced /*= false*/) +{ + if (creature->getParent() != nullptr) { + return false; + } + + if (!map.placeCreature(pos, creature, extendedPos, forced)) { + return false; + } + + creature->incrementReferenceCounter(); + creature->setID(); + creature->addList(); + return true; +} + +bool Game::placeCreature(Creature* creature, const Position& pos, bool extendedPos /*=false*/, bool forced /*= false*/) +{ + if (!internalPlaceCreature(creature, pos, extendedPos, forced)) { + return false; + } + + SpectatorVec spectators; + map.getSpectators(spectators, creature->getPosition(), true); + for (Creature* spectator : spectators) { + if (Player* tmpPlayer = spectator->getPlayer()) { + tmpPlayer->sendCreatureAppear(creature, creature->getPosition(), true); + } + } + + for (Creature* spectator : spectators) { + spectator->onCreatureAppear(creature, true); + } + + creature->getParent()->postAddNotification(creature, nullptr, 0); + + addCreatureCheck(creature); + creature->onPlacedCreature(); + return true; +} + +bool Game::removeCreature(Creature* creature, bool isLogout/* = true*/) +{ + if (creature->isRemoved()) { + return false; + } + + Tile* tile = creature->getTile(); + + std::vector oldStackPosVector; + + SpectatorVec spectators; + map.getSpectators(spectators, tile->getPosition(), true); + for (Creature* spectator : spectators) { + if (Player* player = spectator->getPlayer()) { + oldStackPosVector.push_back(player->canSeeCreature(creature) ? tile->getStackposOfCreature(player, creature) : -1); + } + } + + tile->removeCreature(creature); + + const Position& tilePosition = tile->getPosition(); + + //send to client + size_t i = 0; + for (Creature* spectator : spectators) { + if (Player* player = spectator->getPlayer()) { + player->sendRemoveTileThing(tilePosition, oldStackPosVector[i++]); + } + } + + //event method + for (Creature* spectator : spectators) { + spectator->onRemoveCreature(creature, isLogout); + } + + creature->getParent()->postRemoveNotification(creature, nullptr, 0); + + creature->removeList(); + creature->setRemoved(); + ReleaseCreature(creature); + + removeCreatureCheck(creature); + + for (Creature* summon : creature->summons) { + summon->setSkillLoss(false); + removeCreature(summon); + } + return true; +} + +void Game::playerMoveThing(uint32_t playerId, const Position& fromPos, + uint16_t spriteId, uint8_t fromStackPos, const Position& toPos, uint8_t count) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + uint8_t fromIndex = 0; + if (fromPos.x == 0xFFFF) { + if (fromPos.y & 0x40) { + fromIndex = fromPos.z; + } else { + fromIndex = static_cast(fromPos.y); + } + } else { + fromIndex = fromStackPos; + } + + Thing* thing = internalGetThing(player, fromPos, fromIndex, 0, STACKPOS_MOVE); + if (!thing) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + if (Creature* movingCreature = thing->getCreature()) { + Tile* tile = map.getTile(toPos); + if (!tile) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + if (Position::areInRange<1, 1, 0>(movingCreature->getPosition(), player->getPosition())) { + SchedulerTask* task = createSchedulerTask(1000, + std::bind(&Game::playerMoveCreatureByID, this, player->getID(), + movingCreature->getID(), movingCreature->getPosition(), tile->getPosition())); + player->setNextActionTask(task); + } else { + playerMoveCreature(player, movingCreature, movingCreature->getPosition(), tile); + } + } else if (thing->getItem()) { + Cylinder* toCylinder = internalGetCylinder(player, toPos); + if (!toCylinder) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + playerMoveItem(player, fromPos, spriteId, fromStackPos, toPos, count, thing->getItem(), toCylinder); + } +} + +void Game::playerMoveCreatureByID(uint32_t playerId, uint32_t movingCreatureId, const Position& movingCreatureOrigPos, const Position& toPos) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Creature* movingCreature = getCreatureByID(movingCreatureId); + if (!movingCreature) { + return; + } + + Tile* toTile = map.getTile(toPos); + if (!toTile) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + playerMoveCreature(player, movingCreature, movingCreatureOrigPos, toTile); +} + +void Game::playerMoveCreature(Player* player, Creature* movingCreature, const Position& movingCreatureOrigPos, Tile* toTile) +{ + if (!player->canDoAction()) { + uint32_t delay = player->getNextActionTime(); + SchedulerTask* task = createSchedulerTask(delay, std::bind(&Game::playerMoveCreatureByID, + this, player->getID(), movingCreature->getID(), movingCreatureOrigPos, toTile->getPosition())); + player->setNextActionTask(task); + return; + } + + player->setNextActionTask(nullptr); + + if (!Position::areInRange<1, 1, 0>(movingCreatureOrigPos, player->getPosition())) { + //need to walk to the creature first before moving it + std::forward_list listDir; + if (player->getPathTo(movingCreatureOrigPos, listDir, 0, 1, true, true)) { + g_dispatcher.addTask(createTask(std::bind(&Game::playerAutoWalk, + this, player->getID(), listDir))); + SchedulerTask* task = createSchedulerTask(1500, std::bind(&Game::playerMoveCreatureByID, this, + player->getID(), movingCreature->getID(), movingCreatureOrigPos, toTile->getPosition())); + player->setNextWalkActionTask(task); + } else { + player->sendCancelMessage(RETURNVALUE_THEREISNOWAY); + } + return; + } + + if ((!movingCreature->isPushable() && !player->hasFlag(PlayerFlag_CanPushAllCreatures)) || + (movingCreature->isInGhostMode() && !player->isAccessPlayer())) { + player->sendCancelMessage(RETURNVALUE_NOTMOVEABLE); + return; + } + + //check throw distance + const Position& movingCreaturePos = movingCreature->getPosition(); + const Position& toPos = toTile->getPosition(); + if ((Position::getDistanceX(movingCreaturePos, toPos) > movingCreature->getThrowRange()) || (Position::getDistanceY(movingCreaturePos, toPos) > movingCreature->getThrowRange()) || (Position::getDistanceZ(movingCreaturePos, toPos) * 4 > movingCreature->getThrowRange())) { + player->sendCancelMessage(RETURNVALUE_DESTINATIONOUTOFREACH); + return; + } + + if (player != movingCreature) { + if (toTile->hasFlag(TILESTATE_BLOCKPATH)) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHROOM); + return; + } else if ((movingCreature->getZone() == ZONE_PROTECTION && !toTile->hasFlag(TILESTATE_PROTECTIONZONE)) || (movingCreature->getZone() == ZONE_NOPVP && !toTile->hasFlag(TILESTATE_NOPVPZONE))) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } else { + if (CreatureVector* tileCreatures = toTile->getCreatures()) { + for (Creature* tileCreature : *tileCreatures) { + if (!tileCreature->isInGhostMode()) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHROOM); + return; + } + } + } + + Npc* movingNpc = movingCreature->getNpc(); + if (movingNpc && !Spawns::isInZone(movingNpc->getMasterPos(), movingNpc->getMasterRadius(), toPos)) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHROOM); + return; + } + } + } + + if (!g_events->eventPlayerOnMoveCreature(player, movingCreature, movingCreaturePos, toPos)) { + return; + } + + ReturnValue ret = internalMoveCreature(*movingCreature, *toTile); + if (ret != RETURNVALUE_NOERROR) { + player->sendCancelMessage(ret); + } +} + +ReturnValue Game::internalMoveCreature(Creature* creature, Direction direction, uint32_t flags /*= 0*/) +{ + creature->setLastPosition(creature->getPosition()); + const Position& currentPos = creature->getPosition(); + Position destPos = getNextPosition(direction, currentPos); + Player* player = creature->getPlayer(); + + bool diagonalMovement = (direction & DIRECTION_DIAGONAL_MASK) != 0; + if (player && !diagonalMovement) { + //try go up + if (currentPos.z != 8 && creature->getTile()->hasHeight(3)) { + Tile* tmpTile = map.getTile(currentPos.x, currentPos.y, currentPos.getZ() - 1); + if (tmpTile == nullptr || (tmpTile->getGround() == nullptr && !tmpTile->hasFlag(TILESTATE_BLOCKSOLID))) { + tmpTile = map.getTile(destPos.x, destPos.y, destPos.getZ() - 1); + if (tmpTile && tmpTile->getGround() && !tmpTile->hasFlag(TILESTATE_BLOCKSOLID)) { + flags |= FLAG_IGNOREBLOCKITEM | FLAG_IGNOREBLOCKCREATURE; + + if (!tmpTile->hasFlag(TILESTATE_FLOORCHANGE)) { + player->setDirection(direction); + destPos.z--; + } + } + } + } + + //try go down + if (currentPos.z != 7 && currentPos.z == destPos.z) { + Tile* tmpTile = map.getTile(destPos.x, destPos.y, destPos.z); + if (tmpTile == nullptr || (tmpTile->getGround() == nullptr && !tmpTile->hasFlag(TILESTATE_BLOCKSOLID))) { + tmpTile = map.getTile(destPos.x, destPos.y, destPos.z + 1); + if (tmpTile && tmpTile->hasHeight(3)) { + flags |= FLAG_IGNOREBLOCKITEM | FLAG_IGNOREBLOCKCREATURE; + player->setDirection(direction); + destPos.z++; + } + } + } + } + + Tile* toTile = map.getTile(destPos); + if (!toTile) { + return RETURNVALUE_NOTPOSSIBLE; + } + return internalMoveCreature(*creature, *toTile, flags); +} + +ReturnValue Game::internalMoveCreature(Creature& creature, Tile& toTile, uint32_t flags /*= 0*/) +{ + //check if we can move the creature to the destination + ReturnValue ret = toTile.queryAdd(0, creature, 1, flags); + if (ret != RETURNVALUE_NOERROR) { + return ret; + } + + map.moveCreature(creature, toTile); + if (creature.getParent() != &toTile) { + return RETURNVALUE_NOERROR; + } + + int32_t index = 0; + Item* toItem = nullptr; + Tile* subCylinder = nullptr; + Tile* toCylinder = &toTile; + Tile* fromCylinder = nullptr; + uint32_t n = 0; + + while ((subCylinder = toCylinder->queryDestination(index, creature, &toItem, flags)) != toCylinder) { + map.moveCreature(creature, *subCylinder); + + if (creature.getParent() != subCylinder) { + //could happen if a script move the creature + fromCylinder = nullptr; + break; + } + + fromCylinder = toCylinder; + toCylinder = subCylinder; + flags = 0; + + //to prevent infinite loop + if (++n >= MAP_MAX_LAYERS) { + break; + } + } + + if (fromCylinder) { + const Position& fromPosition = fromCylinder->getPosition(); + const Position& toPosition = toCylinder->getPosition(); + if (fromPosition.z != toPosition.z && (fromPosition.x != toPosition.x || fromPosition.y != toPosition.y)) { + Direction dir = getDirectionTo(fromPosition, toPosition); + if ((dir & DIRECTION_DIAGONAL_MASK) == 0) { + internalCreatureTurn(&creature, dir); + } + } + } + + return RETURNVALUE_NOERROR; +} + +void Game::playerMoveItemByPlayerID(uint32_t playerId, const Position& fromPos, uint16_t spriteId, uint8_t fromStackPos, const Position& toPos, uint8_t count) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + playerMoveItem(player, fromPos, spriteId, fromStackPos, toPos, count, nullptr, nullptr); +} + +void Game::playerMoveItem(Player* player, const Position& fromPos, + uint16_t spriteId, uint8_t fromStackPos, const Position& toPos, uint8_t count, Item* item, Cylinder* toCylinder) +{ + if (!player->canDoAction()) { + uint32_t delay = player->getNextActionTime(); + SchedulerTask* task = createSchedulerTask(delay, std::bind(&Game::playerMoveItemByPlayerID, this, + player->getID(), fromPos, spriteId, fromStackPos, toPos, count)); + player->setNextActionTask(task); + return; + } + + player->setNextActionTask(nullptr); + + if (item == nullptr) { + uint8_t fromIndex = 0; + if (fromPos.x == 0xFFFF) { + if (fromPos.y & 0x40) { + fromIndex = fromPos.z; + } else { + fromIndex = static_cast(fromPos.y); + } + } else { + fromIndex = fromStackPos; + } + + Thing* thing = internalGetThing(player, fromPos, fromIndex, 0, STACKPOS_MOVE); + if (!thing || !thing->getItem()) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + item = thing->getItem(); + } + + if (item->getClientID() != spriteId) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + Cylinder* fromCylinder = internalGetCylinder(player, fromPos); + if (fromCylinder == nullptr) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + if (toCylinder == nullptr) { + toCylinder = internalGetCylinder(player, toPos); + if (toCylinder == nullptr) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + } + + if (!item->isPushable() || item->hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)) { + player->sendCancelMessage(RETURNVALUE_NOTMOVEABLE); + return; + } + + const Position& playerPos = player->getPosition(); + const Position& mapFromPos = fromCylinder->getTile()->getPosition(); + if (playerPos.z != mapFromPos.z) { + player->sendCancelMessage(playerPos.z > mapFromPos.z ? RETURNVALUE_FIRSTGOUPSTAIRS : RETURNVALUE_FIRSTGODOWNSTAIRS); + return; + } + + if (!Position::areInRange<1, 1>(playerPos, mapFromPos)) { + //need to walk to the item first before using it + std::forward_list listDir; + if (player->getPathTo(item->getPosition(), listDir, 0, 1, true, true)) { + g_dispatcher.addTask(createTask(std::bind(&Game::playerAutoWalk, + this, player->getID(), listDir))); + + SchedulerTask* task = createSchedulerTask(400, std::bind(&Game::playerMoveItemByPlayerID, this, + player->getID(), fromPos, spriteId, fromStackPos, toPos, count)); + player->setNextWalkActionTask(task); + } else { + player->sendCancelMessage(RETURNVALUE_THEREISNOWAY); + } + return; + } + + const Tile* toCylinderTile = toCylinder->getTile(); + const Position& mapToPos = toCylinderTile->getPosition(); + + //hangable item specific code + if (item->isHangable() && toCylinderTile->hasFlag(TILESTATE_SUPPORTS_HANGABLE)) { + //destination supports hangable objects so need to move there first + bool vertical = toCylinderTile->hasProperty(CONST_PROP_ISVERTICAL); + if (vertical) { + if (playerPos.x + 1 == mapToPos.x) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + } else { // horizontal + if (playerPos.y + 1 == mapToPos.y) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + } + + if (!Position::areInRange<1, 1, 0>(playerPos, mapToPos)) { + Position walkPos = mapToPos; + if (vertical) { + walkPos.x++; + } else { + walkPos.y++; + } + + Position itemPos = fromPos; + uint8_t itemStackPos = fromStackPos; + + if (fromPos.x != 0xFFFF && Position::areInRange<1, 1>(mapFromPos, playerPos) + && !Position::areInRange<1, 1, 0>(mapFromPos, walkPos)) { + //need to pickup the item first + Item* moveItem = nullptr; + + ReturnValue ret = internalMoveItem(fromCylinder, player, INDEX_WHEREEVER, item, count, &moveItem, 0, player, nullptr, &fromPos, &toPos); + if (ret != RETURNVALUE_NOERROR) { + player->sendCancelMessage(ret); + return; + } + + //changing the position since its now in the inventory of the player + internalGetPosition(moveItem, itemPos, itemStackPos); + } + + std::forward_list listDir; + if (player->getPathTo(walkPos, listDir, 0, 0, true, true)) { + g_dispatcher.addTask(createTask(std::bind(&Game::playerAutoWalk, + this, player->getID(), listDir))); + + SchedulerTask* task = createSchedulerTask(400, std::bind(&Game::playerMoveItemByPlayerID, this, + player->getID(), itemPos, spriteId, itemStackPos, toPos, count)); + player->setNextWalkActionTask(task); + } else { + player->sendCancelMessage(RETURNVALUE_THEREISNOWAY); + } + return; + } + } + + if ((Position::getDistanceX(playerPos, mapToPos) > item->getThrowRange()) || + (Position::getDistanceY(playerPos, mapToPos) > item->getThrowRange()) || + (Position::getDistanceZ(mapFromPos, mapToPos) * 4 > item->getThrowRange())) { + player->sendCancelMessage(RETURNVALUE_DESTINATIONOUTOFREACH); + return; + } + + if (!canThrowObjectTo(mapFromPos, mapToPos)) { + player->sendCancelMessage(RETURNVALUE_CANNOTTHROW); + return; + } + + uint8_t toIndex = 0; + if (toPos.x == 0xFFFF) { + if (toPos.y & 0x40) { + toIndex = toPos.z; + } else { + toIndex = static_cast(toPos.y); + } + } + + ReturnValue ret = internalMoveItem(fromCylinder, toCylinder, toIndex, item, count, nullptr, 0, player, nullptr, &fromPos, &toPos); + if (ret != RETURNVALUE_NOERROR) { + player->sendCancelMessage(ret); + } +} + +ReturnValue Game::internalMoveItem(Cylinder* fromCylinder, Cylinder* toCylinder, int32_t index, + Item* item, uint32_t count, Item** _moveItem, uint32_t flags /*= 0*/, Creature* actor/* = nullptr*/, Item* tradeItem/* = nullptr*/, const Position* fromPos /*= nullptr*/, const Position* toPos/*= nullptr*/) +{ + Player* actorPlayer = actor ? actor->getPlayer() : nullptr; + if (actorPlayer && fromPos && toPos) { + if (!g_events->eventPlayerOnMoveItem(actorPlayer, item, count, *fromPos, *toPos, fromCylinder, toCylinder)) { + return RETURNVALUE_NOTPOSSIBLE; + } + } + + Tile* fromTile = fromCylinder->getTile(); + if (fromTile) { + auto it = browseFields.find(fromTile); + if (it != browseFields.end() && it->second == fromCylinder) { + fromCylinder = fromTile; + } + } + + Item* toItem = nullptr; + + Cylinder* subCylinder; + int floorN = 0; + + while ((subCylinder = toCylinder->queryDestination(index, *item, &toItem, flags)) != toCylinder) { + toCylinder = subCylinder; + flags = 0; + + //to prevent infinite loop + if (++floorN >= MAP_MAX_LAYERS) { + break; + } + } + + //destination is the same as the source? + if (item == toItem) { + return RETURNVALUE_NOERROR; //silently ignore move + } + + //check if we can add this item + ReturnValue ret = toCylinder->queryAdd(index, *item, count, flags, actor); + if (ret == RETURNVALUE_NEEDEXCHANGE) { + //check if we can add it to source cylinder + ret = fromCylinder->queryAdd(fromCylinder->getThingIndex(item), *toItem, toItem->getItemCount(), 0); + if (ret == RETURNVALUE_NOERROR) { + //check how much we can move + uint32_t maxExchangeQueryCount = 0; + ReturnValue retExchangeMaxCount = fromCylinder->queryMaxCount(INDEX_WHEREEVER, *toItem, toItem->getItemCount(), maxExchangeQueryCount, 0); + + if (retExchangeMaxCount != RETURNVALUE_NOERROR && maxExchangeQueryCount == 0) { + return retExchangeMaxCount; + } + + if (toCylinder->queryRemove(*toItem, toItem->getItemCount(), flags) == RETURNVALUE_NOERROR) { + int32_t oldToItemIndex = toCylinder->getThingIndex(toItem); + toCylinder->removeThing(toItem, toItem->getItemCount()); + fromCylinder->addThing(toItem); + + if (oldToItemIndex != -1) { + toCylinder->postRemoveNotification(toItem, fromCylinder, oldToItemIndex); + } + + int32_t newToItemIndex = fromCylinder->getThingIndex(toItem); + if (newToItemIndex != -1) { + fromCylinder->postAddNotification(toItem, toCylinder, newToItemIndex); + } + + ret = toCylinder->queryAdd(index, *item, count, flags); + toItem = nullptr; + } + } + } + + if (ret != RETURNVALUE_NOERROR) { + return ret; + } + + //check how much we can move + uint32_t maxQueryCount = 0; + ReturnValue retMaxCount = toCylinder->queryMaxCount(index, *item, count, maxQueryCount, flags); + if (retMaxCount != RETURNVALUE_NOERROR && maxQueryCount == 0) { + return retMaxCount; + } + + uint32_t m; + if (item->isStackable()) { + m = std::min(count, maxQueryCount); + } else { + m = maxQueryCount; + } + + Item* moveItem = item; + + //check if we can remove this item + ret = fromCylinder->queryRemove(*item, m, flags); + if (ret != RETURNVALUE_NOERROR) { + return ret; + } + + if (tradeItem) { + if (toCylinder->getItem() == tradeItem) { + return RETURNVALUE_NOTENOUGHROOM; + } + + Cylinder* tmpCylinder = toCylinder->getParent(); + while (tmpCylinder) { + if (tmpCylinder->getItem() == tradeItem) { + return RETURNVALUE_NOTENOUGHROOM; + } + + tmpCylinder = tmpCylinder->getParent(); + } + } + + //remove the item + int32_t itemIndex = fromCylinder->getThingIndex(item); + Item* updateItem = nullptr; + fromCylinder->removeThing(item, m); + + //update item(s) + if (item->isStackable()) { + uint32_t n; + + if (item->equals(toItem)) { + n = std::min(100 - toItem->getItemCount(), m); + toCylinder->updateThing(toItem, toItem->getID(), toItem->getItemCount() + n); + updateItem = toItem; + } else { + n = 0; + } + + int32_t newCount = m - n; + if (newCount > 0) { + moveItem = item->clone(); + moveItem->setItemCount(newCount); + } else { + moveItem = nullptr; + } + + if (item->isRemoved()) { + ReleaseItem(item); + } + } + + //add item + if (moveItem /*m - n > 0*/) { + toCylinder->addThing(index, moveItem); + } + + if (itemIndex != -1) { + fromCylinder->postRemoveNotification(item, toCylinder, itemIndex); + } + + if (moveItem) { + int32_t moveItemIndex = toCylinder->getThingIndex(moveItem); + if (moveItemIndex != -1) { + toCylinder->postAddNotification(moveItem, fromCylinder, moveItemIndex); + } + } + + if (updateItem) { + int32_t updateItemIndex = toCylinder->getThingIndex(updateItem); + if (updateItemIndex != -1) { + toCylinder->postAddNotification(updateItem, fromCylinder, updateItemIndex); + } + } + + if (_moveItem) { + if (moveItem) { + *_moveItem = moveItem; + } else { + *_moveItem = item; + } + } + + //we could not move all, inform the player + if (item->isStackable() && maxQueryCount < count) { + return retMaxCount; + } + + if (moveItem && moveItem->getDuration() > 0) { + if (moveItem->getDecaying() != DECAYING_TRUE) { + moveItem->incrementReferenceCounter(); + moveItem->setDecaying(DECAYING_TRUE); + toDecayItems.push_front(moveItem); + } + } + + if (actorPlayer && fromPos && toPos) { + g_events->eventPlayerOnItemMoved(actorPlayer, item, count, *fromPos, *toPos, fromCylinder, toCylinder); + } + + return ret; +} + +ReturnValue Game::internalAddItem(Cylinder* toCylinder, Item* item, int32_t index /*= INDEX_WHEREEVER*/, + uint32_t flags/* = 0*/, bool test/* = false*/) +{ + uint32_t remainderCount = 0; + return internalAddItem(toCylinder, item, index, flags, test, remainderCount); +} + +ReturnValue Game::internalAddItem(Cylinder* toCylinder, Item* item, int32_t index, + uint32_t flags, bool test, uint32_t& remainderCount) +{ + if (toCylinder == nullptr || item == nullptr) { + return RETURNVALUE_NOTPOSSIBLE; + } + + Cylinder* destCylinder = toCylinder; + Item* toItem = nullptr; + toCylinder = toCylinder->queryDestination(index, *item, &toItem, flags); + + //check if we can add this item + ReturnValue ret = toCylinder->queryAdd(index, *item, item->getItemCount(), flags); + if (ret != RETURNVALUE_NOERROR) { + return ret; + } + + /* + Check if we can move add the whole amount, we do this by checking against the original cylinder, + since the queryDestination can return a cylinder that might only hold a part of the full amount. + */ + uint32_t maxQueryCount = 0; + ret = destCylinder->queryMaxCount(INDEX_WHEREEVER, *item, item->getItemCount(), maxQueryCount, flags); + + if (ret != RETURNVALUE_NOERROR) { + return ret; + } + + if (test) { + return RETURNVALUE_NOERROR; + } + + if (item->isStackable() && item->equals(toItem)) { + uint32_t m = std::min(item->getItemCount(), maxQueryCount); + uint32_t n = std::min(100 - toItem->getItemCount(), m); + + toCylinder->updateThing(toItem, toItem->getID(), toItem->getItemCount() + n); + + int32_t count = m - n; + if (count > 0) { + if (item->getItemCount() != count) { + Item* remainderItem = item->clone(); + remainderItem->setItemCount(count); + if (internalAddItem(destCylinder, remainderItem, INDEX_WHEREEVER, flags, false) != RETURNVALUE_NOERROR) { + ReleaseItem(remainderItem); + remainderCount = count; + } + } else { + toCylinder->addThing(index, item); + + int32_t itemIndex = toCylinder->getThingIndex(item); + if (itemIndex != -1) { + toCylinder->postAddNotification(item, nullptr, itemIndex); + } + } + } else { + //fully merged with toItem, item will be destroyed + item->onRemoved(); + ReleaseItem(item); + + int32_t itemIndex = toCylinder->getThingIndex(toItem); + if (itemIndex != -1) { + toCylinder->postAddNotification(toItem, nullptr, itemIndex); + } + } + } else { + toCylinder->addThing(index, item); + + int32_t itemIndex = toCylinder->getThingIndex(item); + if (itemIndex != -1) { + toCylinder->postAddNotification(item, nullptr, itemIndex); + } + } + + if (item->getDuration() > 0) { + item->incrementReferenceCounter(); + item->setDecaying(DECAYING_TRUE); + toDecayItems.push_front(item); + } + + return RETURNVALUE_NOERROR; +} + +ReturnValue Game::internalRemoveItem(Item* item, int32_t count /*= -1*/, bool test /*= false*/, uint32_t flags /*= 0*/) +{ + Cylinder* cylinder = item->getParent(); + if (cylinder == nullptr) { + return RETURNVALUE_NOTPOSSIBLE; + } + + Tile* fromTile = cylinder->getTile(); + if (fromTile) { + auto it = browseFields.find(fromTile); + if (it != browseFields.end() && it->second == cylinder) { + cylinder = fromTile; + } + } + + if (count == -1) { + count = item->getItemCount(); + } + + //check if we can remove this item + ReturnValue ret = cylinder->queryRemove(*item, count, flags | FLAG_IGNORENOTMOVEABLE); + if (ret != RETURNVALUE_NOERROR) { + return ret; + } + + if (!item->canRemove()) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (!test) { + int32_t index = cylinder->getThingIndex(item); + + //remove the item + cylinder->removeThing(item, count); + + if (item->isRemoved()) { + item->onRemoved(); + if (item->canDecay()) { + decayItems->remove(item); + } + ReleaseItem(item); + } + + cylinder->postRemoveNotification(item, nullptr, index); + } + + return RETURNVALUE_NOERROR; +} + +ReturnValue Game::internalPlayerAddItem(Player* player, Item* item, bool dropOnMap /*= true*/, slots_t slot /*= CONST_SLOT_WHEREEVER*/) +{ + uint32_t remainderCount = 0; + ReturnValue ret = internalAddItem(player, item, static_cast(slot), 0, false, remainderCount); + if (remainderCount != 0) { + Item* remainderItem = Item::CreateItem(item->getID(), remainderCount); + ReturnValue remaindRet = internalAddItem(player->getTile(), remainderItem, INDEX_WHEREEVER, FLAG_NOLIMIT); + if (remaindRet != RETURNVALUE_NOERROR) { + ReleaseItem(remainderItem); + } + } + + if (ret != RETURNVALUE_NOERROR && dropOnMap) { + ret = internalAddItem(player->getTile(), item, INDEX_WHEREEVER, FLAG_NOLIMIT); + } + + return ret; +} + +Item* Game::findItemOfType(Cylinder* cylinder, uint16_t itemId, + bool depthSearch /*= true*/, int32_t subType /*= -1*/) const +{ + if (cylinder == nullptr) { + return nullptr; + } + + std::vector containers; + for (size_t i = cylinder->getFirstIndex(), j = cylinder->getLastIndex(); i < j; ++i) { + Thing* thing = cylinder->getThing(i); + if (!thing) { + continue; + } + + Item* item = thing->getItem(); + if (!item) { + continue; + } + + if (item->getID() == itemId && (subType == -1 || subType == item->getSubType())) { + return item; + } + + if (depthSearch) { + Container* container = item->getContainer(); + if (container) { + containers.push_back(container); + } + } + } + + size_t i = 0; + while (i < containers.size()) { + Container* container = containers[i++]; + for (Item* item : container->getItemList()) { + if (item->getID() == itemId && (subType == -1 || subType == item->getSubType())) { + return item; + } + + Container* subContainer = item->getContainer(); + if (subContainer) { + containers.push_back(subContainer); + } + } + } + return nullptr; +} + +bool Game::removeMoney(Cylinder* cylinder, uint64_t money, uint32_t flags /*= 0*/) +{ + if (cylinder == nullptr) { + return false; + } + + if (money == 0) { + return true; + } + + std::vector containers; + + std::multimap moneyMap; + uint64_t moneyCount = 0; + + for (size_t i = cylinder->getFirstIndex(), j = cylinder->getLastIndex(); i < j; ++i) { + Thing* thing = cylinder->getThing(i); + if (!thing) { + continue; + } + + Item* item = thing->getItem(); + if (!item) { + continue; + } + + Container* container = item->getContainer(); + if (container) { + containers.push_back(container); + } else { + const uint32_t worth = item->getWorth(); + if (worth != 0) { + moneyCount += worth; + moneyMap.emplace(worth, item); + } + } + } + + size_t i = 0; + while (i < containers.size()) { + Container* container = containers[i++]; + for (Item* item : container->getItemList()) { + Container* tmpContainer = item->getContainer(); + if (tmpContainer) { + containers.push_back(tmpContainer); + } else { + const uint32_t worth = item->getWorth(); + if (worth != 0) { + moneyCount += worth; + moneyMap.emplace(worth, item); + } + } + } + } + + if (moneyCount < money) { + return false; + } + + for (const auto& moneyEntry : moneyMap) { + Item* item = moneyEntry.second; + if (moneyEntry.first < money) { + internalRemoveItem(item); + money -= moneyEntry.first; + } else if (moneyEntry.first > money) { + const uint32_t worth = moneyEntry.first / item->getItemCount(); + const uint32_t removeCount = std::ceil(money / static_cast(worth)); + + addMoney(cylinder, (worth * removeCount) - money, flags); + internalRemoveItem(item, removeCount); + break; + } else { + internalRemoveItem(item); + break; + } + } + return true; +} + +void Game::addMoney(Cylinder* cylinder, uint64_t money, uint32_t flags /*= 0*/) +{ + if (money == 0) { + return; + } + + uint32_t crystalCoins = money / 10000; + money -= crystalCoins * 10000; + while (crystalCoins > 0) { + const uint16_t count = std::min(100, crystalCoins); + + Item* remaindItem = Item::CreateItem(ITEM_CRYSTAL_COIN, count); + + ReturnValue ret = internalAddItem(cylinder, remaindItem, INDEX_WHEREEVER, flags); + if (ret != RETURNVALUE_NOERROR) { + internalAddItem(cylinder->getTile(), remaindItem, INDEX_WHEREEVER, FLAG_NOLIMIT); + } + + crystalCoins -= count; + } + + uint16_t platinumCoins = money / 100; + if (platinumCoins != 0) { + Item* remaindItem = Item::CreateItem(ITEM_PLATINUM_COIN, platinumCoins); + + ReturnValue ret = internalAddItem(cylinder, remaindItem, INDEX_WHEREEVER, flags); + if (ret != RETURNVALUE_NOERROR) { + internalAddItem(cylinder->getTile(), remaindItem, INDEX_WHEREEVER, FLAG_NOLIMIT); + } + + money -= platinumCoins * 100; + } + + if (money != 0) { + Item* remaindItem = Item::CreateItem(ITEM_GOLD_COIN, money); + + ReturnValue ret = internalAddItem(cylinder, remaindItem, INDEX_WHEREEVER, flags); + if (ret != RETURNVALUE_NOERROR) { + internalAddItem(cylinder->getTile(), remaindItem, INDEX_WHEREEVER, FLAG_NOLIMIT); + } + } +} + +Item* Game::transformItem(Item* item, uint16_t newId, int32_t newCount /*= -1*/) +{ + if (item->getID() == newId && (newCount == -1 || (newCount == item->getSubType() && newCount != 0))) { //chargeless item placed on map = infinite + return item; + } + + Cylinder* cylinder = item->getParent(); + if (cylinder == nullptr) { + return nullptr; + } + + Tile* fromTile = cylinder->getTile(); + if (fromTile) { + auto it = browseFields.find(fromTile); + if (it != browseFields.end() && it->second == cylinder) { + cylinder = fromTile; + } + } + + int32_t itemIndex = cylinder->getThingIndex(item); + if (itemIndex == -1) { + return item; + } + + if (!item->canTransform()) { + return item; + } + + const ItemType& newType = Item::items[newId]; + if (newType.id == 0) { + return item; + } + + const ItemType& curType = Item::items[item->getID()]; + if (curType.alwaysOnTop != newType.alwaysOnTop) { + //This only occurs when you transform items on tiles from a downItem to a topItem (or vice versa) + //Remove the old, and add the new + cylinder->removeThing(item, item->getItemCount()); + cylinder->postRemoveNotification(item, cylinder, itemIndex); + + item->setID(newId); + if (newCount != -1) { + item->setSubType(newCount); + } + cylinder->addThing(item); + + Cylinder* newParent = item->getParent(); + if (newParent == nullptr) { + ReleaseItem(item); + return nullptr; + } + + newParent->postAddNotification(item, cylinder, newParent->getThingIndex(item)); + return item; + } + + if (curType.type == newType.type) { + //Both items has the same type so we can safely change id/subtype + if (newCount == 0 && (item->isStackable() || item->hasAttribute(ITEM_ATTRIBUTE_CHARGES))) { + if (item->isStackable()) { + internalRemoveItem(item); + return nullptr; + } else { + int32_t newItemId = newId; + if (curType.id == newType.id) { + newItemId = item->getDecayTo(); + } + + if (newItemId < 0) { + internalRemoveItem(item); + return nullptr; + } else if (newItemId != newId) { + //Replacing the the old item with the new while maintaining the old position + Item* newItem = Item::CreateItem(newItemId, 1); + if (newItem == nullptr) { + return nullptr; + } + + cylinder->replaceThing(itemIndex, newItem); + cylinder->postAddNotification(newItem, cylinder, itemIndex); + + item->setParent(nullptr); + cylinder->postRemoveNotification(item, cylinder, itemIndex); + ReleaseItem(item); + return newItem; + } else { + return transformItem(item, newItemId); + } + } + } else { + cylinder->postRemoveNotification(item, cylinder, itemIndex); + uint16_t itemId = item->getID(); + int32_t count = item->getSubType(); + + if (curType.id != newType.id) { + if (newType.group != curType.group) { + item->setDefaultSubtype(); + } + + itemId = newId; + } + + if (newCount != -1 && newType.hasSubType()) { + count = newCount; + } + + cylinder->updateThing(item, itemId, count); + cylinder->postAddNotification(item, cylinder, itemIndex); + return item; + } + } + + //Replacing the the old item with the new while maintaining the old position + Item* newItem; + if (newCount == -1) { + newItem = Item::CreateItem(newId); + } else { + newItem = Item::CreateItem(newId, newCount); + } + + if (newItem == nullptr) { + return nullptr; + } + + cylinder->replaceThing(itemIndex, newItem); + cylinder->postAddNotification(newItem, cylinder, itemIndex); + + item->setParent(nullptr); + cylinder->postRemoveNotification(item, cylinder, itemIndex); + ReleaseItem(item); + + if (newItem->getDuration() > 0) { + if (newItem->getDecaying() != DECAYING_TRUE) { + newItem->incrementReferenceCounter(); + newItem->setDecaying(DECAYING_TRUE); + toDecayItems.push_front(newItem); + } + } + + return newItem; +} + +ReturnValue Game::internalTeleport(Thing* thing, const Position& newPos, bool pushMove/* = true*/, uint32_t flags /*= 0*/) +{ + if (newPos == thing->getPosition()) { + return RETURNVALUE_NOERROR; + } else if (thing->isRemoved()) { + return RETURNVALUE_NOTPOSSIBLE; + } + + Tile* toTile = map.getTile(newPos); + if (!toTile) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (Creature* creature = thing->getCreature()) { + ReturnValue ret = toTile->queryAdd(0, *creature, 1, FLAG_NOLIMIT); + if (ret != RETURNVALUE_NOERROR) { + return ret; + } + + map.moveCreature(*creature, *toTile, !pushMove); + return RETURNVALUE_NOERROR; + } else if (Item* item = thing->getItem()) { + return internalMoveItem(item->getParent(), toTile, INDEX_WHEREEVER, item, item->getItemCount(), nullptr, flags); + } + return RETURNVALUE_NOTPOSSIBLE; +} + +Item* searchForItem(Container* container, uint16_t itemId) +{ + for (ContainerIterator it = container->iterator(); it.hasNext(); it.advance()) { + if ((*it)->getID() == itemId) { + return *it; + } + } + + return nullptr; +} + +slots_t getSlotType(const ItemType& it) +{ + slots_t slot = CONST_SLOT_RIGHT; + if (it.weaponType != WeaponType_t::WEAPON_SHIELD) { + int32_t slotPosition = it.slotPosition; + + if (slotPosition & SLOTP_HEAD) { + slot = CONST_SLOT_HEAD; + } else if (slotPosition & SLOTP_NECKLACE) { + slot = CONST_SLOT_NECKLACE; + } else if (slotPosition & SLOTP_ARMOR) { + slot = CONST_SLOT_ARMOR; + } else if (slotPosition & SLOTP_LEGS) { + slot = CONST_SLOT_LEGS; + } else if (slotPosition & SLOTP_FEET) { + slot = CONST_SLOT_FEET; + } else if (slotPosition & SLOTP_RING) { + slot = CONST_SLOT_RING; + } else if (slotPosition & SLOTP_AMMO) { + slot = CONST_SLOT_AMMO; + } else if (slotPosition & SLOTP_TWO_HAND || slotPosition & SLOTP_LEFT) { + slot = CONST_SLOT_LEFT; + } + } + + return slot; +} + +//Implementation of player invoked events +void Game::playerEquipItem(uint32_t playerId, uint16_t spriteId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Item* item = player->getInventoryItem(CONST_SLOT_BACKPACK); + if (!item) { + return; + } + + Container* backpack = item->getContainer(); + if (!backpack) { + return; + } + + const ItemType& it = Item::items.getItemIdByClientId(spriteId); + slots_t slot = getSlotType(it); + + Item* slotItem = player->getInventoryItem(slot); + Item* equipItem = searchForItem(backpack, it.id); + if (slotItem && slotItem->getID() == it.id && (!it.stackable || slotItem->getItemCount() == 100 || !equipItem)) { + internalMoveItem(slotItem->getParent(), player, CONST_SLOT_WHEREEVER, slotItem, slotItem->getItemCount(), nullptr); + } else if (equipItem) { + internalMoveItem(equipItem->getParent(), player, slot, equipItem, equipItem->getItemCount(), nullptr); + } +} + +void Game::playerMove(uint32_t playerId, Direction direction) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->resetIdleTime(); + player->setNextWalkActionTask(nullptr); + + player->startAutoWalk(std::forward_list { direction }); +} + +bool Game::playerBroadcastMessage(Player* player, const std::string& text) const +{ + if (!player->hasFlag(PlayerFlag_CanBroadcast)) { + return false; + } + + std::cout << "> " << player->getName() << " broadcasted: \"" << text << "\"." << std::endl; + + for (const auto& it : players) { + it.second->sendPrivateMessage(player, TALKTYPE_BROADCAST, text); + } + + return true; +} + +void Game::playerCreatePrivateChannel(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player || !player->isPremium()) { + return; + } + + ChatChannel* channel = g_chat->createChannel(*player, CHANNEL_PRIVATE); + if (!channel || !channel->addUser(*player)) { + return; + } + + player->sendCreatePrivateChannel(channel->getId(), channel->getName()); +} + +void Game::playerChannelInvite(uint32_t playerId, const std::string& name) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + PrivateChatChannel* channel = g_chat->getPrivateChannel(*player); + if (!channel) { + return; + } + + Player* invitePlayer = getPlayerByName(name); + if (!invitePlayer) { + return; + } + + if (player == invitePlayer) { + return; + } + + channel->invitePlayer(*player, *invitePlayer); +} + +void Game::playerChannelExclude(uint32_t playerId, const std::string& name) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + PrivateChatChannel* channel = g_chat->getPrivateChannel(*player); + if (!channel) { + return; + } + + Player* excludePlayer = getPlayerByName(name); + if (!excludePlayer) { + return; + } + + if (player == excludePlayer) { + return; + } + + channel->excludePlayer(*player, *excludePlayer); +} + +void Game::playerRequestChannels(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->sendChannelsDialog(); +} + +void Game::playerOpenChannel(uint32_t playerId, uint16_t channelId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + ChatChannel* channel = g_chat->addUserToChannel(*player, channelId); + if (!channel) { + return; + } + + const InvitedMap* invitedUsers = channel->getInvitedUsers(); + const UsersMap* users; + if (!channel->isPublicChannel()) { + users = &channel->getUsers(); + } else { + users = nullptr; + } + + player->sendChannel(channel->getId(), channel->getName(), users, invitedUsers); +} + +void Game::playerCloseChannel(uint32_t playerId, uint16_t channelId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + g_chat->removeUserFromChannel(*player, channelId); +} + +void Game::playerOpenPrivateChannel(uint32_t playerId, std::string& receiver) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (!IOLoginData::formatPlayerName(receiver)) { + player->sendCancelMessage("A player with this name does not exist."); + return; + } + + if (player->getName() == receiver) { + player->sendCancelMessage("You cannot set up a private message channel with yourself."); + return; + } + + player->sendOpenPrivateChannel(receiver); +} + +void Game::playerCloseNpcChannel(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + SpectatorVec spectators; + map.getSpectators(spectators, player->getPosition()); + for (Creature* spectator : spectators) { + if (Npc* npc = spectator->getNpc()) { + npc->onPlayerCloseChannel(player); + } + } +} + +void Game::playerReceivePing(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->receivePing(); +} + +void Game::playerReceivePingBack(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->sendPingBack(); +} + +void Game::playerAutoWalk(uint32_t playerId, const std::forward_list& listDir) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->resetIdleTime(); + player->setNextWalkTask(nullptr); + player->startAutoWalk(listDir); +} + +void Game::playerStopAutoWalk(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->stopWalk(); +} + +void Game::playerUseItemEx(uint32_t playerId, const Position& fromPos, uint8_t fromStackPos, uint16_t fromSpriteId, + const Position& toPos, uint8_t toStackPos, uint16_t toSpriteId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + bool isHotkey = (fromPos.x == 0xFFFF && fromPos.y == 0 && fromPos.z == 0); + if (isHotkey && !g_config.getBoolean(ConfigManager::AIMBOT_HOTKEY_ENABLED)) { + return; + } + + Thing* thing = internalGetThing(player, fromPos, fromStackPos, fromSpriteId, STACKPOS_USEITEM); + if (!thing) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + Item* item = thing->getItem(); + if (!item || !item->isUseable() || item->getClientID() != fromSpriteId) { + player->sendCancelMessage(RETURNVALUE_CANNOTUSETHISOBJECT); + return; + } + + Position walkToPos = fromPos; + ReturnValue ret = g_actions->canUse(player, fromPos); + if (ret == RETURNVALUE_NOERROR) { + ret = g_actions->canUse(player, toPos, item); + if (ret == RETURNVALUE_TOOFARAWAY) { + walkToPos = toPos; + } + } + + if (ret != RETURNVALUE_NOERROR) { + if (ret == RETURNVALUE_TOOFARAWAY) { + Position itemPos = fromPos; + uint8_t itemStackPos = fromStackPos; + + if (fromPos.x != 0xFFFF && toPos.x != 0xFFFF && Position::areInRange<1, 1, 0>(fromPos, player->getPosition()) && + !Position::areInRange<1, 1, 0>(fromPos, toPos)) { + Item* moveItem = nullptr; + + ret = internalMoveItem(item->getParent(), player, INDEX_WHEREEVER, item, item->getItemCount(), &moveItem, 0, player, nullptr, &fromPos, &toPos); + if (ret != RETURNVALUE_NOERROR) { + player->sendCancelMessage(ret); + return; + } + + //changing the position since its now in the inventory of the player + internalGetPosition(moveItem, itemPos, itemStackPos); + } + + std::forward_list listDir; + if (player->getPathTo(walkToPos, listDir, 0, 1, true, true)) { + g_dispatcher.addTask(createTask(std::bind(&Game::playerAutoWalk, this, player->getID(), listDir))); + + SchedulerTask* task = createSchedulerTask(400, std::bind(&Game::playerUseItemEx, this, + playerId, itemPos, itemStackPos, fromSpriteId, toPos, toStackPos, toSpriteId)); + player->setNextWalkActionTask(task); + } else { + player->sendCancelMessage(RETURNVALUE_THEREISNOWAY); + } + return; + } + + player->sendCancelMessage(ret); + return; + } + + if (!player->canDoAction()) { + uint32_t delay = player->getNextActionTime(); + SchedulerTask* task = createSchedulerTask(delay, std::bind(&Game::playerUseItemEx, this, + playerId, fromPos, fromStackPos, fromSpriteId, toPos, toStackPos, toSpriteId)); + player->setNextActionTask(task); + return; + } + + player->resetIdleTime(); + player->setNextActionTask(nullptr); + + g_actions->useItemEx(player, fromPos, toPos, toStackPos, item, isHotkey); +} + +void Game::playerUseItem(uint32_t playerId, const Position& pos, uint8_t stackPos, + uint8_t index, uint16_t spriteId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + bool isHotkey = (pos.x == 0xFFFF && pos.y == 0 && pos.z == 0); + if (isHotkey && !g_config.getBoolean(ConfigManager::AIMBOT_HOTKEY_ENABLED)) { + return; + } + + Thing* thing = internalGetThing(player, pos, stackPos, spriteId, STACKPOS_USEITEM); + if (!thing) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + Item* item = thing->getItem(); + if (!item || item->isUseable() || item->getClientID() != spriteId) { + player->sendCancelMessage(RETURNVALUE_CANNOTUSETHISOBJECT); + return; + } + + ReturnValue ret = g_actions->canUse(player, pos); + if (ret != RETURNVALUE_NOERROR) { + if (ret == RETURNVALUE_TOOFARAWAY) { + std::forward_list listDir; + if (player->getPathTo(pos, listDir, 0, 1, true, true)) { + g_dispatcher.addTask(createTask(std::bind(&Game::playerAutoWalk, + this, player->getID(), listDir))); + + SchedulerTask* task = createSchedulerTask(400, std::bind(&Game::playerUseItem, this, + playerId, pos, stackPos, index, spriteId)); + player->setNextWalkActionTask(task); + return; + } + + ret = RETURNVALUE_THEREISNOWAY; + } + + player->sendCancelMessage(ret); + return; + } + + if (!player->canDoAction()) { + uint32_t delay = player->getNextActionTime(); + SchedulerTask* task = createSchedulerTask(delay, std::bind(&Game::playerUseItem, this, + playerId, pos, stackPos, index, spriteId)); + player->setNextActionTask(task); + return; + } + + player->resetIdleTime(); + player->setNextActionTask(nullptr); + + g_actions->useItem(player, pos, index, item, isHotkey); +} + +void Game::playerUseWithCreature(uint32_t playerId, const Position& fromPos, uint8_t fromStackPos, uint32_t creatureId, uint16_t spriteId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Creature* creature = getCreatureByID(creatureId); + if (!creature) { + return; + } + + if (!Position::areInRange<7, 5, 0>(creature->getPosition(), player->getPosition())) { + return; + } + + bool isHotkey = (fromPos.x == 0xFFFF && fromPos.y == 0 && fromPos.z == 0); + if (!g_config.getBoolean(ConfigManager::AIMBOT_HOTKEY_ENABLED)) { + if (creature->getPlayer() || isHotkey) { + player->sendCancelMessage(RETURNVALUE_DIRECTPLAYERSHOOT); + return; + } + } + + Thing* thing = internalGetThing(player, fromPos, fromStackPos, spriteId, STACKPOS_USEITEM); + if (!thing) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + Item* item = thing->getItem(); + if (!item || !item->isUseable() || item->getClientID() != spriteId) { + player->sendCancelMessage(RETURNVALUE_CANNOTUSETHISOBJECT); + return; + } + + Position toPos = creature->getPosition(); + Position walkToPos = fromPos; + ReturnValue ret = g_actions->canUse(player, fromPos); + if (ret == RETURNVALUE_NOERROR) { + ret = g_actions->canUse(player, toPos, item); + if (ret == RETURNVALUE_TOOFARAWAY) { + walkToPos = toPos; + } + } + + if (ret != RETURNVALUE_NOERROR) { + if (ret == RETURNVALUE_TOOFARAWAY) { + Position itemPos = fromPos; + uint8_t itemStackPos = fromStackPos; + + if (fromPos.x != 0xFFFF && Position::areInRange<1, 1, 0>(fromPos, player->getPosition()) && !Position::areInRange<1, 1, 0>(fromPos, toPos)) { + Item* moveItem = nullptr; + ret = internalMoveItem(item->getParent(), player, INDEX_WHEREEVER, item, item->getItemCount(), &moveItem, 0, player, nullptr, &fromPos, &toPos); + if (ret != RETURNVALUE_NOERROR) { + player->sendCancelMessage(ret); + return; + } + + //changing the position since its now in the inventory of the player + internalGetPosition(moveItem, itemPos, itemStackPos); + } + + std::forward_list listDir; + if (player->getPathTo(walkToPos, listDir, 0, 1, true, true)) { + g_dispatcher.addTask(createTask(std::bind(&Game::playerAutoWalk, + this, player->getID(), listDir))); + + SchedulerTask* task = createSchedulerTask(400, std::bind(&Game::playerUseWithCreature, this, + playerId, itemPos, itemStackPos, creatureId, spriteId)); + player->setNextWalkActionTask(task); + } else { + player->sendCancelMessage(RETURNVALUE_THEREISNOWAY); + } + return; + } + + player->sendCancelMessage(ret); + return; + } + + if (!player->canDoAction()) { + uint32_t delay = player->getNextActionTime(); + SchedulerTask* task = createSchedulerTask(delay, std::bind(&Game::playerUseWithCreature, this, + playerId, fromPos, fromStackPos, creatureId, spriteId)); + player->setNextActionTask(task); + return; + } + + player->resetIdleTime(); + player->setNextActionTask(nullptr); + + g_actions->useItemEx(player, fromPos, creature->getPosition(), creature->getParent()->getThingIndex(creature), item, isHotkey, creature); +} + +void Game::playerCloseContainer(uint32_t playerId, uint8_t cid) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->closeContainer(cid); + player->sendCloseContainer(cid); +} + +void Game::playerMoveUpContainer(uint32_t playerId, uint8_t cid) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Container* container = player->getContainerByID(cid); + if (!container) { + return; + } + + Container* parentContainer = dynamic_cast(container->getRealParent()); + if (!parentContainer) { + Tile* tile = container->getTile(); + if (!tile) { + return; + } + + auto it = browseFields.find(tile); + if (it == browseFields.end()) { + parentContainer = new Container(tile); + parentContainer->incrementReferenceCounter(); + browseFields[tile] = parentContainer; + g_scheduler.addEvent(createSchedulerTask(30000, std::bind(&Game::decreaseBrowseFieldRef, this, tile->getPosition()))); + } else { + parentContainer = it->second; + } + } + + player->addContainer(cid, parentContainer); + player->sendContainer(cid, parentContainer, parentContainer->hasParent(), player->getContainerIndex(cid)); +} + +void Game::playerUpdateContainer(uint32_t playerId, uint8_t cid) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Container* container = player->getContainerByID(cid); + if (!container) { + return; + } + + player->sendContainer(cid, container, container->hasParent(), player->getContainerIndex(cid)); +} + +void Game::playerRotateItem(uint32_t playerId, const Position& pos, uint8_t stackPos, const uint16_t spriteId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Thing* thing = internalGetThing(player, pos, stackPos, 0, STACKPOS_TOPDOWN_ITEM); + if (!thing) { + return; + } + + Item* item = thing->getItem(); + if (!item || item->getClientID() != spriteId || !item->isRotatable() || item->hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + if (pos.x != 0xFFFF && !Position::areInRange<1, 1, 0>(pos, player->getPosition())) { + std::forward_list listDir; + if (player->getPathTo(pos, listDir, 0, 1, true, true)) { + g_dispatcher.addTask(createTask(std::bind(&Game::playerAutoWalk, + this, player->getID(), listDir))); + + SchedulerTask* task = createSchedulerTask(400, std::bind(&Game::playerRotateItem, this, + playerId, pos, stackPos, spriteId)); + player->setNextWalkActionTask(task); + } else { + player->sendCancelMessage(RETURNVALUE_THEREISNOWAY); + } + return; + } + + uint16_t newId = Item::items[item->getID()].rotateTo; + if (newId != 0) { + transformItem(item, newId); + } +} + +void Game::playerWriteItem(uint32_t playerId, uint32_t windowTextId, const std::string& text) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + uint16_t maxTextLength = 0; + uint32_t internalWindowTextId = 0; + + Item* writeItem = player->getWriteItem(internalWindowTextId, maxTextLength); + if (text.length() > maxTextLength || windowTextId != internalWindowTextId) { + return; + } + + if (!writeItem || writeItem->isRemoved()) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + Cylinder* topParent = writeItem->getTopParent(); + + Player* owner = dynamic_cast(topParent); + if (owner && owner != player) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + if (!Position::areInRange<1, 1, 0>(writeItem->getPosition(), player->getPosition())) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + for (auto creatureEvent : player->getCreatureEvents(CREATURE_EVENT_TEXTEDIT)) { + if (!creatureEvent->executeTextEdit(player, writeItem, text)) { + player->setWriteItem(nullptr); + return; + } + } + + if (!text.empty()) { + if (writeItem->getText() != text) { + writeItem->setText(text); + writeItem->setWriter(player->getName()); + writeItem->setDate(time(nullptr)); + } + } else { + writeItem->resetText(); + writeItem->resetWriter(); + writeItem->resetDate(); + } + + uint16_t newId = Item::items[writeItem->getID()].writeOnceItemId; + if (newId != 0) { + transformItem(writeItem, newId); + } + + player->setWriteItem(nullptr); +} + +void Game::playerBrowseField(uint32_t playerId, const Position& pos) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + const Position& playerPos = player->getPosition(); + if (playerPos.z != pos.z) { + player->sendCancelMessage(playerPos.z > pos.z ? RETURNVALUE_FIRSTGOUPSTAIRS : RETURNVALUE_FIRSTGODOWNSTAIRS); + return; + } + + if (!Position::areInRange<1, 1>(playerPos, pos)) { + std::forward_list listDir; + if (player->getPathTo(pos, listDir, 0, 1, true, true)) { + g_dispatcher.addTask(createTask(std::bind(&Game::playerAutoWalk, + this, player->getID(), listDir))); + SchedulerTask* task = createSchedulerTask(400, std::bind( + &Game::playerBrowseField, this, playerId, pos + )); + player->setNextWalkActionTask(task); + } else { + player->sendCancelMessage(RETURNVALUE_THEREISNOWAY); + } + return; + } + + Tile* tile = map.getTile(pos); + if (!tile) { + return; + } + + if (!g_events->eventPlayerOnBrowseField(player, pos)) { + return; + } + + Container* container; + + auto it = browseFields.find(tile); + if (it == browseFields.end()) { + container = new Container(tile); + container->incrementReferenceCounter(); + browseFields[tile] = container; + g_scheduler.addEvent(createSchedulerTask(30000, std::bind(&Game::decreaseBrowseFieldRef, this, tile->getPosition()))); + } else { + container = it->second; + } + + uint8_t dummyContainerId = 0xF - ((pos.x % 3) * 3 + (pos.y % 3)); + Container* openContainer = player->getContainerByID(dummyContainerId); + if (openContainer) { + player->onCloseContainer(openContainer); + player->closeContainer(dummyContainerId); + } else { + player->addContainer(dummyContainerId, container); + player->sendContainer(dummyContainerId, container, false, 0); + } +} + +void Game::playerSeekInContainer(uint32_t playerId, uint8_t containerId, uint16_t index) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Container* container = player->getContainerByID(containerId); + if (!container || !container->hasPagination()) { + return; + } + + if ((index % container->capacity()) != 0 || index >= container->size()) { + return; + } + + player->setContainerIndex(containerId, index); + player->sendContainer(containerId, container, container->hasParent(), index); +} + +void Game::playerUpdateHouseWindow(uint32_t playerId, uint8_t listId, uint32_t windowTextId, const std::string& text) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + uint32_t internalWindowTextId; + uint32_t internalListId; + + House* house = player->getEditHouse(internalWindowTextId, internalListId); + if (house && house->canEditAccessList(internalListId, player) && internalWindowTextId == windowTextId && listId == 0) { + house->setAccessList(internalListId, text); + } + + player->setEditHouse(nullptr); +} + +void Game::playerWrapItem(uint32_t playerId, const Position& position, uint8_t stackPos, const uint16_t spriteId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Thing* thing = internalGetThing(player, position, stackPos, 0, STACKPOS_TOPDOWN_ITEM); + if (!thing) { + return; + } + + Item* item = thing->getItem(); + if (!item || item->getClientID() != spriteId || !item->hasAttribute(ITEM_ATTRIBUTE_WRAPID) || item->hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + if (position.x != 0xFFFF && !Position::areInRange<1, 1, 0>(position, player->getPosition())) { + std::forward_list listDir; + if (player->getPathTo(position, listDir, 0, 1, true, true)) { + g_dispatcher.addTask(createTask(std::bind(&Game::playerAutoWalk, + this, player->getID(), listDir))); + + SchedulerTask* task = createSchedulerTask(400, std::bind(&Game::playerWrapItem, this, + playerId, position, stackPos, spriteId)); + player->setNextWalkActionTask(task); + } else { + player->sendCancelMessage(RETURNVALUE_THEREISNOWAY); + } + return; + } + + g_events->eventPlayerOnWrapItem(player, item); +} + +void Game::playerRequestTrade(uint32_t playerId, const Position& pos, uint8_t stackPos, + uint32_t tradePlayerId, uint16_t spriteId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Player* tradePartner = getPlayerByID(tradePlayerId); + if (!tradePartner || tradePartner == player) { + player->sendTextMessage(MESSAGE_INFO_DESCR, "Sorry, not possible."); + return; + } + + if (!Position::areInRange<2, 2, 0>(tradePartner->getPosition(), player->getPosition())) { + std::ostringstream ss; + ss << tradePartner->getName() << " tells you to move closer."; + player->sendTextMessage(MESSAGE_INFO_DESCR, ss.str()); + return; + } + + if (!canThrowObjectTo(tradePartner->getPosition(), player->getPosition())) { + player->sendCancelMessage(RETURNVALUE_CREATUREISNOTREACHABLE); + return; + } + + Thing* tradeThing = internalGetThing(player, pos, stackPos, 0, STACKPOS_TOPDOWN_ITEM); + if (!tradeThing) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + Item* tradeItem = tradeThing->getItem(); + if (tradeItem->getClientID() != spriteId || !tradeItem->isPickupable() || tradeItem->hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + const Position& playerPosition = player->getPosition(); + const Position& tradeItemPosition = tradeItem->getPosition(); + if (playerPosition.z != tradeItemPosition.z) { + player->sendCancelMessage(playerPosition.z > tradeItemPosition.z ? RETURNVALUE_FIRSTGOUPSTAIRS : RETURNVALUE_FIRSTGODOWNSTAIRS); + return; + } + + if (!Position::areInRange<1, 1>(tradeItemPosition, playerPosition)) { + std::forward_list listDir; + if (player->getPathTo(pos, listDir, 0, 1, true, true)) { + g_dispatcher.addTask(createTask(std::bind(&Game::playerAutoWalk, + this, player->getID(), listDir))); + + SchedulerTask* task = createSchedulerTask(400, std::bind(&Game::playerRequestTrade, this, + playerId, pos, stackPos, tradePlayerId, spriteId)); + player->setNextWalkActionTask(task); + } else { + player->sendCancelMessage(RETURNVALUE_THEREISNOWAY); + } + return; + } + + Container* tradeItemContainer = tradeItem->getContainer(); + if (tradeItemContainer) { + for (const auto& it : tradeItems) { + Item* item = it.first; + if (tradeItem == item) { + player->sendTextMessage(MESSAGE_INFO_DESCR, "This item is already being traded."); + return; + } + + if (tradeItemContainer->isHoldingItem(item)) { + player->sendTextMessage(MESSAGE_INFO_DESCR, "This item is already being traded."); + return; + } + + Container* container = item->getContainer(); + if (container && container->isHoldingItem(tradeItem)) { + player->sendTextMessage(MESSAGE_INFO_DESCR, "This item is already being traded."); + return; + } + } + } else { + for (const auto& it : tradeItems) { + Item* item = it.first; + if (tradeItem == item) { + player->sendTextMessage(MESSAGE_INFO_DESCR, "This item is already being traded."); + return; + } + + Container* container = item->getContainer(); + if (container && container->isHoldingItem(tradeItem)) { + player->sendTextMessage(MESSAGE_INFO_DESCR, "This item is already being traded."); + return; + } + } + } + + Container* tradeContainer = tradeItem->getContainer(); + if (tradeContainer && tradeContainer->getItemHoldingCount() + 1 > 100) { + player->sendTextMessage(MESSAGE_INFO_DESCR, "You can not trade more than 100 items."); + return; + } + + if (!g_events->eventPlayerOnTradeRequest(player, tradePartner, tradeItem)) { + return; + } + + internalStartTrade(player, tradePartner, tradeItem); +} + +bool Game::internalStartTrade(Player* player, Player* tradePartner, Item* tradeItem) +{ + if (player->tradeState != TRADE_NONE && !(player->tradeState == TRADE_ACKNOWLEDGE && player->tradePartner == tradePartner)) { + player->sendCancelMessage(RETURNVALUE_YOUAREALREADYTRADING); + return false; + } else if (tradePartner->tradeState != TRADE_NONE && tradePartner->tradePartner != player) { + player->sendCancelMessage(RETURNVALUE_THISPLAYERISALREADYTRADING); + return false; + } + + player->tradePartner = tradePartner; + player->tradeItem = tradeItem; + player->tradeState = TRADE_INITIATED; + tradeItem->incrementReferenceCounter(); + tradeItems[tradeItem] = player->getID(); + + player->sendTradeItemRequest(player->getName(), tradeItem, true); + + if (tradePartner->tradeState == TRADE_NONE) { + std::ostringstream ss; + ss << player->getName() << " wants to trade with you."; + tradePartner->sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str()); + tradePartner->tradeState = TRADE_ACKNOWLEDGE; + tradePartner->tradePartner = player; + } else { + Item* counterOfferItem = tradePartner->tradeItem; + player->sendTradeItemRequest(tradePartner->getName(), counterOfferItem, false); + tradePartner->sendTradeItemRequest(player->getName(), tradeItem, false); + } + + return true; +} + +void Game::playerAcceptTrade(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (!(player->getTradeState() == TRADE_ACKNOWLEDGE || player->getTradeState() == TRADE_INITIATED)) { + return; + } + + Player* tradePartner = player->tradePartner; + if (!tradePartner) { + return; + } + + if (!canThrowObjectTo(tradePartner->getPosition(), player->getPosition())) { + player->sendCancelMessage(RETURNVALUE_CREATUREISNOTREACHABLE); + return; + } + + player->setTradeState(TRADE_ACCEPT); + + if (tradePartner->getTradeState() == TRADE_ACCEPT) { + Item* playerTradeItem = player->tradeItem; + Item* partnerTradeItem = tradePartner->tradeItem; + + if (!g_events->eventPlayerOnTradeAccept(player, tradePartner, playerTradeItem, partnerTradeItem)) { + internalCloseTrade(player); + return; + } + + player->setTradeState(TRADE_TRANSFER); + tradePartner->setTradeState(TRADE_TRANSFER); + + auto it = tradeItems.find(playerTradeItem); + if (it != tradeItems.end()) { + ReleaseItem(it->first); + tradeItems.erase(it); + } + + it = tradeItems.find(partnerTradeItem); + if (it != tradeItems.end()) { + ReleaseItem(it->first); + tradeItems.erase(it); + } + + bool isSuccess = false; + + ReturnValue tradePartnerRet = RETURNVALUE_NOERROR; + ReturnValue playerRet = RETURNVALUE_NOERROR; + + // if player is trying to trade its own backpack + if (tradePartner->getInventoryItem(CONST_SLOT_BACKPACK) == partnerTradeItem) { + tradePartnerRet = (tradePartner->getInventoryItem(getSlotType(Item::items[playerTradeItem->getID()])) ? RETURNVALUE_NOTENOUGHROOM : RETURNVALUE_NOERROR); + } + + if (player->getInventoryItem(CONST_SLOT_BACKPACK) == playerTradeItem) { + playerRet = (player->getInventoryItem(getSlotType(Item::items[partnerTradeItem->getID()])) ? RETURNVALUE_NOTENOUGHROOM : RETURNVALUE_NOERROR); + } + + if (tradePartnerRet == RETURNVALUE_NOERROR && playerRet == RETURNVALUE_NOERROR) { + tradePartnerRet = internalAddItem(tradePartner, playerTradeItem, INDEX_WHEREEVER, 0, true); + playerRet = internalAddItem(player, partnerTradeItem, INDEX_WHEREEVER, 0, true); + if (tradePartnerRet == RETURNVALUE_NOERROR && playerRet == RETURNVALUE_NOERROR) { + playerRet = internalRemoveItem(playerTradeItem, playerTradeItem->getItemCount(), true); + tradePartnerRet = internalRemoveItem(partnerTradeItem, partnerTradeItem->getItemCount(), true); + if (tradePartnerRet == RETURNVALUE_NOERROR && playerRet == RETURNVALUE_NOERROR) { + tradePartnerRet = internalMoveItem(playerTradeItem->getParent(), tradePartner, INDEX_WHEREEVER, playerTradeItem, playerTradeItem->getItemCount(), nullptr, FLAG_IGNOREAUTOSTACK, nullptr, partnerTradeItem); + if (tradePartnerRet == RETURNVALUE_NOERROR) { + internalMoveItem(partnerTradeItem->getParent(), player, INDEX_WHEREEVER, partnerTradeItem, partnerTradeItem->getItemCount(), nullptr, FLAG_IGNOREAUTOSTACK); + playerTradeItem->onTradeEvent(ON_TRADE_TRANSFER, tradePartner); + partnerTradeItem->onTradeEvent(ON_TRADE_TRANSFER, player); + isSuccess = true; + } + } + } + } + + if (!isSuccess) { + std::string errorDescription; + + if (tradePartner->tradeItem) { + errorDescription = getTradeErrorDescription(tradePartnerRet, playerTradeItem); + tradePartner->sendTextMessage(MESSAGE_EVENT_ADVANCE, errorDescription); + tradePartner->tradeItem->onTradeEvent(ON_TRADE_CANCEL, tradePartner); + } + + if (player->tradeItem) { + errorDescription = getTradeErrorDescription(playerRet, partnerTradeItem); + player->sendTextMessage(MESSAGE_EVENT_ADVANCE, errorDescription); + player->tradeItem->onTradeEvent(ON_TRADE_CANCEL, player); + } + } + + g_events->eventPlayerOnTradeCompleted(player, tradePartner, playerTradeItem, partnerTradeItem, isSuccess); + + player->setTradeState(TRADE_NONE); + player->tradeItem = nullptr; + player->tradePartner = nullptr; + player->sendTradeClose(); + + tradePartner->setTradeState(TRADE_NONE); + tradePartner->tradeItem = nullptr; + tradePartner->tradePartner = nullptr; + tradePartner->sendTradeClose(); + } +} + +std::string Game::getTradeErrorDescription(ReturnValue ret, Item* item) +{ + if (item) { + if (ret == RETURNVALUE_NOTENOUGHCAPACITY) { + std::ostringstream ss; + ss << "You do not have enough capacity to carry"; + + if (item->isStackable() && item->getItemCount() > 1) { + ss << " these objects."; + } else { + ss << " this object."; + } + + ss << "\n " << item->getWeightDescription(); + return ss.str(); + } else if (ret == RETURNVALUE_NOTENOUGHROOM || ret == RETURNVALUE_CONTAINERNOTENOUGHROOM) { + std::ostringstream ss; + ss << "You do not have enough room to carry"; + + if (item->isStackable() && item->getItemCount() > 1) { + ss << " these objects."; + } else { + ss << " this object."; + } + + return ss.str(); + } + } + return "Trade could not be completed."; +} + +void Game::playerLookInTrade(uint32_t playerId, bool lookAtCounterOffer, uint8_t index) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Player* tradePartner = player->tradePartner; + if (!tradePartner) { + return; + } + + Item* tradeItem; + if (lookAtCounterOffer) { + tradeItem = tradePartner->getTradeItem(); + } else { + tradeItem = player->getTradeItem(); + } + + if (!tradeItem) { + return; + } + + const Position& playerPosition = player->getPosition(); + const Position& tradeItemPosition = tradeItem->getPosition(); + + int32_t lookDistance = std::max(Position::getDistanceX(playerPosition, tradeItemPosition), + Position::getDistanceY(playerPosition, tradeItemPosition)); + if (index == 0) { + g_events->eventPlayerOnLookInTrade(player, tradePartner, tradeItem, lookDistance); + return; + } + + Container* tradeContainer = tradeItem->getContainer(); + if (!tradeContainer) { + return; + } + + std::vector containers {tradeContainer}; + size_t i = 0; + while (i < containers.size()) { + const Container* container = containers[i++]; + for (Item* item : container->getItemList()) { + Container* tmpContainer = item->getContainer(); + if (tmpContainer) { + containers.push_back(tmpContainer); + } + + if (--index == 0) { + g_events->eventPlayerOnLookInTrade(player, tradePartner, item, lookDistance); + return; + } + } + } +} + +void Game::playerCloseTrade(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + internalCloseTrade(player); +} + +void Game::internalCloseTrade(Player* player) +{ + Player* tradePartner = player->tradePartner; + if ((tradePartner && tradePartner->getTradeState() == TRADE_TRANSFER) || player->getTradeState() == TRADE_TRANSFER) { + return; + } + + if (player->getTradeItem()) { + auto it = tradeItems.find(player->getTradeItem()); + if (it != tradeItems.end()) { + ReleaseItem(it->first); + tradeItems.erase(it); + } + + player->tradeItem->onTradeEvent(ON_TRADE_CANCEL, player); + player->tradeItem = nullptr; + } + + player->setTradeState(TRADE_NONE); + player->tradePartner = nullptr; + + player->sendTextMessage(MESSAGE_STATUS_SMALL, "Trade cancelled."); + player->sendTradeClose(); + + if (tradePartner) { + if (tradePartner->getTradeItem()) { + auto it = tradeItems.find(tradePartner->getTradeItem()); + if (it != tradeItems.end()) { + ReleaseItem(it->first); + tradeItems.erase(it); + } + + tradePartner->tradeItem->onTradeEvent(ON_TRADE_CANCEL, tradePartner); + tradePartner->tradeItem = nullptr; + } + + tradePartner->setTradeState(TRADE_NONE); + tradePartner->tradePartner = nullptr; + + tradePartner->sendTextMessage(MESSAGE_STATUS_SMALL, "Trade cancelled."); + tradePartner->sendTradeClose(); + } +} + +void Game::playerPurchaseItem(uint32_t playerId, uint16_t spriteId, uint8_t count, uint8_t amount, + bool ignoreCap/* = false*/, bool inBackpacks/* = false*/) +{ + if (amount == 0 || amount > 100) { + return; + } + + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + int32_t onBuy, onSell; + + Npc* merchant = player->getShopOwner(onBuy, onSell); + if (!merchant) { + return; + } + + const ItemType& it = Item::items.getItemIdByClientId(spriteId); + if (it.id == 0) { + return; + } + + uint8_t subType; + if (it.isSplash() || it.isFluidContainer()) { + subType = clientFluidToServer(count); + } else { + subType = count; + } + + if (!player->hasShopItemForSale(it.id, subType)) { + return; + } + + merchant->onPlayerTrade(player, onBuy, it.id, subType, amount, ignoreCap, inBackpacks); +} + +void Game::playerSellItem(uint32_t playerId, uint16_t spriteId, uint8_t count, uint8_t amount, bool ignoreEquipped) +{ + if (amount == 0 || amount > 100) { + return; + } + + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + int32_t onBuy, onSell; + + Npc* merchant = player->getShopOwner(onBuy, onSell); + if (!merchant) { + return; + } + + const ItemType& it = Item::items.getItemIdByClientId(spriteId); + if (it.id == 0) { + return; + } + + uint8_t subType; + if (it.isSplash() || it.isFluidContainer()) { + subType = clientFluidToServer(count); + } else { + subType = count; + } + + merchant->onPlayerTrade(player, onSell, it.id, subType, amount, ignoreEquipped); +} + +void Game::playerCloseShop(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->closeShopWindow(); +} + +void Game::playerLookInShop(uint32_t playerId, uint16_t spriteId, uint8_t count) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + int32_t onBuy, onSell; + + Npc* merchant = player->getShopOwner(onBuy, onSell); + if (!merchant) { + return; + } + + const ItemType& it = Item::items.getItemIdByClientId(spriteId); + if (it.id == 0) { + return; + } + + int32_t subType; + if (it.isFluidContainer() || it.isSplash()) { + subType = clientFluidToServer(count); + } else { + subType = count; + } + + if (!player->hasShopItemForSale(it.id, subType)) { + return; + } + + if (!g_events->eventPlayerOnLookInShop(player, &it, subType)) { + return; + } + + std::ostringstream ss; + ss << "You see " << Item::getDescription(it, 1, nullptr, subType); + player->sendTextMessage(MESSAGE_INFO_DESCR, ss.str()); +} + +void Game::playerLookAt(uint32_t playerId, const Position& pos, uint8_t stackPos) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Thing* thing = internalGetThing(player, pos, stackPos, 0, STACKPOS_LOOK); + if (!thing) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + Position thingPos = thing->getPosition(); + if (!player->canSee(thingPos)) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return; + } + + Position playerPos = player->getPosition(); + + int32_t lookDistance; + if (thing != player) { + lookDistance = std::max(Position::getDistanceX(playerPos, thingPos), Position::getDistanceY(playerPos, thingPos)); + if (playerPos.z != thingPos.z) { + lookDistance += 15; + } + } else { + lookDistance = -1; + } + + g_events->eventPlayerOnLook(player, pos, thing, stackPos, lookDistance); +} + +void Game::playerLookInBattleList(uint32_t playerId, uint32_t creatureId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Creature* creature = getCreatureByID(creatureId); + if (!creature) { + return; + } + + if (!player->canSeeCreature(creature)) { + return; + } + + const Position& creaturePos = creature->getPosition(); + if (!player->canSee(creaturePos)) { + return; + } + + int32_t lookDistance; + if (creature != player) { + const Position& playerPos = player->getPosition(); + lookDistance = std::max(Position::getDistanceX(playerPos, creaturePos), Position::getDistanceY(playerPos, creaturePos)); + if (playerPos.z != creaturePos.z) { + lookDistance += 15; + } + } else { + lookDistance = -1; + } + + g_events->eventPlayerOnLookInBattleList(player, creature, lookDistance); +} + +void Game::playerCancelAttackAndFollow(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + playerSetAttackedCreature(playerId, 0); + playerFollowCreature(playerId, 0); + player->stopWalk(); +} + +void Game::playerSetAttackedCreature(uint32_t playerId, uint32_t creatureId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (player->getAttackedCreature() && creatureId == 0) { + player->setAttackedCreature(nullptr); + player->sendCancelTarget(); + return; + } + + Creature* attackCreature = getCreatureByID(creatureId); + if (!attackCreature) { + player->setAttackedCreature(nullptr); + player->sendCancelTarget(); + return; + } + + ReturnValue ret = Combat::canTargetCreature(player, attackCreature); + if (ret != RETURNVALUE_NOERROR) { + player->sendCancelMessage(ret); + player->sendCancelTarget(); + player->setAttackedCreature(nullptr); + return; + } + + player->setAttackedCreature(attackCreature); + g_dispatcher.addTask(createTask(std::bind(&Game::updateCreatureWalk, this, player->getID()))); +} + +void Game::playerFollowCreature(uint32_t playerId, uint32_t creatureId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->setAttackedCreature(nullptr); + g_dispatcher.addTask(createTask(std::bind(&Game::updateCreatureWalk, this, player->getID()))); + player->setFollowCreature(getCreatureByID(creatureId)); +} + +void Game::playerSetFightModes(uint32_t playerId, fightMode_t fightMode, bool chaseMode, bool secureMode) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->setFightMode(fightMode); + player->setChaseMode(chaseMode); + player->setSecureMode(secureMode); +} + +void Game::playerRequestAddVip(uint32_t playerId, const std::string& name) +{ + if (name.length() > 20) { + return; + } + + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Player* vipPlayer = getPlayerByName(name); + if (!vipPlayer) { + uint32_t guid; + bool specialVip; + std::string formattedName = name; + if (!IOLoginData::getGuidByNameEx(guid, specialVip, formattedName)) { + player->sendTextMessage(MESSAGE_STATUS_SMALL, "A player with this name does not exist."); + return; + } + + if (specialVip && !player->hasFlag(PlayerFlag_SpecialVIP)) { + player->sendTextMessage(MESSAGE_STATUS_SMALL, "You can not add this player."); + return; + } + + player->addVIP(guid, formattedName, VIPSTATUS_OFFLINE); + } else { + if (vipPlayer->hasFlag(PlayerFlag_SpecialVIP) && !player->hasFlag(PlayerFlag_SpecialVIP)) { + player->sendTextMessage(MESSAGE_STATUS_SMALL, "You can not add this player."); + return; + } + + if (!vipPlayer->isInGhostMode() || player->isAccessPlayer()) { + player->addVIP(vipPlayer->getGUID(), vipPlayer->getName(), VIPSTATUS_ONLINE); + } else { + player->addVIP(vipPlayer->getGUID(), vipPlayer->getName(), VIPSTATUS_OFFLINE); + } + } +} + +void Game::playerRequestRemoveVip(uint32_t playerId, uint32_t guid) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->removeVIP(guid); +} + +void Game::playerRequestEditVip(uint32_t playerId, uint32_t guid, const std::string& description, uint32_t icon, bool notify) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->editVIP(guid, description, icon, notify); +} + +void Game::playerTurn(uint32_t playerId, Direction dir) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (!g_events->eventPlayerOnTurn(player, dir)) { + return; + } + + player->resetIdleTime(); + internalCreatureTurn(player, dir); +} + +void Game::playerRequestOutfit(uint32_t playerId) +{ + if (!g_config.getBoolean(ConfigManager::ALLOW_CHANGEOUTFIT)) { + return; + } + + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->sendOutfitWindow(); +} + +void Game::playerToggleMount(uint32_t playerId, bool mount) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->toggleMount(mount); +} + +void Game::playerChangeOutfit(uint32_t playerId, Outfit_t outfit) +{ + if (!g_config.getBoolean(ConfigManager::ALLOW_CHANGEOUTFIT)) { + return; + } + + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + const Outfit* playerOutfit = Outfits::getInstance().getOutfitByLookType(player->getSex(), outfit.lookType); + if (!playerOutfit) { + outfit.lookMount = 0; + } + + if (outfit.lookMount != 0) { + Mount* mount = mounts.getMountByClientID(outfit.lookMount); + if (!mount) { + return; + } + + if (!player->hasMount(mount)) { + return; + } + + if (player->isMounted()) { + Mount* prevMount = mounts.getMountByID(player->getCurrentMount()); + if (prevMount) { + changeSpeed(player, mount->speed - prevMount->speed); + } + + player->setCurrentMount(mount->id); + } else { + player->setCurrentMount(mount->id); + outfit.lookMount = 0; + } + } else if (player->isMounted()) { + player->dismount(); + } + + if (player->canWear(outfit.lookType, outfit.lookAddons)) { + player->defaultOutfit = outfit; + + if (player->hasCondition(CONDITION_OUTFIT)) { + return; + } + + internalCreatureChangeOutfit(player, outfit); + } +} + +void Game::playerShowQuestLog(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->sendQuestLog(); +} + +void Game::playerShowQuestLine(uint32_t playerId, uint16_t questId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Quest* quest = quests.getQuestByID(questId); + if (!quest) { + return; + } + + player->sendQuestLine(quest); +} + +void Game::playerSay(uint32_t playerId, uint16_t channelId, SpeakClasses type, + const std::string& receiver, const std::string& text) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->resetIdleTime(); + + if (playerSaySpell(player, type, text)) { + return; + } + + uint32_t muteTime = player->isMuted(); + if (muteTime > 0) { + std::ostringstream ss; + ss << "You are still muted for " << muteTime << " seconds."; + player->sendTextMessage(MESSAGE_STATUS_SMALL, ss.str()); + return; + } + + if (!text.empty() && text.front() == '/' && player->isAccessPlayer()) { + return; + } + + if (type != TALKTYPE_PRIVATE_PN) { + player->removeMessageBuffer(); + } + + if (channelId == CHANNEL_CAST) { + player->sendChannelMessage(player->getName(), text, TALKTYPE_CHANNEL_R1, channelId); + } + + switch (type) { + case TALKTYPE_SAY: + internalCreatureSay(player, TALKTYPE_SAY, text, false); + break; + + case TALKTYPE_WHISPER: + playerWhisper(player, text); + break; + + case TALKTYPE_YELL: + playerYell(player, text); + break; + + case TALKTYPE_PRIVATE_TO: + case TALKTYPE_PRIVATE_RED_TO: + playerSpeakTo(player, type, receiver, text); + break; + + case TALKTYPE_CHANNEL_O: + case TALKTYPE_CHANNEL_Y: + case TALKTYPE_CHANNEL_R1: + g_chat->talkToChannel(*player, type, text, channelId); + break; + + case TALKTYPE_PRIVATE_PN: + playerSpeakToNpc(player, text); + break; + + case TALKTYPE_BROADCAST: + playerBroadcastMessage(player, text); + break; + + default: + break; + } +} + +bool Game::playerSaySpell(Player* player, SpeakClasses type, const std::string& text) +{ + std::string words = text; + + TalkActionResult_t result = g_talkActions->playerSaySpell(player, type, words); + if (result == TALKACTION_BREAK) { + return true; + } + + result = g_spells->playerSaySpell(player, words); + if (result == TALKACTION_BREAK) { + if (!g_config.getBoolean(ConfigManager::EMOTE_SPELLS)) { + return internalCreatureSay(player, TALKTYPE_SAY, words, false); + } else { + return internalCreatureSay(player, TALKTYPE_MONSTER_SAY, words, false); + } + + } else if (result == TALKACTION_FAILED) { + return true; + } + + return false; +} + +void Game::playerWhisper(Player* player, const std::string& text) +{ + SpectatorVec spectators; + map.getSpectators(spectators, player->getPosition(), false, false, + Map::maxClientViewportX, Map::maxClientViewportX, + Map::maxClientViewportY, Map::maxClientViewportY); + + //send to client + for (Creature* spectator : spectators) { + if (Player* spectatorPlayer = spectator->getPlayer()) { + if (!Position::areInRange<1, 1>(player->getPosition(), spectatorPlayer->getPosition())) { + spectatorPlayer->sendCreatureSay(player, TALKTYPE_WHISPER, "pspsps"); + } else { + spectatorPlayer->sendCreatureSay(player, TALKTYPE_WHISPER, text); + } + } + } + + //event method + for (Creature* spectator : spectators) { + spectator->onCreatureSay(player, TALKTYPE_WHISPER, text); + } +} + +bool Game::playerYell(Player* player, const std::string& text) +{ + if (player->hasCondition(CONDITION_YELLTICKS)) { + player->sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED); + return false; + } + + uint32_t minimumLevel = g_config.getNumber(ConfigManager::YELL_MINIMUM_LEVEL); + if (player->getLevel() < minimumLevel) { + std::ostringstream ss; + ss << "You may not yell unless you have reached level " << minimumLevel; + if (g_config.getBoolean(ConfigManager::YELL_ALLOW_PREMIUM)) { + if (player->isPremium()) { + internalCreatureSay(player, TALKTYPE_YELL, asUpperCaseString(text), false); + return true; + } else { + ss << " or have a premium account"; + } + } + ss << "."; + player->sendTextMessage(MESSAGE_STATUS_SMALL, ss.str()); + return false; + } + + if (player->getAccountType() < ACCOUNT_TYPE_GAMEMASTER) { + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_YELLTICKS, 30000, 0); + player->addCondition(condition); + } + + internalCreatureSay(player, TALKTYPE_YELL, asUpperCaseString(text), false); + return true; +} + +bool Game::playerSpeakTo(Player* player, SpeakClasses type, const std::string& receiver, + const std::string& text) +{ + Player* toPlayer = getPlayerByName(receiver); + if (!toPlayer) { + player->sendTextMessage(MESSAGE_STATUS_SMALL, "A player with this name is not online."); + return false; + } + + if (type == TALKTYPE_PRIVATE_RED_TO && (player->hasFlag(PlayerFlag_CanTalkRedPrivate) || player->getAccountType() >= ACCOUNT_TYPE_GAMEMASTER)) { + type = TALKTYPE_PRIVATE_RED_FROM; + } else { + type = TALKTYPE_PRIVATE_FROM; + } + + toPlayer->sendPrivateMessage(player, type, text); + toPlayer->onCreatureSay(player, type, text); + + if (toPlayer->isInGhostMode() && !player->isAccessPlayer()) { + player->sendTextMessage(MESSAGE_STATUS_SMALL, "A player with this name is not online."); + } else { + std::ostringstream ss; + ss << "Message sent to " << toPlayer->getName() << '.'; + player->sendTextMessage(MESSAGE_STATUS_SMALL, ss.str()); + } + return true; +} + +void Game::playerSpeakToNpc(Player* player, const std::string& text) +{ + SpectatorVec spectators; + map.getSpectators(spectators, player->getPosition()); + for (Creature* spectator : spectators) { + if (spectator->getNpc()) { + spectator->onCreatureSay(player, TALKTYPE_PRIVATE_PN, text); + } + } +} + +//-- +bool Game::canThrowObjectTo(const Position& fromPos, const Position& toPos, bool checkLineOfSight /*= true*/, + int32_t rangex /*= Map::maxClientViewportX*/, int32_t rangey /*= Map::maxClientViewportY*/) const +{ + return map.canThrowObjectTo(fromPos, toPos, checkLineOfSight, rangex, rangey); +} + +bool Game::isSightClear(const Position& fromPos, const Position& toPos, bool floorCheck) const +{ + return map.isSightClear(fromPos, toPos, floorCheck); +} + +bool Game::internalCreatureTurn(Creature* creature, Direction dir) +{ + if (creature->getDirection() == dir) { + return false; + } + + creature->setDirection(dir); + + //send to client + SpectatorVec spectators; + map.getSpectators(spectators, creature->getPosition(), true, true); + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendCreatureTurn(creature); + } + return true; +} + +bool Game::internalCreatureSay(Creature* creature, SpeakClasses type, const std::string& text, + bool ghostMode, SpectatorVec* spectatorsPtr/* = nullptr*/, const Position* pos/* = nullptr*/) +{ + if (text.empty()) { + return false; + } + + if (!pos) { + pos = &creature->getPosition(); + } + + SpectatorVec spectators; + + if (!spectatorsPtr || spectatorsPtr->empty()) { + // This somewhat complex construct ensures that the cached SpectatorVec + // is used if available and if it can be used, else a local vector is + // used (hopefully the compiler will optimize away the construction of + // the temporary when it's not used). + if (type != TALKTYPE_YELL && type != TALKTYPE_MONSTER_YELL) { + map.getSpectators(spectators, *pos, false, false, + Map::maxClientViewportX, Map::maxClientViewportX, + Map::maxClientViewportY, Map::maxClientViewportY); + } else { + map.getSpectators(spectators, *pos, true, false, 18, 18, 14, 14); + } + } else { + spectators = (*spectatorsPtr); + } + + //send to client + for (Creature* spectator : spectators) { + if (Player* tmpPlayer = spectator->getPlayer()) { + if (!ghostMode || tmpPlayer->canSeeCreature(creature)) { + tmpPlayer->sendCreatureSay(creature, type, text, pos); + } + } + } + + //event method + for (Creature* spectator : spectators) { + spectator->onCreatureSay(creature, type, text); + if (creature != spectator) { + g_events->eventCreatureOnHear(spectator, creature, text, type); + } + } + return true; +} + +void Game::checkCreatureWalk(uint32_t creatureId) +{ + Creature* creature = getCreatureByID(creatureId); + if (creature && creature->getHealth() > 0) { + creature->onWalk(); + cleanup(); + } +} + +void Game::updateCreatureWalk(uint32_t creatureId) +{ + Creature* creature = getCreatureByID(creatureId); + if (creature && creature->getHealth() > 0) { + creature->goToFollowCreature(); + } +} + +void Game::checkCreatureAttack(uint32_t creatureId) +{ + Creature* creature = getCreatureByID(creatureId); + if (creature && creature->getHealth() > 0) { + creature->onAttacking(0); + } +} + +void Game::addCreatureCheck(Creature* creature) +{ + creature->creatureCheck = true; + + if (creature->inCheckCreaturesVector) { + // already in a vector + return; + } + + creature->inCheckCreaturesVector = true; + checkCreatureLists[uniform_random(0, EVENT_CREATURECOUNT - 1)].push_back(creature); + creature->incrementReferenceCounter(); +} + +void Game::removeCreatureCheck(Creature* creature) +{ + if (creature->inCheckCreaturesVector) { + creature->creatureCheck = false; + } +} + +void Game::checkCreatures(size_t index) +{ + g_scheduler.addEvent(createSchedulerTask(EVENT_CHECK_CREATURE_INTERVAL, std::bind(&Game::checkCreatures, this, (index + 1) % EVENT_CREATURECOUNT))); + + auto& checkCreatureList = checkCreatureLists[index]; + auto it = checkCreatureList.begin(), end = checkCreatureList.end(); + while (it != end) { + Creature* creature = *it; + if (creature->creatureCheck) { + if (creature->getHealth() > 0) { + creature->onThink(EVENT_CREATURE_THINK_INTERVAL); + creature->onAttacking(EVENT_CREATURE_THINK_INTERVAL); + creature->executeConditions(EVENT_CREATURE_THINK_INTERVAL); + } else { + creature->onDeath(); + } + ++it; + } else { + creature->inCheckCreaturesVector = false; + it = checkCreatureList.erase(it); + ReleaseCreature(creature); + } + } + + cleanup(); +} + +void Game::changeSpeed(Creature* creature, int32_t varSpeedDelta) +{ + int32_t varSpeed = creature->getSpeed() - creature->getBaseSpeed(); + varSpeed += varSpeedDelta; + + creature->setSpeed(varSpeed); + + //send to clients + SpectatorVec spectators; + map.getSpectators(spectators, creature->getPosition(), false, true); + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendChangeSpeed(creature, creature->getStepSpeed()); + } +} + +void Game::internalCreatureChangeOutfit(Creature* creature, const Outfit_t& outfit) +{ + if (!g_events->eventCreatureOnChangeOutfit(creature, outfit)) { + return; + } + + creature->setCurrentOutfit(outfit); + + if (creature->isInvisible()) { + return; + } + + //send to clients + SpectatorVec spectators; + map.getSpectators(spectators, creature->getPosition(), true, true); + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendCreatureChangeOutfit(creature, outfit); + } +} + +void Game::internalCreatureChangeVisible(Creature* creature, bool visible) +{ + //send to clients + SpectatorVec spectators; + map.getSpectators(spectators, creature->getPosition(), true, true); + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendCreatureChangeVisible(creature, visible); + } +} + +void Game::changeLight(const Creature* creature) +{ + //send to clients + SpectatorVec spectators; + map.getSpectators(spectators, creature->getPosition(), true, true); + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendCreatureLight(creature); + } +} + +bool Game::combatBlockHit(CombatDamage& damage, Creature* attacker, Creature* target, bool checkDefense, bool checkArmor, bool field) +{ + if (damage.primary.type == COMBAT_NONE && damage.secondary.type == COMBAT_NONE) { + return true; + } + + if (target->getPlayer() && target->isInGhostMode()) { + return true; + } + + if (damage.primary.value > 0) { + return false; + } + + static const auto sendBlockEffect = [this](BlockType_t blockType, CombatType_t combatType, const Position& targetPos) { + if (blockType == BLOCK_DEFENSE) { + addMagicEffect(targetPos, CONST_ME_POFF); + } else if (blockType == BLOCK_ARMOR) { + addMagicEffect(targetPos, CONST_ME_BLOCKHIT); + } else if (blockType == BLOCK_IMMUNITY) { + uint8_t hitEffect = 0; + switch (combatType) { + case COMBAT_UNDEFINEDDAMAGE: { + return; + } + case COMBAT_ENERGYDAMAGE: + case COMBAT_FIREDAMAGE: + case COMBAT_PHYSICALDAMAGE: + case COMBAT_ICEDAMAGE: + case COMBAT_DEATHDAMAGE: { + hitEffect = CONST_ME_BLOCKHIT; + break; + } + case COMBAT_EARTHDAMAGE: { + hitEffect = CONST_ME_GREEN_RINGS; + break; + } + case COMBAT_HOLYDAMAGE: { + hitEffect = CONST_ME_HOLYDAMAGE; + break; + } + default: { + hitEffect = CONST_ME_POFF; + break; + } + } + addMagicEffect(targetPos, hitEffect); + } + }; + + BlockType_t primaryBlockType, secondaryBlockType; + if (damage.primary.type != COMBAT_NONE) { + damage.primary.value = -damage.primary.value; + primaryBlockType = target->blockHit(attacker, damage.primary.type, damage.primary.value, checkDefense, checkArmor, field); + + damage.primary.value = -damage.primary.value; + sendBlockEffect(primaryBlockType, damage.primary.type, target->getPosition()); + } else { + primaryBlockType = BLOCK_NONE; + } + + if (damage.secondary.type != COMBAT_NONE) { + damage.secondary.value = -damage.secondary.value; + secondaryBlockType = target->blockHit(attacker, damage.secondary.type, damage.secondary.value, false, false, field); + damage.secondary.value = -damage.secondary.value; + sendBlockEffect(secondaryBlockType, damage.secondary.type, target->getPosition()); + } else { + secondaryBlockType = BLOCK_NONE; + } + + damage.blockType = primaryBlockType; + + return (primaryBlockType != BLOCK_NONE) && (secondaryBlockType != BLOCK_NONE); +} + +void Game::combatGetTypeInfo(CombatType_t combatType, Creature* target, TextColor_t& color, uint8_t& effect) +{ + switch (combatType) { + case COMBAT_PHYSICALDAMAGE: { + Item* splash = nullptr; + switch (target->getRace()) { + case RACE_VENOM: + color = TEXTCOLOR_LIGHTGREEN; + effect = CONST_ME_HITBYPOISON; + splash = Item::CreateItem(ITEM_SMALLSPLASH, FLUID_SLIME); + break; + case RACE_BLOOD: + color = TEXTCOLOR_RED; + effect = CONST_ME_DRAWBLOOD; + if (const Tile* tile = target->getTile()) { + if (!tile->hasFlag(TILESTATE_PROTECTIONZONE)) { + splash = Item::CreateItem(ITEM_SMALLSPLASH, FLUID_BLOOD); + } + } + break; + case RACE_UNDEAD: + color = TEXTCOLOR_LIGHTGREY; + effect = CONST_ME_HITAREA; + break; + case RACE_FIRE: + color = TEXTCOLOR_ORANGE; + effect = CONST_ME_DRAWBLOOD; + break; + case RACE_ENERGY: + color = TEXTCOLOR_ELECTRICPURPLE; + effect = CONST_ME_ENERGYHIT; + break; + default: + color = TEXTCOLOR_NONE; + effect = CONST_ME_NONE; + break; + } + + if (splash) { + internalAddItem(target->getTile(), splash, INDEX_WHEREEVER, FLAG_NOLIMIT); + startDecay(splash); + } + + break; + } + + case COMBAT_ENERGYDAMAGE: { + color = TEXTCOLOR_ELECTRICPURPLE; + effect = CONST_ME_ENERGYHIT; + break; + } + + case COMBAT_EARTHDAMAGE: { + color = TEXTCOLOR_LIGHTGREEN; + effect = CONST_ME_GREEN_RINGS; + break; + } + + case COMBAT_DROWNDAMAGE: { + color = TEXTCOLOR_LIGHTBLUE; + effect = CONST_ME_LOSEENERGY; + break; + } + case COMBAT_FIREDAMAGE: { + color = TEXTCOLOR_ORANGE; + effect = CONST_ME_HITBYFIRE; + break; + } + case COMBAT_ICEDAMAGE: { + color = TEXTCOLOR_SKYBLUE; + effect = CONST_ME_ICEATTACK; + break; + } + case COMBAT_HOLYDAMAGE: { + color = TEXTCOLOR_YELLOW; + effect = CONST_ME_HOLYDAMAGE; + break; + } + case COMBAT_DEATHDAMAGE: { + color = TEXTCOLOR_DARKRED; + effect = CONST_ME_SMALLCLOUDS; + break; + } + case COMBAT_LIFEDRAIN: { + color = TEXTCOLOR_RED; + effect = CONST_ME_MAGIC_RED; + break; + } + default: { + color = TEXTCOLOR_NONE; + effect = CONST_ME_NONE; + break; + } + } +} + +bool Game::combatChangeHealth(Creature* attacker, Creature* target, CombatDamage& damage) +{ + const Position& targetPos = target->getPosition(); + if (damage.primary.value > 0) { + if (target->getHealth() <= 0) { + return false; + } + + Player* attackerPlayer; + if (attacker) { + attackerPlayer = attacker->getPlayer(); + } else { + attackerPlayer = nullptr; + } + + Player* targetPlayer = target->getPlayer(); + if (attackerPlayer && targetPlayer && attackerPlayer->getSkull() == SKULL_BLACK && attackerPlayer->getSkullClient(targetPlayer) == SKULL_NONE) { + return false; + } + + if (damage.origin != ORIGIN_NONE) { + const auto& events = target->getCreatureEvents(CREATURE_EVENT_HEALTHCHANGE); + if (!events.empty()) { + for (CreatureEvent* creatureEvent : events) { + creatureEvent->executeHealthChange(target, attacker, damage); + } + damage.origin = ORIGIN_NONE; + return combatChangeHealth(attacker, target, damage); + } + } + + int32_t realHealthChange = target->getHealth(); + target->gainHealth(attacker, damage.primary.value); + realHealthChange = target->getHealth() - realHealthChange; + + if (realHealthChange > 0 && !target->isInGhostMode()) { + std::stringstream ss; + + ss << realHealthChange << (realHealthChange != 1 ? " hitpoints." : " hitpoint."); + std::string damageString = ss.str(); + + std::string spectatorMessage; + + TextMessage message; + message.position = targetPos; + message.primary.value = realHealthChange; + message.primary.color = TEXTCOLOR_PASTELRED; + + SpectatorVec spectators; + map.getSpectators(spectators, targetPos, false, true); + for (Creature* spectator : spectators) { + Player* tmpPlayer = spectator->getPlayer(); + if (tmpPlayer == attackerPlayer && attackerPlayer != targetPlayer) { + ss.str({}); + ss << "You heal " << target->getNameDescription() << " for " << damageString; + message.type = MESSAGE_HEALED; + message.text = ss.str(); + } else if (tmpPlayer == targetPlayer) { + ss.str({}); + if (!attacker) { + ss << "You were healed"; + } else if (targetPlayer == attackerPlayer) { + ss << "You healed yourself"; + } else { + ss << "You were healed by " << attacker->getNameDescription(); + } + ss << " for " << damageString; + message.type = MESSAGE_HEALED; + message.text = ss.str(); + } else { + if (spectatorMessage.empty()) { + ss.str({}); + if (!attacker) { + ss << ucfirst(target->getNameDescription()) << " was healed"; + } else { + ss << ucfirst(attacker->getNameDescription()) << " healed "; + if (attacker == target) { + ss << (targetPlayer ? (targetPlayer->getSex() == PLAYERSEX_FEMALE ? "herself" : "himself") : "itself"); + } else { + ss << target->getNameDescription(); + } + } + ss << " for " << damageString; + spectatorMessage = ss.str(); + } + message.type = MESSAGE_HEALED_OTHERS; + message.text = spectatorMessage; + } + tmpPlayer->sendTextMessage(message); + } + } + } else { + if (!target->isAttackable()) { + if (!target->isInGhostMode()) { + addMagicEffect(targetPos, CONST_ME_POFF); + } + return true; + } + + Player* attackerPlayer; + if (attacker) { + attackerPlayer = attacker->getPlayer(); + } else { + attackerPlayer = nullptr; + } + + Player* targetPlayer = target->getPlayer(); + if (attackerPlayer && targetPlayer && attackerPlayer->getSkull() == SKULL_BLACK && attackerPlayer->getSkullClient(targetPlayer) == SKULL_NONE) { + return false; + } + + damage.primary.value = std::abs(damage.primary.value); + damage.secondary.value = std::abs(damage.secondary.value); + + int32_t healthChange = damage.primary.value + damage.secondary.value; + if (healthChange == 0) { + return true; + } + + TextMessage message; + message.position = targetPos; + + SpectatorVec spectators; + if (targetPlayer && target->hasCondition(CONDITION_MANASHIELD) && damage.primary.type != COMBAT_UNDEFINEDDAMAGE) { + int32_t manaDamage = std::min(targetPlayer->getMana(), healthChange); + if (manaDamage != 0) { + if (damage.origin != ORIGIN_NONE) { + const auto& events = target->getCreatureEvents(CREATURE_EVENT_MANACHANGE); + if (!events.empty()) { + for (CreatureEvent* creatureEvent : events) { + creatureEvent->executeManaChange(target, attacker, damage); + } + healthChange = damage.primary.value + damage.secondary.value; + if (healthChange == 0) { + return true; + } + manaDamage = std::min(targetPlayer->getMana(), healthChange); + } + } + + targetPlayer->drainMana(attacker, manaDamage); + map.getSpectators(spectators, targetPos, true, true); + addMagicEffect(spectators, targetPos, CONST_ME_LOSEENERGY); + + std::stringstream ss; + + std::string damageString = std::to_string(manaDamage); + + std::string spectatorMessage; + + message.primary.value = manaDamage; + message.primary.color = TEXTCOLOR_BLUE; + + for (Creature* spectator : spectators) { + Player* tmpPlayer = spectator->getPlayer(); + if (tmpPlayer->getPosition().z != targetPos.z) { + continue; + } + + if (tmpPlayer == attackerPlayer && attackerPlayer != targetPlayer) { + ss.str({}); + ss << ucfirst(target->getNameDescription()) << " loses " << damageString + " mana due to your attack."; + message.type = MESSAGE_DAMAGE_DEALT; + message.text = ss.str(); + } else if (tmpPlayer == targetPlayer) { + ss.str({}); + ss << "You lose " << damageString << " mana"; + if (!attacker) { + ss << '.'; + } else if (targetPlayer == attackerPlayer) { + ss << " due to your own attack."; + } else { + ss << " due to an attack by " << attacker->getNameDescription() << '.'; + } + message.type = MESSAGE_DAMAGE_RECEIVED; + message.text = ss.str(); + } else { + if (spectatorMessage.empty()) { + ss.str({}); + ss << ucfirst(target->getNameDescription()) << " loses " << damageString + " mana"; + if (attacker) { + ss << " due to "; + if (attacker == target) { + ss << (targetPlayer->getSex() == PLAYERSEX_FEMALE ? "her own attack" : "his own attack"); + } else { + ss << "an attack by " << attacker->getNameDescription(); + } + } + ss << '.'; + spectatorMessage = ss.str(); + } + message.type = MESSAGE_DAMAGE_OTHERS; + message.text = spectatorMessage; + } + tmpPlayer->sendTextMessage(message); + } + + damage.primary.value -= manaDamage; + if (damage.primary.value < 0) { + damage.secondary.value = std::max(0, damage.secondary.value + damage.primary.value); + damage.primary.value = 0; + } + } + } + + int32_t realDamage = damage.primary.value + damage.secondary.value; + if (realDamage == 0) { + return true; + } + + if (damage.origin != ORIGIN_NONE) { + const auto& events = target->getCreatureEvents(CREATURE_EVENT_HEALTHCHANGE); + if (!events.empty()) { + for (CreatureEvent* creatureEvent : events) { + creatureEvent->executeHealthChange(target, attacker, damage); + } + damage.origin = ORIGIN_NONE; + return combatChangeHealth(attacker, target, damage); + } + } + + int32_t targetHealth = target->getHealth(); + if (damage.primary.value >= targetHealth) { + damage.primary.value = targetHealth; + damage.secondary.value = 0; + } else if (damage.secondary.value) { + damage.secondary.value = std::min(damage.secondary.value, targetHealth - damage.primary.value); + } + + realDamage = damage.primary.value + damage.secondary.value; + if (realDamage == 0) { + return true; + } + + if (spectators.empty()) { + map.getSpectators(spectators, targetPos, true, true); + } + + message.primary.value = damage.primary.value; + message.secondary.value = damage.secondary.value; + + uint8_t hitEffect; + if (message.primary.value) { + combatGetTypeInfo(damage.primary.type, target, message.primary.color, hitEffect); + if (hitEffect != CONST_ME_NONE) { + addMagicEffect(spectators, targetPos, hitEffect); + } + } + + if (message.secondary.value) { + combatGetTypeInfo(damage.secondary.type, target, message.secondary.color, hitEffect); + if (hitEffect != CONST_ME_NONE) { + addMagicEffect(spectators, targetPos, hitEffect); + } + } + + if (message.primary.color != TEXTCOLOR_NONE || message.secondary.color != TEXTCOLOR_NONE) { + std::stringstream ss; + + ss << realDamage << (realDamage != 1 ? " hitpoints" : " hitpoint"); + std::string damageString = ss.str(); + + std::string spectatorMessage; + + for (Creature* spectator : spectators) { + Player* tmpPlayer = spectator->getPlayer(); + if (tmpPlayer->getPosition().z != targetPos.z) { + continue; + } + + if (tmpPlayer == attackerPlayer && attackerPlayer != targetPlayer) { + ss.str({}); + ss << ucfirst(target->getNameDescription()) << " loses " << damageString << " due to your attack."; + message.type = MESSAGE_DAMAGE_DEALT; + message.text = ss.str(); + } else if (tmpPlayer == targetPlayer) { + ss.str({}); + ss << "You lose " << damageString; + if (!attacker) { + ss << '.'; + } else if (targetPlayer == attackerPlayer) { + ss << " due to your own attack."; + } else { + ss << " due to an attack by " << attacker->getNameDescription() << '.'; + } + message.type = MESSAGE_DAMAGE_RECEIVED; + message.text = ss.str(); + } else { + message.type = MESSAGE_DAMAGE_OTHERS; + + if (spectatorMessage.empty()) { + ss.str({}); + ss << ucfirst(target->getNameDescription()) << " loses " << damageString; + if (attacker) { + ss << " due to "; + if (attacker == target) { + if (targetPlayer) { + ss << (targetPlayer->getSex() == PLAYERSEX_FEMALE ? "her own attack" : "his own attack"); + } else { + ss << "its own attack"; + } + } else { + ss << "an attack by " << attacker->getNameDescription(); + } + } + ss << '.'; + spectatorMessage = ss.str(); + } + + message.text = spectatorMessage; + } + tmpPlayer->sendTextMessage(message); + } + } + + if (realDamage >= targetHealth) { + for (CreatureEvent* creatureEvent : target->getCreatureEvents(CREATURE_EVENT_PREPAREDEATH)) { + if (!creatureEvent->executeOnPrepareDeath(target, attacker)) { + return false; + } + } + } + + target->drainHealth(attacker, realDamage); + addCreatureHealth(spectators, target); + } + + return true; +} + +bool Game::combatChangeMana(Creature* attacker, Creature* target, CombatDamage& damage) +{ + Player* targetPlayer = target->getPlayer(); + if (!targetPlayer) { + return true; + } + + int32_t manaChange = damage.primary.value + damage.secondary.value; + if (manaChange > 0) { + if (attacker) { + const Player* attackerPlayer = attacker->getPlayer(); + if (attackerPlayer && attackerPlayer->getSkull() == SKULL_BLACK && attackerPlayer->getSkullClient(target) == SKULL_NONE) { + return false; + } + } + + if (damage.origin != ORIGIN_NONE) { + const auto& events = target->getCreatureEvents(CREATURE_EVENT_MANACHANGE); + if (!events.empty()) { + for (CreatureEvent* creatureEvent : events) { + creatureEvent->executeManaChange(target, attacker, damage); + } + damage.origin = ORIGIN_NONE; + return combatChangeMana(attacker, target, damage); + } + } + + int32_t realManaChange = targetPlayer->getMana(); + targetPlayer->changeMana(manaChange); + realManaChange = targetPlayer->getMana() - realManaChange; + + if (realManaChange > 0 && !targetPlayer->isInGhostMode()) { + TextMessage message(MESSAGE_HEALED, "You gained " + std::to_string(realManaChange) + " mana."); + message.position = target->getPosition(); + message.primary.value = realManaChange; + message.primary.color = TEXTCOLOR_MAYABLUE; + targetPlayer->sendTextMessage(message); + } + } else { + const Position& targetPos = target->getPosition(); + if (!target->isAttackable()) { + if (!target->isInGhostMode()) { + addMagicEffect(targetPos, CONST_ME_POFF); + } + return false; + } + + Player* attackerPlayer; + if (attacker) { + attackerPlayer = attacker->getPlayer(); + } else { + attackerPlayer = nullptr; + } + + if (attackerPlayer && attackerPlayer->getSkull() == SKULL_BLACK && attackerPlayer->getSkullClient(targetPlayer) == SKULL_NONE) { + return false; + } + + int32_t manaLoss = std::min(targetPlayer->getMana(), -manaChange); + BlockType_t blockType = target->blockHit(attacker, COMBAT_MANADRAIN, manaLoss); + if (blockType != BLOCK_NONE) { + addMagicEffect(targetPos, CONST_ME_POFF); + return false; + } + + if (manaLoss <= 0) { + return true; + } + + if (damage.origin != ORIGIN_NONE) { + const auto& events = target->getCreatureEvents(CREATURE_EVENT_MANACHANGE); + if (!events.empty()) { + for (CreatureEvent* creatureEvent : events) { + creatureEvent->executeManaChange(target, attacker, damage); + } + damage.origin = ORIGIN_NONE; + return combatChangeMana(attacker, target, damage); + } + } + + targetPlayer->drainMana(attacker, manaLoss); + + std::stringstream ss; + + std::string damageString = std::to_string(manaLoss); + + std::string spectatorMessage; + + TextMessage message; + message.position = targetPos; + message.primary.value = manaLoss; + message.primary.color = TEXTCOLOR_BLUE; + + SpectatorVec spectators; + map.getSpectators(spectators, targetPos, false, true); + for (Creature* spectator : spectators) { + Player* tmpPlayer = spectator->getPlayer(); + if (tmpPlayer == attackerPlayer && attackerPlayer != targetPlayer) { + ss.str({}); + ss << ucfirst(target->getNameDescription()) << " loses " << damageString << " mana due to your attack."; + message.type = MESSAGE_DAMAGE_DEALT; + message.text = ss.str(); + } else if (tmpPlayer == targetPlayer) { + ss.str({}); + ss << "You lose " << damageString << " mana"; + if (!attacker) { + ss << '.'; + } else if (targetPlayer == attackerPlayer) { + ss << " due to your own attack."; + } else { + ss << " mana due to an attack by " << attacker->getNameDescription() << '.'; + } + message.type = MESSAGE_DAMAGE_RECEIVED; + message.text = ss.str(); + } else { + if (spectatorMessage.empty()) { + ss.str({}); + ss << ucfirst(target->getNameDescription()) << " loses " << damageString << " mana"; + if (attacker) { + ss << " due to "; + if (attacker == target) { + ss << (targetPlayer->getSex() == PLAYERSEX_FEMALE ? "her own attack" : "his own attack"); + } else { + ss << "an attack by " << attacker->getNameDescription(); + } + } + ss << '.'; + spectatorMessage = ss.str(); + } + message.type = MESSAGE_DAMAGE_OTHERS; + message.text = spectatorMessage; + } + tmpPlayer->sendTextMessage(message); + } + } + + return true; +} + +void Game::addCreatureHealth(const Creature* target) +{ + SpectatorVec spectators; + map.getSpectators(spectators, target->getPosition(), true, true); + addCreatureHealth(spectators, target); +} + +void Game::addCreatureHealth(const SpectatorVec& spectators, const Creature* target) +{ + for (Creature* spectator : spectators) { + if (Player* tmpPlayer = spectator->getPlayer()) { + tmpPlayer->sendCreatureHealth(target); + } + } +} + +void Game::addMagicEffect(const Position& pos, uint8_t effect) +{ + SpectatorVec spectators; + map.getSpectators(spectators, pos, true, true); + addMagicEffect(spectators, pos, effect); +} + +void Game::addMagicEffect(const SpectatorVec& spectators, const Position& pos, uint8_t effect) +{ + for (Creature* spectator : spectators) { + if (Player* tmpPlayer = spectator->getPlayer()) { + tmpPlayer->sendMagicEffect(pos, effect); + } + } +} + +void Game::addDistanceEffect(const Position& fromPos, const Position& toPos, uint8_t effect) +{ + SpectatorVec spectators, toPosSpectators; + map.getSpectators(spectators, fromPos, false, true); + map.getSpectators(toPosSpectators, toPos, false, true); + spectators.addSpectators(toPosSpectators); + + addDistanceEffect(spectators, fromPos, toPos, effect); +} + +void Game::addDistanceEffect(const SpectatorVec& spectators, const Position& fromPos, const Position& toPos, uint8_t effect) +{ + for (Creature* spectator : spectators) { + if (Player* tmpPlayer = spectator->getPlayer()) { + tmpPlayer->sendDistanceShoot(fromPos, toPos, effect); + } + } +} + +void Game::startDecay(Item* item) +{ + if (!item || !item->canDecay()) { + return; + } + + ItemDecayState_t decayState = item->getDecaying(); + if (decayState == DECAYING_TRUE) { + return; + } + + if (item->getDuration() > 0) { + item->incrementReferenceCounter(); + item->setDecaying(DECAYING_TRUE); + toDecayItems.push_front(item); + } else { + internalDecayItem(item); + } +} + +void Game::internalDecayItem(Item* item) +{ + const ItemType& it = Item::items[item->getID()]; + if (it.decayTo != 0) { + Item* newItem = transformItem(item, item->getDecayTo()); + startDecay(newItem); + } else { + ReturnValue ret = internalRemoveItem(item); + if (ret != RETURNVALUE_NOERROR) { + std::cout << "[Debug - Game::internalDecayItem] internalDecayItem failed, error code: " << static_cast(ret) << ", item id: " << item->getID() << std::endl; + } + } +} + +void Game::checkDecay() +{ + g_scheduler.addEvent(createSchedulerTask(EVENT_DECAYINTERVAL, std::bind(&Game::checkDecay, this))); + + size_t bucket = (lastBucket + 1) % EVENT_DECAY_BUCKETS; + + auto it = decayItems[bucket].begin(), end = decayItems[bucket].end(); + while (it != end) { + Item* item = *it; + if (!item->canDecay()) { + item->setDecaying(DECAYING_FALSE); + ReleaseItem(item); + it = decayItems[bucket].erase(it); + continue; + } + + int32_t duration = item->getDuration(); + int32_t decreaseTime = std::min(EVENT_DECAYINTERVAL * EVENT_DECAY_BUCKETS, duration); + + duration -= decreaseTime; + item->decreaseDuration(decreaseTime); + + if (duration <= 0) { + it = decayItems[bucket].erase(it); + internalDecayItem(item); + ReleaseItem(item); + } else if (duration < EVENT_DECAYINTERVAL * EVENT_DECAY_BUCKETS) { + it = decayItems[bucket].erase(it); + size_t newBucket = (bucket + ((duration + EVENT_DECAYINTERVAL / 2) / 1000)) % EVENT_DECAY_BUCKETS; + if (newBucket == bucket) { + internalDecayItem(item); + ReleaseItem(item); + } else { + decayItems[newBucket].push_back(item); + } + } else { + ++it; + } + } + + lastBucket = bucket; + cleanup(); +} + +void Game::checkLight() +{ + g_scheduler.addEvent(createSchedulerTask(EVENT_LIGHTINTERVAL, std::bind(&Game::checkLight, this))); + + lightHour += lightHourDelta; + + if (lightHour > 1440) { + lightHour -= 1440; + } + + if (std::abs(lightHour - SUNRISE) < 2 * lightHourDelta) { + lightState = LIGHT_STATE_SUNRISE; + } else if (std::abs(lightHour - SUNSET) < 2 * lightHourDelta) { + lightState = LIGHT_STATE_SUNSET; + } + + int32_t newLightLevel = lightLevel; + bool lightChange = false; + + switch (lightState) { + case LIGHT_STATE_SUNRISE: { + newLightLevel += (LIGHT_LEVEL_DAY - LIGHT_LEVEL_NIGHT) / 30; + lightChange = true; + break; + } + case LIGHT_STATE_SUNSET: { + newLightLevel -= (LIGHT_LEVEL_DAY - LIGHT_LEVEL_NIGHT) / 30; + lightChange = true; + break; + } + default: + break; + } + + if (newLightLevel <= LIGHT_LEVEL_NIGHT) { + lightLevel = LIGHT_LEVEL_NIGHT; + lightState = LIGHT_STATE_NIGHT; + } else if (newLightLevel >= LIGHT_LEVEL_DAY) { + lightLevel = LIGHT_LEVEL_DAY; + lightState = LIGHT_STATE_DAY; + } else { + lightLevel = newLightLevel; + } + + if (lightChange) { + LightInfo lightInfo = getWorldLightInfo(); + + for (const auto& it : players) { + it.second->sendWorldLight(lightInfo); + } + } +} + +LightInfo Game::getWorldLightInfo() const +{ + return {lightLevel, 0xD7}; +} + +void Game::shutdown() +{ + std::cout << "Shutting down..." << std::flush; + + g_scheduler.shutdown(); + g_databaseTasks.shutdown(); + g_dispatcher.shutdown(); + map.spawns.clear(); + raids.clear(); + + cleanup(); + + if (serviceManager) { + serviceManager->stop(); + } + + ConnectionManager::getInstance().closeAll(); + + std::cout << " done!" << std::endl; +} + +void Game::cleanup() +{ + //free memory + for (auto creature : ToReleaseCreatures) { + creature->decrementReferenceCounter(); + } + ToReleaseCreatures.clear(); + + for (auto item : ToReleaseItems) { + item->decrementReferenceCounter(); + } + ToReleaseItems.clear(); + + for (Item* item : toDecayItems) { + const uint32_t dur = item->getDuration(); + if (dur >= EVENT_DECAYINTERVAL * EVENT_DECAY_BUCKETS) { + decayItems[lastBucket].push_back(item); + } else { + decayItems[(lastBucket + 1 + dur / 1000) % EVENT_DECAY_BUCKETS].push_back(item); + } + } + toDecayItems.clear(); +} + +void Game::ReleaseCreature(Creature* creature) +{ + ToReleaseCreatures.push_back(creature); +} + +void Game::ReleaseItem(Item* item) +{ + ToReleaseItems.push_back(item); +} + +void Game::broadcastMessage(const std::string& text, MessageClasses type) const +{ + std::cout << "> Broadcasted message: \"" << text << "\"." << std::endl; + for (const auto& it : players) { + it.second->sendTextMessage(type, text); + } +} + +void Game::updateCreatureWalkthrough(const Creature* creature) +{ + //send to clients + SpectatorVec spectators; + map.getSpectators(spectators, creature->getPosition(), true, true); + for (Creature* spectator : spectators) { + Player* tmpPlayer = spectator->getPlayer(); + tmpPlayer->sendCreatureWalkthrough(creature, tmpPlayer->canWalkthroughEx(creature)); + } +} + +void Game::updateCreatureSkull(const Creature* creature) +{ + if (getWorldType() != WORLD_TYPE_PVP) { + return; + } + + SpectatorVec spectators; + map.getSpectators(spectators, creature->getPosition(), true, true); + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendCreatureSkull(creature); + } +} + +void Game::updatePlayerShield(Player* player) +{ + SpectatorVec spectators; + map.getSpectators(spectators, player->getPosition(), true, true); + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendCreatureShield(player); + } +} + +void Game::updatePlayerHelpers(const Player& player) +{ + uint32_t creatureId = player.getID(); + uint16_t helpers = player.getHelpers(); + + SpectatorVec spectators; + map.getSpectators(spectators, player.getPosition(), true, true); + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendCreatureHelpers(creatureId, helpers); + } +} + +void Game::updateCreatureType(Creature* creature) +{ + const Player* masterPlayer = nullptr; + + uint32_t creatureId = creature->getID(); + CreatureType_t creatureType = creature->getType(); + if (creatureType == CREATURETYPE_MONSTER) { + const Creature* master = creature->getMaster(); + if (master) { + masterPlayer = master->getPlayer(); + if (masterPlayer) { + creatureType = CREATURETYPE_SUMMON_OTHERS; + } + } + } + + //send to clients + SpectatorVec spectators; + map.getSpectators(spectators, creature->getPosition(), true, true); + + if (creatureType == CREATURETYPE_SUMMON_OTHERS) { + for (Creature* spectator : spectators) { + Player* player = spectator->getPlayer(); + if (masterPlayer == player) { + player->sendCreatureType(creatureId, CREATURETYPE_SUMMON_OWN); + } else { + player->sendCreatureType(creatureId, creatureType); + } + } + } else { + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendCreatureType(creatureId, creatureType); + } + } +} + +void Game::updatePremium(Account& account) +{ + bool save = false; + time_t timeNow = time(nullptr); + + if (account.premiumDays != 0 && account.premiumDays != std::numeric_limits::max()) { + if (account.lastDay == 0) { + account.lastDay = timeNow; + save = true; + } else { + uint32_t days = (timeNow - account.lastDay) / 86400; + if (days > 0) { + if (days >= account.premiumDays) { + account.premiumDays = 0; + account.lastDay = 0; + } else { + account.premiumDays -= days; + time_t remainder = (timeNow - account.lastDay) % 86400; + account.lastDay = timeNow - remainder; + } + + save = true; + } + } + } else if (account.lastDay != 0) { + account.lastDay = 0; + save = true; + } + + if (save && !IOLoginData::saveAccount(account)) { + std::cout << "> ERROR: Failed to save account: " << account.name << "!" << std::endl; + } +} + +void Game::loadMotdNum() +{ + Database& db = Database::getInstance(); + + DBResult_ptr result = db.storeQuery("SELECT `value` FROM `server_config` WHERE `config` = 'motd_num'"); + if (result) { + motdNum = result->getNumber("value"); + } else { + db.executeQuery("INSERT INTO `server_config` (`config`, `value`) VALUES ('motd_num', '0')"); + } + + result = db.storeQuery("SELECT `value` FROM `server_config` WHERE `config` = 'motd_hash'"); + if (result) { + motdHash = result->getString("value"); + if (motdHash != transformToSHA1(g_config.getString(ConfigManager::MOTD))) { + ++motdNum; + } + } else { + db.executeQuery("INSERT INTO `server_config` (`config`, `value`) VALUES ('motd_hash', '')"); + } +} + +void Game::saveMotdNum() const +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "UPDATE `server_config` SET `value` = '" << motdNum << "' WHERE `config` = 'motd_num'"; + db.executeQuery(query.str()); + + query.str(std::string()); + query << "UPDATE `server_config` SET `value` = '" << transformToSHA1(g_config.getString(ConfigManager::MOTD)) << "' WHERE `config` = 'motd_hash'"; + db.executeQuery(query.str()); +} + +void Game::checkPlayersRecord() +{ + const size_t playersOnline = getPlayersOnline(); + if (playersOnline > playersRecord) { + uint32_t previousRecord = playersRecord; + playersRecord = playersOnline; + + for (auto& it : g_globalEvents->getEventMap(GLOBALEVENT_RECORD)) { + it.second.executeRecord(playersRecord, previousRecord); + } + updatePlayersRecord(); + } +} + +void Game::updatePlayersRecord() const +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "UPDATE `server_config` SET `value` = '" << playersRecord << "' WHERE `config` = 'players_record'"; + db.executeQuery(query.str()); +} + +void Game::loadPlayersRecord() +{ + Database& db = Database::getInstance(); + + DBResult_ptr result = db.storeQuery("SELECT `value` FROM `server_config` WHERE `config` = 'players_record'"); + if (result) { + playersRecord = result->getNumber("value"); + } else { + db.executeQuery("INSERT INTO `server_config` (`config`, `value`) VALUES ('players_record', '0')"); + } +} + +uint64_t Game::getExperienceStage(uint32_t level) +{ + if (!stagesEnabled) { + return g_config.getNumber(ConfigManager::RATE_EXPERIENCE); + } + + if (useLastStageLevel && level >= lastStageLevel) { + return stages[lastStageLevel]; + } + + return stages[level]; +} + +bool Game::loadExperienceStages() +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/XML/stages.xml"); + if (!result) { + printXMLError("Error - Game::loadExperienceStages", "data/XML/stages.xml", result); + return false; + } + + for (auto stageNode : doc.child("stages").children()) { + if (strcasecmp(stageNode.name(), "config") == 0) { + stagesEnabled = stageNode.attribute("enabled").as_bool(); + } else { + uint32_t minLevel, maxLevel, multiplier; + + pugi::xml_attribute minLevelAttribute = stageNode.attribute("minlevel"); + if (minLevelAttribute) { + minLevel = pugi::cast(minLevelAttribute.value()); + } else { + minLevel = 1; + } + + pugi::xml_attribute maxLevelAttribute = stageNode.attribute("maxlevel"); + if (maxLevelAttribute) { + maxLevel = pugi::cast(maxLevelAttribute.value()); + } else { + maxLevel = 0; + lastStageLevel = minLevel; + useLastStageLevel = true; + } + + pugi::xml_attribute multiplierAttribute = stageNode.attribute("multiplier"); + if (multiplierAttribute) { + multiplier = pugi::cast(multiplierAttribute.value()); + } else { + multiplier = 1; + } + + if (useLastStageLevel) { + stages[lastStageLevel] = multiplier; + } else { + for (uint32_t i = minLevel; i <= maxLevel; ++i) { + stages[i] = multiplier; + } + } + } + } + return true; +} + +void Game::playerInviteToParty(uint32_t playerId, uint32_t invitedId) +{ + if (playerId == invitedId) { + return; + } + + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Player* invitedPlayer = getPlayerByID(invitedId); + if (!invitedPlayer || invitedPlayer->isInviting(player)) { + return; + } + + if (invitedPlayer->getParty()) { + std::ostringstream ss; + ss << invitedPlayer->getName() << " is already in a party."; + player->sendTextMessage(MESSAGE_INFO_DESCR, ss.str()); + return; + } + + Party* party = player->getParty(); + if (!party) { + party = new Party(player); + } else if (party->getLeader() != player) { + return; + } + + party->invitePlayer(*invitedPlayer); +} + +void Game::playerJoinParty(uint32_t playerId, uint32_t leaderId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Player* leader = getPlayerByID(leaderId); + if (!leader || !leader->isInviting(player)) { + return; + } + + Party* party = leader->getParty(); + if (!party || party->getLeader() != leader) { + return; + } + + if (player->getParty()) { + player->sendTextMessage(MESSAGE_INFO_DESCR, "You are already in a party."); + return; + } + + party->joinParty(*player); +} + +void Game::playerRevokePartyInvitation(uint32_t playerId, uint32_t invitedId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Party* party = player->getParty(); + if (!party || party->getLeader() != player) { + return; + } + + Player* invitedPlayer = getPlayerByID(invitedId); + if (!invitedPlayer || !player->isInviting(invitedPlayer)) { + return; + } + + party->revokeInvitation(*invitedPlayer); +} + +void Game::playerPassPartyLeadership(uint32_t playerId, uint32_t newLeaderId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Party* party = player->getParty(); + if (!party || party->getLeader() != player) { + return; + } + + Player* newLeader = getPlayerByID(newLeaderId); + if (!newLeader || !player->isPartner(newLeader)) { + return; + } + + party->passPartyLeadership(newLeader); +} + +void Game::playerLeaveParty(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Party* party = player->getParty(); + if (!party || player->hasCondition(CONDITION_INFIGHT)) { + return; + } + + party->leaveParty(player); +} + +void Game::playerEnableSharedPartyExperience(uint32_t playerId, bool sharedExpActive) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Party* party = player->getParty(); + if (!party || (player->hasCondition(CONDITION_INFIGHT) && player->getZone() != ZONE_PROTECTION)) { + return; + } + + party->setSharedExperience(player, sharedExpActive); +} + +void Game::sendGuildMotd(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + Guild* guild = player->getGuild(); + if (guild) { + player->sendChannelMessage("Message of the Day", guild->getMotd(), TALKTYPE_CHANNEL_R1, CHANNEL_GUILD); + } +} + +void Game::kickPlayer(uint32_t playerId, bool displayEffect) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->kickPlayer(displayEffect); +} + +void Game::playerReportRuleViolation(uint32_t playerId, const std::string& targetName, uint8_t reportType, uint8_t reportReason, const std::string& comment, const std::string& translation) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + g_events->eventPlayerOnReportRuleViolation(player, targetName, reportType, reportReason, comment, translation); +} + +void Game::playerReportBug(uint32_t playerId, const std::string& message, const Position& position, uint8_t category) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + g_events->eventPlayerOnReportBug(player, message, position, category); +} + +void Game::playerDebugAssert(uint32_t playerId, const std::string& assertLine, const std::string& date, const std::string& description, const std::string& comment) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + // TODO: move debug assertions to database + FILE* file = fopen("client_assertions.txt", "a"); + if (file) { + fprintf(file, "----- %s - %s (%s) -----\n", formatDate(time(nullptr)).c_str(), player->getName().c_str(), convertIPToString(player->getIP()).c_str()); + fprintf(file, "%s\n%s\n%s\n%s\n", assertLine.c_str(), date.c_str(), description.c_str(), comment.c_str()); + fclose(file); + } +} + +void Game::playerLeaveMarket(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->setInMarket(false); +} + +void Game::playerBrowseMarket(uint32_t playerId, uint16_t spriteId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (!player->isInMarket()) { + return; + } + + const ItemType& it = Item::items.getItemIdByClientId(spriteId); + if (it.id == 0) { + return; + } + + if (it.wareId == 0) { + return; + } + + const MarketOfferList& buyOffers = IOMarket::getActiveOffers(MARKETACTION_BUY, it.id); + const MarketOfferList& sellOffers = IOMarket::getActiveOffers(MARKETACTION_SELL, it.id); + player->sendMarketBrowseItem(it.id, buyOffers, sellOffers); + player->sendMarketDetail(it.id); +} + +void Game::playerBrowseMarketOwnOffers(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (!player->isInMarket()) { + return; + } + + const MarketOfferList& buyOffers = IOMarket::getOwnOffers(MARKETACTION_BUY, player->getGUID()); + const MarketOfferList& sellOffers = IOMarket::getOwnOffers(MARKETACTION_SELL, player->getGUID()); + player->sendMarketBrowseOwnOffers(buyOffers, sellOffers); +} + +void Game::playerBrowseMarketOwnHistory(uint32_t playerId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (!player->isInMarket()) { + return; + } + + const HistoryMarketOfferList& buyOffers = IOMarket::getOwnHistory(MARKETACTION_BUY, player->getGUID()); + const HistoryMarketOfferList& sellOffers = IOMarket::getOwnHistory(MARKETACTION_SELL, player->getGUID()); + player->sendMarketBrowseOwnHistory(buyOffers, sellOffers); +} + +void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spriteId, uint16_t amount, uint32_t price, bool anonymous) +{ + if (amount == 0 || amount > 64000) { + return; + } + + if (price == 0 || price > 999999999) { + return; + } + + if (type != MARKETACTION_BUY && type != MARKETACTION_SELL) { + return; + } + + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (!player->isInMarket()) { + return; + } + + if (g_config.getBoolean(ConfigManager::MARKET_PREMIUM) && !player->isPremium()) { + player->sendMarketLeave(); + return; + } + + const ItemType& itt = Item::items.getItemIdByClientId(spriteId); + if (itt.id == 0 || itt.wareId == 0) { + return; + } + + const ItemType& it = Item::items.getItemIdByClientId(itt.wareId); + if (it.id == 0 || it.wareId == 0) { + return; + } + + if (!it.stackable && amount > 2000) { + return; + } + + const uint32_t maxOfferCount = g_config.getNumber(ConfigManager::MAX_MARKET_OFFERS_AT_A_TIME_PER_PLAYER); + if (maxOfferCount != 0 && IOMarket::getPlayerOfferCount(player->getGUID()) >= maxOfferCount) { + return; + } + + uint64_t fee = (price / 100.) * amount; + if (fee < 20) { + fee = 20; + } else if (fee > 1000) { + fee = 1000; + } + + if (type == MARKETACTION_SELL) { + if (fee > player->bankBalance) { + return; + } + + DepotChest* depotChest = player->getDepotChest(player->getLastDepotId(), false); + if (!depotChest) { + return; + } + + std::forward_list itemList = getMarketItemList(it.wareId, amount, depotChest, player->getInbox()); + if (itemList.empty()) { + return; + } + + if (it.stackable) { + uint16_t tmpAmount = amount; + for (Item* item : itemList) { + uint16_t removeCount = std::min(tmpAmount, item->getItemCount()); + tmpAmount -= removeCount; + internalRemoveItem(item, removeCount); + + if (tmpAmount == 0) { + break; + } + } + } else { + for (Item* item : itemList) { + internalRemoveItem(item); + } + } + + player->bankBalance -= fee; + } else { + uint64_t totalPrice = static_cast(price) * amount; + totalPrice += fee; + if (totalPrice > player->bankBalance) { + return; + } + + player->bankBalance -= totalPrice; + } + + IOMarket::createOffer(player->getGUID(), static_cast(type), it.id, amount, price, anonymous); + + player->sendMarketEnter(player->getLastDepotId()); + const MarketOfferList& buyOffers = IOMarket::getActiveOffers(MARKETACTION_BUY, it.id); + const MarketOfferList& sellOffers = IOMarket::getActiveOffers(MARKETACTION_SELL, it.id); + player->sendMarketBrowseItem(it.id, buyOffers, sellOffers); +} + +void Game::playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16_t counter) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (!player->isInMarket()) { + return; + } + + MarketOfferEx offer = IOMarket::getOfferByCounter(timestamp, counter); + if (offer.id == 0 || offer.playerId != player->getGUID()) { + return; + } + + if (offer.type == MARKETACTION_BUY) { + player->bankBalance += static_cast(offer.price) * offer.amount; + player->sendMarketEnter(player->getLastDepotId()); + } else { + const ItemType& it = Item::items[offer.itemId]; + if (it.id == 0) { + return; + } + + if (it.stackable) { + uint16_t tmpAmount = offer.amount; + while (tmpAmount > 0) { + int32_t stackCount = std::min(100, tmpAmount); + Item* item = Item::CreateItem(it.id, stackCount); + if (internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { + delete item; + break; + } + + tmpAmount -= stackCount; + } + } else { + int32_t subType; + if (it.charges != 0) { + subType = it.charges; + } else { + subType = -1; + } + + for (uint16_t i = 0; i < offer.amount; ++i) { + Item* item = Item::CreateItem(it.id, subType); + if (internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { + delete item; + break; + } + } + } + } + + IOMarket::moveOfferToHistory(offer.id, OFFERSTATE_CANCELLED); + offer.amount = 0; + offer.timestamp += g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION); + player->sendMarketCancelOffer(offer); + player->sendMarketEnter(player->getLastDepotId()); +} + +void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16_t counter, uint16_t amount) +{ + if (amount == 0 || amount > 64000) { + return; + } + + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (!player->isInMarket()) { + return; + } + + MarketOfferEx offer = IOMarket::getOfferByCounter(timestamp, counter); + if (offer.id == 0) { + return; + } + + if (amount > offer.amount) { + return; + } + + const ItemType& it = Item::items[offer.itemId]; + if (it.id == 0) { + return; + } + + uint64_t totalPrice = static_cast(offer.price) * amount; + + if (offer.type == MARKETACTION_BUY) { + DepotChest* depotChest = player->getDepotChest(player->getLastDepotId(), false); + if (!depotChest) { + return; + } + + std::forward_list itemList = getMarketItemList(it.wareId, amount, depotChest, player->getInbox()); + if (itemList.empty()) { + return; + } + + Player* buyerPlayer = getPlayerByGUID(offer.playerId); + if (!buyerPlayer) { + buyerPlayer = new Player(nullptr); + if (!IOLoginData::loadPlayerById(buyerPlayer, offer.playerId)) { + delete buyerPlayer; + return; + } + } + + if (it.stackable) { + uint16_t tmpAmount = amount; + for (Item* item : itemList) { + uint16_t removeCount = std::min(tmpAmount, item->getItemCount()); + tmpAmount -= removeCount; + internalRemoveItem(item, removeCount); + + if (tmpAmount == 0) { + break; + } + } + } else { + for (Item* item : itemList) { + internalRemoveItem(item); + } + } + + player->bankBalance += totalPrice; + + if (it.stackable) { + uint16_t tmpAmount = amount; + while (tmpAmount > 0) { + uint16_t stackCount = std::min(100, tmpAmount); + Item* item = Item::CreateItem(it.id, stackCount); + if (internalAddItem(buyerPlayer->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { + delete item; + break; + } + + tmpAmount -= stackCount; + } + } else { + int32_t subType; + if (it.charges != 0) { + subType = it.charges; + } else { + subType = -1; + } + + for (uint16_t i = 0; i < amount; ++i) { + Item* item = Item::CreateItem(it.id, subType); + if (internalAddItem(buyerPlayer->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { + delete item; + break; + } + } + } + + if (buyerPlayer->isOffline()) { + IOLoginData::savePlayer(buyerPlayer); + delete buyerPlayer; + } else { + buyerPlayer->onReceiveMail(); + } + } else { + if (totalPrice > player->bankBalance) { + return; + } + + player->bankBalance -= totalPrice; + + if (it.stackable) { + uint16_t tmpAmount = amount; + while (tmpAmount > 0) { + uint16_t stackCount = std::min(100, tmpAmount); + Item* item = Item::CreateItem(it.id, stackCount); + if (internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { + delete item; + break; + } + + tmpAmount -= stackCount; + } + } else { + int32_t subType; + if (it.charges != 0) { + subType = it.charges; + } else { + subType = -1; + } + + for (uint16_t i = 0; i < amount; ++i) { + Item* item = Item::CreateItem(it.id, subType); + if (internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { + delete item; + break; + } + } + } + + Player* sellerPlayer = getPlayerByGUID(offer.playerId); + if (sellerPlayer) { + sellerPlayer->bankBalance += totalPrice; + } else { + IOLoginData::increaseBankBalance(offer.playerId, totalPrice); + } + + player->onReceiveMail(); + } + + const int32_t marketOfferDuration = g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION); + + IOMarket::appendHistory(player->getGUID(), (offer.type == MARKETACTION_BUY ? MARKETACTION_SELL : MARKETACTION_BUY), offer.itemId, amount, offer.price, offer.timestamp + marketOfferDuration, OFFERSTATE_ACCEPTEDEX); + + IOMarket::appendHistory(offer.playerId, offer.type, offer.itemId, amount, offer.price, offer.timestamp + marketOfferDuration, OFFERSTATE_ACCEPTED); + + offer.amount -= amount; + + if (offer.amount == 0) { + IOMarket::deleteOffer(offer.id); + } else { + IOMarket::acceptOffer(offer.id, amount); + } + + player->sendMarketEnter(player->getLastDepotId()); + offer.timestamp += marketOfferDuration; + player->sendMarketAcceptOffer(offer); +} + +void Game::parsePlayerExtendedOpcode(uint32_t playerId, uint8_t opcode, const std::string& buffer) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + for (CreatureEvent* creatureEvent : player->getCreatureEvents(CREATURE_EVENT_EXTENDED_OPCODE)) { + creatureEvent->executeExtendedOpcode(player, opcode, buffer); + } +} + +std::forward_list Game::getMarketItemList(uint16_t wareId, uint16_t sufficientCount, DepotChest* depotChest, Inbox* inbox) +{ + std::forward_list itemList; + uint16_t count = 0; + + std::list containers { depotChest, inbox }; + do { + Container* container = containers.front(); + containers.pop_front(); + + for (Item* item : container->getItemList()) { + Container* c = item->getContainer(); + if (c && !c->empty()) { + containers.push_back(c); + continue; + } + + const ItemType& itemType = Item::items[item->getID()]; + if (itemType.wareId != wareId) { + continue; + } + + if (c && (!itemType.isContainer() || c->capacity() != itemType.maxItems)) { + continue; + } + + if (!item->hasMarketAttributes()) { + continue; + } + + itemList.push_front(item); + + count += Item::countByType(item, -1); + if (count >= sufficientCount) { + return itemList; + } + } + } while (!containers.empty()); + return std::forward_list(); +} + +void Game::forceAddCondition(uint32_t creatureId, Condition* condition) +{ + Creature* creature = getCreatureByID(creatureId); + if (!creature) { + delete condition; + return; + } + + creature->addCondition(condition, true); +} + +void Game::forceRemoveCondition(uint32_t creatureId, ConditionType_t type) +{ + Creature* creature = getCreatureByID(creatureId); + if (!creature) { + return; + } + + creature->removeCondition(type, true); +} + +void Game::sendOfflineTrainingDialog(Player* player) +{ + if (!player) { + return; + } + + if (!player->hasModalWindowOpen(offlineTrainingWindow.id)) { + player->sendModalWindow(offlineTrainingWindow); + } +} + +void Game::playerAnswerModalWindow(uint32_t playerId, uint32_t modalWindowId, uint8_t button, uint8_t choice) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (!player->hasModalWindowOpen(modalWindowId)) { + return; + } + + player->onModalWindowHandled(modalWindowId); + + // offline training, hardcoded + if (modalWindowId == std::numeric_limits::max()) { + if (button == 1) { + if (choice == SKILL_SWORD || choice == SKILL_AXE || choice == SKILL_CLUB || choice == SKILL_DISTANCE || choice == SKILL_MAGLEVEL) { + BedItem* bedItem = player->getBedItem(); + if (bedItem && bedItem->sleep(player)) { + player->setOfflineTrainingSkill(choice); + return; + } + } + } else { + player->sendTextMessage(MESSAGE_EVENT_ADVANCE, "Offline training aborted."); + } + + player->setBedItem(nullptr); + } else { + for (auto creatureEvent : player->getCreatureEvents(CREATURE_EVENT_MODALWINDOW)) { + creatureEvent->executeModalWindow(player, modalWindowId, button, choice); + } + } +} + +void Game::addPlayer(Player* player) +{ + const std::string& lowercase_name = asLowerCaseString(player->getName()); + mappedPlayerNames[lowercase_name] = player; + mappedPlayerGuids[player->getGUID()] = player; + wildcardTree.insert(lowercase_name); + players[player->getID()] = player; +} + +void Game::removePlayer(Player* player) +{ + const std::string& lowercase_name = asLowerCaseString(player->getName()); + mappedPlayerNames.erase(lowercase_name); + mappedPlayerGuids.erase(player->getGUID()); + wildcardTree.remove(lowercase_name); + players.erase(player->getID()); +} + +void Game::addNpc(Npc* npc) +{ + npcs[npc->getID()] = npc; +} + +void Game::removeNpc(Npc* npc) +{ + npcs.erase(npc->getID()); +} + +void Game::addMonster(Monster* monster) +{ + monsters[monster->getID()] = monster; +} + +void Game::removeMonster(Monster* monster) +{ + monsters.erase(monster->getID()); +} + +Guild* Game::getGuild(uint32_t id) const +{ + auto it = guilds.find(id); + if (it == guilds.end()) { + return nullptr; + } + return it->second; +} + +void Game::addGuild(Guild* guild) +{ + guilds[guild->getId()] = guild; +} + +void Game::removeGuild(uint32_t guildId) +{ + guilds.erase(guildId); +} + +void Game::decreaseBrowseFieldRef(const Position& pos) +{ + Tile* tile = map.getTile(pos.x, pos.y, pos.z); + if (!tile) { + return; + } + + auto it = browseFields.find(tile); + if (it != browseFields.end()) { + it->second->decrementReferenceCounter(); + } +} + +void Game::internalRemoveItems(std::vector itemList, uint32_t amount, bool stackable) +{ + if (stackable) { + for (Item* item : itemList) { + if (item->getItemCount() > amount) { + internalRemoveItem(item, amount); + break; + } else { + amount -= item->getItemCount(); + internalRemoveItem(item); + } + } + } else { + for (Item* item : itemList) { + internalRemoveItem(item); + } + } +} + +BedItem* Game::getBedBySleeper(uint32_t guid) const +{ + auto it = bedSleepersMap.find(guid); + if (it == bedSleepersMap.end()) { + return nullptr; + } + return it->second; +} + +void Game::setBedSleeper(BedItem* bed, uint32_t guid) +{ + bedSleepersMap[guid] = bed; +} + +void Game::removeBedSleeper(uint32_t guid) +{ + auto it = bedSleepersMap.find(guid); + if (it != bedSleepersMap.end()) { + bedSleepersMap.erase(it); + } +} + +Item* Game::getUniqueItem(uint16_t uniqueId) +{ + auto it = uniqueItems.find(uniqueId); + if (it == uniqueItems.end()) { + return nullptr; + } + return it->second; +} + +bool Game::addUniqueItem(uint16_t uniqueId, Item* item) +{ + auto result = uniqueItems.emplace(uniqueId, item); + if (!result.second) { + std::cout << "Duplicate unique id: " << uniqueId << std::endl; + } + return result.second; +} + +void Game::removeUniqueItem(uint16_t uniqueId) +{ + auto it = uniqueItems.find(uniqueId); + if (it != uniqueItems.end()) { + uniqueItems.erase(it); + } +} + +bool Game::reload(ReloadTypes_t reloadType) +{ + switch (reloadType) { + case RELOAD_TYPE_ACTIONS: return g_actions->reload(); + case RELOAD_TYPE_CHAT: return g_chat->load(); + case RELOAD_TYPE_CONFIG: return g_config.reload(); + case RELOAD_TYPE_CREATURESCRIPTS: { + g_creatureEvents->reload(); + g_creatureEvents->removeInvalidEvents(); + return true; + } + case RELOAD_TYPE_EVENTS: return g_events->load(); + case RELOAD_TYPE_GLOBALEVENTS: return g_globalEvents->reload(); + case RELOAD_TYPE_ITEMS: return Item::items.reload(); + case RELOAD_TYPE_MONSTERS: return g_monsters.reload(); + case RELOAD_TYPE_MOUNTS: return mounts.reload(); + case RELOAD_TYPE_MOVEMENTS: return g_moveEvents->reload(); + case RELOAD_TYPE_NPCS: { + Npcs::reload(); + return true; + } + + case RELOAD_TYPE_QUESTS: return quests.reload(); + case RELOAD_TYPE_RAIDS: return raids.reload() && raids.startup(); + + case RELOAD_TYPE_SPELLS: { + if (!g_spells->reload()) { + std::cout << "[Error - Game::reload] Failed to reload spells." << std::endl; + std::terminate(); + } else if (!g_monsters.reload()) { + std::cout << "[Error - Game::reload] Failed to reload monsters." << std::endl; + std::terminate(); + } + return true; + } + + case RELOAD_TYPE_TALKACTIONS: return g_talkActions->reload(); + + case RELOAD_TYPE_WEAPONS: { + bool results = g_weapons->reload(); + g_weapons->loadDefaults(); + return results; + } + + case RELOAD_TYPE_SCRIPTS: { + // commented out stuff is TODO, once we approach further in revscriptsys + g_actions->clear(true); + g_creatureEvents->clear(true); + g_moveEvents->clear(true); + g_talkActions->clear(true); + g_globalEvents->clear(true); + g_weapons->clear(true); + g_weapons->loadDefaults(); + g_spells->clear(true); + g_scripts->loadScripts("scripts", false, true); + g_creatureEvents->removeInvalidEvents(); + /* + Npcs::reload(); + raids.reload() && raids.startup(); + Item::items.reload(); + quests.reload(); + mounts.reload(); + g_config.reload(); + g_events->load(); + g_chat->load(); + */ + return true; + } + + default: { + if (!g_spells->reload()) { + std::cout << "[Error - Game::reload] Failed to reload spells." << std::endl; + std::terminate(); + } else if (!g_monsters.reload()) { + std::cout << "[Error - Game::reload] Failed to reload monsters." << std::endl; + std::terminate(); + } + + g_actions->reload(); + g_config.reload(); + g_creatureEvents->reload(); + g_monsters.reload(); + g_moveEvents->reload(); + Npcs::reload(); + raids.reload() && raids.startup(); + g_talkActions->reload(); + Item::items.reload(); + g_weapons->reload(); + g_weapons->clear(true); + g_weapons->loadDefaults(); + quests.reload(); + mounts.reload(); + g_globalEvents->reload(); + g_events->load(); + g_chat->load(); + g_actions->clear(true); + g_creatureEvents->clear(true); + g_moveEvents->clear(true); + g_talkActions->clear(true); + g_globalEvents->clear(true); + g_spells->clear(true); + g_scripts->loadScripts("scripts", false, true); + g_creatureEvents->removeInvalidEvents(); + return true; + } + } + return true; +} + diff --git a/src/game.h b/src/game.h new file mode 100644 index 0000000..a06aec9 --- /dev/null +++ b/src/game.h @@ -0,0 +1,574 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_GAME_H_3EC96D67DD024E6093B3BAC29B7A6D7F +#define FS_GAME_H_3EC96D67DD024E6093B3BAC29B7A6D7F + +#include "account.h" +#include "combat.h" +#include "groups.h" +#include "map.h" +#include "position.h" +#include "item.h" +#include "container.h" +#include "player.h" +#include "raids.h" +#include "npc.h" +#include "wildcardtree.h" +#include "quests.h" + +class ServiceManager; +class Creature; +class Monster; +class Npc; +class CombatInfo; + +enum stackPosType_t { + STACKPOS_MOVE, + STACKPOS_LOOK, + STACKPOS_TOPDOWN_ITEM, + STACKPOS_USEITEM, + STACKPOS_USETARGET, +}; + +enum WorldType_t { + WORLD_TYPE_NO_PVP = 1, + WORLD_TYPE_PVP = 2, + WORLD_TYPE_PVP_ENFORCED = 3, +}; + +enum GameState_t { + GAME_STATE_STARTUP, + GAME_STATE_INIT, + GAME_STATE_NORMAL, + GAME_STATE_CLOSED, + GAME_STATE_SHUTDOWN, + GAME_STATE_CLOSING, + GAME_STATE_MAINTAIN, +}; + +enum LightState_t { + LIGHT_STATE_DAY, + LIGHT_STATE_NIGHT, + LIGHT_STATE_SUNSET, + LIGHT_STATE_SUNRISE, +}; + +static constexpr int32_t EVENT_LIGHTINTERVAL = 10000; +static constexpr int32_t EVENT_DECAYINTERVAL = 250; +static constexpr int32_t EVENT_DECAY_BUCKETS = 4; + +/** + * Main Game class. + * This class is responsible to control everything that happens + */ + +class Game +{ + public: + Game(); + ~Game(); + + // non-copyable + Game(const Game&) = delete; + Game& operator=(const Game&) = delete; + + void start(ServiceManager* manager); + + void forceAddCondition(uint32_t creatureId, Condition* condition); + void forceRemoveCondition(uint32_t creatureId, ConditionType_t type); + + bool loadMainMap(const std::string& filename); + void loadMap(const std::string& path); + + /** + * Get the map size - info purpose only + * \param width width of the map + * \param height height of the map + */ + void getMapDimensions(uint32_t& width, uint32_t& height) const { + width = map.width; + height = map.height; + } + + void setWorldType(WorldType_t type); + WorldType_t getWorldType() const { + return worldType; + } + + Cylinder* internalGetCylinder(Player* player, const Position& pos) const; + Thing* internalGetThing(Player* player, const Position& pos, int32_t index, + uint32_t spriteId, stackPosType_t type) const; + static void internalGetPosition(Item* item, Position& pos, uint8_t& stackpos); + + static std::string getTradeErrorDescription(ReturnValue ret, Item* item); + + /** + * Returns a creature based on the unique creature identifier + * \param id is the unique creature id to get a creature pointer to + * \returns A Creature pointer to the creature + */ + Creature* getCreatureByID(uint32_t id); + + /** + * Returns a monster based on the unique creature identifier + * \param id is the unique monster id to get a monster pointer to + * \returns A Monster pointer to the monster + */ + Monster* getMonsterByID(uint32_t id); + + /** + * Returns a npc based on the unique creature identifier + * \param id is the unique npc id to get a npc pointer to + * \returns A NPC pointer to the npc + */ + Npc* getNpcByID(uint32_t id); + + /** + * Returns a player based on the unique creature identifier + * \param id is the unique player id to get a player pointer to + * \returns A Pointer to the player + */ + Player* getPlayerByID(uint32_t id); + + /** + * Returns a creature based on a string name identifier + * \param s is the name identifier + * \returns A Pointer to the creature + */ + Creature* getCreatureByName(const std::string& s); + + /** + * Returns a npc based on a string name identifier + * \param s is the name identifier + * \returns A Pointer to the npc + */ + Npc* getNpcByName(const std::string& s); + + /** + * Returns a player based on a string name identifier + * \param s is the name identifier + * \returns A Pointer to the player + */ + Player* getPlayerByName(const std::string& s); + + /** + * Returns a player based on guid + * \returns A Pointer to the player + */ + Player* getPlayerByGUID(const uint32_t& guid); + + /** + * Returns a player based on a string name identifier, with support for the "~" wildcard. + * \param s is the name identifier, with or without wildcard + * \param player will point to the found player (if any) + * \return "RETURNVALUE_PLAYERWITHTHISNAMEISNOTONLINE" or "RETURNVALUE_NAMEISTOOAMBIGIOUS" + */ + ReturnValue getPlayerByNameWildcard(const std::string& s, Player*& player); + + /** + * Returns a player based on an account number identifier + * \param acc is the account identifier + * \returns A Pointer to the player + */ + Player* getPlayerByAccount(uint32_t acc); + + /* Place Creature on the map without sending out events to the surrounding. + * \param creature Creature to place on the map + * \param pos The position to place the creature + * \param extendedPos If true, the creature will in first-hand be placed 2 tiles away + * \param forced If true, placing the creature will not fail because of obstacles (creatures/items) + */ + bool internalPlaceCreature(Creature* creature, const Position& pos, bool extendedPos = false, bool forced = false); + + /** + * Place Creature on the map. + * \param creature Creature to place on the map + * \param pos The position to place the creature + * \param extendedPos If true, the creature will in first-hand be placed 2 tiles away + * \param force If true, placing the creature will not fail because of obstacles (creatures/items) + */ + bool placeCreature(Creature* creature, const Position& pos, bool extendedPos = false, bool forced = false); + + /** + * Remove Creature from the map. + * Removes the Creature the map + * \param c Creature to remove + */ + bool removeCreature(Creature* creature, bool isLogout = true); + + void addCreatureCheck(Creature* creature); + static void removeCreatureCheck(Creature* creature); + + size_t getPlayersOnline() const { + return players.size(); + } + size_t getMonstersOnline() const { + return monsters.size(); + } + size_t getNpcsOnline() const { + return npcs.size(); + } + uint32_t getPlayersRecord() const { + return playersRecord; + } + + LightInfo getWorldLightInfo() const; + + ReturnValue internalMoveCreature(Creature* creature, Direction direction, uint32_t flags = 0); + ReturnValue internalMoveCreature(Creature& creature, Tile& toTile, uint32_t flags = 0); + + ReturnValue internalMoveItem(Cylinder* fromCylinder, Cylinder* toCylinder, int32_t index, + Item* item, uint32_t count, Item** _moveItem, uint32_t flags = 0, Creature* actor = nullptr, Item* tradeItem = nullptr, const Position* fromPos = nullptr, const Position* toPos = nullptr); + + ReturnValue internalAddItem(Cylinder* toCylinder, Item* item, int32_t index = INDEX_WHEREEVER, + uint32_t flags = 0, bool test = false); + ReturnValue internalAddItem(Cylinder* toCylinder, Item* item, int32_t index, + uint32_t flags, bool test, uint32_t& remainderCount); + ReturnValue internalRemoveItem(Item* item, int32_t count = -1, bool test = false, uint32_t flags = 0); + + ReturnValue internalPlayerAddItem(Player* player, Item* item, bool dropOnMap = true, slots_t slot = CONST_SLOT_WHEREEVER); + + /** + * Find an item of a certain type + * \param cylinder to search the item + * \param itemId is the item to remove + * \param subType is the extra type an item can have such as charges/fluidtype, default is -1 + * meaning it's not used + * \param depthSearch if true it will check child containers aswell + * \returns A pointer to the item to an item and nullptr if not found + */ + Item* findItemOfType(Cylinder* cylinder, uint16_t itemId, + bool depthSearch = true, int32_t subType = -1) const; + + /** + * Remove/Add item(s) with a monetary value + * \param cylinder to remove the money from + * \param money is the amount to remove + * \param flags optional flags to modifiy the default behaviour + * \returns true if the removal was successful + */ + bool removeMoney(Cylinder* cylinder, uint64_t money, uint32_t flags = 0); + + /** + * Add item(s) with monetary value + * \param cylinder which will receive money + * \param money the amount to give + * \param flags optional flags to modify default behavior + */ + void addMoney(Cylinder* cylinder, uint64_t money, uint32_t flags = 0); + + /** + * Transform one item to another type/count + * \param item is the item to transform + * \param newId is the new itemid + * \param newCount is the new count value, use default value (-1) to not change it + * \returns true if the tranformation was successful + */ + Item* transformItem(Item* item, uint16_t newId, int32_t newCount = -1); + + /** + * Teleports an object to another position + * \param thing is the object to teleport + * \param newPos is the new position + * \param pushMove force teleport if false + * \param flags optional flags to modify default behavior + * \returns true if the teleportation was successful + */ + ReturnValue internalTeleport(Thing* thing, const Position& newPos, bool pushMove = true, uint32_t flags = 0); + + /** + * Turn a creature to a different direction. + * \param creature Creature to change the direction + * \param dir Direction to turn to + */ + bool internalCreatureTurn(Creature* creature, Direction dir); + + /** + * Creature wants to say something. + * \param creature Creature pointer + * \param type Type of message + * \param text The text to say + */ + bool internalCreatureSay(Creature* creature, SpeakClasses type, const std::string& text, + bool ghostMode, SpectatorVec* spectatorsPtr = nullptr, const Position* pos = nullptr); + + void loadPlayersRecord(); + void checkPlayersRecord(); + + void sendGuildMotd(uint32_t playerId); + void kickPlayer(uint32_t playerId, bool displayEffect); + void playerReportBug(uint32_t playerId, const std::string& message, const Position& position, uint8_t category); + void playerDebugAssert(uint32_t playerId, const std::string& assertLine, const std::string& date, const std::string& description, const std::string& comment); + void playerAnswerModalWindow(uint32_t playerId, uint32_t modalWindowId, uint8_t button, uint8_t choice); + void playerReportRuleViolation(uint32_t playerId, const std::string& targetName, uint8_t reportType, uint8_t reportReason, const std::string& comment, const std::string& translation); + + bool internalStartTrade(Player* player, Player* tradePartner, Item* tradeItem); + void internalCloseTrade(Player* player); + bool playerBroadcastMessage(Player* player, const std::string& text) const; + void broadcastMessage(const std::string& text, MessageClasses type) const; + + //Implementation of player invoked events + void playerMoveThing(uint32_t playerId, const Position& fromPos, uint16_t spriteId, uint8_t fromStackPos, + const Position& toPos, uint8_t count); + void playerMoveCreatureByID(uint32_t playerId, uint32_t movingCreatureId, const Position& movingCreatureOrigPos, const Position& toPos); + void playerMoveCreature(Player* player, Creature* movingCreature, const Position& movingCreatureOrigPos, Tile* toTile); + void playerMoveItemByPlayerID(uint32_t playerId, const Position& fromPos, uint16_t spriteId, uint8_t fromStackPos, const Position& toPos, uint8_t count); + void playerMoveItem(Player* player, const Position& fromPos, + uint16_t spriteId, uint8_t fromStackPos, const Position& toPos, uint8_t count, Item* item, Cylinder* toCylinder); + void playerEquipItem(uint32_t playerId, uint16_t spriteId); + void playerMove(uint32_t playerId, Direction direction); + void playerCreatePrivateChannel(uint32_t playerId); + void playerChannelInvite(uint32_t playerId, const std::string& name); + void playerChannelExclude(uint32_t playerId, const std::string& name); + void playerRequestChannels(uint32_t playerId); + void playerOpenChannel(uint32_t playerId, uint16_t channelId); + void playerCloseChannel(uint32_t playerId, uint16_t channelId); + void playerOpenPrivateChannel(uint32_t playerId, std::string& receiver); + void playerCloseNpcChannel(uint32_t playerId); + void playerReceivePing(uint32_t playerId); + void playerReceivePingBack(uint32_t playerId); + void playerAutoWalk(uint32_t playerId, const std::forward_list& listDir); + void playerStopAutoWalk(uint32_t playerId); + void playerUseItemEx(uint32_t playerId, const Position& fromPos, uint8_t fromStackPos, + uint16_t fromSpriteId, const Position& toPos, uint8_t toStackPos, uint16_t toSpriteId); + void playerUseItem(uint32_t playerId, const Position& pos, uint8_t stackPos, uint8_t index, uint16_t spriteId); + void playerUseWithCreature(uint32_t playerId, const Position& fromPos, uint8_t fromStackPos, uint32_t creatureId, uint16_t spriteId); + void playerCloseContainer(uint32_t playerId, uint8_t cid); + void playerMoveUpContainer(uint32_t playerId, uint8_t cid); + void playerUpdateContainer(uint32_t playerId, uint8_t cid); + void playerRotateItem(uint32_t playerId, const Position& pos, uint8_t stackPos, const uint16_t spriteId); + void playerWriteItem(uint32_t playerId, uint32_t windowTextId, const std::string& text); + void playerBrowseField(uint32_t playerId, const Position& pos); + void playerSeekInContainer(uint32_t playerId, uint8_t containerId, uint16_t index); + void playerUpdateHouseWindow(uint32_t playerId, uint8_t listId, uint32_t windowTextId, const std::string& text); + void playerWrapItem(uint32_t playerId, const Position& position, uint8_t stackPos, const uint16_t spriteId); + void playerRequestTrade(uint32_t playerId, const Position& pos, uint8_t stackPos, + uint32_t tradePlayerId, uint16_t spriteId); + void playerAcceptTrade(uint32_t playerId); + void playerLookInTrade(uint32_t playerId, bool lookAtCounterOffer, uint8_t index); + void playerPurchaseItem(uint32_t playerId, uint16_t spriteId, uint8_t count, uint8_t amount, + bool ignoreCap = false, bool inBackpacks = false); + void playerSellItem(uint32_t playerId, uint16_t spriteId, uint8_t count, + uint8_t amount, bool ignoreEquipped = false); + void playerCloseShop(uint32_t playerId); + void playerLookInShop(uint32_t playerId, uint16_t spriteId, uint8_t count); + void playerCloseTrade(uint32_t playerId); + void playerSetAttackedCreature(uint32_t playerId, uint32_t creatureId); + void playerFollowCreature(uint32_t playerId, uint32_t creatureId); + void playerCancelAttackAndFollow(uint32_t playerId); + void playerSetFightModes(uint32_t playerId, fightMode_t fightMode, bool chaseMode, bool secureMode); + void playerLookAt(uint32_t playerId, const Position& pos, uint8_t stackPos); + void playerLookInBattleList(uint32_t playerId, uint32_t creatureId); + void playerRequestAddVip(uint32_t playerId, const std::string& name); + void playerRequestRemoveVip(uint32_t playerId, uint32_t guid); + void playerRequestEditVip(uint32_t playerId, uint32_t guid, const std::string& description, uint32_t icon, bool notify); + void playerTurn(uint32_t playerId, Direction dir); + void playerRequestOutfit(uint32_t playerId); + void playerShowQuestLog(uint32_t playerId); + void playerShowQuestLine(uint32_t playerId, uint16_t questId); + void playerSay(uint32_t playerId, uint16_t channelId, SpeakClasses type, + const std::string& receiver, const std::string& text); + void playerChangeOutfit(uint32_t playerId, Outfit_t outfit); + void playerInviteToParty(uint32_t playerId, uint32_t invitedId); + void playerJoinParty(uint32_t playerId, uint32_t leaderId); + void playerRevokePartyInvitation(uint32_t playerId, uint32_t invitedId); + void playerPassPartyLeadership(uint32_t playerId, uint32_t newLeaderId); + void playerLeaveParty(uint32_t playerId); + void playerEnableSharedPartyExperience(uint32_t playerId, bool sharedExpActive); + void playerToggleMount(uint32_t playerId, bool mount); + void playerLeaveMarket(uint32_t playerId); + void playerBrowseMarket(uint32_t playerId, uint16_t spriteId); + void playerBrowseMarketOwnOffers(uint32_t playerId); + void playerBrowseMarketOwnHistory(uint32_t playerId); + void playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spriteId, uint16_t amount, uint32_t price, bool anonymous); + void playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16_t counter); + void playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16_t counter, uint16_t amount); + + void parsePlayerExtendedOpcode(uint32_t playerId, uint8_t opcode, const std::string& buffer); + + std::forward_list getMarketItemList(uint16_t wareId, uint16_t sufficientCount, DepotChest* depotChest, Inbox* inbox); + + static void updatePremium(Account& account); + + void cleanup(); + void shutdown(); + void ReleaseCreature(Creature* creature); + void ReleaseItem(Item* item); + + bool canThrowObjectTo(const Position& fromPos, const Position& toPos, bool checkLineOfSight = true, + int32_t rangex = Map::maxClientViewportX, int32_t rangey = Map::maxClientViewportY) const; + bool isSightClear(const Position& fromPos, const Position& toPos, bool floorCheck) const; + + void changeSpeed(Creature* creature, int32_t varSpeedDelta); + void internalCreatureChangeOutfit(Creature* creature, const Outfit_t& outfit); + void internalCreatureChangeVisible(Creature* creature, bool visible); + void changeLight(const Creature* creature); + void updateCreatureSkull(const Creature* creature); + void updatePlayerShield(Player* player); + void updatePlayerHelpers(const Player& player); + void updateCreatureType(Creature* creature); + void updateCreatureWalkthrough(const Creature* creature); + + GameState_t getGameState() const; + void setGameState(GameState_t newState); + void saveGameState(); + + //Events + void checkCreatureWalk(uint32_t creatureId); + void updateCreatureWalk(uint32_t creatureId); + void checkCreatureAttack(uint32_t creatureId); + void checkCreatures(size_t index); + void checkLight(); + + bool combatBlockHit(CombatDamage& damage, Creature* attacker, Creature* target, bool checkDefense, bool checkArmor, bool field); + + void combatGetTypeInfo(CombatType_t combatType, Creature* target, TextColor_t& color, uint8_t& effect); + + bool combatChangeHealth(Creature* attacker, Creature* target, CombatDamage& damage); + bool combatChangeMana(Creature* attacker, Creature* target, CombatDamage& damage); + + //animation help functions + void addCreatureHealth(const Creature* target); + static void addCreatureHealth(const SpectatorVec& spectators, const Creature* target); + void addMagicEffect(const Position& pos, uint8_t effect); + static void addMagicEffect(const SpectatorVec& spectators, const Position& pos, uint8_t effect); + void addDistanceEffect(const Position& fromPos, const Position& toPos, uint8_t effect); + static void addDistanceEffect(const SpectatorVec& spectators, const Position& fromPos, const Position& toPos, uint8_t effect); + + void startDecay(Item* item); + int32_t getLightHour() const { + return lightHour; + } + + bool loadExperienceStages(); + uint64_t getExperienceStage(uint32_t level); + + void loadMotdNum(); + void saveMotdNum() const; + const std::string& getMotdHash() const { return motdHash; } + uint32_t getMotdNum() const { return motdNum; } + void incrementMotdNum() { motdNum++; } + + void sendOfflineTrainingDialog(Player* player); + + const std::unordered_map& getPlayers() const { return players; } + const std::map& getNpcs() const { return npcs; } + + void addPlayer(Player* player); + void removePlayer(Player* player); + + void addNpc(Npc* npc); + void removeNpc(Npc* npc); + + void addMonster(Monster* monster); + void removeMonster(Monster* monster); + + Guild* getGuild(uint32_t id) const; + void addGuild(Guild* guild); + void removeGuild(uint32_t guildId); + void decreaseBrowseFieldRef(const Position& pos); + + std::unordered_map browseFields; + + void internalRemoveItems(std::vector itemList, uint32_t amount, bool stackable); + + BedItem* getBedBySleeper(uint32_t guid) const; + void setBedSleeper(BedItem* bed, uint32_t guid); + void removeBedSleeper(uint32_t guid); + + Item* getUniqueItem(uint16_t uniqueId); + bool addUniqueItem(uint16_t uniqueId, Item* item); + void removeUniqueItem(uint16_t uniqueId); + + bool reload(ReloadTypes_t reloadType); + + Groups groups; + Map map; + Mounts mounts; + Raids raids; + Quests quests; + + std::forward_list toDecayItems; + + private: + bool playerSaySpell(Player* player, SpeakClasses type, const std::string& text); + void playerWhisper(Player* player, const std::string& text); + bool playerYell(Player* player, const std::string& text); + bool playerSpeakTo(Player* player, SpeakClasses type, const std::string& receiver, const std::string& text); + void playerSpeakToNpc(Player* player, const std::string& text); + + void checkDecay(); + void internalDecayItem(Item* item); + + std::unordered_map players; + std::unordered_map mappedPlayerNames; + std::unordered_map mappedPlayerGuids; + std::unordered_map guilds; + std::unordered_map uniqueItems; + std::map stages; + + std::list decayItems[EVENT_DECAY_BUCKETS]; + std::list checkCreatureLists[EVENT_CREATURECOUNT]; + + std::vector ToReleaseCreatures; + std::vector ToReleaseItems; + + size_t lastBucket = 0; + + WildcardTreeNode wildcardTree { false }; + + std::map npcs; + std::map monsters; + + //list of items that are in trading state, mapped to the player + std::map tradeItems; + + std::map bedSleepersMap; + + ModalWindow offlineTrainingWindow { std::numeric_limits::max(), "Choose a Skill", "Please choose a skill:" }; + + static constexpr int32_t LIGHT_LEVEL_DAY = 250; + static constexpr int32_t LIGHT_LEVEL_NIGHT = 40; + static constexpr int32_t SUNSET = 1305; + static constexpr int32_t SUNRISE = 430; + + GameState_t gameState = GAME_STATE_NORMAL; + WorldType_t worldType = WORLD_TYPE_PVP; + + LightState_t lightState = LIGHT_STATE_DAY; + uint8_t lightLevel = LIGHT_LEVEL_DAY; + int32_t lightHour = SUNRISE + (SUNSET - SUNRISE) / 2; + // (1440 minutes/day)/(3600 seconds/day)*10 seconds event interval + int32_t lightHourDelta = 1400 * 10 / 3600; + + ServiceManager* serviceManager = nullptr; + + void updatePlayersRecord() const; + uint32_t playersRecord = 0; + + std::string motdHash; + uint32_t motdNum = 0; + + uint32_t lastStageLevel = 0; + bool stagesEnabled = false; + bool useLastStageLevel = false; +}; + +#endif diff --git a/src/globalevent.cpp b/src/globalevent.cpp new file mode 100644 index 0000000..daa233e --- /dev/null +++ b/src/globalevent.cpp @@ -0,0 +1,371 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "configmanager.h" +#include "globalevent.h" +#include "tools.h" +#include "scheduler.h" +#include "pugicast.h" + +extern ConfigManager g_config; + +GlobalEvents::GlobalEvents() : + scriptInterface("GlobalEvent Interface") +{ + scriptInterface.initState(); +} + +GlobalEvents::~GlobalEvents() +{ + clear(false); +} + +void GlobalEvents::clearMap(GlobalEventMap& map, bool fromLua) +{ + for (auto it = map.begin(); it != map.end(); ) { + if (fromLua == it->second.fromLua) { + it = map.erase(it); + } else { + ++it; + } + } +} + +void GlobalEvents::clear(bool fromLua) +{ + g_scheduler.stopEvent(thinkEventId); + thinkEventId = 0; + g_scheduler.stopEvent(timerEventId); + timerEventId = 0; + + clearMap(thinkMap, fromLua); + clearMap(serverMap, fromLua); + clearMap(timerMap, fromLua); + + reInitState(fromLua); +} + +Event_ptr GlobalEvents::getEvent(const std::string& nodeName) +{ + if (strcasecmp(nodeName.c_str(), "globalevent") != 0) { + return nullptr; + } + return Event_ptr(new GlobalEvent(&scriptInterface)); +} + +bool GlobalEvents::registerEvent(Event_ptr event, const pugi::xml_node&) +{ + GlobalEvent_ptr globalEvent{static_cast(event.release())}; //event is guaranteed to be a GlobalEvent + if (globalEvent->getEventType() == GLOBALEVENT_TIMER) { + auto result = timerMap.emplace(globalEvent->getName(), std::move(*globalEvent)); + if (result.second) { + if (timerEventId == 0) { + timerEventId = g_scheduler.addEvent(createSchedulerTask(SCHEDULER_MINTICKS, std::bind(&GlobalEvents::timer, this))); + } + return true; + } + } else if (globalEvent->getEventType() != GLOBALEVENT_NONE) { + auto result = serverMap.emplace(globalEvent->getName(), std::move(*globalEvent)); + if (result.second) { + return true; + } + } else { // think event + auto result = thinkMap.emplace(globalEvent->getName(), std::move(*globalEvent)); + if (result.second) { + if (thinkEventId == 0) { + thinkEventId = g_scheduler.addEvent(createSchedulerTask(SCHEDULER_MINTICKS, std::bind(&GlobalEvents::think, this))); + } + return true; + } + } + + std::cout << "[Warning - GlobalEvents::configureEvent] Duplicate registered globalevent with name: " << globalEvent->getName() << std::endl; + return false; +} + +bool GlobalEvents::registerLuaEvent(GlobalEvent* event) +{ + GlobalEvent_ptr globalEvent{ event }; + if (globalEvent->getEventType() == GLOBALEVENT_TIMER) { + auto result = timerMap.emplace(globalEvent->getName(), std::move(*globalEvent)); + if (result.second) { + if (timerEventId == 0) { + timerEventId = g_scheduler.addEvent(createSchedulerTask(SCHEDULER_MINTICKS, std::bind(&GlobalEvents::timer, this))); + } + return true; + } + } else if (globalEvent->getEventType() != GLOBALEVENT_NONE) { + auto result = serverMap.emplace(globalEvent->getName(), std::move(*globalEvent)); + if (result.second) { + return true; + } + } else { // think event + auto result = thinkMap.emplace(globalEvent->getName(), std::move(*globalEvent)); + if (result.second) { + if (thinkEventId == 0) { + thinkEventId = g_scheduler.addEvent(createSchedulerTask(SCHEDULER_MINTICKS, std::bind(&GlobalEvents::think, this))); + } + return true; + } + } + + std::cout << "[Warning - GlobalEvents::configureEvent] Duplicate registered globalevent with name: " << globalEvent->getName() << std::endl; + return false; +} + +void GlobalEvents::startup() const +{ + execute(GLOBALEVENT_STARTUP); +} + +void GlobalEvents::timer() +{ + time_t now = time(nullptr); + + int64_t nextScheduledTime = std::numeric_limits::max(); + + auto it = timerMap.begin(); + while (it != timerMap.end()) { + GlobalEvent& globalEvent = it->second; + + int64_t nextExecutionTime = globalEvent.getNextExecution() - now; + if (nextExecutionTime > 0) { + if (nextExecutionTime < nextScheduledTime) { + nextScheduledTime = nextExecutionTime; + } + + ++it; + continue; + } + + if (!globalEvent.executeEvent()) { + it = timerMap.erase(it); + continue; + } + + nextExecutionTime = 86400; + if (nextExecutionTime < nextScheduledTime) { + nextScheduledTime = nextExecutionTime; + } + + globalEvent.setNextExecution(globalEvent.getNextExecution() + nextExecutionTime); + + ++it; + } + + if (nextScheduledTime != std::numeric_limits::max()) { + timerEventId = g_scheduler.addEvent(createSchedulerTask(std::max(1000, nextScheduledTime * 1000), + std::bind(&GlobalEvents::timer, this))); + } +} + +void GlobalEvents::think() +{ + int64_t now = OTSYS_TIME(); + + int64_t nextScheduledTime = std::numeric_limits::max(); + for (auto& it : thinkMap) { + GlobalEvent& globalEvent = it.second; + + int64_t nextExecutionTime = globalEvent.getNextExecution() - now; + if (nextExecutionTime > 0) { + if (nextExecutionTime < nextScheduledTime) { + nextScheduledTime = nextExecutionTime; + } + continue; + } + + if (!globalEvent.executeEvent()) { + std::cout << "[Error - GlobalEvents::think] Failed to execute event: " << globalEvent.getName() << std::endl; + } + + nextExecutionTime = globalEvent.getInterval(); + if (nextExecutionTime < nextScheduledTime) { + nextScheduledTime = nextExecutionTime; + } + + globalEvent.setNextExecution(globalEvent.getNextExecution() + nextExecutionTime); + } + + if (nextScheduledTime != std::numeric_limits::max()) { + thinkEventId = g_scheduler.addEvent(createSchedulerTask(nextScheduledTime, std::bind(&GlobalEvents::think, this))); + } +} + +void GlobalEvents::execute(GlobalEvent_t type) const +{ + for (const auto& it : serverMap) { + const GlobalEvent& globalEvent = it.second; + if (globalEvent.getEventType() == type) { + globalEvent.executeEvent(); + } + } +} + +GlobalEventMap GlobalEvents::getEventMap(GlobalEvent_t type) +{ + // TODO: This should be better implemented. Maybe have a map for every type. + switch (type) { + case GLOBALEVENT_NONE: return thinkMap; + case GLOBALEVENT_TIMER: return timerMap; + case GLOBALEVENT_STARTUP: + case GLOBALEVENT_SHUTDOWN: + case GLOBALEVENT_RECORD: { + GlobalEventMap retMap; + for (const auto& it : serverMap) { + if (it.second.getEventType() == type) { + retMap.emplace(it.first, it.second); + } + } + return retMap; + } + default: return GlobalEventMap(); + } +} + +GlobalEvent::GlobalEvent(LuaScriptInterface* interface) : Event(interface) {} + +bool GlobalEvent::configureEvent(const pugi::xml_node& node) +{ + pugi::xml_attribute nameAttribute = node.attribute("name"); + if (!nameAttribute) { + std::cout << "[Error - GlobalEvent::configureEvent] Missing name for a globalevent" << std::endl; + return false; + } + + name = nameAttribute.as_string(); + eventType = GLOBALEVENT_NONE; + + pugi::xml_attribute attr; + if ((attr = node.attribute("time"))) { + std::vector params = vectorAtoi(explodeString(attr.as_string(), ":")); + + int32_t hour = params.front(); + if (hour < 0 || hour > 23) { + std::cout << "[Error - GlobalEvent::configureEvent] Invalid hour \"" << attr.as_string() << "\" for globalevent with name: " << name << std::endl; + return false; + } + + interval |= hour << 16; + + int32_t min = 0; + int32_t sec = 0; + if (params.size() > 1) { + min = params[1]; + if (min < 0 || min > 59) { + std::cout << "[Error - GlobalEvent::configureEvent] Invalid minute \"" << attr.as_string() << "\" for globalevent with name: " << name << std::endl; + return false; + } + + if (params.size() > 2) { + sec = params[2]; + if (sec < 0 || sec > 59) { + std::cout << "[Error - GlobalEvent::configureEvent] Invalid second \"" << attr.as_string() << "\" for globalevent with name: " << name << std::endl; + return false; + } + } + } + + time_t current_time = time(nullptr); + tm* timeinfo = localtime(¤t_time); + timeinfo->tm_hour = hour; + timeinfo->tm_min = min; + timeinfo->tm_sec = sec; + + time_t difference = static_cast(difftime(mktime(timeinfo), current_time)); + if (difference < 0) { + difference += 86400; + } + + nextExecution = current_time + difference; + eventType = GLOBALEVENT_TIMER; + } else if ((attr = node.attribute("type"))) { + const char* value = attr.value(); + if (strcasecmp(value, "startup") == 0) { + eventType = GLOBALEVENT_STARTUP; + } else if (strcasecmp(value, "shutdown") == 0) { + eventType = GLOBALEVENT_SHUTDOWN; + } else if (strcasecmp(value, "record") == 0) { + eventType = GLOBALEVENT_RECORD; + } else { + std::cout << "[Error - GlobalEvent::configureEvent] No valid type \"" << attr.as_string() << "\" for globalevent with name " << name << std::endl; + return false; + } + } else if ((attr = node.attribute("interval"))) { + interval = std::max(SCHEDULER_MINTICKS, pugi::cast(attr.value())); + nextExecution = OTSYS_TIME() + interval; + } else { + std::cout << "[Error - GlobalEvent::configureEvent] No interval for globalevent with name " << name << std::endl; + return false; + } + return true; +} + +std::string GlobalEvent::getScriptEventName() const +{ + switch (eventType) { + case GLOBALEVENT_STARTUP: return "onStartup"; + case GLOBALEVENT_SHUTDOWN: return "onShutdown"; + case GLOBALEVENT_RECORD: return "onRecord"; + case GLOBALEVENT_TIMER: return "onTime"; + default: return "onThink"; + } +} + +bool GlobalEvent::executeRecord(uint32_t current, uint32_t old) +{ + //onRecord(current, old) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - GlobalEvent::executeRecord] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(scriptId); + + lua_pushnumber(L, current); + lua_pushnumber(L, old); + return scriptInterface->callFunction(2); +} + +bool GlobalEvent::executeEvent() const +{ + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - GlobalEvent::executeEvent] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(scriptId); + + int32_t params = 0; + if (eventType == GLOBALEVENT_NONE || eventType == GLOBALEVENT_TIMER) { + lua_pushnumber(L, interval); + params = 1; + } + + return scriptInterface->callFunction(params); +} diff --git a/src/globalevent.h b/src/globalevent.h new file mode 100644 index 0000000..6c65482 --- /dev/null +++ b/src/globalevent.h @@ -0,0 +1,126 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_GLOBALEVENT_H_B3FB9B848EA3474B9AFC326873947E3C +#define FS_GLOBALEVENT_H_B3FB9B848EA3474B9AFC326873947E3C +#include "baseevents.h" + +#include "const.h" + +enum GlobalEvent_t { + GLOBALEVENT_NONE, + GLOBALEVENT_TIMER, + + GLOBALEVENT_STARTUP, + GLOBALEVENT_SHUTDOWN, + GLOBALEVENT_RECORD, +}; + +class GlobalEvent; +using GlobalEvent_ptr = std::unique_ptr; +using GlobalEventMap = std::map; + +class GlobalEvents final : public BaseEvents +{ + public: + GlobalEvents(); + ~GlobalEvents(); + + // non-copyable + GlobalEvents(const GlobalEvents&) = delete; + GlobalEvents& operator=(const GlobalEvents&) = delete; + + void startup() const; + + void timer(); + void think(); + void execute(GlobalEvent_t type) const; + + GlobalEventMap getEventMap(GlobalEvent_t type); + static void clearMap(GlobalEventMap& map, bool fromLua); + + bool registerLuaEvent(GlobalEvent* event); + void clear(bool fromLua) override final; + + private: + std::string getScriptBaseName() const override { + return "globalevents"; + } + + Event_ptr getEvent(const std::string& nodeName) override; + bool registerEvent(Event_ptr event, const pugi::xml_node& node) override; + + LuaScriptInterface& getScriptInterface() override { + return scriptInterface; + } + LuaScriptInterface scriptInterface; + + GlobalEventMap thinkMap, serverMap, timerMap; + int32_t thinkEventId = 0, timerEventId = 0; +}; + +class GlobalEvent final : public Event +{ + public: + explicit GlobalEvent(LuaScriptInterface* interface); + + bool configureEvent(const pugi::xml_node& node) override; + + bool executeRecord(uint32_t current, uint32_t old); + bool executeEvent() const; + + GlobalEvent_t getEventType() const { + return eventType; + } + void setEventType(GlobalEvent_t type) { + eventType = type; + } + + const std::string& getName() const { + return name; + } + void setName(std::string eventName) { + name = eventName; + } + + uint32_t getInterval() const { + return interval; + } + void setInterval(uint32_t eventInterval) { + interval |= eventInterval; + } + + int64_t getNextExecution() const { + return nextExecution; + } + void setNextExecution(int64_t time) { + nextExecution = time; + } + + private: + GlobalEvent_t eventType = GLOBALEVENT_NONE; + + std::string getScriptEventName() const override; + + std::string name; + int64_t nextExecution = 0; + uint32_t interval = 0; +}; + +#endif diff --git a/src/groups.cpp b/src/groups.cpp new file mode 100644 index 0000000..90e1d5b --- /dev/null +++ b/src/groups.cpp @@ -0,0 +1,112 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "groups.h" + +#include "pugicast.h" +#include "tools.h" + +const std::unordered_map ParsePlayerFlagMap = { + {"cannotusecombat", PlayerFlag_CannotUseCombat}, + {"cannotattackplayer", PlayerFlag_CannotAttackPlayer}, + {"cannotattackmonster", PlayerFlag_CannotAttackMonster}, + {"cannotbeattacked", PlayerFlag_CannotBeAttacked}, + {"canconvinceall", PlayerFlag_CanConvinceAll}, + {"cansummonall", PlayerFlag_CanSummonAll}, + {"canillusionall", PlayerFlag_CanIllusionAll}, + {"cansenseinvisibility", PlayerFlag_CanSenseInvisibility}, + {"ignoredbymonsters", PlayerFlag_IgnoredByMonsters}, + {"notgaininfight", PlayerFlag_NotGainInFight}, + {"hasinfinitemana", PlayerFlag_HasInfiniteMana}, + {"hasinfinitesoul", PlayerFlag_HasInfiniteSoul}, + {"hasnoexhaustion", PlayerFlag_HasNoExhaustion}, + {"cannotusespells", PlayerFlag_CannotUseSpells}, + {"cannotpickupitem", PlayerFlag_CannotPickupItem}, + {"canalwayslogin", PlayerFlag_CanAlwaysLogin}, + {"canbroadcast", PlayerFlag_CanBroadcast}, + {"canedithouses", PlayerFlag_CanEditHouses}, + {"cannotbebanned", PlayerFlag_CannotBeBanned}, + {"cannotbepushed", PlayerFlag_CannotBePushed}, + {"hasinfinitecapacity", PlayerFlag_HasInfiniteCapacity}, + {"cannotpushallcreatures", PlayerFlag_CanPushAllCreatures}, + {"cantalkredprivate", PlayerFlag_CanTalkRedPrivate}, + {"cantalkredchannel", PlayerFlag_CanTalkRedChannel}, + {"talkorangehelpchannel", PlayerFlag_TalkOrangeHelpChannel}, + {"notgainexperience", PlayerFlag_NotGainExperience}, + {"notgainmana", PlayerFlag_NotGainMana}, + {"notgainhealth", PlayerFlag_NotGainHealth}, + {"notgainskill", PlayerFlag_NotGainSkill}, + {"setmaxspeed", PlayerFlag_SetMaxSpeed}, + {"specialvip", PlayerFlag_SpecialVIP}, + {"notgenerateloot", PlayerFlag_NotGenerateLoot}, + {"cantalkredchannelanonymous", PlayerFlag_CanTalkRedChannelAnonymous}, + {"ignoreprotectionzone", PlayerFlag_IgnoreProtectionZone}, + {"ignorespellcheck", PlayerFlag_IgnoreSpellCheck}, + {"ignoreweaponcheck", PlayerFlag_IgnoreWeaponCheck}, + {"cannotbemuted", PlayerFlag_CannotBeMuted}, + {"isalwayspremium", PlayerFlag_IsAlwaysPremium} +}; + +bool Groups::load() +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/XML/groups.xml"); + if (!result) { + printXMLError("Error - Groups::load", "data/XML/groups.xml", result); + return false; + } + + for (auto groupNode : doc.child("groups").children()) { + Group group; + group.id = pugi::cast(groupNode.attribute("id").value()); + group.name = groupNode.attribute("name").as_string(); + group.access = groupNode.attribute("access").as_bool(); + group.maxDepotItems = pugi::cast(groupNode.attribute("maxdepotitems").value()); + group.maxVipEntries = pugi::cast(groupNode.attribute("maxvipentries").value()); + group.flags = pugi::cast(groupNode.attribute("flags").value()); + if (pugi::xml_node node = groupNode.child("flags")) { + for (auto flagNode : node.children()) { + pugi::xml_attribute attr = flagNode.first_attribute(); + if (!attr || (attr && !attr.as_bool())) { + continue; + } + + auto parseFlag = ParsePlayerFlagMap.find(attr.name()); + if (parseFlag != ParsePlayerFlagMap.end()) { + group.flags |= parseFlag->second; + } + } + } + + groups.push_back(group); + } + return true; +} + +Group* Groups::getGroup(uint16_t id) +{ + for (Group& group : groups) { + if (group.id == id) { + return &group; + } + } + return nullptr; +} diff --git a/src/groups.h b/src/groups.h new file mode 100644 index 0000000..3297ed5 --- /dev/null +++ b/src/groups.h @@ -0,0 +1,41 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_GROUPS_H_EE39438337D148E1983FB79D936DD8F3 +#define FS_GROUPS_H_EE39438337D148E1983FB79D936DD8F3 + +struct Group { + std::string name; + uint64_t flags; + uint32_t maxDepotItems; + uint32_t maxVipEntries; + uint16_t id; + bool access; +}; + +class Groups { + public: + bool load(); + Group* getGroup(uint16_t id); + + private: + std::vector groups; +}; + +#endif diff --git a/src/guild.cpp b/src/guild.cpp new file mode 100644 index 0000000..dd21110 --- /dev/null +++ b/src/guild.cpp @@ -0,0 +1,83 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "guild.h" + +#include "game.h" + +extern Game g_game; + +void Guild::addMember(Player* player) +{ + membersOnline.push_back(player); + for (Player* member : membersOnline) { + g_game.updatePlayerHelpers(*member); + } +} + +void Guild::removeMember(Player* player) +{ + membersOnline.remove(player); + for (Player* member : membersOnline) { + g_game.updatePlayerHelpers(*member); + } + g_game.updatePlayerHelpers(*player); + + if (membersOnline.empty()) { + g_game.removeGuild(id); + delete this; + } +} + +GuildRank_ptr Guild::getRankById(uint32_t rankId) +{ + for (auto rank : ranks) { + if (rank->id == rankId) { + return rank; + } + } + return nullptr; +} + +GuildRank_ptr Guild::getRankByName(const std::string& name) const +{ + for (auto rank : ranks) { + if (rank->name == name) { + return rank; + } + } + return nullptr; +} + +GuildRank_ptr Guild::getRankByLevel(uint8_t level) const +{ + for (auto rank : ranks) { + if (rank->level == level) { + return rank; + } + } + return nullptr; +} + +void Guild::addRank(uint32_t rankId, const std::string& rankName, uint8_t level) +{ + ranks.emplace_back(std::make_shared(rankId, rankName, level)); +} diff --git a/src/guild.h b/src/guild.h new file mode 100644 index 0000000..d3343c8 --- /dev/null +++ b/src/guild.h @@ -0,0 +1,84 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_GUILD_H_C00F0A1D732E4BA88FF62ACBE74D76BC +#define FS_GUILD_H_C00F0A1D732E4BA88FF62ACBE74D76BC + +class Player; + +struct GuildRank { + uint32_t id; + std::string name; + uint8_t level; + + GuildRank(uint32_t id, std::string name, uint8_t level) : + id(id), name(std::move(name)), level(level) {} +}; + +using GuildRank_ptr = std::shared_ptr; + +class Guild +{ + public: + Guild(uint32_t id, std::string name) : name(std::move(name)), id(id) {} + + void addMember(Player* player); + void removeMember(Player* player); + + uint32_t getId() const { + return id; + } + const std::string& getName() const { + return name; + } + const std::list& getMembersOnline() const { + return membersOnline; + } + uint32_t getMemberCount() const { + return memberCount; + } + void setMemberCount(uint32_t count) { + memberCount = count; + } + + const std::vector& getRanks() const { + return ranks; + } + GuildRank_ptr getRankById(uint32_t rankId); + GuildRank_ptr getRankByName(const std::string& name) const; + GuildRank_ptr getRankByLevel(uint8_t level) const; + void addRank(uint32_t rankId, const std::string& rankName, uint8_t level); + + const std::string& getMotd() const { + return motd; + } + void setMotd(const std::string& motd) { + this->motd = motd; + } + + private: + std::list membersOnline; + std::vector ranks; + std::string name; + std::string motd; + uint32_t id; + uint32_t memberCount = 0; +}; + +#endif diff --git a/src/house.cpp b/src/house.cpp new file mode 100644 index 0000000..e011080 --- /dev/null +++ b/src/house.cpp @@ -0,0 +1,754 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "pugicast.h" + +#include "house.h" +#include "iologindata.h" +#include "game.h" +#include "configmanager.h" +#include "bed.h" + +extern ConfigManager g_config; +extern Game g_game; + +House::House(uint32_t houseId) : id(houseId) {} + +void House::addTile(HouseTile* tile) +{ + tile->setFlag(TILESTATE_PROTECTIONZONE); + houseTiles.push_back(tile); +} + +void House::setOwner(uint32_t guid, bool updateDatabase/* = true*/, Player* player/* = nullptr*/) +{ + if (updateDatabase && owner != guid) { + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "UPDATE `houses` SET `owner` = " << guid << ", `bid` = 0, `bid_end` = 0, `last_bid` = 0, `highest_bidder` = 0 WHERE `id` = " << id; + db.executeQuery(query.str()); + } + + if (isLoaded && owner == guid) { + return; + } + + isLoaded = true; + + if (owner != 0) { + //send items to depot + if (player) { + transferToDepot(player); + } else { + transferToDepot(); + } + + for (HouseTile* tile : houseTiles) { + if (const CreatureVector* creatures = tile->getCreatures()) { + for (int32_t i = creatures->size(); --i >= 0;) { + kickPlayer(nullptr, (*creatures)[i]->getPlayer()); + } + } + } + + // Remove players from beds + for (BedItem* bed : bedsList) { + if (bed->getSleeper() != 0) { + bed->wakeUp(nullptr); + } + } + + //clean access lists + owner = 0; + ownerAccountId = 0; + setAccessList(SUBOWNER_LIST, ""); + setAccessList(GUEST_LIST, ""); + + for (Door* door : doorSet) { + door->setAccessList(""); + } + } else { + std::string strRentPeriod = asLowerCaseString(g_config.getString(ConfigManager::HOUSE_RENT_PERIOD)); + time_t currentTime = time(nullptr); + if (strRentPeriod == "yearly") { + currentTime += 24 * 60 * 60 * 365; + } else if (strRentPeriod == "monthly") { + currentTime += 24 * 60 * 60 * 30; + } else if (strRentPeriod == "weekly") { + currentTime += 24 * 60 * 60 * 7; + } else if (strRentPeriod == "daily") { + currentTime += 24 * 60 * 60; + } else { + currentTime = 0; + } + + paidUntil = currentTime; + } + + rentWarnings = 0; + + if (guid != 0) { + std::string name = IOLoginData::getNameByGuid(guid); + if (!name.empty()) { + owner = guid; + ownerName = name; + ownerAccountId = IOLoginData::getAccountIdByPlayerName(name); + } + } + + updateDoorDescription(); +} + +void House::updateDoorDescription() const +{ + std::ostringstream ss; + if (owner != 0) { + ss << "It belongs to house '" << houseName << "'. " << ownerName << " owns this house."; + } else { + ss << "It belongs to house '" << houseName << "'. Nobody owns this house."; + + const int32_t housePrice = g_config.getNumber(ConfigManager::HOUSE_PRICE); + if (housePrice != -1 && g_config.getBoolean(ConfigManager::HOUSE_DOOR_SHOW_PRICE)) { + ss << " It costs " << (houseTiles.size() * housePrice) << " gold coins."; + } + } + + for (const auto& it : doorSet) { + it->setSpecialDescription(ss.str()); + } +} + +AccessHouseLevel_t House::getHouseAccessLevel(const Player* player) +{ + if (!player) { + return HOUSE_OWNER; + } + + if (g_config.getBoolean(ConfigManager::HOUSE_OWNED_BY_ACCOUNT)) { + if (ownerAccountId == player->getAccount()) { + return HOUSE_OWNER; + } + } + + if (player->hasFlag(PlayerFlag_CanEditHouses)) { + return HOUSE_OWNER; + } + + if (player->getGUID() == owner) { + return HOUSE_OWNER; + } + + if (subOwnerList.isInList(player)) { + return HOUSE_SUBOWNER; + } + + if (guestList.isInList(player)) { + return HOUSE_GUEST; + } + + return HOUSE_NOT_INVITED; +} + +bool House::kickPlayer(Player* player, Player* target) +{ + if (!target) { + return false; + } + + HouseTile* houseTile = dynamic_cast(target->getTile()); + if (!houseTile || houseTile->getHouse() != this) { + return false; + } + + if (getHouseAccessLevel(player) < getHouseAccessLevel(target) || target->hasFlag(PlayerFlag_CanEditHouses)) { + return false; + } + + Position oldPosition = target->getPosition(); + if (g_game.internalTeleport(target, getEntryPosition()) == RETURNVALUE_NOERROR) { + g_game.addMagicEffect(oldPosition, CONST_ME_POFF); + g_game.addMagicEffect(getEntryPosition(), CONST_ME_TELEPORT); + } + return true; +} + +void House::setAccessList(uint32_t listId, const std::string& textlist) +{ + if (listId == GUEST_LIST) { + guestList.parseList(textlist); + } else if (listId == SUBOWNER_LIST) { + subOwnerList.parseList(textlist); + } else { + Door* door = getDoorByNumber(listId); + if (door) { + door->setAccessList(textlist); + } + + // We dont have kick anyone + return; + } + + //kick uninvited players + for (HouseTile* tile : houseTiles) { + if (CreatureVector* creatures = tile->getCreatures()) { + for (int32_t i = creatures->size(); --i >= 0;) { + Player* player = (*creatures)[i]->getPlayer(); + if (player && !isInvited(player)) { + kickPlayer(nullptr, player); + } + } + } + } +} + +bool House::transferToDepot() const +{ + if (townId == 0 || owner == 0) { + return false; + } + + Player* player = g_game.getPlayerByGUID(owner); + if (player) { + transferToDepot(player); + } else { + Player tmpPlayer(nullptr); + if (!IOLoginData::loadPlayerById(&tmpPlayer, owner)) { + return false; + } + + transferToDepot(&tmpPlayer); + IOLoginData::savePlayer(&tmpPlayer); + } + return true; +} + +bool House::transferToDepot(Player* player) const +{ + if (townId == 0 || owner == 0) { + return false; + } + + ItemList moveItemList; + for (HouseTile* tile : houseTiles) { + if (const TileItemVector* items = tile->getItemList()) { + for (Item* item : *items) { + if (item->isPickupable()) { + moveItemList.push_back(item); + } else { + Container* container = item->getContainer(); + if (container) { + for (Item* containerItem : container->getItemList()) { + moveItemList.push_back(containerItem); + } + } + } + } + } + } + + for (Item* item : moveItemList) { + g_game.internalMoveItem(item->getParent(), player->getInbox(), INDEX_WHEREEVER, item, item->getItemCount(), nullptr, FLAG_NOLIMIT); + } + return true; +} + +bool House::getAccessList(uint32_t listId, std::string& list) const +{ + if (listId == GUEST_LIST) { + guestList.getList(list); + return true; + } else if (listId == SUBOWNER_LIST) { + subOwnerList.getList(list); + return true; + } + + Door* door = getDoorByNumber(listId); + if (!door) { + return false; + } + + return door->getAccessList(list); +} + +bool House::isInvited(const Player* player) +{ + return getHouseAccessLevel(player) != HOUSE_NOT_INVITED; +} + +void House::addDoor(Door* door) +{ + door->incrementReferenceCounter(); + doorSet.insert(door); + door->setHouse(this); + updateDoorDescription(); +} + +void House::removeDoor(Door* door) +{ + auto it = doorSet.find(door); + if (it != doorSet.end()) { + door->decrementReferenceCounter(); + doorSet.erase(it); + } +} + +void House::addBed(BedItem* bed) +{ + bedsList.push_back(bed); + bed->setHouse(this); +} + +Door* House::getDoorByNumber(uint32_t doorId) const +{ + for (Door* door : doorSet) { + if (door->getDoorId() == doorId) { + return door; + } + } + return nullptr; +} + +Door* House::getDoorByPosition(const Position& pos) +{ + for (Door* door : doorSet) { + if (door->getPosition() == pos) { + return door; + } + } + return nullptr; +} + +bool House::canEditAccessList(uint32_t listId, const Player* player) +{ + switch (getHouseAccessLevel(player)) { + case HOUSE_OWNER: + return true; + + case HOUSE_SUBOWNER: + return listId == GUEST_LIST; + + default: + return false; + } +} + +HouseTransferItem* House::getTransferItem() +{ + if (transferItem != nullptr) { + return nullptr; + } + + transfer_container.setParent(nullptr); + transferItem = HouseTransferItem::createHouseTransferItem(this); + transfer_container.addThing(transferItem); + return transferItem; +} + +void House::resetTransferItem() +{ + if (transferItem) { + Item* tmpItem = transferItem; + transferItem = nullptr; + transfer_container.setParent(nullptr); + + transfer_container.removeThing(tmpItem, tmpItem->getItemCount()); + g_game.ReleaseItem(tmpItem); + } +} + +HouseTransferItem* HouseTransferItem::createHouseTransferItem(House* house) +{ + HouseTransferItem* transferItem = new HouseTransferItem(house); + transferItem->incrementReferenceCounter(); + transferItem->setID(ITEM_DOCUMENT_RO); + transferItem->setSubType(1); + std::ostringstream ss; + ss << "It is a house transfer document for '" << house->getName() << "'."; + transferItem->setSpecialDescription(ss.str()); + return transferItem; +} + +void HouseTransferItem::onTradeEvent(TradeEvents_t event, Player* owner) +{ + if (event == ON_TRADE_TRANSFER) { + if (house) { + house->executeTransfer(this, owner); + } + + g_game.internalRemoveItem(this, 1); + } else if (event == ON_TRADE_CANCEL) { + if (house) { + house->resetTransferItem(); + } + } +} + +bool House::executeTransfer(HouseTransferItem* item, Player* newOwner) +{ + if (transferItem != item) { + return false; + } + + setOwner(newOwner->getGUID()); + transferItem = nullptr; + return true; +} + +void AccessList::parseList(const std::string& list) +{ + playerList.clear(); + guildRankList.clear(); + allowEveryone = false; + this->list = list; + if (list.empty()) { + return; + } + + std::istringstream listStream(list); + std::string line; + + int lineNo = 1; + while (getline(listStream, line)) { + if (++lineNo > 100) { + break; + } + + trimString(line); + trim_left(line, '\t'); + trim_right(line, '\t'); + trimString(line); + + if (line.empty() || line.front() == '#' || line.length() > 100) { + continue; + } + + toLowerCaseString(line); + + std::string::size_type at_pos = line.find("@"); + if (at_pos != std::string::npos) { + if (at_pos == 0) { + addGuild(line.substr(1)); + } else { + addGuildRank(line.substr(0, at_pos - 1), line.substr(at_pos + 1)); + } + } else if (line == "*") { + allowEveryone = true; + } else if (line.find("!") != std::string::npos || line.find("*") != std::string::npos || line.find("?") != std::string::npos) { + continue; // regexp no longer supported + } else { + addPlayer(line); + } + } +} + +void AccessList::addPlayer(const std::string& name) +{ + Player* player = g_game.getPlayerByName(name); + if (player) { + playerList.insert(player->getGUID()); + } else { + uint32_t guid = IOLoginData::getGuidByName(name); + if (guid != 0) { + playerList.insert(guid); + } + } +} + +namespace { + +const Guild* getGuildByName(const std::string& name) +{ + uint32_t guildId = IOGuild::getGuildIdByName(name); + if (guildId == 0) { + return nullptr; + } + + const Guild* guild = g_game.getGuild(guildId); + if (guild) { + return guild; + } + + return IOGuild::loadGuild(guildId); +} + +} + +void AccessList::addGuild(const std::string& name) +{ + const Guild* guild = getGuildByName(name); + if (guild) { + for (auto rank : guild->getRanks()) { + guildRankList.insert(rank->id); + } + } +} + +void AccessList::addGuildRank(const std::string& name, const std::string& rankName) +{ + const Guild* guild = getGuildByName(name); + if (guild) { + GuildRank_ptr rank = guild->getRankByName(rankName); + if (rank) { + guildRankList.insert(rank->id); + } + } +} + +bool AccessList::isInList(const Player* player) +{ + if (allowEveryone) { + return true; + } + + auto playerIt = playerList.find(player->getGUID()); + if (playerIt != playerList.end()) { + return true; + } + + GuildRank_ptr rank = player->getGuildRank(); + return rank && guildRankList.find(rank->id) != guildRankList.end(); +} + +void AccessList::getList(std::string& list) const +{ + list = this->list; +} + +Door::Door(uint16_t type) : Item(type) {} + +Attr_ReadValue Door::readAttr(AttrTypes_t attr, PropStream& propStream) +{ + if (attr == ATTR_HOUSEDOORID) { + uint8_t doorId; + if (!propStream.read(doorId)) { + return ATTR_READ_ERROR; + } + + setDoorId(doorId); + return ATTR_READ_CONTINUE; + } + return Item::readAttr(attr, propStream); +} + +void Door::setHouse(House* house) +{ + if (this->house != nullptr) { + return; + } + + this->house = house; + + if (!accessList) { + accessList.reset(new AccessList()); + } +} + +bool Door::canUse(const Player* player) +{ + if (!house) { + return true; + } + + if (house->getHouseAccessLevel(player) >= HOUSE_SUBOWNER) { + return true; + } + + return accessList->isInList(player); +} + +void Door::setAccessList(const std::string& textlist) +{ + if (!accessList) { + accessList.reset(new AccessList()); + } + + accessList->parseList(textlist); +} + +bool Door::getAccessList(std::string& list) const +{ + if (!house) { + return false; + } + + accessList->getList(list); + return true; +} + +void Door::onRemoved() +{ + Item::onRemoved(); + + if (house) { + house->removeDoor(this); + } +} + +House* Houses::getHouseByPlayerId(uint32_t playerId) +{ + for (const auto& it : houseMap) { + if (it.second->getOwner() == playerId) { + return it.second; + } + } + return nullptr; +} + +bool Houses::loadHousesXML(const std::string& filename) +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(filename.c_str()); + if (!result) { + printXMLError("Error - Houses::loadHousesXML", filename, result); + return false; + } + + for (auto houseNode : doc.child("houses").children()) { + pugi::xml_attribute houseIdAttribute = houseNode.attribute("houseid"); + if (!houseIdAttribute) { + return false; + } + + int32_t houseId = pugi::cast(houseIdAttribute.value()); + + House* house = getHouse(houseId); + if (!house) { + std::cout << "Error: [Houses::loadHousesXML] Unknown house, id = " << houseId << std::endl; + return false; + } + + house->setName(houseNode.attribute("name").as_string()); + + Position entryPos( + pugi::cast(houseNode.attribute("entryx").value()), + pugi::cast(houseNode.attribute("entryy").value()), + pugi::cast(houseNode.attribute("entryz").value()) + ); + if (entryPos.x == 0 && entryPos.y == 0 && entryPos.z == 0) { + std::cout << "[Warning - Houses::loadHousesXML] House entry not set" + << " - Name: " << house->getName() + << " - House id: " << houseId << std::endl; + } + house->setEntryPos(entryPos); + + house->setRent(pugi::cast(houseNode.attribute("rent").value())); + house->setTownId(pugi::cast(houseNode.attribute("townid").value())); + + house->setOwner(0, false); + } + return true; +} + +void Houses::payHouses(RentPeriod_t rentPeriod) const +{ + if (rentPeriod == RENTPERIOD_NEVER) { + return; + } + + time_t currentTime = time(nullptr); + for (const auto& it : houseMap) { + House* house = it.second; + if (house->getOwner() == 0) { + continue; + } + + const uint32_t rent = house->getRent(); + if (rent == 0 || house->getPaidUntil() > currentTime) { + continue; + } + + const uint32_t ownerId = house->getOwner(); + Town* town = g_game.map.towns.getTown(house->getTownId()); + if (!town) { + continue; + } + + Player player(nullptr); + if (!IOLoginData::loadPlayerById(&player, ownerId)) { + // Player doesn't exist, reset house owner + house->setOwner(0); + continue; + } + + if (player.getBankBalance() >= rent) { + player.setBankBalance(player.getBankBalance() - rent); + + time_t paidUntil = currentTime; + switch (rentPeriod) { + case RENTPERIOD_DAILY: + paidUntil += 24 * 60 * 60; + break; + case RENTPERIOD_WEEKLY: + paidUntil += 24 * 60 * 60 * 7; + break; + case RENTPERIOD_MONTHLY: + paidUntil += 24 * 60 * 60 * 30; + break; + case RENTPERIOD_YEARLY: + paidUntil += 24 * 60 * 60 * 365; + break; + default: + break; + } + + house->setPaidUntil(paidUntil); + } else { + if (house->getPayRentWarnings() < 7) { + int32_t daysLeft = 7 - house->getPayRentWarnings(); + + Item* letter = Item::CreateItem(ITEM_LETTER_STAMPED); + std::string period; + + switch (rentPeriod) { + case RENTPERIOD_DAILY: + period = "daily"; + break; + + case RENTPERIOD_WEEKLY: + period = "weekly"; + break; + + case RENTPERIOD_MONTHLY: + period = "monthly"; + break; + + case RENTPERIOD_YEARLY: + period = "annual"; + break; + + default: + break; + } + + std::ostringstream ss; + ss << "Warning! \nThe " << period << " rent of " << house->getRent() << " gold for your house \"" << house->getName() << "\" is payable. Have it within " << daysLeft << " days or you will lose this house."; + letter->setText(ss.str()); + g_game.internalAddItem(player.getInbox(), letter, INDEX_WHEREEVER, FLAG_NOLIMIT); + house->setPayRentWarnings(house->getPayRentWarnings() + 1); + } else { + house->setOwner(0, true, &player); + } + } + + IOLoginData::savePlayer(&player); + } +} diff --git a/src/house.h b/src/house.h new file mode 100644 index 0000000..1a73a02 --- /dev/null +++ b/src/house.h @@ -0,0 +1,315 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_HOUSE_H_EB9732E7771A438F9CD0EFA8CB4C58C4 +#define FS_HOUSE_H_EB9732E7771A438F9CD0EFA8CB4C58C4 + +#include +#include + +#include "container.h" +#include "housetile.h" +#include "position.h" + +class House; +class BedItem; +class Player; + +class AccessList +{ + public: + void parseList(const std::string& list); + void addPlayer(const std::string& name); + void addGuild(const std::string& name); + void addGuildRank(const std::string& name, const std::string& rankName); + + bool isInList(const Player* player); + + void getList(std::string& list) const; + + private: + std::string list; + std::unordered_set playerList; + std::unordered_set guildRankList; + bool allowEveryone = false; +}; + +class Door final : public Item +{ + public: + explicit Door(uint16_t type); + + // non-copyable + Door(const Door&) = delete; + Door& operator=(const Door&) = delete; + + Door* getDoor() override { + return this; + } + const Door* getDoor() const override { + return this; + } + + House* getHouse() { + return house; + } + + //serialization + Attr_ReadValue readAttr(AttrTypes_t attr, PropStream& propStream) override; + void serializeAttr(PropWriteStream&) const override {} + + void setDoorId(uint32_t doorId) { + setIntAttr(ITEM_ATTRIBUTE_DOORID, doorId); + } + uint32_t getDoorId() const { + return getIntAttr(ITEM_ATTRIBUTE_DOORID); + } + + bool canUse(const Player* player); + + void setAccessList(const std::string& textlist); + bool getAccessList(std::string& list) const; + + void onRemoved() override; + + private: + void setHouse(House* house); + + House* house = nullptr; + std::unique_ptr accessList; + friend class House; +}; + +enum AccessList_t { + GUEST_LIST = 0x100, + SUBOWNER_LIST = 0x101, +}; + +enum AccessHouseLevel_t { + HOUSE_NOT_INVITED = 0, + HOUSE_GUEST = 1, + HOUSE_SUBOWNER = 2, + HOUSE_OWNER = 3, +}; + +using HouseTileList = std::list; +using HouseBedItemList = std::list; + +class HouseTransferItem final : public Item +{ + public: + static HouseTransferItem* createHouseTransferItem(House* house); + + explicit HouseTransferItem(House* house) : Item(0), house(house) {} + + void onTradeEvent(TradeEvents_t event, Player* owner) override; + bool canTransform() const override { + return false; + } + + private: + House* house; +}; + +class House +{ + public: + explicit House(uint32_t houseId); + + void addTile(HouseTile* tile); + void updateDoorDescription() const; + + bool canEditAccessList(uint32_t listId, const Player* player); + // listId special values: + // GUEST_LIST guest list + // SUBOWNER_LIST subowner list + void setAccessList(uint32_t listId, const std::string& textlist); + bool getAccessList(uint32_t listId, std::string& list) const; + + bool isInvited(const Player* player); + + AccessHouseLevel_t getHouseAccessLevel(const Player* player); + bool kickPlayer(Player* player, Player* target); + + void setEntryPos(Position pos) { + posEntry = pos; + } + const Position& getEntryPosition() const { + return posEntry; + } + + void setName(std::string houseName) { + this->houseName = houseName; + } + const std::string& getName() const { + return houseName; + } + + void setOwner(uint32_t guid, bool updateDatabase = true, Player* player = nullptr); + uint32_t getOwner() const { + return owner; + } + + void setPaidUntil(time_t paid) { + paidUntil = paid; + } + time_t getPaidUntil() const { + return paidUntil; + } + + void setRent(uint32_t rent) { + this->rent = rent; + } + uint32_t getRent() const { + return rent; + } + + void setPayRentWarnings(uint32_t warnings) { + rentWarnings = warnings; + } + uint32_t getPayRentWarnings() const { + return rentWarnings; + } + + void setTownId(uint32_t townId) { + this->townId = townId; + } + uint32_t getTownId() const { + return townId; + } + + uint32_t getId() const { + return id; + } + + void addDoor(Door* door); + void removeDoor(Door* door); + Door* getDoorByNumber(uint32_t doorId) const; + Door* getDoorByPosition(const Position& pos); + + HouseTransferItem* getTransferItem(); + void resetTransferItem(); + bool executeTransfer(HouseTransferItem* item, Player* newOwner); + + const HouseTileList& getTiles() const { + return houseTiles; + } + + const std::set& getDoors() const { + return doorSet; + } + + void addBed(BedItem* bed); + const HouseBedItemList& getBeds() const { + return bedsList; + } + uint32_t getBedCount() { + return static_cast(std::ceil(bedsList.size() / 2.)); //each bed takes 2 sqms of space, ceil is just for bad maps + } + + private: + bool transferToDepot() const; + bool transferToDepot(Player* player) const; + + AccessList guestList; + AccessList subOwnerList; + + Container transfer_container{ITEM_LOCKER1}; + + HouseTileList houseTiles; + std::set doorSet; + HouseBedItemList bedsList; + + std::string houseName; + std::string ownerName; + + HouseTransferItem* transferItem = nullptr; + + time_t paidUntil = 0; + + uint32_t id; + uint32_t owner = 0; + uint32_t ownerAccountId = 0; + uint32_t rentWarnings = 0; + uint32_t rent = 0; + uint32_t townId = 0; + + Position posEntry = {}; + + bool isLoaded = false; +}; + +using HouseMap = std::map; + +enum RentPeriod_t { + RENTPERIOD_DAILY, + RENTPERIOD_WEEKLY, + RENTPERIOD_MONTHLY, + RENTPERIOD_YEARLY, + RENTPERIOD_NEVER, +}; + +class Houses +{ + public: + Houses() = default; + ~Houses() { + for (const auto& it : houseMap) { + delete it.second; + } + } + + // non-copyable + Houses(const Houses&) = delete; + Houses& operator=(const Houses&) = delete; + + House* addHouse(uint32_t id) { + auto it = houseMap.find(id); + if (it != houseMap.end()) { + return it->second; + } + + House* house = new House(id); + houseMap[id] = house; + return house; + } + + House* getHouse(uint32_t houseId) { + auto it = houseMap.find(houseId); + if (it == houseMap.end()) { + return nullptr; + } + return it->second; + } + + House* getHouseByPlayerId(uint32_t playerId); + + bool loadHousesXML(const std::string& filename); + + void payHouses(RentPeriod_t rentPeriod) const; + + const HouseMap& getHouses() const { + return houseMap; + } + + private: + HouseMap houseMap; +}; + +#endif diff --git a/src/housetile.cpp b/src/housetile.cpp new file mode 100644 index 0000000..3cf8993 --- /dev/null +++ b/src/housetile.cpp @@ -0,0 +1,122 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "housetile.h" +#include "house.h" +#include "game.h" + +extern Game g_game; + +HouseTile::HouseTile(int32_t x, int32_t y, int32_t z, House* house) : + DynamicTile(x, y, z), house(house) {} + +void HouseTile::addThing(int32_t index, Thing* thing) +{ + Tile::addThing(index, thing); + + if (!thing->getParent()) { + return; + } + + if (Item* item = thing->getItem()) { + updateHouse(item); + } +} + +void HouseTile::internalAddThing(uint32_t index, Thing* thing) +{ + Tile::internalAddThing(index, thing); + + if (!thing->getParent()) { + return; + } + + if (Item* item = thing->getItem()) { + updateHouse(item); + } +} + +void HouseTile::updateHouse(Item* item) +{ + if (item->getParent() != this) { + return; + } + + Door* door = item->getDoor(); + if (door) { + if (door->getDoorId() != 0) { + house->addDoor(door); + } + } else { + BedItem* bed = item->getBed(); + if (bed) { + house->addBed(bed); + } + } +} + +ReturnValue HouseTile::queryAdd(int32_t index, const Thing& thing, uint32_t count, uint32_t flags, Creature* actor/* = nullptr*/) const +{ + if (const Creature* creature = thing.getCreature()) { + if (const Player* player = creature->getPlayer()) { + if (!house->isInvited(player)) { + return RETURNVALUE_PLAYERISNOTINVITED; + } + } else { + return RETURNVALUE_NOTPOSSIBLE; + } + } else if (thing.getItem() && actor) { + Player* actorPlayer = actor->getPlayer(); + if (!house->isInvited(actorPlayer)) { + return RETURNVALUE_CANNOTTHROW; + } + } + return Tile::queryAdd(index, thing, count, flags, actor); +} + +Tile* HouseTile::queryDestination(int32_t& index, const Thing& thing, Item** destItem, uint32_t& flags) +{ + if (const Creature* creature = thing.getCreature()) { + if (const Player* player = creature->getPlayer()) { + if (!house->isInvited(player)) { + const Position& entryPos = house->getEntryPosition(); + Tile* destTile = g_game.map.getTile(entryPos); + if (!destTile) { + std::cout << "Error: [HouseTile::queryDestination] House entry not correct" + << " - Name: " << house->getName() + << " - House id: " << house->getId() + << " - Tile not found: " << entryPos << std::endl; + + destTile = g_game.map.getTile(player->getTemplePosition()); + if (!destTile) { + destTile = &(Tile::nullptr_tile); + } + } + + index = -1; + *destItem = nullptr; + return destTile; + } + } + } + + return Tile::queryDestination(index, thing, destItem, flags); +} diff --git a/src/housetile.h b/src/housetile.h new file mode 100644 index 0000000..fcb6d22 --- /dev/null +++ b/src/housetile.h @@ -0,0 +1,52 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_HOUSETILE_H_57D59BEC1CE741D9B142BFC54634505B +#define FS_HOUSETILE_H_57D59BEC1CE741D9B142BFC54634505B + +#include "tile.h" + +class House; + +class HouseTile final : public DynamicTile +{ + public: + HouseTile(int32_t x, int32_t y, int32_t z, House* house); + + //cylinder implementations + ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count, + uint32_t flags, Creature* actor = nullptr) const override; + + Tile* queryDestination(int32_t& index, const Thing& thing, Item** destItem, + uint32_t& flags) override; + + void addThing(int32_t index, Thing* thing) override; + void internalAddThing(uint32_t index, Thing* thing) override; + + House* getHouse() { + return house; + } + + private: + void updateHouse(Item* item); + + House* house; +}; + +#endif diff --git a/src/inbox.cpp b/src/inbox.cpp new file mode 100644 index 0000000..42dff7d --- /dev/null +++ b/src/inbox.cpp @@ -0,0 +1,72 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "inbox.h" +#include "tools.h" + +Inbox::Inbox(uint16_t type) : Container(type, 30, false, true) {} + +ReturnValue Inbox::queryAdd(int32_t, const Thing& thing, uint32_t, + uint32_t flags, Creature*) const +{ + if (!hasBitSet(FLAG_NOLIMIT, flags)) { + return RETURNVALUE_CONTAINERNOTENOUGHROOM; + } + + const Item* item = thing.getItem(); + if (!item) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (item == this) { + return RETURNVALUE_THISISIMPOSSIBLE; + } + + if (!item->isPickupable()) { + return RETURNVALUE_CANNOTPICKUP; + } + + return RETURNVALUE_NOERROR; +} + +void Inbox::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t) +{ + Cylinder* parent = getParent(); + if (parent != nullptr) { + parent->postAddNotification(thing, oldParent, index, LINK_PARENT); + } +} + +void Inbox::postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t) +{ + Cylinder* parent = getParent(); + if (parent != nullptr) { + parent->postRemoveNotification(thing, newParent, index, LINK_PARENT); + } +} + +Cylinder* Inbox::getParent() const +{ + if (parent) { + return parent->getParent(); + } + return nullptr; +} diff --git a/src/inbox.h b/src/inbox.h new file mode 100644 index 0000000..8ad746a --- /dev/null +++ b/src/inbox.h @@ -0,0 +1,49 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_INBOX_H_C3EF10190329447883B9C3479234EE5C +#define FS_INBOX_H_C3EF10190329447883B9C3479234EE5C + +#include "container.h" + +class Inbox final : public Container +{ + public: + explicit Inbox(uint16_t type); + + //cylinder implementations + ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count, + uint32_t flags, Creature* actor = nullptr) const override; + + void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + + //overrides + bool canRemove() const override { + return false; + } + + Cylinder* getParent() const override; + Cylinder* getRealParent() const override { + return parent; + } +}; + +#endif + diff --git a/src/ioguild.cpp b/src/ioguild.cpp new file mode 100644 index 0000000..ed48b14 --- /dev/null +++ b/src/ioguild.cpp @@ -0,0 +1,79 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "database.h" +#include "guild.h" +#include "ioguild.h" + +Guild* IOGuild::loadGuild(uint32_t guildId) +{ + Database& db = Database::getInstance(); + std::ostringstream query; + query << "SELECT `name` FROM `guilds` WHERE `id` = " << guildId; + if (DBResult_ptr result = db.storeQuery(query.str())) { + Guild* guild = new Guild(guildId, result->getString("name")); + + query.str(std::string()); + query << "SELECT `id`, `name`, `level` FROM `guild_ranks` WHERE `guild_id` = " << guildId; + + if ((result = db.storeQuery(query.str()))) { + do { + guild->addRank(result->getNumber("id"), result->getString("name"), result->getNumber("level")); + } while (result->next()); + } + return guild; + } + return nullptr; +} + +uint32_t IOGuild::getGuildIdByName(const std::string& name) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `id` FROM `guilds` WHERE `name` = " << db.escapeString(name); + + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return 0; + } + return result->getNumber("id"); +} + +void IOGuild::getWarList(uint32_t guildId, GuildWarVector& guildWarVector) +{ + std::ostringstream query; + query << "SELECT `guild1`, `guild2` FROM `guild_wars` WHERE (`guild1` = " << guildId << " OR `guild2` = " << guildId << ") AND `ended` = 0 AND `status` = 1"; + + DBResult_ptr result = Database::getInstance().storeQuery(query.str()); + if (!result) { + return; + } + + do { + uint32_t guild1 = result->getNumber("guild1"); + if (guildId != guild1) { + guildWarVector.push_back(guild1); + } else { + guildWarVector.push_back(result->getNumber("guild2")); + } + } while (result->next()); +} diff --git a/src/ioguild.h b/src/ioguild.h new file mode 100644 index 0000000..66e4339 --- /dev/null +++ b/src/ioguild.h @@ -0,0 +1,34 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_IOGUILD_H_EF9ACEBA0B844C388B70FF52E69F1AFF +#define FS_IOGUILD_H_EF9ACEBA0B844C388B70FF52E69F1AFF + +class Guild; +using GuildWarVector = std::vector; + +class IOGuild +{ + public: + static Guild* loadGuild(uint32_t guildId); + static uint32_t getGuildIdByName(const std::string& name); + static void getWarList(uint32_t guildId, GuildWarVector& guildWarVector); +}; + +#endif diff --git a/src/iologindata.cpp b/src/iologindata.cpp new file mode 100644 index 0000000..05b4466 --- /dev/null +++ b/src/iologindata.cpp @@ -0,0 +1,1048 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "iologindata.h" +#include "configmanager.h" +#include "game.h" + +extern ConfigManager g_config; +extern Game g_game; + +Account IOLoginData::loadAccount(uint32_t accno) +{ + Account account; + + std::ostringstream query; + query << "SELECT `id`, `name`, `password`, `type`, `premdays`, `lastday` FROM `accounts` WHERE `id` = " << accno; + DBResult_ptr result = Database::getInstance().storeQuery(query.str()); + if (!result) { + return account; + } + + account.id = result->getNumber("id"); + account.name = result->getString("name"); + account.accountType = static_cast(result->getNumber("type")); + account.premiumDays = result->getNumber("premdays"); + account.lastDay = result->getNumber("lastday"); + return account; +} + +bool IOLoginData::saveAccount(const Account& acc) +{ + std::ostringstream query; + query << "UPDATE `accounts` SET `premdays` = " << acc.premiumDays << ", `lastday` = " << acc.lastDay << " WHERE `id` = " << acc.id; + return Database::getInstance().executeQuery(query.str()); +} + +std::string decodeSecret(const std::string& secret) +{ + // simple base32 decoding + std::string key; + key.reserve(10); + + uint32_t buffer = 0, left = 0; + for (const auto& ch : secret) { + buffer <<= 5; + if (ch >= 'A' && ch <= 'Z') { + buffer |= (ch & 0x1F) - 1; + } else if (ch >= '2' && ch <= '7') { + buffer |= ch - 24; + } else { + // if a key is broken, return empty and the comparison + // will always be false since the token must not be empty + return {}; + } + + left += 5; + if (left >= 8) { + left -= 8; + key.push_back(static_cast(buffer >> left)); + } + } + + return key; +} + +bool IOLoginData::loginserverAuthentication(const std::string& name, const std::string& password, Account& account) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `id`, `name`, `password`, `secret`, `type`, `premdays`, `lastday` FROM `accounts` WHERE `name` = " << db.escapeString(name); + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return false; + } + + if (transformToSHA1(password) != result->getString("password")) { + return false; + } + + account.id = result->getNumber("id"); + account.name = result->getString("name"); + account.key = decodeSecret(result->getString("secret")); + account.accountType = static_cast(result->getNumber("type")); + account.premiumDays = result->getNumber("premdays"); + account.lastDay = result->getNumber("lastday"); + + query.str(std::string()); + query << "SELECT `name`, `deletion` FROM `players` WHERE `account_id` = " << account.id; + result = db.storeQuery(query.str()); + if (result) { + do { + if (result->getNumber("deletion") == 0) { + account.characters.push_back(result->getString("name")); + } + } while (result->next()); + std::sort(account.characters.begin(), account.characters.end()); + } + return true; +} + +uint32_t IOLoginData::gameworldAuthentication(const std::string& accountName, const std::string& password, std::string& characterName, std::string& token, uint32_t tokenTime) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `id`, `password`, `secret` FROM `accounts` WHERE `name` = " << db.escapeString(accountName); + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return 0; + } + + std::string secret = decodeSecret(result->getString("secret")); + if (!secret.empty()) { + if (token.empty()) { + return 0; + } + + bool tokenValid = token == generateToken(secret, tokenTime) || token == generateToken(secret, tokenTime - 1) || token == generateToken(secret, tokenTime + 1); + if (!tokenValid) { + return 0; + } + } + + if (transformToSHA1(password) != result->getString("password")) { + return 0; + } + + uint32_t accountId = result->getNumber("id"); + + query.str(std::string()); + query << "SELECT `account_id`, `name`, `deletion` FROM `players` WHERE `name` = " << db.escapeString(characterName); + result = db.storeQuery(query.str()); + if (!result) { + return 0; + } + + if (result->getNumber("account_id") != accountId || result->getNumber("deletion") != 0) { + return 0; + } + characterName = result->getString("name"); + return accountId; +} + +uint32_t IOLoginData::getAccountIdByPlayerName(const std::string& playerName) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `account_id` FROM `players` WHERE `name` = " << db.escapeString(playerName); + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return 0; + } + return result->getNumber("account_id"); +} + +AccountType_t IOLoginData::getAccountType(uint32_t accountId) +{ + std::ostringstream query; + query << "SELECT `type` FROM `accounts` WHERE `id` = " << accountId; + DBResult_ptr result = Database::getInstance().storeQuery(query.str()); + if (!result) { + return ACCOUNT_TYPE_NORMAL; + } + return static_cast(result->getNumber("type")); +} + +void IOLoginData::setAccountType(uint32_t accountId, AccountType_t accountType) +{ + std::ostringstream query; + query << "UPDATE `accounts` SET `type` = " << static_cast(accountType) << " WHERE `id` = " << accountId; + Database::getInstance().executeQuery(query.str()); +} + +void IOLoginData::updateOnlineStatus(uint32_t guid, bool login) +{ + if (g_config.getBoolean(ConfigManager::ALLOW_CLONES)) { + return; + } + + std::ostringstream query; + if (login) { + query << "INSERT INTO `players_online` VALUES (" << guid << ')'; + } else { + query << "DELETE FROM `players_online` WHERE `player_id` = " << guid; + } + Database::getInstance().executeQuery(query.str()); +} + +bool IOLoginData::preloadPlayer(Player* player, const std::string& name) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `id`, `account_id`, `group_id`, `deletion`, (SELECT `type` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `account_type`"; + if (!g_config.getBoolean(ConfigManager::FREE_PREMIUM)) { + query << ", (SELECT `premdays` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `premium_days`"; + } + query << " FROM `players` WHERE `name` = " << db.escapeString(name); + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return false; + } + + if (result->getNumber("deletion") != 0) { + return false; + } + + player->setGUID(result->getNumber("id")); + Group* group = g_game.groups.getGroup(result->getNumber("group_id")); + if (!group) { + std::cout << "[Error - IOLoginData::preloadPlayer] " << player->name << " has Group ID " << result->getNumber("group_id") << " which doesn't exist." << std::endl; + return false; + } + player->setGroup(group); + player->accountNumber = result->getNumber("account_id"); + player->accountType = static_cast(result->getNumber("account_type")); + if (!g_config.getBoolean(ConfigManager::FREE_PREMIUM)) { + player->premiumDays = result->getNumber("premium_days"); + } else { + player->premiumDays = std::numeric_limits::max(); + } + return true; +} + +bool IOLoginData::loadPlayerById(Player* player, uint32_t id) +{ + Database& db = Database::getInstance(); + std::ostringstream query; + query << "SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries`, `direction` FROM `players` WHERE `id` = " << id; + return loadPlayer(player, db.storeQuery(query.str())); +} + +bool IOLoginData::loadPlayerByName(Player* player, const std::string& name) +{ + Database& db = Database::getInstance(); + std::ostringstream query; + query << "SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, `experience`, `level`, `maglevel`, `health`, `healthmax`, `blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, `lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookaddons`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, `lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, `town_id`, `balance`, `offlinetraining_time`, `offlinetraining_skill`, `stamina`, `skill_fist`, `skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, `skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, `skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, `skill_fishing`, `skill_fishing_tries`, `direction` FROM `players` WHERE `name` = " << db.escapeString(name); + return loadPlayer(player, db.storeQuery(query.str())); +} + +bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) +{ + if (!result) { + return false; + } + + Database& db = Database::getInstance(); + + uint32_t accno = result->getNumber("account_id"); + Account acc = loadAccount(accno); + + player->setGUID(result->getNumber("id")); + player->name = result->getString("name"); + player->accountNumber = accno; + + player->accountType = acc.accountType; + + if (g_config.getBoolean(ConfigManager::FREE_PREMIUM)) { + player->premiumDays = std::numeric_limits::max(); + } else { + player->premiumDays = acc.premiumDays; + } + + Group* group = g_game.groups.getGroup(result->getNumber("group_id")); + if (!group) { + std::cout << "[Error - IOLoginData::loadPlayer] " << player->name << " has Group ID " << result->getNumber("group_id") << " which doesn't exist" << std::endl; + return false; + } + player->setGroup(group); + + player->bankBalance = result->getNumber("balance"); + + player->setSex(static_cast(result->getNumber("sex"))); + player->level = std::max(1, result->getNumber("level")); + + uint64_t experience = result->getNumber("experience"); + + uint64_t currExpCount = Player::getExpForLevel(player->level); + uint64_t nextExpCount = Player::getExpForLevel(player->level + 1); + if (experience < currExpCount || experience > nextExpCount) { + experience = currExpCount; + } + + player->experience = experience; + + if (currExpCount < nextExpCount) { + player->levelPercent = Player::getPercentLevel(player->experience - currExpCount, nextExpCount - currExpCount); + } else { + player->levelPercent = 0; + } + + player->soul = result->getNumber("soul"); + player->capacity = result->getNumber("cap") * 100; + player->blessings = result->getNumber("blessings"); + + unsigned long conditionsSize; + const char* conditions = result->getStream("conditions", conditionsSize); + PropStream propStream; + propStream.init(conditions, conditionsSize); + + Condition* condition = Condition::createCondition(propStream); + while (condition) { + if (condition->unserialize(propStream)) { + player->storedConditionList.push_front(condition); + } else { + delete condition; + } + condition = Condition::createCondition(propStream); + } + + if (!player->setVocation(result->getNumber("vocation"))) { + std::cout << "[Error - IOLoginData::loadPlayer] " << player->name << " has Vocation ID " << result->getNumber("vocation") << " which doesn't exist" << std::endl; + return false; + } + + player->mana = result->getNumber("mana"); + player->manaMax = result->getNumber("manamax"); + player->magLevel = result->getNumber("maglevel"); + + uint64_t nextManaCount = player->vocation->getReqMana(player->magLevel + 1); + uint64_t manaSpent = result->getNumber("manaspent"); + if (manaSpent > nextManaCount) { + manaSpent = 0; + } + + player->manaSpent = manaSpent; + player->magLevelPercent = Player::getPercentLevel(player->manaSpent, nextManaCount); + + player->health = result->getNumber("health"); + player->healthMax = result->getNumber("healthmax"); + + player->defaultOutfit.lookType = result->getNumber("looktype"); + player->defaultOutfit.lookHead = result->getNumber("lookhead"); + player->defaultOutfit.lookBody = result->getNumber("lookbody"); + player->defaultOutfit.lookLegs = result->getNumber("looklegs"); + player->defaultOutfit.lookFeet = result->getNumber("lookfeet"); + player->defaultOutfit.lookAddons = result->getNumber("lookaddons"); + player->currentOutfit = player->defaultOutfit; + player->direction = static_cast (result->getNumber("direction")); + + if (g_game.getWorldType() != WORLD_TYPE_PVP_ENFORCED) { + const time_t skullSeconds = result->getNumber("skulltime") - time(nullptr); + if (skullSeconds > 0) { + //ensure that we round up the number of ticks + player->skullTicks = (skullSeconds + 2); + + uint16_t skull = result->getNumber("skull"); + if (skull == SKULL_RED) { + player->skull = SKULL_RED; + } else if (skull == SKULL_BLACK) { + player->skull = SKULL_BLACK; + } + } + } + + player->loginPosition.x = result->getNumber("posx"); + player->loginPosition.y = result->getNumber("posy"); + player->loginPosition.z = result->getNumber("posz"); + + player->lastLoginSaved = result->getNumber("lastlogin"); + player->lastLogout = result->getNumber("lastlogout"); + + player->offlineTrainingTime = result->getNumber("offlinetraining_time") * 1000; + player->offlineTrainingSkill = result->getNumber("offlinetraining_skill"); + + Town* town = g_game.map.towns.getTown(result->getNumber("town_id")); + if (!town) { + std::cout << "[Error - IOLoginData::loadPlayer] " << player->name << " has Town ID " << result->getNumber("town_id") << " which doesn't exist" << std::endl; + return false; + } + + player->town = town; + + const Position& loginPos = player->loginPosition; + if (loginPos.x == 0 && loginPos.y == 0 && loginPos.z == 0) { + player->loginPosition = player->getTemplePosition(); + } + + player->staminaMinutes = result->getNumber("stamina"); + + static const std::string skillNames[] = {"skill_fist", "skill_club", "skill_sword", "skill_axe", "skill_dist", "skill_shielding", "skill_fishing"}; + static const std::string skillNameTries[] = {"skill_fist_tries", "skill_club_tries", "skill_sword_tries", "skill_axe_tries", "skill_dist_tries", "skill_shielding_tries", "skill_fishing_tries"}; + static constexpr size_t size = sizeof(skillNames) / sizeof(std::string); + for (uint8_t i = 0; i < size; ++i) { + uint16_t skillLevel = result->getNumber(skillNames[i]); + uint64_t skillTries = result->getNumber(skillNameTries[i]); + uint64_t nextSkillTries = player->vocation->getReqSkillTries(i, skillLevel + 1); + if (skillTries > nextSkillTries) { + skillTries = 0; + } + + player->skills[i].level = skillLevel; + player->skills[i].tries = skillTries; + player->skills[i].percent = Player::getPercentLevel(skillTries, nextSkillTries); + } + + std::ostringstream query; + query << "SELECT `guild_id`, `rank_id`, `nick` FROM `guild_membership` WHERE `player_id` = " << player->getGUID(); + if ((result = db.storeQuery(query.str()))) { + uint32_t guildId = result->getNumber("guild_id"); + uint32_t playerRankId = result->getNumber("rank_id"); + player->guildNick = result->getString("nick"); + + Guild* guild = g_game.getGuild(guildId); + if (!guild) { + guild = IOGuild::loadGuild(guildId); + if (guild) { + g_game.addGuild(guild); + } else { + std::cout << "[Warning - IOLoginData::loadPlayer] " << player->name << " has Guild ID " << guildId << " which doesn't exist" << std::endl; + } + } + + if (guild) { + player->guild = guild; + GuildRank_ptr rank = guild->getRankById(playerRankId); + if (!rank) { + query.str(std::string()); + query << "SELECT `id`, `name`, `level` FROM `guild_ranks` WHERE `id` = " << playerRankId; + + if ((result = db.storeQuery(query.str()))) { + guild->addRank(result->getNumber("id"), result->getString("name"), result->getNumber("level")); + } + + rank = guild->getRankById(playerRankId); + if (!rank) { + player->guild = nullptr; + } + } + + player->guildRank = rank; + + IOGuild::getWarList(guildId, player->guildWarVector); + + query.str(std::string()); + query << "SELECT COUNT(*) AS `members` FROM `guild_membership` WHERE `guild_id` = " << guildId; + if ((result = db.storeQuery(query.str()))) { + guild->setMemberCount(result->getNumber("members")); + } + } + } + + query.str(std::string()); + query << "SELECT `player_id`, `name` FROM `player_spells` WHERE `player_id` = " << player->getGUID(); + if ((result = db.storeQuery(query.str()))) { + do { + player->learnedInstantSpellList.emplace_front(result->getString("name")); + } while (result->next()); + } + + //load inventory items + ItemMap itemMap; + + query.str(std::string()); + query << "SELECT `pid`, `sid`, `itemtype`, `count`, `attributes` FROM `player_items` WHERE `player_id` = " << player->getGUID() << " ORDER BY `sid` DESC"; + if ((result = db.storeQuery(query.str()))) { + loadItems(itemMap, result); + + for (ItemMap::const_reverse_iterator it = itemMap.rbegin(), end = itemMap.rend(); it != end; ++it) { + const std::pair& pair = it->second; + Item* item = pair.first; + int32_t pid = pair.second; + if (pid >= CONST_SLOT_FIRST && pid <= CONST_SLOT_LAST) { + player->internalAddThing(pid, item); + } else { + ItemMap::const_iterator it2 = itemMap.find(pid); + if (it2 == itemMap.end()) { + continue; + } + + Container* container = it2->second.first->getContainer(); + if (container) { + container->internalAddThing(item); + } + } + } + } + + //load depot items + itemMap.clear(); + + query.str(std::string()); + query << "SELECT `pid`, `sid`, `itemtype`, `count`, `attributes` FROM `player_depotitems` WHERE `player_id` = " << player->getGUID() << " ORDER BY `sid` DESC"; + if ((result = db.storeQuery(query.str()))) { + loadItems(itemMap, result); + + for (ItemMap::const_reverse_iterator it = itemMap.rbegin(), end = itemMap.rend(); it != end; ++it) { + const std::pair& pair = it->second; + Item* item = pair.first; + + int32_t pid = pair.second; + if (pid >= 0 && pid < 100) { + DepotChest* depotChest = player->getDepotChest(pid, true); + if (depotChest) { + depotChest->internalAddThing(item); + } + } else { + ItemMap::const_iterator it2 = itemMap.find(pid); + if (it2 == itemMap.end()) { + continue; + } + + Container* container = it2->second.first->getContainer(); + if (container) { + container->internalAddThing(item); + } + } + } + } + + //load inbox items + itemMap.clear(); + + query.str(std::string()); + query << "SELECT `pid`, `sid`, `itemtype`, `count`, `attributes` FROM `player_inboxitems` WHERE `player_id` = " << player->getGUID() << " ORDER BY `sid` DESC"; + if ((result = db.storeQuery(query.str()))) { + loadItems(itemMap, result); + + for (ItemMap::const_reverse_iterator it = itemMap.rbegin(), end = itemMap.rend(); it != end; ++it) { + const std::pair& pair = it->second; + Item* item = pair.first; + int32_t pid = pair.second; + + if (pid >= 0 && pid < 100) { + player->getInbox()->internalAddThing(item); + } else { + ItemMap::const_iterator it2 = itemMap.find(pid); + + if (it2 == itemMap.end()) { + continue; + } + + Container* container = it2->second.first->getContainer(); + if (container) { + container->internalAddThing(item); + } + } + } + } + + //load storage map + query.str(std::string()); + query << "SELECT `key`, `value` FROM `player_storage` WHERE `player_id` = " << player->getGUID(); + if ((result = db.storeQuery(query.str()))) { + do { + player->addStorageValue(result->getNumber("key"), result->getNumber("value"), true); + } while (result->next()); + } + + //load vip + query.str(std::string()); + query << "SELECT `player_id` FROM `account_viplist` WHERE `account_id` = " << player->getAccount(); + if ((result = db.storeQuery(query.str()))) { + do { + player->addVIPInternal(result->getNumber("player_id")); + } while (result->next()); + } + + player->updateBaseSpeed(); + player->updateInventoryWeight(); + player->updateItemsLight(true); + return true; +} + +bool IOLoginData::saveItems(const Player* player, const ItemBlockList& itemList, DBInsert& query_insert, PropWriteStream& propWriteStream) +{ + std::ostringstream ss; + + using ContainerBlock = std::pair; + std::list queue; + + int32_t runningId = 100; + + Database& db = Database::getInstance(); + for (const auto& it : itemList) { + int32_t pid = it.first; + Item* item = it.second; + ++runningId; + + propWriteStream.clear(); + item->serializeAttr(propWriteStream); + + size_t attributesSize; + const char* attributes = propWriteStream.getStream(attributesSize); + + ss << player->getGUID() << ',' << pid << ',' << runningId << ',' << item->getID() << ',' << item->getSubType() << ',' << db.escapeBlob(attributes, attributesSize); + if (!query_insert.addRow(ss)) { + return false; + } + + if (Container* container = item->getContainer()) { + queue.emplace_back(container, runningId); + } + } + + while (!queue.empty()) { + const ContainerBlock& cb = queue.front(); + Container* container = cb.first; + int32_t parentId = cb.second; + queue.pop_front(); + + for (Item* item : container->getItemList()) { + ++runningId; + + Container* subContainer = item->getContainer(); + if (subContainer) { + queue.emplace_back(subContainer, runningId); + } + + propWriteStream.clear(); + item->serializeAttr(propWriteStream); + + size_t attributesSize; + const char* attributes = propWriteStream.getStream(attributesSize); + + ss << player->getGUID() << ',' << parentId << ',' << runningId << ',' << item->getID() << ',' << item->getSubType() << ',' << db.escapeBlob(attributes, attributesSize); + if (!query_insert.addRow(ss)) { + return false; + } + } + } + return query_insert.execute(); +} + +bool IOLoginData::savePlayer(Player* player) +{ + if (player->getHealth() <= 0) { + player->changeHealth(1); + } + + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `save` FROM `players` WHERE `id` = " << player->getGUID(); + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return false; + } + + if (result->getNumber("save") == 0) { + query.str(std::string()); + query << "UPDATE `players` SET `lastlogin` = " << player->lastLoginSaved << ", `lastip` = " << player->lastIP << " WHERE `id` = " << player->getGUID(); + return db.executeQuery(query.str()); + } + + //serialize conditions + PropWriteStream propWriteStream; + for (Condition* condition : player->conditions) { + if (condition->isPersistent()) { + condition->serialize(propWriteStream); + propWriteStream.write(CONDITIONATTR_END); + } + } + + size_t conditionsSize; + const char* conditions = propWriteStream.getStream(conditionsSize); + + //First, an UPDATE query to write the player itself + query.str(std::string()); + query << "UPDATE `players` SET "; + query << "`level` = " << player->level << ','; + query << "`group_id` = " << player->group->id << ','; + query << "`vocation` = " << player->getVocationId() << ','; + query << "`health` = " << player->health << ','; + query << "`healthmax` = " << player->healthMax << ','; + query << "`experience` = " << player->experience << ','; + query << "`lookbody` = " << static_cast(player->defaultOutfit.lookBody) << ','; + query << "`lookfeet` = " << static_cast(player->defaultOutfit.lookFeet) << ','; + query << "`lookhead` = " << static_cast(player->defaultOutfit.lookHead) << ','; + query << "`looklegs` = " << static_cast(player->defaultOutfit.lookLegs) << ','; + query << "`looktype` = " << player->defaultOutfit.lookType << ','; + query << "`lookaddons` = " << static_cast(player->defaultOutfit.lookAddons) << ','; + query << "`maglevel` = " << player->magLevel << ','; + query << "`mana` = " << player->mana << ','; + query << "`manamax` = " << player->manaMax << ','; + query << "`manaspent` = " << player->manaSpent << ','; + query << "`soul` = " << static_cast(player->soul) << ','; + query << "`town_id` = " << player->town->getID() << ','; + + const Position& loginPosition = player->getLoginPosition(); + query << "`posx` = " << loginPosition.getX() << ','; + query << "`posy` = " << loginPosition.getY() << ','; + query << "`posz` = " << loginPosition.getZ() << ','; + + query << "`cap` = " << (player->capacity / 100) << ','; + query << "`sex` = " << static_cast(player->sex) << ','; + + if (player->lastLoginSaved != 0) { + query << "`lastlogin` = " << player->lastLoginSaved << ','; + } + + if (player->lastIP != 0) { + query << "`lastip` = " << player->lastIP << ','; + } + + query << "`conditions` = " << db.escapeBlob(conditions, conditionsSize) << ','; + + if (g_game.getWorldType() != WORLD_TYPE_PVP_ENFORCED) { + int64_t skullTime = 0; + + if (player->skullTicks > 0) { + skullTime = time(nullptr) + player->skullTicks; + } + query << "`skulltime` = " << skullTime << ','; + + Skulls_t skull = SKULL_NONE; + if (player->skull == SKULL_RED) { + skull = SKULL_RED; + } else if (player->skull == SKULL_BLACK) { + skull = SKULL_BLACK; + } + query << "`skull` = " << static_cast(skull) << ','; + } + + query << "`lastlogout` = " << player->getLastLogout() << ','; + query << "`balance` = " << player->bankBalance << ','; + query << "`offlinetraining_time` = " << player->getOfflineTrainingTime() / 1000 << ','; + query << "`offlinetraining_skill` = " << player->getOfflineTrainingSkill() << ','; + query << "`stamina` = " << player->getStaminaMinutes() << ','; + + query << "`skill_fist` = " << player->skills[SKILL_FIST].level << ','; + query << "`skill_fist_tries` = " << player->skills[SKILL_FIST].tries << ','; + query << "`skill_club` = " << player->skills[SKILL_CLUB].level << ','; + query << "`skill_club_tries` = " << player->skills[SKILL_CLUB].tries << ','; + query << "`skill_sword` = " << player->skills[SKILL_SWORD].level << ','; + query << "`skill_sword_tries` = " << player->skills[SKILL_SWORD].tries << ','; + query << "`skill_axe` = " << player->skills[SKILL_AXE].level << ','; + query << "`skill_axe_tries` = " << player->skills[SKILL_AXE].tries << ','; + query << "`skill_dist` = " << player->skills[SKILL_DISTANCE].level << ','; + query << "`skill_dist_tries` = " << player->skills[SKILL_DISTANCE].tries << ','; + query << "`skill_shielding` = " << player->skills[SKILL_SHIELD].level << ','; + query << "`skill_shielding_tries` = " << player->skills[SKILL_SHIELD].tries << ','; + query << "`skill_fishing` = " << player->skills[SKILL_FISHING].level << ','; + query << "`skill_fishing_tries` = " << player->skills[SKILL_FISHING].tries << ','; + query << "`direction` = " << static_cast (player->getDirection()) << ','; + + if (!player->isOffline()) { + query << "`onlinetime` = `onlinetime` + " << (time(nullptr) - player->lastLoginSaved) << ','; + } + query << "`blessings` = " << static_cast(player->blessings); + query << " WHERE `id` = " << player->getGUID(); + + DBTransaction transaction; + if (!transaction.begin()) { + return false; + } + + if (!db.executeQuery(query.str())) { + return false; + } + + // learned spells + query.str(std::string()); + query << "DELETE FROM `player_spells` WHERE `player_id` = " << player->getGUID(); + if (!db.executeQuery(query.str())) { + return false; + } + + query.str(std::string()); + + DBInsert spellsQuery("INSERT INTO `player_spells` (`player_id`, `name` ) VALUES "); + for (const std::string& spellName : player->learnedInstantSpellList) { + query << player->getGUID() << ',' << db.escapeString(spellName); + if (!spellsQuery.addRow(query)) { + return false; + } + } + + if (!spellsQuery.execute()) { + return false; + } + + //item saving + query << "DELETE FROM `player_items` WHERE `player_id` = " << player->getGUID(); + if (!db.executeQuery(query.str())) { + return false; + } + + DBInsert itemsQuery("INSERT INTO `player_items` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES "); + + ItemBlockList itemList; + for (int32_t slotId = CONST_SLOT_FIRST; slotId <= CONST_SLOT_LAST; ++slotId) { + Item* item = player->inventory[slotId]; + if (item) { + itemList.emplace_back(slotId, item); + } + } + + if (!saveItems(player, itemList, itemsQuery, propWriteStream)) { + return false; + } + + if (player->lastDepotId != -1) { + //save depot items + query.str(std::string()); + query << "DELETE FROM `player_depotitems` WHERE `player_id` = " << player->getGUID(); + + if (!db.executeQuery(query.str())) { + return false; + } + + DBInsert depotQuery("INSERT INTO `player_depotitems` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES "); + itemList.clear(); + + for (const auto& it : player->depotChests) { + DepotChest* depotChest = it.second; + for (Item* item : depotChest->getItemList()) { + itemList.emplace_back(it.first, item); + } + } + + if (!saveItems(player, itemList, depotQuery, propWriteStream)) { + return false; + } + } + + //save inbox items + query.str(std::string()); + query << "DELETE FROM `player_inboxitems` WHERE `player_id` = " << player->getGUID(); + if (!db.executeQuery(query.str())) { + return false; + } + + DBInsert inboxQuery("INSERT INTO `player_inboxitems` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES "); + itemList.clear(); + + for (Item* item : player->getInbox()->getItemList()) { + itemList.emplace_back(0, item); + } + + if (!saveItems(player, itemList, inboxQuery, propWriteStream)) { + return false; + } + + query.str(std::string()); + query << "DELETE FROM `player_storage` WHERE `player_id` = " << player->getGUID(); + if (!db.executeQuery(query.str())) { + return false; + } + + query.str(std::string()); + + DBInsert storageQuery("INSERT INTO `player_storage` (`player_id`, `key`, `value`) VALUES "); + player->genReservedStorageRange(); + + for (const auto& it : player->storageMap) { + query << player->getGUID() << ',' << it.first << ',' << it.second; + if (!storageQuery.addRow(query)) { + return false; + } + } + + if (!storageQuery.execute()) { + return false; + } + + //End the transaction + return transaction.commit(); +} + +std::string IOLoginData::getNameByGuid(uint32_t guid) +{ + std::ostringstream query; + query << "SELECT `name` FROM `players` WHERE `id` = " << guid; + DBResult_ptr result = Database::getInstance().storeQuery(query.str()); + if (!result) { + return std::string(); + } + return result->getString("name"); +} + +uint32_t IOLoginData::getGuidByName(const std::string& name) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `id` FROM `players` WHERE `name` = " << db.escapeString(name); + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return 0; + } + return result->getNumber("id"); +} + +bool IOLoginData::getGuidByNameEx(uint32_t& guid, bool& specialVip, std::string& name) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `name`, `id`, `group_id`, `account_id` FROM `players` WHERE `name` = " << db.escapeString(name); + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return false; + } + + name = result->getString("name"); + guid = result->getNumber("id"); + Group* group = g_game.groups.getGroup(result->getNumber("group_id")); + + uint64_t flags; + if (group) { + flags = group->flags; + } else { + flags = 0; + } + + specialVip = (flags & PlayerFlag_SpecialVIP) != 0; + return true; +} + +bool IOLoginData::formatPlayerName(std::string& name) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `name` FROM `players` WHERE `name` = " << db.escapeString(name); + + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return false; + } + + name = result->getString("name"); + return true; +} + +void IOLoginData::loadItems(ItemMap& itemMap, DBResult_ptr result) +{ + do { + uint32_t sid = result->getNumber("sid"); + uint32_t pid = result->getNumber("pid"); + uint16_t type = result->getNumber("itemtype"); + uint16_t count = result->getNumber("count"); + + unsigned long attrSize; + const char* attr = result->getStream("attributes", attrSize); + + PropStream propStream; + propStream.init(attr, attrSize); + + Item* item = Item::CreateItem(type, count); + if (item) { + if (!item->unserializeAttr(propStream)) { + std::cout << "WARNING: Serialize error in IOLoginData::loadItems" << std::endl; + } + + std::pair pair(item, pid); + itemMap[sid] = pair; + } + } while (result->next()); +} + +void IOLoginData::increaseBankBalance(uint32_t guid, uint64_t bankBalance) +{ + std::ostringstream query; + query << "UPDATE `players` SET `balance` = `balance` + " << bankBalance << " WHERE `id` = " << guid; + Database::getInstance().executeQuery(query.str()); +} + +bool IOLoginData::hasBiddedOnHouse(uint32_t guid) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `id` FROM `houses` WHERE `highest_bidder` = " << guid << " LIMIT 1"; + return db.storeQuery(query.str()).get() != nullptr; +} + +std::forward_list IOLoginData::getVIPEntries(uint32_t accountId) +{ + std::forward_list entries; + + std::ostringstream query; + query << "SELECT `player_id`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `name`, `description`, `icon`, `notify` FROM `account_viplist` WHERE `account_id` = " << accountId; + + DBResult_ptr result = Database::getInstance().storeQuery(query.str()); + if (result) { + do { + entries.emplace_front( + result->getNumber("player_id"), + result->getString("name"), + result->getString("description"), + result->getNumber("icon"), + result->getNumber("notify") != 0 + ); + } while (result->next()); + } + return entries; +} + +void IOLoginData::addVIPEntry(uint32_t accountId, uint32_t guid, const std::string& description, uint32_t icon, bool notify) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "INSERT INTO `account_viplist` (`account_id`, `player_id`, `description`, `icon`, `notify`) VALUES (" << accountId << ',' << guid << ',' << db.escapeString(description) << ',' << icon << ',' << notify << ')'; + db.executeQuery(query.str()); +} + +void IOLoginData::editVIPEntry(uint32_t accountId, uint32_t guid, const std::string& description, uint32_t icon, bool notify) +{ + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "UPDATE `account_viplist` SET `description` = " << db.escapeString(description) << ", `icon` = " << icon << ", `notify` = " << notify << " WHERE `account_id` = " << accountId << " AND `player_id` = " << guid; + db.executeQuery(query.str()); +} + +void IOLoginData::removeVIPEntry(uint32_t accountId, uint32_t guid) +{ + std::ostringstream query; + query << "DELETE FROM `account_viplist` WHERE `account_id` = " << accountId << " AND `player_id` = " << guid; + Database::getInstance().executeQuery(query.str()); +} + +void IOLoginData::addPremiumDays(uint32_t accountId, int32_t addDays) +{ + std::ostringstream query; + query << "UPDATE `accounts` SET `premdays` = `premdays` + " << addDays << " WHERE `id` = " << accountId; + Database::getInstance().executeQuery(query.str()); +} + +void IOLoginData::removePremiumDays(uint32_t accountId, int32_t removeDays) +{ + std::ostringstream query; + query << "UPDATE `accounts` SET `premdays` = `premdays` - " << removeDays << " WHERE `id` = " << accountId; + Database::getInstance().executeQuery(query.str()); +} diff --git a/src/iologindata.h b/src/iologindata.h new file mode 100644 index 0000000..5a148ca --- /dev/null +++ b/src/iologindata.h @@ -0,0 +1,70 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_IOLOGINDATA_H_28B0440BEC594654AC0F4E1A5E42B2EF +#define FS_IOLOGINDATA_H_28B0440BEC594654AC0F4E1A5E42B2EF + +#include "account.h" +#include "player.h" +#include "database.h" + +using ItemBlockList = std::list>; + +class IOLoginData +{ + public: + static Account loadAccount(uint32_t accno); + static bool saveAccount(const Account& acc); + + static bool loginserverAuthentication(const std::string& name, const std::string& password, Account& account); + static uint32_t gameworldAuthentication(const std::string& accountName, const std::string& password, std::string& characterName, std::string& token, uint32_t tokenTime); + static uint32_t getAccountIdByPlayerName(const std::string& playerName); + + static AccountType_t getAccountType(uint32_t accountId); + static void setAccountType(uint32_t accountId, AccountType_t accountType); + static void updateOnlineStatus(uint32_t guid, bool login); + static bool preloadPlayer(Player* player, const std::string& name); + + static bool loadPlayerById(Player* player, uint32_t id); + static bool loadPlayerByName(Player* player, const std::string& name); + static bool loadPlayer(Player* player, DBResult_ptr result); + static bool savePlayer(Player* player); + static uint32_t getGuidByName(const std::string& name); + static bool getGuidByNameEx(uint32_t& guid, bool& specialVip, std::string& name); + static std::string getNameByGuid(uint32_t guid); + static bool formatPlayerName(std::string& name); + static void increaseBankBalance(uint32_t guid, uint64_t bankBalance); + static bool hasBiddedOnHouse(uint32_t guid); + + static std::forward_list getVIPEntries(uint32_t accountId); + static void addVIPEntry(uint32_t accountId, uint32_t guid, const std::string& description, uint32_t icon, bool notify); + static void editVIPEntry(uint32_t accountId, uint32_t guid, const std::string& description, uint32_t icon, bool notify); + static void removeVIPEntry(uint32_t accountId, uint32_t guid); + + static void addPremiumDays(uint32_t accountId, int32_t addDays); + static void removePremiumDays(uint32_t accountId, int32_t removeDays); + + private: + using ItemMap = std::map>; + + static void loadItems(ItemMap& itemMap, DBResult_ptr result); + static bool saveItems(const Player* player, const ItemBlockList& itemList, DBInsert& query_insert, PropWriteStream& propWriteStream); +}; + +#endif diff --git a/src/iomap.cpp b/src/iomap.cpp new file mode 100644 index 0000000..9c40761 --- /dev/null +++ b/src/iomap.cpp @@ -0,0 +1,482 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "iomap.h" + +#include "bed.h" + +/* + OTBM_ROOTV1 + | + |--- OTBM_MAP_DATA + | | + | |--- OTBM_TILE_AREA + | | |--- OTBM_TILE + | | |--- OTBM_TILE_SQUARE (not implemented) + | | |--- OTBM_TILE_REF (not implemented) + | | |--- OTBM_HOUSETILE + | | + | |--- OTBM_SPAWNS (not implemented) + | | |--- OTBM_SPAWN_AREA (not implemented) + | | |--- OTBM_MONSTER (not implemented) + | | + | |--- OTBM_TOWNS + | | |--- OTBM_TOWN + | | + | |--- OTBM_WAYPOINTS + | |--- OTBM_WAYPOINT + | + |--- OTBM_ITEM_DEF (not implemented) +*/ + +Tile* IOMap::createTile(Item*& ground, Item* item, uint16_t x, uint16_t y, uint8_t z) +{ + if (!ground) { + return new StaticTile(x, y, z); + } + + Tile* tile; + if ((item && item->isBlocking()) || ground->isBlocking()) { + tile = new StaticTile(x, y, z); + } else { + tile = new DynamicTile(x, y, z); + } + + tile->internalAddThing(ground); + ground->startDecaying(); + ground = nullptr; + return tile; +} + +bool IOMap::loadMap(Map* map, const std::string& fileName) +{ + int64_t start = OTSYS_TIME(); + OTB::Loader loader{fileName, OTB::Identifier{{'O', 'T', 'B', 'M'}}}; + auto& root = loader.parseTree(); + + PropStream propStream; + if (!loader.getProps(root, propStream)) { + setLastErrorString("Could not read root property."); + return false; + } + + OTBM_root_header root_header; + if (!propStream.read(root_header)) { + setLastErrorString("Could not read header."); + return false; + } + + uint32_t headerVersion = root_header.version; + if (headerVersion == 0) { + //In otbm version 1 the count variable after splashes/fluidcontainers and stackables + //are saved as attributes instead, this solves alot of problems with items + //that is changed (stackable/charges/fluidcontainer/splash) during an update. + setLastErrorString("This map need to be upgraded by using the latest map editor version to be able to load correctly."); + return false; + } + + if (headerVersion > 2) { + setLastErrorString("Unknown OTBM version detected."); + return false; + } + + if (root_header.majorVersionItems < 3) { + setLastErrorString("This map need to be upgraded by using the latest map editor version to be able to load correctly."); + return false; + } + + if (root_header.majorVersionItems > Item::items.majorVersion) { + setLastErrorString("The map was saved with a different items.otb version, an upgraded items.otb is required."); + return false; + } + + if (root_header.minorVersionItems < CLIENT_VERSION_810) { + setLastErrorString("This map needs to be updated."); + return false; + } + + if (root_header.minorVersionItems > Item::items.minorVersion) { + std::cout << "[Warning - IOMap::loadMap] This map needs an updated items.otb." << std::endl; + } + + std::cout << "> Map size: " << root_header.width << "x" << root_header.height << '.' << std::endl; + map->width = root_header.width; + map->height = root_header.height; + + if (root.children.size() != 1 || root.children[0].type != OTBM_MAP_DATA) { + setLastErrorString("Could not read data node."); + return false; + } + + auto& mapNode = root.children[0]; + if (!parseMapDataAttributes(loader, mapNode, *map, fileName)) { + return false; + } + + for (auto& mapDataNode : mapNode.children) { + if (mapDataNode.type == OTBM_TILE_AREA) { + if (!parseTileArea(loader, mapDataNode, *map)) { + return false; + } + } else if (mapDataNode.type == OTBM_TOWNS) { + if (!parseTowns(loader, mapDataNode, *map)) { + return false; + } + } else if (mapDataNode.type == OTBM_WAYPOINTS && headerVersion > 1) { + if (!parseWaypoints(loader, mapDataNode, *map)) { + return false; + } + } else { + setLastErrorString("Unknown map node."); + return false; + } + } + + std::cout << "> Map loading time: " << (OTSYS_TIME() - start) / (1000.) << " seconds." << std::endl; + return true; +} + +bool IOMap::parseMapDataAttributes(OTB::Loader& loader, const OTB::Node& mapNode, Map& map, const std::string& fileName) +{ + PropStream propStream; + if (!loader.getProps(mapNode, propStream)) { + setLastErrorString("Could not read map data attributes."); + return false; + } + + std::string mapDescription; + std::string tmp; + + uint8_t attribute; + while (propStream.read(attribute)) { + switch (attribute) { + case OTBM_ATTR_DESCRIPTION: + if (!propStream.readString(mapDescription)) { + setLastErrorString("Invalid description tag."); + return false; + } + break; + + case OTBM_ATTR_EXT_SPAWN_FILE: + if (!propStream.readString(tmp)) { + setLastErrorString("Invalid spawn tag."); + return false; + } + + map.spawnfile = fileName.substr(0, fileName.rfind('/') + 1); + map.spawnfile += tmp; + break; + + case OTBM_ATTR_EXT_HOUSE_FILE: + if (!propStream.readString(tmp)) { + setLastErrorString("Invalid house tag."); + return false; + } + + map.housefile = fileName.substr(0, fileName.rfind('/') + 1); + map.housefile += tmp; + break; + + default: + setLastErrorString("Unknown header node."); + return false; + } + } + return true; +} + +bool IOMap::parseTileArea(OTB::Loader& loader, const OTB::Node& tileAreaNode, Map& map) +{ + PropStream propStream; + if (!loader.getProps(tileAreaNode, propStream)) { + setLastErrorString("Invalid map node."); + return false; + } + + OTBM_Destination_coords area_coord; + if (!propStream.read(area_coord)) { + setLastErrorString("Invalid map node."); + return false; + } + + uint16_t base_x = area_coord.x; + uint16_t base_y = area_coord.y; + uint16_t z = area_coord.z; + + for (auto& tileNode : tileAreaNode.children) { + if (tileNode.type != OTBM_TILE && tileNode.type != OTBM_HOUSETILE) { + setLastErrorString("Unknown tile node."); + return false; + } + + if (!loader.getProps(tileNode, propStream)) { + setLastErrorString("Could not read node data."); + return false; + } + + OTBM_Tile_coords tile_coord; + if (!propStream.read(tile_coord)) { + setLastErrorString("Could not read tile position."); + return false; + } + + uint16_t x = base_x + tile_coord.x; + uint16_t y = base_y + tile_coord.y; + + bool isHouseTile = false; + House* house = nullptr; + Tile* tile = nullptr; + Item* ground_item = nullptr; + uint32_t tileflags = TILESTATE_NONE; + + if (tileNode.type == OTBM_HOUSETILE) { + uint32_t houseId; + if (!propStream.read(houseId)) { + std::ostringstream ss; + ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Could not read house id."; + setLastErrorString(ss.str()); + return false; + } + + house = map.houses.addHouse(houseId); + if (!house) { + std::ostringstream ss; + ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Could not create house id: " << houseId; + setLastErrorString(ss.str()); + return false; + } + + tile = new HouseTile(x, y, z, house); + house->addTile(static_cast(tile)); + isHouseTile = true; + } + + uint8_t attribute; + //read tile attributes + while (propStream.read(attribute)) { + switch (attribute) { + case OTBM_ATTR_TILE_FLAGS: { + uint32_t flags; + if (!propStream.read(flags)) { + std::ostringstream ss; + ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Failed to read tile flags."; + setLastErrorString(ss.str()); + return false; + } + + if ((flags & OTBM_TILEFLAG_PROTECTIONZONE) != 0) { + tileflags |= TILESTATE_PROTECTIONZONE; + } else if ((flags & OTBM_TILEFLAG_NOPVPZONE) != 0) { + tileflags |= TILESTATE_NOPVPZONE; + } else if ((flags & OTBM_TILEFLAG_PVPZONE) != 0) { + tileflags |= TILESTATE_PVPZONE; + } + + if ((flags & OTBM_TILEFLAG_NOLOGOUT) != 0) { + tileflags |= TILESTATE_NOLOGOUT; + } + break; + } + + case OTBM_ATTR_ITEM: { + Item* item = Item::CreateItem(propStream); + if (!item) { + std::ostringstream ss; + ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Failed to create item."; + setLastErrorString(ss.str()); + return false; + } + + if (isHouseTile && item->isMoveable()) { + std::cout << "[Warning - IOMap::loadMap] Moveable item with ID: " << item->getID() << ", in house: " << house->getId() << ", at position [x: " << x << ", y: " << y << ", z: " << z << "]." << std::endl; + delete item; + } else { + if (item->getItemCount() == 0) { + item->setItemCount(1); + } + + if (tile) { + tile->internalAddThing(item); + item->startDecaying(); + item->setLoadedFromMap(true); + } else if (item->isGroundTile()) { + delete ground_item; + ground_item = item; + } else { + tile = createTile(ground_item, item, x, y, z); + tile->internalAddThing(item); + item->startDecaying(); + item->setLoadedFromMap(true); + } + } + break; + } + + default: + std::ostringstream ss; + ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Unknown tile attribute."; + setLastErrorString(ss.str()); + return false; + } + } + + for (auto& itemNode : tileNode.children) { + if (itemNode.type != OTBM_ITEM) { + std::ostringstream ss; + ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Unknown node type."; + setLastErrorString(ss.str()); + return false; + } + + PropStream stream; + if (!loader.getProps(itemNode, stream)) { + setLastErrorString("Invalid item node."); + return false; + } + + Item* item = Item::CreateItem(stream); + if (!item) { + std::ostringstream ss; + ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Failed to create item."; + setLastErrorString(ss.str()); + return false; + } + + if (!item->unserializeItemNode(loader, itemNode, stream)) { + std::ostringstream ss; + ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Failed to load item " << item->getID() << '.'; + setLastErrorString(ss.str()); + delete item; + return false; + } + + if (isHouseTile && item->isMoveable()) { + std::cout << "[Warning - IOMap::loadMap] Moveable item with ID: " << item->getID() << ", in house: " << house->getId() << ", at position [x: " << x << ", y: " << y << ", z: " << z << "]." << std::endl; + delete item; + } else { + if (item->getItemCount() == 0) { + item->setItemCount(1); + } + + if (tile) { + tile->internalAddThing(item); + item->startDecaying(); + item->setLoadedFromMap(true); + } else if (item->isGroundTile()) { + delete ground_item; + ground_item = item; + } else { + tile = createTile(ground_item, item, x, y, z); + tile->internalAddThing(item); + item->startDecaying(); + item->setLoadedFromMap(true); + } + } + } + + if (!tile) { + tile = createTile(ground_item, nullptr, x, y, z); + } + + tile->setFlag(static_cast(tileflags)); + + map.setTile(x, y, z, tile); + } + return true; +} + +bool IOMap::parseTowns(OTB::Loader& loader, const OTB::Node& townsNode, Map& map) +{ + for (auto& townNode : townsNode.children) { + PropStream propStream; + if (townNode.type != OTBM_TOWN) { + setLastErrorString("Unknown town node."); + return false; + } + + if (!loader.getProps(townNode, propStream)) { + setLastErrorString("Could not read town data."); + return false; + } + + uint32_t townId; + if (!propStream.read(townId)) { + setLastErrorString("Could not read town id."); + return false; + } + + Town* town = map.towns.getTown(townId); + if (!town) { + town = new Town(townId); + map.towns.addTown(townId, town); + } + + std::string townName; + if (!propStream.readString(townName)) { + setLastErrorString("Could not read town name."); + return false; + } + + town->setName(townName); + + OTBM_Destination_coords town_coords; + if (!propStream.read(town_coords)) { + setLastErrorString("Could not read town coordinates."); + return false; + } + + town->setTemplePos(Position(town_coords.x, town_coords.y, town_coords.z)); + } + return true; +} + + +bool IOMap::parseWaypoints(OTB::Loader& loader, const OTB::Node& waypointsNode, Map& map) +{ + PropStream propStream; + for (auto& node : waypointsNode.children) { + if (node.type != OTBM_WAYPOINT) { + setLastErrorString("Unknown waypoint node."); + return false; + } + + if (!loader.getProps(node, propStream)) { + setLastErrorString("Could not read waypoint data."); + return false; + } + + std::string name; + if (!propStream.readString(name)) { + setLastErrorString("Could not read waypoint name."); + return false; + } + + OTBM_Destination_coords waypoint_coords; + if (!propStream.read(waypoint_coords)) { + setLastErrorString("Could not read waypoint coordinates."); + return false; + } + + map.waypoints[name] = Position(waypoint_coords.x, waypoint_coords.y, waypoint_coords.z); + } + return true; +} + diff --git a/src/iomap.h b/src/iomap.h new file mode 100644 index 0000000..a00d8a4 --- /dev/null +++ b/src/iomap.h @@ -0,0 +1,158 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_IOMAP_H_8085D4B1037A44288494A52FDBB775E4 +#define FS_IOMAP_H_8085D4B1037A44288494A52FDBB775E4 + +#include "item.h" +#include "map.h" +#include "house.h" +#include "spawn.h" +#include "configmanager.h" + +extern ConfigManager g_config; + +enum OTBM_AttrTypes_t { + OTBM_ATTR_DESCRIPTION = 1, + OTBM_ATTR_EXT_FILE = 2, + OTBM_ATTR_TILE_FLAGS = 3, + OTBM_ATTR_ACTION_ID = 4, + OTBM_ATTR_UNIQUE_ID = 5, + OTBM_ATTR_TEXT = 6, + OTBM_ATTR_DESC = 7, + OTBM_ATTR_TELE_DEST = 8, + OTBM_ATTR_ITEM = 9, + OTBM_ATTR_DEPOT_ID = 10, + OTBM_ATTR_EXT_SPAWN_FILE = 11, + OTBM_ATTR_RUNE_CHARGES = 12, + OTBM_ATTR_EXT_HOUSE_FILE = 13, + OTBM_ATTR_HOUSEDOORID = 14, + OTBM_ATTR_COUNT = 15, + OTBM_ATTR_DURATION = 16, + OTBM_ATTR_DECAYING_STATE = 17, + OTBM_ATTR_WRITTENDATE = 18, + OTBM_ATTR_WRITTENBY = 19, + OTBM_ATTR_SLEEPERGUID = 20, + OTBM_ATTR_SLEEPSTART = 21, + OTBM_ATTR_CHARGES = 22, +}; + +enum OTBM_NodeTypes_t { + OTBM_ROOTV1 = 1, + OTBM_MAP_DATA = 2, + OTBM_ITEM_DEF = 3, + OTBM_TILE_AREA = 4, + OTBM_TILE = 5, + OTBM_ITEM = 6, + OTBM_TILE_SQUARE = 7, + OTBM_TILE_REF = 8, + OTBM_SPAWNS = 9, + OTBM_SPAWN_AREA = 10, + OTBM_MONSTER = 11, + OTBM_TOWNS = 12, + OTBM_TOWN = 13, + OTBM_HOUSETILE = 14, + OTBM_WAYPOINTS = 15, + OTBM_WAYPOINT = 16, +}; + +enum OTBM_TileFlag_t : uint32_t { + OTBM_TILEFLAG_PROTECTIONZONE = 1 << 0, + OTBM_TILEFLAG_NOPVPZONE = 1 << 2, + OTBM_TILEFLAG_NOLOGOUT = 1 << 3, + OTBM_TILEFLAG_PVPZONE = 1 << 4 +}; + +#pragma pack(1) + +struct OTBM_root_header { + uint32_t version; + uint16_t width; + uint16_t height; + uint32_t majorVersionItems; + uint32_t minorVersionItems; +}; + +struct OTBM_Destination_coords { + uint16_t x; + uint16_t y; + uint8_t z; +}; + +struct OTBM_Tile_coords { + uint8_t x; + uint8_t y; +}; + +#pragma pack() + +class IOMap +{ + static Tile* createTile(Item*& ground, Item* item, uint16_t x, uint16_t y, uint8_t z); + + public: + bool loadMap(Map* map, const std::string& fileName); + + /* Load the spawns + * \param map pointer to the Map class + * \returns Returns true if the spawns were loaded successfully + */ + static bool loadSpawns(Map* map) { + if (map->spawnfile.empty()) { + //OTBM file doesn't tell us about the spawnfile, + //lets guess it is mapname-spawn.xml. + map->spawnfile = g_config.getString(ConfigManager::MAP_NAME); + map->spawnfile += "-spawn.xml"; + } + + return map->spawns.loadFromXml(map->spawnfile); + } + + /* Load the houses (not house tile-data) + * \param map pointer to the Map class + * \returns Returns true if the houses were loaded successfully + */ + static bool loadHouses(Map* map) { + if (map->housefile.empty()) { + //OTBM file doesn't tell us about the housefile, + //lets guess it is mapname-house.xml. + map->housefile = g_config.getString(ConfigManager::MAP_NAME); + map->housefile += "-house.xml"; + } + + return map->houses.loadHousesXML(map->housefile); + } + + const std::string& getLastErrorString() const { + return errorString; + } + + void setLastErrorString(std::string error) { + errorString = error; + } + + private: + bool parseMapDataAttributes(OTB::Loader& loader, const OTB::Node& mapNode, Map& map, const std::string& fileName); + bool parseWaypoints(OTB::Loader& loader, const OTB::Node& waypointsNode, Map& map); + bool parseTowns(OTB::Loader& loader, const OTB::Node& townsNode, Map& map); + bool parseTileArea(OTB::Loader& loader, const OTB::Node& tileAreaNode, Map& map); + std::string errorString; +}; + +#endif diff --git a/src/iomapserialize.cpp b/src/iomapserialize.cpp new file mode 100644 index 0000000..af606c2 --- /dev/null +++ b/src/iomapserialize.cpp @@ -0,0 +1,372 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "iomapserialize.h" +#include "game.h" +#include "bed.h" + +extern Game g_game; + +void IOMapSerialize::loadHouseItems(Map* map) +{ + int64_t start = OTSYS_TIME(); + + DBResult_ptr result = Database::getInstance().storeQuery("SELECT `data` FROM `tile_store`"); + if (!result) { + return; + } + + do { + unsigned long attrSize; + const char* attr = result->getStream("data", attrSize); + + PropStream propStream; + propStream.init(attr, attrSize); + + uint16_t x, y; + uint8_t z; + if (!propStream.read(x) || !propStream.read(y) || !propStream.read(z)) { + continue; + } + + Tile* tile = map->getTile(x, y, z); + if (!tile) { + continue; + } + + uint32_t item_count; + if (!propStream.read(item_count)) { + continue; + } + + while (item_count--) { + loadItem(propStream, tile); + } + } while (result->next()); + std::cout << "> Loaded house items in: " << (OTSYS_TIME() - start) / (1000.) << " s" << std::endl; +} + +bool IOMapSerialize::saveHouseItems() +{ + int64_t start = OTSYS_TIME(); + Database& db = Database::getInstance(); + std::ostringstream query; + + //Start the transaction + DBTransaction transaction; + if (!transaction.begin()) { + return false; + } + + //clear old tile data + if (!db.executeQuery("DELETE FROM `tile_store`")) { + return false; + } + + DBInsert stmt("INSERT INTO `tile_store` (`house_id`, `data`) VALUES "); + + PropWriteStream stream; + for (const auto& it : g_game.map.houses.getHouses()) { + //save house items + House* house = it.second; + for (HouseTile* tile : house->getTiles()) { + saveTile(stream, tile); + + size_t attributesSize; + const char* attributes = stream.getStream(attributesSize); + if (attributesSize > 0) { + query << house->getId() << ',' << db.escapeBlob(attributes, attributesSize); + if (!stmt.addRow(query)) { + return false; + } + stream.clear(); + } + } + } + + if (!stmt.execute()) { + return false; + } + + //End the transaction + bool success = transaction.commit(); + std::cout << "> Saved house items in: " << + (OTSYS_TIME() - start) / (1000.) << " s" << std::endl; + return success; +} + +bool IOMapSerialize::loadContainer(PropStream& propStream, Container* container) +{ + while (container->serializationCount > 0) { + if (!loadItem(propStream, container)) { + std::cout << "[Warning - IOMapSerialize::loadContainer] Unserialization error for container item: " << container->getID() << std::endl; + return false; + } + container->serializationCount--; + } + + uint8_t endAttr; + if (!propStream.read(endAttr) || endAttr != 0) { + std::cout << "[Warning - IOMapSerialize::loadContainer] Unserialization error for container item: " << container->getID() << std::endl; + return false; + } + return true; +} + +bool IOMapSerialize::loadItem(PropStream& propStream, Cylinder* parent) +{ + uint16_t id; + if (!propStream.read(id)) { + return false; + } + + Tile* tile = nullptr; + if (parent->getParent() == nullptr) { + tile = parent->getTile(); + } + + const ItemType& iType = Item::items[id]; + if (iType.moveable || iType.forceSerialize || !tile) { + //create a new item + Item* item = Item::CreateItem(id); + if (item) { + if (item->unserializeAttr(propStream)) { + Container* container = item->getContainer(); + if (container && !loadContainer(propStream, container)) { + delete item; + return false; + } + + parent->internalAddThing(item); + item->startDecaying(); + } else { + std::cout << "WARNING: Unserialization error in IOMapSerialize::loadItem()" << id << std::endl; + delete item; + return false; + } + } + } else { + // Stationary items like doors/beds/blackboards/bookcases + Item* item = nullptr; + if (const TileItemVector* items = tile->getItemList()) { + for (Item* findItem : *items) { + if (findItem->getID() == id) { + item = findItem; + break; + } else if (iType.isDoor() && findItem->getDoor()) { + item = findItem; + break; + } else if (iType.isBed() && findItem->getBed()) { + item = findItem; + break; + } + } + } + + if (item) { + if (item->unserializeAttr(propStream)) { + Container* container = item->getContainer(); + if (container && !loadContainer(propStream, container)) { + return false; + } + + g_game.transformItem(item, id); + } else { + std::cout << "WARNING: Unserialization error in IOMapSerialize::loadItem()" << id << std::endl; + } + } else { + //The map changed since the last save, just read the attributes + std::unique_ptr dummy(Item::CreateItem(id)); + if (dummy) { + dummy->unserializeAttr(propStream); + Container* container = dummy->getContainer(); + if (container) { + if (!loadContainer(propStream, container)) { + return false; + } + } else if (BedItem* bedItem = dynamic_cast(dummy.get())) { + uint32_t sleeperGUID = bedItem->getSleeper(); + if (sleeperGUID != 0) { + g_game.removeBedSleeper(sleeperGUID); + } + } + } + } + } + return true; +} + +void IOMapSerialize::saveItem(PropWriteStream& stream, const Item* item) +{ + const Container* container = item->getContainer(); + + // Write ID & props + stream.write(item->getID()); + item->serializeAttr(stream); + + if (container) { + // Hack our way into the attributes + stream.write(ATTR_CONTAINER_ITEMS); + stream.write(container->size()); + for (auto it = container->getReversedItems(), end = container->getReversedEnd(); it != end; ++it) { + saveItem(stream, *it); + } + } + + stream.write(0x00); // attr end +} + +void IOMapSerialize::saveTile(PropWriteStream& stream, const Tile* tile) +{ + const TileItemVector* tileItems = tile->getItemList(); + if (!tileItems) { + return; + } + + std::forward_list items; + uint16_t count = 0; + for (Item* item : *tileItems) { + const ItemType& it = Item::items[item->getID()]; + + // Note that these are NEGATED, ie. these are the items that will be saved. + if (!(it.moveable || it.forceSerialize || item->getDoor() || (item->getContainer() && !item->getContainer()->empty()) || it.canWriteText || item->getBed())) { + continue; + } + + items.push_front(item); + ++count; + } + + if (!items.empty()) { + const Position& tilePosition = tile->getPosition(); + stream.write(tilePosition.x); + stream.write(tilePosition.y); + stream.write(tilePosition.z); + + stream.write(count); + for (const Item* item : items) { + saveItem(stream, item); + } + } +} + +bool IOMapSerialize::loadHouseInfo() +{ + Database& db = Database::getInstance(); + + DBResult_ptr result = db.storeQuery("SELECT `id`, `owner`, `paid`, `warnings` FROM `houses`"); + if (!result) { + return false; + } + + do { + House* house = g_game.map.houses.getHouse(result->getNumber("id")); + if (house) { + house->setOwner(result->getNumber("owner"), false); + house->setPaidUntil(result->getNumber("paid")); + house->setPayRentWarnings(result->getNumber("warnings")); + } + } while (result->next()); + + result = db.storeQuery("SELECT `house_id`, `listid`, `list` FROM `house_lists`"); + if (result) { + do { + House* house = g_game.map.houses.getHouse(result->getNumber("house_id")); + if (house) { + house->setAccessList(result->getNumber("listid"), result->getString("list")); + } + } while (result->next()); + } + return true; +} + +bool IOMapSerialize::saveHouseInfo() +{ + Database& db = Database::getInstance(); + + DBTransaction transaction; + if (!transaction.begin()) { + return false; + } + + if (!db.executeQuery("DELETE FROM `house_lists`")) { + return false; + } + + std::ostringstream query; + for (const auto& it : g_game.map.houses.getHouses()) { + House* house = it.second; + query << "SELECT `id` FROM `houses` WHERE `id` = " << house->getId(); + DBResult_ptr result = db.storeQuery(query.str()); + if (result) { + query.str(std::string()); + query << "UPDATE `houses` SET `owner` = " << house->getOwner() << ", `paid` = " << house->getPaidUntil() << ", `warnings` = " << house->getPayRentWarnings() << ", `name` = " << db.escapeString(house->getName()) << ", `town_id` = " << house->getTownId() << ", `rent` = " << house->getRent() << ", `size` = " << house->getTiles().size() << ", `beds` = " << house->getBedCount() << " WHERE `id` = " << house->getId(); + } else { + query.str(std::string()); + query << "INSERT INTO `houses` (`id`, `owner`, `paid`, `warnings`, `name`, `town_id`, `rent`, `size`, `beds`) VALUES (" << house->getId() << ',' << house->getOwner() << ',' << house->getPaidUntil() << ',' << house->getPayRentWarnings() << ',' << db.escapeString(house->getName()) << ',' << house->getTownId() << ',' << house->getRent() << ',' << house->getTiles().size() << ',' << house->getBedCount() << ')'; + } + + db.executeQuery(query.str()); + query.str(std::string()); + } + + DBInsert stmt("INSERT INTO `house_lists` (`house_id` , `listid` , `list`) VALUES "); + + for (const auto& it : g_game.map.houses.getHouses()) { + House* house = it.second; + + std::string listText; + if (house->getAccessList(GUEST_LIST, listText) && !listText.empty()) { + query << house->getId() << ',' << GUEST_LIST << ',' << db.escapeString(listText); + if (!stmt.addRow(query)) { + return false; + } + + listText.clear(); + } + + if (house->getAccessList(SUBOWNER_LIST, listText) && !listText.empty()) { + query << house->getId() << ',' << SUBOWNER_LIST << ',' << db.escapeString(listText); + if (!stmt.addRow(query)) { + return false; + } + + listText.clear(); + } + + for (Door* door : house->getDoors()) { + if (door->getAccessList(listText) && !listText.empty()) { + query << house->getId() << ',' << door->getDoorId() << ',' << db.escapeString(listText); + if (!stmt.addRow(query)) { + return false; + } + + listText.clear(); + } + } + } + + if (!stmt.execute()) { + return false; + } + + return transaction.commit(); +} diff --git a/src/iomapserialize.h b/src/iomapserialize.h new file mode 100644 index 0000000..9840f24 --- /dev/null +++ b/src/iomapserialize.h @@ -0,0 +1,42 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_IOMAPSERIALIZE_H_7E903658F34E44F9BE03A713B55A3D6D +#define FS_IOMAPSERIALIZE_H_7E903658F34E44F9BE03A713B55A3D6D + +#include "database.h" +#include "map.h" + +class IOMapSerialize +{ + public: + static void loadHouseItems(Map* map); + static bool saveHouseItems(); + static bool loadHouseInfo(); + static bool saveHouseInfo(); + + private: + static void saveItem(PropWriteStream& stream, const Item* item); + static void saveTile(PropWriteStream& stream, const Tile* tile); + + static bool loadContainer(PropStream& propStream, Container* container); + static bool loadItem(PropStream& propStream, Cylinder* parent); +}; + +#endif diff --git a/src/iomarket.cpp b/src/iomarket.cpp new file mode 100644 index 0000000..9829663 --- /dev/null +++ b/src/iomarket.cpp @@ -0,0 +1,348 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "iomarket.h" + +#include "configmanager.h" +#include "databasetasks.h" +#include "iologindata.h" +#include "game.h" +#include "scheduler.h" + +extern ConfigManager g_config; +extern Game g_game; + +MarketOfferList IOMarket::getActiveOffers(MarketAction_t action, uint16_t itemId) +{ + MarketOfferList offerList; + + std::ostringstream query; + query << "SELECT `id`, `amount`, `price`, `created`, `anonymous`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `player_name` FROM `market_offers` WHERE `sale` = " << action << " AND `itemtype` = " << itemId; + + DBResult_ptr result = Database::getInstance().storeQuery(query.str()); + if (!result) { + return offerList; + } + + const int32_t marketOfferDuration = g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION); + + do { + MarketOffer offer; + offer.amount = result->getNumber("amount"); + offer.price = result->getNumber("price"); + offer.timestamp = result->getNumber("created") + marketOfferDuration; + offer.counter = result->getNumber("id") & 0xFFFF; + if (result->getNumber("anonymous") == 0) { + offer.playerName = result->getString("player_name"); + } else { + offer.playerName = "Anonymous"; + } + offerList.push_back(offer); + } while (result->next()); + return offerList; +} + +MarketOfferList IOMarket::getOwnOffers(MarketAction_t action, uint32_t playerId) +{ + MarketOfferList offerList; + + const int32_t marketOfferDuration = g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION); + + std::ostringstream query; + query << "SELECT `id`, `amount`, `price`, `created`, `itemtype` FROM `market_offers` WHERE `player_id` = " << playerId << " AND `sale` = " << action; + + DBResult_ptr result = Database::getInstance().storeQuery(query.str()); + if (!result) { + return offerList; + } + + do { + MarketOffer offer; + offer.amount = result->getNumber("amount"); + offer.price = result->getNumber("price"); + offer.timestamp = result->getNumber("created") + marketOfferDuration; + offer.counter = result->getNumber("id") & 0xFFFF; + offer.itemId = result->getNumber("itemtype"); + offerList.push_back(offer); + } while (result->next()); + return offerList; +} + +HistoryMarketOfferList IOMarket::getOwnHistory(MarketAction_t action, uint32_t playerId) +{ + HistoryMarketOfferList offerList; + + std::ostringstream query; + query << "SELECT `itemtype`, `amount`, `price`, `expires_at`, `state` FROM `market_history` WHERE `player_id` = " << playerId << " AND `sale` = " << action; + + DBResult_ptr result = Database::getInstance().storeQuery(query.str()); + if (!result) { + return offerList; + } + + do { + HistoryMarketOffer offer; + offer.itemId = result->getNumber("itemtype"); + offer.amount = result->getNumber("amount"); + offer.price = result->getNumber("price"); + offer.timestamp = result->getNumber("expires_at"); + + MarketOfferState_t offerState = static_cast(result->getNumber("state")); + if (offerState == OFFERSTATE_ACCEPTEDEX) { + offerState = OFFERSTATE_ACCEPTED; + } + + offer.state = offerState; + + offerList.push_back(offer); + } while (result->next()); + return offerList; +} + +void IOMarket::processExpiredOffers(DBResult_ptr result, bool) +{ + if (!result) { + return; + } + + do { + if (!IOMarket::moveOfferToHistory(result->getNumber("id"), OFFERSTATE_EXPIRED)) { + continue; + } + + const uint32_t playerId = result->getNumber("player_id"); + const uint16_t amount = result->getNumber("amount"); + if (result->getNumber("sale") == 1) { + const ItemType& itemType = Item::items[result->getNumber("itemtype")]; + if (itemType.id == 0) { + continue; + } + + Player* player = g_game.getPlayerByGUID(playerId); + if (!player) { + player = new Player(nullptr); + if (!IOLoginData::loadPlayerById(player, playerId)) { + delete player; + continue; + } + } + + if (itemType.stackable) { + uint16_t tmpAmount = amount; + while (tmpAmount > 0) { + uint16_t stackCount = std::min(100, tmpAmount); + Item* item = Item::CreateItem(itemType.id, stackCount); + if (g_game.internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { + delete item; + break; + } + + tmpAmount -= stackCount; + } + } else { + int32_t subType; + if (itemType.charges != 0) { + subType = itemType.charges; + } else { + subType = -1; + } + + for (uint16_t i = 0; i < amount; ++i) { + Item* item = Item::CreateItem(itemType.id, subType); + if (g_game.internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { + delete item; + break; + } + } + } + + if (player->isOffline()) { + IOLoginData::savePlayer(player); + delete player; + } + } else { + uint64_t totalPrice = result->getNumber("price") * amount; + + Player* player = g_game.getPlayerByGUID(playerId); + if (player) { + player->setBankBalance(player->getBankBalance() + totalPrice); + } else { + IOLoginData::increaseBankBalance(playerId, totalPrice); + } + } + } while (result->next()); +} + +void IOMarket::checkExpiredOffers() +{ + const time_t lastExpireDate = time(nullptr) - g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION); + + std::ostringstream query; + query << "SELECT `id`, `amount`, `price`, `itemtype`, `player_id`, `sale` FROM `market_offers` WHERE `created` <= " << lastExpireDate; + g_databaseTasks.addTask(query.str(), IOMarket::processExpiredOffers, true); + + int32_t checkExpiredMarketOffersEachMinutes = g_config.getNumber(ConfigManager::CHECK_EXPIRED_MARKET_OFFERS_EACH_MINUTES); + if (checkExpiredMarketOffersEachMinutes <= 0) { + return; + } + + g_scheduler.addEvent(createSchedulerTask(checkExpiredMarketOffersEachMinutes * 60 * 1000, IOMarket::checkExpiredOffers)); +} + +uint32_t IOMarket::getPlayerOfferCount(uint32_t playerId) +{ + std::ostringstream query; + query << "SELECT COUNT(*) AS `count` FROM `market_offers` WHERE `player_id` = " << playerId; + + DBResult_ptr result = Database::getInstance().storeQuery(query.str()); + if (!result) { + return 0; + } + return result->getNumber("count"); +} + +MarketOfferEx IOMarket::getOfferByCounter(uint32_t timestamp, uint16_t counter) +{ + MarketOfferEx offer; + + const int32_t created = timestamp - g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION); + + std::ostringstream query; + query << "SELECT `id`, `sale`, `itemtype`, `amount`, `created`, `price`, `player_id`, `anonymous`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `player_name` FROM `market_offers` WHERE `created` = " << created << " AND (`id` & 65535) = " << counter << " LIMIT 1"; + + DBResult_ptr result = Database::getInstance().storeQuery(query.str()); + if (!result) { + offer.id = 0; + offer.playerId = 0; + return offer; + } + + offer.id = result->getNumber("id"); + offer.type = static_cast(result->getNumber("sale")); + offer.amount = result->getNumber("amount"); + offer.counter = result->getNumber("id") & 0xFFFF; + offer.timestamp = result->getNumber("created"); + offer.price = result->getNumber("price"); + offer.itemId = result->getNumber("itemtype"); + offer.playerId = result->getNumber("player_id"); + if (result->getNumber("anonymous") == 0) { + offer.playerName = result->getString("player_name"); + } else { + offer.playerName = "Anonymous"; + } + return offer; +} + +void IOMarket::createOffer(uint32_t playerId, MarketAction_t action, uint32_t itemId, uint16_t amount, uint32_t price, bool anonymous) +{ + std::ostringstream query; + query << "INSERT INTO `market_offers` (`player_id`, `sale`, `itemtype`, `amount`, `price`, `created`, `anonymous`) VALUES (" << playerId << ',' << action << ',' << itemId << ',' << amount << ',' << price << ',' << time(nullptr) << ',' << anonymous << ')'; + Database::getInstance().executeQuery(query.str()); +} + +void IOMarket::acceptOffer(uint32_t offerId, uint16_t amount) +{ + std::ostringstream query; + query << "UPDATE `market_offers` SET `amount` = `amount` - " << amount << " WHERE `id` = " << offerId; + Database::getInstance().executeQuery(query.str()); +} + +void IOMarket::deleteOffer(uint32_t offerId) +{ + std::ostringstream query; + query << "DELETE FROM `market_offers` WHERE `id` = " << offerId; + Database::getInstance().executeQuery(query.str()); +} + +void IOMarket::appendHistory(uint32_t playerId, MarketAction_t type, uint16_t itemId, uint16_t amount, uint32_t price, time_t timestamp, MarketOfferState_t state) +{ + std::ostringstream query; + query << "INSERT INTO `market_history` (`player_id`, `sale`, `itemtype`, `amount`, `price`, `expires_at`, `inserted`, `state`) VALUES (" + << playerId << ',' << type << ',' << itemId << ',' << amount << ',' << price << ',' + << timestamp << ',' << time(nullptr) << ',' << state << ')'; + g_databaseTasks.addTask(query.str()); +} + +bool IOMarket::moveOfferToHistory(uint32_t offerId, MarketOfferState_t state) +{ + const int32_t marketOfferDuration = g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION); + + Database& db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `player_id`, `sale`, `itemtype`, `amount`, `price`, `created` FROM `market_offers` WHERE `id` = " << offerId; + + DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return false; + } + + query.str(std::string()); + query << "DELETE FROM `market_offers` WHERE `id` = " << offerId; + if (!db.executeQuery(query.str())) { + return false; + } + + appendHistory(result->getNumber("player_id"), static_cast(result->getNumber("sale")), result->getNumber("itemtype"), result->getNumber("amount"), result->getNumber("price"), result->getNumber("created") + marketOfferDuration, state); + return true; +} + +void IOMarket::updateStatistics() +{ + std::ostringstream query; + query << "SELECT `sale` AS `sale`, `itemtype` AS `itemtype`, COUNT(`price`) AS `num`, MIN(`price`) AS `min`, MAX(`price`) AS `max`, SUM(`price`) AS `sum` FROM `market_history` WHERE `state` = " << OFFERSTATE_ACCEPTED << " GROUP BY `itemtype`, `sale`"; + DBResult_ptr result = Database::getInstance().storeQuery(query.str()); + if (!result) { + return; + } + + do { + MarketStatistics* statistics; + if (result->getNumber("sale") == MARKETACTION_BUY) { + statistics = &purchaseStatistics[result->getNumber("itemtype")]; + } else { + statistics = &saleStatistics[result->getNumber("itemtype")]; + } + + statistics->numTransactions = result->getNumber("num"); + statistics->lowestPrice = result->getNumber("min"); + statistics->totalPrice = result->getNumber("sum"); + statistics->highestPrice = result->getNumber("max"); + } while (result->next()); +} + +MarketStatistics* IOMarket::getPurchaseStatistics(uint16_t itemId) +{ + auto it = purchaseStatistics.find(itemId); + if (it == purchaseStatistics.end()) { + return nullptr; + } + return &it->second; +} + +MarketStatistics* IOMarket::getSaleStatistics(uint16_t itemId) +{ + auto it = saleStatistics.find(itemId); + if (it == saleStatistics.end()) { + return nullptr; + } + return &it->second; +} diff --git a/src/iomarket.h b/src/iomarket.h new file mode 100644 index 0000000..3491e6f --- /dev/null +++ b/src/iomarket.h @@ -0,0 +1,63 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_IOMARKET_H_B981E52C218C42D3B9EF726EBF0E92C9 +#define FS_IOMARKET_H_B981E52C218C42D3B9EF726EBF0E92C9 + +#include "enums.h" +#include "database.h" + +class IOMarket +{ + public: + static IOMarket& getInstance() { + static IOMarket instance; + return instance; + } + + static MarketOfferList getActiveOffers(MarketAction_t action, uint16_t itemId); + static MarketOfferList getOwnOffers(MarketAction_t action, uint32_t playerId); + static HistoryMarketOfferList getOwnHistory(MarketAction_t action, uint32_t playerId); + + static void processExpiredOffers(DBResult_ptr result, bool); + static void checkExpiredOffers(); + + static uint32_t getPlayerOfferCount(uint32_t playerId); + static MarketOfferEx getOfferByCounter(uint32_t timestamp, uint16_t counter); + + static void createOffer(uint32_t playerId, MarketAction_t action, uint32_t itemId, uint16_t amount, uint32_t price, bool anonymous); + static void acceptOffer(uint32_t offerId, uint16_t amount); + static void deleteOffer(uint32_t offerId); + + static void appendHistory(uint32_t playerId, MarketAction_t type, uint16_t itemId, uint16_t amount, uint32_t price, time_t timestamp, MarketOfferState_t state); + static bool moveOfferToHistory(uint32_t offerId, MarketOfferState_t state); + + void updateStatistics(); + + MarketStatistics* getPurchaseStatistics(uint16_t itemId); + MarketStatistics* getSaleStatistics(uint16_t itemId); + + private: + IOMarket() = default; + + std::map purchaseStatistics; + std::map saleStatistics; +}; + +#endif diff --git a/src/item.cpp b/src/item.cpp new file mode 100644 index 0000000..c93892f --- /dev/null +++ b/src/item.cpp @@ -0,0 +1,1794 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "item.h" +#include "container.h" +#include "teleport.h" +#include "trashholder.h" +#include "mailbox.h" +#include "house.h" +#include "game.h" +#include "bed.h" + +#include "actions.h" +#include "spells.h" + +extern Game g_game; +extern Spells* g_spells; +extern Vocations g_vocations; + +Items Item::items; + +Item* Item::CreateItem(const uint16_t type, uint16_t count /*= 0*/) +{ + Item* newItem = nullptr; + + const ItemType& it = Item::items[type]; + if (it.group == ITEM_GROUP_DEPRECATED) { + return nullptr; + } + + if (it.stackable && count == 0) { + count = 1; + } + + if (it.id != 0) { + if (it.isDepot()) { + newItem = new DepotLocker(type); + } else if (it.isContainer()) { + newItem = new Container(type); + } else if (it.isTeleport()) { + newItem = new Teleport(type); + } else if (it.isMagicField()) { + newItem = new MagicField(type); + } else if (it.isDoor()) { + newItem = new Door(type); + } else if (it.isTrashHolder()) { + newItem = new TrashHolder(type); + } else if (it.isMailbox()) { + newItem = new Mailbox(type); + } else if (it.isBed()) { + newItem = new BedItem(type); + } else if (it.id >= 2210 && it.id <= 2212) { + newItem = new Item(type - 3, count); + } else if (it.id == 2215 || it.id == 2216) { + newItem = new Item(type - 2, count); + } else if (it.id >= 2202 && it.id <= 2206) { + newItem = new Item(type - 37, count); + } else if (it.id == 2640) { + newItem = new Item(6132, count); + } else if (it.id == 6301) { + newItem = new Item(6300, count); + } else if (it.id == 18528) { + newItem = new Item(18408, count); + } else { + newItem = new Item(type, count); + } + + newItem->incrementReferenceCounter(); + } + + return newItem; +} + +Container* Item::CreateItemAsContainer(const uint16_t type, uint16_t size) +{ + const ItemType& it = Item::items[type]; + if (it.id == 0 || it.group == ITEM_GROUP_DEPRECATED || it.stackable || it.useable || it.moveable || it.pickupable || it.isDepot() || it.isSplash() || it.isDoor()) { + return nullptr; + } + + Container* newItem = new Container(type, size); + newItem->incrementReferenceCounter(); + return newItem; +} + +Item* Item::CreateItem(PropStream& propStream) +{ + uint16_t id; + if (!propStream.read(id)) { + return nullptr; + } + + switch (id) { + case ITEM_FIREFIELD_PVP_FULL: + id = ITEM_FIREFIELD_PERSISTENT_FULL; + break; + + case ITEM_FIREFIELD_PVP_MEDIUM: + id = ITEM_FIREFIELD_PERSISTENT_MEDIUM; + break; + + case ITEM_FIREFIELD_PVP_SMALL: + id = ITEM_FIREFIELD_PERSISTENT_SMALL; + break; + + case ITEM_ENERGYFIELD_PVP: + id = ITEM_ENERGYFIELD_PERSISTENT; + break; + + case ITEM_POISONFIELD_PVP: + id = ITEM_POISONFIELD_PERSISTENT; + break; + + case ITEM_MAGICWALL: + id = ITEM_MAGICWALL_PERSISTENT; + break; + + case ITEM_WILDGROWTH: + id = ITEM_WILDGROWTH_PERSISTENT; + break; + + default: + break; + } + + return Item::CreateItem(id, 0); +} + +Item::Item(const uint16_t type, uint16_t count /*= 0*/) : + id(type) +{ + const ItemType& it = items[id]; + + if (it.isFluidContainer() || it.isSplash()) { + setFluidType(count); + } else if (it.stackable) { + if (count != 0) { + setItemCount(count); + } else if (it.charges != 0) { + setItemCount(it.charges); + } + } else if (it.charges != 0) { + if (count != 0) { + setCharges(count); + } else { + setCharges(it.charges); + } + } + + setDefaultDuration(); +} + +Item::Item(const Item& i) : + Thing(), id(i.id), count(i.count), loadedFromMap(i.loadedFromMap) +{ + if (i.attributes) { + attributes.reset(new ItemAttributes(*i.attributes)); + } +} + +Item* Item::clone() const +{ + Item* item = Item::CreateItem(id, count); + if (attributes) { + item->attributes.reset(new ItemAttributes(*attributes)); + if (item->getDuration() > 0) { + item->incrementReferenceCounter(); + item->setDecaying(DECAYING_TRUE); + g_game.toDecayItems.push_front(item); + } + } + return item; +} + +bool Item::equals(const Item* otherItem) const +{ + if (!otherItem || id != otherItem->id) { + return false; + } + + const auto& otherAttributes = otherItem->attributes; + if (!attributes) { + return !otherAttributes || (otherAttributes->attributeBits == 0); + } else if (!otherAttributes) { + return (attributes->attributeBits == 0); + } + + if (attributes->attributeBits != otherAttributes->attributeBits) { + return false; + } + + const auto& attributeList = attributes->attributes; + const auto& otherAttributeList = otherAttributes->attributes; + for (const auto& attribute : attributeList) { + if (ItemAttributes::isStrAttrType(attribute.type)) { + for (const auto& otherAttribute : otherAttributeList) { + if (attribute.type == otherAttribute.type && *attribute.value.string != *otherAttribute.value.string) { + return false; + } + } + } else { + for (const auto& otherAttribute : otherAttributeList) { + if (attribute.type == otherAttribute.type && attribute.value.integer != otherAttribute.value.integer) { + return false; + } + } + } + } + return true; +} + +void Item::setDefaultSubtype() +{ + const ItemType& it = items[id]; + + setItemCount(1); + + if (it.charges != 0) { + if (it.stackable) { + setItemCount(it.charges); + } else { + setCharges(it.charges); + } + } +} + +void Item::onRemoved() +{ + ScriptEnvironment::removeTempItem(this); + + if (hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)) { + g_game.removeUniqueItem(getUniqueId()); + } +} + +void Item::setID(uint16_t newid) +{ + const ItemType& prevIt = Item::items[id]; + id = newid; + + const ItemType& it = Item::items[newid]; + uint32_t newDuration = it.decayTime * 1000; + + if (newDuration == 0 && !it.stopTime && it.decayTo < 0) { + removeAttribute(ITEM_ATTRIBUTE_DECAYSTATE); + removeAttribute(ITEM_ATTRIBUTE_DURATION); + } + + removeAttribute(ITEM_ATTRIBUTE_CORPSEOWNER); + + if (newDuration > 0 && (!prevIt.stopTime || !hasAttribute(ITEM_ATTRIBUTE_DURATION))) { + setDecaying(DECAYING_FALSE); + setDuration(newDuration); + } +} + +Cylinder* Item::getTopParent() +{ + Cylinder* aux = getParent(); + Cylinder* prevaux = dynamic_cast(this); + if (!aux) { + return prevaux; + } + + while (aux->getParent() != nullptr) { + prevaux = aux; + aux = aux->getParent(); + } + + if (prevaux) { + return prevaux; + } + return aux; +} + +const Cylinder* Item::getTopParent() const +{ + const Cylinder* aux = getParent(); + const Cylinder* prevaux = dynamic_cast(this); + if (!aux) { + return prevaux; + } + + while (aux->getParent() != nullptr) { + prevaux = aux; + aux = aux->getParent(); + } + + if (prevaux) { + return prevaux; + } + return aux; +} + +Tile* Item::getTile() +{ + Cylinder* cylinder = getTopParent(); + //get root cylinder + if (cylinder && cylinder->getParent()) { + cylinder = cylinder->getParent(); + } + return dynamic_cast(cylinder); +} + +const Tile* Item::getTile() const +{ + const Cylinder* cylinder = getTopParent(); + //get root cylinder + if (cylinder && cylinder->getParent()) { + cylinder = cylinder->getParent(); + } + return dynamic_cast(cylinder); +} + +uint16_t Item::getSubType() const +{ + const ItemType& it = items[id]; + if (it.isFluidContainer() || it.isSplash()) { + return getFluidType(); + } else if (it.stackable) { + return count; + } else if (it.charges != 0) { + return getCharges(); + } + return count; +} + +Player* Item::getHoldingPlayer() const +{ + Cylinder* p = getParent(); + while (p) { + if (p->getCreature()) { + return p->getCreature()->getPlayer(); + } + + p = p->getParent(); + } + return nullptr; +} + +void Item::setSubType(uint16_t n) +{ + const ItemType& it = items[id]; + if (it.isFluidContainer() || it.isSplash()) { + setFluidType(n); + } else if (it.stackable) { + setItemCount(n); + } else if (it.charges != 0) { + setCharges(n); + } else { + setItemCount(n); + } +} + +Attr_ReadValue Item::readAttr(AttrTypes_t attr, PropStream& propStream) +{ + switch (attr) { + case ATTR_COUNT: + case ATTR_RUNE_CHARGES: { + uint8_t count; + if (!propStream.read(count)) { + return ATTR_READ_ERROR; + } + + setSubType(count); + break; + } + + case ATTR_ACTION_ID: { + uint16_t actionId; + if (!propStream.read(actionId)) { + return ATTR_READ_ERROR; + } + + setActionId(actionId); + break; + } + + case ATTR_UNIQUE_ID: { + uint16_t uniqueId; + if (!propStream.read(uniqueId)) { + return ATTR_READ_ERROR; + } + + setUniqueId(uniqueId); + break; + } + + case ATTR_TEXT: { + std::string text; + if (!propStream.readString(text)) { + return ATTR_READ_ERROR; + } + + setText(text); + break; + } + + case ATTR_WRITTENDATE: { + uint32_t writtenDate; + if (!propStream.read(writtenDate)) { + return ATTR_READ_ERROR; + } + + setDate(writtenDate); + break; + } + + case ATTR_WRITTENBY: { + std::string writer; + if (!propStream.readString(writer)) { + return ATTR_READ_ERROR; + } + + setWriter(writer); + break; + } + + case ATTR_DESC: { + std::string text; + if (!propStream.readString(text)) { + return ATTR_READ_ERROR; + } + + setSpecialDescription(text); + break; + } + + case ATTR_CHARGES: { + uint16_t charges; + if (!propStream.read(charges)) { + return ATTR_READ_ERROR; + } + + setSubType(charges); + break; + } + + case ATTR_DURATION: { + int32_t duration; + if (!propStream.read(duration)) { + return ATTR_READ_ERROR; + } + + setDuration(std::max(0, duration)); + break; + } + + case ATTR_DECAYING_STATE: { + uint8_t state; + if (!propStream.read(state)) { + return ATTR_READ_ERROR; + } + + if (state != DECAYING_FALSE) { + setDecaying(DECAYING_PENDING); + } + break; + } + + case ATTR_NAME: { + std::string name; + if (!propStream.readString(name)) { + return ATTR_READ_ERROR; + } + + setStrAttr(ITEM_ATTRIBUTE_NAME, name); + break; + } + + case ATTR_ARTICLE: { + std::string article; + if (!propStream.readString(article)) { + return ATTR_READ_ERROR; + } + + setStrAttr(ITEM_ATTRIBUTE_ARTICLE, article); + break; + } + + case ATTR_PLURALNAME: { + std::string pluralName; + if (!propStream.readString(pluralName)) { + return ATTR_READ_ERROR; + } + + setStrAttr(ITEM_ATTRIBUTE_PLURALNAME, pluralName); + break; + } + + case ATTR_WEIGHT: { + uint32_t weight; + if (!propStream.read(weight)) { + return ATTR_READ_ERROR; + } + + setIntAttr(ITEM_ATTRIBUTE_WEIGHT, weight); + break; + } + + case ATTR_ATTACK: { + int32_t attack; + if (!propStream.read(attack)) { + return ATTR_READ_ERROR; + } + + setIntAttr(ITEM_ATTRIBUTE_ATTACK, attack); + break; + } + + case ATTR_DEFENSE: { + int32_t defense; + if (!propStream.read(defense)) { + return ATTR_READ_ERROR; + } + + setIntAttr(ITEM_ATTRIBUTE_DEFENSE, defense); + break; + } + + case ATTR_EXTRADEFENSE: { + int32_t extraDefense; + if (!propStream.read(extraDefense)) { + return ATTR_READ_ERROR; + } + + setIntAttr(ITEM_ATTRIBUTE_EXTRADEFENSE, extraDefense); + break; + } + + case ATTR_ARMOR: { + int32_t armor; + if (!propStream.read(armor)) { + return ATTR_READ_ERROR; + } + + setIntAttr(ITEM_ATTRIBUTE_ARMOR, armor); + break; + } + + case ATTR_HITCHANCE: { + int8_t hitChance; + if (!propStream.read(hitChance)) { + return ATTR_READ_ERROR; + } + + setIntAttr(ITEM_ATTRIBUTE_HITCHANCE, hitChance); + break; + } + + case ATTR_SHOOTRANGE: { + uint8_t shootRange; + if (!propStream.read(shootRange)) { + return ATTR_READ_ERROR; + } + + setIntAttr(ITEM_ATTRIBUTE_SHOOTRANGE, shootRange); + break; + } + + case ATTR_DECAYTO: { + int32_t decayTo; + if (!propStream.read(decayTo)) { + return ATTR_READ_ERROR; + } + + setIntAttr(ITEM_ATTRIBUTE_DECAYTO, decayTo); + break; + } + + case ATTR_WRAPID: { + uint16_t wrapId; + if (!propStream.read(wrapId)) { + return ATTR_READ_ERROR; + } + + setIntAttr(ITEM_ATTRIBUTE_WRAPID, wrapId); + break; + } + + //these should be handled through derived classes + //If these are called then something has changed in the items.xml since the map was saved + //just read the values + + //Depot class + case ATTR_DEPOT_ID: { + if (!propStream.skip(2)) { + return ATTR_READ_ERROR; + } + break; + } + + //Door class + case ATTR_HOUSEDOORID: { + if (!propStream.skip(1)) { + return ATTR_READ_ERROR; + } + break; + } + + //Bed class + case ATTR_SLEEPERGUID: { + if (!propStream.skip(4)) { + return ATTR_READ_ERROR; + } + break; + } + + case ATTR_SLEEPSTART: { + if (!propStream.skip(4)) { + return ATTR_READ_ERROR; + } + break; + } + + //Teleport class + case ATTR_TELE_DEST: { + if (!propStream.skip(5)) { + return ATTR_READ_ERROR; + } + break; + } + + //Container class + case ATTR_CONTAINER_ITEMS: { + return ATTR_READ_ERROR; + } + + case ATTR_CUSTOM_ATTRIBUTES: { + uint64_t size; + if (!propStream.read(size)) { + return ATTR_READ_ERROR; + } + + for (uint64_t i = 0; i < size; i++) { + // Unserialize key type and value + std::string key; + if (!propStream.readString(key)) { + return ATTR_READ_ERROR; + }; + + // Unserialize value type and value + ItemAttributes::CustomAttribute val; + if (!val.unserialize(propStream)) { + return ATTR_READ_ERROR; + } + + setCustomAttribute(key, val); + } + break; + } + + default: + return ATTR_READ_ERROR; + } + + return ATTR_READ_CONTINUE; +} + +bool Item::unserializeAttr(PropStream& propStream) +{ + uint8_t attr_type; + while (propStream.read(attr_type) && attr_type != 0) { + Attr_ReadValue ret = readAttr(static_cast(attr_type), propStream); + if (ret == ATTR_READ_ERROR) { + return false; + } else if (ret == ATTR_READ_END) { + return true; + } + } + return true; +} + +bool Item::unserializeItemNode(OTB::Loader&, const OTB::Node&, PropStream& propStream) +{ + return unserializeAttr(propStream); +} + +void Item::serializeAttr(PropWriteStream& propWriteStream) const +{ + const ItemType& it = items[id]; + if (it.stackable || it.isFluidContainer() || it.isSplash()) { + propWriteStream.write(ATTR_COUNT); + propWriteStream.write(getSubType()); + } + + uint16_t charges = getCharges(); + if (charges != 0) { + propWriteStream.write(ATTR_CHARGES); + propWriteStream.write(charges); + } + + if (it.moveable) { + uint16_t actionId = getActionId(); + if (actionId != 0) { + propWriteStream.write(ATTR_ACTION_ID); + propWriteStream.write(actionId); + } + } + + const std::string& text = getText(); + if (!text.empty()) { + propWriteStream.write(ATTR_TEXT); + propWriteStream.writeString(text); + } + + const time_t writtenDate = getDate(); + if (writtenDate != 0) { + propWriteStream.write(ATTR_WRITTENDATE); + propWriteStream.write(writtenDate); + } + + const std::string& writer = getWriter(); + if (!writer.empty()) { + propWriteStream.write(ATTR_WRITTENBY); + propWriteStream.writeString(writer); + } + + const std::string& specialDesc = getSpecialDescription(); + if (!specialDesc.empty()) { + propWriteStream.write(ATTR_DESC); + propWriteStream.writeString(specialDesc); + } + + if (hasAttribute(ITEM_ATTRIBUTE_DURATION)) { + propWriteStream.write(ATTR_DURATION); + propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_DURATION)); + } + + ItemDecayState_t decayState = getDecaying(); + if (decayState == DECAYING_TRUE || decayState == DECAYING_PENDING) { + propWriteStream.write(ATTR_DECAYING_STATE); + propWriteStream.write(decayState); + } + + if (hasAttribute(ITEM_ATTRIBUTE_NAME)) { + propWriteStream.write(ATTR_NAME); + propWriteStream.writeString(getStrAttr(ITEM_ATTRIBUTE_NAME)); + } + + if (hasAttribute(ITEM_ATTRIBUTE_ARTICLE)) { + propWriteStream.write(ATTR_ARTICLE); + propWriteStream.writeString(getStrAttr(ITEM_ATTRIBUTE_ARTICLE)); + } + + if (hasAttribute(ITEM_ATTRIBUTE_PLURALNAME)) { + propWriteStream.write(ATTR_PLURALNAME); + propWriteStream.writeString(getStrAttr(ITEM_ATTRIBUTE_PLURALNAME)); + } + + if (hasAttribute(ITEM_ATTRIBUTE_WEIGHT)) { + propWriteStream.write(ATTR_WEIGHT); + propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_WEIGHT)); + } + + if (hasAttribute(ITEM_ATTRIBUTE_ATTACK)) { + propWriteStream.write(ATTR_ATTACK); + propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_ATTACK)); + } + + if (hasAttribute(ITEM_ATTRIBUTE_DEFENSE)) { + propWriteStream.write(ATTR_DEFENSE); + propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_DEFENSE)); + } + + if (hasAttribute(ITEM_ATTRIBUTE_EXTRADEFENSE)) { + propWriteStream.write(ATTR_EXTRADEFENSE); + propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_EXTRADEFENSE)); + } + + if (hasAttribute(ITEM_ATTRIBUTE_ARMOR)) { + propWriteStream.write(ATTR_ARMOR); + propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_ARMOR)); + } + + if (hasAttribute(ITEM_ATTRIBUTE_HITCHANCE)) { + propWriteStream.write(ATTR_HITCHANCE); + propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_HITCHANCE)); + } + + if (hasAttribute(ITEM_ATTRIBUTE_SHOOTRANGE)) { + propWriteStream.write(ATTR_SHOOTRANGE); + propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_SHOOTRANGE)); + } + + if (hasAttribute(ITEM_ATTRIBUTE_DECAYTO)) { + propWriteStream.write(ATTR_DECAYTO); + propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_DECAYTO)); + } + + if (hasAttribute(ITEM_ATTRIBUTE_WRAPID)) { + propWriteStream.write(ATTR_WRAPID); + propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_WRAPID)); + } + + if (hasAttribute(ITEM_ATTRIBUTE_CUSTOM)) { + const ItemAttributes::CustomAttributeMap* customAttrMap = attributes->getCustomAttributeMap(); + propWriteStream.write(ATTR_CUSTOM_ATTRIBUTES); + propWriteStream.write(static_cast(customAttrMap->size())); + for (const auto &entry : *customAttrMap) { + // Serializing key type and value + propWriteStream.writeString(entry.first); + + // Serializing value type and value + entry.second.serialize(propWriteStream); + } + } +} + +bool Item::hasProperty(ITEMPROPERTY prop) const +{ + const ItemType& it = items[id]; + switch (prop) { + case CONST_PROP_BLOCKSOLID: return it.blockSolid; + case CONST_PROP_MOVEABLE: return it.moveable && !hasAttribute(ITEM_ATTRIBUTE_UNIQUEID); + case CONST_PROP_HASHEIGHT: return it.hasHeight; + case CONST_PROP_BLOCKPROJECTILE: return it.blockProjectile; + case CONST_PROP_BLOCKPATH: return it.blockPathFind; + case CONST_PROP_ISVERTICAL: return it.isVertical; + case CONST_PROP_ISHORIZONTAL: return it.isHorizontal; + case CONST_PROP_IMMOVABLEBLOCKSOLID: return it.blockSolid && (!it.moveable || hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)); + case CONST_PROP_IMMOVABLEBLOCKPATH: return it.blockPathFind && (!it.moveable || hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)); + case CONST_PROP_IMMOVABLENOFIELDBLOCKPATH: return !it.isMagicField() && it.blockPathFind && (!it.moveable || hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)); + case CONST_PROP_NOFIELDBLOCKPATH: return !it.isMagicField() && it.blockPathFind; + case CONST_PROP_SUPPORTHANGABLE: return it.isHorizontal || it.isVertical; + default: return false; + } +} + +uint32_t Item::getWeight() const +{ + uint32_t weight = getBaseWeight(); + if (isStackable()) { + return weight * std::max(1, getItemCount()); + } + return weight; +} + +std::string Item::getDescription(const ItemType& it, int32_t lookDistance, + const Item* item /*= nullptr*/, int32_t subType /*= -1*/, bool addArticle /*= true*/) +{ + const std::string* text = nullptr; + + std::ostringstream s; + s << getNameDescription(it, item, subType, addArticle); + + if (item) { + subType = item->getSubType(); + } + + if (it.isRune()) { + if (it.runeLevel > 0 || it.runeMagLevel > 0) { + if (RuneSpell* rune = g_spells->getRuneSpell(it.id)) { + int32_t tmpSubType = subType; + if (item) { + tmpSubType = item->getSubType(); + } + s << ". " << (it.stackable && tmpSubType > 1 ? "They" : "It") << " can only be used by "; + + const VocSpellMap& vocMap = rune->getVocMap(); + std::vector showVocMap; + + // vocations are usually listed with the unpromoted and promoted version, the latter being + // hidden from description, so `total / 2` is most likely the amount of vocations to be shown. + showVocMap.reserve(vocMap.size() / 2); + for (const auto& voc : vocMap) { + if (voc.second) { + showVocMap.push_back(g_vocations.getVocation(voc.first)); + } + } + + if (!showVocMap.empty()) { + auto vocIt = showVocMap.begin(), vocLast = (showVocMap.end() - 1); + while (vocIt != vocLast) { + s << asLowerCaseString((*vocIt)->getVocName()) << "s"; + if (++vocIt == vocLast) { + s << " and "; + } else { + s << ", "; + } + } + s << asLowerCaseString((*vocLast)->getVocName()) << "s"; + } else { + s << "players"; + } + + s << " with"; + + if (it.runeLevel > 0) { + s << " level " << it.runeLevel; + } + + if (it.runeMagLevel > 0) { + if (it.runeLevel > 0) { + s << " and"; + } + + s << " magic level " << it.runeMagLevel; + } + + s << " or higher"; + } + } + } else if (it.weaponType != WEAPON_NONE) { + bool begin = true; + if (it.weaponType == WEAPON_DISTANCE && it.ammoType != AMMO_NONE) { + s << " (Range:" << static_cast(item ? item->getShootRange() : it.shootRange); + + int32_t attack; + int8_t hitChance; + if (item) { + attack = item->getAttack(); + hitChance = item->getHitChance(); + } else { + attack = it.attack; + hitChance = it.hitChance; + } + + if (attack != 0) { + s << ", Atk" << std::showpos << attack << std::noshowpos; + } + + if (hitChance != 0) { + s << ", Hit%" << std::showpos << static_cast(hitChance) << std::noshowpos; + } + + begin = false; + } else if (it.weaponType != WEAPON_AMMO) { + + int32_t attack, defense, extraDefense; + if (item) { + attack = item->getAttack(); + defense = item->getDefense(); + extraDefense = item->getExtraDefense(); + } else { + attack = it.attack; + defense = it.defense; + extraDefense = it.extraDefense; + } + + if (attack != 0) { + begin = false; + s << " (Atk:" << attack; + + if (it.abilities && it.abilities->elementType != COMBAT_NONE && it.abilities->elementDamage != 0) { + s << " physical + " << it.abilities->elementDamage << ' ' << getCombatName(it.abilities->elementType); + } + } + + if (defense != 0 || extraDefense != 0) { + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "Def:" << defense; + if (extraDefense != 0) { + s << ' ' << std::showpos << extraDefense << std::noshowpos; + } + } + } + + if (it.abilities) { + for (uint8_t i = SKILL_FIRST; i <= SKILL_LAST; i++) { + if (!it.abilities->skills[i]) { + continue; + } + + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << getSkillName(i) << ' ' << std::showpos << it.abilities->skills[i] << std::noshowpos; + } + + for (uint8_t i = SPECIALSKILL_FIRST; i <= SPECIALSKILL_LAST; i++) { + if (!it.abilities->specialSkills[i]) { + continue; + } + + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << getSpecialSkillName(i) << ' ' << std::showpos << it.abilities->specialSkills[i] << '%' << std::noshowpos; + } + + if (it.abilities->stats[STAT_MAGICPOINTS]) { + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "magic level " << std::showpos << it.abilities->stats[STAT_MAGICPOINTS] << std::noshowpos; + } + + int16_t show = it.abilities->absorbPercent[0]; + if (show != 0) { + for (size_t i = 1; i < COMBAT_COUNT; ++i) { + if (it.abilities->absorbPercent[i] != show) { + show = 0; + break; + } + } + } + + if (show == 0) { + bool tmp = true; + + for (size_t i = 0; i < COMBAT_COUNT; ++i) { + if (it.abilities->absorbPercent[i] == 0) { + continue; + } + + if (tmp) { + tmp = false; + + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "protection "; + } else { + s << ", "; + } + + s << getCombatName(indexToCombatType(i)) << ' ' << std::showpos << it.abilities->absorbPercent[i] << std::noshowpos << '%'; + } + } else { + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "protection all " << std::showpos << show << std::noshowpos << '%'; + } + + show = it.abilities->fieldAbsorbPercent[0]; + if (show != 0) { + for (size_t i = 1; i < COMBAT_COUNT; ++i) { + if (it.abilities->absorbPercent[i] != show) { + show = 0; + break; + } + } + } + + if (show == 0) { + bool tmp = true; + + for (size_t i = 0; i < COMBAT_COUNT; ++i) { + if (it.abilities->fieldAbsorbPercent[i] == 0) { + continue; + } + + if (tmp) { + tmp = false; + + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "protection "; + } else { + s << ", "; + } + + s << getCombatName(indexToCombatType(i)) << " field " << std::showpos << it.abilities->fieldAbsorbPercent[i] << std::noshowpos << '%'; + } + } else { + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "protection all fields " << std::showpos << show << std::noshowpos << '%'; + } + + if (it.abilities->speed) { + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "speed " << std::showpos << (it.abilities->speed >> 1) << std::noshowpos; + } + } + + if (!begin) { + s << ')'; + } + } else if (it.armor != 0 || (item && item->getArmor() != 0) || it.showAttributes) { + bool begin = true; + + int32_t armor = (item ? item->getArmor() : it.armor); + if (armor != 0) { + s << " (Arm:" << armor; + begin = false; + } + + if (it.abilities) { + for (uint8_t i = SKILL_FIRST; i <= SKILL_LAST; i++) { + if (!it.abilities->skills[i]) { + continue; + } + + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << getSkillName(i) << ' ' << std::showpos << it.abilities->skills[i] << std::noshowpos; + } + + if (it.abilities->stats[STAT_MAGICPOINTS]) { + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "magic level " << std::showpos << it.abilities->stats[STAT_MAGICPOINTS] << std::noshowpos; + } + + int16_t show = it.abilities->absorbPercent[0]; + if (show != 0) { + for (size_t i = 1; i < COMBAT_COUNT; ++i) { + if (it.abilities->absorbPercent[i] != show) { + show = 0; + break; + } + } + } + + if (!show) { + bool protectionBegin = true; + for (size_t i = 0; i < COMBAT_COUNT; ++i) { + if (it.abilities->absorbPercent[i] == 0) { + continue; + } + + if (protectionBegin) { + protectionBegin = false; + + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "protection "; + } else { + s << ", "; + } + + s << getCombatName(indexToCombatType(i)) << ' ' << std::showpos << it.abilities->absorbPercent[i] << std::noshowpos << '%'; + } + } else { + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "protection all " << std::showpos << show << std::noshowpos << '%'; + } + + show = it.abilities->fieldAbsorbPercent[0]; + if (show != 0) { + for (size_t i = 1; i < COMBAT_COUNT; ++i) { + if (it.abilities->absorbPercent[i] != show) { + show = 0; + break; + } + } + } + + if (!show) { + bool tmp = true; + + for (size_t i = 0; i < COMBAT_COUNT; ++i) { + if (it.abilities->fieldAbsorbPercent[i] == 0) { + continue; + } + + if (tmp) { + tmp = false; + + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "protection "; + } else { + s << ", "; + } + + s << getCombatName(indexToCombatType(i)) << " field " << std::showpos << it.abilities->fieldAbsorbPercent[i] << std::noshowpos << '%'; + } + } else { + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "protection all fields " << std::showpos << show << std::noshowpos << '%'; + } + + if (it.abilities->speed) { + if (begin) { + begin = false; + s << " ("; + } else { + s << ", "; + } + + s << "speed " << std::showpos << (it.abilities->speed >> 1) << std::noshowpos; + } + } + + if (!begin) { + s << ')'; + } + } else if (it.isContainer() || (item && item->getContainer())) { + uint32_t volume = 0; + if (!item || !item->hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)) { + if (it.isContainer()) { + volume = it.maxItems; + } else { + volume = item->getContainer()->capacity(); + } + } + + if (volume != 0) { + s << " (Vol:" << volume << ')'; + } + } else { + bool found = true; + + if (it.abilities) { + if (it.abilities->speed > 0) { + s << " (speed " << std::showpos << (it.abilities->speed / 2) << std::noshowpos << ')'; + } else if (hasBitSet(CONDITION_DRUNK, it.abilities->conditionSuppressions)) { + s << " (hard drinking)"; + } else if (it.abilities->invisible) { + s << " (invisibility)"; + } else if (it.abilities->regeneration) { + s << " (faster regeneration)"; + } else if (it.abilities->manaShield) { + s << " (mana shield)"; + } else { + found = false; + } + } else { + found = false; + } + + if (!found) { + if (it.isKey()) { + s << " (Key:" << std::setfill('0') << std::setw(4) << (item ? item->getActionId() : 0) << ')'; + } else if (it.isFluidContainer()) { + if (subType > 0) { + const std::string& itemName = items[subType].name; + s << " of " << (!itemName.empty() ? itemName : "unknown"); + } else { + s << ". It is empty"; + } + } else if (it.isSplash()) { + s << " of "; + + if (subType > 0 && !items[subType].name.empty()) { + s << items[subType].name; + } else { + s << "unknown"; + } + } else if (it.allowDistRead && (it.id < 7369 || it.id > 7371)) { + s << ".\n"; + + if (lookDistance <= 4) { + if (item) { + text = &item->getText(); + if (!text->empty()) { + const std::string& writer = item->getWriter(); + if (!writer.empty()) { + s << writer << " wrote"; + time_t date = item->getDate(); + if (date != 0) { + s << " on " << formatDateShort(date); + } + s << ": "; + } else { + s << "You read: "; + } + s << *text; + } else { + s << "Nothing is written on it"; + } + } else { + s << "Nothing is written on it"; + } + } else { + s << "You are too far away to read it"; + } + } else if (it.levelDoor != 0 && item) { + uint16_t actionId = item->getActionId(); + if (actionId >= it.levelDoor) { + s << " for level " << (actionId - it.levelDoor); + } + } + } + } + + if (it.showCharges) { + s << " that has " << subType << " charge" << (subType != 1 ? "s" : "") << " left"; + } + + if (it.showDuration) { + if (item && item->hasAttribute(ITEM_ATTRIBUTE_DURATION)) { + uint32_t duration = item->getDuration() / 1000; + s << " that will expire in "; + + if (duration >= 86400) { + uint16_t days = duration / 86400; + uint16_t hours = (duration % 86400) / 3600; + s << days << " day" << (days != 1 ? "s" : ""); + + if (hours > 0) { + s << " and " << hours << " hour" << (hours != 1 ? "s" : ""); + } + } else if (duration >= 3600) { + uint16_t hours = duration / 3600; + uint16_t minutes = (duration % 3600) / 60; + s << hours << " hour" << (hours != 1 ? "s" : ""); + + if (minutes > 0) { + s << " and " << minutes << " minute" << (minutes != 1 ? "s" : ""); + } + } else if (duration >= 60) { + uint16_t minutes = duration / 60; + s << minutes << " minute" << (minutes != 1 ? "s" : ""); + uint16_t seconds = duration % 60; + + if (seconds > 0) { + s << " and " << seconds << " second" << (seconds != 1 ? "s" : ""); + } + } else { + s << duration << " second" << (duration != 1 ? "s" : ""); + } + } else { + s << " that is brand-new"; + } + } + + if (!it.allowDistRead || (it.id >= 7369 && it.id <= 7371)) { + s << '.'; + } else { + if (!text && item) { + text = &item->getText(); + } + + if (!text || text->empty()) { + s << '.'; + } + } + + if (it.wieldInfo != 0) { + s << "\nIt can only be wielded properly by "; + + if (it.wieldInfo & WIELDINFO_PREMIUM) { + s << "premium "; + } + + if (!it.vocationString.empty()) { + s << it.vocationString; + } else { + s << "players"; + } + + if (it.wieldInfo & WIELDINFO_LEVEL) { + s << " of level " << it.minReqLevel << " or higher"; + } + + if (it.wieldInfo & WIELDINFO_MAGLV) { + if (it.wieldInfo & WIELDINFO_LEVEL) { + s << " and"; + } else { + s << " of"; + } + + s << " magic level " << it.minReqMagicLevel << " or higher"; + } + + s << '.'; + } + + if (lookDistance <= 1) { + if (item) { + const uint32_t weight = item->getWeight(); + if (weight != 0 && it.pickupable) { + s << '\n' << getWeightDescription(it, weight, item->getItemCount()); + } + } else if (it.weight != 0 && it.pickupable) { + s << '\n' << getWeightDescription(it, it.weight); + } + } + + if (item) { + const std::string& specialDescription = item->getSpecialDescription(); + if (!specialDescription.empty()) { + s << '\n' << specialDescription; + } else if (lookDistance <= 1 && !it.description.empty()) { + s << '\n' << it.description; + } + } else if (lookDistance <= 1 && !it.description.empty()) { + s << '\n' << it.description; + } + + if (it.allowDistRead && it.id >= 7369 && it.id <= 7371) { + if (!text && item) { + text = &item->getText(); + } + + if (text && !text->empty()) { + s << '\n' << *text; + } + } + return s.str(); +} + +std::string Item::getDescription(int32_t lookDistance) const +{ + const ItemType& it = items[id]; + return getDescription(it, lookDistance, this); +} + +std::string Item::getNameDescription(const ItemType& it, const Item* item /*= nullptr*/, int32_t subType /*= -1*/, bool addArticle /*= true*/) +{ + if (item) { + subType = item->getSubType(); + } + + std::ostringstream s; + + const std::string& name = (item ? item->getName() : it.name); + if (!name.empty()) { + if (it.stackable && subType > 1) { + if (it.showCount) { + s << subType << ' '; + } + + s << (item ? item->getPluralName() : it.getPluralName()); + } else { + if (addArticle) { + const std::string& article = (item ? item->getArticle() : it.article); + if (!article.empty()) { + s << article << ' '; + } + } + + s << name; + } + } else { + if (addArticle) { + s << "an "; + } + s << "item of type " << it.id; + } + return s.str(); +} + +std::string Item::getNameDescription() const +{ + const ItemType& it = items[id]; + return getNameDescription(it, this); +} + +std::string Item::getWeightDescription(const ItemType& it, uint32_t weight, uint32_t count /*= 1*/) +{ + std::ostringstream ss; + if (it.stackable && count > 1 && it.showCount != 0) { + ss << "They weigh "; + } else { + ss << "It weighs "; + } + + if (weight < 10) { + ss << "0.0" << weight; + } else if (weight < 100) { + ss << "0." << weight; + } else { + std::string weightString = std::to_string(weight); + weightString.insert(weightString.end() - 2, '.'); + ss << weightString; + } + + ss << " oz."; + return ss.str(); +} + +std::string Item::getWeightDescription(uint32_t weight) const +{ + const ItemType& it = Item::items[id]; + return getWeightDescription(it, weight, getItemCount()); +} + +std::string Item::getWeightDescription() const +{ + uint32_t weight = getWeight(); + if (weight == 0) { + return std::string(); + } + return getWeightDescription(weight); +} + +void Item::setUniqueId(uint16_t n) +{ + if (hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)) { + return; + } + + if (g_game.addUniqueItem(n, this)) { + getAttributes()->setUniqueId(n); + } +} + +bool Item::canDecay() const +{ + if (isRemoved()) { + return false; + } + + const ItemType& it = Item::items[id]; + if (getDecayTo() < 0 || it.decayTime == 0) { + return false; + } + + if (hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)) { + return false; + } + + return true; +} + +uint32_t Item::getWorth() const +{ + switch (id) { + case ITEM_GOLD_COIN: + return count; + + case ITEM_PLATINUM_COIN: + return count * 100; + + case ITEM_CRYSTAL_COIN: + return count * 10000; + + default: + return 0; + } +} + +LightInfo Item::getLightInfo() const +{ + const ItemType& it = items[id]; + return {it.lightLevel, it.lightColor}; +} + +std::string ItemAttributes::emptyString; +int64_t ItemAttributes::emptyInt; +double ItemAttributes::emptyDouble; +bool ItemAttributes::emptyBool; + +const std::string& ItemAttributes::getStrAttr(itemAttrTypes type) const +{ + if (!isStrAttrType(type)) { + return emptyString; + } + + const Attribute* attr = getExistingAttr(type); + if (!attr) { + return emptyString; + } + return *attr->value.string; +} + +void ItemAttributes::setStrAttr(itemAttrTypes type, const std::string& value) +{ + if (!isStrAttrType(type)) { + return; + } + + if (value.empty()) { + return; + } + + Attribute& attr = getAttr(type); + delete attr.value.string; + attr.value.string = new std::string(value); +} + +void ItemAttributes::removeAttribute(itemAttrTypes type) +{ + if (!hasAttribute(type)) { + return; + } + + auto prev_it = attributes.rbegin(); + if ((*prev_it).type == type) { + attributes.pop_back(); + } else { + auto it = prev_it, end = attributes.rend(); + while (++it != end) { + if ((*it).type == type) { + (*it) = attributes.back(); + attributes.pop_back(); + break; + } + prev_it = it; + } + } + attributeBits &= ~type; +} + +int64_t ItemAttributes::getIntAttr(itemAttrTypes type) const +{ + if (!isIntAttrType(type)) { + return 0; + } + + const Attribute* attr = getExistingAttr(type); + if (!attr) { + return 0; + } + return attr->value.integer; +} + +void ItemAttributes::setIntAttr(itemAttrTypes type, int64_t value) +{ + if (!isIntAttrType(type)) { + return; + } + + getAttr(type).value.integer = value; +} + +void ItemAttributes::increaseIntAttr(itemAttrTypes type, int64_t value) +{ + if (!isIntAttrType(type)) { + return; + } + + getAttr(type).value.integer += value; +} + +const ItemAttributes::Attribute* ItemAttributes::getExistingAttr(itemAttrTypes type) const +{ + if (hasAttribute(type)) { + for (const Attribute& attribute : attributes) { + if (attribute.type == type) { + return &attribute; + } + } + } + return nullptr; +} + +ItemAttributes::Attribute& ItemAttributes::getAttr(itemAttrTypes type) +{ + if (hasAttribute(type)) { + for (Attribute& attribute : attributes) { + if (attribute.type == type) { + return attribute; + } + } + } + + attributeBits |= type; + attributes.emplace_back(type); + return attributes.back(); +} + +void Item::startDecaying() +{ + g_game.startDecay(this); +} + +bool Item::hasMarketAttributes() const +{ + if (attributes == nullptr) { + return true; + } + + for (const auto& attr : attributes->getList()) { + if (attr.type == ITEM_ATTRIBUTE_CHARGES) { + uint16_t charges = static_cast(attr.value.integer); + if (charges != items[id].charges) { + return false; + } + } else if (attr.type == ITEM_ATTRIBUTE_DURATION) { + uint32_t duration = static_cast(attr.value.integer); + if (duration != getDefaultDuration()) { + return false; + } + } else { + return false; + } + } + return true; +} + +template<> +const std::string& ItemAttributes::CustomAttribute::get() { + if (value.type() == typeid(std::string)) { + return boost::get(value); + } + + return emptyString; +} + +template<> +const int64_t& ItemAttributes::CustomAttribute::get() { + if (value.type() == typeid(int64_t)) { + return boost::get(value); + } + + return emptyInt; +} + +template<> +const double& ItemAttributes::CustomAttribute::get() { + if (value.type() == typeid(double)) { + return boost::get(value); + } + + return emptyDouble; +} + +template<> +const bool& ItemAttributes::CustomAttribute::get() { + if (value.type() == typeid(bool)) { + return boost::get(value); + } + + return emptyBool; +} diff --git a/src/item.h b/src/item.h new file mode 100644 index 0000000..56e6525 --- /dev/null +++ b/src/item.h @@ -0,0 +1,1034 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_ITEM_H_009A319FB13D477D9EEFFBBD9BB83562 +#define FS_ITEM_H_009A319FB13D477D9EEFFBBD9BB83562 + +#include "cylinder.h" +#include "thing.h" +#include "items.h" +#include "luascript.h" +#include "tools.h" +#include + +#include +#include +#include + +class Creature; +class Player; +class Container; +class Depot; +class Teleport; +class TrashHolder; +class Mailbox; +class Door; +class MagicField; +class BedItem; + +enum ITEMPROPERTY { + CONST_PROP_BLOCKSOLID = 0, + CONST_PROP_HASHEIGHT, + CONST_PROP_BLOCKPROJECTILE, + CONST_PROP_BLOCKPATH, + CONST_PROP_ISVERTICAL, + CONST_PROP_ISHORIZONTAL, + CONST_PROP_MOVEABLE, + CONST_PROP_IMMOVABLEBLOCKSOLID, + CONST_PROP_IMMOVABLEBLOCKPATH, + CONST_PROP_IMMOVABLENOFIELDBLOCKPATH, + CONST_PROP_NOFIELDBLOCKPATH, + CONST_PROP_SUPPORTHANGABLE, +}; + +enum TradeEvents_t { + ON_TRADE_TRANSFER, + ON_TRADE_CANCEL, +}; + +enum ItemDecayState_t : uint8_t { + DECAYING_FALSE = 0, + DECAYING_TRUE, + DECAYING_PENDING, +}; + +enum AttrTypes_t { + //ATTR_DESCRIPTION = 1, + //ATTR_EXT_FILE = 2, + ATTR_TILE_FLAGS = 3, + ATTR_ACTION_ID = 4, + ATTR_UNIQUE_ID = 5, + ATTR_TEXT = 6, + ATTR_DESC = 7, + ATTR_TELE_DEST = 8, + ATTR_ITEM = 9, + ATTR_DEPOT_ID = 10, + //ATTR_EXT_SPAWN_FILE = 11, + ATTR_RUNE_CHARGES = 12, + //ATTR_EXT_HOUSE_FILE = 13, + ATTR_HOUSEDOORID = 14, + ATTR_COUNT = 15, + ATTR_DURATION = 16, + ATTR_DECAYING_STATE = 17, + ATTR_WRITTENDATE = 18, + ATTR_WRITTENBY = 19, + ATTR_SLEEPERGUID = 20, + ATTR_SLEEPSTART = 21, + ATTR_CHARGES = 22, + ATTR_CONTAINER_ITEMS = 23, + ATTR_NAME = 24, + ATTR_ARTICLE = 25, + ATTR_PLURALNAME = 26, + ATTR_WEIGHT = 27, + ATTR_ATTACK = 28, + ATTR_DEFENSE = 29, + ATTR_EXTRADEFENSE = 30, + ATTR_ARMOR = 31, + ATTR_HITCHANCE = 32, + ATTR_SHOOTRANGE = 33, + ATTR_CUSTOM_ATTRIBUTES = 34, + ATTR_DECAYTO = 35, + ATTR_WRAPID = 36 +}; + +enum Attr_ReadValue { + ATTR_READ_CONTINUE, + ATTR_READ_ERROR, + ATTR_READ_END, +}; + +class ItemAttributes +{ + public: + ItemAttributes() = default; + + void setSpecialDescription(const std::string& desc) { + setStrAttr(ITEM_ATTRIBUTE_DESCRIPTION, desc); + } + const std::string& getSpecialDescription() const { + return getStrAttr(ITEM_ATTRIBUTE_DESCRIPTION); + } + + void setText(const std::string& text) { + setStrAttr(ITEM_ATTRIBUTE_TEXT, text); + } + void resetText() { + removeAttribute(ITEM_ATTRIBUTE_TEXT); + } + const std::string& getText() const { + return getStrAttr(ITEM_ATTRIBUTE_TEXT); + } + + void setDate(int32_t n) { + setIntAttr(ITEM_ATTRIBUTE_DATE, n); + } + void resetDate() { + removeAttribute(ITEM_ATTRIBUTE_DATE); + } + time_t getDate() const { + return static_cast(getIntAttr(ITEM_ATTRIBUTE_DATE)); + } + + void setWriter(const std::string& writer) { + setStrAttr(ITEM_ATTRIBUTE_WRITER, writer); + } + void resetWriter() { + removeAttribute(ITEM_ATTRIBUTE_WRITER); + } + const std::string& getWriter() const { + return getStrAttr(ITEM_ATTRIBUTE_WRITER); + } + + void setActionId(uint16_t n) { + setIntAttr(ITEM_ATTRIBUTE_ACTIONID, n); + } + uint16_t getActionId() const { + return static_cast(getIntAttr(ITEM_ATTRIBUTE_ACTIONID)); + } + + void setUniqueId(uint16_t n) { + setIntAttr(ITEM_ATTRIBUTE_UNIQUEID, n); + } + uint16_t getUniqueId() const { + return static_cast(getIntAttr(ITEM_ATTRIBUTE_UNIQUEID)); + } + + void setCharges(uint16_t n) { + setIntAttr(ITEM_ATTRIBUTE_CHARGES, n); + } + uint16_t getCharges() const { + return static_cast(getIntAttr(ITEM_ATTRIBUTE_CHARGES)); + } + + void setFluidType(uint16_t n) { + setIntAttr(ITEM_ATTRIBUTE_FLUIDTYPE, n); + } + uint16_t getFluidType() const { + return static_cast(getIntAttr(ITEM_ATTRIBUTE_FLUIDTYPE)); + } + + void setOwner(uint32_t owner) { + setIntAttr(ITEM_ATTRIBUTE_OWNER, owner); + } + uint32_t getOwner() const { + return getIntAttr(ITEM_ATTRIBUTE_OWNER); + } + + void setCorpseOwner(uint32_t corpseOwner) { + setIntAttr(ITEM_ATTRIBUTE_CORPSEOWNER, corpseOwner); + } + uint32_t getCorpseOwner() const { + return getIntAttr(ITEM_ATTRIBUTE_CORPSEOWNER); + } + + void setDuration(int32_t time) { + setIntAttr(ITEM_ATTRIBUTE_DURATION, time); + } + void decreaseDuration(int32_t time) { + increaseIntAttr(ITEM_ATTRIBUTE_DURATION, -time); + } + uint32_t getDuration() const { + return getIntAttr(ITEM_ATTRIBUTE_DURATION); + } + + void setDecaying(ItemDecayState_t decayState) { + setIntAttr(ITEM_ATTRIBUTE_DECAYSTATE, decayState); + } + ItemDecayState_t getDecaying() const { + return static_cast(getIntAttr(ITEM_ATTRIBUTE_DECAYSTATE)); + } + + struct CustomAttribute + { + typedef boost::variant VariantAttribute; + VariantAttribute value; + + CustomAttribute() : value(boost::blank()) {} + + template + explicit CustomAttribute(const T& v) : value(v) {} + + template + void set(const T& v) { + value = v; + } + + template + const T& get(); + + struct PushLuaVisitor : public boost::static_visitor<> { + lua_State* L; + + explicit PushLuaVisitor(lua_State* L) : boost::static_visitor<>(), L(L) {} + + void operator()(const boost::blank&) const { + lua_pushnil(L); + } + + void operator()(const std::string& v) const { + LuaScriptInterface::pushString(L, v); + } + + void operator()(bool v) const { + LuaScriptInterface::pushBoolean(L, v); + } + + void operator()(const int64_t& v) const { + lua_pushnumber(L, v); + } + + void operator()(const double& v) const { + lua_pushnumber(L, v); + } + }; + + void pushToLua(lua_State* L) const { + boost::apply_visitor(PushLuaVisitor(L), value); + } + + struct SerializeVisitor : public boost::static_visitor<> { + PropWriteStream& propWriteStream; + + explicit SerializeVisitor(PropWriteStream& propWriteStream) : boost::static_visitor<>(), propWriteStream(propWriteStream) {} + + void operator()(const boost::blank&) const { + } + + void operator()(const std::string& v) const { + propWriteStream.writeString(v); + } + + template + void operator()(const T& v) const { + propWriteStream.write(v); + } + }; + + void serialize(PropWriteStream& propWriteStream) const { + propWriteStream.write(static_cast(value.which())); + boost::apply_visitor(SerializeVisitor(propWriteStream), value); + } + + bool unserialize(PropStream& propStream) { + // This is hard coded so it's not general, depends on the position of the variants. + uint8_t pos; + if (!propStream.read(pos)) { + return false; + } + + switch (pos) { + case 1: { // std::string + std::string tmp; + if (!propStream.readString(tmp)) { + return false; + } + value = tmp; + break; + } + + case 2: { // int64_t + int64_t tmp; + if (!propStream.read(tmp)) { + return false; + } + value = tmp; + break; + } + + case 3: { // double + double tmp; + if (!propStream.read(tmp)) { + return false; + } + value = tmp; + break; + } + + case 4: { // bool + bool tmp; + if (!propStream.read(tmp)) { + return false; + } + value = tmp; + break; + } + + default: { + value = boost::blank(); + return false; + } + } + return true; + } + }; + + private: + bool hasAttribute(itemAttrTypes type) const { + return (type & attributeBits) != 0; + } + void removeAttribute(itemAttrTypes type); + + static std::string emptyString; + static int64_t emptyInt; + static double emptyDouble; + static bool emptyBool; + + typedef std::unordered_map CustomAttributeMap; + + struct Attribute + { + union { + int64_t integer; + std::string* string; + CustomAttributeMap* custom; + } value; + itemAttrTypes type; + + explicit Attribute(itemAttrTypes type) : type(type) { + memset(&value, 0, sizeof(value)); + } + Attribute(const Attribute& i) { + type = i.type; + if (ItemAttributes::isIntAttrType(type)) { + value.integer = i.value.integer; + } else if (ItemAttributes::isStrAttrType(type)) { + value.string = new std::string(*i.value.string); + } else if (ItemAttributes::isCustomAttrType(type)) { + value.custom = new CustomAttributeMap(*i.value.custom); + } else { + memset(&value, 0, sizeof(value)); + } + } + Attribute(Attribute&& attribute) : value(attribute.value), type(attribute.type) { + memset(&attribute.value, 0, sizeof(value)); + attribute.type = ITEM_ATTRIBUTE_NONE; + } + ~Attribute() { + if (ItemAttributes::isStrAttrType(type)) { + delete value.string; + } else if (ItemAttributes::isCustomAttrType(type)) { + delete value.custom; + } + } + Attribute& operator=(Attribute other) { + Attribute::swap(*this, other); + return *this; + } + Attribute& operator=(Attribute&& other) { + if (this != &other) { + if (ItemAttributes::isStrAttrType(type)) { + delete value.string; + } else if (ItemAttributes::isCustomAttrType(type)) { + delete value.custom; + } + + value = other.value; + type = other.type; + + memset(&other.value, 0, sizeof(value)); + other.type = ITEM_ATTRIBUTE_NONE; + } + return *this; + } + + static void swap(Attribute& first, Attribute& second) { + std::swap(first.value, second.value); + std::swap(first.type, second.type); + } + }; + + std::vector attributes; + uint32_t attributeBits = 0; + + const std::string& getStrAttr(itemAttrTypes type) const; + void setStrAttr(itemAttrTypes type, const std::string& value); + + int64_t getIntAttr(itemAttrTypes type) const; + void setIntAttr(itemAttrTypes type, int64_t value); + void increaseIntAttr(itemAttrTypes type, int64_t value); + + const Attribute* getExistingAttr(itemAttrTypes type) const; + Attribute& getAttr(itemAttrTypes type); + + CustomAttributeMap* getCustomAttributeMap() { + if (!hasAttribute(ITEM_ATTRIBUTE_CUSTOM)) { + return nullptr; + } + + return getAttr(ITEM_ATTRIBUTE_CUSTOM).value.custom; + } + + template + void setCustomAttribute(int64_t key, R value) { + std::string tmp = boost::lexical_cast(key); + setCustomAttribute(tmp, value); + } + + void setCustomAttribute(int64_t key, CustomAttribute& value) { + std::string tmp = boost::lexical_cast(key); + setCustomAttribute(tmp, value); + } + + template + void setCustomAttribute(std::string& key, R value) { + toLowerCaseString(key); + if (hasAttribute(ITEM_ATTRIBUTE_CUSTOM)) { + removeCustomAttribute(key); + } else { + getAttr(ITEM_ATTRIBUTE_CUSTOM).value.custom = new CustomAttributeMap(); + } + getAttr(ITEM_ATTRIBUTE_CUSTOM).value.custom->emplace(key, value); + } + + void setCustomAttribute(std::string& key, CustomAttribute& value) { + toLowerCaseString(key); + if (hasAttribute(ITEM_ATTRIBUTE_CUSTOM)) { + removeCustomAttribute(key); + } else { + getAttr(ITEM_ATTRIBUTE_CUSTOM).value.custom = new CustomAttributeMap(); + } + getAttr(ITEM_ATTRIBUTE_CUSTOM).value.custom->insert(std::make_pair(std::move(key), std::move(value))); + } + + const CustomAttribute* getCustomAttribute(int64_t key) { + std::string tmp = boost::lexical_cast(key); + return getCustomAttribute(tmp); + } + + const CustomAttribute* getCustomAttribute(const std::string& key) { + if (const CustomAttributeMap* customAttrMap = getCustomAttributeMap()) { + auto it = customAttrMap->find(asLowerCaseString(key)); + if (it != customAttrMap->end()) { + return &(it->second); + } + } + return nullptr; + } + + bool removeCustomAttribute(int64_t key) { + std::string tmp = boost::lexical_cast(key); + return removeCustomAttribute(tmp); + } + + bool removeCustomAttribute(const std::string& key) { + if (CustomAttributeMap* customAttrMap = getCustomAttributeMap()) { + auto it = customAttrMap->find(asLowerCaseString(key)); + if (it != customAttrMap->end()) { + customAttrMap->erase(it); + return true; + } + } + return false; + } + + const static uint32_t intAttributeTypes = ITEM_ATTRIBUTE_ACTIONID | ITEM_ATTRIBUTE_UNIQUEID | ITEM_ATTRIBUTE_DATE + | ITEM_ATTRIBUTE_WEIGHT | ITEM_ATTRIBUTE_ATTACK | ITEM_ATTRIBUTE_DEFENSE | ITEM_ATTRIBUTE_EXTRADEFENSE + | ITEM_ATTRIBUTE_ARMOR | ITEM_ATTRIBUTE_HITCHANCE | ITEM_ATTRIBUTE_SHOOTRANGE | ITEM_ATTRIBUTE_OWNER + | ITEM_ATTRIBUTE_DURATION | ITEM_ATTRIBUTE_DECAYSTATE | ITEM_ATTRIBUTE_CORPSEOWNER | ITEM_ATTRIBUTE_CHARGES + | ITEM_ATTRIBUTE_FLUIDTYPE | ITEM_ATTRIBUTE_DOORID | ITEM_ATTRIBUTE_DECAYTO | ITEM_ATTRIBUTE_WRAPID; + const static uint32_t stringAttributeTypes = ITEM_ATTRIBUTE_DESCRIPTION | ITEM_ATTRIBUTE_TEXT | ITEM_ATTRIBUTE_WRITER + | ITEM_ATTRIBUTE_NAME | ITEM_ATTRIBUTE_ARTICLE | ITEM_ATTRIBUTE_PLURALNAME; + + public: + static bool isIntAttrType(itemAttrTypes type) { + return (type & intAttributeTypes) == type; + } + static bool isStrAttrType(itemAttrTypes type) { + return (type & stringAttributeTypes) == type; + } + inline static bool isCustomAttrType(itemAttrTypes type) { + return (type & ITEM_ATTRIBUTE_CUSTOM) == type; + } + + const std::vector& getList() const { + return attributes; + } + + friend class Item; +}; + +class Item : virtual public Thing +{ + public: + //Factory member to create item of right type based on type + static Item* CreateItem(const uint16_t type, uint16_t count = 0); + static Container* CreateItemAsContainer(const uint16_t type, uint16_t size); + static Item* CreateItem(PropStream& propStream); + static Items items; + + // Constructor for items + Item(const uint16_t type, uint16_t count = 0); + Item(const Item& i); + virtual Item* clone() const; + + virtual ~Item() = default; + + // non-assignable + Item& operator=(const Item&) = delete; + + bool equals(const Item* otherItem) const; + + Item* getItem() override final { + return this; + } + const Item* getItem() const override final { + return this; + } + virtual Teleport* getTeleport() { + return nullptr; + } + virtual const Teleport* getTeleport() const { + return nullptr; + } + virtual TrashHolder* getTrashHolder() { + return nullptr; + } + virtual const TrashHolder* getTrashHolder() const { + return nullptr; + } + virtual Mailbox* getMailbox() { + return nullptr; + } + virtual const Mailbox* getMailbox() const { + return nullptr; + } + virtual Door* getDoor() { + return nullptr; + } + virtual const Door* getDoor() const { + return nullptr; + } + virtual MagicField* getMagicField() { + return nullptr; + } + virtual const MagicField* getMagicField() const { + return nullptr; + } + virtual BedItem* getBed() { + return nullptr; + } + virtual const BedItem* getBed() const { + return nullptr; + } + + const std::string& getStrAttr(itemAttrTypes type) const { + if (!attributes) { + return ItemAttributes::emptyString; + } + return attributes->getStrAttr(type); + } + void setStrAttr(itemAttrTypes type, const std::string& value) { + getAttributes()->setStrAttr(type, value); + } + + int32_t getIntAttr(itemAttrTypes type) const { + if (!attributes) { + return 0; + } + return attributes->getIntAttr(type); + } + void setIntAttr(itemAttrTypes type, int32_t value) { + getAttributes()->setIntAttr(type, value); + } + void increaseIntAttr(itemAttrTypes type, int32_t value) { + getAttributes()->increaseIntAttr(type, value); + } + + void removeAttribute(itemAttrTypes type) { + if (attributes) { + attributes->removeAttribute(type); + } + } + bool hasAttribute(itemAttrTypes type) const { + if (!attributes) { + return false; + } + return attributes->hasAttribute(type); + } + + template + void setCustomAttribute(std::string& key, R value) { + getAttributes()->setCustomAttribute(key, value); + } + + void setCustomAttribute(std::string& key, ItemAttributes::CustomAttribute& value) { + getAttributes()->setCustomAttribute(key, value); + } + + const ItemAttributes::CustomAttribute* getCustomAttribute(int64_t key) { + return getAttributes()->getCustomAttribute(key); + } + + const ItemAttributes::CustomAttribute* getCustomAttribute(const std::string& key) { + return getAttributes()->getCustomAttribute(key); + } + + bool removeCustomAttribute(int64_t key) { + return getAttributes()->removeCustomAttribute(key); + } + + bool removeCustomAttribute(const std::string& key) { + return getAttributes()->removeCustomAttribute(key); + } + + void setSpecialDescription(const std::string& desc) { + setStrAttr(ITEM_ATTRIBUTE_DESCRIPTION, desc); + } + const std::string& getSpecialDescription() const { + return getStrAttr(ITEM_ATTRIBUTE_DESCRIPTION); + } + + void setText(const std::string& text) { + setStrAttr(ITEM_ATTRIBUTE_TEXT, text); + } + void resetText() { + removeAttribute(ITEM_ATTRIBUTE_TEXT); + } + const std::string& getText() const { + return getStrAttr(ITEM_ATTRIBUTE_TEXT); + } + + void setDate(int32_t n) { + setIntAttr(ITEM_ATTRIBUTE_DATE, n); + } + void resetDate() { + removeAttribute(ITEM_ATTRIBUTE_DATE); + } + time_t getDate() const { + return static_cast(getIntAttr(ITEM_ATTRIBUTE_DATE)); + } + + void setWriter(const std::string& writer) { + setStrAttr(ITEM_ATTRIBUTE_WRITER, writer); + } + void resetWriter() { + removeAttribute(ITEM_ATTRIBUTE_WRITER); + } + const std::string& getWriter() const { + return getStrAttr(ITEM_ATTRIBUTE_WRITER); + } + + void setActionId(uint16_t n) { + if (n < 100) { + n = 100; + } + + setIntAttr(ITEM_ATTRIBUTE_ACTIONID, n); + } + uint16_t getActionId() const { + if (!attributes) { + return 0; + } + return static_cast(getIntAttr(ITEM_ATTRIBUTE_ACTIONID)); + } + + uint16_t getUniqueId() const { + if (!attributes) { + return 0; + } + return static_cast(getIntAttr(ITEM_ATTRIBUTE_UNIQUEID)); + } + + void setCharges(uint16_t n) { + setIntAttr(ITEM_ATTRIBUTE_CHARGES, n); + } + uint16_t getCharges() const { + if (!attributes) { + return 0; + } + return static_cast(getIntAttr(ITEM_ATTRIBUTE_CHARGES)); + } + + void setFluidType(uint16_t n) { + setIntAttr(ITEM_ATTRIBUTE_FLUIDTYPE, n); + } + uint16_t getFluidType() const { + if (!attributes) { + return 0; + } + return static_cast(getIntAttr(ITEM_ATTRIBUTE_FLUIDTYPE)); + } + + void setOwner(uint32_t owner) { + setIntAttr(ITEM_ATTRIBUTE_OWNER, owner); + } + uint32_t getOwner() const { + if (!attributes) { + return 0; + } + return getIntAttr(ITEM_ATTRIBUTE_OWNER); + } + + void setCorpseOwner(uint32_t corpseOwner) { + setIntAttr(ITEM_ATTRIBUTE_CORPSEOWNER, corpseOwner); + } + uint32_t getCorpseOwner() const { + if (!attributes) { + return 0; + } + return getIntAttr(ITEM_ATTRIBUTE_CORPSEOWNER); + } + + void setDuration(int32_t time) { + setIntAttr(ITEM_ATTRIBUTE_DURATION, time); + } + void decreaseDuration(int32_t time) { + increaseIntAttr(ITEM_ATTRIBUTE_DURATION, -time); + } + uint32_t getDuration() const { + if (!attributes) { + return 0; + } + return getIntAttr(ITEM_ATTRIBUTE_DURATION); + } + + void setDecaying(ItemDecayState_t decayState) { + setIntAttr(ITEM_ATTRIBUTE_DECAYSTATE, decayState); + } + ItemDecayState_t getDecaying() const { + if (!attributes) { + return DECAYING_FALSE; + } + return static_cast(getIntAttr(ITEM_ATTRIBUTE_DECAYSTATE)); + } + + void setDecayTo(int32_t decayTo) { + setIntAttr(ITEM_ATTRIBUTE_DECAYTO, decayTo); + } + int32_t getDecayTo() const { + if (hasAttribute(ITEM_ATTRIBUTE_DECAYTO)) { + return getIntAttr(ITEM_ATTRIBUTE_DECAYTO); + } + return items[id].decayTo; + } + + static std::string getDescription(const ItemType& it, int32_t lookDistance, const Item* item = nullptr, int32_t subType = -1, bool addArticle = true); + static std::string getNameDescription(const ItemType& it, const Item* item = nullptr, int32_t subType = -1, bool addArticle = true); + static std::string getWeightDescription(const ItemType& it, uint32_t weight, uint32_t count = 1); + + std::string getDescription(int32_t lookDistance) const override final; + std::string getNameDescription() const; + std::string getWeightDescription() const; + + //serialization + virtual Attr_ReadValue readAttr(AttrTypes_t attr, PropStream& propStream); + bool unserializeAttr(PropStream& propStream); + virtual bool unserializeItemNode(OTB::Loader&, const OTB::Node&, PropStream& propStream); + + virtual void serializeAttr(PropWriteStream& propWriteStream) const; + + bool isPushable() const override final { + return isMoveable(); + } + int32_t getThrowRange() const override final { + return (isPickupable() ? 15 : 2); + } + + uint16_t getID() const { + return id; + } + uint16_t getClientID() const { + return items[id].clientId; + } + void setID(uint16_t newid); + + // Returns the player that is holding this item in his inventory + Player* getHoldingPlayer() const; + + WeaponType_t getWeaponType() const { + return items[id].weaponType; + } + Ammo_t getAmmoType() const { + return items[id].ammoType; + } + uint8_t getShootRange() const { + if (hasAttribute(ITEM_ATTRIBUTE_SHOOTRANGE)) { + return getIntAttr(ITEM_ATTRIBUTE_SHOOTRANGE); + } + return items[id].shootRange; + } + + virtual uint32_t getWeight() const; + uint32_t getBaseWeight() const { + if (hasAttribute(ITEM_ATTRIBUTE_WEIGHT)) { + return getIntAttr(ITEM_ATTRIBUTE_WEIGHT); + } + return items[id].weight; + } + int32_t getAttack() const { + if (hasAttribute(ITEM_ATTRIBUTE_ATTACK)) { + return getIntAttr(ITEM_ATTRIBUTE_ATTACK); + } + return items[id].attack; + } + int32_t getArmor() const { + if (hasAttribute(ITEM_ATTRIBUTE_ARMOR)) { + return getIntAttr(ITEM_ATTRIBUTE_ARMOR); + } + return items[id].armor; + } + int32_t getDefense() const { + if (hasAttribute(ITEM_ATTRIBUTE_DEFENSE)) { + return getIntAttr(ITEM_ATTRIBUTE_DEFENSE); + } + return items[id].defense; + } + int32_t getExtraDefense() const { + if (hasAttribute(ITEM_ATTRIBUTE_EXTRADEFENSE)) { + return getIntAttr(ITEM_ATTRIBUTE_EXTRADEFENSE); + } + return items[id].extraDefense; + } + int32_t getSlotPosition() const { + return items[id].slotPosition; + } + int8_t getHitChance() const { + if (hasAttribute(ITEM_ATTRIBUTE_HITCHANCE)) { + return getIntAttr(ITEM_ATTRIBUTE_HITCHANCE); + } + return items[id].hitChance; + } + + uint32_t getWorth() const; + LightInfo getLightInfo() const; + + bool hasProperty(ITEMPROPERTY prop) const; + bool isBlocking() const { + return items[id].blockSolid; + } + bool isStackable() const { + return items[id].stackable; + } + bool isAlwaysOnTop() const { + return items[id].alwaysOnTop; + } + bool isGroundTile() const { + return items[id].isGroundTile(); + } + bool isMagicField() const { + return items[id].isMagicField(); + } + bool isMoveable() const { + return items[id].moveable; + } + bool isPickupable() const { + return items[id].pickupable; + } + bool isUseable() const { + return items[id].useable; + } + bool isHangable() const { + return items[id].isHangable; + } + bool isRotatable() const { + const ItemType& it = items[id]; + return it.rotatable && it.rotateTo; + } + bool hasWalkStack() const { + return items[id].walkStack; + } + + const std::string& getName() const { + if (hasAttribute(ITEM_ATTRIBUTE_NAME)) { + return getStrAttr(ITEM_ATTRIBUTE_NAME); + } + return items[id].name; + } + const std::string getPluralName() const { + if (hasAttribute(ITEM_ATTRIBUTE_PLURALNAME)) { + return getStrAttr(ITEM_ATTRIBUTE_PLURALNAME); + } + return items[id].getPluralName(); + } + const std::string& getArticle() const { + if (hasAttribute(ITEM_ATTRIBUTE_ARTICLE)) { + return getStrAttr(ITEM_ATTRIBUTE_ARTICLE); + } + return items[id].article; + } + + // get the number of items + uint16_t getItemCount() const { + return count; + } + void setItemCount(uint8_t n) { + count = n; + } + + static uint32_t countByType(const Item* i, int32_t subType) { + if (subType == -1 || subType == i->getSubType()) { + return i->getItemCount(); + } + + return 0; + } + + void setDefaultSubtype(); + uint16_t getSubType() const; + void setSubType(uint16_t n); + + void setUniqueId(uint16_t n); + + void setDefaultDuration() { + uint32_t duration = getDefaultDuration(); + if (duration != 0) { + setDuration(duration); + } + } + uint32_t getDefaultDuration() const { + return items[id].decayTime * 1000; + } + bool canDecay() const; + + virtual bool canRemove() const { + return true; + } + virtual bool canTransform() const { + return true; + } + virtual void onRemoved(); + virtual void onTradeEvent(TradeEvents_t, Player*) {} + + virtual void startDecaying(); + + bool isLoadedFromMap() const { + return loadedFromMap; + } + void setLoadedFromMap(bool value) { + loadedFromMap = value; + } + bool isCleanable() const { + return !loadedFromMap && canRemove() && isPickupable() && !hasAttribute(ITEM_ATTRIBUTE_UNIQUEID) && !hasAttribute(ITEM_ATTRIBUTE_ACTIONID); + } + + bool hasMarketAttributes() const; + + std::unique_ptr& getAttributes() { + if (!attributes) { + attributes.reset(new ItemAttributes()); + } + return attributes; + } + + void incrementReferenceCounter() { + ++referenceCounter; + } + void decrementReferenceCounter() { + if (--referenceCounter == 0) { + delete this; + } + } + + Cylinder* getParent() const override { + return parent; + } + void setParent(Cylinder* cylinder) override { + parent = cylinder; + } + Cylinder* getTopParent(); + const Cylinder* getTopParent() const; + Tile* getTile() override; + const Tile* getTile() const override; + bool isRemoved() const override { + return !parent || parent->isRemoved(); + } + + protected: + Cylinder* parent = nullptr; + + uint16_t id; // the same id as in ItemType + + private: + std::string getWeightDescription(uint32_t weight) const; + + std::unique_ptr attributes; + + uint32_t referenceCounter = 0; + + uint8_t count = 1; // number of stacked items + + bool loadedFromMap = false; + + //Don't add variables here, use the ItemAttribute class. +}; + +using ItemList = std::list; +using ItemDeque = std::deque; + +#endif diff --git a/src/itemloader.h b/src/itemloader.h new file mode 100644 index 0000000..159ee2a --- /dev/null +++ b/src/itemloader.h @@ -0,0 +1,198 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_ITEMLOADER_H_107F1D3EECC94CD0A0F528843010D5D4 +#define FS_ITEMLOADER_H_107F1D3EECC94CD0A0F528843010D5D4 + +#include "fileloader.h" + +enum itemgroup_t { + ITEM_GROUP_NONE, + + ITEM_GROUP_GROUND, + ITEM_GROUP_CONTAINER, + ITEM_GROUP_WEAPON, //deprecated + ITEM_GROUP_AMMUNITION, //deprecated + ITEM_GROUP_ARMOR, //deprecated + ITEM_GROUP_CHARGES, + ITEM_GROUP_TELEPORT, //deprecated + ITEM_GROUP_MAGICFIELD, //deprecated + ITEM_GROUP_WRITEABLE, //deprecated + ITEM_GROUP_KEY, //deprecated + ITEM_GROUP_SPLASH, + ITEM_GROUP_FLUID, + ITEM_GROUP_DOOR, //deprecated + ITEM_GROUP_DEPRECATED, + + ITEM_GROUP_LAST +}; + +/////////OTB specific////////////// +enum clientVersion_t { + CLIENT_VERSION_750 = 1, + CLIENT_VERSION_755 = 2, + CLIENT_VERSION_760 = 3, + CLIENT_VERSION_770 = 3, + CLIENT_VERSION_780 = 4, + CLIENT_VERSION_790 = 5, + CLIENT_VERSION_792 = 6, + CLIENT_VERSION_800 = 7, + CLIENT_VERSION_810 = 8, + CLIENT_VERSION_811 = 9, + CLIENT_VERSION_820 = 10, + CLIENT_VERSION_830 = 11, + CLIENT_VERSION_840 = 12, + CLIENT_VERSION_841 = 13, + CLIENT_VERSION_842 = 14, + CLIENT_VERSION_850 = 15, + CLIENT_VERSION_854_BAD = 16, + CLIENT_VERSION_854 = 17, + CLIENT_VERSION_855 = 18, + CLIENT_VERSION_860_OLD = 19, + CLIENT_VERSION_860 = 20, + CLIENT_VERSION_861 = 21, + CLIENT_VERSION_862 = 22, + CLIENT_VERSION_870 = 23, + CLIENT_VERSION_871 = 24, + CLIENT_VERSION_872 = 25, + CLIENT_VERSION_873 = 26, + CLIENT_VERSION_900 = 27, + CLIENT_VERSION_910 = 28, + CLIENT_VERSION_920 = 29, + CLIENT_VERSION_940 = 30, + CLIENT_VERSION_944_V1 = 31, + CLIENT_VERSION_944_V2 = 32, + CLIENT_VERSION_944_V3 = 33, + CLIENT_VERSION_944_V4 = 34, + CLIENT_VERSION_946 = 35, + CLIENT_VERSION_950 = 36, + CLIENT_VERSION_952 = 37, + CLIENT_VERSION_953 = 38, + CLIENT_VERSION_954 = 39, + CLIENT_VERSION_960 = 40, + CLIENT_VERSION_961 = 41, + CLIENT_VERSION_963 = 42, + CLIENT_VERSION_970 = 43, + CLIENT_VERSION_980 = 44, + CLIENT_VERSION_981 = 45, + CLIENT_VERSION_982 = 46, + CLIENT_VERSION_983 = 47, + CLIENT_VERSION_985 = 48, + CLIENT_VERSION_986 = 49, + CLIENT_VERSION_1010 = 50, + CLIENT_VERSION_1020 = 51, + CLIENT_VERSION_1021 = 52, + CLIENT_VERSION_1030 = 53, + CLIENT_VERSION_1031 = 54, + CLIENT_VERSION_1035 = 55, + CLIENT_VERSION_1076 = 56, + CLIENT_VERSION_1098 = 57, +}; + +enum rootattrib_ { + ROOT_ATTR_VERSION = 0x01, +}; + +enum itemattrib_t { + ITEM_ATTR_FIRST = 0x10, + ITEM_ATTR_SERVERID = ITEM_ATTR_FIRST, + ITEM_ATTR_CLIENTID, + ITEM_ATTR_NAME, + ITEM_ATTR_DESCR, + ITEM_ATTR_SPEED, + ITEM_ATTR_SLOT, + ITEM_ATTR_MAXITEMS, + ITEM_ATTR_WEIGHT, + ITEM_ATTR_WEAPON, + ITEM_ATTR_AMU, + ITEM_ATTR_ARMOR, + ITEM_ATTR_MAGLEVEL, + ITEM_ATTR_MAGFIELDTYPE, + ITEM_ATTR_WRITEABLE, + ITEM_ATTR_ROTATETO, + ITEM_ATTR_DECAY, + ITEM_ATTR_SPRITEHASH, + ITEM_ATTR_MINIMAPCOLOR, + ITEM_ATTR_07, + ITEM_ATTR_08, + ITEM_ATTR_LIGHT, + + //1-byte aligned + ITEM_ATTR_DECAY2, //deprecated + ITEM_ATTR_WEAPON2, //deprecated + ITEM_ATTR_AMU2, //deprecated + ITEM_ATTR_ARMOR2, //deprecated + ITEM_ATTR_WRITEABLE2, //deprecated + ITEM_ATTR_LIGHT2, + ITEM_ATTR_TOPORDER, + ITEM_ATTR_WRITEABLE3, //deprecated + + ITEM_ATTR_WAREID, + + ITEM_ATTR_LAST +}; + +enum itemflags_t { + FLAG_BLOCK_SOLID = 1 << 0, + FLAG_BLOCK_PROJECTILE = 1 << 1, + FLAG_BLOCK_PATHFIND = 1 << 2, + FLAG_HAS_HEIGHT = 1 << 3, + FLAG_USEABLE = 1 << 4, + FLAG_PICKUPABLE = 1 << 5, + FLAG_MOVEABLE = 1 << 6, + FLAG_STACKABLE = 1 << 7, + FLAG_FLOORCHANGEDOWN = 1 << 8, // unused + FLAG_FLOORCHANGENORTH = 1 << 9, // unused + FLAG_FLOORCHANGEEAST = 1 << 10, // unused + FLAG_FLOORCHANGESOUTH = 1 << 11, // unused + FLAG_FLOORCHANGEWEST = 1 << 12, // unused + FLAG_ALWAYSONTOP = 1 << 13, + FLAG_READABLE = 1 << 14, + FLAG_ROTATABLE = 1 << 15, + FLAG_HANGABLE = 1 << 16, + FLAG_VERTICAL = 1 << 17, + FLAG_HORIZONTAL = 1 << 18, + FLAG_CANNOTDECAY = 1 << 19, // unused + FLAG_ALLOWDISTREAD = 1 << 20, + FLAG_UNUSED = 1 << 21, // unused + FLAG_CLIENTCHARGES = 1 << 22, /* deprecated */ + FLAG_LOOKTHROUGH = 1 << 23, + FLAG_ANIMATION = 1 << 24, + FLAG_FULLTILE = 1 << 25, // unused + FLAG_FORCEUSE = 1 << 26, +}; + +//1-byte aligned structs +#pragma pack(1) + +struct VERSIONINFO { + uint32_t dwMajorVersion; + uint32_t dwMinorVersion; + uint32_t dwBuildNumber; + uint8_t CSDVersion[128]; +}; + +struct lightBlock2 { + uint16_t lightLevel; + uint16_t lightColor; +}; + +#pragma pack() +/////////OTB specific////////////// +#endif diff --git a/src/items.cpp b/src/items.cpp new file mode 100644 index 0000000..ec0d39e --- /dev/null +++ b/src/items.cpp @@ -0,0 +1,1380 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "items.h" +#include "spells.h" +#include "movement.h" +#include "weapons.h" + +#include "pugicast.h" + +extern MoveEvents* g_moveEvents; +extern Weapons* g_weapons; + +const std::unordered_map ItemParseAttributesMap = { + {"type", ITEM_PARSE_TYPE}, + {"description", ITEM_PARSE_DESCRIPTION}, + {"runespellname", ITEM_PARSE_RUNESPELLNAME}, + {"weight", ITEM_PARSE_WEIGHT}, + {"showcount", ITEM_PARSE_SHOWCOUNT}, + {"armor", ITEM_PARSE_ARMOR}, + {"defense", ITEM_PARSE_DEFENSE}, + {"extradef", ITEM_PARSE_EXTRADEF}, + {"attack", ITEM_PARSE_ATTACK}, + {"rotateto", ITEM_PARSE_ROTATETO}, + {"moveable", ITEM_PARSE_MOVEABLE}, + {"movable", ITEM_PARSE_MOVEABLE}, + {"blockprojectile", ITEM_PARSE_BLOCKPROJECTILE}, + {"allowpickupable", ITEM_PARSE_PICKUPABLE}, + {"pickupable", ITEM_PARSE_PICKUPABLE}, + {"forceserialize", ITEM_PARSE_FORCESERIALIZE}, + {"forcesave", ITEM_PARSE_FORCESERIALIZE}, + {"floorchange", ITEM_PARSE_FLOORCHANGE}, + {"corpsetype", ITEM_PARSE_CORPSETYPE}, + {"containersize", ITEM_PARSE_CONTAINERSIZE}, + {"fluidsource", ITEM_PARSE_FLUIDSOURCE}, + {"readable", ITEM_PARSE_READABLE}, + {"writeable", ITEM_PARSE_WRITEABLE}, + {"maxtextlen", ITEM_PARSE_MAXTEXTLEN}, + {"writeonceitemid", ITEM_PARSE_WRITEONCEITEMID}, + {"weapontype", ITEM_PARSE_WEAPONTYPE}, + {"slottype", ITEM_PARSE_SLOTTYPE}, + {"ammotype", ITEM_PARSE_AMMOTYPE}, + {"shoottype", ITEM_PARSE_SHOOTTYPE}, + {"effect", ITEM_PARSE_EFFECT}, + {"range", ITEM_PARSE_RANGE}, + {"stopduration", ITEM_PARSE_STOPDURATION}, + {"decayto", ITEM_PARSE_DECAYTO}, + {"transformequipto", ITEM_PARSE_TRANSFORMEQUIPTO}, + {"transformdeequipto", ITEM_PARSE_TRANSFORMDEEQUIPTO}, + {"duration", ITEM_PARSE_DURATION}, + {"showduration", ITEM_PARSE_SHOWDURATION}, + {"charges", ITEM_PARSE_CHARGES}, + {"showcharges", ITEM_PARSE_SHOWCHARGES}, + {"showattributes", ITEM_PARSE_SHOWATTRIBUTES}, + {"hitchance", ITEM_PARSE_HITCHANCE}, + {"maxhitchance", ITEM_PARSE_MAXHITCHANCE}, + {"invisible", ITEM_PARSE_INVISIBLE}, + {"speed", ITEM_PARSE_SPEED}, + {"healthgain", ITEM_PARSE_HEALTHGAIN}, + {"healthticks", ITEM_PARSE_HEALTHTICKS}, + {"managain", ITEM_PARSE_MANAGAIN}, + {"manaticks", ITEM_PARSE_MANATICKS}, + {"manashield", ITEM_PARSE_MANASHIELD}, + {"skillsword", ITEM_PARSE_SKILLSWORD}, + {"skillaxe", ITEM_PARSE_SKILLAXE}, + {"skillclub", ITEM_PARSE_SKILLCLUB}, + {"skilldist", ITEM_PARSE_SKILLDIST}, + {"skillfish", ITEM_PARSE_SKILLFISH}, + {"skillshield", ITEM_PARSE_SKILLSHIELD}, + {"skillfist", ITEM_PARSE_SKILLFIST}, + {"maxhitpoints", ITEM_PARSE_MAXHITPOINTS}, + {"maxhitpointspercent", ITEM_PARSE_MAXHITPOINTSPERCENT}, + {"maxmanapoints", ITEM_PARSE_MAXMANAPOINTS}, + {"maxmanapointspercent", ITEM_PARSE_MAXMANAPOINTSPERCENT}, + {"magicpoints", ITEM_PARSE_MAGICPOINTS}, + {"magiclevelpoints", ITEM_PARSE_MAGICPOINTS}, + {"magicpointspercent", ITEM_PARSE_MAGICPOINTSPERCENT}, + {"criticalhitchance", ITEM_PARSE_CRITICALHITCHANCE}, + {"criticalhitamount", ITEM_PARSE_CRITICALHITAMOUNT}, + {"lifeleechchance", ITEM_PARSE_LIFELEECHCHANCE}, + {"lifeleechamount", ITEM_PARSE_LIFELEECHAMOUNT}, + {"manaleechchance", ITEM_PARSE_MANALEECHCHANCE}, + {"manaleechamount", ITEM_PARSE_MANALEECHAMOUNT}, + {"fieldabsorbpercentenergy", ITEM_PARSE_FIELDABSORBPERCENTENERGY}, + {"fieldabsorbpercentfire", ITEM_PARSE_FIELDABSORBPERCENTFIRE}, + {"fieldabsorbpercentpoison", ITEM_PARSE_FIELDABSORBPERCENTPOISON}, + {"fieldabsorbpercentearth", ITEM_PARSE_FIELDABSORBPERCENTPOISON}, + {"absorbpercentall", ITEM_PARSE_ABSORBPERCENTALL}, + {"absorbpercentallelements", ITEM_PARSE_ABSORBPERCENTALL}, + {"absorbpercentelements", ITEM_PARSE_ABSORBPERCENTELEMENTS}, + {"absorbpercentmagic", ITEM_PARSE_ABSORBPERCENTMAGIC}, + {"absorbpercentenergy", ITEM_PARSE_ABSORBPERCENTENERGY}, + {"absorbpercentfire", ITEM_PARSE_ABSORBPERCENTFIRE}, + {"absorbpercentpoison", ITEM_PARSE_ABSORBPERCENTPOISON}, + {"absorbpercentearth", ITEM_PARSE_ABSORBPERCENTPOISON}, + {"absorbpercentice", ITEM_PARSE_ABSORBPERCENTICE}, + {"absorbpercentholy", ITEM_PARSE_ABSORBPERCENTHOLY}, + {"absorbpercentdeath", ITEM_PARSE_ABSORBPERCENTDEATH}, + {"absorbpercentlifedrain", ITEM_PARSE_ABSORBPERCENTLIFEDRAIN}, + {"absorbpercentmanadrain", ITEM_PARSE_ABSORBPERCENTMANADRAIN}, + {"absorbpercentdrown", ITEM_PARSE_ABSORBPERCENTDROWN}, + {"absorbpercentphysical", ITEM_PARSE_ABSORBPERCENTPHYSICAL}, + {"absorbpercenthealing", ITEM_PARSE_ABSORBPERCENTHEALING}, + {"absorbpercentundefined", ITEM_PARSE_ABSORBPERCENTUNDEFINED}, + {"suppressdrunk", ITEM_PARSE_SUPPRESSDRUNK}, + {"suppressenergy", ITEM_PARSE_SUPPRESSENERGY}, + {"suppressfire", ITEM_PARSE_SUPPRESSFIRE}, + {"suppresspoison", ITEM_PARSE_SUPPRESSPOISON}, + {"suppressdrown", ITEM_PARSE_SUPPRESSDROWN}, + {"suppressphysical", ITEM_PARSE_SUPPRESSPHYSICAL}, + {"suppressfreeze", ITEM_PARSE_SUPPRESSFREEZE}, + {"suppressdazzle", ITEM_PARSE_SUPPRESSDAZZLE}, + {"suppresscurse", ITEM_PARSE_SUPPRESSCURSE}, + {"field", ITEM_PARSE_FIELD}, + {"replaceable", ITEM_PARSE_REPLACEABLE}, + {"partnerdirection", ITEM_PARSE_PARTNERDIRECTION}, + {"leveldoor", ITEM_PARSE_LEVELDOOR}, + {"maletransformto", ITEM_PARSE_MALETRANSFORMTO}, + {"malesleeper", ITEM_PARSE_MALETRANSFORMTO}, + {"femaletransformto", ITEM_PARSE_FEMALETRANSFORMTO}, + {"femalesleeper", ITEM_PARSE_FEMALETRANSFORMTO}, + {"transformto", ITEM_PARSE_TRANSFORMTO}, + {"destroyto", ITEM_PARSE_DESTROYTO}, + {"elementice", ITEM_PARSE_ELEMENTICE}, + {"elementearth", ITEM_PARSE_ELEMENTEARTH}, + {"elementfire", ITEM_PARSE_ELEMENTFIRE}, + {"elementenergy", ITEM_PARSE_ELEMENTENERGY}, + {"walkstack", ITEM_PARSE_WALKSTACK}, + {"blocking", ITEM_PARSE_BLOCKING}, + {"allowdistread", ITEM_PARSE_ALLOWDISTREAD}, +}; + +const std::unordered_map ItemTypesMap = { + {"key", ITEM_TYPE_KEY}, + {"magicfield", ITEM_TYPE_MAGICFIELD}, + {"container", ITEM_TYPE_CONTAINER}, + {"depot", ITEM_TYPE_DEPOT}, + {"mailbox", ITEM_TYPE_MAILBOX}, + {"trashholder", ITEM_TYPE_TRASHHOLDER}, + {"teleport", ITEM_TYPE_TELEPORT}, + {"door", ITEM_TYPE_DOOR}, + {"bed", ITEM_TYPE_BED}, + {"rune", ITEM_TYPE_RUNE}, +}; + +const std::unordered_map TileStatesMap = { + {"down", TILESTATE_FLOORCHANGE_DOWN}, + {"north", TILESTATE_FLOORCHANGE_NORTH}, + {"south", TILESTATE_FLOORCHANGE_SOUTH}, + {"southalt", TILESTATE_FLOORCHANGE_SOUTH_ALT}, + {"west", TILESTATE_FLOORCHANGE_WEST}, + {"east", TILESTATE_FLOORCHANGE_EAST}, + {"eastalt", TILESTATE_FLOORCHANGE_EAST_ALT}, +}; + +const std::unordered_map RaceTypesMap = { + {"venom", RACE_VENOM}, + {"blood", RACE_BLOOD}, + {"undead", RACE_UNDEAD}, + {"fire", RACE_FIRE}, + {"energy", RACE_ENERGY}, +}; + +const std::unordered_map WeaponTypesMap = { + {"sword", WEAPON_SWORD}, + {"club", WEAPON_CLUB}, + {"axe", WEAPON_AXE}, + {"shield", WEAPON_SHIELD}, + {"distance", WEAPON_DISTANCE}, + {"wand", WEAPON_WAND}, + {"ammunition", WEAPON_AMMO}, +}; + +const std::unordered_map FluidTypesMap = { + {"water", FLUID_WATER}, + {"blood", FLUID_BLOOD}, + {"beer", FLUID_BEER}, + {"slime", FLUID_SLIME}, + {"lemonade", FLUID_LEMONADE}, + {"milk", FLUID_MILK}, + {"mana", FLUID_MANA}, + {"life", FLUID_LIFE}, + {"oil", FLUID_OIL}, + {"urine", FLUID_URINE}, + {"coconut", FLUID_COCONUTMILK}, + {"wine", FLUID_WINE}, + {"mud", FLUID_MUD}, + {"fruitjuice", FLUID_FRUITJUICE}, + {"lava", FLUID_LAVA}, + {"rum", FLUID_RUM}, + {"swamp", FLUID_SWAMP}, + {"tea", FLUID_TEA}, + {"mead", FLUID_MEAD}, +}; + + +Items::Items() +{ + items.reserve(30000); + nameToItems.reserve(30000); +} + +void Items::clear() +{ + items.clear(); + clientIdToServerIdMap.clear(); + nameToItems.clear(); +} + +bool Items::reload() +{ + clear(); + loadFromOtb("data/items/items.otb"); + + if (!loadFromXml()) { + return false; + } + + g_moveEvents->reload(); + g_weapons->reload(); + g_weapons->loadDefaults(); + return true; +} + +constexpr auto OTBI = OTB::Identifier{{'O','T', 'B', 'I'}}; + +bool Items::loadFromOtb(const std::string& file) +{ + OTB::Loader loader{file, OTBI}; + + auto& root = loader.parseTree(); + + PropStream props; + if (loader.getProps(root, props)) { + //4 byte flags + //attributes + //0x01 = version data + uint32_t flags; + if (!props.read(flags)) { + return false; + } + + uint8_t attr; + if (!props.read(attr)) { + return false; + } + + if (attr == ROOT_ATTR_VERSION) { + uint16_t datalen; + if (!props.read(datalen)) { + return false; + } + + if (datalen != sizeof(VERSIONINFO)) { + return false; + } + + VERSIONINFO vi; + if (!props.read(vi)) { + return false; + } + + majorVersion = vi.dwMajorVersion; //items otb format file version + minorVersion = vi.dwMinorVersion; //client version + buildNumber = vi.dwBuildNumber; //revision + } + } + + if (majorVersion == 0xFFFFFFFF) { + std::cout << "[Warning - Items::loadFromOtb] items.otb using generic client version." << std::endl; + } else if (majorVersion != 3) { + std::cout << "Old version detected, a newer version of items.otb is required." << std::endl; + return false; + } else if (minorVersion < CLIENT_VERSION_1098) { + std::cout << "A newer version of items.otb is required." << std::endl; + return false; + } + + for (auto& itemNode : root.children) { + PropStream stream; + if (!loader.getProps(itemNode, stream)) { + return false; + } + + uint32_t flags; + if (!stream.read(flags)) { + return false; + } + + uint16_t serverId = 0; + uint16_t clientId = 0; + uint16_t speed = 0; + uint16_t wareId = 0; + uint8_t lightLevel = 0; + uint8_t lightColor = 0; + uint8_t alwaysOnTopOrder = 0; + + uint8_t attrib; + while (stream.read(attrib)) { + uint16_t datalen; + if (!stream.read(datalen)) { + return false; + } + + switch (attrib) { + case ITEM_ATTR_SERVERID: { + if (datalen != sizeof(uint16_t)) { + return false; + } + + if (!stream.read(serverId)) { + return false; + } + + if (serverId > 30000 && serverId < 30100) { + serverId -= 30000; + } + break; + } + + case ITEM_ATTR_CLIENTID: { + if (datalen != sizeof(uint16_t)) { + return false; + } + + if (!stream.read(clientId)) { + return false; + } + break; + } + + case ITEM_ATTR_SPEED: { + if (datalen != sizeof(uint16_t)) { + return false; + } + + if (!stream.read(speed)) { + return false; + } + break; + } + + case ITEM_ATTR_LIGHT2: { + if (datalen != sizeof(lightBlock2)) { + return false; + } + + lightBlock2 lb2; + if (!stream.read(lb2)) { + return false; + } + + lightLevel = static_cast(lb2.lightLevel); + lightColor = static_cast(lb2.lightColor); + break; + } + + case ITEM_ATTR_TOPORDER: { + if (datalen != sizeof(uint8_t)) { + return false; + } + + if (!stream.read(alwaysOnTopOrder)) { + return false; + } + break; + } + + case ITEM_ATTR_WAREID: { + if (datalen != sizeof(uint16_t)) { + return false; + } + + if (!stream.read(wareId)) { + return false; + } + break; + } + + default: { + //skip unknown attributes + if (!stream.skip(datalen)) { + return false; + } + break; + } + } + } + + clientIdToServerIdMap.emplace(clientId, serverId); + + // store the found item + if (serverId >= items.size()) { + items.resize(serverId + 1); + } + ItemType& iType = items[serverId]; + + iType.group = static_cast(itemNode.type); + switch (itemNode.type) { + case ITEM_GROUP_CONTAINER: + iType.type = ITEM_TYPE_CONTAINER; + break; + case ITEM_GROUP_DOOR: + //not used + iType.type = ITEM_TYPE_DOOR; + break; + case ITEM_GROUP_MAGICFIELD: + //not used + iType.type = ITEM_TYPE_MAGICFIELD; + break; + case ITEM_GROUP_TELEPORT: + //not used + iType.type = ITEM_TYPE_TELEPORT; + break; + case ITEM_GROUP_NONE: + case ITEM_GROUP_GROUND: + case ITEM_GROUP_SPLASH: + case ITEM_GROUP_FLUID: + case ITEM_GROUP_CHARGES: + case ITEM_GROUP_DEPRECATED: + break; + default: + return false; + } + + iType.blockSolid = hasBitSet(FLAG_BLOCK_SOLID, flags); + iType.blockProjectile = hasBitSet(FLAG_BLOCK_PROJECTILE, flags); + iType.blockPathFind = hasBitSet(FLAG_BLOCK_PATHFIND, flags); + iType.hasHeight = hasBitSet(FLAG_HAS_HEIGHT, flags); + iType.useable = hasBitSet(FLAG_USEABLE, flags); + iType.pickupable = hasBitSet(FLAG_PICKUPABLE, flags); + iType.moveable = hasBitSet(FLAG_MOVEABLE, flags); + iType.stackable = hasBitSet(FLAG_STACKABLE, flags); + + iType.alwaysOnTop = hasBitSet(FLAG_ALWAYSONTOP, flags); + iType.isVertical = hasBitSet(FLAG_VERTICAL, flags); + iType.isHorizontal = hasBitSet(FLAG_HORIZONTAL, flags); + iType.isHangable = hasBitSet(FLAG_HANGABLE, flags); + iType.allowDistRead = hasBitSet(FLAG_ALLOWDISTREAD, flags); + iType.rotatable = hasBitSet(FLAG_ROTATABLE, flags); + iType.canReadText = hasBitSet(FLAG_READABLE, flags); + iType.lookThrough = hasBitSet(FLAG_LOOKTHROUGH, flags); + iType.isAnimation = hasBitSet(FLAG_ANIMATION, flags); + // iType.walkStack = !hasBitSet(FLAG_FULLTILE, flags); + iType.forceUse = hasBitSet(FLAG_FORCEUSE, flags); + + iType.id = serverId; + iType.clientId = clientId; + iType.speed = speed; + iType.lightLevel = lightLevel; + iType.lightColor = lightColor; + iType.wareId = wareId; + iType.alwaysOnTopOrder = alwaysOnTopOrder; + } + + items.shrink_to_fit(); + return true; +} + +bool Items::loadFromXml() +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/items/items.xml"); + if (!result) { + printXMLError("Error - Items::loadFromXml", "data/items/items.xml", result); + return false; + } + + for (auto itemNode : doc.child("items").children()) { + pugi::xml_attribute idAttribute = itemNode.attribute("id"); + if (idAttribute) { + parseItemNode(itemNode, pugi::cast(idAttribute.value())); + continue; + } + + pugi::xml_attribute fromIdAttribute = itemNode.attribute("fromid"); + if (!fromIdAttribute) { + std::cout << "[Warning - Items::loadFromXml] No item id found" << std::endl; + continue; + } + + pugi::xml_attribute toIdAttribute = itemNode.attribute("toid"); + if (!toIdAttribute) { + std::cout << "[Warning - Items::loadFromXml] fromid (" << fromIdAttribute.value() << ") without toid" << std::endl; + continue; + } + + uint16_t id = pugi::cast(fromIdAttribute.value()); + uint16_t toId = pugi::cast(toIdAttribute.value()); + while (id <= toId) { + parseItemNode(itemNode, id++); + } + } + + buildInventoryList(); + return true; +} + +void Items::buildInventoryList() +{ + inventory.reserve(items.size()); + for (const auto& type: items) { + if (type.weaponType != WEAPON_NONE || type.ammoType != AMMO_NONE || + type.attack != 0 || type.defense != 0 || + type.extraDefense != 0 || type.armor != 0 || + type.slotPosition & SLOTP_NECKLACE || + type.slotPosition & SLOTP_RING || + type.slotPosition & SLOTP_AMMO || + type.slotPosition & SLOTP_FEET || + type.slotPosition & SLOTP_HEAD || + type.slotPosition & SLOTP_ARMOR || + type.slotPosition & SLOTP_LEGS) + { + inventory.push_back(type.clientId); + } + } + inventory.shrink_to_fit(); + std::sort(inventory.begin(), inventory.end()); +} + +void Items::parseItemNode(const pugi::xml_node& itemNode, uint16_t id) +{ + if (id > 30000 && id < 30100) { + id -= 30000; + + if (id >= items.size()) { + items.resize(id + 1); + } + ItemType& iType = items[id]; + iType.id = id; + } + + ItemType& it = getItemType(id); + if (it.id == 0) { + return; + } + + it.name = itemNode.attribute("name").as_string(); + + nameToItems.insert({ asLowerCaseString(it.name), id }); + + pugi::xml_attribute articleAttribute = itemNode.attribute("article"); + if (articleAttribute) { + it.article = articleAttribute.as_string(); + } + + pugi::xml_attribute pluralAttribute = itemNode.attribute("plural"); + if (pluralAttribute) { + it.pluralName = pluralAttribute.as_string(); + } + + Abilities& abilities = it.getAbilities(); + + for (auto attributeNode : itemNode.children()) { + pugi::xml_attribute keyAttribute = attributeNode.attribute("key"); + if (!keyAttribute) { + continue; + } + + pugi::xml_attribute valueAttribute = attributeNode.attribute("value"); + if (!valueAttribute) { + continue; + } + + std::string tmpStrValue = asLowerCaseString(keyAttribute.as_string()); + auto parseAttribute = ItemParseAttributesMap.find(tmpStrValue); + if (parseAttribute != ItemParseAttributesMap.end()) { + ItemParseAttributes_t parseType = parseAttribute->second; + switch (parseType) { + case ITEM_PARSE_TYPE: { + tmpStrValue = asLowerCaseString(valueAttribute.as_string()); + auto it2 = ItemTypesMap.find(tmpStrValue); + if (it2 != ItemTypesMap.end()) { + it.type = it2->second; + if (it.type == ITEM_TYPE_CONTAINER) { + it.group = ITEM_GROUP_CONTAINER; + } + } else { + std::cout << "[Warning - Items::parseItemNode] Unknown type: " << valueAttribute.as_string() << std::endl; + } + break; + } + + case ITEM_PARSE_DESCRIPTION: { + it.description = valueAttribute.as_string(); + break; + } + + case ITEM_PARSE_RUNESPELLNAME: { + it.runeSpellName = valueAttribute.as_string(); + break; + } + + case ITEM_PARSE_WEIGHT: { + it.weight = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_SHOWCOUNT: { + it.showCount = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_ARMOR: { + it.armor = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_DEFENSE: { + it.defense = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_EXTRADEF: { + it.extraDefense = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ATTACK: { + it.attack = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ROTATETO: { + it.rotateTo = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MOVEABLE: { + it.moveable = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_BLOCKPROJECTILE: { + it.blockProjectile = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_PICKUPABLE: { + it.allowPickupable = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_FORCESERIALIZE: { + it.forceSerialize = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_FLOORCHANGE: { + tmpStrValue = asLowerCaseString(valueAttribute.as_string()); + auto it2 = TileStatesMap.find(tmpStrValue); + if (it2 != TileStatesMap.end()) { + it.floorChange |= it2->second; + } else { + std::cout << "[Warning - Items::parseItemNode] Unknown floorChange: " << valueAttribute.as_string() << std::endl; + } + break; + } + + case ITEM_PARSE_CORPSETYPE: { + tmpStrValue = asLowerCaseString(valueAttribute.as_string()); + auto it2 = RaceTypesMap.find(tmpStrValue); + if (it2 != RaceTypesMap.end()) { + it.corpseType = it2->second; + } else { + std::cout << "[Warning - Items::parseItemNode] Unknown corpseType: " << valueAttribute.as_string() << std::endl; + } + break; + } + + case ITEM_PARSE_CONTAINERSIZE: { + it.maxItems = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_FLUIDSOURCE: { + tmpStrValue = asLowerCaseString(valueAttribute.as_string()); + auto it2 = FluidTypesMap.find(tmpStrValue); + if (it2 != FluidTypesMap.end()) { + it.fluidSource = it2->second; + } else { + std::cout << "[Warning - Items::parseItemNode] Unknown fluidSource: " << valueAttribute.as_string() << std::endl; + } + break; + } + + case ITEM_PARSE_READABLE: { + it.canReadText = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_WRITEABLE: { + it.canWriteText = valueAttribute.as_bool(); + it.canReadText = it.canWriteText; + break; + } + + case ITEM_PARSE_MAXTEXTLEN: { + it.maxTextLen = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_WRITEONCEITEMID: { + it.writeOnceItemId = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_WEAPONTYPE: { + tmpStrValue = asLowerCaseString(valueAttribute.as_string()); + auto it2 = WeaponTypesMap.find(tmpStrValue); + if (it2 != WeaponTypesMap.end()) { + it.weaponType = it2->second; + } else { + std::cout << "[Warning - Items::parseItemNode] Unknown weaponType: " << valueAttribute.as_string() << std::endl; + } + break; + } + + case ITEM_PARSE_SLOTTYPE: { + tmpStrValue = asLowerCaseString(valueAttribute.as_string()); + if (tmpStrValue == "head") { + it.slotPosition |= SLOTP_HEAD; + } else if (tmpStrValue == "body") { + it.slotPosition |= SLOTP_ARMOR; + } else if (tmpStrValue == "legs") { + it.slotPosition |= SLOTP_LEGS; + } else if (tmpStrValue == "feet") { + it.slotPosition |= SLOTP_FEET; + } else if (tmpStrValue == "backpack") { + it.slotPosition |= SLOTP_BACKPACK; + } else if (tmpStrValue == "two-handed") { + it.slotPosition |= SLOTP_TWO_HAND; + } else if (tmpStrValue == "right-hand") { + it.slotPosition &= ~SLOTP_LEFT; + } else if (tmpStrValue == "left-hand") { + it.slotPosition &= ~SLOTP_RIGHT; + } else if (tmpStrValue == "necklace") { + it.slotPosition |= SLOTP_NECKLACE; + } else if (tmpStrValue == "ring") { + it.slotPosition |= SLOTP_RING; + } else if (tmpStrValue == "ammo") { + it.slotPosition |= SLOTP_AMMO; + } else if (tmpStrValue == "hand") { + it.slotPosition |= SLOTP_HAND; + } else { + std::cout << "[Warning - Items::parseItemNode] Unknown slotType: " << valueAttribute.as_string() << std::endl; + } + break; + } + + case ITEM_PARSE_AMMOTYPE: { + it.ammoType = getAmmoType(asLowerCaseString(valueAttribute.as_string())); + if (it.ammoType == AMMO_NONE) { + std::cout << "[Warning - Items::parseItemNode] Unknown ammoType: " << valueAttribute.as_string() << std::endl; + } + break; + } + + case ITEM_PARSE_SHOOTTYPE: { + ShootType_t shoot = getShootType(asLowerCaseString(valueAttribute.as_string())); + if (shoot != CONST_ANI_NONE) { + it.shootType = shoot; + } else { + std::cout << "[Warning - Items::parseItemNode] Unknown shootType: " << valueAttribute.as_string() << std::endl; + } + break; + } + + case ITEM_PARSE_EFFECT: { + MagicEffectClasses effect = getMagicEffect(asLowerCaseString(valueAttribute.as_string())); + if (effect != CONST_ME_NONE) { + it.magicEffect = effect; + } else { + std::cout << "[Warning - Items::parseItemNode] Unknown effect: " << valueAttribute.as_string() << std::endl; + } + break; + } + + case ITEM_PARSE_RANGE: { + it.shootRange = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_STOPDURATION: { + it.stopTime = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_DECAYTO: { + it.decayTo = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_TRANSFORMEQUIPTO: { + it.transformEquipTo = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_TRANSFORMDEEQUIPTO: { + it.transformDeEquipTo = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_DURATION: { + it.decayTime = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_SHOWDURATION: { + it.showDuration = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_CHARGES: { + it.charges = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_SHOWCHARGES: { + it.showCharges = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_SHOWATTRIBUTES: { + it.showAttributes = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_HITCHANCE: { + it.hitChance = std::min(100, std::max(-100, pugi::cast(valueAttribute.value()))); + break; + } + + case ITEM_PARSE_MAXHITCHANCE: { + it.maxHitChance = std::min(100, pugi::cast(valueAttribute.value())); + break; + } + + case ITEM_PARSE_INVISIBLE: { + abilities.invisible = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_SPEED: { + abilities.speed = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_HEALTHGAIN: { + abilities.regeneration = true; + abilities.healthGain = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_HEALTHTICKS: { + abilities.regeneration = true; + abilities.healthTicks = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MANAGAIN: { + abilities.regeneration = true; + abilities.manaGain = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MANATICKS: { + abilities.regeneration = true; + abilities.manaTicks = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MANASHIELD: { + abilities.manaShield = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_SKILLSWORD: { + abilities.skills[SKILL_SWORD] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_SKILLAXE: { + abilities.skills[SKILL_AXE] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_SKILLCLUB: { + abilities.skills[SKILL_CLUB] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_SKILLDIST: { + abilities.skills[SKILL_DISTANCE] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_SKILLFISH: { + abilities.skills[SKILL_FISHING] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_SKILLSHIELD: { + abilities.skills[SKILL_SHIELD] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_SKILLFIST: { + abilities.skills[SKILL_FIST] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_CRITICALHITAMOUNT: { + abilities.specialSkills[SPECIALSKILL_CRITICALHITAMOUNT] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_CRITICALHITCHANCE: { + abilities.specialSkills[SPECIALSKILL_CRITICALHITCHANCE] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MANALEECHAMOUNT: { + abilities.specialSkills[SPECIALSKILL_MANALEECHAMOUNT] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MANALEECHCHANCE: { + abilities.specialSkills[SPECIALSKILL_MANALEECHCHANCE] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_LIFELEECHAMOUNT: { + abilities.specialSkills[SPECIALSKILL_LIFELEECHAMOUNT] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_LIFELEECHCHANCE: { + abilities.specialSkills[SPECIALSKILL_LIFELEECHCHANCE] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MAXHITPOINTS: { + abilities.stats[STAT_MAXHITPOINTS] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MAXHITPOINTSPERCENT: { + abilities.statsPercent[STAT_MAXHITPOINTS] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MAXMANAPOINTS: { + abilities.stats[STAT_MAXMANAPOINTS] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MAXMANAPOINTSPERCENT: { + abilities.statsPercent[STAT_MAXMANAPOINTS] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MAGICPOINTS: { + abilities.stats[STAT_MAGICPOINTS] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MAGICPOINTSPERCENT: { + abilities.statsPercent[STAT_MAGICPOINTS] = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_FIELDABSORBPERCENTENERGY: { + abilities.fieldAbsorbPercent[combatTypeToIndex(COMBAT_ENERGYDAMAGE)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_FIELDABSORBPERCENTFIRE: { + abilities.fieldAbsorbPercent[combatTypeToIndex(COMBAT_FIREDAMAGE)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_FIELDABSORBPERCENTPOISON: { + abilities.fieldAbsorbPercent[combatTypeToIndex(COMBAT_EARTHDAMAGE)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ABSORBPERCENTALL: { + int16_t value = pugi::cast(valueAttribute.value()); + for (auto& i : abilities.absorbPercent) { + i += value; + } + break; + } + + case ITEM_PARSE_ABSORBPERCENTELEMENTS: { + int16_t value = pugi::cast(valueAttribute.value()); + abilities.absorbPercent[combatTypeToIndex(COMBAT_ENERGYDAMAGE)] += value; + abilities.absorbPercent[combatTypeToIndex(COMBAT_FIREDAMAGE)] += value; + abilities.absorbPercent[combatTypeToIndex(COMBAT_EARTHDAMAGE)] += value; + abilities.absorbPercent[combatTypeToIndex(COMBAT_ICEDAMAGE)] += value; + break; + } + + case ITEM_PARSE_ABSORBPERCENTMAGIC: { + int16_t value = pugi::cast(valueAttribute.value()); + abilities.absorbPercent[combatTypeToIndex(COMBAT_ENERGYDAMAGE)] += value; + abilities.absorbPercent[combatTypeToIndex(COMBAT_FIREDAMAGE)] += value; + abilities.absorbPercent[combatTypeToIndex(COMBAT_EARTHDAMAGE)] += value; + abilities.absorbPercent[combatTypeToIndex(COMBAT_ICEDAMAGE)] += value; + abilities.absorbPercent[combatTypeToIndex(COMBAT_HOLYDAMAGE)] += value; + abilities.absorbPercent[combatTypeToIndex(COMBAT_DEATHDAMAGE)] += value; + break; + } + + case ITEM_PARSE_ABSORBPERCENTENERGY: { + abilities.absorbPercent[combatTypeToIndex(COMBAT_ENERGYDAMAGE)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ABSORBPERCENTFIRE: { + abilities.absorbPercent[combatTypeToIndex(COMBAT_FIREDAMAGE)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ABSORBPERCENTPOISON: { + abilities.absorbPercent[combatTypeToIndex(COMBAT_EARTHDAMAGE)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ABSORBPERCENTICE: { + abilities.absorbPercent[combatTypeToIndex(COMBAT_ICEDAMAGE)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ABSORBPERCENTHOLY: { + abilities.absorbPercent[combatTypeToIndex(COMBAT_HOLYDAMAGE)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ABSORBPERCENTDEATH: { + abilities.absorbPercent[combatTypeToIndex(COMBAT_DEATHDAMAGE)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ABSORBPERCENTLIFEDRAIN: { + abilities.absorbPercent[combatTypeToIndex(COMBAT_LIFEDRAIN)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ABSORBPERCENTMANADRAIN: { + abilities.absorbPercent[combatTypeToIndex(COMBAT_MANADRAIN)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ABSORBPERCENTDROWN: { + abilities.absorbPercent[combatTypeToIndex(COMBAT_DROWNDAMAGE)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ABSORBPERCENTPHYSICAL: { + abilities.absorbPercent[combatTypeToIndex(COMBAT_PHYSICALDAMAGE)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ABSORBPERCENTHEALING: { + abilities.absorbPercent[combatTypeToIndex(COMBAT_HEALING)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ABSORBPERCENTUNDEFINED: { + abilities.absorbPercent[combatTypeToIndex(COMBAT_UNDEFINEDDAMAGE)] += pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_SUPPRESSDRUNK: { + if (valueAttribute.as_bool()) { + abilities.conditionSuppressions |= CONDITION_DRUNK; + } + break; + } + + case ITEM_PARSE_SUPPRESSENERGY: { + if (valueAttribute.as_bool()) { + abilities.conditionSuppressions |= CONDITION_ENERGY; + } + break; + } + + case ITEM_PARSE_SUPPRESSFIRE: { + if (valueAttribute.as_bool()) { + abilities.conditionSuppressions |= CONDITION_FIRE; + } + break; + } + + case ITEM_PARSE_SUPPRESSPOISON: { + if (valueAttribute.as_bool()) { + abilities.conditionSuppressions |= CONDITION_POISON; + } + break; + } + + case ITEM_PARSE_SUPPRESSDROWN: { + if (valueAttribute.as_bool()) { + abilities.conditionSuppressions |= CONDITION_DROWN; + } + break; + } + + case ITEM_PARSE_SUPPRESSPHYSICAL: { + if (valueAttribute.as_bool()) { + abilities.conditionSuppressions |= CONDITION_BLEEDING; + } + break; + } + + case ITEM_PARSE_SUPPRESSFREEZE: { + if (valueAttribute.as_bool()) { + abilities.conditionSuppressions |= CONDITION_FREEZING; + } + break; + } + + case ITEM_PARSE_SUPPRESSDAZZLE: { + if (valueAttribute.as_bool()) { + abilities.conditionSuppressions |= CONDITION_DAZZLED; + } + break; + } + + case ITEM_PARSE_SUPPRESSCURSE: { + if (valueAttribute.as_bool()) { + abilities.conditionSuppressions |= CONDITION_CURSED; + } + break; + } + + case ITEM_PARSE_FIELD: { + it.group = ITEM_GROUP_MAGICFIELD; + it.type = ITEM_TYPE_MAGICFIELD; + + CombatType_t combatType = COMBAT_NONE; + ConditionDamage* conditionDamage = nullptr; + + tmpStrValue = asLowerCaseString(valueAttribute.as_string()); + if (tmpStrValue == "fire") { + conditionDamage = new ConditionDamage(CONDITIONID_COMBAT, CONDITION_FIRE); + combatType = COMBAT_FIREDAMAGE; + } else if (tmpStrValue == "energy") { + conditionDamage = new ConditionDamage(CONDITIONID_COMBAT, CONDITION_ENERGY); + combatType = COMBAT_ENERGYDAMAGE; + } else if (tmpStrValue == "poison") { + conditionDamage = new ConditionDamage(CONDITIONID_COMBAT, CONDITION_POISON); + combatType = COMBAT_EARTHDAMAGE; + } else if (tmpStrValue == "drown") { + conditionDamage = new ConditionDamage(CONDITIONID_COMBAT, CONDITION_DROWN); + combatType = COMBAT_DROWNDAMAGE; + } else if (tmpStrValue == "physical") { + conditionDamage = new ConditionDamage(CONDITIONID_COMBAT, CONDITION_BLEEDING); + combatType = COMBAT_PHYSICALDAMAGE; + } else { + std::cout << "[Warning - Items::parseItemNode] Unknown field value: " << valueAttribute.as_string() << std::endl; + } + + if (combatType != COMBAT_NONE) { + it.combatType = combatType; + it.conditionDamage.reset(conditionDamage); + + uint32_t ticks = 0; + int32_t start = 0; + int32_t count = 1; + for (auto subAttributeNode : attributeNode.children()) { + pugi::xml_attribute subKeyAttribute = subAttributeNode.attribute("key"); + if (!subKeyAttribute) { + continue; + } + + pugi::xml_attribute subValueAttribute = subAttributeNode.attribute("value"); + if (!subValueAttribute) { + continue; + } + + tmpStrValue = asLowerCaseString(subKeyAttribute.as_string()); + if (tmpStrValue == "initdamage") { + conditionDamage->setInitDamage(-pugi::cast(subValueAttribute.value())); + } else if (tmpStrValue == "ticks") { + ticks = pugi::cast(subValueAttribute.value()); + } else if (tmpStrValue == "count") { + count = std::max(1, pugi::cast(subValueAttribute.value())); + } else if (tmpStrValue == "start") { + start = std::max(0, pugi::cast(subValueAttribute.value())); + } else if (tmpStrValue == "damage") { + int32_t damage = -pugi::cast(subValueAttribute.value()); + if (start > 0) { + std::list damageList; + ConditionDamage::generateDamageList(damage, start, damageList); + for (int32_t damageValue : damageList) { + conditionDamage->addDamage(1, ticks, -damageValue); + } + + start = 0; + } else { + conditionDamage->addDamage(count, ticks, damage); + } + } + } + + conditionDamage->setParam(CONDITION_PARAM_FIELD, 1); + + if (conditionDamage->getTotalDamage() > 0) { + conditionDamage->setParam(CONDITION_PARAM_FORCEUPDATE, 1); + } + } + break; + } + + case ITEM_PARSE_REPLACEABLE: { + it.replaceable = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_PARTNERDIRECTION: { + it.bedPartnerDir = getDirection(valueAttribute.as_string()); + break; + } + + case ITEM_PARSE_LEVELDOOR: { + it.levelDoor = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_MALETRANSFORMTO: { + uint16_t value = pugi::cast(valueAttribute.value()); + it.transformToOnUse[PLAYERSEX_MALE] = value; + ItemType& other = getItemType(value); + if (other.transformToFree == 0) { + other.transformToFree = it.id; + } + + if (it.transformToOnUse[PLAYERSEX_FEMALE] == 0) { + it.transformToOnUse[PLAYERSEX_FEMALE] = value; + } + break; + } + + case ITEM_PARSE_FEMALETRANSFORMTO: { + uint16_t value = pugi::cast(valueAttribute.value()); + it.transformToOnUse[PLAYERSEX_FEMALE] = value; + + ItemType& other = getItemType(value); + if (other.transformToFree == 0) { + other.transformToFree = it.id; + } + + if (it.transformToOnUse[PLAYERSEX_MALE] == 0) { + it.transformToOnUse[PLAYERSEX_MALE] = value; + } + break; + } + + case ITEM_PARSE_TRANSFORMTO: { + it.transformToFree = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_DESTROYTO: { + it.destroyTo = pugi::cast(valueAttribute.value()); + break; + } + + case ITEM_PARSE_ELEMENTICE: { + abilities.elementDamage = pugi::cast(valueAttribute.value()); + abilities.elementType = COMBAT_ICEDAMAGE; + break; + } + + case ITEM_PARSE_ELEMENTEARTH: { + abilities.elementDamage = pugi::cast(valueAttribute.value()); + abilities.elementType = COMBAT_EARTHDAMAGE; + break; + } + + case ITEM_PARSE_ELEMENTFIRE: { + abilities.elementDamage = pugi::cast(valueAttribute.value()); + abilities.elementType = COMBAT_FIREDAMAGE; + break; + } + + case ITEM_PARSE_ELEMENTENERGY: { + abilities.elementDamage = pugi::cast(valueAttribute.value()); + abilities.elementType = COMBAT_ENERGYDAMAGE; + break; + } + + case ITEM_PARSE_WALKSTACK: { + it.walkStack = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_BLOCKING: { + it.blockSolid = valueAttribute.as_bool(); + break; + } + + case ITEM_PARSE_ALLOWDISTREAD: { + it.allowDistRead = booleanString(valueAttribute.as_string()); + break; + } + + default: { + // It should not ever get to here, only if you add a new key to the map and don't configure a case for it. + std::cout << "[Warning - Items::parseItemNode] Not configured key value: " << keyAttribute.as_string() << std::endl; + break; + } + } + } else { + std::cout << "[Warning - Items::parseItemNode] Unknown key value: " << keyAttribute.as_string() << std::endl; + } + } + + //check bed items + if ((it.transformToFree != 0 || it.transformToOnUse[PLAYERSEX_FEMALE] != 0 || it.transformToOnUse[PLAYERSEX_MALE] != 0) && it.type != ITEM_TYPE_BED) { + std::cout << "[Warning - Items::parseItemNode] Item " << it.id << " is not set as a bed-type" << std::endl; + } +} + +ItemType& Items::getItemType(size_t id) +{ + if (id < items.size()) { + return items[id]; + } + return items.front(); +} + +const ItemType& Items::getItemType(size_t id) const +{ + if (id < items.size()) { + return items[id]; + } + return items.front(); +} + +const ItemType& Items::getItemIdByClientId(uint16_t spriteId) const +{ + if (spriteId >= 100) { + if (uint16_t serverId = clientIdToServerIdMap.getServerId(spriteId)) { + return getItemType(serverId); + } + } + return items.front(); +} + +uint16_t Items::getItemIdByName(const std::string& name) +{ + auto result = nameToItems.find(asLowerCaseString(name)); + + if (result == nameToItems.end()) + return 0; + + return result->second; +} diff --git a/src/items.h b/src/items.h new file mode 100644 index 0000000..3e7b08c --- /dev/null +++ b/src/items.h @@ -0,0 +1,461 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_ITEMS_H_4E2221634ABA45FE85BA50F710669B3C +#define FS_ITEMS_H_4E2221634ABA45FE85BA50F710669B3C + +#include "const.h" +#include "enums.h" +#include "itemloader.h" +#include "position.h" + +enum SlotPositionBits : uint32_t { + SLOTP_WHEREEVER = 0xFFFFFFFF, + SLOTP_HEAD = 1 << 0, + SLOTP_NECKLACE = 1 << 1, + SLOTP_BACKPACK = 1 << 2, + SLOTP_ARMOR = 1 << 3, + SLOTP_RIGHT = 1 << 4, + SLOTP_LEFT = 1 << 5, + SLOTP_LEGS = 1 << 6, + SLOTP_FEET = 1 << 7, + SLOTP_RING = 1 << 8, + SLOTP_AMMO = 1 << 9, + SLOTP_DEPOT = 1 << 10, + SLOTP_TWO_HAND = 1 << 11, + SLOTP_HAND = (SLOTP_LEFT | SLOTP_RIGHT) +}; + +enum ItemTypes_t { + ITEM_TYPE_NONE, + ITEM_TYPE_DEPOT, + ITEM_TYPE_MAILBOX, + ITEM_TYPE_TRASHHOLDER, + ITEM_TYPE_CONTAINER, + ITEM_TYPE_DOOR, + ITEM_TYPE_MAGICFIELD, + ITEM_TYPE_TELEPORT, + ITEM_TYPE_BED, + ITEM_TYPE_KEY, + ITEM_TYPE_RUNE, + ITEM_TYPE_LAST +}; + +enum ItemParseAttributes_t { + ITEM_PARSE_TYPE, + ITEM_PARSE_DESCRIPTION, + ITEM_PARSE_RUNESPELLNAME, + ITEM_PARSE_WEIGHT, + ITEM_PARSE_SHOWCOUNT, + ITEM_PARSE_ARMOR, + ITEM_PARSE_DEFENSE, + ITEM_PARSE_EXTRADEF, + ITEM_PARSE_ATTACK, + ITEM_PARSE_ROTATETO, + ITEM_PARSE_MOVEABLE, + ITEM_PARSE_BLOCKPROJECTILE, + ITEM_PARSE_PICKUPABLE, + ITEM_PARSE_FORCESERIALIZE, + ITEM_PARSE_FLOORCHANGE, + ITEM_PARSE_CORPSETYPE, + ITEM_PARSE_CONTAINERSIZE, + ITEM_PARSE_FLUIDSOURCE, + ITEM_PARSE_READABLE, + ITEM_PARSE_WRITEABLE, + ITEM_PARSE_MAXTEXTLEN, + ITEM_PARSE_WRITEONCEITEMID, + ITEM_PARSE_WEAPONTYPE, + ITEM_PARSE_SLOTTYPE, + ITEM_PARSE_AMMOTYPE, + ITEM_PARSE_SHOOTTYPE, + ITEM_PARSE_EFFECT, + ITEM_PARSE_RANGE, + ITEM_PARSE_STOPDURATION, + ITEM_PARSE_DECAYTO, + ITEM_PARSE_TRANSFORMEQUIPTO, + ITEM_PARSE_TRANSFORMDEEQUIPTO, + ITEM_PARSE_DURATION, + ITEM_PARSE_SHOWDURATION, + ITEM_PARSE_CHARGES, + ITEM_PARSE_SHOWCHARGES, + ITEM_PARSE_SHOWATTRIBUTES, + ITEM_PARSE_HITCHANCE, + ITEM_PARSE_MAXHITCHANCE, + ITEM_PARSE_INVISIBLE, + ITEM_PARSE_SPEED, + ITEM_PARSE_HEALTHGAIN, + ITEM_PARSE_HEALTHTICKS, + ITEM_PARSE_MANAGAIN, + ITEM_PARSE_MANATICKS, + ITEM_PARSE_MANASHIELD, + ITEM_PARSE_SKILLSWORD, + ITEM_PARSE_SKILLAXE, + ITEM_PARSE_SKILLCLUB, + ITEM_PARSE_SKILLDIST, + ITEM_PARSE_SKILLFISH, + ITEM_PARSE_SKILLSHIELD, + ITEM_PARSE_SKILLFIST, + ITEM_PARSE_MAXHITPOINTS, + ITEM_PARSE_MAXHITPOINTSPERCENT, + ITEM_PARSE_MAXMANAPOINTS, + ITEM_PARSE_MAXMANAPOINTSPERCENT, + ITEM_PARSE_MAGICPOINTS, + ITEM_PARSE_MAGICPOINTSPERCENT, + ITEM_PARSE_CRITICALHITCHANCE, + ITEM_PARSE_CRITICALHITAMOUNT, + ITEM_PARSE_LIFELEECHCHANCE, + ITEM_PARSE_LIFELEECHAMOUNT, + ITEM_PARSE_MANALEECHCHANCE, + ITEM_PARSE_MANALEECHAMOUNT, + ITEM_PARSE_FIELDABSORBPERCENTENERGY, + ITEM_PARSE_FIELDABSORBPERCENTFIRE, + ITEM_PARSE_FIELDABSORBPERCENTPOISON, + ITEM_PARSE_ABSORBPERCENTALL, + ITEM_PARSE_ABSORBPERCENTELEMENTS, + ITEM_PARSE_ABSORBPERCENTMAGIC, + ITEM_PARSE_ABSORBPERCENTENERGY, + ITEM_PARSE_ABSORBPERCENTFIRE, + ITEM_PARSE_ABSORBPERCENTPOISON, + ITEM_PARSE_ABSORBPERCENTICE, + ITEM_PARSE_ABSORBPERCENTHOLY, + ITEM_PARSE_ABSORBPERCENTDEATH, + ITEM_PARSE_ABSORBPERCENTLIFEDRAIN, + ITEM_PARSE_ABSORBPERCENTMANADRAIN, + ITEM_PARSE_ABSORBPERCENTDROWN, + ITEM_PARSE_ABSORBPERCENTPHYSICAL, + ITEM_PARSE_ABSORBPERCENTHEALING, + ITEM_PARSE_ABSORBPERCENTUNDEFINED, + ITEM_PARSE_SUPPRESSDRUNK, + ITEM_PARSE_SUPPRESSENERGY, + ITEM_PARSE_SUPPRESSFIRE, + ITEM_PARSE_SUPPRESSPOISON, + ITEM_PARSE_SUPPRESSDROWN, + ITEM_PARSE_SUPPRESSPHYSICAL, + ITEM_PARSE_SUPPRESSFREEZE, + ITEM_PARSE_SUPPRESSDAZZLE, + ITEM_PARSE_SUPPRESSCURSE, + ITEM_PARSE_FIELD, + ITEM_PARSE_REPLACEABLE, + ITEM_PARSE_PARTNERDIRECTION, + ITEM_PARSE_LEVELDOOR, + ITEM_PARSE_MALETRANSFORMTO, + ITEM_PARSE_FEMALETRANSFORMTO, + ITEM_PARSE_TRANSFORMTO, + ITEM_PARSE_DESTROYTO, + ITEM_PARSE_ELEMENTICE, + ITEM_PARSE_ELEMENTEARTH, + ITEM_PARSE_ELEMENTFIRE, + ITEM_PARSE_ELEMENTENERGY, + ITEM_PARSE_WALKSTACK, + ITEM_PARSE_BLOCKING, + ITEM_PARSE_ALLOWDISTREAD, +}; + +struct Abilities { + uint32_t healthGain = 0; + uint32_t healthTicks = 0; + uint32_t manaGain = 0; + uint32_t manaTicks = 0; + + uint32_t conditionImmunities = 0; + uint32_t conditionSuppressions = 0; + + //stats modifiers + int32_t stats[STAT_LAST + 1] = { 0 }; + int32_t statsPercent[STAT_LAST + 1] = { 0 }; + + //extra skill modifiers + int32_t skills[SKILL_LAST + 1] = { 0 }; + int32_t specialSkills[SPECIALSKILL_LAST + 1] = { 0 }; + + int32_t speed = 0; + + // field damage abilities modifiers + int16_t fieldAbsorbPercent[COMBAT_COUNT] = { 0 }; + + //damage abilities modifiers + int16_t absorbPercent[COMBAT_COUNT] = { 0 }; + + //elemental damage + uint16_t elementDamage = 0; + CombatType_t elementType = COMBAT_NONE; + + bool manaShield = false; + bool invisible = false; + bool regeneration = false; +}; + +class ConditionDamage; + +class ItemType +{ + public: + ItemType() = default; + + //non-copyable + ItemType(const ItemType& other) = delete; + ItemType& operator=(const ItemType& other) = delete; + + ItemType(ItemType&& other) = default; + ItemType& operator=(ItemType&& other) = default; + + bool isGroundTile() const { + return group == ITEM_GROUP_GROUND; + } + bool isContainer() const { + return group == ITEM_GROUP_CONTAINER; + } + bool isSplash() const { + return group == ITEM_GROUP_SPLASH; + } + bool isFluidContainer() const { + return group == ITEM_GROUP_FLUID; + } + + bool isDoor() const { + return (type == ITEM_TYPE_DOOR); + } + bool isMagicField() const { + return (type == ITEM_TYPE_MAGICFIELD); + } + bool isTeleport() const { + return (type == ITEM_TYPE_TELEPORT); + } + bool isKey() const { + return (type == ITEM_TYPE_KEY); + } + bool isDepot() const { + return (type == ITEM_TYPE_DEPOT); + } + bool isMailbox() const { + return (type == ITEM_TYPE_MAILBOX); + } + bool isTrashHolder() const { + return (type == ITEM_TYPE_TRASHHOLDER); + } + bool isBed() const { + return (type == ITEM_TYPE_BED); + } + bool isRune() const { + return (type == ITEM_TYPE_RUNE); + } + bool isPickupable() const { + return (allowPickupable || pickupable); + } + bool isUseable() const { + return (useable); + } + bool hasSubType() const { + return (isFluidContainer() || isSplash() || stackable || charges != 0); + } + + Abilities& getAbilities() { + if (!abilities) { + abilities.reset(new Abilities()); + } + return *abilities; + } + + std::string getPluralName() const { + if (!pluralName.empty()) { + return pluralName; + } + + if (showCount == 0) { + return name; + } + + std::string str; + str.reserve(name.length() + 1); + str.assign(name); + str += 's'; + return str; + } + + itemgroup_t group = ITEM_GROUP_NONE; + ItemTypes_t type = ITEM_TYPE_NONE; + uint16_t id = 0; + uint16_t clientId = 0; + bool stackable = false; + bool isAnimation = false; + + std::string name; + std::string article; + std::string pluralName; + std::string description; + std::string runeSpellName; + std::string vocationString; + + std::unique_ptr abilities; + std::unique_ptr conditionDamage; + + uint32_t weight = 0; + uint32_t levelDoor = 0; + uint32_t decayTime = 0; + uint32_t wieldInfo = 0; + uint32_t minReqLevel = 0; + uint32_t minReqMagicLevel = 0; + uint32_t charges = 0; + int32_t maxHitChance = -1; + int32_t decayTo = -1; + int32_t attack = 0; + int32_t defense = 0; + int32_t extraDefense = 0; + int32_t armor = 0; + uint16_t rotateTo = 0; + int32_t runeMagLevel = 0; + int32_t runeLevel = 0; + + CombatType_t combatType = COMBAT_NONE; + + uint16_t transformToOnUse[2] = {0, 0}; + uint16_t transformToFree = 0; + uint16_t destroyTo = 0; + uint16_t maxTextLen = 0; + uint16_t writeOnceItemId = 0; + uint16_t transformEquipTo = 0; + uint16_t transformDeEquipTo = 0; + uint16_t maxItems = 8; + uint16_t slotPosition = SLOTP_HAND; + uint16_t speed = 0; + uint16_t wareId = 0; + + MagicEffectClasses magicEffect = CONST_ME_NONE; + Direction bedPartnerDir = DIRECTION_NONE; + WeaponType_t weaponType = WEAPON_NONE; + Ammo_t ammoType = AMMO_NONE; + ShootType_t shootType = CONST_ANI_NONE; + RaceType_t corpseType = RACE_NONE; + FluidTypes_t fluidSource = FLUID_NONE; + + uint8_t floorChange = 0; + uint8_t alwaysOnTopOrder = 0; + uint8_t lightLevel = 0; + uint8_t lightColor = 0; + uint8_t shootRange = 1; + int8_t hitChance = 0; + + bool forceUse = false; + bool forceSerialize = false; + bool hasHeight = false; + bool walkStack = true; + bool blockSolid = false; + bool blockPickupable = false; + bool blockProjectile = false; + bool blockPathFind = false; + bool allowPickupable = false; + bool showDuration = false; + bool showCharges = false; + bool showAttributes = false; + bool replaceable = true; + bool pickupable = false; + bool rotatable = false; + bool useable = false; + bool moveable = false; + bool alwaysOnTop = false; + bool canReadText = false; + bool canWriteText = false; + bool isVertical = false; + bool isHorizontal = false; + bool isHangable = false; + bool allowDistRead = false; + bool lookThrough = false; + bool stopTime = false; + bool showCount = true; +}; + +class Items +{ + public: + using NameMap = std::unordered_multimap; + using InventoryVector = std::vector; + + Items(); + + // non-copyable + Items(const Items&) = delete; + Items& operator=(const Items&) = delete; + + bool reload(); + void clear(); + + bool loadFromOtb(const std::string& file); + + const ItemType& operator[](size_t id) const { + return getItemType(id); + } + const ItemType& getItemType(size_t id) const; + ItemType& getItemType(size_t id); + const ItemType& getItemIdByClientId(uint16_t spriteId) const; + + uint16_t getItemIdByName(const std::string& name); + + uint32_t majorVersion = 0; + uint32_t minorVersion = 0; + uint32_t buildNumber = 0; + + bool loadFromXml(); + void parseItemNode(const pugi::xml_node& itemNode, uint16_t id); + + void buildInventoryList(); + const InventoryVector& getInventory() const { + return inventory; + } + + size_t size() const { + return items.size(); + } + + NameMap nameToItems; + + private: + std::vector items; + InventoryVector inventory; + class ClientIdToServerIdMap + { + public: + ClientIdToServerIdMap() { + vec.reserve(30000); + } + + void emplace(uint16_t clientId, uint16_t serverId) { + if (clientId >= vec.size()) { + vec.resize(clientId + 1, 0); + } + if (vec[clientId] == 0) { + vec[clientId] = serverId; + } + } + + uint16_t getServerId(uint16_t clientId) const { + uint16_t serverId = 0; + if (clientId < vec.size()) { + serverId = vec[clientId]; + } + return serverId; + } + + void clear() { + vec.clear(); + } + private: + std::vector vec; + } clientIdToServerIdMap; +}; +#endif diff --git a/src/lockfree.h b/src/lockfree.h new file mode 100644 index 0000000..b555139 --- /dev/null +++ b/src/lockfree.h @@ -0,0 +1,77 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_LOCKFREE_H_8C707AEB7C7235A2FBC5D4EDDF03B008 +#define FS_LOCKFREE_H_8C707AEB7C7235A2FBC5D4EDDF03B008 + +#if _MSC_FULL_VER >= 190023918 // Workaround for VS2015 Update 2. Boost.Lockfree is a header-only library, so this should be safe to do. +#define _ENABLE_ATOMIC_ALIGNMENT_FIX +#endif + +#include + +/* + * we use this to avoid instantiating multiple free lists for objects of the + * same size and it can be replaced by a variable template in C++14 + * + * template + * boost::lockfree::stack lockfreeFreeList; + */ +template +struct LockfreeFreeList +{ + using FreeList = boost::lockfree::stack>; + static FreeList& get() + { + static FreeList freeList; + return freeList; + } +}; + +template +class LockfreePoolingAllocator : public std::allocator +{ + public: + LockfreePoolingAllocator() = default; + + template ::value>::type> + explicit constexpr LockfreePoolingAllocator(const U&) {} + using value_type = T; + + T* allocate(size_t) const { + auto& inst = LockfreeFreeList::get(); + void* p; // NOTE: p doesn't have to be initialized + if (!inst.pop(p)) { + //Acquire memory without calling the constructor of T + p = operator new (sizeof(T)); + } + return static_cast(p); + } + + void deallocate(T* p, size_t) const { + auto& inst = LockfreeFreeList::get(); + if (!inst.bounded_push(p)) { + //Release memory without calling the destructor of T + //(it has already been called at this point) + operator delete(p); + } + } +}; + +#endif diff --git a/src/luascript.cpp b/src/luascript.cpp new file mode 100644 index 0000000..fc640bd --- /dev/null +++ b/src/luascript.cpp @@ -0,0 +1,16178 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include + +#include "luascript.h" +#include "chat.h" +#include "player.h" +#include "game.h" +#include "protocolstatus.h" +#include "spells.h" +#include "iologindata.h" +#include "configmanager.h" +#include "teleport.h" +#include "databasemanager.h" +#include "bed.h" +#include "monster.h" +#include "scheduler.h" +#include "databasetasks.h" +#include "events.h" +#include "movement.h" +#include "globalevent.h" +#include "script.h" +#include "weapons.h" + +extern Chat* g_chat; +extern Game g_game; +extern Monsters g_monsters; +extern ConfigManager g_config; +extern Vocations g_vocations; +extern Spells* g_spells; +extern Events* g_events; +extern Actions* g_actions; +extern TalkActions* g_talkActions; +extern CreatureEvents* g_creatureEvents; +extern MoveEvents* g_moveEvents; +extern GlobalEvents* g_globalEvents; +extern Scripts* g_scripts; +extern Weapons* g_weapons; + +ScriptEnvironment::DBResultMap ScriptEnvironment::tempResults; +uint32_t ScriptEnvironment::lastResultId = 0; + +std::multimap ScriptEnvironment::tempItems; + +LuaEnvironment g_luaEnvironment; + +ScriptEnvironment::ScriptEnvironment() +{ + resetEnv(); +} + +ScriptEnvironment::~ScriptEnvironment() +{ + resetEnv(); +} + +void ScriptEnvironment::resetEnv() +{ + scriptId = 0; + callbackId = 0; + timerEvent = false; + interface = nullptr; + localMap.clear(); + tempResults.clear(); + + auto pair = tempItems.equal_range(this); + auto it = pair.first; + while (it != pair.second) { + Item* item = it->second; + if (item->getParent() == VirtualCylinder::virtualCylinder) { + g_game.ReleaseItem(item); + } + it = tempItems.erase(it); + } +} + +bool ScriptEnvironment::setCallbackId(int32_t callbackId, LuaScriptInterface* scriptInterface) +{ + if (this->callbackId != 0) { + //nested callbacks are not allowed + if (interface) { + interface->reportErrorFunc("Nested callbacks!"); + } + return false; + } + + this->callbackId = callbackId; + interface = scriptInterface; + return true; +} + +void ScriptEnvironment::getEventInfo(int32_t& scriptId, LuaScriptInterface*& scriptInterface, int32_t& callbackId, bool& timerEvent) const +{ + scriptId = this->scriptId; + scriptInterface = interface; + callbackId = this->callbackId; + timerEvent = this->timerEvent; +} + +uint32_t ScriptEnvironment::addThing(Thing* thing) +{ + if (!thing || thing->isRemoved()) { + return 0; + } + + Creature* creature = thing->getCreature(); + if (creature) { + return creature->getID(); + } + + Item* item = thing->getItem(); + if (item && item->hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)) { + return item->getUniqueId(); + } + + for (const auto& it : localMap) { + if (it.second == item) { + return it.first; + } + } + + localMap[++lastUID] = item; + return lastUID; +} + +void ScriptEnvironment::insertItem(uint32_t uid, Item* item) +{ + auto result = localMap.emplace(uid, item); + if (!result.second) { + std::cout << std::endl << "Lua Script Error: Thing uid already taken."; + } +} + +Thing* ScriptEnvironment::getThingByUID(uint32_t uid) +{ + if (uid >= 0x10000000) { + return g_game.getCreatureByID(uid); + } + + if (uid <= std::numeric_limits::max()) { + Item* item = g_game.getUniqueItem(uid); + if (item && !item->isRemoved()) { + return item; + } + return nullptr; + } + + auto it = localMap.find(uid); + if (it != localMap.end()) { + Item* item = it->second; + if (!item->isRemoved()) { + return item; + } + } + return nullptr; +} + +Item* ScriptEnvironment::getItemByUID(uint32_t uid) +{ + Thing* thing = getThingByUID(uid); + if (!thing) { + return nullptr; + } + return thing->getItem(); +} + +Container* ScriptEnvironment::getContainerByUID(uint32_t uid) +{ + Item* item = getItemByUID(uid); + if (!item) { + return nullptr; + } + return item->getContainer(); +} + +void ScriptEnvironment::removeItemByUID(uint32_t uid) +{ + if (uid <= std::numeric_limits::max()) { + g_game.removeUniqueItem(uid); + return; + } + + auto it = localMap.find(uid); + if (it != localMap.end()) { + localMap.erase(it); + } +} + +void ScriptEnvironment::addTempItem(Item* item) +{ + tempItems.emplace(this, item); +} + +void ScriptEnvironment::removeTempItem(Item* item) +{ + for (auto it = tempItems.begin(), end = tempItems.end(); it != end; ++it) { + if (it->second == item) { + tempItems.erase(it); + break; + } + } +} + +uint32_t ScriptEnvironment::addResult(DBResult_ptr res) +{ + tempResults[++lastResultId] = res; + return lastResultId; +} + +bool ScriptEnvironment::removeResult(uint32_t id) +{ + auto it = tempResults.find(id); + if (it == tempResults.end()) { + return false; + } + + tempResults.erase(it); + return true; +} + +DBResult_ptr ScriptEnvironment::getResultByID(uint32_t id) +{ + auto it = tempResults.find(id); + if (it == tempResults.end()) { + return nullptr; + } + return it->second; +} + +std::string LuaScriptInterface::getErrorDesc(ErrorCode_t code) +{ + switch (code) { + case LUA_ERROR_PLAYER_NOT_FOUND: return "Player not found"; + case LUA_ERROR_CREATURE_NOT_FOUND: return "Creature not found"; + case LUA_ERROR_ITEM_NOT_FOUND: return "Item not found"; + case LUA_ERROR_THING_NOT_FOUND: return "Thing not found"; + case LUA_ERROR_TILE_NOT_FOUND: return "Tile not found"; + case LUA_ERROR_HOUSE_NOT_FOUND: return "House not found"; + case LUA_ERROR_COMBAT_NOT_FOUND: return "Combat not found"; + case LUA_ERROR_CONDITION_NOT_FOUND: return "Condition not found"; + case LUA_ERROR_AREA_NOT_FOUND: return "Area not found"; + case LUA_ERROR_CONTAINER_NOT_FOUND: return "Container not found"; + case LUA_ERROR_VARIANT_NOT_FOUND: return "Variant not found"; + case LUA_ERROR_VARIANT_UNKNOWN: return "Unknown variant type"; + case LUA_ERROR_SPELL_NOT_FOUND: return "Spell not found"; + default: return "Bad error code"; + } +} + +ScriptEnvironment LuaScriptInterface::scriptEnv[16]; +int32_t LuaScriptInterface::scriptEnvIndex = -1; + +LuaScriptInterface::LuaScriptInterface(std::string interfaceName) : interfaceName(std::move(interfaceName)) +{ + if (!g_luaEnvironment.getLuaState()) { + g_luaEnvironment.initState(); + } +} + +LuaScriptInterface::~LuaScriptInterface() +{ + closeState(); +} + +bool LuaScriptInterface::reInitState() +{ + g_luaEnvironment.clearCombatObjects(this); + g_luaEnvironment.clearAreaObjects(this); + + closeState(); + return initState(); +} + +/// Same as lua_pcall, but adds stack trace to error strings in called function. +int LuaScriptInterface::protectedCall(lua_State* L, int nargs, int nresults) +{ + int error_index = lua_gettop(L) - nargs; + lua_pushcfunction(L, luaErrorHandler); + lua_insert(L, error_index); + + int ret = lua_pcall(L, nargs, nresults, error_index); + lua_remove(L, error_index); + return ret; +} + +int32_t LuaScriptInterface::loadFile(const std::string& file, Npc* npc /* = nullptr*/) +{ + //loads file as a chunk at stack top + int ret = luaL_loadfile(luaState, file.c_str()); + if (ret != 0) { + lastLuaError = popString(luaState); + return -1; + } + + //check that it is loaded as a function + if (!isFunction(luaState, -1)) { + return -1; + } + + loadingFile = file; + + if (!reserveScriptEnv()) { + return -1; + } + + ScriptEnvironment* env = getScriptEnv(); + env->setScriptId(EVENT_ID_LOADING, this); + env->setNpc(npc); + + //execute it + ret = protectedCall(luaState, 0, 0); + if (ret != 0) { + reportError(nullptr, popString(luaState)); + resetScriptEnv(); + return -1; + } + + resetScriptEnv(); + return 0; +} + +int32_t LuaScriptInterface::getEvent(const std::string& eventName) +{ + //get our events table + lua_rawgeti(luaState, LUA_REGISTRYINDEX, eventTableRef); + if (!isTable(luaState, -1)) { + lua_pop(luaState, 1); + return -1; + } + + //get current event function pointer + lua_getglobal(luaState, eventName.c_str()); + if (!isFunction(luaState, -1)) { + lua_pop(luaState, 2); + return -1; + } + + //save in our events table + lua_pushvalue(luaState, -1); + lua_rawseti(luaState, -3, runningEventId); + lua_pop(luaState, 2); + + //reset global value of this event + lua_pushnil(luaState); + lua_setglobal(luaState, eventName.c_str()); + + cacheFiles[runningEventId] = loadingFile + ":" + eventName; + return runningEventId++; +} + +int32_t LuaScriptInterface::getEvent() +{ + //check if function is on the stack + if (!isFunction(luaState, -1)) { + return -1; + } + + //get our events table + lua_rawgeti(luaState, LUA_REGISTRYINDEX, eventTableRef); + if (!isTable(luaState, -1)) { + lua_pop(luaState, 1); + return -1; + } + + //save in our events table + lua_pushvalue(luaState, -2); + lua_rawseti(luaState, -2, runningEventId); + lua_pop(luaState, 2); + + cacheFiles[runningEventId] = loadingFile + ":callback"; + return runningEventId++; +} + +int32_t LuaScriptInterface::getMetaEvent(const std::string& globalName, const std::string& eventName) +{ + //get our events table + lua_rawgeti(luaState, LUA_REGISTRYINDEX, eventTableRef); + if (!isTable(luaState, -1)) { + lua_pop(luaState, 1); + return -1; + } + + //get current event function pointer + lua_getglobal(luaState, globalName.c_str()); + lua_getfield(luaState, -1, eventName.c_str()); + if (!isFunction(luaState, -1)) { + lua_pop(luaState, 3); + return -1; + } + + //save in our events table + lua_pushvalue(luaState, -1); + lua_rawseti(luaState, -4, runningEventId); + lua_pop(luaState, 1); + + //reset global value of this event + lua_pushnil(luaState); + lua_setfield(luaState, -2, eventName.c_str()); + lua_pop(luaState, 2); + + cacheFiles[runningEventId] = loadingFile + ":" + globalName + "@" + eventName; + return runningEventId++; +} + +const std::string& LuaScriptInterface::getFileById(int32_t scriptId) +{ + if (scriptId == EVENT_ID_LOADING) { + return loadingFile; + } + + auto it = cacheFiles.find(scriptId); + if (it == cacheFiles.end()) { + static const std::string& unk = "(Unknown scriptfile)"; + return unk; + } + return it->second; +} + +std::string LuaScriptInterface::getStackTrace(const std::string& error_desc) +{ + lua_getglobal(luaState, "debug"); + if (!isTable(luaState, -1)) { + lua_pop(luaState, 1); + return error_desc; + } + + lua_getfield(luaState, -1, "traceback"); + if (!isFunction(luaState, -1)) { + lua_pop(luaState, 2); + return error_desc; + } + + lua_replace(luaState, -2); + pushString(luaState, error_desc); + lua_call(luaState, 1, 1); + return popString(luaState); +} + +void LuaScriptInterface::reportError(const char* function, const std::string& error_desc, bool stack_trace/* = false*/) +{ + int32_t scriptId; + int32_t callbackId; + bool timerEvent; + LuaScriptInterface* scriptInterface; + getScriptEnv()->getEventInfo(scriptId, scriptInterface, callbackId, timerEvent); + + std::cout << std::endl << "Lua Script Error: "; + + if (scriptInterface) { + std::cout << '[' << scriptInterface->getInterfaceName() << "] " << std::endl; + + if (timerEvent) { + std::cout << "in a timer event called from: " << std::endl; + } + + if (callbackId) { + std::cout << "in callback: " << scriptInterface->getFileById(callbackId) << std::endl; + } + + std::cout << scriptInterface->getFileById(scriptId) << std::endl; + } + + if (function) { + std::cout << function << "(). "; + } + + if (stack_trace && scriptInterface) { + std::cout << scriptInterface->getStackTrace(error_desc) << std::endl; + } else { + std::cout << error_desc << std::endl; + } +} + +bool LuaScriptInterface::pushFunction(int32_t functionId) +{ + lua_rawgeti(luaState, LUA_REGISTRYINDEX, eventTableRef); + if (!isTable(luaState, -1)) { + return false; + } + + lua_rawgeti(luaState, -1, functionId); + lua_replace(luaState, -2); + return isFunction(luaState, -1); +} + +bool LuaScriptInterface::initState() +{ + luaState = g_luaEnvironment.getLuaState(); + if (!luaState) { + return false; + } + + lua_newtable(luaState); + eventTableRef = luaL_ref(luaState, LUA_REGISTRYINDEX); + runningEventId = EVENT_ID_USER; + return true; +} + +bool LuaScriptInterface::closeState() +{ + if (!g_luaEnvironment.getLuaState() || !luaState) { + return false; + } + + cacheFiles.clear(); + if (eventTableRef != -1) { + luaL_unref(luaState, LUA_REGISTRYINDEX, eventTableRef); + eventTableRef = -1; + } + + luaState = nullptr; + return true; +} + +int LuaScriptInterface::luaErrorHandler(lua_State* L) +{ + const std::string& errorMessage = popString(L); + auto interface = getScriptEnv()->getScriptInterface(); + assert(interface); //This fires if the ScriptEnvironment hasn't been setup + pushString(L, interface->getStackTrace(errorMessage)); + return 1; +} + +bool LuaScriptInterface::callFunction(int params) +{ + bool result = false; + int size = lua_gettop(luaState); + if (protectedCall(luaState, params, 1) != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::getString(luaState, -1)); + } else { + result = LuaScriptInterface::getBoolean(luaState, -1); + } + + lua_pop(luaState, 1); + if ((lua_gettop(luaState) + params + 1) != size) { + LuaScriptInterface::reportError(nullptr, "Stack size changed!"); + } + + resetScriptEnv(); + return result; +} + +void LuaScriptInterface::callVoidFunction(int params) +{ + int size = lua_gettop(luaState); + if (protectedCall(luaState, params, 0) != 0) { + LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(luaState)); + } + + if ((lua_gettop(luaState) + params + 1) != size) { + LuaScriptInterface::reportError(nullptr, "Stack size changed!"); + } + + resetScriptEnv(); +} + +void LuaScriptInterface::pushVariant(lua_State* L, const LuaVariant& var) +{ + lua_createtable(L, 0, 2); + setField(L, "type", var.type); + switch (var.type) { + case VARIANT_NUMBER: + setField(L, "number", var.number); + break; + case VARIANT_STRING: + setField(L, "string", var.text); + break; + case VARIANT_TARGETPOSITION: + case VARIANT_POSITION: { + pushPosition(L, var.pos); + lua_setfield(L, -2, "pos"); + break; + } + default: + break; + } + setMetatable(L, -1, "Variant"); +} + +void LuaScriptInterface::pushThing(lua_State* L, Thing* thing) +{ + if (!thing) { + lua_createtable(L, 0, 4); + setField(L, "uid", 0); + setField(L, "itemid", 0); + setField(L, "actionid", 0); + setField(L, "type", 0); + return; + } + + if (Item* item = thing->getItem()) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } else if (Creature* creature = thing->getCreature()) { + pushUserdata(L, creature); + setCreatureMetatable(L, -1, creature); + } else { + lua_pushnil(L); + } +} + +void LuaScriptInterface::pushCylinder(lua_State* L, Cylinder* cylinder) +{ + if (Creature* creature = cylinder->getCreature()) { + pushUserdata(L, creature); + setCreatureMetatable(L, -1, creature); + } else if (Item* parentItem = cylinder->getItem()) { + pushUserdata(L, parentItem); + setItemMetatable(L, -1, parentItem); + } else if (Tile* tile = cylinder->getTile()) { + pushUserdata(L, tile); + setMetatable(L, -1, "Tile"); + } else if (cylinder == VirtualCylinder::virtualCylinder) { + pushBoolean(L, true); + } else { + lua_pushnil(L); + } +} + +void LuaScriptInterface::pushString(lua_State* L, const std::string& value) +{ + lua_pushlstring(L, value.c_str(), value.length()); +} + +void LuaScriptInterface::pushCallback(lua_State* L, int32_t callback) +{ + lua_rawgeti(L, LUA_REGISTRYINDEX, callback); +} + +std::string LuaScriptInterface::popString(lua_State* L) +{ + if (lua_gettop(L) == 0) { + return std::string(); + } + + std::string str(getString(L, -1)); + lua_pop(L, 1); + return str; +} + +int32_t LuaScriptInterface::popCallback(lua_State* L) +{ + return luaL_ref(L, LUA_REGISTRYINDEX); +} + +// Metatables +void LuaScriptInterface::setMetatable(lua_State* L, int32_t index, const std::string& name) +{ + luaL_getmetatable(L, name.c_str()); + lua_setmetatable(L, index - 1); +} + +void LuaScriptInterface::setWeakMetatable(lua_State* L, int32_t index, const std::string& name) +{ + static std::set weakObjectTypes; + const std::string& weakName = name + "_weak"; + + auto result = weakObjectTypes.emplace(name); + if (result.second) { + luaL_getmetatable(L, name.c_str()); + int childMetatable = lua_gettop(L); + + luaL_newmetatable(L, weakName.c_str()); + int metatable = lua_gettop(L); + + static const std::vector methodKeys = {"__index", "__metatable", "__eq"}; + for (const std::string& metaKey : methodKeys) { + lua_getfield(L, childMetatable, metaKey.c_str()); + lua_setfield(L, metatable, metaKey.c_str()); + } + + static const std::vector methodIndexes = {'h', 'p', 't'}; + for (int metaIndex : methodIndexes) { + lua_rawgeti(L, childMetatable, metaIndex); + lua_rawseti(L, metatable, metaIndex); + } + + lua_pushnil(L); + lua_setfield(L, metatable, "__gc"); + + lua_remove(L, childMetatable); + } else { + luaL_getmetatable(L, weakName.c_str()); + } + lua_setmetatable(L, index - 1); +} + +void LuaScriptInterface::setItemMetatable(lua_State* L, int32_t index, const Item* item) +{ + if (item->getContainer()) { + luaL_getmetatable(L, "Container"); + } else if (item->getTeleport()) { + luaL_getmetatable(L, "Teleport"); + } else { + luaL_getmetatable(L, "Item"); + } + lua_setmetatable(L, index - 1); +} + +void LuaScriptInterface::setCreatureMetatable(lua_State* L, int32_t index, const Creature* creature) +{ + if (creature->getPlayer()) { + luaL_getmetatable(L, "Player"); + } else if (creature->getMonster()) { + luaL_getmetatable(L, "Monster"); + } else { + luaL_getmetatable(L, "Npc"); + } + lua_setmetatable(L, index - 1); +} + +// Get +std::string LuaScriptInterface::getString(lua_State* L, int32_t arg) +{ + size_t len; + const char* c_str = lua_tolstring(L, arg, &len); + if (!c_str || len == 0) { + return std::string(); + } + return std::string(c_str, len); +} + +Position LuaScriptInterface::getPosition(lua_State* L, int32_t arg, int32_t& stackpos) +{ + Position position; + position.x = getField(L, arg, "x"); + position.y = getField(L, arg, "y"); + position.z = getField(L, arg, "z"); + + lua_getfield(L, arg, "stackpos"); + if (lua_isnil(L, -1) == 1) { + stackpos = 0; + } else { + stackpos = getNumber(L, -1); + } + + lua_pop(L, 4); + return position; +} + +Position LuaScriptInterface::getPosition(lua_State* L, int32_t arg) +{ + Position position; + position.x = getField(L, arg, "x"); + position.y = getField(L, arg, "y"); + position.z = getField(L, arg, "z"); + + lua_pop(L, 3); + return position; +} + +Outfit_t LuaScriptInterface::getOutfit(lua_State* L, int32_t arg) +{ + Outfit_t outfit; + outfit.lookMount = getField(L, arg, "lookMount"); + outfit.lookAddons = getField(L, arg, "lookAddons"); + + outfit.lookFeet = getField(L, arg, "lookFeet"); + outfit.lookLegs = getField(L, arg, "lookLegs"); + outfit.lookBody = getField(L, arg, "lookBody"); + outfit.lookHead = getField(L, arg, "lookHead"); + + outfit.lookTypeEx = getField(L, arg, "lookTypeEx"); + outfit.lookType = getField(L, arg, "lookType"); + + lua_pop(L, 8); + return outfit; +} + +LuaVariant LuaScriptInterface::getVariant(lua_State* L, int32_t arg) +{ + LuaVariant var; + switch (var.type = getField(L, arg, "type")) { + case VARIANT_NUMBER: { + var.number = getField(L, arg, "number"); + lua_pop(L, 2); + break; + } + + case VARIANT_STRING: { + var.text = getFieldString(L, arg, "string"); + lua_pop(L, 2); + break; + } + + case VARIANT_POSITION: + case VARIANT_TARGETPOSITION: { + lua_getfield(L, arg, "pos"); + var.pos = getPosition(L, lua_gettop(L)); + lua_pop(L, 2); + break; + } + + default: { + var.type = VARIANT_NONE; + lua_pop(L, 1); + break; + } + } + return var; +} + +InstantSpell* LuaScriptInterface::getInstantSpell(lua_State* L, int32_t arg) +{ + InstantSpell* spell = g_spells->getInstantSpellByName(getFieldString(L, arg, "name")); + lua_pop(L, 1); + return spell; +} + +Thing* LuaScriptInterface::getThing(lua_State* L, int32_t arg) +{ + Thing* thing; + if (lua_getmetatable(L, arg) != 0) { + lua_rawgeti(L, -1, 't'); + switch(getNumber(L, -1)) { + case LuaData_Item: + thing = getUserdata(L, arg); + break; + case LuaData_Container: + thing = getUserdata(L, arg); + break; + case LuaData_Teleport: + thing = getUserdata(L, arg); + break; + case LuaData_Player: + thing = getUserdata(L, arg); + break; + case LuaData_Monster: + thing = getUserdata(L, arg); + break; + case LuaData_Npc: + thing = getUserdata(L, arg); + break; + default: + thing = nullptr; + break; + } + lua_pop(L, 2); + } else { + thing = getScriptEnv()->getThingByUID(getNumber(L, arg)); + } + return thing; +} + +Creature* LuaScriptInterface::getCreature(lua_State* L, int32_t arg) +{ + if (isUserdata(L, arg)) { + return getUserdata(L, arg); + } + return g_game.getCreatureByID(getNumber(L, arg)); +} + +Player* LuaScriptInterface::getPlayer(lua_State* L, int32_t arg) +{ + if (isUserdata(L, arg)) { + return getUserdata(L, arg); + } + return g_game.getPlayerByID(getNumber(L, arg)); +} + +std::string LuaScriptInterface::getFieldString(lua_State* L, int32_t arg, const std::string& key) +{ + lua_getfield(L, arg, key.c_str()); + return getString(L, -1); +} + +LuaDataType LuaScriptInterface::getUserdataType(lua_State* L, int32_t arg) +{ + if (lua_getmetatable(L, arg) == 0) { + return LuaData_Unknown; + } + lua_rawgeti(L, -1, 't'); + + LuaDataType type = getNumber(L, -1); + lua_pop(L, 2); + + return type; +} + +// Push +void LuaScriptInterface::pushBoolean(lua_State* L, bool value) +{ + lua_pushboolean(L, value ? 1 : 0); +} + +void LuaScriptInterface::pushCombatDamage(lua_State* L, const CombatDamage& damage) +{ + lua_pushnumber(L, damage.primary.value); + lua_pushnumber(L, damage.primary.type); + lua_pushnumber(L, damage.secondary.value); + lua_pushnumber(L, damage.secondary.type); + lua_pushnumber(L, damage.origin); +} + +void LuaScriptInterface::pushInstantSpell(lua_State* L, const InstantSpell& spell) +{ + lua_createtable(L, 0, 6); + + setField(L, "name", spell.getName()); + setField(L, "words", spell.getWords()); + setField(L, "level", spell.getLevel()); + setField(L, "mlevel", spell.getMagicLevel()); + setField(L, "mana", spell.getMana()); + setField(L, "manapercent", spell.getManaPercent()); + + setMetatable(L, -1, "Spell"); +} + +void LuaScriptInterface::pushPosition(lua_State* L, const Position& position, int32_t stackpos/* = 0*/) +{ + lua_createtable(L, 0, 4); + + setField(L, "x", position.x); + setField(L, "y", position.y); + setField(L, "z", position.z); + setField(L, "stackpos", stackpos); + + setMetatable(L, -1, "Position"); +} + +void LuaScriptInterface::pushOutfit(lua_State* L, const Outfit_t& outfit) +{ + lua_createtable(L, 0, 8); + setField(L, "lookType", outfit.lookType); + setField(L, "lookTypeEx", outfit.lookTypeEx); + setField(L, "lookHead", outfit.lookHead); + setField(L, "lookBody", outfit.lookBody); + setField(L, "lookLegs", outfit.lookLegs); + setField(L, "lookFeet", outfit.lookFeet); + setField(L, "lookAddons", outfit.lookAddons); + setField(L, "lookMount", outfit.lookMount); +} + +void LuaScriptInterface::pushLoot(lua_State* L, const std::vector& lootList) +{ + lua_createtable(L, lootList.size(), 0); + + int index = 0; + for (const auto& lootBlock : lootList) { + lua_createtable(L, 0, 7); + + setField(L, "itemId", lootBlock.id); + setField(L, "chance", lootBlock.chance); + setField(L, "subType", lootBlock.subType); + setField(L, "maxCount", lootBlock.countmax); + setField(L, "actionId", lootBlock.actionId); + setField(L, "text", lootBlock.text); + + pushLoot(L, lootBlock.childLoot); + lua_setfield(L, -2, "childLoot"); + + lua_rawseti(L, -2, ++index); + } +} + +#define registerEnum(value) { std::string enumName = #value; registerGlobalVariable(enumName.substr(enumName.find_last_of(':') + 1), value); } +#define registerEnumIn(tableName, value) { std::string enumName = #value; registerVariable(tableName, enumName.substr(enumName.find_last_of(':') + 1), value); } + +void LuaScriptInterface::registerFunctions() +{ + //doPlayerAddItem(uid, itemid, count/subtype) + //doPlayerAddItem(cid, itemid, count, canDropOnMap, subtype) + //Returns uid of the created item + lua_register(luaState, "doPlayerAddItem", LuaScriptInterface::luaDoPlayerAddItem); + + //doSetCreatureLight(cid, lightLevel, lightColor, time) + lua_register(luaState, "doSetCreatureLight", LuaScriptInterface::luaDoSetCreatureLight); + + //isValidUID(uid) + lua_register(luaState, "isValidUID", LuaScriptInterface::luaIsValidUID); + + //isDepot(uid) + lua_register(luaState, "isDepot", LuaScriptInterface::luaIsDepot); + + //isMovable(uid) + lua_register(luaState, "isMovable", LuaScriptInterface::luaIsMoveable); + + //doAddContainerItem(uid, itemid, count/subtype) + lua_register(luaState, "doAddContainerItem", LuaScriptInterface::luaDoAddContainerItem); + + //getDepotId(uid) + lua_register(luaState, "getDepotId", LuaScriptInterface::luaGetDepotId); + + //getWorldTime() + lua_register(luaState, "getWorldTime", LuaScriptInterface::luaGetWorldTime); + + //getWorldLight() + lua_register(luaState, "getWorldLight", LuaScriptInterface::luaGetWorldLight); + + //getWorldUpTime() + lua_register(luaState, "getWorldUpTime", LuaScriptInterface::luaGetWorldUpTime); + + //createCombatArea( {area}, {extArea} ) + lua_register(luaState, "createCombatArea", LuaScriptInterface::luaCreateCombatArea); + + //doAreaCombat(cid, type, pos, area, min, max, effect) + lua_register(luaState, "doAreaCombat", LuaScriptInterface::luaDoAreaCombat); + + //doTargetCombat(cid, target, type, min, max, effect) + lua_register(luaState, "doTargetCombat", LuaScriptInterface::luaDoTargetCombat); + + //doChallengeCreature(cid, target) + lua_register(luaState, "doChallengeCreature", LuaScriptInterface::luaDoChallengeCreature); + + //addEvent(callback, delay, ...) + lua_register(luaState, "addEvent", LuaScriptInterface::luaAddEvent); + + //stopEvent(eventid) + lua_register(luaState, "stopEvent", LuaScriptInterface::luaStopEvent); + + //saveServer() + lua_register(luaState, "saveServer", LuaScriptInterface::luaSaveServer); + + //cleanMap() + lua_register(luaState, "cleanMap", LuaScriptInterface::luaCleanMap); + + //debugPrint(text) + lua_register(luaState, "debugPrint", LuaScriptInterface::luaDebugPrint); + + //isInWar(cid, target) + lua_register(luaState, "isInWar", LuaScriptInterface::luaIsInWar); + + //getWaypointPosition(name) + lua_register(luaState, "getWaypointPositionByName", LuaScriptInterface::luaGetWaypointPositionByName); + + //sendChannelMessage(channelId, type, message) + lua_register(luaState, "sendChannelMessage", LuaScriptInterface::luaSendChannelMessage); + + //sendGuildChannelMessage(guildId, type, message) + lua_register(luaState, "sendGuildChannelMessage", LuaScriptInterface::luaSendGuildChannelMessage); + +#ifndef LUAJIT_VERSION + //bit operations for Lua, based on bitlib project release 24 + //bit.bnot, bit.band, bit.bor, bit.bxor, bit.lshift, bit.rshift + luaL_register(luaState, "bit", LuaScriptInterface::luaBitReg); +#endif + + //configManager table + luaL_register(luaState, "configManager", LuaScriptInterface::luaConfigManagerTable); + + //db table + luaL_register(luaState, "db", LuaScriptInterface::luaDatabaseTable); + + //result table + luaL_register(luaState, "result", LuaScriptInterface::luaResultTable); + + /* New functions */ + //registerClass(className, baseClass, newFunction) + //registerTable(tableName) + //registerMethod(className, functionName, function) + //registerMetaMethod(className, functionName, function) + //registerGlobalMethod(functionName, function) + //registerVariable(tableName, name, value) + //registerGlobalVariable(name, value) + //registerEnum(value) + //registerEnumIn(tableName, value) + + // Enums + registerEnum(ACCOUNT_TYPE_NORMAL) + registerEnum(ACCOUNT_TYPE_TUTOR) + registerEnum(ACCOUNT_TYPE_SENIORTUTOR) + registerEnum(ACCOUNT_TYPE_GAMEMASTER) + registerEnum(ACCOUNT_TYPE_GOD) + + registerEnum(BUG_CATEGORY_MAP) + registerEnum(BUG_CATEGORY_TYPO) + registerEnum(BUG_CATEGORY_TECHNICAL) + registerEnum(BUG_CATEGORY_OTHER) + + registerEnum(CALLBACK_PARAM_LEVELMAGICVALUE) + registerEnum(CALLBACK_PARAM_SKILLVALUE) + registerEnum(CALLBACK_PARAM_TARGETTILE) + registerEnum(CALLBACK_PARAM_TARGETCREATURE) + + registerEnum(COMBAT_FORMULA_UNDEFINED) + registerEnum(COMBAT_FORMULA_LEVELMAGIC) + registerEnum(COMBAT_FORMULA_SKILL) + registerEnum(COMBAT_FORMULA_DAMAGE) + + registerEnum(DIRECTION_NORTH) + registerEnum(DIRECTION_EAST) + registerEnum(DIRECTION_SOUTH) + registerEnum(DIRECTION_WEST) + registerEnum(DIRECTION_SOUTHWEST) + registerEnum(DIRECTION_SOUTHEAST) + registerEnum(DIRECTION_NORTHWEST) + registerEnum(DIRECTION_NORTHEAST) + + registerEnum(COMBAT_NONE) + registerEnum(COMBAT_PHYSICALDAMAGE) + registerEnum(COMBAT_ENERGYDAMAGE) + registerEnum(COMBAT_EARTHDAMAGE) + registerEnum(COMBAT_FIREDAMAGE) + registerEnum(COMBAT_UNDEFINEDDAMAGE) + registerEnum(COMBAT_LIFEDRAIN) + registerEnum(COMBAT_MANADRAIN) + registerEnum(COMBAT_HEALING) + registerEnum(COMBAT_DROWNDAMAGE) + registerEnum(COMBAT_ICEDAMAGE) + registerEnum(COMBAT_HOLYDAMAGE) + registerEnum(COMBAT_DEATHDAMAGE) + + registerEnum(COMBAT_PARAM_TYPE) + registerEnum(COMBAT_PARAM_EFFECT) + registerEnum(COMBAT_PARAM_DISTANCEEFFECT) + registerEnum(COMBAT_PARAM_BLOCKSHIELD) + registerEnum(COMBAT_PARAM_BLOCKARMOR) + registerEnum(COMBAT_PARAM_TARGETCASTERORTOPMOST) + registerEnum(COMBAT_PARAM_CREATEITEM) + registerEnum(COMBAT_PARAM_AGGRESSIVE) + registerEnum(COMBAT_PARAM_DISPEL) + registerEnum(COMBAT_PARAM_USECHARGES) + + registerEnum(CONDITION_NONE) + registerEnum(CONDITION_POISON) + registerEnum(CONDITION_FIRE) + registerEnum(CONDITION_ENERGY) + registerEnum(CONDITION_BLEEDING) + registerEnum(CONDITION_HASTE) + registerEnum(CONDITION_PARALYZE) + registerEnum(CONDITION_OUTFIT) + registerEnum(CONDITION_INVISIBLE) + registerEnum(CONDITION_LIGHT) + registerEnum(CONDITION_MANASHIELD) + registerEnum(CONDITION_INFIGHT) + registerEnum(CONDITION_DRUNK) + registerEnum(CONDITION_EXHAUST_WEAPON) + registerEnum(CONDITION_REGENERATION) + registerEnum(CONDITION_SOUL) + registerEnum(CONDITION_DROWN) + registerEnum(CONDITION_MUTED) + registerEnum(CONDITION_CHANNELMUTEDTICKS) + registerEnum(CONDITION_YELLTICKS) + registerEnum(CONDITION_ATTRIBUTES) + registerEnum(CONDITION_FREEZING) + registerEnum(CONDITION_DAZZLED) + registerEnum(CONDITION_CURSED) + registerEnum(CONDITION_EXHAUST_COMBAT) + registerEnum(CONDITION_EXHAUST_HEAL) + registerEnum(CONDITION_PACIFIED) + registerEnum(CONDITION_SPELLCOOLDOWN) + registerEnum(CONDITION_SPELLGROUPCOOLDOWN) + + registerEnum(CONDITIONID_DEFAULT) + registerEnum(CONDITIONID_COMBAT) + registerEnum(CONDITIONID_HEAD) + registerEnum(CONDITIONID_NECKLACE) + registerEnum(CONDITIONID_BACKPACK) + registerEnum(CONDITIONID_ARMOR) + registerEnum(CONDITIONID_RIGHT) + registerEnum(CONDITIONID_LEFT) + registerEnum(CONDITIONID_LEGS) + registerEnum(CONDITIONID_FEET) + registerEnum(CONDITIONID_RING) + registerEnum(CONDITIONID_AMMO) + + registerEnum(CONDITION_PARAM_OWNER) + registerEnum(CONDITION_PARAM_TICKS) + registerEnum(CONDITION_PARAM_HEALTHGAIN) + registerEnum(CONDITION_PARAM_HEALTHTICKS) + registerEnum(CONDITION_PARAM_MANAGAIN) + registerEnum(CONDITION_PARAM_MANATICKS) + registerEnum(CONDITION_PARAM_DELAYED) + registerEnum(CONDITION_PARAM_SPEED) + registerEnum(CONDITION_PARAM_LIGHT_LEVEL) + registerEnum(CONDITION_PARAM_LIGHT_COLOR) + registerEnum(CONDITION_PARAM_SOULGAIN) + registerEnum(CONDITION_PARAM_SOULTICKS) + registerEnum(CONDITION_PARAM_MINVALUE) + registerEnum(CONDITION_PARAM_MAXVALUE) + registerEnum(CONDITION_PARAM_STARTVALUE) + registerEnum(CONDITION_PARAM_TICKINTERVAL) + registerEnum(CONDITION_PARAM_FORCEUPDATE) + registerEnum(CONDITION_PARAM_SKILL_MELEE) + registerEnum(CONDITION_PARAM_SKILL_FIST) + registerEnum(CONDITION_PARAM_SKILL_CLUB) + registerEnum(CONDITION_PARAM_SKILL_SWORD) + registerEnum(CONDITION_PARAM_SKILL_AXE) + registerEnum(CONDITION_PARAM_SKILL_DISTANCE) + registerEnum(CONDITION_PARAM_SKILL_SHIELD) + registerEnum(CONDITION_PARAM_SKILL_FISHING) + registerEnum(CONDITION_PARAM_STAT_MAXHITPOINTS) + registerEnum(CONDITION_PARAM_STAT_MAXMANAPOINTS) + registerEnum(CONDITION_PARAM_STAT_MAGICPOINTS) + registerEnum(CONDITION_PARAM_STAT_MAXHITPOINTSPERCENT) + registerEnum(CONDITION_PARAM_STAT_MAXMANAPOINTSPERCENT) + registerEnum(CONDITION_PARAM_STAT_MAGICPOINTSPERCENT) + registerEnum(CONDITION_PARAM_PERIODICDAMAGE) + registerEnum(CONDITION_PARAM_SKILL_MELEEPERCENT) + registerEnum(CONDITION_PARAM_SKILL_FISTPERCENT) + registerEnum(CONDITION_PARAM_SKILL_CLUBPERCENT) + registerEnum(CONDITION_PARAM_SKILL_SWORDPERCENT) + registerEnum(CONDITION_PARAM_SKILL_AXEPERCENT) + registerEnum(CONDITION_PARAM_SKILL_DISTANCEPERCENT) + registerEnum(CONDITION_PARAM_SKILL_SHIELDPERCENT) + registerEnum(CONDITION_PARAM_SKILL_FISHINGPERCENT) + registerEnum(CONDITION_PARAM_BUFF_SPELL) + registerEnum(CONDITION_PARAM_SUBID) + registerEnum(CONDITION_PARAM_FIELD) + registerEnum(CONDITION_PARAM_DISABLE_DEFENSE) + registerEnum(CONDITION_PARAM_SPECIALSKILL_CRITICALHITCHANCE) + registerEnum(CONDITION_PARAM_SPECIALSKILL_CRITICALHITAMOUNT) + registerEnum(CONDITION_PARAM_SPECIALSKILL_LIFELEECHCHANCE) + registerEnum(CONDITION_PARAM_SPECIALSKILL_LIFELEECHAMOUNT) + registerEnum(CONDITION_PARAM_SPECIALSKILL_MANALEECHCHANCE) + registerEnum(CONDITION_PARAM_SPECIALSKILL_MANALEECHAMOUNT) + registerEnum(CONDITION_PARAM_AGGRESSIVE) + + registerEnum(CONST_ME_NONE) + registerEnum(CONST_ME_DRAWBLOOD) + registerEnum(CONST_ME_LOSEENERGY) + registerEnum(CONST_ME_POFF) + registerEnum(CONST_ME_BLOCKHIT) + registerEnum(CONST_ME_EXPLOSIONAREA) + registerEnum(CONST_ME_EXPLOSIONHIT) + registerEnum(CONST_ME_FIREAREA) + registerEnum(CONST_ME_YELLOW_RINGS) + registerEnum(CONST_ME_GREEN_RINGS) + registerEnum(CONST_ME_HITAREA) + registerEnum(CONST_ME_TELEPORT) + registerEnum(CONST_ME_ENERGYHIT) + registerEnum(CONST_ME_MAGIC_BLUE) + registerEnum(CONST_ME_MAGIC_RED) + registerEnum(CONST_ME_MAGIC_GREEN) + registerEnum(CONST_ME_HITBYFIRE) + registerEnum(CONST_ME_HITBYPOISON) + registerEnum(CONST_ME_MORTAREA) + registerEnum(CONST_ME_SOUND_GREEN) + registerEnum(CONST_ME_SOUND_RED) + registerEnum(CONST_ME_POISONAREA) + registerEnum(CONST_ME_SOUND_YELLOW) + registerEnum(CONST_ME_SOUND_PURPLE) + registerEnum(CONST_ME_SOUND_BLUE) + registerEnum(CONST_ME_SOUND_WHITE) + registerEnum(CONST_ME_BUBBLES) + registerEnum(CONST_ME_CRAPS) + registerEnum(CONST_ME_GIFT_WRAPS) + registerEnum(CONST_ME_FIREWORK_YELLOW) + registerEnum(CONST_ME_FIREWORK_RED) + registerEnum(CONST_ME_FIREWORK_BLUE) + registerEnum(CONST_ME_STUN) + registerEnum(CONST_ME_SLEEP) + registerEnum(CONST_ME_WATERCREATURE) + registerEnum(CONST_ME_GROUNDSHAKER) + registerEnum(CONST_ME_HEARTS) + registerEnum(CONST_ME_FIREATTACK) + registerEnum(CONST_ME_ENERGYAREA) + registerEnum(CONST_ME_SMALLCLOUDS) + registerEnum(CONST_ME_HOLYDAMAGE) + registerEnum(CONST_ME_BIGCLOUDS) + registerEnum(CONST_ME_ICEAREA) + registerEnum(CONST_ME_ICETORNADO) + registerEnum(CONST_ME_ICEATTACK) + registerEnum(CONST_ME_STONES) + registerEnum(CONST_ME_SMALLPLANTS) + registerEnum(CONST_ME_CARNIPHILA) + registerEnum(CONST_ME_PURPLEENERGY) + registerEnum(CONST_ME_YELLOWENERGY) + registerEnum(CONST_ME_HOLYAREA) + registerEnum(CONST_ME_BIGPLANTS) + registerEnum(CONST_ME_CAKE) + registerEnum(CONST_ME_GIANTICE) + registerEnum(CONST_ME_WATERSPLASH) + registerEnum(CONST_ME_PLANTATTACK) + registerEnum(CONST_ME_TUTORIALARROW) + registerEnum(CONST_ME_TUTORIALSQUARE) + registerEnum(CONST_ME_MIRRORHORIZONTAL) + registerEnum(CONST_ME_MIRRORVERTICAL) + registerEnum(CONST_ME_SKULLHORIZONTAL) + registerEnum(CONST_ME_SKULLVERTICAL) + registerEnum(CONST_ME_ASSASSIN) + registerEnum(CONST_ME_STEPSHORIZONTAL) + registerEnum(CONST_ME_BLOODYSTEPS) + registerEnum(CONST_ME_STEPSVERTICAL) + registerEnum(CONST_ME_YALAHARIGHOST) + registerEnum(CONST_ME_BATS) + registerEnum(CONST_ME_SMOKE) + registerEnum(CONST_ME_INSECTS) + registerEnum(CONST_ME_DRAGONHEAD) + registerEnum(CONST_ME_ORCSHAMAN) + registerEnum(CONST_ME_ORCSHAMAN_FIRE) + registerEnum(CONST_ME_THUNDER) + registerEnum(CONST_ME_FERUMBRAS) + registerEnum(CONST_ME_CONFETTI_HORIZONTAL) + registerEnum(CONST_ME_CONFETTI_VERTICAL) + registerEnum(CONST_ME_BLACKSMOKE) + registerEnum(CONST_ME_REDSMOKE) + registerEnum(CONST_ME_YELLOWSMOKE) + registerEnum(CONST_ME_GREENSMOKE) + registerEnum(CONST_ME_PURPLESMOKE) + registerEnum(CONST_ME_EARLY_THUNDER) + registerEnum(CONST_ME_RAGIAZ_BONECAPSULE) + registerEnum(CONST_ME_CRITICAL_DAMAGE) + registerEnum(CONST_ME_PLUNGING_FISH) + + registerEnum(CONST_ANI_NONE) + registerEnum(CONST_ANI_SPEAR) + registerEnum(CONST_ANI_BOLT) + registerEnum(CONST_ANI_ARROW) + registerEnum(CONST_ANI_FIRE) + registerEnum(CONST_ANI_ENERGY) + registerEnum(CONST_ANI_POISONARROW) + registerEnum(CONST_ANI_BURSTARROW) + registerEnum(CONST_ANI_THROWINGSTAR) + registerEnum(CONST_ANI_THROWINGKNIFE) + registerEnum(CONST_ANI_SMALLSTONE) + registerEnum(CONST_ANI_DEATH) + registerEnum(CONST_ANI_LARGEROCK) + registerEnum(CONST_ANI_SNOWBALL) + registerEnum(CONST_ANI_POWERBOLT) + registerEnum(CONST_ANI_POISON) + registerEnum(CONST_ANI_INFERNALBOLT) + registerEnum(CONST_ANI_HUNTINGSPEAR) + registerEnum(CONST_ANI_ENCHANTEDSPEAR) + registerEnum(CONST_ANI_REDSTAR) + registerEnum(CONST_ANI_GREENSTAR) + registerEnum(CONST_ANI_ROYALSPEAR) + registerEnum(CONST_ANI_SNIPERARROW) + registerEnum(CONST_ANI_ONYXARROW) + registerEnum(CONST_ANI_PIERCINGBOLT) + registerEnum(CONST_ANI_WHIRLWINDSWORD) + registerEnum(CONST_ANI_WHIRLWINDAXE) + registerEnum(CONST_ANI_WHIRLWINDCLUB) + registerEnum(CONST_ANI_ETHEREALSPEAR) + registerEnum(CONST_ANI_ICE) + registerEnum(CONST_ANI_EARTH) + registerEnum(CONST_ANI_HOLY) + registerEnum(CONST_ANI_SUDDENDEATH) + registerEnum(CONST_ANI_FLASHARROW) + registerEnum(CONST_ANI_FLAMMINGARROW) + registerEnum(CONST_ANI_SHIVERARROW) + registerEnum(CONST_ANI_ENERGYBALL) + registerEnum(CONST_ANI_SMALLICE) + registerEnum(CONST_ANI_SMALLHOLY) + registerEnum(CONST_ANI_SMALLEARTH) + registerEnum(CONST_ANI_EARTHARROW) + registerEnum(CONST_ANI_EXPLOSION) + registerEnum(CONST_ANI_CAKE) + registerEnum(CONST_ANI_TARSALARROW) + registerEnum(CONST_ANI_VORTEXBOLT) + registerEnum(CONST_ANI_PRISMATICBOLT) + registerEnum(CONST_ANI_CRYSTALLINEARROW) + registerEnum(CONST_ANI_DRILLBOLT) + registerEnum(CONST_ANI_ENVENOMEDARROW) + registerEnum(CONST_ANI_GLOOTHSPEAR) + registerEnum(CONST_ANI_SIMPLEARROW) + registerEnum(CONST_ANI_WEAPONTYPE) + + registerEnum(CONST_PROP_BLOCKSOLID) + registerEnum(CONST_PROP_HASHEIGHT) + registerEnum(CONST_PROP_BLOCKPROJECTILE) + registerEnum(CONST_PROP_BLOCKPATH) + registerEnum(CONST_PROP_ISVERTICAL) + registerEnum(CONST_PROP_ISHORIZONTAL) + registerEnum(CONST_PROP_MOVEABLE) + registerEnum(CONST_PROP_IMMOVABLEBLOCKSOLID) + registerEnum(CONST_PROP_IMMOVABLEBLOCKPATH) + registerEnum(CONST_PROP_IMMOVABLENOFIELDBLOCKPATH) + registerEnum(CONST_PROP_NOFIELDBLOCKPATH) + registerEnum(CONST_PROP_SUPPORTHANGABLE) + + registerEnum(CONST_SLOT_HEAD) + registerEnum(CONST_SLOT_NECKLACE) + registerEnum(CONST_SLOT_BACKPACK) + registerEnum(CONST_SLOT_ARMOR) + registerEnum(CONST_SLOT_RIGHT) + registerEnum(CONST_SLOT_LEFT) + registerEnum(CONST_SLOT_LEGS) + registerEnum(CONST_SLOT_FEET) + registerEnum(CONST_SLOT_RING) + registerEnum(CONST_SLOT_AMMO) + + registerEnum(CREATURE_EVENT_NONE) + registerEnum(CREATURE_EVENT_LOGIN) + registerEnum(CREATURE_EVENT_LOGOUT) + registerEnum(CREATURE_EVENT_THINK) + registerEnum(CREATURE_EVENT_PREPAREDEATH) + registerEnum(CREATURE_EVENT_DEATH) + registerEnum(CREATURE_EVENT_KILL) + registerEnum(CREATURE_EVENT_ADVANCE) + registerEnum(CREATURE_EVENT_MODALWINDOW) + registerEnum(CREATURE_EVENT_TEXTEDIT) + registerEnum(CREATURE_EVENT_HEALTHCHANGE) + registerEnum(CREATURE_EVENT_MANACHANGE) + registerEnum(CREATURE_EVENT_EXTENDED_OPCODE) + + registerEnum(GAME_STATE_STARTUP) + registerEnum(GAME_STATE_INIT) + registerEnum(GAME_STATE_NORMAL) + registerEnum(GAME_STATE_CLOSED) + registerEnum(GAME_STATE_SHUTDOWN) + registerEnum(GAME_STATE_CLOSING) + registerEnum(GAME_STATE_MAINTAIN) + + registerEnum(MESSAGE_STATUS_CONSOLE_BLUE) + registerEnum(MESSAGE_STATUS_CONSOLE_RED) + registerEnum(MESSAGE_STATUS_DEFAULT) + registerEnum(MESSAGE_STATUS_WARNING) + registerEnum(MESSAGE_EVENT_ADVANCE) + registerEnum(MESSAGE_STATUS_SMALL) + registerEnum(MESSAGE_INFO_DESCR) + registerEnum(MESSAGE_DAMAGE_DEALT) + registerEnum(MESSAGE_DAMAGE_RECEIVED) + registerEnum(MESSAGE_HEALED) + registerEnum(MESSAGE_EXPERIENCE) + registerEnum(MESSAGE_DAMAGE_OTHERS) + registerEnum(MESSAGE_HEALED_OTHERS) + registerEnum(MESSAGE_EXPERIENCE_OTHERS) + registerEnum(MESSAGE_EVENT_DEFAULT) + registerEnum(MESSAGE_GUILD) + registerEnum(MESSAGE_PARTY_MANAGEMENT) + registerEnum(MESSAGE_PARTY) + registerEnum(MESSAGE_EVENT_ORANGE) + registerEnum(MESSAGE_STATUS_CONSOLE_ORANGE) + registerEnum(MESSAGE_LOOT) + + registerEnum(CREATURETYPE_PLAYER) + registerEnum(CREATURETYPE_MONSTER) + registerEnum(CREATURETYPE_NPC) + registerEnum(CREATURETYPE_SUMMON_OWN) + registerEnum(CREATURETYPE_SUMMON_OTHERS) + + registerEnum(CLIENTOS_LINUX) + registerEnum(CLIENTOS_WINDOWS) + registerEnum(CLIENTOS_FLASH) + registerEnum(CLIENTOS_OTCLIENT_LINUX) + registerEnum(CLIENTOS_OTCLIENT_WINDOWS) + registerEnum(CLIENTOS_OTCLIENT_MAC) + + registerEnum(FIGHTMODE_ATTACK) + registerEnum(FIGHTMODE_BALANCED) + registerEnum(FIGHTMODE_DEFENSE) + + registerEnum(ITEM_ATTRIBUTE_NONE) + registerEnum(ITEM_ATTRIBUTE_ACTIONID) + registerEnum(ITEM_ATTRIBUTE_UNIQUEID) + registerEnum(ITEM_ATTRIBUTE_DESCRIPTION) + registerEnum(ITEM_ATTRIBUTE_TEXT) + registerEnum(ITEM_ATTRIBUTE_DATE) + registerEnum(ITEM_ATTRIBUTE_WRITER) + registerEnum(ITEM_ATTRIBUTE_NAME) + registerEnum(ITEM_ATTRIBUTE_ARTICLE) + registerEnum(ITEM_ATTRIBUTE_PLURALNAME) + registerEnum(ITEM_ATTRIBUTE_WEIGHT) + registerEnum(ITEM_ATTRIBUTE_ATTACK) + registerEnum(ITEM_ATTRIBUTE_DEFENSE) + registerEnum(ITEM_ATTRIBUTE_EXTRADEFENSE) + registerEnum(ITEM_ATTRIBUTE_ARMOR) + registerEnum(ITEM_ATTRIBUTE_HITCHANCE) + registerEnum(ITEM_ATTRIBUTE_SHOOTRANGE) + registerEnum(ITEM_ATTRIBUTE_OWNER) + registerEnum(ITEM_ATTRIBUTE_DURATION) + registerEnum(ITEM_ATTRIBUTE_DECAYSTATE) + registerEnum(ITEM_ATTRIBUTE_CORPSEOWNER) + registerEnum(ITEM_ATTRIBUTE_CHARGES) + registerEnum(ITEM_ATTRIBUTE_FLUIDTYPE) + registerEnum(ITEM_ATTRIBUTE_DOORID) + + registerEnum(ITEM_TYPE_DEPOT) + registerEnum(ITEM_TYPE_MAILBOX) + registerEnum(ITEM_TYPE_TRASHHOLDER) + registerEnum(ITEM_TYPE_CONTAINER) + registerEnum(ITEM_TYPE_DOOR) + registerEnum(ITEM_TYPE_MAGICFIELD) + registerEnum(ITEM_TYPE_TELEPORT) + registerEnum(ITEM_TYPE_BED) + registerEnum(ITEM_TYPE_KEY) + registerEnum(ITEM_TYPE_RUNE) + + registerEnum(ITEM_BAG) + registerEnum(ITEM_SHOPPING_BAG) + registerEnum(ITEM_GOLD_COIN) + registerEnum(ITEM_PLATINUM_COIN) + registerEnum(ITEM_CRYSTAL_COIN) + registerEnum(ITEM_AMULETOFLOSS) + registerEnum(ITEM_PARCEL) + registerEnum(ITEM_LABEL) + registerEnum(ITEM_FIREFIELD_PVP_FULL) + registerEnum(ITEM_FIREFIELD_PVP_MEDIUM) + registerEnum(ITEM_FIREFIELD_PVP_SMALL) + registerEnum(ITEM_FIREFIELD_PERSISTENT_FULL) + registerEnum(ITEM_FIREFIELD_PERSISTENT_MEDIUM) + registerEnum(ITEM_FIREFIELD_PERSISTENT_SMALL) + registerEnum(ITEM_FIREFIELD_NOPVP) + registerEnum(ITEM_POISONFIELD_PVP) + registerEnum(ITEM_POISONFIELD_PERSISTENT) + registerEnum(ITEM_POISONFIELD_NOPVP) + registerEnum(ITEM_ENERGYFIELD_PVP) + registerEnum(ITEM_ENERGYFIELD_PERSISTENT) + registerEnum(ITEM_ENERGYFIELD_NOPVP) + registerEnum(ITEM_MAGICWALL) + registerEnum(ITEM_MAGICWALL_PERSISTENT) + registerEnum(ITEM_MAGICWALL_SAFE) + registerEnum(ITEM_WILDGROWTH) + registerEnum(ITEM_WILDGROWTH_PERSISTENT) + registerEnum(ITEM_WILDGROWTH_SAFE) + + registerEnum(PlayerFlag_CannotUseCombat) + registerEnum(PlayerFlag_CannotAttackPlayer) + registerEnum(PlayerFlag_CannotAttackMonster) + registerEnum(PlayerFlag_CannotBeAttacked) + registerEnum(PlayerFlag_CanConvinceAll) + registerEnum(PlayerFlag_CanSummonAll) + registerEnum(PlayerFlag_CanIllusionAll) + registerEnum(PlayerFlag_CanSenseInvisibility) + registerEnum(PlayerFlag_IgnoredByMonsters) + registerEnum(PlayerFlag_NotGainInFight) + registerEnum(PlayerFlag_HasInfiniteMana) + registerEnum(PlayerFlag_HasInfiniteSoul) + registerEnum(PlayerFlag_HasNoExhaustion) + registerEnum(PlayerFlag_CannotUseSpells) + registerEnum(PlayerFlag_CannotPickupItem) + registerEnum(PlayerFlag_CanAlwaysLogin) + registerEnum(PlayerFlag_CanBroadcast) + registerEnum(PlayerFlag_CanEditHouses) + registerEnum(PlayerFlag_CannotBeBanned) + registerEnum(PlayerFlag_CannotBePushed) + registerEnum(PlayerFlag_HasInfiniteCapacity) + registerEnum(PlayerFlag_CanPushAllCreatures) + registerEnum(PlayerFlag_CanTalkRedPrivate) + registerEnum(PlayerFlag_CanTalkRedChannel) + registerEnum(PlayerFlag_TalkOrangeHelpChannel) + registerEnum(PlayerFlag_NotGainExperience) + registerEnum(PlayerFlag_NotGainMana) + registerEnum(PlayerFlag_NotGainHealth) + registerEnum(PlayerFlag_NotGainSkill) + registerEnum(PlayerFlag_SetMaxSpeed) + registerEnum(PlayerFlag_SpecialVIP) + registerEnum(PlayerFlag_NotGenerateLoot) + registerEnum(PlayerFlag_CanTalkRedChannelAnonymous) + registerEnum(PlayerFlag_IgnoreProtectionZone) + registerEnum(PlayerFlag_IgnoreSpellCheck) + registerEnum(PlayerFlag_IgnoreWeaponCheck) + registerEnum(PlayerFlag_CannotBeMuted) + registerEnum(PlayerFlag_IsAlwaysPremium) + + registerEnum(PLAYERSEX_FEMALE) + registerEnum(PLAYERSEX_MALE) + + registerEnum(REPORT_REASON_NAMEINAPPROPRIATE) + registerEnum(REPORT_REASON_NAMEPOORFORMATTED) + registerEnum(REPORT_REASON_NAMEADVERTISING) + registerEnum(REPORT_REASON_NAMEUNFITTING) + registerEnum(REPORT_REASON_NAMERULEVIOLATION) + registerEnum(REPORT_REASON_INSULTINGSTATEMENT) + registerEnum(REPORT_REASON_SPAMMING) + registerEnum(REPORT_REASON_ADVERTISINGSTATEMENT) + registerEnum(REPORT_REASON_UNFITTINGSTATEMENT) + registerEnum(REPORT_REASON_LANGUAGESTATEMENT) + registerEnum(REPORT_REASON_DISCLOSURE) + registerEnum(REPORT_REASON_RULEVIOLATION) + registerEnum(REPORT_REASON_STATEMENT_BUGABUSE) + registerEnum(REPORT_REASON_UNOFFICIALSOFTWARE) + registerEnum(REPORT_REASON_PRETENDING) + registerEnum(REPORT_REASON_HARASSINGOWNERS) + registerEnum(REPORT_REASON_FALSEINFO) + registerEnum(REPORT_REASON_ACCOUNTSHARING) + registerEnum(REPORT_REASON_STEALINGDATA) + registerEnum(REPORT_REASON_SERVICEATTACKING) + registerEnum(REPORT_REASON_SERVICEAGREEMENT) + + registerEnum(REPORT_TYPE_NAME) + registerEnum(REPORT_TYPE_STATEMENT) + registerEnum(REPORT_TYPE_BOT) + + registerEnum(VOCATION_NONE) + + registerEnum(SKILL_FIST) + registerEnum(SKILL_CLUB) + registerEnum(SKILL_SWORD) + registerEnum(SKILL_AXE) + registerEnum(SKILL_DISTANCE) + registerEnum(SKILL_SHIELD) + registerEnum(SKILL_FISHING) + registerEnum(SKILL_MAGLEVEL) + registerEnum(SKILL_LEVEL) + + registerEnum(SPECIALSKILL_CRITICALHITCHANCE) + registerEnum(SPECIALSKILL_CRITICALHITAMOUNT) + registerEnum(SPECIALSKILL_LIFELEECHCHANCE) + registerEnum(SPECIALSKILL_LIFELEECHAMOUNT) + registerEnum(SPECIALSKILL_MANALEECHCHANCE) + registerEnum(SPECIALSKILL_MANALEECHAMOUNT) + + registerEnum(SKULL_NONE) + registerEnum(SKULL_YELLOW) + registerEnum(SKULL_GREEN) + registerEnum(SKULL_WHITE) + registerEnum(SKULL_RED) + registerEnum(SKULL_BLACK) + registerEnum(SKULL_ORANGE) + + registerEnum(TALKTYPE_SAY) + registerEnum(TALKTYPE_WHISPER) + registerEnum(TALKTYPE_YELL) + registerEnum(TALKTYPE_PRIVATE_FROM) + registerEnum(TALKTYPE_PRIVATE_TO) + registerEnum(TALKTYPE_CHANNEL_Y) + registerEnum(TALKTYPE_CHANNEL_O) + registerEnum(TALKTYPE_PRIVATE_NP) + registerEnum(TALKTYPE_PRIVATE_PN) + registerEnum(TALKTYPE_BROADCAST) + registerEnum(TALKTYPE_CHANNEL_R1) + registerEnum(TALKTYPE_PRIVATE_RED_FROM) + registerEnum(TALKTYPE_PRIVATE_RED_TO) + registerEnum(TALKTYPE_MONSTER_SAY) + registerEnum(TALKTYPE_MONSTER_YELL) + registerEnum(TALKTYPE_CHANNEL_R2) + + registerEnum(TEXTCOLOR_BLUE) + registerEnum(TEXTCOLOR_LIGHTGREEN) + registerEnum(TEXTCOLOR_LIGHTBLUE) + registerEnum(TEXTCOLOR_MAYABLUE) + registerEnum(TEXTCOLOR_DARKRED) + registerEnum(TEXTCOLOR_LIGHTGREY) + registerEnum(TEXTCOLOR_SKYBLUE) + registerEnum(TEXTCOLOR_PURPLE) + registerEnum(TEXTCOLOR_ELECTRICPURPLE) + registerEnum(TEXTCOLOR_RED) + registerEnum(TEXTCOLOR_PASTELRED) + registerEnum(TEXTCOLOR_ORANGE) + registerEnum(TEXTCOLOR_YELLOW) + registerEnum(TEXTCOLOR_WHITE_EXP) + registerEnum(TEXTCOLOR_NONE) + + registerEnum(TILESTATE_NONE) + registerEnum(TILESTATE_PROTECTIONZONE) + registerEnum(TILESTATE_NOPVPZONE) + registerEnum(TILESTATE_NOLOGOUT) + registerEnum(TILESTATE_PVPZONE) + registerEnum(TILESTATE_FLOORCHANGE) + registerEnum(TILESTATE_FLOORCHANGE_DOWN) + registerEnum(TILESTATE_FLOORCHANGE_NORTH) + registerEnum(TILESTATE_FLOORCHANGE_SOUTH) + registerEnum(TILESTATE_FLOORCHANGE_EAST) + registerEnum(TILESTATE_FLOORCHANGE_WEST) + registerEnum(TILESTATE_TELEPORT) + registerEnum(TILESTATE_MAGICFIELD) + registerEnum(TILESTATE_MAILBOX) + registerEnum(TILESTATE_TRASHHOLDER) + registerEnum(TILESTATE_BED) + registerEnum(TILESTATE_DEPOT) + registerEnum(TILESTATE_BLOCKSOLID) + registerEnum(TILESTATE_BLOCKPATH) + registerEnum(TILESTATE_IMMOVABLEBLOCKSOLID) + registerEnum(TILESTATE_IMMOVABLEBLOCKPATH) + registerEnum(TILESTATE_IMMOVABLENOFIELDBLOCKPATH) + registerEnum(TILESTATE_NOFIELDBLOCKPATH) + registerEnum(TILESTATE_FLOORCHANGE_SOUTH_ALT) + registerEnum(TILESTATE_FLOORCHANGE_EAST_ALT) + registerEnum(TILESTATE_SUPPORTS_HANGABLE) + + registerEnum(WEAPON_NONE) + registerEnum(WEAPON_SWORD) + registerEnum(WEAPON_CLUB) + registerEnum(WEAPON_AXE) + registerEnum(WEAPON_SHIELD) + registerEnum(WEAPON_DISTANCE) + registerEnum(WEAPON_WAND) + registerEnum(WEAPON_AMMO) + + registerEnum(WORLD_TYPE_NO_PVP) + registerEnum(WORLD_TYPE_PVP) + registerEnum(WORLD_TYPE_PVP_ENFORCED) + + // Use with container:addItem, container:addItemEx and possibly other functions. + registerEnum(FLAG_NOLIMIT) + registerEnum(FLAG_IGNOREBLOCKITEM) + registerEnum(FLAG_IGNOREBLOCKCREATURE) + registerEnum(FLAG_CHILDISOWNER) + registerEnum(FLAG_PATHFINDING) + registerEnum(FLAG_IGNOREFIELDDAMAGE) + registerEnum(FLAG_IGNORENOTMOVEABLE) + registerEnum(FLAG_IGNOREAUTOSTACK) + + // Use with itemType:getSlotPosition + registerEnum(SLOTP_WHEREEVER) + registerEnum(SLOTP_HEAD) + registerEnum(SLOTP_NECKLACE) + registerEnum(SLOTP_BACKPACK) + registerEnum(SLOTP_ARMOR) + registerEnum(SLOTP_RIGHT) + registerEnum(SLOTP_LEFT) + registerEnum(SLOTP_LEGS) + registerEnum(SLOTP_FEET) + registerEnum(SLOTP_RING) + registerEnum(SLOTP_AMMO) + registerEnum(SLOTP_DEPOT) + registerEnum(SLOTP_TWO_HAND) + + // Use with combat functions + registerEnum(ORIGIN_NONE) + registerEnum(ORIGIN_CONDITION) + registerEnum(ORIGIN_SPELL) + registerEnum(ORIGIN_MELEE) + registerEnum(ORIGIN_RANGED) + + // Use with house:getAccessList, house:setAccessList + registerEnum(GUEST_LIST) + registerEnum(SUBOWNER_LIST) + + // Use with npc:setSpeechBubble + registerEnum(SPEECHBUBBLE_NONE) + registerEnum(SPEECHBUBBLE_NORMAL) + registerEnum(SPEECHBUBBLE_TRADE) + registerEnum(SPEECHBUBBLE_QUEST) + registerEnum(SPEECHBUBBLE_QUESTTRADER) + + // Use with player:addMapMark + registerEnum(MAPMARK_TICK) + registerEnum(MAPMARK_QUESTION) + registerEnum(MAPMARK_EXCLAMATION) + registerEnum(MAPMARK_STAR) + registerEnum(MAPMARK_CROSS) + registerEnum(MAPMARK_TEMPLE) + registerEnum(MAPMARK_KISS) + registerEnum(MAPMARK_SHOVEL) + registerEnum(MAPMARK_SWORD) + registerEnum(MAPMARK_FLAG) + registerEnum(MAPMARK_LOCK) + registerEnum(MAPMARK_BAG) + registerEnum(MAPMARK_SKULL) + registerEnum(MAPMARK_DOLLAR) + registerEnum(MAPMARK_REDNORTH) + registerEnum(MAPMARK_REDSOUTH) + registerEnum(MAPMARK_REDEAST) + registerEnum(MAPMARK_REDWEST) + registerEnum(MAPMARK_GREENNORTH) + registerEnum(MAPMARK_GREENSOUTH) + + // Use with Game.getReturnMessage + registerEnum(RETURNVALUE_NOERROR) + registerEnum(RETURNVALUE_NOTPOSSIBLE) + registerEnum(RETURNVALUE_NOTENOUGHROOM) + registerEnum(RETURNVALUE_PLAYERISPZLOCKED) + registerEnum(RETURNVALUE_PLAYERISNOTINVITED) + registerEnum(RETURNVALUE_CANNOTTHROW) + registerEnum(RETURNVALUE_THEREISNOWAY) + registerEnum(RETURNVALUE_DESTINATIONOUTOFREACH) + registerEnum(RETURNVALUE_CREATUREBLOCK) + registerEnum(RETURNVALUE_NOTMOVEABLE) + registerEnum(RETURNVALUE_DROPTWOHANDEDITEM) + registerEnum(RETURNVALUE_BOTHHANDSNEEDTOBEFREE) + registerEnum(RETURNVALUE_CANONLYUSEONEWEAPON) + registerEnum(RETURNVALUE_NEEDEXCHANGE) + registerEnum(RETURNVALUE_CANNOTBEDRESSED) + registerEnum(RETURNVALUE_PUTTHISOBJECTINYOURHAND) + registerEnum(RETURNVALUE_PUTTHISOBJECTINBOTHHANDS) + registerEnum(RETURNVALUE_TOOFARAWAY) + registerEnum(RETURNVALUE_FIRSTGODOWNSTAIRS) + registerEnum(RETURNVALUE_FIRSTGOUPSTAIRS) + registerEnum(RETURNVALUE_CONTAINERNOTENOUGHROOM) + registerEnum(RETURNVALUE_NOTENOUGHCAPACITY) + registerEnum(RETURNVALUE_CANNOTPICKUP) + registerEnum(RETURNVALUE_THISISIMPOSSIBLE) + registerEnum(RETURNVALUE_DEPOTISFULL) + registerEnum(RETURNVALUE_CREATUREDOESNOTEXIST) + registerEnum(RETURNVALUE_CANNOTUSETHISOBJECT) + registerEnum(RETURNVALUE_PLAYERWITHTHISNAMEISNOTONLINE) + registerEnum(RETURNVALUE_NOTREQUIREDLEVELTOUSERUNE) + registerEnum(RETURNVALUE_YOUAREALREADYTRADING) + registerEnum(RETURNVALUE_THISPLAYERISALREADYTRADING) + registerEnum(RETURNVALUE_YOUMAYNOTLOGOUTDURINGAFIGHT) + registerEnum(RETURNVALUE_DIRECTPLAYERSHOOT) + registerEnum(RETURNVALUE_NOTENOUGHLEVEL) + registerEnum(RETURNVALUE_NOTENOUGHMAGICLEVEL) + registerEnum(RETURNVALUE_NOTENOUGHMANA) + registerEnum(RETURNVALUE_NOTENOUGHSOUL) + registerEnum(RETURNVALUE_YOUAREEXHAUSTED) + registerEnum(RETURNVALUE_YOUCANNOTUSEOBJECTSTHATFAST) + registerEnum(RETURNVALUE_PLAYERISNOTREACHABLE) + registerEnum(RETURNVALUE_CANONLYUSETHISRUNEONCREATURES) + registerEnum(RETURNVALUE_ACTIONNOTPERMITTEDINPROTECTIONZONE) + registerEnum(RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER) + registerEnum(RETURNVALUE_YOUMAYNOTATTACKAPERSONINPROTECTIONZONE) + registerEnum(RETURNVALUE_YOUMAYNOTATTACKAPERSONWHILEINPROTECTIONZONE) + registerEnum(RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE) + registerEnum(RETURNVALUE_YOUCANONLYUSEITONCREATURES) + registerEnum(RETURNVALUE_CREATUREISNOTREACHABLE) + registerEnum(RETURNVALUE_TURNSECUREMODETOATTACKUNMARKEDPLAYERS) + registerEnum(RETURNVALUE_YOUNEEDPREMIUMACCOUNT) + registerEnum(RETURNVALUE_YOUNEEDTOLEARNTHISSPELL) + registerEnum(RETURNVALUE_YOURVOCATIONCANNOTUSETHISSPELL) + registerEnum(RETURNVALUE_YOUNEEDAWEAPONTOUSETHISSPELL) + registerEnum(RETURNVALUE_PLAYERISPZLOCKEDLEAVEPVPZONE) + registerEnum(RETURNVALUE_PLAYERISPZLOCKEDENTERPVPZONE) + registerEnum(RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE) + registerEnum(RETURNVALUE_YOUCANNOTLOGOUTHERE) + registerEnum(RETURNVALUE_YOUNEEDAMAGICITEMTOCASTSPELL) + registerEnum(RETURNVALUE_CANNOTCONJUREITEMHERE) + registerEnum(RETURNVALUE_YOUNEEDTOSPLITYOURSPEARS) + registerEnum(RETURNVALUE_NAMEISTOOAMBIGUOUS) + registerEnum(RETURNVALUE_CANONLYUSEONESHIELD) + registerEnum(RETURNVALUE_NOPARTYMEMBERSINRANGE) + registerEnum(RETURNVALUE_YOUARENOTTHEOWNER) + registerEnum(RETURNVALUE_TRADEPLAYERFARAWAY) + registerEnum(RETURNVALUE_YOUDONTOWNTHISHOUSE) + registerEnum(RETURNVALUE_TRADEPLAYERALREADYOWNSAHOUSE) + registerEnum(RETURNVALUE_TRADEPLAYERHIGHESTBIDDER) + registerEnum(RETURNVALUE_YOUCANNOTTRADETHISHOUSE) + registerEnum(RETURNVALUE_YOUDONTHAVEREQUIREDPROFESSION) + + registerEnum(RELOAD_TYPE_ALL) + registerEnum(RELOAD_TYPE_ACTIONS) + registerEnum(RELOAD_TYPE_CHAT) + registerEnum(RELOAD_TYPE_CONFIG) + registerEnum(RELOAD_TYPE_CREATURESCRIPTS) + registerEnum(RELOAD_TYPE_EVENTS) + registerEnum(RELOAD_TYPE_GLOBAL) + registerEnum(RELOAD_TYPE_GLOBALEVENTS) + registerEnum(RELOAD_TYPE_ITEMS) + registerEnum(RELOAD_TYPE_MONSTERS) + registerEnum(RELOAD_TYPE_MOUNTS) + registerEnum(RELOAD_TYPE_MOVEMENTS) + registerEnum(RELOAD_TYPE_NPCS) + registerEnum(RELOAD_TYPE_QUESTS) + registerEnum(RELOAD_TYPE_RAIDS) + registerEnum(RELOAD_TYPE_SCRIPTS) + registerEnum(RELOAD_TYPE_SPELLS) + registerEnum(RELOAD_TYPE_TALKACTIONS) + registerEnum(RELOAD_TYPE_WEAPONS) + + registerEnum(ZONE_PROTECTION) + registerEnum(ZONE_NOPVP) + registerEnum(ZONE_PVP) + registerEnum(ZONE_NOLOGOUT) + registerEnum(ZONE_NORMAL) + + registerEnum(MAX_LOOTCHANCE) + + registerEnum(SPELL_INSTANT) + registerEnum(SPELL_RUNE) + + registerEnum(MONSTERS_EVENT_THINK) + registerEnum(MONSTERS_EVENT_APPEAR) + registerEnum(MONSTERS_EVENT_DISAPPEAR) + registerEnum(MONSTERS_EVENT_MOVE) + registerEnum(MONSTERS_EVENT_SAY) + + // _G + registerGlobalVariable("INDEX_WHEREEVER", INDEX_WHEREEVER); + registerGlobalBoolean("VIRTUAL_PARENT", true); + + registerGlobalMethod("isType", LuaScriptInterface::luaIsType); + registerGlobalMethod("rawgetmetatable", LuaScriptInterface::luaRawGetMetatable); + + // configKeys + registerTable("configKeys"); + + registerEnumIn("configKeys", ConfigManager::ALLOW_CHANGEOUTFIT) + registerEnumIn("configKeys", ConfigManager::ONE_PLAYER_ON_ACCOUNT) + registerEnumIn("configKeys", ConfigManager::AIMBOT_HOTKEY_ENABLED) + registerEnumIn("configKeys", ConfigManager::REMOVE_RUNE_CHARGES) + registerEnumIn("configKeys", ConfigManager::REMOVE_WEAPON_AMMO) + registerEnumIn("configKeys", ConfigManager::REMOVE_WEAPON_CHARGES) + registerEnumIn("configKeys", ConfigManager::REMOVE_POTION_CHARGES) + registerEnumIn("configKeys", ConfigManager::EXPERIENCE_FROM_PLAYERS) + registerEnumIn("configKeys", ConfigManager::FREE_PREMIUM) + registerEnumIn("configKeys", ConfigManager::REPLACE_KICK_ON_LOGIN) + registerEnumIn("configKeys", ConfigManager::ALLOW_CLONES) + registerEnumIn("configKeys", ConfigManager::BIND_ONLY_GLOBAL_ADDRESS) + registerEnumIn("configKeys", ConfigManager::OPTIMIZE_DATABASE) + registerEnumIn("configKeys", ConfigManager::MARKET_PREMIUM) + registerEnumIn("configKeys", ConfigManager::EMOTE_SPELLS) + registerEnumIn("configKeys", ConfigManager::STAMINA_SYSTEM) + registerEnumIn("configKeys", ConfigManager::WARN_UNSAFE_SCRIPTS) + registerEnumIn("configKeys", ConfigManager::CONVERT_UNSAFE_SCRIPTS) + registerEnumIn("configKeys", ConfigManager::CLASSIC_EQUIPMENT_SLOTS) + registerEnumIn("configKeys", ConfigManager::CLASSIC_ATTACK_SPEED) + registerEnumIn("configKeys", ConfigManager::SERVER_SAVE_NOTIFY_MESSAGE) + registerEnumIn("configKeys", ConfigManager::SERVER_SAVE_NOTIFY_DURATION) + registerEnumIn("configKeys", ConfigManager::SERVER_SAVE_CLEAN_MAP) + registerEnumIn("configKeys", ConfigManager::SERVER_SAVE_CLOSE) + registerEnumIn("configKeys", ConfigManager::SERVER_SAVE_SHUTDOWN) + registerEnumIn("configKeys", ConfigManager::ONLINE_OFFLINE_CHARLIST) + + registerEnumIn("configKeys", ConfigManager::MAP_NAME) + registerEnumIn("configKeys", ConfigManager::HOUSE_RENT_PERIOD) + registerEnumIn("configKeys", ConfigManager::SERVER_NAME) + registerEnumIn("configKeys", ConfigManager::OWNER_NAME) + registerEnumIn("configKeys", ConfigManager::OWNER_EMAIL) + registerEnumIn("configKeys", ConfigManager::URL) + registerEnumIn("configKeys", ConfigManager::LOCATION) + registerEnumIn("configKeys", ConfigManager::IP) + registerEnumIn("configKeys", ConfigManager::MOTD) + registerEnumIn("configKeys", ConfigManager::WORLD_TYPE) + registerEnumIn("configKeys", ConfigManager::MYSQL_HOST) + registerEnumIn("configKeys", ConfigManager::MYSQL_USER) + registerEnumIn("configKeys", ConfigManager::MYSQL_PASS) + registerEnumIn("configKeys", ConfigManager::MYSQL_DB) + registerEnumIn("configKeys", ConfigManager::MYSQL_SOCK) + registerEnumIn("configKeys", ConfigManager::DEFAULT_PRIORITY) + registerEnumIn("configKeys", ConfigManager::MAP_AUTHOR) + + registerEnumIn("configKeys", ConfigManager::SQL_PORT) + registerEnumIn("configKeys", ConfigManager::MAX_PLAYERS) + registerEnumIn("configKeys", ConfigManager::PZ_LOCKED) + registerEnumIn("configKeys", ConfigManager::DEFAULT_DESPAWNRANGE) + registerEnumIn("configKeys", ConfigManager::DEFAULT_DESPAWNRADIUS) + registerEnumIn("configKeys", ConfigManager::RATE_EXPERIENCE) + registerEnumIn("configKeys", ConfigManager::RATE_SKILL) + registerEnumIn("configKeys", ConfigManager::RATE_LOOT) + registerEnumIn("configKeys", ConfigManager::RATE_MAGIC) + registerEnumIn("configKeys", ConfigManager::RATE_SPAWN) + registerEnumIn("configKeys", ConfigManager::HOUSE_PRICE) + registerEnumIn("configKeys", ConfigManager::KILLS_TO_RED) + registerEnumIn("configKeys", ConfigManager::KILLS_TO_BLACK) + registerEnumIn("configKeys", ConfigManager::MAX_MESSAGEBUFFER) + registerEnumIn("configKeys", ConfigManager::ACTIONS_DELAY_INTERVAL) + registerEnumIn("configKeys", ConfigManager::EX_ACTIONS_DELAY_INTERVAL) + registerEnumIn("configKeys", ConfigManager::KICK_AFTER_MINUTES) + registerEnumIn("configKeys", ConfigManager::PROTECTION_LEVEL) + registerEnumIn("configKeys", ConfigManager::DEATH_LOSE_PERCENT) + registerEnumIn("configKeys", ConfigManager::STATUSQUERY_TIMEOUT) + registerEnumIn("configKeys", ConfigManager::FRAG_TIME) + registerEnumIn("configKeys", ConfigManager::WHITE_SKULL_TIME) + registerEnumIn("configKeys", ConfigManager::GAME_PORT) + registerEnumIn("configKeys", ConfigManager::LOGIN_PORT) + registerEnumIn("configKeys", ConfigManager::STATUS_PORT) + registerEnumIn("configKeys", ConfigManager::STAIRHOP_DELAY) + registerEnumIn("configKeys", ConfigManager::MARKET_OFFER_DURATION) + registerEnumIn("configKeys", ConfigManager::CHECK_EXPIRED_MARKET_OFFERS_EACH_MINUTES) + registerEnumIn("configKeys", ConfigManager::MAX_MARKET_OFFERS_AT_A_TIME_PER_PLAYER) + registerEnumIn("configKeys", ConfigManager::EXP_FROM_PLAYERS_LEVEL_RANGE) + registerEnumIn("configKeys", ConfigManager::MAX_PACKETS_PER_SECOND) + + // os + registerMethod("os", "mtime", LuaScriptInterface::luaSystemTime); + + // table + registerMethod("table", "create", LuaScriptInterface::luaTableCreate); + + // Game + registerTable("Game"); + + registerMethod("Game", "getSpectators", LuaScriptInterface::luaGameGetSpectators); + registerMethod("Game", "getPlayers", LuaScriptInterface::luaGameGetPlayers); + registerMethod("Game", "loadMap", LuaScriptInterface::luaGameLoadMap); + + registerMethod("Game", "getExperienceStage", LuaScriptInterface::luaGameGetExperienceStage); + registerMethod("Game", "getMonsterCount", LuaScriptInterface::luaGameGetMonsterCount); + registerMethod("Game", "getPlayerCount", LuaScriptInterface::luaGameGetPlayerCount); + registerMethod("Game", "getNpcCount", LuaScriptInterface::luaGameGetNpcCount); + registerMethod("Game", "getMonsterTypes", LuaScriptInterface::luaGameGetMonsterTypes); + + registerMethod("Game", "getTowns", LuaScriptInterface::luaGameGetTowns); + registerMethod("Game", "getHouses", LuaScriptInterface::luaGameGetHouses); + + registerMethod("Game", "getGameState", LuaScriptInterface::luaGameGetGameState); + registerMethod("Game", "setGameState", LuaScriptInterface::luaGameSetGameState); + + registerMethod("Game", "getWorldType", LuaScriptInterface::luaGameGetWorldType); + registerMethod("Game", "setWorldType", LuaScriptInterface::luaGameSetWorldType); + + registerMethod("Game", "getReturnMessage", LuaScriptInterface::luaGameGetReturnMessage); + + registerMethod("Game", "createItem", LuaScriptInterface::luaGameCreateItem); + registerMethod("Game", "createContainer", LuaScriptInterface::luaGameCreateContainer); + registerMethod("Game", "createMonster", LuaScriptInterface::luaGameCreateMonster); + registerMethod("Game", "createNpc", LuaScriptInterface::luaGameCreateNpc); + registerMethod("Game", "createTile", LuaScriptInterface::luaGameCreateTile); + registerMethod("Game", "createMonsterType", LuaScriptInterface::luaGameCreateMonsterType); + + registerMethod("Game", "startRaid", LuaScriptInterface::luaGameStartRaid); + + registerMethod("Game", "getClientVersion", LuaScriptInterface::luaGameGetClientVersion); + + registerMethod("Game", "reload", LuaScriptInterface::luaGameReload); + + // Variant + registerClass("Variant", "", LuaScriptInterface::luaVariantCreate); + + registerMethod("Variant", "getNumber", LuaScriptInterface::luaVariantGetNumber); + registerMethod("Variant", "getString", LuaScriptInterface::luaVariantGetString); + registerMethod("Variant", "getPosition", LuaScriptInterface::luaVariantGetPosition); + + // Position + registerClass("Position", "", LuaScriptInterface::luaPositionCreate); + registerMetaMethod("Position", "__add", LuaScriptInterface::luaPositionAdd); + registerMetaMethod("Position", "__sub", LuaScriptInterface::luaPositionSub); + registerMetaMethod("Position", "__eq", LuaScriptInterface::luaPositionCompare); + + registerMethod("Position", "getDistance", LuaScriptInterface::luaPositionGetDistance); + registerMethod("Position", "isSightClear", LuaScriptInterface::luaPositionIsSightClear); + + registerMethod("Position", "sendMagicEffect", LuaScriptInterface::luaPositionSendMagicEffect); + registerMethod("Position", "sendDistanceEffect", LuaScriptInterface::luaPositionSendDistanceEffect); + + // Tile + registerClass("Tile", "", LuaScriptInterface::luaTileCreate); + registerMetaMethod("Tile", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Tile", "getPosition", LuaScriptInterface::luaTileGetPosition); + registerMethod("Tile", "getGround", LuaScriptInterface::luaTileGetGround); + registerMethod("Tile", "getThing", LuaScriptInterface::luaTileGetThing); + registerMethod("Tile", "getThingCount", LuaScriptInterface::luaTileGetThingCount); + registerMethod("Tile", "getTopVisibleThing", LuaScriptInterface::luaTileGetTopVisibleThing); + + registerMethod("Tile", "getTopTopItem", LuaScriptInterface::luaTileGetTopTopItem); + registerMethod("Tile", "getTopDownItem", LuaScriptInterface::luaTileGetTopDownItem); + registerMethod("Tile", "getFieldItem", LuaScriptInterface::luaTileGetFieldItem); + + registerMethod("Tile", "getItemById", LuaScriptInterface::luaTileGetItemById); + registerMethod("Tile", "getItemByType", LuaScriptInterface::luaTileGetItemByType); + registerMethod("Tile", "getItemByTopOrder", LuaScriptInterface::luaTileGetItemByTopOrder); + registerMethod("Tile", "getItemCountById", LuaScriptInterface::luaTileGetItemCountById); + + registerMethod("Tile", "getBottomCreature", LuaScriptInterface::luaTileGetBottomCreature); + registerMethod("Tile", "getTopCreature", LuaScriptInterface::luaTileGetTopCreature); + registerMethod("Tile", "getBottomVisibleCreature", LuaScriptInterface::luaTileGetBottomVisibleCreature); + registerMethod("Tile", "getTopVisibleCreature", LuaScriptInterface::luaTileGetTopVisibleCreature); + + registerMethod("Tile", "getItems", LuaScriptInterface::luaTileGetItems); + registerMethod("Tile", "getItemCount", LuaScriptInterface::luaTileGetItemCount); + registerMethod("Tile", "getDownItemCount", LuaScriptInterface::luaTileGetDownItemCount); + registerMethod("Tile", "getTopItemCount", LuaScriptInterface::luaTileGetTopItemCount); + + registerMethod("Tile", "getCreatures", LuaScriptInterface::luaTileGetCreatures); + registerMethod("Tile", "getCreatureCount", LuaScriptInterface::luaTileGetCreatureCount); + + registerMethod("Tile", "getThingIndex", LuaScriptInterface::luaTileGetThingIndex); + + registerMethod("Tile", "hasProperty", LuaScriptInterface::luaTileHasProperty); + registerMethod("Tile", "hasFlag", LuaScriptInterface::luaTileHasFlag); + + registerMethod("Tile", "queryAdd", LuaScriptInterface::luaTileQueryAdd); + registerMethod("Tile", "addItem", LuaScriptInterface::luaTileAddItem); + registerMethod("Tile", "addItemEx", LuaScriptInterface::luaTileAddItemEx); + + registerMethod("Tile", "getHouse", LuaScriptInterface::luaTileGetHouse); + + // NetworkMessage + registerClass("NetworkMessage", "", LuaScriptInterface::luaNetworkMessageCreate); + registerMetaMethod("NetworkMessage", "__eq", LuaScriptInterface::luaUserdataCompare); + registerMetaMethod("NetworkMessage", "__gc", LuaScriptInterface::luaNetworkMessageDelete); + registerMethod("NetworkMessage", "delete", LuaScriptInterface::luaNetworkMessageDelete); + + registerMethod("NetworkMessage", "getByte", LuaScriptInterface::luaNetworkMessageGetByte); + registerMethod("NetworkMessage", "getU16", LuaScriptInterface::luaNetworkMessageGetU16); + registerMethod("NetworkMessage", "getU32", LuaScriptInterface::luaNetworkMessageGetU32); + registerMethod("NetworkMessage", "getU64", LuaScriptInterface::luaNetworkMessageGetU64); + registerMethod("NetworkMessage", "getString", LuaScriptInterface::luaNetworkMessageGetString); + registerMethod("NetworkMessage", "getPosition", LuaScriptInterface::luaNetworkMessageGetPosition); + + registerMethod("NetworkMessage", "addByte", LuaScriptInterface::luaNetworkMessageAddByte); + registerMethod("NetworkMessage", "addU16", LuaScriptInterface::luaNetworkMessageAddU16); + registerMethod("NetworkMessage", "addU32", LuaScriptInterface::luaNetworkMessageAddU32); + registerMethod("NetworkMessage", "addU64", LuaScriptInterface::luaNetworkMessageAddU64); + registerMethod("NetworkMessage", "addString", LuaScriptInterface::luaNetworkMessageAddString); + registerMethod("NetworkMessage", "addPosition", LuaScriptInterface::luaNetworkMessageAddPosition); + registerMethod("NetworkMessage", "addDouble", LuaScriptInterface::luaNetworkMessageAddDouble); + registerMethod("NetworkMessage", "addItem", LuaScriptInterface::luaNetworkMessageAddItem); + registerMethod("NetworkMessage", "addItemId", LuaScriptInterface::luaNetworkMessageAddItemId); + + registerMethod("NetworkMessage", "reset", LuaScriptInterface::luaNetworkMessageReset); + registerMethod("NetworkMessage", "skipBytes", LuaScriptInterface::luaNetworkMessageSkipBytes); + registerMethod("NetworkMessage", "sendToPlayer", LuaScriptInterface::luaNetworkMessageSendToPlayer); + + // ModalWindow + registerClass("ModalWindow", "", LuaScriptInterface::luaModalWindowCreate); + registerMetaMethod("ModalWindow", "__eq", LuaScriptInterface::luaUserdataCompare); + registerMetaMethod("ModalWindow", "__gc", LuaScriptInterface::luaModalWindowDelete); + registerMethod("ModalWindow", "delete", LuaScriptInterface::luaModalWindowDelete); + + registerMethod("ModalWindow", "getId", LuaScriptInterface::luaModalWindowGetId); + registerMethod("ModalWindow", "getTitle", LuaScriptInterface::luaModalWindowGetTitle); + registerMethod("ModalWindow", "getMessage", LuaScriptInterface::luaModalWindowGetMessage); + + registerMethod("ModalWindow", "setTitle", LuaScriptInterface::luaModalWindowSetTitle); + registerMethod("ModalWindow", "setMessage", LuaScriptInterface::luaModalWindowSetMessage); + + registerMethod("ModalWindow", "getButtonCount", LuaScriptInterface::luaModalWindowGetButtonCount); + registerMethod("ModalWindow", "getChoiceCount", LuaScriptInterface::luaModalWindowGetChoiceCount); + + registerMethod("ModalWindow", "addButton", LuaScriptInterface::luaModalWindowAddButton); + registerMethod("ModalWindow", "addChoice", LuaScriptInterface::luaModalWindowAddChoice); + + registerMethod("ModalWindow", "getDefaultEnterButton", LuaScriptInterface::luaModalWindowGetDefaultEnterButton); + registerMethod("ModalWindow", "setDefaultEnterButton", LuaScriptInterface::luaModalWindowSetDefaultEnterButton); + + registerMethod("ModalWindow", "getDefaultEscapeButton", LuaScriptInterface::luaModalWindowGetDefaultEscapeButton); + registerMethod("ModalWindow", "setDefaultEscapeButton", LuaScriptInterface::luaModalWindowSetDefaultEscapeButton); + + registerMethod("ModalWindow", "hasPriority", LuaScriptInterface::luaModalWindowHasPriority); + registerMethod("ModalWindow", "setPriority", LuaScriptInterface::luaModalWindowSetPriority); + + registerMethod("ModalWindow", "sendToPlayer", LuaScriptInterface::luaModalWindowSendToPlayer); + + // Item + registerClass("Item", "", LuaScriptInterface::luaItemCreate); + registerMetaMethod("Item", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Item", "isItem", LuaScriptInterface::luaItemIsItem); + + registerMethod("Item", "getParent", LuaScriptInterface::luaItemGetParent); + registerMethod("Item", "getTopParent", LuaScriptInterface::luaItemGetTopParent); + + registerMethod("Item", "getId", LuaScriptInterface::luaItemGetId); + + registerMethod("Item", "clone", LuaScriptInterface::luaItemClone); + registerMethod("Item", "split", LuaScriptInterface::luaItemSplit); + registerMethod("Item", "remove", LuaScriptInterface::luaItemRemove); + + registerMethod("Item", "getUniqueId", LuaScriptInterface::luaItemGetUniqueId); + registerMethod("Item", "getActionId", LuaScriptInterface::luaItemGetActionId); + registerMethod("Item", "setActionId", LuaScriptInterface::luaItemSetActionId); + + registerMethod("Item", "getCount", LuaScriptInterface::luaItemGetCount); + registerMethod("Item", "getCharges", LuaScriptInterface::luaItemGetCharges); + registerMethod("Item", "getFluidType", LuaScriptInterface::luaItemGetFluidType); + registerMethod("Item", "getWeight", LuaScriptInterface::luaItemGetWeight); + + registerMethod("Item", "getSubType", LuaScriptInterface::luaItemGetSubType); + + registerMethod("Item", "getName", LuaScriptInterface::luaItemGetName); + registerMethod("Item", "getPluralName", LuaScriptInterface::luaItemGetPluralName); + registerMethod("Item", "getArticle", LuaScriptInterface::luaItemGetArticle); + + registerMethod("Item", "getPosition", LuaScriptInterface::luaItemGetPosition); + registerMethod("Item", "getTile", LuaScriptInterface::luaItemGetTile); + + registerMethod("Item", "hasAttribute", LuaScriptInterface::luaItemHasAttribute); + registerMethod("Item", "getAttribute", LuaScriptInterface::luaItemGetAttribute); + registerMethod("Item", "setAttribute", LuaScriptInterface::luaItemSetAttribute); + registerMethod("Item", "removeAttribute", LuaScriptInterface::luaItemRemoveAttribute); + registerMethod("Item", "getCustomAttribute", LuaScriptInterface::luaItemGetCustomAttribute); + registerMethod("Item", "setCustomAttribute", LuaScriptInterface::luaItemSetCustomAttribute); + registerMethod("Item", "removeCustomAttribute", LuaScriptInterface::luaItemRemoveCustomAttribute); + + registerMethod("Item", "moveTo", LuaScriptInterface::luaItemMoveTo); + registerMethod("Item", "transform", LuaScriptInterface::luaItemTransform); + registerMethod("Item", "decay", LuaScriptInterface::luaItemDecay); + + registerMethod("Item", "getDescription", LuaScriptInterface::luaItemGetDescription); + + registerMethod("Item", "hasProperty", LuaScriptInterface::luaItemHasProperty); + registerMethod("Item", "isLoadedFromMap", LuaScriptInterface::luaItemIsLoadedFromMap); + + // Container + registerClass("Container", "Item", LuaScriptInterface::luaContainerCreate); + registerMetaMethod("Container", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Container", "getSize", LuaScriptInterface::luaContainerGetSize); + registerMethod("Container", "getCapacity", LuaScriptInterface::luaContainerGetCapacity); + registerMethod("Container", "getEmptySlots", LuaScriptInterface::luaContainerGetEmptySlots); + registerMethod("Container", "getContentDescription", LuaScriptInterface::luaContainerGetContentDescription); + registerMethod("Container", "getItemHoldingCount", LuaScriptInterface::luaContainerGetItemHoldingCount); + registerMethod("Container", "getItemCountById", LuaScriptInterface::luaContainerGetItemCountById); + + registerMethod("Container", "getItem", LuaScriptInterface::luaContainerGetItem); + registerMethod("Container", "hasItem", LuaScriptInterface::luaContainerHasItem); + registerMethod("Container", "addItem", LuaScriptInterface::luaContainerAddItem); + registerMethod("Container", "addItemEx", LuaScriptInterface::luaContainerAddItemEx); + registerMethod("Container", "getCorpseOwner", LuaScriptInterface::luaContainerGetCorpseOwner); + + // Teleport + registerClass("Teleport", "Item", LuaScriptInterface::luaTeleportCreate); + registerMetaMethod("Teleport", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Teleport", "getDestination", LuaScriptInterface::luaTeleportGetDestination); + registerMethod("Teleport", "setDestination", LuaScriptInterface::luaTeleportSetDestination); + + // Creature + registerClass("Creature", "", LuaScriptInterface::luaCreatureCreate); + registerMetaMethod("Creature", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Creature", "getEvents", LuaScriptInterface::luaCreatureGetEvents); + registerMethod("Creature", "registerEvent", LuaScriptInterface::luaCreatureRegisterEvent); + registerMethod("Creature", "unregisterEvent", LuaScriptInterface::luaCreatureUnregisterEvent); + + registerMethod("Creature", "isRemoved", LuaScriptInterface::luaCreatureIsRemoved); + registerMethod("Creature", "isCreature", LuaScriptInterface::luaCreatureIsCreature); + registerMethod("Creature", "isInGhostMode", LuaScriptInterface::luaCreatureIsInGhostMode); + registerMethod("Creature", "isHealthHidden", LuaScriptInterface::luaCreatureIsHealthHidden); + registerMethod("Creature", "isImmune", LuaScriptInterface::luaCreatureIsImmune); + + registerMethod("Creature", "canSee", LuaScriptInterface::luaCreatureCanSee); + registerMethod("Creature", "canSeeCreature", LuaScriptInterface::luaCreatureCanSeeCreature); + + registerMethod("Creature", "getParent", LuaScriptInterface::luaCreatureGetParent); + + registerMethod("Creature", "getId", LuaScriptInterface::luaCreatureGetId); + registerMethod("Creature", "getName", LuaScriptInterface::luaCreatureGetName); + + registerMethod("Creature", "getTarget", LuaScriptInterface::luaCreatureGetTarget); + registerMethod("Creature", "setTarget", LuaScriptInterface::luaCreatureSetTarget); + + registerMethod("Creature", "getFollowCreature", LuaScriptInterface::luaCreatureGetFollowCreature); + registerMethod("Creature", "setFollowCreature", LuaScriptInterface::luaCreatureSetFollowCreature); + + registerMethod("Creature", "getMaster", LuaScriptInterface::luaCreatureGetMaster); + registerMethod("Creature", "setMaster", LuaScriptInterface::luaCreatureSetMaster); + + registerMethod("Creature", "getLight", LuaScriptInterface::luaCreatureGetLight); + registerMethod("Creature", "setLight", LuaScriptInterface::luaCreatureSetLight); + + registerMethod("Creature", "getSpeed", LuaScriptInterface::luaCreatureGetSpeed); + registerMethod("Creature", "getBaseSpeed", LuaScriptInterface::luaCreatureGetBaseSpeed); + registerMethod("Creature", "changeSpeed", LuaScriptInterface::luaCreatureChangeSpeed); + + registerMethod("Creature", "setDropLoot", LuaScriptInterface::luaCreatureSetDropLoot); + registerMethod("Creature", "setSkillLoss", LuaScriptInterface::luaCreatureSetSkillLoss); + + registerMethod("Creature", "getPosition", LuaScriptInterface::luaCreatureGetPosition); + registerMethod("Creature", "getTile", LuaScriptInterface::luaCreatureGetTile); + registerMethod("Creature", "getDirection", LuaScriptInterface::luaCreatureGetDirection); + registerMethod("Creature", "setDirection", LuaScriptInterface::luaCreatureSetDirection); + + registerMethod("Creature", "getHealth", LuaScriptInterface::luaCreatureGetHealth); + registerMethod("Creature", "setHealth", LuaScriptInterface::luaCreatureSetHealth); + registerMethod("Creature", "addHealth", LuaScriptInterface::luaCreatureAddHealth); + registerMethod("Creature", "getMaxHealth", LuaScriptInterface::luaCreatureGetMaxHealth); + registerMethod("Creature", "setMaxHealth", LuaScriptInterface::luaCreatureSetMaxHealth); + registerMethod("Creature", "setHiddenHealth", LuaScriptInterface::luaCreatureSetHiddenHealth); + + registerMethod("Creature", "getSkull", LuaScriptInterface::luaCreatureGetSkull); + registerMethod("Creature", "setSkull", LuaScriptInterface::luaCreatureSetSkull); + + registerMethod("Creature", "getOutfit", LuaScriptInterface::luaCreatureGetOutfit); + registerMethod("Creature", "setOutfit", LuaScriptInterface::luaCreatureSetOutfit); + + registerMethod("Creature", "getCondition", LuaScriptInterface::luaCreatureGetCondition); + registerMethod("Creature", "addCondition", LuaScriptInterface::luaCreatureAddCondition); + registerMethod("Creature", "removeCondition", LuaScriptInterface::luaCreatureRemoveCondition); + registerMethod("Creature", "hasCondition", LuaScriptInterface::luaCreatureHasCondition); + + registerMethod("Creature", "remove", LuaScriptInterface::luaCreatureRemove); + registerMethod("Creature", "teleportTo", LuaScriptInterface::luaCreatureTeleportTo); + registerMethod("Creature", "say", LuaScriptInterface::luaCreatureSay); + + registerMethod("Creature", "getDamageMap", LuaScriptInterface::luaCreatureGetDamageMap); + + registerMethod("Creature", "getSummons", LuaScriptInterface::luaCreatureGetSummons); + + registerMethod("Creature", "getDescription", LuaScriptInterface::luaCreatureGetDescription); + + registerMethod("Creature", "getPathTo", LuaScriptInterface::luaCreatureGetPathTo); + registerMethod("Creature", "move", LuaScriptInterface::luaCreatureMove); + + registerMethod("Creature", "getZone", LuaScriptInterface::luaCreatureGetZone); + + // Player + registerClass("Player", "Creature", LuaScriptInterface::luaPlayerCreate); + registerMetaMethod("Player", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Player", "isPlayer", LuaScriptInterface::luaPlayerIsPlayer); + + registerMethod("Player", "getGuid", LuaScriptInterface::luaPlayerGetGuid); + registerMethod("Player", "getIp", LuaScriptInterface::luaPlayerGetIp); + registerMethod("Player", "getAccountId", LuaScriptInterface::luaPlayerGetAccountId); + registerMethod("Player", "getLastLoginSaved", LuaScriptInterface::luaPlayerGetLastLoginSaved); + registerMethod("Player", "getLastLogout", LuaScriptInterface::luaPlayerGetLastLogout); + + registerMethod("Player", "getAccountType", LuaScriptInterface::luaPlayerGetAccountType); + registerMethod("Player", "setAccountType", LuaScriptInterface::luaPlayerSetAccountType); + + registerMethod("Player", "getCapacity", LuaScriptInterface::luaPlayerGetCapacity); + registerMethod("Player", "setCapacity", LuaScriptInterface::luaPlayerSetCapacity); + + registerMethod("Player", "getFreeCapacity", LuaScriptInterface::luaPlayerGetFreeCapacity); + + registerMethod("Player", "getDepotChest", LuaScriptInterface::luaPlayerGetDepotChest); + registerMethod("Player", "getInbox", LuaScriptInterface::luaPlayerGetInbox); + + registerMethod("Player", "getSkullTime", LuaScriptInterface::luaPlayerGetSkullTime); + registerMethod("Player", "setSkullTime", LuaScriptInterface::luaPlayerSetSkullTime); + registerMethod("Player", "getDeathPenalty", LuaScriptInterface::luaPlayerGetDeathPenalty); + + registerMethod("Player", "getExperience", LuaScriptInterface::luaPlayerGetExperience); + registerMethod("Player", "addExperience", LuaScriptInterface::luaPlayerAddExperience); + registerMethod("Player", "removeExperience", LuaScriptInterface::luaPlayerRemoveExperience); + registerMethod("Player", "getLevel", LuaScriptInterface::luaPlayerGetLevel); + + registerMethod("Player", "getMagicLevel", LuaScriptInterface::luaPlayerGetMagicLevel); + registerMethod("Player", "getBaseMagicLevel", LuaScriptInterface::luaPlayerGetBaseMagicLevel); + registerMethod("Player", "getMana", LuaScriptInterface::luaPlayerGetMana); + registerMethod("Player", "addMana", LuaScriptInterface::luaPlayerAddMana); + registerMethod("Player", "getMaxMana", LuaScriptInterface::luaPlayerGetMaxMana); + registerMethod("Player", "setMaxMana", LuaScriptInterface::luaPlayerSetMaxMana); + registerMethod("Player", "getManaSpent", LuaScriptInterface::luaPlayerGetManaSpent); + registerMethod("Player", "addManaSpent", LuaScriptInterface::luaPlayerAddManaSpent); + + registerMethod("Player", "getBaseMaxHealth", LuaScriptInterface::luaPlayerGetBaseMaxHealth); + registerMethod("Player", "getBaseMaxMana", LuaScriptInterface::luaPlayerGetBaseMaxMana); + + registerMethod("Player", "getSkillLevel", LuaScriptInterface::luaPlayerGetSkillLevel); + registerMethod("Player", "getEffectiveSkillLevel", LuaScriptInterface::luaPlayerGetEffectiveSkillLevel); + registerMethod("Player", "getSkillPercent", LuaScriptInterface::luaPlayerGetSkillPercent); + registerMethod("Player", "getSkillTries", LuaScriptInterface::luaPlayerGetSkillTries); + registerMethod("Player", "addSkillTries", LuaScriptInterface::luaPlayerAddSkillTries); + registerMethod("Player", "getSpecialSkill", LuaScriptInterface::luaPlayerGetSpecialSkill); + registerMethod("Player", "addSpecialSkill", LuaScriptInterface::luaPlayerAddSpecialSkill); + + registerMethod("Player", "addOfflineTrainingTime", LuaScriptInterface::luaPlayerAddOfflineTrainingTime); + registerMethod("Player", "getOfflineTrainingTime", LuaScriptInterface::luaPlayerGetOfflineTrainingTime); + registerMethod("Player", "removeOfflineTrainingTime", LuaScriptInterface::luaPlayerRemoveOfflineTrainingTime); + + registerMethod("Player", "addOfflineTrainingTries", LuaScriptInterface::luaPlayerAddOfflineTrainingTries); + + registerMethod("Player", "getOfflineTrainingSkill", LuaScriptInterface::luaPlayerGetOfflineTrainingSkill); + registerMethod("Player", "setOfflineTrainingSkill", LuaScriptInterface::luaPlayerSetOfflineTrainingSkill); + + registerMethod("Player", "getItemCount", LuaScriptInterface::luaPlayerGetItemCount); + registerMethod("Player", "getItemById", LuaScriptInterface::luaPlayerGetItemById); + + registerMethod("Player", "getVocation", LuaScriptInterface::luaPlayerGetVocation); + registerMethod("Player", "setVocation", LuaScriptInterface::luaPlayerSetVocation); + + registerMethod("Player", "getSex", LuaScriptInterface::luaPlayerGetSex); + registerMethod("Player", "setSex", LuaScriptInterface::luaPlayerSetSex); + + registerMethod("Player", "getTown", LuaScriptInterface::luaPlayerGetTown); + registerMethod("Player", "setTown", LuaScriptInterface::luaPlayerSetTown); + + registerMethod("Player", "getGuild", LuaScriptInterface::luaPlayerGetGuild); + registerMethod("Player", "setGuild", LuaScriptInterface::luaPlayerSetGuild); + + registerMethod("Player", "getGuildLevel", LuaScriptInterface::luaPlayerGetGuildLevel); + registerMethod("Player", "setGuildLevel", LuaScriptInterface::luaPlayerSetGuildLevel); + + registerMethod("Player", "getGuildNick", LuaScriptInterface::luaPlayerGetGuildNick); + registerMethod("Player", "setGuildNick", LuaScriptInterface::luaPlayerSetGuildNick); + + registerMethod("Player", "getGroup", LuaScriptInterface::luaPlayerGetGroup); + registerMethod("Player", "setGroup", LuaScriptInterface::luaPlayerSetGroup); + + registerMethod("Player", "getStamina", LuaScriptInterface::luaPlayerGetStamina); + registerMethod("Player", "setStamina", LuaScriptInterface::luaPlayerSetStamina); + + registerMethod("Player", "getSoul", LuaScriptInterface::luaPlayerGetSoul); + registerMethod("Player", "addSoul", LuaScriptInterface::luaPlayerAddSoul); + registerMethod("Player", "getMaxSoul", LuaScriptInterface::luaPlayerGetMaxSoul); + + registerMethod("Player", "getBankBalance", LuaScriptInterface::luaPlayerGetBankBalance); + registerMethod("Player", "setBankBalance", LuaScriptInterface::luaPlayerSetBankBalance); + + registerMethod("Player", "getStorageValue", LuaScriptInterface::luaPlayerGetStorageValue); + registerMethod("Player", "setStorageValue", LuaScriptInterface::luaPlayerSetStorageValue); + + registerMethod("Player", "addItem", LuaScriptInterface::luaPlayerAddItem); + registerMethod("Player", "addItemEx", LuaScriptInterface::luaPlayerAddItemEx); + registerMethod("Player", "removeItem", LuaScriptInterface::luaPlayerRemoveItem); + + registerMethod("Player", "getMoney", LuaScriptInterface::luaPlayerGetMoney); + registerMethod("Player", "addMoney", LuaScriptInterface::luaPlayerAddMoney); + registerMethod("Player", "removeMoney", LuaScriptInterface::luaPlayerRemoveMoney); + + registerMethod("Player", "showTextDialog", LuaScriptInterface::luaPlayerShowTextDialog); + + registerMethod("Player", "sendTextMessage", LuaScriptInterface::luaPlayerSendTextMessage); + registerMethod("Player", "sendChannelMessage", LuaScriptInterface::luaPlayerSendChannelMessage); + registerMethod("Player", "sendPrivateMessage", LuaScriptInterface::luaPlayerSendPrivateMessage); + registerMethod("Player", "channelSay", LuaScriptInterface::luaPlayerChannelSay); + registerMethod("Player", "openChannel", LuaScriptInterface::luaPlayerOpenChannel); + + registerMethod("Player", "getSlotItem", LuaScriptInterface::luaPlayerGetSlotItem); + + registerMethod("Player", "getParty", LuaScriptInterface::luaPlayerGetParty); + + registerMethod("Player", "addOutfit", LuaScriptInterface::luaPlayerAddOutfit); + registerMethod("Player", "addOutfitAddon", LuaScriptInterface::luaPlayerAddOutfitAddon); + registerMethod("Player", "removeOutfit", LuaScriptInterface::luaPlayerRemoveOutfit); + registerMethod("Player", "removeOutfitAddon", LuaScriptInterface::luaPlayerRemoveOutfitAddon); + registerMethod("Player", "hasOutfit", LuaScriptInterface::luaPlayerHasOutfit); + registerMethod("Player", "sendOutfitWindow", LuaScriptInterface::luaPlayerSendOutfitWindow); + + registerMethod("Player", "addMount", LuaScriptInterface::luaPlayerAddMount); + registerMethod("Player", "removeMount", LuaScriptInterface::luaPlayerRemoveMount); + registerMethod("Player", "hasMount", LuaScriptInterface::luaPlayerHasMount); + + registerMethod("Player", "getPremiumDays", LuaScriptInterface::luaPlayerGetPremiumDays); + registerMethod("Player", "addPremiumDays", LuaScriptInterface::luaPlayerAddPremiumDays); + registerMethod("Player", "removePremiumDays", LuaScriptInterface::luaPlayerRemovePremiumDays); + + registerMethod("Player", "hasBlessing", LuaScriptInterface::luaPlayerHasBlessing); + registerMethod("Player", "addBlessing", LuaScriptInterface::luaPlayerAddBlessing); + registerMethod("Player", "removeBlessing", LuaScriptInterface::luaPlayerRemoveBlessing); + + registerMethod("Player", "canLearnSpell", LuaScriptInterface::luaPlayerCanLearnSpell); + registerMethod("Player", "learnSpell", LuaScriptInterface::luaPlayerLearnSpell); + registerMethod("Player", "forgetSpell", LuaScriptInterface::luaPlayerForgetSpell); + registerMethod("Player", "hasLearnedSpell", LuaScriptInterface::luaPlayerHasLearnedSpell); + + registerMethod("Player", "sendTutorial", LuaScriptInterface::luaPlayerSendTutorial); + registerMethod("Player", "addMapMark", LuaScriptInterface::luaPlayerAddMapMark); + + registerMethod("Player", "save", LuaScriptInterface::luaPlayerSave); + registerMethod("Player", "popupFYI", LuaScriptInterface::luaPlayerPopupFYI); + + registerMethod("Player", "isPzLocked", LuaScriptInterface::luaPlayerIsPzLocked); + + registerMethod("Player", "getClient", LuaScriptInterface::luaPlayerGetClient); + + registerMethod("Player", "getHouse", LuaScriptInterface::luaPlayerGetHouse); + registerMethod("Player", "sendHouseWindow", LuaScriptInterface::luaPlayerSendHouseWindow); + registerMethod("Player", "setEditHouse", LuaScriptInterface::luaPlayerSetEditHouse); + + registerMethod("Player", "setGhostMode", LuaScriptInterface::luaPlayerSetGhostMode); + + registerMethod("Player", "getContainerId", LuaScriptInterface::luaPlayerGetContainerId); + registerMethod("Player", "getContainerById", LuaScriptInterface::luaPlayerGetContainerById); + registerMethod("Player", "getContainerIndex", LuaScriptInterface::luaPlayerGetContainerIndex); + + registerMethod("Player", "startLiveCast", LuaScriptInterface::luaPlayerStartLiveCast); + registerMethod("Player", "stopLiveCast", LuaScriptInterface::luaPlayerStopLiveCast); + registerMethod("Player", "isLiveCaster", LuaScriptInterface::luaPlayerIsLiveCaster); + + registerMethod("Player", "getInstantSpells", LuaScriptInterface::luaPlayerGetInstantSpells); + registerMethod("Player", "canCast", LuaScriptInterface::luaPlayerCanCast); + + registerMethod("Player", "hasChaseMode", LuaScriptInterface::luaPlayerHasChaseMode); + registerMethod("Player", "hasSecureMode", LuaScriptInterface::luaPlayerHasSecureMode); + registerMethod("Player", "getFightMode", LuaScriptInterface::luaPlayerGetFightMode); + + // Monster + registerClass("Monster", "Creature", LuaScriptInterface::luaMonsterCreate); + registerMetaMethod("Monster", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Monster", "isMonster", LuaScriptInterface::luaMonsterIsMonster); + + registerMethod("Monster", "getType", LuaScriptInterface::luaMonsterGetType); + + registerMethod("Monster", "getSpawnPosition", LuaScriptInterface::luaMonsterGetSpawnPosition); + registerMethod("Monster", "isInSpawnRange", LuaScriptInterface::luaMonsterIsInSpawnRange); + + registerMethod("Monster", "isIdle", LuaScriptInterface::luaMonsterIsIdle); + registerMethod("Monster", "setIdle", LuaScriptInterface::luaMonsterSetIdle); + + registerMethod("Monster", "isTarget", LuaScriptInterface::luaMonsterIsTarget); + registerMethod("Monster", "isOpponent", LuaScriptInterface::luaMonsterIsOpponent); + registerMethod("Monster", "isFriend", LuaScriptInterface::luaMonsterIsFriend); + + registerMethod("Monster", "addFriend", LuaScriptInterface::luaMonsterAddFriend); + registerMethod("Monster", "removeFriend", LuaScriptInterface::luaMonsterRemoveFriend); + registerMethod("Monster", "getFriendList", LuaScriptInterface::luaMonsterGetFriendList); + registerMethod("Monster", "getFriendCount", LuaScriptInterface::luaMonsterGetFriendCount); + + registerMethod("Monster", "addTarget", LuaScriptInterface::luaMonsterAddTarget); + registerMethod("Monster", "removeTarget", LuaScriptInterface::luaMonsterRemoveTarget); + registerMethod("Monster", "getTargetList", LuaScriptInterface::luaMonsterGetTargetList); + registerMethod("Monster", "getTargetCount", LuaScriptInterface::luaMonsterGetTargetCount); + + registerMethod("Monster", "selectTarget", LuaScriptInterface::luaMonsterSelectTarget); + registerMethod("Monster", "searchTarget", LuaScriptInterface::luaMonsterSearchTarget); + + // Npc + registerClass("Npc", "Creature", LuaScriptInterface::luaNpcCreate); + registerMetaMethod("Npc", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Npc", "isNpc", LuaScriptInterface::luaNpcIsNpc); + + registerMethod("Npc", "setMasterPos", LuaScriptInterface::luaNpcSetMasterPos); + + registerMethod("Npc", "getSpeechBubble", LuaScriptInterface::luaNpcGetSpeechBubble); + registerMethod("Npc", "setSpeechBubble", LuaScriptInterface::luaNpcSetSpeechBubble); + + // Guild + registerClass("Guild", "", LuaScriptInterface::luaGuildCreate); + registerMetaMethod("Guild", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Guild", "getId", LuaScriptInterface::luaGuildGetId); + registerMethod("Guild", "getName", LuaScriptInterface::luaGuildGetName); + registerMethod("Guild", "getMembersOnline", LuaScriptInterface::luaGuildGetMembersOnline); + + registerMethod("Guild", "addRank", LuaScriptInterface::luaGuildAddRank); + registerMethod("Guild", "getRankById", LuaScriptInterface::luaGuildGetRankById); + registerMethod("Guild", "getRankByLevel", LuaScriptInterface::luaGuildGetRankByLevel); + + registerMethod("Guild", "getMotd", LuaScriptInterface::luaGuildGetMotd); + registerMethod("Guild", "setMotd", LuaScriptInterface::luaGuildSetMotd); + + // Group + registerClass("Group", "", LuaScriptInterface::luaGroupCreate); + registerMetaMethod("Group", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Group", "getId", LuaScriptInterface::luaGroupGetId); + registerMethod("Group", "getName", LuaScriptInterface::luaGroupGetName); + registerMethod("Group", "getFlags", LuaScriptInterface::luaGroupGetFlags); + registerMethod("Group", "getAccess", LuaScriptInterface::luaGroupGetAccess); + registerMethod("Group", "getMaxDepotItems", LuaScriptInterface::luaGroupGetMaxDepotItems); + registerMethod("Group", "getMaxVipEntries", LuaScriptInterface::luaGroupGetMaxVipEntries); + registerMethod("Group", "hasFlag", LuaScriptInterface::luaGroupHasFlag); + + // Vocation + registerClass("Vocation", "", LuaScriptInterface::luaVocationCreate); + registerMetaMethod("Vocation", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Vocation", "getId", LuaScriptInterface::luaVocationGetId); + registerMethod("Vocation", "getClientId", LuaScriptInterface::luaVocationGetClientId); + registerMethod("Vocation", "getName", LuaScriptInterface::luaVocationGetName); + registerMethod("Vocation", "getDescription", LuaScriptInterface::luaVocationGetDescription); + + registerMethod("Vocation", "getRequiredSkillTries", LuaScriptInterface::luaVocationGetRequiredSkillTries); + registerMethod("Vocation", "getRequiredManaSpent", LuaScriptInterface::luaVocationGetRequiredManaSpent); + + registerMethod("Vocation", "getCapacityGain", LuaScriptInterface::luaVocationGetCapacityGain); + + registerMethod("Vocation", "getHealthGain", LuaScriptInterface::luaVocationGetHealthGain); + registerMethod("Vocation", "getHealthGainTicks", LuaScriptInterface::luaVocationGetHealthGainTicks); + registerMethod("Vocation", "getHealthGainAmount", LuaScriptInterface::luaVocationGetHealthGainAmount); + + registerMethod("Vocation", "getManaGain", LuaScriptInterface::luaVocationGetManaGain); + registerMethod("Vocation", "getManaGainTicks", LuaScriptInterface::luaVocationGetManaGainTicks); + registerMethod("Vocation", "getManaGainAmount", LuaScriptInterface::luaVocationGetManaGainAmount); + + registerMethod("Vocation", "getMaxSoul", LuaScriptInterface::luaVocationGetMaxSoul); + registerMethod("Vocation", "getSoulGainTicks", LuaScriptInterface::luaVocationGetSoulGainTicks); + + registerMethod("Vocation", "getAttackSpeed", LuaScriptInterface::luaVocationGetAttackSpeed); + registerMethod("Vocation", "getBaseSpeed", LuaScriptInterface::luaVocationGetBaseSpeed); + + registerMethod("Vocation", "getDemotion", LuaScriptInterface::luaVocationGetDemotion); + registerMethod("Vocation", "getPromotion", LuaScriptInterface::luaVocationGetPromotion); + + // Town + registerClass("Town", "", LuaScriptInterface::luaTownCreate); + registerMetaMethod("Town", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Town", "getId", LuaScriptInterface::luaTownGetId); + registerMethod("Town", "getName", LuaScriptInterface::luaTownGetName); + registerMethod("Town", "getTemplePosition", LuaScriptInterface::luaTownGetTemplePosition); + + // House + registerClass("House", "", LuaScriptInterface::luaHouseCreate); + registerMetaMethod("House", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("House", "getId", LuaScriptInterface::luaHouseGetId); + registerMethod("House", "getName", LuaScriptInterface::luaHouseGetName); + registerMethod("House", "getTown", LuaScriptInterface::luaHouseGetTown); + registerMethod("House", "getExitPosition", LuaScriptInterface::luaHouseGetExitPosition); + registerMethod("House", "getRent", LuaScriptInterface::luaHouseGetRent); + + registerMethod("House", "getOwnerGuid", LuaScriptInterface::luaHouseGetOwnerGuid); + registerMethod("House", "setOwnerGuid", LuaScriptInterface::luaHouseSetOwnerGuid); + registerMethod("House", "startTrade", LuaScriptInterface::luaHouseStartTrade); + + registerMethod("House", "getBeds", LuaScriptInterface::luaHouseGetBeds); + registerMethod("House", "getBedCount", LuaScriptInterface::luaHouseGetBedCount); + + registerMethod("House", "getDoors", LuaScriptInterface::luaHouseGetDoors); + registerMethod("House", "getDoorCount", LuaScriptInterface::luaHouseGetDoorCount); + registerMethod("House", "getDoorIdByPosition", LuaScriptInterface::luaHouseGetDoorIdByPosition); + + registerMethod("House", "getTiles", LuaScriptInterface::luaHouseGetTiles); + registerMethod("House", "getItems", LuaScriptInterface::luaHouseGetItems); + registerMethod("House", "getTileCount", LuaScriptInterface::luaHouseGetTileCount); + + registerMethod("House", "canEditAccessList", LuaScriptInterface::luaHouseCanEditAccessList); + registerMethod("House", "getAccessList", LuaScriptInterface::luaHouseGetAccessList); + registerMethod("House", "setAccessList", LuaScriptInterface::luaHouseSetAccessList); + + registerMethod("House", "kickPlayer", LuaScriptInterface::luaHouseKickPlayer); + + // ItemType + registerClass("ItemType", "", LuaScriptInterface::luaItemTypeCreate); + registerMetaMethod("ItemType", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("ItemType", "isCorpse", LuaScriptInterface::luaItemTypeIsCorpse); + registerMethod("ItemType", "isDoor", LuaScriptInterface::luaItemTypeIsDoor); + registerMethod("ItemType", "isContainer", LuaScriptInterface::luaItemTypeIsContainer); + registerMethod("ItemType", "isFluidContainer", LuaScriptInterface::luaItemTypeIsFluidContainer); + registerMethod("ItemType", "isMovable", LuaScriptInterface::luaItemTypeIsMovable); + registerMethod("ItemType", "isRune", LuaScriptInterface::luaItemTypeIsRune); + registerMethod("ItemType", "isStackable", LuaScriptInterface::luaItemTypeIsStackable); + registerMethod("ItemType", "isReadable", LuaScriptInterface::luaItemTypeIsReadable); + registerMethod("ItemType", "isWritable", LuaScriptInterface::luaItemTypeIsWritable); + registerMethod("ItemType", "isBlocking", LuaScriptInterface::luaItemTypeIsBlocking); + registerMethod("ItemType", "isGroundTile", LuaScriptInterface::luaItemTypeIsGroundTile); + registerMethod("ItemType", "isMagicField", LuaScriptInterface::luaItemTypeIsMagicField); + registerMethod("ItemType", "isUseable", LuaScriptInterface::luaItemTypeIsUseable); + registerMethod("ItemType", "isPickupable", LuaScriptInterface::luaItemTypeIsPickupable); + + registerMethod("ItemType", "getType", LuaScriptInterface::luaItemTypeGetType); + registerMethod("ItemType", "getId", LuaScriptInterface::luaItemTypeGetId); + registerMethod("ItemType", "getClientId", LuaScriptInterface::luaItemTypeGetClientId); + registerMethod("ItemType", "getName", LuaScriptInterface::luaItemTypeGetName); + registerMethod("ItemType", "getPluralName", LuaScriptInterface::luaItemTypeGetPluralName); + registerMethod("ItemType", "getArticle", LuaScriptInterface::luaItemTypeGetArticle); + registerMethod("ItemType", "getDescription", LuaScriptInterface::luaItemTypeGetDescription); + registerMethod("ItemType", "getSlotPosition", LuaScriptInterface::luaItemTypeGetSlotPosition); + + registerMethod("ItemType", "getCharges", LuaScriptInterface::luaItemTypeGetCharges); + registerMethod("ItemType", "getFluidSource", LuaScriptInterface::luaItemTypeGetFluidSource); + registerMethod("ItemType", "getCapacity", LuaScriptInterface::luaItemTypeGetCapacity); + registerMethod("ItemType", "getWeight", LuaScriptInterface::luaItemTypeGetWeight); + + registerMethod("ItemType", "getHitChance", LuaScriptInterface::luaItemTypeGetHitChance); + registerMethod("ItemType", "getShootRange", LuaScriptInterface::luaItemTypeGetShootRange); + + registerMethod("ItemType", "getAttack", LuaScriptInterface::luaItemTypeGetAttack); + registerMethod("ItemType", "getDefense", LuaScriptInterface::luaItemTypeGetDefense); + registerMethod("ItemType", "getExtraDefense", LuaScriptInterface::luaItemTypeGetExtraDefense); + registerMethod("ItemType", "getArmor", LuaScriptInterface::luaItemTypeGetArmor); + registerMethod("ItemType", "getWeaponType", LuaScriptInterface::luaItemTypeGetWeaponType); + + registerMethod("ItemType", "getElementType", LuaScriptInterface::luaItemTypeGetElementType); + registerMethod("ItemType", "getElementDamage", LuaScriptInterface::luaItemTypeGetElementDamage); + + registerMethod("ItemType", "getTransformEquipId", LuaScriptInterface::luaItemTypeGetTransformEquipId); + registerMethod("ItemType", "getTransformDeEquipId", LuaScriptInterface::luaItemTypeGetTransformDeEquipId); + registerMethod("ItemType", "getDestroyId", LuaScriptInterface::luaItemTypeGetDestroyId); + registerMethod("ItemType", "getDecayId", LuaScriptInterface::luaItemTypeGetDecayId); + registerMethod("ItemType", "getRequiredLevel", LuaScriptInterface::luaItemTypeGetRequiredLevel); + registerMethod("ItemType", "getAmmoType", LuaScriptInterface::luaItemTypeGetAmmoType); + registerMethod("ItemType", "getCorpseType", LuaScriptInterface::luaItemTypeGetCorpseType); + + registerMethod("ItemType", "hasSubType", LuaScriptInterface::luaItemTypeHasSubType); + + // Combat + registerClass("Combat", "", LuaScriptInterface::luaCombatCreate); + registerMetaMethod("Combat", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Combat", "setParameter", LuaScriptInterface::luaCombatSetParameter); + registerMethod("Combat", "setFormula", LuaScriptInterface::luaCombatSetFormula); + + registerMethod("Combat", "setArea", LuaScriptInterface::luaCombatSetArea); + registerMethod("Combat", "addCondition", LuaScriptInterface::luaCombatAddCondition); + registerMethod("Combat", "clearConditions", LuaScriptInterface::luaCombatClearConditions); + registerMethod("Combat", "setCallback", LuaScriptInterface::luaCombatSetCallback); + registerMethod("Combat", "setOrigin", LuaScriptInterface::luaCombatSetOrigin); + + registerMethod("Combat", "execute", LuaScriptInterface::luaCombatExecute); + + // Condition + registerClass("Condition", "", LuaScriptInterface::luaConditionCreate); + registerMetaMethod("Condition", "__eq", LuaScriptInterface::luaUserdataCompare); + registerMetaMethod("Condition", "__gc", LuaScriptInterface::luaConditionDelete); + registerMethod("Condition", "delete", LuaScriptInterface::luaConditionDelete); + + registerMethod("Condition", "getId", LuaScriptInterface::luaConditionGetId); + registerMethod("Condition", "getSubId", LuaScriptInterface::luaConditionGetSubId); + registerMethod("Condition", "getType", LuaScriptInterface::luaConditionGetType); + registerMethod("Condition", "getIcons", LuaScriptInterface::luaConditionGetIcons); + registerMethod("Condition", "getEndTime", LuaScriptInterface::luaConditionGetEndTime); + + registerMethod("Condition", "clone", LuaScriptInterface::luaConditionClone); + + registerMethod("Condition", "getTicks", LuaScriptInterface::luaConditionGetTicks); + registerMethod("Condition", "setTicks", LuaScriptInterface::luaConditionSetTicks); + + registerMethod("Condition", "setParameter", LuaScriptInterface::luaConditionSetParameter); + registerMethod("Condition", "setFormula", LuaScriptInterface::luaConditionSetFormula); + registerMethod("Condition", "setOutfit", LuaScriptInterface::luaConditionSetOutfit); + + registerMethod("Condition", "addDamage", LuaScriptInterface::luaConditionAddDamage); + + // MonsterType + registerClass("MonsterType", "", LuaScriptInterface::luaMonsterTypeCreate); + registerMetaMethod("MonsterType", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("MonsterType", "isAttackable", LuaScriptInterface::luaMonsterTypeIsAttackable); + registerMethod("MonsterType", "isConvinceable", LuaScriptInterface::luaMonsterTypeIsConvinceable); + registerMethod("MonsterType", "isSummonable", LuaScriptInterface::luaMonsterTypeIsSummonable); + registerMethod("MonsterType", "isIllusionable", LuaScriptInterface::luaMonsterTypeIsIllusionable); + registerMethod("MonsterType", "isHostile", LuaScriptInterface::luaMonsterTypeIsHostile); + registerMethod("MonsterType", "isPushable", LuaScriptInterface::luaMonsterTypeIsPushable); + registerMethod("MonsterType", "isHealthHidden", LuaScriptInterface::luaMonsterTypeIsHealthHidden); + registerMethod("MonsterType", "isBoss", LuaScriptInterface::luaMonsterTypeIsBoss); + + registerMethod("MonsterType", "canPushItems", LuaScriptInterface::luaMonsterTypeCanPushItems); + registerMethod("MonsterType", "canPushCreatures", LuaScriptInterface::luaMonsterTypeCanPushCreatures); + + registerMethod("MonsterType", "name", LuaScriptInterface::luaMonsterTypeName); + + registerMethod("MonsterType", "nameDescription", LuaScriptInterface::luaMonsterTypeNameDescription); + + registerMethod("MonsterType", "health", LuaScriptInterface::luaMonsterTypeHealth); + registerMethod("MonsterType", "maxHealth", LuaScriptInterface::luaMonsterTypeMaxHealth); + registerMethod("MonsterType", "runHealth", LuaScriptInterface::luaMonsterTypeRunHealth); + registerMethod("MonsterType", "experience", LuaScriptInterface::luaMonsterTypeExperience); + registerMethod("MonsterType", "skull", LuaScriptInterface::luaMonsterTypeSkull); + + registerMethod("MonsterType", "combatImmunities", LuaScriptInterface::luaMonsterTypeCombatImmunities); + registerMethod("MonsterType", "conditionImmunities", LuaScriptInterface::luaMonsterTypeConditionImmunities); + + registerMethod("MonsterType", "getAttackList", LuaScriptInterface::luaMonsterTypeGetAttackList); + registerMethod("MonsterType", "addAttack", LuaScriptInterface::luaMonsterTypeAddAttack); + + registerMethod("MonsterType", "getDefenseList", LuaScriptInterface::luaMonsterTypeGetDefenseList); + registerMethod("MonsterType", "addDefense", LuaScriptInterface::luaMonsterTypeAddDefense); + + registerMethod("MonsterType", "getElementList", LuaScriptInterface::luaMonsterTypeGetElementList); + registerMethod("MonsterType", "addElement", LuaScriptInterface::luaMonsterTypeAddElement); + + registerMethod("MonsterType", "getVoices", LuaScriptInterface::luaMonsterTypeGetVoices); + registerMethod("MonsterType", "addVoice", LuaScriptInterface::luaMonsterTypeAddVoice); + + registerMethod("MonsterType", "getLoot", LuaScriptInterface::luaMonsterTypeGetLoot); + registerMethod("MonsterType", "addLoot", LuaScriptInterface::luaMonsterTypeAddLoot); + + registerMethod("MonsterType", "getCreatureEvents", LuaScriptInterface::luaMonsterTypeGetCreatureEvents); + registerMethod("MonsterType", "registerEvent", LuaScriptInterface::luaMonsterTypeRegisterEvent); + + registerMethod("MonsterType", "eventType", LuaScriptInterface::luaMonsterTypeEventType); + registerMethod("MonsterType", "onThink", LuaScriptInterface::luaMonsterTypeEventOnCallback); + registerMethod("MonsterType", "onAppear", LuaScriptInterface::luaMonsterTypeEventOnCallback); + registerMethod("MonsterType", "onDisappear", LuaScriptInterface::luaMonsterTypeEventOnCallback); + registerMethod("MonsterType", "onMove", LuaScriptInterface::luaMonsterTypeEventOnCallback); + registerMethod("MonsterType", "onSay", LuaScriptInterface::luaMonsterTypeEventOnCallback); + + registerMethod("MonsterType", "getSummonList", LuaScriptInterface::luaMonsterTypeGetSummonList); + registerMethod("MonsterType", "addSummon", LuaScriptInterface::luaMonsterTypeAddSummon); + + registerMethod("MonsterType", "maxSummons", LuaScriptInterface::luaMonsterTypeMaxSummons); + + registerMethod("MonsterType", "armor", LuaScriptInterface::luaMonsterTypeArmor); + registerMethod("MonsterType", "defense", LuaScriptInterface::luaMonsterTypeDefense); + registerMethod("MonsterType", "outfit", LuaScriptInterface::luaMonsterTypeOutfit); + registerMethod("MonsterType", "race", LuaScriptInterface::luaMonsterTypeRace); + registerMethod("MonsterType", "corpseId", LuaScriptInterface::luaMonsterTypeCorpseId); + registerMethod("MonsterType", "manaCost", LuaScriptInterface::luaMonsterTypeManaCost); + registerMethod("MonsterType", "baseSpeed", LuaScriptInterface::luaMonsterTypeBaseSpeed); + registerMethod("MonsterType", "light", LuaScriptInterface::luaMonsterTypeLight); + + registerMethod("MonsterType", "staticAttackChance", LuaScriptInterface::luaMonsterTypeStaticAttackChance); + registerMethod("MonsterType", "targetDistance", LuaScriptInterface::luaMonsterTypeTargetDistance); + registerMethod("MonsterType", "yellChance", LuaScriptInterface::luaMonsterTypeYellChance); + registerMethod("MonsterType", "yellSpeedTicks", LuaScriptInterface::luaMonsterTypeYellSpeedTicks); + registerMethod("MonsterType", "changeTargetChance", LuaScriptInterface::luaMonsterTypeChangeTargetChance); + registerMethod("MonsterType", "changeTargetSpeed", LuaScriptInterface::luaMonsterTypeChangeTargetSpeed); + + // Loot + registerClass("Loot", "", LuaScriptInterface::luaCreateLoot); + registerMetaMethod("Loot", "__gc", LuaScriptInterface::luaDeleteLoot); + registerMethod("Loot", "delete", LuaScriptInterface::luaDeleteLoot); + + registerMethod("Loot", "setId", LuaScriptInterface::luaLootSetId); + registerMethod("Loot", "setMaxCount", LuaScriptInterface::luaLootSetMaxCount); + registerMethod("Loot", "setSubType", LuaScriptInterface::luaLootSetSubType); + registerMethod("Loot", "setChance", LuaScriptInterface::luaLootSetChance); + registerMethod("Loot", "setActionId", LuaScriptInterface::luaLootSetActionId); + registerMethod("Loot", "setDescription", LuaScriptInterface::luaLootSetDescription); + registerMethod("Loot", "addChildLoot", LuaScriptInterface::luaLootAddChildLoot); + + // MonsterSpell + registerClass("MonsterSpell", "", LuaScriptInterface::luaCreateMonsterSpell); + registerMetaMethod("MonsterSpell", "__gc", LuaScriptInterface::luaDeleteMonsterSpell); + registerMethod("MonsterSpell", "delete", LuaScriptInterface::luaDeleteMonsterSpell); + + registerMethod("MonsterSpell", "setType", LuaScriptInterface::luaMonsterSpellSetType); + registerMethod("MonsterSpell", "setScriptName", LuaScriptInterface::luaMonsterSpellSetScriptName); + registerMethod("MonsterSpell", "setChance", LuaScriptInterface::luaMonsterSpellSetChance); + registerMethod("MonsterSpell", "setInterval", LuaScriptInterface::luaMonsterSpellSetInterval); + registerMethod("MonsterSpell", "setRange", LuaScriptInterface::luaMonsterSpellSetRange); + registerMethod("MonsterSpell", "setCombatValue", LuaScriptInterface::luaMonsterSpellSetCombatValue); + registerMethod("MonsterSpell", "setCombatType", LuaScriptInterface::luaMonsterSpellSetCombatType); + registerMethod("MonsterSpell", "setAttackValue", LuaScriptInterface::luaMonsterSpellSetAttackValue); + registerMethod("MonsterSpell", "setNeedTarget", LuaScriptInterface::luaMonsterSpellSetNeedTarget); + registerMethod("MonsterSpell", "setCombatLength", LuaScriptInterface::luaMonsterSpellSetCombatLength); + registerMethod("MonsterSpell", "setCombatSpread", LuaScriptInterface::luaMonsterSpellSetCombatSpread); + registerMethod("MonsterSpell", "setCombatRadius", LuaScriptInterface::luaMonsterSpellSetCombatRadius); + registerMethod("MonsterSpell", "setConditionType", LuaScriptInterface::luaMonsterSpellSetConditionType); + registerMethod("MonsterSpell", "setConditionDamage", LuaScriptInterface::luaMonsterSpellSetConditionDamage); + registerMethod("MonsterSpell", "setConditionSpeedChange", LuaScriptInterface::luaMonsterSpellSetConditionSpeedChange); + registerMethod("MonsterSpell", "setConditionDuration", LuaScriptInterface::luaMonsterSpellSetConditionDuration); + registerMethod("MonsterSpell", "setConditionTickInterval", LuaScriptInterface::luaMonsterSpellSetConditionTickInterval); + registerMethod("MonsterSpell", "setCombatShootEffect", LuaScriptInterface::luaMonsterSpellSetCombatShootEffect); + registerMethod("MonsterSpell", "setCombatEffect", LuaScriptInterface::luaMonsterSpellSetCombatEffect); + + // Party + registerClass("Party", "", LuaScriptInterface::luaPartyCreate); + registerMetaMethod("Party", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Party", "disband", LuaScriptInterface::luaPartyDisband); + + registerMethod("Party", "getLeader", LuaScriptInterface::luaPartyGetLeader); + registerMethod("Party", "setLeader", LuaScriptInterface::luaPartySetLeader); + + registerMethod("Party", "getMembers", LuaScriptInterface::luaPartyGetMembers); + registerMethod("Party", "getMemberCount", LuaScriptInterface::luaPartyGetMemberCount); + + registerMethod("Party", "getInvitees", LuaScriptInterface::luaPartyGetInvitees); + registerMethod("Party", "getInviteeCount", LuaScriptInterface::luaPartyGetInviteeCount); + + registerMethod("Party", "addInvite", LuaScriptInterface::luaPartyAddInvite); + registerMethod("Party", "removeInvite", LuaScriptInterface::luaPartyRemoveInvite); + + registerMethod("Party", "addMember", LuaScriptInterface::luaPartyAddMember); + registerMethod("Party", "removeMember", LuaScriptInterface::luaPartyRemoveMember); + + registerMethod("Party", "isSharedExperienceActive", LuaScriptInterface::luaPartyIsSharedExperienceActive); + registerMethod("Party", "isSharedExperienceEnabled", LuaScriptInterface::luaPartyIsSharedExperienceEnabled); + registerMethod("Party", "shareExperience", LuaScriptInterface::luaPartyShareExperience); + registerMethod("Party", "setSharedExperience", LuaScriptInterface::luaPartySetSharedExperience); + + // Spells + registerClass("Spell", "", LuaScriptInterface::luaSpellCreate); + registerMetaMethod("Spell", "__eq", LuaScriptInterface::luaUserdataCompare); + + registerMethod("Spell", "onCastSpell", LuaScriptInterface::luaSpellOnCastSpell); + registerMethod("Spell", "register", LuaScriptInterface::luaSpellRegister); + registerMethod("Spell", "name", LuaScriptInterface::luaSpellName); + registerMethod("Spell", "id", LuaScriptInterface::luaSpellId); + registerMethod("Spell", "group", LuaScriptInterface::luaSpellGroup); + registerMethod("Spell", "cooldown", LuaScriptInterface::luaSpellCooldown); + registerMethod("Spell", "groupCooldown", LuaScriptInterface::luaSpellGroupCooldown); + registerMethod("Spell", "level", LuaScriptInterface::luaSpellLevel); + registerMethod("Spell", "magicLevel", LuaScriptInterface::luaSpellMagicLevel); + registerMethod("Spell", "mana", LuaScriptInterface::luaSpellMana); + registerMethod("Spell", "manaPercent", LuaScriptInterface::luaSpellManaPercent); + registerMethod("Spell", "soul", LuaScriptInterface::luaSpellSoul); + registerMethod("Spell", "range", LuaScriptInterface::luaSpellRange); + registerMethod("Spell", "isPremium", LuaScriptInterface::luaSpellPremium); + registerMethod("Spell", "isEnabled", LuaScriptInterface::luaSpellEnabled); + registerMethod("Spell", "needTarget", LuaScriptInterface::luaSpellNeedTarget); + registerMethod("Spell", "needWeapon", LuaScriptInterface::luaSpellNeedWeapon); + registerMethod("Spell", "needLearn", LuaScriptInterface::luaSpellNeedLearn); + registerMethod("Spell", "isSelfTarget", LuaScriptInterface::luaSpellSelfTarget); + registerMethod("Spell", "isBlocking", LuaScriptInterface::luaSpellBlocking); + registerMethod("Spell", "isAggressive", LuaScriptInterface::luaSpellAggressive); + registerMethod("Spell", "vocation", LuaScriptInterface::luaSpellVocation); + + // only for InstantSpell + registerMethod("Spell", "words", LuaScriptInterface::luaSpellWords); + registerMethod("Spell", "needDirection", LuaScriptInterface::luaSpellNeedDirection); + registerMethod("Spell", "hasParams", LuaScriptInterface::luaSpellHasParams); + registerMethod("Spell", "hasPlayerNameParam", LuaScriptInterface::luaSpellHasPlayerNameParam); + registerMethod("Spell", "needCasterTargetOrDirection", LuaScriptInterface::luaSpellNeedCasterTargetOrDirection); + registerMethod("Spell", "isBlockingWalls", LuaScriptInterface::luaSpellIsBlockingWalls); + + // only for RuneSpells + registerMethod("Spell", "runeId", LuaScriptInterface::luaSpellRuneId); + registerMethod("Spell", "charges", LuaScriptInterface::luaSpellCharges); + registerMethod("Spell", "allowFarUse", LuaScriptInterface::luaSpellAllowFarUse); + registerMethod("Spell", "blockWalls", LuaScriptInterface::luaSpellBlockWalls); + registerMethod("Spell", "checkFloor", LuaScriptInterface::luaSpellCheckFloor); + + // Action + registerClass("Action", "", LuaScriptInterface::luaCreateAction); + registerMethod("Action", "onUse", LuaScriptInterface::luaActionOnUse); + registerMethod("Action", "register", LuaScriptInterface::luaActionRegister); + registerMethod("Action", "id", LuaScriptInterface::luaActionItemId); + registerMethod("Action", "aid", LuaScriptInterface::luaActionActionId); + registerMethod("Action", "uid", LuaScriptInterface::luaActionUniqueId); + registerMethod("Action", "allowFarUse", LuaScriptInterface::luaActionAllowFarUse); + registerMethod("Action", "blockWalls", LuaScriptInterface::luaActionBlockWalls); + registerMethod("Action", "checkFloor", LuaScriptInterface::luaActionCheckFloor); + + // TalkAction + registerClass("TalkAction", "", LuaScriptInterface::luaCreateTalkaction); + registerMethod("TalkAction", "onSay", LuaScriptInterface::luaTalkactionOnSay); + registerMethod("TalkAction", "register", LuaScriptInterface::luaTalkactionRegister); + registerMethod("TalkAction", "separator", LuaScriptInterface::luaTalkactionSeparator); + + // CreatureEvent + registerClass("CreatureEvent", "", LuaScriptInterface::luaCreateCreatureEvent); + registerMethod("CreatureEvent", "type", LuaScriptInterface::luaCreatureEventType); + registerMethod("CreatureEvent", "register", LuaScriptInterface::luaCreatureEventRegister); + registerMethod("CreatureEvent", "onLogin", LuaScriptInterface::luaCreatureEventOnCallback); + registerMethod("CreatureEvent", "onLogout", LuaScriptInterface::luaCreatureEventOnCallback); + registerMethod("CreatureEvent", "onThink", LuaScriptInterface::luaCreatureEventOnCallback); + registerMethod("CreatureEvent", "onPrepareDeath", LuaScriptInterface::luaCreatureEventOnCallback); + registerMethod("CreatureEvent", "onDeath", LuaScriptInterface::luaCreatureEventOnCallback); + registerMethod("CreatureEvent", "onKill", LuaScriptInterface::luaCreatureEventOnCallback); + registerMethod("CreatureEvent", "onAdvance", LuaScriptInterface::luaCreatureEventOnCallback); + registerMethod("CreatureEvent", "onModalWindow", LuaScriptInterface::luaCreatureEventOnCallback); + registerMethod("CreatureEvent", "onTextEdit", LuaScriptInterface::luaCreatureEventOnCallback); + registerMethod("CreatureEvent", "onHealthChange", LuaScriptInterface::luaCreatureEventOnCallback); + registerMethod("CreatureEvent", "onManaChange", LuaScriptInterface::luaCreatureEventOnCallback); + registerMethod("CreatureEvent", "onExtendedOpcode", LuaScriptInterface::luaCreatureEventOnCallback); + + // MoveEvent + registerClass("MoveEvent", "", LuaScriptInterface::luaCreateMoveEvent); + registerMethod("MoveEvent", "type", LuaScriptInterface::luaMoveEventType); + registerMethod("MoveEvent", "register", LuaScriptInterface::luaMoveEventRegister); + registerMethod("MoveEvent", "level", LuaScriptInterface::luaMoveEventLevel); + registerMethod("MoveEvent", "magicLevel", LuaScriptInterface::luaMoveEventMagLevel); + registerMethod("MoveEvent", "slot", LuaScriptInterface::luaMoveEventSlot); + registerMethod("MoveEvent", "id", LuaScriptInterface::luaMoveEventItemId); + registerMethod("MoveEvent", "aid", LuaScriptInterface::luaMoveEventActionId); + registerMethod("MoveEvent", "uid", LuaScriptInterface::luaMoveEventUniqueId); + registerMethod("MoveEvent", "position", LuaScriptInterface::luaMoveEventPosition); + registerMethod("MoveEvent", "premium", LuaScriptInterface::luaMoveEventPremium); + registerMethod("MoveEvent", "vocation", LuaScriptInterface::luaMoveEventVocation); + registerMethod("MoveEvent", "onEquip", LuaScriptInterface::luaMoveEventOnCallback); + registerMethod("MoveEvent", "onDeEquip", LuaScriptInterface::luaMoveEventOnCallback); + registerMethod("MoveEvent", "onStepIn", LuaScriptInterface::luaMoveEventOnCallback); + registerMethod("MoveEvent", "onStepOut", LuaScriptInterface::luaMoveEventOnCallback); + registerMethod("MoveEvent", "onAddItem", LuaScriptInterface::luaMoveEventOnCallback); + registerMethod("MoveEvent", "onRemoveItem", LuaScriptInterface::luaMoveEventOnCallback); + + // GlobalEvent + registerClass("GlobalEvent", "", LuaScriptInterface::luaCreateGlobalEvent); + registerMethod("GlobalEvent", "type", LuaScriptInterface::luaGlobalEventType); + registerMethod("GlobalEvent", "register", LuaScriptInterface::luaGlobalEventRegister); + registerMethod("GlobalEvent", "time", LuaScriptInterface::luaGlobalEventTime); + registerMethod("GlobalEvent", "interval", LuaScriptInterface::luaGlobalEventInterval); + registerMethod("GlobalEvent", "onThink", LuaScriptInterface::luaGlobalEventOnCallback); + registerMethod("GlobalEvent", "onTime", LuaScriptInterface::luaGlobalEventOnCallback); + registerMethod("GlobalEvent", "onStartup", LuaScriptInterface::luaGlobalEventOnCallback); + registerMethod("GlobalEvent", "onShutdown", LuaScriptInterface::luaGlobalEventOnCallback); + registerMethod("GlobalEvent", "onRecord", LuaScriptInterface::luaGlobalEventOnCallback); + + // Weapon + registerClass("Weapon", "", LuaScriptInterface::luaCreateWeapon); + registerMethod("Weapon", "action", LuaScriptInterface::luaWeaponAction); + registerMethod("Weapon", "register", LuaScriptInterface::luaWeaponRegister); + registerMethod("Weapon", "id", LuaScriptInterface::luaWeaponId); + registerMethod("Weapon", "level", LuaScriptInterface::luaWeaponLevel); + registerMethod("Weapon", "magicLevel", LuaScriptInterface::luaWeaponMagicLevel); + registerMethod("Weapon", "mana", LuaScriptInterface::luaWeaponMana); + registerMethod("Weapon", "manaPercent", LuaScriptInterface::luaWeaponManaPercent); + registerMethod("Weapon", "health", LuaScriptInterface::luaWeaponHealth); + registerMethod("Weapon", "healthPercent", LuaScriptInterface::luaWeaponHealthPercent); + registerMethod("Weapon", "soul", LuaScriptInterface::luaWeaponSoul); + registerMethod("Weapon", "breakChance", LuaScriptInterface::luaWeaponBreakChance); + registerMethod("Weapon", "premium", LuaScriptInterface::luaWeaponPremium); + registerMethod("Weapon", "wieldUnproperly", LuaScriptInterface::luaWeaponUnproperly); + registerMethod("Weapon", "vocation", LuaScriptInterface::luaWeaponVocation); + registerMethod("Weapon", "onUseWeapon", LuaScriptInterface::luaWeaponOnUseWeapon); + registerMethod("Weapon", "element", LuaScriptInterface::luaWeaponElement); + registerMethod("Weapon", "attack", LuaScriptInterface::luaWeaponAttack); + registerMethod("Weapon", "defense", LuaScriptInterface::luaWeaponDefense); + registerMethod("Weapon", "range", LuaScriptInterface::luaWeaponRange); + registerMethod("Weapon", "charges", LuaScriptInterface::luaWeaponCharges); + registerMethod("Weapon", "duration", LuaScriptInterface::luaWeaponDuration); + registerMethod("Weapon", "decayTo", LuaScriptInterface::luaWeaponDecayTo); + registerMethod("Weapon", "transformEquipTo", LuaScriptInterface::luaWeaponTransformEquipTo); + registerMethod("Weapon", "transformDeEquipTo", LuaScriptInterface::luaWeaponTransformDeEquipTo); + registerMethod("Weapon", "slotType", LuaScriptInterface::luaWeaponSlotType); + registerMethod("Weapon", "hitChance", LuaScriptInterface::luaWeaponHitChance); + registerMethod("Weapon", "extraElement", LuaScriptInterface::luaWeaponExtraElement); + + // exclusively for distance weapons + registerMethod("Weapon", "ammoType", LuaScriptInterface::luaWeaponAmmoType); + registerMethod("Weapon", "maxHitChance", LuaScriptInterface::luaWeaponMaxHitChance); + + // exclusively for wands + registerMethod("Weapon", "damage", LuaScriptInterface::luaWeaponWandDamage); + + // exclusively for wands & distance weapons + registerMethod("Weapon", "shootType", LuaScriptInterface::luaWeaponShootType); +} + +#undef registerEnum +#undef registerEnumIn + +void LuaScriptInterface::registerClass(const std::string& className, const std::string& baseClass, lua_CFunction newFunction/* = nullptr*/) +{ + // className = {} + lua_newtable(luaState); + lua_pushvalue(luaState, -1); + lua_setglobal(luaState, className.c_str()); + int methods = lua_gettop(luaState); + + // methodsTable = {} + lua_newtable(luaState); + int methodsTable = lua_gettop(luaState); + + if (newFunction) { + // className.__call = newFunction + lua_pushcfunction(luaState, newFunction); + lua_setfield(luaState, methodsTable, "__call"); + } + + uint32_t parents = 0; + if (!baseClass.empty()) { + lua_getglobal(luaState, baseClass.c_str()); + lua_rawgeti(luaState, -1, 'p'); + parents = getNumber(luaState, -1) + 1; + lua_pop(luaState, 1); + lua_setfield(luaState, methodsTable, "__index"); + } + + // setmetatable(className, methodsTable) + lua_setmetatable(luaState, methods); + + // className.metatable = {} + luaL_newmetatable(luaState, className.c_str()); + int metatable = lua_gettop(luaState); + + // className.metatable.__metatable = className + lua_pushvalue(luaState, methods); + lua_setfield(luaState, metatable, "__metatable"); + + // className.metatable.__index = className + lua_pushvalue(luaState, methods); + lua_setfield(luaState, metatable, "__index"); + + // className.metatable['h'] = hash + lua_pushnumber(luaState, std::hash()(className)); + lua_rawseti(luaState, metatable, 'h'); + + // className.metatable['p'] = parents + lua_pushnumber(luaState, parents); + lua_rawseti(luaState, metatable, 'p'); + + // className.metatable['t'] = type + if (className == "Item") { + lua_pushnumber(luaState, LuaData_Item); + } else if (className == "Container") { + lua_pushnumber(luaState, LuaData_Container); + } else if (className == "Teleport") { + lua_pushnumber(luaState, LuaData_Teleport); + } else if (className == "Player") { + lua_pushnumber(luaState, LuaData_Player); + } else if (className == "Monster") { + lua_pushnumber(luaState, LuaData_Monster); + } else if (className == "Npc") { + lua_pushnumber(luaState, LuaData_Npc); + } else if (className == "Tile") { + lua_pushnumber(luaState, LuaData_Tile); + } else { + lua_pushnumber(luaState, LuaData_Unknown); + } + lua_rawseti(luaState, metatable, 't'); + + // pop className, className.metatable + lua_pop(luaState, 2); +} + +void LuaScriptInterface::registerTable(const std::string& tableName) +{ + // _G[tableName] = {} + lua_newtable(luaState); + lua_setglobal(luaState, tableName.c_str()); +} + +void LuaScriptInterface::registerMethod(const std::string& globalName, const std::string& methodName, lua_CFunction func) +{ + // globalName.methodName = func + lua_getglobal(luaState, globalName.c_str()); + lua_pushcfunction(luaState, func); + lua_setfield(luaState, -2, methodName.c_str()); + + // pop globalName + lua_pop(luaState, 1); +} + +void LuaScriptInterface::registerMetaMethod(const std::string& className, const std::string& methodName, lua_CFunction func) +{ + // className.metatable.methodName = func + luaL_getmetatable(luaState, className.c_str()); + lua_pushcfunction(luaState, func); + lua_setfield(luaState, -2, methodName.c_str()); + + // pop className.metatable + lua_pop(luaState, 1); +} + +void LuaScriptInterface::registerGlobalMethod(const std::string& functionName, lua_CFunction func) +{ + // _G[functionName] = func + lua_pushcfunction(luaState, func); + lua_setglobal(luaState, functionName.c_str()); +} + +void LuaScriptInterface::registerVariable(const std::string& tableName, const std::string& name, lua_Number value) +{ + // tableName.name = value + lua_getglobal(luaState, tableName.c_str()); + setField(luaState, name.c_str(), value); + + // pop tableName + lua_pop(luaState, 1); +} + +void LuaScriptInterface::registerGlobalVariable(const std::string& name, lua_Number value) +{ + // _G[name] = value + lua_pushnumber(luaState, value); + lua_setglobal(luaState, name.c_str()); +} + +void LuaScriptInterface::registerGlobalBoolean(const std::string& name, bool value) +{ + // _G[name] = value + pushBoolean(luaState, value); + lua_setglobal(luaState, name.c_str()); +} + +int LuaScriptInterface::luaDoPlayerAddItem(lua_State* L) +{ + //doPlayerAddItem(cid, itemid, count/subtype, canDropOnMap) + //doPlayerAddItem(cid, itemid, count, canDropOnMap, subtype) + Player* player = getPlayer(L, 1); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + uint16_t itemId = getNumber(L, 2); + int32_t count = getNumber(L, 3, 1); + bool canDropOnMap = getBoolean(L, 4, true); + uint16_t subType = getNumber(L, 5, 1); + + const ItemType& it = Item::items[itemId]; + int32_t itemCount; + + auto parameters = lua_gettop(L); + if (parameters > 4) { + //subtype already supplied, count then is the amount + itemCount = std::max(1, count); + } else if (it.hasSubType()) { + if (it.stackable) { + itemCount = static_cast(std::ceil(static_cast(count) / 100)); + } else { + itemCount = 1; + } + subType = count; + } else { + itemCount = std::max(1, count); + } + + while (itemCount > 0) { + uint16_t stackCount = subType; + if (it.stackable && stackCount > 100) { + stackCount = 100; + } + + Item* newItem = Item::CreateItem(itemId, stackCount); + if (!newItem) { + reportErrorFunc(getErrorDesc(LUA_ERROR_ITEM_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + if (it.stackable) { + subType -= stackCount; + } + + ReturnValue ret = g_game.internalPlayerAddItem(player, newItem, canDropOnMap); + if (ret != RETURNVALUE_NOERROR) { + delete newItem; + pushBoolean(L, false); + return 1; + } + + if (--itemCount == 0) { + if (newItem->getParent()) { + uint32_t uid = getScriptEnv()->addThing(newItem); + lua_pushnumber(L, uid); + return 1; + } else { + //stackable item stacked with existing object, newItem will be released + pushBoolean(L, false); + return 1; + } + } + } + + pushBoolean(L, false); + return 1; +} + +int LuaScriptInterface::luaDebugPrint(lua_State* L) +{ + //debugPrint(text) + reportErrorFunc(getString(L, -1)); + return 0; +} + +int LuaScriptInterface::luaGetWorldTime(lua_State* L) +{ + //getWorldTime() + uint32_t time = g_game.getLightHour(); + lua_pushnumber(L, time); + return 1; +} + +int LuaScriptInterface::luaGetWorldLight(lua_State* L) +{ + //getWorldLight() + LightInfo lightInfo = g_game.getWorldLightInfo(); + lua_pushnumber(L, lightInfo.level); + lua_pushnumber(L, lightInfo.color); + return 2; +} + +int LuaScriptInterface::luaGetWorldUpTime(lua_State* L) +{ + //getWorldUpTime() + uint64_t uptime = (OTSYS_TIME() - ProtocolStatus::start) / 1000; + lua_pushnumber(L, uptime); + return 1; +} + +bool LuaScriptInterface::getArea(lua_State* L, std::list& list, uint32_t& rows) +{ + lua_pushnil(L); + for (rows = 0; lua_next(L, -2) != 0; ++rows) { + if (!isTable(L, -1)) { + return false; + } + + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (!isNumber(L, -1)) { + return false; + } + list.push_back(getNumber(L, -1)); + lua_pop(L, 1); + } + + lua_pop(L, 1); + } + + lua_pop(L, 1); + return (rows != 0); +} + +int LuaScriptInterface::luaCreateCombatArea(lua_State* L) +{ + //createCombatArea( {area}, {extArea} ) + ScriptEnvironment* env = getScriptEnv(); + if (env->getScriptId() != EVENT_ID_LOADING) { + reportErrorFunc("This function can only be used while loading the script."); + pushBoolean(L, false); + return 1; + } + + uint32_t areaId = g_luaEnvironment.createAreaObject(env->getScriptInterface()); + AreaCombat* area = g_luaEnvironment.getAreaObject(areaId); + + int parameters = lua_gettop(L); + if (parameters >= 2) { + uint32_t rowsExtArea; + std::list listExtArea; + if (!isTable(L, 2) || !getArea(L, listExtArea, rowsExtArea)) { + reportErrorFunc("Invalid extended area table."); + pushBoolean(L, false); + return 1; + } + area->setupExtArea(listExtArea, rowsExtArea); + } + + uint32_t rowsArea = 0; + std::list listArea; + if (!isTable(L, 1) || !getArea(L, listArea, rowsArea)) { + reportErrorFunc("Invalid area table."); + pushBoolean(L, false); + return 1; + } + + area->setupArea(listArea, rowsArea); + lua_pushnumber(L, areaId); + return 1; +} + +int LuaScriptInterface::luaDoAreaCombat(lua_State* L) +{ + //doAreaCombat(cid, type, pos, area, min, max, effect[, origin = ORIGIN_SPELL]) + Creature* creature = getCreature(L, 1); + if (!creature && (!isNumber(L, 1) || getNumber(L, 1) != 0)) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + uint32_t areaId = getNumber(L, 4); + const AreaCombat* area = g_luaEnvironment.getAreaObject(areaId); + if (area || areaId == 0) { + CombatType_t combatType = getNumber(L, 2); + + CombatParams params; + params.combatType = combatType; + params.impactEffect = getNumber(L, 7); + + CombatDamage damage; + damage.origin = getNumber(L, 8, ORIGIN_SPELL); + damage.primary.type = combatType; + damage.primary.value = normal_random(getNumber(L, 6), getNumber(L, 5)); + + Combat::doAreaCombat(creature, getPosition(L, 3), area, damage, params); + pushBoolean(L, true); + } else { + reportErrorFunc(getErrorDesc(LUA_ERROR_AREA_NOT_FOUND)); + pushBoolean(L, false); + } + return 1; +} + +int LuaScriptInterface::luaDoTargetCombat(lua_State* L) +{ + //doTargetCombat(cid, target, type, min, max, effect[, origin = ORIGIN_SPELL]) + Creature* creature = getCreature(L, 1); + if (!creature && (!isNumber(L, 1) || getNumber(L, 1) != 0)) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + Creature* target = getCreature(L, 2); + if (!target) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + CombatType_t combatType = getNumber(L, 3); + + CombatParams params; + params.combatType = combatType; + params.impactEffect = getNumber(L, 6); + + CombatDamage damage; + damage.origin = getNumber(L, 7, ORIGIN_SPELL); + damage.primary.type = combatType; + damage.primary.value = normal_random(getNumber(L, 4), getNumber(L, 5)); + + Combat::doTargetCombat(creature, target, damage, params); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaDoChallengeCreature(lua_State* L) +{ + //doChallengeCreature(cid, target) + Creature* creature = getCreature(L, 1); + if (!creature) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + Creature* target = getCreature(L, 2); + if (!target) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + target->challengeCreature(creature); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaIsValidUID(lua_State* L) +{ + //isValidUID(uid) + pushBoolean(L, getScriptEnv()->getThingByUID(getNumber(L, -1)) != nullptr); + return 1; +} + +int LuaScriptInterface::luaIsDepot(lua_State* L) +{ + //isDepot(uid) + Container* container = getScriptEnv()->getContainerByUID(getNumber(L, -1)); + pushBoolean(L, container && container->getDepotLocker()); + return 1; +} + +int LuaScriptInterface::luaIsMoveable(lua_State* L) +{ + //isMoveable(uid) + //isMovable(uid) + Thing* thing = getScriptEnv()->getThingByUID(getNumber(L, -1)); + pushBoolean(L, thing && thing->isPushable()); + return 1; +} + +int LuaScriptInterface::luaDoAddContainerItem(lua_State* L) +{ + //doAddContainerItem(uid, itemid, count/subtype) + uint32_t uid = getNumber(L, 1); + + ScriptEnvironment* env = getScriptEnv(); + Container* container = env->getContainerByUID(uid); + if (!container) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CONTAINER_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + uint16_t itemId = getNumber(L, 2); + const ItemType& it = Item::items[itemId]; + + int32_t itemCount = 1; + int32_t subType = 1; + uint32_t count = getNumber(L, 3, 1); + + if (it.hasSubType()) { + if (it.stackable) { + itemCount = static_cast(std::ceil(static_cast(count) / 100)); + } + + subType = count; + } else { + itemCount = std::max(1, count); + } + + while (itemCount > 0) { + int32_t stackCount = std::min(100, subType); + Item* newItem = Item::CreateItem(itemId, stackCount); + if (!newItem) { + reportErrorFunc(getErrorDesc(LUA_ERROR_ITEM_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + if (it.stackable) { + subType -= stackCount; + } + + ReturnValue ret = g_game.internalAddItem(container, newItem); + if (ret != RETURNVALUE_NOERROR) { + delete newItem; + pushBoolean(L, false); + return 1; + } + + if (--itemCount == 0) { + if (newItem->getParent()) { + lua_pushnumber(L, env->addThing(newItem)); + } else { + //stackable item stacked with existing object, newItem will be released + pushBoolean(L, false); + } + return 1; + } + } + + pushBoolean(L, false); + return 1; +} + +int LuaScriptInterface::luaGetDepotId(lua_State* L) +{ + //getDepotId(uid) + uint32_t uid = getNumber(L, -1); + + Container* container = getScriptEnv()->getContainerByUID(uid); + if (!container) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CONTAINER_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + DepotLocker* depotLocker = container->getDepotLocker(); + if (!depotLocker) { + reportErrorFunc("Depot not found"); + pushBoolean(L, false); + return 1; + } + + lua_pushnumber(L, depotLocker->getDepotId()); + return 1; +} + +int LuaScriptInterface::luaDoSetCreatureLight(lua_State* L) +{ + //doSetCreatureLight(cid, lightLevel, lightColor, time) + Creature* creature = getCreature(L, 1); + if (!creature) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + uint16_t level = getNumber(L, 2); + uint16_t color = getNumber(L, 3); + uint32_t time = getNumber(L, 4); + Condition* condition = Condition::createCondition(CONDITIONID_COMBAT, CONDITION_LIGHT, time, level | (color << 8)); + creature->addCondition(condition); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaAddEvent(lua_State* L) +{ + //addEvent(callback, delay, ...) + lua_State* globalState = g_luaEnvironment.getLuaState(); + if (!globalState) { + reportErrorFunc("No valid script interface!"); + pushBoolean(L, false); + return 1; + } else if (globalState != L) { + lua_xmove(L, globalState, lua_gettop(L)); + } + + int parameters = lua_gettop(globalState); + if (!isFunction(globalState, -parameters)) { //-parameters means the first parameter from left to right + reportErrorFunc("callback parameter should be a function."); + pushBoolean(L, false); + return 1; + } + + if (g_config.getBoolean(ConfigManager::WARN_UNSAFE_SCRIPTS) || g_config.getBoolean(ConfigManager::CONVERT_UNSAFE_SCRIPTS)) { + std::vector> indexes; + for (int i = 3; i <= parameters; ++i) { + if (lua_getmetatable(globalState, i) == 0) { + continue; + } + lua_rawgeti(L, -1, 't'); + + LuaDataType type = getNumber(L, -1); + if (type != LuaData_Unknown && type != LuaData_Tile) { + indexes.push_back({i, type}); + } + lua_pop(globalState, 2); + } + + if (!indexes.empty()) { + if (g_config.getBoolean(ConfigManager::WARN_UNSAFE_SCRIPTS)) { + bool plural = indexes.size() > 1; + + std::string warningString = "Argument"; + if (plural) { + warningString += 's'; + } + + for (const auto& entry : indexes) { + if (entry == indexes.front()) { + warningString += ' '; + } else if (entry == indexes.back()) { + warningString += " and "; + } else { + warningString += ", "; + } + warningString += '#'; + warningString += std::to_string(entry.first); + } + + if (plural) { + warningString += " are unsafe"; + } else { + warningString += " is unsafe"; + } + + reportErrorFunc(warningString); + } + + if (g_config.getBoolean(ConfigManager::CONVERT_UNSAFE_SCRIPTS)) { + for (const auto& entry : indexes) { + switch (entry.second) { + case LuaData_Item: + case LuaData_Container: + case LuaData_Teleport: { + lua_getglobal(globalState, "Item"); + lua_getfield(globalState, -1, "getUniqueId"); + break; + } + case LuaData_Player: + case LuaData_Monster: + case LuaData_Npc: { + lua_getglobal(globalState, "Creature"); + lua_getfield(globalState, -1, "getId"); + break; + } + default: + break; + } + lua_replace(globalState, -2); + lua_pushvalue(globalState, entry.first); + lua_call(globalState, 1, 1); + lua_replace(globalState, entry.first); + } + } + } + } + + LuaTimerEventDesc eventDesc; + for (int i = 0; i < parameters - 2; ++i) { //-2 because addEvent needs at least two parameters + eventDesc.parameters.push_back(luaL_ref(globalState, LUA_REGISTRYINDEX)); + } + + uint32_t delay = std::max(100, getNumber(globalState, 2)); + lua_pop(globalState, 1); + + eventDesc.function = luaL_ref(globalState, LUA_REGISTRYINDEX); + eventDesc.scriptId = getScriptEnv()->getScriptId(); + + auto& lastTimerEventId = g_luaEnvironment.lastEventTimerId; + eventDesc.eventId = g_scheduler.addEvent(createSchedulerTask( + delay, std::bind(&LuaEnvironment::executeTimerEvent, &g_luaEnvironment, lastTimerEventId) + )); + + g_luaEnvironment.timerEvents.emplace(lastTimerEventId, std::move(eventDesc)); + lua_pushnumber(L, lastTimerEventId++); + return 1; +} + +int LuaScriptInterface::luaStopEvent(lua_State* L) +{ + //stopEvent(eventid) + lua_State* globalState = g_luaEnvironment.getLuaState(); + if (!globalState) { + reportErrorFunc("No valid script interface!"); + pushBoolean(L, false); + return 1; + } + + uint32_t eventId = getNumber(L, 1); + + auto& timerEvents = g_luaEnvironment.timerEvents; + auto it = timerEvents.find(eventId); + if (it == timerEvents.end()) { + pushBoolean(L, false); + return 1; + } + + LuaTimerEventDesc timerEventDesc = std::move(it->second); + timerEvents.erase(it); + + g_scheduler.stopEvent(timerEventDesc.eventId); + luaL_unref(globalState, LUA_REGISTRYINDEX, timerEventDesc.function); + + for (auto parameter : timerEventDesc.parameters) { + luaL_unref(globalState, LUA_REGISTRYINDEX, parameter); + } + + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaSaveServer(lua_State* L) +{ + g_game.saveGameState(); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaCleanMap(lua_State* L) +{ + lua_pushnumber(L, g_game.map.clean()); + return 1; +} + +int LuaScriptInterface::luaIsInWar(lua_State* L) +{ + //isInWar(cid, target) + Player* player = getPlayer(L, 1); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + Player* targetPlayer = getPlayer(L, 2); + if (!targetPlayer) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + pushBoolean(L, player->isInWar(targetPlayer)); + return 1; +} + +int LuaScriptInterface::luaGetWaypointPositionByName(lua_State* L) +{ + //getWaypointPositionByName(name) + auto& waypoints = g_game.map.waypoints; + + auto it = waypoints.find(getString(L, -1)); + if (it != waypoints.end()) { + pushPosition(L, it->second); + } else { + pushBoolean(L, false); + } + return 1; +} + +int LuaScriptInterface::luaSendChannelMessage(lua_State* L) +{ + //sendChannelMessage(channelId, type, message) + uint32_t channelId = getNumber(L, 1); + ChatChannel* channel = g_chat->getChannelById(channelId); + if (!channel) { + pushBoolean(L, false); + return 1; + } + + SpeakClasses type = getNumber(L, 2); + std::string message = getString(L, 3); + channel->sendToAll(message, type); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaSendGuildChannelMessage(lua_State* L) +{ + //sendGuildChannelMessage(guildId, type, message) + uint32_t guildId = getNumber(L, 1); + ChatChannel* channel = g_chat->getGuildChannelById(guildId); + if (!channel) { + pushBoolean(L, false); + return 1; + } + + SpeakClasses type = getNumber(L, 2); + std::string message = getString(L, 3); + channel->sendToAll(message, type); + pushBoolean(L, true); + return 1; +} + +std::string LuaScriptInterface::escapeString(const std::string& string) +{ + std::string s = string; + replaceString(s, "\\", "\\\\"); + replaceString(s, "\"", "\\\""); + replaceString(s, "'", "\\'"); + replaceString(s, "[[", "\\[["); + return s; +} + +#ifndef LUAJIT_VERSION +const luaL_Reg LuaScriptInterface::luaBitReg[] = { + //{"tobit", LuaScriptInterface::luaBitToBit}, + {"bnot", LuaScriptInterface::luaBitNot}, + {"band", LuaScriptInterface::luaBitAnd}, + {"bor", LuaScriptInterface::luaBitOr}, + {"bxor", LuaScriptInterface::luaBitXor}, + {"lshift", LuaScriptInterface::luaBitLeftShift}, + {"rshift", LuaScriptInterface::luaBitRightShift}, + //{"arshift", LuaScriptInterface::luaBitArithmeticalRightShift}, + //{"rol", LuaScriptInterface::luaBitRotateLeft}, + //{"ror", LuaScriptInterface::luaBitRotateRight}, + //{"bswap", LuaScriptInterface::luaBitSwapEndian}, + //{"tohex", LuaScriptInterface::luaBitToHex}, + {nullptr, nullptr} +}; + +int LuaScriptInterface::luaBitNot(lua_State* L) +{ + lua_pushnumber(L, ~getNumber(L, -1)); + return 1; +} + +#define MULTIOP(name, op) \ +int LuaScriptInterface::luaBit##name(lua_State* L) \ +{ \ + int n = lua_gettop(L); \ + uint32_t w = getNumber(L, -1); \ + for (int i = 1; i < n; ++i) \ + w op getNumber(L, i); \ + lua_pushnumber(L, w); \ + return 1; \ +} + +MULTIOP(And, &= ) +MULTIOP(Or, |= ) +MULTIOP(Xor, ^= ) + +#define SHIFTOP(name, op) \ +int LuaScriptInterface::luaBit##name(lua_State* L) \ +{ \ + uint32_t n1 = getNumber(L, 1), n2 = getNumber(L, 2); \ + lua_pushnumber(L, (n1 op n2)); \ + return 1; \ +} + +SHIFTOP(LeftShift, << ) +SHIFTOP(RightShift, >> ) +#endif + +const luaL_Reg LuaScriptInterface::luaConfigManagerTable[] = { + {"getString", LuaScriptInterface::luaConfigManagerGetString}, + {"getNumber", LuaScriptInterface::luaConfigManagerGetNumber}, + {"getBoolean", LuaScriptInterface::luaConfigManagerGetBoolean}, + {nullptr, nullptr} +}; + +int LuaScriptInterface::luaConfigManagerGetString(lua_State* L) +{ + pushString(L, g_config.getString(getNumber(L, -1))); + return 1; +} + +int LuaScriptInterface::luaConfigManagerGetNumber(lua_State* L) +{ + lua_pushnumber(L, g_config.getNumber(getNumber(L, -1))); + return 1; +} + +int LuaScriptInterface::luaConfigManagerGetBoolean(lua_State* L) +{ + pushBoolean(L, g_config.getBoolean(getNumber(L, -1))); + return 1; +} + +const luaL_Reg LuaScriptInterface::luaDatabaseTable[] = { + {"query", LuaScriptInterface::luaDatabaseExecute}, + {"asyncQuery", LuaScriptInterface::luaDatabaseAsyncExecute}, + {"storeQuery", LuaScriptInterface::luaDatabaseStoreQuery}, + {"asyncStoreQuery", LuaScriptInterface::luaDatabaseAsyncStoreQuery}, + {"escapeString", LuaScriptInterface::luaDatabaseEscapeString}, + {"escapeBlob", LuaScriptInterface::luaDatabaseEscapeBlob}, + {"lastInsertId", LuaScriptInterface::luaDatabaseLastInsertId}, + {"tableExists", LuaScriptInterface::luaDatabaseTableExists}, + {nullptr, nullptr} +}; + +int LuaScriptInterface::luaDatabaseExecute(lua_State* L) +{ + pushBoolean(L, Database::getInstance().executeQuery(getString(L, -1))); + return 1; +} + +int LuaScriptInterface::luaDatabaseAsyncExecute(lua_State* L) +{ + std::function callback; + if (lua_gettop(L) > 1) { + int32_t ref = luaL_ref(L, LUA_REGISTRYINDEX); + auto scriptId = getScriptEnv()->getScriptId(); + callback = [ref, scriptId](DBResult_ptr, bool success) { + lua_State* luaState = g_luaEnvironment.getLuaState(); + if (!luaState) { + return; + } + + if (!LuaScriptInterface::reserveScriptEnv()) { + luaL_unref(luaState, LUA_REGISTRYINDEX, ref); + return; + } + + lua_rawgeti(luaState, LUA_REGISTRYINDEX, ref); + pushBoolean(luaState, success); + auto env = getScriptEnv(); + env->setScriptId(scriptId, &g_luaEnvironment); + g_luaEnvironment.callFunction(1); + + luaL_unref(luaState, LUA_REGISTRYINDEX, ref); + }; + } + g_databaseTasks.addTask(getString(L, -1), callback); + return 0; +} + +int LuaScriptInterface::luaDatabaseStoreQuery(lua_State* L) +{ + if (DBResult_ptr res = Database::getInstance().storeQuery(getString(L, -1))) { + lua_pushnumber(L, ScriptEnvironment::addResult(res)); + } else { + pushBoolean(L, false); + } + return 1; +} + +int LuaScriptInterface::luaDatabaseAsyncStoreQuery(lua_State* L) +{ + std::function callback; + if (lua_gettop(L) > 1) { + int32_t ref = luaL_ref(L, LUA_REGISTRYINDEX); + auto scriptId = getScriptEnv()->getScriptId(); + callback = [ref, scriptId](DBResult_ptr result, bool) { + lua_State* luaState = g_luaEnvironment.getLuaState(); + if (!luaState) { + return; + } + + if (!LuaScriptInterface::reserveScriptEnv()) { + luaL_unref(luaState, LUA_REGISTRYINDEX, ref); + return; + } + + lua_rawgeti(luaState, LUA_REGISTRYINDEX, ref); + if (result) { + lua_pushnumber(luaState, ScriptEnvironment::addResult(result)); + } else { + pushBoolean(luaState, false); + } + auto env = getScriptEnv(); + env->setScriptId(scriptId, &g_luaEnvironment); + g_luaEnvironment.callFunction(1); + + luaL_unref(luaState, LUA_REGISTRYINDEX, ref); + }; + } + g_databaseTasks.addTask(getString(L, -1), callback, true); + return 0; +} + +int LuaScriptInterface::luaDatabaseEscapeString(lua_State* L) +{ + pushString(L, Database::getInstance().escapeString(getString(L, -1))); + return 1; +} + +int LuaScriptInterface::luaDatabaseEscapeBlob(lua_State* L) +{ + uint32_t length = getNumber(L, 2); + pushString(L, Database::getInstance().escapeBlob(getString(L, 1).c_str(), length)); + return 1; +} + +int LuaScriptInterface::luaDatabaseLastInsertId(lua_State* L) +{ + lua_pushnumber(L, Database::getInstance().getLastInsertId()); + return 1; +} + +int LuaScriptInterface::luaDatabaseTableExists(lua_State* L) +{ + pushBoolean(L, DatabaseManager::tableExists(getString(L, -1))); + return 1; +} + +const luaL_Reg LuaScriptInterface::luaResultTable[] = { + {"getNumber", LuaScriptInterface::luaResultGetNumber}, + {"getString", LuaScriptInterface::luaResultGetString}, + {"getStream", LuaScriptInterface::luaResultGetStream}, + {"next", LuaScriptInterface::luaResultNext}, + {"free", LuaScriptInterface::luaResultFree}, + {nullptr, nullptr} +}; + +int LuaScriptInterface::luaResultGetNumber(lua_State* L) +{ + DBResult_ptr res = ScriptEnvironment::getResultByID(getNumber(L, 1)); + if (!res) { + pushBoolean(L, false); + return 1; + } + + const std::string& s = getString(L, 2); + lua_pushnumber(L, res->getNumber(s)); + return 1; +} + +int LuaScriptInterface::luaResultGetString(lua_State* L) +{ + DBResult_ptr res = ScriptEnvironment::getResultByID(getNumber(L, 1)); + if (!res) { + pushBoolean(L, false); + return 1; + } + + const std::string& s = getString(L, 2); + pushString(L, res->getString(s)); + return 1; +} + +int LuaScriptInterface::luaResultGetStream(lua_State* L) +{ + DBResult_ptr res = ScriptEnvironment::getResultByID(getNumber(L, 1)); + if (!res) { + pushBoolean(L, false); + return 1; + } + + unsigned long length; + const char* stream = res->getStream(getString(L, 2), length); + lua_pushlstring(L, stream, length); + lua_pushnumber(L, length); + return 2; +} + +int LuaScriptInterface::luaResultNext(lua_State* L) +{ + DBResult_ptr res = ScriptEnvironment::getResultByID(getNumber(L, -1)); + if (!res) { + pushBoolean(L, false); + return 1; + } + + pushBoolean(L, res->next()); + return 1; +} + +int LuaScriptInterface::luaResultFree(lua_State* L) +{ + pushBoolean(L, ScriptEnvironment::removeResult(getNumber(L, -1))); + return 1; +} + +// Userdata +int LuaScriptInterface::luaUserdataCompare(lua_State* L) +{ + // userdataA == userdataB + pushBoolean(L, getUserdata(L, 1) == getUserdata(L, 2)); + return 1; +} + +// _G +int LuaScriptInterface::luaIsType(lua_State* L) +{ + // isType(derived, base) + lua_getmetatable(L, -2); + lua_getmetatable(L, -2); + + lua_rawgeti(L, -2, 'p'); + uint_fast8_t parentsB = getNumber(L, 1); + + lua_rawgeti(L, -3, 'h'); + size_t hashB = getNumber(L, 1); + + lua_rawgeti(L, -3, 'p'); + uint_fast8_t parentsA = getNumber(L, 1); + for (uint_fast8_t i = parentsA; i < parentsB; ++i) { + lua_getfield(L, -3, "__index"); + lua_replace(L, -4); + } + + lua_rawgeti(L, -4, 'h'); + size_t hashA = getNumber(L, 1); + + pushBoolean(L, hashA == hashB); + return 1; +} + +int LuaScriptInterface::luaRawGetMetatable(lua_State* L) +{ + // rawgetmetatable(metatableName) + luaL_getmetatable(L, getString(L, 1).c_str()); + return 1; +} + +// os +int LuaScriptInterface::luaSystemTime(lua_State* L) +{ + // os.mtime() + lua_pushnumber(L, OTSYS_TIME()); + return 1; +} + +// table +int LuaScriptInterface::luaTableCreate(lua_State* L) +{ + // table.create(arrayLength, keyLength) + lua_createtable(L, getNumber(L, 1), getNumber(L, 2)); + return 1; +} + +// Game +int LuaScriptInterface::luaGameGetSpectators(lua_State* L) +{ + // Game.getSpectators(position[, multifloor = false[, onlyPlayer = false[, minRangeX = 0[, maxRangeX = 0[, minRangeY = 0[, maxRangeY = 0]]]]]]) + const Position& position = getPosition(L, 1); + bool multifloor = getBoolean(L, 2, false); + bool onlyPlayers = getBoolean(L, 3, false); + int32_t minRangeX = getNumber(L, 4, 0); + int32_t maxRangeX = getNumber(L, 5, 0); + int32_t minRangeY = getNumber(L, 6, 0); + int32_t maxRangeY = getNumber(L, 7, 0); + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, position, multifloor, onlyPlayers, minRangeX, maxRangeX, minRangeY, maxRangeY); + + lua_createtable(L, spectators.size(), 0); + + int index = 0; + for (Creature* creature : spectators) { + pushUserdata(L, creature); + setCreatureMetatable(L, -1, creature); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaGameGetPlayers(lua_State* L) +{ + // Game.getPlayers() + lua_createtable(L, g_game.getPlayersOnline(), 0); + + int index = 0; + for (const auto& playerEntry : g_game.getPlayers()) { + pushUserdata(L, playerEntry.second); + setMetatable(L, -1, "Player"); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaGameLoadMap(lua_State* L) +{ + // Game.loadMap(path) + const std::string& path = getString(L, 1); + g_dispatcher.addTask(createTask([path]() { + try { + g_game.loadMap(path); + } catch (const std::exception& e) { + // FIXME: Should only catch some exceptions + std::cout << "[Error - LuaScriptInterface::luaGameLoadMap] Failed to load map: " + << e.what() << std::endl; + } + })); + return 0; +} + +int LuaScriptInterface::luaGameGetExperienceStage(lua_State* L) +{ + // Game.getExperienceStage(level) + uint32_t level = getNumber(L, 1); + lua_pushnumber(L, g_game.getExperienceStage(level)); + return 1; +} + +int LuaScriptInterface::luaGameGetMonsterCount(lua_State* L) +{ + // Game.getMonsterCount() + lua_pushnumber(L, g_game.getMonstersOnline()); + return 1; +} + +int LuaScriptInterface::luaGameGetPlayerCount(lua_State* L) +{ + // Game.getPlayerCount() + lua_pushnumber(L, g_game.getPlayersOnline()); + return 1; +} + +int LuaScriptInterface::luaGameGetNpcCount(lua_State* L) +{ + // Game.getNpcCount() + lua_pushnumber(L, g_game.getNpcsOnline()); + return 1; +} + +int LuaScriptInterface::luaGameGetMonsterTypes(lua_State* L) +{ + // Game.getMonsterTypes() + auto& type = g_monsters.monsters; + lua_createtable(L, type.size(), 0); + + for (auto& mType : type) { + pushUserdata(L, &mType.second); + setMetatable(L, -1, "MonsterType"); + lua_setfield(L, -2, mType.first.c_str()); + } + return 1; +} + +int LuaScriptInterface::luaGameGetTowns(lua_State* L) +{ + // Game.getTowns() + const auto& towns = g_game.map.towns.getTowns(); + lua_createtable(L, towns.size(), 0); + + int index = 0; + for (auto townEntry : towns) { + pushUserdata(L, townEntry.second); + setMetatable(L, -1, "Town"); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaGameGetHouses(lua_State* L) +{ + // Game.getHouses() + const auto& houses = g_game.map.houses.getHouses(); + lua_createtable(L, houses.size(), 0); + + int index = 0; + for (auto houseEntry : houses) { + pushUserdata(L, houseEntry.second); + setMetatable(L, -1, "House"); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaGameGetGameState(lua_State* L) +{ + // Game.getGameState() + lua_pushnumber(L, g_game.getGameState()); + return 1; +} + +int LuaScriptInterface::luaGameSetGameState(lua_State* L) +{ + // Game.setGameState(state) + GameState_t state = getNumber(L, 1); + g_game.setGameState(state); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaGameGetWorldType(lua_State* L) +{ + // Game.getWorldType() + lua_pushnumber(L, g_game.getWorldType()); + return 1; +} + +int LuaScriptInterface::luaGameSetWorldType(lua_State* L) +{ + // Game.setWorldType(type) + WorldType_t type = getNumber(L, 1); + g_game.setWorldType(type); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaGameGetReturnMessage(lua_State* L) +{ + // Game.getReturnMessage(value) + ReturnValue value = getNumber(L, 1); + pushString(L, getReturnMessage(value)); + return 1; +} + +int LuaScriptInterface::luaGameCreateItem(lua_State* L) +{ + // Game.createItem(itemId[, count[, position]]) + uint16_t count = getNumber(L, 2, 1); + uint16_t id; + if (isNumber(L, 1)) { + id = getNumber(L, 1); + } else { + id = Item::items.getItemIdByName(getString(L, 1)); + if (id == 0) { + lua_pushnil(L); + return 1; + } + } + + const ItemType& it = Item::items[id]; + if (it.stackable) { + count = std::min(count, 100); + } + + Item* item = Item::CreateItem(id, count); + if (!item) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) >= 3) { + const Position& position = getPosition(L, 3); + Tile* tile = g_game.map.getTile(position); + if (!tile) { + delete item; + lua_pushnil(L); + return 1; + } + + g_game.internalAddItem(tile, item, INDEX_WHEREEVER, FLAG_NOLIMIT); + } else { + getScriptEnv()->addTempItem(item); + item->setParent(VirtualCylinder::virtualCylinder); + } + + pushUserdata(L, item); + setItemMetatable(L, -1, item); + return 1; +} + +int LuaScriptInterface::luaGameCreateContainer(lua_State* L) +{ + // Game.createContainer(itemId, size[, position]) + uint16_t size = getNumber(L, 2); + uint16_t id; + if (isNumber(L, 1)) { + id = getNumber(L, 1); + } else { + id = Item::items.getItemIdByName(getString(L, 1)); + if (id == 0) { + lua_pushnil(L); + return 1; + } + } + + Container* container = Item::CreateItemAsContainer(id, size); + if (!container) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) >= 3) { + const Position& position = getPosition(L, 3); + Tile* tile = g_game.map.getTile(position); + if (!tile) { + delete container; + lua_pushnil(L); + return 1; + } + + g_game.internalAddItem(tile, container, INDEX_WHEREEVER, FLAG_NOLIMIT); + } else { + getScriptEnv()->addTempItem(container); + container->setParent(VirtualCylinder::virtualCylinder); + } + + pushUserdata(L, container); + setMetatable(L, -1, "Container"); + return 1; +} + +int LuaScriptInterface::luaGameCreateMonster(lua_State* L) +{ + // Game.createMonster(monsterName, position[, extended = false[, force = false]]) + Monster* monster = Monster::createMonster(getString(L, 1)); + if (!monster) { + lua_pushnil(L); + return 1; + } + + const Position& position = getPosition(L, 2); + bool extended = getBoolean(L, 3, false); + bool force = getBoolean(L, 4, false); + if (g_events->eventMonsterOnSpawn(monster, position, false, true) || force) { + if (g_game.placeCreature(monster, position, extended, force)) { + pushUserdata(L, monster); + setMetatable(L, -1, "Monster"); + } else { + delete monster; + lua_pushnil(L); + } + } else { + delete monster; + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGameCreateNpc(lua_State* L) +{ + // Game.createNpc(npcName, position[, extended = false[, force = false]]) + Npc* npc = Npc::createNpc(getString(L, 1)); + if (!npc) { + lua_pushnil(L); + return 1; + } + + const Position& position = getPosition(L, 2); + bool extended = getBoolean(L, 3, false); + bool force = getBoolean(L, 4, false); + if (g_game.placeCreature(npc, position, extended, force)) { + pushUserdata(L, npc); + setMetatable(L, -1, "Npc"); + } else { + delete npc; + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGameCreateTile(lua_State* L) +{ + // Game.createTile(x, y, z[, isDynamic = false]) + // Game.createTile(position[, isDynamic = false]) + Position position; + bool isDynamic; + if (isTable(L, 1)) { + position = getPosition(L, 1); + isDynamic = getBoolean(L, 2, false); + } else { + position.x = getNumber(L, 1); + position.y = getNumber(L, 2); + position.z = getNumber(L, 3); + isDynamic = getBoolean(L, 4, false); + } + + Tile* tile = g_game.map.getTile(position); + if (!tile) { + if (isDynamic) { + tile = new DynamicTile(position.x, position.y, position.z); + } else { + tile = new StaticTile(position.x, position.y, position.z); + } + + g_game.map.setTile(position, tile); + } + + pushUserdata(L, tile); + setMetatable(L, -1, "Tile"); + return 1; +} + +int LuaScriptInterface::luaGameCreateMonsterType(lua_State* L) +{ + // Game.createMonsterType(name) + if (getScriptEnv()->getScriptInterface() != &g_scripts->getScriptInterface()) { + reportErrorFunc("MonsterTypes can only be registered in the Scripts interface."); + lua_pushnil(L); + return 1; + } + + const std::string& name = getString(L, 1); + if (name.length() == 0) { + lua_pushnil(L); + return 1; + } + + MonsterType* monsterType = g_monsters.getMonsterType(name, false); + if (!monsterType) { + monsterType = &g_monsters.monsters[asLowerCaseString(name)]; + monsterType->name = name; + monsterType->nameDescription = "a " + name; + } else { + monsterType->info.lootItems.clear(); + monsterType->info.attackSpells.clear(); + monsterType->info.defenseSpells.clear(); + } + + pushUserdata(L, monsterType); + setMetatable(L, -1, "MonsterType"); + return 1; +} + +int LuaScriptInterface::luaGameStartRaid(lua_State* L) +{ + // Game.startRaid(raidName) + const std::string& raidName = getString(L, 1); + + Raid* raid = g_game.raids.getRaidByName(raidName); + if (!raid || !raid->isLoaded()) { + lua_pushnumber(L, RETURNVALUE_NOSUCHRAIDEXISTS); + return 1; + } + + if (g_game.raids.getRunning()) { + lua_pushnumber(L, RETURNVALUE_ANOTHERRAIDISALREADYEXECUTING); + return 1; + } + + g_game.raids.setRunning(raid); + raid->startRaid(); + lua_pushnumber(L, RETURNVALUE_NOERROR); + return 1; +} + +int LuaScriptInterface::luaGameGetClientVersion(lua_State* L) +{ + // Game.getClientVersion() + lua_createtable(L, 0, 3); + setField(L, "min", CLIENT_VERSION_MIN); + setField(L, "max", CLIENT_VERSION_MAX); + setField(L, "string", CLIENT_VERSION_STR); + return 1; +} + +int LuaScriptInterface::luaGameReload(lua_State* L) +{ + // Game.reload(reloadType) + ReloadTypes_t reloadType = getNumber(L, 1); + if (reloadType == RELOAD_TYPE_GLOBAL) { + pushBoolean(L, g_luaEnvironment.loadFile("data/global.lua") == 0); + pushBoolean(L, g_scripts->loadScripts("scripts/lib", true, true)); + } else { + pushBoolean(L, g_game.reload(reloadType)); + } + lua_gc(g_luaEnvironment.getLuaState(), LUA_GCCOLLECT, 0); + return 1; +} + +// Variant +int LuaScriptInterface::luaVariantCreate(lua_State* L) +{ + // Variant(number or string or position or thing) + LuaVariant variant; + if (isUserdata(L, 2)) { + if (Thing* thing = getThing(L, 2)) { + variant.type = VARIANT_TARGETPOSITION; + variant.pos = thing->getPosition(); + } + } else if (isTable(L, 2)) { + variant.type = VARIANT_POSITION; + variant.pos = getPosition(L, 2); + } else if (isNumber(L, 2)) { + variant.type = VARIANT_NUMBER; + variant.number = getNumber(L, 2); + } else if (isString(L, 2)) { + variant.type = VARIANT_STRING; + variant.text = getString(L, 2); + } + pushVariant(L, variant); + return 1; +} + +int LuaScriptInterface::luaVariantGetNumber(lua_State* L) +{ + // Variant:getNumber() + const LuaVariant& variant = getVariant(L, 1); + if (variant.type == VARIANT_NUMBER) { + lua_pushnumber(L, variant.number); + } else { + lua_pushnumber(L, 0); + } + return 1; +} + +int LuaScriptInterface::luaVariantGetString(lua_State* L) +{ + // Variant:getString() + const LuaVariant& variant = getVariant(L, 1); + if (variant.type == VARIANT_STRING) { + pushString(L, variant.text); + } else { + pushString(L, std::string()); + } + return 1; +} + +int LuaScriptInterface::luaVariantGetPosition(lua_State* L) +{ + // Variant:getPosition() + const LuaVariant& variant = getVariant(L, 1); + if (variant.type == VARIANT_POSITION || variant.type == VARIANT_TARGETPOSITION) { + pushPosition(L, variant.pos); + } else { + pushPosition(L, Position()); + } + return 1; +} + +// Position +int LuaScriptInterface::luaPositionCreate(lua_State* L) +{ + // Position([x = 0[, y = 0[, z = 0[, stackpos = 0]]]]) + // Position([position]) + if (lua_gettop(L) <= 1) { + pushPosition(L, Position()); + return 1; + } + + int32_t stackpos; + if (isTable(L, 2)) { + const Position& position = getPosition(L, 2, stackpos); + pushPosition(L, position, stackpos); + } else { + uint16_t x = getNumber(L, 2, 0); + uint16_t y = getNumber(L, 3, 0); + uint8_t z = getNumber(L, 4, 0); + stackpos = getNumber(L, 5, 0); + + pushPosition(L, Position(x, y, z), stackpos); + } + return 1; +} + +int LuaScriptInterface::luaPositionAdd(lua_State* L) +{ + // positionValue = position + positionEx + int32_t stackpos; + const Position& position = getPosition(L, 1, stackpos); + + Position positionEx; + if (stackpos == 0) { + positionEx = getPosition(L, 2, stackpos); + } else { + positionEx = getPosition(L, 2); + } + + pushPosition(L, position + positionEx, stackpos); + return 1; +} + +int LuaScriptInterface::luaPositionSub(lua_State* L) +{ + // positionValue = position - positionEx + int32_t stackpos; + const Position& position = getPosition(L, 1, stackpos); + + Position positionEx; + if (stackpos == 0) { + positionEx = getPosition(L, 2, stackpos); + } else { + positionEx = getPosition(L, 2); + } + + pushPosition(L, position - positionEx, stackpos); + return 1; +} + +int LuaScriptInterface::luaPositionCompare(lua_State* L) +{ + // position == positionEx + const Position& positionEx = getPosition(L, 2); + const Position& position = getPosition(L, 1); + pushBoolean(L, position == positionEx); + return 1; +} + +int LuaScriptInterface::luaPositionGetDistance(lua_State* L) +{ + // position:getDistance(positionEx) + const Position& positionEx = getPosition(L, 2); + const Position& position = getPosition(L, 1); + lua_pushnumber(L, std::max( + std::max( + std::abs(Position::getDistanceX(position, positionEx)), + std::abs(Position::getDistanceY(position, positionEx)) + ), + std::abs(Position::getDistanceZ(position, positionEx)) + )); + return 1; +} + +int LuaScriptInterface::luaPositionIsSightClear(lua_State* L) +{ + // position:isSightClear(positionEx[, sameFloor = true]) + bool sameFloor = getBoolean(L, 3, true); + const Position& positionEx = getPosition(L, 2); + const Position& position = getPosition(L, 1); + pushBoolean(L, g_game.isSightClear(position, positionEx, sameFloor)); + return 1; +} + +int LuaScriptInterface::luaPositionSendMagicEffect(lua_State* L) +{ + // position:sendMagicEffect(magicEffect[, player = nullptr]) + SpectatorVec spectators; + if (lua_gettop(L) >= 3) { + Player* player = getPlayer(L, 3); + if (player) { + spectators.emplace_back(player); + } + } + + MagicEffectClasses magicEffect = getNumber(L, 2); + const Position& position = getPosition(L, 1); + if (!spectators.empty()) { + Game::addMagicEffect(spectators, position, magicEffect); + } else { + g_game.addMagicEffect(position, magicEffect); + } + + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPositionSendDistanceEffect(lua_State* L) +{ + // position:sendDistanceEffect(positionEx, distanceEffect[, player = nullptr]) + SpectatorVec spectators; + if (lua_gettop(L) >= 4) { + Player* player = getPlayer(L, 4); + if (player) { + spectators.emplace_back(player); + } + } + + ShootType_t distanceEffect = getNumber(L, 3); + const Position& positionEx = getPosition(L, 2); + const Position& position = getPosition(L, 1); + if (!spectators.empty()) { + Game::addDistanceEffect(spectators, position, positionEx, distanceEffect); + } else { + g_game.addDistanceEffect(position, positionEx, distanceEffect); + } + + pushBoolean(L, true); + return 1; +} + +// Tile +int LuaScriptInterface::luaTileCreate(lua_State* L) +{ + // Tile(x, y, z) + // Tile(position) + Tile* tile; + if (isTable(L, 2)) { + tile = g_game.map.getTile(getPosition(L, 2)); + } else { + uint8_t z = getNumber(L, 4); + uint16_t y = getNumber(L, 3); + uint16_t x = getNumber(L, 2); + tile = g_game.map.getTile(x, y, z); + } + + if (tile) { + pushUserdata(L, tile); + setMetatable(L, -1, "Tile"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetPosition(lua_State* L) +{ + // tile:getPosition() + Tile* tile = getUserdata(L, 1); + if (tile) { + pushPosition(L, tile->getPosition()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetGround(lua_State* L) +{ + // tile:getGround() + Tile* tile = getUserdata(L, 1); + if (tile && tile->getGround()) { + pushUserdata(L, tile->getGround()); + setItemMetatable(L, -1, tile->getGround()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetThing(lua_State* L) +{ + // tile:getThing(index) + int32_t index = getNumber(L, 2); + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + Thing* thing = tile->getThing(index); + if (!thing) { + lua_pushnil(L); + return 1; + } + + if (Creature* creature = thing->getCreature()) { + pushUserdata(L, creature); + setCreatureMetatable(L, -1, creature); + } else if (Item* item = thing->getItem()) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetThingCount(lua_State* L) +{ + // tile:getThingCount() + Tile* tile = getUserdata(L, 1); + if (tile) { + lua_pushnumber(L, tile->getThingCount()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetTopVisibleThing(lua_State* L) +{ + // tile:getTopVisibleThing(creature) + Creature* creature = getCreature(L, 2); + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + Thing* thing = tile->getTopVisibleThing(creature); + if (!thing) { + lua_pushnil(L); + return 1; + } + + if (Creature* visibleCreature = thing->getCreature()) { + pushUserdata(L, visibleCreature); + setCreatureMetatable(L, -1, visibleCreature); + } else if (Item* visibleItem = thing->getItem()) { + pushUserdata(L, visibleItem); + setItemMetatable(L, -1, visibleItem); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetTopTopItem(lua_State* L) +{ + // tile:getTopTopItem() + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + Item* item = tile->getTopTopItem(); + if (item) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetTopDownItem(lua_State* L) +{ + // tile:getTopDownItem() + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + Item* item = tile->getTopDownItem(); + if (item) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetFieldItem(lua_State* L) +{ + // tile:getFieldItem() + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + Item* item = tile->getFieldItem(); + if (item) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetItemById(lua_State* L) +{ + // tile:getItemById(itemId[, subType = -1]) + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + int32_t subType = getNumber(L, 3, -1); + + Item* item = g_game.findItemOfType(tile, itemId, false, subType); + if (item) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetItemByType(lua_State* L) +{ + // tile:getItemByType(itemType) + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + bool found; + + ItemTypes_t itemType = getNumber(L, 2); + switch (itemType) { + case ITEM_TYPE_TELEPORT: + found = tile->hasFlag(TILESTATE_TELEPORT); + break; + case ITEM_TYPE_MAGICFIELD: + found = tile->hasFlag(TILESTATE_MAGICFIELD); + break; + case ITEM_TYPE_MAILBOX: + found = tile->hasFlag(TILESTATE_MAILBOX); + break; + case ITEM_TYPE_TRASHHOLDER: + found = tile->hasFlag(TILESTATE_TRASHHOLDER); + break; + case ITEM_TYPE_BED: + found = tile->hasFlag(TILESTATE_BED); + break; + case ITEM_TYPE_DEPOT: + found = tile->hasFlag(TILESTATE_DEPOT); + break; + default: + found = true; + break; + } + + if (!found) { + lua_pushnil(L); + return 1; + } + + if (Item* item = tile->getGround()) { + const ItemType& it = Item::items[item->getID()]; + if (it.type == itemType) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + return 1; + } + } + + if (const TileItemVector* items = tile->getItemList()) { + for (Item* item : *items) { + const ItemType& it = Item::items[item->getID()]; + if (it.type == itemType) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + return 1; + } + } + } + + lua_pushnil(L); + return 1; +} + +int LuaScriptInterface::luaTileGetItemByTopOrder(lua_State* L) +{ + // tile:getItemByTopOrder(topOrder) + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + int32_t topOrder = getNumber(L, 2); + + Item* item = tile->getItemByTopOrder(topOrder); + if (!item) { + lua_pushnil(L); + return 1; + } + + pushUserdata(L, item); + setItemMetatable(L, -1, item); + return 1; +} + +int LuaScriptInterface::luaTileGetItemCountById(lua_State* L) +{ + // tile:getItemCountById(itemId[, subType = -1]) + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + int32_t subType = getNumber(L, 3, -1); + + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + + lua_pushnumber(L, tile->getItemTypeCount(itemId, subType)); + return 1; +} + +int LuaScriptInterface::luaTileGetBottomCreature(lua_State* L) +{ + // tile:getBottomCreature() + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + const Creature* creature = tile->getBottomCreature(); + if (!creature) { + lua_pushnil(L); + return 1; + } + + pushUserdata(L, creature); + setCreatureMetatable(L, -1, creature); + return 1; +} + +int LuaScriptInterface::luaTileGetTopCreature(lua_State* L) +{ + // tile:getTopCreature() + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + Creature* creature = tile->getTopCreature(); + if (!creature) { + lua_pushnil(L); + return 1; + } + + pushUserdata(L, creature); + setCreatureMetatable(L, -1, creature); + return 1; +} + +int LuaScriptInterface::luaTileGetBottomVisibleCreature(lua_State* L) +{ + // tile:getBottomVisibleCreature(creature) + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + Creature* creature = getCreature(L, 2); + if (!creature) { + lua_pushnil(L); + return 1; + } + + const Creature* visibleCreature = tile->getBottomVisibleCreature(creature); + if (visibleCreature) { + pushUserdata(L, visibleCreature); + setCreatureMetatable(L, -1, visibleCreature); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetTopVisibleCreature(lua_State* L) +{ + // tile:getTopVisibleCreature(creature) + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + Creature* creature = getCreature(L, 2); + if (!creature) { + lua_pushnil(L); + return 1; + } + + Creature* visibleCreature = tile->getTopVisibleCreature(creature); + if (visibleCreature) { + pushUserdata(L, visibleCreature); + setCreatureMetatable(L, -1, visibleCreature); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetItems(lua_State* L) +{ + // tile:getItems() + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + TileItemVector* itemVector = tile->getItemList(); + if (!itemVector) { + lua_pushnil(L); + return 1; + } + + lua_createtable(L, itemVector->size(), 0); + + int index = 0; + for (Item* item : *itemVector) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaTileGetItemCount(lua_State* L) +{ + // tile:getItemCount() + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + lua_pushnumber(L, tile->getItemCount()); + return 1; +} + +int LuaScriptInterface::luaTileGetDownItemCount(lua_State* L) +{ + // tile:getDownItemCount() + Tile* tile = getUserdata(L, 1); + if (tile) { + lua_pushnumber(L, tile->getDownItemCount()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileGetTopItemCount(lua_State* L) +{ + // tile:getTopItemCount() + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + lua_pushnumber(L, tile->getTopItemCount()); + return 1; +} + +int LuaScriptInterface::luaTileGetCreatures(lua_State* L) +{ + // tile:getCreatures() + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + CreatureVector* creatureVector = tile->getCreatures(); + if (!creatureVector) { + lua_pushnil(L); + return 1; + } + + lua_createtable(L, creatureVector->size(), 0); + + int index = 0; + for (Creature* creature : *creatureVector) { + pushUserdata(L, creature); + setCreatureMetatable(L, -1, creature); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaTileGetCreatureCount(lua_State* L) +{ + // tile:getCreatureCount() + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + lua_pushnumber(L, tile->getCreatureCount()); + return 1; +} + +int LuaScriptInterface::luaTileHasProperty(lua_State* L) +{ + // tile:hasProperty(property[, item]) + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + Item* item; + if (lua_gettop(L) >= 3) { + item = getUserdata(L, 3); + } else { + item = nullptr; + } + + ITEMPROPERTY property = getNumber(L, 2); + if (item) { + pushBoolean(L, tile->hasProperty(item, property)); + } else { + pushBoolean(L, tile->hasProperty(property)); + } + return 1; +} + +int LuaScriptInterface::luaTileGetThingIndex(lua_State* L) +{ + // tile:getThingIndex(thing) + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + Thing* thing = getThing(L, 2); + if (thing) { + lua_pushnumber(L, tile->getThingIndex(thing)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileHasFlag(lua_State* L) +{ + // tile:hasFlag(flag) + Tile* tile = getUserdata(L, 1); + if (tile) { + tileflags_t flag = getNumber(L, 2); + pushBoolean(L, tile->hasFlag(flag)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileQueryAdd(lua_State* L) +{ + // tile:queryAdd(thing[, flags]) + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + Thing* thing = getThing(L, 2); + if (thing) { + uint32_t flags = getNumber(L, 3, 0); + lua_pushnumber(L, tile->queryAdd(0, *thing, 1, flags)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileAddItem(lua_State* L) +{ + // tile:addItem(itemId[, count/subType = 1[, flags = 0]]) + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + + uint32_t subType = getNumber(L, 3, 1); + + Item* item = Item::CreateItem(itemId, std::min(subType, 100)); + if (!item) { + lua_pushnil(L); + return 1; + } + + uint32_t flags = getNumber(L, 4, 0); + + ReturnValue ret = g_game.internalAddItem(tile, item, INDEX_WHEREEVER, flags); + if (ret == RETURNVALUE_NOERROR) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } else { + delete item; + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTileAddItemEx(lua_State* L) +{ + // tile:addItemEx(item[, flags = 0]) + Item* item = getUserdata(L, 2); + if (!item) { + lua_pushnil(L); + return 1; + } + + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + if (item->getParent() != VirtualCylinder::virtualCylinder) { + reportErrorFunc("Item already has a parent"); + lua_pushnil(L); + return 1; + } + + uint32_t flags = getNumber(L, 3, 0); + ReturnValue ret = g_game.internalAddItem(tile, item, INDEX_WHEREEVER, flags); + if (ret == RETURNVALUE_NOERROR) { + ScriptEnvironment::removeTempItem(item); + } + lua_pushnumber(L, ret); + return 1; +} + +int LuaScriptInterface::luaTileGetHouse(lua_State* L) +{ + // tile:getHouse() + Tile* tile = getUserdata(L, 1); + if (!tile) { + lua_pushnil(L); + return 1; + } + + if (HouseTile* houseTile = dynamic_cast(tile)) { + pushUserdata(L, houseTile->getHouse()); + setMetatable(L, -1, "House"); + } else { + lua_pushnil(L); + } + return 1; +} + +// NetworkMessage +int LuaScriptInterface::luaNetworkMessageCreate(lua_State* L) +{ + // NetworkMessage() + pushUserdata(L, new NetworkMessage); + setMetatable(L, -1, "NetworkMessage"); + return 1; +} + +int LuaScriptInterface::luaNetworkMessageDelete(lua_State* L) +{ + NetworkMessage** messagePtr = getRawUserdata(L, 1); + if (messagePtr && *messagePtr) { + delete *messagePtr; + *messagePtr = nullptr; + } + return 0; +} + +int LuaScriptInterface::luaNetworkMessageGetByte(lua_State* L) +{ + // networkMessage:getByte() + NetworkMessage* message = getUserdata(L, 1); + if (message) { + lua_pushnumber(L, message->getByte()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageGetU16(lua_State* L) +{ + // networkMessage:getU16() + NetworkMessage* message = getUserdata(L, 1); + if (message) { + lua_pushnumber(L, message->get()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageGetU32(lua_State* L) +{ + // networkMessage:getU32() + NetworkMessage* message = getUserdata(L, 1); + if (message) { + lua_pushnumber(L, message->get()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageGetU64(lua_State* L) +{ + // networkMessage:getU64() + NetworkMessage* message = getUserdata(L, 1); + if (message) { + lua_pushnumber(L, message->get()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageGetString(lua_State* L) +{ + // networkMessage:getString() + NetworkMessage* message = getUserdata(L, 1); + if (message) { + pushString(L, message->getString()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageGetPosition(lua_State* L) +{ + // networkMessage:getPosition() + NetworkMessage* message = getUserdata(L, 1); + if (message) { + pushPosition(L, message->getPosition()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageAddByte(lua_State* L) +{ + // networkMessage:addByte(number) + uint8_t number = getNumber(L, 2); + NetworkMessage* message = getUserdata(L, 1); + if (message) { + message->addByte(number); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageAddU16(lua_State* L) +{ + // networkMessage:addU16(number) + uint16_t number = getNumber(L, 2); + NetworkMessage* message = getUserdata(L, 1); + if (message) { + message->add(number); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageAddU32(lua_State* L) +{ + // networkMessage:addU32(number) + uint32_t number = getNumber(L, 2); + NetworkMessage* message = getUserdata(L, 1); + if (message) { + message->add(number); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageAddU64(lua_State* L) +{ + // networkMessage:addU64(number) + uint64_t number = getNumber(L, 2); + NetworkMessage* message = getUserdata(L, 1); + if (message) { + message->add(number); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageAddString(lua_State* L) +{ + // networkMessage:addString(string) + const std::string& string = getString(L, 2); + NetworkMessage* message = getUserdata(L, 1); + if (message) { + message->addString(string); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageAddPosition(lua_State* L) +{ + // networkMessage:addPosition(position) + const Position& position = getPosition(L, 2); + NetworkMessage* message = getUserdata(L, 1); + if (message) { + message->addPosition(position); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageAddDouble(lua_State* L) +{ + // networkMessage:addDouble(number) + double number = getNumber(L, 2); + NetworkMessage* message = getUserdata(L, 1); + if (message) { + message->addDouble(number); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageAddItem(lua_State* L) +{ + // networkMessage:addItem(item) + Item* item = getUserdata(L, 2); + if (!item) { + reportErrorFunc(getErrorDesc(LUA_ERROR_ITEM_NOT_FOUND)); + lua_pushnil(L); + return 1; + } + + NetworkMessage* message = getUserdata(L, 1); + if (message) { + message->addItem(item); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageAddItemId(lua_State* L) +{ + // networkMessage:addItemId(itemId) + NetworkMessage* message = getUserdata(L, 1); + if (!message) { + lua_pushnil(L); + return 1; + } + + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + + message->addItemId(itemId); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaNetworkMessageReset(lua_State* L) +{ + // networkMessage:reset() + NetworkMessage* message = getUserdata(L, 1); + if (message) { + message->reset(); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageSkipBytes(lua_State* L) +{ + // networkMessage:skipBytes(number) + int16_t number = getNumber(L, 2); + NetworkMessage* message = getUserdata(L, 1); + if (message) { + message->skipBytes(number); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNetworkMessageSendToPlayer(lua_State* L) +{ + // networkMessage:sendToPlayer(player[, broadcast]) + NetworkMessage* message = getUserdata(L, 1); + if (!message) { + lua_pushnil(L); + return 1; + } + + Player* player = getPlayer(L, 2); + if (player) { + bool broadcast = getBoolean(L, 3, true); + player->sendNetworkMessage(*message, broadcast); + pushBoolean(L, true); + } else { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + lua_pushnil(L); + } + return 1; +} + +// ModalWindow +int LuaScriptInterface::luaModalWindowCreate(lua_State* L) +{ + // ModalWindow(id, title, message) + const std::string& message = getString(L, 4); + const std::string& title = getString(L, 3); + uint32_t id = getNumber(L, 2); + + pushUserdata(L, new ModalWindow(id, title, message)); + setMetatable(L, -1, "ModalWindow"); + return 1; +} + +int LuaScriptInterface::luaModalWindowDelete(lua_State* L) +{ + ModalWindow** windowPtr = getRawUserdata(L, 1); + if (windowPtr && *windowPtr) { + delete *windowPtr; + *windowPtr = nullptr; + } + return 0; +} + +int LuaScriptInterface::luaModalWindowGetId(lua_State* L) +{ + // modalWindow:getId() + ModalWindow* window = getUserdata(L, 1); + if (window) { + lua_pushnumber(L, window->id); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowGetTitle(lua_State* L) +{ + // modalWindow:getTitle() + ModalWindow* window = getUserdata(L, 1); + if (window) { + pushString(L, window->title); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowGetMessage(lua_State* L) +{ + // modalWindow:getMessage() + ModalWindow* window = getUserdata(L, 1); + if (window) { + pushString(L, window->message); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowSetTitle(lua_State* L) +{ + // modalWindow:setTitle(text) + const std::string& text = getString(L, 2); + ModalWindow* window = getUserdata(L, 1); + if (window) { + window->title = text; + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowSetMessage(lua_State* L) +{ + // modalWindow:setMessage(text) + const std::string& text = getString(L, 2); + ModalWindow* window = getUserdata(L, 1); + if (window) { + window->message = text; + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowGetButtonCount(lua_State* L) +{ + // modalWindow:getButtonCount() + ModalWindow* window = getUserdata(L, 1); + if (window) { + lua_pushnumber(L, window->buttons.size()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowGetChoiceCount(lua_State* L) +{ + // modalWindow:getChoiceCount() + ModalWindow* window = getUserdata(L, 1); + if (window) { + lua_pushnumber(L, window->choices.size()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowAddButton(lua_State* L) +{ + // modalWindow:addButton(id, text) + const std::string& text = getString(L, 3); + uint8_t id = getNumber(L, 2); + ModalWindow* window = getUserdata(L, 1); + if (window) { + window->buttons.emplace_back(text, id); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowAddChoice(lua_State* L) +{ + // modalWindow:addChoice(id, text) + const std::string& text = getString(L, 3); + uint8_t id = getNumber(L, 2); + ModalWindow* window = getUserdata(L, 1); + if (window) { + window->choices.emplace_back(text, id); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowGetDefaultEnterButton(lua_State* L) +{ + // modalWindow:getDefaultEnterButton() + ModalWindow* window = getUserdata(L, 1); + if (window) { + lua_pushnumber(L, window->defaultEnterButton); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowSetDefaultEnterButton(lua_State* L) +{ + // modalWindow:setDefaultEnterButton(buttonId) + ModalWindow* window = getUserdata(L, 1); + if (window) { + window->defaultEnterButton = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowGetDefaultEscapeButton(lua_State* L) +{ + // modalWindow:getDefaultEscapeButton() + ModalWindow* window = getUserdata(L, 1); + if (window) { + lua_pushnumber(L, window->defaultEscapeButton); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowSetDefaultEscapeButton(lua_State* L) +{ + // modalWindow:setDefaultEscapeButton(buttonId) + ModalWindow* window = getUserdata(L, 1); + if (window) { + window->defaultEscapeButton = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowHasPriority(lua_State* L) +{ + // modalWindow:hasPriority() + ModalWindow* window = getUserdata(L, 1); + if (window) { + pushBoolean(L, window->priority); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowSetPriority(lua_State* L) +{ + // modalWindow:setPriority(priority) + ModalWindow* window = getUserdata(L, 1); + if (window) { + window->priority = getBoolean(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaModalWindowSendToPlayer(lua_State* L) +{ + // modalWindow:sendToPlayer(player) + Player* player = getPlayer(L, 2); + if (!player) { + lua_pushnil(L); + return 1; + } + + ModalWindow* window = getUserdata(L, 1); + if (window) { + if (!player->hasModalWindowOpen(window->id)) { + player->sendModalWindow(*window); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +// Item +int LuaScriptInterface::luaItemCreate(lua_State* L) +{ + // Item(uid) + uint32_t id = getNumber(L, 2); + + Item* item = getScriptEnv()->getItemByUID(id); + if (item) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemIsItem(lua_State* L) +{ + // item:isItem() + pushBoolean(L, getUserdata(L, 1) != nullptr); + return 1; +} + +int LuaScriptInterface::luaItemGetParent(lua_State* L) +{ + // item:getParent() + Item* item = getUserdata(L, 1); + if (!item) { + lua_pushnil(L); + return 1; + } + + Cylinder* parent = item->getParent(); + if (!parent) { + lua_pushnil(L); + return 1; + } + + pushCylinder(L, parent); + return 1; +} + +int LuaScriptInterface::luaItemGetTopParent(lua_State* L) +{ + // item:getTopParent() + Item* item = getUserdata(L, 1); + if (!item) { + lua_pushnil(L); + return 1; + } + + Cylinder* topParent = item->getTopParent(); + if (!topParent) { + lua_pushnil(L); + return 1; + } + + pushCylinder(L, topParent); + return 1; +} + +int LuaScriptInterface::luaItemGetId(lua_State* L) +{ + // item:getId() + Item* item = getUserdata(L, 1); + if (item) { + lua_pushnumber(L, item->getID()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemClone(lua_State* L) +{ + // item:clone() + Item* item = getUserdata(L, 1); + if (!item) { + lua_pushnil(L); + return 1; + } + + Item* clone = item->clone(); + if (!clone) { + lua_pushnil(L); + return 1; + } + + getScriptEnv()->addTempItem(clone); + clone->setParent(VirtualCylinder::virtualCylinder); + + pushUserdata(L, clone); + setItemMetatable(L, -1, clone); + return 1; +} + +int LuaScriptInterface::luaItemSplit(lua_State* L) +{ + // item:split([count = 1]) + Item** itemPtr = getRawUserdata(L, 1); + if (!itemPtr) { + lua_pushnil(L); + return 1; + } + + Item* item = *itemPtr; + if (!item || !item->isStackable()) { + lua_pushnil(L); + return 1; + } + + uint16_t count = std::min(getNumber(L, 2, 1), item->getItemCount()); + uint16_t diff = item->getItemCount() - count; + + Item* splitItem = item->clone(); + if (!splitItem) { + lua_pushnil(L); + return 1; + } + + splitItem->setItemCount(count); + + ScriptEnvironment* env = getScriptEnv(); + uint32_t uid = env->addThing(item); + + Item* newItem = g_game.transformItem(item, item->getID(), diff); + if (item->isRemoved()) { + env->removeItemByUID(uid); + } + + if (newItem && newItem != item) { + env->insertItem(uid, newItem); + } + + *itemPtr = newItem; + + splitItem->setParent(VirtualCylinder::virtualCylinder); + env->addTempItem(splitItem); + + pushUserdata(L, splitItem); + setItemMetatable(L, -1, splitItem); + return 1; +} + +int LuaScriptInterface::luaItemRemove(lua_State* L) +{ + // item:remove([count = -1]) + Item* item = getUserdata(L, 1); + if (item) { + int32_t count = getNumber(L, 2, -1); + pushBoolean(L, g_game.internalRemoveItem(item, count) == RETURNVALUE_NOERROR); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetUniqueId(lua_State* L) +{ + // item:getUniqueId() + Item* item = getUserdata(L, 1); + if (item) { + uint32_t uniqueId = item->getUniqueId(); + if (uniqueId == 0) { + uniqueId = getScriptEnv()->addThing(item); + } + lua_pushnumber(L, uniqueId); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetActionId(lua_State* L) +{ + // item:getActionId() + Item* item = getUserdata(L, 1); + if (item) { + lua_pushnumber(L, item->getActionId()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemSetActionId(lua_State* L) +{ + // item:setActionId(actionId) + uint16_t actionId = getNumber(L, 2); + Item* item = getUserdata(L, 1); + if (item) { + item->setActionId(actionId); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetCount(lua_State* L) +{ + // item:getCount() + Item* item = getUserdata(L, 1); + if (item) { + lua_pushnumber(L, item->getItemCount()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetCharges(lua_State* L) +{ + // item:getCharges() + Item* item = getUserdata(L, 1); + if (item) { + lua_pushnumber(L, item->getCharges()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetFluidType(lua_State* L) +{ + // item:getFluidType() + Item* item = getUserdata(L, 1); + if (item) { + lua_pushnumber(L, item->getFluidType()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetWeight(lua_State* L) +{ + // item:getWeight() + Item* item = getUserdata(L, 1); + if (item) { + lua_pushnumber(L, item->getWeight()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetSubType(lua_State* L) +{ + // item:getSubType() + Item* item = getUserdata(L, 1); + if (item) { + lua_pushnumber(L, item->getSubType()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetName(lua_State* L) +{ + // item:getName() + Item* item = getUserdata(L, 1); + if (item) { + pushString(L, item->getName()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetPluralName(lua_State* L) +{ + // item:getPluralName() + Item* item = getUserdata(L, 1); + if (item) { + pushString(L, item->getPluralName()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetArticle(lua_State* L) +{ + // item:getArticle() + Item* item = getUserdata(L, 1); + if (item) { + pushString(L, item->getArticle()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetPosition(lua_State* L) +{ + // item:getPosition() + Item* item = getUserdata(L, 1); + if (item) { + pushPosition(L, item->getPosition()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetTile(lua_State* L) +{ + // item:getTile() + Item* item = getUserdata(L, 1); + if (!item) { + lua_pushnil(L); + return 1; + } + + Tile* tile = item->getTile(); + if (tile) { + pushUserdata(L, tile); + setMetatable(L, -1, "Tile"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemHasAttribute(lua_State* L) +{ + // item:hasAttribute(key) + Item* item = getUserdata(L, 1); + if (!item) { + lua_pushnil(L); + return 1; + } + + itemAttrTypes attribute; + if (isNumber(L, 2)) { + attribute = getNumber(L, 2); + } else if (isString(L, 2)) { + attribute = stringToItemAttribute(getString(L, 2)); + } else { + attribute = ITEM_ATTRIBUTE_NONE; + } + + pushBoolean(L, item->hasAttribute(attribute)); + return 1; +} + +int LuaScriptInterface::luaItemGetAttribute(lua_State* L) +{ + // item:getAttribute(key) + Item* item = getUserdata(L, 1); + if (!item) { + lua_pushnil(L); + return 1; + } + + itemAttrTypes attribute; + if (isNumber(L, 2)) { + attribute = getNumber(L, 2); + } else if (isString(L, 2)) { + attribute = stringToItemAttribute(getString(L, 2)); + } else { + attribute = ITEM_ATTRIBUTE_NONE; + } + + if (ItemAttributes::isIntAttrType(attribute)) { + lua_pushnumber(L, item->getIntAttr(attribute)); + } else if (ItemAttributes::isStrAttrType(attribute)) { + pushString(L, item->getStrAttr(attribute)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemSetAttribute(lua_State* L) +{ + // item:setAttribute(key, value) + Item* item = getUserdata(L, 1); + if (!item) { + lua_pushnil(L); + return 1; + } + + itemAttrTypes attribute; + if (isNumber(L, 2)) { + attribute = getNumber(L, 2); + } else if (isString(L, 2)) { + attribute = stringToItemAttribute(getString(L, 2)); + } else { + attribute = ITEM_ATTRIBUTE_NONE; + } + + if (ItemAttributes::isIntAttrType(attribute)) { + if (attribute == ITEM_ATTRIBUTE_UNIQUEID) { + reportErrorFunc("Attempt to set protected key \"uid\""); + pushBoolean(L, false); + return 1; + } + + item->setIntAttr(attribute, getNumber(L, 3)); + pushBoolean(L, true); + } else if (ItemAttributes::isStrAttrType(attribute)) { + item->setStrAttr(attribute, getString(L, 3)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemRemoveAttribute(lua_State* L) +{ + // item:removeAttribute(key) + Item* item = getUserdata(L, 1); + if (!item) { + lua_pushnil(L); + return 1; + } + + itemAttrTypes attribute; + if (isNumber(L, 2)) { + attribute = getNumber(L, 2); + } else if (isString(L, 2)) { + attribute = stringToItemAttribute(getString(L, 2)); + } else { + attribute = ITEM_ATTRIBUTE_NONE; + } + + bool ret = attribute != ITEM_ATTRIBUTE_UNIQUEID; + if (ret) { + item->removeAttribute(attribute); + } else { + reportErrorFunc("Attempt to erase protected key \"uid\""); + } + pushBoolean(L, ret); + return 1; +} + +int LuaScriptInterface::luaItemGetCustomAttribute(lua_State* L) { + // item:getCustomAttribute(key) + Item* item = getUserdata(L, 1); + if (!item) { + lua_pushnil(L); + return 1; + } + + const ItemAttributes::CustomAttribute* attr; + if (isNumber(L, 2)) { + attr = item->getCustomAttribute(getNumber(L, 2)); + } else if (isString(L, 2)) { + attr = item->getCustomAttribute(getString(L, 2)); + } else { + lua_pushnil(L); + return 1; + } + + if (attr) { + attr->pushToLua(L); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemSetCustomAttribute(lua_State* L) { + // item:setCustomAttribute(key, value) + Item* item = getUserdata(L, 1); + if (!item) { + lua_pushnil(L); + return 1; + } + + std::string key; + if (isNumber(L, 2)) { + key = boost::lexical_cast(getNumber(L, 2)); + } else if (isString(L, 2)) { + key = getString(L, 2); + } else { + lua_pushnil(L); + return 1; + } + + ItemAttributes::CustomAttribute val; + if (isNumber(L, 3)) { + double tmp = getNumber(L, 3); + if (std::floor(tmp) < tmp) { + val.set(tmp); + } else { + val.set(tmp); + } + } else if (isString(L, 3)) { + val.set(getString(L, 3)); + } else if (isBoolean(L, 3)) { + val.set(getBoolean(L, 3)); + } else { + lua_pushnil(L); + return 1; + } + + item->setCustomAttribute(key, val); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaItemRemoveCustomAttribute(lua_State* L) { + // item:removeCustomAttribute(key) + Item* item = getUserdata(L, 1); + if (!item) { + lua_pushnil(L); + return 1; + } + + if (isNumber(L, 2)) { + pushBoolean(L, item->removeCustomAttribute(getNumber(L, 2))); + } else if (isString(L, 2)) { + pushBoolean(L, item->removeCustomAttribute(getString(L, 2))); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemMoveTo(lua_State* L) +{ + // item:moveTo(position or cylinder[, flags]) + Item** itemPtr = getRawUserdata(L, 1); + if (!itemPtr) { + lua_pushnil(L); + return 1; + } + + Item* item = *itemPtr; + if (!item || item->isRemoved()) { + lua_pushnil(L); + return 1; + } + + Cylinder* toCylinder; + if (isUserdata(L, 2)) { + const LuaDataType type = getUserdataType(L, 2); + switch (type) { + case LuaData_Container: + toCylinder = getUserdata(L, 2); + break; + case LuaData_Player: + toCylinder = getUserdata(L, 2); + break; + case LuaData_Tile: + toCylinder = getUserdata(L, 2); + break; + default: + toCylinder = nullptr; + break; + } + } else { + toCylinder = g_game.map.getTile(getPosition(L, 2)); + } + + if (!toCylinder) { + lua_pushnil(L); + return 1; + } + + if (item->getParent() == toCylinder) { + pushBoolean(L, true); + return 1; + } + + uint32_t flags = getNumber(L, 3, FLAG_NOLIMIT | FLAG_IGNOREBLOCKITEM | FLAG_IGNOREBLOCKCREATURE | FLAG_IGNORENOTMOVEABLE); + + if (item->getParent() == VirtualCylinder::virtualCylinder) { + pushBoolean(L, g_game.internalAddItem(toCylinder, item, INDEX_WHEREEVER, flags) == RETURNVALUE_NOERROR); + } else { + Item* moveItem = nullptr; + ReturnValue ret = g_game.internalMoveItem(item->getParent(), toCylinder, INDEX_WHEREEVER, item, item->getItemCount(), &moveItem, flags); + if (moveItem) { + *itemPtr = moveItem; + } + pushBoolean(L, ret == RETURNVALUE_NOERROR); + } + return 1; +} + +int LuaScriptInterface::luaItemTransform(lua_State* L) +{ + // item:transform(itemId[, count/subType = -1]) + Item** itemPtr = getRawUserdata(L, 1); + if (!itemPtr) { + lua_pushnil(L); + return 1; + } + + Item*& item = *itemPtr; + if (!item) { + lua_pushnil(L); + return 1; + } + + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + + int32_t subType = getNumber(L, 3, -1); + if (item->getID() == itemId && (subType == -1 || subType == item->getSubType())) { + pushBoolean(L, true); + return 1; + } + + const ItemType& it = Item::items[itemId]; + if (it.stackable) { + subType = std::min(subType, 100); + } + + ScriptEnvironment* env = getScriptEnv(); + uint32_t uid = env->addThing(item); + + Item* newItem = g_game.transformItem(item, itemId, subType); + if (item->isRemoved()) { + env->removeItemByUID(uid); + } + + if (newItem && newItem != item) { + env->insertItem(uid, newItem); + } + + item = newItem; + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaItemDecay(lua_State* L) +{ + // item:decay(decayId) + Item* item = getUserdata(L, 1); + if (item) { + if (isNumber(L, 2)) { + item->setDecayTo(getNumber(L, 2)); + } + + g_game.startDecay(item); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemGetDescription(lua_State* L) +{ + // item:getDescription(distance) + Item* item = getUserdata(L, 1); + if (item) { + int32_t distance = getNumber(L, 2); + pushString(L, item->getDescription(distance)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemHasProperty(lua_State* L) +{ + // item:hasProperty(property) + Item* item = getUserdata(L, 1); + if (item) { + ITEMPROPERTY property = getNumber(L, 2); + pushBoolean(L, item->hasProperty(property)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemIsLoadedFromMap(lua_State* L) +{ + // item:isLoadedFromMap() + Item* item = getUserdata(L, 1); + if (item) { + pushBoolean(L, item->isLoadedFromMap()); + } else { + lua_pushnil(L); + } + return 1; +} + +// Container +int LuaScriptInterface::luaContainerCreate(lua_State* L) +{ + // Container(uid) + uint32_t id = getNumber(L, 2); + + Container* container = getScriptEnv()->getContainerByUID(id); + if (container) { + pushUserdata(L, container); + setMetatable(L, -1, "Container"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaContainerGetSize(lua_State* L) +{ + // container:getSize() + Container* container = getUserdata(L, 1); + if (container) { + lua_pushnumber(L, container->size()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaContainerGetCapacity(lua_State* L) +{ + // container:getCapacity() + Container* container = getUserdata(L, 1); + if (container) { + lua_pushnumber(L, container->capacity()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaContainerGetEmptySlots(lua_State* L) +{ + // container:getEmptySlots([recursive = false]) + Container* container = getUserdata(L, 1); + if (!container) { + lua_pushnil(L); + return 1; + } + + uint32_t slots = container->capacity() - container->size(); + bool recursive = getBoolean(L, 2, false); + if (recursive) { + for (ContainerIterator it = container->iterator(); it.hasNext(); it.advance()) { + if (Container* tmpContainer = (*it)->getContainer()) { + slots += tmpContainer->capacity() - tmpContainer->size(); + } + } + } + lua_pushnumber(L, slots); + return 1; +} + +int LuaScriptInterface::luaContainerGetItemHoldingCount(lua_State* L) +{ + // container:getItemHoldingCount() + Container* container = getUserdata(L, 1); + if (container) { + lua_pushnumber(L, container->getItemHoldingCount()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaContainerGetItem(lua_State* L) +{ + // container:getItem(index) + Container* container = getUserdata(L, 1); + if (!container) { + lua_pushnil(L); + return 1; + } + + uint32_t index = getNumber(L, 2); + Item* item = container->getItemByIndex(index); + if (item) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaContainerHasItem(lua_State* L) +{ + // container:hasItem(item) + Item* item = getUserdata(L, 2); + Container* container = getUserdata(L, 1); + if (container) { + pushBoolean(L, container->isHoldingItem(item)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaContainerAddItem(lua_State* L) +{ + // container:addItem(itemId[, count/subType = 1[, index = INDEX_WHEREEVER[, flags = 0]]]) + Container* container = getUserdata(L, 1); + if (!container) { + lua_pushnil(L); + return 1; + } + + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + + uint32_t count = getNumber(L, 3, 1); + const ItemType& it = Item::items[itemId]; + if (it.stackable) { + count = std::min(count, 100); + } + + Item* item = Item::CreateItem(itemId, count); + if (!item) { + lua_pushnil(L); + return 1; + } + + int32_t index = getNumber(L, 4, INDEX_WHEREEVER); + uint32_t flags = getNumber(L, 5, 0); + + ReturnValue ret = g_game.internalAddItem(container, item, index, flags); + if (ret == RETURNVALUE_NOERROR) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } else { + delete item; + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaContainerAddItemEx(lua_State* L) +{ + // container:addItemEx(item[, index = INDEX_WHEREEVER[, flags = 0]]) + Item* item = getUserdata(L, 2); + if (!item) { + lua_pushnil(L); + return 1; + } + + Container* container = getUserdata(L, 1); + if (!container) { + lua_pushnil(L); + return 1; + } + + if (item->getParent() != VirtualCylinder::virtualCylinder) { + reportErrorFunc("Item already has a parent"); + lua_pushnil(L); + return 1; + } + + int32_t index = getNumber(L, 3, INDEX_WHEREEVER); + uint32_t flags = getNumber(L, 4, 0); + ReturnValue ret = g_game.internalAddItem(container, item, index, flags); + if (ret == RETURNVALUE_NOERROR) { + ScriptEnvironment::removeTempItem(item); + } + lua_pushnumber(L, ret); + return 1; +} + +int LuaScriptInterface::luaContainerGetCorpseOwner(lua_State* L) +{ + // container:getCorpseOwner() + Container* container = getUserdata(L, 1); + if (container) { + lua_pushnumber(L, container->getCorpseOwner()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaContainerGetItemCountById(lua_State* L) +{ + // container:getItemCountById(itemId[, subType = -1]) + Container* container = getUserdata(L, 1); + if (!container) { + lua_pushnil(L); + return 1; + } + + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + + int32_t subType = getNumber(L, 3, -1); + lua_pushnumber(L, container->getItemTypeCount(itemId, subType)); + return 1; +} + +int LuaScriptInterface::luaContainerGetContentDescription(lua_State* L) +{ + // container:getContentDescription() + Container* container = getUserdata(L, 1); + if (container) { + pushString(L, container->getContentDescription()); + } else { + lua_pushnil(L); + } + return 1; +} + +// Teleport +int LuaScriptInterface::luaTeleportCreate(lua_State* L) +{ + // Teleport(uid) + uint32_t id = getNumber(L, 2); + + Item* item = getScriptEnv()->getItemByUID(id); + if (item && item->getTeleport()) { + pushUserdata(L, item); + setMetatable(L, -1, "Teleport"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTeleportGetDestination(lua_State* L) +{ + // teleport:getDestination() + Teleport* teleport = getUserdata(L, 1); + if (teleport) { + pushPosition(L, teleport->getDestPos()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTeleportSetDestination(lua_State* L) +{ + // teleport:setDestination(position) + Teleport* teleport = getUserdata(L, 1); + if (teleport) { + teleport->setDestPos(getPosition(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +// Creature +int LuaScriptInterface::luaCreatureCreate(lua_State* L) +{ + // Creature(id or name or userdata) + Creature* creature; + if (isNumber(L, 2)) { + creature = g_game.getCreatureByID(getNumber(L, 2)); + } else if (isString(L, 2)) { + creature = g_game.getCreatureByName(getString(L, 2)); + } else if (isUserdata(L, 2)) { + LuaDataType type = getUserdataType(L, 2); + if (type != LuaData_Player && type != LuaData_Monster && type != LuaData_Npc) { + lua_pushnil(L); + return 1; + } + creature = getUserdata(L, 2); + } else { + creature = nullptr; + } + + if (creature) { + pushUserdata(L, creature); + setCreatureMetatable(L, -1, creature); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetEvents(lua_State* L) +{ + // creature:getEvents(type) + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + CreatureEventType_t eventType = getNumber(L, 2); + const auto& eventList = creature->getCreatureEvents(eventType); + lua_createtable(L, eventList.size(), 0); + + int index = 0; + for (CreatureEvent* event : eventList) { + pushString(L, event->getName()); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaCreatureRegisterEvent(lua_State* L) +{ + // creature:registerEvent(name) + Creature* creature = getUserdata(L, 1); + if (creature) { + const std::string& name = getString(L, 2); + pushBoolean(L, creature->registerCreatureEvent(name)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureUnregisterEvent(lua_State* L) +{ + // creature:unregisterEvent(name) + const std::string& name = getString(L, 2); + Creature* creature = getUserdata(L, 1); + if (creature) { + pushBoolean(L, creature->unregisterCreatureEvent(name)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureIsRemoved(lua_State* L) +{ + // creature:isRemoved() + const Creature* creature = getUserdata(L, 1); + if (creature) { + pushBoolean(L, creature->isRemoved()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureIsCreature(lua_State* L) +{ + // creature:isCreature() + pushBoolean(L, getUserdata(L, 1) != nullptr); + return 1; +} + +int LuaScriptInterface::luaCreatureIsInGhostMode(lua_State* L) +{ + // creature:isInGhostMode() + const Creature* creature = getUserdata(L, 1); + if (creature) { + pushBoolean(L, creature->isInGhostMode()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureIsHealthHidden(lua_State* L) +{ + // creature:isHealthHidden() + const Creature* creature = getUserdata(L, 1); + if (creature) { + pushBoolean(L, creature->isHealthHidden()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureCanSee(lua_State* L) +{ + // creature:canSee(position) + const Creature* creature = getUserdata(L, 1); + if (creature) { + const Position& position = getPosition(L, 2); + pushBoolean(L, creature->canSee(position)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureCanSeeCreature(lua_State* L) +{ + // creature:canSeeCreature(creature) + const Creature* creature = getUserdata(L, 1); + if (creature) { + const Creature* otherCreature = getCreature(L, 2); + pushBoolean(L, creature->canSeeCreature(otherCreature)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetParent(lua_State* L) +{ + // creature:getParent() + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + Cylinder* parent = creature->getParent(); + if (!parent) { + lua_pushnil(L); + return 1; + } + + pushCylinder(L, parent); + return 1; +} + +int LuaScriptInterface::luaCreatureGetId(lua_State* L) +{ + // creature:getId() + const Creature* creature = getUserdata(L, 1); + if (creature) { + lua_pushnumber(L, creature->getID()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetName(lua_State* L) +{ + // creature:getName() + const Creature* creature = getUserdata(L, 1); + if (creature) { + pushString(L, creature->getName()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetTarget(lua_State* L) +{ + // creature:getTarget() + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + Creature* target = creature->getAttackedCreature(); + if (target) { + pushUserdata(L, target); + setCreatureMetatable(L, -1, target); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureSetTarget(lua_State* L) +{ + // creature:setTarget(target) + Creature* creature = getUserdata(L, 1); + if (creature) { + Creature* target = getCreature(L, 2); + pushBoolean(L, creature->setAttackedCreature(target)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetFollowCreature(lua_State* L) +{ + // creature:getFollowCreature() + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + Creature* followCreature = creature->getFollowCreature(); + if (followCreature) { + pushUserdata(L, followCreature); + setCreatureMetatable(L, -1, followCreature); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureSetFollowCreature(lua_State* L) +{ + // creature:setFollowCreature(followedCreature) + Creature* creature = getUserdata(L, 1); + if (creature) { + Creature* followCreature = getCreature(L, 2); + pushBoolean(L, creature->setFollowCreature(followCreature)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetMaster(lua_State* L) +{ + // creature:getMaster() + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + Creature* master = creature->getMaster(); + if (!master) { + lua_pushnil(L); + return 1; + } + + pushUserdata(L, master); + setCreatureMetatable(L, -1, master); + return 1; +} + +int LuaScriptInterface::luaCreatureSetMaster(lua_State* L) +{ + // creature:setMaster(master) + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + pushBoolean(L, creature->setMaster(getCreature(L, 2))); + g_game.updateCreatureType(creature); + return 1; +} + +int LuaScriptInterface::luaCreatureGetLight(lua_State* L) +{ + // creature:getLight() + const Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + LightInfo lightInfo = creature->getCreatureLight(); + lua_pushnumber(L, lightInfo.level); + lua_pushnumber(L, lightInfo.color); + return 2; +} + +int LuaScriptInterface::luaCreatureSetLight(lua_State* L) +{ + // creature:setLight(color, level) + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + LightInfo light; + light.color = getNumber(L, 2); + light.level = getNumber(L, 3); + creature->setCreatureLight(light); + g_game.changeLight(creature); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaCreatureGetSpeed(lua_State* L) +{ + // creature:getSpeed() + const Creature* creature = getUserdata(L, 1); + if (creature) { + lua_pushnumber(L, creature->getSpeed()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetBaseSpeed(lua_State* L) +{ + // creature:getBaseSpeed() + const Creature* creature = getUserdata(L, 1); + if (creature) { + lua_pushnumber(L, creature->getBaseSpeed()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureChangeSpeed(lua_State* L) +{ + // creature:changeSpeed(delta) + Creature* creature = getCreature(L, 1); + if (!creature) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + int32_t delta = getNumber(L, 2); + g_game.changeSpeed(creature, delta); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaCreatureSetDropLoot(lua_State* L) +{ + // creature:setDropLoot(doDrop) + Creature* creature = getUserdata(L, 1); + if (creature) { + creature->setDropLoot(getBoolean(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureSetSkillLoss(lua_State* L) +{ + // creature:setSkillLoss(skillLoss) + Creature* creature = getUserdata(L, 1); + if (creature) { + creature->setSkillLoss(getBoolean(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetPosition(lua_State* L) +{ + // creature:getPosition() + const Creature* creature = getUserdata(L, 1); + if (creature) { + pushPosition(L, creature->getPosition()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetTile(lua_State* L) +{ + // creature:getTile() + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + Tile* tile = creature->getTile(); + if (tile) { + pushUserdata(L, tile); + setMetatable(L, -1, "Tile"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetDirection(lua_State* L) +{ + // creature:getDirection() + const Creature* creature = getUserdata(L, 1); + if (creature) { + lua_pushnumber(L, creature->getDirection()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureSetDirection(lua_State* L) +{ + // creature:setDirection(direction) + Creature* creature = getUserdata(L, 1); + if (creature) { + pushBoolean(L, g_game.internalCreatureTurn(creature, getNumber(L, 2))); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetHealth(lua_State* L) +{ + // creature:getHealth() + const Creature* creature = getUserdata(L, 1); + if (creature) { + lua_pushnumber(L, creature->getHealth()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureSetHealth(lua_State* L) +{ + // creature:setHealth(health) + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + creature->health = std::min(getNumber(L, 2), creature->healthMax); + g_game.addCreatureHealth(creature); + + Player* player = creature->getPlayer(); + if (player) { + player->sendStats(); + } + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaCreatureAddHealth(lua_State* L) +{ + // creature:addHealth(healthChange) + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + CombatDamage damage; + damage.primary.value = getNumber(L, 2); + if (damage.primary.value >= 0) { + damage.primary.type = COMBAT_HEALING; + } else { + damage.primary.type = COMBAT_UNDEFINEDDAMAGE; + } + pushBoolean(L, g_game.combatChangeHealth(nullptr, creature, damage)); + return 1; +} + +int LuaScriptInterface::luaCreatureGetMaxHealth(lua_State* L) +{ + // creature:getMaxHealth() + const Creature* creature = getUserdata(L, 1); + if (creature) { + lua_pushnumber(L, creature->getMaxHealth()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureSetMaxHealth(lua_State* L) +{ + // creature:setMaxHealth(maxHealth) + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + creature->healthMax = getNumber(L, 2); + creature->health = std::min(creature->health, creature->healthMax); + g_game.addCreatureHealth(creature); + + Player* player = creature->getPlayer(); + if (player) { + player->sendStats(); + } + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaCreatureSetHiddenHealth(lua_State* L) +{ + // creature:setHiddenHealth(hide) + Creature* creature = getUserdata(L, 1); + if (creature) { + creature->setHiddenHealth(getBoolean(L, 2)); + g_game.addCreatureHealth(creature); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetSkull(lua_State* L) +{ + // creature:getSkull() + Creature* creature = getUserdata(L, 1); + if (creature) { + lua_pushnumber(L, creature->getSkull()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureSetSkull(lua_State* L) +{ + // creature:setSkull(skull) + Creature* creature = getUserdata(L, 1); + if (creature) { + creature->setSkull(getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetOutfit(lua_State* L) +{ + // creature:getOutfit() + const Creature* creature = getUserdata(L, 1); + if (creature) { + pushOutfit(L, creature->getCurrentOutfit()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureSetOutfit(lua_State* L) +{ + // creature:setOutfit(outfit) + Creature* creature = getUserdata(L, 1); + if (creature) { + creature->defaultOutfit = getOutfit(L, 2); + g_game.internalCreatureChangeOutfit(creature, creature->defaultOutfit); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetCondition(lua_State* L) +{ + // creature:getCondition(conditionType[, conditionId = CONDITIONID_COMBAT[, subId = 0]]) + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + ConditionType_t conditionType = getNumber(L, 2); + ConditionId_t conditionId = getNumber(L, 3, CONDITIONID_COMBAT); + uint32_t subId = getNumber(L, 4, 0); + + Condition* condition = creature->getCondition(conditionType, conditionId, subId); + if (condition) { + pushUserdata(L, condition); + setWeakMetatable(L, -1, "Condition"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureAddCondition(lua_State* L) +{ + // creature:addCondition(condition[, force = false]) + Creature* creature = getUserdata(L, 1); + Condition* condition = getUserdata(L, 2); + if (creature && condition) { + bool force = getBoolean(L, 3, false); + pushBoolean(L, creature->addCondition(condition->clone(), force)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureRemoveCondition(lua_State* L) +{ + // creature:removeCondition(conditionType[, conditionId = CONDITIONID_COMBAT[, subId = 0[, force = false]]]) + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + ConditionType_t conditionType = getNumber(L, 2); + ConditionId_t conditionId = getNumber(L, 3, CONDITIONID_COMBAT); + uint32_t subId = getNumber(L, 4, 0); + Condition* condition = creature->getCondition(conditionType, conditionId, subId); + if (condition) { + bool force = getBoolean(L, 5, false); + creature->removeCondition(condition, force); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureHasCondition(lua_State* L) +{ + // creature:hasCondition(conditionType[, subId = 0]) + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + ConditionType_t conditionType = getNumber(L, 2); + uint32_t subId = getNumber(L, 3, 0); + pushBoolean(L, creature->hasCondition(conditionType, subId)); + return 1; +} + +int LuaScriptInterface::luaCreatureIsImmune(lua_State* L) +{ + // creature:isImmune(condition or conditionType) + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + if (isNumber(L, 2)) { + pushBoolean(L, creature->isImmune(getNumber(L, 2))); + } else if (Condition* condition = getUserdata(L, 2)) { + pushBoolean(L, creature->isImmune(condition->getType())); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureRemove(lua_State* L) +{ + // creature:remove() + Creature** creaturePtr = getRawUserdata(L, 1); + if (!creaturePtr) { + lua_pushnil(L); + return 1; + } + + Creature* creature = *creaturePtr; + if (!creature) { + lua_pushnil(L); + return 1; + } + + Player* player = creature->getPlayer(); + if (player) { + player->kickPlayer(true); + } else { + g_game.removeCreature(creature); + } + + *creaturePtr = nullptr; + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaCreatureTeleportTo(lua_State* L) +{ + // creature:teleportTo(position[, pushMovement = false]) + bool pushMovement = getBoolean(L, 3, false); + + const Position& position = getPosition(L, 2); + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + const Position oldPosition = creature->getPosition(); + if (g_game.internalTeleport(creature, position, pushMovement) != RETURNVALUE_NOERROR) { + pushBoolean(L, false); + return 1; + } + + if (pushMovement) { + if (oldPosition.x == position.x) { + if (oldPosition.y < position.y) { + g_game.internalCreatureTurn(creature, DIRECTION_SOUTH); + } else { + g_game.internalCreatureTurn(creature, DIRECTION_NORTH); + } + } else if (oldPosition.x > position.x) { + g_game.internalCreatureTurn(creature, DIRECTION_WEST); + } else if (oldPosition.x < position.x) { + g_game.internalCreatureTurn(creature, DIRECTION_EAST); + } + } + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaCreatureSay(lua_State* L) +{ + // creature:say(text[, type = TALKTYPE_MONSTER_SAY[, ghost = false[, target = nullptr[, position]]]]) + int parameters = lua_gettop(L); + + Position position; + if (parameters >= 6) { + position = getPosition(L, 6); + if (!position.x || !position.y) { + reportErrorFunc("Invalid position specified."); + pushBoolean(L, false); + return 1; + } + } + + Creature* target = nullptr; + if (parameters >= 5) { + target = getCreature(L, 5); + } + + bool ghost = getBoolean(L, 4, false); + + SpeakClasses type = getNumber(L, 3, TALKTYPE_MONSTER_SAY); + const std::string& text = getString(L, 2); + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + SpectatorVec spectators; + if (target) { + spectators.emplace_back(target); + } + + if (position.x != 0) { + pushBoolean(L, g_game.internalCreatureSay(creature, type, text, ghost, &spectators, &position)); + } else { + pushBoolean(L, g_game.internalCreatureSay(creature, type, text, ghost, &spectators)); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetDamageMap(lua_State* L) +{ + // creature:getDamageMap() + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + lua_createtable(L, creature->damageMap.size(), 0); + for (auto damageEntry : creature->damageMap) { + lua_createtable(L, 0, 2); + setField(L, "total", damageEntry.second.total); + setField(L, "ticks", damageEntry.second.ticks); + lua_rawseti(L, -2, damageEntry.first); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetSummons(lua_State* L) +{ + // creature:getSummons() + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + lua_createtable(L, creature->getSummonCount(), 0); + + int index = 0; + for (Creature* summon : creature->getSummons()) { + pushUserdata(L, summon); + setCreatureMetatable(L, -1, summon); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetDescription(lua_State* L) +{ + // creature:getDescription(distance) + int32_t distance = getNumber(L, 2); + Creature* creature = getUserdata(L, 1); + if (creature) { + pushString(L, creature->getDescription(distance)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetPathTo(lua_State* L) +{ + // creature:getPathTo(pos[, minTargetDist = 0[, maxTargetDist = 1[, fullPathSearch = true[, clearSight = true[, maxSearchDist = 0]]]]]) + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + const Position& position = getPosition(L, 2); + + FindPathParams fpp; + fpp.minTargetDist = getNumber(L, 3, 0); + fpp.maxTargetDist = getNumber(L, 4, 1); + fpp.fullPathSearch = getBoolean(L, 5, fpp.fullPathSearch); + fpp.clearSight = getBoolean(L, 6, fpp.clearSight); + fpp.maxSearchDist = getNumber(L, 7, fpp.maxSearchDist); + + std::forward_list dirList; + if (creature->getPathTo(position, dirList, fpp)) { + lua_newtable(L); + + int index = 0; + for (Direction dir : dirList) { + lua_pushnumber(L, dir); + lua_rawseti(L, -2, ++index); + } + } else { + pushBoolean(L, false); + } + return 1; +} + +int LuaScriptInterface::luaCreatureMove(lua_State* L) +{ + // creature:move(direction) + // creature:move(tile[, flags = 0]) + Creature* creature = getUserdata(L, 1); + if (!creature) { + lua_pushnil(L); + return 1; + } + + if (isNumber(L, 2)) { + Direction direction = getNumber(L, 2); + if (direction > DIRECTION_LAST) { + lua_pushnil(L); + return 1; + } + lua_pushnumber(L, g_game.internalMoveCreature(creature, direction, FLAG_NOLIMIT)); + } else { + Tile* tile = getUserdata(L, 2); + if (!tile) { + lua_pushnil(L); + return 1; + } + lua_pushnumber(L, g_game.internalMoveCreature(*creature, *tile, getNumber(L, 3))); + } + return 1; +} + +int LuaScriptInterface::luaCreatureGetZone(lua_State* L) +{ + // creature:getZone() + Creature* creature = getUserdata(L, 1); + if (creature) { + lua_pushnumber(L, creature->getZone()); + } else { + lua_pushnil(L); + } + return 1; +} + +// Player +int LuaScriptInterface::luaPlayerCreate(lua_State* L) +{ + // Player(id or guid or name or userdata) + Player* player; + if (isNumber(L, 2)) { + uint32_t id = getNumber(L, 2); + if (id >= 0x10000000 && id <= Player::playerAutoID) { + player = g_game.getPlayerByID(id); + } else { + player = g_game.getPlayerByGUID(id); + } + } else if (isString(L, 2)) { + ReturnValue ret = g_game.getPlayerByNameWildcard(getString(L, 2), player); + if (ret != RETURNVALUE_NOERROR) { + lua_pushnil(L); + lua_pushnumber(L, ret); + return 2; + } + } else if (isUserdata(L, 2)) { + if (getUserdataType(L, 2) != LuaData_Player) { + lua_pushnil(L); + return 1; + } + player = getUserdata(L, 2); + } else { + player = nullptr; + } + + if (player) { + pushUserdata(L, player); + setMetatable(L, -1, "Player"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerIsPlayer(lua_State* L) +{ + // player:isPlayer() + pushBoolean(L, getUserdata(L, 1) != nullptr); + return 1; +} + +int LuaScriptInterface::luaPlayerGetGuid(lua_State* L) +{ + // player:getGuid() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getGUID()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetIp(lua_State* L) +{ + // player:getIp() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getIP()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetAccountId(lua_State* L) +{ + // player:getAccountId() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getAccount()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetLastLoginSaved(lua_State* L) +{ + // player:getLastLoginSaved() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getLastLoginSaved()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetLastLogout(lua_State* L) +{ + // player:getLastLogout() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getLastLogout()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetAccountType(lua_State* L) +{ + // player:getAccountType() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getAccountType()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetAccountType(lua_State* L) +{ + // player:setAccountType(accountType) + Player* player = getUserdata(L, 1); + if (player) { + player->accountType = getNumber(L, 2); + IOLoginData::setAccountType(player->getAccount(), player->accountType); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetCapacity(lua_State* L) +{ + // player:getCapacity() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getCapacity()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetCapacity(lua_State* L) +{ + // player:setCapacity(capacity) + Player* player = getUserdata(L, 1); + if (player) { + player->capacity = getNumber(L, 2); + player->sendStats(); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetFreeCapacity(lua_State* L) +{ + // player:getFreeCapacity() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getFreeCapacity()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetDepotChest(lua_State* L) +{ + // player:getDepotChest(depotId[, autoCreate = false]) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + uint32_t depotId = getNumber(L, 2); + bool autoCreate = getBoolean(L, 3, false); + DepotChest* depotChest = player->getDepotChest(depotId, autoCreate); + if (depotChest) { + player->setLastDepotId(depotId); // FIXME: workaround for #2251 + pushUserdata(L, depotChest); + setItemMetatable(L, -1, depotChest); + } else { + pushBoolean(L, false); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetInbox(lua_State* L) +{ + // player:getInbox() + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + Inbox* inbox = player->getInbox(); + if (inbox) { + pushUserdata(L, inbox); + setItemMetatable(L, -1, inbox); + } else { + pushBoolean(L, false); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetSkullTime(lua_State* L) +{ + // player:getSkullTime() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getSkullTicks()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetSkullTime(lua_State* L) +{ + // player:setSkullTime(skullTime) + Player* player = getUserdata(L, 1); + if (player) { + player->setSkullTicks(getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetDeathPenalty(lua_State* L) +{ + // player:getDeathPenalty() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getLostPercent() * 100); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetExperience(lua_State* L) +{ + // player:getExperience() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getExperience()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddExperience(lua_State* L) +{ + // player:addExperience(experience[, sendText = false]) + Player* player = getUserdata(L, 1); + if (player) { + int64_t experience = getNumber(L, 2); + bool sendText = getBoolean(L, 3, false); + player->addExperience(nullptr, experience, sendText); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerRemoveExperience(lua_State* L) +{ + // player:removeExperience(experience[, sendText = false]) + Player* player = getUserdata(L, 1); + if (player) { + int64_t experience = getNumber(L, 2); + bool sendText = getBoolean(L, 3, false); + player->removeExperience(experience, sendText); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetLevel(lua_State* L) +{ + // player:getLevel() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getLevel()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetMagicLevel(lua_State* L) +{ + // player:getMagicLevel() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getMagicLevel()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetBaseMagicLevel(lua_State* L) +{ + // player:getBaseMagicLevel() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getBaseMagicLevel()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetMana(lua_State* L) +{ + // player:getMana() + const Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getMana()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddMana(lua_State* L) +{ + // player:addMana(manaChange[, animationOnLoss = false]) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + int32_t manaChange = getNumber(L, 2); + bool animationOnLoss = getBoolean(L, 3, false); + if (!animationOnLoss && manaChange < 0) { + player->changeMana(manaChange); + } else { + CombatDamage damage; + damage.primary.value = manaChange; + damage.origin = ORIGIN_NONE; + g_game.combatChangeMana(nullptr, player, damage); + } + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerGetMaxMana(lua_State* L) +{ + // player:getMaxMana() + const Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getMaxMana()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetMaxMana(lua_State* L) +{ + // player:setMaxMana(maxMana) + Player* player = getPlayer(L, 1); + if (player) { + player->manaMax = getNumber(L, 2); + player->mana = std::min(player->mana, player->manaMax); + player->sendStats(); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetManaSpent(lua_State* L) +{ + // player:getManaSpent() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getSpentMana()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddManaSpent(lua_State* L) +{ + // player:addManaSpent(amount) + Player* player = getUserdata(L, 1); + if (player) { + player->addManaSpent(getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetBaseMaxHealth(lua_State* L) +{ + // player:getBaseMaxHealth() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->healthMax); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetBaseMaxMana(lua_State* L) +{ + // player:getBaseMaxMana() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->manaMax); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetSkillLevel(lua_State* L) +{ + // player:getSkillLevel(skillType) + skills_t skillType = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (player && skillType <= SKILL_LAST) { + lua_pushnumber(L, player->skills[skillType].level); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetEffectiveSkillLevel(lua_State* L) +{ + // player:getEffectiveSkillLevel(skillType) + skills_t skillType = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (player && skillType <= SKILL_LAST) { + lua_pushnumber(L, player->getSkillLevel(skillType)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetSkillPercent(lua_State* L) +{ + // player:getSkillPercent(skillType) + skills_t skillType = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (player && skillType <= SKILL_LAST) { + lua_pushnumber(L, player->skills[skillType].percent); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetSkillTries(lua_State* L) +{ + // player:getSkillTries(skillType) + skills_t skillType = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (player && skillType <= SKILL_LAST) { + lua_pushnumber(L, player->skills[skillType].tries); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddSkillTries(lua_State* L) +{ + // player:addSkillTries(skillType, tries) + Player* player = getUserdata(L, 1); + if (player) { + skills_t skillType = getNumber(L, 2); + uint64_t tries = getNumber(L, 3); + player->addSkillAdvance(skillType, tries); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetSpecialSkill(lua_State* L) +{ + // player:getSpecialSkill(specialSkillType) + SpecialSkills_t specialSkillType = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (player && specialSkillType <= SPECIALSKILL_LAST) { + lua_pushnumber(L, player->getSpecialSkill(specialSkillType)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddSpecialSkill(lua_State* L) +{ + // player:addSpecialSkill(specialSkillType, value) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + SpecialSkills_t specialSkillType = getNumber(L, 2); + if (specialSkillType > SPECIALSKILL_LAST) { + lua_pushnil(L); + return 1; + } + + player->setVarSpecialSkill(specialSkillType, getNumber(L, 3)); + player->sendSkills(); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerAddOfflineTrainingTime(lua_State* L) +{ + // player:addOfflineTrainingTime(time) + Player* player = getUserdata(L, 1); + if (player) { + int32_t time = getNumber(L, 2); + player->addOfflineTrainingTime(time); + player->sendStats(); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + + +int LuaScriptInterface::luaPlayerGetOfflineTrainingTime(lua_State* L) +{ + // player:getOfflineTrainingTime() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getOfflineTrainingTime()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerRemoveOfflineTrainingTime(lua_State* L) +{ + // player:removeOfflineTrainingTime(time) + Player* player = getUserdata(L, 1); + if (player) { + int32_t time = getNumber(L, 2); + player->removeOfflineTrainingTime(time); + player->sendStats(); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddOfflineTrainingTries(lua_State* L) +{ + // player:addOfflineTrainingTries(skillType, tries) + Player* player = getUserdata(L, 1); + if (player) { + skills_t skillType = getNumber(L, 2); + uint64_t tries = getNumber(L, 3); + pushBoolean(L, player->addOfflineTrainingTries(skillType, tries)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetOfflineTrainingSkill(lua_State* L) +{ + // player:getOfflineTrainingSkill() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getOfflineTrainingSkill()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetOfflineTrainingSkill(lua_State* L) +{ + // player:setOfflineTrainingSkill(skillId) + Player* player = getUserdata(L, 1); + if (player) { + uint32_t skillId = getNumber(L, 2); + player->setOfflineTrainingSkill(skillId); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetItemCount(lua_State* L) +{ + // player:getItemCount(itemId[, subType = -1]) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + + int32_t subType = getNumber(L, 3, -1); + lua_pushnumber(L, player->getItemTypeCount(itemId, subType)); + return 1; +} + +int LuaScriptInterface::luaPlayerGetItemById(lua_State* L) +{ + // player:getItemById(itemId, deepSearch[, subType = -1]) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + bool deepSearch = getBoolean(L, 3); + int32_t subType = getNumber(L, 4, -1); + + Item* item = g_game.findItemOfType(player, itemId, deepSearch, subType); + if (item) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetVocation(lua_State* L) +{ + // player:getVocation() + Player* player = getUserdata(L, 1); + if (player) { + pushUserdata(L, player->getVocation()); + setMetatable(L, -1, "Vocation"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetVocation(lua_State* L) +{ + // player:setVocation(id or name or userdata) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + Vocation* vocation; + if (isNumber(L, 2)) { + vocation = g_vocations.getVocation(getNumber(L, 2)); + } else if (isString(L, 2)) { + vocation = g_vocations.getVocation(g_vocations.getVocationId(getString(L, 2))); + } else if (isUserdata(L, 2)) { + vocation = getUserdata(L, 2); + } else { + vocation = nullptr; + } + + if (!vocation) { + pushBoolean(L, false); + return 1; + } + + player->setVocation(vocation->getId()); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerGetSex(lua_State* L) +{ + // player:getSex() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getSex()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetSex(lua_State* L) +{ + // player:setSex(newSex) + Player* player = getUserdata(L, 1); + if (player) { + PlayerSex_t newSex = getNumber(L, 2); + player->setSex(newSex); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetTown(lua_State* L) +{ + // player:getTown() + Player* player = getUserdata(L, 1); + if (player) { + pushUserdata(L, player->getTown()); + setMetatable(L, -1, "Town"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetTown(lua_State* L) +{ + // player:setTown(town) + Town* town = getUserdata(L, 2); + if (!town) { + pushBoolean(L, false); + return 1; + } + + Player* player = getUserdata(L, 1); + if (player) { + player->setTown(town); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetGuild(lua_State* L) +{ + // player:getGuild() + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + Guild* guild = player->getGuild(); + if (!guild) { + lua_pushnil(L); + return 1; + } + + pushUserdata(L, guild); + setMetatable(L, -1, "Guild"); + return 1; +} + +int LuaScriptInterface::luaPlayerSetGuild(lua_State* L) +{ + // player:setGuild(guild) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + player->setGuild(getUserdata(L, 2)); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerGetGuildLevel(lua_State* L) +{ + // player:getGuildLevel() + Player* player = getUserdata(L, 1); + if (player && player->getGuild()) { + lua_pushnumber(L, player->getGuildRank()->level); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetGuildLevel(lua_State* L) +{ + // player:setGuildLevel(level) + uint8_t level = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (!player || !player->getGuild()) { + lua_pushnil(L); + return 1; + } + + GuildRank_ptr rank = player->getGuild()->getRankByLevel(level); + if (!rank) { + pushBoolean(L, false); + } else { + player->setGuildRank(rank); + pushBoolean(L, true); + } + + return 1; +} + +int LuaScriptInterface::luaPlayerGetGuildNick(lua_State* L) +{ + // player:getGuildNick() + Player* player = getUserdata(L, 1); + if (player) { + pushString(L, player->getGuildNick()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetGuildNick(lua_State* L) +{ + // player:setGuildNick(nick) + const std::string& nick = getString(L, 2); + Player* player = getUserdata(L, 1); + if (player) { + player->setGuildNick(nick); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetGroup(lua_State* L) +{ + // player:getGroup() + Player* player = getUserdata(L, 1); + if (player) { + pushUserdata(L, player->getGroup()); + setMetatable(L, -1, "Group"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetGroup(lua_State* L) +{ + // player:setGroup(group) + Group* group = getUserdata(L, 2); + if (!group) { + pushBoolean(L, false); + return 1; + } + + Player* player = getUserdata(L, 1); + if (player) { + player->setGroup(group); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetStamina(lua_State* L) +{ + // player:getStamina() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getStaminaMinutes()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetStamina(lua_State* L) +{ + // player:setStamina(stamina) + uint16_t stamina = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (player) { + player->staminaMinutes = std::min(2520, stamina); + player->sendStats(); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetSoul(lua_State* L) +{ + // player:getSoul() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getSoul()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddSoul(lua_State* L) +{ + // player:addSoul(soulChange) + int32_t soulChange = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (player) { + player->changeSoul(soulChange); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetMaxSoul(lua_State* L) +{ + // player:getMaxSoul() + Player* player = getUserdata(L, 1); + if (player && player->vocation) { + lua_pushnumber(L, player->vocation->getSoulMax()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetBankBalance(lua_State* L) +{ + // player:getBankBalance() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getBankBalance()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetBankBalance(lua_State* L) +{ + // player:setBankBalance(bankBalance) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + int64_t balance = getNumber(L, 2); + if (balance < 0) { + reportErrorFunc("Invalid bank balance value."); + lua_pushnil(L); + return 1; + } + + player->setBankBalance(balance); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerGetStorageValue(lua_State* L) +{ + // player:getStorageValue(key) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + uint32_t key = getNumber(L, 2); + int32_t value; + if (player->getStorageValue(key, value)) { + lua_pushnumber(L, value); + } else { + lua_pushnumber(L, -1); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSetStorageValue(lua_State* L) +{ + // player:setStorageValue(key, value) + int32_t value = getNumber(L, 3); + uint32_t key = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (IS_IN_KEYRANGE(key, RESERVED_RANGE)) { + std::ostringstream ss; + ss << "Accessing reserved range: " << key; + reportErrorFunc(ss.str()); + pushBoolean(L, false); + return 1; + } + + if (player) { + player->addStorageValue(key, value); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddItem(lua_State* L) +{ + // player:addItem(itemId[, count = 1[, canDropOnMap = true[, subType = 1[, slot = CONST_SLOT_WHEREEVER]]]]) + Player* player = getUserdata(L, 1); + if (!player) { + pushBoolean(L, false); + return 1; + } + + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + + int32_t count = getNumber(L, 3, 1); + int32_t subType = getNumber(L, 5, 1); + + const ItemType& it = Item::items[itemId]; + + int32_t itemCount = 1; + int parameters = lua_gettop(L); + if (parameters >= 4) { + itemCount = std::max(1, count); + } else if (it.hasSubType()) { + if (it.stackable) { + itemCount = std::ceil(count / 100.f); + } + + subType = count; + } else { + itemCount = std::max(1, count); + } + + bool hasTable = itemCount > 1; + if (hasTable) { + lua_newtable(L); + } else if (itemCount == 0) { + lua_pushnil(L); + return 1; + } + + bool canDropOnMap = getBoolean(L, 4, true); + slots_t slot = getNumber(L, 6, CONST_SLOT_WHEREEVER); + for (int32_t i = 1; i <= itemCount; ++i) { + int32_t stackCount = subType; + if (it.stackable) { + stackCount = std::min(stackCount, 100); + subType -= stackCount; + } + + Item* item = Item::CreateItem(itemId, stackCount); + if (!item) { + if (!hasTable) { + lua_pushnil(L); + } + return 1; + } + + ReturnValue ret = g_game.internalPlayerAddItem(player, item, canDropOnMap, slot); + if (ret != RETURNVALUE_NOERROR) { + delete item; + if (!hasTable) { + lua_pushnil(L); + } + return 1; + } + + if (hasTable) { + lua_pushnumber(L, i); + pushUserdata(L, item); + setItemMetatable(L, -1, item); + lua_settable(L, -3); + } else { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddItemEx(lua_State* L) +{ + // player:addItemEx(item[, canDropOnMap = false[, index = INDEX_WHEREEVER[, flags = 0]]]) + // player:addItemEx(item[, canDropOnMap = true[, slot = CONST_SLOT_WHEREEVER]]) + Item* item = getUserdata(L, 2); + if (!item) { + reportErrorFunc(getErrorDesc(LUA_ERROR_ITEM_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + if (item->getParent() != VirtualCylinder::virtualCylinder) { + reportErrorFunc("Item already has a parent"); + pushBoolean(L, false); + return 1; + } + + bool canDropOnMap = getBoolean(L, 3, false); + ReturnValue returnValue; + if (canDropOnMap) { + slots_t slot = getNumber(L, 4, CONST_SLOT_WHEREEVER); + returnValue = g_game.internalPlayerAddItem(player, item, true, slot); + } else { + int32_t index = getNumber(L, 4, INDEX_WHEREEVER); + uint32_t flags = getNumber(L, 5, 0); + returnValue = g_game.internalAddItem(player, item, index, flags); + } + + if (returnValue == RETURNVALUE_NOERROR) { + ScriptEnvironment::removeTempItem(item); + } + lua_pushnumber(L, returnValue); + return 1; +} + +int LuaScriptInterface::luaPlayerRemoveItem(lua_State* L) +{ + // player:removeItem(itemId, count[, subType = -1[, ignoreEquipped = false]]) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + uint16_t itemId; + if (isNumber(L, 2)) { + itemId = getNumber(L, 2); + } else { + itemId = Item::items.getItemIdByName(getString(L, 2)); + if (itemId == 0) { + lua_pushnil(L); + return 1; + } + } + + uint32_t count = getNumber(L, 3); + int32_t subType = getNumber(L, 4, -1); + bool ignoreEquipped = getBoolean(L, 5, false); + pushBoolean(L, player->removeItemOfType(itemId, count, subType, ignoreEquipped)); + return 1; +} + +int LuaScriptInterface::luaPlayerGetMoney(lua_State* L) +{ + // player:getMoney() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getMoney()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddMoney(lua_State* L) +{ + // player:addMoney(money) + uint64_t money = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (player) { + g_game.addMoney(player, money); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerRemoveMoney(lua_State* L) +{ + // player:removeMoney(money) + Player* player = getUserdata(L, 1); + if (player) { + uint64_t money = getNumber(L, 2); + pushBoolean(L, g_game.removeMoney(player, money)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerShowTextDialog(lua_State* L) +{ + // player:showTextDialog(id or name or userdata[, text[, canWrite[, length]]]) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + int32_t length = getNumber(L, 5, -1); + bool canWrite = getBoolean(L, 4, false); + std::string text; + + int parameters = lua_gettop(L); + if (parameters >= 3) { + text = getString(L, 3); + } + + Item* item; + if (isNumber(L, 2)) { + item = Item::CreateItem(getNumber(L, 2)); + } else if (isString(L, 2)) { + item = Item::CreateItem(Item::items.getItemIdByName(getString(L, 2))); + } else if (isUserdata(L, 2)) { + if (getUserdataType(L, 2) != LuaData_Item) { + pushBoolean(L, false); + return 1; + } + + item = getUserdata(L, 2); + } else { + item = nullptr; + } + + if (!item) { + reportErrorFunc(getErrorDesc(LUA_ERROR_ITEM_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + if (length < 0) { + length = Item::items[item->getID()].maxTextLen; + } + + if (!text.empty()) { + item->setText(text); + length = std::max(text.size(), length); + } + + item->setParent(player); + player->setWriteItem(item, length); + player->sendTextWindow(item, length, canWrite); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerSendTextMessage(lua_State* L) +{ + // player:sendTextMessage(type, text[, position, primaryValue = 0, primaryColor = TEXTCOLOR_NONE[, secondaryValue = 0, secondaryColor = TEXTCOLOR_NONE]]) + // player:sendTextMessage(type, text, channelId) + + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + int parameters = lua_gettop(L); + + TextMessage message(getNumber(L, 2), getString(L, 3)); + if (parameters == 4) { + uint16_t channelId = getNumber(L, 4); + ChatChannel* channel = g_chat->getChannel(*player, channelId); + if (!channel || !channel->hasUser(*player)) { + pushBoolean(L, false); + return 1; + } + message.channelId = channelId; + } else { + if (parameters >= 6) { + message.position = getPosition(L, 4); + message.primary.value = getNumber(L, 5); + message.primary.color = getNumber(L, 6); + } + + if (parameters >= 8) { + message.secondary.value = getNumber(L, 7); + message.secondary.color = getNumber(L, 8); + } + } + + player->sendTextMessage(message); + pushBoolean(L, true); + + return 1; +} + +int LuaScriptInterface::luaPlayerSendChannelMessage(lua_State* L) +{ + // player:sendChannelMessage(author, text, type, channelId) + uint16_t channelId = getNumber(L, 5); + SpeakClasses type = getNumber(L, 4); + const std::string& text = getString(L, 3); + const std::string& author = getString(L, 2); + Player* player = getUserdata(L, 1); + if (player) { + player->sendChannelMessage(author, text, type, channelId); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSendPrivateMessage(lua_State* L) +{ + // player:sendPrivateMessage(speaker, text[, type]) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + const Player* speaker = getUserdata(L, 2); + const std::string& text = getString(L, 3); + SpeakClasses type = getNumber(L, 4, TALKTYPE_PRIVATE_FROM); + player->sendPrivateMessage(speaker, type, text); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerChannelSay(lua_State* L) +{ + // player:channelSay(speaker, type, text, channelId) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + Creature* speaker = getCreature(L, 2); + SpeakClasses type = getNumber(L, 3); + const std::string& text = getString(L, 4); + uint16_t channelId = getNumber(L, 5); + player->sendToChannel(speaker, type, text, channelId); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerOpenChannel(lua_State* L) +{ + // player:openChannel(channelId) + uint16_t channelId = getNumber(L, 2); + Player* player = getUserdata(L, 1); + if (player) { + g_game.playerOpenChannel(player->getID(), channelId); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetSlotItem(lua_State* L) +{ + // player:getSlotItem(slot) + const Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + uint32_t slot = getNumber(L, 2); + Thing* thing = player->getThing(slot); + if (!thing) { + lua_pushnil(L); + return 1; + } + + Item* item = thing->getItem(); + if (item) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetParty(lua_State* L) +{ + // player:getParty() + const Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + Party* party = player->getParty(); + if (party) { + pushUserdata(L, party); + setMetatable(L, -1, "Party"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddOutfit(lua_State* L) +{ + // player:addOutfit(lookType) + Player* player = getUserdata(L, 1); + if (player) { + player->addOutfit(getNumber(L, 2), 0); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddOutfitAddon(lua_State* L) +{ + // player:addOutfitAddon(lookType, addon) + Player* player = getUserdata(L, 1); + if (player) { + uint16_t lookType = getNumber(L, 2); + uint8_t addon = getNumber(L, 3); + player->addOutfit(lookType, addon); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerRemoveOutfit(lua_State* L) +{ + // player:removeOutfit(lookType) + Player* player = getUserdata(L, 1); + if (player) { + uint16_t lookType = getNumber(L, 2); + pushBoolean(L, player->removeOutfit(lookType)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerRemoveOutfitAddon(lua_State* L) +{ + // player:removeOutfitAddon(lookType, addon) + Player* player = getUserdata(L, 1); + if (player) { + uint16_t lookType = getNumber(L, 2); + uint8_t addon = getNumber(L, 3); + pushBoolean(L, player->removeOutfitAddon(lookType, addon)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerHasOutfit(lua_State* L) +{ + // player:hasOutfit(lookType[, addon = 0]) + Player* player = getUserdata(L, 1); + if (player) { + uint16_t lookType = getNumber(L, 2); + uint8_t addon = getNumber(L, 3, 0); + pushBoolean(L, player->canWear(lookType, addon)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSendOutfitWindow(lua_State* L) +{ + // player:sendOutfitWindow() + Player* player = getUserdata(L, 1); + if (player) { + player->sendOutfitWindow(); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddMount(lua_State* L) { + // player:addMount(mountId or mountName) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + uint8_t mountId; + if (isNumber(L, 2)) { + mountId = getNumber(L, 2); + } else { + Mount* mount = g_game.mounts.getMountByName(getString(L, 2)); + if (!mount) { + lua_pushnil(L); + return 1; + } + mountId = mount->id; + } + pushBoolean(L, player->tameMount(mountId)); + return 1; +} + +int LuaScriptInterface::luaPlayerRemoveMount(lua_State* L) { + // player:removeMount(mountId or mountName) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + uint8_t mountId; + if (isNumber(L, 2)) { + mountId = getNumber(L, 2); + } else { + Mount* mount = g_game.mounts.getMountByName(getString(L, 2)); + if (!mount) { + lua_pushnil(L); + return 1; + } + mountId = mount->id; + } + pushBoolean(L, player->untameMount(mountId)); + return 1; +} + +int LuaScriptInterface::luaPlayerHasMount(lua_State* L) { + // player:hasMount(mountId or mountName) + const Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + Mount* mount = nullptr; + if (isNumber(L, 2)) { + mount = g_game.mounts.getMountByID(getNumber(L, 2)); + } else { + mount = g_game.mounts.getMountByName(getString(L, 2)); + } + + if (mount) { + pushBoolean(L, player->hasMount(mount)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetPremiumDays(lua_State* L) +{ + // player:getPremiumDays() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->premiumDays); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddPremiumDays(lua_State* L) +{ + // player:addPremiumDays(days) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + if (player->premiumDays != std::numeric_limits::max()) { + uint16_t days = getNumber(L, 2); + int32_t addDays = std::min(0xFFFE - player->premiumDays, days); + if (addDays > 0) { + player->setPremiumDays(player->premiumDays + addDays); + IOLoginData::addPremiumDays(player->getAccount(), addDays); + } + } + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerRemovePremiumDays(lua_State* L) +{ + // player:removePremiumDays(days) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + if (player->premiumDays != std::numeric_limits::max()) { + uint16_t days = getNumber(L, 2); + int32_t removeDays = std::min(player->premiumDays, days); + if (removeDays > 0) { + player->setPremiumDays(player->premiumDays - removeDays); + IOLoginData::removePremiumDays(player->getAccount(), removeDays); + } + } + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerHasBlessing(lua_State* L) +{ + // player:hasBlessing(blessing) + uint8_t blessing = getNumber(L, 2) - 1; + Player* player = getUserdata(L, 1); + if (player) { + pushBoolean(L, player->hasBlessing(blessing)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddBlessing(lua_State* L) +{ + // player:addBlessing(blessing) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + uint8_t blessing = getNumber(L, 2) - 1; + if (player->hasBlessing(blessing)) { + pushBoolean(L, false); + return 1; + } + + player->addBlessing(1 << blessing); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerRemoveBlessing(lua_State* L) +{ + // player:removeBlessing(blessing) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + uint8_t blessing = getNumber(L, 2) - 1; + if (!player->hasBlessing(blessing)) { + pushBoolean(L, false); + return 1; + } + + player->removeBlessing(1 << blessing); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerCanLearnSpell(lua_State* L) +{ + // player:canLearnSpell(spellName) + const Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + const std::string& spellName = getString(L, 2); + InstantSpell* spell = g_spells->getInstantSpellByName(spellName); + if (!spell) { + reportErrorFunc("Spell \"" + spellName + "\" not found"); + pushBoolean(L, false); + return 1; + } + + if (player->hasFlag(PlayerFlag_IgnoreSpellCheck)) { + pushBoolean(L, true); + return 1; + } + + const auto& vocMap = spell->getVocMap(); + if (vocMap.count(player->getVocationId()) == 0) { + pushBoolean(L, false); + } else if (player->getLevel() < spell->getLevel()) { + pushBoolean(L, false); + } else if (player->getMagicLevel() < spell->getMagicLevel()) { + pushBoolean(L, false); + } else { + pushBoolean(L, true); + } + return 1; +} + +int LuaScriptInterface::luaPlayerLearnSpell(lua_State* L) +{ + // player:learnSpell(spellName) + Player* player = getUserdata(L, 1); + if (player) { + const std::string& spellName = getString(L, 2); + player->learnInstantSpell(spellName); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerForgetSpell(lua_State* L) +{ + // player:forgetSpell(spellName) + Player* player = getUserdata(L, 1); + if (player) { + const std::string& spellName = getString(L, 2); + player->forgetInstantSpell(spellName); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerHasLearnedSpell(lua_State* L) +{ + // player:hasLearnedSpell(spellName) + Player* player = getUserdata(L, 1); + if (player) { + const std::string& spellName = getString(L, 2); + pushBoolean(L, player->hasLearnedInstantSpell(spellName)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSendTutorial(lua_State* L) +{ + // player:sendTutorial(tutorialId) + Player* player = getUserdata(L, 1); + if (player) { + uint8_t tutorialId = getNumber(L, 2); + player->sendTutorial(tutorialId); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerAddMapMark(lua_State* L) +{ + // player:addMapMark(position, type, description) + Player* player = getUserdata(L, 1); + if (player) { + const Position& position = getPosition(L, 2); + uint8_t type = getNumber(L, 3); + const std::string& description = getString(L, 4); + player->sendAddMarker(position, type, description); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSave(lua_State* L) +{ + // player:save() + Player* player = getUserdata(L, 1); + if (player) { + player->loginPosition = player->getPosition(); + pushBoolean(L, IOLoginData::savePlayer(player)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerPopupFYI(lua_State* L) +{ + // player:popupFYI(message) + Player* player = getUserdata(L, 1); + if (player) { + const std::string& message = getString(L, 2); + player->sendFYIBox(message); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerIsPzLocked(lua_State* L) +{ + // player:isPzLocked() + Player* player = getUserdata(L, 1); + if (player) { + pushBoolean(L, player->isPzLocked()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetClient(lua_State* L) +{ + // player:getClient() + Player* player = getUserdata(L, 1); + if (player) { + lua_createtable(L, 0, 2); + setField(L, "version", player->getProtocolVersion()); + setField(L, "os", player->getOperatingSystem()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetHouse(lua_State* L) +{ + // player:getHouse() + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + House* house = g_game.map.houses.getHouseByPlayerId(player->getGUID()); + if (house) { + pushUserdata(L, house); + setMetatable(L, -1, "House"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerSendHouseWindow(lua_State* L) +{ + // player:sendHouseWindow(house, listId) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + House* house = getUserdata(L, 2); + if (!house) { + lua_pushnil(L); + return 1; + } + + uint32_t listId = getNumber(L, 3); + player->sendHouseWindow(house, listId); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerSetEditHouse(lua_State* L) +{ + // player:setEditHouse(house, listId) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + House* house = getUserdata(L, 2); + if (!house) { + lua_pushnil(L); + return 1; + } + + uint32_t listId = getNumber(L, 3); + player->setEditHouse(house, listId); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerSetGhostMode(lua_State* L) +{ + // player:setGhostMode(enabled[, showEffect=true]) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + bool enabled = getBoolean(L, 2); + if (player->isInGhostMode() == enabled) { + pushBoolean(L, true); + return 1; + } + + bool showEffect = getBoolean(L, 3, true); + + player->switchGhostMode(); + + Tile* tile = player->getTile(); + const Position& position = player->getPosition(); + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, position, true, true); + for (Creature* spectator : spectators) { + Player* tmpPlayer = spectator->getPlayer(); + if (tmpPlayer != player && !tmpPlayer->isAccessPlayer()) { + if (enabled) { + tmpPlayer->sendRemoveTileThing(position, tile->getStackposOfCreature(tmpPlayer, player)); + } else { + tmpPlayer->sendCreatureAppear(player, position, showEffect); + } + } else { + tmpPlayer->sendCreatureChangeVisible(player, !enabled); + } + } + + if (player->isInGhostMode()) { + for (const auto& it : g_game.getPlayers()) { + if (!it.second->isAccessPlayer()) { + it.second->notifyStatusChange(player, VIPSTATUS_OFFLINE); + } + } + IOLoginData::updateOnlineStatus(player->getGUID(), false); + } else { + for (const auto& it : g_game.getPlayers()) { + if (!it.second->isAccessPlayer()) { + it.second->notifyStatusChange(player, VIPSTATUS_ONLINE); + } + } + IOLoginData::updateOnlineStatus(player->getGUID(), true); + } + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaPlayerGetContainerId(lua_State* L) +{ + // player:getContainerId(container) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + Container* container = getUserdata(L, 2); + if (container) { + lua_pushnumber(L, player->getContainerID(container)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetContainerById(lua_State* L) +{ + // player:getContainerById(id) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + Container* container = player->getContainerByID(getNumber(L, 2)); + if (container) { + pushUserdata(L, container); + setMetatable(L, -1, "Container"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetContainerIndex(lua_State* L) +{ + // player:getContainerIndex(id) + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->getContainerIndex(getNumber(L, 2))); + } else { + lua_pushnil(L); + } + return 1; +} + +int32_t LuaScriptInterface::luaPlayerStartLiveCast(lua_State* L) +{ + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + std::string password; + if (lua_gettop(L) == 2) { + password = getString(L, 2); + } + + lua_pushboolean(L, player->startLiveCast(password)); + return 1; +} + +int32_t LuaScriptInterface::luaPlayerStopLiveCast(lua_State* L) +{ + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + lua_pushboolean(L, player->stopLiveCast()); + return 1; +} + +int32_t LuaScriptInterface::luaPlayerIsLiveCaster(lua_State* L) +{ + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + lua_pushboolean(L, player->isLiveCaster()); + return 1; +} + +int LuaScriptInterface::luaPlayerGetInstantSpells(lua_State* L) +{ + // player:getInstantSpells() + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + std::vector spells; + for (auto& spell : g_spells->getInstantSpells()) { + if (spell.second.canCast(player)) { + spells.push_back(&spell.second); + } + } + + lua_createtable(L, spells.size(), 0); + + int index = 0; + for (auto spell : spells) { + pushInstantSpell(L, *spell); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaPlayerCanCast(lua_State* L) +{ + // player:canCast(spell) + Player* player = getUserdata(L, 1); + InstantSpell* spell = getUserdata(L, 2); + if (player && spell) { + pushBoolean(L, spell->canCast(player)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerHasChaseMode(lua_State* L) +{ + // player:hasChaseMode() + Player* player = getUserdata(L, 1); + if (player) { + pushBoolean(L, player->chaseMode); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerHasSecureMode(lua_State* L) +{ + // player:hasSecureMode() + Player* player = getUserdata(L, 1); + if (player) { + pushBoolean(L, player->secureMode); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPlayerGetFightMode(lua_State* L) +{ + // player:getFightMode() + Player* player = getUserdata(L, 1); + if (player) { + lua_pushnumber(L, player->fightMode); + } else { + lua_pushnil(L); + } + return 1; +} + +// Monster +int LuaScriptInterface::luaMonsterCreate(lua_State* L) +{ + // Monster(id or userdata) + Monster* monster; + if (isNumber(L, 2)) { + monster = g_game.getMonsterByID(getNumber(L, 2)); + } else if (isUserdata(L, 2)) { + if (getUserdataType(L, 2) != LuaData_Monster) { + lua_pushnil(L); + return 1; + } + monster = getUserdata(L, 2); + } else { + monster = nullptr; + } + + if (monster) { + pushUserdata(L, monster); + setMetatable(L, -1, "Monster"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterIsMonster(lua_State* L) +{ + // monster:isMonster() + pushBoolean(L, getUserdata(L, 1) != nullptr); + return 1; +} + +int LuaScriptInterface::luaMonsterGetType(lua_State* L) +{ + // monster:getType() + const Monster* monster = getUserdata(L, 1); + if (monster) { + pushUserdata(L, monster->mType); + setMetatable(L, -1, "MonsterType"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterGetSpawnPosition(lua_State* L) +{ + // monster:getSpawnPosition() + const Monster* monster = getUserdata(L, 1); + if (monster) { + pushPosition(L, monster->getMasterPos()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterIsInSpawnRange(lua_State* L) +{ + // monster:isInSpawnRange([position]) + Monster* monster = getUserdata(L, 1); + if (monster) { + pushBoolean(L, monster->isInSpawnRange(lua_gettop(L) >= 2 ? getPosition(L, 2) : monster->getPosition())); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterIsIdle(lua_State* L) +{ + // monster:isIdle() + Monster* monster = getUserdata(L, 1); + if (monster) { + pushBoolean(L, monster->getIdleStatus()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSetIdle(lua_State* L) +{ + // monster:setIdle(idle) + Monster* monster = getUserdata(L, 1); + if (!monster) { + lua_pushnil(L); + return 1; + } + + monster->setIdle(getBoolean(L, 2)); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaMonsterIsTarget(lua_State* L) +{ + // monster:isTarget(creature) + Monster* monster = getUserdata(L, 1); + if (monster) { + const Creature* creature = getCreature(L, 2); + pushBoolean(L, monster->isTarget(creature)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterIsOpponent(lua_State* L) +{ + // monster:isOpponent(creature) + Monster* monster = getUserdata(L, 1); + if (monster) { + const Creature* creature = getCreature(L, 2); + pushBoolean(L, monster->isOpponent(creature)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterIsFriend(lua_State* L) +{ + // monster:isFriend(creature) + Monster* monster = getUserdata(L, 1); + if (monster) { + const Creature* creature = getCreature(L, 2); + pushBoolean(L, monster->isFriend(creature)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterAddFriend(lua_State* L) +{ + // monster:addFriend(creature) + Monster* monster = getUserdata(L, 1); + if (monster) { + Creature* creature = getCreature(L, 2); + monster->addFriend(creature); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterRemoveFriend(lua_State* L) +{ + // monster:removeFriend(creature) + Monster* monster = getUserdata(L, 1); + if (monster) { + Creature* creature = getCreature(L, 2); + monster->removeFriend(creature); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterGetFriendList(lua_State* L) +{ + // monster:getFriendList() + Monster* monster = getUserdata(L, 1); + if (!monster) { + lua_pushnil(L); + return 1; + } + + const auto& friendList = monster->getFriendList(); + lua_createtable(L, friendList.size(), 0); + + int index = 0; + for (Creature* creature : friendList) { + pushUserdata(L, creature); + setCreatureMetatable(L, -1, creature); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaMonsterGetFriendCount(lua_State* L) +{ + // monster:getFriendCount() + Monster* monster = getUserdata(L, 1); + if (monster) { + lua_pushnumber(L, monster->getFriendList().size()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterAddTarget(lua_State* L) +{ + // monster:addTarget(creature[, pushFront = false]) + Monster* monster = getUserdata(L, 1); + if (!monster) { + lua_pushnil(L); + return 1; + } + + Creature* creature = getCreature(L, 2); + bool pushFront = getBoolean(L, 3, false); + monster->addTarget(creature, pushFront); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaMonsterRemoveTarget(lua_State* L) +{ + // monster:removeTarget(creature) + Monster* monster = getUserdata(L, 1); + if (!monster) { + lua_pushnil(L); + return 1; + } + + monster->removeTarget(getCreature(L, 2)); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaMonsterGetTargetList(lua_State* L) +{ + // monster:getTargetList() + Monster* monster = getUserdata(L, 1); + if (!monster) { + lua_pushnil(L); + return 1; + } + + const auto& targetList = monster->getTargetList(); + lua_createtable(L, targetList.size(), 0); + + int index = 0; + for (Creature* creature : targetList) { + pushUserdata(L, creature); + setCreatureMetatable(L, -1, creature); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaMonsterGetTargetCount(lua_State* L) +{ + // monster:getTargetCount() + Monster* monster = getUserdata(L, 1); + if (monster) { + lua_pushnumber(L, monster->getTargetList().size()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSelectTarget(lua_State* L) +{ + // monster:selectTarget(creature) + Monster* monster = getUserdata(L, 1); + if (monster) { + Creature* creature = getCreature(L, 2); + pushBoolean(L, monster->selectTarget(creature)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSearchTarget(lua_State* L) +{ + // monster:searchTarget([searchType = TARGETSEARCH_DEFAULT]) + Monster* monster = getUserdata(L, 1); + if (monster) { + TargetSearchType_t searchType = getNumber(L, 2, TARGETSEARCH_DEFAULT); + pushBoolean(L, monster->searchTarget(searchType)); + } else { + lua_pushnil(L); + } + return 1; +} + +// Npc +int LuaScriptInterface::luaNpcCreate(lua_State* L) +{ + // Npc([id or name or userdata]) + Npc* npc; + if (lua_gettop(L) >= 2) { + if (isNumber(L, 2)) { + npc = g_game.getNpcByID(getNumber(L, 2)); + } else if (isString(L, 2)) { + npc = g_game.getNpcByName(getString(L, 2)); + } else if (isUserdata(L, 2)) { + if (getUserdataType(L, 2) != LuaData_Npc) { + lua_pushnil(L); + return 1; + } + npc = getUserdata(L, 2); + } else { + npc = nullptr; + } + } else { + npc = getScriptEnv()->getNpc(); + } + + if (npc) { + pushUserdata(L, npc); + setMetatable(L, -1, "Npc"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNpcIsNpc(lua_State* L) +{ + // npc:isNpc() + pushBoolean(L, getUserdata(L, 1) != nullptr); + return 1; +} + +int LuaScriptInterface::luaNpcSetMasterPos(lua_State* L) +{ + // npc:setMasterPos(pos[, radius]) + Npc* npc = getUserdata(L, 1); + if (!npc) { + lua_pushnil(L); + return 1; + } + + const Position& pos = getPosition(L, 2); + int32_t radius = getNumber(L, 3, 1); + npc->setMasterPos(pos, radius); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaNpcGetSpeechBubble(lua_State* L) +{ + // npc:getSpeechBubble() + Npc* npc = getUserdata(L, 1); + if (npc) { + lua_pushnumber(L, npc->getSpeechBubble()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaNpcSetSpeechBubble(lua_State* L) +{ + // npc:setSpeechBubble(speechBubble) + Npc* npc = getUserdata(L, 1); + if (npc) { + npc->setSpeechBubble(getNumber(L, 2)); + } + return 0; +} + +// Guild +int LuaScriptInterface::luaGuildCreate(lua_State* L) +{ + // Guild(id) + uint32_t id = getNumber(L, 2); + + Guild* guild = g_game.getGuild(id); + if (guild) { + pushUserdata(L, guild); + setMetatable(L, -1, "Guild"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGuildGetId(lua_State* L) +{ + // guild:getId() + Guild* guild = getUserdata(L, 1); + if (guild) { + lua_pushnumber(L, guild->getId()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGuildGetName(lua_State* L) +{ + // guild:getName() + Guild* guild = getUserdata(L, 1); + if (guild) { + pushString(L, guild->getName()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGuildGetMembersOnline(lua_State* L) +{ + // guild:getMembersOnline() + const Guild* guild = getUserdata(L, 1); + if (!guild) { + lua_pushnil(L); + return 1; + } + + const auto& members = guild->getMembersOnline(); + lua_createtable(L, members.size(), 0); + + int index = 0; + for (Player* player : members) { + pushUserdata(L, player); + setMetatable(L, -1, "Player"); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaGuildAddRank(lua_State* L) +{ + // guild:addRank(id, name, level) + Guild* guild = getUserdata(L, 1); + if (guild) { + uint32_t id = getNumber(L, 2); + const std::string& name = getString(L, 3); + uint8_t level = getNumber(L, 4); + guild->addRank(id, name, level); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGuildGetRankById(lua_State* L) +{ + // guild:getRankById(id) + Guild* guild = getUserdata(L, 1); + if (!guild) { + lua_pushnil(L); + return 1; + } + + uint32_t id = getNumber(L, 2); + GuildRank_ptr rank = guild->getRankById(id); + if (rank) { + lua_createtable(L, 0, 3); + setField(L, "id", rank->id); + setField(L, "name", rank->name); + setField(L, "level", rank->level); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGuildGetRankByLevel(lua_State* L) +{ + // guild:getRankByLevel(level) + const Guild* guild = getUserdata(L, 1); + if (!guild) { + lua_pushnil(L); + return 1; + } + + uint8_t level = getNumber(L, 2); + GuildRank_ptr rank = guild->getRankByLevel(level); + if (rank) { + lua_createtable(L, 0, 3); + setField(L, "id", rank->id); + setField(L, "name", rank->name); + setField(L, "level", rank->level); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGuildGetMotd(lua_State* L) +{ + // guild:getMotd() + Guild* guild = getUserdata(L, 1); + if (guild) { + pushString(L, guild->getMotd()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGuildSetMotd(lua_State* L) +{ + // guild:setMotd(motd) + const std::string& motd = getString(L, 2); + Guild* guild = getUserdata(L, 1); + if (guild) { + guild->setMotd(motd); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +// Group +int LuaScriptInterface::luaGroupCreate(lua_State* L) +{ + // Group(id) + uint32_t id = getNumber(L, 2); + + Group* group = g_game.groups.getGroup(id); + if (group) { + pushUserdata(L, group); + setMetatable(L, -1, "Group"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGroupGetId(lua_State* L) +{ + // group:getId() + Group* group = getUserdata(L, 1); + if (group) { + lua_pushnumber(L, group->id); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGroupGetName(lua_State* L) +{ + // group:getName() + Group* group = getUserdata(L, 1); + if (group) { + pushString(L, group->name); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGroupGetFlags(lua_State* L) +{ + // group:getFlags() + Group* group = getUserdata(L, 1); + if (group) { + lua_pushnumber(L, group->flags); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGroupGetAccess(lua_State* L) +{ + // group:getAccess() + Group* group = getUserdata(L, 1); + if (group) { + pushBoolean(L, group->access); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGroupGetMaxDepotItems(lua_State* L) +{ + // group:getMaxDepotItems() + Group* group = getUserdata(L, 1); + if (group) { + lua_pushnumber(L, group->maxDepotItems); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGroupGetMaxVipEntries(lua_State* L) +{ + // group:getMaxVipEntries() + Group* group = getUserdata(L, 1); + if (group) { + lua_pushnumber(L, group->maxVipEntries); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGroupHasFlag(lua_State* L) +{ + // group:hasFlag(flag) + Group* group = getUserdata(L, 1); + if (group) { + PlayerFlags flag = getNumber(L, 2); + pushBoolean(L, (group->flags & flag) != 0); + } else { + lua_pushnil(L); + } + return 1; +} + +// Vocation +int LuaScriptInterface::luaVocationCreate(lua_State* L) +{ + // Vocation(id or name) + uint32_t id; + if (isNumber(L, 2)) { + id = getNumber(L, 2); + } else { + id = g_vocations.getVocationId(getString(L, 2)); + } + + Vocation* vocation = g_vocations.getVocation(id); + if (vocation) { + pushUserdata(L, vocation); + setMetatable(L, -1, "Vocation"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetId(lua_State* L) +{ + // vocation:getId() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getId()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetClientId(lua_State* L) +{ + // vocation:getClientId() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getClientId()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetName(lua_State* L) +{ + // vocation:getName() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + pushString(L, vocation->getVocName()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetDescription(lua_State* L) +{ + // vocation:getDescription() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + pushString(L, vocation->getVocDescription()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetRequiredSkillTries(lua_State* L) +{ + // vocation:getRequiredSkillTries(skillType, skillLevel) + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + skills_t skillType = getNumber(L, 2); + uint16_t skillLevel = getNumber(L, 3); + lua_pushnumber(L, vocation->getReqSkillTries(skillType, skillLevel)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetRequiredManaSpent(lua_State* L) +{ + // vocation:getRequiredManaSpent(magicLevel) + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + uint32_t magicLevel = getNumber(L, 2); + lua_pushnumber(L, vocation->getReqMana(magicLevel)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetCapacityGain(lua_State* L) +{ + // vocation:getCapacityGain() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getCapGain()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetHealthGain(lua_State* L) +{ + // vocation:getHealthGain() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getHPGain()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetHealthGainTicks(lua_State* L) +{ + // vocation:getHealthGainTicks() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getHealthGainTicks()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetHealthGainAmount(lua_State* L) +{ + // vocation:getHealthGainAmount() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getHealthGainAmount()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetManaGain(lua_State* L) +{ + // vocation:getManaGain() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getManaGain()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetManaGainTicks(lua_State* L) +{ + // vocation:getManaGainTicks() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getManaGainTicks()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetManaGainAmount(lua_State* L) +{ + // vocation:getManaGainAmount() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getManaGainAmount()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetMaxSoul(lua_State* L) +{ + // vocation:getMaxSoul() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getSoulMax()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetSoulGainTicks(lua_State* L) +{ + // vocation:getSoulGainTicks() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getSoulGainTicks()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetAttackSpeed(lua_State* L) +{ + // vocation:getAttackSpeed() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getAttackSpeed()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetBaseSpeed(lua_State* L) +{ + // vocation:getBaseSpeed() + Vocation* vocation = getUserdata(L, 1); + if (vocation) { + lua_pushnumber(L, vocation->getBaseSpeed()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetDemotion(lua_State* L) +{ + // vocation:getDemotion() + Vocation* vocation = getUserdata(L, 1); + if (!vocation) { + lua_pushnil(L); + return 1; + } + + uint16_t fromId = vocation->getFromVocation(); + if (fromId == VOCATION_NONE) { + lua_pushnil(L); + return 1; + } + + Vocation* demotedVocation = g_vocations.getVocation(fromId); + if (demotedVocation && demotedVocation != vocation) { + pushUserdata(L, demotedVocation); + setMetatable(L, -1, "Vocation"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaVocationGetPromotion(lua_State* L) +{ + // vocation:getPromotion() + Vocation* vocation = getUserdata(L, 1); + if (!vocation) { + lua_pushnil(L); + return 1; + } + + uint16_t promotedId = g_vocations.getPromotedVocation(vocation->getId()); + if (promotedId == VOCATION_NONE) { + lua_pushnil(L); + return 1; + } + + Vocation* promotedVocation = g_vocations.getVocation(promotedId); + if (promotedVocation && promotedVocation != vocation) { + pushUserdata(L, promotedVocation); + setMetatable(L, -1, "Vocation"); + } else { + lua_pushnil(L); + } + return 1; +} + +// Town +int LuaScriptInterface::luaTownCreate(lua_State* L) +{ + // Town(id or name) + Town* town; + if (isNumber(L, 2)) { + town = g_game.map.towns.getTown(getNumber(L, 2)); + } else if (isString(L, 2)) { + town = g_game.map.towns.getTown(getString(L, 2)); + } else { + town = nullptr; + } + + if (town) { + pushUserdata(L, town); + setMetatable(L, -1, "Town"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTownGetId(lua_State* L) +{ + // town:getId() + Town* town = getUserdata(L, 1); + if (town) { + lua_pushnumber(L, town->getID()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTownGetName(lua_State* L) +{ + // town:getName() + Town* town = getUserdata(L, 1); + if (town) { + pushString(L, town->getName()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTownGetTemplePosition(lua_State* L) +{ + // town:getTemplePosition() + Town* town = getUserdata(L, 1); + if (town) { + pushPosition(L, town->getTemplePosition()); + } else { + lua_pushnil(L); + } + return 1; +} + +// House +int LuaScriptInterface::luaHouseCreate(lua_State* L) +{ + // House(id) + House* house = g_game.map.houses.getHouse(getNumber(L, 2)); + if (house) { + pushUserdata(L, house); + setMetatable(L, -1, "House"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetId(lua_State* L) +{ + // house:getId() + House* house = getUserdata(L, 1); + if (house) { + lua_pushnumber(L, house->getId()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetName(lua_State* L) +{ + // house:getName() + House* house = getUserdata(L, 1); + if (house) { + pushString(L, house->getName()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetTown(lua_State* L) +{ + // house:getTown() + House* house = getUserdata(L, 1); + if (!house) { + lua_pushnil(L); + return 1; + } + + Town* town = g_game.map.towns.getTown(house->getTownId()); + if (town) { + pushUserdata(L, town); + setMetatable(L, -1, "Town"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetExitPosition(lua_State* L) +{ + // house:getExitPosition() + House* house = getUserdata(L, 1); + if (house) { + pushPosition(L, house->getEntryPosition()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetRent(lua_State* L) +{ + // house:getRent() + House* house = getUserdata(L, 1); + if (house) { + lua_pushnumber(L, house->getRent()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetOwnerGuid(lua_State* L) +{ + // house:getOwnerGuid() + House* house = getUserdata(L, 1); + if (house) { + lua_pushnumber(L, house->getOwner()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseSetOwnerGuid(lua_State* L) +{ + // house:setOwnerGuid(guid[, updateDatabase = true]) + House* house = getUserdata(L, 1); + if (house) { + uint32_t guid = getNumber(L, 2); + bool updateDatabase = getBoolean(L, 3, true); + house->setOwner(guid, updateDatabase); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseStartTrade(lua_State* L) +{ + // house:startTrade(player, tradePartner) + House* house = getUserdata(L, 1); + Player* player = getUserdata(L, 2); + Player* tradePartner = getUserdata(L, 3); + + if (!player || !tradePartner || !house) { + lua_pushnil(L); + return 1; + } + + if (!Position::areInRange<2, 2, 0>(tradePartner->getPosition(), player->getPosition())) { + lua_pushnumber(L, RETURNVALUE_TRADEPLAYERFARAWAY); + return 1; + } + + if (house->getOwner() != player->getGUID()) { + lua_pushnumber(L, RETURNVALUE_YOUDONTOWNTHISHOUSE); + return 1; + } + + if (g_game.map.houses.getHouseByPlayerId(tradePartner->getGUID())) { + lua_pushnumber(L, RETURNVALUE_TRADEPLAYERALREADYOWNSAHOUSE); + return 1; + } + + if (IOLoginData::hasBiddedOnHouse(tradePartner->getGUID())) { + lua_pushnumber(L, RETURNVALUE_TRADEPLAYERHIGHESTBIDDER); + return 1; + } + + Item* transferItem = house->getTransferItem(); + if (!transferItem) { + lua_pushnumber(L, RETURNVALUE_YOUCANNOTTRADETHISHOUSE); + return 1; + } + + transferItem->getParent()->setParent(player); + if (!g_game.internalStartTrade(player, tradePartner, transferItem)) { + house->resetTransferItem(); + } + + lua_pushnumber(L, RETURNVALUE_NOERROR); + return 1; +} + +int LuaScriptInterface::luaHouseGetBeds(lua_State* L) +{ + // house:getBeds() + House* house = getUserdata(L, 1); + if (!house) { + lua_pushnil(L); + return 1; + } + + const auto& beds = house->getBeds(); + lua_createtable(L, beds.size(), 0); + + int index = 0; + for (BedItem* bedItem : beds) { + pushUserdata(L, bedItem); + setItemMetatable(L, -1, bedItem); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetBedCount(lua_State* L) +{ + // house:getBedCount() + House* house = getUserdata(L, 1); + if (house) { + lua_pushnumber(L, house->getBedCount()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetDoors(lua_State* L) +{ + // house:getDoors() + House* house = getUserdata(L, 1); + if (!house) { + lua_pushnil(L); + return 1; + } + + const auto& doors = house->getDoors(); + lua_createtable(L, doors.size(), 0); + + int index = 0; + for (Door* door : doors) { + pushUserdata(L, door); + setItemMetatable(L, -1, door); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetDoorCount(lua_State* L) +{ + // house:getDoorCount() + House* house = getUserdata(L, 1); + if (house) { + lua_pushnumber(L, house->getDoors().size()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetDoorIdByPosition(lua_State* L) +{ + // house:getDoorIdByPosition(position) + House* house = getUserdata(L, 1); + if (!house) { + lua_pushnil(L); + return 1; + } + + Door* door = house->getDoorByPosition(getPosition(L, 2)); + if (door) { + lua_pushnumber(L, door->getDoorId()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetTiles(lua_State* L) +{ + // house:getTiles() + House* house = getUserdata(L, 1); + if (!house) { + lua_pushnil(L); + return 1; + } + + const auto& tiles = house->getTiles(); + lua_createtable(L, tiles.size(), 0); + + int index = 0; + for (Tile* tile : tiles) { + pushUserdata(L, tile); + setMetatable(L, -1, "Tile"); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetItems(lua_State* L) +{ + // house:getItems() + House* house = getUserdata(L, 1); + if (!house) { + lua_pushnil(L); + return 1; + } + + const auto& tiles = house->getTiles(); + lua_newtable(L); + + int index = 0; + for (Tile* tile : tiles) { + TileItemVector* itemVector = tile->getItemList(); + if(itemVector) { + for(Item* item : *itemVector) { + pushUserdata(L, item); + setItemMetatable(L, -1, item); + lua_rawseti(L, -2, ++index); + } + } + } + return 1; +} + +int LuaScriptInterface::luaHouseGetTileCount(lua_State* L) +{ + // house:getTileCount() + House* house = getUserdata(L, 1); + if (house) { + lua_pushnumber(L, house->getTiles().size()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseCanEditAccessList(lua_State* L) +{ + // house:canEditAccessList(listId, player) + House* house = getUserdata(L, 1); + if (!house) { + lua_pushnil(L); + return 1; + } + + uint32_t listId = getNumber(L, 2); + Player* player = getPlayer(L, 3); + + pushBoolean(L, house->canEditAccessList(listId, player)); + return 1; +} + +int LuaScriptInterface::luaHouseGetAccessList(lua_State* L) +{ + // house:getAccessList(listId) + House* house = getUserdata(L, 1); + if (!house) { + lua_pushnil(L); + return 1; + } + + std::string list; + uint32_t listId = getNumber(L, 2); + if (house->getAccessList(listId, list)) { + pushString(L, list); + } else { + pushBoolean(L, false); + } + return 1; +} + +int LuaScriptInterface::luaHouseSetAccessList(lua_State* L) +{ + // house:setAccessList(listId, list) + House* house = getUserdata(L, 1); + if (!house) { + lua_pushnil(L); + return 1; + } + + uint32_t listId = getNumber(L, 2); + const std::string& list = getString(L, 3); + house->setAccessList(listId, list); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaHouseKickPlayer(lua_State* L) +{ + // house:kickPlayer(player, targetPlayer) + House* house = getUserdata(L, 1); + if (!house) { + lua_pushnil(L); + return 1; + } + + pushBoolean(L, house->kickPlayer(getPlayer(L, 2), getPlayer(L, 3))); + return 1; +} + +// ItemType +int LuaScriptInterface::luaItemTypeCreate(lua_State* L) +{ + // ItemType(id or name) + uint32_t id; + if (isNumber(L, 2)) { + id = getNumber(L, 2); + } else if (isString(L, 2)) { + id = Item::items.getItemIdByName(getString(L, 2)); + } else { + lua_pushnil(L); + return 1; + } + + const ItemType& itemType = Item::items[id]; + pushUserdata(L, &itemType); + setMetatable(L, -1, "ItemType"); + return 1; +} + +int LuaScriptInterface::luaItemTypeIsCorpse(lua_State* L) +{ + // itemType:isCorpse() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->corpseType != RACE_NONE); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsDoor(lua_State* L) +{ + // itemType:isDoor() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->isDoor()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsContainer(lua_State* L) +{ + // itemType:isContainer() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->isContainer()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsFluidContainer(lua_State* L) +{ + // itemType:isFluidContainer() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->isFluidContainer()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsMovable(lua_State* L) +{ + // itemType:isMovable() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->moveable); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsRune(lua_State* L) +{ + // itemType:isRune() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->isRune()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsStackable(lua_State* L) +{ + // itemType:isStackable() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->stackable); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsReadable(lua_State* L) +{ + // itemType:isReadable() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->canReadText); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsWritable(lua_State* L) +{ + // itemType:isWritable() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->canWriteText); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsBlocking(lua_State* L) +{ + // itemType:isBlocking() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->blockProjectile || itemType->blockSolid); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsGroundTile(lua_State* L) +{ + // itemType:isGroundTile() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->isGroundTile()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsMagicField(lua_State* L) +{ + // itemType:isMagicField() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->isMagicField()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsUseable(lua_State* L) +{ + // itemType:isUseable() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->isUseable()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeIsPickupable(lua_State* L) +{ + // itemType:isPickupable() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->isPickupable()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetType(lua_State* L) +{ + // itemType:getType() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->type); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetId(lua_State* L) +{ + // itemType:getId() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->id); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetClientId(lua_State* L) +{ + // itemType:getClientId() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->clientId); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetName(lua_State* L) +{ + // itemType:getName() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushString(L, itemType->name); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetPluralName(lua_State* L) +{ + // itemType:getPluralName() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushString(L, itemType->getPluralName()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetArticle(lua_State* L) +{ + // itemType:getArticle() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushString(L, itemType->article); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetDescription(lua_State* L) +{ + // itemType:getDescription() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushString(L, itemType->description); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetSlotPosition(lua_State *L) +{ + // itemType:getSlotPosition() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->slotPosition); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetCharges(lua_State* L) +{ + // itemType:getCharges() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->charges); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetFluidSource(lua_State* L) +{ + // itemType:getFluidSource() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->fluidSource); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetCapacity(lua_State* L) +{ + // itemType:getCapacity() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->maxItems); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetWeight(lua_State* L) +{ + // itemType:getWeight([count = 1]) + uint16_t count = getNumber(L, 2, 1); + + const ItemType* itemType = getUserdata(L, 1); + if (!itemType) { + lua_pushnil(L); + return 1; + } + + uint64_t weight = static_cast(itemType->weight) * std::max(1, count); + lua_pushnumber(L, weight); + return 1; +} + +int LuaScriptInterface::luaItemTypeGetHitChance(lua_State* L) +{ + // itemType:getHitChance() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->hitChance); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetShootRange(lua_State* L) +{ + // itemType:getShootRange() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->shootRange); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetAttack(lua_State* L) +{ + // itemType:getAttack() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->attack); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetDefense(lua_State* L) +{ + // itemType:getDefense() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->defense); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetExtraDefense(lua_State* L) +{ + // itemType:getExtraDefense() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->extraDefense); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetArmor(lua_State* L) +{ + // itemType:getArmor() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->armor); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetWeaponType(lua_State* L) +{ + // itemType:getWeaponType() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->weaponType); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetAmmoType(lua_State* L) +{ + // itemType:getAmmoType() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->ammoType); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetCorpseType(lua_State* L) +{ + // itemType:getCorpseType() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->corpseType); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetElementType(lua_State* L) +{ + // itemType:getElementType() + const ItemType* itemType = getUserdata(L, 1); + if (!itemType) { + lua_pushnil(L); + return 1; + } + + auto& abilities = itemType->abilities; + if (abilities) { + lua_pushnumber(L, abilities->elementType); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetElementDamage(lua_State* L) +{ + // itemType:getElementDamage() + const ItemType* itemType = getUserdata(L, 1); + if (!itemType) { + lua_pushnil(L); + return 1; + } + + auto& abilities = itemType->abilities; + if (abilities) { + lua_pushnumber(L, abilities->elementDamage); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetTransformEquipId(lua_State* L) +{ + // itemType:getTransformEquipId() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->transformEquipTo); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetTransformDeEquipId(lua_State* L) +{ + // itemType:getTransformDeEquipId() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->transformDeEquipTo); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetDestroyId(lua_State* L) +{ + // itemType:getDestroyId() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->destroyTo); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetDecayId(lua_State* L) +{ + // itemType:getDecayId() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->decayTo); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeGetRequiredLevel(lua_State* L) +{ + // itemType:getRequiredLevel() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + lua_pushnumber(L, itemType->minReqLevel); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaItemTypeHasSubType(lua_State* L) +{ + // itemType:hasSubType() + const ItemType* itemType = getUserdata(L, 1); + if (itemType) { + pushBoolean(L, itemType->hasSubType()); + } else { + lua_pushnil(L); + } + return 1; +} + +// Combat +int LuaScriptInterface::luaCombatCreate(lua_State* L) +{ + // Combat() + pushUserdata(L, g_luaEnvironment.createCombatObject(getScriptEnv()->getScriptInterface())); + setMetatable(L, -1, "Combat"); + return 1; +} + +int LuaScriptInterface::luaCombatSetParameter(lua_State* L) +{ + // combat:setParameter(key, value) + Combat* combat = getUserdata(L, 1); + if (!combat) { + lua_pushnil(L); + return 1; + } + + CombatParam_t key = getNumber(L, 2); + uint32_t value; + if (isBoolean(L, 3)) { + value = getBoolean(L, 3) ? 1 : 0; + } else { + value = getNumber(L, 3); + } + combat->setParam(key, value); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaCombatSetFormula(lua_State* L) +{ + // combat:setFormula(type, mina, minb, maxa, maxb) + Combat* combat = getUserdata(L, 1); + if (!combat) { + lua_pushnil(L); + return 1; + } + + formulaType_t type = getNumber(L, 2); + double mina = getNumber(L, 3); + double minb = getNumber(L, 4); + double maxa = getNumber(L, 5); + double maxb = getNumber(L, 6); + combat->setPlayerCombatValues(type, mina, minb, maxa, maxb); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaCombatSetArea(lua_State* L) +{ + // combat:setArea(area) + if (getScriptEnv()->getScriptId() != EVENT_ID_LOADING) { + reportErrorFunc("This function can only be used while loading the script."); + lua_pushnil(L); + return 1; + } + + const AreaCombat* area = g_luaEnvironment.getAreaObject(getNumber(L, 2)); + if (!area) { + reportErrorFunc(getErrorDesc(LUA_ERROR_AREA_NOT_FOUND)); + lua_pushnil(L); + return 1; + } + + Combat* combat = getUserdata(L, 1); + if (combat) { + combat->setArea(new AreaCombat(*area)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCombatAddCondition(lua_State* L) +{ + // combat:addCondition(condition) + Condition* condition = getUserdata(L, 2); + Combat* combat = getUserdata(L, 1); + if (combat && condition) { + combat->addCondition(condition->clone()); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCombatClearConditions(lua_State* L) +{ + // combat:clearConditions() + Combat* combat = getUserdata(L, 1); + if (combat) { + combat->clearConditions(); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCombatSetCallback(lua_State* L) +{ + // combat:setCallback(key, function) + Combat* combat = getUserdata(L, 1); + if (!combat) { + lua_pushnil(L); + return 1; + } + + CallBackParam_t key = getNumber(L, 2); + if (!combat->setCallback(key)) { + lua_pushnil(L); + return 1; + } + + CallBack* callback = combat->getCallback(key); + if (!callback) { + lua_pushnil(L); + return 1; + } + + const std::string& function = getString(L, 3); + pushBoolean(L, callback->loadCallBack(getScriptEnv()->getScriptInterface(), function)); + return 1; +} + +int LuaScriptInterface::luaCombatSetOrigin(lua_State* L) +{ + // combat:setOrigin(origin) + Combat* combat = getUserdata(L, 1); + if (combat) { + combat->setOrigin(getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCombatExecute(lua_State* L) +{ + // combat:execute(creature, variant) + Combat* combat = getUserdata(L, 1); + if (!combat) { + pushBoolean(L, false); + return 1; + } + + if (isUserdata(L, 2)) { + LuaDataType type = getUserdataType(L, 2); + if (type != LuaData_Player && type != LuaData_Monster && type != LuaData_Npc) { + pushBoolean(L, false); + return 1; + } + } + + Creature* creature = getCreature(L, 2); + + const LuaVariant& variant = getVariant(L, 3); + switch (variant.type) { + case VARIANT_NUMBER: { + Creature* target = g_game.getCreatureByID(variant.number); + if (!target) { + pushBoolean(L, false); + return 1; + } + + if (combat->hasArea()) { + combat->doCombat(creature, target->getPosition()); + } else { + combat->doCombat(creature, target); + } + break; + } + + case VARIANT_POSITION: { + combat->doCombat(creature, variant.pos); + break; + } + + case VARIANT_TARGETPOSITION: { + if (combat->hasArea()) { + combat->doCombat(creature, variant.pos); + } else { + combat->postCombatEffects(creature, variant.pos); + g_game.addMagicEffect(variant.pos, CONST_ME_POFF); + } + break; + } + + case VARIANT_STRING: { + Player* target = g_game.getPlayerByName(variant.text); + if (!target) { + pushBoolean(L, false); + return 1; + } + + combat->doCombat(creature, target); + break; + } + + case VARIANT_NONE: { + reportErrorFunc(getErrorDesc(LUA_ERROR_VARIANT_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + default: { + break; + } + } + + pushBoolean(L, true); + return 1; +} + +// Condition +int LuaScriptInterface::luaConditionCreate(lua_State* L) +{ + // Condition(conditionType[, conditionId = CONDITIONID_COMBAT]) + ConditionType_t conditionType = getNumber(L, 2); + ConditionId_t conditionId = getNumber(L, 3, CONDITIONID_COMBAT); + + Condition* condition = Condition::createCondition(conditionId, conditionType, 0, 0); + if (condition) { + pushUserdata(L, condition); + setMetatable(L, -1, "Condition"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaConditionDelete(lua_State* L) +{ + // condition:delete() + Condition** conditionPtr = getRawUserdata(L, 1); + if (conditionPtr && *conditionPtr) { + delete *conditionPtr; + *conditionPtr = nullptr; + } + return 0; +} + +int LuaScriptInterface::luaConditionGetId(lua_State* L) +{ + // condition:getId() + Condition* condition = getUserdata(L, 1); + if (condition) { + lua_pushnumber(L, condition->getId()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaConditionGetSubId(lua_State* L) +{ + // condition:getSubId() + Condition* condition = getUserdata(L, 1); + if (condition) { + lua_pushnumber(L, condition->getSubId()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaConditionGetType(lua_State* L) +{ + // condition:getType() + Condition* condition = getUserdata(L, 1); + if (condition) { + lua_pushnumber(L, condition->getType()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaConditionGetIcons(lua_State* L) +{ + // condition:getIcons() + Condition* condition = getUserdata(L, 1); + if (condition) { + lua_pushnumber(L, condition->getIcons()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaConditionGetEndTime(lua_State* L) +{ + // condition:getEndTime() + Condition* condition = getUserdata(L, 1); + if (condition) { + lua_pushnumber(L, condition->getEndTime()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaConditionClone(lua_State* L) +{ + // condition:clone() + Condition* condition = getUserdata(L, 1); + if (condition) { + pushUserdata(L, condition->clone()); + setMetatable(L, -1, "Condition"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaConditionGetTicks(lua_State* L) +{ + // condition:getTicks() + Condition* condition = getUserdata(L, 1); + if (condition) { + lua_pushnumber(L, condition->getTicks()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaConditionSetTicks(lua_State* L) +{ + // condition:setTicks(ticks) + int32_t ticks = getNumber(L, 2); + Condition* condition = getUserdata(L, 1); + if (condition) { + condition->setTicks(ticks); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaConditionSetParameter(lua_State* L) +{ + // condition:setParameter(key, value) + Condition* condition = getUserdata(L, 1); + if (!condition) { + lua_pushnil(L); + return 1; + } + + ConditionParam_t key = getNumber(L, 2); + int32_t value; + if (isBoolean(L, 3)) { + value = getBoolean(L, 3) ? 1 : 0; + } else { + value = getNumber(L, 3); + } + condition->setParam(key, value); + pushBoolean(L, true); + return 1; +} + +int LuaScriptInterface::luaConditionSetFormula(lua_State* L) +{ + // condition:setFormula(mina, minb, maxa, maxb) + double maxb = getNumber(L, 5); + double maxa = getNumber(L, 4); + double minb = getNumber(L, 3); + double mina = getNumber(L, 2); + ConditionSpeed* condition = dynamic_cast(getUserdata(L, 1)); + if (condition) { + condition->setFormulaVars(mina, minb, maxa, maxb); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaConditionSetOutfit(lua_State* L) +{ + // condition:setOutfit(outfit) + // condition:setOutfit(lookTypeEx, lookType, lookHead, lookBody, lookLegs, lookFeet[, lookAddons[, lookMount]]) + Outfit_t outfit; + if (isTable(L, 2)) { + outfit = getOutfit(L, 2); + } else { + outfit.lookMount = getNumber(L, 9, outfit.lookMount); + outfit.lookAddons = getNumber(L, 8, outfit.lookAddons); + outfit.lookFeet = getNumber(L, 7); + outfit.lookLegs = getNumber(L, 6); + outfit.lookBody = getNumber(L, 5); + outfit.lookHead = getNumber(L, 4); + outfit.lookType = getNumber(L, 3); + outfit.lookTypeEx = getNumber(L, 2); + } + + ConditionOutfit* condition = dynamic_cast(getUserdata(L, 1)); + if (condition) { + condition->setOutfit(outfit); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaConditionAddDamage(lua_State* L) +{ + // condition:addDamage(rounds, time, value) + int32_t value = getNumber(L, 4); + int32_t time = getNumber(L, 3); + int32_t rounds = getNumber(L, 2); + ConditionDamage* condition = dynamic_cast(getUserdata(L, 1)); + if (condition) { + pushBoolean(L, condition->addDamage(rounds, time, value)); + } else { + lua_pushnil(L); + } + return 1; +} + +// MonsterType +int LuaScriptInterface::luaMonsterTypeCreate(lua_State* L) +{ + // MonsterType(name) + MonsterType* monsterType = g_monsters.getMonsterType(getString(L, 2)); + if (monsterType) { + pushUserdata(L, monsterType); + setMetatable(L, -1, "MonsterType"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeIsAttackable(lua_State* L) +{ + // get: monsterType:isAttackable() set: monsterType:isAttackable(bool) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushBoolean(L, monsterType->info.isAttackable); + } else { + monsterType->info.isAttackable = getBoolean(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeIsConvinceable(lua_State* L) +{ + // get: monsterType:isConvinceable() set: monsterType:isConvinceable(bool) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushBoolean(L, monsterType->info.isConvinceable); + } else { + monsterType->info.isConvinceable = getBoolean(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeIsSummonable(lua_State* L) +{ + // get: monsterType:isSummonable() set: monsterType:isSummonable(bool) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushBoolean(L, monsterType->info.isSummonable); + } else { + monsterType->info.isSummonable = getBoolean(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeIsIllusionable(lua_State* L) +{ + // get: monsterType:isIllusionable() set: monsterType:isIllusionable(bool) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushBoolean(L, monsterType->info.isIllusionable); + } else { + monsterType->info.isIllusionable = getBoolean(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeIsHostile(lua_State* L) +{ + // get: monsterType:isHostile() set: monsterType:isHostile(bool) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushBoolean(L, monsterType->info.isHostile); + } else { + monsterType->info.isHostile = getBoolean(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeIsPushable(lua_State* L) +{ + // get: monsterType:isPushable() set: monsterType:isPushable(bool) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushBoolean(L, monsterType->info.pushable); + } else { + monsterType->info.pushable = getBoolean(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeIsHealthHidden(lua_State* L) +{ + // get: monsterType:isHealthHidden() set: monsterType:isHealthHidden(bool) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushBoolean(L, monsterType->info.hiddenHealth); + } else { + monsterType->info.hiddenHealth = getBoolean(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeIsBoss(lua_State* L) +{ + // get: monsterType:isBoss() set: monsterType:isBoss(bool) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushBoolean(L, monsterType->info.isBoss); + } else { + monsterType->info.isBoss = getBoolean(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeCanPushItems(lua_State* L) +{ + // get: monsterType:canPushItems() set: monsterType:canPushItems(bool) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushBoolean(L, monsterType->info.canPushItems); + } else { + monsterType->info.canPushItems = getBoolean(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeCanPushCreatures(lua_State* L) +{ + // get: monsterType:canPushCreatures() set: monsterType:canPushCreatures(bool) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushBoolean(L, monsterType->info.canPushCreatures); + } else { + monsterType->info.canPushCreatures = getBoolean(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int32_t LuaScriptInterface::luaMonsterTypeName(lua_State* L) +{ + // get: monsterType:name() set: monsterType:name(name) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushString(L, monsterType->name); + } else { + monsterType->name = getString(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeNameDescription(lua_State* L) +{ + // get: monsterType:nameDescription() set: monsterType:nameDescription(desc) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushString(L, monsterType->nameDescription); + } else { + monsterType->nameDescription = getString(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeHealth(lua_State* L) +{ + // get: monsterType:health() set: monsterType:health(health) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.health); + } else { + monsterType->info.health = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeMaxHealth(lua_State* L) +{ + // get: monsterType:maxHealth() set: monsterType:maxHealth(health) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.healthMax); + } else { + monsterType->info.healthMax = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeRunHealth(lua_State* L) +{ + // get: monsterType:runHealth() set: monsterType:runHealth(health) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.runAwayHealth); + } else { + monsterType->info.runAwayHealth = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeExperience(lua_State* L) +{ + // get: monsterType:experience() set: monsterType:experience(exp) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.experience); + } else { + monsterType->info.experience = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeSkull(lua_State* L) +{ + // get: monsterType:skull() set: monsterType:skull(str/constant) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.skull); + } else { + if (isNumber(L, 2)) { + monsterType->info.skull = getNumber(L, 2); + } else { + monsterType->info.skull = getSkullType(getString(L, 2)); + } + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeCombatImmunities(lua_State* L) +{ + // get: monsterType:combatImmunities() set: monsterType:combatImmunities(immunity) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.damageImmunities); + } else { + std::string immunity = getString(L, 2); + if (immunity == "physical") { + monsterType->info.damageImmunities |= COMBAT_PHYSICALDAMAGE; + pushBoolean(L, true); + } else if (immunity == "energy") { + monsterType->info.damageImmunities |= COMBAT_ENERGYDAMAGE; + pushBoolean(L, true); + } else if (immunity == "fire") { + monsterType->info.damageImmunities |= COMBAT_FIREDAMAGE; + pushBoolean(L, true); + } else if (immunity == "poison" || immunity == "earth") { + monsterType->info.damageImmunities |= COMBAT_EARTHDAMAGE; + pushBoolean(L, true); + } else if (immunity == "drown") { + monsterType->info.damageImmunities |= COMBAT_DROWNDAMAGE; + pushBoolean(L, true); + } else if (immunity == "ice") { + monsterType->info.damageImmunities |= COMBAT_ICEDAMAGE; + pushBoolean(L, true); + } else if (immunity == "holy") { + monsterType->info.damageImmunities |= COMBAT_HOLYDAMAGE; + pushBoolean(L, true); + } else if (immunity == "death") { + monsterType->info.damageImmunities |= COMBAT_DEATHDAMAGE; + pushBoolean(L, true); + } else if (immunity == "lifedrain") { + monsterType->info.damageImmunities |= COMBAT_LIFEDRAIN; + pushBoolean(L, true); + } else if (immunity == "manadrain") { + monsterType->info.damageImmunities |= COMBAT_MANADRAIN; + pushBoolean(L, true); + } else { + std::cout << "[Warning - Monsters::loadMonster] Unknown immunity name " << immunity << " for monster: " << monsterType->name << std::endl; + lua_pushnil(L); + } + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeConditionImmunities(lua_State* L) +{ + // get: monsterType:conditionImmunities() set: monsterType:conditionImmunities(immunity) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.conditionImmunities); + } else { + std::string immunity = getString(L, 2); + if (immunity == "physical") { + monsterType->info.conditionImmunities |= CONDITION_BLEEDING; + pushBoolean(L, true); + } else if (immunity == "energy") { + monsterType->info.conditionImmunities |= CONDITION_ENERGY; + pushBoolean(L, true); + } else if (immunity == "fire") { + monsterType->info.conditionImmunities |= CONDITION_FIRE; + pushBoolean(L, true); + } else if (immunity == "poison" || immunity == "earth") { + monsterType->info.conditionImmunities |= CONDITION_POISON; + pushBoolean(L, true); + } else if (immunity == "drown") { + monsterType->info.conditionImmunities |= CONDITION_DROWN; + pushBoolean(L, true); + } else if (immunity == "ice") { + monsterType->info.conditionImmunities |= CONDITION_FREEZING; + pushBoolean(L, true); + } else if (immunity == "holy") { + monsterType->info.conditionImmunities |= CONDITION_DAZZLED; + pushBoolean(L, true); + } else if (immunity == "death") { + monsterType->info.conditionImmunities |= CONDITION_CURSED; + pushBoolean(L, true); + } else if (immunity == "paralyze") { + monsterType->info.conditionImmunities |= CONDITION_PARALYZE; + pushBoolean(L, true); + } else if (immunity == "outfit") { + monsterType->info.conditionImmunities |= CONDITION_OUTFIT; + pushBoolean(L, true); + } else if (immunity == "drunk") { + monsterType->info.conditionImmunities |= CONDITION_DRUNK; + pushBoolean(L, true); + } else if (immunity == "invisible" || immunity == "invisibility") { + monsterType->info.conditionImmunities |= CONDITION_INVISIBLE; + pushBoolean(L, true); + } else if (immunity == "bleed") { + monsterType->info.conditionImmunities |= CONDITION_BLEEDING; + pushBoolean(L, true); + } else { + std::cout << "[Warning - Monsters::loadMonster] Unknown immunity name " << immunity << " for monster: " << monsterType->name << std::endl; + lua_pushnil(L); + } + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeGetAttackList(lua_State* L) +{ + // monsterType:getAttackList() + MonsterType* monsterType = getUserdata(L, 1); + if (!monsterType) { + lua_pushnil(L); + return 1; + } + + lua_createtable(L, monsterType->info.attackSpells.size(), 0); + + int index = 0; + for (const auto& spellBlock : monsterType->info.attackSpells) { + lua_createtable(L, 0, 8); + + setField(L, "chance", spellBlock.chance); + setField(L, "isCombatSpell", spellBlock.combatSpell ? 1 : 0); + setField(L, "isMelee", spellBlock.isMelee ? 1 : 0); + setField(L, "minCombatValue", spellBlock.minCombatValue); + setField(L, "maxCombatValue", spellBlock.maxCombatValue); + setField(L, "range", spellBlock.range); + setField(L, "speed", spellBlock.speed); + pushUserdata(L, static_cast(spellBlock.spell)); + lua_setfield(L, -2, "spell"); + + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeAddAttack(lua_State* L) +{ + // monsterType:addAttack(monsterspell) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + MonsterSpell* spell = getUserdata(L, 2); + if (spell) { + spellBlock_t sb; + if (g_monsters.deserializeSpell(spell, sb, monsterType->name)) { + monsterType->info.attackSpells.push_back(std::move(sb)); + } else { + std::cout << monsterType->name << std::endl; + std::cout << "[Warning - Monsters::loadMonster] Cant load spell. " << spell->name << std::endl; + } + } else { + lua_pushnil(L); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeGetDefenseList(lua_State* L) +{ + // monsterType:getDefenseList() + MonsterType* monsterType = getUserdata(L, 1); + if (!monsterType) { + lua_pushnil(L); + return 1; + } + + lua_createtable(L, monsterType->info.defenseSpells.size(), 0); + + + int index = 0; + for (const auto& spellBlock : monsterType->info.defenseSpells) { + lua_createtable(L, 0, 8); + + setField(L, "chance", spellBlock.chance); + setField(L, "isCombatSpell", spellBlock.combatSpell ? 1 : 0); + setField(L, "isMelee", spellBlock.isMelee ? 1 : 0); + setField(L, "minCombatValue", spellBlock.minCombatValue); + setField(L, "maxCombatValue", spellBlock.maxCombatValue); + setField(L, "range", spellBlock.range); + setField(L, "speed", spellBlock.speed); + pushUserdata(L, static_cast(spellBlock.spell)); + lua_setfield(L, -2, "spell"); + + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeAddDefense(lua_State* L) +{ + // monsterType:addDefense(monsterspell) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + MonsterSpell* spell = getUserdata(L, 2); + if (spell) { + spellBlock_t sb; + if (g_monsters.deserializeSpell(spell, sb, monsterType->name)) { + monsterType->info.defenseSpells.push_back(std::move(sb)); + } else { + std::cout << monsterType->name << std::endl; + std::cout << "[Warning - Monsters::loadMonster] Cant load spell. " << spell->name << std::endl; + } + } else { + lua_pushnil(L); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeGetElementList(lua_State* L) +{ + // monsterType:getElementList() + MonsterType* monsterType = getUserdata(L, 1); + if (!monsterType) { + lua_pushnil(L); + return 1; + } + + lua_createtable(L, monsterType->info.elementMap.size(), 0); + for (const auto& elementEntry : monsterType->info.elementMap) { + lua_pushnumber(L, elementEntry.second); + lua_rawseti(L, -2, elementEntry.first); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeAddElement(lua_State* L) +{ + // monsterType:addElement(type, percent) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + CombatType_t element = getNumber(L, 2); + monsterType->info.elementMap[element] = getNumber(L, 3); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeGetVoices(lua_State* L) +{ + // monsterType:getVoices() + MonsterType* monsterType = getUserdata(L, 1); + if (!monsterType) { + lua_pushnil(L); + return 1; + } + + int index = 0; + lua_createtable(L, monsterType->info.voiceVector.size(), 0); + for (const auto& voiceBlock : monsterType->info.voiceVector) { + lua_createtable(L, 0, 2); + setField(L, "text", voiceBlock.text); + setField(L, "yellText", voiceBlock.yellText); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeAddVoice(lua_State* L) +{ + // monsterType:addVoice(sentence, interval, chance, yell) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + voiceBlock_t voice; + voice.text = getString(L, 2); + monsterType->info.yellSpeedTicks = getNumber(L, 3); + monsterType->info.yellChance = getNumber(L, 4); + voice.yellText = getBoolean(L, 5); + monsterType->info.voiceVector.push_back(voice); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeGetLoot(lua_State* L) +{ + // monsterType:getLoot() + MonsterType* monsterType = getUserdata(L, 1); + if (!monsterType) { + lua_pushnil(L); + return 1; + } + + pushLoot(L, monsterType->info.lootItems); + return 1; +} + +int LuaScriptInterface::luaMonsterTypeAddLoot(lua_State* L) +{ + // monsterType:addLoot(loot) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + Loot* loot = getUserdata(L, 2); + if (loot) { + monsterType->loadLoot(monsterType, loot->lootBlock); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeGetCreatureEvents(lua_State* L) +{ + // monsterType:getCreatureEvents() + MonsterType* monsterType = getUserdata(L, 1); + if (!monsterType) { + lua_pushnil(L); + return 1; + } + + int index = 0; + lua_createtable(L, monsterType->info.scripts.size(), 0); + for (const std::string& creatureEvent : monsterType->info.scripts) { + pushString(L, creatureEvent); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeRegisterEvent(lua_State* L) +{ + // monsterType:registerEvent(name) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + monsterType->info.scripts.push_back(getString(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeEventOnCallback(lua_State* L) +{ + // monsterType:onThink(callback) + // monsterType:onAppear(callback) + // monsterType:onDisappear(callback) + // monsterType:onMove(callback) + // monsterType:onSay(callback) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (monsterType->loadCallback(&g_scripts->getScriptInterface())) { + pushBoolean(L, true); + return 1; + } + pushBoolean(L, false); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeEventType(lua_State* L) +{ + // monstertype:eventType(event) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + monsterType->info.eventType = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeGetSummonList(lua_State* L) +{ + // monsterType:getSummonList() + MonsterType* monsterType = getUserdata(L, 1); + if (!monsterType) { + lua_pushnil(L); + return 1; + } + + int index = 0; + lua_createtable(L, monsterType->info.summons.size(), 0); + for (const auto& summonBlock : monsterType->info.summons) { + lua_createtable(L, 0, 3); + setField(L, "name", summonBlock.name); + setField(L, "speed", summonBlock.speed); + setField(L, "chance", summonBlock.chance); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeAddSummon(lua_State* L) +{ + // monsterType:addSummon(name, interval, chance) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + summonBlock_t summon; + summon.name = getString(L, 2); + summon.chance = getNumber(L, 3); + summon.speed = getNumber(L, 4); + monsterType->info.summons.push_back(summon); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeMaxSummons(lua_State* L) +{ + // get: monsterType:maxSummons() set: monsterType:maxSummons(ammount) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.maxSummons); + } else { + monsterType->info.maxSummons = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeArmor(lua_State* L) +{ + // get: monsterType:armor() set: monsterType:armor(armor) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.armor); + } else { + monsterType->info.armor = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeDefense(lua_State* L) +{ + // get: monsterType:defense() set: monsterType:defense(defense) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.defense); + } else { + monsterType->info.defense = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeOutfit(lua_State* L) +{ + // get: monsterType:outfit() set: monsterType:outfit(outfit) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + pushOutfit(L, monsterType->info.outfit); + } else { + monsterType->info.outfit = getOutfit(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeRace(lua_State* L) +{ + // get: monsterType:race() set: monsterType:race(race) + MonsterType* monsterType = getUserdata(L, 1); + std::string race = getString(L, 2); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.race); + } else { + if (race == "venom") { + monsterType->info.race = RACE_VENOM; + } else if (race == "blood") { + monsterType->info.race = RACE_BLOOD; + } else if (race == "undead") { + monsterType->info.race = RACE_UNDEAD; + } else if (race == "fire") { + monsterType->info.race = RACE_FIRE; + } else if (race == "energy") { + monsterType->info.race = RACE_ENERGY; + } else { + std::cout << "[Warning - Monsters::loadMonster] Unknown race type " << race << "." << std::endl; + lua_pushnil(L); + return 1; + } + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeCorpseId(lua_State* L) +{ + // get: monsterType:corpseId() set: monsterType:corpseId(id) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.lookcorpse); + } else { + monsterType->info.lookcorpse = getNumber(L, 2); + lua_pushboolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeManaCost(lua_State* L) +{ + // get: monsterType:manaCost() set: monsterType:manaCost(mana) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.manaCost); + } else { + monsterType->info.manaCost = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeBaseSpeed(lua_State* L) +{ + // get: monsterType:baseSpeed() set: monsterType:baseSpeed(speed) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.baseSpeed); + } else { + monsterType->info.baseSpeed = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeLight(lua_State* L) +{ + // get: monsterType:light() set: monsterType:light(color, level) + MonsterType* monsterType = getUserdata(L, 1); + if (!monsterType) { + lua_pushnil(L); + return 1; + } + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.light.level); + lua_pushnumber(L, monsterType->info.light.color); + return 2; + } else { + monsterType->info.light.color = getNumber(L, 2); + monsterType->info.light.level = getNumber(L, 3); + pushBoolean(L, true); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeStaticAttackChance(lua_State* L) +{ + // get: monsterType:staticAttackChance() set: monsterType:staticAttackChance(chance) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.staticAttackChance); + } else { + monsterType->info.staticAttackChance = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeTargetDistance(lua_State* L) +{ + // get: monsterType:targetDistance() set: monsterType:targetDistance(distance) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.targetDistance); + } else { + monsterType->info.targetDistance = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeYellChance(lua_State* L) +{ + // get: monsterType:yellChance() set: monsterType:yellChance(chance) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.yellChance); + } else { + monsterType->info.yellChance = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeYellSpeedTicks(lua_State* L) +{ + // get: monsterType:yellSpeedTicks() set: monsterType:yellSpeedTicks(rate) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.yellSpeedTicks); + } else { + monsterType->info.yellSpeedTicks = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeChangeTargetChance(lua_State* L) +{ + // get: monsterType:changeTargetChance() set: monsterType:changeTargetChance(chance) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.changeTargetChance); + } else { + monsterType->info.changeTargetChance = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterTypeChangeTargetSpeed(lua_State* L) +{ + // get: monsterType:changeTargetSpeed() set: monsterType:changeTargetSpeed(speed) + MonsterType* monsterType = getUserdata(L, 1); + if (monsterType) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, monsterType->info.changeTargetSpeed); + } else { + monsterType->info.changeTargetSpeed = getNumber(L, 2); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +// Loot +int LuaScriptInterface::luaCreateLoot(lua_State* L) +{ + // Loot() will create a new loot item + Loot* loot = new Loot(); + if (loot) { + pushUserdata(L, loot); + setMetatable(L, -1, "Loot"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaDeleteLoot(lua_State* L) +{ + // loot:delete() loot:__gc() + Loot** lootPtr = getRawUserdata(L, 1); + if (lootPtr && *lootPtr) { + delete *lootPtr; + *lootPtr = nullptr; + } + return 0; +} + +int LuaScriptInterface::luaLootSetId(lua_State* L) +{ + // loot:setId(id or name) + Loot* loot = getUserdata(L, 1); + if (loot) { + if (isNumber(L, 2)) { + loot->lootBlock.id = getNumber(L, 2); + } else { + auto name = getString(L, 2); + auto ids = Item::items.nameToItems.equal_range(asLowerCaseString(name)); + + if (ids.first == Item::items.nameToItems.cend()) { + std::cout << "[Warning - Loot:setId] Unknown loot item \"" << name << "\". " << std::endl; + pushBoolean(L, false); + return 1; + } + + if (std::next(ids.first) != ids.second) { + std::cout << "[Warning - Loot:setId] Non-unique loot item \"" << name << "\". " << std::endl; + pushBoolean(L, false); + return 1; + } + + loot->lootBlock.id = ids.first->second; + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaLootSetSubType(lua_State* L) +{ + // loot:setSubType(type) + Loot* loot = getUserdata(L, 1); + if (loot) { + loot->lootBlock.subType = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaLootSetChance(lua_State* L) +{ + // loot:setChance(chance) + Loot* loot = getUserdata(L, 1); + if (loot) { + loot->lootBlock.chance = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaLootSetMaxCount(lua_State* L) +{ + // loot:setMaxCount(max) + Loot* loot = getUserdata(L, 1); + if (loot) { + loot->lootBlock.countmax = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaLootSetActionId(lua_State* L) +{ + // loot:setActionId(actionid) + Loot* loot = getUserdata(L, 1); + if (loot) { + loot->lootBlock.actionId = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaLootSetDescription(lua_State* L) +{ + // loot:setDescription(desc) + Loot* loot = getUserdata(L, 1); + if (loot) { + loot->lootBlock.text = getString(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaLootAddChildLoot(lua_State* L) +{ + // loot:addChildLoot(loot) + Loot* loot = getUserdata(L, 1); + if (loot) { + loot->lootBlock.childLoot.push_back(getUserdata(L, 2)->lootBlock); + } else { + lua_pushnil(L); + } + return 1; +} + +// MonsterSpell +int LuaScriptInterface::luaCreateMonsterSpell(lua_State* L) +{ + // MonsterSpell() will create a new Monster Spell + MonsterSpell* spell = new MonsterSpell(); + if (spell) { + pushUserdata(L, spell); + setMetatable(L, -1, "MonsterSpell"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaDeleteMonsterSpell(lua_State* L) +{ + // monsterSpell:delete() monsterSpell:__gc() + MonsterSpell** monsterSpellPtr = getRawUserdata(L, 1); + if (monsterSpellPtr && *monsterSpellPtr) { + delete *monsterSpellPtr; + *monsterSpellPtr = nullptr; + } + return 0; +} + +int LuaScriptInterface::luaMonsterSpellSetType(lua_State* L) +{ + // monsterSpell:setType(type) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->name = getString(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetScriptName(lua_State* L) +{ + // monsterSpell:setScriptName(name) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->scriptName = getString(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetChance(lua_State* L) +{ + // monsterSpell:setChance(chance) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->chance = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetInterval(lua_State* L) +{ + // monsterSpell:setInterval(interval) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->interval = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetRange(lua_State* L) +{ + // monsterSpell:setRange(range) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->range = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetCombatValue(lua_State* L) +{ + // monsterSpell:setCombatValue(min, max) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->minCombatValue = getNumber(L, 2); + spell->maxCombatValue = getNumber(L, 3); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetCombatType(lua_State* L) +{ + // monsterSpell:setCombatType(combatType_t) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->combatType = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetAttackValue(lua_State* L) +{ + // monsterSpell:setAttackValue(attack, skill) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->attack = getNumber(L, 2); + spell->skill = getNumber(L, 3); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetNeedTarget(lua_State* L) +{ + // monsterSpell:setNeedTarget(bool) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->needTarget = getBoolean(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetCombatLength(lua_State* L) +{ + // monsterSpell:setCombatLength(length) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->length = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetCombatSpread(lua_State* L) +{ + // monsterSpell:setCombatSpread(spread) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->spread = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetCombatRadius(lua_State* L) +{ + // monsterSpell:setCombatRadius(radius) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->radius = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetConditionType(lua_State* L) +{ + // monsterSpell:setConditionType(type) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->conditionType = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetConditionDamage(lua_State* L) +{ + // monsterSpell:setConditionDamage(min, max, start) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->conditionMinDamage = getNumber(L, 2); + spell->conditionMaxDamage = getNumber(L, 3); + spell->conditionStartDamage = getNumber(L, 4); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetConditionSpeedChange(lua_State* L) +{ + // monsterSpell:setConditionSpeedChange(minSpeed[, maxSpeed]) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->minSpeedChange = getNumber(L, 2); + spell->maxSpeedChange = getNumber(L, 3, 0); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetConditionDuration(lua_State* L) +{ + // monsterSpell:setConditionDuration(duration) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->duration = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetConditionTickInterval(lua_State* L) +{ + // monsterSpell:setConditionTickInterval(interval) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->tickInterval = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetCombatShootEffect(lua_State* L) +{ + // monsterSpell:setCombatShootEffect(effect) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->shoot = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMonsterSpellSetCombatEffect(lua_State* L) +{ + // monsterSpell:setCombatEffect(effect) + MonsterSpell* spell = getUserdata(L, 1); + if (spell) { + spell->effect = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +// Party +int32_t LuaScriptInterface::luaPartyCreate(lua_State* L) +{ + // Party(userdata) + Player* player = getUserdata(L, 2); + if (!player) { + lua_pushnil(L); + return 1; + } + + Party* party = player->getParty(); + if (!party) { + party = new Party(player); + g_game.updatePlayerShield(player); + player->sendCreatureSkull(player); + pushUserdata(L, party); + setMetatable(L, -1, "Party"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartyDisband(lua_State* L) +{ + // party:disband() + Party** partyPtr = getRawUserdata(L, 1); + if (partyPtr && *partyPtr) { + Party*& party = *partyPtr; + party->disband(); + party = nullptr; + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartyGetLeader(lua_State* L) +{ + // party:getLeader() + Party* party = getUserdata(L, 1); + if (!party) { + lua_pushnil(L); + return 1; + } + + Player* leader = party->getLeader(); + if (leader) { + pushUserdata(L, leader); + setMetatable(L, -1, "Player"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartySetLeader(lua_State* L) +{ + // party:setLeader(player) + Player* player = getPlayer(L, 2); + Party* party = getUserdata(L, 1); + if (party && player) { + pushBoolean(L, party->passPartyLeadership(player)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartyGetMembers(lua_State* L) +{ + // party:getMembers() + Party* party = getUserdata(L, 1); + if (!party) { + lua_pushnil(L); + return 1; + } + + int index = 0; + lua_createtable(L, party->getMemberCount(), 0); + for (Player* player : party->getMembers()) { + pushUserdata(L, player); + setMetatable(L, -1, "Player"); + lua_rawseti(L, -2, ++index); + } + return 1; +} + +int LuaScriptInterface::luaPartyGetMemberCount(lua_State* L) +{ + // party:getMemberCount() + Party* party = getUserdata(L, 1); + if (party) { + lua_pushnumber(L, party->getMemberCount()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartyGetInvitees(lua_State* L) +{ + // party:getInvitees() + Party* party = getUserdata(L, 1); + if (party) { + lua_createtable(L, party->getInvitationCount(), 0); + + int index = 0; + for (Player* player : party->getInvitees()) { + pushUserdata(L, player); + setMetatable(L, -1, "Player"); + lua_rawseti(L, -2, ++index); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartyGetInviteeCount(lua_State* L) +{ + // party:getInviteeCount() + Party* party = getUserdata(L, 1); + if (party) { + lua_pushnumber(L, party->getInvitationCount()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartyAddInvite(lua_State* L) +{ + // party:addInvite(player) + Player* player = getPlayer(L, 2); + Party* party = getUserdata(L, 1); + if (party && player) { + pushBoolean(L, party->invitePlayer(*player)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartyRemoveInvite(lua_State* L) +{ + // party:removeInvite(player) + Player* player = getPlayer(L, 2); + Party* party = getUserdata(L, 1); + if (party && player) { + pushBoolean(L, party->removeInvite(*player)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartyAddMember(lua_State* L) +{ + // party:addMember(player) + Player* player = getPlayer(L, 2); + Party* party = getUserdata(L, 1); + if (party && player) { + pushBoolean(L, party->joinParty(*player)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartyRemoveMember(lua_State* L) +{ + // party:removeMember(player) + Player* player = getPlayer(L, 2); + Party* party = getUserdata(L, 1); + if (party && player) { + pushBoolean(L, party->leaveParty(player)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartyIsSharedExperienceActive(lua_State* L) +{ + // party:isSharedExperienceActive() + Party* party = getUserdata(L, 1); + if (party) { + pushBoolean(L, party->isSharedExperienceActive()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartyIsSharedExperienceEnabled(lua_State* L) +{ + // party:isSharedExperienceEnabled() + Party* party = getUserdata(L, 1); + if (party) { + pushBoolean(L, party->isSharedExperienceEnabled()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartyShareExperience(lua_State* L) +{ + // party:shareExperience(experience) + uint64_t experience = getNumber(L, 2); + Party* party = getUserdata(L, 1); + if (party) { + party->shareExperience(experience); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaPartySetSharedExperience(lua_State* L) +{ + // party:setSharedExperience(active) + bool active = getBoolean(L, 2); + Party* party = getUserdata(L, 1); + if (party) { + pushBoolean(L, party->setSharedExperience(party->getLeader(), active)); + } else { + lua_pushnil(L); + } + return 1; +} + +// Spells +int LuaScriptInterface::luaSpellCreate(lua_State* L) +{ + // Spell(words, name or id) to get an existing spell + // Spell(type) ex: Spell(SPELL_INSTANT) or Spell(SPELL_RUNE) to create a new spell + if (lua_gettop(L) == 1) { + std::cout << "[Error - Spell::luaSpellCreate] There is no parameter set!" << std::endl; + lua_pushnil(L); + return 1; + } + + SpellType_t type = getNumber(L, 2); + + if (isString(L, 2)) { + std::string tmp = asLowerCaseString(getString(L, 2)); + if (tmp == "instant") { + type = SPELL_INSTANT; + } else if (tmp == "rune") { + type = SPELL_RUNE; + } + } + + if (type == SPELL_INSTANT) { + InstantSpell* spell = new InstantSpell(getScriptEnv()->getScriptInterface()); + spell->fromLua = true; + pushUserdata(L, spell); + setMetatable(L, -1, "Spell"); + spell->spellType = SPELL_INSTANT; + return 1; + } else if (type == SPELL_RUNE) { + RuneSpell* spell = new RuneSpell(getScriptEnv()->getScriptInterface()); + spell->fromLua = true; + pushUserdata(L, spell); + setMetatable(L, -1, "Spell"); + spell->spellType = SPELL_RUNE; + return 1; + } + + // isNumber(L, 2) doesn't work here for some reason, maybe a bug? + if (getNumber(L, 2)) { + InstantSpell* instant = g_spells->getInstantSpellById(getNumber(L, 2)); + if (instant) { + pushUserdata(L, instant); + setMetatable(L, -1, "Spell"); + return 1; + } + RuneSpell* rune = g_spells->getRuneSpell(getNumber(L, 2)); + if (rune) { + pushUserdata(L, rune); + setMetatable(L, -1, "Spell"); + return 1; + } + } else if (isString(L, 2)) { + std::string arg = getString(L, 2); + InstantSpell* instant = g_spells->getInstantSpellByName(arg); + if (instant) { + pushUserdata(L, instant); + setMetatable(L, -1, "Spell"); + return 1; + } + instant = g_spells->getInstantSpell(arg); + if (instant) { + pushUserdata(L, instant); + setMetatable(L, -1, "Spell"); + return 1; + } + RuneSpell* rune = g_spells->getRuneSpellByName(arg); + if (rune) { + pushUserdata(L, rune); + setMetatable(L, -1, "Spell"); + return 1; + } + } + lua_pushnil(L); + return 1; +} + +int LuaScriptInterface::luaSpellOnCastSpell(lua_State* L) +{ + // spell:onCastSpell(callback) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (spell->spellType == SPELL_INSTANT) { + InstantSpell* instant = dynamic_cast(getUserdata(L, 1)); + if (!instant->loadCallback()) { + pushBoolean(L, false); + return 1; + } + instant->scripted = true; + pushBoolean(L, true); + } else if (spell->spellType == SPELL_RUNE) { + RuneSpell* rune = dynamic_cast(getUserdata(L, 1)); + if (!rune->loadCallback()) { + pushBoolean(L, false); + return 1; + } + rune->scripted = true; + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellRegister(lua_State* L) +{ + // spell:register() + Spell* spell = getUserdata(L, 1); + if (spell) { + if (spell->spellType == SPELL_INSTANT) { + InstantSpell* instant = dynamic_cast(getUserdata(L, 1)); + if (!instant->isScripted()) { + pushBoolean(L, false); + return 1; + } + pushBoolean(L, g_spells->registerInstantLuaEvent(instant)); + } else if (spell->spellType == SPELL_RUNE) { + RuneSpell* rune = dynamic_cast(getUserdata(L, 1)); + if (rune->getMagicLevel() != 0 || rune->getLevel() != 0) { + //Change information in the ItemType to get accurate description + ItemType& iType = Item::items.getItemType(rune->getRuneItemId()); + iType.name = rune->getName(); + iType.runeMagLevel = rune->getMagicLevel(); + iType.runeLevel = rune->getLevel(); + iType.charges = rune->getCharges(); + } + if (!rune->isScripted()) { + pushBoolean(L, false); + return 1; + } + pushBoolean(L, g_spells->registerRuneLuaEvent(rune)); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellName(lua_State* L) +{ + // spell:name(name) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + pushString(L, spell->getName()); + } else { + spell->setName(getString(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellId(lua_State* L) +{ + // spell:id(id) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, spell->getId()); + } else { + spell->setId(getNumber(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellGroup(lua_State* L) +{ + // spell:group(primaryGroup[, secondaryGroup]) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, spell->getGroup()); + lua_pushnumber(L, spell->getSecondaryGroup()); + return 2; + } else if (lua_gettop(L) == 2) { + SpellGroup_t group = getNumber(L, 2); + if (group) { + spell->setGroup(group); + pushBoolean(L, true); + } else if (isString(L, 2)) { + group = stringToSpellGroup(getString(L, 2)); + if (group != SPELLGROUP_NONE) { + spell->setGroup(group); + } else { + std::cout << "[Warning - Spell::group] Unknown group: " << getString(L, 2) << std::endl; + pushBoolean(L, false); + return 1; + } + pushBoolean(L, true); + } else { + std::cout << "[Warning - Spell::group] Unknown group: " << getString(L, 2) << std::endl; + pushBoolean(L, false); + return 1; + } + } else { + SpellGroup_t primaryGroup = getNumber(L, 2); + SpellGroup_t secondaryGroup = getNumber(L, 2); + if (primaryGroup && secondaryGroup) { + spell->setGroup(primaryGroup); + spell->setSecondaryGroup(secondaryGroup); + pushBoolean(L, true); + } else if (isString(L, 2) && isString(L, 3)) { + primaryGroup = stringToSpellGroup(getString(L, 2)); + if (primaryGroup != SPELLGROUP_NONE) { + spell->setGroup(primaryGroup); + } else { + std::cout << "[Warning - Spell::group] Unknown primaryGroup: " << getString(L, 2) << std::endl; + pushBoolean(L, false); + return 1; + } + secondaryGroup = stringToSpellGroup(getString(L, 3)); + if (secondaryGroup != SPELLGROUP_NONE) { + spell->setSecondaryGroup(secondaryGroup); + } else { + std::cout << "[Warning - Spell::group] Unknown secondaryGroup: " << getString(L, 3) << std::endl; + pushBoolean(L, false); + return 1; + } + pushBoolean(L, true); + } else { + std::cout << "[Warning - Spell::group] Unknown primaryGroup: " << getString(L, 2) << " or secondaryGroup: " << getString(L, 3) << std::endl; + pushBoolean(L, false); + return 1; + } + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellCooldown(lua_State* L) +{ + // spell:cooldown(cooldown) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, spell->getCooldown()); + } else { + spell->setCooldown(getNumber(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellGroupCooldown(lua_State* L) +{ + // spell:groupCooldown(primaryGroupCd[, secondaryGroupCd]) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, spell->getGroupCooldown()); + lua_pushnumber(L, spell->getSecondaryCooldown()); + return 2; + } else if (lua_gettop(L) == 2) { + spell->setGroupCooldown(getNumber(L, 2)); + pushBoolean(L, true); + } else { + spell->setGroupCooldown(getNumber(L, 2)); + spell->setSecondaryCooldown(getNumber(L, 3)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellLevel(lua_State* L) +{ + // spell:level(lvl) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, spell->getLevel()); + } else { + spell->setLevel(getNumber(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellMagicLevel(lua_State* L) +{ + // spell:magicLevel(lvl) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, spell->getMagicLevel()); + } else { + spell->setMagicLevel(getNumber(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellMana(lua_State* L) +{ + // spell:mana(mana) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, spell->getMana()); + } else { + spell->setMana(getNumber(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellManaPercent(lua_State* L) +{ + // spell:manaPercent(percent) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, spell->getManaPercent()); + } else { + spell->setManaPercent(getNumber(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellSoul(lua_State* L) +{ + // spell:soul(soul) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, spell->getSoulCost()); + } else { + spell->setSoulCost(getNumber(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellRange(lua_State* L) +{ + // spell:range(range) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + lua_pushnumber(L, spell->getRange()); + } else { + spell->setRange(getNumber(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellPremium(lua_State* L) +{ + // spell:isPremium(bool) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->isPremium()); + } else { + spell->setPremium(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellEnabled(lua_State* L) +{ + // spell:isEnabled(bool) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->isEnabled()); + } else { + spell->setEnabled(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellNeedTarget(lua_State* L) +{ + // spell:needTarget(bool) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getNeedTarget()); + } else { + spell->setNeedTarget(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellNeedWeapon(lua_State* L) +{ + // spell:needWeapon(bool) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getNeedWeapon()); + } else { + spell->setNeedWeapon(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellNeedLearn(lua_State* L) +{ + // spell:needLearn(bool) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getNeedLearn()); + } else { + spell->setNeedLearn(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellSelfTarget(lua_State* L) +{ + // spell:isSelfTarget(bool) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getSelfTarget()); + } else { + spell->setSelfTarget(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellBlocking(lua_State* L) +{ + // spell:isBlocking(blockingSolid, blockingCreature) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getBlockingSolid()); + pushBoolean(L, spell->getBlockingCreature()); + return 2; + } else { + spell->setBlockingSolid(getBoolean(L, 2)); + spell->setBlockingCreature(getBoolean(L, 3)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellAggressive(lua_State* L) +{ + // spell:isAggressive(bool) + Spell* spell = getUserdata(L, 1); + if (spell) { + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getAggressive()); + } else { + spell->setAggressive(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaSpellVocation(lua_State* L) +{ + // spell:vocation(vocation) + Spell* spell = getUserdata(L, 1); + if (!spell) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) == 1) { + lua_createtable(L, 0, 0); + int i = 0; + for (auto& voc : spell->getVocMap()) { + std::string name = g_vocations.getVocation(voc.first)->getVocName(); + pushString(L, name); + lua_rawseti(L, -2, ++i); + } + setMetatable(L, -1, "Spell"); + } else { + int parameters = lua_gettop(L) - 1; // - 1 because self is a parameter aswell, which we want to skip ofc + for (int i = 0; i < parameters; ++i) { + std::vector vocList = explodeString(getString(L, 2 + i), ";"); + spell->addVocMap(g_vocations.getVocationId(vocList[0]), vocList.size() > 1 ? booleanString(vocList[1]) : false); + } + pushBoolean(L, true); + } + return 1; +} + +// only for InstantSpells +int LuaScriptInterface::luaSpellWords(lua_State* L) +{ + // spell:words(words[, separator = ""]) + InstantSpell* spell = dynamic_cast(getUserdata(L, 1)); + if (spell) { + // if spell != SPELL_INSTANT, it means that this actually is no InstantSpell, so we return nil + if (spell->spellType != SPELL_INSTANT) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) == 1) { + pushString(L, spell->getWords()); + pushString(L, spell->getSeparator()); + return 2; + } else { + std::string sep = ""; + if (lua_gettop(L) == 3) { + sep = getString(L, 3); + } + spell->setWords(getString(L, 2)); + spell->setSeparator(sep); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +// only for InstantSpells +int LuaScriptInterface::luaSpellNeedDirection(lua_State* L) +{ + // spell:needDirection(bool) + InstantSpell* spell = dynamic_cast(getUserdata(L, 1)); + if (spell) { + // if spell != SPELL_INSTANT, it means that this actually is no InstantSpell, so we return nil + if (spell->spellType != SPELL_INSTANT) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getNeedDirection()); + } else { + spell->setNeedDirection(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +// only for InstantSpells +int LuaScriptInterface::luaSpellHasParams(lua_State* L) +{ + // spell:hasParams(bool) + InstantSpell* spell = dynamic_cast(getUserdata(L, 1)); + if (spell) { + // if spell != SPELL_INSTANT, it means that this actually is no InstantSpell, so we return nil + if (spell->spellType != SPELL_INSTANT) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getHasParam()); + } else { + spell->setHasParam(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +// only for InstantSpells +int LuaScriptInterface::luaSpellHasPlayerNameParam(lua_State* L) +{ + // spell:hasPlayerNameParam(bool) + InstantSpell* spell = dynamic_cast(getUserdata(L, 1)); + if (spell) { + // if spell != SPELL_INSTANT, it means that this actually is no InstantSpell, so we return nil + if (spell->spellType != SPELL_INSTANT) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getHasPlayerNameParam()); + } else { + spell->setHasPlayerNameParam(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +// only for InstantSpells +int LuaScriptInterface::luaSpellNeedCasterTargetOrDirection(lua_State* L) +{ + // spell:needCasterTargetOrDirection(bool) + InstantSpell* spell = dynamic_cast(getUserdata(L, 1)); + if (spell) { + // if spell != SPELL_INSTANT, it means that this actually is no InstantSpell, so we return nil + if (spell->spellType != SPELL_INSTANT) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getNeedCasterTargetOrDirection()); + } else { + spell->setNeedCasterTargetOrDirection(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +// only for InstantSpells +int LuaScriptInterface::luaSpellIsBlockingWalls(lua_State* L) +{ + // spell:blockWalls(bool) + InstantSpell* spell = dynamic_cast(getUserdata(L, 1)); + if (spell) { + // if spell != SPELL_INSTANT, it means that this actually is no InstantSpell, so we return nil + if (spell->spellType != SPELL_INSTANT) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getBlockWalls()); + } else { + spell->setBlockWalls(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +// only for RuneSpells +int LuaScriptInterface::luaSpellRuneId(lua_State* L) +{ + // spell:runeId(id) + RuneSpell* spell = dynamic_cast(getUserdata(L, 1)); + if (spell) { + // if spell != SPELL_RUNE, it means that this actually is no RuneSpell, so we return nil + if (spell->spellType != SPELL_RUNE) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) == 1) { + lua_pushnumber(L, spell->getRuneItemId()); + } else { + spell->setRuneItemId(getNumber(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +// only for RuneSpells +int LuaScriptInterface::luaSpellCharges(lua_State* L) +{ + // spell:charges(charges) + RuneSpell* spell = dynamic_cast(getUserdata(L, 1)); + if (spell) { + // if spell != SPELL_RUNE, it means that this actually is no RuneSpell, so we return nil + if (spell->spellType != SPELL_RUNE) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) == 1) { + lua_pushnumber(L, spell->getCharges()); + } else { + spell->setCharges(getNumber(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +// only for RuneSpells +int LuaScriptInterface::luaSpellAllowFarUse(lua_State* L) +{ + // spell:allowFarUse(bool) + RuneSpell* spell = dynamic_cast(getUserdata(L, 1)); + if (spell) { + // if spell != SPELL_RUNE, it means that this actually is no RuneSpell, so we return nil + if (spell->spellType != SPELL_RUNE) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getAllowFarUse()); + } else { + spell->setAllowFarUse(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +// only for RuneSpells +int LuaScriptInterface::luaSpellBlockWalls(lua_State* L) +{ + // spell:blockWalls(bool) + RuneSpell* spell = dynamic_cast(getUserdata(L, 1)); + if (spell) { + // if spell != SPELL_RUNE, it means that this actually is no RuneSpell, so we return nil + if (spell->spellType != SPELL_RUNE) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getCheckLineOfSight()); + } else { + spell->setCheckLineOfSight(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +// only for RuneSpells +int LuaScriptInterface::luaSpellCheckFloor(lua_State* L) +{ + // spell:checkFloor(bool) + RuneSpell* spell = dynamic_cast(getUserdata(L, 1)); + if (spell) { + // if spell != SPELL_RUNE, it means that this actually is no RuneSpell, so we return nil + if (spell->spellType != SPELL_RUNE) { + lua_pushnil(L); + return 1; + } + + if (lua_gettop(L) == 1) { + pushBoolean(L, spell->getCheckFloor()); + } else { + spell->setCheckFloor(getBoolean(L, 2)); + pushBoolean(L, true); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreateAction(lua_State* L) +{ + // Action() + if (getScriptEnv()->getScriptInterface() != &g_scripts->getScriptInterface()) { + reportErrorFunc("Actions can only be registered in the Scripts interface."); + lua_pushnil(L); + return 1; + } + + Action* action = new Action(getScriptEnv()->getScriptInterface()); + if (action) { + action->fromLua = true; + pushUserdata(L, action); + setMetatable(L, -1, "Action"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaActionOnUse(lua_State* L) +{ + // action:onUse(callback) + Action* action = getUserdata(L, 1); + if (action) { + if (!action->loadCallback()) { + pushBoolean(L, false); + return 1; + } + action->scripted = true; + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaActionRegister(lua_State* L) +{ + // action:register() + Action* action = getUserdata(L, 1); + if (action) { + if (!action->isScripted()) { + pushBoolean(L, false); + return 1; + } + pushBoolean(L, g_actions->registerLuaEvent(action)); + action->getActionIdRange().clear(); + action->getItemIdRange().clear(); + action->getUniqueIdRange().clear(); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaActionItemId(lua_State* L) +{ + // action:id(ids) + Action* action = getUserdata(L, 1); + if (action) { + int parameters = lua_gettop(L) - 1; // - 1 because self is a parameter aswell, which we want to skip ofc + if (parameters > 1) { + for (int i = 0; i < parameters; ++i) { + action->addItemId(getNumber(L, 2 + i)); + } + } else { + action->addItemId(getNumber(L, 2)); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaActionActionId(lua_State* L) +{ + // action:aid(aids) + Action* action = getUserdata(L, 1); + if (action) { + int parameters = lua_gettop(L) - 1; // - 1 because self is a parameter aswell, which we want to skip ofc + if (parameters > 1) { + for (int i = 0; i < parameters; ++i) { + action->addActionId(getNumber(L, 2 + i)); + } + } else { + action->addActionId(getNumber(L, 2)); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaActionUniqueId(lua_State* L) +{ + // action:uid(uids) + Action* action = getUserdata(L, 1); + if (action) { + int parameters = lua_gettop(L) - 1; // - 1 because self is a parameter aswell, which we want to skip ofc + if (parameters > 1) { + for (int i = 0; i < parameters; ++i) { + action->addUniqueId(getNumber(L, 2 + i)); + } + } else { + action->addUniqueId(getNumber(L, 2)); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaActionAllowFarUse(lua_State* L) +{ + // action:allowFarUse(bool) + Action* action = getUserdata(L, 1); + if (action) { + action->setAllowFarUse(getBoolean(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaActionBlockWalls(lua_State* L) +{ + // action:blockWalls(bool) + Action* action = getUserdata(L, 1); + if (action) { + action->setCheckLineOfSight(getBoolean(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaActionCheckFloor(lua_State* L) +{ + // action:checkFloor(bool) + Action* action = getUserdata(L, 1); + if (action) { + action->setCheckFloor(getBoolean(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreateTalkaction(lua_State* L) +{ + // TalkAction(words) + if (getScriptEnv()->getScriptInterface() != &g_scripts->getScriptInterface()) { + reportErrorFunc("TalkActions can only be registered in the Scripts interface."); + lua_pushnil(L); + return 1; + } + + TalkAction* talk = new TalkAction(getScriptEnv()->getScriptInterface()); + if (talk) { + talk->setWords(getString(L, 2)); + talk->fromLua = true; + pushUserdata(L, talk); + setMetatable(L, -1, "TalkAction"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTalkactionOnSay(lua_State* L) +{ + // talkAction:onSay(callback) + TalkAction* talk = getUserdata(L, 1); + if (talk) { + if (!talk->loadCallback()) { + pushBoolean(L, false); + return 1; + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTalkactionRegister(lua_State* L) +{ + // talkAction:register() + TalkAction* talk = getUserdata(L, 1); + if (talk) { + if (!talk->isScripted()) { + pushBoolean(L, false); + return 1; + } + pushBoolean(L, g_talkActions->registerLuaEvent(talk)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaTalkactionSeparator(lua_State* L) +{ + // talkAction:separator(sep) + TalkAction* talk = getUserdata(L, 1); + if (talk) { + talk->setSeparator(getString(L, 2).c_str()); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreateCreatureEvent(lua_State* L) +{ + // CreatureEvent(eventName) + if (getScriptEnv()->getScriptInterface() != &g_scripts->getScriptInterface()) { + reportErrorFunc("CreatureEvents can only be registered in the Scripts interface."); + lua_pushnil(L); + return 1; + } + + CreatureEvent* creature = new CreatureEvent(getScriptEnv()->getScriptInterface()); + if (creature) { + creature->setName(getString(L, 2)); + creature->fromLua = true; + pushUserdata(L, creature); + setMetatable(L, -1, "CreatureEvent"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureEventType(lua_State* L) +{ + // creatureevent:type(callback) + CreatureEvent* creature = getUserdata(L, 1); + if (creature) { + std::string typeName = getString(L, 2); + std::string tmpStr = asLowerCaseString(typeName); + if (tmpStr == "login") { + creature->setEventType(CREATURE_EVENT_LOGIN); + } else if (tmpStr == "logout") { + creature->setEventType(CREATURE_EVENT_LOGOUT); + } else if (tmpStr == "think") { + creature->setEventType(CREATURE_EVENT_THINK); + } else if (tmpStr == "preparedeath") { + creature->setEventType(CREATURE_EVENT_PREPAREDEATH); + } else if (tmpStr == "death") { + creature->setEventType(CREATURE_EVENT_DEATH); + } else if (tmpStr == "kill") { + creature->setEventType(CREATURE_EVENT_KILL); + } else if (tmpStr == "advance") { + creature->setEventType(CREATURE_EVENT_ADVANCE); + } else if (tmpStr == "modalwindow") { + creature->setEventType(CREATURE_EVENT_MODALWINDOW); + } else if (tmpStr == "textedit") { + creature->setEventType(CREATURE_EVENT_TEXTEDIT); + } else if (tmpStr == "healthchange") { + creature->setEventType(CREATURE_EVENT_HEALTHCHANGE); + } else if (tmpStr == "manachange") { + creature->setEventType(CREATURE_EVENT_MANACHANGE); + } else if (tmpStr == "extendedopcode") { + creature->setEventType(CREATURE_EVENT_EXTENDED_OPCODE); + } else { + std::cout << "[Error - CreatureEvent::configureLuaEvent] Invalid type for creature event: " << typeName << std::endl; + pushBoolean(L, false); + } + creature->setLoaded(true); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureEventRegister(lua_State* L) +{ + // creatureevent:register() + CreatureEvent* creature = getUserdata(L, 1); + if (creature) { + if (!creature->isScripted()) { + pushBoolean(L, false); + return 1; + } + pushBoolean(L, g_creatureEvents->registerLuaEvent(creature)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreatureEventOnCallback(lua_State* L) +{ + // creatureevent:onLogin / logout / etc. (callback) + CreatureEvent* creature = getUserdata(L, 1); + if (creature) { + if (!creature->loadCallback()) { + pushBoolean(L, false); + return 1; + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreateMoveEvent(lua_State* L) +{ + // MoveEvent() + if (getScriptEnv()->getScriptInterface() != &g_scripts->getScriptInterface()) { + reportErrorFunc("MoveEvents can only be registered in the Scripts interface."); + lua_pushnil(L); + return 1; + } + + MoveEvent* moveevent = new MoveEvent(getScriptEnv()->getScriptInterface()); + if (moveevent) { + moveevent->fromLua = true; + pushUserdata(L, moveevent); + setMetatable(L, -1, "MoveEvent"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMoveEventType(lua_State* L) +{ + // moveevent:type(callback) + MoveEvent* moveevent = getUserdata(L, 1); + if (moveevent) { + std::string typeName = getString(L, 2); + std::string tmpStr = asLowerCaseString(typeName); + if (tmpStr == "stepin") { + moveevent->setEventType(MOVE_EVENT_STEP_IN); + moveevent->stepFunction = moveevent->StepInField; + } else if (tmpStr == "stepout") { + moveevent->setEventType(MOVE_EVENT_STEP_OUT); + moveevent->stepFunction = moveevent->StepOutField; + } else if (tmpStr == "equip") { + moveevent->setEventType(MOVE_EVENT_EQUIP); + moveevent->equipFunction = moveevent->EquipItem; + } else if (tmpStr == "deequip") { + moveevent->setEventType(MOVE_EVENT_DEEQUIP); + moveevent->equipFunction = moveevent->DeEquipItem; + } else if (tmpStr == "additem") { + moveevent->setEventType(MOVE_EVENT_ADD_ITEM_ITEMTILE); + moveevent->moveFunction = moveevent->AddItemField; + } else if (tmpStr == "removeitem") { + moveevent->setEventType(MOVE_EVENT_REMOVE_ITEM_ITEMTILE); + moveevent->moveFunction = moveevent->RemoveItemField; + } else { + std::cout << "Error: [MoveEvent::configureMoveEvent] No valid event name " << typeName << std::endl; + pushBoolean(L, false); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMoveEventRegister(lua_State* L) +{ + // moveevent:register() + MoveEvent* moveevent = getUserdata(L, 1); + if (moveevent) { + if ((moveevent->getEventType() == MOVE_EVENT_EQUIP || moveevent->getEventType() == MOVE_EVENT_DEEQUIP) && moveevent->getSlot() == SLOTP_WHEREEVER) { + uint32_t id = moveevent->getItemIdRange().at(0); + ItemType& it = Item::items.getItemType(id); + moveevent->setSlot(it.slotPosition); + } + if (!moveevent->isScripted()) { + pushBoolean(L, g_moveEvents->registerLuaFunction(moveevent)); + return 1; + } + pushBoolean(L, g_moveEvents->registerLuaEvent(moveevent)); + moveevent->getItemIdRange().clear(); + moveevent->getActionIdRange().clear(); + moveevent->getUniqueIdRange().clear(); + moveevent->getPosList().clear(); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMoveEventOnCallback(lua_State* L) +{ + // moveevent:onEquip / deEquip / etc. (callback) + MoveEvent* moveevent = getUserdata(L, 1); + if (moveevent) { + if (!moveevent->loadCallback()) { + pushBoolean(L, false); + return 1; + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMoveEventSlot(lua_State* L) +{ + // moveevent:slot(slot) + MoveEvent* moveevent = getUserdata(L, 1); + if (moveevent) { + if (moveevent->getEventType() == MOVE_EVENT_EQUIP || moveevent->getEventType() == MOVE_EVENT_DEEQUIP) { + if (!moveevent->getSlotName().empty()) { + std::string slotName = getString(L, 2); + std::string tmpStr = asLowerCaseString(slotName); + tmpStr = asLowerCaseString(moveevent->getSlotName()); + if (tmpStr == "head") { + moveevent->setSlot(SLOTP_HEAD); + } else if (tmpStr == "necklace") { + moveevent->setSlot(SLOTP_NECKLACE); + } else if (tmpStr == "backpack") { + moveevent->setSlot(SLOTP_BACKPACK); + } else if (tmpStr == "armor" || tmpStr == "body") { + moveevent->setSlot(SLOTP_ARMOR); + } else if (tmpStr == "right-hand") { + moveevent->setSlot(SLOTP_RIGHT); + } else if (tmpStr == "left-hand") { + moveevent->setSlot(SLOTP_LEFT); + } else if (tmpStr == "hand" || tmpStr == "shield") { + moveevent->setSlot(SLOTP_RIGHT | SLOTP_LEFT); + } else if (tmpStr == "legs") { + moveevent->setSlot(SLOTP_LEGS); + } else if (tmpStr == "feet") { + moveevent->setSlot(SLOTP_FEET); + } else if (tmpStr == "ring") { + moveevent->setSlot(SLOTP_RING); + } else if (tmpStr == "ammo") { + moveevent->setSlot(SLOTP_AMMO); + } else { + std::cout << "[Warning - MoveEvent::configureMoveEvent] Unknown slot type: " << moveevent->getSlotName() << std::endl; + } + } + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMoveEventLevel(lua_State* L) +{ + // moveevent:level(lvl) + MoveEvent* moveevent = getUserdata(L, 1); + if (moveevent) { + moveevent->setRequiredLevel(getNumber(L, 2)); + moveevent->setWieldInfo(WIELDINFO_LEVEL); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMoveEventMagLevel(lua_State* L) +{ + // moveevent:magicLevel(lvl) + MoveEvent* moveevent = getUserdata(L, 1); + if (moveevent) { + moveevent->setRequiredMagLevel(getNumber(L, 2)); + moveevent->setWieldInfo(WIELDINFO_MAGLV); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMoveEventPremium(lua_State* L) +{ + // moveevent:premium(bool) + MoveEvent* moveevent = getUserdata(L, 1); + if (moveevent) { + moveevent->setNeedPremium(getBoolean(L, 2)); + moveevent->setWieldInfo(WIELDINFO_PREMIUM); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMoveEventVocation(lua_State* L) +{ + // moveevent:vocation(vocName[, showInDescription = false, lastVoc = false]) + MoveEvent* moveevent = getUserdata(L, 1); + if (moveevent) { + moveevent->addVocEquipMap(getString(L, 2)); + moveevent->setWieldInfo(WIELDINFO_VOCREQ); + std::string tmp; + bool showInDescription = false; + bool lastVoc = false; + if (getBoolean(L, 3)) { + showInDescription = getBoolean(L, 3); + } + if (getBoolean(L, 4)) { + lastVoc = getBoolean(L, 4); + } + if (showInDescription) { + if (moveevent->getVocationString().empty()) { + tmp = asLowerCaseString(getString(L, 2)); + tmp += "s"; + moveevent->setVocationString(tmp); + } else { + tmp = moveevent->getVocationString(); + if (lastVoc) { + tmp += " and "; + } else { + tmp += ", "; + } + tmp += asLowerCaseString(getString(L, 2)); + tmp += "s"; + moveevent->setVocationString(tmp); + } + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMoveEventItemId(lua_State* L) +{ + // moveevent:id(ids) + MoveEvent* moveevent = getUserdata(L, 1); + if (moveevent) { + int parameters = lua_gettop(L) - 1; // - 1 because self is a parameter aswell, which we want to skip ofc + if (parameters > 1) { + for (int i = 0; i < parameters; ++i) { + moveevent->addItemId(getNumber(L, 2 + i)); + } + } else { + moveevent->addItemId(getNumber(L, 2)); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMoveEventActionId(lua_State* L) +{ + // moveevent:aid(ids) + MoveEvent* moveevent = getUserdata(L, 1); + if (moveevent) { + int parameters = lua_gettop(L) - 1; // - 1 because self is a parameter aswell, which we want to skip ofc + if (parameters > 1) { + for (int i = 0; i < parameters; ++i) { + moveevent->addActionId(getNumber(L, 2 + i)); + } + } else { + moveevent->addActionId(getNumber(L, 2)); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMoveEventUniqueId(lua_State* L) +{ + // moveevent:uid(ids) + MoveEvent* moveevent = getUserdata(L, 1); + if (moveevent) { + int parameters = lua_gettop(L) - 1; // - 1 because self is a parameter aswell, which we want to skip ofc + if (parameters > 1) { + for (int i = 0; i < parameters; ++i) { + moveevent->addUniqueId(getNumber(L, 2 + i)); + } + } else { + moveevent->addUniqueId(getNumber(L, 2)); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaMoveEventPosition(lua_State* L) +{ + // moveevent:position(positions) + MoveEvent* moveevent = getUserdata(L, 1); + if (moveevent) { + int parameters = lua_gettop(L) - 1; // - 1 because self is a parameter aswell, which we want to skip ofc + if (parameters > 1) { + for (int i = 0; i < parameters; ++i) { + moveevent->addPosList(getPosition(L, 2 + i)); + } + } else { + moveevent->addPosList(getPosition(L, 2)); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaCreateGlobalEvent(lua_State* L) +{ + // GlobalEvent(eventName) + if (getScriptEnv()->getScriptInterface() != &g_scripts->getScriptInterface()) { + reportErrorFunc("GlobalEvents can only be registered in the Scripts interface."); + lua_pushnil(L); + return 1; + } + + GlobalEvent* global = new GlobalEvent(getScriptEnv()->getScriptInterface()); + if (global) { + global->setName(getString(L, 2)); + global->setEventType(GLOBALEVENT_NONE); + global->fromLua = true; + pushUserdata(L, global); + setMetatable(L, -1, "GlobalEvent"); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGlobalEventType(lua_State* L) +{ + // globalevent:type(callback) + GlobalEvent* global = getUserdata(L, 1); + if (global) { + std::string typeName = getString(L, 2); + std::string tmpStr = asLowerCaseString(typeName); + if (tmpStr == "startup") { + global->setEventType(GLOBALEVENT_STARTUP); + } else if (tmpStr == "shutdown") { + global->setEventType(GLOBALEVENT_SHUTDOWN); + } else if (tmpStr == "record") { + global->setEventType(GLOBALEVENT_RECORD); + } else { + std::cout << "[Error - CreatureEvent::configureLuaEvent] Invalid type for global event: " << typeName << std::endl; + pushBoolean(L, false); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGlobalEventRegister(lua_State* L) +{ + // globalevent:register() + GlobalEvent* globalevent = getUserdata(L, 1); + if (globalevent) { + if (!globalevent->isScripted()) { + pushBoolean(L, false); + return 1; + } + pushBoolean(L, g_globalEvents->registerLuaEvent(globalevent)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGlobalEventOnCallback(lua_State* L) +{ + // globalevent:onThink / record / etc. (callback) + GlobalEvent* globalevent = getUserdata(L, 1); + if (globalevent) { + if (!globalevent->loadCallback()) { + pushBoolean(L, false); + return 1; + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGlobalEventTime(lua_State* L) +{ + // globalevent:time(time) + GlobalEvent* globalevent = getUserdata(L, 1); + if (globalevent) { + std::string timer = getString(L, 2); + std::vector params = vectorAtoi(explodeString(timer, ":")); + + int32_t hour = params.front(); + if (hour < 0 || hour > 23) { + std::cout << "[Error - GlobalEvent::configureEvent] Invalid hour \"" << timer << "\" for globalevent with name: " << globalevent->getName() << std::endl; + pushBoolean(L, false); + return 1; + } + + globalevent->setInterval(hour << 16); + + int32_t min = 0; + int32_t sec = 0; + if (params.size() > 1) { + min = params[1]; + if (min < 0 || min > 59) { + std::cout << "[Error - GlobalEvent::configureEvent] Invalid minute \"" << timer << "\" for globalevent with name: " << globalevent->getName() << std::endl; + pushBoolean(L, false); + return 1; + } + + if (params.size() > 2) { + sec = params[2]; + if (sec < 0 || sec > 59) { + std::cout << "[Error - GlobalEvent::configureEvent] Invalid second \"" << timer << "\" for globalevent with name: " << globalevent->getName() << std::endl; + pushBoolean(L, false); + return 1; + } + } + } + + time_t current_time = time(nullptr); + tm* timeinfo = localtime(¤t_time); + timeinfo->tm_hour = hour; + timeinfo->tm_min = min; + timeinfo->tm_sec = sec; + + time_t difference = static_cast(difftime(mktime(timeinfo), current_time)); + if (difference < 0) { + difference += 86400; + } + + globalevent->setNextExecution(current_time + difference); + globalevent->setEventType(GLOBALEVENT_TIMER); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGlobalEventInterval(lua_State* L) +{ + // globalevent:interval(interval) + GlobalEvent* globalevent = getUserdata(L, 1); + if (globalevent) { + globalevent->setInterval(getNumber(L, 2)); + globalevent->setNextExecution(OTSYS_TIME() + getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +// Weapon +int LuaScriptInterface::luaCreateWeapon(lua_State* L) +{ + // Weapon(type) + if (getScriptEnv()->getScriptInterface() != &g_scripts->getScriptInterface()) { + reportErrorFunc("Weapons can only be registered in the Scripts interface."); + lua_pushnil(L); + return 1; + } + + WeaponType_t type = getNumber(L, 2); + switch (type) { + case WEAPON_SWORD: + case WEAPON_AXE: + case WEAPON_CLUB: { + WeaponMelee* weapon = new WeaponMelee(getScriptEnv()->getScriptInterface()); + if (weapon) { + pushUserdata(L, weapon); + setMetatable(L, -1, "Weapon"); + weapon->weaponType = type; + } else { + lua_pushnil(L); + } + break; + } + case WEAPON_DISTANCE: + case WEAPON_AMMO: { + WeaponDistance* weapon = new WeaponDistance(getScriptEnv()->getScriptInterface()); + if (weapon) { + pushUserdata(L, weapon); + setMetatable(L, -1, "Weapon"); + weapon->weaponType = type; + } else { + lua_pushnil(L); + } + break; + } + case WEAPON_WAND: { + WeaponWand* weapon = new WeaponWand(getScriptEnv()->getScriptInterface()); + if (weapon) { + pushUserdata(L, weapon); + setMetatable(L, -1, "Weapon"); + weapon->weaponType = type; + } else { + lua_pushnil(L); + } + break; + } + default: { + lua_pushnil(L); + break; + } + } + return 1; +} + +int LuaScriptInterface::luaWeaponAction(lua_State* L) +{ + // weapon:action(callback) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + std::string typeName = getString(L, 2); + std::string tmpStr = asLowerCaseString(typeName); + if (tmpStr == "removecount") { + weapon->action = WEAPONACTION_REMOVECOUNT; + } else if (tmpStr == "removecharge") { + weapon->action = WEAPONACTION_REMOVECHARGE; + } else if (tmpStr == "move") { + weapon->action = WEAPONACTION_MOVE; + } else { + std::cout << "Error: [Weapon::action] No valid action " << typeName << std::endl; + pushBoolean(L, false); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponRegister(lua_State* L) +{ + // weapon:register() + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + if (weapon->weaponType == WEAPON_DISTANCE || weapon->weaponType == WEAPON_AMMO) { + weapon = getUserdata(L, 1); + } else if (weapon->weaponType == WEAPON_WAND) { + weapon = getUserdata(L, 1); + } else { + weapon = getUserdata(L, 1); + } + + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + it.weaponType = weapon->weaponType; + + if (weapon->getWieldInfo() != 0) { + it.wieldInfo = weapon->getWieldInfo(); + it.vocationString = weapon->getVocationString(); + it.minReqLevel = weapon->getReqLevel(); + it.minReqMagicLevel = weapon->getReqMagLv(); + } + + weapon->configureWeapon(it); + pushBoolean(L, g_weapons->registerLuaEvent(weapon)); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponOnUseWeapon(lua_State* L) +{ + // weapon:onUseWeapon(callback) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + if (!weapon->loadCallback()) { + pushBoolean(L, false); + return 1; + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponUnproperly(lua_State* L) +{ + // weapon:wieldedUnproperly(bool) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + weapon->setWieldUnproperly(getBoolean(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponLevel(lua_State* L) +{ + // weapon:level(lvl) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + weapon->setRequiredLevel(getNumber(L, 2)); + weapon->setWieldInfo(WIELDINFO_LEVEL); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponMagicLevel(lua_State* L) +{ + // weapon:magicLevel(lvl) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + weapon->setRequiredMagLevel(getNumber(L, 2)); + weapon->setWieldInfo(WIELDINFO_MAGLV); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponMana(lua_State* L) +{ + // weapon:mana(mana) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + weapon->setMana(getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponManaPercent(lua_State* L) +{ + // weapon:manaPercent(percent) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + weapon->setManaPercent(getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponHealth(lua_State* L) +{ + // weapon:health(health) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + weapon->setHealth(getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponHealthPercent(lua_State* L) +{ + // weapon:healthPercent(percent) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + weapon->setHealthPercent(getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponSoul(lua_State* L) +{ + // weapon:soul(soul) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + weapon->setSoul(getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponBreakChance(lua_State* L) +{ + // weapon:breakChance(percent) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + weapon->setBreakChance(getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponWandDamage(lua_State* L) +{ + // weapon:damage(damage[min, max]) only use this if the weapon is a wand! + WeaponWand* weapon = getUserdata(L, 1); + if (weapon) { + weapon->setMinChange(getNumber(L, 2)); + if (lua_gettop(L) > 2) { + weapon->setMaxChange(getNumber(L, 3)); + } else { + weapon->setMaxChange(getNumber(L, 2)); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponElement(lua_State* L) +{ + // weapon:element(combatType) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + if (!getNumber(L, 2)) { + std::string element = getString(L, 2); + std::string tmpStrValue = asLowerCaseString(element); + if (tmpStrValue == "earth") { + weapon->params.combatType = COMBAT_EARTHDAMAGE; + } else if (tmpStrValue == "ice") { + weapon->params.combatType = COMBAT_ICEDAMAGE; + } else if (tmpStrValue == "energy") { + weapon->params.combatType = COMBAT_ENERGYDAMAGE; + } else if (tmpStrValue == "fire") { + weapon->params.combatType = COMBAT_FIREDAMAGE; + } else if (tmpStrValue == "death") { + weapon->params.combatType = COMBAT_DEATHDAMAGE; + } else if (tmpStrValue == "holy") { + weapon->params.combatType = COMBAT_HOLYDAMAGE; + } else { + std::cout << "[Warning - weapon:element] Type \"" << element << "\" does not exist." << std::endl; + } + } else { + weapon->params.combatType = getNumber(L, 2); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponPremium(lua_State* L) +{ + // weapon:premium(bool) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + weapon->setNeedPremium(getBoolean(L, 2)); + weapon->setWieldInfo(WIELDINFO_PREMIUM); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponVocation(lua_State* L) +{ + // weapon:vocation(vocName[, showInDescription = false, lastVoc = false]) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + weapon->addVocWeaponMap(getString(L, 2)); + weapon->setWieldInfo(WIELDINFO_VOCREQ); + std::string tmp; + bool showInDescription = getBoolean(L, 3, false); + bool lastVoc = getBoolean(L, 4, false); + + if (showInDescription) { + if (weapon->getVocationString().empty()) { + tmp = asLowerCaseString(getString(L, 2)); + tmp += "s"; + weapon->setVocationString(tmp); + } else { + tmp = weapon->getVocationString(); + if (lastVoc) { + tmp += " and "; + } else { + tmp += ", "; + } + tmp += asLowerCaseString(getString(L, 2)); + tmp += "s"; + weapon->setVocationString(tmp); + } + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponId(lua_State* L) +{ + // weapon:id(id) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + weapon->setID(getNumber(L, 2)); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponAttack(lua_State* L) +{ + // weapon:attack(atk) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + it.attack = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponDefense(lua_State* L) +{ + // weapon:defense(defense[, extraDefense]) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + it.defense = getNumber(L, 2); + if (lua_gettop(L) > 2) { + it.extraDefense = getNumber(L, 3); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponRange(lua_State* L) +{ + // weapon:range(range) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + it.shootRange = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponCharges(lua_State* L) +{ + // weapon:charges(charges[, showCharges = true]) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + bool showCharges = getBoolean(L, 3, true); + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + + it.charges = getNumber(L, 2); + it.showCharges = showCharges; + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponDuration(lua_State* L) +{ + // weapon:duration(duration[, showDuration = true]) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + bool showDuration = getBoolean(L, 3, true); + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + + it.decayTime = getNumber(L, 2); + it.showDuration = showDuration; + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponDecayTo(lua_State* L) +{ + // weapon:decayTo([itemid = 0] + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + uint16_t itemid = getNumber(L, 2, 0); + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + + it.decayTo = itemid; + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponTransformEquipTo(lua_State* L) +{ + // weapon:transformEquipTo(itemid) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + it.transformEquipTo = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponTransformDeEquipTo(lua_State* L) +{ + // weapon:transformDeEquipTo(itemid) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + it.transformDeEquipTo = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponShootType(lua_State* L) +{ + // weapon:shootType(type) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + it.shootType = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponSlotType(lua_State* L) +{ + // weapon:slotType(slot) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + std::string slot = getString(L, 2); + + if (slot == "two-handed") { + it.slotPosition |= SLOTP_TWO_HAND; + } else { + it.slotPosition |= SLOTP_HAND; + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponAmmoType(lua_State* L) +{ + // weapon:ammoType(type) + WeaponDistance* weapon = getUserdata(L, 1); + if (weapon) { + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + std::string type = getString(L, 2); + + if (type == "arrow") { + it.ammoType = AMMO_ARROW; + } else if (type == "bolt"){ + it.ammoType = AMMO_BOLT; + } else { + std::cout << "[Warning - weapon:ammoType] Type \"" << type << "\" does not exist." << std::endl; + lua_pushnil(L); + return 1; + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponHitChance(lua_State* L) +{ + // weapon:hitChance(chance) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + it.hitChance = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponMaxHitChance(lua_State* L) +{ + // weapon:maxHitChance(max) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + it.maxHitChance = getNumber(L, 2); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaWeaponExtraElement(lua_State* L) +{ + // weapon:extraElement(atk, combatType) + Weapon* weapon = getUserdata(L, 1); + if (weapon) { + uint16_t id = weapon->getID(); + ItemType& it = Item::items.getItemType(id); + it.abilities.get()->elementDamage = getNumber(L, 2); + + if (!getNumber(L, 3)) { + std::string element = getString(L, 3); + std::string tmpStrValue = asLowerCaseString(element); + if (tmpStrValue == "earth") { + it.abilities.get()->elementType = COMBAT_EARTHDAMAGE; + } else if (tmpStrValue == "ice") { + it.abilities.get()->elementType = COMBAT_ICEDAMAGE; + } else if (tmpStrValue == "energy") { + it.abilities.get()->elementType = COMBAT_ENERGYDAMAGE; + } else if (tmpStrValue == "fire") { + it.abilities.get()->elementType = COMBAT_FIREDAMAGE; + } else if (tmpStrValue == "death") { + it.abilities.get()->elementType = COMBAT_DEATHDAMAGE; + } else if (tmpStrValue == "holy") { + it.abilities.get()->elementType = COMBAT_HOLYDAMAGE; + } else { + std::cout << "[Warning - weapon:extraElement] Type \"" << element << "\" does not exist." << std::endl; + } + } else { + it.abilities.get()->elementType = getNumber(L, 3); + } + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +// +LuaEnvironment::LuaEnvironment() : LuaScriptInterface("Main Interface") {} + +LuaEnvironment::~LuaEnvironment() +{ + delete testInterface; + closeState(); +} + +bool LuaEnvironment::initState() +{ + luaState = luaL_newstate(); + if (!luaState) { + return false; + } + + luaL_openlibs(luaState); + registerFunctions(); + + runningEventId = EVENT_ID_USER; + return true; +} + +bool LuaEnvironment::reInitState() +{ + // TODO: get children, reload children + closeState(); + return initState(); +} + +bool LuaEnvironment::closeState() +{ + if (!luaState) { + return false; + } + + for (const auto& combatEntry : combatIdMap) { + clearCombatObjects(combatEntry.first); + } + + for (const auto& areaEntry : areaIdMap) { + clearAreaObjects(areaEntry.first); + } + + for (auto& timerEntry : timerEvents) { + LuaTimerEventDesc timerEventDesc = std::move(timerEntry.second); + for (int32_t parameter : timerEventDesc.parameters) { + luaL_unref(luaState, LUA_REGISTRYINDEX, parameter); + } + luaL_unref(luaState, LUA_REGISTRYINDEX, timerEventDesc.function); + } + + combatIdMap.clear(); + areaIdMap.clear(); + timerEvents.clear(); + cacheFiles.clear(); + + lua_close(luaState); + luaState = nullptr; + return true; +} + +LuaScriptInterface* LuaEnvironment::getTestInterface() +{ + if (!testInterface) { + testInterface = new LuaScriptInterface("Test Interface"); + testInterface->initState(); + } + return testInterface; +} + +Combat* LuaEnvironment::getCombatObject(uint32_t id) const +{ + auto it = combatMap.find(id); + if (it == combatMap.end()) { + return nullptr; + } + return it->second; +} + +Combat* LuaEnvironment::createCombatObject(LuaScriptInterface* interface) +{ + Combat* combat = new Combat; + combatMap[++lastCombatId] = combat; + combatIdMap[interface].push_back(lastCombatId); + return combat; +} + +void LuaEnvironment::clearCombatObjects(LuaScriptInterface* interface) +{ + auto it = combatIdMap.find(interface); + if (it == combatIdMap.end()) { + return; + } + + for (uint32_t id : it->second) { + auto itt = combatMap.find(id); + if (itt != combatMap.end()) { + delete itt->second; + combatMap.erase(itt); + } + } + it->second.clear(); +} + +AreaCombat* LuaEnvironment::getAreaObject(uint32_t id) const +{ + auto it = areaMap.find(id); + if (it == areaMap.end()) { + return nullptr; + } + return it->second; +} + +uint32_t LuaEnvironment::createAreaObject(LuaScriptInterface* interface) +{ + areaMap[++lastAreaId] = new AreaCombat; + areaIdMap[interface].push_back(lastAreaId); + return lastAreaId; +} + +void LuaEnvironment::clearAreaObjects(LuaScriptInterface* interface) +{ + auto it = areaIdMap.find(interface); + if (it == areaIdMap.end()) { + return; + } + + for (uint32_t id : it->second) { + auto itt = areaMap.find(id); + if (itt != areaMap.end()) { + delete itt->second; + areaMap.erase(itt); + } + } + it->second.clear(); +} + +void LuaEnvironment::executeTimerEvent(uint32_t eventIndex) +{ + auto it = timerEvents.find(eventIndex); + if (it == timerEvents.end()) { + return; + } + + LuaTimerEventDesc timerEventDesc = std::move(it->second); + timerEvents.erase(it); + + //push function + lua_rawgeti(luaState, LUA_REGISTRYINDEX, timerEventDesc.function); + + //push parameters + for (auto parameter : boost::adaptors::reverse(timerEventDesc.parameters)) { + lua_rawgeti(luaState, LUA_REGISTRYINDEX, parameter); + } + + //call the function + if (reserveScriptEnv()) { + ScriptEnvironment* env = getScriptEnv(); + env->setTimerEvent(); + env->setScriptId(timerEventDesc.scriptId, this); + callFunction(timerEventDesc.parameters.size()); + } else { + std::cout << "[Error - LuaScriptInterface::executeTimerEvent] Call stack overflow" << std::endl; + } + + //free resources + luaL_unref(luaState, LUA_REGISTRYINDEX, timerEventDesc.function); + for (auto parameter : timerEventDesc.parameters) { + luaL_unref(luaState, LUA_REGISTRYINDEX, parameter); + } +} diff --git a/src/luascript.h b/src/luascript.h new file mode 100644 index 0000000..0b8e984 --- /dev/null +++ b/src/luascript.h @@ -0,0 +1,1519 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_LUASCRIPT_H_5344B2BC907E46E3943EA78574A212D8 +#define FS_LUASCRIPT_H_5344B2BC907E46E3943EA78574A212D8 + +#include + +#if LUA_VERSION_NUM >= 502 +#ifndef LUA_COMPAT_ALL +#ifndef LUA_COMPAT_MODULE +#define luaL_register(L, libname, l) (luaL_newlib(L, l), lua_pushvalue(L, -1), lua_setglobal(L, libname)) +#endif +#undef lua_equal +#define lua_equal(L, i1, i2) lua_compare(L, (i1), (i2), LUA_OPEQ) +#endif +#endif + +#include "database.h" +#include "enums.h" +#include "position.h" +#include + +class Thing; +class Creature; +class Player; +class Item; +class Container; +class AreaCombat; +class Combat; +class Condition; +class Npc; +class Monster; +class InstantSpell; + +enum { + EVENT_ID_LOADING = 1, + EVENT_ID_USER = 1000, +}; + +enum LuaVariantType_t { + VARIANT_NONE, + + VARIANT_NUMBER, + VARIANT_POSITION, + VARIANT_TARGETPOSITION, + VARIANT_STRING, +}; + +enum LuaDataType { + LuaData_Unknown, + + LuaData_Item, + LuaData_Container, + LuaData_Teleport, + LuaData_Player, + LuaData_Monster, + LuaData_Npc, + LuaData_Tile, +}; + +struct LuaVariant { + LuaVariantType_t type = VARIANT_NONE; + std::string text; + Position pos; + uint32_t number = 0; +}; + +struct LuaTimerEventDesc { + int32_t scriptId = -1; + int32_t function = -1; + std::list parameters; + uint32_t eventId = 0; + + LuaTimerEventDesc() = default; + LuaTimerEventDesc(LuaTimerEventDesc&& other) = default; +}; + +class LuaScriptInterface; +class Cylinder; +class Game; + +struct LootBlock; + +class ScriptEnvironment +{ + public: + ScriptEnvironment(); + ~ScriptEnvironment(); + + // non-copyable + ScriptEnvironment(const ScriptEnvironment&) = delete; + ScriptEnvironment& operator=(const ScriptEnvironment&) = delete; + + void resetEnv(); + + void setScriptId(int32_t scriptId, LuaScriptInterface* scriptInterface) { + this->scriptId = scriptId; + interface = scriptInterface; + } + bool setCallbackId(int32_t callbackId, LuaScriptInterface* scriptInterface); + + int32_t getScriptId() const { + return scriptId; + } + LuaScriptInterface* getScriptInterface() { + return interface; + } + + void setTimerEvent() { + timerEvent = true; + } + + void getEventInfo(int32_t& scriptId, LuaScriptInterface*& scriptInterface, int32_t& callbackId, bool& timerEvent) const; + + void addTempItem(Item* item); + static void removeTempItem(Item* item); + uint32_t addThing(Thing* thing); + void insertItem(uint32_t uid, Item* item); + + static DBResult_ptr getResultByID(uint32_t id); + static uint32_t addResult(DBResult_ptr res); + static bool removeResult(uint32_t id); + + void setNpc(Npc* npc) { + curNpc = npc; + } + Npc* getNpc() const { + return curNpc; + } + + Thing* getThingByUID(uint32_t uid); + Item* getItemByUID(uint32_t uid); + Container* getContainerByUID(uint32_t uid); + void removeItemByUID(uint32_t uid); + + private: + using VariantVector = std::vector; + using StorageMap = std::map; + using DBResultMap = std::map; + + LuaScriptInterface* interface; + + //for npc scripts + Npc* curNpc = nullptr; + + //temporary item list + static std::multimap tempItems; + + //local item map + std::unordered_map localMap; + uint32_t lastUID = std::numeric_limits::max(); + + //script file id + int32_t scriptId; + int32_t callbackId; + bool timerEvent; + + //result map + static uint32_t lastResultId; + static DBResultMap tempResults; +}; + +#define reportErrorFunc(a) reportError(__FUNCTION__, a, true) + +enum ErrorCode_t { + LUA_ERROR_PLAYER_NOT_FOUND, + LUA_ERROR_CREATURE_NOT_FOUND, + LUA_ERROR_ITEM_NOT_FOUND, + LUA_ERROR_THING_NOT_FOUND, + LUA_ERROR_TILE_NOT_FOUND, + LUA_ERROR_HOUSE_NOT_FOUND, + LUA_ERROR_COMBAT_NOT_FOUND, + LUA_ERROR_CONDITION_NOT_FOUND, + LUA_ERROR_AREA_NOT_FOUND, + LUA_ERROR_CONTAINER_NOT_FOUND, + LUA_ERROR_VARIANT_NOT_FOUND, + LUA_ERROR_VARIANT_UNKNOWN, + LUA_ERROR_SPELL_NOT_FOUND, +}; + +class LuaScriptInterface +{ + public: + explicit LuaScriptInterface(std::string interfaceName); + virtual ~LuaScriptInterface(); + + // non-copyable + LuaScriptInterface(const LuaScriptInterface&) = delete; + LuaScriptInterface& operator=(const LuaScriptInterface&) = delete; + + virtual bool initState(); + bool reInitState(); + + int32_t loadFile(const std::string& file, Npc* npc = nullptr); + + const std::string& getFileById(int32_t scriptId); + int32_t getEvent(const std::string& eventName); + int32_t getEvent(); + int32_t getMetaEvent(const std::string& globalName, const std::string& eventName); + + static ScriptEnvironment* getScriptEnv() { + assert(scriptEnvIndex >= 0 && scriptEnvIndex < 16); + return scriptEnv + scriptEnvIndex; + } + + static bool reserveScriptEnv() { + return ++scriptEnvIndex < 16; + } + + static void resetScriptEnv() { + assert(scriptEnvIndex >= 0); + scriptEnv[scriptEnvIndex--].resetEnv(); + } + + static void reportError(const char* function, const std::string& error_desc, bool stack_trace = false); + + const std::string& getInterfaceName() const { + return interfaceName; + } + const std::string& getLastLuaError() const { + return lastLuaError; + } + + lua_State* getLuaState() const { + return luaState; + } + + bool pushFunction(int32_t functionId); + + static int luaErrorHandler(lua_State* L); + bool callFunction(int params); + void callVoidFunction(int params); + + //push/pop common structures + static void pushThing(lua_State* L, Thing* thing); + static void pushVariant(lua_State* L, const LuaVariant& var); + static void pushString(lua_State* L, const std::string& value); + static void pushCallback(lua_State* L, int32_t callback); + static void pushCylinder(lua_State* L, Cylinder* cylinder); + + static std::string popString(lua_State* L); + static int32_t popCallback(lua_State* L); + + // Userdata + template + static void pushUserdata(lua_State* L, T* value) + { + T** userdata = static_cast(lua_newuserdata(L, sizeof(T*))); + *userdata = value; + } + + // Metatables + static void setMetatable(lua_State* L, int32_t index, const std::string& name); + static void setWeakMetatable(lua_State* L, int32_t index, const std::string& name); + + static void setItemMetatable(lua_State* L, int32_t index, const Item* item); + static void setCreatureMetatable(lua_State* L, int32_t index, const Creature* creature); + + // Get + template + static typename std::enable_if::value, T>::type + getNumber(lua_State* L, int32_t arg) + { + return static_cast(static_cast(lua_tonumber(L, arg))); + } + template + static typename std::enable_if::value || std::is_floating_point::value, T>::type + getNumber(lua_State* L, int32_t arg) + { + return static_cast(lua_tonumber(L, arg)); + } + template + static T getNumber(lua_State *L, int32_t arg, T defaultValue) + { + const auto parameters = lua_gettop(L); + if (parameters == 0 || arg > parameters) { + return defaultValue; + } + return getNumber(L, arg); + } + template + static T* getUserdata(lua_State* L, int32_t arg) + { + T** userdata = getRawUserdata(L, arg); + if (!userdata) { + return nullptr; + } + return *userdata; + } + template + static T** getRawUserdata(lua_State* L, int32_t arg) + { + return static_cast(lua_touserdata(L, arg)); + } + + static bool getBoolean(lua_State* L, int32_t arg) + { + return lua_toboolean(L, arg) != 0; + } + static bool getBoolean(lua_State* L, int32_t arg, bool defaultValue) + { + const auto parameters = lua_gettop(L); + if (parameters == 0 || arg > parameters) { + return defaultValue; + } + return lua_toboolean(L, arg) != 0; + } + + static std::string getString(lua_State* L, int32_t arg); + static Position getPosition(lua_State* L, int32_t arg, int32_t& stackpos); + static Position getPosition(lua_State* L, int32_t arg); + static Outfit_t getOutfit(lua_State* L, int32_t arg); + static LuaVariant getVariant(lua_State* L, int32_t arg); + static InstantSpell* getInstantSpell(lua_State* L, int32_t arg); + + static Thing* getThing(lua_State* L, int32_t arg); + static Creature* getCreature(lua_State* L, int32_t arg); + static Player* getPlayer(lua_State* L, int32_t arg); + + template + static T getField(lua_State* L, int32_t arg, const std::string& key) + { + lua_getfield(L, arg, key.c_str()); + return getNumber(L, -1); + } + + static std::string getFieldString(lua_State* L, int32_t arg, const std::string& key); + + static LuaDataType getUserdataType(lua_State* L, int32_t arg); + + // Is + static bool isNumber(lua_State* L, int32_t arg) + { + return lua_type(L, arg) == LUA_TNUMBER; + } + static bool isString(lua_State* L, int32_t arg) + { + return lua_isstring(L, arg) != 0; + } + static bool isBoolean(lua_State* L, int32_t arg) + { + return lua_isboolean(L, arg); + } + static bool isTable(lua_State* L, int32_t arg) + { + return lua_istable(L, arg); + } + static bool isFunction(lua_State* L, int32_t arg) + { + return lua_isfunction(L, arg); + } + static bool isUserdata(lua_State* L, int32_t arg) + { + return lua_isuserdata(L, arg) != 0; + } + + // Push + static void pushBoolean(lua_State* L, bool value); + static void pushCombatDamage(lua_State* L, const CombatDamage& damage); + static void pushInstantSpell(lua_State* L, const InstantSpell& spell); + static void pushPosition(lua_State* L, const Position& position, int32_t stackpos = 0); + static void pushOutfit(lua_State* L, const Outfit_t& outfit); + static void pushLoot(lua_State* L, const std::vector& lootList); + + // + static void setField(lua_State* L, const char* index, lua_Number value) + { + lua_pushnumber(L, value); + lua_setfield(L, -2, index); + } + + static void setField(lua_State* L, const char* index, const std::string& value) + { + pushString(L, value); + lua_setfield(L, -2, index); + } + + static std::string escapeString(const std::string& string); + +#ifndef LUAJIT_VERSION + static const luaL_Reg luaBitReg[7]; +#endif + static const luaL_Reg luaConfigManagerTable[4]; + static const luaL_Reg luaDatabaseTable[9]; + static const luaL_Reg luaResultTable[6]; + + static int protectedCall(lua_State* L, int nargs, int nresults); + + protected: + virtual bool closeState(); + + void registerFunctions(); + + void registerMethod(const std::string& globalName, const std::string& methodName, lua_CFunction func); + + static std::string getErrorDesc(ErrorCode_t code); + + lua_State* luaState = nullptr; + + int32_t eventTableRef = -1; + int32_t runningEventId = EVENT_ID_USER; + + //script file cache + std::map cacheFiles; + + private: + void registerClass(const std::string& className, const std::string& baseClass, lua_CFunction newFunction = nullptr); + void registerTable(const std::string& tableName); + void registerMetaMethod(const std::string& className, const std::string& methodName, lua_CFunction func); + void registerGlobalMethod(const std::string& functionName, lua_CFunction func); + void registerVariable(const std::string& tableName, const std::string& name, lua_Number value); + void registerGlobalVariable(const std::string& name, lua_Number value); + void registerGlobalBoolean(const std::string& name, bool value); + + std::string getStackTrace(const std::string& error_desc); + + static bool getArea(lua_State* L, std::list& list, uint32_t& rows); + + //lua functions + static int luaDoPlayerAddItem(lua_State* L); + static int luaDoSetCreatureLight(lua_State* L); + + //get item info + static int luaGetDepotId(lua_State* L); + + //get world info + static int luaGetWorldTime(lua_State* L); + static int luaGetWorldLight(lua_State* L); + static int luaGetWorldUpTime(lua_State* L); + + //type validation + static int luaIsDepot(lua_State* L); + static int luaIsMoveable(lua_State* L); + static int luaIsValidUID(lua_State* L); + + //container + static int luaDoAddContainerItem(lua_State* L); + + // + static int luaCreateCombatArea(lua_State* L); + + static int luaDoAreaCombat(lua_State* L); + static int luaDoTargetCombat(lua_State* L); + + static int luaDoChallengeCreature(lua_State* L); + + static int luaDebugPrint(lua_State* L); + static int luaAddEvent(lua_State* L); + static int luaStopEvent(lua_State* L); + + static int luaSaveServer(lua_State* L); + static int luaCleanMap(lua_State* L); + + static int luaIsInWar(lua_State* L); + + static int luaGetWaypointPositionByName(lua_State* L); + + static int luaSendChannelMessage(lua_State* L); + static int luaSendGuildChannelMessage(lua_State* L); + +#ifndef LUAJIT_VERSION + static int luaBitNot(lua_State* L); + static int luaBitAnd(lua_State* L); + static int luaBitOr(lua_State* L); + static int luaBitXor(lua_State* L); + static int luaBitLeftShift(lua_State* L); + static int luaBitRightShift(lua_State* L); +#endif + + static int luaConfigManagerGetString(lua_State* L); + static int luaConfigManagerGetNumber(lua_State* L); + static int luaConfigManagerGetBoolean(lua_State* L); + + static int luaDatabaseExecute(lua_State* L); + static int luaDatabaseAsyncExecute(lua_State* L); + static int luaDatabaseStoreQuery(lua_State* L); + static int luaDatabaseAsyncStoreQuery(lua_State* L); + static int luaDatabaseEscapeString(lua_State* L); + static int luaDatabaseEscapeBlob(lua_State* L); + static int luaDatabaseLastInsertId(lua_State* L); + static int luaDatabaseTableExists(lua_State* L); + + static int luaResultGetNumber(lua_State* L); + static int luaResultGetString(lua_State* L); + static int luaResultGetStream(lua_State* L); + static int luaResultNext(lua_State* L); + static int luaResultFree(lua_State* L); + + // Userdata + static int luaUserdataCompare(lua_State* L); + + // _G + static int luaIsType(lua_State* L); + static int luaRawGetMetatable(lua_State* L); + + // os + static int luaSystemTime(lua_State* L); + + // table + static int luaTableCreate(lua_State* L); + + // Game + static int luaGameGetSpectators(lua_State* L); + static int luaGameGetPlayers(lua_State* L); + static int luaGameLoadMap(lua_State* L); + + static int luaGameGetExperienceStage(lua_State* L); + static int luaGameGetMonsterCount(lua_State* L); + static int luaGameGetPlayerCount(lua_State* L); + static int luaGameGetNpcCount(lua_State* L); + static int luaGameGetMonsterTypes(lua_State* L); + + static int luaGameGetTowns(lua_State* L); + static int luaGameGetHouses(lua_State* L); + + static int luaGameGetGameState(lua_State* L); + static int luaGameSetGameState(lua_State* L); + + static int luaGameGetWorldType(lua_State* L); + static int luaGameSetWorldType(lua_State* L); + + static int luaGameGetReturnMessage(lua_State* L); + + static int luaGameCreateItem(lua_State* L); + static int luaGameCreateContainer(lua_State* L); + static int luaGameCreateMonster(lua_State* L); + static int luaGameCreateNpc(lua_State* L); + static int luaGameCreateTile(lua_State* L); + static int luaGameCreateMonsterType(lua_State* L); + + static int luaGameStartRaid(lua_State* L); + + static int luaGameGetClientVersion(lua_State* L); + + static int luaGameReload(lua_State* L); + + // Variant + static int luaVariantCreate(lua_State* L); + + static int luaVariantGetNumber(lua_State* L); + static int luaVariantGetString(lua_State* L); + static int luaVariantGetPosition(lua_State* L); + + // Position + static int luaPositionCreate(lua_State* L); + static int luaPositionAdd(lua_State* L); + static int luaPositionSub(lua_State* L); + static int luaPositionCompare(lua_State* L); + + static int luaPositionGetDistance(lua_State* L); + static int luaPositionIsSightClear(lua_State* L); + + static int luaPositionSendMagicEffect(lua_State* L); + static int luaPositionSendDistanceEffect(lua_State* L); + + // Tile + static int luaTileCreate(lua_State* L); + + static int luaTileGetPosition(lua_State* L); + static int luaTileGetGround(lua_State* L); + static int luaTileGetThing(lua_State* L); + static int luaTileGetThingCount(lua_State* L); + static int luaTileGetTopVisibleThing(lua_State* L); + + static int luaTileGetTopTopItem(lua_State* L); + static int luaTileGetTopDownItem(lua_State* L); + static int luaTileGetFieldItem(lua_State* L); + + static int luaTileGetItemById(lua_State* L); + static int luaTileGetItemByType(lua_State* L); + static int luaTileGetItemByTopOrder(lua_State* L); + static int luaTileGetItemCountById(lua_State* L); + + static int luaTileGetBottomCreature(lua_State* L); + static int luaTileGetTopCreature(lua_State* L); + static int luaTileGetBottomVisibleCreature(lua_State* L); + static int luaTileGetTopVisibleCreature(lua_State* L); + + static int luaTileGetItems(lua_State* L); + static int luaTileGetItemCount(lua_State* L); + static int luaTileGetDownItemCount(lua_State* L); + static int luaTileGetTopItemCount(lua_State* L); + + static int luaTileGetCreatures(lua_State* L); + static int luaTileGetCreatureCount(lua_State* L); + + static int luaTileHasProperty(lua_State* L); + static int luaTileHasFlag(lua_State* L); + + static int luaTileGetThingIndex(lua_State* L); + + static int luaTileQueryAdd(lua_State* L); + static int luaTileAddItem(lua_State* L); + static int luaTileAddItemEx(lua_State* L); + + static int luaTileGetHouse(lua_State* L); + + // NetworkMessage + static int luaNetworkMessageCreate(lua_State* L); + static int luaNetworkMessageDelete(lua_State* L); + + static int luaNetworkMessageGetByte(lua_State* L); + static int luaNetworkMessageGetU16(lua_State* L); + static int luaNetworkMessageGetU32(lua_State* L); + static int luaNetworkMessageGetU64(lua_State* L); + static int luaNetworkMessageGetString(lua_State* L); + static int luaNetworkMessageGetPosition(lua_State* L); + + static int luaNetworkMessageAddByte(lua_State* L); + static int luaNetworkMessageAddU16(lua_State* L); + static int luaNetworkMessageAddU32(lua_State* L); + static int luaNetworkMessageAddU64(lua_State* L); + static int luaNetworkMessageAddString(lua_State* L); + static int luaNetworkMessageAddPosition(lua_State* L); + static int luaNetworkMessageAddDouble(lua_State* L); + static int luaNetworkMessageAddItem(lua_State* L); + static int luaNetworkMessageAddItemId(lua_State* L); + + static int luaNetworkMessageReset(lua_State* L); + static int luaNetworkMessageSkipBytes(lua_State* L); + static int luaNetworkMessageSendToPlayer(lua_State* L); + + // ModalWindow + static int luaModalWindowCreate(lua_State* L); + static int luaModalWindowDelete(lua_State* L); + + static int luaModalWindowGetId(lua_State* L); + static int luaModalWindowGetTitle(lua_State* L); + static int luaModalWindowGetMessage(lua_State* L); + + static int luaModalWindowSetTitle(lua_State* L); + static int luaModalWindowSetMessage(lua_State* L); + + static int luaModalWindowGetButtonCount(lua_State* L); + static int luaModalWindowGetChoiceCount(lua_State* L); + + static int luaModalWindowAddButton(lua_State* L); + static int luaModalWindowAddChoice(lua_State* L); + + static int luaModalWindowGetDefaultEnterButton(lua_State* L); + static int luaModalWindowSetDefaultEnterButton(lua_State* L); + + static int luaModalWindowGetDefaultEscapeButton(lua_State* L); + static int luaModalWindowSetDefaultEscapeButton(lua_State* L); + + static int luaModalWindowHasPriority(lua_State* L); + static int luaModalWindowSetPriority(lua_State* L); + + static int luaModalWindowSendToPlayer(lua_State* L); + + // Item + static int luaItemCreate(lua_State* L); + + static int luaItemIsItem(lua_State* L); + + static int luaItemGetParent(lua_State* L); + static int luaItemGetTopParent(lua_State* L); + + static int luaItemGetId(lua_State* L); + + static int luaItemClone(lua_State* L); + static int luaItemSplit(lua_State* L); + static int luaItemRemove(lua_State* L); + + static int luaItemGetUniqueId(lua_State* L); + static int luaItemGetActionId(lua_State* L); + static int luaItemSetActionId(lua_State* L); + + static int luaItemGetCount(lua_State* L); + static int luaItemGetCharges(lua_State* L); + static int luaItemGetFluidType(lua_State* L); + static int luaItemGetWeight(lua_State* L); + + static int luaItemGetSubType(lua_State* L); + + static int luaItemGetName(lua_State* L); + static int luaItemGetPluralName(lua_State* L); + static int luaItemGetArticle(lua_State* L); + + static int luaItemGetPosition(lua_State* L); + static int luaItemGetTile(lua_State* L); + + static int luaItemHasAttribute(lua_State* L); + static int luaItemGetAttribute(lua_State* L); + static int luaItemSetAttribute(lua_State* L); + static int luaItemRemoveAttribute(lua_State* L); + static int luaItemGetCustomAttribute(lua_State* L); + static int luaItemSetCustomAttribute(lua_State* L); + static int luaItemRemoveCustomAttribute(lua_State* L); + + static int luaItemMoveTo(lua_State* L); + static int luaItemTransform(lua_State* L); + static int luaItemDecay(lua_State* L); + + static int luaItemGetDescription(lua_State* L); + + static int luaItemHasProperty(lua_State* L); + static int luaItemIsLoadedFromMap(lua_State* L); + + // Container + static int luaContainerCreate(lua_State* L); + + static int luaContainerGetSize(lua_State* L); + static int luaContainerGetCapacity(lua_State* L); + static int luaContainerGetEmptySlots(lua_State* L); + static int luaContainerGetContentDescription(lua_State* L); + static int luaContainerGetItemHoldingCount(lua_State* L); + static int luaContainerGetItemCountById(lua_State* L); + + static int luaContainerGetItem(lua_State* L); + static int luaContainerHasItem(lua_State* L); + static int luaContainerAddItem(lua_State* L); + static int luaContainerAddItemEx(lua_State* L); + static int luaContainerGetCorpseOwner(lua_State* L); + + // Teleport + static int luaTeleportCreate(lua_State* L); + + static int luaTeleportGetDestination(lua_State* L); + static int luaTeleportSetDestination(lua_State* L); + + // Creature + static int luaCreatureCreate(lua_State* L); + + static int luaCreatureGetEvents(lua_State* L); + static int luaCreatureRegisterEvent(lua_State* L); + static int luaCreatureUnregisterEvent(lua_State* L); + + static int luaCreatureIsRemoved(lua_State* L); + static int luaCreatureIsCreature(lua_State* L); + static int luaCreatureIsInGhostMode(lua_State* L); + static int luaCreatureIsHealthHidden(lua_State* L); + static int luaCreatureIsImmune(lua_State* L); + + static int luaCreatureCanSee(lua_State* L); + static int luaCreatureCanSeeCreature(lua_State* L); + + static int luaCreatureGetParent(lua_State* L); + + static int luaCreatureGetId(lua_State* L); + static int luaCreatureGetName(lua_State* L); + + static int luaCreatureGetTarget(lua_State* L); + static int luaCreatureSetTarget(lua_State* L); + + static int luaCreatureGetFollowCreature(lua_State* L); + static int luaCreatureSetFollowCreature(lua_State* L); + + static int luaCreatureGetMaster(lua_State* L); + static int luaCreatureSetMaster(lua_State* L); + + static int luaCreatureGetLight(lua_State* L); + static int luaCreatureSetLight(lua_State* L); + + static int luaCreatureGetSpeed(lua_State* L); + static int luaCreatureGetBaseSpeed(lua_State* L); + static int luaCreatureChangeSpeed(lua_State* L); + + static int luaCreatureSetDropLoot(lua_State* L); + static int luaCreatureSetSkillLoss(lua_State* L); + + static int luaCreatureGetPosition(lua_State* L); + static int luaCreatureGetTile(lua_State* L); + static int luaCreatureGetDirection(lua_State* L); + static int luaCreatureSetDirection(lua_State* L); + + static int luaCreatureGetHealth(lua_State* L); + static int luaCreatureSetHealth(lua_State* L); + static int luaCreatureAddHealth(lua_State* L); + static int luaCreatureGetMaxHealth(lua_State* L); + static int luaCreatureSetMaxHealth(lua_State* L); + static int luaCreatureSetHiddenHealth(lua_State* L); + + static int luaCreatureGetSkull(lua_State* L); + static int luaCreatureSetSkull(lua_State* L); + + static int luaCreatureGetOutfit(lua_State* L); + static int luaCreatureSetOutfit(lua_State* L); + + static int luaCreatureGetCondition(lua_State* L); + static int luaCreatureAddCondition(lua_State* L); + static int luaCreatureRemoveCondition(lua_State* L); + static int luaCreatureHasCondition(lua_State* L); + + static int luaCreatureRemove(lua_State* L); + static int luaCreatureTeleportTo(lua_State* L); + static int luaCreatureSay(lua_State* L); + + static int luaCreatureGetDamageMap(lua_State* L); + + static int luaCreatureGetSummons(lua_State* L); + + static int luaCreatureGetDescription(lua_State* L); + + static int luaCreatureGetPathTo(lua_State* L); + static int luaCreatureMove(lua_State* L); + + static int luaCreatureGetZone(lua_State* L); + + // Player + static int luaPlayerCreate(lua_State* L); + + static int luaPlayerIsPlayer(lua_State* L); + + static int luaPlayerGetGuid(lua_State* L); + static int luaPlayerGetIp(lua_State* L); + static int luaPlayerGetAccountId(lua_State* L); + static int luaPlayerGetLastLoginSaved(lua_State* L); + static int luaPlayerGetLastLogout(lua_State* L); + + static int luaPlayerGetAccountType(lua_State* L); + static int luaPlayerSetAccountType(lua_State* L); + + static int luaPlayerGetCapacity(lua_State* L); + static int luaPlayerSetCapacity(lua_State* L); + + static int luaPlayerGetFreeCapacity(lua_State* L); + + static int luaPlayerGetDepotChest(lua_State* L); + static int luaPlayerGetInbox(lua_State* L); + + static int luaPlayerGetSkullTime(lua_State* L); + static int luaPlayerSetSkullTime(lua_State* L); + static int luaPlayerGetDeathPenalty(lua_State* L); + + static int luaPlayerGetExperience(lua_State* L); + static int luaPlayerAddExperience(lua_State* L); + static int luaPlayerRemoveExperience(lua_State* L); + static int luaPlayerGetLevel(lua_State* L); + + static int luaPlayerGetMagicLevel(lua_State* L); + static int luaPlayerGetBaseMagicLevel(lua_State* L); + static int luaPlayerGetMana(lua_State* L); + static int luaPlayerAddMana(lua_State* L); + static int luaPlayerGetMaxMana(lua_State* L); + static int luaPlayerSetMaxMana(lua_State* L); + static int luaPlayerGetManaSpent(lua_State* L); + static int luaPlayerAddManaSpent(lua_State* L); + + static int luaPlayerGetBaseMaxHealth(lua_State* L); + static int luaPlayerGetBaseMaxMana(lua_State* L); + + static int luaPlayerGetSkillLevel(lua_State* L); + static int luaPlayerGetEffectiveSkillLevel(lua_State* L); + static int luaPlayerGetSkillPercent(lua_State* L); + static int luaPlayerGetSkillTries(lua_State* L); + static int luaPlayerAddSkillTries(lua_State* L); + static int luaPlayerGetSpecialSkill(lua_State* L); + static int luaPlayerAddSpecialSkill(lua_State* L); + + static int luaPlayerAddOfflineTrainingTime(lua_State* L); + static int luaPlayerGetOfflineTrainingTime(lua_State* L); + static int luaPlayerRemoveOfflineTrainingTime(lua_State* L); + + static int luaPlayerAddOfflineTrainingTries(lua_State* L); + + static int luaPlayerGetOfflineTrainingSkill(lua_State* L); + static int luaPlayerSetOfflineTrainingSkill(lua_State* L); + + static int luaPlayerGetItemCount(lua_State* L); + static int luaPlayerGetItemById(lua_State* L); + + static int luaPlayerGetVocation(lua_State* L); + static int luaPlayerSetVocation(lua_State* L); + + static int luaPlayerGetSex(lua_State* L); + static int luaPlayerSetSex(lua_State* L); + + static int luaPlayerGetTown(lua_State* L); + static int luaPlayerSetTown(lua_State* L); + + static int luaPlayerGetGuild(lua_State* L); + static int luaPlayerSetGuild(lua_State* L); + + static int luaPlayerGetGuildLevel(lua_State* L); + static int luaPlayerSetGuildLevel(lua_State* L); + + static int luaPlayerGetGuildNick(lua_State* L); + static int luaPlayerSetGuildNick(lua_State* L); + + static int luaPlayerGetGroup(lua_State* L); + static int luaPlayerSetGroup(lua_State* L); + + static int luaPlayerGetStamina(lua_State* L); + static int luaPlayerSetStamina(lua_State* L); + + static int luaPlayerGetSoul(lua_State* L); + static int luaPlayerAddSoul(lua_State* L); + static int luaPlayerGetMaxSoul(lua_State* L); + + static int luaPlayerGetBankBalance(lua_State* L); + static int luaPlayerSetBankBalance(lua_State* L); + + static int luaPlayerGetStorageValue(lua_State* L); + static int luaPlayerSetStorageValue(lua_State* L); + + static int luaPlayerAddItem(lua_State* L); + static int luaPlayerAddItemEx(lua_State* L); + static int luaPlayerRemoveItem(lua_State* L); + + static int luaPlayerGetMoney(lua_State* L); + static int luaPlayerAddMoney(lua_State* L); + static int luaPlayerRemoveMoney(lua_State* L); + + static int luaPlayerShowTextDialog(lua_State* L); + + static int luaPlayerSendTextMessage(lua_State* L); + static int luaPlayerSendChannelMessage(lua_State* L); + static int luaPlayerSendPrivateMessage(lua_State* L); + + static int luaPlayerChannelSay(lua_State* L); + static int luaPlayerOpenChannel(lua_State* L); + + static int luaPlayerGetSlotItem(lua_State* L); + + static int luaPlayerGetParty(lua_State* L); + + static int luaPlayerAddOutfit(lua_State* L); + static int luaPlayerAddOutfitAddon(lua_State* L); + static int luaPlayerRemoveOutfit(lua_State* L); + static int luaPlayerRemoveOutfitAddon(lua_State* L); + static int luaPlayerHasOutfit(lua_State* L); + static int luaPlayerSendOutfitWindow(lua_State* L); + + static int luaPlayerAddMount(lua_State* L); + static int luaPlayerRemoveMount(lua_State* L); + static int luaPlayerHasMount(lua_State* L); + + static int luaPlayerGetPremiumDays(lua_State* L); + static int luaPlayerAddPremiumDays(lua_State* L); + static int luaPlayerRemovePremiumDays(lua_State* L); + + static int luaPlayerHasBlessing(lua_State* L); + static int luaPlayerAddBlessing(lua_State* L); + static int luaPlayerRemoveBlessing(lua_State* L); + + static int luaPlayerCanLearnSpell(lua_State* L); + static int luaPlayerLearnSpell(lua_State* L); + static int luaPlayerForgetSpell(lua_State* L); + static int luaPlayerHasLearnedSpell(lua_State* L); + + static int luaPlayerSendTutorial(lua_State* L); + static int luaPlayerAddMapMark(lua_State* L); + + static int luaPlayerSave(lua_State* L); + static int luaPlayerPopupFYI(lua_State* L); + + static int luaPlayerIsPzLocked(lua_State* L); + + static int luaPlayerGetClient(lua_State* L); + + static int luaPlayerGetHouse(lua_State* L); + static int luaPlayerSendHouseWindow(lua_State* L); + static int luaPlayerSetEditHouse(lua_State* L); + + static int luaPlayerSetGhostMode(lua_State* L); + + static int luaPlayerGetContainerId(lua_State* L); + static int luaPlayerGetContainerById(lua_State* L); + static int luaPlayerGetContainerIndex(lua_State* L); + + static int32_t luaPlayerStartLiveCast(lua_State* L); + static int32_t luaPlayerStopLiveCast(lua_State* L); + static int32_t luaPlayerIsLiveCaster(lua_State* L); + + static int luaPlayerGetInstantSpells(lua_State* L); + static int luaPlayerCanCast(lua_State* L); + + static int luaPlayerHasChaseMode(lua_State* L); + static int luaPlayerHasSecureMode(lua_State* L); + static int luaPlayerGetFightMode(lua_State* L); + + // Monster + static int luaMonsterCreate(lua_State* L); + + static int luaMonsterIsMonster(lua_State* L); + + static int luaMonsterGetType(lua_State* L); + + static int luaMonsterGetSpawnPosition(lua_State* L); + static int luaMonsterIsInSpawnRange(lua_State* L); + + static int luaMonsterIsIdle(lua_State* L); + static int luaMonsterSetIdle(lua_State* L); + + static int luaMonsterIsTarget(lua_State* L); + static int luaMonsterIsOpponent(lua_State* L); + static int luaMonsterIsFriend(lua_State* L); + + static int luaMonsterAddFriend(lua_State* L); + static int luaMonsterRemoveFriend(lua_State* L); + static int luaMonsterGetFriendList(lua_State* L); + static int luaMonsterGetFriendCount(lua_State* L); + + static int luaMonsterAddTarget(lua_State* L); + static int luaMonsterRemoveTarget(lua_State* L); + static int luaMonsterGetTargetList(lua_State* L); + static int luaMonsterGetTargetCount(lua_State* L); + + static int luaMonsterSelectTarget(lua_State* L); + static int luaMonsterSearchTarget(lua_State* L); + + // Npc + static int luaNpcCreate(lua_State* L); + + static int luaNpcIsNpc(lua_State* L); + + static int luaNpcSetMasterPos(lua_State* L); + + static int luaNpcGetSpeechBubble(lua_State* L); + static int luaNpcSetSpeechBubble(lua_State* L); + + // Guild + static int luaGuildCreate(lua_State* L); + + static int luaGuildGetId(lua_State* L); + static int luaGuildGetName(lua_State* L); + static int luaGuildGetMembersOnline(lua_State* L); + + static int luaGuildAddRank(lua_State* L); + static int luaGuildGetRankById(lua_State* L); + static int luaGuildGetRankByLevel(lua_State* L); + + static int luaGuildGetMotd(lua_State* L); + static int luaGuildSetMotd(lua_State* L); + + // Group + static int luaGroupCreate(lua_State* L); + + static int luaGroupGetId(lua_State* L); + static int luaGroupGetName(lua_State* L); + static int luaGroupGetFlags(lua_State* L); + static int luaGroupGetAccess(lua_State* L); + static int luaGroupGetMaxDepotItems(lua_State* L); + static int luaGroupGetMaxVipEntries(lua_State* L); + static int luaGroupHasFlag(lua_State* L); + + // Vocation + static int luaVocationCreate(lua_State* L); + + static int luaVocationGetId(lua_State* L); + static int luaVocationGetClientId(lua_State* L); + static int luaVocationGetName(lua_State* L); + static int luaVocationGetDescription(lua_State* L); + + static int luaVocationGetRequiredSkillTries(lua_State* L); + static int luaVocationGetRequiredManaSpent(lua_State* L); + + static int luaVocationGetCapacityGain(lua_State* L); + + static int luaVocationGetHealthGain(lua_State* L); + static int luaVocationGetHealthGainTicks(lua_State* L); + static int luaVocationGetHealthGainAmount(lua_State* L); + + static int luaVocationGetManaGain(lua_State* L); + static int luaVocationGetManaGainTicks(lua_State* L); + static int luaVocationGetManaGainAmount(lua_State* L); + + static int luaVocationGetMaxSoul(lua_State* L); + static int luaVocationGetSoulGainTicks(lua_State* L); + + static int luaVocationGetAttackSpeed(lua_State* L); + static int luaVocationGetBaseSpeed(lua_State* L); + + static int luaVocationGetDemotion(lua_State* L); + static int luaVocationGetPromotion(lua_State* L); + + // Town + static int luaTownCreate(lua_State* L); + + static int luaTownGetId(lua_State* L); + static int luaTownGetName(lua_State* L); + static int luaTownGetTemplePosition(lua_State* L); + + // House + static int luaHouseCreate(lua_State* L); + + static int luaHouseGetId(lua_State* L); + static int luaHouseGetName(lua_State* L); + static int luaHouseGetTown(lua_State* L); + static int luaHouseGetExitPosition(lua_State* L); + static int luaHouseGetRent(lua_State* L); + + static int luaHouseGetOwnerGuid(lua_State* L); + static int luaHouseSetOwnerGuid(lua_State* L); + static int luaHouseStartTrade(lua_State* L); + + static int luaHouseGetBeds(lua_State* L); + static int luaHouseGetBedCount(lua_State* L); + + static int luaHouseGetDoors(lua_State* L); + static int luaHouseGetDoorCount(lua_State* L); + static int luaHouseGetDoorIdByPosition(lua_State* L); + + static int luaHouseGetTiles(lua_State* L); + static int luaHouseGetItems(lua_State* L); + static int luaHouseGetTileCount(lua_State* L); + + static int luaHouseCanEditAccessList(lua_State* L); + static int luaHouseGetAccessList(lua_State* L); + static int luaHouseSetAccessList(lua_State* L); + + static int luaHouseKickPlayer(lua_State* L); + + // ItemType + static int luaItemTypeCreate(lua_State* L); + + static int luaItemTypeIsCorpse(lua_State* L); + static int luaItemTypeIsDoor(lua_State* L); + static int luaItemTypeIsContainer(lua_State* L); + static int luaItemTypeIsFluidContainer(lua_State* L); + static int luaItemTypeIsMovable(lua_State* L); + static int luaItemTypeIsRune(lua_State* L); + static int luaItemTypeIsStackable(lua_State* L); + static int luaItemTypeIsReadable(lua_State* L); + static int luaItemTypeIsWritable(lua_State* L); + static int luaItemTypeIsBlocking(lua_State* L); + static int luaItemTypeIsGroundTile(lua_State* L); + static int luaItemTypeIsMagicField(lua_State* L); + static int luaItemTypeIsUseable(lua_State* L); + static int luaItemTypeIsPickupable(lua_State* L); + + static int luaItemTypeGetType(lua_State* L); + static int luaItemTypeGetId(lua_State* L); + static int luaItemTypeGetClientId(lua_State* L); + static int luaItemTypeGetName(lua_State* L); + static int luaItemTypeGetPluralName(lua_State* L); + static int luaItemTypeGetArticle(lua_State* L); + static int luaItemTypeGetDescription(lua_State* L); + static int luaItemTypeGetSlotPosition(lua_State *L); + + static int luaItemTypeGetCharges(lua_State* L); + static int luaItemTypeGetFluidSource(lua_State* L); + static int luaItemTypeGetCapacity(lua_State* L); + static int luaItemTypeGetWeight(lua_State* L); + + static int luaItemTypeGetHitChance(lua_State* L); + static int luaItemTypeGetShootRange(lua_State* L); + static int luaItemTypeGetAttack(lua_State* L); + static int luaItemTypeGetDefense(lua_State* L); + static int luaItemTypeGetExtraDefense(lua_State* L); + static int luaItemTypeGetArmor(lua_State* L); + static int luaItemTypeGetWeaponType(lua_State* L); + + static int luaItemTypeGetElementType(lua_State* L); + static int luaItemTypeGetElementDamage(lua_State* L); + + static int luaItemTypeGetTransformEquipId(lua_State* L); + static int luaItemTypeGetTransformDeEquipId(lua_State* L); + static int luaItemTypeGetDestroyId(lua_State* L); + static int luaItemTypeGetDecayId(lua_State* L); + static int luaItemTypeGetRequiredLevel(lua_State* L); + static int luaItemTypeGetAmmoType(lua_State* L); + static int luaItemTypeGetCorpseType(lua_State* L); + + static int luaItemTypeHasSubType(lua_State* L); + + // Combat + static int luaCombatCreate(lua_State* L); + + static int luaCombatSetParameter(lua_State* L); + static int luaCombatSetFormula(lua_State* L); + + static int luaCombatSetArea(lua_State* L); + static int luaCombatAddCondition(lua_State* L); + static int luaCombatClearConditions(lua_State* L); + static int luaCombatSetCallback(lua_State* L); + static int luaCombatSetOrigin(lua_State* L); + + static int luaCombatExecute(lua_State* L); + + // Condition + static int luaConditionCreate(lua_State* L); + static int luaConditionDelete(lua_State* L); + + static int luaConditionGetId(lua_State* L); + static int luaConditionGetSubId(lua_State* L); + static int luaConditionGetType(lua_State* L); + static int luaConditionGetIcons(lua_State* L); + static int luaConditionGetEndTime(lua_State* L); + + static int luaConditionClone(lua_State* L); + + static int luaConditionGetTicks(lua_State* L); + static int luaConditionSetTicks(lua_State* L); + + static int luaConditionSetParameter(lua_State* L); + static int luaConditionSetFormula(lua_State* L); + static int luaConditionSetOutfit(lua_State* L); + + static int luaConditionAddDamage(lua_State* L); + + // MonsterType + static int luaMonsterTypeCreate(lua_State* L); + + static int luaMonsterTypeIsAttackable(lua_State* L); + static int luaMonsterTypeIsConvinceable(lua_State* L); + static int luaMonsterTypeIsSummonable(lua_State* L); + static int luaMonsterTypeIsIllusionable(lua_State* L); + static int luaMonsterTypeIsHostile(lua_State* L); + static int luaMonsterTypeIsPushable(lua_State* L); + static int luaMonsterTypeIsHealthHidden(lua_State* L); + static int luaMonsterTypeIsBoss(lua_State* L); + + static int luaMonsterTypeCanPushItems(lua_State* L); + static int luaMonsterTypeCanPushCreatures(lua_State* L); + + static int luaMonsterTypeName(lua_State* L); + static int luaMonsterTypeNameDescription(lua_State* L); + + static int luaMonsterTypeHealth(lua_State* L); + static int luaMonsterTypeMaxHealth(lua_State* L); + static int luaMonsterTypeRunHealth(lua_State* L); + static int luaMonsterTypeExperience(lua_State* L); + static int luaMonsterTypeSkull(lua_State* L); + + static int luaMonsterTypeCombatImmunities(lua_State* L); + static int luaMonsterTypeConditionImmunities(lua_State* L); + + static int luaMonsterTypeGetAttackList(lua_State* L); + static int luaMonsterTypeAddAttack(lua_State* L); + + static int luaMonsterTypeGetDefenseList(lua_State* L); + static int luaMonsterTypeAddDefense(lua_State* L); + + static int luaMonsterTypeGetElementList(lua_State* L); + static int luaMonsterTypeAddElement(lua_State* L); + + static int luaMonsterTypeGetVoices(lua_State* L); + static int luaMonsterTypeAddVoice(lua_State* L); + + static int luaMonsterTypeGetLoot(lua_State* L); + static int luaMonsterTypeAddLoot(lua_State* L); + + static int luaMonsterTypeGetCreatureEvents(lua_State* L); + static int luaMonsterTypeRegisterEvent(lua_State* L); + + static int luaMonsterTypeEventOnCallback(lua_State* L); + static int luaMonsterTypeEventType(lua_State* L); + + static int luaMonsterTypeGetSummonList(lua_State* L); + static int luaMonsterTypeAddSummon(lua_State* L); + + static int luaMonsterTypeMaxSummons(lua_State* L); + + static int luaMonsterTypeArmor(lua_State* L); + static int luaMonsterTypeDefense(lua_State* L); + static int luaMonsterTypeOutfit(lua_State* L); + static int luaMonsterTypeRace(lua_State* L); + static int luaMonsterTypeCorpseId(lua_State* L); + static int luaMonsterTypeManaCost(lua_State* L); + static int luaMonsterTypeBaseSpeed(lua_State* L); + static int luaMonsterTypeLight(lua_State* L); + + static int luaMonsterTypeStaticAttackChance(lua_State* L); + static int luaMonsterTypeTargetDistance(lua_State* L); + static int luaMonsterTypeYellChance(lua_State* L); + static int luaMonsterTypeYellSpeedTicks(lua_State* L); + static int luaMonsterTypeChangeTargetChance(lua_State* L); + static int luaMonsterTypeChangeTargetSpeed(lua_State* L); + + // Loot + static int luaCreateLoot(lua_State* L); + static int luaDeleteLoot(lua_State* L); + static int luaLootSetId(lua_State* L); + static int luaLootSetMaxCount(lua_State* L); + static int luaLootSetSubType(lua_State* L); + static int luaLootSetChance(lua_State* L); + static int luaLootSetActionId(lua_State* L); + static int luaLootSetDescription(lua_State* L); + static int luaLootAddChildLoot(lua_State* L); + + // MonsterSpell + static int luaCreateMonsterSpell(lua_State* L); + static int luaDeleteMonsterSpell(lua_State* L); + static int luaMonsterSpellSetType(lua_State* L); + static int luaMonsterSpellSetScriptName(lua_State* L); + static int luaMonsterSpellSetChance(lua_State* L); + static int luaMonsterSpellSetInterval(lua_State* L); + static int luaMonsterSpellSetRange(lua_State* L); + static int luaMonsterSpellSetCombatValue(lua_State* L); + static int luaMonsterSpellSetCombatType(lua_State* L); + static int luaMonsterSpellSetAttackValue(lua_State* L); + static int luaMonsterSpellSetNeedTarget(lua_State* L); + static int luaMonsterSpellSetCombatLength(lua_State* L); + static int luaMonsterSpellSetCombatSpread(lua_State* L); + static int luaMonsterSpellSetCombatRadius(lua_State* L); + static int luaMonsterSpellSetConditionType(lua_State* L); + static int luaMonsterSpellSetConditionDamage(lua_State* L); + static int luaMonsterSpellSetConditionSpeedChange(lua_State* L); + static int luaMonsterSpellSetConditionDuration(lua_State* L); + static int luaMonsterSpellSetConditionTickInterval(lua_State* L); + static int luaMonsterSpellSetCombatShootEffect(lua_State* L); + static int luaMonsterSpellSetCombatEffect(lua_State* L); + + // Party + static int luaPartyCreate(lua_State* L); + static int luaPartyDisband(lua_State* L); + + static int luaPartyGetLeader(lua_State* L); + static int luaPartySetLeader(lua_State* L); + + static int luaPartyGetMembers(lua_State* L); + static int luaPartyGetMemberCount(lua_State* L); + + static int luaPartyGetInvitees(lua_State* L); + static int luaPartyGetInviteeCount(lua_State* L); + + static int luaPartyAddInvite(lua_State* L); + static int luaPartyRemoveInvite(lua_State* L); + + static int luaPartyAddMember(lua_State* L); + static int luaPartyRemoveMember(lua_State* L); + + static int luaPartyIsSharedExperienceActive(lua_State* L); + static int luaPartyIsSharedExperienceEnabled(lua_State* L); + static int luaPartyShareExperience(lua_State* L); + static int luaPartySetSharedExperience(lua_State* L); + + // Spells + static int luaSpellCreate(lua_State* L); + + static int luaSpellOnCastSpell(lua_State* L); + static int luaSpellRegister(lua_State* L); + static int luaSpellName(lua_State* L); + static int luaSpellId(lua_State* L); + static int luaSpellGroup(lua_State* L); + static int luaSpellCooldown(lua_State* L); + static int luaSpellGroupCooldown(lua_State* L); + static int luaSpellLevel(lua_State* L); + static int luaSpellMagicLevel(lua_State* L); + static int luaSpellMana(lua_State* L); + static int luaSpellManaPercent(lua_State* L); + static int luaSpellSoul(lua_State* L); + static int luaSpellRange(lua_State* L); + static int luaSpellPremium(lua_State* L); + static int luaSpellEnabled(lua_State* L); + static int luaSpellNeedTarget(lua_State* L); + static int luaSpellNeedWeapon(lua_State* L); + static int luaSpellNeedLearn(lua_State* L); + static int luaSpellSelfTarget(lua_State* L); + static int luaSpellBlocking(lua_State* L); + static int luaSpellAggressive(lua_State* L); + static int luaSpellVocation(lua_State* L); + + // only for InstantSpells + static int luaSpellWords(lua_State* L); + static int luaSpellNeedDirection(lua_State* L); + static int luaSpellHasParams(lua_State* L); + static int luaSpellHasPlayerNameParam(lua_State* L); + static int luaSpellNeedCasterTargetOrDirection(lua_State* L); + static int luaSpellIsBlockingWalls(lua_State* L); + + // only for RuneSpells + static int luaSpellRuneId(lua_State* L); + static int luaSpellCharges(lua_State* L); + static int luaSpellAllowFarUse(lua_State* L); + static int luaSpellBlockWalls(lua_State* L); + static int luaSpellCheckFloor(lua_State* L); + + // Actions + static int luaCreateAction(lua_State* L); + static int luaActionOnUse(lua_State* L); + static int luaActionRegister(lua_State* L); + static int luaActionItemId(lua_State* L); + static int luaActionActionId(lua_State* L); + static int luaActionUniqueId(lua_State* L); + static int luaActionAllowFarUse(lua_State* L); + static int luaActionBlockWalls(lua_State* L); + static int luaActionCheckFloor(lua_State* L); + + // Talkactions + static int luaCreateTalkaction(lua_State* L); + static int luaTalkactionOnSay(lua_State* L); + static int luaTalkactionRegister(lua_State* L); + static int luaTalkactionSeparator(lua_State* L); + + // CreatureEvents + static int luaCreateCreatureEvent(lua_State* L); + static int luaCreatureEventType(lua_State* L); + static int luaCreatureEventRegister(lua_State* L); + static int luaCreatureEventOnCallback(lua_State* L); + + // MoveEvents + static int luaCreateMoveEvent(lua_State* L); + static int luaMoveEventType(lua_State* L); + static int luaMoveEventRegister(lua_State* L); + static int luaMoveEventOnCallback(lua_State* L); + static int luaMoveEventLevel(lua_State* L); + static int luaMoveEventSlot(lua_State* L); + static int luaMoveEventMagLevel(lua_State* L); + static int luaMoveEventPremium(lua_State* L); + static int luaMoveEventVocation(lua_State* L); + static int luaMoveEventItemId(lua_State* L); + static int luaMoveEventActionId(lua_State* L); + static int luaMoveEventUniqueId(lua_State* L); + static int luaMoveEventPosition(lua_State* L); + + // GlobalEvents + static int luaCreateGlobalEvent(lua_State* L); + static int luaGlobalEventType(lua_State* L); + static int luaGlobalEventRegister(lua_State* L); + static int luaGlobalEventOnCallback(lua_State* L); + static int luaGlobalEventTime(lua_State* L); + static int luaGlobalEventInterval(lua_State* L); + + // Weapon + static int luaCreateWeapon(lua_State* L); + static int luaWeaponId(lua_State* L); + static int luaWeaponLevel(lua_State* L); + static int luaWeaponMagicLevel(lua_State* L); + static int luaWeaponMana(lua_State* L); + static int luaWeaponManaPercent(lua_State* L); + static int luaWeaponHealth(lua_State* L); + static int luaWeaponHealthPercent(lua_State* L); + static int luaWeaponSoul(lua_State* L); + static int luaWeaponPremium(lua_State* L); + static int luaWeaponBreakChance(lua_State* L); + static int luaWeaponAction(lua_State* L); + static int luaWeaponUnproperly(lua_State* L); + static int luaWeaponVocation(lua_State* L); + static int luaWeaponOnUseWeapon(lua_State* L); + static int luaWeaponRegister(lua_State* L); + static int luaWeaponElement(lua_State* L); + static int luaWeaponAttack(lua_State* L); + static int luaWeaponDefense(lua_State* L); + static int luaWeaponRange(lua_State* L); + static int luaWeaponCharges(lua_State* L); + static int luaWeaponDuration(lua_State* L); + static int luaWeaponDecayTo(lua_State* L); + static int luaWeaponTransformEquipTo(lua_State* L); + static int luaWeaponTransformDeEquipTo(lua_State* L); + static int luaWeaponSlotType(lua_State* L); + static int luaWeaponHitChance(lua_State* L); + static int luaWeaponExtraElement(lua_State* L); + + // exclusively for distance weapons + static int luaWeaponMaxHitChance(lua_State* L); + static int luaWeaponAmmoType(lua_State* L); + + // exclusively for wands + static int luaWeaponWandDamage(lua_State* L); + + // exclusively for wands & distance weapons + static int luaWeaponShootType(lua_State* L); + + // + std::string lastLuaError; + + std::string interfaceName; + + static ScriptEnvironment scriptEnv[16]; + static int32_t scriptEnvIndex; + + std::string loadingFile; +}; + +class LuaEnvironment : public LuaScriptInterface +{ + public: + LuaEnvironment(); + ~LuaEnvironment(); + + // non-copyable + LuaEnvironment(const LuaEnvironment&) = delete; + LuaEnvironment& operator=(const LuaEnvironment&) = delete; + + bool initState() override; + bool reInitState(); + bool closeState() override; + + LuaScriptInterface* getTestInterface(); + + Combat* getCombatObject(uint32_t id) const; + Combat* createCombatObject(LuaScriptInterface* interface); + void clearCombatObjects(LuaScriptInterface* interface); + + AreaCombat* getAreaObject(uint32_t id) const; + uint32_t createAreaObject(LuaScriptInterface* interface); + void clearAreaObjects(LuaScriptInterface* interface); + + private: + void executeTimerEvent(uint32_t eventIndex); + + std::unordered_map timerEvents; + std::unordered_map combatMap; + std::unordered_map areaMap; + + std::unordered_map> combatIdMap; + std::unordered_map> areaIdMap; + + LuaScriptInterface* testInterface = nullptr; + + uint32_t lastEventTimerId = 1; + uint32_t lastCombatId = 0; + uint32_t lastAreaId = 0; + + friend class LuaScriptInterface; + friend class CombatSpell; +}; + +#endif diff --git a/src/mailbox.cpp b/src/mailbox.cpp new file mode 100644 index 0000000..98423da --- /dev/null +++ b/src/mailbox.cpp @@ -0,0 +1,152 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "mailbox.h" +#include "game.h" +#include "iologindata.h" + +extern Game g_game; + +ReturnValue Mailbox::queryAdd(int32_t, const Thing& thing, uint32_t, uint32_t, Creature*) const +{ + const Item* item = thing.getItem(); + if (item && Mailbox::canSend(item)) { + return RETURNVALUE_NOERROR; + } + return RETURNVALUE_NOTPOSSIBLE; +} + +ReturnValue Mailbox::queryMaxCount(int32_t, const Thing&, uint32_t count, uint32_t& maxQueryCount, uint32_t) const +{ + maxQueryCount = std::max(1, count); + return RETURNVALUE_NOERROR; +} + +ReturnValue Mailbox::queryRemove(const Thing&, uint32_t, uint32_t) const +{ + return RETURNVALUE_NOTPOSSIBLE; +} + +Cylinder* Mailbox::queryDestination(int32_t&, const Thing&, Item**, uint32_t&) +{ + return this; +} + +void Mailbox::addThing(Thing* thing) +{ + return addThing(0, thing); +} + +void Mailbox::addThing(int32_t, Thing* thing) +{ + Item* item = thing->getItem(); + if (item && Mailbox::canSend(item)) { + sendItem(item); + } +} + +void Mailbox::updateThing(Thing*, uint16_t, uint32_t) +{ + // +} + +void Mailbox::replaceThing(uint32_t, Thing*) +{ + // +} + +void Mailbox::removeThing(Thing*, uint32_t) +{ + // +} + +void Mailbox::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t) +{ + getParent()->postAddNotification(thing, oldParent, index, LINK_PARENT); +} + +void Mailbox::postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t) +{ + getParent()->postRemoveNotification(thing, newParent, index, LINK_PARENT); +} + +bool Mailbox::sendItem(Item* item) const +{ + std::string receiver; + if (!getReceiver(item, receiver)) { + return false; + } + + /**No need to continue if its still empty**/ + if (receiver.empty()) { + return false; + } + + Player* player = g_game.getPlayerByName(receiver); + if (player) { + if (g_game.internalMoveItem(item->getParent(), player->getInbox(), INDEX_WHEREEVER, + item, item->getItemCount(), nullptr, FLAG_NOLIMIT) == RETURNVALUE_NOERROR) { + g_game.transformItem(item, item->getID() + 1); + player->onReceiveMail(); + return true; + } + } else { + Player tmpPlayer(nullptr); + if (!IOLoginData::loadPlayerByName(&tmpPlayer, receiver)) { + return false; + } + + if (g_game.internalMoveItem(item->getParent(), tmpPlayer.getInbox(), INDEX_WHEREEVER, + item, item->getItemCount(), nullptr, FLAG_NOLIMIT) == RETURNVALUE_NOERROR) { + g_game.transformItem(item, item->getID() + 1); + IOLoginData::savePlayer(&tmpPlayer); + return true; + } + } + return false; +} + +bool Mailbox::getReceiver(Item* item, std::string& name) const +{ + const Container* container = item->getContainer(); + if (container) { + for (Item* containerItem : container->getItemList()) { + if (containerItem->getID() == ITEM_LABEL && getReceiver(containerItem, name)) { + return true; + } + } + return false; + } + + const std::string& text = item->getText(); + if (text.empty()) { + return false; + } + + name = getFirstLine(text); + trimString(name); + return true; +} + +bool Mailbox::canSend(const Item* item) +{ + return item->getID() == ITEM_PARCEL || item->getID() == ITEM_LETTER; +} diff --git a/src/mailbox.h b/src/mailbox.h new file mode 100644 index 0000000..400b6c6 --- /dev/null +++ b/src/mailbox.h @@ -0,0 +1,66 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_MAILBOX_H_D231C6BE8D384CAAA3AE410C1323F9DB +#define FS_MAILBOX_H_D231C6BE8D384CAAA3AE410C1323F9DB + +#include "item.h" +#include "cylinder.h" +#include "const.h" + +class Mailbox final : public Item, public Cylinder +{ + public: + explicit Mailbox(uint16_t itemId) : Item(itemId) {} + + Mailbox* getMailbox() override { + return this; + } + const Mailbox* getMailbox() const override { + return this; + } + + //cylinder implementations + ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count, + uint32_t flags, Creature* actor = nullptr) const override; + ReturnValue queryMaxCount(int32_t index, const Thing& thing, uint32_t count, + uint32_t& maxQueryCount, uint32_t flags) const override; + ReturnValue queryRemove(const Thing& thing, uint32_t count, uint32_t flags) const override; + Cylinder* queryDestination(int32_t& index, const Thing& thing, Item** destItem, + uint32_t& flags) override; + + void addThing(Thing* thing) override; + void addThing(int32_t index, Thing* thing) override; + + void updateThing(Thing* thing, uint16_t itemId, uint32_t count) override; + void replaceThing(uint32_t index, Thing* thing) override; + + void removeThing(Thing* thing, uint32_t count) override; + + void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + + private: + bool getReceiver(Item* item, std::string& name) const; + bool sendItem(Item* item) const; + + static bool canSend(const Item* item); +}; + +#endif diff --git a/src/map.cpp b/src/map.cpp new file mode 100644 index 0000000..45ef801 --- /dev/null +++ b/src/map.cpp @@ -0,0 +1,1017 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "iomap.h" +#include "iomapserialize.h" +#include "combat.h" +#include "creature.h" +#include "game.h" +#include "monster.h" + +extern Game g_game; + +bool Map::loadMap(const std::string& identifier, bool loadHouses) +{ + IOMap loader; + if (!loader.loadMap(this, identifier)) { + std::cout << "[Fatal - Map::loadMap] " << loader.getLastErrorString() << std::endl; + return false; + } + + if (!IOMap::loadSpawns(this)) { + std::cout << "[Warning - Map::loadMap] Failed to load spawn data." << std::endl; + } + + if (loadHouses) { + if (!IOMap::loadHouses(this)) { + std::cout << "[Warning - Map::loadMap] Failed to load house data." << std::endl; + } + + IOMapSerialize::loadHouseInfo(); + IOMapSerialize::loadHouseItems(this); + } + return true; +} + +bool Map::save() +{ + bool saved = false; + for (uint32_t tries = 0; tries < 3; tries++) { + if (IOMapSerialize::saveHouseInfo()) { + saved = true; + break; + } + } + + if (!saved) { + return false; + } + + saved = false; + for (uint32_t tries = 0; tries < 3; tries++) { + if (IOMapSerialize::saveHouseItems()) { + saved = true; + break; + } + } + return saved; +} + +Tile* Map::getTile(uint16_t x, uint16_t y, uint8_t z) const +{ + if (z >= MAP_MAX_LAYERS) { + return nullptr; + } + + const QTreeLeafNode* leaf = QTreeNode::getLeafStatic(&root, x, y); + if (!leaf) { + return nullptr; + } + + const Floor* floor = leaf->getFloor(z); + if (!floor) { + return nullptr; + } + return floor->tiles[x & FLOOR_MASK][y & FLOOR_MASK]; +} + +void Map::setTile(uint16_t x, uint16_t y, uint8_t z, Tile* newTile) +{ + if (z >= MAP_MAX_LAYERS) { + std::cout << "ERROR: Attempt to set tile on invalid coordinate " << Position(x, y, z) << "!" << std::endl; + return; + } + + QTreeLeafNode::newLeaf = false; + QTreeLeafNode* leaf = root.createLeaf(x, y, 15); + + if (QTreeLeafNode::newLeaf) { + //update north + QTreeLeafNode* northLeaf = root.getLeaf(x, y - FLOOR_SIZE); + if (northLeaf) { + northLeaf->leafS = leaf; + } + + //update west leaf + QTreeLeafNode* westLeaf = root.getLeaf(x - FLOOR_SIZE, y); + if (westLeaf) { + westLeaf->leafE = leaf; + } + + //update south + QTreeLeafNode* southLeaf = root.getLeaf(x, y + FLOOR_SIZE); + if (southLeaf) { + leaf->leafS = southLeaf; + } + + //update east + QTreeLeafNode* eastLeaf = root.getLeaf(x + FLOOR_SIZE, y); + if (eastLeaf) { + leaf->leafE = eastLeaf; + } + } + + Floor* floor = leaf->createFloor(z); + uint32_t offsetX = x & FLOOR_MASK; + uint32_t offsetY = y & FLOOR_MASK; + + Tile*& tile = floor->tiles[offsetX][offsetY]; + if (tile) { + TileItemVector* items = newTile->getItemList(); + if (items) { + for (auto it = items->rbegin(), end = items->rend(); it != end; ++it) { + tile->addThing(*it); + } + items->clear(); + } + + Item* ground = newTile->getGround(); + if (ground) { + tile->addThing(ground); + newTile->setGround(nullptr); + } + delete newTile; + } else { + tile = newTile; + } +} + +bool Map::placeCreature(const Position& centerPos, Creature* creature, bool extendedPos/* = false*/, bool forceLogin/* = false*/) +{ + bool foundTile; + bool placeInPZ; + + Tile* tile = getTile(centerPos.x, centerPos.y, centerPos.z); + if (tile) { + placeInPZ = tile->hasFlag(TILESTATE_PROTECTIONZONE); + ReturnValue ret = tile->queryAdd(0, *creature, 1, FLAG_IGNOREBLOCKITEM); + foundTile = forceLogin || ret == RETURNVALUE_NOERROR || ret == RETURNVALUE_PLAYERISNOTINVITED; + } else { + placeInPZ = false; + foundTile = false; + } + + if (!foundTile) { + static std::vector> extendedRelList { + {0, -2}, + {-1, -1}, {0, -1}, {1, -1}, + {-2, 0}, {-1, 0}, {1, 0}, {2, 0}, + {-1, 1}, {0, 1}, {1, 1}, + {0, 2} + }; + + static std::vector> normalRelList { + {-1, -1}, {0, -1}, {1, -1}, + {-1, 0}, {1, 0}, + {-1, 1}, {0, 1}, {1, 1} + }; + + std::vector>& relList = (extendedPos ? extendedRelList : normalRelList); + + if (extendedPos) { + std::shuffle(relList.begin(), relList.begin() + 4, getRandomGenerator()); + std::shuffle(relList.begin() + 4, relList.end(), getRandomGenerator()); + } else { + std::shuffle(relList.begin(), relList.end(), getRandomGenerator()); + } + + for (const auto& it : relList) { + Position tryPos(centerPos.x + it.first, centerPos.y + it.second, centerPos.z); + + tile = getTile(tryPos.x, tryPos.y, tryPos.z); + if (!tile || (placeInPZ && !tile->hasFlag(TILESTATE_PROTECTIONZONE))) { + continue; + } + + if (tile->queryAdd(0, *creature, 1, 0) == RETURNVALUE_NOERROR) { + if (!extendedPos || isSightClear(centerPos, tryPos, false)) { + foundTile = true; + break; + } + } + } + + if (!foundTile) { + return false; + } + } + + int32_t index = 0; + uint32_t flags = 0; + Item* toItem = nullptr; + + Cylinder* toCylinder = tile->queryDestination(index, *creature, &toItem, flags); + toCylinder->internalAddThing(creature); + + const Position& dest = toCylinder->getPosition(); + getQTNode(dest.x, dest.y)->addCreature(creature); + return true; +} + +void Map::moveCreature(Creature& creature, Tile& newTile, bool forceTeleport/* = false*/) +{ + Tile& oldTile = *creature.getTile(); + + Position oldPos = oldTile.getPosition(); + Position newPos = newTile.getPosition(); + + bool teleport = forceTeleport || !newTile.getGround() || !Position::areInRange<1, 1, 0>(oldPos, newPos); + + SpectatorVec spectators, newPosSpectators; + getSpectators(spectators, oldPos, true); + getSpectators(newPosSpectators, newPos, true); + spectators.addSpectators(newPosSpectators); + + std::vector oldStackPosVector; + for (Creature* spectator : spectators) { + if (Player* tmpPlayer = spectator->getPlayer()) { + if (tmpPlayer->canSeeCreature(&creature)) { + oldStackPosVector.push_back(oldTile.getClientIndexOfCreature(tmpPlayer, &creature)); + } else { + oldStackPosVector.push_back(-1); + } + } + } + + //remove the creature + oldTile.removeThing(&creature, 0); + + QTreeLeafNode* leaf = getQTNode(oldPos.x, oldPos.y); + QTreeLeafNode* new_leaf = getQTNode(newPos.x, newPos.y); + + // Switch the node ownership + if (leaf != new_leaf) { + leaf->removeCreature(&creature); + new_leaf->addCreature(&creature); + } + + //add the creature + newTile.addThing(&creature); + + if (!teleport) { + if (oldPos.y > newPos.y) { + creature.setDirection(DIRECTION_NORTH); + } else if (oldPos.y < newPos.y) { + creature.setDirection(DIRECTION_SOUTH); + } + + if (oldPos.x < newPos.x) { + creature.setDirection(DIRECTION_EAST); + } else if (oldPos.x > newPos.x) { + creature.setDirection(DIRECTION_WEST); + } + } + + //send to client + size_t i = 0; + for (Creature* spectator : spectators) { + if (Player* tmpPlayer = spectator->getPlayer()) { + //Use the correct stackpos + int32_t stackpos = oldStackPosVector[i++]; + if (stackpos != -1) { + tmpPlayer->sendCreatureMove(&creature, newPos, newTile.getStackposOfCreature(tmpPlayer, &creature), oldPos, stackpos, teleport); + } + } + } + + //event method + for (Creature* spectator : spectators) { + spectator->onCreatureMove(&creature, &newTile, newPos, &oldTile, oldPos, teleport); + } + + oldTile.postRemoveNotification(&creature, &newTile, 0); + newTile.postAddNotification(&creature, &oldTile, 0); +} + +void Map::getSpectatorsInternal(SpectatorVec& spectators, const Position& centerPos, int32_t minRangeX, int32_t maxRangeX, int32_t minRangeY, int32_t maxRangeY, int32_t minRangeZ, int32_t maxRangeZ, bool onlyPlayers) const +{ + int_fast16_t min_y = centerPos.y + minRangeY; + int_fast16_t min_x = centerPos.x + minRangeX; + int_fast16_t max_y = centerPos.y + maxRangeY; + int_fast16_t max_x = centerPos.x + maxRangeX; + + int32_t minoffset = centerPos.getZ() - maxRangeZ; + uint16_t x1 = std::min(0xFFFF, std::max(0, (min_x + minoffset))); + uint16_t y1 = std::min(0xFFFF, std::max(0, (min_y + minoffset))); + + int32_t maxoffset = centerPos.getZ() - minRangeZ; + uint16_t x2 = std::min(0xFFFF, std::max(0, (max_x + maxoffset))); + uint16_t y2 = std::min(0xFFFF, std::max(0, (max_y + maxoffset))); + + int32_t startx1 = x1 - (x1 % FLOOR_SIZE); + int32_t starty1 = y1 - (y1 % FLOOR_SIZE); + int32_t endx2 = x2 - (x2 % FLOOR_SIZE); + int32_t endy2 = y2 - (y2 % FLOOR_SIZE); + + const QTreeLeafNode* startLeaf = QTreeNode::getLeafStatic(&root, startx1, starty1); + const QTreeLeafNode* leafS = startLeaf; + const QTreeLeafNode* leafE; + + for (int_fast32_t ny = starty1; ny <= endy2; ny += FLOOR_SIZE) { + leafE = leafS; + for (int_fast32_t nx = startx1; nx <= endx2; nx += FLOOR_SIZE) { + if (leafE) { + const CreatureVector& node_list = (onlyPlayers ? leafE->player_list : leafE->creature_list); + for (Creature* creature : node_list) { + const Position& cpos = creature->getPosition(); + if (minRangeZ > cpos.z || maxRangeZ < cpos.z) { + continue; + } + + int_fast16_t offsetZ = Position::getOffsetZ(centerPos, cpos); + if ((min_y + offsetZ) > cpos.y || (max_y + offsetZ) < cpos.y || (min_x + offsetZ) > cpos.x || (max_x + offsetZ) < cpos.x) { + continue; + } + + spectators.emplace_back(creature); + } + leafE = leafE->leafE; + } else { + leafE = QTreeNode::getLeafStatic(&root, nx + FLOOR_SIZE, ny); + } + } + + if (leafS) { + leafS = leafS->leafS; + } else { + leafS = QTreeNode::getLeafStatic(&root, startx1, ny + FLOOR_SIZE); + } + } +} + +void Map::getSpectators(SpectatorVec& spectators, const Position& centerPos, bool multifloor /*= false*/, bool onlyPlayers /*= false*/, int32_t minRangeX /*= 0*/, int32_t maxRangeX /*= 0*/, int32_t minRangeY /*= 0*/, int32_t maxRangeY /*= 0*/) +{ + if (centerPos.z >= MAP_MAX_LAYERS) { + return; + } + + bool foundCache = false; + bool cacheResult = false; + + minRangeX = (minRangeX == 0 ? -maxViewportX : -minRangeX); + maxRangeX = (maxRangeX == 0 ? maxViewportX : maxRangeX); + minRangeY = (minRangeY == 0 ? -maxViewportY : -minRangeY); + maxRangeY = (maxRangeY == 0 ? maxViewportY : maxRangeY); + + if (minRangeX == -maxViewportX && maxRangeX == maxViewportX && minRangeY == -maxViewportY && maxRangeY == maxViewportY && multifloor) { + if (onlyPlayers) { + auto it = playersSpectatorCache.find(centerPos); + if (it != playersSpectatorCache.end()) { + if (!spectators.empty()) { + const SpectatorVec& cachedSpectators = it->second; + spectators.insert(spectators.end(), cachedSpectators.begin(), cachedSpectators.end()); + } else { + spectators = it->second; + } + + foundCache = true; + } + } + + if (!foundCache) { + auto it = spectatorCache.find(centerPos); + if (it != spectatorCache.end()) { + if (!onlyPlayers) { + if (!spectators.empty()) { + const SpectatorVec& cachedSpectators = it->second; + spectators.insert(spectators.end(), cachedSpectators.begin(), cachedSpectators.end()); + } else { + spectators = it->second; + } + } else { + const SpectatorVec& cachedSpectators = it->second; + for (Creature* spectator : cachedSpectators) { + if (spectator->getPlayer()) { + spectators.emplace_back(spectator); + } + } + } + + foundCache = true; + } else { + cacheResult = true; + } + } + } + + if (!foundCache) { + int32_t minRangeZ; + int32_t maxRangeZ; + + if (multifloor) { + if (centerPos.z > 7) { + //underground + + //8->15 + minRangeZ = std::max(centerPos.getZ() - 2, 0); + maxRangeZ = std::min(centerPos.getZ() + 2, MAP_MAX_LAYERS - 1); + } else if (centerPos.z == 6) { + minRangeZ = 0; + maxRangeZ = 8; + } else if (centerPos.z == 7) { + minRangeZ = 0; + maxRangeZ = 9; + } else { + minRangeZ = 0; + maxRangeZ = 7; + } + } else { + minRangeZ = centerPos.z; + maxRangeZ = centerPos.z; + } + + getSpectatorsInternal(spectators, centerPos, minRangeX, maxRangeX, minRangeY, maxRangeY, minRangeZ, maxRangeZ, onlyPlayers); + + if (cacheResult) { + if (onlyPlayers) { + playersSpectatorCache[centerPos] = spectators; + } else { + spectatorCache[centerPos] = spectators; + } + } + } +} + +void Map::clearSpectatorCache() +{ + spectatorCache.clear(); +} + +void Map::clearPlayersSpectatorCache() +{ + playersSpectatorCache.clear(); +} + +bool Map::canThrowObjectTo(const Position& fromPos, const Position& toPos, bool checkLineOfSight /*= true*/, + int32_t rangex /*= Map::maxClientViewportX*/, int32_t rangey /*= Map::maxClientViewportY*/) const +{ + //z checks + //underground 8->15 + //ground level and above 7->0 + if ((fromPos.z >= 8 && toPos.z < 8) || (toPos.z >= 8 && fromPos.z < 8)) { + return false; + } + + int32_t deltaz = Position::getDistanceZ(fromPos, toPos); + if (deltaz > 2) { + return false; + } + + if ((Position::getDistanceX(fromPos, toPos) - deltaz) > rangex) { + return false; + } + + //distance checks + if ((Position::getDistanceY(fromPos, toPos) - deltaz) > rangey) { + return false; + } + + if (!checkLineOfSight) { + return true; + } + return isSightClear(fromPos, toPos, false); +} + +bool Map::checkSightLine(const Position& fromPos, const Position& toPos) const +{ + if (fromPos == toPos) { + return true; + } + + Position start(fromPos.z > toPos.z ? toPos : fromPos); + Position destination(fromPos.z > toPos.z ? fromPos : toPos); + + const int8_t mx = start.x < destination.x ? 1 : start.x == destination.x ? 0 : -1; + const int8_t my = start.y < destination.y ? 1 : start.y == destination.y ? 0 : -1; + + int32_t A = Position::getOffsetY(destination, start); + int32_t B = Position::getOffsetX(start, destination); + int32_t C = -(A * destination.x + B * destination.y); + + while (start.x != destination.x || start.y != destination.y) { + int32_t move_hor = std::abs(A * (start.x + mx) + B * (start.y) + C); + int32_t move_ver = std::abs(A * (start.x) + B * (start.y + my) + C); + int32_t move_cross = std::abs(A * (start.x + mx) + B * (start.y + my) + C); + + if (start.y != destination.y && (start.x == destination.x || move_hor > move_ver || move_hor > move_cross)) { + start.y += my; + } + + if (start.x != destination.x && (start.y == destination.y || move_ver > move_hor || move_ver > move_cross)) { + start.x += mx; + } + + const Tile* tile = getTile(start.x, start.y, start.z); + if (tile && tile->hasProperty(CONST_PROP_BLOCKPROJECTILE)) { + return false; + } + } + + // now we need to perform a jump between floors to see if everything is clear (literally) + while (start.z != destination.z) { + const Tile* tile = getTile(start.x, start.y, start.z); + if (tile && tile->getThingCount() > 0) { + return false; + } + + start.z++; + } + + return true; +} + +bool Map::isSightClear(const Position& fromPos, const Position& toPos, bool floorCheck) const +{ + if (floorCheck && fromPos.z != toPos.z) { + return false; + } + + // Cast two converging rays and see if either yields a result. + return checkSightLine(fromPos, toPos) || checkSightLine(toPos, fromPos); +} + +const Tile* Map::canWalkTo(const Creature& creature, const Position& pos) const +{ + int32_t walkCache = creature.getWalkCache(pos); + if (walkCache == 0) { + return nullptr; + } else if (walkCache == 1) { + return getTile(pos.x, pos.y, pos.z); + } + + //used for non-cached tiles + Tile* tile = getTile(pos.x, pos.y, pos.z); + if (creature.getTile() != tile) { + if (!tile || tile->queryAdd(0, creature, 1, FLAG_PATHFINDING | FLAG_IGNOREFIELDDAMAGE) != RETURNVALUE_NOERROR) { + return nullptr; + } + } + return tile; +} + +bool Map::getPathMatching(const Creature& creature, std::forward_list& dirList, const FrozenPathingConditionCall& pathCondition, const FindPathParams& fpp) const +{ + Position pos = creature.getPosition(); + Position endPos; + + AStarNodes nodes(pos.x, pos.y); + + int32_t bestMatch = 0; + + static int_fast32_t dirNeighbors[8][5][2] = { + {{-1, 0}, {0, 1}, {1, 0}, {1, 1}, {-1, 1}}, + {{-1, 0}, {0, 1}, {0, -1}, {-1, -1}, {-1, 1}}, + {{-1, 0}, {1, 0}, {0, -1}, {-1, -1}, {1, -1}}, + {{0, 1}, {1, 0}, {0, -1}, {1, -1}, {1, 1}}, + {{1, 0}, {0, -1}, {-1, -1}, {1, -1}, {1, 1}}, + {{-1, 0}, {0, -1}, {-1, -1}, {1, -1}, {-1, 1}}, + {{0, 1}, {1, 0}, {1, -1}, {1, 1}, {-1, 1}}, + {{-1, 0}, {0, 1}, {-1, -1}, {1, 1}, {-1, 1}} + }; + static int_fast32_t allNeighbors[8][2] = { + {-1, 0}, {0, 1}, {1, 0}, {0, -1}, {-1, -1}, {1, -1}, {1, 1}, {-1, 1} + }; + + const Position startPos = pos; + + AStarNode* found = nullptr; + while (fpp.maxSearchDist != 0 || nodes.getClosedNodes() < 100) { + AStarNode* n = nodes.getBestNode(); + if (!n) { + if (found) { + break; + } + return false; + } + + const int_fast32_t x = n->x; + const int_fast32_t y = n->y; + pos.x = x; + pos.y = y; + if (pathCondition(startPos, pos, fpp, bestMatch)) { + found = n; + endPos = pos; + if (bestMatch == 0) { + break; + } + } + + uint_fast32_t dirCount; + int_fast32_t* neighbors; + if (n->parent) { + const int_fast32_t offset_x = n->parent->x - x; + const int_fast32_t offset_y = n->parent->y - y; + if (offset_y == 0) { + if (offset_x == -1) { + neighbors = *dirNeighbors[DIRECTION_WEST]; + } else { + neighbors = *dirNeighbors[DIRECTION_EAST]; + } + } else if (!fpp.allowDiagonal || offset_x == 0) { + if (offset_y == -1) { + neighbors = *dirNeighbors[DIRECTION_NORTH]; + } else { + neighbors = *dirNeighbors[DIRECTION_SOUTH]; + } + } else if (offset_y == -1) { + if (offset_x == -1) { + neighbors = *dirNeighbors[DIRECTION_NORTHWEST]; + } else { + neighbors = *dirNeighbors[DIRECTION_NORTHEAST]; + } + } else if (offset_x == -1) { + neighbors = *dirNeighbors[DIRECTION_SOUTHWEST]; + } else { + neighbors = *dirNeighbors[DIRECTION_SOUTHEAST]; + } + dirCount = fpp.allowDiagonal ? 5 : 3; + } else { + dirCount = 8; + neighbors = *allNeighbors; + } + + const int_fast32_t f = n->f; + for (uint_fast32_t i = 0; i < dirCount; ++i) { + pos.x = x + *neighbors++; + pos.y = y + *neighbors++; + + if (fpp.maxSearchDist != 0 && (Position::getDistanceX(startPos, pos) > fpp.maxSearchDist || Position::getDistanceY(startPos, pos) > fpp.maxSearchDist)) { + continue; + } + + if (fpp.keepDistance && !pathCondition.isInRange(startPos, pos, fpp)) { + continue; + } + + const Tile* tile; + AStarNode* neighborNode = nodes.getNodeByPosition(pos.x, pos.y); + if (neighborNode) { + tile = getTile(pos.x, pos.y, pos.z); + } else { + tile = canWalkTo(creature, pos); + if (!tile) { + continue; + } + } + + //The cost (g) for this neighbor + const int_fast32_t cost = AStarNodes::getMapWalkCost(n, pos); + const int_fast32_t extraCost = AStarNodes::getTileWalkCost(creature, tile); + const int_fast32_t newf = f + cost + extraCost; + + if (neighborNode) { + if (neighborNode->f <= newf) { + //The node on the closed/open list is cheaper than this one + continue; + } + + neighborNode->f = newf; + neighborNode->parent = n; + nodes.openNode(neighborNode); + } else { + //Does not exist in the open/closed list, create a new node + neighborNode = nodes.createOpenNode(n, pos.x, pos.y, newf); + if (!neighborNode) { + if (found) { + break; + } + return false; + } + } + } + + nodes.closeNode(n); + } + + if (!found) { + return false; + } + + int_fast32_t prevx = endPos.x; + int_fast32_t prevy = endPos.y; + + found = found->parent; + while (found) { + pos.x = found->x; + pos.y = found->y; + + int_fast32_t dx = pos.getX() - prevx; + int_fast32_t dy = pos.getY() - prevy; + + prevx = pos.x; + prevy = pos.y; + + if (dx == 1 && dy == 1) { + dirList.push_front(DIRECTION_NORTHWEST); + } else if (dx == -1 && dy == 1) { + dirList.push_front(DIRECTION_NORTHEAST); + } else if (dx == 1 && dy == -1) { + dirList.push_front(DIRECTION_SOUTHWEST); + } else if (dx == -1 && dy == -1) { + dirList.push_front(DIRECTION_SOUTHEAST); + } else if (dx == 1) { + dirList.push_front(DIRECTION_WEST); + } else if (dx == -1) { + dirList.push_front(DIRECTION_EAST); + } else if (dy == 1) { + dirList.push_front(DIRECTION_NORTH); + } else if (dy == -1) { + dirList.push_front(DIRECTION_SOUTH); + } + + found = found->parent; + } + return true; +} + +// AStarNodes + +AStarNodes::AStarNodes(uint32_t x, uint32_t y) + : nodes(), openNodes() +{ + curNode = 1; + closedNodes = 0; + openNodes[0] = true; + + AStarNode& startNode = nodes[0]; + startNode.parent = nullptr; + startNode.x = x; + startNode.y = y; + startNode.f = 0; + nodeTable[(x << 16) | y] = nodes; +} + +AStarNode* AStarNodes::createOpenNode(AStarNode* parent, uint32_t x, uint32_t y, int_fast32_t f) +{ + if (curNode >= MAX_NODES) { + return nullptr; + } + + size_t retNode = curNode++; + openNodes[retNode] = true; + + AStarNode* node = nodes + retNode; + nodeTable[(x << 16) | y] = node; + node->parent = parent; + node->x = x; + node->y = y; + node->f = f; + return node; +} + +AStarNode* AStarNodes::getBestNode() +{ + if (curNode == 0) { + return nullptr; + } + + int32_t best_node_f = std::numeric_limits::max(); + int32_t best_node = -1; + for (size_t i = 0; i < curNode; i++) { + if (openNodes[i] && nodes[i].f < best_node_f) { + best_node_f = nodes[i].f; + best_node = i; + } + } + + if (best_node >= 0) { + return nodes + best_node; + } + return nullptr; +} + +void AStarNodes::closeNode(AStarNode* node) +{ + size_t index = node - nodes; + assert(index < MAX_NODES); + openNodes[index] = false; + ++closedNodes; +} + +void AStarNodes::openNode(AStarNode* node) +{ + size_t index = node - nodes; + assert(index < MAX_NODES); + if (!openNodes[index]) { + openNodes[index] = true; + --closedNodes; + } +} + +int_fast32_t AStarNodes::getClosedNodes() const +{ + return closedNodes; +} + +AStarNode* AStarNodes::getNodeByPosition(uint32_t x, uint32_t y) +{ + auto it = nodeTable.find((x << 16) | y); + if (it == nodeTable.end()) { + return nullptr; + } + return it->second; +} + +int_fast32_t AStarNodes::getMapWalkCost(AStarNode* node, const Position& neighborPos) +{ + if (std::abs(node->x - neighborPos.x) == std::abs(node->y - neighborPos.y)) { + //diagonal movement extra cost + return MAP_DIAGONALWALKCOST; + } + return MAP_NORMALWALKCOST; +} + +int_fast32_t AStarNodes::getTileWalkCost(const Creature& creature, const Tile* tile) +{ + int_fast32_t cost = 0; + if (tile->getTopVisibleCreature(&creature) != nullptr) { + //destroy creature cost + cost += MAP_NORMALWALKCOST * 3; + } + + if (const MagicField* field = tile->getFieldItem()) { + CombatType_t combatType = field->getCombatType(); + const Monster* monster = creature.getMonster(); + if (!creature.isImmune(combatType) && !creature.hasCondition(Combat::DamageToConditionType(combatType)) && (monster && !monster->canWalkOnFieldType(combatType))) { + cost += MAP_NORMALWALKCOST * 18; + } + } + return cost; +} + +// Floor +Floor::~Floor() +{ + for (auto& row : tiles) { + for (auto tile : row) { + delete tile; + } + } +} + +// QTreeNode +QTreeNode::~QTreeNode() +{ + for (auto* ptr : child) { + delete ptr; + } +} + +QTreeLeafNode* QTreeNode::getLeaf(uint32_t x, uint32_t y) +{ + if (leaf) { + return static_cast(this); + } + + QTreeNode* node = child[((x & 0x8000) >> 15) | ((y & 0x8000) >> 14)]; + if (!node) { + return nullptr; + } + return node->getLeaf(x << 1, y << 1); +} + +QTreeLeafNode* QTreeNode::createLeaf(uint32_t x, uint32_t y, uint32_t level) +{ + if (!isLeaf()) { + uint32_t index = ((x & 0x8000) >> 15) | ((y & 0x8000) >> 14); + if (!child[index]) { + if (level != FLOOR_BITS) { + child[index] = new QTreeNode(); + } else { + child[index] = new QTreeLeafNode(); + QTreeLeafNode::newLeaf = true; + } + } + return child[index]->createLeaf(x * 2, y * 2, level - 1); + } + return static_cast(this); +} + +// QTreeLeafNode +bool QTreeLeafNode::newLeaf = false; + +QTreeLeafNode::~QTreeLeafNode() +{ + for (auto* ptr : array) { + delete ptr; + } +} + +Floor* QTreeLeafNode::createFloor(uint32_t z) +{ + if (!array[z]) { + array[z] = new Floor(); + } + return array[z]; +} + +void QTreeLeafNode::addCreature(Creature* c) +{ + creature_list.push_back(c); + + if (c->getPlayer()) { + player_list.push_back(c); + } +} + +void QTreeLeafNode::removeCreature(Creature* c) +{ + auto iter = std::find(creature_list.begin(), creature_list.end(), c); + assert(iter != creature_list.end()); + *iter = creature_list.back(); + creature_list.pop_back(); + + if (c->getPlayer()) { + iter = std::find(player_list.begin(), player_list.end(), c); + assert(iter != player_list.end()); + *iter = player_list.back(); + player_list.pop_back(); + } +} + +uint32_t Map::clean() const +{ + uint64_t start = OTSYS_TIME(); + size_t count = 0, tiles = 0; + + if (g_game.getGameState() == GAME_STATE_NORMAL) { + g_game.setGameState(GAME_STATE_MAINTAIN); + } + + std::vector nodes { + &root + }; + std::vector toRemove; + do { + const QTreeNode* node = nodes.back(); + nodes.pop_back(); + if (node->isLeaf()) { + const QTreeLeafNode* leafNode = static_cast(node); + for (uint8_t z = 0; z < MAP_MAX_LAYERS; ++z) { + Floor* floor = leafNode->getFloor(z); + if (!floor) { + continue; + } + + for (auto& row : floor->tiles) { + for (auto tile : row) { + if (!tile || tile->hasFlag(TILESTATE_PROTECTIONZONE)) { + continue; + } + + TileItemVector* itemList = tile->getItemList(); + if (!itemList) { + continue; + } + + ++tiles; + for (Item* item : *itemList) { + if (item->isCleanable()) { + toRemove.push_back(item); + } + } + + for (Item* item : toRemove) { + g_game.internalRemoveItem(item, -1); + } + count += toRemove.size(); + toRemove.clear(); + } + } + } + } else { + for (auto childNode : node->child) { + if (childNode) { + nodes.push_back(childNode); + } + } + } + } while (!nodes.empty()); + + if (g_game.getGameState() == GAME_STATE_MAINTAIN) { + g_game.setGameState(GAME_STATE_NORMAL); + } + + std::cout << "> CLEAN: Removed " << count << " item" << (count != 1 ? "s" : "") + << " from " << tiles << " tile" << (tiles != 1 ? "s" : "") << " in " + << (OTSYS_TIME() - start) / (1000.) << " seconds." << std::endl; + return count; +} diff --git a/src/map.h b/src/map.h new file mode 100644 index 0000000..390eb6d --- /dev/null +++ b/src/map.h @@ -0,0 +1,290 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_MAP_H_E3953D57C058461F856F5221D359DAFA +#define FS_MAP_H_E3953D57C058461F856F5221D359DAFA + +#include "position.h" +#include "item.h" +#include "fileloader.h" + +#include "tools.h" +#include "tile.h" +#include "town.h" +#include "house.h" +#include "spawn.h" + +class Creature; +class Player; +class Game; +class Tile; +class Map; + +static constexpr int32_t MAP_MAX_LAYERS = 16; + +struct FindPathParams; +struct AStarNode { + AStarNode* parent; + int_fast32_t f; + uint16_t x, y; +}; + +static constexpr int32_t MAX_NODES = 512; + +static constexpr int32_t MAP_NORMALWALKCOST = 10; +static constexpr int32_t MAP_DIAGONALWALKCOST = 25; + +class AStarNodes +{ + public: + AStarNodes(uint32_t x, uint32_t y); + + AStarNode* createOpenNode(AStarNode* parent, uint32_t x, uint32_t y, int_fast32_t f); + AStarNode* getBestNode(); + void closeNode(AStarNode* node); + void openNode(AStarNode* node); + int_fast32_t getClosedNodes() const; + AStarNode* getNodeByPosition(uint32_t x, uint32_t y); + + static int_fast32_t getMapWalkCost(AStarNode* node, const Position& neighborPos); + static int_fast32_t getTileWalkCost(const Creature& creature, const Tile* tile); + + private: + AStarNode nodes[MAX_NODES]; + bool openNodes[MAX_NODES]; + std::unordered_map nodeTable; + size_t curNode; + int_fast32_t closedNodes; +}; + +using SpectatorCache = std::map; + +static constexpr int32_t FLOOR_BITS = 3; +static constexpr int32_t FLOOR_SIZE = (1 << FLOOR_BITS); +static constexpr int32_t FLOOR_MASK = (FLOOR_SIZE - 1); + +struct Floor { + constexpr Floor() = default; + ~Floor(); + + // non-copyable + Floor(const Floor&) = delete; + Floor& operator=(const Floor&) = delete; + + Tile* tiles[FLOOR_SIZE][FLOOR_SIZE] = {}; +}; + +class FrozenPathingConditionCall; +class QTreeLeafNode; + +class QTreeNode +{ + public: + constexpr QTreeNode() = default; + virtual ~QTreeNode(); + + // non-copyable + QTreeNode(const QTreeNode&) = delete; + QTreeNode& operator=(const QTreeNode&) = delete; + + bool isLeaf() const { + return leaf; + } + + QTreeLeafNode* getLeaf(uint32_t x, uint32_t y); + + template + static Leaf getLeafStatic(Node node, uint32_t x, uint32_t y) + { + do { + node = node->child[((x & 0x8000) >> 15) | ((y & 0x8000) >> 14)]; + if (!node) { + return nullptr; + } + + x <<= 1; + y <<= 1; + } while (!node->leaf); + return static_cast(node); + } + + QTreeLeafNode* createLeaf(uint32_t x, uint32_t y, uint32_t level); + + protected: + bool leaf = false; + + private: + QTreeNode* child[4] = {}; + + friend class Map; +}; + +class QTreeLeafNode final : public QTreeNode +{ + public: + QTreeLeafNode() { leaf = true; newLeaf = true; } + ~QTreeLeafNode(); + + // non-copyable + QTreeLeafNode(const QTreeLeafNode&) = delete; + QTreeLeafNode& operator=(const QTreeLeafNode&) = delete; + + Floor* createFloor(uint32_t z); + Floor* getFloor(uint8_t z) const { + return array[z]; + } + + void addCreature(Creature* c); + void removeCreature(Creature* c); + + private: + static bool newLeaf; + QTreeLeafNode* leafS = nullptr; + QTreeLeafNode* leafE = nullptr; + Floor* array[MAP_MAX_LAYERS] = {}; + CreatureVector creature_list; + CreatureVector player_list; + + friend class Map; + friend class QTreeNode; +}; + +/** + * Map class. + * Holds all the actual map-data + */ + +class Map +{ + public: + static constexpr int32_t maxViewportX = 11; //min value: maxClientViewportX + 1 + static constexpr int32_t maxViewportY = 11; //min value: maxClientViewportY + 1 + static constexpr int32_t maxClientViewportX = 8; + static constexpr int32_t maxClientViewportY = 6; + + uint32_t clean() const; + + /** + * Load a map. + * \returns true if the map was loaded successfully + */ + bool loadMap(const std::string& identifier, bool loadHouses); + + /** + * Save a map. + * \returns true if the map was saved successfully + */ + static bool save(); + + /** + * Get a single tile. + * \returns A pointer to that tile. + */ + Tile* getTile(uint16_t x, uint16_t y, uint8_t z) const; + Tile* getTile(const Position& pos) const { + return getTile(pos.x, pos.y, pos.z); + } + + /** + * Set a single tile. + */ + void setTile(uint16_t x, uint16_t y, uint8_t z, Tile* newTile); + void setTile(const Position& pos, Tile* newTile) { + setTile(pos.x, pos.y, pos.z, newTile); + } + + /** + * Place a creature on the map + * \param centerPos The position to place the creature + * \param creature Creature to place on the map + * \param extendedPos If true, the creature will in first-hand be placed 2 tiles away + * \param forceLogin If true, placing the creature will not fail becase of obstacles (creatures/chests) + */ + bool placeCreature(const Position& centerPos, Creature* creature, bool extendedPos = false, bool forceLogin = false); + + void moveCreature(Creature& creature, Tile& newTile, bool forceTeleport = false); + + void getSpectators(SpectatorVec& spectators, const Position& centerPos, bool multifloor = false, bool onlyPlayers = false, + int32_t minRangeX = 0, int32_t maxRangeX = 0, + int32_t minRangeY = 0, int32_t maxRangeY = 0); + + void clearSpectatorCache(); + void clearPlayersSpectatorCache(); + + /** + * Checks if you can throw an object to that position + * \param fromPos from Source point + * \param toPos Destination point + * \param rangex maximum allowed range horizontially + * \param rangey maximum allowed range vertically + * \param checkLineOfSight checks if there is any blocking objects in the way + * \returns The result if you can throw there or not + */ + bool canThrowObjectTo(const Position& fromPos, const Position& toPos, bool checkLineOfSight = true, + int32_t rangex = Map::maxClientViewportX, int32_t rangey = Map::maxClientViewportY) const; + + /** + * Checks if path is clear from fromPos to toPos + * Notice: This only checks a straight line if the path is clear, for path finding use getPathTo. + * \param fromPos from Source point + * \param toPos Destination point + * \param floorCheck if true then view is not clear if fromPos.z is not the same as toPos.z + * \returns The result if there is no obstacles + */ + bool isSightClear(const Position& fromPos, const Position& toPos, bool floorCheck) const; + bool checkSightLine(const Position& fromPos, const Position& toPos) const; + + const Tile* canWalkTo(const Creature& creature, const Position& pos) const; + + bool getPathMatching(const Creature& creature, std::forward_list& dirList, + const FrozenPathingConditionCall& pathCondition, const FindPathParams& fpp) const; + + std::map waypoints; + + QTreeLeafNode* getQTNode(uint16_t x, uint16_t y) { + return QTreeNode::getLeafStatic(&root, x, y); + } + + Spawns spawns; + Towns towns; + Houses houses; + + private: + SpectatorCache spectatorCache; + SpectatorCache playersSpectatorCache; + + QTreeNode root; + + std::string spawnfile; + std::string housefile; + + uint32_t width = 0; + uint32_t height = 0; + + // Actually scans the map for spectators + void getSpectatorsInternal(SpectatorVec& spectators, const Position& centerPos, + int32_t minRangeX, int32_t maxRangeX, + int32_t minRangeY, int32_t maxRangeY, + int32_t minRangeZ, int32_t maxRangeZ, bool onlyPlayers) const; + + friend class Game; + friend class IOMap; +}; + +#endif diff --git a/src/monster.cpp b/src/monster.cpp new file mode 100644 index 0000000..70c6277 --- /dev/null +++ b/src/monster.cpp @@ -0,0 +1,1996 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "monster.h" +#include "game.h" +#include "spells.h" +#include "events.h" + +extern Game g_game; +extern Monsters g_monsters; +extern Events* g_events; + +int32_t Monster::despawnRange; +int32_t Monster::despawnRadius; + +uint32_t Monster::monsterAutoID = 0x40000000; + +Monster* Monster::createMonster(const std::string& name) +{ + MonsterType* mType = g_monsters.getMonsterType(name); + if (!mType) { + return nullptr; + } + return new Monster(mType); +} + +Monster::Monster(MonsterType* mType) : + Creature(), + strDescription(mType->nameDescription), + mType(mType) +{ + defaultOutfit = mType->info.outfit; + currentOutfit = mType->info.outfit; + skull = mType->info.skull; + health = mType->info.health; + healthMax = mType->info.healthMax; + baseSpeed = mType->info.baseSpeed; + internalLight = mType->info.light; + hiddenHealth = mType->info.hiddenHealth; + + // register creature events + for (const std::string& scriptName : mType->info.scripts) { + if (!registerCreatureEvent(scriptName)) { + std::cout << "[Warning - Monster::Monster] Unknown event name: " << scriptName << std::endl; + } + } +} + +Monster::~Monster() +{ + clearTargetList(); + clearFriendList(); +} + +void Monster::addList() +{ + g_game.addMonster(this); +} + +void Monster::removeList() +{ + g_game.removeMonster(this); +} + +bool Monster::canSee(const Position& pos) const +{ + return Creature::canSee(getPosition(), pos, 9, 9); +} + +bool Monster::canWalkOnFieldType(CombatType_t combatType) const +{ + switch (combatType) { + case COMBAT_ENERGYDAMAGE: + return mType->info.canWalkOnEnergy; + case COMBAT_FIREDAMAGE: + return mType->info.canWalkOnFire; + case COMBAT_EARTHDAMAGE: + return mType->info.canWalkOnPoison; + default: + return true; + } +} + +void Monster::onAttackedCreatureDisappear(bool) +{ + attackTicks = 0; + extraMeleeAttack = true; +} + +void Monster::onCreatureAppear(Creature* creature, bool isLogin) +{ + Creature::onCreatureAppear(creature, isLogin); + + if (mType->info.creatureAppearEvent != -1) { + // onCreatureAppear(self, creature) + LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - Monster::onCreatureAppear] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(mType->info.creatureAppearEvent, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(mType->info.creatureAppearEvent); + + LuaScriptInterface::pushUserdata(L, this); + LuaScriptInterface::setMetatable(L, -1, "Monster"); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + if (scriptInterface->callFunction(2)) { + return; + } + } + + if (creature == this) { + //We just spawned lets look around to see who is there. + if (isSummon()) { + isMasterInRange = canSee(getMaster()->getPosition()); + } + + updateTargetList(); + updateIdleStatus(); + } else { + onCreatureEnter(creature); + } +} + +void Monster::onRemoveCreature(Creature* creature, bool isLogout) +{ + Creature::onRemoveCreature(creature, isLogout); + + if (mType->info.creatureDisappearEvent != -1) { + // onCreatureDisappear(self, creature) + LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - Monster::onCreatureDisappear] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(mType->info.creatureDisappearEvent, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(mType->info.creatureDisappearEvent); + + LuaScriptInterface::pushUserdata(L, this); + LuaScriptInterface::setMetatable(L, -1, "Monster"); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + if (scriptInterface->callFunction(2)) { + return; + } + } + + if (creature == this) { + if (spawn) { + spawn->startSpawnCheck(); + } + + setIdle(true); + } else { + onCreatureLeave(creature); + } +} + +void Monster::onCreatureMove(Creature* creature, const Tile* newTile, const Position& newPos, + const Tile* oldTile, const Position& oldPos, bool teleport) +{ + Creature::onCreatureMove(creature, newTile, newPos, oldTile, oldPos, teleport); + + if (mType->info.creatureMoveEvent != -1) { + // onCreatureMove(self, creature, oldPosition, newPosition) + LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - Monster::onCreatureMove] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(mType->info.creatureMoveEvent, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(mType->info.creatureMoveEvent); + + LuaScriptInterface::pushUserdata(L, this); + LuaScriptInterface::setMetatable(L, -1, "Monster"); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + LuaScriptInterface::pushPosition(L, oldPos); + LuaScriptInterface::pushPosition(L, newPos); + + if (scriptInterface->callFunction(4)) { + return; + } + } + + if (creature == this) { + if (isSummon()) { + isMasterInRange = canSee(getMaster()->getPosition()); + } + + updateTargetList(); + updateIdleStatus(); + } else { + bool canSeeNewPos = canSee(newPos); + bool canSeeOldPos = canSee(oldPos); + + if (canSeeNewPos && !canSeeOldPos) { + onCreatureEnter(creature); + } else if (!canSeeNewPos && canSeeOldPos) { + onCreatureLeave(creature); + } + + if (canSeeNewPos && isSummon() && getMaster() == creature) { + isMasterInRange = true; //Follow master again + } + + updateIdleStatus(); + + if (!isSummon()) { + if (followCreature) { + const Position& followPosition = followCreature->getPosition(); + const Position& position = getPosition(); + + int32_t offset_x = Position::getDistanceX(followPosition, position); + int32_t offset_y = Position::getDistanceY(followPosition, position); + if ((offset_x > 1 || offset_y > 1) && mType->info.changeTargetChance > 0) { + Direction dir = getDirectionTo(position, followPosition); + const Position& checkPosition = getNextPosition(dir, position); + + Tile* tile = g_game.map.getTile(checkPosition); + if (tile) { + Creature* topCreature = tile->getTopCreature(); + if (topCreature && followCreature != topCreature && isOpponent(topCreature)) { + selectTarget(topCreature); + } + } + } + } else if (isOpponent(creature)) { + //we have no target lets try pick this one + selectTarget(creature); + } + } + } +} + +void Monster::onCreatureSay(Creature* creature, SpeakClasses type, const std::string& text) +{ + Creature::onCreatureSay(creature, type, text); + + if (mType->info.creatureSayEvent != -1) { + // onCreatureSay(self, creature, type, message) + LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - Monster::onCreatureSay] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(mType->info.creatureSayEvent, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(mType->info.creatureSayEvent); + + LuaScriptInterface::pushUserdata(L, this); + LuaScriptInterface::setMetatable(L, -1, "Monster"); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + lua_pushnumber(L, type); + LuaScriptInterface::pushString(L, text); + + scriptInterface->callVoidFunction(4); + } +} + +void Monster::addFriend(Creature* creature) +{ + assert(creature != this); + auto result = friendList.insert(creature); + if (result.second) { + creature->incrementReferenceCounter(); + } +} + +void Monster::removeFriend(Creature* creature) +{ + auto it = friendList.find(creature); + if (it != friendList.end()) { + creature->decrementReferenceCounter(); + friendList.erase(it); + } +} + +void Monster::addTarget(Creature* creature, bool pushFront/* = false*/) +{ + assert(creature != this); + if (std::find(targetList.begin(), targetList.end(), creature) == targetList.end()) { + creature->incrementReferenceCounter(); + if (pushFront) { + targetList.push_front(creature); + } else { + targetList.push_back(creature); + } + } +} + +void Monster::removeTarget(Creature* creature) +{ + auto it = std::find(targetList.begin(), targetList.end(), creature); + if (it != targetList.end()) { + creature->decrementReferenceCounter(); + targetList.erase(it); + } +} + +void Monster::updateTargetList() +{ + auto friendIterator = friendList.begin(); + while (friendIterator != friendList.end()) { + Creature* creature = *friendIterator; + if (creature->getHealth() <= 0 || !canSee(creature->getPosition())) { + creature->decrementReferenceCounter(); + friendIterator = friendList.erase(friendIterator); + } else { + ++friendIterator; + } + } + + auto targetIterator = targetList.begin(); + while (targetIterator != targetList.end()) { + Creature* creature = *targetIterator; + if (creature->getHealth() <= 0 || !canSee(creature->getPosition())) { + creature->decrementReferenceCounter(); + targetIterator = targetList.erase(targetIterator); + } else { + ++targetIterator; + } + } + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, position, true); + spectators.erase(this); + for (Creature* spectator : spectators) { + if (canSee(spectator->getPosition())) { + onCreatureFound(spectator); + } + } +} + +void Monster::clearTargetList() +{ + for (Creature* creature : targetList) { + creature->decrementReferenceCounter(); + } + targetList.clear(); +} + +void Monster::clearFriendList() +{ + for (Creature* creature : friendList) { + creature->decrementReferenceCounter(); + } + friendList.clear(); +} + +void Monster::onCreatureFound(Creature* creature, bool pushFront/* = false*/) +{ + if (isFriend(creature)) { + addFriend(creature); + } + + if (isOpponent(creature)) { + addTarget(creature, pushFront); + } + + updateIdleStatus(); +} + +void Monster::onCreatureEnter(Creature* creature) +{ + // std::cout << "onCreatureEnter - " << creature->getName() << std::endl; + + if (getMaster() == creature) { + //Follow master again + isMasterInRange = true; + } + + onCreatureFound(creature, true); +} + +bool Monster::isFriend(const Creature* creature) const +{ + if (isSummon() && getMaster()->getPlayer()) { + const Player* masterPlayer = getMaster()->getPlayer(); + const Player* tmpPlayer = nullptr; + + if (creature->getPlayer()) { + tmpPlayer = creature->getPlayer(); + } else { + const Creature* creatureMaster = creature->getMaster(); + + if (creatureMaster && creatureMaster->getPlayer()) { + tmpPlayer = creatureMaster->getPlayer(); + } + } + + if (tmpPlayer && (tmpPlayer == getMaster() || masterPlayer->isPartner(tmpPlayer))) { + return true; + } + } else if (creature->getMonster() && !creature->isSummon()) { + return true; + } + + return false; +} + +bool Monster::isOpponent(const Creature* creature) const +{ + if (isSummon() && getMaster()->getPlayer()) { + if (creature != getMaster()) { + return true; + } + } else { + if ((creature->getPlayer() && !creature->getPlayer()->hasFlag(PlayerFlag_IgnoredByMonsters)) || + (creature->getMaster() && creature->getMaster()->getPlayer())) { + return true; + } + } + + return false; +} + +void Monster::onCreatureLeave(Creature* creature) +{ + // std::cout << "onCreatureLeave - " << creature->getName() << std::endl; + + if (getMaster() == creature) { + //Take random steps and only use defense abilities (e.g. heal) until its master comes back + isMasterInRange = false; + } + + //update friendList + if (isFriend(creature)) { + removeFriend(creature); + } + + //update targetList + if (isOpponent(creature)) { + removeTarget(creature); + if (targetList.empty()) { + updateIdleStatus(); + } + } +} + +bool Monster::searchTarget(TargetSearchType_t searchType /*= TARGETSEARCH_DEFAULT*/) +{ + std::list resultList; + const Position& myPos = getPosition(); + + for (Creature* creature : targetList) { + if (followCreature != creature && isTarget(creature)) { + if (searchType == TARGETSEARCH_RANDOM || canUseAttack(myPos, creature)) { + resultList.push_back(creature); + } + } + } + + switch (searchType) { + case TARGETSEARCH_NEAREST: { + Creature* target = nullptr; + if (!resultList.empty()) { + auto it = resultList.begin(); + target = *it; + + if (++it != resultList.end()) { + const Position& targetPosition = target->getPosition(); + int32_t minRange = Position::getDistanceX(myPos, targetPosition) + Position::getDistanceY(myPos, targetPosition); + do { + const Position& pos = (*it)->getPosition(); + + int32_t distance = Position::getDistanceX(myPos, pos) + Position::getDistanceY(myPos, pos); + if (distance < minRange) { + target = *it; + minRange = distance; + } + } while (++it != resultList.end()); + } + } else { + int32_t minRange = std::numeric_limits::max(); + for (Creature* creature : targetList) { + if (!isTarget(creature)) { + continue; + } + + const Position& pos = creature->getPosition(); + int32_t distance = Position::getDistanceX(myPos, pos) + Position::getDistanceY(myPos, pos); + if (distance < minRange) { + target = creature; + minRange = distance; + } + } + } + + if (target && selectTarget(target)) { + return true; + } + break; + } + + case TARGETSEARCH_DEFAULT: + case TARGETSEARCH_ATTACKRANGE: + case TARGETSEARCH_RANDOM: + default: { + if (!resultList.empty()) { + auto it = resultList.begin(); + std::advance(it, uniform_random(0, resultList.size() - 1)); + return selectTarget(*it); + } + + if (searchType == TARGETSEARCH_ATTACKRANGE) { + return false; + } + + break; + } + } + + //lets just pick the first target in the list + for (Creature* target : targetList) { + if (followCreature != target && selectTarget(target)) { + return true; + } + } + return false; +} + +void Monster::onFollowCreatureComplete(const Creature* creature) +{ + if (creature) { + auto it = std::find(targetList.begin(), targetList.end(), creature); + if (it != targetList.end()) { + Creature* target = (*it); + targetList.erase(it); + + if (hasFollowPath) { + targetList.push_front(target); + } else if (!isSummon()) { + targetList.push_back(target); + } else { + target->decrementReferenceCounter(); + } + } + } +} + +BlockType_t Monster::blockHit(Creature* attacker, CombatType_t combatType, int32_t& damage, + bool checkDefense /* = false*/, bool checkArmor /* = false*/, bool /* field = false */) +{ + BlockType_t blockType = Creature::blockHit(attacker, combatType, damage, checkDefense, checkArmor); + + if (damage != 0) { + int32_t elementMod = 0; + auto it = mType->info.elementMap.find(combatType); + if (it != mType->info.elementMap.end()) { + elementMod = it->second; + } + + if (elementMod != 0) { + damage = static_cast(std::round(damage * ((100 - elementMod) / 100.))); + if (damage <= 0) { + damage = 0; + blockType = BLOCK_ARMOR; + } + } + } + + return blockType; +} + + +bool Monster::isTarget(const Creature* creature) const +{ + if (creature->isRemoved() || !creature->isAttackable() || + creature->getZone() == ZONE_PROTECTION || !canSeeCreature(creature)) { + return false; + } + + if (creature->getPosition().z != getPosition().z) { + return false; + } + return true; +} + +bool Monster::selectTarget(Creature* creature) +{ + if (!isTarget(creature)) { + return false; + } + + auto it = std::find(targetList.begin(), targetList.end(), creature); + if (it == targetList.end()) { + //Target not found in our target list. + return false; + } + + if (isHostile() || isSummon()) { + if (setAttackedCreature(creature) && !isSummon()) { + g_dispatcher.addTask(createTask(std::bind(&Game::checkCreatureAttack, &g_game, getID()))); + } + } + return setFollowCreature(creature); +} + +void Monster::setIdle(bool idle) +{ + if (isRemoved() || getHealth() <= 0) { + return; + } + + isIdle = idle; + + if (!isIdle) { + g_game.addCreatureCheck(this); + } else { + onIdleStatus(); + clearTargetList(); + clearFriendList(); + Game::removeCreatureCheck(this); + } +} + +void Monster::updateIdleStatus() +{ + bool idle = false; + if (!isSummon() && targetList.empty()) { + // check if there are aggressive conditions + idle = std::find_if(conditions.begin(), conditions.end(), [](Condition* condition) { + return condition->isAggressive(); + }) == conditions.end(); + } + + setIdle(idle); +} + +void Monster::onAddCondition(ConditionType_t type) +{ + if (type == CONDITION_FIRE || type == CONDITION_ENERGY || type == CONDITION_POISON) { + updateMapCache(); + } + + updateIdleStatus(); +} + +void Monster::onEndCondition(ConditionType_t type) +{ + if (type == CONDITION_FIRE || type == CONDITION_ENERGY || type == CONDITION_POISON) { + ignoreFieldDamage = false; + updateMapCache(); + } + + updateIdleStatus(); +} + +void Monster::onThink(uint32_t interval) +{ + Creature::onThink(interval); + + if (mType->info.thinkEvent != -1) { + // onThink(self, interval) + LuaScriptInterface* scriptInterface = mType->info.scriptInterface; + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - Monster::onThink] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(mType->info.thinkEvent, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(mType->info.thinkEvent); + + LuaScriptInterface::pushUserdata(L, this); + LuaScriptInterface::setMetatable(L, -1, "Monster"); + + lua_pushnumber(L, interval); + + if (scriptInterface->callFunction(2)) { + return; + } + } + + if (!isInSpawnRange(position)) { + g_game.internalTeleport(this, masterPos); + setIdle(true); + } else { + updateIdleStatus(); + + if (!isIdle) { + addEventWalk(); + + if (isSummon()) { + if (!attackedCreature) { + if (getMaster() && getMaster()->getAttackedCreature()) { + //This happens if the monster is summoned during combat + selectTarget(getMaster()->getAttackedCreature()); + } else if (getMaster() != followCreature) { + //Our master has not ordered us to attack anything, lets follow him around instead. + setFollowCreature(getMaster()); + } + } else if (attackedCreature == this) { + setFollowCreature(nullptr); + } else if (followCreature != attackedCreature) { + //This happens just after a master orders an attack, so lets follow it aswell. + setFollowCreature(attackedCreature); + } + } else if (!targetList.empty()) { + if (!followCreature || !hasFollowPath) { + searchTarget(); + } else if (isFleeing()) { + if (attackedCreature && !canUseAttack(getPosition(), attackedCreature)) { + searchTarget(TARGETSEARCH_ATTACKRANGE); + } + } + } + + onThinkTarget(interval); + onThinkYell(interval); + onThinkDefense(interval); + } + } +} + +void Monster::doAttacking(uint32_t interval) +{ + if (!attackedCreature || (isSummon() && attackedCreature == this)) { + return; + } + + bool updateLook = true; + bool resetTicks = interval != 0; + attackTicks += interval; + + const Position& myPos = getPosition(); + const Position& targetPos = attackedCreature->getPosition(); + + for (const spellBlock_t& spellBlock : mType->info.attackSpells) { + bool inRange = false; + + if (attackedCreature == nullptr) { + break; + } + + if (canUseSpell(myPos, targetPos, spellBlock, interval, inRange, resetTicks)) { + if (spellBlock.chance >= static_cast(uniform_random(1, 100))) { + if (updateLook) { + updateLookDirection(); + updateLook = false; + } + + minCombatValue = spellBlock.minCombatValue; + maxCombatValue = spellBlock.maxCombatValue; + spellBlock.spell->castSpell(this, attackedCreature); + + if (spellBlock.isMelee) { + extraMeleeAttack = false; + } + } + } + + if (!inRange && spellBlock.isMelee) { + //melee swing out of reach + extraMeleeAttack = true; + } + } + + if (updateLook) { + updateLookDirection(); + } + + if (resetTicks) { + attackTicks = 0; + } +} + +bool Monster::canUseAttack(const Position& pos, const Creature* target) const +{ + if (isHostile()) { + const Position& targetPos = target->getPosition(); + uint32_t distance = std::max(Position::getDistanceX(pos, targetPos), Position::getDistanceY(pos, targetPos)); + for (const spellBlock_t& spellBlock : mType->info.attackSpells) { + if (spellBlock.range != 0 && distance <= spellBlock.range) { + return g_game.isSightClear(pos, targetPos, true); + } + } + return false; + } + return true; +} + +bool Monster::canUseSpell(const Position& pos, const Position& targetPos, + const spellBlock_t& sb, uint32_t interval, bool& inRange, bool& resetTicks) +{ + inRange = true; + + if (sb.isMelee && isFleeing()) { + return false; + } + + if (extraMeleeAttack) { + lastMeleeAttack = OTSYS_TIME(); + } else if (sb.isMelee && (OTSYS_TIME() - lastMeleeAttack) < 1500) { + return false; + } + + if (!sb.isMelee || !extraMeleeAttack) { + if (sb.speed > attackTicks) { + resetTicks = false; + return false; + } + + if (attackTicks % sb.speed >= interval) { + //already used this spell for this round + return false; + } + } + + if (sb.range != 0 && std::max(Position::getDistanceX(pos, targetPos), Position::getDistanceY(pos, targetPos)) > sb.range) { + inRange = false; + return false; + } + return true; +} + +void Monster::onThinkTarget(uint32_t interval) +{ + if (!isSummon()) { + if (mType->info.changeTargetSpeed != 0) { + bool canChangeTarget = true; + + if (challengeFocusDuration > 0) { + challengeFocusDuration -= interval; + + if (challengeFocusDuration <= 0) { + challengeFocusDuration = 0; + } + } + + if (targetChangeCooldown > 0) { + targetChangeCooldown -= interval; + + if (targetChangeCooldown <= 0) { + targetChangeCooldown = 0; + targetChangeTicks = mType->info.changeTargetSpeed; + } else { + canChangeTarget = false; + } + } + + if (canChangeTarget) { + targetChangeTicks += interval; + + if (targetChangeTicks >= mType->info.changeTargetSpeed) { + targetChangeTicks = 0; + targetChangeCooldown = mType->info.changeTargetSpeed; + + if (challengeFocusDuration > 0) { + challengeFocusDuration = 0; + } + + if (mType->info.changeTargetChance >= uniform_random(1, 100)) { + if (mType->info.targetDistance <= 1) { + searchTarget(TARGETSEARCH_RANDOM); + } else { + searchTarget(TARGETSEARCH_NEAREST); + } + } + } + } + } + } +} + +void Monster::onThinkDefense(uint32_t interval) +{ + bool resetTicks = true; + defenseTicks += interval; + + for (const spellBlock_t& spellBlock : mType->info.defenseSpells) { + if (spellBlock.speed > defenseTicks) { + resetTicks = false; + continue; + } + + if (defenseTicks % spellBlock.speed >= interval) { + //already used this spell for this round + continue; + } + + if ((spellBlock.chance >= static_cast(uniform_random(1, 100)))) { + minCombatValue = spellBlock.minCombatValue; + maxCombatValue = spellBlock.maxCombatValue; + spellBlock.spell->castSpell(this, this); + } + } + + if (!isSummon() && summons.size() < mType->info.maxSummons && hasFollowPath) { + for (const summonBlock_t& summonBlock : mType->info.summons) { + if (summonBlock.speed > defenseTicks) { + resetTicks = false; + continue; + } + + if (summons.size() >= mType->info.maxSummons) { + continue; + } + + if (defenseTicks % summonBlock.speed >= interval) { + //already used this spell for this round + continue; + } + + uint32_t summonCount = 0; + for (Creature* summon : summons) { + if (summon->getName() == summonBlock.name) { + ++summonCount; + } + } + + if (summonCount >= summonBlock.max) { + continue; + } + + if (summonBlock.chance < static_cast(uniform_random(1, 100))) { + continue; + } + + Monster* summon = Monster::createMonster(summonBlock.name); + if (summon) { + if (g_game.placeCreature(summon, getPosition(), false, summonBlock.force)) { + summon->setDropLoot(false); + summon->setSkillLoss(false); + summon->setMaster(this); + g_game.addMagicEffect(getPosition(), CONST_ME_MAGIC_BLUE); + g_game.addMagicEffect(summon->getPosition(), CONST_ME_TELEPORT); + } else { + delete summon; + } + } + } + } + + if (resetTicks) { + defenseTicks = 0; + } +} + +void Monster::onThinkYell(uint32_t interval) +{ + if (mType->info.yellSpeedTicks == 0) { + return; + } + + yellTicks += interval; + if (yellTicks >= mType->info.yellSpeedTicks) { + yellTicks = 0; + + if (!mType->info.voiceVector.empty() && (mType->info.yellChance >= static_cast(uniform_random(1, 100)))) { + uint32_t index = uniform_random(0, mType->info.voiceVector.size() - 1); + const voiceBlock_t& vb = mType->info.voiceVector[index]; + + if (vb.yellText) { + g_game.internalCreatureSay(this, TALKTYPE_MONSTER_YELL, vb.text, false); + } else { + g_game.internalCreatureSay(this, TALKTYPE_MONSTER_SAY, vb.text, false); + } + } + } +} + +void Monster::onWalk() +{ + Creature::onWalk(); +} + +bool Monster::pushItem(Item* item) +{ + const Position& centerPos = item->getPosition(); + + static std::vector> relList { + {-1, -1}, {0, -1}, {1, -1}, + {-1, 0}, {1, 0}, + {-1, 1}, {0, 1}, {1, 1} + }; + + std::shuffle(relList.begin(), relList.end(), getRandomGenerator()); + + for (const auto& it : relList) { + Position tryPos(centerPos.x + it.first, centerPos.y + it.second, centerPos.z); + Tile* tile = g_game.map.getTile(tryPos); + if (tile && g_game.canThrowObjectTo(centerPos, tryPos)) { + if (g_game.internalMoveItem(item->getParent(), tile, INDEX_WHEREEVER, item, item->getItemCount(), nullptr) == RETURNVALUE_NOERROR) { + return true; + } + } + } + return false; +} + +void Monster::pushItems(Tile* tile) +{ + //We can not use iterators here since we can push the item to another tile + //which will invalidate the iterator. + //start from the end to minimize the amount of traffic + if (TileItemVector* items = tile->getItemList()) { + uint32_t moveCount = 0; + uint32_t removeCount = 0; + + int32_t downItemSize = tile->getDownItemCount(); + for (int32_t i = downItemSize; --i >= 0;) { + Item* item = items->at(i); + if (item && item->hasProperty(CONST_PROP_MOVEABLE) && (item->hasProperty(CONST_PROP_BLOCKPATH) + || item->hasProperty(CONST_PROP_BLOCKSOLID))) { + if (moveCount < 20 && Monster::pushItem(item)) { + ++moveCount; + } else if (g_game.internalRemoveItem(item) == RETURNVALUE_NOERROR) { + ++removeCount; + } + } + } + + if (removeCount > 0) { + g_game.addMagicEffect(tile->getPosition(), CONST_ME_POFF); + } + } +} + +bool Monster::pushCreature(Creature* creature) +{ + static std::vector dirList { + DIRECTION_NORTH, + DIRECTION_WEST, DIRECTION_EAST, + DIRECTION_SOUTH + }; + std::shuffle(dirList.begin(), dirList.end(), getRandomGenerator()); + + for (Direction dir : dirList) { + const Position& tryPos = Spells::getCasterPosition(creature, dir); + Tile* toTile = g_game.map.getTile(tryPos); + if (toTile && !toTile->hasFlag(TILESTATE_BLOCKPATH)) { + if (g_game.internalMoveCreature(creature, dir) == RETURNVALUE_NOERROR) { + return true; + } + } + } + return false; +} + +void Monster::pushCreatures(Tile* tile) +{ + //We can not use iterators here since we can push a creature to another tile + //which will invalidate the iterator. + if (CreatureVector* creatures = tile->getCreatures()) { + uint32_t removeCount = 0; + Monster* lastPushedMonster = nullptr; + + for (size_t i = 0; i < creatures->size();) { + Monster* monster = creatures->at(i)->getMonster(); + if (monster && monster->isPushable()) { + if (monster != lastPushedMonster && Monster::pushCreature(monster)) { + lastPushedMonster = monster; + continue; + } + + monster->changeHealth(-monster->getHealth()); + monster->setDropLoot(false); + removeCount++; + } + + ++i; + } + + if (removeCount > 0) { + g_game.addMagicEffect(tile->getPosition(), CONST_ME_BLOCKHIT); + } + } +} + +bool Monster::getNextStep(Direction& direction, uint32_t& flags) +{ + if (isIdle || getHealth() <= 0) { + //we dont have anyone watching might aswell stop walking + eventWalk = 0; + return false; + } + + bool result = false; + if ((!followCreature || !hasFollowPath) && (!isSummon() || !isMasterInRange)) { + if (getTimeSinceLastMove() >= 1000) { + randomStepping = true; + //choose a random direction + result = getRandomStep(getPosition(), direction); + } + } else if ((isSummon() && isMasterInRange) || followCreature) { + randomStepping = false; + result = Creature::getNextStep(direction, flags); + if (result) { + flags |= FLAG_PATHFINDING; + } else { + if (ignoreFieldDamage) { + ignoreFieldDamage = false; + updateMapCache(); + } + //target dancing + if (attackedCreature && attackedCreature == followCreature) { + if (isFleeing()) { + result = getDanceStep(getPosition(), direction, false, false); + } else if (mType->info.staticAttackChance < static_cast(uniform_random(1, 100))) { + result = getDanceStep(getPosition(), direction); + } + } + } + } + + if (result && (canPushItems() || canPushCreatures())) { + const Position& pos = Spells::getCasterPosition(this, direction); + Tile* tile = g_game.map.getTile(pos); + if (tile) { + if (canPushItems()) { + Monster::pushItems(tile); + } + + if (canPushCreatures()) { + Monster::pushCreatures(tile); + } + } + } + + return result; +} + +bool Monster::getRandomStep(const Position& creaturePos, Direction& direction) const +{ + static std::vector dirList{ + DIRECTION_NORTH, + DIRECTION_WEST, DIRECTION_EAST, + DIRECTION_SOUTH + }; + std::shuffle(dirList.begin(), dirList.end(), getRandomGenerator()); + + for (Direction dir : dirList) { + if (canWalkTo(creaturePos, dir)) { + direction = dir; + return true; + } + } + return false; +} + +bool Monster::getDanceStep(const Position& creaturePos, Direction& direction, + bool keepAttack /*= true*/, bool keepDistance /*= true*/) +{ + bool canDoAttackNow = canUseAttack(creaturePos, attackedCreature); + + assert(attackedCreature != nullptr); + const Position& centerPos = attackedCreature->getPosition(); + + int_fast32_t offset_x = Position::getOffsetX(creaturePos, centerPos); + int_fast32_t offset_y = Position::getOffsetY(creaturePos, centerPos); + + int_fast32_t distance_x = std::abs(offset_x); + int_fast32_t distance_y = std::abs(offset_y); + + uint32_t centerToDist = std::max(distance_x, distance_y); + + std::vector dirList; + + if (!keepDistance || offset_y >= 0) { + uint32_t tmpDist = std::max(distance_x, std::abs((creaturePos.getY() - 1) - centerPos.getY())); + if (tmpDist == centerToDist && canWalkTo(creaturePos, DIRECTION_NORTH)) { + bool result = true; + + if (keepAttack) { + result = (!canDoAttackNow || canUseAttack(Position(creaturePos.x, creaturePos.y - 1, creaturePos.z), attackedCreature)); + } + + if (result) { + dirList.push_back(DIRECTION_NORTH); + } + } + } + + if (!keepDistance || offset_y <= 0) { + uint32_t tmpDist = std::max(distance_x, std::abs((creaturePos.getY() + 1) - centerPos.getY())); + if (tmpDist == centerToDist && canWalkTo(creaturePos, DIRECTION_SOUTH)) { + bool result = true; + + if (keepAttack) { + result = (!canDoAttackNow || canUseAttack(Position(creaturePos.x, creaturePos.y + 1, creaturePos.z), attackedCreature)); + } + + if (result) { + dirList.push_back(DIRECTION_SOUTH); + } + } + } + + if (!keepDistance || offset_x <= 0) { + uint32_t tmpDist = std::max(std::abs((creaturePos.getX() + 1) - centerPos.getX()), distance_y); + if (tmpDist == centerToDist && canWalkTo(creaturePos, DIRECTION_EAST)) { + bool result = true; + + if (keepAttack) { + result = (!canDoAttackNow || canUseAttack(Position(creaturePos.x + 1, creaturePos.y, creaturePos.z), attackedCreature)); + } + + if (result) { + dirList.push_back(DIRECTION_EAST); + } + } + } + + if (!keepDistance || offset_x >= 0) { + uint32_t tmpDist = std::max(std::abs((creaturePos.getX() - 1) - centerPos.getX()), distance_y); + if (tmpDist == centerToDist && canWalkTo(creaturePos, DIRECTION_WEST)) { + bool result = true; + + if (keepAttack) { + result = (!canDoAttackNow || canUseAttack(Position(creaturePos.x - 1, creaturePos.y, creaturePos.z), attackedCreature)); + } + + if (result) { + dirList.push_back(DIRECTION_WEST); + } + } + } + + if (!dirList.empty()) { + std::shuffle(dirList.begin(), dirList.end(), getRandomGenerator()); + direction = dirList[uniform_random(0, dirList.size() - 1)]; + return true; + } + return false; +} + +bool Monster::getDistanceStep(const Position& targetPos, Direction& direction, bool flee /* = false */) +{ + const Position& creaturePos = getPosition(); + + int_fast32_t dx = Position::getDistanceX(creaturePos, targetPos); + int_fast32_t dy = Position::getDistanceY(creaturePos, targetPos); + + int32_t distance = std::max(dx, dy); + + if (!flee && (distance > mType->info.targetDistance || !g_game.isSightClear(creaturePos, targetPos, true))) { + return false; // let the A* calculate it + } else if (!flee && distance == mType->info.targetDistance) { + return true; // we don't really care here, since it's what we wanted to reach (a dancestep will take of dancing in that position) + } + + int_fast32_t offsetx = Position::getOffsetX(creaturePos, targetPos); + int_fast32_t offsety = Position::getOffsetY(creaturePos, targetPos); + + if (dx <= 1 && dy <= 1) { + //seems like a target is near, it this case we need to slow down our movements (as a monster) + if (stepDuration < 2) { + stepDuration++; + } + } else if (stepDuration > 0) { + stepDuration--; + } + + if (offsetx == 0 && offsety == 0) { + return getRandomStep(creaturePos, direction); // player is "on" the monster so let's get some random step and rest will be taken care later. + } + + if (dx == dy) { + //player is diagonal to the monster + if (offsetx >= 1 && offsety >= 1) { + // player is NW + //escape to SE, S or E [and some extra] + bool s = canWalkTo(creaturePos, DIRECTION_SOUTH); + bool e = canWalkTo(creaturePos, DIRECTION_EAST); + + if (s && e) { + direction = boolean_random() ? DIRECTION_SOUTH : DIRECTION_EAST; + return true; + } else if (s) { + direction = DIRECTION_SOUTH; + return true; + } else if (e) { + direction = DIRECTION_EAST; + return true; + } else if (canWalkTo(creaturePos, DIRECTION_SOUTHEAST)) { + direction = DIRECTION_SOUTHEAST; + return true; + } + + /* fleeing */ + bool n = canWalkTo(creaturePos, DIRECTION_NORTH); + bool w = canWalkTo(creaturePos, DIRECTION_WEST); + + if (flee) { + if (n && w) { + direction = boolean_random() ? DIRECTION_NORTH : DIRECTION_WEST; + return true; + } else if (n) { + direction = DIRECTION_NORTH; + return true; + } else if (w) { + direction = DIRECTION_WEST; + return true; + } + } + + /* end of fleeing */ + + if (w && canWalkTo(creaturePos, DIRECTION_SOUTHWEST)) { + direction = DIRECTION_WEST; + } else if (n && canWalkTo(creaturePos, DIRECTION_NORTHEAST)) { + direction = DIRECTION_NORTH; + } + + return true; + } else if (offsetx <= -1 && offsety <= -1) { + //player is SE + //escape to NW , W or N [and some extra] + bool w = canWalkTo(creaturePos, DIRECTION_WEST); + bool n = canWalkTo(creaturePos, DIRECTION_NORTH); + + if (w && n) { + direction = boolean_random() ? DIRECTION_WEST : DIRECTION_NORTH; + return true; + } else if (w) { + direction = DIRECTION_WEST; + return true; + } else if (n) { + direction = DIRECTION_NORTH; + return true; + } + + if (canWalkTo(creaturePos, DIRECTION_NORTHWEST)) { + direction = DIRECTION_NORTHWEST; + return true; + } + + /* fleeing */ + bool s = canWalkTo(creaturePos, DIRECTION_SOUTH); + bool e = canWalkTo(creaturePos, DIRECTION_EAST); + + if (flee) { + if (s && e) { + direction = boolean_random() ? DIRECTION_SOUTH : DIRECTION_EAST; + return true; + } else if (s) { + direction = DIRECTION_SOUTH; + return true; + } else if (e) { + direction = DIRECTION_EAST; + return true; + } + } + + /* end of fleeing */ + + if (s && canWalkTo(creaturePos, DIRECTION_SOUTHWEST)) { + direction = DIRECTION_SOUTH; + } else if (e && canWalkTo(creaturePos, DIRECTION_NORTHEAST)) { + direction = DIRECTION_EAST; + } + + return true; + } else if (offsetx >= 1 && offsety <= -1) { + //player is SW + //escape to NE, N, E [and some extra] + bool n = canWalkTo(creaturePos, DIRECTION_NORTH); + bool e = canWalkTo(creaturePos, DIRECTION_EAST); + if (n && e) { + direction = boolean_random() ? DIRECTION_NORTH : DIRECTION_EAST; + return true; + } else if (n) { + direction = DIRECTION_NORTH; + return true; + } else if (e) { + direction = DIRECTION_EAST; + return true; + } + + if (canWalkTo(creaturePos, DIRECTION_NORTHEAST)) { + direction = DIRECTION_NORTHEAST; + return true; + } + + /* fleeing */ + bool s = canWalkTo(creaturePos, DIRECTION_SOUTH); + bool w = canWalkTo(creaturePos, DIRECTION_WEST); + + if (flee) { + if (s && w) { + direction = boolean_random() ? DIRECTION_SOUTH : DIRECTION_WEST; + return true; + } else if (s) { + direction = DIRECTION_SOUTH; + return true; + } else if (w) { + direction = DIRECTION_WEST; + return true; + } + } + + /* end of fleeing */ + + if (w && canWalkTo(creaturePos, DIRECTION_NORTHWEST)) { + direction = DIRECTION_WEST; + } else if (s && canWalkTo(creaturePos, DIRECTION_SOUTHEAST)) { + direction = DIRECTION_SOUTH; + } + + return true; + } else if (offsetx <= -1 && offsety >= 1) { + // player is NE + //escape to SW, S, W [and some extra] + bool w = canWalkTo(creaturePos, DIRECTION_WEST); + bool s = canWalkTo(creaturePos, DIRECTION_SOUTH); + if (w && s) { + direction = boolean_random() ? DIRECTION_WEST : DIRECTION_SOUTH; + return true; + } else if (w) { + direction = DIRECTION_WEST; + return true; + } else if (s) { + direction = DIRECTION_SOUTH; + return true; + } else if (canWalkTo(creaturePos, DIRECTION_SOUTHWEST)) { + direction = DIRECTION_SOUTHWEST; + return true; + } + + /* fleeing */ + bool n = canWalkTo(creaturePos, DIRECTION_NORTH); + bool e = canWalkTo(creaturePos, DIRECTION_EAST); + + if (flee) { + if (n && e) { + direction = boolean_random() ? DIRECTION_NORTH : DIRECTION_EAST; + return true; + } else if (n) { + direction = DIRECTION_NORTH; + return true; + } else if (e) { + direction = DIRECTION_EAST; + return true; + } + } + + /* end of fleeing */ + + if (e && canWalkTo(creaturePos, DIRECTION_SOUTHEAST)) { + direction = DIRECTION_EAST; + } else if (n && canWalkTo(creaturePos, DIRECTION_NORTHWEST)) { + direction = DIRECTION_NORTH; + } + + return true; + } + } + + //Now let's decide where the player is located to the monster (what direction) so we can decide where to escape. + if (dy > dx) { + Direction playerDir = offsety < 0 ? DIRECTION_SOUTH : DIRECTION_NORTH; + switch (playerDir) { + case DIRECTION_NORTH: { + // Player is to the NORTH, so obviously we need to check if we can go SOUTH, if not then let's choose WEST or EAST and again if we can't we need to decide about some diagonal movements. + if (canWalkTo(creaturePos, DIRECTION_SOUTH)) { + direction = DIRECTION_SOUTH; + return true; + } + + bool w = canWalkTo(creaturePos, DIRECTION_WEST); + bool e = canWalkTo(creaturePos, DIRECTION_EAST); + if (w && e && offsetx == 0) { + direction = boolean_random() ? DIRECTION_WEST : DIRECTION_EAST; + return true; + } else if (w && offsetx <= 0) { + direction = DIRECTION_WEST; + return true; + } else if (e && offsetx >= 0) { + direction = DIRECTION_EAST; + return true; + } + + /* fleeing */ + if (flee) { + if (w && e) { + direction = boolean_random() ? DIRECTION_WEST : DIRECTION_EAST; + return true; + } else if (w) { + direction = DIRECTION_WEST; + return true; + } else if (e) { + direction = DIRECTION_EAST; + return true; + } + } + + /* end of fleeing */ + + bool sw = canWalkTo(creaturePos, DIRECTION_SOUTHWEST); + bool se = canWalkTo(creaturePos, DIRECTION_SOUTHEAST); + if (sw || se) { + // we can move both dirs + if (sw && se) { + direction = boolean_random() ? DIRECTION_SOUTHWEST : DIRECTION_SOUTHEAST; + } else if (w) { + direction = DIRECTION_WEST; + } else if (sw) { + direction = DIRECTION_SOUTHWEST; + } else if (e) { + direction = DIRECTION_EAST; + } else if (se) { + direction = DIRECTION_SOUTHEAST; + } + return true; + } + + /* fleeing */ + if (flee && canWalkTo(creaturePos, DIRECTION_NORTH)) { + // towards player, yea + direction = DIRECTION_NORTH; + return true; + } + + /* end of fleeing */ + break; + } + + case DIRECTION_SOUTH: { + if (canWalkTo(creaturePos, DIRECTION_NORTH)) { + direction = DIRECTION_NORTH; + return true; + } + + bool w = canWalkTo(creaturePos, DIRECTION_WEST); + bool e = canWalkTo(creaturePos, DIRECTION_EAST); + if (w && e && offsetx == 0) { + direction = boolean_random() ? DIRECTION_WEST : DIRECTION_EAST; + return true; + } else if (w && offsetx <= 0) { + direction = DIRECTION_WEST; + return true; + } else if (e && offsetx >= 0) { + direction = DIRECTION_EAST; + return true; + } + + /* fleeing */ + if (flee) { + if (w && e) { + direction = boolean_random() ? DIRECTION_WEST : DIRECTION_EAST; + return true; + } else if (w) { + direction = DIRECTION_WEST; + return true; + } else if (e) { + direction = DIRECTION_EAST; + return true; + } + } + + /* end of fleeing */ + + bool nw = canWalkTo(creaturePos, DIRECTION_NORTHWEST); + bool ne = canWalkTo(creaturePos, DIRECTION_NORTHEAST); + if (nw || ne) { + // we can move both dirs + if (nw && ne) { + direction = boolean_random() ? DIRECTION_NORTHWEST : DIRECTION_NORTHEAST; + } else if (w) { + direction = DIRECTION_WEST; + } else if (nw) { + direction = DIRECTION_NORTHWEST; + } else if (e) { + direction = DIRECTION_EAST; + } else if (ne) { + direction = DIRECTION_NORTHEAST; + } + return true; + } + + /* fleeing */ + if (flee && canWalkTo(creaturePos, DIRECTION_SOUTH)) { + // towards player, yea + direction = DIRECTION_SOUTH; + return true; + } + + /* end of fleeing */ + break; + } + + default: + break; + } + } else { + Direction playerDir = offsetx < 0 ? DIRECTION_EAST : DIRECTION_WEST; + switch (playerDir) { + case DIRECTION_WEST: { + if (canWalkTo(creaturePos, DIRECTION_EAST)) { + direction = DIRECTION_EAST; + return true; + } + + bool n = canWalkTo(creaturePos, DIRECTION_NORTH); + bool s = canWalkTo(creaturePos, DIRECTION_SOUTH); + if (n && s && offsety == 0) { + direction = boolean_random() ? DIRECTION_NORTH : DIRECTION_SOUTH; + return true; + } else if (n && offsety <= 0) { + direction = DIRECTION_NORTH; + return true; + } else if (s && offsety >= 0) { + direction = DIRECTION_SOUTH; + return true; + } + + /* fleeing */ + if (flee) { + if (n && s) { + direction = boolean_random() ? DIRECTION_NORTH : DIRECTION_SOUTH; + return true; + } else if (n) { + direction = DIRECTION_NORTH; + return true; + } else if (s) { + direction = DIRECTION_SOUTH; + return true; + } + } + + /* end of fleeing */ + + bool se = canWalkTo(creaturePos, DIRECTION_SOUTHEAST); + bool ne = canWalkTo(creaturePos, DIRECTION_NORTHEAST); + if (se || ne) { + if (se && ne) { + direction = boolean_random() ? DIRECTION_SOUTHEAST : DIRECTION_NORTHEAST; + } else if (s) { + direction = DIRECTION_SOUTH; + } else if (se) { + direction = DIRECTION_SOUTHEAST; + } else if (n) { + direction = DIRECTION_NORTH; + } else if (ne) { + direction = DIRECTION_NORTHEAST; + } + return true; + } + + /* fleeing */ + if (flee && canWalkTo(creaturePos, DIRECTION_WEST)) { + // towards player, yea + direction = DIRECTION_WEST; + return true; + } + + /* end of fleeing */ + break; + } + + case DIRECTION_EAST: { + if (canWalkTo(creaturePos, DIRECTION_WEST)) { + direction = DIRECTION_WEST; + return true; + } + + bool n = canWalkTo(creaturePos, DIRECTION_NORTH); + bool s = canWalkTo(creaturePos, DIRECTION_SOUTH); + if (n && s && offsety == 0) { + direction = boolean_random() ? DIRECTION_NORTH : DIRECTION_SOUTH; + return true; + } else if (n && offsety <= 0) { + direction = DIRECTION_NORTH; + return true; + } else if (s && offsety >= 0) { + direction = DIRECTION_SOUTH; + return true; + } + + /* fleeing */ + if (flee) { + if (n && s) { + direction = boolean_random() ? DIRECTION_NORTH : DIRECTION_SOUTH; + return true; + } else if (n) { + direction = DIRECTION_NORTH; + return true; + } else if (s) { + direction = DIRECTION_SOUTH; + return true; + } + } + + /* end of fleeing */ + + bool nw = canWalkTo(creaturePos, DIRECTION_NORTHWEST); + bool sw = canWalkTo(creaturePos, DIRECTION_SOUTHWEST); + if (nw || sw) { + if (nw && sw) { + direction = boolean_random() ? DIRECTION_NORTHWEST : DIRECTION_SOUTHWEST; + } else if (n) { + direction = DIRECTION_NORTH; + } else if (nw) { + direction = DIRECTION_NORTHWEST; + } else if (s) { + direction = DIRECTION_SOUTH; + } else if (sw) { + direction = DIRECTION_SOUTHWEST; + } + return true; + } + + /* fleeing */ + if (flee && canWalkTo(creaturePos, DIRECTION_EAST)) { + // towards player, yea + direction = DIRECTION_EAST; + return true; + } + + /* end of fleeing */ + break; + } + + default: + break; + } + } + + return true; +} + +bool Monster::canWalkTo(Position pos, Direction direction) const +{ + pos = getNextPosition(direction, pos); + if (isInSpawnRange(pos)) { + if (getWalkCache(pos) == 0) { + return false; + } + + Tile* tile = g_game.map.getTile(pos); + if (tile && tile->getTopVisibleCreature(this) == nullptr && tile->queryAdd(0, *this, 1, FLAG_PATHFINDING) == RETURNVALUE_NOERROR) { + return true; + } + } + return false; +} + +void Monster::death(Creature*) +{ + setAttackedCreature(nullptr); + + for (Creature* summon : summons) { + summon->changeHealth(-summon->getHealth()); + summon->removeMaster(); + } + summons.clear(); + + clearTargetList(); + clearFriendList(); + onIdleStatus(); +} + +Item* Monster::getCorpse(Creature* lastHitCreature, Creature* mostDamageCreature) +{ + Item* corpse = Creature::getCorpse(lastHitCreature, mostDamageCreature); + if (corpse) { + if (mostDamageCreature) { + if (mostDamageCreature->getPlayer()) { + corpse->setCorpseOwner(mostDamageCreature->getID()); + } else { + const Creature* mostDamageCreatureMaster = mostDamageCreature->getMaster(); + if (mostDamageCreatureMaster && mostDamageCreatureMaster->getPlayer()) { + corpse->setCorpseOwner(mostDamageCreatureMaster->getID()); + } + } + } + } + return corpse; +} + +bool Monster::isInSpawnRange(const Position& pos) const +{ + if (!spawn) { + return true; + } + + if (Monster::despawnRadius == 0) { + return true; + } + + if (!Spawns::isInZone(masterPos, Monster::despawnRadius, pos)) { + return false; + } + + if (Monster::despawnRange == 0) { + return true; + } + + if (Position::getDistanceZ(pos, masterPos) > Monster::despawnRange) { + return false; + } + + return true; +} + +bool Monster::getCombatValues(int32_t& min, int32_t& max) +{ + if (minCombatValue == 0 && maxCombatValue == 0) { + return false; + } + + min = minCombatValue; + max = maxCombatValue; + return true; +} + +void Monster::updateLookDirection() +{ + Direction newDir = getDirection(); + + if (attackedCreature) { + const Position& pos = getPosition(); + const Position& attackedCreaturePos = attackedCreature->getPosition(); + int_fast32_t offsetx = Position::getOffsetX(attackedCreaturePos, pos); + int_fast32_t offsety = Position::getOffsetY(attackedCreaturePos, pos); + + int32_t dx = std::abs(offsetx); + int32_t dy = std::abs(offsety); + if (dx > dy) { + //look EAST/WEST + if (offsetx < 0) { + newDir = DIRECTION_WEST; + } else { + newDir = DIRECTION_EAST; + } + } else if (dx < dy) { + //look NORTH/SOUTH + if (offsety < 0) { + newDir = DIRECTION_NORTH; + } else { + newDir = DIRECTION_SOUTH; + } + } else { + Direction dir = getDirection(); + if (offsetx < 0 && offsety < 0) { + if (dir == DIRECTION_SOUTH) { + newDir = DIRECTION_WEST; + } else if (dir == DIRECTION_NORTH) { + newDir = DIRECTION_WEST; + } else if (dir == DIRECTION_EAST) { + newDir = DIRECTION_NORTH; + } + } else if (offsetx < 0 && offsety > 0) { + if (dir == DIRECTION_NORTH) { + newDir = DIRECTION_WEST; + } else if (dir == DIRECTION_SOUTH) { + newDir = DIRECTION_WEST; + } else if (dir == DIRECTION_EAST) { + newDir = DIRECTION_SOUTH; + } + } else if (offsetx > 0 && offsety < 0) { + if (dir == DIRECTION_SOUTH) { + newDir = DIRECTION_EAST; + } else if (dir == DIRECTION_NORTH) { + newDir = DIRECTION_EAST; + } else if (dir == DIRECTION_WEST) { + newDir = DIRECTION_NORTH; + } + } else { + if (dir == DIRECTION_NORTH) { + newDir = DIRECTION_EAST; + } else if (dir == DIRECTION_SOUTH) { + newDir = DIRECTION_EAST; + } else if (dir == DIRECTION_WEST) { + newDir = DIRECTION_SOUTH; + } + } + } + } + + g_game.internalCreatureTurn(this, newDir); +} + +void Monster::dropLoot(Container* corpse, Creature*) +{ + if (corpse && lootDrop) { + g_events->eventMonsterOnDropLoot(this, corpse); + } +} + +void Monster::setNormalCreatureLight() +{ + internalLight = mType->info.light; +} + +void Monster::drainHealth(Creature* attacker, int32_t damage) +{ + Creature::drainHealth(attacker, damage); + + if (damage > 0 && randomStepping) { + ignoreFieldDamage = true; + updateMapCache(); + } + + if (isInvisible()) { + removeCondition(CONDITION_INVISIBLE); + } +} + +void Monster::changeHealth(int32_t healthChange, bool sendHealthChange/* = true*/) +{ + //In case a player with ignore flag set attacks the monster + setIdle(false); + Creature::changeHealth(healthChange, sendHealthChange); +} + +bool Monster::challengeCreature(Creature* creature) +{ + if (isSummon()) { + return false; + } + + bool result = selectTarget(creature); + if (result) { + targetChangeCooldown = 8000; + challengeFocusDuration = targetChangeCooldown; + targetChangeTicks = 0; + } + return result; +} + +void Monster::getPathSearchParams(const Creature* creature, FindPathParams& fpp) const +{ + Creature::getPathSearchParams(creature, fpp); + + fpp.minTargetDist = 1; + fpp.maxTargetDist = mType->info.targetDistance; + + if (isSummon()) { + if (getMaster() == creature) { + fpp.maxTargetDist = 2; + fpp.fullPathSearch = true; + } else if (mType->info.targetDistance <= 1) { + fpp.fullPathSearch = true; + } else { + fpp.fullPathSearch = !canUseAttack(getPosition(), creature); + } + } else if (isFleeing()) { + //Distance should be higher than the client view range (Map::maxClientViewportX/Map::maxClientViewportY) + fpp.maxTargetDist = Map::maxViewportX; + fpp.clearSight = false; + fpp.keepDistance = true; + fpp.fullPathSearch = false; + } else if (mType->info.targetDistance <= 1) { + fpp.fullPathSearch = true; + } else { + fpp.fullPathSearch = !canUseAttack(getPosition(), creature); + } +} diff --git a/src/monster.h b/src/monster.h new file mode 100644 index 0000000..dd86f3c --- /dev/null +++ b/src/monster.h @@ -0,0 +1,281 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_MONSTER_H_9F5EEFE64314418CA7DA41D1B9409DD0 +#define FS_MONSTER_H_9F5EEFE64314418CA7DA41D1B9409DD0 + +#include "tile.h" +#include "monsters.h" + +class Creature; +class Game; +class Spawn; + +using CreatureHashSet = std::unordered_set; +using CreatureList = std::list; + +enum TargetSearchType_t { + TARGETSEARCH_DEFAULT, + TARGETSEARCH_RANDOM, + TARGETSEARCH_ATTACKRANGE, + TARGETSEARCH_NEAREST, +}; + +class Monster final : public Creature +{ + public: + static Monster* createMonster(const std::string& name); + static int32_t despawnRange; + static int32_t despawnRadius; + + explicit Monster(MonsterType* mType); + ~Monster(); + + // non-copyable + Monster(const Monster&) = delete; + Monster& operator=(const Monster&) = delete; + + Monster* getMonster() override { + return this; + } + const Monster* getMonster() const override { + return this; + } + + void setID() override { + if (id == 0) { + id = monsterAutoID++; + } + } + + void removeList() override; + void addList() override; + + const std::string& getName() const override { + return mType->name; + } + const std::string& getNameDescription() const override { + return mType->nameDescription; + } + std::string getDescription(int32_t) const override { + return strDescription + '.'; + } + + CreatureType_t getType() const override { + return CREATURETYPE_MONSTER; + } + + const Position& getMasterPos() const { + return masterPos; + } + void setMasterPos(Position pos) { + masterPos = pos; + } + + RaceType_t getRace() const override { + return mType->info.race; + } + int32_t getArmor() const override { + return mType->info.armor; + } + int32_t getDefense() const override { + return mType->info.defense; + } + bool isPushable() const override { + return mType->info.pushable && baseSpeed != 0; + } + bool isAttackable() const override { + return mType->info.isAttackable; + } + + bool canPushItems() const { + return mType->info.canPushItems; + } + bool canPushCreatures() const { + return mType->info.canPushCreatures; + } + bool isHostile() const { + return mType->info.isHostile; + } + bool canSee(const Position& pos) const override; + bool canSeeInvisibility() const override { + return isImmune(CONDITION_INVISIBLE); + } + uint32_t getManaCost() const { + return mType->info.manaCost; + } + void setSpawn(Spawn* spawn) { + this->spawn = spawn; + } + bool canWalkOnFieldType(CombatType_t combatType) const; + + + void onAttackedCreatureDisappear(bool isLogout) override; + + void onCreatureAppear(Creature* creature, bool isLogin) override; + void onRemoveCreature(Creature* creature, bool isLogout) override; + void onCreatureMove(Creature* creature, const Tile* newTile, const Position& newPos, const Tile* oldTile, const Position& oldPos, bool teleport) override; + void onCreatureSay(Creature* creature, SpeakClasses type, const std::string& text) override; + + void drainHealth(Creature* attacker, int32_t damage) override; + void changeHealth(int32_t healthChange, bool sendHealthChange = true) override; + void onWalk() override; + bool getNextStep(Direction& direction, uint32_t& flags) override; + void onFollowCreatureComplete(const Creature* creature) override; + + void onThink(uint32_t interval) override; + + bool challengeCreature(Creature* creature) override; + + void setNormalCreatureLight() override; + bool getCombatValues(int32_t& min, int32_t& max) override; + + void doAttacking(uint32_t interval) override; + bool hasExtraSwing() override { + return extraMeleeAttack; + } + + bool searchTarget(TargetSearchType_t searchType = TARGETSEARCH_DEFAULT); + bool selectTarget(Creature* creature); + + const CreatureList& getTargetList() const { + return targetList; + } + const CreatureHashSet& getFriendList() const { + return friendList; + } + + bool isTarget(const Creature* creature) const; + bool isFleeing() const { + return !isSummon() && getHealth() <= mType->info.runAwayHealth && challengeFocusDuration <= 0; + } + + bool getDistanceStep(const Position& targetPos, Direction& direction, bool flee = false); + bool isTargetNearby() const { + return stepDuration >= 1; + } + bool isIgnoringFieldDamage() const { + return ignoreFieldDamage; + } + + BlockType_t blockHit(Creature* attacker, CombatType_t combatType, int32_t& damage, + bool checkDefense = false, bool checkArmor = false, bool field = false) override; + + static uint32_t monsterAutoID; + + private: + CreatureHashSet friendList; + CreatureList targetList; + + std::string strDescription; + + MonsterType* mType; + Spawn* spawn = nullptr; + + int64_t lastMeleeAttack = 0; + + uint32_t attackTicks = 0; + uint32_t targetTicks = 0; + uint32_t targetChangeTicks = 0; + uint32_t defenseTicks = 0; + uint32_t yellTicks = 0; + int32_t minCombatValue = 0; + int32_t maxCombatValue = 0; + int32_t targetChangeCooldown = 0; + int32_t challengeFocusDuration = 0; + int32_t stepDuration = 0; + + Position masterPos; + + bool isIdle = true; + bool extraMeleeAttack = false; + bool isMasterInRange = false; + bool randomStepping = false; + bool ignoreFieldDamage = false; + + void onCreatureEnter(Creature* creature); + void onCreatureLeave(Creature* creature); + void onCreatureFound(Creature* creature, bool pushFront = false); + + void updateLookDirection(); + + void addFriend(Creature* creature); + void removeFriend(Creature* creature); + void addTarget(Creature* creature, bool pushFront = false); + void removeTarget(Creature* creature); + + void updateTargetList(); + void clearTargetList(); + void clearFriendList(); + + void death(Creature* lastHitCreature) override; + Item* getCorpse(Creature* lastHitCreature, Creature* mostDamageCreature) override; + + void setIdle(bool idle); + void updateIdleStatus(); + bool getIdleStatus() const { + return isIdle; + } + + void onAddCondition(ConditionType_t type) override; + void onEndCondition(ConditionType_t type) override; + + bool canUseAttack(const Position& pos, const Creature* target) const; + bool canUseSpell(const Position& pos, const Position& targetPos, + const spellBlock_t& sb, uint32_t interval, bool& inRange, bool& resetTicks); + bool getRandomStep(const Position& creaturePos, Direction& direction) const; + bool getDanceStep(const Position& creaturePos, Direction& direction, + bool keepAttack = true, bool keepDistance = true); + bool isInSpawnRange(const Position& pos) const; + bool canWalkTo(Position pos, Direction direction) const; + + static bool pushItem(Item* item); + static void pushItems(Tile* tile); + static bool pushCreature(Creature* creature); + static void pushCreatures(Tile* tile); + + void onThinkTarget(uint32_t interval); + void onThinkYell(uint32_t interval); + void onThinkDefense(uint32_t interval); + + bool isFriend(const Creature* creature) const; + bool isOpponent(const Creature* creature) const; + + uint64_t getLostExperience() const override { + return skillLoss ? mType->info.experience : 0; + } + uint16_t getLookCorpse() const override { + return mType->info.lookcorpse; + } + void dropLoot(Container* corpse, Creature* lastHitCreature) override; + uint32_t getDamageImmunities() const override { + return mType->info.damageImmunities; + } + uint32_t getConditionImmunities() const override { + return mType->info.conditionImmunities; + } + void getPathSearchParams(const Creature* creature, FindPathParams& fpp) const override; + bool useCacheMap() const override { + return !randomStepping; + } + + friend class LuaScriptInterface; +}; + +#endif diff --git a/src/monsters.cpp b/src/monsters.cpp new file mode 100644 index 0000000..4f5876c --- /dev/null +++ b/src/monsters.cpp @@ -0,0 +1,1394 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "monsters.h" +#include "monster.h" +#include "spells.h" +#include "combat.h" +#include "weapons.h" +#include "configmanager.h" +#include "game.h" + +#include "pugicast.h" + +extern Game g_game; +extern Spells* g_spells; +extern Monsters g_monsters; +extern ConfigManager g_config; + +spellBlock_t::~spellBlock_t() +{ + if (combatSpell) { + delete spell; + } +} + +void MonsterType::loadLoot(MonsterType* monsterType, LootBlock lootBlock) +{ + if (lootBlock.childLoot.empty()) { + bool isContainer = Item::items[lootBlock.id].isContainer(); + if (isContainer) { + for (LootBlock child : lootBlock.childLoot) { + lootBlock.childLoot.push_back(child); + } + } + monsterType->info.lootItems.push_back(lootBlock); + } else { + monsterType->info.lootItems.push_back(lootBlock); + } +} + +bool Monsters::loadFromXml(bool reloading /*= false*/) +{ + unloadedMonsters = {}; + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/monster/monsters.xml"); + if (!result) { + printXMLError("Error - Monsters::loadFromXml", "data/monster/monsters.xml", result); + return false; + } + + loaded = true; + + for (auto monsterNode : doc.child("monsters").children()) { + std::string name = asLowerCaseString(monsterNode.attribute("name").as_string()); + std::string file = "data/monster/" + std::string(monsterNode.attribute("file").as_string()); + auto forceLoad = g_config.getBoolean(ConfigManager::FORCE_MONSTERTYPE_LOAD); + if (forceLoad) { + loadMonster(file, name, true); + continue; + } + + if (reloading && monsters.find(name) != monsters.end()) { + loadMonster(file, name, true); + } else { + unloadedMonsters.emplace(name, file); + } + } + return true; +} + +bool Monsters::reload() +{ + loaded = false; + + scriptInterface.reset(); + + return loadFromXml(true); +} + +ConditionDamage* Monsters::getDamageCondition(ConditionType_t conditionType, + int32_t maxDamage, int32_t minDamage, int32_t startDamage, uint32_t tickInterval) +{ + ConditionDamage* condition = static_cast(Condition::createCondition(CONDITIONID_COMBAT, conditionType, 0, 0)); + condition->setParam(CONDITION_PARAM_TICKINTERVAL, tickInterval); + condition->setParam(CONDITION_PARAM_MINVALUE, minDamage); + condition->setParam(CONDITION_PARAM_MAXVALUE, maxDamage); + condition->setParam(CONDITION_PARAM_STARTVALUE, startDamage); + condition->setParam(CONDITION_PARAM_DELAYED, 1); + return condition; +} + +bool Monsters::deserializeSpell(const pugi::xml_node& node, spellBlock_t& sb, const std::string& description) +{ + std::string name; + std::string scriptName; + bool isScripted; + + pugi::xml_attribute attr; + if ((attr = node.attribute("script"))) { + scriptName = attr.as_string(); + isScripted = true; + } else if ((attr = node.attribute("name"))) { + name = attr.as_string(); + isScripted = false; + } else { + return false; + } + + if ((attr = node.attribute("speed")) || (attr = node.attribute("interval"))) { + sb.speed = std::max(1, pugi::cast(attr.value())); + } + + if ((attr = node.attribute("chance"))) { + uint32_t chance = pugi::cast(attr.value()); + if (chance > 100) { + chance = 100; + } + sb.chance = chance; + } + + if ((attr = node.attribute("range"))) { + uint32_t range = pugi::cast(attr.value()); + if (range > (Map::maxViewportX * 2)) { + range = Map::maxViewportX * 2; + } + sb.range = range; + } + + if ((attr = node.attribute("min"))) { + sb.minCombatValue = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("max"))) { + sb.maxCombatValue = pugi::cast(attr.value()); + + //normalize values + if (std::abs(sb.minCombatValue) > std::abs(sb.maxCombatValue)) { + int32_t value = sb.maxCombatValue; + sb.maxCombatValue = sb.minCombatValue; + sb.minCombatValue = value; + } + } + + if (auto spell = g_spells->getSpellByName(name)) { + sb.spell = spell; + return true; + } + + CombatSpell* combatSpell = nullptr; + bool needTarget = false; + bool needDirection = false; + + if (isScripted) { + if ((attr = node.attribute("direction"))) { + needDirection = attr.as_bool(); + } + + if ((attr = node.attribute("target"))) { + needTarget = attr.as_bool(); + } + + std::unique_ptr combatSpellPtr(new CombatSpell(nullptr, needTarget, needDirection)); + if (!combatSpellPtr->loadScript("data/" + g_spells->getScriptBaseName() + "/scripts/" + scriptName)) { + return false; + } + + if (!combatSpellPtr->loadScriptCombat()) { + return false; + } + + combatSpell = combatSpellPtr.release(); + combatSpell->getCombat()->setPlayerCombatValues(COMBAT_FORMULA_DAMAGE, sb.minCombatValue, 0, sb.maxCombatValue, 0); + } else { + Combat* combat = new Combat; + if ((attr = node.attribute("length"))) { + int32_t length = pugi::cast(attr.value()); + if (length > 0) { + int32_t spread = 3; + + //need direction spell + if ((attr = node.attribute("spread"))) { + spread = std::max(0, pugi::cast(attr.value())); + } + + AreaCombat* area = new AreaCombat(); + area->setupArea(length, spread); + combat->setArea(area); + + needDirection = true; + } + } + + if ((attr = node.attribute("radius"))) { + int32_t radius = pugi::cast(attr.value()); + + //target spell + if ((attr = node.attribute("target"))) { + needTarget = attr.as_bool(); + } + + AreaCombat* area = new AreaCombat(); + area->setupArea(radius); + combat->setArea(area); + } + + std::string tmpName = asLowerCaseString(name); + + if (tmpName == "melee") { + sb.isMelee = true; + + pugi::xml_attribute attackAttribute, skillAttribute; + if ((attackAttribute = node.attribute("attack")) && (skillAttribute = node.attribute("skill"))) { + sb.minCombatValue = 0; + sb.maxCombatValue = -Weapons::getMaxMeleeDamage(pugi::cast(skillAttribute.value()), pugi::cast(attackAttribute.value())); + } + + ConditionType_t conditionType = CONDITION_NONE; + int32_t minDamage = 0; + int32_t maxDamage = 0; + uint32_t tickInterval = 2000; + + if ((attr = node.attribute("fire"))) { + conditionType = CONDITION_FIRE; + + minDamage = pugi::cast(attr.value()); + maxDamage = minDamage; + tickInterval = 9000; + } else if ((attr = node.attribute("poison"))) { + conditionType = CONDITION_POISON; + + minDamage = pugi::cast(attr.value()); + maxDamage = minDamage; + tickInterval = 4000; + } else if ((attr = node.attribute("energy"))) { + conditionType = CONDITION_ENERGY; + + minDamage = pugi::cast(attr.value()); + maxDamage = minDamage; + tickInterval = 10000; + } else if ((attr = node.attribute("drown"))) { + conditionType = CONDITION_DROWN; + + minDamage = pugi::cast(attr.value()); + maxDamage = minDamage; + tickInterval = 5000; + } else if ((attr = node.attribute("freeze"))) { + conditionType = CONDITION_FREEZING; + + minDamage = pugi::cast(attr.value()); + maxDamage = minDamage; + tickInterval = 8000; + } else if ((attr = node.attribute("dazzle"))) { + conditionType = CONDITION_DAZZLED; + + minDamage = pugi::cast(attr.value()); + maxDamage = minDamage; + tickInterval = 10000; + } else if ((attr = node.attribute("curse"))) { + conditionType = CONDITION_CURSED; + + minDamage = pugi::cast(attr.value()); + maxDamage = minDamage; + tickInterval = 4000; + } else if ((attr = node.attribute("bleed")) || (attr = node.attribute("physical"))) { + conditionType = CONDITION_BLEEDING; + tickInterval = 4000; + } + + if ((attr = node.attribute("tick"))) { + int32_t value = pugi::cast(attr.value()); + if (value > 0) { + tickInterval = value; + } + } + + if (conditionType != CONDITION_NONE) { + Condition* condition = getDamageCondition(conditionType, maxDamage, minDamage, 0, tickInterval); + combat->addCondition(condition); + } + + sb.range = 1; + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE); + combat->setParam(COMBAT_PARAM_BLOCKARMOR, 1); + combat->setParam(COMBAT_PARAM_BLOCKSHIELD, 1); + combat->setOrigin(ORIGIN_MELEE); + } else if (tmpName == "physical") { + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE); + combat->setParam(COMBAT_PARAM_BLOCKARMOR, 1); + combat->setOrigin(ORIGIN_RANGED); + } else if (tmpName == "bleed") { + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE); + } else if (tmpName == "poison" || tmpName == "earth") { + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE); + } else if (tmpName == "fire") { + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE); + } else if (tmpName == "energy") { + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_ENERGYDAMAGE); + } else if (tmpName == "drown") { + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_DROWNDAMAGE); + } else if (tmpName == "ice") { + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_ICEDAMAGE); + } else if (tmpName == "holy") { + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_HOLYDAMAGE); + } else if (tmpName == "death") { + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_DEATHDAMAGE); + } else if (tmpName == "lifedrain") { + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_LIFEDRAIN); + } else if (tmpName == "manadrain") { + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_MANADRAIN); + } else if (tmpName == "healing") { + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_HEALING); + combat->setParam(COMBAT_PARAM_AGGRESSIVE, 0); + } else if (tmpName == "speed") { + int32_t minSpeedChange = 0; + int32_t maxSpeedChange = 0; + int32_t duration = 10000; + + if ((attr = node.attribute("duration"))) { + duration = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("speedchange"))) { + minSpeedChange = pugi::cast(attr.value()); + } else if ((attr = node.attribute("minspeedchange"))) { + minSpeedChange = pugi::cast(attr.value()); + + if ((attr = node.attribute("maxspeedchange"))) { + maxSpeedChange = pugi::cast(attr.value()); + } + + if (minSpeedChange == 0) { + std::cout << "[Error - Monsters::deserializeSpell] - " << description << " - missing speedchange/minspeedchange value" << std::endl; + delete combat; + return false; + } + + if (minSpeedChange < -1000) { + std::cout << "[Warning - Monsters::deserializeSpell] - " << description << " - you cannot reduce a creatures speed below -1000 (100%)" << std::endl; + minSpeedChange = -1000; + } + + if (maxSpeedChange == 0) { + maxSpeedChange = minSpeedChange; // static speedchange value + } + } + + ConditionType_t conditionType; + if (minSpeedChange >= 0) { + conditionType = CONDITION_HASTE; + combat->setParam(COMBAT_PARAM_AGGRESSIVE, 0); + } else { + conditionType = CONDITION_PARALYZE; + } + + ConditionSpeed* condition = static_cast(Condition::createCondition(CONDITIONID_COMBAT, conditionType, duration, 0)); + condition->setFormulaVars(minSpeedChange / 1000.0, 0, maxSpeedChange / 1000.0, 0); + combat->addCondition(condition); + } else if (tmpName == "outfit") { + int32_t duration = 10000; + + if ((attr = node.attribute("duration"))) { + duration = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("monster"))) { + MonsterType* mType = g_monsters.getMonsterType(attr.as_string()); + if (mType) { + ConditionOutfit* condition = static_cast(Condition::createCondition(CONDITIONID_COMBAT, CONDITION_OUTFIT, duration, 0)); + condition->setOutfit(mType->info.outfit); + combat->setParam(COMBAT_PARAM_AGGRESSIVE, 0); + combat->addCondition(condition); + } + } else if ((attr = node.attribute("item"))) { + Outfit_t outfit; + outfit.lookTypeEx = pugi::cast(attr.value()); + + ConditionOutfit* condition = static_cast(Condition::createCondition(CONDITIONID_COMBAT, CONDITION_OUTFIT, duration, 0)); + condition->setOutfit(outfit); + combat->setParam(COMBAT_PARAM_AGGRESSIVE, 0); + combat->addCondition(condition); + } + } else if (tmpName == "invisible") { + int32_t duration = 10000; + + if ((attr = node.attribute("duration"))) { + duration = pugi::cast(attr.value()); + } + + Condition* condition = Condition::createCondition(CONDITIONID_COMBAT, CONDITION_INVISIBLE, duration, 0); + combat->setParam(COMBAT_PARAM_AGGRESSIVE, 0); + combat->addCondition(condition); + } else if (tmpName == "drunk") { + int32_t duration = 10000; + + if ((attr = node.attribute("duration"))) { + duration = pugi::cast(attr.value()); + } + + Condition* condition = Condition::createCondition(CONDITIONID_COMBAT, CONDITION_DRUNK, duration, 0); + combat->addCondition(condition); + } else if (tmpName == "firefield") { + combat->setParam(COMBAT_PARAM_CREATEITEM, ITEM_FIREFIELD_PVP_FULL); + } else if (tmpName == "poisonfield") { + combat->setParam(COMBAT_PARAM_CREATEITEM, ITEM_POISONFIELD_PVP); + } else if (tmpName == "energyfield") { + combat->setParam(COMBAT_PARAM_CREATEITEM, ITEM_ENERGYFIELD_PVP); + } else if (tmpName == "firecondition" || tmpName == "energycondition" || + tmpName == "earthcondition" || tmpName == "poisoncondition" || + tmpName == "icecondition" || tmpName == "freezecondition" || + tmpName == "deathcondition" || tmpName == "cursecondition" || + tmpName == "holycondition" || tmpName == "dazzlecondition" || + tmpName == "drowncondition" || tmpName == "bleedcondition" || + tmpName == "physicalcondition") { + ConditionType_t conditionType = CONDITION_NONE; + uint32_t tickInterval = 2000; + + if (tmpName == "firecondition") { + conditionType = CONDITION_FIRE; + tickInterval = 10000; + } else if (tmpName == "poisoncondition" || tmpName == "earthcondition") { + conditionType = CONDITION_POISON; + tickInterval = 4000; + } else if (tmpName == "energycondition") { + conditionType = CONDITION_ENERGY; + tickInterval = 10000; + } else if (tmpName == "drowncondition") { + conditionType = CONDITION_DROWN; + tickInterval = 5000; + } else if (tmpName == "freezecondition" || tmpName == "icecondition") { + conditionType = CONDITION_FREEZING; + tickInterval = 10000; + } else if (tmpName == "cursecondition" || tmpName == "deathcondition") { + conditionType = CONDITION_CURSED; + tickInterval = 4000; + } else if (tmpName == "dazzlecondition" || tmpName == "holycondition") { + conditionType = CONDITION_DAZZLED; + tickInterval = 10000; + } else if (tmpName == "physicalcondition" || tmpName == "bleedcondition") { + conditionType = CONDITION_BLEEDING; + tickInterval = 4000; + } + + if ((attr = node.attribute("tick"))) { + int32_t value = pugi::cast(attr.value()); + if (value > 0) { + tickInterval = value; + } + } + + int32_t minDamage = std::abs(sb.minCombatValue); + int32_t maxDamage = std::abs(sb.maxCombatValue); + int32_t startDamage = 0; + + if ((attr = node.attribute("start"))) { + int32_t value = std::abs(pugi::cast(attr.value())); + if (value <= minDamage) { + startDamage = value; + } + } + + Condition* condition = getDamageCondition(conditionType, maxDamage, minDamage, startDamage, tickInterval); + combat->addCondition(condition); + } else if (tmpName == "strength") { + // + } else if (tmpName == "effect") { + // + } else { + std::cout << "[Error - Monsters::deserializeSpell] - " << description << " - Unknown spell name: " << name << std::endl; + delete combat; + return false; + } + + combat->setPlayerCombatValues(COMBAT_FORMULA_DAMAGE, sb.minCombatValue, 0, sb.maxCombatValue, 0); + combatSpell = new CombatSpell(combat, needTarget, needDirection); + + for (auto attributeNode : node.children()) { + if ((attr = attributeNode.attribute("key"))) { + const char* value = attr.value(); + if (strcasecmp(value, "shooteffect") == 0) { + if ((attr = attributeNode.attribute("value"))) { + ShootType_t shoot = getShootType(asLowerCaseString(attr.as_string())); + if (shoot != CONST_ANI_NONE) { + combat->setParam(COMBAT_PARAM_DISTANCEEFFECT, shoot); + } else { + std::cout << "[Warning - Monsters::deserializeSpell] " << description << " - Unknown shootEffect: " << attr.as_string() << std::endl; + } + } + } else if (strcasecmp(value, "areaeffect") == 0) { + if ((attr = attributeNode.attribute("value"))) { + MagicEffectClasses effect = getMagicEffect(asLowerCaseString(attr.as_string())); + if (effect != CONST_ME_NONE) { + combat->setParam(COMBAT_PARAM_EFFECT, effect); + } else { + std::cout << "[Warning - Monsters::deserializeSpell] " << description << " - Unknown areaEffect: " << attr.as_string() << std::endl; + } + } + } else { + std::cout << "[Warning - Monsters::deserializeSpells] Effect type \"" << attr.as_string() << "\" does not exist." << std::endl; + } + } + } + } + + sb.spell = combatSpell; + if (combatSpell) { + sb.combatSpell = true; + } + return true; +} + +bool Monsters::deserializeSpell(MonsterSpell* spell, spellBlock_t& sb, const std::string& description) +{ + if (!spell->scriptName.empty()) { + spell->isScripted = true; + } else if (!spell->name.empty()) { + spell->isScripted = false; + } else { + return false; + } + + sb.speed = spell->interval; + + if (spell->chance > 100) { + sb.chance = 100; + } else { + sb.chance = spell->chance; + } + + if (spell->range > (Map::maxViewportX * 2)) { + spell->range = Map::maxViewportX * 2; + } + sb.range = spell->range; + + sb.minCombatValue = spell->minCombatValue; + sb.maxCombatValue = spell->maxCombatValue; + if (std::abs(sb.minCombatValue) > std::abs(sb.maxCombatValue)) { + int32_t value = sb.maxCombatValue; + sb.maxCombatValue = sb.minCombatValue; + sb.minCombatValue = value; + } + + sb.spell = g_spells->getSpellByName(spell->name); + if (sb.spell) { + return true; + } + + CombatSpell* combatSpell = nullptr; + + if (spell->isScripted) { + std::unique_ptr combatSpellPtr(new CombatSpell(nullptr, spell->needTarget, spell->needDirection)); + if (!combatSpellPtr->loadScript("data/" + g_spells->getScriptBaseName() + "/scripts/" + spell->scriptName)) { + std::cout << "cannot find file" << std::endl; + return false; + } + + if (!combatSpellPtr->loadScriptCombat()) { + return false; + } + + combatSpell = combatSpellPtr.release(); + combatSpell->getCombat()->setPlayerCombatValues(COMBAT_FORMULA_DAMAGE, sb.minCombatValue, 0, sb.maxCombatValue, 0); + } else { + std::unique_ptr combat{ new Combat }; + sb.combatSpell = true; + + if (spell->length > 0) { + spell->spread = std::max(0, spell->spread); + + AreaCombat* area = new AreaCombat(); + area->setupArea(spell->length, spell->spread); + combat->setArea(area); + + spell->needDirection = true; + } + + if (spell->radius > 0) { + AreaCombat* area = new AreaCombat(); + area->setupArea(spell->radius); + combat->setArea(area); + } + + std::string tmpName = asLowerCaseString(spell->name); + + if (tmpName == "melee") { + sb.isMelee = true; + + if (spell->attack > 0 && spell->skill > 0) { + sb.minCombatValue = 0; + sb.maxCombatValue = -Weapons::getMaxMeleeDamage(spell->skill, spell->attack); + } + + ConditionType_t conditionType = CONDITION_NONE; + int32_t minDamage = 0; + int32_t maxDamage = 0; + uint32_t tickInterval = 2000; + + if (spell->conditionType != CONDITION_NONE) { + conditionType = spell->conditionType; + + minDamage = spell->conditionMinDamage; + maxDamage = minDamage; + if (spell->tickInterval != 0) { + tickInterval = spell->tickInterval; + } + + Condition* condition = getDamageCondition(conditionType, maxDamage, minDamage, spell->conditionStartDamage, tickInterval); + combat->addCondition(condition); + } + + sb.range = 1; + combat->setParam(COMBAT_PARAM_TYPE, COMBAT_PHYSICALDAMAGE); + combat->setParam(COMBAT_PARAM_BLOCKARMOR, 1); + combat->setParam(COMBAT_PARAM_BLOCKSHIELD, 1); + combat->setOrigin(ORIGIN_MELEE); + } else if (tmpName == "combat") { + if (spell->combatType == COMBAT_PHYSICALDAMAGE) { + combat->setParam(COMBAT_PARAM_BLOCKARMOR, 1); + combat->setOrigin(ORIGIN_RANGED); + } else if (spell->combatType == COMBAT_HEALING) { + combat->setParam(COMBAT_PARAM_AGGRESSIVE, 0); + } + combat->setParam(COMBAT_PARAM_TYPE, spell->combatType); + } else if (tmpName == "speed") { + int32_t minSpeedChange = 0; + int32_t maxSpeedChange = 0; + int32_t duration = 10000; + + if (spell->duration != 0) { + duration = spell->duration; + } + + if (spell->minSpeedChange != 0) { + minSpeedChange = spell->minSpeedChange; + } else { + std::cout << "[Error - Monsters::deserializeSpell] - " << description << " - missing speedchange/minspeedchange value" << std::endl; + delete spell; + return false; + } + + if (minSpeedChange < -1000) { + std::cout << "[Warning - Monsters::deserializeSpell] - " << description << " - you cannot reduce a creatures speed below -1000 (100%)" << std::endl; + minSpeedChange = -1000; + } + + if (spell->maxSpeedChange != 0) { + maxSpeedChange = spell->maxSpeedChange; + } else { + maxSpeedChange = minSpeedChange; // static speedchange value + } + + ConditionType_t conditionType; + if (minSpeedChange >= 0) { + conditionType = CONDITION_HASTE; + combat->setParam(COMBAT_PARAM_AGGRESSIVE, 0); + } else { + conditionType = CONDITION_PARALYZE; + } + + ConditionSpeed* condition = static_cast(Condition::createCondition(CONDITIONID_COMBAT, conditionType, duration, 0)); + condition->setFormulaVars(minSpeedChange / 1000.0, 0, maxSpeedChange / 1000.0, 0); + combat->addCondition(condition); + } else if (tmpName == "outfit") { + int32_t duration = 10000; + + if (spell->duration != 0) { + duration = spell->duration; + } + + ConditionOutfit* condition = static_cast(Condition::createCondition(CONDITIONID_COMBAT, CONDITION_OUTFIT, duration, 0)); + condition->setOutfit(spell->outfit); + combat->setParam(COMBAT_PARAM_AGGRESSIVE, 0); + combat->addCondition(condition); + } else if (tmpName == "invisible") { + int32_t duration = 10000; + + if (spell->duration != 0) { + duration = spell->duration; + } + + Condition* condition = Condition::createCondition(CONDITIONID_COMBAT, CONDITION_INVISIBLE, duration, 0); + combat->setParam(COMBAT_PARAM_AGGRESSIVE, 0); + combat->addCondition(condition); + } else if (tmpName == "drunk") { + int32_t duration = 10000; + + if (spell->duration != 0) { + duration = spell->duration; + } + + Condition* condition = Condition::createCondition(CONDITIONID_COMBAT, CONDITION_DRUNK, duration, 0); + combat->addCondition(condition); + } else if (tmpName == "firefield") { + combat->setParam(COMBAT_PARAM_CREATEITEM, ITEM_FIREFIELD_PVP_FULL); + } else if (tmpName == "poisonfield") { + combat->setParam(COMBAT_PARAM_CREATEITEM, ITEM_POISONFIELD_PVP); + } else if (tmpName == "energyfield") { + combat->setParam(COMBAT_PARAM_CREATEITEM, ITEM_ENERGYFIELD_PVP); + } else if (tmpName == "condition") { + uint32_t tickInterval = 2000; + + if (spell->conditionType == CONDITION_NONE) { + std::cout << "[Error - Monsters::deserializeSpell] - " << description << " - Condition is not set for: " << spell->name << std::endl; + } + + if (spell->tickInterval != 0) { + int32_t value = spell->tickInterval; + if (value > 0) { + tickInterval = value; + } + } + + int32_t minDamage = std::abs(spell->conditionMinDamage); + int32_t maxDamage = std::abs(spell->conditionMaxDamage); + int32_t startDamage = 0; + + if (spell->conditionStartDamage != 0) { + int32_t value = std::abs(spell->conditionStartDamage); + if (value <= minDamage) { + startDamage = value; + } + } + + Condition* condition = getDamageCondition(spell->conditionType, maxDamage, minDamage, startDamage, tickInterval); + combat->addCondition(condition); + } else if (tmpName == "strength") { + // + } else if (tmpName == "effect") { + // + } else { + std::cout << "[Error - Monsters::deserializeSpell] - " << description << " - Unknown spell name: " << spell->name << std::endl; + } + + if (spell->needTarget) { + if (spell->shoot != CONST_ANI_NONE) { + combat->setParam(COMBAT_PARAM_DISTANCEEFFECT, spell->shoot); + } + } + + if (spell->effect != CONST_ME_NONE) { + combat->setParam(COMBAT_PARAM_EFFECT, spell->effect); + } + + combat->setPlayerCombatValues(COMBAT_FORMULA_DAMAGE, sb.minCombatValue, 0, sb.maxCombatValue, 0); + combatSpell = new CombatSpell(combat.release(), spell->needTarget, spell->needDirection); + } + + sb.spell = combatSpell; + if (combatSpell) { + sb.combatSpell = true; + } + return true; +} + +MonsterType* Monsters::loadMonster(const std::string& file, const std::string& monsterName, bool reloading /*= false*/) +{ + MonsterType* mType = nullptr; + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(file.c_str()); + if (!result) { + printXMLError("Error - Monsters::loadMonster", file, result); + return nullptr; + } + + pugi::xml_node monsterNode = doc.child("monster"); + if (!monsterNode) { + std::cout << "[Error - Monsters::loadMonster] Missing monster node in: " << file << std::endl; + return nullptr; + } + + pugi::xml_attribute attr; + if (!(attr = monsterNode.attribute("name"))) { + std::cout << "[Error - Monsters::loadMonster] Missing name in: " << file << std::endl; + return nullptr; + } + + if (reloading) { + auto it = monsters.find(asLowerCaseString(monsterName)); + if (it != monsters.end()) { + mType = &it->second; + mType->info = {}; + } + } + + if (!mType) { + mType = &monsters[asLowerCaseString(monsterName)]; + } + + mType->name = attr.as_string(); + + if ((attr = monsterNode.attribute("nameDescription"))) { + mType->nameDescription = attr.as_string(); + } else { + mType->nameDescription = "a " + asLowerCaseString(mType->name); + } + + if ((attr = monsterNode.attribute("race"))) { + std::string tmpStrValue = asLowerCaseString(attr.as_string()); + uint16_t tmpInt = pugi::cast(attr.value()); + if (tmpStrValue == "venom" || tmpInt == 1) { + mType->info.race = RACE_VENOM; + } else if (tmpStrValue == "blood" || tmpInt == 2) { + mType->info.race = RACE_BLOOD; + } else if (tmpStrValue == "undead" || tmpInt == 3) { + mType->info.race = RACE_UNDEAD; + } else if (tmpStrValue == "fire" || tmpInt == 4) { + mType->info.race = RACE_FIRE; + } else if (tmpStrValue == "energy" || tmpInt == 5) { + mType->info.race = RACE_ENERGY; + } else { + std::cout << "[Warning - Monsters::loadMonster] Unknown race type " << attr.as_string() << ". " << file << std::endl; + } + } + + if ((attr = monsterNode.attribute("experience"))) { + mType->info.experience = pugi::cast(attr.value()); + } + + if ((attr = monsterNode.attribute("speed"))) { + mType->info.baseSpeed = pugi::cast(attr.value()); + } + + if ((attr = monsterNode.attribute("manacost"))) { + mType->info.manaCost = pugi::cast(attr.value()); + } + + if ((attr = monsterNode.attribute("skull"))) { + mType->info.skull = getSkullType(asLowerCaseString(attr.as_string())); + } + + if ((attr = monsterNode.attribute("script"))) { + if (!scriptInterface) { + scriptInterface.reset(new LuaScriptInterface("Monster Interface")); + scriptInterface->initState(); + } + + std::string script = attr.as_string(); + if (scriptInterface->loadFile("data/monster/scripts/" + script) == 0) { + mType->info.scriptInterface = scriptInterface.get(); + mType->info.creatureAppearEvent = scriptInterface->getEvent("onCreatureAppear"); + mType->info.creatureDisappearEvent = scriptInterface->getEvent("onCreatureDisappear"); + mType->info.creatureMoveEvent = scriptInterface->getEvent("onCreatureMove"); + mType->info.creatureSayEvent = scriptInterface->getEvent("onCreatureSay"); + mType->info.thinkEvent = scriptInterface->getEvent("onThink"); + } else { + std::cout << "[Warning - Monsters::loadMonster] Can not load script: " << script << std::endl; + std::cout << scriptInterface->getLastLuaError() << std::endl; + } + } + + pugi::xml_node node; + if ((node = monsterNode.child("health"))) { + if ((attr = node.attribute("now"))) { + mType->info.health = pugi::cast(attr.value()); + } else { + std::cout << "[Error - Monsters::loadMonster] Missing health now. " << file << std::endl; + } + + if ((attr = node.attribute("max"))) { + mType->info.healthMax = pugi::cast(attr.value()); + } else { + std::cout << "[Error - Monsters::loadMonster] Missing health max. " << file << std::endl; + } + } + + if ((node = monsterNode.child("flags"))) { + for (auto flagNode : node.children()) { + attr = flagNode.first_attribute(); + const char* attrName = attr.name(); + if (strcasecmp(attrName, "summonable") == 0) { + mType->info.isSummonable = attr.as_bool(); + } else if (strcasecmp(attrName, "attackable") == 0) { + mType->info.isAttackable = attr.as_bool(); + } else if (strcasecmp(attrName, "hostile") == 0) { + mType->info.isHostile = attr.as_bool(); + } else if (strcasecmp(attrName, "illusionable") == 0) { + mType->info.isIllusionable = attr.as_bool(); + } else if (strcasecmp(attrName, "convinceable") == 0) { + mType->info.isConvinceable = attr.as_bool(); + } else if (strcasecmp(attrName, "pushable") == 0) { + mType->info.pushable = attr.as_bool(); + } else if (strcasecmp(attrName, "isboss") == 0) { + mType->info.isBoss = attr.as_bool(); + } else if (strcasecmp(attrName, "canpushitems") == 0) { + mType->info.canPushItems = attr.as_bool(); + } else if (strcasecmp(attrName, "canpushcreatures") == 0) { + mType->info.canPushCreatures = attr.as_bool(); + } else if (strcasecmp(attrName, "staticattack") == 0) { + uint32_t staticAttack = pugi::cast(attr.value()); + if (staticAttack > 100) { + std::cout << "[Warning - Monsters::loadMonster] staticattack greater than 100. " << file << std::endl; + staticAttack = 100; + } + + mType->info.staticAttackChance = staticAttack; + } else if (strcasecmp(attrName, "lightlevel") == 0) { + mType->info.light.level = pugi::cast(attr.value()); + } else if (strcasecmp(attrName, "lightcolor") == 0) { + mType->info.light.color = pugi::cast(attr.value()); + } else if (strcasecmp(attrName, "targetdistance") == 0) { + mType->info.targetDistance = std::max(1, pugi::cast(attr.value())); + } else if (strcasecmp(attrName, "runonhealth") == 0) { + mType->info.runAwayHealth = pugi::cast(attr.value()); + } else if (strcasecmp(attrName, "hidehealth") == 0) { + mType->info.hiddenHealth = attr.as_bool(); + } else if (strcasecmp(attrName, "canwalkonenergy") == 0) { + mType->info.canWalkOnEnergy = attr.as_bool(); + } else if (strcasecmp(attrName, "canwalkonfire") == 0) { + mType->info.canWalkOnFire = attr.as_bool(); + } else if (strcasecmp(attrName, "canwalkonpoison") == 0) { + mType->info.canWalkOnPoison = attr.as_bool(); + } else { + std::cout << "[Warning - Monsters::loadMonster] Unknown flag attribute: " << attrName << ". " << file << std::endl; + } + } + + //if a monster can push creatures, + // it should not be pushable + if (mType->info.canPushCreatures) { + mType->info.pushable = false; + } + } + + if ((node = monsterNode.child("targetchange"))) { + if ((attr = node.attribute("speed")) || (attr = node.attribute("interval"))) { + mType->info.changeTargetSpeed = pugi::cast(attr.value()); + } else { + std::cout << "[Warning - Monsters::loadMonster] Missing targetchange speed. " << file << std::endl; + } + + if ((attr = node.attribute("chance"))) { + mType->info.changeTargetChance = pugi::cast(attr.value()); + } else { + std::cout << "[Warning - Monsters::loadMonster] Missing targetchange chance. " << file << std::endl; + } + } + + if ((node = monsterNode.child("look"))) { + if ((attr = node.attribute("type"))) { + mType->info.outfit.lookType = pugi::cast(attr.value()); + + if ((attr = node.attribute("head"))) { + mType->info.outfit.lookHead = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("body"))) { + mType->info.outfit.lookBody = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("legs"))) { + mType->info.outfit.lookLegs = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("feet"))) { + mType->info.outfit.lookFeet = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("addons"))) { + mType->info.outfit.lookAddons = pugi::cast(attr.value()); + } + } else if ((attr = node.attribute("typeex"))) { + mType->info.outfit.lookTypeEx = pugi::cast(attr.value()); + } else { + std::cout << "[Warning - Monsters::loadMonster] Missing look type/typeex. " << file << std::endl; + } + + if ((attr = node.attribute("mount"))) { + mType->info.outfit.lookMount = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("corpse"))) { + mType->info.lookcorpse = pugi::cast(attr.value()); + } + } + + if ((node = monsterNode.child("attacks"))) { + for (auto attackNode : node.children()) { + spellBlock_t sb; + if (deserializeSpell(attackNode, sb, monsterName)) { + mType->info.attackSpells.emplace_back(std::move(sb)); + } else { + std::cout << "[Warning - Monsters::loadMonster] Cant load spell. " << file << std::endl; + } + } + } + + if ((node = monsterNode.child("defenses"))) { + if ((attr = node.attribute("defense"))) { + mType->info.defense = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("armor"))) { + mType->info.armor = pugi::cast(attr.value()); + } + + for (auto defenseNode : node.children()) { + spellBlock_t sb; + if (deserializeSpell(defenseNode, sb, monsterName)) { + mType->info.defenseSpells.emplace_back(std::move(sb)); + } else { + std::cout << "[Warning - Monsters::loadMonster] Cant load spell. " << file << std::endl; + } + } + } + + if ((node = monsterNode.child("immunities"))) { + for (auto immunityNode : node.children()) { + if ((attr = immunityNode.attribute("name"))) { + std::string tmpStrValue = asLowerCaseString(attr.as_string()); + if (tmpStrValue == "physical") { + mType->info.damageImmunities |= COMBAT_PHYSICALDAMAGE; + mType->info.conditionImmunities |= CONDITION_BLEEDING; + } else if (tmpStrValue == "energy") { + mType->info.damageImmunities |= COMBAT_ENERGYDAMAGE; + mType->info.conditionImmunities |= CONDITION_ENERGY; + } else if (tmpStrValue == "fire") { + mType->info.damageImmunities |= COMBAT_FIREDAMAGE; + mType->info.conditionImmunities |= CONDITION_FIRE; + } else if (tmpStrValue == "poison" || + tmpStrValue == "earth") { + mType->info.damageImmunities |= COMBAT_EARTHDAMAGE; + mType->info.conditionImmunities |= CONDITION_POISON; + } else if (tmpStrValue == "drown") { + mType->info.damageImmunities |= COMBAT_DROWNDAMAGE; + mType->info.conditionImmunities |= CONDITION_DROWN; + } else if (tmpStrValue == "ice") { + mType->info.damageImmunities |= COMBAT_ICEDAMAGE; + mType->info.conditionImmunities |= CONDITION_FREEZING; + } else if (tmpStrValue == "holy") { + mType->info.damageImmunities |= COMBAT_HOLYDAMAGE; + mType->info.conditionImmunities |= CONDITION_DAZZLED; + } else if (tmpStrValue == "death") { + mType->info.damageImmunities |= COMBAT_DEATHDAMAGE; + mType->info.conditionImmunities |= CONDITION_CURSED; + } else if (tmpStrValue == "lifedrain") { + mType->info.damageImmunities |= COMBAT_LIFEDRAIN; + } else if (tmpStrValue == "manadrain") { + mType->info.damageImmunities |= COMBAT_MANADRAIN; + } else if (tmpStrValue == "paralyze") { + mType->info.conditionImmunities |= CONDITION_PARALYZE; + } else if (tmpStrValue == "outfit") { + mType->info.conditionImmunities |= CONDITION_OUTFIT; + } else if (tmpStrValue == "drunk") { + mType->info.conditionImmunities |= CONDITION_DRUNK; + } else if (tmpStrValue == "invisible" || tmpStrValue == "invisibility") { + mType->info.conditionImmunities |= CONDITION_INVISIBLE; + } else if (tmpStrValue == "bleed") { + mType->info.conditionImmunities |= CONDITION_BLEEDING; + } else { + std::cout << "[Warning - Monsters::loadMonster] Unknown immunity name " << attr.as_string() << ". " << file << std::endl; + } + } else if ((attr = immunityNode.attribute("physical"))) { + if (attr.as_bool()) { + mType->info.damageImmunities |= COMBAT_PHYSICALDAMAGE; + mType->info.conditionImmunities |= CONDITION_BLEEDING; + } + } else if ((attr = immunityNode.attribute("energy"))) { + if (attr.as_bool()) { + mType->info.damageImmunities |= COMBAT_ENERGYDAMAGE; + mType->info.conditionImmunities |= CONDITION_ENERGY; + } + } else if ((attr = immunityNode.attribute("fire"))) { + if (attr.as_bool()) { + mType->info.damageImmunities |= COMBAT_FIREDAMAGE; + mType->info.conditionImmunities |= CONDITION_FIRE; + } + } else if ((attr = immunityNode.attribute("poison")) || (attr = immunityNode.attribute("earth"))) { + if (attr.as_bool()) { + mType->info.damageImmunities |= COMBAT_EARTHDAMAGE; + mType->info.conditionImmunities |= CONDITION_POISON; + } + } else if ((attr = immunityNode.attribute("drown"))) { + if (attr.as_bool()) { + mType->info.damageImmunities |= COMBAT_DROWNDAMAGE; + mType->info.conditionImmunities |= CONDITION_DROWN; + } + } else if ((attr = immunityNode.attribute("ice"))) { + if (attr.as_bool()) { + mType->info.damageImmunities |= COMBAT_ICEDAMAGE; + mType->info.conditionImmunities |= CONDITION_FREEZING; + } + } else if ((attr = immunityNode.attribute("holy"))) { + if (attr.as_bool()) { + mType->info.damageImmunities |= COMBAT_HOLYDAMAGE; + mType->info.conditionImmunities |= CONDITION_DAZZLED; + } + } else if ((attr = immunityNode.attribute("death"))) { + if (attr.as_bool()) { + mType->info.damageImmunities |= COMBAT_DEATHDAMAGE; + mType->info.conditionImmunities |= CONDITION_CURSED; + } + } else if ((attr = immunityNode.attribute("lifedrain"))) { + if (attr.as_bool()) { + mType->info.damageImmunities |= COMBAT_LIFEDRAIN; + } + } else if ((attr = immunityNode.attribute("manadrain"))) { + if (attr.as_bool()) { + mType->info.damageImmunities |= COMBAT_MANADRAIN; + } + } else if ((attr = immunityNode.attribute("paralyze"))) { + if (attr.as_bool()) { + mType->info.conditionImmunities |= CONDITION_PARALYZE; + } + } else if ((attr = immunityNode.attribute("outfit"))) { + if (attr.as_bool()) { + mType->info.conditionImmunities |= CONDITION_OUTFIT; + } + } else if ((attr = immunityNode.attribute("bleed"))) { + if (attr.as_bool()) { + mType->info.conditionImmunities |= CONDITION_BLEEDING; + } + } else if ((attr = immunityNode.attribute("drunk"))) { + if (attr.as_bool()) { + mType->info.conditionImmunities |= CONDITION_DRUNK; + } + } else if ((attr = immunityNode.attribute("invisible")) || (attr = immunityNode.attribute("invisibility"))) { + if (attr.as_bool()) { + mType->info.conditionImmunities |= CONDITION_INVISIBLE; + } + } else { + std::cout << "[Warning - Monsters::loadMonster] Unknown immunity. " << file << std::endl; + } + } + } + + if ((node = monsterNode.child("voices"))) { + if ((attr = node.attribute("speed")) || (attr = node.attribute("interval"))) { + mType->info.yellSpeedTicks = pugi::cast(attr.value()); + } else { + std::cout << "[Warning - Monsters::loadMonster] Missing voices speed. " << file << std::endl; + } + + if ((attr = node.attribute("chance"))) { + mType->info.yellChance = pugi::cast(attr.value()); + } else { + std::cout << "[Warning - Monsters::loadMonster] Missing voices chance. " << file << std::endl; + } + + for (auto voiceNode : node.children()) { + voiceBlock_t vb; + if ((attr = voiceNode.attribute("sentence"))) { + vb.text = attr.as_string(); + } else { + std::cout << "[Warning - Monsters::loadMonster] Missing voice sentence. " << file << std::endl; + } + + if ((attr = voiceNode.attribute("yell"))) { + vb.yellText = attr.as_bool(); + } else { + vb.yellText = false; + } + mType->info.voiceVector.emplace_back(vb); + } + } + + if ((node = monsterNode.child("loot"))) { + for (auto lootNode : node.children()) { + LootBlock lootBlock; + if (loadLootItem(lootNode, lootBlock)) { + mType->info.lootItems.emplace_back(std::move(lootBlock)); + } else { + std::cout << "[Warning - Monsters::loadMonster] Cant load loot. " << file << std::endl; + } + } + } + + if ((node = monsterNode.child("elements"))) { + for (auto elementNode : node.children()) { + if ((attr = elementNode.attribute("physicalPercent"))) { + mType->info.elementMap[COMBAT_PHYSICALDAMAGE] = pugi::cast(attr.value()); + } else if ((attr = elementNode.attribute("icePercent"))) { + mType->info.elementMap[COMBAT_ICEDAMAGE] = pugi::cast(attr.value()); + } else if ((attr = elementNode.attribute("poisonPercent")) || (attr = elementNode.attribute("earthPercent"))) { + mType->info.elementMap[COMBAT_EARTHDAMAGE] = pugi::cast(attr.value()); + } else if ((attr = elementNode.attribute("firePercent"))) { + mType->info.elementMap[COMBAT_FIREDAMAGE] = pugi::cast(attr.value()); + } else if ((attr = elementNode.attribute("energyPercent"))) { + mType->info.elementMap[COMBAT_ENERGYDAMAGE] = pugi::cast(attr.value()); + } else if ((attr = elementNode.attribute("holyPercent"))) { + mType->info.elementMap[COMBAT_HOLYDAMAGE] = pugi::cast(attr.value()); + } else if ((attr = elementNode.attribute("deathPercent"))) { + mType->info.elementMap[COMBAT_DEATHDAMAGE] = pugi::cast(attr.value()); + } else if ((attr = elementNode.attribute("drownPercent"))) { + mType->info.elementMap[COMBAT_DROWNDAMAGE] = pugi::cast(attr.value()); + } else if ((attr = elementNode.attribute("lifedrainPercent"))) { + mType->info.elementMap[COMBAT_LIFEDRAIN] = pugi::cast(attr.value()); + } else if ((attr = elementNode.attribute("manadrainPercent"))) { + mType->info.elementMap[COMBAT_MANADRAIN] = pugi::cast(attr.value()); + } else { + std::cout << "[Warning - Monsters::loadMonster] Unknown element percent. " << file << std::endl; + } + } + } + + if ((node = monsterNode.child("summons"))) { + if ((attr = node.attribute("maxSummons"))) { + mType->info.maxSummons = std::min(pugi::cast(attr.value()), 100); + } else { + std::cout << "[Warning - Monsters::loadMonster] Missing summons maxSummons. " << file << std::endl; + } + + for (auto summonNode : node.children()) { + int32_t chance = 100; + int32_t speed = 1000; + int32_t max = mType->info.maxSummons; + bool force = false; + + if ((attr = summonNode.attribute("speed")) || (attr = summonNode.attribute("interval"))) { + speed = std::max(1, pugi::cast(attr.value())); + } + + if ((attr = summonNode.attribute("chance"))) { + chance = pugi::cast(attr.value()); + } + + if ((attr = summonNode.attribute("max"))) { + max = pugi::cast(attr.value()); + } + + if ((attr = summonNode.attribute("force"))) { + force = attr.as_bool(); + } + + if ((attr = summonNode.attribute("name"))) { + summonBlock_t sb; + sb.name = attr.as_string(); + sb.speed = speed; + sb.chance = chance; + sb.max = max; + sb.force = force; + mType->info.summons.emplace_back(sb); + } else { + std::cout << "[Warning - Monsters::loadMonster] Missing summon name. " << file << std::endl; + } + } + } + + if ((node = monsterNode.child("script"))) { + for (auto eventNode : node.children()) { + if ((attr = eventNode.attribute("name"))) { + mType->info.scripts.emplace_back(attr.as_string()); + } else { + std::cout << "[Warning - Monsters::loadMonster] Missing name for script event. " << file << std::endl; + } + } + } + + mType->info.summons.shrink_to_fit(); + mType->info.lootItems.shrink_to_fit(); + mType->info.attackSpells.shrink_to_fit(); + mType->info.defenseSpells.shrink_to_fit(); + mType->info.voiceVector.shrink_to_fit(); + mType->info.scripts.shrink_to_fit(); + return mType; +} + +bool MonsterType::loadCallback(LuaScriptInterface* scriptInterface) +{ + int32_t id = scriptInterface->getEvent(); + if (id == -1) { + std::cout << "[Warning - MonsterType::loadCallback] Event not found. " << std::endl; + return false; + } + + info.scriptInterface = scriptInterface; + if (info.eventType == MONSTERS_EVENT_THINK) { + info.thinkEvent = id; + } else if (info.eventType == MONSTERS_EVENT_APPEAR) { + info.creatureAppearEvent = id; + } else if (info.eventType == MONSTERS_EVENT_DISAPPEAR) { + info.creatureDisappearEvent = id; + } else if (info.eventType == MONSTERS_EVENT_MOVE) { + info.creatureMoveEvent = id; + } else if (info.eventType == MONSTERS_EVENT_SAY) { + info.creatureSayEvent = id; + } + return true; +} + +bool Monsters::loadLootItem(const pugi::xml_node& node, LootBlock& lootBlock) +{ + pugi::xml_attribute attr; + if ((attr = node.attribute("id"))) { + lootBlock.id = pugi::cast(attr.value()); + } else if ((attr = node.attribute("name"))) { + auto name = attr.as_string(); + auto ids = Item::items.nameToItems.equal_range(asLowerCaseString(name)); + + if (ids.first == Item::items.nameToItems.cend()) { + std::cout << "[Warning - Monsters::loadMonster] Unknown loot item \"" << name << "\". " << std::endl; + return false; + } + + uint32_t id = ids.first->second; + + if (std::next(ids.first) != ids.second) { + std::cout << "[Warning - Monsters::loadMonster] Non-unique loot item \"" << name << "\". " << std::endl; + return false; + } + + lootBlock.id = id; + } + + if (lootBlock.id == 0) { + return false; + } + + if ((attr = node.attribute("countmax"))) { + lootBlock.countmax = std::max(1, pugi::cast(attr.value())); + } else { + lootBlock.countmax = 1; + } + + if ((attr = node.attribute("chance")) || (attr = node.attribute("chance1"))) { + int32_t lootChance = pugi::cast(attr.value()); + if (lootChance > static_cast(MAX_LOOTCHANCE)) { + std::cout << "[Warning - Monsters::loadMonster] Invalid \"chance\" "<< lootChance <<" used for loot, the max is " << MAX_LOOTCHANCE << ". " << std::endl; + } + lootBlock.chance = std::min(MAX_LOOTCHANCE, lootChance); + } else { + lootBlock.chance = MAX_LOOTCHANCE; + } + + if (Item::items[lootBlock.id].isContainer()) { + loadLootContainer(node, lootBlock); + } + + //optional + if ((attr = node.attribute("subtype"))) { + lootBlock.subType = pugi::cast(attr.value()); + } else { + uint32_t charges = Item::items[lootBlock.id].charges; + if (charges != 0) { + lootBlock.subType = charges; + } + } + + if ((attr = node.attribute("actionId"))) { + lootBlock.actionId = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("text"))) { + lootBlock.text = attr.as_string(); + } + return true; +} + +void Monsters::loadLootContainer(const pugi::xml_node& node, LootBlock& lBlock) +{ + for (auto subNode : node.children()) { + LootBlock lootBlock; + if (loadLootItem(subNode, lootBlock)) { + lBlock.childLoot.emplace_back(std::move(lootBlock)); + } + } +} + +MonsterType* Monsters::getMonsterType(const std::string& name, bool loadFromFile /*= true */) +{ + std::string lowerCaseName = asLowerCaseString(name); + + auto it = monsters.find(lowerCaseName); + if (it == monsters.end()) { + auto it2 = unloadedMonsters.find(lowerCaseName); + if (it2 == unloadedMonsters.end()) { + return nullptr; + } + + if (!loadFromFile) { + return nullptr; + } + + return loadMonster(it2->second, name); + } + return &it->second; +} diff --git a/src/monsters.h b/src/monsters.h new file mode 100644 index 0000000..aab948c --- /dev/null +++ b/src/monsters.h @@ -0,0 +1,261 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_MONSTERS_H_776E8327BCE2450EB7C4A260785E6C0D +#define FS_MONSTERS_H_776E8327BCE2450EB7C4A260785E6C0D + +#include "creature.h" + + +const uint32_t MAX_LOOTCHANCE = 100000; + +struct LootBlock { + uint16_t id; + uint32_t countmax; + uint32_t chance; + + //optional + int32_t subType; + int32_t actionId; + std::string text; + + std::vector childLoot; + LootBlock() { + id = 0; + countmax = 1; + chance = 0; + + subType = -1; + actionId = -1; + } +}; + +class Loot { + public: + Loot() = default; + + // non-copyable + Loot(const Loot&) = delete; + Loot& operator=(const Loot&) = delete; + + LootBlock lootBlock; +}; + +struct summonBlock_t { + std::string name; + uint32_t chance; + uint32_t speed; + uint32_t max; + bool force = false; +}; + +class BaseSpell; +struct spellBlock_t { + constexpr spellBlock_t() = default; + ~spellBlock_t(); + spellBlock_t(const spellBlock_t& other) = delete; + spellBlock_t& operator=(const spellBlock_t& other) = delete; + spellBlock_t(spellBlock_t&& other) : + spell(other.spell), + chance(other.chance), + speed(other.speed), + range(other.range), + minCombatValue(other.minCombatValue), + maxCombatValue(other.maxCombatValue), + combatSpell(other.combatSpell), + isMelee(other.isMelee) { + other.spell = nullptr; + } + + BaseSpell* spell = nullptr; + uint32_t chance = 100; + uint32_t speed = 2000; + uint32_t range = 0; + int32_t minCombatValue = 0; + int32_t maxCombatValue = 0; + bool combatSpell = false; + bool isMelee = false; +}; + +struct voiceBlock_t { + std::string text; + bool yellText; +}; + +class MonsterType +{ + struct MonsterInfo { + LuaScriptInterface* scriptInterface; + + std::map elementMap; + + std::vector voiceVector; + + std::vector lootItems; + std::vector scripts; + std::vector attackSpells; + std::vector defenseSpells; + std::vector summons; + + Skulls_t skull = SKULL_NONE; + Outfit_t outfit = {}; + RaceType_t race = RACE_BLOOD; + + LightInfo light = {}; + uint16_t lookcorpse = 0; + + uint64_t experience = 0; + + uint32_t manaCost = 0; + uint32_t yellChance = 0; + uint32_t yellSpeedTicks = 0; + uint32_t staticAttackChance = 95; + uint32_t maxSummons = 0; + uint32_t changeTargetSpeed = 0; + uint32_t conditionImmunities = 0; + uint32_t damageImmunities = 0; + uint32_t baseSpeed = 200; + + int32_t creatureAppearEvent = -1; + int32_t creatureDisappearEvent = -1; + int32_t creatureMoveEvent = -1; + int32_t creatureSayEvent = -1; + int32_t thinkEvent = -1; + int32_t targetDistance = 1; + int32_t runAwayHealth = 0; + int32_t health = 100; + int32_t healthMax = 100; + int32_t changeTargetChance = 0; + int32_t defense = 0; + int32_t armor = 0; + + bool canPushItems = false; + bool canPushCreatures = false; + bool pushable = true; + bool isSummonable = false; + bool isIllusionable = false; + bool isConvinceable = false; + bool isAttackable = true; + bool isHostile = true; + bool hiddenHealth = false; + bool isBoss = false; + bool canWalkOnEnergy = true; + bool canWalkOnFire = true; + bool canWalkOnPoison = true; + + MonstersEvent_t eventType = MONSTERS_EVENT_NONE; + }; + + public: + MonsterType() = default; + + // non-copyable + MonsterType(const MonsterType&) = delete; + MonsterType& operator=(const MonsterType&) = delete; + + bool loadCallback(LuaScriptInterface* scriptInterface); + + std::string name; + std::string nameDescription; + + MonsterInfo info; + + void loadLoot(MonsterType* monsterType, LootBlock lootblock); +}; + +class MonsterSpell +{ + public: + MonsterSpell() = default; + + MonsterSpell(const MonsterSpell&) = delete; + MonsterSpell& operator=(const MonsterSpell&) = delete; + + std::string name = ""; + std::string scriptName = ""; + + uint8_t chance = 100; + uint8_t range = 0; + + uint16_t interval = 2000; + + int32_t minCombatValue = 0; + int32_t maxCombatValue = 0; + int32_t attack = 0; + int32_t skill = 0; + int32_t length = 0; + int32_t spread = 0; + int32_t radius = 0; + int32_t conditionMinDamage = 0; + int32_t conditionMaxDamage = 0; + int32_t conditionStartDamage = 0; + int32_t tickInterval = 0; + int32_t minSpeedChange = 0; + int32_t maxSpeedChange = 0; + int32_t duration = 0; + + bool isScripted = false; + bool needTarget = false; + bool needDirection = false; + bool combatSpell = false; + bool isMelee = false; + + Outfit_t outfit = {}; + ShootType_t shoot = CONST_ANI_NONE; + MagicEffectClasses effect = CONST_ME_NONE; + ConditionType_t conditionType = CONDITION_NONE; + CombatType_t combatType = COMBAT_UNDEFINEDDAMAGE; +}; + +class Monsters +{ + public: + Monsters() = default; + // non-copyable + Monsters(const Monsters&) = delete; + Monsters& operator=(const Monsters&) = delete; + + bool loadFromXml(bool reloading = false); + bool isLoaded() const { + return loaded; + } + bool reload(); + + MonsterType* getMonsterType(const std::string& name, bool loadFromFile = true); + bool deserializeSpell(MonsterSpell* spell, spellBlock_t& sb, const std::string& description = ""); + + std::unique_ptr scriptInterface; + std::map monsters; + + private: + ConditionDamage* getDamageCondition(ConditionType_t conditionType, + int32_t maxDamage, int32_t minDamage, int32_t startDamage, uint32_t tickInterval); + bool deserializeSpell(const pugi::xml_node& node, spellBlock_t& sb, const std::string& description = ""); + + MonsterType* loadMonster(const std::string& file, const std::string& monsterName, bool reloading = false); + + void loadLootContainer(const pugi::xml_node& node, LootBlock&); + bool loadLootItem(const pugi::xml_node& node, LootBlock&); + + std::map unloadedMonsters; + + bool loaded = false; +}; + +#endif diff --git a/src/mounts.cpp b/src/mounts.cpp new file mode 100644 index 0000000..ce10d97 --- /dev/null +++ b/src/mounts.cpp @@ -0,0 +1,82 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "mounts.h" + +#include "pugicast.h" +#include "tools.h" + +bool Mounts::reload() +{ + mounts.clear(); + return loadFromXml(); +} + +bool Mounts::loadFromXml() +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/XML/mounts.xml"); + if (!result) { + printXMLError("Error - Mounts::loadFromXml", "data/XML/mounts.xml", result); + return false; + } + + for (auto mountNode : doc.child("mounts").children()) { + mounts.emplace_back( + static_cast(pugi::cast(mountNode.attribute("id").value())), + pugi::cast(mountNode.attribute("clientid").value()), + mountNode.attribute("name").as_string(), + pugi::cast(mountNode.attribute("speed").value()), + mountNode.attribute("premium").as_bool() + ); + } + mounts.shrink_to_fit(); + return true; +} + +Mount* Mounts::getMountByID(uint8_t id) +{ + auto it = std::find_if(mounts.begin(), mounts.end(), [id](const Mount& mount) { + return mount.id == id; + }); + + return it != mounts.end() ? &*it : nullptr; +} + +Mount* Mounts::getMountByName(const std::string& name) { + auto mountName = name.c_str(); + for (auto& it : mounts) { + if (strcasecmp(mountName, it.name.c_str()) == 0) { + return ⁢ + } + } + + return nullptr; +} + +Mount* Mounts::getMountByClientID(uint16_t clientId) +{ + auto it = std::find_if(mounts.begin(), mounts.end(), [clientId](const Mount& mount) { + return mount.clientId == clientId; + }); + + return it != mounts.end() ? &*it : nullptr; +} diff --git a/src/mounts.h b/src/mounts.h new file mode 100644 index 0000000..c63d425 --- /dev/null +++ b/src/mounts.h @@ -0,0 +1,52 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_MOUNTS_H_73716D11906A4C5C9F4A7B68D34C9BA6 +#define FS_MOUNTS_H_73716D11906A4C5C9F4A7B68D34C9BA6 + +struct Mount +{ + Mount(uint8_t id, uint16_t clientId, std::string name, int32_t speed, bool premium) : + name(std::move(name)), speed(speed), clientId(clientId), id(id), premium(premium) {} + + std::string name; + int32_t speed; + uint16_t clientId; + uint8_t id; + bool premium; +}; + +class Mounts +{ + public: + bool reload(); + bool loadFromXml(); + Mount* getMountByID(uint8_t id); + Mount* getMountByName(const std::string& name); + Mount* getMountByClientID(uint16_t clientId); + + const std::vector& getMounts() const { + return mounts; + } + + private: + std::vector mounts; +}; + +#endif diff --git a/src/movement.cpp b/src/movement.cpp new file mode 100644 index 0000000..55d0b63 --- /dev/null +++ b/src/movement.cpp @@ -0,0 +1,1008 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "game.h" + +#include "pugicast.h" + +#include "movement.h" + +extern Game g_game; +extern Vocations g_vocations; + +MoveEvents::MoveEvents() : + scriptInterface("MoveEvents Interface") +{ + scriptInterface.initState(); +} + +MoveEvents::~MoveEvents() +{ + clear(false); +} + +void MoveEvents::clearMap(MoveListMap& map, bool fromLua) +{ + for (auto it = map.begin(); it != map.end(); ++it) { + for (int eventType = MOVE_EVENT_STEP_IN; eventType < MOVE_EVENT_LAST; ++eventType) { + auto& moveEvents = it->second.moveEvent[eventType]; + for (auto find = moveEvents.begin(); find != moveEvents.end(); ) { + if (fromLua == find->fromLua) { + find = moveEvents.erase(find); + } else { + ++find; + } + } + } + } +} + +void MoveEvents::clearPosMap(MovePosListMap& map, bool fromLua) +{ + for (auto it = map.begin(); it != map.end(); ++it) { + for (int eventType = MOVE_EVENT_STEP_IN; eventType < MOVE_EVENT_LAST; ++eventType) { + auto& moveEvents = it->second.moveEvent[eventType]; + for (auto find = moveEvents.begin(); find != moveEvents.end(); ) { + if (fromLua == find->fromLua) { + find = moveEvents.erase(find); + } else { + ++find; + } + } + } + } +} + +void MoveEvents::clear(bool fromLua) +{ + clearMap(itemIdMap, fromLua); + clearMap(actionIdMap, fromLua); + clearMap(uniqueIdMap, fromLua); + clearPosMap(positionMap, fromLua); + + reInitState(fromLua); +} + +LuaScriptInterface& MoveEvents::getScriptInterface() +{ + return scriptInterface; +} + +std::string MoveEvents::getScriptBaseName() const +{ + return "movements"; +} + +Event_ptr MoveEvents::getEvent(const std::string& nodeName) +{ + if (strcasecmp(nodeName.c_str(), "movevent") != 0) { + return nullptr; + } + return Event_ptr(new MoveEvent(&scriptInterface)); +} + +bool MoveEvents::registerEvent(Event_ptr event, const pugi::xml_node& node) +{ + MoveEvent_ptr moveEvent{static_cast(event.release())}; //event is guaranteed to be a MoveEvent + + const MoveEvent_t eventType = moveEvent->getEventType(); + if (eventType == MOVE_EVENT_ADD_ITEM || eventType == MOVE_EVENT_REMOVE_ITEM) { + pugi::xml_attribute tileItemAttribute = node.attribute("tileitem"); + if (tileItemAttribute && pugi::cast(tileItemAttribute.value()) == 1) { + switch (eventType) { + case MOVE_EVENT_ADD_ITEM: + moveEvent->setEventType(MOVE_EVENT_ADD_ITEM_ITEMTILE); + break; + case MOVE_EVENT_REMOVE_ITEM: + moveEvent->setEventType(MOVE_EVENT_REMOVE_ITEM_ITEMTILE); + break; + default: + break; + } + } + } + + pugi::xml_attribute attr; + if ((attr = node.attribute("itemid"))) { + int32_t id = pugi::cast(attr.value()); + if (moveEvent->getEventType() == MOVE_EVENT_EQUIP) { + ItemType& it = Item::items.getItemType(id); + it.wieldInfo = moveEvent->getWieldInfo(); + it.minReqLevel = moveEvent->getReqLevel(); + it.minReqMagicLevel = moveEvent->getReqMagLv(); + it.vocationString = moveEvent->getVocationString(); + } + addEvent(std::move(*moveEvent), id, itemIdMap); + } else if ((attr = node.attribute("fromid"))) { + uint32_t id = pugi::cast(attr.value()); + uint32_t endId = pugi::cast(node.attribute("toid").value()); + + addEvent(*moveEvent, id, itemIdMap); + + if (moveEvent->getEventType() == MOVE_EVENT_EQUIP) { + ItemType& it = Item::items.getItemType(id); + it.wieldInfo = moveEvent->getWieldInfo(); + it.minReqLevel = moveEvent->getReqLevel(); + it.minReqMagicLevel = moveEvent->getReqMagLv(); + it.vocationString = moveEvent->getVocationString(); + + while (++id <= endId) { + addEvent(*moveEvent, id, itemIdMap); + + ItemType& tit = Item::items.getItemType(id); + tit.wieldInfo = moveEvent->getWieldInfo(); + tit.minReqLevel = moveEvent->getReqLevel(); + tit.minReqMagicLevel = moveEvent->getReqMagLv(); + tit.vocationString = moveEvent->getVocationString(); + } + } else { + while (++id <= endId) { + addEvent(*moveEvent, id, itemIdMap); + } + } + } else if ((attr = node.attribute("uniqueid"))) { + addEvent(std::move(*moveEvent), pugi::cast(attr.value()), uniqueIdMap); + } else if ((attr = node.attribute("fromuid"))) { + uint32_t id = pugi::cast(attr.value()); + uint32_t endId = pugi::cast(node.attribute("touid").value()); + addEvent(*moveEvent, id, uniqueIdMap); + while (++id <= endId) { + addEvent(*moveEvent, id, uniqueIdMap); + } + } else if ((attr = node.attribute("actionid"))) { + addEvent(std::move(*moveEvent), pugi::cast(attr.value()), actionIdMap); + } else if ((attr = node.attribute("fromaid"))) { + uint32_t id = pugi::cast(attr.value()); + uint32_t endId = pugi::cast(node.attribute("toaid").value()); + addEvent(*moveEvent, id, actionIdMap); + while (++id <= endId) { + addEvent(*moveEvent, id, actionIdMap); + } + } else if ((attr = node.attribute("pos"))) { + std::vector posList = vectorAtoi(explodeString(attr.as_string(), ";")); + if (posList.size() < 3) { + return false; + } + + Position pos(posList[0], posList[1], posList[2]); + addEvent(std::move(*moveEvent), pos, positionMap); + } else { + return false; + } + return true; +} + +bool MoveEvents::registerLuaFunction(MoveEvent* event) +{ + MoveEvent_ptr moveEvent{ event }; + if (moveEvent->getItemIdRange().size() > 0) { + if (moveEvent->getItemIdRange().size() == 1) { + uint32_t id = moveEvent->getItemIdRange().at(0); + addEvent(*moveEvent, id, itemIdMap); + if (moveEvent->getEventType() == MOVE_EVENT_EQUIP) { + ItemType& it = Item::items.getItemType(id); + it.wieldInfo = moveEvent->getWieldInfo(); + it.minReqLevel = moveEvent->getReqLevel(); + it.minReqMagicLevel = moveEvent->getReqMagLv(); + it.vocationString = moveEvent->getVocationString(); + } + } else { + uint32_t iterId = 0; + while (++iterId < moveEvent->getItemIdRange().size()) { + if (moveEvent->getEventType() == MOVE_EVENT_EQUIP) { + ItemType& it = Item::items.getItemType(moveEvent->getItemIdRange().at(iterId)); + it.wieldInfo = moveEvent->getWieldInfo(); + it.minReqLevel = moveEvent->getReqLevel(); + it.minReqMagicLevel = moveEvent->getReqMagLv(); + it.vocationString = moveEvent->getVocationString(); + } + addEvent(*moveEvent, moveEvent->getItemIdRange().at(iterId), itemIdMap); + } + } + } else { + return false; + } + return true; +} + +bool MoveEvents::registerLuaEvent(MoveEvent* event) +{ + MoveEvent_ptr moveEvent{ event }; + if (moveEvent->getItemIdRange().size() > 0) { + if (moveEvent->getItemIdRange().size() == 1) { + uint32_t id = moveEvent->getItemIdRange().at(0); + addEvent(*moveEvent, id, itemIdMap); + if (moveEvent->getEventType() == MOVE_EVENT_EQUIP) { + ItemType& it = Item::items.getItemType(id); + it.wieldInfo = moveEvent->getWieldInfo(); + it.minReqLevel = moveEvent->getReqLevel(); + it.minReqMagicLevel = moveEvent->getReqMagLv(); + it.vocationString = moveEvent->getVocationString(); + } + } else { + auto v = moveEvent->getItemIdRange(); + for (auto i = v.begin(); i != v.end(); i++) { + if (moveEvent->getEventType() == MOVE_EVENT_EQUIP) { + ItemType& it = Item::items.getItemType(*i); + it.wieldInfo = moveEvent->getWieldInfo(); + it.minReqLevel = moveEvent->getReqLevel(); + it.minReqMagicLevel = moveEvent->getReqMagLv(); + it.vocationString = moveEvent->getVocationString(); + } + addEvent(*moveEvent, *i, itemIdMap); + } + } + } else if (moveEvent->getActionIdRange().size() > 0) { + if (moveEvent->getActionIdRange().size() == 1) { + int32_t id = moveEvent->getActionIdRange().at(0); + addEvent(*moveEvent, id, actionIdMap); + } else { + auto v = moveEvent->getActionIdRange(); + for (auto i = v.begin(); i != v.end(); i++) { + addEvent(*moveEvent, *i, actionIdMap); + } + } + } else if (moveEvent->getUniqueIdRange().size() > 0) { + if (moveEvent->getUniqueIdRange().size() == 1) { + int32_t id = moveEvent->getUniqueIdRange().at(0); + addEvent(*moveEvent, id, uniqueIdMap); + } else { + auto v = moveEvent->getUniqueIdRange(); + for (auto i = v.begin(); i != v.end(); i++) { + addEvent(*moveEvent, *i, uniqueIdMap); + } + } + } else if (moveEvent->getPosList().size() > 0) { + if (moveEvent->getPosList().size() == 1) { + Position pos = moveEvent->getPosList().at(0); + addEvent(*moveEvent, pos, positionMap); + } else { + auto v = moveEvent->getPosList(); + for (auto i = v.begin(); i != v.end(); i++) { + addEvent(*moveEvent, *i, positionMap); + } + } + } else { + return false; + } + + return true; +} + +void MoveEvents::addEvent(MoveEvent moveEvent, int32_t id, MoveListMap& map) +{ + auto it = map.find(id); + if (it == map.end()) { + MoveEventList moveEventList; + moveEventList.moveEvent[moveEvent.getEventType()].push_back(std::move(moveEvent)); + map[id] = moveEventList; + } else { + std::list& moveEventList = it->second.moveEvent[moveEvent.getEventType()]; + for (MoveEvent& existingMoveEvent : moveEventList) { + if (existingMoveEvent.getSlot() == moveEvent.getSlot()) { + std::cout << "[Warning - MoveEvents::addEvent] Duplicate move event found: " << id << std::endl; + } + } + moveEventList.push_back(std::move(moveEvent)); + } +} + +MoveEvent* MoveEvents::getEvent(Item* item, MoveEvent_t eventType, slots_t slot) +{ + uint32_t slotp; + switch (slot) { + case CONST_SLOT_HEAD: slotp = SLOTP_HEAD; break; + case CONST_SLOT_NECKLACE: slotp = SLOTP_NECKLACE; break; + case CONST_SLOT_BACKPACK: slotp = SLOTP_BACKPACK; break; + case CONST_SLOT_ARMOR: slotp = SLOTP_ARMOR; break; + case CONST_SLOT_RIGHT: slotp = SLOTP_RIGHT; break; + case CONST_SLOT_LEFT: slotp = SLOTP_LEFT; break; + case CONST_SLOT_LEGS: slotp = SLOTP_LEGS; break; + case CONST_SLOT_FEET: slotp = SLOTP_FEET; break; + case CONST_SLOT_AMMO: slotp = SLOTP_AMMO; break; + case CONST_SLOT_RING: slotp = SLOTP_RING; break; + default: slotp = 0; break; + } + + auto it = itemIdMap.find(item->getID()); + if (it != itemIdMap.end()) { + std::list& moveEventList = it->second.moveEvent[eventType]; + for (MoveEvent& moveEvent : moveEventList) { + if ((moveEvent.getSlot() & slotp) != 0) { + return &moveEvent; + } + } + } + return nullptr; +} + +MoveEvent* MoveEvents::getEvent(Item* item, MoveEvent_t eventType) +{ + MoveListMap::iterator it; + + if (item->hasAttribute(ITEM_ATTRIBUTE_UNIQUEID)) { + it = uniqueIdMap.find(item->getUniqueId()); + if (it != uniqueIdMap.end()) { + std::list& moveEventList = it->second.moveEvent[eventType]; + if (!moveEventList.empty()) { + return &(*moveEventList.begin()); + } + } + } + + if (item->hasAttribute(ITEM_ATTRIBUTE_ACTIONID)) { + it = actionIdMap.find(item->getActionId()); + if (it != actionIdMap.end()) { + std::list& moveEventList = it->second.moveEvent[eventType]; + if (!moveEventList.empty()) { + return &(*moveEventList.begin()); + } + } + } + + it = itemIdMap.find(item->getID()); + if (it != itemIdMap.end()) { + std::list& moveEventList = it->second.moveEvent[eventType]; + if (!moveEventList.empty()) { + return &(*moveEventList.begin()); + } + } + return nullptr; +} + +void MoveEvents::addEvent(MoveEvent moveEvent, const Position& pos, MovePosListMap& map) +{ + auto it = map.find(pos); + if (it == map.end()) { + MoveEventList moveEventList; + moveEventList.moveEvent[moveEvent.getEventType()].push_back(std::move(moveEvent)); + map[pos] = moveEventList; + } else { + std::list& moveEventList = it->second.moveEvent[moveEvent.getEventType()]; + if (!moveEventList.empty()) { + std::cout << "[Warning - MoveEvents::addEvent] Duplicate move event found: " << pos << std::endl; + } + + moveEventList.push_back(std::move(moveEvent)); + } +} + +MoveEvent* MoveEvents::getEvent(const Tile* tile, MoveEvent_t eventType) +{ + auto it = positionMap.find(tile->getPosition()); + if (it != positionMap.end()) { + std::list& moveEventList = it->second.moveEvent[eventType]; + if (!moveEventList.empty()) { + return &(*moveEventList.begin()); + } + } + return nullptr; +} + +uint32_t MoveEvents::onCreatureMove(Creature* creature, const Tile* tile, MoveEvent_t eventType) +{ + const Position& pos = tile->getPosition(); + + uint32_t ret = 1; + + MoveEvent* moveEvent = getEvent(tile, eventType); + if (moveEvent) { + ret &= moveEvent->fireStepEvent(creature, nullptr, pos); + } + + for (size_t i = tile->getFirstIndex(), j = tile->getLastIndex(); i < j; ++i) { + Thing* thing = tile->getThing(i); + if (!thing) { + continue; + } + + Item* tileItem = thing->getItem(); + if (!tileItem) { + continue; + } + + moveEvent = getEvent(tileItem, eventType); + if (moveEvent) { + ret &= moveEvent->fireStepEvent(creature, tileItem, pos); + } + } + return ret; +} + +ReturnValue MoveEvents::onPlayerEquip(Player* player, Item* item, slots_t slot, bool isCheck) +{ + MoveEvent* moveEvent = getEvent(item, MOVE_EVENT_EQUIP, slot); + if (!moveEvent) { + return RETURNVALUE_NOERROR; + } + return moveEvent->fireEquip(player, item, slot, isCheck); +} + +ReturnValue MoveEvents::onPlayerDeEquip(Player* player, Item* item, slots_t slot) +{ + MoveEvent* moveEvent = getEvent(item, MOVE_EVENT_DEEQUIP, slot); + if (!moveEvent) { + return RETURNVALUE_NOERROR; + } + return moveEvent->fireEquip(player, item, slot, false); +} + +uint32_t MoveEvents::onItemMove(Item* item, Tile* tile, bool isAdd) +{ + MoveEvent_t eventType1, eventType2; + if (isAdd) { + eventType1 = MOVE_EVENT_ADD_ITEM; + eventType2 = MOVE_EVENT_ADD_ITEM_ITEMTILE; + } else { + eventType1 = MOVE_EVENT_REMOVE_ITEM; + eventType2 = MOVE_EVENT_REMOVE_ITEM_ITEMTILE; + } + + uint32_t ret = 1; + MoveEvent* moveEvent = getEvent(tile, eventType1); + if (moveEvent) { + ret &= moveEvent->fireAddRemItem(item, nullptr, tile->getPosition()); + } + + moveEvent = getEvent(item, eventType1); + if (moveEvent) { + ret &= moveEvent->fireAddRemItem(item, nullptr, tile->getPosition()); + } + + for (size_t i = tile->getFirstIndex(), j = tile->getLastIndex(); i < j; ++i) { + Thing* thing = tile->getThing(i); + if (!thing) { + continue; + } + + Item* tileItem = thing->getItem(); + if (!tileItem || tileItem == item) { + continue; + } + + moveEvent = getEvent(tileItem, eventType2); + if (moveEvent) { + ret &= moveEvent->fireAddRemItem(item, tileItem, tile->getPosition()); + } + } + return ret; +} + +MoveEvent::MoveEvent(LuaScriptInterface* interface) : Event(interface) {} + +std::string MoveEvent::getScriptEventName() const +{ + switch (eventType) { + case MOVE_EVENT_STEP_IN: return "onStepIn"; + case MOVE_EVENT_STEP_OUT: return "onStepOut"; + case MOVE_EVENT_EQUIP: return "onEquip"; + case MOVE_EVENT_DEEQUIP: return "onDeEquip"; + case MOVE_EVENT_ADD_ITEM: return "onAddItem"; + case MOVE_EVENT_REMOVE_ITEM: return "onRemoveItem"; + default: + std::cout << "[Error - MoveEvent::getScriptEventName] Invalid event type" << std::endl; + return std::string(); + } +} + +bool MoveEvent::configureEvent(const pugi::xml_node& node) +{ + pugi::xml_attribute eventAttr = node.attribute("event"); + if (!eventAttr) { + std::cout << "[Error - MoveEvent::configureMoveEvent] Missing event" << std::endl; + return false; + } + + std::string tmpStr = asLowerCaseString(eventAttr.as_string()); + if (tmpStr == "stepin") { + eventType = MOVE_EVENT_STEP_IN; + } else if (tmpStr == "stepout") { + eventType = MOVE_EVENT_STEP_OUT; + } else if (tmpStr == "equip") { + eventType = MOVE_EVENT_EQUIP; + } else if (tmpStr == "deequip") { + eventType = MOVE_EVENT_DEEQUIP; + } else if (tmpStr == "additem") { + eventType = MOVE_EVENT_ADD_ITEM; + } else if (tmpStr == "removeitem") { + eventType = MOVE_EVENT_REMOVE_ITEM; + } else { + std::cout << "Error: [MoveEvent::configureMoveEvent] No valid event name " << eventAttr.as_string() << std::endl; + return false; + } + + if (eventType == MOVE_EVENT_EQUIP || eventType == MOVE_EVENT_DEEQUIP) { + pugi::xml_attribute slotAttribute = node.attribute("slot"); + if (slotAttribute) { + tmpStr = asLowerCaseString(slotAttribute.as_string()); + if (tmpStr == "head") { + slot = SLOTP_HEAD; + } else if (tmpStr == "necklace") { + slot = SLOTP_NECKLACE; + } else if (tmpStr == "backpack") { + slot = SLOTP_BACKPACK; + } else if (tmpStr == "armor") { + slot = SLOTP_ARMOR; + } else if (tmpStr == "right-hand") { + slot = SLOTP_RIGHT; + } else if (tmpStr == "left-hand") { + slot = SLOTP_LEFT; + } else if (tmpStr == "hand" || tmpStr == "shield") { + slot = SLOTP_RIGHT | SLOTP_LEFT; + } else if (tmpStr == "legs") { + slot = SLOTP_LEGS; + } else if (tmpStr == "feet") { + slot = SLOTP_FEET; + } else if (tmpStr == "ring") { + slot = SLOTP_RING; + } else if (tmpStr == "ammo") { + slot = SLOTP_AMMO; + } else { + std::cout << "[Warning - MoveEvent::configureMoveEvent] Unknown slot type: " << slotAttribute.as_string() << std::endl; + } + } + + wieldInfo = 0; + + pugi::xml_attribute levelAttribute = node.attribute("level"); + if (levelAttribute) { + reqLevel = pugi::cast(levelAttribute.value()); + if (reqLevel > 0) { + wieldInfo |= WIELDINFO_LEVEL; + } + } + + pugi::xml_attribute magLevelAttribute = node.attribute("maglevel"); + if (magLevelAttribute) { + reqMagLevel = pugi::cast(magLevelAttribute.value()); + if (reqMagLevel > 0) { + wieldInfo |= WIELDINFO_MAGLV; + } + } + + pugi::xml_attribute premiumAttribute = node.attribute("premium"); + if (premiumAttribute) { + premium = premiumAttribute.as_bool(); + if (premium) { + wieldInfo |= WIELDINFO_PREMIUM; + } + } + + //Gather vocation information + std::list vocStringList; + for (auto vocationNode : node.children()) { + pugi::xml_attribute vocationNameAttribute = vocationNode.attribute("name"); + if (!vocationNameAttribute) { + continue; + } + + int32_t vocationId = g_vocations.getVocationId(vocationNameAttribute.as_string()); + if (vocationId != -1) { + vocEquipMap[vocationId] = true; + if (vocationNode.attribute("showInDescription").as_bool(true)) { + vocStringList.push_back(asLowerCaseString(vocationNameAttribute.as_string())); + } + } + } + + if (!vocEquipMap.empty()) { + wieldInfo |= WIELDINFO_VOCREQ; + } + + for (const std::string& str : vocStringList) { + if (!vocationString.empty()) { + if (str != vocStringList.back()) { + vocationString.push_back(','); + vocationString.push_back(' '); + } else { + vocationString += " and "; + } + } + + vocationString += str; + vocationString.push_back('s'); + } + } + return true; +} + +uint32_t MoveEvent::StepInField(Creature* creature, Item* item, const Position&) +{ + MagicField* field = item->getMagicField(); + if (field) { + field->onStepInField(creature); + return 1; + } + + return LUA_ERROR_ITEM_NOT_FOUND; +} + +uint32_t MoveEvent::StepOutField(Creature*, Item*, const Position&) +{ + return 1; +} + +uint32_t MoveEvent::AddItemField(Item* item, Item*, const Position&) +{ + if (MagicField* field = item->getMagicField()) { + Tile* tile = item->getTile(); + if (CreatureVector* creatures = tile->getCreatures()) { + for (Creature* creature : *creatures) { + field->onStepInField(creature); + } + } + return 1; + } + return LUA_ERROR_ITEM_NOT_FOUND; +} + +uint32_t MoveEvent::RemoveItemField(Item*, Item*, const Position&) +{ + return 1; +} + +ReturnValue MoveEvent::EquipItem(MoveEvent* moveEvent, Player* player, Item* item, slots_t slot, bool isCheck) +{ + if (!player->hasFlag(PlayerFlag_IgnoreWeaponCheck) && moveEvent->getWieldInfo() != 0) { + const VocEquipMap& vocEquipMap = moveEvent->getVocEquipMap(); + if (!vocEquipMap.empty() && vocEquipMap.find(player->getVocationId()) == vocEquipMap.end()) { + return RETURNVALUE_YOUDONTHAVEREQUIREDPROFESSION; + } + + if (player->getLevel() < moveEvent->getReqLevel()) { + return RETURNVALUE_NOTENOUGHLEVEL; + } + + if (player->getMagicLevel() < moveEvent->getReqMagLv()) { + return RETURNVALUE_NOTENOUGHMAGICLEVEL; + } + + if (moveEvent->isPremium() && !player->isPremium()) { + return RETURNVALUE_YOUNEEDPREMIUMACCOUNT; + } + } + + if (isCheck) { + return RETURNVALUE_NOERROR; + } + + if (player->isItemAbilityEnabled(slot)) { + return RETURNVALUE_NOERROR; + } + + const ItemType& it = Item::items[item->getID()]; + if (it.transformEquipTo != 0) { + Item* newItem = g_game.transformItem(item, it.transformEquipTo); + g_game.startDecay(newItem); + } else { + player->setItemAbility(slot, true); + } + + if (!it.abilities) { + return RETURNVALUE_NOERROR; + } + + if (it.abilities->invisible) { + Condition* condition = Condition::createCondition(static_cast(slot), CONDITION_INVISIBLE, -1, 0); + player->addCondition(condition); + } + + if (it.abilities->manaShield) { + Condition* condition = Condition::createCondition(static_cast(slot), CONDITION_MANASHIELD, -1, 0); + player->addCondition(condition); + } + + if (it.abilities->speed != 0) { + g_game.changeSpeed(player, it.abilities->speed); + } + + if (it.abilities->conditionSuppressions != 0) { + player->addConditionSuppressions(it.abilities->conditionSuppressions); + player->sendIcons(); + } + + if (it.abilities->regeneration) { + Condition* condition = Condition::createCondition(static_cast(slot), CONDITION_REGENERATION, -1, 0); + + if (it.abilities->healthGain != 0) { + condition->setParam(CONDITION_PARAM_HEALTHGAIN, it.abilities->healthGain); + } + + if (it.abilities->healthTicks != 0) { + condition->setParam(CONDITION_PARAM_HEALTHTICKS, it.abilities->healthTicks); + } + + if (it.abilities->manaGain != 0) { + condition->setParam(CONDITION_PARAM_MANAGAIN, it.abilities->manaGain); + } + + if (it.abilities->manaTicks != 0) { + condition->setParam(CONDITION_PARAM_MANATICKS, it.abilities->manaTicks); + } + + player->addCondition(condition); + } + + //skill modifiers + bool needUpdateSkills = false; + + for (int32_t i = SKILL_FIRST; i <= SKILL_LAST; ++i) { + if (it.abilities->skills[i]) { + needUpdateSkills = true; + player->setVarSkill(static_cast(i), it.abilities->skills[i]); + } + } + + for (int32_t i = SPECIALSKILL_FIRST; i <= SPECIALSKILL_LAST; ++i) { + if (it.abilities->specialSkills[i]) { + needUpdateSkills = true; + player->setVarSpecialSkill(static_cast(i), it.abilities->specialSkills[i]); + } + } + + if (needUpdateSkills) { + player->sendSkills(); + } + + //stat modifiers + bool needUpdateStats = false; + + for (int32_t s = STAT_FIRST; s <= STAT_LAST; ++s) { + if (it.abilities->stats[s]) { + needUpdateStats = true; + player->setVarStats(static_cast(s), it.abilities->stats[s]); + } + + if (it.abilities->statsPercent[s]) { + needUpdateStats = true; + player->setVarStats(static_cast(s), static_cast(player->getDefaultStats(static_cast(s)) * ((it.abilities->statsPercent[s] - 100) / 100.f))); + } + } + + if (needUpdateStats) { + player->sendStats(); + } + + return RETURNVALUE_NOERROR; +} + +ReturnValue MoveEvent::DeEquipItem(MoveEvent*, Player* player, Item* item, slots_t slot, bool) +{ + if (!player->isItemAbilityEnabled(slot)) { + return RETURNVALUE_NOERROR; + } + + player->setItemAbility(slot, false); + + const ItemType& it = Item::items[item->getID()]; + if (it.transformDeEquipTo != 0) { + g_game.transformItem(item, it.transformDeEquipTo); + g_game.startDecay(item); + } + + if (!it.abilities) { + return RETURNVALUE_NOERROR; + } + + if (it.abilities->invisible) { + player->removeCondition(CONDITION_INVISIBLE, static_cast(slot)); + } + + if (it.abilities->manaShield) { + player->removeCondition(CONDITION_MANASHIELD, static_cast(slot)); + } + + if (it.abilities->speed != 0) { + g_game.changeSpeed(player, -it.abilities->speed); + } + + if (it.abilities->conditionSuppressions != 0) { + player->removeConditionSuppressions(it.abilities->conditionSuppressions); + player->sendIcons(); + } + + if (it.abilities->regeneration) { + player->removeCondition(CONDITION_REGENERATION, static_cast(slot)); + } + + //skill modifiers + bool needUpdateSkills = false; + + for (int32_t i = SKILL_FIRST; i <= SKILL_LAST; ++i) { + if (it.abilities->skills[i] != 0) { + needUpdateSkills = true; + player->setVarSkill(static_cast(i), -it.abilities->skills[i]); + } + } + + for (int32_t i = SPECIALSKILL_FIRST; i <= SPECIALSKILL_LAST; ++i) { + if (it.abilities->specialSkills[i] != 0) { + needUpdateSkills = true; + player->setVarSpecialSkill(static_cast(i), -it.abilities->specialSkills[i]); + } + } + + if (needUpdateSkills) { + player->sendSkills(); + } + + //stat modifiers + bool needUpdateStats = false; + + for (int32_t s = STAT_FIRST; s <= STAT_LAST; ++s) { + if (it.abilities->stats[s]) { + needUpdateStats = true; + player->setVarStats(static_cast(s), -it.abilities->stats[s]); + } + + if (it.abilities->statsPercent[s]) { + needUpdateStats = true; + player->setVarStats(static_cast(s), -static_cast(player->getDefaultStats(static_cast(s)) * ((it.abilities->statsPercent[s] - 100) / 100.f))); + } + } + + if (needUpdateStats) { + player->sendStats(); + } + + return RETURNVALUE_NOERROR; +} + +bool MoveEvent::loadFunction(const pugi::xml_attribute& attr, bool isScripted) +{ + const char* functionName = attr.as_string(); + if (strcasecmp(functionName, "onstepinfield") == 0) { + stepFunction = StepInField; + } else if (strcasecmp(functionName, "onstepoutfield") == 0) { + stepFunction = StepOutField; + } else if (strcasecmp(functionName, "onaddfield") == 0) { + moveFunction = AddItemField; + } else if (strcasecmp(functionName, "onremovefield") == 0) { + moveFunction = RemoveItemField; + } else if (strcasecmp(functionName, "onequipitem") == 0) { + equipFunction = EquipItem; + } else if (strcasecmp(functionName, "ondeequipitem") == 0) { + equipFunction = DeEquipItem; + } else { + if (!isScripted) { + std::cout << "[Warning - MoveEvent::loadFunction] Function \"" << functionName << "\" does not exist." << std::endl; + return false; + } + } + + if (!isScripted) { + scripted = false; + } + return true; +} + +MoveEvent_t MoveEvent::getEventType() const +{ + return eventType; +} + +void MoveEvent::setEventType(MoveEvent_t type) +{ + eventType = type; +} + +uint32_t MoveEvent::fireStepEvent(Creature* creature, Item* item, const Position& pos) +{ + if (scripted) { + return executeStep(creature, item, pos); + } else { + return stepFunction(creature, item, pos); + } +} + +bool MoveEvent::executeStep(Creature* creature, Item* item, const Position& pos) +{ + //onStepIn(creature, item, pos, fromPosition) + //onStepOut(creature, item, pos, fromPosition) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - MoveEvent::executeStep] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + LuaScriptInterface::pushThing(L, item); + LuaScriptInterface::pushPosition(L, pos); + LuaScriptInterface::pushPosition(L, creature->getLastPosition()); + + return scriptInterface->callFunction(4); +} + +ReturnValue MoveEvent::fireEquip(Player* player, Item* item, slots_t slot, bool isCheck) +{ + if (scripted) { + if (!equipFunction || equipFunction(this, player, item, slot, isCheck) == RETURNVALUE_NOERROR) { + if (executeEquip(player, item, slot, isCheck)) { + return RETURNVALUE_NOERROR; + } + return RETURNVALUE_CANNOTBEDRESSED; + } + return equipFunction(this, player, item, slot, isCheck); + } else { + return equipFunction(this, player, item, slot, isCheck); + } +} + +bool MoveEvent::executeEquip(Player* player, Item* item, slots_t slot, bool isCheck) +{ + //onEquip(player, item, slot, isCheck) + //onDeEquip(player, item, slot, isCheck) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - MoveEvent::executeEquip] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + LuaScriptInterface::pushThing(L, item); + lua_pushnumber(L, slot); + LuaScriptInterface::pushBoolean(L, isCheck); + + return scriptInterface->callFunction(4); +} + +uint32_t MoveEvent::fireAddRemItem(Item* item, Item* tileItem, const Position& pos) +{ + if (scripted) { + return executeAddRemItem(item, tileItem, pos); + } else { + return moveFunction(item, tileItem, pos); + } +} + +bool MoveEvent::executeAddRemItem(Item* item, Item* tileItem, const Position& pos) +{ + //onaddItem(moveitem, tileitem, pos) + //onRemoveItem(moveitem, tileitem, pos) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - MoveEvent::executeAddRemItem] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + LuaScriptInterface::pushThing(L, item); + LuaScriptInterface::pushThing(L, tileItem); + LuaScriptInterface::pushPosition(L, pos); + + return scriptInterface->callFunction(3); +} diff --git a/src/movement.h b/src/movement.h new file mode 100644 index 0000000..c070cff --- /dev/null +++ b/src/movement.h @@ -0,0 +1,256 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_MOVEMENT_H_5E0D2626D4634ACA83AC6509518E5F49 +#define FS_MOVEMENT_H_5E0D2626D4634ACA83AC6509518E5F49 + +#include "baseevents.h" +#include "item.h" +#include "luascript.h" +#include "vocation.h" + +extern Vocations g_vocations; + +enum MoveEvent_t { + MOVE_EVENT_STEP_IN, + MOVE_EVENT_STEP_OUT, + MOVE_EVENT_EQUIP, + MOVE_EVENT_DEEQUIP, + MOVE_EVENT_ADD_ITEM, + MOVE_EVENT_REMOVE_ITEM, + MOVE_EVENT_ADD_ITEM_ITEMTILE, + MOVE_EVENT_REMOVE_ITEM_ITEMTILE, + + MOVE_EVENT_LAST, + MOVE_EVENT_NONE +}; + +class MoveEvent; +using MoveEvent_ptr = std::unique_ptr; + +struct MoveEventList { + std::list moveEvent[MOVE_EVENT_LAST]; +}; + +using VocEquipMap = std::map; + +class MoveEvents final : public BaseEvents +{ + public: + MoveEvents(); + ~MoveEvents(); + + // non-copyable + MoveEvents(const MoveEvents&) = delete; + MoveEvents& operator=(const MoveEvents&) = delete; + + uint32_t onCreatureMove(Creature* creature, const Tile* tile, MoveEvent_t eventType); + ReturnValue onPlayerEquip(Player* player, Item* item, slots_t slot, bool isCheck); + ReturnValue onPlayerDeEquip(Player* player, Item* item, slots_t slot); + uint32_t onItemMove(Item* item, Tile* tile, bool isAdd); + + MoveEvent* getEvent(Item* item, MoveEvent_t eventType); + + bool registerLuaEvent(MoveEvent* event); + bool registerLuaFunction(MoveEvent* event); + void clear(bool fromLua) override final; + + private: + using MoveListMap = std::map; + using MovePosListMap = std::map; + void clearMap(MoveListMap& map, bool fromLua); + void clearPosMap(MovePosListMap& map, bool fromLua); + + LuaScriptInterface& getScriptInterface() override; + std::string getScriptBaseName() const override; + Event_ptr getEvent(const std::string& nodeName) override; + bool registerEvent(Event_ptr event, const pugi::xml_node& node) override; + + void addEvent(MoveEvent moveEvent, int32_t id, MoveListMap& map); + + void addEvent(MoveEvent moveEvent, const Position& pos, MovePosListMap& map); + MoveEvent* getEvent(const Tile* tile, MoveEvent_t eventType); + + MoveEvent* getEvent(Item* item, MoveEvent_t eventType, slots_t slot); + + MoveListMap uniqueIdMap; + MoveListMap actionIdMap; + MoveListMap itemIdMap; + MovePosListMap positionMap; + + LuaScriptInterface scriptInterface; +}; + +using StepFunction = std::function; +using MoveFunction = std::function; +using EquipFunction = std::function; + +class MoveEvent final : public Event +{ + public: + explicit MoveEvent(LuaScriptInterface* interface); + + MoveEvent_t getEventType() const; + void setEventType(MoveEvent_t type); + + bool configureEvent(const pugi::xml_node& node) override; + bool loadFunction(const pugi::xml_attribute& attr, bool isScripted) override; + + uint32_t fireStepEvent(Creature* creature, Item* item, const Position& pos); + uint32_t fireAddRemItem(Item* item, Item* tileItem, const Position& pos); + ReturnValue fireEquip(Player* player, Item* item, slots_t slot, bool isCheck); + + uint32_t getSlot() const { + return slot; + } + + //scripting + bool executeStep(Creature* creature, Item* item, const Position& pos); + bool executeEquip(Player* player, Item* item, slots_t slot, bool isCheck); + bool executeAddRemItem(Item* item, Item* tileItem, const Position& pos); + // + + //onEquip information + uint32_t getReqLevel() const { + return reqLevel; + } + uint32_t getReqMagLv() const { + return reqMagLevel; + } + bool isPremium() const { + return premium; + } + const std::string& getVocationString() const { + return vocationString; + } + void setVocationString(const std::string& str) { + vocationString = str; + } + uint32_t getWieldInfo() const { + return wieldInfo; + } + const VocEquipMap& getVocEquipMap() const { + return vocEquipMap; + } + void addVocEquipMap(std::string vocName) { + int32_t vocationId = g_vocations.getVocationId(vocName); + if (vocationId != -1) { + vocEquipMap[vocationId] = true; + } + } + bool getTileItem() const { + return tileItem; + } + void setTileItem(bool b) { + tileItem = b; + } + std::vector getItemIdRange() { + return itemIdRange; + } + void addItemId(uint32_t id) { + itemIdRange.emplace_back(id); + } + std::vector getActionIdRange() { + return actionIdRange; + } + void addActionId(uint32_t id) { + actionIdRange.emplace_back(id); + } + std::vector getUniqueIdRange() { + return uniqueIdRange; + } + void addUniqueId(uint32_t id) { + uniqueIdRange.emplace_back(id); + } + std::vector getPosList() { + return posList; + } + void addPosList(Position pos) { + posList.emplace_back(pos); + } + std::string getSlotName() { + return slotName; + } + void setSlotName(std::string name) { + slotName = name; + } + void setSlot(uint32_t s) { + slot = s; + } + uint32_t getRequiredLevel() { + return reqLevel; + } + void setRequiredLevel(uint32_t level) { + reqLevel = level; + } + uint32_t getRequiredMagLevel() { + return reqMagLevel; + } + void setRequiredMagLevel(uint32_t level) { + reqMagLevel = level; + } + bool needPremium() { + return premium; + } + void setNeedPremium(bool b) { + premium = b; + } + uint32_t getWieldInfo() { + return wieldInfo; + } + void setWieldInfo(WieldInfo_t info) { + wieldInfo |= info; + } + + static uint32_t StepInField(Creature* creature, Item* item, const Position& pos); + static uint32_t StepOutField(Creature* creature, Item* item, const Position& pos); + + static uint32_t AddItemField(Item* item, Item* tileItem, const Position& pos); + static uint32_t RemoveItemField(Item* item, Item* tileItem, const Position& pos); + + static ReturnValue EquipItem(MoveEvent* moveEvent, Player* player, Item* item, slots_t slot, bool boolean); + static ReturnValue DeEquipItem(MoveEvent* moveEvent, Player* player, Item* item, slots_t slot, bool boolean); + + MoveEvent_t eventType = MOVE_EVENT_NONE; + StepFunction stepFunction; + MoveFunction moveFunction; + EquipFunction equipFunction; + + private: + std::string getScriptEventName() const override; + + uint32_t slot = SLOTP_WHEREEVER; + std::string slotName; + + //onEquip information + uint32_t reqLevel = 0; + uint32_t reqMagLevel = 0; + bool premium = false; + std::string vocationString; + uint32_t wieldInfo = 0; + VocEquipMap vocEquipMap; + bool tileItem = false; + + std::vector itemIdRange; + std::vector actionIdRange; + std::vector uniqueIdRange; + std::vector posList; +}; + +#endif diff --git a/src/networkmessage.cpp b/src/networkmessage.cpp new file mode 100644 index 0000000..e3f7f4a --- /dev/null +++ b/src/networkmessage.cpp @@ -0,0 +1,138 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "networkmessage.h" + +#include "container.h" +#include "creature.h" + +std::string NetworkMessage::getString(uint16_t stringLen/* = 0*/) +{ + if (stringLen == 0) { + stringLen = get(); + } + + if (!canRead(stringLen)) { + return std::string(); + } + + char* v = reinterpret_cast(buffer) + info.position; //does not break strict aliasing + info.position += stringLen; + return std::string(v, stringLen); +} + +Position NetworkMessage::getPosition() +{ + Position pos; + pos.x = get(); + pos.y = get(); + pos.z = getByte(); + return pos; +} + +void NetworkMessage::addString(const std::string& value) +{ + size_t stringLen = value.length(); + if (!canAdd(stringLen + 2) || stringLen > 8192) { + return; + } + + add(stringLen); + memcpy(buffer + info.position, value.c_str(), stringLen); + info.position += stringLen; + info.length += stringLen; +} + +void NetworkMessage::addDouble(double value, uint8_t precision/* = 2*/) +{ + addByte(precision); + add(static_cast((value * std::pow(static_cast(10), precision)) + std::numeric_limits::max())); +} + +void NetworkMessage::addBytes(const char* bytes, size_t size) +{ + if (!canAdd(size) || size > 8192) { + return; + } + + memcpy(buffer + info.position, bytes, size); + info.position += size; + info.length += size; +} + +void NetworkMessage::addPaddingBytes(size_t n) +{ + if (!canAdd(n)) { + return; + } + + memset(buffer + info.position, 0x33, n); + info.length += n; +} + +void NetworkMessage::addPosition(const Position& pos) +{ + add(pos.x); + add(pos.y); + addByte(pos.z); +} + +void NetworkMessage::addItem(uint16_t id, uint8_t count) +{ + const ItemType& it = Item::items[id]; + + add(it.clientId); + + addByte(0xFF); // MARK_UNMARKED + + if (it.stackable) { + addByte(count); + } else if (it.isSplash() || it.isFluidContainer()) { + addByte(fluidMap[count & 7]); + } + + if (it.isAnimation) { + addByte(0xFE); // random phase (0xFF for async) + } +} + +void NetworkMessage::addItem(const Item* item) +{ + const ItemType& it = Item::items[item->getID()]; + + add(it.clientId); + addByte(0xFF); // MARK_UNMARKED + + if (it.stackable) { + addByte(std::min(0xFF, item->getItemCount())); + } else if (it.isSplash() || it.isFluidContainer()) { + addByte(fluidMap[item->getFluidType() & 7]); + } + + if (it.isAnimation) { + addByte(0xFE); // random phase (0xFF for async) + } +} + +void NetworkMessage::addItemId(uint16_t itemId) +{ + add(Item::items[itemId].clientId); +} diff --git a/src/networkmessage.h b/src/networkmessage.h new file mode 100644 index 0000000..7118f44 --- /dev/null +++ b/src/networkmessage.h @@ -0,0 +1,176 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_NETWORKMESSAGE_H_B853CFED58D1413A87ACED07B2926E03 +#define FS_NETWORKMESSAGE_H_B853CFED58D1413A87ACED07B2926E03 + +#include "const.h" + +class Item; +class Creature; +class Player; +struct Position; +class RSA; + +class NetworkMessage +{ + public: + using MsgSize_t = uint16_t; + // Headers: + // 2 bytes for unencrypted message size + // 4 bytes for checksum + // 2 bytes for encrypted message size + static constexpr MsgSize_t INITIAL_BUFFER_POSITION = 8; + enum { HEADER_LENGTH = 2 }; + enum { CHECKSUM_LENGTH = 4 }; + enum { XTEA_MULTIPLE = 8 }; + enum { MAX_BODY_LENGTH = NETWORKMESSAGE_MAXSIZE - HEADER_LENGTH - CHECKSUM_LENGTH - XTEA_MULTIPLE }; + enum { MAX_PROTOCOL_BODY_LENGTH = MAX_BODY_LENGTH - 10 }; + + NetworkMessage() = default; + + void reset() { + info = {}; + } + + // simply read functions for incoming message + uint8_t getByte() { + if (!canRead(1)) { + return 0; + } + + return buffer[info.position++]; + } + + uint8_t getPreviousByte() { + return buffer[--info.position]; + } + + template + T get() { + if (!canRead(sizeof(T))) { + return 0; + } + + T v; + memcpy(&v, buffer + info.position, sizeof(T)); + info.position += sizeof(T); + return v; + } + + std::string getString(uint16_t stringLen = 0); + Position getPosition(); + + // skips count unknown/unused bytes in an incoming message + void skipBytes(int16_t count) { + info.position += count; + } + + // simply write functions for outgoing message + void addByte(uint8_t value) { + if (!canAdd(1)) { + return; + } + + buffer[info.position++] = value; + info.length++; + } + + template + void add(T value) { + if (!canAdd(sizeof(T))) { + return; + } + + memcpy(buffer + info.position, &value, sizeof(T)); + info.position += sizeof(T); + info.length += sizeof(T); + } + + void addBytes(const char* bytes, size_t size); + void addPaddingBytes(size_t n); + + void addString(const std::string& value); + + void addDouble(double value, uint8_t precision = 2); + + // write functions for complex types + void addPosition(const Position& pos); + void addItem(uint16_t id, uint8_t count); + void addItem(const Item* item); + void addItemId(uint16_t itemId); + + MsgSize_t getLength() const { + return info.length; + } + + void setLength(MsgSize_t newLength) { + info.length = newLength; + } + + MsgSize_t getBufferPosition() const { + return info.position; + } + + uint16_t getLengthHeader() const { + return static_cast(buffer[0] | buffer[1] << 8); + } + + bool isOverrun() const { + return info.overrun; + } + + uint8_t* getBuffer() { + return buffer; + } + + const uint8_t* getBuffer() const { + return buffer; + } + + uint8_t* getBodyBuffer() { + info.position = 2; + return buffer + HEADER_LENGTH; + } + + protected: + struct NetworkMessageInfo { + MsgSize_t length = 0; + MsgSize_t position = INITIAL_BUFFER_POSITION; + bool overrun = false; + }; + + NetworkMessageInfo info; + uint8_t buffer[NETWORKMESSAGE_MAXSIZE]; + + private: + bool canAdd(size_t size) const { + return (size + info.position) < MAX_BODY_LENGTH; + } + + bool canRead(int32_t size) { + if ((info.position + size) > (info.length + 8) || size >= (NETWORKMESSAGE_MAXSIZE - info.position)) { + info.overrun = true; + return false; + } + return true; + } +}; + +#endif // #ifndef __NETWORK_MESSAGE_H__ diff --git a/src/npc.cpp b/src/npc.cpp new file mode 100644 index 0000000..292b4b1 --- /dev/null +++ b/src/npc.cpp @@ -0,0 +1,1302 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "npc.h" +#include "game.h" +#include "pugicast.h" + +extern Game g_game; +extern LuaEnvironment g_luaEnvironment; + +uint32_t Npc::npcAutoID = 0x80000000; +NpcScriptInterface* Npc::scriptInterface = nullptr; + +void Npcs::reload() +{ + const std::map& npcs = g_game.getNpcs(); + for (const auto& it : npcs) { + it.second->closeAllShopWindows(); + } + + delete Npc::scriptInterface; + Npc::scriptInterface = nullptr; + + for (const auto& it : npcs) { + it.second->reload(); + } +} + +Npc* Npc::createNpc(const std::string& name) +{ + std::unique_ptr npc(new Npc(name)); + if (!npc->load()) { + return nullptr; + } + return npc.release(); +} + +Npc::Npc(const std::string& name) : + Creature(), + filename("data/npc/" + name + ".xml"), + npcEventHandler(nullptr), + masterRadius(-1), + loaded(false) +{ + reset(); +} + +Npc::~Npc() +{ + reset(); +} + +void Npc::addList() +{ + g_game.addNpc(this); +} + +void Npc::removeList() +{ + g_game.removeNpc(this); +} + +bool Npc::load() +{ + if (loaded) { + return true; + } + + reset(); + + if (!scriptInterface) { + scriptInterface = new NpcScriptInterface(); + scriptInterface->loadNpcLib("data/npc/lib/npc.lua"); + } + + loaded = loadFromXml(); + return loaded; +} + +void Npc::reset() +{ + loaded = false; + isIdle = true; + walkTicks = 1500; + pushable = true; + floorChange = false; + attackable = false; + ignoreHeight = true; + focusCreature = 0; + speechBubble = SPEECHBUBBLE_NONE; + + delete npcEventHandler; + npcEventHandler = nullptr; + + parameters.clear(); + shopPlayerSet.clear(); +} + +void Npc::reload() +{ + reset(); + load(); + + // Simulate that the creature is placed on the map again. + if (npcEventHandler) { + npcEventHandler->onCreatureAppear(this); + } + + if (walkTicks > 0) { + addEventWalk(); + } +} + +bool Npc::loadFromXml() +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(filename.c_str()); + if (!result) { + printXMLError("Error - Npc::loadFromXml", filename, result); + return false; + } + + pugi::xml_node npcNode = doc.child("npc"); + if (!npcNode) { + std::cout << "[Error - Npc::loadFromXml] Missing npc tag in " << filename << std::endl; + return false; + } + + name = npcNode.attribute("name").as_string(); + attackable = npcNode.attribute("attackable").as_bool(); + floorChange = npcNode.attribute("floorchange").as_bool(); + + pugi::xml_attribute attr; + if ((attr = npcNode.attribute("speed"))) { + baseSpeed = pugi::cast(attr.value()); + } else { + baseSpeed = 100; + } + + if ((attr = npcNode.attribute("pushable"))) { + pushable = attr.as_bool(); + } + + if ((attr = npcNode.attribute("walkinterval"))) { + walkTicks = pugi::cast(attr.value()); + } + + if ((attr = npcNode.attribute("walkradius"))) { + masterRadius = pugi::cast(attr.value()); + } + + if ((attr = npcNode.attribute("ignoreheight"))) { + ignoreHeight = attr.as_bool(); + } + + if ((attr = npcNode.attribute("speechbubble"))) { + speechBubble = pugi::cast(attr.value()); + } + + if ((attr = npcNode.attribute("skull"))) { + setSkull(getSkullType(asLowerCaseString(attr.as_string()))); + } + + pugi::xml_node healthNode = npcNode.child("health"); + if (healthNode) { + if ((attr = healthNode.attribute("now"))) { + health = pugi::cast(attr.value()); + } else { + health = 100; + } + + if ((attr = healthNode.attribute("max"))) { + healthMax = pugi::cast(attr.value()); + } else { + healthMax = 100; + } + } + + pugi::xml_node lookNode = npcNode.child("look"); + if (lookNode) { + pugi::xml_attribute lookTypeAttribute = lookNode.attribute("type"); + if (lookTypeAttribute) { + defaultOutfit.lookType = pugi::cast(lookTypeAttribute.value()); + defaultOutfit.lookHead = pugi::cast(lookNode.attribute("head").value()); + defaultOutfit.lookBody = pugi::cast(lookNode.attribute("body").value()); + defaultOutfit.lookLegs = pugi::cast(lookNode.attribute("legs").value()); + defaultOutfit.lookFeet = pugi::cast(lookNode.attribute("feet").value()); + defaultOutfit.lookAddons = pugi::cast(lookNode.attribute("addons").value()); + } else if ((attr = lookNode.attribute("typeex"))) { + defaultOutfit.lookTypeEx = pugi::cast(attr.value()); + } + defaultOutfit.lookMount = pugi::cast(lookNode.attribute("mount").value()); + + currentOutfit = defaultOutfit; + } + + for (auto parameterNode : npcNode.child("parameters").children()) { + parameters[parameterNode.attribute("key").as_string()] = parameterNode.attribute("value").as_string(); + } + + pugi::xml_attribute scriptFile = npcNode.attribute("script"); + if (scriptFile) { + npcEventHandler = new NpcEventsHandler(scriptFile.as_string(), this); + if (!npcEventHandler->isLoaded()) { + delete npcEventHandler; + npcEventHandler = nullptr; + return false; + } + } + return true; +} + +bool Npc::canSee(const Position& pos) const +{ + if (pos.z != getPosition().z) { + return false; + } + return Creature::canSee(getPosition(), pos, 3, 3); +} + +std::string Npc::getDescription(int32_t) const +{ + std::string descr; + descr.reserve(name.length() + 1); + descr.assign(name); + descr.push_back('.'); + return descr; +} + +void Npc::onCreatureAppear(Creature* creature, bool isLogin) +{ + Creature::onCreatureAppear(creature, isLogin); + + if (creature == this) { + if (walkTicks > 0) { + addEventWalk(); + } + + if (npcEventHandler) { + npcEventHandler->onCreatureAppear(creature); + } + } else if (Player* player = creature->getPlayer()) { + if (npcEventHandler) { + npcEventHandler->onCreatureAppear(creature); + } + + spectators.insert(player); + updateIdleStatus(); + } +} + +void Npc::onRemoveCreature(Creature* creature, bool isLogout) +{ + Creature::onRemoveCreature(creature, isLogout); + + if (creature == this) { + closeAllShopWindows(); + if (npcEventHandler) { + npcEventHandler->onCreatureDisappear(creature); + } + } else if (Player* player = creature->getPlayer()) { + if (npcEventHandler) { + npcEventHandler->onCreatureDisappear(creature); + } + + spectators.erase(player); + updateIdleStatus(); + } +} + +void Npc::onCreatureMove(Creature* creature, const Tile* newTile, const Position& newPos, + const Tile* oldTile, const Position& oldPos, bool teleport) +{ + Creature::onCreatureMove(creature, newTile, newPos, oldTile, oldPos, teleport); + + if (creature == this || creature->getPlayer()) { + if (npcEventHandler) { + npcEventHandler->onCreatureMove(creature, oldPos, newPos); + } + + if (creature != this) { + Player* player = creature->getPlayer(); + + // if player is now in range, add to spectators list, otherwise erase + if (player->canSee(position)) { + spectators.insert(player); + } else { + spectators.erase(player); + } + + updateIdleStatus(); + } + } +} + +void Npc::onCreatureSay(Creature* creature, SpeakClasses type, const std::string& text) +{ + if (creature->getID() == id) { + return; + } + + //only players for script events + Player* player = creature->getPlayer(); + if (player) { + if (npcEventHandler) { + npcEventHandler->onCreatureSay(player, type, text); + } + } +} + +void Npc::onPlayerCloseChannel(Player* player) +{ + if (npcEventHandler) { + npcEventHandler->onPlayerCloseChannel(player); + } +} + +void Npc::onThink(uint32_t interval) +{ + Creature::onThink(interval); + + if (npcEventHandler) { + npcEventHandler->onThink(); + } + + if (!isIdle && getTimeSinceLastMove() >= walkTicks) { + addEventWalk(); + } +} + +void Npc::doSay(const std::string& text) +{ + g_game.internalCreatureSay(this, TALKTYPE_SAY, text, false); +} + +void Npc::doSayToPlayer(Player* player, const std::string& text) +{ + if (player) { + player->sendCreatureSay(this, TALKTYPE_PRIVATE_NP, text); + player->onCreatureSay(this, TALKTYPE_PRIVATE_NP, text); + } +} + +void Npc::onPlayerTrade(Player* player, int32_t callback, uint16_t itemId, uint8_t count, + uint8_t amount, bool ignore/* = false*/, bool inBackpacks/* = false*/) +{ + if (npcEventHandler) { + npcEventHandler->onPlayerTrade(player, callback, itemId, count, amount, ignore, inBackpacks); + } + player->sendSaleItemList(); +} + +void Npc::onPlayerEndTrade(Player* player, int32_t buyCallback, int32_t sellCallback) +{ + lua_State* L = getScriptInterface()->getLuaState(); + + if (buyCallback != -1) { + luaL_unref(L, LUA_REGISTRYINDEX, buyCallback); + } + + if (sellCallback != -1) { + luaL_unref(L, LUA_REGISTRYINDEX, sellCallback); + } + + removeShopPlayer(player); + + if (npcEventHandler) { + npcEventHandler->onPlayerEndTrade(player); + } +} + +bool Npc::getNextStep(Direction& dir, uint32_t& flags) +{ + if (Creature::getNextStep(dir, flags)) { + return true; + } + + if (walkTicks == 0) { + return false; + } + + if (focusCreature != 0) { + return false; + } + + if (getTimeSinceLastMove() < walkTicks) { + return false; + } + + return getRandomStep(dir); +} + +void Npc::setIdle(bool idle) +{ + if (isRemoved() || getHealth() <= 0) { + return; + } + + isIdle = idle; + + if (isIdle) { + onIdleStatus(); + } +} + +void Npc::updateIdleStatus() +{ + bool status = spectators.empty(); + if (status != isIdle) { + setIdle(status); + } +} + +bool Npc::canWalkTo(const Position& fromPos, Direction dir) const +{ + if (masterRadius == 0) { + return false; + } + + Position toPos = getNextPosition(dir, fromPos); + if (!Spawns::isInZone(masterPos, masterRadius, toPos)) { + return false; + } + + Tile* tile = g_game.map.getTile(toPos); + if (!tile || tile->queryAdd(0, *this, 1, 0) != RETURNVALUE_NOERROR) { + return false; + } + + if (!floorChange && (tile->hasFlag(TILESTATE_FLOORCHANGE) || tile->getTeleportItem())) { + return false; + } + + if (!ignoreHeight && tile->hasHeight(1)) { + return false; + } + + return true; +} + +bool Npc::getRandomStep(Direction& dir) const +{ + std::vector dirList; + const Position& creaturePos = getPosition(); + + if (canWalkTo(creaturePos, DIRECTION_NORTH)) { + dirList.push_back(DIRECTION_NORTH); + } + + if (canWalkTo(creaturePos, DIRECTION_SOUTH)) { + dirList.push_back(DIRECTION_SOUTH); + } + + if (canWalkTo(creaturePos, DIRECTION_EAST)) { + dirList.push_back(DIRECTION_EAST); + } + + if (canWalkTo(creaturePos, DIRECTION_WEST)) { + dirList.push_back(DIRECTION_WEST); + } + + if (dirList.empty()) { + return false; + } + + dir = dirList[uniform_random(0, dirList.size() - 1)]; + return true; +} + +void Npc::doMoveTo(const Position& pos) +{ + std::forward_list listDir; + if (getPathTo(pos, listDir, 1, 1, true, true)) { + startAutoWalk(listDir); + } +} + +void Npc::turnToCreature(Creature* creature) +{ + const Position& creaturePos = creature->getPosition(); + const Position& myPos = getPosition(); + const auto dx = Position::getOffsetX(myPos, creaturePos); + const auto dy = Position::getOffsetY(myPos, creaturePos); + + float tan; + if (dx != 0) { + tan = static_cast(dy) / dx; + } else { + tan = 10; + } + + Direction dir; + if (std::abs(tan) < 1) { + if (dx > 0) { + dir = DIRECTION_WEST; + } else { + dir = DIRECTION_EAST; + } + } else { + if (dy > 0) { + dir = DIRECTION_NORTH; + } else { + dir = DIRECTION_SOUTH; + } + } + g_game.internalCreatureTurn(this, dir); +} + +void Npc::setCreatureFocus(Creature* creature) +{ + if (creature) { + focusCreature = creature->getID(); + turnToCreature(creature); + } else { + focusCreature = 0; + } +} + +void Npc::addShopPlayer(Player* player) +{ + shopPlayerSet.insert(player); +} + +void Npc::removeShopPlayer(Player* player) +{ + shopPlayerSet.erase(player); +} + +void Npc::closeAllShopWindows() +{ + while (!shopPlayerSet.empty()) { + Player* player = *shopPlayerSet.begin(); + if (!player->closeShopWindow()) { + removeShopPlayer(player); + } + } +} + +NpcScriptInterface* Npc::getScriptInterface() +{ + return scriptInterface; +} + +NpcScriptInterface::NpcScriptInterface() : + LuaScriptInterface("Npc interface") +{ + libLoaded = false; + initState(); +} + +bool NpcScriptInterface::initState() +{ + luaState = g_luaEnvironment.getLuaState(); + if (!luaState) { + return false; + } + + registerFunctions(); + + lua_newtable(luaState); + eventTableRef = luaL_ref(luaState, LUA_REGISTRYINDEX); + runningEventId = EVENT_ID_USER; + return true; +} + +bool NpcScriptInterface::closeState() +{ + libLoaded = false; + LuaScriptInterface::closeState(); + return true; +} + +bool NpcScriptInterface::loadNpcLib(const std::string& file) +{ + if (libLoaded) { + return true; + } + + if (loadFile(file) == -1) { + std::cout << "[Warning - NpcScriptInterface::loadNpcLib] Can not load " << file << std::endl; + return false; + } + + libLoaded = true; + return true; +} + +void NpcScriptInterface::registerFunctions() +{ + //npc exclusive functions + lua_register(luaState, "selfSay", NpcScriptInterface::luaActionSay); + lua_register(luaState, "selfMove", NpcScriptInterface::luaActionMove); + lua_register(luaState, "selfMoveTo", NpcScriptInterface::luaActionMoveTo); + lua_register(luaState, "selfTurn", NpcScriptInterface::luaActionTurn); + lua_register(luaState, "selfFollow", NpcScriptInterface::luaActionFollow); + lua_register(luaState, "getDistanceTo", NpcScriptInterface::luagetDistanceTo); + lua_register(luaState, "doNpcSetCreatureFocus", NpcScriptInterface::luaSetNpcFocus); + lua_register(luaState, "getNpcCid", NpcScriptInterface::luaGetNpcCid); + lua_register(luaState, "getNpcParameter", NpcScriptInterface::luaGetNpcParameter); + lua_register(luaState, "openShopWindow", NpcScriptInterface::luaOpenShopWindow); + lua_register(luaState, "closeShopWindow", NpcScriptInterface::luaCloseShopWindow); + lua_register(luaState, "doSellItem", NpcScriptInterface::luaDoSellItem); + + // metatable + registerMethod("Npc", "getParameter", NpcScriptInterface::luaNpcGetParameter); + registerMethod("Npc", "setFocus", NpcScriptInterface::luaNpcSetFocus); + + registerMethod("Npc", "openShopWindow", NpcScriptInterface::luaNpcOpenShopWindow); + registerMethod("Npc", "closeShopWindow", NpcScriptInterface::luaNpcCloseShopWindow); +} + +int NpcScriptInterface::luaActionSay(lua_State* L) +{ + //selfSay(words[, target]) + Npc* npc = getScriptEnv()->getNpc(); + if (!npc) { + return 0; + } + + const std::string& text = getString(L, 1); + if (lua_gettop(L) >= 2) { + Player* target = getPlayer(L, 2); + if (target) { + npc->doSayToPlayer(target, text); + return 0; + } + } + + npc->doSay(text); + return 0; +} + +int NpcScriptInterface::luaActionMove(lua_State* L) +{ + //selfMove(direction) + Npc* npc = getScriptEnv()->getNpc(); + if (npc) { + g_game.internalMoveCreature(npc, getNumber(L, 1)); + } + return 0; +} + +int NpcScriptInterface::luaActionMoveTo(lua_State* L) +{ + //selfMoveTo(x,y,z) + Npc* npc = getScriptEnv()->getNpc(); + if (!npc) { + return 0; + } + + npc->doMoveTo(Position( + getNumber(L, 1), + getNumber(L, 2), + getNumber(L, 3) + )); + return 0; +} + +int NpcScriptInterface::luaActionTurn(lua_State* L) +{ + //selfTurn(direction) + Npc* npc = getScriptEnv()->getNpc(); + if (npc) { + g_game.internalCreatureTurn(npc, getNumber(L, 1)); + } + return 0; +} + +int NpcScriptInterface::luaActionFollow(lua_State* L) +{ + //selfFollow(player) + Npc* npc = getScriptEnv()->getNpc(); + if (!npc) { + pushBoolean(L, false); + return 1; + } + + pushBoolean(L, npc->setFollowCreature(getPlayer(L, 1))); + return 1; +} + +int NpcScriptInterface::luagetDistanceTo(lua_State* L) +{ + //getDistanceTo(uid) + ScriptEnvironment* env = getScriptEnv(); + + Npc* npc = env->getNpc(); + if (!npc) { + reportErrorFunc(getErrorDesc(LUA_ERROR_THING_NOT_FOUND)); + lua_pushnil(L); + return 1; + } + + uint32_t uid = getNumber(L, -1); + + Thing* thing = env->getThingByUID(uid); + if (!thing) { + reportErrorFunc(getErrorDesc(LUA_ERROR_THING_NOT_FOUND)); + lua_pushnil(L); + return 1; + } + + const Position& thingPos = thing->getPosition(); + const Position& npcPos = npc->getPosition(); + if (npcPos.z != thingPos.z) { + lua_pushnumber(L, -1); + } else { + int32_t dist = std::max(Position::getDistanceX(npcPos, thingPos), Position::getDistanceY(npcPos, thingPos)); + lua_pushnumber(L, dist); + } + return 1; +} + +int NpcScriptInterface::luaSetNpcFocus(lua_State* L) +{ + //doNpcSetCreatureFocus(cid) + Npc* npc = getScriptEnv()->getNpc(); + if (npc) { + npc->setCreatureFocus(getCreature(L, -1)); + } + return 0; +} + +int NpcScriptInterface::luaGetNpcCid(lua_State* L) +{ + //getNpcCid() + Npc* npc = getScriptEnv()->getNpc(); + if (npc) { + lua_pushnumber(L, npc->getID()); + } else { + lua_pushnil(L); + } + return 1; +} + +int NpcScriptInterface::luaGetNpcParameter(lua_State* L) +{ + //getNpcParameter(paramKey) + Npc* npc = getScriptEnv()->getNpc(); + if (!npc) { + lua_pushnil(L); + return 1; + } + + std::string paramKey = getString(L, -1); + + auto it = npc->parameters.find(paramKey); + if (it != npc->parameters.end()) { + LuaScriptInterface::pushString(L, it->second); + } else { + lua_pushnil(L); + } + return 1; +} + +int NpcScriptInterface::luaOpenShopWindow(lua_State* L) +{ + //openShopWindow(cid, items, onBuy callback, onSell callback) + int32_t sellCallback; + if (lua_isfunction(L, -1) == 0) { + sellCallback = -1; + lua_pop(L, 1); // skip it - use default value + } else { + sellCallback = popCallback(L); + } + + int32_t buyCallback; + if (lua_isfunction(L, -1) == 0) { + buyCallback = -1; + lua_pop(L, 1); // skip it - use default value + } else { + buyCallback = popCallback(L); + } + + if (lua_istable(L, -1) == 0) { + reportError(__FUNCTION__, "item list is not a table."); + pushBoolean(L, false); + return 1; + } + + std::list items; + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + const auto tableIndex = lua_gettop(L); + ShopInfo item; + + item.itemId = getField(L, tableIndex, "id"); + item.subType = getField(L, tableIndex, "subType"); + if (item.subType == 0) { + item.subType = getField(L, tableIndex, "subtype"); + lua_pop(L, 1); + } + + item.buyPrice = getField(L, tableIndex, "buy"); + item.sellPrice = getField(L, tableIndex, "sell"); + item.realName = getFieldString(L, tableIndex, "name"); + + items.push_back(item); + lua_pop(L, 6); + } + lua_pop(L, 1); + + Player* player = getPlayer(L, -1); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + //Close any eventual other shop window currently open. + player->closeShopWindow(false); + + Npc* npc = getScriptEnv()->getNpc(); + if (!npc) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + npc->addShopPlayer(player); + player->setShopOwner(npc, buyCallback, sellCallback); + player->openShopWindow(npc, items); + + pushBoolean(L, true); + return 1; +} + +int NpcScriptInterface::luaCloseShopWindow(lua_State* L) +{ + //closeShopWindow(cid) + Npc* npc = getScriptEnv()->getNpc(); + if (!npc) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + Player* player = getPlayer(L, 1); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + int32_t buyCallback; + int32_t sellCallback; + + Npc* merchant = player->getShopOwner(buyCallback, sellCallback); + + //Check if we actually have a shop window with this player. + if (merchant == npc) { + player->sendCloseShop(); + + if (buyCallback != -1) { + luaL_unref(L, LUA_REGISTRYINDEX, buyCallback); + } + + if (sellCallback != -1) { + luaL_unref(L, LUA_REGISTRYINDEX, sellCallback); + } + + player->setShopOwner(nullptr, -1, -1); + npc->removeShopPlayer(player); + } + + pushBoolean(L, true); + return 1; +} + +int NpcScriptInterface::luaDoSellItem(lua_State* L) +{ + //doSellItem(cid, itemid, amount, subtype, actionid, canDropOnMap) + Player* player = getPlayer(L, 1); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + uint32_t sellCount = 0; + + uint32_t itemId = getNumber(L, 2); + uint32_t amount = getNumber(L, 3); + uint32_t subType; + + int32_t n = getNumber(L, 4, -1); + if (n != -1) { + subType = n; + } else { + subType = 1; + } + + uint32_t actionId = getNumber(L, 5, 0); + bool canDropOnMap = getBoolean(L, 6, true); + + const ItemType& it = Item::items[itemId]; + if (it.stackable) { + while (amount > 0) { + int32_t stackCount = std::min(100, amount); + Item* item = Item::CreateItem(it.id, stackCount); + if (item && actionId != 0) { + item->setActionId(actionId); + } + + if (g_game.internalPlayerAddItem(player, item, canDropOnMap) != RETURNVALUE_NOERROR) { + delete item; + lua_pushnumber(L, sellCount); + return 1; + } + + amount -= stackCount; + sellCount += stackCount; + } + } else { + for (uint32_t i = 0; i < amount; ++i) { + Item* item = Item::CreateItem(it.id, subType); + if (item && actionId != 0) { + item->setActionId(actionId); + } + + if (g_game.internalPlayerAddItem(player, item, canDropOnMap) != RETURNVALUE_NOERROR) { + delete item; + lua_pushnumber(L, sellCount); + return 1; + } + + ++sellCount; + } + } + + lua_pushnumber(L, sellCount); + return 1; +} + +int NpcScriptInterface::luaNpcGetParameter(lua_State* L) +{ + // npc:getParameter(key) + const std::string& key = getString(L, 2); + Npc* npc = getUserdata(L, 1); + if (npc) { + auto it = npc->parameters.find(key); + if (it != npc->parameters.end()) { + pushString(L, it->second); + } else { + lua_pushnil(L); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int NpcScriptInterface::luaNpcSetFocus(lua_State* L) +{ + // npc:setFocus(creature) + Creature* creature = getCreature(L, 2); + Npc* npc = getUserdata(L, 1); + if (npc) { + npc->setCreatureFocus(creature); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int NpcScriptInterface::luaNpcOpenShopWindow(lua_State* L) +{ + // npc:openShopWindow(cid, items, buyCallback, sellCallback) + if (!isTable(L, 3)) { + reportErrorFunc("item list is not a table."); + pushBoolean(L, false); + return 1; + } + + Player* player = getPlayer(L, 2); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + Npc* npc = getUserdata(L, 1); + if (!npc) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + int32_t sellCallback = -1; + if (LuaScriptInterface::isFunction(L, 5)) { + sellCallback = luaL_ref(L, LUA_REGISTRYINDEX); + } + + int32_t buyCallback = -1; + if (LuaScriptInterface::isFunction(L, 4)) { + buyCallback = luaL_ref(L, LUA_REGISTRYINDEX); + } + + std::list items; + + lua_pushnil(L); + while (lua_next(L, 3) != 0) { + const auto tableIndex = lua_gettop(L); + ShopInfo item; + + item.itemId = getField(L, tableIndex, "id"); + item.subType = getField(L, tableIndex, "subType"); + if (item.subType == 0) { + item.subType = getField(L, tableIndex, "subtype"); + lua_pop(L, 1); + } + + item.buyPrice = getField(L, tableIndex, "buy"); + item.sellPrice = getField(L, tableIndex, "sell"); + item.realName = getFieldString(L, tableIndex, "name"); + + items.push_back(item); + lua_pop(L, 6); + } + lua_pop(L, 1); + + player->closeShopWindow(false); + npc->addShopPlayer(player); + + player->setShopOwner(npc, buyCallback, sellCallback); + player->openShopWindow(npc, items); + + pushBoolean(L, true); + return 1; +} + +int NpcScriptInterface::luaNpcCloseShopWindow(lua_State* L) +{ + // npc:closeShopWindow(player) + Player* player = getPlayer(L, 2); + if (!player) { + reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + Npc* npc = getUserdata(L, 1); + if (!npc) { + reportErrorFunc(getErrorDesc(LUA_ERROR_CREATURE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + int32_t buyCallback; + int32_t sellCallback; + + Npc* merchant = player->getShopOwner(buyCallback, sellCallback); + if (merchant == npc) { + player->sendCloseShop(); + if (buyCallback != -1) { + luaL_unref(L, LUA_REGISTRYINDEX, buyCallback); + } + + if (sellCallback != -1) { + luaL_unref(L, LUA_REGISTRYINDEX, sellCallback); + } + + player->setShopOwner(nullptr, -1, -1); + npc->removeShopPlayer(player); + } + + pushBoolean(L, true); + return 1; +} + +NpcEventsHandler::NpcEventsHandler(const std::string& file, Npc* npc) : + npc(npc), scriptInterface(npc->getScriptInterface()) +{ + loaded = scriptInterface->loadFile("data/npc/scripts/" + file, npc) == 0; + if (!loaded) { + std::cout << "[Warning - NpcScript::NpcScript] Can not load script: " << file << std::endl; + std::cout << scriptInterface->getLastLuaError() << std::endl; + } else { + creatureSayEvent = scriptInterface->getEvent("onCreatureSay"); + creatureDisappearEvent = scriptInterface->getEvent("onCreatureDisappear"); + creatureAppearEvent = scriptInterface->getEvent("onCreatureAppear"); + creatureMoveEvent = scriptInterface->getEvent("onCreatureMove"); + playerCloseChannelEvent = scriptInterface->getEvent("onPlayerCloseChannel"); + playerEndTradeEvent = scriptInterface->getEvent("onPlayerEndTrade"); + thinkEvent = scriptInterface->getEvent("onThink"); + } +} + +bool NpcEventsHandler::isLoaded() const +{ + return loaded; +} + +void NpcEventsHandler::onCreatureAppear(Creature* creature) +{ + if (creatureAppearEvent == -1) { + return; + } + + //onCreatureAppear(creature) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - NpcScript::onCreatureAppear] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(creatureAppearEvent, scriptInterface); + env->setNpc(npc); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(creatureAppearEvent); + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + scriptInterface->callFunction(1); +} + +void NpcEventsHandler::onCreatureDisappear(Creature* creature) +{ + if (creatureDisappearEvent == -1) { + return; + } + + //onCreatureDisappear(creature) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - NpcScript::onCreatureDisappear] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(creatureDisappearEvent, scriptInterface); + env->setNpc(npc); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(creatureDisappearEvent); + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + scriptInterface->callFunction(1); +} + +void NpcEventsHandler::onCreatureMove(Creature* creature, const Position& oldPos, const Position& newPos) +{ + if (creatureMoveEvent == -1) { + return; + } + + //onCreatureMove(creature, oldPos, newPos) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - NpcScript::onCreatureMove] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(creatureMoveEvent, scriptInterface); + env->setNpc(npc); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(creatureMoveEvent); + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + LuaScriptInterface::pushPosition(L, oldPos); + LuaScriptInterface::pushPosition(L, newPos); + scriptInterface->callFunction(3); +} + +void NpcEventsHandler::onCreatureSay(Creature* creature, SpeakClasses type, const std::string& text) +{ + if (creatureSayEvent == -1) { + return; + } + + //onCreatureSay(creature, type, msg) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - NpcScript::onCreatureSay] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(creatureSayEvent, scriptInterface); + env->setNpc(npc); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(creatureSayEvent); + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + lua_pushnumber(L, type); + LuaScriptInterface::pushString(L, text); + scriptInterface->callFunction(3); +} + +void NpcEventsHandler::onPlayerTrade(Player* player, int32_t callback, uint16_t itemId, + uint8_t count, uint8_t amount, bool ignore, bool inBackpacks) +{ + if (callback == -1) { + return; + } + + //onBuy(player, itemid, count, amount, ignore, inbackpacks) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - NpcScript::onPlayerTrade] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(-1, scriptInterface); + env->setNpc(npc); + + lua_State* L = scriptInterface->getLuaState(); + LuaScriptInterface::pushCallback(L, callback); + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + lua_pushnumber(L, itemId); + lua_pushnumber(L, count); + lua_pushnumber(L, amount); + LuaScriptInterface::pushBoolean(L, ignore); + LuaScriptInterface::pushBoolean(L, inBackpacks); + scriptInterface->callFunction(6); +} + +void NpcEventsHandler::onPlayerCloseChannel(Player* player) +{ + if (playerCloseChannelEvent == -1) { + return; + } + + //onPlayerCloseChannel(player) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - NpcScript::onPlayerCloseChannel] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(playerCloseChannelEvent, scriptInterface); + env->setNpc(npc); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(playerCloseChannelEvent); + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + scriptInterface->callFunction(1); +} + +void NpcEventsHandler::onPlayerEndTrade(Player* player) +{ + if (playerEndTradeEvent == -1) { + return; + } + + //onPlayerEndTrade(player) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - NpcScript::onPlayerEndTrade] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(playerEndTradeEvent, scriptInterface); + env->setNpc(npc); + + lua_State* L = scriptInterface->getLuaState(); + scriptInterface->pushFunction(playerEndTradeEvent); + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + scriptInterface->callFunction(1); +} + +void NpcEventsHandler::onThink() +{ + if (thinkEvent == -1) { + return; + } + + //onThink() + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - NpcScript::onThink] Call stack overflow" << std::endl; + return; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(thinkEvent, scriptInterface); + env->setNpc(npc); + + scriptInterface->pushFunction(thinkEvent); + scriptInterface->callFunction(0); +} diff --git a/src/npc.h b/src/npc.h new file mode 100644 index 0000000..cd463bb --- /dev/null +++ b/src/npc.h @@ -0,0 +1,255 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_NPC_H_B090D0CB549D4435AFA03647195D156F +#define FS_NPC_H_B090D0CB549D4435AFA03647195D156F + +#include "creature.h" +#include "luascript.h" + +#include + +class Npc; +class Player; + +class Npcs +{ + public: + static void reload(); +}; + +class NpcScriptInterface final : public LuaScriptInterface +{ + public: + NpcScriptInterface(); + + bool loadNpcLib(const std::string& file); + + private: + void registerFunctions(); + + static int luaActionSay(lua_State* L); + static int luaActionMove(lua_State* L); + static int luaActionMoveTo(lua_State* L); + static int luaActionTurn(lua_State* L); + static int luaActionFollow(lua_State* L); + static int luagetDistanceTo(lua_State* L); + static int luaSetNpcFocus(lua_State* L); + static int luaGetNpcCid(lua_State* L); + static int luaGetNpcParameter(lua_State* L); + static int luaOpenShopWindow(lua_State* L); + static int luaCloseShopWindow(lua_State* L); + static int luaDoSellItem(lua_State* L); + + // metatable + static int luaNpcGetParameter(lua_State* L); + static int luaNpcSetFocus(lua_State* L); + + static int luaNpcOpenShopWindow(lua_State* L); + static int luaNpcCloseShopWindow(lua_State* L); + + private: + bool initState() override; + bool closeState() override; + + bool libLoaded; +}; + +class NpcEventsHandler +{ + public: + NpcEventsHandler(const std::string& file, Npc* npc); + + void onCreatureAppear(Creature* creature); + void onCreatureDisappear(Creature* creature); + void onCreatureMove(Creature* creature, const Position& oldPos, const Position& newPos); + void onCreatureSay(Creature* creature, SpeakClasses, const std::string& text); + void onPlayerTrade(Player* player, int32_t callback, uint16_t itemId, uint8_t count, uint8_t amount, bool ignore = false, bool inBackpacks = false); + void onPlayerCloseChannel(Player* player); + void onPlayerEndTrade(Player* player); + void onThink(); + + bool isLoaded() const; + + private: + Npc* npc; + NpcScriptInterface* scriptInterface; + + int32_t creatureAppearEvent = -1; + int32_t creatureDisappearEvent = -1; + int32_t creatureMoveEvent = -1; + int32_t creatureSayEvent = -1; + int32_t playerCloseChannelEvent = -1; + int32_t playerEndTradeEvent = -1; + int32_t thinkEvent = -1; + bool loaded = false; +}; + +class Npc final : public Creature +{ + public: + ~Npc(); + + // non-copyable + Npc(const Npc&) = delete; + Npc& operator=(const Npc&) = delete; + + Npc* getNpc() override { + return this; + } + const Npc* getNpc() const override { + return this; + } + + bool isPushable() const override { + return pushable && walkTicks != 0; + } + + void setID() override { + if (id == 0) { + id = npcAutoID++; + } + } + + void removeList() override; + void addList() override; + + static Npc* createNpc(const std::string& name); + + bool canSee(const Position& pos) const override; + + bool load(); + void reload(); + + const std::string& getName() const override { + return name; + } + const std::string& getNameDescription() const override { + return name; + } + + CreatureType_t getType() const override { + return CREATURETYPE_NPC; + } + + uint8_t getSpeechBubble() const override { + return speechBubble; + } + void setSpeechBubble(const uint8_t bubble) { + speechBubble = bubble; + } + + void doSay(const std::string& text); + void doSayToPlayer(Player* player, const std::string& text); + + void doMoveTo(const Position& pos); + + int32_t getMasterRadius() const { + return masterRadius; + } + const Position& getMasterPos() const { + return masterPos; + } + void setMasterPos(Position pos, int32_t radius = 1) { + masterPos = pos; + if (masterRadius == -1) { + masterRadius = radius; + } + } + + void onPlayerCloseChannel(Player* player); + void onPlayerTrade(Player* player, int32_t callback, uint16_t itemId, uint8_t count, + uint8_t amount, bool ignore = false, bool inBackpacks = false); + void onPlayerEndTrade(Player* player, int32_t buyCallback, int32_t sellCallback); + + void turnToCreature(Creature* creature); + void setCreatureFocus(Creature* creature); + + NpcScriptInterface* getScriptInterface(); + + static uint32_t npcAutoID; + + private: + explicit Npc(const std::string& name); + + void onCreatureAppear(Creature* creature, bool isLogin) override; + void onRemoveCreature(Creature* creature, bool isLogout) override; + void onCreatureMove(Creature* creature, const Tile* newTile, const Position& newPos, + const Tile* oldTile, const Position& oldPos, bool teleport) override; + + void onCreatureSay(Creature* creature, SpeakClasses type, const std::string& text) override; + void onThink(uint32_t interval) override; + std::string getDescription(int32_t lookDistance) const override; + + bool isImmune(CombatType_t) const override { + return !attackable; + } + bool isImmune(ConditionType_t) const override { + return !attackable; + } + bool isAttackable() const override { + return attackable; + } + bool getNextStep(Direction& dir, uint32_t& flags) override; + + void setIdle(bool idle); + void updateIdleStatus(); + + bool canWalkTo(const Position& fromPos, Direction dir) const; + bool getRandomStep(Direction& dir) const; + + void reset(); + bool loadFromXml(); + + void addShopPlayer(Player* player); + void removeShopPlayer(Player* player); + void closeAllShopWindows(); + + std::map parameters; + + std::set shopPlayerSet; + std::set spectators; + + std::string name; + std::string filename; + + NpcEventsHandler* npcEventHandler; + + Position masterPos; + + uint32_t walkTicks; + int32_t focusCreature; + int32_t masterRadius; + + uint8_t speechBubble; + + bool floorChange; + bool attackable; + bool ignoreHeight; + bool loaded; + bool isIdle; + bool pushable; + + static NpcScriptInterface* scriptInterface; + + friend class Npcs; + friend class NpcScriptInterface; +}; + +#endif diff --git a/src/otpch.cpp b/src/otpch.cpp new file mode 100644 index 0000000..bc54154 --- /dev/null +++ b/src/otpch.cpp @@ -0,0 +1,20 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" diff --git a/src/otpch.h b/src/otpch.h new file mode 100644 index 0000000..e154370 --- /dev/null +++ b/src/otpch.h @@ -0,0 +1,44 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#define FS_OTPCH_H_F00C737DA6CA4C8D90F57430C614367F + +// Definitions should be global. +#include "definitions.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include diff --git a/src/otserv.cpp b/src/otserv.cpp new file mode 100644 index 0000000..53ebc85 --- /dev/null +++ b/src/otserv.cpp @@ -0,0 +1,332 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "server.h" + +#include "game.h" + +#include "iomarket.h" + +#include "configmanager.h" +#include "scriptmanager.h" +#include "rsa.h" +#include "protocolspectator.h" +#include "protocolold.h" +#include "protocollogin.h" +#include "protocolstatus.h" +#include "databasemanager.h" +#include "scheduler.h" +#include "databasetasks.h" +#include "script.h" +#include +#if __has_include("gitmetadata.h") + #include "gitmetadata.h" +#endif + +DatabaseTasks g_databaseTasks; +Dispatcher g_dispatcher; +Scheduler g_scheduler; + +Game g_game; +ConfigManager g_config; +Monsters g_monsters; +Vocations g_vocations; +extern Scripts* g_scripts; +RSA g_RSA; + +std::mutex g_loaderLock; +std::condition_variable g_loaderSignal; +std::unique_lock g_loaderUniqueLock(g_loaderLock); + +void startupErrorMessage(const std::string& errorStr) +{ + std::cout << "> ERROR: " << errorStr << std::endl; + g_loaderSignal.notify_all(); +} + +void mainLoader(int argc, char* argv[], ServiceManager* services); + +[[noreturn]] void badAllocationHandler() +{ + // Use functions that only use stack allocation + puts("Allocation failed, server out of memory.\nDecrease the size of your map or compile in 64 bits mode.\n"); + getchar(); + exit(-1); +} + +int main(int argc, char* argv[]) +{ + // Setup bad allocation handler + std::set_new_handler(badAllocationHandler); + + ServiceManager serviceManager; + + g_dispatcher.start(); + g_scheduler.start(); + + g_dispatcher.addTask(createTask(std::bind(mainLoader, argc, argv, &serviceManager))); + + g_loaderSignal.wait(g_loaderUniqueLock); + + if (serviceManager.is_running()) { + std::cout << ">> " << g_config.getString(ConfigManager::SERVER_NAME) << " Server Online!" << std::endl << std::endl; + serviceManager.run(); + } else { + std::cout << ">> No services running. The server is NOT online." << std::endl; + g_scheduler.shutdown(); + g_databaseTasks.shutdown(); + g_dispatcher.shutdown(); + } + + g_scheduler.join(); + g_databaseTasks.join(); + g_dispatcher.join(); + return 0; +} + +void mainLoader(int, char*[], ServiceManager* services) +{ + //dispatcher thread + g_game.setGameState(GAME_STATE_STARTUP); + + srand(static_cast(OTSYS_TIME())); +#ifdef _WIN32 + SetConsoleTitle(STATUS_SERVER_NAME); +#endif +#if defined(GIT_RETRIEVED_STATE) && GIT_RETRIEVED_STATE + std::cout << STATUS_SERVER_NAME << " - Version " << GIT_DESCRIBE << std::endl; + std::cout << "Git SHA1 " << GIT_SHORT_SHA1 << " dated " << GIT_COMMIT_DATE_ISO8601 << std::endl; + #if GIT_IS_DIRTY + std::cout << "*** DIRTY - NOT OFFICIAL RELEASE ***" << std::endl; + #endif +#else + std::cout << STATUS_SERVER_NAME << " - Version " << STATUS_SERVER_VERSION << std::endl; +#endif + std::cout << std::endl; + + std::cout << "Compiled with " << BOOST_COMPILER << std::endl; + std::cout << "Compiled on " << __DATE__ << ' ' << __TIME__ << " for platform "; +#if defined(__amd64__) || defined(_M_X64) + std::cout << "x64" << std::endl; +#elif defined(__i386__) || defined(_M_IX86) || defined(_X86_) + std::cout << "x86" << std::endl; +#elif defined(__arm__) + std::cout << "ARM" << std::endl; +#else + std::cout << "unknown" << std::endl; +#endif +#if defined(LUAJIT_VERSION) + std::cout << "Linked with " << LUAJIT_VERSION << " for Lua support" << std::endl; +#else + std::cout << "Linked with " << LUA_RELEASE << " for Lua support" << std::endl; +#endif + std::cout << std::endl; + + std::cout << "A server developed by " << STATUS_SERVER_DEVELOPERS << std::endl; + std::cout << "Visit our forum for updates, support, and resources: https://otland.net/." << std::endl; + std::cout << std::endl; + + // check if config.lua or config.lua.dist exist + std::ifstream c_test("./config.lua"); + if (!c_test.is_open()) { + std::ifstream config_lua_dist("./config.lua.dist"); + if (config_lua_dist.is_open()) { + std::cout << ">> copying config.lua.dist to config.lua" << std::endl; + std::ofstream config_lua("config.lua"); + config_lua << config_lua_dist.rdbuf(); + config_lua.close(); + config_lua_dist.close(); + } + } else { + c_test.close(); + } + + // read global config + std::cout << ">> Loading config" << std::endl; + if (!g_config.load()) { + startupErrorMessage("Unable to load config.lua!"); + return; + } + +#ifdef _WIN32 + const std::string& defaultPriority = g_config.getString(ConfigManager::DEFAULT_PRIORITY); + if (strcasecmp(defaultPriority.c_str(), "high") == 0) { + SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); + } else if (strcasecmp(defaultPriority.c_str(), "above-normal") == 0) { + SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS); + } +#endif + + //set RSA key + try { + g_RSA.loadPEM("key.pem"); + } catch(const std::exception& e) { + startupErrorMessage(e.what()); + return; + } + + std::cout << ">> Establishing database connection..." << std::flush; + + if (!Database::getInstance().connect()) { + startupErrorMessage("Failed to connect to database."); + return; + } + + std::cout << " MySQL " << Database::getClientVersion() << std::endl; + + // run database manager + std::cout << ">> Running database manager" << std::endl; + + if (!DatabaseManager::isDatabaseSetup()) { + startupErrorMessage("The database you have specified in config.lua is empty, please import the schema.sql to your database."); + return; + } + g_databaseTasks.start(); + + DatabaseManager::updateDatabase(); + + if (g_config.getBoolean(ConfigManager::OPTIMIZE_DATABASE) && !DatabaseManager::optimizeTables()) { + std::cout << "> No tables were optimized." << std::endl; + } + + //load vocations + std::cout << ">> Loading vocations" << std::endl; + if (!g_vocations.loadFromXml()) { + startupErrorMessage("Unable to load vocations!"); + return; + } + + // load item data + std::cout << ">> Loading items" << std::endl; + if (!Item::items.loadFromOtb("data/items/items.otb")) { + startupErrorMessage("Unable to load items (OTB)!"); + return; + } + + if (!Item::items.loadFromXml()) { + startupErrorMessage("Unable to load items (XML)!"); + return; + } + + std::cout << ">> Loading script systems" << std::endl; + if (!ScriptingManager::getInstance().loadScriptSystems()) { + startupErrorMessage("Failed to load script systems"); + return; + } + + std::cout << ">> Loading lua scripts" << std::endl; + if (!g_scripts->loadScripts("scripts", false, false)) { + startupErrorMessage("Failed to load lua scripts"); + return; + } + + std::cout << ">> Loading monsters" << std::endl; + if (!g_monsters.loadFromXml()) { + startupErrorMessage("Unable to load monsters!"); + return; + } + + std::cout << ">> Loading lua monsters" << std::endl; + if (!g_scripts->loadScripts("monster", false, false)) { + startupErrorMessage("Failed to load lua monsters"); + return; + } + + std::cout << ">> Loading outfits" << std::endl; + if (!Outfits::getInstance().loadFromXml()) { + startupErrorMessage("Unable to load outfits!"); + return; + } + + std::cout << ">> Checking world type... " << std::flush; + std::string worldType = asLowerCaseString(g_config.getString(ConfigManager::WORLD_TYPE)); + if (worldType == "pvp") { + g_game.setWorldType(WORLD_TYPE_PVP); + } else if (worldType == "no-pvp") { + g_game.setWorldType(WORLD_TYPE_NO_PVP); + } else if (worldType == "pvp-enforced") { + g_game.setWorldType(WORLD_TYPE_PVP_ENFORCED); + } else { + std::cout << std::endl; + + std::ostringstream ss; + ss << "> ERROR: Unknown world type: " << g_config.getString(ConfigManager::WORLD_TYPE) << ", valid world types are: pvp, no-pvp and pvp-enforced."; + startupErrorMessage(ss.str()); + return; + } + std::cout << asUpperCaseString(worldType) << std::endl; + + std::cout << ">> Loading map" << std::endl; + if (!g_game.loadMainMap(g_config.getString(ConfigManager::MAP_NAME))) { + startupErrorMessage("Failed to load map"); + return; + } + + std::cout << ">> Initializing gamestate" << std::endl; + g_game.setGameState(GAME_STATE_INIT); + + // Game client protocols + services->add(static_cast(g_config.getNumber(ConfigManager::GAME_PORT))); + services->add(static_cast(g_config.getNumber(ConfigManager::LOGIN_PORT))); + if (g_config.getBoolean(ConfigManager::ENABLE_LIVE_CASTING)) { + ProtocolGame::clearLiveCastInfo(); + services->add(static_cast(g_config.getNumber(ConfigManager::LIVE_CAST_PORT))); + std::cout << ">> Cast System Enabled." << std::endl; + } + + // OT protocols + services->add(static_cast(g_config.getNumber(ConfigManager::STATUS_PORT))); + + // Legacy login protocol + services->add(static_cast(g_config.getNumber(ConfigManager::LOGIN_PORT))); + + RentPeriod_t rentPeriod; + std::string strRentPeriod = asLowerCaseString(g_config.getString(ConfigManager::HOUSE_RENT_PERIOD)); + + if (strRentPeriod == "yearly") { + rentPeriod = RENTPERIOD_YEARLY; + } else if (strRentPeriod == "weekly") { + rentPeriod = RENTPERIOD_WEEKLY; + } else if (strRentPeriod == "monthly") { + rentPeriod = RENTPERIOD_MONTHLY; + } else if (strRentPeriod == "daily") { + rentPeriod = RENTPERIOD_DAILY; + } else { + rentPeriod = RENTPERIOD_NEVER; + } + + g_game.map.houses.payHouses(rentPeriod); + + IOMarket::checkExpiredOffers(); + IOMarket::getInstance().updateStatistics(); + + std::cout << ">> Loaded all modules, server starting up..." << std::endl; + +#ifndef _WIN32 + if (getuid() == 0 || geteuid() == 0) { + std::cout << "> Warning: " << STATUS_SERVER_NAME << " has been executed as root user, please consider running it as a normal user." << std::endl; + } +#endif + + g_game.start(services); + g_game.setGameState(GAME_STATE_NORMAL); + g_loaderSignal.notify_all(); +} diff --git a/src/outfit.cpp b/src/outfit.cpp new file mode 100644 index 0000000..9984e9a --- /dev/null +++ b/src/outfit.cpp @@ -0,0 +1,77 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "outfit.h" + +#include "pugicast.h" +#include "tools.h" + +bool Outfits::loadFromXml() +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/XML/outfits.xml"); + if (!result) { + printXMLError("Error - Outfits::loadFromXml", "data/XML/outfits.xml", result); + return false; + } + + for (auto outfitNode : doc.child("outfits").children()) { + pugi::xml_attribute attr; + if ((attr = outfitNode.attribute("enabled")) && !attr.as_bool()) { + continue; + } + + if (!(attr = outfitNode.attribute("type"))) { + std::cout << "[Warning - Outfits::loadFromXml] Missing outfit type." << std::endl; + continue; + } + + uint16_t type = pugi::cast(attr.value()); + if (type > PLAYERSEX_LAST) { + std::cout << "[Warning - Outfits::loadFromXml] Invalid outfit type " << type << "." << std::endl; + continue; + } + + pugi::xml_attribute lookTypeAttribute = outfitNode.attribute("looktype"); + if (!lookTypeAttribute) { + std::cout << "[Warning - Outfits::loadFromXml] Missing looktype on outfit." << std::endl; + continue; + } + + outfits[type].emplace_back( + outfitNode.attribute("name").as_string(), + pugi::cast(lookTypeAttribute.value()), + outfitNode.attribute("premium").as_bool(), + outfitNode.attribute("unlocked").as_bool(true) + ); + } + return true; +} + +const Outfit* Outfits::getOutfitByLookType(PlayerSex_t sex, uint16_t lookType) const +{ + for (const Outfit& outfit : outfits[sex]) { + if (outfit.lookType == lookType) { + return &outfit; + } + } + return nullptr; +} diff --git a/src/outfit.h b/src/outfit.h new file mode 100644 index 0000000..50aefa6 --- /dev/null +++ b/src/outfit.h @@ -0,0 +1,63 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_OUTFIT_H_C56E7A707E3F422C8C93D9BE09916AA3 +#define FS_OUTFIT_H_C56E7A707E3F422C8C93D9BE09916AA3 + +#include "enums.h" + +struct Outfit { + Outfit(std::string name, uint16_t lookType, bool premium, bool unlocked) : + name(std::move(name)), lookType(lookType), premium(premium), unlocked(unlocked) {} + + std::string name; + uint16_t lookType; + bool premium; + bool unlocked; +}; + +struct ProtocolOutfit { + ProtocolOutfit(const std::string& name, uint16_t lookType, uint8_t addons) : + name(name), lookType(lookType), addons(addons) {} + + const std::string& name; + uint16_t lookType; + uint8_t addons; +}; + +class Outfits +{ + public: + static Outfits& getInstance() { + static Outfits instance; + return instance; + } + + bool loadFromXml(); + + const Outfit* getOutfitByLookType(PlayerSex_t sex, uint16_t lookType) const; + const std::vector& getOutfits(PlayerSex_t sex) const { + return outfits[sex]; + } + + private: + std::vector outfits[PLAYERSEX_LAST + 1]; +}; + +#endif diff --git a/src/outputmessage.cpp b/src/outputmessage.cpp new file mode 100644 index 0000000..b4c0f77 --- /dev/null +++ b/src/outputmessage.cpp @@ -0,0 +1,77 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "outputmessage.h" +#include "protocol.h" +#include "lockfree.h" +#include "scheduler.h" + +extern Scheduler g_scheduler; + +const uint16_t OUTPUTMESSAGE_FREE_LIST_CAPACITY = 2048; +const std::chrono::milliseconds OUTPUTMESSAGE_AUTOSEND_DELAY {10}; + +void OutputMessagePool::scheduleSendAll() +{ + auto functor = std::bind(&OutputMessagePool::sendAll, this); + g_scheduler.addEvent(createSchedulerTask(OUTPUTMESSAGE_AUTOSEND_DELAY.count(), functor)); +} + +void OutputMessagePool::sendAll() +{ + //dispatcher thread + for (auto& protocol : bufferedProtocols) { + auto& msg = protocol->getCurrentBuffer(); + if (msg) { + protocol->send(std::move(msg)); + } + } + + if (!bufferedProtocols.empty()) { + scheduleSendAll(); + } +} + +void OutputMessagePool::addProtocolToAutosend(Protocol_ptr protocol) +{ + //dispatcher thread + if (bufferedProtocols.empty()) { + scheduleSendAll(); + } + bufferedProtocols.emplace_back(protocol); +} + +void OutputMessagePool::removeProtocolFromAutosend(const Protocol_ptr& protocol) +{ + //dispatcher thread + auto it = std::find(bufferedProtocols.begin(), bufferedProtocols.end(), protocol); + if (it != bufferedProtocols.end()) { + std::swap(*it, bufferedProtocols.back()); + bufferedProtocols.pop_back(); + } +} + +OutputMessage_ptr OutputMessagePool::getOutputMessage() +{ + // LockfreePoolingAllocator will leave (void* allocate) ill-formed because + // of sizeof(T), so this guaranatees that only one list will be initialized + return std::allocate_shared(LockfreePoolingAllocator()); +} diff --git a/src/outputmessage.h b/src/outputmessage.h new file mode 100644 index 0000000..66e8278 --- /dev/null +++ b/src/outputmessage.h @@ -0,0 +1,118 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_OUTPUTMESSAGE_H_C06AAED85C7A43939F22D229297C0CC1 +#define FS_OUTPUTMESSAGE_H_C06AAED85C7A43939F22D229297C0CC1 + +#include "networkmessage.h" +#include "connection.h" +#include "tools.h" + +class Protocol; + +class OutputMessage : public NetworkMessage +{ + public: + OutputMessage() = default; + + // non-copyable + OutputMessage(const OutputMessage&) = delete; + OutputMessage& operator=(const OutputMessage&) = delete; + + uint8_t* getOutputBuffer() { + return buffer + outputBufferStart; + } + + void writeMessageLength() { + add_header(info.length); + } + + void addCryptoHeader(bool addChecksum) { + if (addChecksum) { + add_header(adlerChecksum(buffer + outputBufferStart, info.length)); + } + + writeMessageLength(); + } + + void append(const NetworkMessage& msg) { + auto msgLen = msg.getLength(); + memcpy(buffer + info.position, msg.getBuffer() + 8, msgLen); + info.length += msgLen; + info.position += msgLen; + } + + bool isBroadcastMsg() const { + return isBroadcastMesssage; + } + void setBroadcastMsg(bool isBroadcastMesssage) { + this->isBroadcastMesssage = isBroadcastMesssage; + } + + void append(const OutputMessage_ptr& msg) { + auto msgLen = msg->getLength(); + memcpy(buffer + info.position, msg->getBuffer() + 8, msgLen); + info.length += msgLen; + info.position += msgLen; + } + + protected: + bool isBroadcastMesssage {false}; + + private: + template + void add_header(T add) { + assert(outputBufferStart >= sizeof(T)); + outputBufferStart -= sizeof(T); + memcpy(buffer + outputBufferStart, &add, sizeof(T)); + //added header size to the message size + info.length += sizeof(T); + } + + MsgSize_t outputBufferStart = INITIAL_BUFFER_POSITION; +}; + +class OutputMessagePool +{ + public: + // non-copyable + OutputMessagePool(const OutputMessagePool&) = delete; + OutputMessagePool& operator=(const OutputMessagePool&) = delete; + + static OutputMessagePool& getInstance() { + static OutputMessagePool instance; + return instance; + } + + void sendAll(); + void scheduleSendAll(); + + static OutputMessage_ptr getOutputMessage(); + + void addProtocolToAutosend(Protocol_ptr protocol); + void removeProtocolFromAutosend(const Protocol_ptr& protocol); + private: + OutputMessagePool() = default; + //NOTE: A vector is used here because this container is mostly read + //and relatively rarely modified (only when a client connects/disconnects) + std::vector bufferedProtocols; +}; + + +#endif diff --git a/src/party.cpp b/src/party.cpp new file mode 100644 index 0000000..1f1cd50 --- /dev/null +++ b/src/party.cpp @@ -0,0 +1,463 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "party.h" +#include "game.h" +#include "configmanager.h" +#include "events.h" + +extern Game g_game; +extern ConfigManager g_config; +extern Events* g_events; + +Party::Party(Player* leader) : leader(leader) +{ + leader->setParty(this); +} + +void Party::disband() +{ + if (!g_events->eventPartyOnDisband(this)) { + return; + } + + Player* currentLeader = leader; + leader = nullptr; + + currentLeader->setParty(nullptr); + currentLeader->sendClosePrivate(CHANNEL_PARTY); + g_game.updatePlayerShield(currentLeader); + g_game.updatePlayerHelpers(*currentLeader); + currentLeader->sendCreatureSkull(currentLeader); + currentLeader->sendTextMessage(MESSAGE_INFO_DESCR, "Your party has been disbanded."); + + for (Player* invitee : inviteList) { + invitee->removePartyInvitation(this); + currentLeader->sendCreatureShield(invitee); + } + inviteList.clear(); + + for (Player* member : memberList) { + member->setParty(nullptr); + member->sendClosePrivate(CHANNEL_PARTY); + member->sendTextMessage(MESSAGE_INFO_DESCR, "Your party has been disbanded."); + } + + for (Player* member : memberList) { + g_game.updatePlayerShield(member); + + for (Player* otherMember : memberList) { + otherMember->sendCreatureSkull(member); + } + + member->sendCreatureSkull(currentLeader); + currentLeader->sendCreatureSkull(member); + g_game.updatePlayerHelpers(*member); + } + memberList.clear(); + delete this; +} + +bool Party::leaveParty(Player* player) +{ + if (!player) { + return false; + } + + if (player->getParty() != this && leader != player) { + return false; + } + + if (!g_events->eventPartyOnLeave(this, player)) { + return false; + } + + bool missingLeader = false; + if (leader == player) { + if (!memberList.empty()) { + if (memberList.size() == 1 && inviteList.empty()) { + missingLeader = true; + } else { + passPartyLeadership(memberList.front()); + } + } else { + missingLeader = true; + } + } + + //since we already passed the leadership, we remove the player from the list + auto it = std::find(memberList.begin(), memberList.end(), player); + if (it != memberList.end()) { + memberList.erase(it); + } + + player->setParty(nullptr); + player->sendClosePrivate(CHANNEL_PARTY); + g_game.updatePlayerShield(player); + g_game.updatePlayerHelpers(*player); + + for (Player* member : memberList) { + member->sendCreatureSkull(player); + player->sendPlayerPartyIcons(member); + g_game.updatePlayerHelpers(*member); + } + + leader->sendCreatureSkull(player); + player->sendCreatureSkull(player); + player->sendPlayerPartyIcons(leader); + + player->sendTextMessage(MESSAGE_INFO_DESCR, "You have left the party."); + + updateSharedExperience(); + + clearPlayerPoints(player); + + std::ostringstream ss; + ss << player->getName() << " has left the party."; + broadcastPartyMessage(MESSAGE_INFO_DESCR, ss.str()); + + if (missingLeader || empty()) { + disband(); + } + + return true; +} + +bool Party::passPartyLeadership(Player* player) +{ + if (!player || leader == player || player->getParty() != this) { + return false; + } + + //Remove it before to broadcast the message correctly + auto it = std::find(memberList.begin(), memberList.end(), player); + if (it != memberList.end()) { + memberList.erase(it); + } + + std::ostringstream ss; + ss << player->getName() << " is now the leader of the party."; + broadcastPartyMessage(MESSAGE_INFO_DESCR, ss.str(), true); + + Player* oldLeader = leader; + leader = player; + + memberList.insert(memberList.begin(), oldLeader); + + updateSharedExperience(); + + for (Player* member : memberList) { + member->sendCreatureShield(oldLeader); + member->sendCreatureShield(leader); + } + + for (Player* invitee : inviteList) { + invitee->sendCreatureShield(oldLeader); + invitee->sendCreatureShield(leader); + } + + leader->sendCreatureShield(oldLeader); + leader->sendCreatureShield(leader); + + player->sendTextMessage(MESSAGE_INFO_DESCR, "You are now the leader of the party."); + return true; +} + +bool Party::joinParty(Player& player) +{ + if (!g_events->eventPartyOnJoin(this, &player)) { + return false; + } + + auto it = std::find(inviteList.begin(), inviteList.end(), &player); + if (it == inviteList.end()) { + return false; + } + + inviteList.erase(it); + + std::ostringstream ss; + ss << player.getName() << " has joined the party."; + broadcastPartyMessage(MESSAGE_INFO_DESCR, ss.str()); + + player.setParty(this); + + g_game.updatePlayerShield(&player); + + for (Player* member : memberList) { + member->sendCreatureSkull(&player); + player.sendPlayerPartyIcons(member); + } + + player.sendCreatureSkull(&player); + leader->sendCreatureSkull(&player); + player.sendPlayerPartyIcons(leader); + + memberList.push_back(&player); + + g_game.updatePlayerHelpers(player); + + player.removePartyInvitation(this); + updateSharedExperience(); + + const std::string& leaderName = leader->getName(); + ss.str(std::string()); + ss << "You have joined " << leaderName << "'" << (leaderName.back() == 's' ? "" : "s") << + " party. Open the party channel to communicate with your companions."; + player.sendTextMessage(MESSAGE_INFO_DESCR, ss.str()); + return true; +} + +bool Party::removeInvite(Player& player, bool removeFromPlayer/* = true*/) +{ + auto it = std::find(inviteList.begin(), inviteList.end(), &player); + if (it == inviteList.end()) { + return false; + } + + inviteList.erase(it); + + leader->sendCreatureShield(&player); + player.sendCreatureShield(leader); + + if (removeFromPlayer) { + player.removePartyInvitation(this); + } + + if (empty()) { + disband(); + } else { + for (Player* member : memberList) { + g_game.updatePlayerHelpers(*member); + } + + g_game.updatePlayerHelpers(*leader); + } + + return true; +} + +void Party::revokeInvitation(Player& player) +{ + std::ostringstream ss; + ss << leader->getName() << " has revoked " << (leader->getSex() == PLAYERSEX_FEMALE ? "her" : "his") << " invitation."; + player.sendTextMessage(MESSAGE_INFO_DESCR, ss.str()); + + ss.str(std::string()); + ss << "Invitation for " << player.getName() << " has been revoked."; + leader->sendTextMessage(MESSAGE_INFO_DESCR, ss.str()); + + removeInvite(player); +} + +bool Party::invitePlayer(Player& player) +{ + if (isPlayerInvited(&player)) { + return false; + } + + std::ostringstream ss; + ss << player.getName() << " has been invited."; + + if (empty()) { + ss << " Open the party channel to communicate with your members."; + g_game.updatePlayerShield(leader); + leader->sendCreatureSkull(leader); + } + + leader->sendTextMessage(MESSAGE_INFO_DESCR, ss.str()); + + inviteList.push_back(&player); + + for (Player* member : memberList) { + g_game.updatePlayerHelpers(*member); + } + g_game.updatePlayerHelpers(*leader); + + leader->sendCreatureShield(&player); + player.sendCreatureShield(leader); + + player.addPartyInvitation(this); + + ss.str(std::string()); + ss << leader->getName() << " has invited you to " << (leader->getSex() == PLAYERSEX_FEMALE ? "her" : "his") << " party."; + player.sendTextMessage(MESSAGE_INFO_DESCR, ss.str()); + return true; +} + +bool Party::isPlayerInvited(const Player* player) const +{ + return std::find(inviteList.begin(), inviteList.end(), player) != inviteList.end(); +} + +void Party::updateAllPartyIcons() +{ + for (Player* member : memberList) { + for (Player* otherMember : memberList) { + member->sendCreatureShield(otherMember); + } + + member->sendCreatureShield(leader); + leader->sendCreatureShield(member); + } + leader->sendCreatureShield(leader); +} + +void Party::broadcastPartyMessage(MessageClasses msgClass, const std::string& msg, bool sendToInvitations /*= false*/) +{ + for (Player* member : memberList) { + member->sendTextMessage(msgClass, msg); + } + + leader->sendTextMessage(msgClass, msg); + + if (sendToInvitations) { + for (Player* invitee : inviteList) { + invitee->sendTextMessage(msgClass, msg); + } + } +} + +void Party::updateSharedExperience() +{ + if (sharedExpActive) { + bool result = canEnableSharedExperience(); + if (result != sharedExpEnabled) { + sharedExpEnabled = result; + updateAllPartyIcons(); + } + } +} + +bool Party::setSharedExperience(Player* player, bool sharedExpActive) +{ + if (!player || leader != player) { + return false; + } + + if (this->sharedExpActive == sharedExpActive) { + return true; + } + + this->sharedExpActive = sharedExpActive; + + if (sharedExpActive) { + this->sharedExpEnabled = canEnableSharedExperience(); + + if (this->sharedExpEnabled) { + leader->sendTextMessage(MESSAGE_INFO_DESCR, "Shared Experience is now active."); + } else { + leader->sendTextMessage(MESSAGE_INFO_DESCR, "Shared Experience has been activated, but some members of your party are inactive."); + } + } else { + leader->sendTextMessage(MESSAGE_INFO_DESCR, "Shared Experience has been deactivated."); + } + + updateAllPartyIcons(); + return true; +} + +void Party::shareExperience(uint64_t experience, Creature* source/* = nullptr*/) +{ + uint64_t shareExperience = experience; + g_events->eventPartyOnShareExperience(this, shareExperience); + + for (Player* member : memberList) { + member->onGainSharedExperience(shareExperience, source); + } + leader->onGainSharedExperience(shareExperience, source); +} + +bool Party::canUseSharedExperience(const Player* player) const +{ + if (memberList.empty()) { + return false; + } + + uint32_t highestLevel = leader->getLevel(); + for (Player* member : memberList) { + if (member->getLevel() > highestLevel) { + highestLevel = member->getLevel(); + } + } + + uint32_t minLevel = static_cast(std::ceil((static_cast(highestLevel) * 2) / 3)); + if (player->getLevel() < minLevel) { + return false; + } + + if (!Position::areInRange<30, 30, 1>(leader->getPosition(), player->getPosition())) { + return false; + } + + if (!player->hasFlag(PlayerFlag_NotGainInFight)) { + //check if the player has healed/attacked anything recently + auto it = ticksMap.find(player->getID()); + if (it == ticksMap.end()) { + return false; + } + + uint64_t timeDiff = OTSYS_TIME() - it->second; + if (timeDiff > static_cast(g_config.getNumber(ConfigManager::PZ_LOCKED))) { + return false; + } + } + return true; +} + +bool Party::canEnableSharedExperience() +{ + if (!canUseSharedExperience(leader)) { + return false; + } + + for (Player* member : memberList) { + if (!canUseSharedExperience(member)) { + return false; + } + } + return true; +} + +void Party::updatePlayerTicks(Player* player, uint32_t points) +{ + if (points != 0 && !player->hasFlag(PlayerFlag_NotGainInFight)) { + ticksMap[player->getID()] = OTSYS_TIME(); + updateSharedExperience(); + } +} + +void Party::clearPlayerPoints(Player* player) +{ + auto it = ticksMap.find(player->getID()); + if (it != ticksMap.end()) { + ticksMap.erase(it); + updateSharedExperience(); + } +} + +bool Party::canOpenCorpse(uint32_t ownerId) const +{ + if (Player* player = g_game.getPlayerByID(ownerId)) { + return leader->getID() == ownerId || player->getParty() == this; + } + return false; +} diff --git a/src/party.h b/src/party.h new file mode 100644 index 0000000..50c10c0 --- /dev/null +++ b/src/party.h @@ -0,0 +1,97 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_PARTY_H_41D4D7CF417C4CC99FAE94D552255044 +#define FS_PARTY_H_41D4D7CF417C4CC99FAE94D552255044 + +#include "player.h" +#include "monsters.h" + +class Player; +class Party; + +using PlayerVector = std::vector; + +class Party +{ + public: + explicit Party(Player* leader); + + Player* getLeader() const { + return leader; + } + PlayerVector& getMembers() { + return memberList; + } + const PlayerVector& getInvitees() const { + return inviteList; + } + size_t getMemberCount() const { + return memberList.size(); + } + size_t getInvitationCount() const { + return inviteList.size(); + } + + void disband(); + bool invitePlayer(Player& player); + bool joinParty(Player& player); + void revokeInvitation(Player& player); + bool passPartyLeadership(Player* player); + bool leaveParty(Player* player); + + bool removeInvite(Player& player, bool removeFromPlayer = true); + + bool isPlayerInvited(const Player* player) const; + void updateAllPartyIcons(); + void broadcastPartyMessage(MessageClasses msgClass, const std::string& msg, bool sendToInvitations = false); + bool empty() const { + return memberList.empty() && inviteList.empty(); + } + bool canOpenCorpse(uint32_t ownerId) const; + + void shareExperience(uint64_t experience, Creature* source = nullptr); + bool setSharedExperience(Player* player, bool sharedExpActive); + bool isSharedExperienceActive() const { + return sharedExpActive; + } + bool isSharedExperienceEnabled() const { + return sharedExpEnabled; + } + bool canUseSharedExperience(const Player* player) const; + void updateSharedExperience(); + + void updatePlayerTicks(Player* player, uint32_t points); + void clearPlayerPoints(Player* player); + + private: + bool canEnableSharedExperience(); + + std::map ticksMap; + + PlayerVector memberList; + PlayerVector inviteList; + + Player* leader; + + bool sharedExpActive = false; + bool sharedExpEnabled = false; +}; + +#endif diff --git a/src/player.cpp b/src/player.cpp new file mode 100644 index 0000000..8da85c6 --- /dev/null +++ b/src/player.cpp @@ -0,0 +1,4552 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include + +#include "bed.h" +#include "chat.h" +#include "combat.h" +#include "configmanager.h" +#include "creatureevent.h" +#include "events.h" +#include "game.h" +#include "iologindata.h" +#include "monster.h" +#include "movement.h" +#include "scheduler.h" +#include "weapons.h" + +extern ConfigManager g_config; +extern Game g_game; +extern Chat* g_chat; +extern Vocations g_vocations; +extern MoveEvents* g_moveEvents; +extern Weapons* g_weapons; +extern CreatureEvents* g_creatureEvents; +extern Events* g_events; + +MuteCountMap Player::muteCountMap; + +uint32_t Player::playerAutoID = 0x10000000; + +Player::Player(ProtocolGame_ptr p) : + Creature(), lastPing(OTSYS_TIME()), lastPong(lastPing), inbox(new Inbox(ITEM_INBOX)), client(std::move(p)) +{ + inbox->incrementReferenceCounter(); +} + +Player::~Player() +{ + for (Item* item : inventory) { + if (item) { + item->setParent(nullptr); + item->decrementReferenceCounter(); + } + } + + for (const auto& it : depotLockerMap) { + it.second->removeInbox(inbox); + it.second->decrementReferenceCounter(); + } + + inbox->decrementReferenceCounter(); + + setWriteItem(nullptr); + setEditHouse(nullptr); +} + +bool Player::setVocation(uint16_t vocId) +{ + Vocation* voc = g_vocations.getVocation(vocId); + if (!voc) { + return false; + } + vocation = voc; + + Condition* condition = getCondition(CONDITION_REGENERATION, CONDITIONID_DEFAULT); + if (condition) { + condition->setParam(CONDITION_PARAM_HEALTHGAIN, vocation->getHealthGainAmount()); + condition->setParam(CONDITION_PARAM_HEALTHTICKS, vocation->getHealthGainTicks() * 1000); + condition->setParam(CONDITION_PARAM_MANAGAIN, vocation->getManaGainAmount()); + condition->setParam(CONDITION_PARAM_MANATICKS, vocation->getManaGainTicks() * 1000); + } + return true; +} + +bool Player::isPushable() const +{ + if (hasFlag(PlayerFlag_CannotBePushed)) { + return false; + } + return Creature::isPushable(); +} + +std::string Player::getDescription(int32_t lookDistance) const +{ + std::ostringstream s; + + if (lookDistance == -1) { + s << "yourself."; + + if (group->access) { + s << " You are " << group->name << '.'; + } else if (vocation->getId() != VOCATION_NONE) { + s << " You are " << vocation->getVocDescription() << '.'; + } else { + s << " You have no vocation."; + } + } else { + s << name; + if (!group->access) { + s << " (Level " << level << ')'; + } + s << '.'; + + if (sex == PLAYERSEX_FEMALE) { + s << " She"; + } else { + s << " He"; + } + + if (group->access) { + s << " is " << group->name << '.'; + } else if (vocation->getId() != VOCATION_NONE) { + s << " is " << vocation->getVocDescription() << '.'; + } else { + s << " has no vocation."; + } + } + + if (party) { + if (lookDistance == -1) { + s << " Your party has "; + } else if (sex == PLAYERSEX_FEMALE) { + s << " She is in a party with "; + } else { + s << " He is in a party with "; + } + + size_t memberCount = party->getMemberCount() + 1; + if (memberCount == 1) { + s << "1 member and "; + } else { + s << memberCount << " members and "; + } + + size_t invitationCount = party->getInvitationCount(); + if (invitationCount == 1) { + s << "1 pending invitation."; + } else { + s << invitationCount << " pending invitations."; + } + } + + if (!guild || !guildRank) { + return s.str(); + } + + if (lookDistance == -1) { + s << " You are "; + } else if (sex == PLAYERSEX_FEMALE) { + s << " She is "; + } else { + s << " He is "; + } + + s << guildRank->name << " of the " << guild->getName(); + if (!guildNick.empty()) { + s << " (" << guildNick << ')'; + } + + size_t memberCount = guild->getMemberCount(); + if (memberCount == 1) { + s << ", which has 1 member, " << guild->getMembersOnline().size() << " of them online."; + } else { + s << ", which has " << memberCount << " members, " << guild->getMembersOnline().size() << " of them online."; + } + return s.str(); +} + +Item* Player::getInventoryItem(slots_t slot) const +{ + if (slot < CONST_SLOT_FIRST || slot > CONST_SLOT_LAST) { + return nullptr; + } + return inventory[slot]; +} + +void Player::addConditionSuppressions(uint32_t conditions) +{ + conditionSuppressions |= conditions; +} + +void Player::removeConditionSuppressions(uint32_t conditions) +{ + conditionSuppressions &= ~conditions; +} + +Item* Player::getWeapon(slots_t slot, bool ignoreAmmo) const +{ + Item* item = inventory[slot]; + if (!item) { + return nullptr; + } + + WeaponType_t weaponType = item->getWeaponType(); + if (weaponType == WEAPON_NONE || weaponType == WEAPON_SHIELD || weaponType == WEAPON_AMMO) { + return nullptr; + } + + if (!ignoreAmmo && weaponType == WEAPON_DISTANCE) { + const ItemType& it = Item::items[item->getID()]; + if (it.ammoType != AMMO_NONE) { + Item* ammoItem = inventory[CONST_SLOT_AMMO]; + if (!ammoItem || ammoItem->getAmmoType() != it.ammoType) { + return nullptr; + } + item = ammoItem; + } + } + return item; +} + +Item* Player::getWeapon(bool ignoreAmmo/* = false*/) const +{ + Item* item = getWeapon(CONST_SLOT_LEFT, ignoreAmmo); + if (item) { + return item; + } + + item = getWeapon(CONST_SLOT_RIGHT, ignoreAmmo); + if (item) { + return item; + } + return nullptr; +} + +WeaponType_t Player::getWeaponType() const +{ + Item* item = getWeapon(); + if (!item) { + return WEAPON_NONE; + } + return item->getWeaponType(); +} + +int32_t Player::getWeaponSkill(const Item* item) const +{ + if (!item) { + return getSkillLevel(SKILL_FIST); + } + + int32_t attackSkill; + + WeaponType_t weaponType = item->getWeaponType(); + switch (weaponType) { + case WEAPON_SWORD: { + attackSkill = getSkillLevel(SKILL_SWORD); + break; + } + + case WEAPON_CLUB: { + attackSkill = getSkillLevel(SKILL_CLUB); + break; + } + + case WEAPON_AXE: { + attackSkill = getSkillLevel(SKILL_AXE); + break; + } + + case WEAPON_DISTANCE: { + attackSkill = getSkillLevel(SKILL_DISTANCE); + break; + } + + default: { + attackSkill = 0; + break; + } + } + return attackSkill; +} + +int32_t Player::getArmor() const +{ + int32_t armor = 0; + + static const slots_t armorSlots[] = {CONST_SLOT_HEAD, CONST_SLOT_NECKLACE, CONST_SLOT_ARMOR, CONST_SLOT_LEGS, CONST_SLOT_FEET, CONST_SLOT_RING}; + for (slots_t slot : armorSlots) { + Item* inventoryItem = inventory[slot]; + if (inventoryItem) { + armor += inventoryItem->getArmor(); + } + } + return static_cast(armor * vocation->armorMultiplier); +} + +void Player::getShieldAndWeapon(const Item*& shield, const Item*& weapon) const +{ + shield = nullptr; + weapon = nullptr; + + for (uint32_t slot = CONST_SLOT_RIGHT; slot <= CONST_SLOT_LEFT; slot++) { + Item* item = inventory[slot]; + if (!item) { + continue; + } + + switch (item->getWeaponType()) { + case WEAPON_NONE: + break; + + case WEAPON_SHIELD: { + if (!shield || item->getDefense() > shield->getDefense()) { + shield = item; + } + break; + } + + default: { // weapons that are not shields + weapon = item; + break; + } + } + } +} + +int32_t Player::getDefense() const +{ + int32_t defenseSkill = getSkillLevel(SKILL_FIST); + int32_t defenseValue = 7; + const Item* weapon; + const Item* shield; + getShieldAndWeapon(shield, weapon); + + if (weapon) { + defenseValue = weapon->getDefense() + weapon->getExtraDefense(); + defenseSkill = getWeaponSkill(weapon); + } + + if (shield) { + defenseValue = weapon != nullptr ? shield->getDefense() + weapon->getExtraDefense() : shield->getDefense(); + defenseSkill = getSkillLevel(SKILL_SHIELD); + } + + if (defenseSkill == 0) { + switch (fightMode) { + case FIGHTMODE_ATTACK: + case FIGHTMODE_BALANCED: + return 1; + + case FIGHTMODE_DEFENSE: + return 2; + } + } + + return (defenseSkill / 4. + 2.23) * defenseValue * 0.15 * getDefenseFactor() * vocation->defenseMultiplier; +} + +float Player::getAttackFactor() const +{ + switch (fightMode) { + case FIGHTMODE_ATTACK: return 1.0f; + case FIGHTMODE_BALANCED: return 1.2f; + case FIGHTMODE_DEFENSE: return 2.0f; + default: return 1.0f; + } +} + +float Player::getDefenseFactor() const +{ + switch (fightMode) { + case FIGHTMODE_ATTACK: return (OTSYS_TIME() - lastAttack) < getAttackSpeed() ? 0.5f : 1.0f; + case FIGHTMODE_BALANCED: return (OTSYS_TIME() - lastAttack) < getAttackSpeed() ? 0.75f : 1.0f; + case FIGHTMODE_DEFENSE: return 1.0f; + default: return 1.0f; + } +} + +uint16_t Player::getClientIcons() const +{ + uint16_t icons = 0; + for (Condition* condition : conditions) { + if (!isSuppress(condition->getType())) { + icons |= condition->getIcons(); + } + } + + if (pzLocked) { + icons |= ICON_REDSWORDS; + } + + if (tile->hasFlag(TILESTATE_PROTECTIONZONE)) { + icons |= ICON_PIGEON; + + // Don't show ICON_SWORDS if player is in protection zone. + if (hasBitSet(ICON_SWORDS, icons)) { + icons &= ~ICON_SWORDS; + } + } + + // Game client debugs with 10 or more icons + // so let's prevent that from happening. + std::bitset<20> icon_bitset(static_cast(icons)); + for (size_t pos = 0, bits_set = icon_bitset.count(); bits_set >= 10; ++pos) { + if (icon_bitset[pos]) { + icon_bitset.reset(pos); + --bits_set; + } + } + return icon_bitset.to_ulong(); +} + +void Player::updateInventoryWeight() +{ + if (hasFlag(PlayerFlag_HasInfiniteCapacity)) { + return; + } + + inventoryWeight = 0; + for (int i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) { + const Item* item = inventory[i]; + if (item) { + inventoryWeight += item->getWeight(); + } + } +} + +void Player::addSkillAdvance(skills_t skill, uint64_t count) +{ + uint64_t currReqTries = vocation->getReqSkillTries(skill, skills[skill].level); + uint64_t nextReqTries = vocation->getReqSkillTries(skill, skills[skill].level + 1); + if (currReqTries >= nextReqTries) { + //player has reached max skill + return; + } + + g_events->eventPlayerOnGainSkillTries(this, skill, count); + if (count == 0) { + return; + } + + bool sendUpdateSkills = false; + while ((skills[skill].tries + count) >= nextReqTries) { + count -= nextReqTries - skills[skill].tries; + skills[skill].level++; + skills[skill].tries = 0; + skills[skill].percent = 0; + + std::ostringstream ss; + ss << "You advanced to " << getSkillName(skill) << " level " << skills[skill].level << '.'; + sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str()); + + g_creatureEvents->playerAdvance(this, skill, (skills[skill].level - 1), skills[skill].level); + + sendUpdateSkills = true; + currReqTries = nextReqTries; + nextReqTries = vocation->getReqSkillTries(skill, skills[skill].level + 1); + if (currReqTries >= nextReqTries) { + count = 0; + break; + } + } + + skills[skill].tries += count; + + uint32_t newPercent; + if (nextReqTries > currReqTries) { + newPercent = Player::getPercentLevel(skills[skill].tries, nextReqTries); + } else { + newPercent = 0; + } + + if (skills[skill].percent != newPercent) { + skills[skill].percent = newPercent; + sendUpdateSkills = true; + } + + if (sendUpdateSkills) { + sendSkills(); + } +} + +void Player::setVarStats(stats_t stat, int32_t modifier) +{ + varStats[stat] += modifier; + + switch (stat) { + case STAT_MAXHITPOINTS: { + if (getHealth() > getMaxHealth()) { + Creature::changeHealth(getMaxHealth() - getHealth()); + } else { + g_game.addCreatureHealth(this); + } + break; + } + + case STAT_MAXMANAPOINTS: { + if (getMana() > getMaxMana()) { + changeMana(getMaxMana() - getMana()); + } + break; + } + + default: { + break; + } + } +} + +int32_t Player::getDefaultStats(stats_t stat) const +{ + switch (stat) { + case STAT_MAXHITPOINTS: return healthMax; + case STAT_MAXMANAPOINTS: return manaMax; + case STAT_MAGICPOINTS: return getBaseMagicLevel(); + default: return 0; + } +} + +void Player::addContainer(uint8_t cid, Container* container) +{ + if (cid > 0xF) { + return; + } + + if (container->getID() == ITEM_BROWSEFIELD) { + container->incrementReferenceCounter(); + } + + auto it = openContainers.find(cid); + if (it != openContainers.end()) { + OpenContainer& openContainer = it->second; + Container* oldContainer = openContainer.container; + if (oldContainer->getID() == ITEM_BROWSEFIELD) { + oldContainer->decrementReferenceCounter(); + } + + openContainer.container = container; + openContainer.index = 0; + } else { + OpenContainer openContainer; + openContainer.container = container; + openContainer.index = 0; + openContainers[cid] = openContainer; + } +} + +void Player::closeContainer(uint8_t cid) +{ + auto it = openContainers.find(cid); + if (it == openContainers.end()) { + return; + } + + OpenContainer openContainer = it->second; + Container* container = openContainer.container; + openContainers.erase(it); + + if (container && container->getID() == ITEM_BROWSEFIELD) { + container->decrementReferenceCounter(); + } +} + +void Player::setContainerIndex(uint8_t cid, uint16_t index) +{ + auto it = openContainers.find(cid); + if (it == openContainers.end()) { + return; + } + it->second.index = index; +} + +Container* Player::getContainerByID(uint8_t cid) +{ + auto it = openContainers.find(cid); + if (it == openContainers.end()) { + return nullptr; + } + return it->second.container; +} + +int8_t Player::getContainerID(const Container* container) const +{ + for (const auto& it : openContainers) { + if (it.second.container == container) { + return it.first; + } + } + return -1; +} + +uint16_t Player::getContainerIndex(uint8_t cid) const +{ + auto it = openContainers.find(cid); + if (it == openContainers.end()) { + return 0; + } + return it->second.index; +} + +bool Player::canOpenCorpse(uint32_t ownerId) const +{ + return getID() == ownerId || (party && party->canOpenCorpse(ownerId)); +} + +uint16_t Player::getLookCorpse() const +{ + if (sex == PLAYERSEX_FEMALE) { + return ITEM_FEMALE_CORPSE; + } else { + return ITEM_MALE_CORPSE; + } +} + +void Player::addStorageValue(const uint32_t key, const int32_t value, const bool isLogin/* = false*/) +{ + if (IS_IN_KEYRANGE(key, RESERVED_RANGE)) { + if (IS_IN_KEYRANGE(key, OUTFITS_RANGE)) { + outfits.emplace_back( + value >> 16, + value & 0xFF + ); + return; + } else if (IS_IN_KEYRANGE(key, MOUNTS_RANGE)) { + // do nothing + } else { + std::cout << "Warning: unknown reserved key: " << key << " player: " << getName() << std::endl; + return; + } + } + + if (value != -1) { + int32_t oldValue; + getStorageValue(key, oldValue); + + storageMap[key] = value; + + if (!isLogin) { + auto currentFrameTime = g_dispatcher.getDispatcherCycle(); + if (lastQuestlogUpdate != currentFrameTime && g_game.quests.isQuestStorage(key, value, oldValue)) { + lastQuestlogUpdate = currentFrameTime; + sendTextMessage(MESSAGE_EVENT_ADVANCE, "Your questlog has been updated."); + } + } + } else { + storageMap.erase(key); + } +} + +bool Player::getStorageValue(const uint32_t key, int32_t& value) const +{ + auto it = storageMap.find(key); + if (it == storageMap.end()) { + value = -1; + return false; + } + + value = it->second; + return true; +} + +bool Player::canSee(const Position& pos) const +{ + if (!client) { + return false; + } + return client->canSee(pos); +} + +bool Player::canSeeCreature(const Creature* creature) const +{ + if (creature == this) { + return true; + } + + if (creature->isInGhostMode() && !group->access) { + return false; + } + + if (!creature->getPlayer() && !canSeeInvisibility() && creature->isInvisible()) { + return false; + } + return true; +} + +bool Player::canWalkthrough(const Creature* creature) const +{ + if (group->access || creature->isInGhostMode()) { + return true; + } + + const Player* player = creature->getPlayer(); + if (!player) { + return false; + } + + const Tile* playerTile = player->getTile(); + if (!playerTile || (!playerTile->hasFlag(TILESTATE_PROTECTIONZONE) && player->getLevel() > static_cast(g_config.getNumber(ConfigManager::PROTECTION_LEVEL)))) { + return false; + } + + const Item* playerTileGround = playerTile->getGround(); + if (!playerTileGround || !playerTileGround->hasWalkStack()) { + return false; + } + + Player* thisPlayer = const_cast(this); + if ((OTSYS_TIME() - lastWalkthroughAttempt) > 2000) { + thisPlayer->setLastWalkthroughAttempt(OTSYS_TIME()); + return false; + } + + if (creature->getPosition() != lastWalkthroughPosition) { + thisPlayer->setLastWalkthroughPosition(creature->getPosition()); + return false; + } + + thisPlayer->setLastWalkthroughPosition(creature->getPosition()); + return true; +} + +bool Player::canWalkthroughEx(const Creature* creature) const +{ + if (group->access) { + return true; + } + + const Player* player = creature->getPlayer(); + if (!player) { + return false; + } + + const Tile* playerTile = player->getTile(); + return playerTile && (playerTile->hasFlag(TILESTATE_PROTECTIONZONE) || player->getLevel() <= static_cast(g_config.getNumber(ConfigManager::PROTECTION_LEVEL))); +} + +void Player::onReceiveMail() const +{ + if (isNearDepotBox()) { + sendTextMessage(MESSAGE_EVENT_ADVANCE, "New mail has arrived."); + } +} + +bool Player::isNearDepotBox() const +{ + const Position& pos = getPosition(); + for (int32_t cx = -1; cx <= 1; ++cx) { + for (int32_t cy = -1; cy <= 1; ++cy) { + Tile* tile = g_game.map.getTile(pos.x + cx, pos.y + cy, pos.z); + if (!tile) { + continue; + } + + if (tile->hasFlag(TILESTATE_DEPOT)) { + return true; + } + } + } + return false; +} + +DepotChest* Player::getDepotChest(uint32_t depotId, bool autoCreate) +{ + auto it = depotChests.find(depotId); + if (it != depotChests.end()) { + return it->second; + } + + if (!autoCreate) { + return nullptr; + } + + DepotChest* depotChest = new DepotChest(ITEM_DEPOT); + depotChest->incrementReferenceCounter(); + depotChest->setMaxDepotItems(getMaxDepotItems()); + depotChests[depotId] = depotChest; + return depotChest; +} + +DepotLocker* Player::getDepotLocker(uint32_t depotId) +{ + auto it = depotLockerMap.find(depotId); + if (it != depotLockerMap.end()) { + inbox->setParent(it->second); + return it->second; + } + + DepotLocker* depotLocker = new DepotLocker(ITEM_LOCKER1); + depotLocker->setDepotId(depotId); + depotLocker->internalAddThing(Item::CreateItem(ITEM_MARKET)); + depotLocker->internalAddThing(inbox); + depotLocker->internalAddThing(getDepotChest(depotId, true)); + depotLockerMap[depotId] = depotLocker; + return depotLocker; +} + +void Player::sendCancelMessage(ReturnValue message) const +{ + sendCancelMessage(getReturnMessage(message)); +} + +void Player::sendStats() +{ + if (client) { + client->sendStats(); + lastStatsTrainingTime = getOfflineTrainingTime() / 60 / 1000; + } +} + +void Player::sendPing() +{ + int64_t timeNow = OTSYS_TIME(); + + bool hasLostConnection = false; + if ((timeNow - lastPing) >= 5000) { + lastPing = timeNow; + if (client) { + client->sendPing(); + } else { + hasLostConnection = true; + } + } + + int64_t noPongTime = timeNow - lastPong; + if ((hasLostConnection || noPongTime >= 7000) && attackedCreature && attackedCreature->getPlayer()) { + setAttackedCreature(nullptr); + } + + if (noPongTime >= 60000 && canLogout()) { + if (g_creatureEvents->playerLogout(this)) { + if (client) { + client->logout(true, true); + } else { + g_game.removeCreature(this, true); + } + } + } +} + +Item* Player::getWriteItem(uint32_t& windowTextId, uint16_t& maxWriteLen) +{ + windowTextId = this->windowTextId; + maxWriteLen = this->maxWriteLen; + return writeItem; +} + +void Player::setWriteItem(Item* item, uint16_t maxWriteLen /*= 0*/) +{ + windowTextId++; + + if (writeItem) { + writeItem->decrementReferenceCounter(); + } + + if (item) { + writeItem = item; + this->maxWriteLen = maxWriteLen; + writeItem->incrementReferenceCounter(); + } else { + writeItem = nullptr; + this->maxWriteLen = 0; + } +} + +House* Player::getEditHouse(uint32_t& windowTextId, uint32_t& listId) +{ + windowTextId = this->windowTextId; + listId = this->editListId; + return editHouse; +} + +void Player::setEditHouse(House* house, uint32_t listId /*= 0*/) +{ + windowTextId++; + editHouse = house; + editListId = listId; +} + +void Player::sendHouseWindow(House* house, uint32_t listId) const +{ + if (!client) { + return; + } + + std::string text; + if (house->getAccessList(listId, text)) { + client->sendHouseWindow(windowTextId, text); + } +} + +//container +void Player::sendAddContainerItem(const Container* container, const Item* item) +{ + if (!client) { + return; + } + + for (const auto& it : openContainers) { + const OpenContainer& openContainer = it.second; + if (openContainer.container != container) { + continue; + } + + uint16_t slot = openContainer.index; + if (container->getID() == ITEM_BROWSEFIELD) { + uint16_t containerSize = container->size() - 1; + uint16_t pageEnd = openContainer.index + container->capacity() - 1; + if (containerSize > pageEnd) { + slot = pageEnd; + item = container->getItemByIndex(pageEnd); + } else { + slot = containerSize; + } + } else if (openContainer.index >= container->capacity()) { + item = container->getItemByIndex(openContainer.index - 1); + } + client->sendAddContainerItem(it.first, slot, item); + } +} + +void Player::sendUpdateContainerItem(const Container* container, uint16_t slot, const Item* newItem) +{ + if (!client) { + return; + } + + for (const auto& it : openContainers) { + const OpenContainer& openContainer = it.second; + if (openContainer.container != container) { + continue; + } + + if (slot < openContainer.index) { + continue; + } + + uint16_t pageEnd = openContainer.index + container->capacity(); + if (slot >= pageEnd) { + continue; + } + + client->sendUpdateContainerItem(it.first, slot, newItem); + } +} + +void Player::sendRemoveContainerItem(const Container* container, uint16_t slot) +{ + if (!client) { + return; + } + + for (auto& it : openContainers) { + OpenContainer& openContainer = it.second; + if (openContainer.container != container) { + continue; + } + + uint16_t& firstIndex = openContainer.index; + if (firstIndex > 0 && firstIndex >= container->size() - 1) { + firstIndex -= container->capacity(); + sendContainer(it.first, container, false, firstIndex); + } + + client->sendRemoveContainerItem(it.first, std::max(slot, firstIndex), container->getItemByIndex(container->capacity() + firstIndex)); + } +} + +void Player::onUpdateTileItem(const Tile* tile, const Position& pos, const Item* oldItem, + const ItemType& oldType, const Item* newItem, const ItemType& newType) +{ + Creature::onUpdateTileItem(tile, pos, oldItem, oldType, newItem, newType); + + if (oldItem != newItem) { + onRemoveTileItem(tile, pos, oldType, oldItem); + } + + if (tradeState != TRADE_TRANSFER) { + if (tradeItem && oldItem == tradeItem) { + g_game.internalCloseTrade(this); + } + } +} + +void Player::onRemoveTileItem(const Tile* tile, const Position& pos, const ItemType& iType, + const Item* item) +{ + Creature::onRemoveTileItem(tile, pos, iType, item); + + if (tradeState != TRADE_TRANSFER) { + checkTradeState(item); + + if (tradeItem) { + const Container* container = item->getContainer(); + if (container && container->isHoldingItem(tradeItem)) { + g_game.internalCloseTrade(this); + } + } + } +} + +void Player::onCreatureAppear(Creature* creature, bool isLogin) +{ + Creature::onCreatureAppear(creature, isLogin); + + if (isLogin && creature == this) { + sendItems(); + + for (int32_t slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_LAST; ++slot) { + Item* item = inventory[slot]; + if (item) { + item->startDecaying(); + g_moveEvents->onPlayerEquip(this, item, static_cast(slot), false); + } + } + + for (Condition* condition : storedConditionList) { + addCondition(condition); + } + storedConditionList.clear(); + + BedItem* bed = g_game.getBedBySleeper(guid); + if (bed) { + bed->wakeUp(this); + } + + Account account = IOLoginData::loadAccount(accountNumber); + Game::updatePremium(account); + + std::cout << name << " has logged in." << std::endl; + + if (guild) { + guild->addMember(this); + } + + int32_t offlineTime; + if (getLastLogout() != 0) { + // Not counting more than 21 days to prevent overflow when multiplying with 1000 (for milliseconds). + offlineTime = std::min(time(nullptr) - getLastLogout(), 86400 * 21); + } else { + offlineTime = 0; + } + + for (Condition* condition : getMuteConditions()) { + condition->setTicks(condition->getTicks() - (offlineTime * 1000)); + if (condition->getTicks() <= 0) { + removeCondition(condition); + } + } + + g_game.checkPlayersRecord(); + IOLoginData::updateOnlineStatus(guid, true); + } +} + +void Player::onAttackedCreatureDisappear(bool isLogout) +{ + sendCancelTarget(); + + if (!isLogout) { + sendTextMessage(MESSAGE_STATUS_SMALL, "Target lost."); + } +} + +void Player::onFollowCreatureDisappear(bool isLogout) +{ + sendCancelTarget(); + + if (!isLogout) { + sendTextMessage(MESSAGE_STATUS_SMALL, "Target lost."); + } +} + +void Player::onChangeZone(ZoneType_t zone) +{ + if (zone == ZONE_PROTECTION) { + if (attackedCreature && !hasFlag(PlayerFlag_IgnoreProtectionZone)) { + setAttackedCreature(nullptr); + onAttackedCreatureDisappear(false); + } + + if (!group->access && isMounted()) { + dismount(); + g_game.internalCreatureChangeOutfit(this, defaultOutfit); + wasMounted = true; + } + } else { + if (wasMounted) { + toggleMount(true); + wasMounted = false; + } + } + + g_game.updateCreatureWalkthrough(this); + sendIcons(); +} + +void Player::onAttackedCreatureChangeZone(ZoneType_t zone) +{ + if (zone == ZONE_PROTECTION) { + if (!hasFlag(PlayerFlag_IgnoreProtectionZone)) { + setAttackedCreature(nullptr); + onAttackedCreatureDisappear(false); + } + } else if (zone == ZONE_NOPVP) { + if (attackedCreature->getPlayer()) { + if (!hasFlag(PlayerFlag_IgnoreProtectionZone)) { + setAttackedCreature(nullptr); + onAttackedCreatureDisappear(false); + } + } + } else if (zone == ZONE_NORMAL) { + //attackedCreature can leave a pvp zone if not pzlocked + if (g_game.getWorldType() == WORLD_TYPE_NO_PVP) { + if (attackedCreature->getPlayer()) { + setAttackedCreature(nullptr); + onAttackedCreatureDisappear(false); + } + } + } +} + +void Player::onRemoveCreature(Creature* creature, bool isLogout) +{ + Creature::onRemoveCreature(creature, isLogout); + + if (creature == this) { + if (isLogout) { + loginPosition = getPosition(); + } + + lastLogout = time(nullptr); + + if (eventWalk != 0) { + setFollowCreature(nullptr); + } + + if (tradePartner) { + g_game.internalCloseTrade(this); + } + + closeShopWindow(); + + clearPartyInvitations(); + + if (party) { + party->leaveParty(this); + } + + g_chat->removeUserFromAllChannels(*this); + + std::cout << getName() << " has logged out." << std::endl; + + if (guild) { + guild->removeMember(this); + } + + IOLoginData::updateOnlineStatus(guid, false); + + bool saved = false; + for (uint32_t tries = 0; tries < 3; ++tries) { + if (IOLoginData::savePlayer(this)) { + saved = true; + break; + } + } + + if (!saved) { + std::cout << "Error while saving player: " << getName() << std::endl; + } + } +} + +void Player::openShopWindow(Npc* npc, const std::list& shop) +{ + shopItemList = shop; + sendShop(npc); + sendSaleItemList(); +} + +bool Player::closeShopWindow(bool sendCloseShopWindow /*= true*/) +{ + //unreference callbacks + int32_t onBuy; + int32_t onSell; + + Npc* npc = getShopOwner(onBuy, onSell); + if (!npc) { + shopItemList.clear(); + return false; + } + + setShopOwner(nullptr, -1, -1); + npc->onPlayerEndTrade(this, onBuy, onSell); + + if (sendCloseShopWindow) { + sendCloseShop(); + } + + shopItemList.clear(); + return true; +} + +void Player::onWalk(Direction& dir) +{ + Creature::onWalk(dir); + setNextActionTask(nullptr); + setNextAction(OTSYS_TIME() + getStepDuration(dir)); +} + +void Player::onCreatureMove(Creature* creature, const Tile* newTile, const Position& newPos, + const Tile* oldTile, const Position& oldPos, bool teleport) +{ + Creature::onCreatureMove(creature, newTile, newPos, oldTile, oldPos, teleport); + + if (hasFollowPath && (creature == followCreature || (creature == this && followCreature))) { + isUpdatingPath = false; + g_dispatcher.addTask(createTask(std::bind(&Game::updateCreatureWalk, &g_game, getID()))); + } + + if (creature != this) { + return; + } + + if (tradeState != TRADE_TRANSFER) { + //check if we should close trade + if (tradeItem && !Position::areInRange<1, 1, 0>(tradeItem->getPosition(), getPosition())) { + g_game.internalCloseTrade(this); + } + + if (tradePartner && !Position::areInRange<2, 2, 0>(tradePartner->getPosition(), getPosition())) { + g_game.internalCloseTrade(this); + } + } + + // close modal windows + if (!modalWindows.empty()) { + // TODO: This shouldn't be hardcoded + for (uint32_t modalWindowId : modalWindows) { + if (modalWindowId == std::numeric_limits::max()) { + sendTextMessage(MESSAGE_EVENT_ADVANCE, "Offline training aborted."); + break; + } + } + modalWindows.clear(); + } + + // leave market + if (inMarket) { + inMarket = false; + } + + if (party) { + party->updateSharedExperience(); + } + + if (teleport || oldPos.z != newPos.z) { + int32_t ticks = g_config.getNumber(ConfigManager::STAIRHOP_DELAY); + if (ticks > 0) { + if (Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_PACIFIED, ticks, 0)) { + addCondition(condition); + } + } + } +} + +//container +void Player::onAddContainerItem(const Item* item) +{ + checkTradeState(item); +} + +void Player::onUpdateContainerItem(const Container* container, const Item* oldItem, const Item* newItem) +{ + if (oldItem != newItem) { + onRemoveContainerItem(container, oldItem); + } + + if (tradeState != TRADE_TRANSFER) { + checkTradeState(oldItem); + } +} + +void Player::onRemoveContainerItem(const Container* container, const Item* item) +{ + if (tradeState != TRADE_TRANSFER) { + checkTradeState(item); + + if (tradeItem) { + if (tradeItem->getParent() != container && container->isHoldingItem(tradeItem)) { + g_game.internalCloseTrade(this); + } + } + } +} + +void Player::onCloseContainer(const Container* container) +{ + if (!client) { + return; + } + + for (const auto& it : openContainers) { + if (it.second.container == container) { + client->sendCloseContainer(it.first); + } + } +} + +void Player::onSendContainer(const Container* container) +{ + if (!client) { + return; + } + + bool hasParent = container->hasParent(); + for (const auto& it : openContainers) { + const OpenContainer& openContainer = it.second; + if (openContainer.container == container) { + client->sendContainer(it.first, container, hasParent, openContainer.index); + } + } +} + +//inventory +void Player::onUpdateInventoryItem(Item* oldItem, Item* newItem) +{ + if (oldItem != newItem) { + onRemoveInventoryItem(oldItem); + } + + if (tradeState != TRADE_TRANSFER) { + checkTradeState(oldItem); + } +} + +void Player::onRemoveInventoryItem(Item* item) +{ + if (tradeState != TRADE_TRANSFER) { + checkTradeState(item); + + if (tradeItem) { + const Container* container = item->getContainer(); + if (container && container->isHoldingItem(tradeItem)) { + g_game.internalCloseTrade(this); + } + } + } +} + +void Player::checkTradeState(const Item* item) +{ + if (!tradeItem || tradeState == TRADE_TRANSFER) { + return; + } + + if (tradeItem == item) { + g_game.internalCloseTrade(this); + } else { + const Container* container = dynamic_cast(item->getParent()); + while (container) { + if (container == tradeItem) { + g_game.internalCloseTrade(this); + break; + } + + container = dynamic_cast(container->getParent()); + } + } +} + +void Player::setNextWalkActionTask(SchedulerTask* task) +{ + if (walkTaskEvent != 0) { + g_scheduler.stopEvent(walkTaskEvent); + walkTaskEvent = 0; + } + + delete walkTask; + walkTask = task; +} + +void Player::setNextWalkTask(SchedulerTask* task) +{ + if (nextStepEvent != 0) { + g_scheduler.stopEvent(nextStepEvent); + nextStepEvent = 0; + } + + if (task) { + nextStepEvent = g_scheduler.addEvent(task); + resetIdleTime(); + } +} + +void Player::setNextActionTask(SchedulerTask* task) +{ + if (actionTaskEvent != 0) { + g_scheduler.stopEvent(actionTaskEvent); + actionTaskEvent = 0; + } + + if (task) { + actionTaskEvent = g_scheduler.addEvent(task); + resetIdleTime(); + } +} + +uint32_t Player::getNextActionTime() const +{ + return std::max(SCHEDULER_MINTICKS, nextAction - OTSYS_TIME()); +} + +void Player::onThink(uint32_t interval) +{ + Creature::onThink(interval); + + sendPing(); + + MessageBufferTicks += interval; + if (MessageBufferTicks >= 1500) { + MessageBufferTicks = 0; + addMessageBuffer(); + } + + if (!getTile()->hasFlag(TILESTATE_NOLOGOUT) && !isAccessPlayer()) { + idleTime += interval; + const int32_t kickAfterMinutes = g_config.getNumber(ConfigManager::KICK_AFTER_MINUTES); + if (idleTime > (kickAfterMinutes * 60000) + 60000) { + kickPlayer(true); + } else if (client && idleTime == 60000 * kickAfterMinutes) { + std::ostringstream ss; + ss << "There was no variation in your behaviour for " << kickAfterMinutes << " minutes. You will be disconnected in one minute if there is no change in your actions until then."; + client->sendTextMessage(TextMessage(MESSAGE_STATUS_WARNING, ss.str())); + } + } + + if (g_game.getWorldType() != WORLD_TYPE_PVP_ENFORCED) { + checkSkullTicks(interval / 1000); + } + + addOfflineTrainingTime(interval); + if (lastStatsTrainingTime != getOfflineTrainingTime() / 60 / 1000) { + sendStats(); + } +} + +uint32_t Player::isMuted() const +{ + if (hasFlag(PlayerFlag_CannotBeMuted)) { + return 0; + } + + int32_t muteTicks = 0; + for (Condition* condition : conditions) { + if (condition->getType() == CONDITION_MUTED && condition->getTicks() > muteTicks) { + muteTicks = condition->getTicks(); + } + } + return static_cast(muteTicks) / 1000; +} + +void Player::addMessageBuffer() +{ + if (MessageBufferCount > 0 && g_config.getNumber(ConfigManager::MAX_MESSAGEBUFFER) != 0 && !hasFlag(PlayerFlag_CannotBeMuted)) { + --MessageBufferCount; + } +} + +void Player::removeMessageBuffer() +{ + if (hasFlag(PlayerFlag_CannotBeMuted)) { + return; + } + + const int32_t maxMessageBuffer = g_config.getNumber(ConfigManager::MAX_MESSAGEBUFFER); + if (maxMessageBuffer != 0 && MessageBufferCount <= maxMessageBuffer + 1) { + if (++MessageBufferCount > maxMessageBuffer) { + uint32_t muteCount = 1; + auto it = muteCountMap.find(guid); + if (it != muteCountMap.end()) { + muteCount = it->second; + } + + uint32_t muteTime = 5 * muteCount * muteCount; + muteCountMap[guid] = muteCount + 1; + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_MUTED, muteTime * 1000, 0); + addCondition(condition); + + std::ostringstream ss; + ss << "You are muted for " << muteTime << " seconds."; + sendTextMessage(MESSAGE_STATUS_SMALL, ss.str()); + } + } +} + +void Player::drainHealth(Creature* attacker, int32_t damage) +{ + Creature::drainHealth(attacker, damage); + sendStats(); +} + +void Player::drainMana(Creature* attacker, int32_t manaLoss) +{ + onAttacked(); + changeMana(-manaLoss); + + if (attacker) { + addDamagePoints(attacker, manaLoss); + } + + sendStats(); +} + +void Player::addManaSpent(uint64_t amount) +{ + if (hasFlag(PlayerFlag_NotGainMana)) { + return; + } + + uint64_t currReqMana = vocation->getReqMana(magLevel); + uint64_t nextReqMana = vocation->getReqMana(magLevel + 1); + if (currReqMana >= nextReqMana) { + //player has reached max magic level + return; + } + + g_events->eventPlayerOnGainSkillTries(this, SKILL_MAGLEVEL, amount); + if (amount == 0) { + return; + } + + bool sendUpdateStats = false; + while ((manaSpent + amount) >= nextReqMana) { + amount -= nextReqMana - manaSpent; + + magLevel++; + manaSpent = 0; + + std::ostringstream ss; + ss << "You advanced to magic level " << magLevel << '.'; + sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str()); + + g_creatureEvents->playerAdvance(this, SKILL_MAGLEVEL, magLevel - 1, magLevel); + + sendUpdateStats = true; + currReqMana = nextReqMana; + nextReqMana = vocation->getReqMana(magLevel + 1); + if (currReqMana >= nextReqMana) { + return; + } + } + + manaSpent += amount; + + uint8_t oldPercent = magLevelPercent; + if (nextReqMana > currReqMana) { + magLevelPercent = Player::getPercentLevel(manaSpent, nextReqMana); + } else { + magLevelPercent = 0; + } + + if (oldPercent != magLevelPercent) { + sendUpdateStats = true; + } + + if (sendUpdateStats) { + sendStats(); + } +} + +void Player::addExperience(Creature* source, uint64_t exp, bool sendText/* = false*/) +{ + uint64_t currLevelExp = Player::getExpForLevel(level); + uint64_t nextLevelExp = Player::getExpForLevel(level + 1); + uint64_t rawExp = exp; + if (currLevelExp >= nextLevelExp) { + //player has reached max level + levelPercent = 0; + sendStats(); + return; + } + + g_events->eventPlayerOnGainExperience(this, source, exp, rawExp); + if (exp == 0) { + return; + } + + experience += exp; + + if (sendText) { + std::string expString = std::to_string(exp) + (exp != 1 ? " experience points." : " experience point."); + + TextMessage message(MESSAGE_EXPERIENCE, "You gained " + expString); + message.position = position; + message.primary.value = exp; + message.primary.color = TEXTCOLOR_WHITE_EXP; + sendTextMessage(message); + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, position, false, true); + spectators.erase(this); + if (!spectators.empty()) { + message.type = MESSAGE_EXPERIENCE_OTHERS; + message.text = getName() + " gained " + expString; + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendTextMessage(message); + } + } + } + + uint32_t prevLevel = level; + while (experience >= nextLevelExp) { + ++level; + healthMax += vocation->getHPGain(); + health += vocation->getHPGain(); + manaMax += vocation->getManaGain(); + mana += vocation->getManaGain(); + capacity += vocation->getCapGain(); + + currLevelExp = nextLevelExp; + nextLevelExp = Player::getExpForLevel(level + 1); + if (currLevelExp >= nextLevelExp) { + //player has reached max level + break; + } + } + + if (prevLevel != level) { + health = getMaxHealth(); + mana = getMaxMana(); + + updateBaseSpeed(); + setBaseSpeed(getBaseSpeed()); + + g_game.changeSpeed(this, 0); + g_game.addCreatureHealth(this); + + const uint32_t protectionLevel = static_cast(g_config.getNumber(ConfigManager::PROTECTION_LEVEL)); + if (prevLevel < protectionLevel && level >= protectionLevel) { + g_game.updateCreatureWalkthrough(this); + } + + if (party) { + party->updateSharedExperience(); + } + + g_creatureEvents->playerAdvance(this, SKILL_LEVEL, prevLevel, level); + + std::ostringstream ss; + ss << "You advanced from Level " << prevLevel << " to Level " << level << '.'; + sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str()); + } + + if (nextLevelExp > currLevelExp) { + levelPercent = Player::getPercentLevel(experience - currLevelExp, nextLevelExp - currLevelExp); + } else { + levelPercent = 0; + } + sendStats(); +} + +void Player::removeExperience(uint64_t exp, bool sendText/* = false*/) +{ + if (experience == 0 || exp == 0) { + return; + } + + g_events->eventPlayerOnLoseExperience(this, exp); + if (exp == 0) { + return; + } + + uint64_t lostExp = experience; + experience = std::max(0, experience - exp); + + if (sendText) { + lostExp -= experience; + + std::string expString = std::to_string(lostExp) + (lostExp != 1 ? " experience points." : " experience point."); + + TextMessage message(MESSAGE_EXPERIENCE, "You lost " + expString); + message.position = position; + message.primary.value = lostExp; + message.primary.color = TEXTCOLOR_RED; + sendTextMessage(message); + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, position, false, true); + spectators.erase(this); + if (!spectators.empty()) { + message.type = MESSAGE_EXPERIENCE_OTHERS; + message.text = getName() + " lost " + expString; + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendTextMessage(message); + } + } + } + + uint32_t oldLevel = level; + uint64_t currLevelExp = Player::getExpForLevel(level); + + while (level > 1 && experience < currLevelExp) { + --level; + healthMax = std::max(0, healthMax - vocation->getHPGain()); + manaMax = std::max(0, manaMax - vocation->getManaGain()); + capacity = std::max(0, capacity - vocation->getCapGain()); + currLevelExp = Player::getExpForLevel(level); + } + + if (oldLevel != level) { + health = getMaxHealth(); + mana = getMaxMana(); + + updateBaseSpeed(); + setBaseSpeed(getBaseSpeed()); + + g_game.changeSpeed(this, 0); + g_game.addCreatureHealth(this); + + const uint32_t protectionLevel = static_cast(g_config.getNumber(ConfigManager::PROTECTION_LEVEL)); + if (oldLevel >= protectionLevel && level < protectionLevel) { + g_game.updateCreatureWalkthrough(this); + } + + if (party) { + party->updateSharedExperience(); + } + + std::ostringstream ss; + ss << "You were downgraded from Level " << oldLevel << " to Level " << level << '.'; + sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str()); + } + + uint64_t nextLevelExp = Player::getExpForLevel(level + 1); + if (nextLevelExp > currLevelExp) { + levelPercent = Player::getPercentLevel(experience - currLevelExp, nextLevelExp - currLevelExp); + } else { + levelPercent = 0; + } + sendStats(); +} + +uint8_t Player::getPercentLevel(uint64_t count, uint64_t nextLevelCount) +{ + if (nextLevelCount == 0) { + return 0; + } + + uint8_t result = (count * 100) / nextLevelCount; + if (result > 100) { + return 0; + } + return result; +} + +void Player::onBlockHit() +{ + if (shieldBlockCount > 0) { + --shieldBlockCount; + + if (hasShield()) { + addSkillAdvance(SKILL_SHIELD, 1); + } + } +} + +void Player::onAttackedCreatureBlockHit(BlockType_t blockType) +{ + lastAttackBlockType = blockType; + + switch (blockType) { + case BLOCK_NONE: { + addAttackSkillPoint = true; + bloodHitCount = 30; + shieldBlockCount = 30; + break; + } + + case BLOCK_DEFENSE: + case BLOCK_ARMOR: { + //need to draw blood every 30 hits + if (bloodHitCount > 0) { + addAttackSkillPoint = true; + --bloodHitCount; + } else { + addAttackSkillPoint = false; + } + break; + } + + default: { + addAttackSkillPoint = false; + break; + } + } +} + +bool Player::hasShield() const +{ + Item* item = inventory[CONST_SLOT_LEFT]; + if (item && item->getWeaponType() == WEAPON_SHIELD) { + return true; + } + + item = inventory[CONST_SLOT_RIGHT]; + if (item && item->getWeaponType() == WEAPON_SHIELD) { + return true; + } + return false; +} + +BlockType_t Player::blockHit(Creature* attacker, CombatType_t combatType, int32_t& damage, + bool checkDefense /* = false*/, bool checkArmor /* = false*/, bool field /* = false*/) +{ + BlockType_t blockType = Creature::blockHit(attacker, combatType, damage, checkDefense, checkArmor, field); + + if (attacker) { + sendCreatureSquare(attacker, SQ_COLOR_BLACK); + } + + if (blockType != BLOCK_NONE) { + return blockType; + } + + if (damage <= 0) { + damage = 0; + return BLOCK_ARMOR; + } + + for (int32_t slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_LAST; ++slot) { + if (!isItemAbilityEnabled(static_cast(slot))) { + continue; + } + + Item* item = inventory[slot]; + if (!item) { + continue; + } + + const ItemType& it = Item::items[item->getID()]; + if (!it.abilities) { + if (damage <= 0) { + damage = 0; + return BLOCK_ARMOR; + } + + continue; + } + + const int16_t& absorbPercent = it.abilities->absorbPercent[combatTypeToIndex(combatType)]; + if (absorbPercent != 0) { + damage -= std::round(damage * (absorbPercent / 100.)); + + uint16_t charges = item->getCharges(); + if (charges != 0) { + g_game.transformItem(item, item->getID(), charges - 1); + } + } + + if (field) { + const int16_t& fieldAbsorbPercent = it.abilities->fieldAbsorbPercent[combatTypeToIndex(combatType)]; + if (fieldAbsorbPercent != 0) { + damage -= std::round(damage * (fieldAbsorbPercent / 100.)); + + uint16_t charges = item->getCharges(); + if (charges != 0) { + g_game.transformItem(item, item->getID(), charges - 1); + } + } + } + } + + if (damage <= 0) { + damage = 0; + blockType = BLOCK_ARMOR; + } + return blockType; +} + +uint32_t Player::getIP() const +{ + if (client) { + return client->getIP(); + } + + return 0; +} + +void Player::death(Creature* lastHitCreature) +{ + loginPosition = town->getTemplePosition(); + + if (skillLoss) { + uint8_t unfairFightReduction = 100; + bool lastHitPlayer = Player::lastHitIsPlayer(lastHitCreature); + + if (lastHitPlayer) { + uint32_t sumLevels = 0; + uint32_t inFightTicks = g_config.getNumber(ConfigManager::PZ_LOCKED); + for (const auto& it : damageMap) { + CountBlock_t cb = it.second; + if ((OTSYS_TIME() - cb.ticks) <= inFightTicks) { + Player* damageDealer = g_game.getPlayerByID(it.first); + if (damageDealer) { + sumLevels += damageDealer->getLevel(); + } + } + } + + if (sumLevels > level) { + double reduce = level / static_cast(sumLevels); + unfairFightReduction = std::max(20, std::floor((reduce * 100) + 0.5)); + } + } + + //Magic level loss + uint64_t sumMana = 0; + uint64_t lostMana = 0; + + //sum up all the mana + for (uint32_t i = 1; i <= magLevel; ++i) { + sumMana += vocation->getReqMana(i); + } + + sumMana += manaSpent; + + double deathLossPercent = getLostPercent() * (unfairFightReduction / 100.); + + lostMana = static_cast(sumMana * deathLossPercent); + + while (lostMana > manaSpent && magLevel > 0) { + lostMana -= manaSpent; + manaSpent = vocation->getReqMana(magLevel); + magLevel--; + } + + manaSpent -= lostMana; + + uint64_t nextReqMana = vocation->getReqMana(magLevel + 1); + if (nextReqMana > vocation->getReqMana(magLevel)) { + magLevelPercent = Player::getPercentLevel(manaSpent, nextReqMana); + } else { + magLevelPercent = 0; + } + + //Skill loss + for (uint8_t i = SKILL_FIRST; i <= SKILL_LAST; ++i) { //for each skill + uint64_t sumSkillTries = 0; + for (uint16_t c = 11; c <= skills[i].level; ++c) { //sum up all required tries for all skill levels + sumSkillTries += vocation->getReqSkillTries(i, c); + } + + sumSkillTries += skills[i].tries; + + uint32_t lostSkillTries = static_cast(sumSkillTries * deathLossPercent); + while (lostSkillTries > skills[i].tries) { + lostSkillTries -= skills[i].tries; + + if (skills[i].level <= 10) { + skills[i].level = 10; + skills[i].tries = 0; + lostSkillTries = 0; + break; + } + + skills[i].tries = vocation->getReqSkillTries(i, skills[i].level); + skills[i].level--; + } + + skills[i].tries = std::max(0, skills[i].tries - lostSkillTries); + skills[i].percent = Player::getPercentLevel(skills[i].tries, vocation->getReqSkillTries(i, skills[i].level)); + } + + //Level loss + uint64_t expLoss = static_cast(experience * deathLossPercent); + g_events->eventPlayerOnLoseExperience(this, expLoss); + + if (expLoss != 0) { + uint32_t oldLevel = level; + + if (vocation->getId() == VOCATION_NONE || level > 7) { + experience -= expLoss; + } + + while (level > 1 && experience < Player::getExpForLevel(level)) { + --level; + healthMax = std::max(0, healthMax - vocation->getHPGain()); + manaMax = std::max(0, manaMax - vocation->getManaGain()); + capacity = std::max(0, capacity - vocation->getCapGain()); + } + + if (oldLevel != level) { + std::ostringstream ss; + ss << "You were downgraded from Level " << oldLevel << " to Level " << level << '.'; + sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str()); + } + + uint64_t currLevelExp = Player::getExpForLevel(level); + uint64_t nextLevelExp = Player::getExpForLevel(level + 1); + if (nextLevelExp > currLevelExp) { + levelPercent = Player::getPercentLevel(experience - currLevelExp, nextLevelExp - currLevelExp); + } else { + levelPercent = 0; + } + } + + std::bitset<6> bitset(blessings); + if (bitset[5]) { + if (lastHitPlayer) { + bitset.reset(5); + blessings = bitset.to_ulong(); + } else { + blessings = 32; + } + } else { + blessings = 0; + } + + sendStats(); + sendSkills(); + sendReLoginWindow(unfairFightReduction); + + if (getSkull() == SKULL_BLACK) { + health = 40; + mana = 0; + } else { + health = healthMax; + mana = manaMax; + } + + auto it = conditions.begin(), end = conditions.end(); + while (it != end) { + Condition* condition = *it; + if (condition->isPersistent()) { + it = conditions.erase(it); + + condition->endCondition(this); + onEndCondition(condition->getType()); + delete condition; + } else { + ++it; + } + } + } else { + setSkillLoss(true); + + auto it = conditions.begin(), end = conditions.end(); + while (it != end) { + Condition* condition = *it; + if (condition->isPersistent()) { + it = conditions.erase(it); + + condition->endCondition(this); + onEndCondition(condition->getType()); + delete condition; + } else { + ++it; + } + } + + health = healthMax; + g_game.internalTeleport(this, getTemplePosition(), true); + g_game.addCreatureHealth(this); + onThink(EVENT_CREATURE_THINK_INTERVAL); + onIdleStatus(); + sendStats(); + } +} + +bool Player::dropCorpse(Creature* lastHitCreature, Creature* mostDamageCreature, bool lastHitUnjustified, bool mostDamageUnjustified) +{ + if (getZone() != ZONE_PVP || !Player::lastHitIsPlayer(lastHitCreature)) { + return Creature::dropCorpse(lastHitCreature, mostDamageCreature, lastHitUnjustified, mostDamageUnjustified); + } + + setDropLoot(true); + return false; +} + +Item* Player::getCorpse(Creature* lastHitCreature, Creature* mostDamageCreature) +{ + Item* corpse = Creature::getCorpse(lastHitCreature, mostDamageCreature); + if (corpse && corpse->getContainer()) { + std::ostringstream ss; + if (lastHitCreature) { + ss << "You recognize " << getNameDescription() << ". " << (getSex() == PLAYERSEX_FEMALE ? "She" : "He") << " was killed by " << lastHitCreature->getNameDescription() << '.'; + } else { + ss << "You recognize " << getNameDescription() << '.'; + } + + corpse->setSpecialDescription(ss.str()); + } + return corpse; +} + +void Player::addInFightTicks(bool pzlock /*= false*/) +{ + if (hasFlag(PlayerFlag_NotGainInFight)) { + return; + } + + if (pzlock) { + pzLocked = true; + } + + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_INFIGHT, g_config.getNumber(ConfigManager::PZ_LOCKED), 0); + addCondition(condition); +} + +void Player::removeList() +{ + g_game.removePlayer(this); + + for (const auto& it : g_game.getPlayers()) { + it.second->notifyStatusChange(this, VIPSTATUS_OFFLINE); + } +} + +void Player::addList() +{ + for (const auto& it : g_game.getPlayers()) { + it.second->notifyStatusChange(this, VIPSTATUS_ONLINE); + } + + g_game.addPlayer(this); +} + +void Player::kickPlayer(bool displayEffect) +{ + g_creatureEvents->playerLogout(this); + if (client) { + client->logout(displayEffect, true); + } else { + g_game.removeCreature(this); + } +} + +void Player::notifyStatusChange(Player* loginPlayer, VipStatus_t status) +{ + if (!client) { + return; + } + + auto it = VIPList.find(loginPlayer->guid); + if (it == VIPList.end()) { + return; + } + + client->sendUpdatedVIPStatus(loginPlayer->guid, status); + + if (status == VIPSTATUS_ONLINE) { + client->sendTextMessage(TextMessage(MESSAGE_STATUS_SMALL, loginPlayer->getName() + " has logged in.")); + } else if (status == VIPSTATUS_OFFLINE) { + client->sendTextMessage(TextMessage(MESSAGE_STATUS_SMALL, loginPlayer->getName() + " has logged out.")); + } +} + +bool Player::removeVIP(uint32_t vipGuid) +{ + if (VIPList.erase(vipGuid) == 0) { + return false; + } + + IOLoginData::removeVIPEntry(accountNumber, vipGuid); + return true; +} + +bool Player::addVIP(uint32_t vipGuid, const std::string& vipName, VipStatus_t status) +{ + if (VIPList.size() >= getMaxVIPEntries() || VIPList.size() == 200) { // max number of buddies is 200 in 9.53 + sendTextMessage(MESSAGE_STATUS_SMALL, "You cannot add more buddies."); + return false; + } + + auto result = VIPList.insert(vipGuid); + if (!result.second) { + sendTextMessage(MESSAGE_STATUS_SMALL, "This player is already in your list."); + return false; + } + + IOLoginData::addVIPEntry(accountNumber, vipGuid, "", 0, false); + if (client) { + client->sendVIP(vipGuid, vipName, "", 0, false, status); + } + return true; +} + +bool Player::addVIPInternal(uint32_t vipGuid) +{ + if (VIPList.size() >= getMaxVIPEntries() || VIPList.size() == 200) { // max number of buddies is 200 in 9.53 + return false; + } + + return VIPList.insert(vipGuid).second; +} + +bool Player::editVIP(uint32_t vipGuid, const std::string& description, uint32_t icon, bool notify) +{ + auto it = VIPList.find(vipGuid); + if (it == VIPList.end()) { + return false; // player is not in VIP + } + + IOLoginData::editVIPEntry(accountNumber, vipGuid, description, icon, notify); + return true; +} + +//close container and its child containers +void Player::autoCloseContainers(const Container* container) +{ + std::vector closeList; + for (const auto& it : openContainers) { + Container* tmpContainer = it.second.container; + while (tmpContainer) { + if (tmpContainer->isRemoved() || tmpContainer == container) { + closeList.push_back(it.first); + break; + } + + tmpContainer = dynamic_cast(tmpContainer->getParent()); + } + } + + for (uint32_t containerId : closeList) { + closeContainer(containerId); + if (client) { + client->sendCloseContainer(containerId); + } + } +} + +bool Player::hasCapacity(const Item* item, uint32_t count) const +{ + if (hasFlag(PlayerFlag_CannotPickupItem)) { + return false; + } + + if (hasFlag(PlayerFlag_HasInfiniteCapacity) || item->getTopParent() == this) { + return true; + } + + uint32_t itemWeight = item->getContainer() != nullptr ? item->getWeight() : item->getBaseWeight(); + if (item->isStackable()) { + itemWeight *= count; + } + return itemWeight <= getFreeCapacity(); +} + +ReturnValue Player::queryAdd(int32_t index, const Thing& thing, uint32_t count, uint32_t flags, Creature*) const +{ + const Item* item = thing.getItem(); + if (item == nullptr) { + return RETURNVALUE_NOTPOSSIBLE; + } + + bool childIsOwner = hasBitSet(FLAG_CHILDISOWNER, flags); + if (childIsOwner) { + //a child container is querying the player, just check if enough capacity + bool skipLimit = hasBitSet(FLAG_NOLIMIT, flags); + if (skipLimit || hasCapacity(item, count)) { + return RETURNVALUE_NOERROR; + } + return RETURNVALUE_NOTENOUGHCAPACITY; + } + + if (!item->isPickupable()) { + return RETURNVALUE_CANNOTPICKUP; + } + + ReturnValue ret = RETURNVALUE_NOERROR; + + const int32_t& slotPosition = item->getSlotPosition(); + if ((slotPosition & SLOTP_HEAD) || (slotPosition & SLOTP_NECKLACE) || + (slotPosition & SLOTP_BACKPACK) || (slotPosition & SLOTP_ARMOR) || + (slotPosition & SLOTP_LEGS) || (slotPosition & SLOTP_FEET) || + (slotPosition & SLOTP_RING)) { + ret = RETURNVALUE_CANNOTBEDRESSED; + } else if (slotPosition & SLOTP_TWO_HAND) { + ret = RETURNVALUE_PUTTHISOBJECTINBOTHHANDS; + } else if ((slotPosition & SLOTP_RIGHT) || (slotPosition & SLOTP_LEFT)) { + if (!g_config.getBoolean(ConfigManager::CLASSIC_EQUIPMENT_SLOTS)) { + ret = RETURNVALUE_CANNOTBEDRESSED; + } else { + ret = RETURNVALUE_PUTTHISOBJECTINYOURHAND; + } + } + + switch (index) { + case CONST_SLOT_HEAD: { + if (slotPosition & SLOTP_HEAD) { + ret = RETURNVALUE_NOERROR; + } + break; + } + + case CONST_SLOT_NECKLACE: { + if (slotPosition & SLOTP_NECKLACE) { + ret = RETURNVALUE_NOERROR; + } + break; + } + + case CONST_SLOT_BACKPACK: { + if (slotPosition & SLOTP_BACKPACK) { + ret = RETURNVALUE_NOERROR; + } + break; + } + + case CONST_SLOT_ARMOR: { + if (slotPosition & SLOTP_ARMOR) { + ret = RETURNVALUE_NOERROR; + } + break; + } + + case CONST_SLOT_RIGHT: { + if (slotPosition & SLOTP_RIGHT) { + if (!g_config.getBoolean(ConfigManager::CLASSIC_EQUIPMENT_SLOTS)) { + if (item->getWeaponType() != WEAPON_SHIELD) { + ret = RETURNVALUE_CANNOTBEDRESSED; + } else { + const Item* leftItem = inventory[CONST_SLOT_LEFT]; + if (leftItem) { + if ((leftItem->getSlotPosition() | slotPosition) & SLOTP_TWO_HAND) { + ret = RETURNVALUE_BOTHHANDSNEEDTOBEFREE; + } else { + ret = RETURNVALUE_NOERROR; + } + } else { + ret = RETURNVALUE_NOERROR; + } + } + } else if (slotPosition & SLOTP_TWO_HAND) { + if (inventory[CONST_SLOT_LEFT] && inventory[CONST_SLOT_LEFT] != item) { + ret = RETURNVALUE_BOTHHANDSNEEDTOBEFREE; + } else { + ret = RETURNVALUE_NOERROR; + } + } else if (inventory[CONST_SLOT_LEFT]) { + const Item* leftItem = inventory[CONST_SLOT_LEFT]; + WeaponType_t type = item->getWeaponType(), leftType = leftItem->getWeaponType(); + + if (leftItem->getSlotPosition() & SLOTP_TWO_HAND) { + ret = RETURNVALUE_DROPTWOHANDEDITEM; + } else if (item == leftItem && count == item->getItemCount()) { + ret = RETURNVALUE_NOERROR; + } else if (leftType == WEAPON_SHIELD && type == WEAPON_SHIELD) { + ret = RETURNVALUE_CANONLYUSEONESHIELD; + } else if (leftType == WEAPON_NONE || type == WEAPON_NONE || + leftType == WEAPON_SHIELD || leftType == WEAPON_AMMO + || type == WEAPON_SHIELD || type == WEAPON_AMMO) { + ret = RETURNVALUE_NOERROR; + } else { + ret = RETURNVALUE_CANONLYUSEONEWEAPON; + } + } else { + ret = RETURNVALUE_NOERROR; + } + } + break; + } + + case CONST_SLOT_LEFT: { + if (slotPosition & SLOTP_LEFT) { + if (!g_config.getBoolean(ConfigManager::CLASSIC_EQUIPMENT_SLOTS)) { + WeaponType_t type = item->getWeaponType(); + if (type == WEAPON_NONE || type == WEAPON_SHIELD) { + ret = RETURNVALUE_CANNOTBEDRESSED; + } else if (inventory[CONST_SLOT_RIGHT] && (slotPosition & SLOTP_TWO_HAND)) { + ret = RETURNVALUE_BOTHHANDSNEEDTOBEFREE; + } else { + ret = RETURNVALUE_NOERROR; + } + } else if (slotPosition & SLOTP_TWO_HAND) { + if (inventory[CONST_SLOT_RIGHT] && inventory[CONST_SLOT_RIGHT] != item) { + ret = RETURNVALUE_BOTHHANDSNEEDTOBEFREE; + } else { + ret = RETURNVALUE_NOERROR; + } + } else if (inventory[CONST_SLOT_RIGHT]) { + const Item* rightItem = inventory[CONST_SLOT_RIGHT]; + WeaponType_t type = item->getWeaponType(), rightType = rightItem->getWeaponType(); + + if (rightItem->getSlotPosition() & SLOTP_TWO_HAND) { + ret = RETURNVALUE_DROPTWOHANDEDITEM; + } else if (item == rightItem && count == item->getItemCount()) { + ret = RETURNVALUE_NOERROR; + } else if (rightType == WEAPON_SHIELD && type == WEAPON_SHIELD) { + ret = RETURNVALUE_CANONLYUSEONESHIELD; + } else if (rightType == WEAPON_NONE || type == WEAPON_NONE || + rightType == WEAPON_SHIELD || rightType == WEAPON_AMMO + || type == WEAPON_SHIELD || type == WEAPON_AMMO) { + ret = RETURNVALUE_NOERROR; + } else { + ret = RETURNVALUE_CANONLYUSEONEWEAPON; + } + } else { + ret = RETURNVALUE_NOERROR; + } + } + break; + } + + case CONST_SLOT_LEGS: { + if (slotPosition & SLOTP_LEGS) { + ret = RETURNVALUE_NOERROR; + } + break; + } + + case CONST_SLOT_FEET: { + if (slotPosition & SLOTP_FEET) { + ret = RETURNVALUE_NOERROR; + } + break; + } + + case CONST_SLOT_RING: { + if (slotPosition & SLOTP_RING) { + ret = RETURNVALUE_NOERROR; + } + break; + } + + case CONST_SLOT_AMMO: { + if ((slotPosition & SLOTP_AMMO) || g_config.getBoolean(ConfigManager::CLASSIC_EQUIPMENT_SLOTS)) { + ret = RETURNVALUE_NOERROR; + } + break; + } + + case CONST_SLOT_WHEREEVER: + case -1: + ret = RETURNVALUE_NOTENOUGHROOM; + break; + + default: + ret = RETURNVALUE_NOTPOSSIBLE; + break; + } + + if (ret != RETURNVALUE_NOERROR && ret != RETURNVALUE_NOTENOUGHROOM) { + return ret; + } + + //check if enough capacity + if (!hasCapacity(item, count)) { + return RETURNVALUE_NOTENOUGHCAPACITY; + } + + ret = g_moveEvents->onPlayerEquip(const_cast(this), const_cast(item), static_cast(index), true); + if (ret != RETURNVALUE_NOERROR) { + return ret; + } + + //need an exchange with source? (destination item is swapped with currently moved item) + const Item* inventoryItem = getInventoryItem(static_cast(index)); + if (inventoryItem && (!inventoryItem->isStackable() || inventoryItem->getID() != item->getID())) { + const Cylinder* cylinder = item->getTopParent(); + if (cylinder && (dynamic_cast(cylinder) || dynamic_cast(cylinder))) { + return RETURNVALUE_NEEDEXCHANGE; + } + + return RETURNVALUE_NOTENOUGHROOM; + } + return ret; +} + +ReturnValue Player::queryMaxCount(int32_t index, const Thing& thing, uint32_t count, uint32_t& maxQueryCount, + uint32_t flags) const +{ + const Item* item = thing.getItem(); + if (item == nullptr) { + maxQueryCount = 0; + return RETURNVALUE_NOTPOSSIBLE; + } + + if (index == INDEX_WHEREEVER) { + uint32_t n = 0; + for (int32_t slotIndex = CONST_SLOT_FIRST; slotIndex <= CONST_SLOT_LAST; ++slotIndex) { + Item* inventoryItem = inventory[slotIndex]; + if (inventoryItem) { + if (Container* subContainer = inventoryItem->getContainer()) { + uint32_t queryCount = 0; + subContainer->queryMaxCount(INDEX_WHEREEVER, *item, item->getItemCount(), queryCount, flags); + n += queryCount; + + //iterate through all items, including sub-containers (deep search) + for (ContainerIterator it = subContainer->iterator(); it.hasNext(); it.advance()) { + if (Container* tmpContainer = (*it)->getContainer()) { + queryCount = 0; + tmpContainer->queryMaxCount(INDEX_WHEREEVER, *item, item->getItemCount(), queryCount, flags); + n += queryCount; + } + } + } else if (inventoryItem->isStackable() && item->equals(inventoryItem) && inventoryItem->getItemCount() < 100) { + uint32_t remainder = (100 - inventoryItem->getItemCount()); + + if (queryAdd(slotIndex, *item, remainder, flags) == RETURNVALUE_NOERROR) { + n += remainder; + } + } + } else if (queryAdd(slotIndex, *item, item->getItemCount(), flags) == RETURNVALUE_NOERROR) { //empty slot + if (item->isStackable()) { + n += 100; + } else { + ++n; + } + } + } + + maxQueryCount = n; + } else { + const Item* destItem = nullptr; + + const Thing* destThing = getThing(index); + if (destThing) { + destItem = destThing->getItem(); + } + + if (destItem) { + if (destItem->isStackable() && item->equals(destItem) && destItem->getItemCount() < 100) { + maxQueryCount = 100 - destItem->getItemCount(); + } else { + maxQueryCount = 0; + } + } else if (queryAdd(index, *item, count, flags) == RETURNVALUE_NOERROR) { //empty slot + if (item->isStackable()) { + maxQueryCount = 100; + } else { + maxQueryCount = 1; + } + + return RETURNVALUE_NOERROR; + } + } + + if (maxQueryCount < count) { + return RETURNVALUE_NOTENOUGHROOM; + } else { + return RETURNVALUE_NOERROR; + } +} + +ReturnValue Player::queryRemove(const Thing& thing, uint32_t count, uint32_t flags) const +{ + int32_t index = getThingIndex(&thing); + if (index == -1) { + return RETURNVALUE_NOTPOSSIBLE; + } + + const Item* item = thing.getItem(); + if (item == nullptr) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (count == 0 || (item->isStackable() && count > item->getItemCount())) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (!item->isMoveable() && !hasBitSet(FLAG_IGNORENOTMOVEABLE, flags)) { + return RETURNVALUE_NOTMOVEABLE; + } + + return RETURNVALUE_NOERROR; +} + +Cylinder* Player::queryDestination(int32_t& index, const Thing& thing, Item** destItem, + uint32_t& flags) +{ + if (index == 0 /*drop to capacity window*/ || index == INDEX_WHEREEVER) { + *destItem = nullptr; + + const Item* item = thing.getItem(); + if (item == nullptr) { + return this; + } + + bool autoStack = !((flags & FLAG_IGNOREAUTOSTACK) == FLAG_IGNOREAUTOSTACK); + bool isStackable = item->isStackable(); + + std::vector containers; + + for (uint32_t slotIndex = CONST_SLOT_FIRST; slotIndex <= CONST_SLOT_LAST; ++slotIndex) { + Item* inventoryItem = inventory[slotIndex]; + if (inventoryItem) { + if (inventoryItem == tradeItem) { + continue; + } + + if (inventoryItem == item) { + continue; + } + + if (autoStack && isStackable) { + //try find an already existing item to stack with + if (queryAdd(slotIndex, *item, item->getItemCount(), 0) == RETURNVALUE_NOERROR) { + if (inventoryItem->equals(item) && inventoryItem->getItemCount() < 100) { + index = slotIndex; + *destItem = inventoryItem; + return this; + } + } + + if (Container* subContainer = inventoryItem->getContainer()) { + containers.push_back(subContainer); + } + } else if (Container* subContainer = inventoryItem->getContainer()) { + containers.push_back(subContainer); + } + } else if (queryAdd(slotIndex, *item, item->getItemCount(), flags) == RETURNVALUE_NOERROR) { //empty slot + index = slotIndex; + *destItem = nullptr; + return this; + } + } + + size_t i = 0; + while (i < containers.size()) { + Container* tmpContainer = containers[i++]; + if (!autoStack || !isStackable) { + //we need to find first empty container as fast as we can for non-stackable items + uint32_t n = tmpContainer->capacity() - tmpContainer->size(); + while (n) { + if (tmpContainer->queryAdd(tmpContainer->capacity() - n, *item, item->getItemCount(), flags) == RETURNVALUE_NOERROR) { + index = tmpContainer->capacity() - n; + *destItem = nullptr; + return tmpContainer; + } + + n--; + } + + for (Item* tmpContainerItem : tmpContainer->getItemList()) { + if (Container* subContainer = tmpContainerItem->getContainer()) { + containers.push_back(subContainer); + } + } + + continue; + } + + uint32_t n = 0; + + for (Item* tmpItem : tmpContainer->getItemList()) { + if (tmpItem == tradeItem) { + continue; + } + + if (tmpItem == item) { + continue; + } + + //try find an already existing item to stack with + if (tmpItem->equals(item) && tmpItem->getItemCount() < 100) { + index = n; + *destItem = tmpItem; + return tmpContainer; + } + + if (Container* subContainer = tmpItem->getContainer()) { + containers.push_back(subContainer); + } + + n++; + } + + if (n < tmpContainer->capacity() && tmpContainer->queryAdd(n, *item, item->getItemCount(), flags) == RETURNVALUE_NOERROR) { + index = n; + *destItem = nullptr; + return tmpContainer; + } + } + + return this; + } + + Thing* destThing = getThing(index); + if (destThing) { + *destItem = destThing->getItem(); + } + + Cylinder* subCylinder = dynamic_cast(destThing); + if (subCylinder) { + index = INDEX_WHEREEVER; + *destItem = nullptr; + return subCylinder; + } else { + return this; + } +} + +void Player::addThing(int32_t index, Thing* thing) +{ + if (index < CONST_SLOT_FIRST || index > CONST_SLOT_LAST) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + Item* item = thing->getItem(); + if (!item) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + item->setParent(this); + inventory[index] = item; + + //send to client + sendInventoryItem(static_cast(index), item); +} + +void Player::updateThing(Thing* thing, uint16_t itemId, uint32_t count) +{ + int32_t index = getThingIndex(thing); + if (index == -1) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + Item* item = thing->getItem(); + if (!item) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + item->setID(itemId); + item->setSubType(count); + + //send to client + sendInventoryItem(static_cast(index), item); + + //event methods + onUpdateInventoryItem(item, item); +} + +void Player::replaceThing(uint32_t index, Thing* thing) +{ + if (index > CONST_SLOT_LAST) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + Item* oldItem = getInventoryItem(static_cast(index)); + if (!oldItem) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + Item* item = thing->getItem(); + if (!item) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + //send to client + sendInventoryItem(static_cast(index), item); + + //event methods + onUpdateInventoryItem(oldItem, item); + + item->setParent(this); + + inventory[index] = item; +} + +void Player::removeThing(Thing* thing, uint32_t count) +{ + Item* item = thing->getItem(); + if (!item) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + int32_t index = getThingIndex(thing); + if (index == -1) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + if (item->isStackable()) { + if (count == item->getItemCount()) { + //send change to client + sendInventoryItem(static_cast(index), nullptr); + + //event methods + onRemoveInventoryItem(item); + + item->setParent(nullptr); + inventory[index] = nullptr; + } else { + uint8_t newCount = static_cast(std::max(0, item->getItemCount() - count)); + item->setItemCount(newCount); + + //send change to client + sendInventoryItem(static_cast(index), item); + + //event methods + onUpdateInventoryItem(item, item); + } + } else { + //send change to client + sendInventoryItem(static_cast(index), nullptr); + + //event methods + onRemoveInventoryItem(item); + + item->setParent(nullptr); + inventory[index] = nullptr; + } +} + +int32_t Player::getThingIndex(const Thing* thing) const +{ + for (int i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) { + if (inventory[i] == thing) { + return i; + } + } + return -1; +} + +size_t Player::getFirstIndex() const +{ + return CONST_SLOT_FIRST; +} + +size_t Player::getLastIndex() const +{ + return CONST_SLOT_LAST + 1; +} + +uint32_t Player::getItemTypeCount(uint16_t itemId, int32_t subType /*= -1*/) const +{ + uint32_t count = 0; + for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; i++) { + Item* item = inventory[i]; + if (!item) { + continue; + } + + if (item->getID() == itemId) { + count += Item::countByType(item, subType); + } + + if (Container* container = item->getContainer()) { + for (ContainerIterator it = container->iterator(); it.hasNext(); it.advance()) { + if ((*it)->getID() == itemId) { + count += Item::countByType(*it, subType); + } + } + } + } + return count; +} + +bool Player::removeItemOfType(uint16_t itemId, uint32_t amount, int32_t subType, bool ignoreEquipped/* = false*/) const +{ + if (amount == 0) { + return true; + } + + std::vector itemList; + + uint32_t count = 0; + for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; i++) { + Item* item = inventory[i]; + if (!item) { + continue; + } + + if (!ignoreEquipped && item->getID() == itemId) { + uint32_t itemCount = Item::countByType(item, subType); + if (itemCount == 0) { + continue; + } + + itemList.push_back(item); + + count += itemCount; + if (count >= amount) { + g_game.internalRemoveItems(std::move(itemList), amount, Item::items[itemId].stackable); + return true; + } + } else if (Container* container = item->getContainer()) { + for (ContainerIterator it = container->iterator(); it.hasNext(); it.advance()) { + Item* containerItem = *it; + if (containerItem->getID() == itemId) { + uint32_t itemCount = Item::countByType(containerItem, subType); + if (itemCount == 0) { + continue; + } + + itemList.push_back(containerItem); + + count += itemCount; + if (count >= amount) { + g_game.internalRemoveItems(std::move(itemList), amount, Item::items[itemId].stackable); + return true; + } + } + } + } + } + return false; +} + +std::map& Player::getAllItemTypeCount(std::map& countMap) const +{ + for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; i++) { + Item* item = inventory[i]; + if (!item) { + continue; + } + + countMap[item->getID()] += Item::countByType(item, -1); + + if (Container* container = item->getContainer()) { + for (ContainerIterator it = container->iterator(); it.hasNext(); it.advance()) { + countMap[(*it)->getID()] += Item::countByType(*it, -1); + } + } + } + return countMap; +} + +Thing* Player::getThing(size_t index) const +{ + if (index >= CONST_SLOT_FIRST && index <= CONST_SLOT_LAST) { + return inventory[index]; + } + return nullptr; +} + +void Player::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link /*= LINK_OWNER*/) +{ + if (link == LINK_OWNER) { + //calling movement scripts + g_moveEvents->onPlayerEquip(this, thing->getItem(), static_cast(index), false); + } + + bool requireListUpdate = false; + + if (link == LINK_OWNER || link == LINK_TOPPARENT) { + const Item* i = (oldParent ? oldParent->getItem() : nullptr); + + // Check if we owned the old container too, so we don't need to do anything, + // as the list was updated in postRemoveNotification + assert(i ? i->getContainer() != nullptr : true); + + if (i) { + requireListUpdate = i->getContainer()->getHoldingPlayer() != this; + } else { + requireListUpdate = oldParent != this; + } + + updateInventoryWeight(); + updateItemsLight(); + sendStats(); + } + + if (const Item* item = thing->getItem()) { + if (const Container* container = item->getContainer()) { + onSendContainer(container); + } + + if (shopOwner && requireListUpdate) { + updateSaleShopList(item); + } + } else if (const Creature* creature = thing->getCreature()) { + if (creature == this) { + //check containers + std::vector containers; + + for (const auto& it : openContainers) { + Container* container = it.second.container; + if (!Position::areInRange<1, 1, 0>(container->getPosition(), getPosition())) { + containers.push_back(container); + } + } + + for (const Container* container : containers) { + autoCloseContainers(container); + } + } + } +} + +void Player::postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link /*= LINK_OWNER*/) +{ + if (link == LINK_OWNER) { + //calling movement scripts + g_moveEvents->onPlayerDeEquip(this, thing->getItem(), static_cast(index)); + } + + bool requireListUpdate = false; + + if (link == LINK_OWNER || link == LINK_TOPPARENT) { + const Item* i = (newParent ? newParent->getItem() : nullptr); + + // Check if we owned the old container too, so we don't need to do anything, + // as the list was updated in postRemoveNotification + assert(i ? i->getContainer() != nullptr : true); + + if (i) { + requireListUpdate = i->getContainer()->getHoldingPlayer() != this; + } else { + requireListUpdate = newParent != this; + } + + updateInventoryWeight(); + updateItemsLight(); + sendStats(); + } + + if (const Item* item = thing->getItem()) { + if (const Container* container = item->getContainer()) { + if (container->isRemoved() || !Position::areInRange<1, 1, 0>(getPosition(), container->getPosition())) { + autoCloseContainers(container); + } else if (container->getTopParent() == this) { + onSendContainer(container); + } else if (const Container* topContainer = dynamic_cast(container->getTopParent())) { + if (const DepotChest* depotChest = dynamic_cast(topContainer)) { + bool isOwner = false; + + for (const auto& it : depotChests) { + if (it.second == depotChest) { + isOwner = true; + onSendContainer(container); + } + } + + if (!isOwner) { + autoCloseContainers(container); + } + } else { + onSendContainer(container); + } + } else { + autoCloseContainers(container); + } + } + + if (shopOwner && requireListUpdate) { + updateSaleShopList(item); + } + } +} + +bool Player::updateSaleShopList(const Item* item) +{ + uint16_t itemId = item->getID(); + if (itemId != ITEM_GOLD_COIN && itemId != ITEM_PLATINUM_COIN && itemId != ITEM_CRYSTAL_COIN) { + auto it = std::find_if(shopItemList.begin(), shopItemList.end(), [itemId](const ShopInfo& shopInfo) { return shopInfo.itemId == itemId && shopInfo.sellPrice != 0; }); + if (it == shopItemList.end()) { + const Container* container = item->getContainer(); + if (!container) { + return false; + } + + const auto& items = container->getItemList(); + return std::any_of(items.begin(), items.end(), [this](const Item* containerItem) { + return updateSaleShopList(containerItem); + }); + } + } + + if (client) { + client->sendSaleItemList(shopItemList); + } + return true; +} + +bool Player::hasShopItemForSale(uint32_t itemId, uint8_t subType) const +{ + const ItemType& itemType = Item::items[itemId]; + return std::any_of(shopItemList.begin(), shopItemList.end(), [&](const ShopInfo& shopInfo) { + return shopInfo.itemId == itemId && shopInfo.buyPrice != 0 && (!itemType.isFluidContainer() || shopInfo.subType == subType); + }); +} + +void Player::internalAddThing(Thing* thing) +{ + internalAddThing(0, thing); +} + +void Player::internalAddThing(uint32_t index, Thing* thing) +{ + Item* item = thing->getItem(); + if (!item) { + return; + } + + //index == 0 means we should equip this item at the most appropiate slot (no action required here) + if (index > CONST_SLOT_WHEREEVER && index <= CONST_SLOT_LAST) { + if (inventory[index]) { + return; + } + + inventory[index] = item; + item->setParent(this); + } +} + +bool Player::setFollowCreature(Creature* creature) +{ + if (!Creature::setFollowCreature(creature)) { + setFollowCreature(nullptr); + setAttackedCreature(nullptr); + + sendCancelMessage(RETURNVALUE_THEREISNOWAY); + sendCancelTarget(); + stopWalk(); + return false; + } + return true; +} + +bool Player::setAttackedCreature(Creature* creature) +{ + if (!Creature::setAttackedCreature(creature)) { + sendCancelTarget(); + return false; + } + + if (chaseMode && creature) { + if (followCreature != creature) { + //chase opponent + setFollowCreature(creature); + } + } else if (followCreature) { + setFollowCreature(nullptr); + } + + if (creature) { + g_dispatcher.addTask(createTask(std::bind(&Game::checkCreatureAttack, &g_game, getID()))); + } + return true; +} + +void Player::goToFollowCreature() +{ + if (!walkTask) { + if ((OTSYS_TIME() - lastFailedFollow) < 2000) { + return; + } + + Creature::goToFollowCreature(); + + if (followCreature && !hasFollowPath) { + lastFailedFollow = OTSYS_TIME(); + } + } +} + +void Player::getPathSearchParams(const Creature* creature, FindPathParams& fpp) const +{ + Creature::getPathSearchParams(creature, fpp); + fpp.fullPathSearch = true; +} + +void Player::doAttacking(uint32_t) +{ + if (lastAttack == 0) { + lastAttack = OTSYS_TIME() - getAttackSpeed() - 1; + } + + if (hasCondition(CONDITION_PACIFIED)) { + return; + } + + if ((OTSYS_TIME() - lastAttack) >= getAttackSpeed()) { + bool result = false; + + Item* tool = getWeapon(); + const Weapon* weapon = g_weapons->getWeapon(tool); + uint32_t delay = getAttackSpeed(); + bool classicSpeed = g_config.getBoolean(ConfigManager::CLASSIC_ATTACK_SPEED); + + if (weapon) { + if (!weapon->interruptSwing()) { + result = weapon->useWeapon(this, tool, attackedCreature); + } else if (!classicSpeed && !canDoAction()) { + delay = getNextActionTime(); + } else { + result = weapon->useWeapon(this, tool, attackedCreature); + } + } else { + result = Weapon::useFist(this, attackedCreature); + } + + SchedulerTask* task = createSchedulerTask(std::max(SCHEDULER_MINTICKS, delay), std::bind(&Game::checkCreatureAttack, &g_game, getID())); + if (!classicSpeed) { + setNextActionTask(task); + } else { + g_scheduler.addEvent(task); + } + + if (result) { + lastAttack = OTSYS_TIME(); + } + } +} + +uint64_t Player::getGainedExperience(Creature* attacker) const +{ + if (g_config.getBoolean(ConfigManager::EXPERIENCE_FROM_PLAYERS)) { + Player* attackerPlayer = attacker->getPlayer(); + if (attackerPlayer && attackerPlayer != this && skillLoss && std::abs(static_cast(attackerPlayer->getLevel() - level)) <= g_config.getNumber(ConfigManager::EXP_FROM_PLAYERS_LEVEL_RANGE)) { + return std::max(0, std::floor(getLostExperience() * getDamageRatio(attacker) * 0.75)); + } + } + return 0; +} + +void Player::onFollowCreature(const Creature* creature) +{ + if (!creature) { + stopWalk(); + } +} + +void Player::setChaseMode(bool mode) +{ + bool prevChaseMode = chaseMode; + chaseMode = mode; + + if (prevChaseMode != chaseMode) { + if (chaseMode) { + if (!followCreature && attackedCreature) { + //chase opponent + setFollowCreature(attackedCreature); + } + } else if (attackedCreature) { + setFollowCreature(nullptr); + cancelNextWalk = true; + } + } +} + +void Player::onWalkAborted() +{ + setNextWalkActionTask(nullptr); + sendCancelWalk(); +} + +void Player::onWalkComplete() +{ + if (walkTask) { + walkTaskEvent = g_scheduler.addEvent(walkTask); + walkTask = nullptr; + } +} + +void Player::stopWalk() +{ + cancelNextWalk = true; +} + +LightInfo Player::getCreatureLight() const +{ + if (internalLight.level > itemsLight.level) { + return internalLight; + } + return itemsLight; +} + +void Player::updateItemsLight(bool internal /*=false*/) +{ + LightInfo maxLight; + + for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) { + Item* item = inventory[i]; + if (item) { + LightInfo curLight = item->getLightInfo(); + + if (curLight.level > maxLight.level) { + maxLight = std::move(curLight); + } + } + } + + if (itemsLight.level != maxLight.level || itemsLight.color != maxLight.color) { + itemsLight = maxLight; + + if (!internal) { + g_game.changeLight(this); + } + } +} + +void Player::onAddCondition(ConditionType_t type) +{ + Creature::onAddCondition(type); + + if (type == CONDITION_OUTFIT && isMounted()) { + dismount(); + } + + sendIcons(); +} + +void Player::onAddCombatCondition(ConditionType_t type) +{ + switch (type) { + case CONDITION_POISON: + sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are poisoned."); + break; + + case CONDITION_DROWN: + sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are drowning."); + break; + + case CONDITION_PARALYZE: + sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are paralyzed."); + break; + + case CONDITION_DRUNK: + sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are drunk."); + break; + + case CONDITION_CURSED: + sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are cursed."); + break; + + case CONDITION_FREEZING: + sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are freezing."); + break; + + case CONDITION_DAZZLED: + sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are dazzled."); + break; + + case CONDITION_BLEEDING: + sendTextMessage(MESSAGE_STATUS_DEFAULT, "You are bleeding."); + break; + + default: + break; + } +} + +void Player::onEndCondition(ConditionType_t type) +{ + Creature::onEndCondition(type); + + if (type == CONDITION_INFIGHT) { + onIdleStatus(); + pzLocked = false; + clearAttacked(); + + if (getSkull() != SKULL_RED && getSkull() != SKULL_BLACK) { + setSkull(SKULL_NONE); + } + } + + sendIcons(); +} + +void Player::onCombatRemoveCondition(Condition* condition) +{ + //Creature::onCombatRemoveCondition(condition); + if (condition->getId() > 0) { + //Means the condition is from an item, id == slot + if (g_game.getWorldType() == WORLD_TYPE_PVP_ENFORCED) { + Item* item = getInventoryItem(static_cast(condition->getId())); + if (item) { + //25% chance to destroy the item + if (25 >= uniform_random(1, 100)) { + g_game.internalRemoveItem(item); + } + } + } + } else { + if (!canDoAction()) { + const uint32_t delay = getNextActionTime(); + const int32_t ticks = delay - (delay % EVENT_CREATURE_THINK_INTERVAL); + if (ticks < 0) { + removeCondition(condition); + } else { + condition->setTicks(ticks); + } + } else { + removeCondition(condition); + } + } +} + +void Player::onAttackedCreature(Creature* target, bool addFightTicks /* = true */) +{ + Creature::onAttackedCreature(target); + + if (target->getZone() == ZONE_PVP) { + return; + } + + if (target == this) { + if (addFightTicks) { + addInFightTicks(); + } + return; + } + + if (hasFlag(PlayerFlag_NotGainInFight)) { + return; + } + + Player* targetPlayer = target->getPlayer(); + if (targetPlayer && !isPartner(targetPlayer) && !isGuildMate(targetPlayer)) { + if (!pzLocked && g_game.getWorldType() == WORLD_TYPE_PVP_ENFORCED) { + pzLocked = true; + sendIcons(); + } + + targetPlayer->addInFightTicks(); + + if (getSkull() == SKULL_NONE && getSkullClient(targetPlayer) == SKULL_YELLOW) { + addAttacked(targetPlayer); + targetPlayer->sendCreatureSkull(this); + } else if (!targetPlayer->hasAttacked(this)) { + if (!pzLocked) { + pzLocked = true; + sendIcons(); + } + + if (!Combat::isInPvpZone(this, targetPlayer) && !isInWar(targetPlayer)) { + addAttacked(targetPlayer); + + if (targetPlayer->getSkull() == SKULL_NONE && getSkull() == SKULL_NONE) { + setSkull(SKULL_WHITE); + } + + if (getSkull() == SKULL_NONE) { + targetPlayer->sendCreatureSkull(this); + } + } + } + } + + if (addFightTicks) { + addInFightTicks(); + } +} + +void Player::onAttacked() +{ + Creature::onAttacked(); + + addInFightTicks(); +} + +void Player::onIdleStatus() +{ + Creature::onIdleStatus(); + + if (party) { + party->clearPlayerPoints(this); + } +} + +void Player::onPlacedCreature() +{ + //scripting event - onLogin + if (!g_creatureEvents->playerLogin(this)) { + kickPlayer(true); + } +} + +void Player::onAttackedCreatureDrainHealth(Creature* target, int32_t points) +{ + Creature::onAttackedCreatureDrainHealth(target, points); + + if (target) { + if (party && !Combat::isPlayerCombat(target)) { + Monster* tmpMonster = target->getMonster(); + if (tmpMonster && tmpMonster->isHostile()) { + //We have fulfilled a requirement for shared experience + party->updatePlayerTicks(this, points); + } + } + } +} + +void Player::onTargetCreatureGainHealth(Creature* target, int32_t points) +{ + if (target && party) { + Player* tmpPlayer = nullptr; + + if (target->getPlayer()) { + tmpPlayer = target->getPlayer(); + } else if (Creature* targetMaster = target->getMaster()) { + if (Player* targetMasterPlayer = targetMaster->getPlayer()) { + tmpPlayer = targetMasterPlayer; + } + } + + if (isPartner(tmpPlayer)) { + party->updatePlayerTicks(this, points); + } + } +} + +bool Player::onKilledCreature(Creature* target, bool lastHit/* = true*/) +{ + bool unjustified = false; + + if (hasFlag(PlayerFlag_NotGenerateLoot)) { + target->setDropLoot(false); + } + + Creature::onKilledCreature(target, lastHit); + + Player* targetPlayer = target->getPlayer(); + if (!targetPlayer) { + return false; + } + + if (targetPlayer->getZone() == ZONE_PVP) { + targetPlayer->setDropLoot(false); + targetPlayer->setSkillLoss(false); + } else if (!hasFlag(PlayerFlag_NotGainInFight) && !isPartner(targetPlayer)) { + if (!Combat::isInPvpZone(this, targetPlayer) && hasAttacked(targetPlayer) && !targetPlayer->hasAttacked(this) && !isGuildMate(targetPlayer) && targetPlayer != this) { + if (targetPlayer->getSkull() == SKULL_NONE && !isInWar(targetPlayer)) { + unjustified = true; + addUnjustifiedDead(targetPlayer); + } + + if (lastHit && hasCondition(CONDITION_INFIGHT)) { + pzLocked = true; + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_INFIGHT, g_config.getNumber(ConfigManager::WHITE_SKULL_TIME), 0); + addCondition(condition); + } + } + } + + return unjustified; +} + +void Player::gainExperience(uint64_t gainExp, Creature* source) +{ + if (hasFlag(PlayerFlag_NotGainExperience) || gainExp == 0 || staminaMinutes == 0) { + return; + } + + addExperience(source, gainExp, true); +} + +void Player::onGainExperience(uint64_t gainExp, Creature* target) +{ + if (hasFlag(PlayerFlag_NotGainExperience)) { + return; + } + + if (target && !target->getPlayer() && party && party->isSharedExperienceActive() && party->isSharedExperienceEnabled()) { + party->shareExperience(gainExp, target); + //We will get a share of the experience through the sharing mechanism + return; + } + + Creature::onGainExperience(gainExp, target); + gainExperience(gainExp, target); +} + +void Player::onGainSharedExperience(uint64_t gainExp, Creature* source) +{ + gainExperience(gainExp, source); +} + +bool Player::isImmune(CombatType_t type) const +{ + if (hasFlag(PlayerFlag_CannotBeAttacked)) { + return true; + } + return Creature::isImmune(type); +} + +bool Player::isImmune(ConditionType_t type) const +{ + if (hasFlag(PlayerFlag_CannotBeAttacked)) { + return true; + } + return Creature::isImmune(type); +} + +bool Player::isAttackable() const +{ + return !hasFlag(PlayerFlag_CannotBeAttacked); +} + +bool Player::lastHitIsPlayer(Creature* lastHitCreature) +{ + if (!lastHitCreature) { + return false; + } + + if (lastHitCreature->getPlayer()) { + return true; + } + + Creature* lastHitMaster = lastHitCreature->getMaster(); + return lastHitMaster && lastHitMaster->getPlayer(); +} + +void Player::changeHealth(int32_t healthChange, bool sendHealthChange/* = true*/) +{ + Creature::changeHealth(healthChange, sendHealthChange); + sendStats(); +} + +void Player::changeMana(int32_t manaChange) +{ + if (!hasFlag(PlayerFlag_HasInfiniteMana)) { + if (manaChange > 0) { + mana += std::min(manaChange, getMaxMana() - mana); + } else { + mana = std::max(0, mana + manaChange); + } + } + + sendStats(); +} + +void Player::changeSoul(int32_t soulChange) +{ + if (soulChange > 0) { + soul += std::min(soulChange, vocation->getSoulMax() - soul); + } else { + soul = std::max(0, soul + soulChange); + } + + sendStats(); +} + +bool Player::canWear(uint32_t lookType, uint8_t addons) const +{ + if (group->access) { + return true; + } + + const Outfit* outfit = Outfits::getInstance().getOutfitByLookType(sex, lookType); + if (!outfit) { + return false; + } + + if (outfit->premium && !isPremium()) { + return false; + } + + if (outfit->unlocked && addons == 0) { + return true; + } + + for (const OutfitEntry& outfitEntry : outfits) { + if (outfitEntry.lookType != lookType) { + continue; + } + return (outfitEntry.addons & addons) == addons; + } + return false; +} + +bool Player::canLogout() +{ + if (isConnecting) { + return false; + } + + if (getTile()->hasFlag(TILESTATE_NOLOGOUT)) { + return false; + } + + if (getTile()->hasFlag(TILESTATE_PROTECTIONZONE)) { + return true; + } + + return !isPzLocked() && !hasCondition(CONDITION_INFIGHT); +} + +void Player::genReservedStorageRange() +{ + //generate outfits range + uint32_t base_key = PSTRG_OUTFITS_RANGE_START; + for (const OutfitEntry& entry : outfits) { + storageMap[++base_key] = (entry.lookType << 16) | entry.addons; + } +} + +void Player::addOutfit(uint16_t lookType, uint8_t addons) +{ + for (OutfitEntry& outfitEntry : outfits) { + if (outfitEntry.lookType == lookType) { + outfitEntry.addons |= addons; + return; + } + } + outfits.emplace_back(lookType, addons); +} + +bool Player::removeOutfit(uint16_t lookType) +{ + for (auto it = outfits.begin(), end = outfits.end(); it != end; ++it) { + OutfitEntry& entry = *it; + if (entry.lookType == lookType) { + outfits.erase(it); + return true; + } + } + return false; +} + +bool Player::removeOutfitAddon(uint16_t lookType, uint8_t addons) +{ + for (OutfitEntry& outfitEntry : outfits) { + if (outfitEntry.lookType == lookType) { + outfitEntry.addons &= ~addons; + return true; + } + } + return false; +} + +bool Player::getOutfitAddons(const Outfit& outfit, uint8_t& addons) const +{ + if (group->access) { + addons = 3; + return true; + } + + if (outfit.premium && !isPremium()) { + return false; + } + + for (const OutfitEntry& outfitEntry : outfits) { + if (outfitEntry.lookType != outfit.lookType) { + continue; + } + + addons = outfitEntry.addons; + return true; + } + + if (!outfit.unlocked) { + return false; + } + + addons = 0; + return true; +} + +void Player::setSex(PlayerSex_t newSex) +{ + sex = newSex; +} + +Skulls_t Player::getSkull() const +{ + if (hasFlag(PlayerFlag_NotGainInFight)) { + return SKULL_NONE; + } + return skull; +} + +Skulls_t Player::getSkullClient(const Creature* creature) const +{ + if (!creature || g_game.getWorldType() != WORLD_TYPE_PVP) { + return SKULL_NONE; + } + + const Player* player = creature->getPlayer(); + if (!player || player->getSkull() != SKULL_NONE) { + return Creature::getSkullClient(creature); + } + + if (isInWar(player)) { + return SKULL_GREEN; + } + + if (!player->getGuildWarVector().empty() && guild == player->getGuild()) { + return SKULL_GREEN; + } + + if (player->hasAttacked(this)) { + return SKULL_YELLOW; + } + + if (isPartner(player)) { + return SKULL_GREEN; + } + return Creature::getSkullClient(creature); +} + +bool Player::hasAttacked(const Player* attacked) const +{ + if (hasFlag(PlayerFlag_NotGainInFight) || !attacked) { + return false; + } + + return attackedSet.find(attacked->guid) != attackedSet.end(); +} + +void Player::addAttacked(const Player* attacked) +{ + if (hasFlag(PlayerFlag_NotGainInFight) || !attacked || attacked == this) { + return; + } + + attackedSet.insert(attacked->guid); +} + +void Player::removeAttacked(const Player* attacked) +{ + if (!attacked || attacked == this) { + return; + } + + auto it = attackedSet.find(attacked->guid); + if (it != attackedSet.end()) { + attackedSet.erase(it); + } +} + +void Player::clearAttacked() +{ + attackedSet.clear(); +} + +void Player::addUnjustifiedDead(const Player* attacked) +{ + if (hasFlag(PlayerFlag_NotGainInFight) || attacked == this || g_game.getWorldType() == WORLD_TYPE_PVP_ENFORCED) { + return; + } + + sendTextMessage(MESSAGE_EVENT_ADVANCE, "Warning! The murder of " + attacked->getName() + " was not justified."); + + skullTicks += g_config.getNumber(ConfigManager::FRAG_TIME); + + if (getSkull() != SKULL_BLACK) { + if (g_config.getNumber(ConfigManager::KILLS_TO_BLACK) != 0 && skullTicks > (g_config.getNumber(ConfigManager::KILLS_TO_BLACK) - 1) * static_cast(g_config.getNumber(ConfigManager::FRAG_TIME))) { + setSkull(SKULL_BLACK); + } else if (getSkull() != SKULL_RED && g_config.getNumber(ConfigManager::KILLS_TO_RED) != 0 && skullTicks > (g_config.getNumber(ConfigManager::KILLS_TO_RED) - 1) * static_cast(g_config.getNumber(ConfigManager::FRAG_TIME))) { + setSkull(SKULL_RED); + } + } +} + +void Player::checkSkullTicks(int64_t ticks) +{ + int64_t newTicks = skullTicks - ticks; + if (newTicks < 0) { + skullTicks = 0; + } else { + skullTicks = newTicks; + } + + if ((skull == SKULL_RED || skull == SKULL_BLACK) && skullTicks < 1 && !hasCondition(CONDITION_INFIGHT)) { + setSkull(SKULL_NONE); + } +} + +bool Player::isPromoted() const +{ + uint16_t promotedVocation = g_vocations.getPromotedVocation(vocation->getId()); + return promotedVocation == VOCATION_NONE && vocation->getId() != promotedVocation; +} + +double Player::getLostPercent() const +{ + int32_t blessingCount = std::bitset<5>(blessings).count(); + + int32_t deathLosePercent = g_config.getNumber(ConfigManager::DEATH_LOSE_PERCENT); + if (deathLosePercent != -1) { + if (isPromoted()) { + deathLosePercent -= 3; + } + + deathLosePercent -= blessingCount; + return std::max(0, deathLosePercent) / 100.; + } + + double lossPercent; + if (level >= 25) { + double tmpLevel = level + (levelPercent / 100.); + lossPercent = static_cast((tmpLevel + 50) * 50 * ((tmpLevel * tmpLevel) - (5 * tmpLevel) + 8)) / experience; + } else { + lossPercent = 10; + } + + double percentReduction = 0; + if (isPromoted()) { + percentReduction += 30; + } + percentReduction += blessingCount * 8; + return lossPercent * (1 - (percentReduction / 100.)) / 100.; +} + +void Player::learnInstantSpell(const std::string& spellName) +{ + if (!hasLearnedInstantSpell(spellName)) { + learnedInstantSpellList.push_front(spellName); + } +} + +void Player::forgetInstantSpell(const std::string& spellName) +{ + learnedInstantSpellList.remove(spellName); +} + +bool Player::hasLearnedInstantSpell(const std::string& spellName) const +{ + if (hasFlag(PlayerFlag_CannotUseSpells)) { + return false; + } + + if (hasFlag(PlayerFlag_IgnoreSpellCheck)) { + return true; + } + + for (const auto& learnedSpellName : learnedInstantSpellList) { + if (strcasecmp(learnedSpellName.c_str(), spellName.c_str()) == 0) { + return true; + } + } + return false; +} + +bool Player::isInWar(const Player* player) const +{ + if (!player || !guild) { + return false; + } + + const Guild* playerGuild = player->getGuild(); + if (!playerGuild) { + return false; + } + + return isInWarList(playerGuild->getId()) && player->isInWarList(guild->getId()); +} + +bool Player::isInWarList(uint32_t guildId) const +{ + return std::find(guildWarVector.begin(), guildWarVector.end(), guildId) != guildWarVector.end(); +} + +bool Player::isPremium() const +{ + if (g_config.getBoolean(ConfigManager::FREE_PREMIUM) || hasFlag(PlayerFlag_IsAlwaysPremium)) { + return true; + } + + return premiumDays > 0; +} + +void Player::setPremiumDays(int32_t v) +{ + premiumDays = v; + sendBasicData(); +} + +PartyShields_t Player::getPartyShield(const Player* player) const +{ + if (!player) { + return SHIELD_NONE; + } + + if (party) { + if (party->getLeader() == player) { + if (party->isSharedExperienceActive()) { + if (party->isSharedExperienceEnabled()) { + return SHIELD_YELLOW_SHAREDEXP; + } + + if (party->canUseSharedExperience(player)) { + return SHIELD_YELLOW_NOSHAREDEXP; + } + + return SHIELD_YELLOW_NOSHAREDEXP_BLINK; + } + + return SHIELD_YELLOW; + } + + if (player->party == party) { + if (party->isSharedExperienceActive()) { + if (party->isSharedExperienceEnabled()) { + return SHIELD_BLUE_SHAREDEXP; + } + + if (party->canUseSharedExperience(player)) { + return SHIELD_BLUE_NOSHAREDEXP; + } + + return SHIELD_BLUE_NOSHAREDEXP_BLINK; + } + + return SHIELD_BLUE; + } + + if (isInviting(player)) { + return SHIELD_WHITEBLUE; + } + } + + if (player->isInviting(this)) { + return SHIELD_WHITEYELLOW; + } + + if (player->party) { + return SHIELD_GRAY; + } + + return SHIELD_NONE; +} + +bool Player::isInviting(const Player* player) const +{ + if (!player || !party || party->getLeader() != this) { + return false; + } + return party->isPlayerInvited(player); +} + +bool Player::isPartner(const Player* player) const +{ + if (!player || !party || player == this) { + return false; + } + return party == player->party; +} + +bool Player::isGuildMate(const Player* player) const +{ + if (!player || !guild) { + return false; + } + return guild == player->guild; +} + +void Player::sendPlayerPartyIcons(Player* player) +{ + sendCreatureShield(player); + sendCreatureSkull(player); +} + +bool Player::addPartyInvitation(Party* party) +{ + auto it = std::find(invitePartyList.begin(), invitePartyList.end(), party); + if (it != invitePartyList.end()) { + return false; + } + + invitePartyList.push_front(party); + return true; +} + +void Player::removePartyInvitation(Party* party) +{ + invitePartyList.remove(party); +} + +void Player::clearPartyInvitations() +{ + for (Party* invitingParty : invitePartyList) { + invitingParty->removeInvite(*this, false); + } + invitePartyList.clear(); +} + +GuildEmblems_t Player::getGuildEmblem(const Player* player) const +{ + if (!player) { + return GUILDEMBLEM_NONE; + } + + const Guild* playerGuild = player->getGuild(); + if (!playerGuild) { + return GUILDEMBLEM_NONE; + } + + if (player->getGuildWarVector().empty()) { + if (guild == playerGuild) { + return GUILDEMBLEM_MEMBER; + } else { + return GUILDEMBLEM_OTHER; + } + } else if (guild == playerGuild) { + return GUILDEMBLEM_ALLY; + } else if (isInWar(player)) { + return GUILDEMBLEM_ENEMY; + } + + return GUILDEMBLEM_NEUTRAL; +} + +uint8_t Player::getCurrentMount() const +{ + int32_t value; + if (getStorageValue(PSTRG_MOUNTS_CURRENTMOUNT, value)) { + return value; + } + return 0; +} + +void Player::setCurrentMount(uint8_t mountId) +{ + addStorageValue(PSTRG_MOUNTS_CURRENTMOUNT, mountId); +} + +bool Player::toggleMount(bool mount) +{ + if ((OTSYS_TIME() - lastToggleMount) < 3000 && !wasMounted) { + sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED); + return false; + } + + if (mount) { + if (isMounted()) { + return false; + } + + if (!group->access && tile->hasFlag(TILESTATE_PROTECTIONZONE)) { + sendCancelMessage(RETURNVALUE_ACTIONNOTPERMITTEDINPROTECTIONZONE); + return false; + } + + const Outfit* playerOutfit = Outfits::getInstance().getOutfitByLookType(getSex(), defaultOutfit.lookType); + if (!playerOutfit) { + return false; + } + + uint8_t currentMountId = getCurrentMount(); + if (currentMountId == 0) { + sendOutfitWindow(); + return false; + } + + Mount* currentMount = g_game.mounts.getMountByID(currentMountId); + if (!currentMount) { + return false; + } + + if (!hasMount(currentMount)) { + setCurrentMount(0); + sendOutfitWindow(); + return false; + } + + if (currentMount->premium && !isPremium()) { + sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT); + return false; + } + + if (hasCondition(CONDITION_OUTFIT)) { + sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return false; + } + + defaultOutfit.lookMount = currentMount->clientId; + + if (currentMount->speed != 0) { + g_game.changeSpeed(this, currentMount->speed); + } + } else { + if (!isMounted()) { + return false; + } + + dismount(); + } + + g_game.internalCreatureChangeOutfit(this, defaultOutfit); + lastToggleMount = OTSYS_TIME(); + return true; +} + +bool Player::tameMount(uint8_t mountId) +{ + if (!g_game.mounts.getMountByID(mountId)) { + return false; + } + + const uint8_t tmpMountId = mountId - 1; + const uint32_t key = PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31); + + int32_t value; + if (getStorageValue(key, value)) { + value |= (1 << (tmpMountId % 31)); + } else { + value = (1 << (tmpMountId % 31)); + } + + addStorageValue(key, value); + return true; +} + +bool Player::untameMount(uint8_t mountId) +{ + if (!g_game.mounts.getMountByID(mountId)) { + return false; + } + + const uint8_t tmpMountId = mountId - 1; + const uint32_t key = PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31); + + int32_t value; + if (!getStorageValue(key, value)) { + return true; + } + + value &= ~(1 << (tmpMountId % 31)); + addStorageValue(key, value); + + if (getCurrentMount() == mountId) { + if (isMounted()) { + dismount(); + g_game.internalCreatureChangeOutfit(this, defaultOutfit); + } + + setCurrentMount(0); + } + + return true; +} + +bool Player::hasMount(const Mount* mount) const +{ + if (isAccessPlayer()) { + return true; + } + + if (mount->premium && !isPremium()) { + return false; + } + + const uint8_t tmpMountId = mount->id - 1; + + int32_t value; + if (!getStorageValue(PSTRG_MOUNTS_RANGE_START + (tmpMountId / 31), value)) { + return false; + } + + return ((1 << (tmpMountId % 31)) & value) != 0; +} + +void Player::dismount() +{ + Mount* mount = g_game.mounts.getMountByID(getCurrentMount()); + if (mount && mount->speed > 0) { + g_game.changeSpeed(this, -mount->speed); + } + + defaultOutfit.lookMount = 0; +} + +bool Player::addOfflineTrainingTries(skills_t skill, uint64_t tries) +{ + if (tries == 0 || skill == SKILL_LEVEL) { + return false; + } + + bool sendUpdate = false; + uint32_t oldSkillValue, newSkillValue; + long double oldPercentToNextLevel, newPercentToNextLevel; + + if (skill == SKILL_MAGLEVEL) { + uint64_t currReqMana = vocation->getReqMana(magLevel); + uint64_t nextReqMana = vocation->getReqMana(magLevel + 1); + + if (currReqMana >= nextReqMana) { + return false; + } + + oldSkillValue = magLevel; + oldPercentToNextLevel = static_cast(manaSpent * 100) / nextReqMana; + + g_events->eventPlayerOnGainSkillTries(this, SKILL_MAGLEVEL, tries); + uint32_t currMagLevel = magLevel; + + while ((manaSpent + tries) >= nextReqMana) { + tries -= nextReqMana - manaSpent; + + magLevel++; + manaSpent = 0; + + g_creatureEvents->playerAdvance(this, SKILL_MAGLEVEL, magLevel - 1, magLevel); + + sendUpdate = true; + currReqMana = nextReqMana; + nextReqMana = vocation->getReqMana(magLevel + 1); + + if (currReqMana >= nextReqMana) { + tries = 0; + break; + } + } + + manaSpent += tries; + + if (magLevel != currMagLevel) { + std::ostringstream ss; + ss << "You advanced to magic level " << magLevel << '.'; + sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str()); + } + + uint8_t newPercent; + if (nextReqMana > currReqMana) { + newPercent = Player::getPercentLevel(manaSpent, nextReqMana); + newPercentToNextLevel = static_cast(manaSpent * 100) / nextReqMana; + } else { + newPercent = 0; + newPercentToNextLevel = 0; + } + + if (newPercent != magLevelPercent) { + magLevelPercent = newPercent; + sendUpdate = true; + } + + newSkillValue = magLevel; + } else { + uint64_t currReqTries = vocation->getReqSkillTries(skill, skills[skill].level); + uint64_t nextReqTries = vocation->getReqSkillTries(skill, skills[skill].level + 1); + if (currReqTries >= nextReqTries) { + return false; + } + + oldSkillValue = skills[skill].level; + oldPercentToNextLevel = static_cast(skills[skill].tries * 100) / nextReqTries; + + g_events->eventPlayerOnGainSkillTries(this, skill, tries); + uint32_t currSkillLevel = skills[skill].level; + + while ((skills[skill].tries + tries) >= nextReqTries) { + tries -= nextReqTries - skills[skill].tries; + + skills[skill].level++; + skills[skill].tries = 0; + skills[skill].percent = 0; + + g_creatureEvents->playerAdvance(this, skill, (skills[skill].level - 1), skills[skill].level); + + sendUpdate = true; + currReqTries = nextReqTries; + nextReqTries = vocation->getReqSkillTries(skill, skills[skill].level + 1); + + if (currReqTries >= nextReqTries) { + tries = 0; + break; + } + } + + skills[skill].tries += tries; + + if (currSkillLevel != skills[skill].level) { + std::ostringstream ss; + ss << "You advanced to " << getSkillName(skill) << " level " << skills[skill].level << '.'; + sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str()); + } + + uint8_t newPercent; + if (nextReqTries > currReqTries) { + newPercent = Player::getPercentLevel(skills[skill].tries, nextReqTries); + newPercentToNextLevel = static_cast(skills[skill].tries * 100) / nextReqTries; + } else { + newPercent = 0; + newPercentToNextLevel = 0; + } + + if (skills[skill].percent != newPercent) { + skills[skill].percent = newPercent; + sendUpdate = true; + } + + newSkillValue = skills[skill].level; + } + + if (sendUpdate) { + sendSkills(); + } + + std::ostringstream ss; + ss << std::fixed << std::setprecision(2) << "Your " << ucwords(getSkillName(skill)) << " skill changed from level " << oldSkillValue << " (with " << oldPercentToNextLevel << "% progress towards level " << (oldSkillValue + 1) << ") to level " << newSkillValue << " (with " << newPercentToNextLevel << "% progress towards level " << (newSkillValue + 1) << ')'; + sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str()); + return sendUpdate; +} + +bool Player::hasModalWindowOpen(uint32_t modalWindowId) const +{ + return find(modalWindows.begin(), modalWindows.end(), modalWindowId) != modalWindows.end(); +} + +void Player::onModalWindowHandled(uint32_t modalWindowId) +{ + modalWindows.remove(modalWindowId); +} + +void Player::sendModalWindow(const ModalWindow& modalWindow) +{ + if (!client) { + return; + } + + modalWindows.push_front(modalWindow.id); + client->sendModalWindow(modalWindow); +} + +void Player::clearModalWindows() +{ + modalWindows.clear(); +} + +uint16_t Player::getHelpers() const +{ + uint16_t helpers; + + if (guild && party) { + std::unordered_set helperSet; + + const auto& guildMembers = guild->getMembersOnline(); + helperSet.insert(guildMembers.begin(), guildMembers.end()); + + const auto& partyMembers = party->getMembers(); + helperSet.insert(partyMembers.begin(), partyMembers.end()); + + const auto& partyInvitees = party->getInvitees(); + helperSet.insert(partyInvitees.begin(), partyInvitees.end()); + + helperSet.insert(party->getLeader()); + + helpers = helperSet.size(); + } else if (guild) { + helpers = guild->getMembersOnline().size(); + } else if (party) { + helpers = party->getMemberCount() + party->getInvitationCount() + 1; + } else { + helpers = 0; + } + + return helpers; +} + +void Player::sendClosePrivate(uint16_t channelId) +{ + if (channelId == CHANNEL_GUILD || channelId == CHANNEL_PARTY) { + g_chat->removeUserFromChannel(*this, channelId); + } + + if (client) { + client->sendClosePrivate(channelId); + } +} + +uint64_t Player::getMoney() const +{ + std::vector containers; + uint64_t moneyCount = 0; + + for (int32_t i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) { + Item* item = inventory[i]; + if (!item) { + continue; + } + + const Container* container = item->getContainer(); + if (container) { + containers.push_back(container); + } else { + moneyCount += item->getWorth(); + } + } + + size_t i = 0; + while (i < containers.size()) { + const Container* container = containers[i++]; + for (const Item* item : container->getItemList()) { + const Container* tmpContainer = item->getContainer(); + if (tmpContainer) { + containers.push_back(tmpContainer); + } else { + moneyCount += item->getWorth(); + } + } + } + return moneyCount; +} + +size_t Player::getMaxVIPEntries() const +{ + if (group->maxVipEntries != 0) { + return group->maxVipEntries; + } else if (isPremium()) { + return 100; + } + return 20; +} + +size_t Player::getMaxDepotItems() const +{ + if (group->maxDepotItems != 0) { + return group->maxDepotItems; + } else if (isPremium()) { + return 2000; + } + return 1000; +} + +std::forward_list Player::getMuteConditions() const +{ + std::forward_list muteConditions; + for (Condition* condition : conditions) { + if (condition->getTicks() <= 0) { + continue; + } + + ConditionType_t type = condition->getType(); + if (type != CONDITION_MUTED && type != CONDITION_CHANNELMUTEDTICKS && type != CONDITION_YELLTICKS) { + continue; + } + + muteConditions.push_front(condition); + } + return muteConditions; +} + +void Player::setGuild(Guild* guild) +{ + if (guild == this->guild) { + return; + } + + Guild* oldGuild = this->guild; + + this->guildNick.clear(); + this->guild = nullptr; + this->guildRank = nullptr; + + if (guild) { + GuildRank_ptr rank = guild->getRankByLevel(1); + if (!rank) { + return; + } + + this->guild = guild; + this->guildRank = rank; + guild->addMember(this); + } + + if (oldGuild) { + oldGuild->removeMember(this); + } +} diff --git a/src/player.h b/src/player.h new file mode 100644 index 0000000..6606bd7 --- /dev/null +++ b/src/player.h @@ -0,0 +1,1372 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_PLAYER_H_4083D3D3A05B4EDE891B31BB720CD06F +#define FS_PLAYER_H_4083D3D3A05B4EDE891B31BB720CD06F + +#include "creature.h" +#include "container.h" +#include "cylinder.h" +#include "outfit.h" +#include "enums.h" +#include "vocation.h" +#include "protocolgame.h" +#include "ioguild.h" +#include "party.h" +#include "inbox.h" +#include "depotchest.h" +#include "depotlocker.h" +#include "guild.h" +#include "groups.h" +#include "town.h" +#include "mounts.h" + +class House; +class NetworkMessage; +class Weapon; +class ProtocolGame; +class Npc; +class Party; +class SchedulerTask; +class Bed; +class Guild; + +enum skillsid_t { + SKILLVALUE_LEVEL = 0, + SKILLVALUE_TRIES = 1, + SKILLVALUE_PERCENT = 2, +}; + +enum fightMode_t : uint8_t { + FIGHTMODE_ATTACK = 1, + FIGHTMODE_BALANCED = 2, + FIGHTMODE_DEFENSE = 3, +}; + +enum pvpMode_t : uint8_t { + PVP_MODE_DOVE = 0, + PVP_MODE_WHITE_HAND = 1, + PVP_MODE_YELLOW_HAND = 2, + PVP_MODE_RED_FIST = 3, +}; + +enum tradestate_t : uint8_t { + TRADE_NONE, + TRADE_INITIATED, + TRADE_ACCEPT, + TRADE_ACKNOWLEDGE, + TRADE_TRANSFER, +}; + +struct VIPEntry { + VIPEntry(uint32_t guid, std::string name, std::string description, uint32_t icon, bool notify) : + guid(guid), name(std::move(name)), description(std::move(description)), icon(icon), notify(notify) {} + + uint32_t guid; + std::string name; + std::string description; + uint32_t icon; + bool notify; +}; + +struct OpenContainer { + Container* container; + uint16_t index; +}; + +struct OutfitEntry { + constexpr OutfitEntry(uint16_t lookType, uint8_t addons) : lookType(lookType), addons(addons) {} + + uint16_t lookType; + uint8_t addons; +}; + +struct Skill { + uint64_t tries = 0; + uint16_t level = 10; + uint8_t percent = 0; +}; + +using MuteCountMap = std::map; + +static constexpr int32_t PLAYER_MAX_SPEED = 1500; +static constexpr int32_t PLAYER_MIN_SPEED = 10; + +class Player final : public Creature, public Cylinder +{ + public: + explicit Player(ProtocolGame_ptr p); + ~Player(); + + // non-copyable + Player(const Player&) = delete; + Player& operator=(const Player&) = delete; + + Player* getPlayer() override { + return this; + } + const Player* getPlayer() const override { + return this; + } + + void setID() override { + if (id == 0) { + id = playerAutoID++; + } + } + + static MuteCountMap muteCountMap; + + const std::string& getName() const override { + return name; + } + void setName(std::string name) { + this->name = std::move(name); + } + const std::string& getNameDescription() const override { + return name; + } + std::string getDescription(int32_t lookDistance) const override; + + CreatureType_t getType() const override { + return CREATURETYPE_PLAYER; + } + + uint8_t getCurrentMount() const; + void setCurrentMount(uint8_t mountId); + bool isMounted() const { + return defaultOutfit.lookMount != 0; + } + bool toggleMount(bool mount); + bool tameMount(uint8_t mountId); + bool untameMount(uint8_t mountId); + bool hasMount(const Mount* mount) const; + void dismount(); + + void sendFYIBox(const std::string& message) { + if (client) { + client->sendFYIBox(message); + } + } + + void setGUID(uint32_t guid) { + this->guid = guid; + } + uint32_t getGUID() const { + return guid; + } + bool canSeeInvisibility() const override { + return hasFlag(PlayerFlag_CanSenseInvisibility) || group->access; + } + + void removeList() override; + void addList() override; + void kickPlayer(bool displayEffect); + + static uint64_t getExpForLevel(int32_t lv) { + lv--; + return ((50ULL * lv * lv * lv) - (150ULL * lv * lv) + (400ULL * lv)) / 3ULL; + } + + uint16_t getStaminaMinutes() const { + return staminaMinutes; + } + + bool addOfflineTrainingTries(skills_t skill, uint64_t tries); + + void addOfflineTrainingTime(int32_t addTime) { + offlineTrainingTime = std::min(12 * 3600 * 1000, offlineTrainingTime + addTime); + } + void removeOfflineTrainingTime(int32_t removeTime) { + offlineTrainingTime = std::max(0, offlineTrainingTime - removeTime); + } + int32_t getOfflineTrainingTime() const { + return offlineTrainingTime; + } + + int32_t getOfflineTrainingSkill() const { + return offlineTrainingSkill; + } + void setOfflineTrainingSkill(int32_t skill) { + offlineTrainingSkill = skill; + } + + uint64_t getBankBalance() const { + return bankBalance; + } + void setBankBalance(uint64_t balance) { + bankBalance = balance; + } + + Guild* getGuild() const { + return guild; + } + void setGuild(Guild* guild); + + GuildRank_ptr getGuildRank() const { + return guildRank; + } + void setGuildRank(GuildRank_ptr newGuildRank) { + guildRank = newGuildRank; + } + + bool isGuildMate(const Player* player) const; + + const std::string& getGuildNick() const { + return guildNick; + } + void setGuildNick(std::string nick) { + guildNick = nick; + } + + bool isInWar(const Player* player) const; + bool isInWarList(uint32_t guildId) const; + + void setLastWalkthroughAttempt(int64_t walkthroughAttempt) { + lastWalkthroughAttempt = walkthroughAttempt; + } + void setLastWalkthroughPosition(Position walkthroughPosition) { + lastWalkthroughPosition = walkthroughPosition; + } + + Inbox* getInbox() const { + return inbox; + } + + uint16_t getClientIcons() const; + + const GuildWarVector& getGuildWarVector() const { + return guildWarVector; + } + + Vocation* getVocation() const { + return vocation; + } + + OperatingSystem_t getOperatingSystem() const { + return operatingSystem; + } + void setOperatingSystem(OperatingSystem_t clientos) { + operatingSystem = clientos; + } + + uint16_t getProtocolVersion() const { + if (!client) { + return 0; + } + + return client->getVersion(); + } + + bool hasSecureMode() const { + return secureMode; + } + + void setParty(Party* party) { + this->party = party; + } + Party* getParty() const { + return party; + } + PartyShields_t getPartyShield(const Player* player) const; + bool isInviting(const Player* player) const; + bool isPartner(const Player* player) const; + void sendPlayerPartyIcons(Player* player); + bool addPartyInvitation(Party* party); + void removePartyInvitation(Party* party); + void clearPartyInvitations(); + + GuildEmblems_t getGuildEmblem(const Player* player) const; + + uint64_t getSpentMana() const { + return manaSpent; + } + + bool hasFlag(PlayerFlags value) const { + return (group->flags & value) != 0; + } + + BedItem* getBedItem() { + return bedItem; + } + void setBedItem(BedItem* b) { + bedItem = b; + } + + void addBlessing(uint8_t blessing) { + blessings |= blessing; + } + void removeBlessing(uint8_t blessing) { + blessings &= ~blessing; + } + bool hasBlessing(uint8_t value) const { + return (blessings & (static_cast(1) << value)) != 0; + } + + bool isOffline() const { + return (getID() == 0); + } + void disconnect() { + if (client) { + client->disconnect(); + } + } + uint32_t getIP() const; + + void addContainer(uint8_t cid, Container* container); + void closeContainer(uint8_t cid); + void setContainerIndex(uint8_t cid, uint16_t index); + + Container* getContainerByID(uint8_t cid); + int8_t getContainerID(const Container* container) const; + uint16_t getContainerIndex(uint8_t cid) const; + + bool canOpenCorpse(uint32_t ownerId) const; + + void addStorageValue(const uint32_t key, const int32_t value, const bool isLogin = false); + bool getStorageValue(const uint32_t key, int32_t& value) const; + void genReservedStorageRange(); + + void setGroup(Group* newGroup) { + group = newGroup; + } + Group* getGroup() const { + return group; + } + + void setInMarket(bool value) { + inMarket = value; + } + bool isInMarket() const { + return inMarket; + } + + void setLastDepotId(int16_t newId) { + lastDepotId = newId; + } + int16_t getLastDepotId() const { + return lastDepotId; + } + + void resetIdleTime() { + idleTime = 0; + } + + bool isInGhostMode() const override { + return ghostMode; + } + void switchGhostMode() { + ghostMode = !ghostMode; + } + + uint32_t getAccount() const { + return accountNumber; + } + AccountType_t getAccountType() const { + return accountType; + } + uint32_t getLevel() const { + return level; + } + uint8_t getLevelPercent() const { + return levelPercent; + } + uint32_t getMagicLevel() const { + return std::max(0, magLevel + varStats[STAT_MAGICPOINTS]); + } + uint32_t getBaseMagicLevel() const { + return magLevel; + } + uint8_t getMagicLevelPercent() const { + return magLevelPercent; + } + uint8_t getSoul() const { + return soul; + } + bool isAccessPlayer() const { + return group->access; + } + bool isPremium() const; + void setPremiumDays(int32_t v); + + uint16_t getHelpers() const; + + bool setVocation(uint16_t vocId); + uint16_t getVocationId() const { + return vocation->getId(); + } + + PlayerSex_t getSex() const { + return sex; + } + void setSex(PlayerSex_t); + uint64_t getExperience() const { + return experience; + } + + time_t getLastLoginSaved() const { + return lastLoginSaved; + } + + time_t getLastLogout() const { + return lastLogout; + } + + const Position& getLoginPosition() const { + return loginPosition; + } + const Position& getTemplePosition() const { + return town->getTemplePosition(); + } + Town* getTown() const { + return town; + } + void setTown(Town* town) { + this->town = town; + } + + void clearModalWindows(); + bool hasModalWindowOpen(uint32_t modalWindowId) const; + void onModalWindowHandled(uint32_t modalWindowId); + + bool isPushable() const override; + uint32_t isMuted() const; + void addMessageBuffer(); + void removeMessageBuffer(); + + bool removeItemOfType(uint16_t itemId, uint32_t amount, int32_t subType, bool ignoreEquipped = false) const; + + uint32_t getCapacity() const { + if (hasFlag(PlayerFlag_CannotPickupItem)) { + return 0; + } else if (hasFlag(PlayerFlag_HasInfiniteCapacity)) { + return std::numeric_limits::max(); + } + return capacity; + } + + uint32_t getFreeCapacity() const { + if (hasFlag(PlayerFlag_CannotPickupItem)) { + return 0; + } else if (hasFlag(PlayerFlag_HasInfiniteCapacity)) { + return std::numeric_limits::max(); + } else { + return std::max(0, capacity - inventoryWeight); + } + } + + int32_t getMaxHealth() const override { + return std::max(1, healthMax + varStats[STAT_MAXHITPOINTS]); + } + uint32_t getMana() const { + return mana; + } + uint32_t getMaxMana() const { + return std::max(0, manaMax + varStats[STAT_MAXMANAPOINTS]); + } + + Item* getInventoryItem(slots_t slot) const; + + bool isItemAbilityEnabled(slots_t slot) const { + return inventoryAbilities[slot]; + } + void setItemAbility(slots_t slot, bool enabled) { + inventoryAbilities[slot] = enabled; + } + + void setVarSkill(skills_t skill, int32_t modifier) { + varSkills[skill] += modifier; + } + + void setVarSpecialSkill(SpecialSkills_t skill, int32_t modifier) { + varSpecialSkills[skill] += modifier; + } + + void setVarStats(stats_t stat, int32_t modifier); + int32_t getDefaultStats(stats_t stat) const; + + void addConditionSuppressions(uint32_t conditions); + void removeConditionSuppressions(uint32_t conditions); + + DepotChest* getDepotChest(uint32_t depotId, bool autoCreate); + DepotLocker* getDepotLocker(uint32_t depotId); + void onReceiveMail() const; + bool isNearDepotBox() const; + + bool canSee(const Position& pos) const override; + bool canSeeCreature(const Creature* creature) const override; + + bool canWalkthrough(const Creature* creature) const; + bool canWalkthroughEx(const Creature* creature) const; + + RaceType_t getRace() const override { + return RACE_BLOOD; + } + + uint64_t getMoney() const; + + //safe-trade functions + void setTradeState(tradestate_t state) { + tradeState = state; + } + tradestate_t getTradeState() const { + return tradeState; + } + Item* getTradeItem() { + return tradeItem; + } + + //shop functions + void setShopOwner(Npc* owner, int32_t onBuy, int32_t onSell) { + shopOwner = owner; + purchaseCallback = onBuy; + saleCallback = onSell; + } + + Npc* getShopOwner(int32_t& onBuy, int32_t& onSell) { + onBuy = purchaseCallback; + onSell = saleCallback; + return shopOwner; + } + + const Npc* getShopOwner(int32_t& onBuy, int32_t& onSell) const { + onBuy = purchaseCallback; + onSell = saleCallback; + return shopOwner; + } + + //V.I.P. functions + void notifyStatusChange(Player* loginPlayer, VipStatus_t status); + bool removeVIP(uint32_t vipGuid); + bool addVIP(uint32_t vipGuid, const std::string& vipName, VipStatus_t status); + bool addVIPInternal(uint32_t vipGuid); + bool editVIP(uint32_t vipGuid, const std::string& description, uint32_t icon, bool notify); + + //follow functions + bool setFollowCreature(Creature* creature) override; + void goToFollowCreature() override; + + //follow events + void onFollowCreature(const Creature* creature) override; + + //walk events + void onWalk(Direction& dir) override; + void onWalkAborted() override; + void onWalkComplete() override; + + void stopWalk(); + void openShopWindow(Npc* npc, const std::list& shop); + bool closeShopWindow(bool sendCloseShopWindow = true); + bool updateSaleShopList(const Item* item); + bool hasShopItemForSale(uint32_t itemId, uint8_t subType) const; + + void setChaseMode(bool mode); + void setFightMode(fightMode_t mode) { + fightMode = mode; + } + void setSecureMode(bool mode) { + secureMode = mode; + } + + //combat functions + bool setAttackedCreature(Creature* creature) override; + bool isImmune(CombatType_t type) const override; + bool isImmune(ConditionType_t type) const override; + bool hasShield() const; + bool isAttackable() const override; + static bool lastHitIsPlayer(Creature* lastHitCreature); + + void changeHealth(int32_t healthChange, bool sendHealthChange = true) override; + void changeMana(int32_t manaChange); + void changeSoul(int32_t soulChange); + + bool isPzLocked() const { + return pzLocked; + } + BlockType_t blockHit(Creature* attacker, CombatType_t combatType, int32_t& damage, + bool checkDefense = false, bool checkArmor = false, bool field = false) override; + void doAttacking(uint32_t interval) override; + bool hasExtraSwing() override { + return lastAttack > 0 && ((OTSYS_TIME() - lastAttack) >= getAttackSpeed()); + } + + uint16_t getSpecialSkill(uint8_t skill) const { + return std::max(0, varSpecialSkills[skill]); + } + uint16_t getSkillLevel(uint8_t skill) const { + return std::max(0, skills[skill].level + varSkills[skill]); + } + uint16_t getBaseSkill(uint8_t skill) const { + return skills[skill].level; + } + uint8_t getSkillPercent(uint8_t skill) const { + return skills[skill].percent; + } + + bool getAddAttackSkill() const { + return addAttackSkillPoint; + } + BlockType_t getLastAttackBlockType() const { + return lastAttackBlockType; + } + + Item* getWeapon(slots_t slot, bool ignoreAmmo) const; + Item* getWeapon(bool ignoreAmmo = false) const; + WeaponType_t getWeaponType() const; + int32_t getWeaponSkill(const Item* item) const; + void getShieldAndWeapon(const Item*& shield, const Item*& weapon) const; + + void drainHealth(Creature* attacker, int32_t damage) override; + void drainMana(Creature* attacker, int32_t manaLoss); + void addManaSpent(uint64_t amount); + void addSkillAdvance(skills_t skill, uint64_t count); + + int32_t getArmor() const override; + int32_t getDefense() const override; + float getAttackFactor() const override; + float getDefenseFactor() const override; + + void addInFightTicks(bool pzlock = false); + + uint64_t getGainedExperience(Creature* attacker) const override; + + //combat event functions + void onAddCondition(ConditionType_t type) override; + void onAddCombatCondition(ConditionType_t type) override; + void onEndCondition(ConditionType_t type) override; + void onCombatRemoveCondition(Condition* condition) override; + void onAttackedCreature(Creature* target, bool addFightTicks = true) override; + void onAttacked() override; + void onAttackedCreatureDrainHealth(Creature* target, int32_t points) override; + void onTargetCreatureGainHealth(Creature* target, int32_t points) override; + bool onKilledCreature(Creature* target, bool lastHit = true) override; + void onGainExperience(uint64_t gainExp, Creature* target) override; + void onGainSharedExperience(uint64_t gainExp, Creature* source); + void onAttackedCreatureBlockHit(BlockType_t blockType) override; + void onBlockHit() override; + void onChangeZone(ZoneType_t zone) override; + void onAttackedCreatureChangeZone(ZoneType_t zone) override; + void onIdleStatus() override; + void onPlacedCreature() override; + + LightInfo getCreatureLight() const override; + + Skulls_t getSkull() const override; + Skulls_t getSkullClient(const Creature* creature) const override; + int64_t getSkullTicks() const { return skullTicks; } + void setSkullTicks(int64_t ticks) { skullTicks = ticks; } + + bool hasAttacked(const Player* attacked) const; + void addAttacked(const Player* attacked); + void removeAttacked(const Player* attacked); + void clearAttacked(); + void addUnjustifiedDead(const Player* attacked); + void sendCreatureSkull(const Creature* creature) const { + if (client) { + client->sendCreatureSkull(creature); + } + } + void checkSkullTicks(int64_t ticks); + + bool canWear(uint32_t lookType, uint8_t addons) const; + void addOutfit(uint16_t lookType, uint8_t addons); + bool removeOutfit(uint16_t lookType); + bool removeOutfitAddon(uint16_t lookType, uint8_t addons); + bool getOutfitAddons(const Outfit& outfit, uint8_t& addons) const; + + bool canLogout(); + + size_t getMaxVIPEntries() const; + size_t getMaxDepotItems() const; + + //tile + //send methods + void sendAddTileItem(const Tile* tile, const Position& pos, const Item* item) { + if (client) { + int32_t stackpos = tile->getStackposOfItem(this, item); + if (stackpos != -1) { + client->sendAddTileItem(pos, stackpos, item); + } + } + } + void sendUpdateTileItem(const Tile* tile, const Position& pos, const Item* item) { + if (client) { + int32_t stackpos = tile->getStackposOfItem(this, item); + if (stackpos != -1) { + client->sendUpdateTileItem(pos, stackpos, item); + } + } + } + void sendRemoveTileThing(const Position& pos, int32_t stackpos) { + if (stackpos != -1 && client) { + client->sendRemoveTileThing(pos, stackpos); + } + } + void sendUpdateTile(const Tile* tile, const Position& pos) { + if (client) { + client->sendUpdateTile(tile, pos); + } + } + + void sendChannelMessage(const std::string& author, const std::string& text, SpeakClasses type, uint16_t channel) { + if (client) { + client->sendChannelMessage(author, text, type, channel); + } + } + void sendChannelEvent(uint16_t channelId, const std::string& playerName, ChannelEvent_t channelEvent) { + if (client) { + client->sendChannelEvent(channelId, playerName, channelEvent); + } + } + void sendCreatureAppear(const Creature* creature, const Position& pos, bool isLogin) { + if (client) { + client->sendAddCreature(creature, pos, creature->getTile()->getStackposOfCreature(this, creature), isLogin); + } + } + void sendCreatureMove(const Creature* creature, const Position& newPos, int32_t newStackPos, const Position& oldPos, int32_t oldStackPos, bool teleport) { + if (client) { + client->sendMoveCreature(creature, newPos, newStackPos, oldPos, oldStackPos, teleport); + } + } + void sendCreatureTurn(const Creature* creature) { + if (client && canSeeCreature(creature)) { + int32_t stackpos = creature->getTile()->getStackposOfCreature(this, creature); + if (stackpos != -1) { + client->sendCreatureTurn(creature, stackpos); + } + } + } + void sendCreatureSay(const Creature* creature, SpeakClasses type, const std::string& text, const Position* pos = nullptr) { + if (client) { + client->sendCreatureSay(creature, type, text, pos); + } + } + void sendPrivateMessage(const Player* speaker, SpeakClasses type, const std::string& text) { + if (client) { + client->sendPrivateMessage(speaker, type, text); + } + } + void sendCreatureSquare(const Creature* creature, SquareColor_t color) { + if (client) { + client->sendCreatureSquare(creature, color); + } + } + void sendCreatureChangeOutfit(const Creature* creature, const Outfit_t& outfit) { + if (client) { + client->sendCreatureOutfit(creature, outfit); + } + } + void sendCreatureChangeVisible(const Creature* creature, bool visible) { + if (!client) { + return; + } + + if (creature->getPlayer()) { + if (visible) { + client->sendCreatureOutfit(creature, creature->getCurrentOutfit()); + } else { + static Outfit_t outfit; + client->sendCreatureOutfit(creature, outfit); + } + } else if (canSeeInvisibility()) { + client->sendCreatureOutfit(creature, creature->getCurrentOutfit()); + } else { + int32_t stackpos = creature->getTile()->getStackposOfCreature(this, creature); + if (stackpos == -1) { + return; + } + + if (visible) { + client->sendAddCreature(creature, creature->getPosition(), stackpos, false); + } else { + client->sendRemoveTileThing(creature->getPosition(), stackpos); + } + } + } + void sendCreatureLight(const Creature* creature) { + if (client) { + client->sendCreatureLight(creature); + } + } + void sendCreatureWalkthrough(const Creature* creature, bool walkthrough) { + if (client) { + client->sendCreatureWalkthrough(creature, walkthrough); + } + } + void sendCreatureShield(const Creature* creature) { + if (client) { + client->sendCreatureShield(creature); + } + } + void sendCreatureType(uint32_t creatureId, uint8_t creatureType) { + if (client) { + client->sendCreatureType(creatureId, creatureType); + } + } + void sendCreatureHelpers(uint32_t creatureId, uint16_t helpers) { + if (client) { + client->sendCreatureHelpers(creatureId, helpers); + } + } + void sendSpellCooldown(uint8_t spellId, uint32_t time) { + if (client) { + client->sendSpellCooldown(spellId, time); + } + } + void sendSpellGroupCooldown(SpellGroup_t groupId, uint32_t time) { + if (client) { + client->sendSpellGroupCooldown(groupId, time); + } + } + void sendModalWindow(const ModalWindow& modalWindow); + + //container + void sendAddContainerItem(const Container* container, const Item* item); + void sendUpdateContainerItem(const Container* container, uint16_t slot, const Item* newItem); + void sendRemoveContainerItem(const Container* container, uint16_t slot); + void sendContainer(uint8_t cid, const Container* container, bool hasParent, uint16_t firstIndex) { + if (client) { + client->sendContainer(cid, container, hasParent, firstIndex); + } + } + + //inventory + void sendInventoryItem(slots_t slot, const Item* item) { + if (client) { + client->sendInventoryItem(slot, item); + } + } + void sendItems() { + if (client) { + client->sendItems(); + } + } + + //event methods + void onUpdateTileItem(const Tile* tile, const Position& pos, const Item* oldItem, + const ItemType& oldType, const Item* newItem, const ItemType& newType) override; + void onRemoveTileItem(const Tile* tile, const Position& pos, const ItemType& iType, + const Item* item) override; + + void onCreatureAppear(Creature* creature, bool isLogin) override; + void onRemoveCreature(Creature* creature, bool isLogout) override; + void onCreatureMove(Creature* creature, const Tile* newTile, const Position& newPos, + const Tile* oldTile, const Position& oldPos, bool teleport) override; + + void onAttackedCreatureDisappear(bool isLogout) override; + void onFollowCreatureDisappear(bool isLogout) override; + + //container + void onAddContainerItem(const Item* item); + void onUpdateContainerItem(const Container* container, const Item* oldItem, const Item* newItem); + void onRemoveContainerItem(const Container* container, const Item* item); + + void onCloseContainer(const Container* container); + void onSendContainer(const Container* container); + void autoCloseContainers(const Container* container); + + //inventory + void onUpdateInventoryItem(Item* oldItem, Item* newItem); + void onRemoveInventoryItem(Item* item); + + void sendCancelMessage(const std::string& msg) const { + if (client) { + client->sendTextMessage(TextMessage(MESSAGE_STATUS_SMALL, msg)); + } + } + void sendCancelMessage(ReturnValue message) const; + void sendCancelTarget() const { + if (client) { + client->sendCancelTarget(); + } + } + void sendCancelWalk() const { + if (client) { + client->sendCancelWalk(); + } + } + void sendChangeSpeed(const Creature* creature, uint32_t newSpeed) const { + if (client) { + client->sendChangeSpeed(creature, newSpeed); + } + } + void sendCreatureHealth(const Creature* creature) const { + if (client) { + client->sendCreatureHealth(creature); + } + } + void sendDistanceShoot(const Position& from, const Position& to, unsigned char type) const { + if (client) { + client->sendDistanceShoot(from, to, type); + } + } + void sendHouseWindow(House* house, uint32_t listId) const; + void sendCreatePrivateChannel(uint16_t channelId, const std::string& channelName) { + if (client) { + client->sendCreatePrivateChannel(channelId, channelName); + } + } + void sendClosePrivate(uint16_t channelId); + void sendIcons() const { + if (client) { + client->sendIcons(getClientIcons()); + } + } + void sendMagicEffect(const Position& pos, uint8_t type) const { + if (client) { + client->sendMagicEffect(pos, type); + } + } + void sendPing(); + void sendPingBack() const { + if (client) { + client->sendPingBack(); + } + } + void sendStats(); + void sendBasicData() const { + if (client) { + client->sendBasicData(); + } + } + void sendSkills() const { + if (client) { + client->sendSkills(); + } + } + void sendTextMessage(MessageClasses mclass, const std::string& message) const { + if (client) { + client->sendTextMessage(TextMessage(mclass, message)); + } + } + void sendTextMessage(const TextMessage& message) const { + if (client) { + client->sendTextMessage(message); + } + } + void sendReLoginWindow(uint8_t unfairFightReduction) const { + if (client) { + client->sendReLoginWindow(unfairFightReduction); + } + } + void sendTextWindow(Item* item, uint16_t maxlen, bool canWrite) const { + if (client) { + client->sendTextWindow(windowTextId, item, maxlen, canWrite); + } + } + void sendTextWindow(uint32_t itemId, const std::string& text) const { + if (client) { + client->sendTextWindow(windowTextId, itemId, text); + } + } + void sendToChannel(const Creature* creature, SpeakClasses type, const std::string& text, uint16_t channelId) const { + if (client) { + client->sendToChannel(creature, type, text, channelId); + } + } + void sendShop(Npc* npc) const { + if (client) { + client->sendShop(npc, shopItemList); + } + } + void sendSaleItemList() const { + if (client) { + client->sendSaleItemList(shopItemList); + } + } + void sendCloseShop() const { + if (client) { + client->sendCloseShop(); + } + } + void sendMarketEnter(uint32_t depotId) const { + if (client) { + client->sendMarketEnter(depotId); + } + } + void sendMarketLeave() { + inMarket = false; + if (client) { + client->sendMarketLeave(); + } + } + void sendMarketBrowseItem(uint16_t itemId, const MarketOfferList& buyOffers, const MarketOfferList& sellOffers) const { + if (client) { + client->sendMarketBrowseItem(itemId, buyOffers, sellOffers); + } + } + void sendMarketBrowseOwnOffers(const MarketOfferList& buyOffers, const MarketOfferList& sellOffers) const { + if (client) { + client->sendMarketBrowseOwnOffers(buyOffers, sellOffers); + } + } + void sendMarketBrowseOwnHistory(const HistoryMarketOfferList& buyOffers, const HistoryMarketOfferList& sellOffers) const { + if (client) { + client->sendMarketBrowseOwnHistory(buyOffers, sellOffers); + } + } + void sendMarketDetail(uint16_t itemId) const { + if (client) { + client->sendMarketDetail(itemId); + } + } + void sendMarketAcceptOffer(const MarketOfferEx& offer) const { + if (client) { + client->sendMarketAcceptOffer(offer); + } + } + void sendMarketCancelOffer(const MarketOfferEx& offer) const { + if (client) { + client->sendMarketCancelOffer(offer); + } + } + void sendTradeItemRequest(const std::string& traderName, const Item* item, bool ack) const { + if (client) { + client->sendTradeItemRequest(traderName, item, ack); + } + } + void sendTradeClose() const { + if (client) { + client->sendCloseTrade(); + } + } + void sendWorldLight(LightInfo lightInfo) { + if (client) { + client->sendWorldLight(lightInfo); + } + } + void sendChannelsDialog() { + if (client) { + client->sendChannelsDialog(); + } + } + void sendOpenPrivateChannel(const std::string& receiver) { + if (client) { + client->sendOpenPrivateChannel(receiver); + } + } + void sendOutfitWindow() { + if (client) { + client->sendOutfitWindow(); + } + } + void sendCloseContainer(uint8_t cid) { + if (client) { + client->sendCloseContainer(cid); + } + } + + void sendChannel(uint16_t channelId, const std::string& channelName, const UsersMap* channelUsers, const InvitedMap* invitedUsers) { + if (client) { + client->sendChannel(channelId, channelName, channelUsers, invitedUsers); + } + } + void sendTutorial(uint8_t tutorialId) { + if (client) { + client->sendTutorial(tutorialId); + } + } + void sendAddMarker(const Position& pos, uint8_t markType, const std::string& desc) { + if (client) { + client->sendAddMarker(pos, markType, desc); + } + } + void sendQuestLog() { + if (client) { + client->sendQuestLog(); + } + } + void sendQuestLine(const Quest* quest) { + if (client) { + client->sendQuestLine(quest); + } + } + void sendEnterWorld() { + if (client) { + client->sendEnterWorld(); + } + } + void sendFightModes() { + if (client) { + client->sendFightModes(); + } + } + void sendNetworkMessage(const NetworkMessage& message, bool broadcast = true) { + if (client) { + client->writeToOutputBuffer(message, broadcast); + } + } + + void receivePing() { + lastPong = OTSYS_TIME(); + } + + void onThink(uint32_t interval) override; + + void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + + void setNextAction(int64_t time) { + if (time > nextAction) { + nextAction = time; + } + } + bool canDoAction() const { + return nextAction <= OTSYS_TIME(); + } + uint32_t getNextActionTime() const; + + Item* getWriteItem(uint32_t& windowTextId, uint16_t& maxWriteLen); + void setWriteItem(Item* item, uint16_t maxWriteLen = 0); + + House* getEditHouse(uint32_t& windowTextId, uint32_t& listId); + void setEditHouse(House* house, uint32_t listId = 0); + + void learnInstantSpell(const std::string& spellName); + void forgetInstantSpell(const std::string& spellName); + bool hasLearnedInstantSpell(const std::string& spellName) const; + + bool startLiveCast(const std::string& password) { + return client && client->startLiveCast(password); + } + + bool stopLiveCast() { + return client && client->stopLiveCast(); + } + + bool isLiveCaster() const { + return client && client->isLiveCaster(); + } + + const std::map& getOpenContainers() const { + return openContainers; + } + + private: + std::forward_list getMuteConditions() const; + + void checkTradeState(const Item* item); + bool hasCapacity(const Item* item, uint32_t count) const; + + void gainExperience(uint64_t gainExp, Creature* source); + void addExperience(Creature* source, uint64_t exp, bool sendText = false); + void removeExperience(uint64_t exp, bool sendText = false); + + void updateInventoryWeight(); + + void setNextWalkActionTask(SchedulerTask* task); + void setNextWalkTask(SchedulerTask* task); + void setNextActionTask(SchedulerTask* task); + + void death(Creature* lastHitCreature) override; + bool dropCorpse(Creature* lastHitCreature, Creature* mostDamageCreature, bool lastHitUnjustified, bool mostDamageUnjustified) override; + Item* getCorpse(Creature* lastHitCreature, Creature* mostDamageCreature) override; + + //cylinder implementations + ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count, + uint32_t flags, Creature* actor = nullptr) const override; + ReturnValue queryMaxCount(int32_t index, const Thing& thing, uint32_t count, uint32_t& maxQueryCount, + uint32_t flags) const override; + ReturnValue queryRemove(const Thing& thing, uint32_t count, uint32_t flags) const override; + Cylinder* queryDestination(int32_t& index, const Thing& thing, Item** destItem, + uint32_t& flags) override; + + void addThing(Thing*) override {} + void addThing(int32_t index, Thing* thing) override; + + void updateThing(Thing* thing, uint16_t itemId, uint32_t count) override; + void replaceThing(uint32_t index, Thing* thing) override; + + void removeThing(Thing* thing, uint32_t count) override; + + int32_t getThingIndex(const Thing* thing) const override; + size_t getFirstIndex() const override; + size_t getLastIndex() const override; + uint32_t getItemTypeCount(uint16_t itemId, int32_t subType = -1) const override; + std::map& getAllItemTypeCount(std::map& countMap) const override; + Thing* getThing(size_t index) const override; + + void internalAddThing(Thing* thing) override; + void internalAddThing(uint32_t index, Thing* thing) override; + + std::unordered_set attackedSet; + std::unordered_set VIPList; + + std::map openContainers; + std::map depotLockerMap; + std::map depotChests; + std::map storageMap; + + std::vector outfits; + GuildWarVector guildWarVector; + + std::list shopItemList; + + std::forward_list invitePartyList; + std::forward_list modalWindows; + std::forward_list learnedInstantSpellList; + std::forward_list storedConditionList; // TODO: This variable is only temporarily used when logging in, get rid of it somehow + + std::string name; + std::string guildNick; + + Skill skills[SKILL_LAST + 1]; + LightInfo itemsLight; + Position loginPosition; + Position lastWalkthroughPosition; + + time_t lastLoginSaved = 0; + time_t lastLogout = 0; + + uint64_t experience = 0; + uint64_t manaSpent = 0; + uint64_t lastAttack = 0; + uint64_t bankBalance = 0; + uint64_t lastQuestlogUpdate = 0; + int64_t lastFailedFollow = 0; + int64_t skullTicks = 0; + int64_t lastWalkthroughAttempt = 0; + int64_t lastToggleMount = 0; + int64_t lastPing; + int64_t lastPong; + int64_t nextAction = 0; + + BedItem* bedItem = nullptr; + Guild* guild = nullptr; + GuildRank_ptr guildRank = nullptr; + Group* group = nullptr; + Inbox* inbox; + Item* tradeItem = nullptr; + Item* inventory[CONST_SLOT_LAST + 1] = {}; + Item* writeItem = nullptr; + House* editHouse = nullptr; + Npc* shopOwner = nullptr; + Party* party = nullptr; + Player* tradePartner = nullptr; + ProtocolGame_ptr client; + SchedulerTask* walkTask = nullptr; + Town* town = nullptr; + Vocation* vocation = nullptr; + + uint32_t inventoryWeight = 0; + uint32_t capacity = 40000; + uint32_t damageImmunities = 0; + uint32_t conditionImmunities = 0; + uint32_t conditionSuppressions = 0; + uint32_t level = 1; + uint32_t magLevel = 0; + uint32_t actionTaskEvent = 0; + uint32_t nextStepEvent = 0; + uint32_t walkTaskEvent = 0; + uint32_t MessageBufferTicks = 0; + uint32_t lastIP = 0; + uint32_t accountNumber = 0; + uint32_t guid = 0; + uint32_t windowTextId = 0; + uint32_t editListId = 0; + uint32_t mana = 0; + uint32_t manaMax = 0; + int32_t varSkills[SKILL_LAST + 1] = {}; + int32_t varSpecialSkills[SPECIALSKILL_LAST + 1] = {}; + int32_t varStats[STAT_LAST + 1] = {}; + int32_t purchaseCallback = -1; + int32_t saleCallback = -1; + int32_t MessageBufferCount = 0; + int32_t premiumDays = 0; + int32_t bloodHitCount = 0; + int32_t shieldBlockCount = 0; + int32_t offlineTrainingSkill = -1; + int32_t offlineTrainingTime = 0; + int32_t idleTime = 0; + + uint16_t lastStatsTrainingTime = 0; + uint16_t staminaMinutes = 2520; + uint16_t maxWriteLen = 0; + int16_t lastDepotId = -1; + + uint8_t soul = 0; + uint8_t blessings = 0; + uint8_t levelPercent = 0; + uint8_t magLevelPercent = 0; + + PlayerSex_t sex = PLAYERSEX_FEMALE; + OperatingSystem_t operatingSystem = CLIENTOS_NONE; + BlockType_t lastAttackBlockType = BLOCK_NONE; + tradestate_t tradeState = TRADE_NONE; + fightMode_t fightMode = FIGHTMODE_ATTACK; + AccountType_t accountType = ACCOUNT_TYPE_NORMAL; + + bool chaseMode = false; + bool secureMode = false; + bool inMarket = false; + bool wasMounted = false; + bool ghostMode = false; + bool pzLocked = false; + bool isConnecting = false; + bool addAttackSkillPoint = false; + bool inventoryAbilities[CONST_SLOT_LAST + 1] = {}; + + static uint32_t playerAutoID; + + void updateItemsLight(bool internal = false); + int32_t getStepSpeed() const override { + return std::max(PLAYER_MIN_SPEED, std::min(PLAYER_MAX_SPEED, getSpeed())); + } + void updateBaseSpeed() { + if (!hasFlag(PlayerFlag_SetMaxSpeed)) { + baseSpeed = vocation->getBaseSpeed() + (2 * (level - 1)); + } else { + baseSpeed = PLAYER_MAX_SPEED; + } + } + + bool isPromoted() const; + + uint32_t getAttackSpeed() const { + return vocation->getAttackSpeed(); + } + + static uint8_t getPercentLevel(uint64_t count, uint64_t nextLevelCount); + double getLostPercent() const; + uint64_t getLostExperience() const override { + return skillLoss ? static_cast(experience * getLostPercent()) : 0; + } + uint32_t getDamageImmunities() const override { + return damageImmunities; + } + uint32_t getConditionImmunities() const override { + return conditionImmunities; + } + uint32_t getConditionSuppressions() const override { + return conditionSuppressions; + } + uint16_t getLookCorpse() const override; + void getPathSearchParams(const Creature* creature, FindPathParams& fpp) const override; + + friend class Game; + friend class Npc; + friend class LuaScriptInterface; + friend class Map; + friend class Actions; + friend class IOLoginData; + friend class ProtocolGameBase; + friend class ProtocolGame; +}; + +#endif diff --git a/src/position.cpp b/src/position.cpp new file mode 100644 index 0000000..247aace --- /dev/null +++ b/src/position.cpp @@ -0,0 +1,73 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "position.h" + +std::ostream& operator<<(std::ostream& os, const Position& pos) +{ + os << "( " << std::setw(5) << std::setfill('0') << pos.x; + os << " / " << std::setw(5) << std::setfill('0') << pos.y; + os << " / " << std::setw(3) << std::setfill('0') << pos.getZ(); + os << " )"; + return os; +} + +std::ostream& operator<<(std::ostream& os, const Direction& dir) +{ + switch (dir) { + case DIRECTION_NORTH: + os << "North"; + break; + + case DIRECTION_EAST: + os << "East"; + break; + + case DIRECTION_WEST: + os << "West"; + break; + + case DIRECTION_SOUTH: + os << "South"; + break; + + case DIRECTION_SOUTHWEST: + os << "South-West"; + break; + + case DIRECTION_SOUTHEAST: + os << "South-East"; + break; + + case DIRECTION_NORTHWEST: + os << "North-West"; + break; + + case DIRECTION_NORTHEAST: + os << "North-East"; + break; + + default: + break; + } + + return os; +} diff --git a/src/position.h b/src/position.h new file mode 100644 index 0000000..1d48840 --- /dev/null +++ b/src/position.h @@ -0,0 +1,134 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_POSITION_H_5B684192F7034FB8857C8280D2CC6C75 +#define FS_POSITION_H_5B684192F7034FB8857C8280D2CC6C75 + +enum Direction : uint8_t { + DIRECTION_NORTH = 0, + DIRECTION_EAST = 1, + DIRECTION_SOUTH = 2, + DIRECTION_WEST = 3, + + DIRECTION_DIAGONAL_MASK = 4, + DIRECTION_SOUTHWEST = DIRECTION_DIAGONAL_MASK | 0, + DIRECTION_SOUTHEAST = DIRECTION_DIAGONAL_MASK | 1, + DIRECTION_NORTHWEST = DIRECTION_DIAGONAL_MASK | 2, + DIRECTION_NORTHEAST = DIRECTION_DIAGONAL_MASK | 3, + + DIRECTION_LAST = DIRECTION_NORTHEAST, + DIRECTION_NONE = 8, +}; + +struct Position +{ + constexpr Position() = default; + constexpr Position(uint16_t x, uint16_t y, uint8_t z) : x(x), y(y), z(z) {} + + template + static bool areInRange(const Position& p1, const Position& p2) { + return Position::getDistanceX(p1, p2) <= deltax && Position::getDistanceY(p1, p2) <= deltay; + } + + template + static bool areInRange(const Position& p1, const Position& p2) { + return Position::getDistanceX(p1, p2) <= deltax && Position::getDistanceY(p1, p2) <= deltay && Position::getDistanceZ(p1, p2) <= deltaz; + } + + static int_fast32_t getOffsetX(const Position& p1, const Position& p2) { + return p1.getX() - p2.getX(); + } + static int_fast32_t getOffsetY(const Position& p1, const Position& p2) { + return p1.getY() - p2.getY(); + } + static int_fast16_t getOffsetZ(const Position& p1, const Position& p2) { + return p1.getZ() - p2.getZ(); + } + + static int32_t getDistanceX(const Position& p1, const Position& p2) { + return std::abs(Position::getOffsetX(p1, p2)); + } + static int32_t getDistanceY(const Position& p1, const Position& p2) { + return std::abs(Position::getOffsetY(p1, p2)); + } + static int16_t getDistanceZ(const Position& p1, const Position& p2) { + return std::abs(Position::getOffsetZ(p1, p2)); + } + + uint16_t x = 0; + uint16_t y = 0; + uint8_t z = 0; + + bool operator<(const Position& p) const { + if (z < p.z) { + return true; + } + + if (z > p.z) { + return false; + } + + if (y < p.y) { + return true; + } + + if (y > p.y) { + return false; + } + + if (x < p.x) { + return true; + } + + if (x > p.x) { + return false; + } + + return false; + } + + bool operator>(const Position& p) const { + return ! (*this < p); + } + + bool operator==(const Position& p) const { + return p.x == x && p.y == y && p.z == z; + } + + bool operator!=(const Position& p) const { + return p.x != x || p.y != y || p.z != z; + } + + Position operator+(const Position& p1) const { + return Position(x + p1.x, y + p1.y, z + p1.z); + } + + Position operator-(const Position& p1) const { + return Position(x - p1.x, y - p1.y, z - p1.z); + } + + int_fast32_t getX() const { return x; } + int_fast32_t getY() const { return y; } + int_fast16_t getZ() const { return z; } +}; + +std::ostream& operator<<(std::ostream&, const Position&); +std::ostream& operator<<(std::ostream&, const Direction&); + +#endif diff --git a/src/protocol.cpp b/src/protocol.cpp new file mode 100644 index 0000000..6980580 --- /dev/null +++ b/src/protocol.cpp @@ -0,0 +1,109 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "protocol.h" +#include "outputmessage.h" +#include "rsa.h" +#include "xtea.h" + +extern RSA g_RSA; + +void Protocol::onSendMessage(const OutputMessage_ptr& msg) const +{ + if (!rawMessages) { + msg->writeMessageLength(); + + if (encryptionEnabled) { + XTEA_encrypt(*msg); + msg->addCryptoHeader(checksumEnabled); + } + } +} + +void Protocol::onRecvMessage(NetworkMessage& msg) +{ + if (encryptionEnabled && !XTEA_decrypt(msg)) { + return; + } + + parsePacket(msg); +} + +OutputMessage_ptr Protocol::getOutputBuffer(int32_t size) +{ + //dispatcher thread + if (!outputBuffer) { + outputBuffer = OutputMessagePool::getOutputMessage(); + } else if ((outputBuffer->getLength() + size) > NetworkMessage::MAX_PROTOCOL_BODY_LENGTH) { + send(outputBuffer); + outputBuffer = OutputMessagePool::getOutputMessage(); + } + return outputBuffer; +} + +void Protocol::XTEA_encrypt(OutputMessage& msg) const +{ + // The message must be a multiple of 8 + size_t paddingBytes = msg.getLength() % 8u; + if (paddingBytes != 0) { + msg.addPaddingBytes(8 - paddingBytes); + } + + uint8_t* buffer = msg.getOutputBuffer(); + xtea::encrypt(buffer, msg.getLength(), key); +} + +bool Protocol::XTEA_decrypt(NetworkMessage& msg) const +{ + if (((msg.getLength() - 6) & 7) != 0) { + return false; + } + + uint8_t* buffer = msg.getBuffer() + msg.getBufferPosition(); + xtea::decrypt(buffer, msg.getLength() - 6, key); + + uint16_t innerLength = msg.get(); + if (innerLength + 8 > msg.getLength()) { + return false; + } + + msg.setLength(innerLength); + return true; +} + +bool Protocol::RSA_decrypt(NetworkMessage& msg) +{ + if ((msg.getLength() - msg.getBufferPosition()) < 128) { + return false; + } + + g_RSA.decrypt(reinterpret_cast(msg.getBuffer()) + msg.getBufferPosition()); //does not break strict aliasing + return msg.getByte() == 0; +} + +uint32_t Protocol::getIP() const +{ + if (auto connection = getConnection()) { + return connection->getIP(); + } + + return 0; +} diff --git a/src/protocol.h b/src/protocol.h new file mode 100644 index 0000000..3cd757d --- /dev/null +++ b/src/protocol.h @@ -0,0 +1,105 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_PROTOCOL_H_D71405071ACF4137A4B1203899DE80E1 +#define FS_PROTOCOL_H_D71405071ACF4137A4B1203899DE80E1 + +#include "connection.h" +#include "xtea.h" + +class Protocol : public std::enable_shared_from_this +{ + public: + explicit Protocol(Connection_ptr connection) : connection(connection) {} + virtual ~Protocol() = default; + + // non-copyable + Protocol(const Protocol&) = delete; + Protocol& operator=(const Protocol&) = delete; + + virtual void parsePacket(NetworkMessage&) {} + + virtual void onSendMessage(const OutputMessage_ptr& msg) const; + void onRecvMessage(NetworkMessage& msg); + virtual void onRecvFirstMessage(NetworkMessage& msg) = 0; + virtual void onConnect() {} + + bool isConnectionExpired() const { + return connection.expired(); + } + + Connection_ptr getConnection() const { + return connection.lock(); + } + + uint32_t getIP() const; + + //Use this function for autosend messages only + OutputMessage_ptr getOutputBuffer(int32_t size); + + OutputMessage_ptr& getCurrentBuffer() { + return outputBuffer; + } + + void send(OutputMessage_ptr msg) const { + if (auto connection = getConnection()) { + connection->send(msg); + } + } + + protected: + void disconnect() const { + if (auto connection = getConnection()) { + connection->close(); + } + } + void enableXTEAEncryption() { + encryptionEnabled = true; + } + void setXTEAKey(xtea::key key) { + this->key = std::move(key); + } + void disableChecksum() { + checksumEnabled = false; + } + + static bool RSA_decrypt(NetworkMessage& msg); + + void setRawMessages(bool value) { + rawMessages = value; + } + + virtual void release() {} + + private: + void XTEA_encrypt(OutputMessage& msg) const; + bool XTEA_decrypt(NetworkMessage& msg) const; + + friend class Connection; + + OutputMessage_ptr outputBuffer; + + const ConnectionWeak_ptr connection; + xtea::key key; + bool encryptionEnabled = false; + bool checksumEnabled = true; + bool rawMessages = false; +}; + +#endif diff --git a/src/protocolgame.cpp b/src/protocolgame.cpp new file mode 100644 index 0000000..12ba82a --- /dev/null +++ b/src/protocolgame.cpp @@ -0,0 +1,2514 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include + +#include "protocolgame.h" + +#include "outputmessage.h" + +#include "player.h" + +#include "configmanager.h" +#include "actions.h" +#include "game.h" +#include "iologindata.h" +#include "iomarket.h" +#include "waitlist.h" +#include "ban.h" +#include "scheduler.h" +#include "databasetasks.h" + +extern Game g_game; +extern ConfigManager g_config; +extern Actions actions; +extern CreatureEvents* g_creatureEvents; +extern Chat* g_chat; + +ProtocolGame::LiveCastsMap ProtocolGame::liveCasts; + +void ProtocolGame::release() +{ + //dispatcher thread + stopLiveCast(); + if (player && player->client == shared_from_this()) { + player->client.reset(); + player->decrementReferenceCounter(); + player = nullptr; + } + + OutputMessagePool::getInstance().removeProtocolFromAutosend(shared_from_this()); + Protocol::release(); +} + +void ProtocolGame::login(const std::string& name, uint32_t accountId, OperatingSystem_t operatingSystem) +{ + //dispatcher thread + Player* foundPlayer = g_game.getPlayerByName(name); + if (!foundPlayer || g_config.getBoolean(ConfigManager::ALLOW_CLONES)) { + player = new Player(getThis()); + player->setName(name); + + player->incrementReferenceCounter(); + player->setID(); + + if (!IOLoginData::preloadPlayer(player, name)) { + disconnectClient("Your character could not be loaded."); + return; + } + + if (IOBan::isPlayerNamelocked(player->getGUID())) { + disconnectClient("Your character has been namelocked."); + return; + } + + if (g_game.getGameState() == GAME_STATE_CLOSING && !player->hasFlag(PlayerFlag_CanAlwaysLogin)) { + disconnectClient("The game is just going down.\nPlease try again later."); + return; + } + + if (g_game.getGameState() == GAME_STATE_CLOSED && !player->hasFlag(PlayerFlag_CanAlwaysLogin)) { + disconnectClient("Server is currently closed.\nPlease try again later."); + return; + } + + if (g_config.getBoolean(ConfigManager::ONE_PLAYER_ON_ACCOUNT) && player->getAccountType() < ACCOUNT_TYPE_GAMEMASTER && g_game.getPlayerByAccount(player->getAccount())) { + disconnectClient("You may only login with one character\nof your account at the same time."); + return; + } + + if (!player->hasFlag(PlayerFlag_CannotBeBanned)) { + BanInfo banInfo; + if (IOBan::isAccountBanned(accountId, banInfo)) { + if (banInfo.reason.empty()) { + banInfo.reason = "(none)"; + } + + std::ostringstream ss; + if (banInfo.expiresAt > 0) { + ss << "Your account has been banned until " << formatDateShort(banInfo.expiresAt) << " by " << banInfo.bannedBy << ".\n\nReason specified:\n" << banInfo.reason; + } else { + ss << "Your account has been permanently banned by " << banInfo.bannedBy << ".\n\nReason specified:\n" << banInfo.reason; + } + disconnectClient(ss.str()); + return; + } + } + + WaitingList& waitingList = WaitingList::getInstance(); + if (!waitingList.clientLogin(player)) { + uint32_t currentSlot = waitingList.getClientSlot(player); + uint32_t retryTime = WaitingList::getTime(currentSlot); + std::ostringstream ss; + + ss << "Too many players online.\nYou are at place " + << currentSlot << " on the waiting list."; + + auto output = OutputMessagePool::getOutputMessage(); + output->addByte(0x16); + output->addString(ss.str()); + output->addByte(retryTime); + send(output); + disconnect(); + return; + } + + if (!IOLoginData::loadPlayerById(player, player->getGUID())) { + disconnectClient("Your character could not be loaded."); + return; + } + + player->setOperatingSystem(operatingSystem); + + if (!g_game.placeCreature(player, player->getLoginPosition())) { + if (!g_game.placeCreature(player, player->getTemplePosition(), false, true)) { + disconnectClient("Temple position is wrong. Contact the administrator."); + return; + } + } + + if (operatingSystem >= CLIENTOS_OTCLIENT_LINUX) { + player->registerCreatureEvent("ExtendedOpcode"); + } + + player->lastIP = player->getIP(); + player->lastLoginSaved = std::max(time(nullptr), player->lastLoginSaved + 1); + acceptPackets = true; + } else { + if (eventConnect != 0 || !g_config.getBoolean(ConfigManager::REPLACE_KICK_ON_LOGIN)) { + //Already trying to connect + disconnectClient("You are already logged in."); + return; + } + + if (foundPlayer->client) { + foundPlayer->disconnect(); + foundPlayer->isConnecting = true; + + eventConnect = g_scheduler.addEvent(createSchedulerTask(1000, std::bind(&ProtocolGame::connect, getThis(), foundPlayer->getID(), operatingSystem))); + } else { + connect(foundPlayer->getID(), operatingSystem); + } + } + OutputMessagePool::getInstance().addProtocolToAutosend(shared_from_this()); +} + +void ProtocolGame::connect(uint32_t playerId, OperatingSystem_t operatingSystem) +{ + eventConnect = 0; + + Player* foundPlayer = g_game.getPlayerByID(playerId); + if (!foundPlayer || foundPlayer->client) { + disconnectClient("You are already logged in."); + return; + } + + if (isConnectionExpired()) { + //ProtocolGame::release() has been called at this point and the Connection object + //no longer exists, so we return to prevent leakage of the Player. + return; + } + + player = foundPlayer; + player->incrementReferenceCounter(); + + g_chat->removeUserFromAllChannels(*player); + player->clearModalWindows(); + player->setOperatingSystem(operatingSystem); + player->isConnecting = false; + + player->client = getThis(); + sendAddCreature(player, player->getPosition(), 0, false); + player->lastIP = player->getIP(); + player->lastLoginSaved = std::max(time(nullptr), player->lastLoginSaved + 1); + acceptPackets = true; +} + +void ProtocolGame::logout(bool displayEffect, bool forced) +{ + //dispatcher thread + if (!player) { + return; + } + + if (!player->isRemoved()) { + if (!forced) { + if (!player->isAccessPlayer()) { + if (player->getTile()->hasFlag(TILESTATE_NOLOGOUT)) { + player->sendCancelMessage(RETURNVALUE_YOUCANNOTLOGOUTHERE); + return; + } + + if (!player->getTile()->hasFlag(TILESTATE_PROTECTIONZONE) && player->hasCondition(CONDITION_INFIGHT)) { + player->sendCancelMessage(RETURNVALUE_YOUMAYNOTLOGOUTDURINGAFIGHT); + return; + } + } + + //scripting event - onLogout + if (!g_creatureEvents->playerLogout(player)) { + //Let the script handle the error message + return; + } + } + + if (displayEffect && player->getHealth() > 0) { + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + } + } + + stopLiveCast(); + disconnect(); + + g_game.removeCreature(player); +} + +bool ProtocolGame::startLiveCast(const std::string& password /*= ""*/) +{ + auto connection = getConnection(); + if (!g_config.getBoolean(ConfigManager::ENABLE_LIVE_CASTING) || isLiveCaster() || !player || player->isRemoved() || !connection || liveCasts.size() >= getMaxLiveCastCount()) { + return false; + } + + { + std::lock_guard lock {liveCastLock}; + //DO NOT do any send operations here + liveCastName = player->getName(); + liveCastPassword = password; + isCaster.store(true, std::memory_order_relaxed); + } + + liveCasts.insert(std::make_pair(player, getThis())); + + registerLiveCast(); + //Send a "dummy" channel + sendChannel(CHANNEL_CAST, LIVE_CAST_CHAT_NAME, nullptr, nullptr); + return true; +} + +bool ProtocolGame::stopLiveCast() +{ + //dispatcher + if (!isLiveCaster()) { + return false; + } + + CastSpectatorVec spectators; + + { + std::lock_guard lock {liveCastLock}; + //DO NOT do any send operations here + std::swap(this->spectators, spectators); + isCaster.store(false, std::memory_order_relaxed); + } + + liveCasts.erase(player); + for (auto& spectator : spectators) { + spectator->onLiveCastStop(); + } + unregisterLiveCast(); + + return true; +} + +void ProtocolGame::clearLiveCastInfo() +{ + static std::once_flag flag; + std::call_once(flag, []() { + assert(g_game.getGameState() == GAME_STATE_INIT); + std::ostringstream query; + query << "TRUNCATE TABLE `live_casts`;"; + g_databaseTasks.addTask(query.str()); + }); +} + +void ProtocolGame::registerLiveCast() +{ + std::ostringstream query; + query << "INSERT into `live_casts` (`player_id`, `cast_name`, `password`) VALUES (" << player->getGUID() << ", '" + << getLiveCastName() << "', " << isPasswordProtected() << ");"; + g_databaseTasks.addTask(query.str()); +} + +void ProtocolGame::unregisterLiveCast() +{ + std::ostringstream query; + query << "DELETE FROM `live_casts` WHERE `player_id`=" << player->getGUID() << ";"; + g_databaseTasks.addTask(query.str()); +} + +void ProtocolGame::updateLiveCastInfo() +{ + std::ostringstream query; + query << "UPDATE `live_casts` SET `cast_name`='" << getLiveCastName() << "', `password`=" + << isPasswordProtected() << ", `spectators`=" << getSpectatorCount() + << " WHERE `player_id`=" << player->getGUID() << ";"; + g_databaseTasks.addTask(query.str()); +} + +void ProtocolGame::addSpectator(ProtocolSpectator_ptr spectatorClient) +{ + std::lock_guard lock(liveCastLock); + //DO NOT do any send operations here + spectators.emplace_back(spectatorClient); + updateLiveCastInfo(); +} + +void ProtocolGame::removeSpectator(ProtocolSpectator_ptr spectatorClient) +{ + std::lock_guard lock(liveCastLock); + //DO NOT do any send operations here + auto it = std::find(spectators.begin(), spectators.end(), spectatorClient); + if (it != spectators.end()) { + spectators.erase(it); + updateLiveCastInfo(); + } +} + +void ProtocolGame::onRecvFirstMessage(NetworkMessage& msg) +{ + if (g_game.getGameState() == GAME_STATE_SHUTDOWN) { + disconnect(); + return; + } + + OperatingSystem_t operatingSystem = static_cast(msg.get()); + version = msg.get(); + + msg.skipBytes(7); // U32 client version, U8 client type, U16 dat revision + + if (!Protocol::RSA_decrypt(msg)) { + disconnect(); + return; + } + + xtea::key key; + key[0] = msg.get(); + key[1] = msg.get(); + key[2] = msg.get(); + key[3] = msg.get(); + enableXTEAEncryption(); + setXTEAKey(std::move(key)); + + if (operatingSystem >= CLIENTOS_OTCLIENT_LINUX) { + NetworkMessage opcodeMessage; + opcodeMessage.addByte(0x32); + opcodeMessage.addByte(0x00); + opcodeMessage.add(0x00); + writeToOutputBuffer(opcodeMessage); + } + + msg.skipBytes(1); // gamemaster flag + + std::string sessionKey = msg.getString(); + + auto sessionArgs = explodeString(sessionKey, "\n", 4); + if (sessionArgs.size() != 4) { + disconnect(); + return; + } + + std::string& accountName = sessionArgs[0]; + std::string& password = sessionArgs[1]; + std::string& token = sessionArgs[2]; + uint32_t tokenTime = 0; + try { + tokenTime = std::stoul(sessionArgs[3]); + } catch (const std::invalid_argument&) { + disconnectClient("Malformed token packet."); + return; + } catch (const std::out_of_range&) { + disconnectClient("Token time is too long."); + return; + } + + if (accountName.empty()) { + disconnectClient("You must enter your account name."); + return; + } + + std::string characterName = msg.getString(); + + uint32_t timeStamp = msg.get(); + uint8_t randNumber = msg.getByte(); + if (challengeTimestamp != timeStamp || challengeRandom != randNumber) { + disconnect(); + return; + } + + if (version < CLIENT_VERSION_MIN || version > CLIENT_VERSION_MAX) { + std::ostringstream ss; + ss << "Only clients with protocol " << CLIENT_VERSION_STR << " allowed!"; + disconnectClient(ss.str()); + return; + } + + if (g_game.getGameState() == GAME_STATE_STARTUP) { + disconnectClient("Gameworld is starting up. Please wait."); + return; + } + + if (g_game.getGameState() == GAME_STATE_MAINTAIN) { + disconnectClient("Gameworld is under maintenance. Please re-connect in a while."); + return; + } + + BanInfo banInfo; + if (IOBan::isIpBanned(getIP(), banInfo)) { + if (banInfo.reason.empty()) { + banInfo.reason = "(none)"; + } + + std::ostringstream ss; + ss << "Your IP has been banned until " << formatDateShort(banInfo.expiresAt) << " by " << banInfo.bannedBy << ".\n\nReason specified:\n" << banInfo.reason; + disconnectClient(ss.str()); + return; + } + + uint32_t accountId = IOLoginData::gameworldAuthentication(accountName, password, characterName, token, tokenTime); + if (accountId == 0) { + disconnectClient("Account name or password is not correct."); + return; + } + + g_dispatcher.addTask(createTask(std::bind(&ProtocolGame::login, getThis(), characterName, accountId, operatingSystem))); +} + +void ProtocolGame::disconnectClient(const std::string& message) const +{ + auto output = OutputMessagePool::getOutputMessage(); + output->addByte(0x14); + output->addString(message); + send(output); + disconnect(); +} + +void ProtocolGame::writeToOutputBuffer(const NetworkMessage& msg, bool broadcast /*= true*/) +{ + if (!broadcast && isLiveCaster()) { + //We're casting and we need to send a packet that's not supposed to be broadcast so we need a new messasge. + //This shouldn't impact performance by a huge amount as most packets can be broadcast. + auto out = OutputMessagePool::getOutputMessage(); + out->append(msg); + send(std::move(out)); + } else { + auto out = getOutputBuffer(msg.getLength()); + if (isLiveCaster()) { + out->setBroadcastMsg(true); + } + out->append(msg); + } +} + +void ProtocolGame::parsePacket(NetworkMessage& msg) +{ + if (!acceptPackets || g_game.getGameState() == GAME_STATE_SHUTDOWN || msg.getLength() <= 0) { + return; + } + + uint8_t recvbyte = msg.getByte(); + + //a dead player can not perform actions + if (!player || player->isRemoved() || player->getHealth() <= 0) { + auto this_ptr = getThis(); + g_dispatcher.addTask(createTask([this_ptr]() {this_ptr->stopLiveCast();})); + + if (recvbyte == 0x0F) { + disconnect(); + return; + } + + if (recvbyte != 0x14) { + return; + } + } + + switch (recvbyte) { + case 0x14: g_dispatcher.addTask(createTask(std::bind(&ProtocolGame::logout, getThis(), true, false))); break; + case 0x1D: addGameTask(&Game::playerReceivePingBack, player->getID()); break; + case 0x1E: addGameTask(&Game::playerReceivePing, player->getID()); break; + case 0x32: parseExtendedOpcode(msg); break; //otclient extended opcode + case 0x64: parseAutoWalk(msg); break; + case 0x65: addGameTask(&Game::playerMove, player->getID(), DIRECTION_NORTH); break; + case 0x66: addGameTask(&Game::playerMove, player->getID(), DIRECTION_EAST); break; + case 0x67: addGameTask(&Game::playerMove, player->getID(), DIRECTION_SOUTH); break; + case 0x68: addGameTask(&Game::playerMove, player->getID(), DIRECTION_WEST); break; + case 0x69: addGameTask(&Game::playerStopAutoWalk, player->getID()); break; + case 0x6A: addGameTask(&Game::playerMove, player->getID(), DIRECTION_NORTHEAST); break; + case 0x6B: addGameTask(&Game::playerMove, player->getID(), DIRECTION_SOUTHEAST); break; + case 0x6C: addGameTask(&Game::playerMove, player->getID(), DIRECTION_SOUTHWEST); break; + case 0x6D: addGameTask(&Game::playerMove, player->getID(), DIRECTION_NORTHWEST); break; + case 0x6F: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_NORTH); break; + case 0x70: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_EAST); break; + case 0x71: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_SOUTH); break; + case 0x72: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerTurn, player->getID(), DIRECTION_WEST); break; + case 0x77: parseEquipObject(msg); break; + case 0x78: parseThrow(msg); break; + case 0x79: parseLookInShop(msg); break; + case 0x7A: parsePlayerPurchase(msg); break; + case 0x7B: parsePlayerSale(msg); break; + case 0x7C: addGameTask(&Game::playerCloseShop, player->getID()); break; + case 0x7D: parseRequestTrade(msg); break; + case 0x7E: parseLookInTrade(msg); break; + case 0x7F: addGameTask(&Game::playerAcceptTrade, player->getID()); break; + case 0x80: addGameTask(&Game::playerCloseTrade, player->getID()); break; + case 0x82: parseUseItem(msg); break; + case 0x83: parseUseItemEx(msg); break; + case 0x84: parseUseWithCreature(msg); break; + case 0x85: parseRotateItem(msg); break; + case 0x87: parseCloseContainer(msg); break; + case 0x88: parseUpArrowContainer(msg); break; + case 0x89: parseTextWindow(msg); break; + case 0x8A: parseHouseWindow(msg); break; + case 0x8B: parseWrapItem(msg); break; + case 0x8C: parseLookAt(msg); break; + case 0x8D: parseLookInBattleList(msg); break; + case 0x8E: /* join aggression */ break; + case 0x96: parseSay(msg); break; + case 0x97: addGameTask(&Game::playerRequestChannels, player->getID()); break; + case 0x98: parseOpenChannel(msg); break; + case 0x99: parseCloseChannel(msg); break; + case 0x9A: parseOpenPrivateChannel(msg); break; + case 0x9E: addGameTask(&Game::playerCloseNpcChannel, player->getID()); break; + case 0xA0: parseFightModes(msg); break; + case 0xA1: parseAttack(msg); break; + case 0xA2: parseFollow(msg); break; + case 0xA3: parseInviteToParty(msg); break; + case 0xA4: parseJoinParty(msg); break; + case 0xA5: parseRevokePartyInvite(msg); break; + case 0xA6: parsePassPartyLeadership(msg); break; + case 0xA7: addGameTask(&Game::playerLeaveParty, player->getID()); break; + case 0xA8: parseEnableSharedPartyExperience(msg); break; + case 0xAA: addGameTask(&Game::playerCreatePrivateChannel, player->getID()); break; + case 0xAB: parseChannelInvite(msg); break; + case 0xAC: parseChannelExclude(msg); break; + case 0xBE: addGameTask(&Game::playerCancelAttackAndFollow, player->getID()); break; + case 0xC9: /* update tile */ break; + case 0xCA: parseUpdateContainer(msg); break; + case 0xCB: parseBrowseField(msg); break; + case 0xCC: parseSeekInContainer(msg); break; + case 0xD2: addGameTask(&Game::playerRequestOutfit, player->getID()); break; + case 0xD3: parseSetOutfit(msg); break; + case 0xD4: parseToggleMount(msg); break; + case 0xDC: parseAddVip(msg); break; + case 0xDD: parseRemoveVip(msg); break; + case 0xDE: parseEditVip(msg); break; + case 0xE6: parseBugReport(msg); break; + case 0xE7: /* thank you */ break; + case 0xE8: parseDebugAssert(msg); break; + case 0xF0: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerShowQuestLog, player->getID()); break; + case 0xF1: parseQuestLine(msg); break; + case 0xF2: parseRuleViolationReport(msg); break; + case 0xF3: /* get object info */ break; + case 0xF4: parseMarketLeave(); break; + case 0xF5: parseMarketBrowse(msg); break; + case 0xF6: parseMarketCreateOffer(msg); break; + case 0xF7: parseMarketCancelOffer(msg); break; + case 0xF8: parseMarketAcceptOffer(msg); break; + case 0xF9: parseModalWindowAnswer(msg); break; + + default: + // std::cout << "Player: " << player->getName() << " sent an unknown packet header: 0x" << std::hex << static_cast(recvbyte) << std::dec << "!" << std::endl; + break; + } + + if (msg.isOverrun()) { + disconnect(); + } +} + +// Parse methods +void ProtocolGame::parseChannelInvite(NetworkMessage& msg) +{ + const std::string name = msg.getString(); + addGameTask(&Game::playerChannelInvite, player->getID(), name); +} + +void ProtocolGame::parseChannelExclude(NetworkMessage& msg) +{ + const std::string name = msg.getString(); + addGameTask(&Game::playerChannelExclude, player->getID(), name); +} + +void ProtocolGame::parseOpenChannel(NetworkMessage& msg) +{ + uint16_t channelId = msg.get(); + addGameTask(&Game::playerOpenChannel, player->getID(), channelId); +} + +void ProtocolGame::parseCloseChannel(NetworkMessage& msg) +{ + uint16_t channelId = msg.get(); + addGameTask(&Game::playerCloseChannel, player->getID(), channelId); +} + +void ProtocolGame::parseOpenPrivateChannel(NetworkMessage& msg) +{ + const std::string receiver = msg.getString(); + addGameTask(&Game::playerOpenPrivateChannel, player->getID(), receiver); +} + +void ProtocolGame::parseAutoWalk(NetworkMessage& msg) +{ + uint8_t numdirs = msg.getByte(); + if (numdirs == 0 || (msg.getBufferPosition() + numdirs) != (msg.getLength() + 8)) { + return; + } + + msg.skipBytes(numdirs); + + std::forward_list path; + for (uint8_t i = 0; i < numdirs; ++i) { + uint8_t rawdir = msg.getPreviousByte(); + switch (rawdir) { + case 1: path.push_front(DIRECTION_EAST); break; + case 2: path.push_front(DIRECTION_NORTHEAST); break; + case 3: path.push_front(DIRECTION_NORTH); break; + case 4: path.push_front(DIRECTION_NORTHWEST); break; + case 5: path.push_front(DIRECTION_WEST); break; + case 6: path.push_front(DIRECTION_SOUTHWEST); break; + case 7: path.push_front(DIRECTION_SOUTH); break; + case 8: path.push_front(DIRECTION_SOUTHEAST); break; + default: break; + } + } + + if (path.empty()) { + return; + } + + addGameTask(&Game::playerAutoWalk, player->getID(), path); +} + +void ProtocolGame::parseSetOutfit(NetworkMessage& msg) +{ + Outfit_t newOutfit; + newOutfit.lookType = msg.get(); + newOutfit.lookHead = msg.getByte(); + newOutfit.lookBody = msg.getByte(); + newOutfit.lookLegs = msg.getByte(); + newOutfit.lookFeet = msg.getByte(); + newOutfit.lookAddons = msg.getByte(); + newOutfit.lookMount = msg.get(); + addGameTask(&Game::playerChangeOutfit, player->getID(), newOutfit); +} + +void ProtocolGame::parseToggleMount(NetworkMessage& msg) +{ + bool mount = msg.getByte() != 0; + addGameTask(&Game::playerToggleMount, player->getID(), mount); +} + +void ProtocolGame::parseUseItem(NetworkMessage& msg) +{ + Position pos = msg.getPosition(); + uint16_t spriteId = msg.get(); + uint8_t stackpos = msg.getByte(); + uint8_t index = msg.getByte(); + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerUseItem, player->getID(), pos, stackpos, index, spriteId); +} + +void ProtocolGame::parseUseItemEx(NetworkMessage& msg) +{ + Position fromPos = msg.getPosition(); + uint16_t fromSpriteId = msg.get(); + uint8_t fromStackPos = msg.getByte(); + Position toPos = msg.getPosition(); + uint16_t toSpriteId = msg.get(); + uint8_t toStackPos = msg.getByte(); + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerUseItemEx, player->getID(), fromPos, fromStackPos, fromSpriteId, toPos, toStackPos, toSpriteId); +} + +void ProtocolGame::parseUseWithCreature(NetworkMessage& msg) +{ + Position fromPos = msg.getPosition(); + uint16_t spriteId = msg.get(); + uint8_t fromStackPos = msg.getByte(); + uint32_t creatureId = msg.get(); + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerUseWithCreature, player->getID(), fromPos, fromStackPos, creatureId, spriteId); +} + +void ProtocolGame::parseCloseContainer(NetworkMessage& msg) +{ + uint8_t cid = msg.getByte(); + addGameTask(&Game::playerCloseContainer, player->getID(), cid); +} + +void ProtocolGame::parseUpArrowContainer(NetworkMessage& msg) +{ + uint8_t cid = msg.getByte(); + addGameTask(&Game::playerMoveUpContainer, player->getID(), cid); +} + +void ProtocolGame::parseUpdateContainer(NetworkMessage& msg) +{ + uint8_t cid = msg.getByte(); + addGameTask(&Game::playerUpdateContainer, player->getID(), cid); +} + +void ProtocolGame::parseThrow(NetworkMessage& msg) +{ + Position fromPos = msg.getPosition(); + uint16_t spriteId = msg.get(); + uint8_t fromStackpos = msg.getByte(); + Position toPos = msg.getPosition(); + uint8_t count = msg.getByte(); + + if (toPos != fromPos) { + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerMoveThing, player->getID(), fromPos, spriteId, fromStackpos, toPos, count); + } +} + +void ProtocolGame::parseLookAt(NetworkMessage& msg) +{ + Position pos = msg.getPosition(); + msg.skipBytes(2); // spriteId + uint8_t stackpos = msg.getByte(); + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerLookAt, player->getID(), pos, stackpos); +} + +void ProtocolGame::parseLookInBattleList(NetworkMessage& msg) +{ + uint32_t creatureId = msg.get(); + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerLookInBattleList, player->getID(), creatureId); +} + +void ProtocolGame::parseSay(NetworkMessage& msg) +{ + std::string receiver; + uint16_t channelId; + + SpeakClasses type = static_cast(msg.getByte()); + switch (type) { + case TALKTYPE_PRIVATE_TO: + case TALKTYPE_PRIVATE_RED_TO: + receiver = msg.getString(); + channelId = 0; + break; + + case TALKTYPE_CHANNEL_Y: + case TALKTYPE_CHANNEL_R1: + channelId = msg.get(); + break; + + default: + channelId = 0; + break; + } + + const std::string text = msg.getString(); + if (text.length() > 255) { + return; + } + + addGameTask(&Game::playerSay, player->getID(), channelId, type, receiver, text); +} + +void ProtocolGame::parseFightModes(NetworkMessage& msg) +{ + uint8_t rawFightMode = msg.getByte(); // 1 - offensive, 2 - balanced, 3 - defensive + uint8_t rawChaseMode = msg.getByte(); // 0 - stand while fightning, 1 - chase opponent + uint8_t rawSecureMode = msg.getByte(); // 0 - can't attack unmarked, 1 - can attack unmarked + // uint8_t rawPvpMode = msg.getByte(); // pvp mode introduced in 10.0 + + fightMode_t fightMode; + if (rawFightMode == 1) { + fightMode = FIGHTMODE_ATTACK; + } else if (rawFightMode == 2) { + fightMode = FIGHTMODE_BALANCED; + } else { + fightMode = FIGHTMODE_DEFENSE; + } + + addGameTask(&Game::playerSetFightModes, player->getID(), fightMode, rawChaseMode != 0, rawSecureMode != 0); +} + +void ProtocolGame::parseAttack(NetworkMessage& msg) +{ + uint32_t creatureId = msg.get(); + // msg.get(); creatureId (same as above) + addGameTask(&Game::playerSetAttackedCreature, player->getID(), creatureId); +} + +void ProtocolGame::parseFollow(NetworkMessage& msg) +{ + uint32_t creatureId = msg.get(); + // msg.get(); creatureId (same as above) + addGameTask(&Game::playerFollowCreature, player->getID(), creatureId); +} + +void ProtocolGame::parseEquipObject(NetworkMessage& msg) +{ + uint16_t spriteId = msg.get(); + // msg.get(); + + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerEquipItem, player->getID(), spriteId); +} + +void ProtocolGame::parseTextWindow(NetworkMessage& msg) +{ + uint32_t windowTextId = msg.get(); + const std::string newText = msg.getString(); + addGameTask(&Game::playerWriteItem, player->getID(), windowTextId, newText); +} + +void ProtocolGame::parseHouseWindow(NetworkMessage& msg) +{ + uint8_t doorId = msg.getByte(); + uint32_t id = msg.get(); + const std::string text = msg.getString(); + addGameTask(&Game::playerUpdateHouseWindow, player->getID(), doorId, id, text); +} + +void ProtocolGame::parseWrapItem(NetworkMessage& msg) +{ + Position pos = msg.getPosition(); + uint16_t spriteId = msg.get(); + uint8_t stackpos = msg.getByte(); + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerWrapItem, player->getID(), pos, stackpos, spriteId); +} + +void ProtocolGame::parseLookInShop(NetworkMessage& msg) +{ + uint16_t id = msg.get(); + uint8_t count = msg.getByte(); + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerLookInShop, player->getID(), id, count); +} + +void ProtocolGame::parsePlayerPurchase(NetworkMessage& msg) +{ + uint16_t id = msg.get(); + uint8_t count = msg.getByte(); + uint8_t amount = msg.getByte(); + bool ignoreCap = msg.getByte() != 0; + bool inBackpacks = msg.getByte() != 0; + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerPurchaseItem, player->getID(), id, count, amount, ignoreCap, inBackpacks); +} + +void ProtocolGame::parsePlayerSale(NetworkMessage& msg) +{ + uint16_t id = msg.get(); + uint8_t count = msg.getByte(); + uint8_t amount = msg.getByte(); + bool ignoreEquipped = msg.getByte() != 0; + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerSellItem, player->getID(), id, count, amount, ignoreEquipped); +} + +void ProtocolGame::parseRequestTrade(NetworkMessage& msg) +{ + Position pos = msg.getPosition(); + uint16_t spriteId = msg.get(); + uint8_t stackpos = msg.getByte(); + uint32_t playerId = msg.get(); + addGameTask(&Game::playerRequestTrade, player->getID(), pos, stackpos, playerId, spriteId); +} + +void ProtocolGame::parseLookInTrade(NetworkMessage& msg) +{ + bool counterOffer = (msg.getByte() == 0x01); + uint8_t index = msg.getByte(); + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerLookInTrade, player->getID(), counterOffer, index); +} + +void ProtocolGame::parseAddVip(NetworkMessage& msg) +{ + const std::string name = msg.getString(); + addGameTask(&Game::playerRequestAddVip, player->getID(), name); +} + +void ProtocolGame::parseRemoveVip(NetworkMessage& msg) +{ + uint32_t guid = msg.get(); + addGameTask(&Game::playerRequestRemoveVip, player->getID(), guid); +} + +void ProtocolGame::parseEditVip(NetworkMessage& msg) +{ + uint32_t guid = msg.get(); + const std::string description = msg.getString(); + uint32_t icon = std::min(10, msg.get()); // 10 is max icon in 9.63 + bool notify = msg.getByte() != 0; + addGameTask(&Game::playerRequestEditVip, player->getID(), guid, description, icon, notify); +} + +void ProtocolGame::parseRotateItem(NetworkMessage& msg) +{ + Position pos = msg.getPosition(); + uint16_t spriteId = msg.get(); + uint8_t stackpos = msg.getByte(); + addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerRotateItem, player->getID(), pos, stackpos, spriteId); +} + +void ProtocolGame::parseRuleViolationReport(NetworkMessage& msg) +{ + uint8_t reportType = msg.getByte(); + uint8_t reportReason = msg.getByte(); + const std::string& targetName = msg.getString(); + const std::string& comment = msg.getString(); + std::string translation; + if (reportType == REPORT_TYPE_NAME) { + translation = msg.getString(); + } else if (reportType == REPORT_TYPE_STATEMENT) { + translation = msg.getString(); + msg.get(); // statement id, used to get whatever player have said, we don't log that. + } + + addGameTask(&Game::playerReportRuleViolation, player->getID(), targetName, reportType, reportReason, comment, translation); +} + +void ProtocolGame::parseBugReport(NetworkMessage& msg) +{ + uint8_t category = msg.getByte(); + std::string message = msg.getString(); + + Position position; + if (category == BUG_CATEGORY_MAP) { + position = msg.getPosition(); + } + + addGameTask(&Game::playerReportBug, player->getID(), message, position, category); +} + +void ProtocolGame::parseDebugAssert(NetworkMessage& msg) +{ + if (debugAssertSent) { + return; + } + + debugAssertSent = true; + + std::string assertLine = msg.getString(); + std::string date = msg.getString(); + std::string description = msg.getString(); + std::string comment = msg.getString(); + addGameTask(&Game::playerDebugAssert, player->getID(), assertLine, date, description, comment); +} + +void ProtocolGame::parseInviteToParty(NetworkMessage& msg) +{ + uint32_t targetId = msg.get(); + addGameTask(&Game::playerInviteToParty, player->getID(), targetId); +} + +void ProtocolGame::parseJoinParty(NetworkMessage& msg) +{ + uint32_t targetId = msg.get(); + addGameTask(&Game::playerJoinParty, player->getID(), targetId); +} + +void ProtocolGame::parseRevokePartyInvite(NetworkMessage& msg) +{ + uint32_t targetId = msg.get(); + addGameTask(&Game::playerRevokePartyInvitation, player->getID(), targetId); +} + +void ProtocolGame::parsePassPartyLeadership(NetworkMessage& msg) +{ + uint32_t targetId = msg.get(); + addGameTask(&Game::playerPassPartyLeadership, player->getID(), targetId); +} + +void ProtocolGame::parseEnableSharedPartyExperience(NetworkMessage& msg) +{ + bool sharedExpActive = msg.getByte() == 1; + addGameTask(&Game::playerEnableSharedPartyExperience, player->getID(), sharedExpActive); +} + +void ProtocolGame::parseQuestLine(NetworkMessage& msg) +{ + uint16_t questId = msg.get(); + addGameTask(&Game::playerShowQuestLine, player->getID(), questId); +} + +void ProtocolGame::parseMarketLeave() +{ + addGameTask(&Game::playerLeaveMarket, player->getID()); +} + +void ProtocolGame::parseMarketBrowse(NetworkMessage& msg) +{ + uint16_t browseId = msg.get(); + + if (browseId == MARKETREQUEST_OWN_OFFERS) { + addGameTask(&Game::playerBrowseMarketOwnOffers, player->getID()); + } else if (browseId == MARKETREQUEST_OWN_HISTORY) { + addGameTask(&Game::playerBrowseMarketOwnHistory, player->getID()); + } else { + addGameTask(&Game::playerBrowseMarket, player->getID(), browseId); + } +} + +void ProtocolGame::parseMarketCreateOffer(NetworkMessage& msg) +{ + uint8_t type = msg.getByte(); + uint16_t spriteId = msg.get(); + uint16_t amount = msg.get(); + uint32_t price = msg.get(); + bool anonymous = (msg.getByte() != 0); + addGameTask(&Game::playerCreateMarketOffer, player->getID(), type, spriteId, amount, price, anonymous); +} + +void ProtocolGame::parseMarketCancelOffer(NetworkMessage& msg) +{ + uint32_t timestamp = msg.get(); + uint16_t counter = msg.get(); + addGameTask(&Game::playerCancelMarketOffer, player->getID(), timestamp, counter); +} + +void ProtocolGame::parseMarketAcceptOffer(NetworkMessage& msg) +{ + uint32_t timestamp = msg.get(); + uint16_t counter = msg.get(); + uint16_t amount = msg.get(); + addGameTask(&Game::playerAcceptMarketOffer, player->getID(), timestamp, counter, amount); +} + +void ProtocolGame::parseModalWindowAnswer(NetworkMessage& msg) +{ + uint32_t id = msg.get(); + uint8_t button = msg.getByte(); + uint8_t choice = msg.getByte(); + addGameTask(&Game::playerAnswerModalWindow, player->getID(), id, button, choice); +} + +void ProtocolGame::parseBrowseField(NetworkMessage& msg) +{ + const Position& pos = msg.getPosition(); + addGameTask(&Game::playerBrowseField, player->getID(), pos); +} + +void ProtocolGame::parseSeekInContainer(NetworkMessage& msg) +{ + uint8_t containerId = msg.getByte(); + uint16_t index = msg.get(); + addGameTask(&Game::playerSeekInContainer, player->getID(), containerId, index); +} + +// Send methods +void ProtocolGame::sendOpenPrivateChannel(const std::string& receiver) +{ + NetworkMessage msg; + msg.addByte(0xAD); + msg.addString(receiver); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendChannelEvent(uint16_t channelId, const std::string& playerName, ChannelEvent_t channelEvent) +{ + NetworkMessage msg; + msg.addByte(0xF3); + msg.add(channelId); + msg.addString(playerName); + msg.addByte(channelEvent); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCreatureOutfit(const Creature* creature, const Outfit_t& outfit) +{ + if (!canSee(creature)) { + return; + } + + NetworkMessage msg; + msg.addByte(0x8E); + msg.add(creature->getID()); + AddOutfit(msg, outfit); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCreatureWalkthrough(const Creature* creature, bool walkthrough) +{ + if (!canSee(creature)) { + return; + } + + NetworkMessage msg; + msg.addByte(0x92); + msg.add(creature->getID()); + msg.addByte(walkthrough ? 0x00 : 0x01); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCreatureShield(const Creature* creature) +{ + if (!canSee(creature)) { + return; + } + + NetworkMessage msg; + msg.addByte(0x91); + msg.add(creature->getID()); + msg.addByte(player->getPartyShield(creature->getPlayer())); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCreatureSkull(const Creature* creature) +{ + if (g_game.getWorldType() != WORLD_TYPE_PVP) { + return; + } + + if (!canSee(creature)) { + return; + } + + NetworkMessage msg; + msg.addByte(0x90); + msg.add(creature->getID()); + msg.addByte(player->getSkullClient(creature)); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCreatureType(uint32_t creatureId, uint8_t creatureType) +{ + NetworkMessage msg; + msg.addByte(0x95); + msg.add(creatureId); + msg.addByte(creatureType); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCreatureHelpers(uint32_t creatureId, uint16_t helpers) +{ + NetworkMessage msg; + msg.addByte(0x94); + msg.add(creatureId); + msg.add(helpers); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCreatureSquare(const Creature* creature, SquareColor_t color) +{ + if (!canSee(creature)) { + return; + } + + NetworkMessage msg; + msg.addByte(0x93); + msg.add(creature->getID()); + msg.addByte(0x01); + msg.addByte(color); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendTutorial(uint8_t tutorialId) +{ + NetworkMessage msg; + msg.addByte(0xDC); + msg.addByte(tutorialId); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendAddMarker(const Position& pos, uint8_t markType, const std::string& desc) +{ + NetworkMessage msg; + msg.addByte(0xDD); + msg.addPosition(pos); + msg.addByte(markType); + msg.addString(desc); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendReLoginWindow(uint8_t unfairFightReduction) +{ + NetworkMessage msg; + msg.addByte(0x28); + msg.addByte(0x00); + msg.addByte(unfairFightReduction); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendTextMessage(const TextMessage& message) +{ + NetworkMessage msg; + msg.addByte(0xB4); + msg.addByte(message.type); + switch (message.type) { + case MESSAGE_DAMAGE_DEALT: + case MESSAGE_DAMAGE_RECEIVED: + case MESSAGE_DAMAGE_OTHERS: { + msg.addPosition(message.position); + msg.add(message.primary.value); + msg.addByte(message.primary.color); + msg.add(message.secondary.value); + msg.addByte(message.secondary.color); + break; + } + case MESSAGE_HEALED: + case MESSAGE_HEALED_OTHERS: + case MESSAGE_EXPERIENCE: + case MESSAGE_EXPERIENCE_OTHERS: { + msg.addPosition(message.position); + msg.add(message.primary.value); + msg.addByte(message.primary.color); + break; + } + case MESSAGE_GUILD: + case MESSAGE_PARTY_MANAGEMENT: + case MESSAGE_PARTY: + msg.add(message.channelId); + break; + default: { + break; + } + } + msg.addString(message.text); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendClosePrivate(uint16_t channelId) +{ + NetworkMessage msg; + msg.addByte(0xB3); + msg.add(channelId); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCreatePrivateChannel(uint16_t channelId, const std::string& channelName) +{ + NetworkMessage msg; + msg.addByte(0xB2); + msg.add(channelId); + msg.addString(channelName); + msg.add(0x01); + msg.addString(player->getName()); + msg.add(0x00); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendChannelsDialog() +{ + NetworkMessage msg; + msg.addByte(0xAB); + + const ChannelList& list = g_chat->getChannelList(*player); + msg.addByte(list.size()); + for (ChatChannel* channel : list) { + msg.add(channel->getId()); + msg.addString(channel->getName()); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendChannelMessage(const std::string& author, const std::string& text, SpeakClasses type, uint16_t channel) +{ + NetworkMessage msg; + msg.addByte(0xAA); + msg.add(0x00); + msg.addString(author); + msg.add(0x00); + msg.addByte(type); + msg.add(channel); + msg.addString(text); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendIcons(uint16_t icons) +{ + NetworkMessage msg; + msg.addByte(0xA2); + msg.add(icons); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendShop(Npc* npc, const ShopInfoList& itemList) +{ + NetworkMessage msg; + msg.addByte(0x7A); + msg.addString(npc->getName()); + + uint16_t itemsToSend = std::min(itemList.size(), std::numeric_limits::max()); + msg.add(itemsToSend); + + uint16_t i = 0; + for (auto it = itemList.begin(); i < itemsToSend; ++it, ++i) { + AddShopItem(msg, *it); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCloseShop() +{ + NetworkMessage msg; + msg.addByte(0x7C); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendSaleItemList(const std::list& shop) +{ + NetworkMessage msg; + msg.addByte(0x7B); + msg.add(player->getMoney() + player->getBankBalance()); + + std::map saleMap; + + if (shop.size() <= 5) { + // For very small shops it's not worth it to create the complete map + for (const ShopInfo& shopInfo : shop) { + if (shopInfo.sellPrice == 0) { + continue; + } + + int8_t subtype = -1; + + const ItemType& itemType = Item::items[shopInfo.itemId]; + if (itemType.hasSubType() && !itemType.stackable) { + subtype = (shopInfo.subType == 0 ? -1 : shopInfo.subType); + } + + uint32_t count = player->getItemTypeCount(shopInfo.itemId, subtype); + if (count > 0) { + saleMap[shopInfo.itemId] = count; + } + } + } else { + // Large shop, it's better to get a cached map of all item counts and use it + // We need a temporary map since the finished map should only contain items + // available in the shop + std::map tempSaleMap; + player->getAllItemTypeCount(tempSaleMap); + + // We must still check manually for the special items that require subtype matches + // (That is, fluids such as potions etc., actually these items are very few since + // health potions now use their own ID) + for (const ShopInfo& shopInfo : shop) { + if (shopInfo.sellPrice == 0) { + continue; + } + + int8_t subtype = -1; + + const ItemType& itemType = Item::items[shopInfo.itemId]; + if (itemType.hasSubType() && !itemType.stackable) { + subtype = (shopInfo.subType == 0 ? -1 : shopInfo.subType); + } + + if (subtype != -1) { + uint32_t count; + if (!itemType.isFluidContainer() && !itemType.isSplash()) { + count = player->getItemTypeCount(shopInfo.itemId, subtype); // This shop item requires extra checks + } else { + count = subtype; + } + + if (count > 0) { + saleMap[shopInfo.itemId] = count; + } + } else { + std::map::const_iterator findIt = tempSaleMap.find(shopInfo.itemId); + if (findIt != tempSaleMap.end() && findIt->second > 0) { + saleMap[shopInfo.itemId] = findIt->second; + } + } + } + } + + uint8_t itemsToSend = std::min(saleMap.size(), std::numeric_limits::max()); + msg.addByte(itemsToSend); + + uint8_t i = 0; + for (std::map::const_iterator it = saleMap.begin(); i < itemsToSend; ++it, ++i) { + msg.addItemId(it->first); + msg.addByte(std::min(it->second, std::numeric_limits::max())); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendMarketEnter(uint32_t depotId) +{ + NetworkMessage msg; + msg.addByte(0xF6); + + msg.add(player->getBankBalance()); + msg.addByte(std::min(IOMarket::getPlayerOfferCount(player->getGUID()), std::numeric_limits::max())); + + DepotChest* depotChest = player->getDepotChest(depotId, false); + if (!depotChest) { + msg.add(0x00); + writeToOutputBuffer(msg); + return; + } + + player->setInMarket(true); + + std::map depotItems; + std::forward_list containerList { depotChest, player->getInbox() }; + + do { + Container* container = containerList.front(); + containerList.pop_front(); + + for (Item* item : container->getItemList()) { + Container* c = item->getContainer(); + if (c && !c->empty()) { + containerList.push_front(c); + continue; + } + + const ItemType& itemType = Item::items[item->getID()]; + if (itemType.wareId == 0) { + continue; + } + + if (c && (!itemType.isContainer() || c->capacity() != itemType.maxItems)) { + continue; + } + + if (!item->hasMarketAttributes()) { + continue; + } + + depotItems[itemType.wareId] += Item::countByType(item, -1); + } + } while (!containerList.empty()); + + uint16_t itemsToSend = std::min(depotItems.size(), std::numeric_limits::max()); + msg.add(itemsToSend); + + uint16_t i = 0; + for (std::map::const_iterator it = depotItems.begin(); i < itemsToSend; ++it, ++i) { + msg.add(it->first); + msg.add(std::min(0xFFFF, it->second)); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendMarketLeave() +{ + NetworkMessage msg; + msg.addByte(0xF7); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendMarketBrowseItem(uint16_t itemId, const MarketOfferList& buyOffers, const MarketOfferList& sellOffers) +{ + NetworkMessage msg; + + msg.addByte(0xF9); + msg.addItemId(itemId); + + msg.add(buyOffers.size()); + for (const MarketOffer& offer : buyOffers) { + msg.add(offer.timestamp); + msg.add(offer.counter); + msg.add(offer.amount); + msg.add(offer.price); + msg.addString(offer.playerName); + } + + msg.add(sellOffers.size()); + for (const MarketOffer& offer : sellOffers) { + msg.add(offer.timestamp); + msg.add(offer.counter); + msg.add(offer.amount); + msg.add(offer.price); + msg.addString(offer.playerName); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendMarketAcceptOffer(const MarketOfferEx& offer) +{ + NetworkMessage msg; + msg.addByte(0xF9); + msg.addItemId(offer.itemId); + + if (offer.type == MARKETACTION_BUY) { + msg.add(0x01); + msg.add(offer.timestamp); + msg.add(offer.counter); + msg.add(offer.amount); + msg.add(offer.price); + msg.addString(offer.playerName); + msg.add(0x00); + } else { + msg.add(0x00); + msg.add(0x01); + msg.add(offer.timestamp); + msg.add(offer.counter); + msg.add(offer.amount); + msg.add(offer.price); + msg.addString(offer.playerName); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendMarketBrowseOwnOffers(const MarketOfferList& buyOffers, const MarketOfferList& sellOffers) +{ + NetworkMessage msg; + msg.addByte(0xF9); + msg.add(MARKETREQUEST_OWN_OFFERS); + + msg.add(buyOffers.size()); + for (const MarketOffer& offer : buyOffers) { + msg.add(offer.timestamp); + msg.add(offer.counter); + msg.addItemId(offer.itemId); + msg.add(offer.amount); + msg.add(offer.price); + } + + msg.add(sellOffers.size()); + for (const MarketOffer& offer : sellOffers) { + msg.add(offer.timestamp); + msg.add(offer.counter); + msg.addItemId(offer.itemId); + msg.add(offer.amount); + msg.add(offer.price); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendMarketCancelOffer(const MarketOfferEx& offer) +{ + NetworkMessage msg; + msg.addByte(0xF9); + msg.add(MARKETREQUEST_OWN_OFFERS); + + if (offer.type == MARKETACTION_BUY) { + msg.add(0x01); + msg.add(offer.timestamp); + msg.add(offer.counter); + msg.addItemId(offer.itemId); + msg.add(offer.amount); + msg.add(offer.price); + msg.add(0x00); + } else { + msg.add(0x00); + msg.add(0x01); + msg.add(offer.timestamp); + msg.add(offer.counter); + msg.addItemId(offer.itemId); + msg.add(offer.amount); + msg.add(offer.price); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendMarketBrowseOwnHistory(const HistoryMarketOfferList& buyOffers, const HistoryMarketOfferList& sellOffers) +{ + uint32_t i = 0; + std::map counterMap; + uint32_t buyOffersToSend = std::min(buyOffers.size(), 810 + std::max(0, 810 - sellOffers.size())); + uint32_t sellOffersToSend = std::min(sellOffers.size(), 810 + std::max(0, 810 - buyOffers.size())); + + NetworkMessage msg; + msg.addByte(0xF9); + msg.add(MARKETREQUEST_OWN_HISTORY); + + msg.add(buyOffersToSend); + for (auto it = buyOffers.begin(); i < buyOffersToSend; ++it, ++i) { + msg.add(it->timestamp); + msg.add(counterMap[it->timestamp]++); + msg.addItemId(it->itemId); + msg.add(it->amount); + msg.add(it->price); + msg.addByte(it->state); + } + + counterMap.clear(); + i = 0; + + msg.add(sellOffersToSend); + for (auto it = sellOffers.begin(); i < sellOffersToSend; ++it, ++i) { + msg.add(it->timestamp); + msg.add(counterMap[it->timestamp]++); + msg.addItemId(it->itemId); + msg.add(it->amount); + msg.add(it->price); + msg.addByte(it->state); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendMarketDetail(uint16_t itemId) +{ + NetworkMessage msg; + msg.addByte(0xF8); + msg.addItemId(itemId); + + const ItemType& it = Item::items[itemId]; + if (it.armor != 0) { + msg.addString(std::to_string(it.armor)); + } else { + msg.add(0x00); + } + + if (it.attack != 0) { + // TODO: chance to hit, range + // example: + // "attack +x, chance to hit +y%, z fields" + if (it.abilities && it.abilities->elementType != COMBAT_NONE && it.abilities->elementDamage != 0) { + std::ostringstream ss; + ss << it.attack << " physical +" << it.abilities->elementDamage << ' ' << getCombatName(it.abilities->elementType); + msg.addString(ss.str()); + } else { + msg.addString(std::to_string(it.attack)); + } + } else { + msg.add(0x00); + } + + if (it.isContainer()) { + msg.addString(std::to_string(it.maxItems)); + } else { + msg.add(0x00); + } + + if (it.defense != 0) { + if (it.extraDefense != 0) { + std::ostringstream ss; + ss << it.defense << ' ' << std::showpos << it.extraDefense << std::noshowpos; + msg.addString(ss.str()); + } else { + msg.addString(std::to_string(it.defense)); + } + } else { + msg.add(0x00); + } + + if (!it.description.empty()) { + const std::string& descr = it.description; + if (descr.back() == '.') { + msg.addString(std::string(descr, 0, descr.length() - 1)); + } else { + msg.addString(descr); + } + } else { + msg.add(0x00); + } + + if (it.decayTime != 0) { + std::ostringstream ss; + ss << it.decayTime << " seconds"; + msg.addString(ss.str()); + } else { + msg.add(0x00); + } + + if (it.abilities) { + std::ostringstream ss; + bool separator = false; + + for (size_t i = 0; i < COMBAT_COUNT; ++i) { + if (it.abilities->absorbPercent[i] == 0) { + continue; + } + + if (separator) { + ss << ", "; + } else { + separator = true; + } + + ss << getCombatName(indexToCombatType(i)) << ' ' << std::showpos << it.abilities->absorbPercent[i] << std::noshowpos << '%'; + } + + msg.addString(ss.str()); + } else { + msg.add(0x00); + } + + if (it.minReqLevel != 0) { + msg.addString(std::to_string(it.minReqLevel)); + } else { + msg.add(0x00); + } + + if (it.minReqMagicLevel != 0) { + msg.addString(std::to_string(it.minReqMagicLevel)); + } else { + msg.add(0x00); + } + + msg.addString(it.vocationString); + + msg.addString(it.runeSpellName); + + if (it.abilities) { + std::ostringstream ss; + bool separator = false; + + for (uint8_t i = SKILL_FIRST; i <= SKILL_LAST; i++) { + if (!it.abilities->skills[i]) { + continue; + } + + if (separator) { + ss << ", "; + } else { + separator = true; + } + + ss << getSkillName(i) << ' ' << std::showpos << it.abilities->skills[i] << std::noshowpos; + } + + if (it.abilities->stats[STAT_MAGICPOINTS] != 0) { + if (separator) { + ss << ", "; + } else { + separator = true; + } + + ss << "magic level " << std::showpos << it.abilities->stats[STAT_MAGICPOINTS] << std::noshowpos; + } + + if (it.abilities->speed != 0) { + if (separator) { + ss << ", "; + } + + ss << "speed " << std::showpos << (it.abilities->speed >> 1) << std::noshowpos; + } + + msg.addString(ss.str()); + } else { + msg.add(0x00); + } + + if (it.charges != 0) { + msg.addString(std::to_string(it.charges)); + } else { + msg.add(0x00); + } + + std::string weaponName = getWeaponName(it.weaponType); + + if (it.slotPosition & SLOTP_TWO_HAND) { + if (!weaponName.empty()) { + weaponName += ", two-handed"; + } else { + weaponName = "two-handed"; + } + } + + msg.addString(weaponName); + + if (it.weight != 0) { + std::ostringstream ss; + if (it.weight < 10) { + ss << "0.0" << it.weight; + } else if (it.weight < 100) { + ss << "0." << it.weight; + } else { + std::string weightString = std::to_string(it.weight); + weightString.insert(weightString.end() - 2, '.'); + ss << weightString; + } + ss << " oz"; + msg.addString(ss.str()); + } else { + msg.add(0x00); + } + + MarketStatistics* statistics = IOMarket::getInstance().getPurchaseStatistics(itemId); + if (statistics) { + msg.addByte(0x01); + msg.add(statistics->numTransactions); + msg.add(std::min(std::numeric_limits::max(), statistics->totalPrice)); + msg.add(statistics->highestPrice); + msg.add(statistics->lowestPrice); + } else { + msg.addByte(0x00); + } + + statistics = IOMarket::getInstance().getSaleStatistics(itemId); + if (statistics) { + msg.addByte(0x01); + msg.add(statistics->numTransactions); + msg.add(std::min(std::numeric_limits::max(), statistics->totalPrice)); + msg.add(statistics->highestPrice); + msg.add(statistics->lowestPrice); + } else { + msg.addByte(0x00); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendQuestLog() +{ + NetworkMessage msg; + msg.addByte(0xF0); + msg.add(g_game.quests.getQuestsCount(player)); + + for (const Quest& quest : g_game.quests.getQuests()) { + if (quest.isStarted(player)) { + msg.add(quest.getID()); + msg.addString(quest.getName()); + msg.addByte(quest.isCompleted(player)); + } + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendQuestLine(const Quest* quest) +{ + NetworkMessage msg; + msg.addByte(0xF1); + msg.add(quest->getID()); + msg.addByte(quest->getMissionsCount(player)); + + for (const Mission& mission : quest->getMissions()) { + if (mission.isStarted(player)) { + msg.addString(mission.getName(player)); + msg.addString(mission.getDescription(player)); + } + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendTradeItemRequest(const std::string& traderName, const Item* item, bool ack) +{ + NetworkMessage msg; + + if (ack) { + msg.addByte(0x7D); + } else { + msg.addByte(0x7E); + } + + msg.addString(traderName); + + if (const Container* tradeContainer = item->getContainer()) { + std::list listContainer {tradeContainer}; + std::list itemList {tradeContainer}; + while (!listContainer.empty()) { + const Container* container = listContainer.front(); + listContainer.pop_front(); + + for (Item* containerItem : container->getItemList()) { + Container* tmpContainer = containerItem->getContainer(); + if (tmpContainer) { + listContainer.push_back(tmpContainer); + } + itemList.push_back(containerItem); + } + } + + msg.addByte(itemList.size()); + for (const Item* listItem : itemList) { + msg.addItem(listItem); + } + } else { + msg.addByte(0x01); + msg.addItem(item); + } + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCloseTrade() +{ + NetworkMessage msg; + msg.addByte(0x7F); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCloseContainer(uint8_t cid) +{ + NetworkMessage msg; + msg.addByte(0x6F); + msg.addByte(cid); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCreatureTurn(const Creature* creature, uint32_t stackPos) +{ + if (!canSee(creature)) { + return; + } + + NetworkMessage msg; + msg.addByte(0x6B); + msg.addPosition(creature->getPosition()); + msg.addByte(stackPos); + msg.add(0x63); + msg.add(creature->getID()); + msg.addByte(creature->getDirection()); + msg.addByte(player->canWalkthroughEx(creature) ? 0x00 : 0x01); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCreatureSay(const Creature* creature, SpeakClasses type, const std::string& text, const Position* pos/* = nullptr*/) +{ + NetworkMessage msg; + msg.addByte(0xAA); + + static uint32_t statementId = 0; + msg.add(++statementId); + + msg.addString(creature->getName()); + + //Add level only for players + if (const Player* speaker = creature->getPlayer()) { + msg.add(speaker->getLevel()); + } else { + msg.add(0x00); + } + + msg.addByte(type); + if (pos) { + msg.addPosition(*pos); + } else { + msg.addPosition(creature->getPosition()); + } + + msg.addString(text); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendToChannel(const Creature* creature, SpeakClasses type, const std::string& text, uint16_t channelId) +{ + NetworkMessage msg; + msg.addByte(0xAA); + + static uint32_t statementId = 0; + msg.add(++statementId); + if (!creature) { + msg.add(0x00); + } else if (type == TALKTYPE_CHANNEL_R2) { + msg.add(0x00); + type = TALKTYPE_CHANNEL_R1; + } else { + msg.addString(creature->getName()); + //Add level only for players + if (const Player* speaker = creature->getPlayer()) { + msg.add(speaker->getLevel()); + } else { + msg.add(0x00); + } + } + + msg.addByte(type); + msg.add(channelId); + msg.addString(text); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendPrivateMessage(const Player* speaker, SpeakClasses type, const std::string& text) +{ + NetworkMessage msg; + msg.addByte(0xAA); + static uint32_t statementId = 0; + msg.add(++statementId); + if (speaker) { + msg.addString(speaker->getName()); + msg.add(speaker->getLevel()); + } else { + msg.add(0x00); + } + msg.addByte(type); + msg.addString(text); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCancelTarget() +{ + NetworkMessage msg; + msg.addByte(0xA3); + msg.add(0x00); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendChangeSpeed(const Creature* creature, uint32_t speed) +{ + NetworkMessage msg; + msg.addByte(0x8F); + msg.add(creature->getID()); + msg.add(creature->getBaseSpeed() / 2); + msg.add(speed / 2); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendDistanceShoot(const Position& from, const Position& to, uint8_t type) +{ + NetworkMessage msg; + msg.addByte(0x85); + msg.addPosition(from); + msg.addPosition(to); + msg.addByte(type); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCreatureHealth(const Creature* creature) +{ + NetworkMessage msg; + msg.addByte(0x8C); + msg.add(creature->getID()); + + if (creature->isHealthHidden()) { + msg.addByte(0x00); + } else { + msg.addByte(std::ceil((static_cast(creature->getHealth()) / std::max(creature->getMaxHealth(), 1)) * 100)); + } + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendFYIBox(const std::string& message) +{ + NetworkMessage msg; + msg.addByte(0x15); + msg.addString(message); + writeToOutputBuffer(msg); +} + +//tile + +void ProtocolGame::sendAddTileItem(const Position& pos, uint32_t stackpos, const Item* item) +{ + if (!canSee(pos)) { + return; + } + + NetworkMessage msg; + msg.addByte(0x6A); + msg.addPosition(pos); + msg.addByte(stackpos); + msg.addItem(item); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendUpdateTileItem(const Position& pos, uint32_t stackpos, const Item* item) +{ + if (!canSee(pos)) { + return; + } + + NetworkMessage msg; + msg.addByte(0x6B); + msg.addPosition(pos); + msg.addByte(stackpos); + msg.addItem(item); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendRemoveTileThing(const Position& pos, uint32_t stackpos) +{ + if (!canSee(pos)) { + return; + } + + NetworkMessage msg; + RemoveTileThing(msg, pos, stackpos); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendFightModes() +{ + NetworkMessage msg; + msg.addByte(0xA7); + msg.addByte(player->fightMode); + msg.addByte(player->chaseMode); + msg.addByte(player->secureMode); + msg.addByte(PVP_MODE_DOVE); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendMoveCreature(const Creature* creature, const Position& newPos, int32_t newStackPos, const Position& oldPos, int32_t oldStackPos, bool teleport) +{ + if (creature == player) { + if (oldStackPos >= 10) { + sendMapDescription(newPos); + } else if (teleport) { + NetworkMessage msg; + RemoveTileThing(msg, oldPos, oldStackPos); + writeToOutputBuffer(msg); + sendMapDescription(newPos); + } else { + NetworkMessage msg; + if (oldPos.z == 7 && newPos.z >= 8) { + RemoveTileThing(msg, oldPos, oldStackPos); + } else { + msg.addByte(0x6D); + msg.addPosition(oldPos); + msg.addByte(oldStackPos); + msg.addPosition(newPos); + } + + if (newPos.z > oldPos.z) { + MoveDownCreature(msg, creature, newPos, oldPos); + } else if (newPos.z < oldPos.z) { + MoveUpCreature(msg, creature, newPos, oldPos); + } + + if (oldPos.y > newPos.y) { // north, for old x + msg.addByte(0x65); + GetMapDescription(oldPos.x - 8, newPos.y - 6, newPos.z, 18, 1, msg); + } else if (oldPos.y < newPos.y) { // south, for old x + msg.addByte(0x67); + GetMapDescription(oldPos.x - 8, newPos.y + 7, newPos.z, 18, 1, msg); + } + + if (oldPos.x < newPos.x) { // east, [with new y] + msg.addByte(0x66); + GetMapDescription(newPos.x + 9, newPos.y - 6, newPos.z, 1, 14, msg); + } else if (oldPos.x > newPos.x) { // west, [with new y] + msg.addByte(0x68); + GetMapDescription(newPos.x - 8, newPos.y - 6, newPos.z, 1, 14, msg); + } + writeToOutputBuffer(msg); + } + } else if (canSee(oldPos) && canSee(creature->getPosition())) { + if (teleport || (oldPos.z == 7 && newPos.z >= 8) || oldStackPos >= 10) { + sendRemoveTileThing(oldPos, oldStackPos); + sendAddCreature(creature, newPos, newStackPos, false); + } else { + NetworkMessage msg; + msg.addByte(0x6D); + msg.addPosition(oldPos); + msg.addByte(oldStackPos); + msg.addPosition(creature->getPosition()); + writeToOutputBuffer(msg); + } + } else if (canSee(oldPos)) { + sendRemoveTileThing(oldPos, oldStackPos); + } else if (canSee(creature->getPosition())) { + sendAddCreature(creature, newPos, newStackPos, false); + } +} + +void ProtocolGame::sendItems() +{ + NetworkMessage msg; + msg.addByte(0xF5); + + const std::vector& inventory = Item::items.getInventory(); + msg.add(inventory.size() + 11); + for (uint16_t i = 1; i <= 11; i++) { + msg.add(i); + msg.addByte(0); //always 0 + msg.add(1); // always 1 + } + + for (auto clientId : inventory) { + msg.add(clientId); + msg.addByte(0); //always 0 + msg.add(1); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendAddContainerItem(uint8_t cid, uint16_t slot, const Item* item) +{ + NetworkMessage msg; + msg.addByte(0x70); + msg.addByte(cid); + msg.add(slot); + msg.addItem(item); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendUpdateContainerItem(uint8_t cid, uint16_t slot, const Item* item) +{ + NetworkMessage msg; + msg.addByte(0x71); + msg.addByte(cid); + msg.add(slot); + msg.addItem(item); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendRemoveContainerItem(uint8_t cid, uint16_t slot, const Item* lastItem) +{ + NetworkMessage msg; + msg.addByte(0x72); + msg.addByte(cid); + msg.add(slot); + if (lastItem) { + msg.addItem(lastItem); + } else { + msg.add(0x00); + } + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendTextWindow(uint32_t windowTextId, Item* item, uint16_t maxlen, bool canWrite) +{ + NetworkMessage msg; + msg.addByte(0x96); + msg.add(windowTextId); + msg.addItem(item); + + if (canWrite) { + msg.add(maxlen); + msg.addString(item->getText()); + } else { + const std::string& text = item->getText(); + msg.add(text.size()); + msg.addString(text); + } + + const std::string& writer = item->getWriter(); + if (!writer.empty()) { + msg.addString(writer); + } else { + msg.add(0x00); + } + + time_t writtenDate = item->getDate(); + if (writtenDate != 0) { + msg.addString(formatDateShort(writtenDate)); + } else { + msg.add(0x00); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendTextWindow(uint32_t windowTextId, uint32_t itemId, const std::string& text) +{ + NetworkMessage msg; + msg.addByte(0x96); + msg.add(windowTextId); + msg.addItem(itemId, 1); + msg.add(text.size()); + msg.addString(text); + msg.add(0x00); + msg.add(0x00); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendHouseWindow(uint32_t windowTextId, const std::string& text) +{ + NetworkMessage msg; + msg.addByte(0x97); + msg.addByte(0x00); + msg.add(windowTextId); + msg.addString(text); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendOutfitWindow() +{ + NetworkMessage msg; + msg.addByte(0xC8); + + Outfit_t currentOutfit = player->getDefaultOutfit(); + Mount* currentMount = g_game.mounts.getMountByID(player->getCurrentMount()); + if (currentMount) { + currentOutfit.lookMount = currentMount->clientId; + } + + AddOutfit(msg, currentOutfit); + + std::vector protocolOutfits; + if (player->isAccessPlayer()) { + static const std::string gamemasterOutfitName = "Gamemaster"; + protocolOutfits.emplace_back(gamemasterOutfitName, 75, 0); + } + + const auto& outfits = Outfits::getInstance().getOutfits(player->getSex()); + protocolOutfits.reserve(outfits.size()); + for (const Outfit& outfit : outfits) { + uint8_t addons; + if (!player->getOutfitAddons(outfit, addons)) { + continue; + } + + protocolOutfits.emplace_back(outfit.name, outfit.lookType, addons); + if (protocolOutfits.size() == 100) { // Game client doesn't allow more than 100 outfits + break; + } + } + + msg.addByte(protocolOutfits.size()); + for (const ProtocolOutfit& outfit : protocolOutfits) { + msg.add(outfit.lookType); + msg.addString(outfit.name); + msg.addByte(outfit.addons); + } + + std::vector mounts; + for (const Mount& mount : g_game.mounts.getMounts()) { + if (player->hasMount(&mount)) { + mounts.push_back(&mount); + } + } + + msg.addByte(mounts.size()); + for (const Mount* mount : mounts) { + msg.add(mount->clientId); + msg.addString(mount->name); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendUpdatedVIPStatus(uint32_t guid, VipStatus_t newStatus) +{ + NetworkMessage msg; + msg.addByte(0xD3); + msg.add(guid); + msg.addByte(newStatus); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendVIPEntries() +{ + const std::forward_list& vipEntries = IOLoginData::getVIPEntries(player->getAccount()); + + for (const VIPEntry& entry : vipEntries) { + VipStatus_t vipStatus = VIPSTATUS_ONLINE; + + Player* vipPlayer = g_game.getPlayerByGUID(entry.guid); + + if (!vipPlayer || vipPlayer->isInGhostMode() || player->isAccessPlayer()) { + vipStatus = VIPSTATUS_OFFLINE; + } + + sendVIP(entry.guid, entry.name, entry.description, entry.icon, entry.notify, vipStatus); + } +} + +void ProtocolGame::sendSpellCooldown(uint8_t spellId, uint32_t time) +{ + NetworkMessage msg; + msg.addByte(0xA4); + msg.addByte(spellId); + msg.add(time); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendSpellGroupCooldown(SpellGroup_t groupId, uint32_t time) +{ + NetworkMessage msg; + msg.addByte(0xA5); + msg.addByte(groupId); + msg.add(time); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendModalWindow(const ModalWindow& modalWindow) +{ + NetworkMessage msg; + msg.addByte(0xFA); + + msg.add(modalWindow.id); + msg.addString(modalWindow.title); + msg.addString(modalWindow.message); + + msg.addByte(modalWindow.buttons.size()); + for (const auto& it : modalWindow.buttons) { + msg.addString(it.first); + msg.addByte(it.second); + } + + msg.addByte(modalWindow.choices.size()); + for (const auto& it : modalWindow.choices) { + msg.addString(it.first); + msg.addByte(it.second); + } + + msg.addByte(modalWindow.defaultEscapeButton); + msg.addByte(modalWindow.defaultEnterButton); + msg.addByte(modalWindow.priority ? 0x01 : 0x00); + + writeToOutputBuffer(msg); +} + +////////////// Add common messages + +//tile +void ProtocolGame::MoveUpCreature(NetworkMessage& msg, const Creature* creature, const Position& newPos, const Position& oldPos) +{ + if (creature != player) { + return; + } + + //floor change up + msg.addByte(0xBE); + + //going to surface + if (newPos.z == 7) { + int32_t skip = -1; + GetFloorDescription(msg, oldPos.x - 8, oldPos.y - 6, 5, 18, 14, 3, skip); //(floor 7 and 6 already set) + GetFloorDescription(msg, oldPos.x - 8, oldPos.y - 6, 4, 18, 14, 4, skip); + GetFloorDescription(msg, oldPos.x - 8, oldPos.y - 6, 3, 18, 14, 5, skip); + GetFloorDescription(msg, oldPos.x - 8, oldPos.y - 6, 2, 18, 14, 6, skip); + GetFloorDescription(msg, oldPos.x - 8, oldPos.y - 6, 1, 18, 14, 7, skip); + GetFloorDescription(msg, oldPos.x - 8, oldPos.y - 6, 0, 18, 14, 8, skip); + + if (skip >= 0) { + msg.addByte(skip); + msg.addByte(0xFF); + } + } + //underground, going one floor up (still underground) + else if (newPos.z > 7) { + int32_t skip = -1; + GetFloorDescription(msg, oldPos.x - 8, oldPos.y - 6, oldPos.getZ() - 3, 18, 14, 3, skip); + + if (skip >= 0) { + msg.addByte(skip); + msg.addByte(0xFF); + } + } + + //moving up a floor up makes us out of sync + //west + msg.addByte(0x68); + GetMapDescription(oldPos.x - 8, oldPos.y - 5, newPos.z, 1, 14, msg); + + //north + msg.addByte(0x65); + GetMapDescription(oldPos.x - 8, oldPos.y - 6, newPos.z, 18, 1, msg); +} + +void ProtocolGame::MoveDownCreature(NetworkMessage& msg, const Creature* creature, const Position& newPos, const Position& oldPos) +{ + if (creature != player) { + return; + } + + //floor change down + msg.addByte(0xBF); + + //going from surface to underground + if (newPos.z == 8) { + int32_t skip = -1; + + GetFloorDescription(msg, oldPos.x - 8, oldPos.y - 6, newPos.z, 18, 14, -1, skip); + GetFloorDescription(msg, oldPos.x - 8, oldPos.y - 6, newPos.z + 1, 18, 14, -2, skip); + GetFloorDescription(msg, oldPos.x - 8, oldPos.y - 6, newPos.z + 2, 18, 14, -3, skip); + + if (skip >= 0) { + msg.addByte(skip); + msg.addByte(0xFF); + } + } + //going further down + else if (newPos.z > oldPos.z && newPos.z > 8 && newPos.z < 14) { + int32_t skip = -1; + GetFloorDescription(msg, oldPos.x - 8, oldPos.y - 6, newPos.z + 2, 18, 14, -3, skip); + + if (skip >= 0) { + msg.addByte(skip); + msg.addByte(0xFF); + } + } + + //moving down a floor makes us out of sync + //east + msg.addByte(0x66); + GetMapDescription(oldPos.x + 9, oldPos.y - 7, newPos.z, 1, 14, msg); + + //south + msg.addByte(0x67); + GetMapDescription(oldPos.x - 8, oldPos.y + 7, newPos.z, 18, 1, msg); +} + +void ProtocolGame::AddShopItem(NetworkMessage& msg, const ShopInfo& item) +{ + const ItemType& it = Item::items[item.itemId]; + msg.add(it.clientId); + + if (it.isSplash() || it.isFluidContainer()) { + msg.addByte(serverFluidToClient(item.subType)); + } else { + msg.addByte(0x00); + } + + msg.addString(item.realName); + msg.add(it.weight); + msg.add(item.buyPrice); + msg.add(item.sellPrice); +} + +void ProtocolGame::parseExtendedOpcode(NetworkMessage& msg) +{ + uint8_t opcode = msg.getByte(); + const std::string& buffer = msg.getString(); + + // process additional opcodes via lua script event + addGameTask(&Game::parsePlayerExtendedOpcode, player->getID(), opcode, buffer); +} diff --git a/src/protocolgame.h b/src/protocolgame.h new file mode 100644 index 0000000..3b400a7 --- /dev/null +++ b/src/protocolgame.h @@ -0,0 +1,386 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_PROTOCOLGAME_H_FACA2A2D1A9348B78E8FD7E8003EBB87 +#define FS_PROTOCOLGAME_H_FACA2A2D1A9348B78E8FD7E8003EBB87 + +#include "protocol.h" +#include "chat.h" +#include "creature.h" +#include "tasks.h" +#include "protocolgamebase.h" +#include "protocolspectator.h" + +class NetworkMessage; +class Player; +class Game; +class House; +class Container; +class Tile; +class Connection; +class Quest; +class ProtocolGame; +class ProtocolSpectator; +using ProtocolGame_ptr = std::shared_ptr; + +extern Game g_game; + +struct TextMessage +{ + MessageClasses type = MESSAGE_STATUS_DEFAULT; + std::string text; + Position position; + uint16_t channelId; + struct { + int32_t value = 0; + TextColor_t color; + } primary, secondary; + + TextMessage() = default; + TextMessage(MessageClasses type, std::string text) : type(type), text(std::move(text)) {} +}; + +class ProtocolGame final : public ProtocolGameBase +{ + public: + // static protocol information + static const char* protocol_name() { + return "gameworld protocol"; + } + + explicit ProtocolGame(Connection_ptr connection): + ProtocolGameBase(connection) {} + + void login(const std::string& name, uint32_t accountId, OperatingSystem_t operatingSystem); + void logout(bool displayEffect, bool forced); + + uint16_t getVersion() const { + return version; + } + + const std::unordered_set& getKnownCreatures() const { + return knownCreatureSet; + } + + typedef std::unordered_map LiveCastsMap; + typedef std::vector CastSpectatorVec; + + /** \brief Adds a spectator from the spectators vector. + * \param spectatorClient pointer to the \ref ProtocolSpectator object representing the spectator + */ + void addSpectator(ProtocolSpectator_ptr spectatorClient); + + /** \brief Removes a spectator from the spectators vector. + * \param spectatorClient pointer to the \ref ProtocolSpectator object representing the spectator + */ + void removeSpectator(ProtocolSpectator_ptr spectatorClient); + + /** \brief Starts the live cast. + * \param password live cast password(optional) + * \returns bool type indicating whether starting the cast was successful + */ + bool startLiveCast(const std::string& password = ""); + + /** \brief Stops the live cast and disconnects all spectators. + * \returns bool type indicating whether stopping the cast was successful + */ + bool stopLiveCast(); + + const CastSpectatorVec& getLiveCastSpectators() const { + return spectators; + } + + size_t getSpectatorCount() const { + return spectators.size(); + } + + bool isLiveCaster() const { + return isCaster.load(std::memory_order_relaxed); + } + + std::mutex liveCastLock; + + /** \brief Adds a new live cast to the list of available casts + */ + void registerLiveCast(); + + /** \brief Removes a live cast from the list of available casts + */ + void unregisterLiveCast(); + + /** \brief Update live cast info in the database. + * \param player pointer to the casting \ref Player object + * \param client pointer to the caster's \ref ProtocolGame object + */ + void updateLiveCastInfo(); + + /** \brief Clears all live casts. Used to make sure there aro no live cast db rows left should a crash occur. + * \warning Only supposed to be called once. + */ + static void clearLiveCastInfo(); + + /** \brief Finds the caster's \ref ProtocolGame object + * \param player pointer to the casting \ref Player object + * \returns A pointer to the \ref ProtocolGame of the caster + */ + static ProtocolGame_ptr getLiveCast(Player* player) { + const auto it = liveCasts.find(player); + return it != liveCasts.end() ? it->second : nullptr; + } + + const std::string& getLiveCastName() const { + return liveCastName; + } + + const std::string& getLiveCastPassword() const { + return liveCastPassword; + } + + bool isPasswordProtected() const { + return !liveCastPassword.empty(); + } + + static const LiveCastsMap& getLiveCasts() { + return liveCasts; + } + + /** \brief Allows spectators to send text messages to the caster + * and then get broadcast to the rest of the spectators + * \param text string containing the text message + */ + void broadcastSpectatorMessage(const std::string& text) { + if (player) { + sendChannelMessage("Spectator", text, TALKTYPE_CHANNEL_Y, CHANNEL_CAST); + } + } + + static uint8_t getMaxLiveCastCount() { + return std::numeric_limits::max(); + } + + private: + ProtocolGame_ptr getThis() { + return std::static_pointer_cast(shared_from_this()); + } + void connect(uint32_t playerId, OperatingSystem_t operatingSystem); + void disconnectClient(const std::string& message) const; + void writeToOutputBuffer(const NetworkMessage& msg, bool broadcast = true) final; + + void release() override; + + void checkCreatureAsKnown(uint32_t id, bool& known, uint32_t& removedKnown); + + // we have all the parse methods + void parsePacket(NetworkMessage& msg) override; + void onRecvFirstMessage(NetworkMessage& msg) override; + + //Parse methods + void parseAutoWalk(NetworkMessage& msg); + void parseSetOutfit(NetworkMessage& msg); + void parseSay(NetworkMessage& msg); + void parseLookAt(NetworkMessage& msg); + void parseLookInBattleList(NetworkMessage& msg); + void parseFightModes(NetworkMessage& msg); + void parseAttack(NetworkMessage& msg); + void parseFollow(NetworkMessage& msg); + void parseEquipObject(NetworkMessage& msg); + + void parseBugReport(NetworkMessage& msg); + void parseDebugAssert(NetworkMessage& msg); + void parseRuleViolationReport(NetworkMessage& msg); + + void parseThrow(NetworkMessage& msg); + void parseUseItemEx(NetworkMessage& msg); + void parseUseWithCreature(NetworkMessage& msg); + void parseUseItem(NetworkMessage& msg); + void parseCloseContainer(NetworkMessage& msg); + void parseUpArrowContainer(NetworkMessage& msg); + void parseUpdateContainer(NetworkMessage& msg); + void parseTextWindow(NetworkMessage& msg); + void parseHouseWindow(NetworkMessage& msg); + void parseWrapItem(NetworkMessage& msg); + + void parseLookInShop(NetworkMessage& msg); + void parsePlayerPurchase(NetworkMessage& msg); + void parsePlayerSale(NetworkMessage& msg); + + void parseQuestLine(NetworkMessage& msg); + + void parseInviteToParty(NetworkMessage& msg); + void parseJoinParty(NetworkMessage& msg); + void parseRevokePartyInvite(NetworkMessage& msg); + void parsePassPartyLeadership(NetworkMessage& msg); + void parseEnableSharedPartyExperience(NetworkMessage& msg); + + void parseToggleMount(NetworkMessage& msg); + + void parseModalWindowAnswer(NetworkMessage& msg); + + void parseBrowseField(NetworkMessage& msg); + void parseSeekInContainer(NetworkMessage& msg); + + //trade methods + void parseRequestTrade(NetworkMessage& msg); + void parseLookInTrade(NetworkMessage& msg); + + //market methods + void parseMarketLeave(); + void parseMarketBrowse(NetworkMessage& msg); + void parseMarketCreateOffer(NetworkMessage& msg); + void parseMarketCancelOffer(NetworkMessage& msg); + void parseMarketAcceptOffer(NetworkMessage& msg); + + //VIP methods + void parseAddVip(NetworkMessage& msg); + void parseRemoveVip(NetworkMessage& msg); + void parseEditVip(NetworkMessage& msg); + + void parseRotateItem(NetworkMessage& msg); + + //Channel tabs + void parseChannelInvite(NetworkMessage& msg); + void parseChannelExclude(NetworkMessage& msg); + void parseOpenChannel(NetworkMessage& msg); + void parseOpenPrivateChannel(NetworkMessage& msg); + void parseCloseChannel(NetworkMessage& msg); + + //Send functions + void sendChannelMessage(const std::string& author, const std::string& text, SpeakClasses type, uint16_t channel); + void sendChannelEvent(uint16_t channelId, const std::string& playerName, ChannelEvent_t channelEvent); + void sendClosePrivate(uint16_t channelId); + void sendCreatePrivateChannel(uint16_t channelId, const std::string& channelName); + void sendChannelsDialog(); + void sendOpenPrivateChannel(const std::string& receiver); + void sendToChannel(const Creature* creature, SpeakClasses type, const std::string& text, uint16_t channelId); + void sendPrivateMessage(const Player* speaker, SpeakClasses type, const std::string& text); + void sendIcons(uint16_t icons); + void sendFYIBox(const std::string& message); + + void sendDistanceShoot(const Position& from, const Position& to, uint8_t type); + void sendCreatureHealth(const Creature* creature); + void sendCreatureTurn(const Creature* creature, uint32_t stackPos); + void sendCreatureSay(const Creature* creature, SpeakClasses type, const std::string& text, const Position* pos = nullptr); + + void sendQuestLog(); + void sendQuestLine(const Quest* quest); + + void sendChangeSpeed(const Creature* creature, uint32_t speed); + void sendCancelTarget(); + void sendCreatureOutfit(const Creature* creature, const Outfit_t& outfit); + void sendTextMessage(const TextMessage& message); + void sendReLoginWindow(uint8_t unfairFightReduction); + + void sendTutorial(uint8_t tutorialId); + void sendAddMarker(const Position& pos, uint8_t markType, const std::string& desc); + + void sendCreatureWalkthrough(const Creature* creature, bool walkthrough); + void sendCreatureShield(const Creature* creature); + void sendCreatureSkull(const Creature* creature); + void sendCreatureType(uint32_t creatureId, uint8_t creatureType); + void sendCreatureHelpers(uint32_t creatureId, uint16_t helpers); + + void sendShop(Npc* npc, const ShopInfoList& itemList); + void sendCloseShop(); + void sendSaleItemList(const std::list& shop); + void sendMarketEnter(uint32_t depotId); + void sendMarketLeave(); + void sendMarketBrowseItem(uint16_t itemId, const MarketOfferList& buyOffers, const MarketOfferList& sellOffers); + void sendMarketAcceptOffer(const MarketOfferEx& offer); + void sendMarketBrowseOwnOffers(const MarketOfferList& buyOffers, const MarketOfferList& sellOffers); + void sendMarketCancelOffer(const MarketOfferEx& offer); + void sendMarketBrowseOwnHistory(const HistoryMarketOfferList& buyOffers, const HistoryMarketOfferList& sellOffers); + void sendMarketDetail(uint16_t itemId); + void sendTradeItemRequest(const std::string& traderName, const Item* item, bool ack); + void sendCloseTrade(); + + void sendTextWindow(uint32_t windowTextId, Item* item, uint16_t maxlen, bool canWrite); + void sendTextWindow(uint32_t windowTextId, uint32_t itemId, const std::string& text); + void sendHouseWindow(uint32_t windowTextId, const std::string& text); + void sendOutfitWindow(); + + void sendUpdatedVIPStatus(uint32_t guid, VipStatus_t newStatus); + void sendVIPEntries(); + + void sendFightModes(); + + void sendCreatureSquare(const Creature* creature, SquareColor_t color); + + void sendSpellCooldown(uint8_t spellId, uint32_t time); + void sendSpellGroupCooldown(SpellGroup_t groupId, uint32_t time); + + //tiles + + void sendAddTileItem(const Position& pos, uint32_t stackpos, const Item* item); + void sendUpdateTileItem(const Position& pos, uint32_t stackpos, const Item* item); + void sendRemoveTileThing(const Position& pos, uint32_t stackpos); + + void sendMoveCreature(const Creature* creature, const Position& newPos, int32_t newStackPos, + const Position& oldPos, int32_t oldStackPos, bool teleport); + + //containers + void sendAddContainerItem(uint8_t cid, uint16_t slot, const Item* item); + void sendUpdateContainerItem(uint8_t cid, uint16_t slot, const Item* item); + void sendRemoveContainerItem(uint8_t cid, uint16_t slot, const Item* lastItem); + + void sendCloseContainer(uint8_t cid); + + //inventory + void sendItems(); + + //messages + void sendModalWindow(const ModalWindow& modalWindow); + + //Help functions + void MoveUpCreature(NetworkMessage& msg, const Creature* creature, const Position& newPos, const Position& oldPos); + void MoveDownCreature(NetworkMessage& msg, const Creature* creature, const Position& newPos, const Position& oldPos); + + //shop + void AddShopItem(NetworkMessage& msg, const ShopInfo& item); + + //otclient + void parseExtendedOpcode(NetworkMessage& msg); + + friend class Player; + + // Helpers so we don't need to bind every time + template + void addGameTask(Callable function, Args&&... args) { + g_dispatcher.addTask(createTask(std::bind(function, &g_game, std::forward(args)...))); + } + + template + void addGameTaskTimed(uint32_t delay, Callable function, Args&&... args) { + g_dispatcher.addTask(createTask(delay, std::bind(function, &g_game, std::forward(args)...))); + } + + static LiveCastsMap liveCasts; ///< Stores all available casts. + + std::atomic isCaster {false}; ///< Determines if this \ref ProtocolGame object is casting + + /// list of spectators \warning This variable should only be accessed after locking \ref liveCastLock + CastSpectatorVec spectators; + + /// Live cast name that is also used as login + std::string liveCastName; + + /// Password used to access the live cast + std::string liveCastPassword; +}; + +#endif diff --git a/src/protocolgamebase.cpp b/src/protocolgamebase.cpp new file mode 100644 index 0000000..79d4976 --- /dev/null +++ b/src/protocolgamebase.cpp @@ -0,0 +1,731 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2015 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" +#include +#include "protocolgamebase.h" +#include "game.h" +#include "iologindata.h" +#include "tile.h" +#include "outputmessage.h" + +extern Game g_game; + +void ProtocolGameBase::onConnect() +{ + auto output = OutputMessagePool::getOutputMessage(); + static std::random_device rd; + static std::ranlux24 generator(rd()); + static std::uniform_int_distribution randNumber(0x00, 0xFF); + + // Skip checksum + output->skipBytes(sizeof(uint32_t)); + + // Packet length & type + output->add(0x0006); + output->addByte(0x1F); + + // Add timestamp & random number + challengeTimestamp = static_cast(time(nullptr)); + output->add(challengeTimestamp); + + challengeRandom = randNumber(generator); + output->addByte(challengeRandom); + + // Go back and write checksum + output->skipBytes(-12); + output->add(adlerChecksum(output->getOutputBuffer() + sizeof(uint32_t), 8)); + + send(output); +} + +void ProtocolGameBase::AddOutfit(NetworkMessage& msg, const Outfit_t& outfit) +{ + msg.add(outfit.lookType); + + if (outfit.lookType != 0) { + msg.addByte(outfit.lookHead); + msg.addByte(outfit.lookBody); + msg.addByte(outfit.lookLegs); + msg.addByte(outfit.lookFeet); + msg.addByte(outfit.lookAddons); + } else { + msg.addItemId(outfit.lookTypeEx); + } + + msg.add(outfit.lookMount); +} + +void ProtocolGameBase::checkCreatureAsKnown(uint32_t id, bool& known, uint32_t& removedKnown) +{ + auto result = knownCreatureSet.insert(id); + if (!result.second) { + known = true; + return; + } + + known = false; + + if (knownCreatureSet.size() > 1300) { + // Look for a creature to remove + for (auto it = knownCreatureSet.begin(), end = knownCreatureSet.end(); it != end; ++it) { + Creature* creature = g_game.getCreatureByID(*it); + if (!canSee(creature)) { + removedKnown = *it; + knownCreatureSet.erase(it); + return; + } + } + + // Bad situation. Let's just remove anyone. + auto it = knownCreatureSet.begin(); + if (*it == id) { + ++it; + } + + removedKnown = *it; + knownCreatureSet.erase(it); + } else { + removedKnown = 0; + } +} + +void ProtocolGameBase::AddCreature(NetworkMessage& msg, const Creature* creature, bool known, uint32_t remove) +{ + CreatureType_t creatureType = creature->getType(); + + const Player* otherPlayer = creature->getPlayer(); + + if (known) { + msg.add(0x62); + msg.add(creature->getID()); + } else { + msg.add(0x61); + msg.add(remove); + msg.add(creature->getID()); + msg.addByte(creatureType); + msg.addString(creature->getName()); + } + + if (creature->isHealthHidden()) { + msg.addByte(0x00); + } else { + msg.addByte(std::ceil((static_cast(creature->getHealth()) / std::max(creature->getMaxHealth(), 1)) * 100)); + } + + msg.addByte(creature->getDirection()); + + if (!creature->isInGhostMode() && !creature->isInvisible()) { + AddOutfit(msg, creature->getCurrentOutfit()); + } else { + static Outfit_t outfit; + AddOutfit(msg, outfit); + } + + LightInfo lightInfo = creature->getCreatureLight(); + msg.addByte(player->isAccessPlayer() ? 0xFF : lightInfo.level); + msg.addByte(lightInfo.color); + + msg.add(creature->getStepSpeed() / 2); + + msg.addByte(player->getSkullClient(creature)); + msg.addByte(player->getPartyShield(otherPlayer)); + + if (!known) { + msg.addByte(player->getGuildEmblem(otherPlayer)); + } + + if (creatureType == CREATURETYPE_MONSTER) { + const Creature* master = creature->getMaster(); + if (master) { + const Player* masterPlayer = master->getPlayer(); + if (masterPlayer) { + if (masterPlayer == player) { + creatureType = CREATURETYPE_SUMMON_OWN; + } else { + creatureType = CREATURETYPE_SUMMON_OTHERS; + } + } + } + } + + msg.addByte(creatureType); // Type (for summons) + msg.addByte(creature->getSpeechBubble()); + msg.addByte(0xFF); // MARK_UNMARKED + + if (otherPlayer) { + msg.add(otherPlayer->getHelpers()); + } else { + msg.add(0x00); + } + + msg.addByte(player->canWalkthroughEx(creature) ? 0x00 : 0x01); +} + +void ProtocolGameBase::AddPlayerStats(NetworkMessage& msg) +{ + msg.addByte(0xA0); + + msg.add(std::min(player->getHealth(), std::numeric_limits::max())); + msg.add(std::min(player->getMaxHealth(), std::numeric_limits::max())); + + msg.add(player->getFreeCapacity()); + msg.add(player->getCapacity()); + + msg.add(player->getExperience()); + + msg.add(player->getLevel()); + msg.addByte(player->getLevelPercent()); + + msg.add(100); // base xp gain rate + msg.add(0); // xp voucher + msg.add(0); // low level bonus + msg.add(0); // xp boost + msg.add(100); // stamina multiplier (100 = x1.0) + + msg.add(std::min(player->getMana(), std::numeric_limits::max())); + msg.add(std::min(player->getMaxMana(), std::numeric_limits::max())); + + msg.addByte(std::min(player->getMagicLevel(), std::numeric_limits::max())); + msg.addByte(std::min(player->getBaseMagicLevel(), std::numeric_limits::max())); + msg.addByte(player->getMagicLevelPercent()); + + msg.addByte(player->getSoul()); + + msg.add(player->getStaminaMinutes()); + + msg.add(player->getBaseSpeed() / 2); + + Condition* condition = player->getCondition(CONDITION_REGENERATION); + msg.add(condition ? condition->getTicks() / 1000 : 0x00); + + msg.add(player->getOfflineTrainingTime() / 60 / 1000); + + msg.add(0); // xp boost time (seconds) + msg.addByte(0); // enables exp boost in the store +} + +void ProtocolGameBase::AddPlayerSkills(NetworkMessage& msg) +{ + msg.addByte(0xA1); + + for (uint8_t i = SKILL_FIRST; i <= SKILL_LAST; ++i) { + msg.add(std::min(player->getSkillLevel(i), std::numeric_limits::max())); + msg.add(player->getBaseSkill(i)); + msg.addByte(player->getSkillPercent(i)); + } + + for (uint8_t i = SPECIALSKILL_FIRST; i <= SPECIALSKILL_LAST; ++i) { + msg.add(std::min(100, player->varSpecialSkills[i])); + msg.add(0); + } +} + +void ProtocolGameBase::AddWorldLight(NetworkMessage& msg, const LightInfo& lightInfo) +{ + msg.addByte(0x82); + msg.addByte((player->isAccessPlayer() ? 0xFF : lightInfo.level)); + msg.addByte(lightInfo.color); +} + +void ProtocolGameBase::AddCreatureLight(NetworkMessage& msg, const Creature* creature) +{ + LightInfo lightInfo = creature->getCreatureLight(); + + msg.addByte(0x8D); + msg.add(creature->getID()); + msg.addByte((player->isAccessPlayer() ? 0xFF : lightInfo.level)); + msg.addByte(lightInfo.color); +} + +bool ProtocolGameBase::canSee(const Creature* c) const +{ + if (!c || !player || c->isRemoved()) { + return false; + } + + if (!player->canSeeCreature(c)) { + return false; + } + + return canSee(c->getPosition()); +} + +bool ProtocolGameBase::canSee(const Position& pos) const +{ + return canSee(pos.x, pos.y, pos.z); +} + +bool ProtocolGameBase::canSee(int32_t x, int32_t y, int32_t z) const +{ + if (!player) { + return false; + } + + const Position& myPos = player->getPosition(); + if (myPos.z <= 7) { + //we are on ground level or above (7 -> 0) + //view is from 7 -> 0 + if (z > 7) { + return false; + } + } else if (myPos.z >= 8) { + //we are underground (8 -> 15) + //view is +/- 2 from the floor we stand on + if (std::abs(myPos.getZ() - z) > 2) { + return false; + } + } + + //negative offset means that the action taken place is on a lower floor than ourself + int32_t offsetz = myPos.getZ() - z; + if ((x >= myPos.getX() - 8 + offsetz) && (x <= myPos.getX() + 9 + offsetz) && + (y >= myPos.getY() - 6 + offsetz) && (y <= myPos.getY() + 7 + offsetz)) { + return true; + } + return false; +} + +//tile +void ProtocolGameBase::RemoveTileThing(NetworkMessage& msg, const Position& pos, uint32_t stackpos) +{ + if (stackpos >= 10) { + return; + } + + msg.addByte(0x6C); + msg.addPosition(pos); + msg.addByte(stackpos); +} + +void ProtocolGameBase::sendUpdateTile(const Tile* tile, const Position& pos) +{ + if (!canSee(pos)) { + return; + } + + NetworkMessage msg; + msg.addByte(0x69); + msg.addPosition(pos); + + if (tile) { + GetTileDescription(tile, msg); + msg.addByte(0x00); + msg.addByte(0xFF); + } else { + msg.addByte(0x01); + msg.addByte(0xFF); + } + + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::GetTileDescription(const Tile* tile, NetworkMessage& msg) +{ + msg.add(0x00); //environmental effects + + int32_t count; + Item* ground = tile->getGround(); + if (ground) { + msg.addItem(ground); + count = 1; + } else { + count = 0; + } + + const TileItemVector* items = tile->getItemList(); + if (items) { + for (auto it = items->getBeginTopItem(), end = items->getEndTopItem(); it != end; ++it) { + msg.addItem(*it); + + count++; + if (count == 9 && tile->getPosition() == player->getPosition()) { + break; + } else if (count == 10) { + return; + } + } + } + + const CreatureVector* creatures = tile->getCreatures(); + if (creatures) { + bool playerAdded = false; + for (const Creature* creature : boost::adaptors::reverse(*creatures)) { + if (!player->canSeeCreature(creature)) { + continue; + } + + if (tile->getPosition() == player->getPosition() && count == 9 && !playerAdded) { + creature = player; + } + + if (creature->getID() == player->getID()) { + playerAdded = true; + } + + bool known; + uint32_t removedKnown; + checkCreatureAsKnown(creature->getID(), known, removedKnown); + AddCreature(msg, creature, known, removedKnown); + + if (++count == 10) { + return; + } + } + } + + if (items) { + for (auto it = items->getBeginDownItem(), end = items->getEndDownItem(); it != end; ++it) { + msg.addItem(*it); + + if (++count == 10) { + return; + } + } + } +} + +void ProtocolGameBase::GetMapDescription(int32_t x, int32_t y, int32_t z, int32_t width, int32_t height, NetworkMessage& msg) +{ + int32_t skip = -1; + int32_t startz, endz, zstep; + + if (z > 7) { + startz = z - 2; + endz = std::min(MAP_MAX_LAYERS - 1, z + 2); + zstep = 1; + } else { + startz = 7; + endz = 0; + zstep = -1; + } + + for (int32_t nz = startz; nz != endz + zstep; nz += zstep) { + GetFloorDescription(msg, x, y, nz, width, height, z - nz, skip); + } + + if (skip >= 0) { + msg.addByte(skip); + msg.addByte(0xFF); + } +} + +void ProtocolGameBase::GetFloorDescription(NetworkMessage& msg, int32_t x, int32_t y, int32_t z, int32_t width, int32_t height, int32_t offset, int32_t& skip) +{ + for (int32_t nx = 0; nx < width; nx++) { + for (int32_t ny = 0; ny < height; ny++) { + Tile* tile = g_game.map.getTile(x + nx + offset, y + ny + offset, z); + if (tile) { + if (skip >= 0) { + msg.addByte(skip); + msg.addByte(0xFF); + } + + skip = 0; + GetTileDescription(tile, msg); + } else if (skip == 0xFE) { + msg.addByte(0xFF); + msg.addByte(0xFF); + skip = -1; + } else { + ++skip; + } + } + } +} + +void ProtocolGameBase::sendContainer(uint8_t cid, const Container* container, bool hasParent, uint16_t firstIndex) +{ + NetworkMessage msg; + msg.addByte(0x6E); + + msg.addByte(cid); + + if (container->getID() == ITEM_BROWSEFIELD) { + msg.addItem(ITEM_BAG, 1); + msg.addString("Browse Field"); + } else { + msg.addItem(container); + msg.addString(container->getName()); + } + + msg.addByte(container->capacity()); + + msg.addByte(hasParent ? 0x01 : 0x00); + + msg.addByte(container->isUnlocked() ? 0x01 : 0x00); // Drag and drop + msg.addByte(container->hasPagination() ? 0x01 : 0x00); // Pagination + + uint32_t containerSize = container->size(); + msg.add(containerSize); + msg.add(firstIndex); + if (firstIndex < containerSize) { + uint8_t itemsToSend = std::min(std::min(container->capacity(), containerSize - firstIndex), std::numeric_limits::max()); + + msg.addByte(itemsToSend); + for (auto it = container->getItemList().begin() + firstIndex, end = it + itemsToSend; it != end; ++it) { + msg.addItem(*it); + } + } else { + msg.addByte(0x00); + } + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendChannel(uint16_t channelId, const std::string& channelName, const UsersMap* channelUsers, const InvitedMap* invitedUsers) +{ + NetworkMessage msg; + msg.addByte(0xAC); + + msg.add(channelId); + msg.addString(channelName); + + if (channelUsers) { + msg.add(channelUsers->size()); + for (const auto& it : *channelUsers) { + msg.addString(it.second->getName()); + } + } else { + msg.add(0x00); + } + + if (invitedUsers) { + msg.add(invitedUsers->size()); + for (const auto& it : *invitedUsers) { + msg.addString(it.second->getName()); + } + } else { + msg.add(0x00); + } + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendMagicEffect(const Position& pos, uint8_t type) +{ + if (!canSee(pos)) { + return; + } + + NetworkMessage msg; + msg.addByte(0x83); + msg.addPosition(pos); + msg.addByte(type); + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendAddCreature(const Creature* creature, const Position& pos, int32_t stackpos, bool isLogin) +{ + if (!canSee(pos)) { + return; + } + + if (creature != player) { + if (stackpos != -1) { + NetworkMessage msg; + msg.addByte(0x6A); + msg.addPosition(pos); + msg.addByte(stackpos); + + bool known; + uint32_t removedKnown; + checkCreatureAsKnown(creature->getID(), known, removedKnown); + AddCreature(msg, creature, known, removedKnown); + writeToOutputBuffer(msg); + } + + if (isLogin) { + sendMagicEffect(pos, CONST_ME_TELEPORT); + } + return; + } + + NetworkMessage msg; + msg.addByte(0x17); + + msg.add(player->getID()); + msg.add(0x32); // beat duration (50) + + msg.addDouble(Creature::speedA, 3); + msg.addDouble(Creature::speedB, 3); + msg.addDouble(Creature::speedC, 3); + + // can report bugs? + if (player->getAccountType() >= ACCOUNT_TYPE_TUTOR) { + msg.addByte(0x01); + } else { + msg.addByte(0x00); + } + + msg.addByte(0x00); // can change pvp framing option + msg.addByte(0x00); // expert mode button enabled + + msg.add(0x00); // URL (string) to ingame store images + msg.add(25); // premium coin package size + + writeToOutputBuffer(msg); + + sendPendingStateEntered(); + sendEnterWorld(); + sendMapDescription(pos); + + if (isLogin) { + sendMagicEffect(pos, CONST_ME_TELEPORT); + } + + for (int i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) { + sendInventoryItem(static_cast(i), player->getInventoryItem(static_cast(i))); + } + + sendStats(); + sendSkills(); + + //gameworld light-settings + sendWorldLight(g_game.getWorldLightInfo()); + + //player light level + sendCreatureLight(creature); + + //sendVIPEntries(); + + sendBasicData(); + player->sendIcons(); +} + +void ProtocolGameBase::sendStats() +{ + NetworkMessage msg; + AddPlayerStats(msg); + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendBasicData() +{ + NetworkMessage msg; + msg.addByte(0x9F); + if (player->isPremium()) { + msg.addByte(1); + msg.add(time(nullptr) + (player->premiumDays * 86400)); + } else { + msg.addByte(0); + msg.add(0); + } + msg.addByte(player->getVocation()->getClientId()); + msg.add(0xFF); // number of known spells + for (uint8_t spellId = 0x00; spellId < 0xFF; spellId++) { + msg.addByte(spellId); + } + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendPendingStateEntered() +{ + NetworkMessage msg; + msg.addByte(0x0A); + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendEnterWorld() +{ + NetworkMessage msg; + msg.addByte(0x0F); + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendInventoryItem(slots_t slot, const Item* item) +{ + NetworkMessage msg; + if (item) { + msg.addByte(0x78); + msg.addByte(slot); + msg.addItem(item); + } else { + msg.addByte(0x79); + msg.addByte(slot); + } + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendSkills() +{ + NetworkMessage msg; + AddPlayerSkills(msg); + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendCreatureLight(const Creature* creature) +{ + if (!canSee(creature)) { + return; + } + + NetworkMessage msg; + AddCreatureLight(msg, creature); + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendWorldLight(const LightInfo& lightInfo) +{ + NetworkMessage msg; + AddWorldLight(msg, lightInfo); + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendMapDescription(const Position& pos) +{ + NetworkMessage msg; + msg.addByte(0x64); + msg.addPosition(player->getPosition()); + GetMapDescription(pos.x - 8, pos.y - 6, pos.z, 18, 14, msg); + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendVIP(uint32_t guid, const std::string& name, const std::string& description, uint32_t icon, bool notify, VipStatus_t status) +{ + NetworkMessage msg; + msg.addByte(0xD2); + msg.add(guid); + msg.addString(name); + msg.addString(description); + msg.add(std::min(10, icon)); + msg.addByte(notify ? 0x01 : 0x00); + msg.addByte(status); + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendCancelWalk() +{ + NetworkMessage msg; + msg.addByte(0xB5); + msg.addByte(player->getDirection()); + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendPing() +{ + NetworkMessage msg; + msg.addByte(0x1D); + writeToOutputBuffer(msg); +} + +void ProtocolGameBase::sendPingBack() +{ + NetworkMessage msg; + msg.addByte(0x1E); + writeToOutputBuffer(msg); +} diff --git a/src/protocolgamebase.h b/src/protocolgamebase.h new file mode 100644 index 0000000..ef105c7 --- /dev/null +++ b/src/protocolgamebase.h @@ -0,0 +1,120 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2015 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_PROTOCOLGAMEBASE_H_A28CB86652D0AF6760E43DCD9ACED40D +#define FS_PROTOCOLGAMEBASE_H_A28CB86652D0AF6760E43DCD9ACED40D + +#include "protocol.h" +#include "creature.h" +#include "outfit.h" +#include "chat.h" + +class NetworkMessage; +class Player; +class Game; +class House; +class Container; +class Tile; +class Connection; +class Quest; + +/** \brief Contains methods and member variables common to both the game and spectator protocols + */ +class ProtocolGameBase : public Protocol { + public: + // static protocol information + enum {server_sends_first = true}; + enum {protocol_identifier = 0}; // Not required as we send first + enum {use_checksum = true}; + + protected: + explicit ProtocolGameBase(Connection_ptr connection): + Protocol(connection), + player(nullptr), + eventConnect(0), + version(CLIENT_VERSION_MIN), + challengeTimestamp(0), + challengeRandom(0), + debugAssertSent(false), + acceptPackets(false) {} + + virtual void writeToOutputBuffer(const NetworkMessage& msg, bool broadcast = true) = 0; + void onConnect() final; + + void checkCreatureAsKnown(uint32_t id, bool& known, uint32_t& removedKnown); + void AddCreature(NetworkMessage& msg, const Creature* creature, bool known, uint32_t remove); + void AddPlayerStats(NetworkMessage& msg); + void AddPlayerSkills(NetworkMessage& msg); + void AddWorldLight(NetworkMessage& msg, const LightInfo& lightInfo); + void AddCreatureLight(NetworkMessage& msg, const Creature* creature); + void AddOutfit(NetworkMessage& msg, const Outfit_t& outfit); + + // translate a tile to clientreadable format + void GetTileDescription(const Tile* tile, NetworkMessage& msg); + // translate a floor to clientreadable format + void GetFloorDescription(NetworkMessage& msg, int32_t x, int32_t y, int32_t z, + int32_t width, int32_t height, int32_t offset, int32_t& skip); + // translate a map area to clientreadable format + void GetMapDescription(int32_t x, int32_t y, int32_t z, + int32_t width, int32_t height, NetworkMessage& msg); + + static void RemoveTileThing(NetworkMessage& msg, const Position& pos, uint32_t stackpos); + + void sendUpdateTile(const Tile* tile, const Position& pos); + void sendContainer(uint8_t cid, const Container* container, bool hasParent, uint16_t firstIndex); + void sendChannel(uint16_t channelId, const std::string& channelName, const UsersMap* channelUsers, const InvitedMap* invitedUsers); + void sendAddCreature(const Creature* creature, const Position& pos, int32_t stackpos, bool isLogin); + void sendMagicEffect(const Position& pos, uint8_t type); + void sendStats(); + void sendBasicData(); + void sendPendingStateEntered(); + void sendEnterWorld(); + //inventory + void sendInventoryItem(slots_t slot, const Item* item); + + void sendSkills(); + + void sendCreatureLight(const Creature* creature); + void sendWorldLight(const LightInfo& lightInfo); + void sendMapDescription(const Position& pos); + + void sendVIP(uint32_t guid, const std::string& name, const std::string& description, uint32_t icon, bool notify, VipStatus_t status); + void sendCancelWalk(); + + void sendPing(); + void sendPingBack(); + + bool canSee(int32_t x, int32_t y, int32_t z) const; + bool canSee(const Creature*) const; + bool canSee(const Position& pos) const; + + Player* player; + uint32_t eventConnect; + uint16_t version; + + uint32_t challengeTimestamp; + uint8_t challengeRandom; + + bool debugAssertSent; + bool acceptPackets; + + std::unordered_set knownCreatureSet; +}; +#endif + diff --git a/src/protocollogin.cpp b/src/protocollogin.cpp new file mode 100644 index 0000000..430dd17 --- /dev/null +++ b/src/protocollogin.cpp @@ -0,0 +1,304 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "protocollogin.h" + +#include "outputmessage.h" +#include "tasks.h" + +#include "configmanager.h" +#include "iologindata.h" +#include "ban.h" +#include "game.h" + +extern ConfigManager g_config; +extern Game g_game; + +void ProtocolLogin::disconnectClient(const std::string& message, uint16_t version) +{ + auto output = OutputMessagePool::getOutputMessage(); + + output->addByte(version >= 1076 ? 0x0B : 0x0A); + output->addString(message); + send(output); + + disconnect(); +} + +void ProtocolLogin::getCharacterList(const std::string& accountName, const std::string& password, const std::string& token, uint16_t version) +{ + Account account; + if (!IOLoginData::loginserverAuthentication(accountName, password, account)) { + disconnectClient("Account name or password is not correct.", version); + return; + } + + uint32_t ticks = time(nullptr) / AUTHENTICATOR_PERIOD; + + auto output = OutputMessagePool::getOutputMessage(); + if (!account.key.empty()) { + if (token.empty() || !(token == generateToken(account.key, ticks) || token == generateToken(account.key, ticks - 1) || token == generateToken(account.key, ticks + 1))) { + output->addByte(0x0D); + output->addByte(0); + send(output); + disconnect(); + return; + } + output->addByte(0x0C); + output->addByte(0); + } + + //Update premium days + Game::updatePremium(account); + + const std::string& motd = g_config.getString(ConfigManager::MOTD); + if (!motd.empty()) { + //Add MOTD + output->addByte(0x14); + + std::ostringstream ss; + ss << g_game.getMotdNum() << "\n" << motd; + output->addString(ss.str()); + } + + //Add session key + output->addByte(0x28); + output->addString(accountName + "\n" + password + "\n" + token + "\n" + std::to_string(ticks)); + + //Add char list + output->addByte(0x64); + + uint8_t size = std::min(std::numeric_limits::max(), account.characters.size()); + + if (g_config.getBoolean(ConfigManager::ONLINE_OFFLINE_CHARLIST)) { + output->addByte(2); // number of worlds + + for (uint8_t i = 0; i < 2; i++) { + output->addByte(i); // world id + output->addString(i == 0 ? "Offline" : "Online"); + output->addString(g_config.getString(ConfigManager::IP)); + output->add(g_config.getNumber(ConfigManager::GAME_PORT)); + output->addByte(0); + } + } else { + output->addByte(1); // number of worlds + output->addByte(0); // world id + output->addString(g_config.getString(ConfigManager::SERVER_NAME)); + output->addString(g_config.getString(ConfigManager::IP)); + output->add(g_config.getNumber(ConfigManager::GAME_PORT)); + output->addByte(0); + } + + output->addByte(size); + for (uint8_t i = 0; i < size; i++) { + const std::string& character = account.characters[i]; + if (g_config.getBoolean(ConfigManager::ONLINE_OFFLINE_CHARLIST)) { + output->addByte(g_game.getPlayerByName(character) ? 1 : 0); + } else { + output->addByte(0); + } + output->addString(character); + } + + //Add premium days + output->addByte(0); + if (g_config.getBoolean(ConfigManager::FREE_PREMIUM)) { + output->addByte(1); + output->add(0); + } else { + output->addByte(account.premiumDays > 0 ? 1 : 0); + output->add(time(nullptr) + (account.premiumDays * 86400)); + } + + send(output); + + disconnect(); +} + +void ProtocolLogin::getCastingStreamsList(const std::string& password, uint16_t version) +{ + + uint32_t ticks = time(nullptr) / AUTHENTICATOR_PERIOD; + + auto output = OutputMessagePool::getOutputMessage(); + output->addByte(0x0C); + output->addByte(0); + + + const std::string& motd = g_config.getString(ConfigManager::MOTD); + if (!motd.empty()) { + //Add MOTD + output->addByte(0x14); + + std::ostringstream ss; + ss << g_game.getMotdNum() << "\n" << motd; + output->addString(ss.str()); + } + + //Add session key + output->addByte(0x28); + output->addString(password); + + //Add char list + output->addByte(0x64); + + if (g_config.getBoolean(ConfigManager::ONLINE_OFFLINE_CHARLIST)) { + output->addByte(2); // number of worlds + + for (uint8_t i = 0; i < 2; i++) { + output->addByte(i); // world id + output->addString(i == 0 ? "Offline" : "Online"); + output->addString(g_config.getString(ConfigManager::IP)); + output->add(g_config.getNumber(ConfigManager::LIVE_CAST_PORT)); + output->addByte(0); + } + } else { + output->addByte(1); // number of worlds + output->addByte(0); // world id + output->addString(g_config.getString(ConfigManager::SERVER_NAME)); + output->addString(g_config.getString(ConfigManager::IP)); + output->add(g_config.getNumber(ConfigManager::LIVE_CAST_PORT)); + output->addByte(0); + } + + const auto& casts = ProtocolGame::getLiveCasts(); + output->addByte(casts.size()); + + for (const auto& cast : casts) { + output->addByte(0); + output->addString(cast.first->getName()); + } + + + output->addByte(0); + output->addByte(1); + output->add(0); + + + send(output); + + disconnect(); +} + +void ProtocolLogin::onRecvFirstMessage(NetworkMessage& msg) +{ + if (g_game.getGameState() == GAME_STATE_SHUTDOWN) { + disconnect(); + return; + } + + msg.skipBytes(2); // client OS + + uint16_t version = msg.get(); + if (version >= 971) { + msg.skipBytes(17); + } else { + msg.skipBytes(12); + } + /* + * Skipped bytes: + * 4 bytes: protocolVersion + * 12 bytes: dat, spr, pic signatures (4 bytes each) + * 1 byte: 0 + */ + + if (version <= 760) { + std::ostringstream ss; + ss << "Only clients with protocol " << CLIENT_VERSION_STR << " allowed!"; + disconnectClient(ss.str(), version); + return; + } + + if (!Protocol::RSA_decrypt(msg)) { + disconnect(); + return; + } + + xtea::key key; + key[0] = msg.get(); + key[1] = msg.get(); + key[2] = msg.get(); + key[3] = msg.get(); + enableXTEAEncryption(); + setXTEAKey(std::move(key)); + + if (version < CLIENT_VERSION_MIN || version > CLIENT_VERSION_MAX) { + std::ostringstream ss; + ss << "Only clients with protocol " << CLIENT_VERSION_STR << " allowed!"; + disconnectClient(ss.str(), version); + return; + } + + if (g_game.getGameState() == GAME_STATE_STARTUP) { + disconnectClient("Gameworld is starting up. Please wait.", version); + return; + } + + if (g_game.getGameState() == GAME_STATE_MAINTAIN) { + disconnectClient("Gameworld is under maintenance.\nPlease re-connect in a while.", version); + return; + } + + BanInfo banInfo; + auto connection = getConnection(); + if (!connection) { + return; + } + + if (IOBan::isIpBanned(connection->getIP(), banInfo)) { + if (banInfo.reason.empty()) { + banInfo.reason = "(none)"; + } + + std::ostringstream ss; + ss << "Your IP has been banned until " << formatDateShort(banInfo.expiresAt) << " by " << banInfo.bannedBy << ".\n\nReason specified:\n" << banInfo.reason; + disconnectClient(ss.str(), version); + return; + } + + std::string accountName = msg.getString(); + std::string password = msg.getString(); + auto thisPtr = std::static_pointer_cast(shared_from_this()); + if (accountName.empty()) { + if (g_config.getBoolean(ConfigManager::ENABLE_LIVE_CASTING)) { + g_dispatcher.addTask(createTask(std::bind(&ProtocolLogin::getCastingStreamsList, thisPtr, password, version))); + } else { + disconnectClient("Invalid account name.", version); + } + return; + } + + if (password.empty()) { + disconnectClient("Invalid password.", version); + return; + } + + // read authenticator token and stay logged in flag from last 128 bytes + msg.skipBytes((msg.getLength() - 128) - msg.getBufferPosition()); + if (!Protocol::RSA_decrypt(msg)) { + disconnectClient("Invalid authentification token.", version); + return; + } + + std::string authToken = msg.getString(); + g_dispatcher.addTask(createTask(std::bind(&ProtocolLogin::getCharacterList, thisPtr, accountName, password, authToken, version))); +} + diff --git a/src/protocollogin.h b/src/protocollogin.h new file mode 100644 index 0000000..bc71d89 --- /dev/null +++ b/src/protocollogin.h @@ -0,0 +1,51 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_PROTOCOLLOGIN_H_1238F4B473074DF2ABC595C29E81C46D +#define FS_PROTOCOLLOGIN_H_1238F4B473074DF2ABC595C29E81C46D + +#include "protocol.h" + +class NetworkMessage; +class OutputMessage; + +class ProtocolLogin : public Protocol +{ + public: + // static protocol information + enum {server_sends_first = false}; + enum {protocol_identifier = 0x01}; + enum {use_checksum = true}; + static const char* protocol_name() { + return "login protocol"; + } + + explicit ProtocolLogin(Connection_ptr connection) : Protocol(connection) {} + + void onRecvFirstMessage(NetworkMessage& msg) override; + + private: + void disconnectClient(const std::string& message, uint16_t version); + + void getCharacterList(const std::string& accountName, const std::string& password, const std::string& token, uint16_t version); + void getCastingStreamsList(const std::string& password, uint16_t version); +}; + +#endif + diff --git a/src/protocolold.cpp b/src/protocolold.cpp new file mode 100644 index 0000000..e981d56 --- /dev/null +++ b/src/protocolold.cpp @@ -0,0 +1,77 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "protocolold.h" +#include "outputmessage.h" + +#include "game.h" + +extern Game g_game; + +void ProtocolOld::disconnectClient(const std::string& message) +{ + auto output = OutputMessagePool::getOutputMessage(); + output->addByte(0x0A); + output->addString(message); + send(output); + + disconnect(); +} + +void ProtocolOld::onRecvFirstMessage(NetworkMessage& msg) +{ + if (g_game.getGameState() == GAME_STATE_SHUTDOWN) { + disconnect(); + return; + } + + /*uint16_t clientOS =*/ msg.get(); + uint16_t version = msg.get(); + msg.skipBytes(12); + + if (version <= 760) { + std::ostringstream ss; + ss << "Only clients with protocol " << CLIENT_VERSION_STR << " allowed!"; + disconnectClient(ss.str()); + return; + } + + if (!Protocol::RSA_decrypt(msg)) { + disconnect(); + return; + } + + xtea::key key; + key[0] = msg.get(); + key[1] = msg.get(); + key[2] = msg.get(); + key[3] = msg.get(); + enableXTEAEncryption(); + setXTEAKey(std::move(key)); + + if (version <= 822) { + disableChecksum(); + } + + std::ostringstream ss; + ss << "Only clients with protocol " << CLIENT_VERSION_STR << " allowed!"; + disconnectClient(ss.str()); +} diff --git a/src/protocolold.h b/src/protocolold.h new file mode 100644 index 0000000..708e763 --- /dev/null +++ b/src/protocolold.h @@ -0,0 +1,46 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_PROTOCOLOLD_H_5487B862FE144AE0904D098A3238E161 +#define FS_PROTOCOLOLD_H_5487B862FE144AE0904D098A3238E161 + +#include "protocol.h" + +class NetworkMessage; + +class ProtocolOld final : public Protocol +{ + public: + // static protocol information + enum {server_sends_first = false}; + enum {protocol_identifier = 0x01}; + enum {use_checksum = false}; + static const char* protocol_name() { + return "old login protocol"; + } + + explicit ProtocolOld(Connection_ptr connection) : Protocol(connection) {} + + void onRecvFirstMessage(NetworkMessage& msg) override; + + private: + void disconnectClient(const std::string& message); +}; + +#endif diff --git a/src/protocolspectator.cpp b/src/protocolspectator.cpp new file mode 100644 index 0000000..9ac202e --- /dev/null +++ b/src/protocolspectator.cpp @@ -0,0 +1,380 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2014 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "protocolgame.h" +#include "protocolspectator.h" + +#include "outputmessage.h" + +#include "tile.h" +#include "player.h" +#include "chat.h" + +#include "configmanager.h" + +#include "game.h" + +#include "connection.h" +#include "scheduler.h" +#include "ban.h" + +extern Game g_game; +extern ConfigManager g_config; +extern Chat* g_chat; + +ProtocolSpectator::ProtocolSpectator(Connection_ptr connection): + ProtocolGameBase(connection), + client(nullptr) +{ + +} + +void ProtocolSpectator::disconnectSpectator(const std::string& message) const +{ + auto output = OutputMessagePool::getOutputMessage(); + output->addByte(0x14); + output->addString(message); + send(std::move(output)); + disconnect(); +} + +void ProtocolSpectator::onRecvFirstMessage(NetworkMessage& msg) +{ + if (g_game.getGameState() == GAME_STATE_SHUTDOWN) { + disconnect(); + return; + } + + operatingSystem = (OperatingSystem_t)msg.get(); + version = msg.get(); + + msg.skipBytes(7); // U32 clientVersion, U8 clientType + + if (!RSA_decrypt(msg)) { + disconnect(); + return; + } + + xtea::key key; + key[0] = msg.get(); + key[1] = msg.get(); + key[2] = msg.get(); + key[3] = msg.get(); + enableXTEAEncryption(); + setXTEAKey(std::move(key)); + + if (operatingSystem >= CLIENTOS_OTCLIENT_LINUX) { + NetworkMessage opcodeMessage; + opcodeMessage.addByte(0x32); + opcodeMessage.addByte(0x00); + opcodeMessage.add(0x00); + writeToOutputBuffer(opcodeMessage); + } + + msg.skipBytes(1); // gamemaster flag + std::string password = msg.getString(); + std::string characterName = msg.getString(); + + uint32_t timeStamp = msg.get(); + uint8_t randNumber = msg.getByte(); + if (challengeTimestamp != timeStamp || challengeRandom != randNumber) { + disconnect(); + return; + } + + if (version < CLIENT_VERSION_MIN || version > CLIENT_VERSION_MAX) { + std::ostringstream ss; + ss << "Only clients with protocol " << CLIENT_VERSION_STR << " allowed!"; + disconnectSpectator(ss.str()); + return; + } + + if (g_game.getGameState() == GAME_STATE_STARTUP) { + disconnectSpectator("Gameworld is starting up. Please wait."); + return; + } + + if (g_game.getGameState() == GAME_STATE_MAINTAIN) { + disconnectSpectator("Gameworld is under maintenance.\nPlease re-connect in a while."); + return; + } + + BanInfo banInfo; + if (IOBan::isIpBanned(getIP(), banInfo)) { + if (banInfo.reason.empty()) { + banInfo.reason = "(none)"; + } + + std::ostringstream ss; + ss << "Your IP has been banned until " << formatDateShort(banInfo.expiresAt) << " by " << banInfo.bannedBy << ".\n\nReason specified:\n" << banInfo.reason; + disconnectSpectator(ss.str()); + return; + } + password.erase(password.begin()); //Erase whitespace from the front of the password string + g_dispatcher.addTask(createTask(std::bind(&ProtocolSpectator::login, std::static_pointer_cast(shared_from_this()), characterName, password))); +} + +void ProtocolSpectator::sendEmptyTileOnPlayerPos(const Tile* tile, const Position& playerPos) +{ + NetworkMessage msg; + + msg.addByte(0x69); + msg.addPosition(playerPos); + + msg.add(0x00); + msg.addItem(tile->getGround()); + + msg.addByte(0x00); + msg.addByte(0xFF); + writeToOutputBuffer(msg); +} + +void ProtocolSpectator::addDummyCreature(NetworkMessage& msg, const uint32_t& creatureID, const Position& playerPos) +{ + // add dummy creature + CreatureType_t creatureType = CREATURETYPE_NPC; + if(creatureID <= 0x10000000) { + creatureType = CREATURETYPE_PLAYER; + } else if(creatureID <= 0x40000000) { + creatureType = CREATURETYPE_MONSTER; + } + msg.addByte(0x6A); + msg.addPosition(playerPos); + msg.addByte(1); //stackpos + msg.add(0x61); // is not known + msg.add(0); // remove no creature + msg.add(creatureID); // creature id + msg.addByte(creatureType); // creature type + msg.addString("Dummy"); + msg.addByte(0x00); // health percent + msg.addByte(DIRECTION_NORTH); // direction + AddOutfit(msg, player->getCurrentOutfit()); // outfit + msg.addByte(0); // light level + msg.addByte(0); // light color + msg.add(200); // speed + msg.addByte(SKULL_NONE); // skull type + msg.addByte(SHIELD_NONE); // party shield + msg.addByte(GUILDEMBLEM_NONE); // guild emblem + msg.addByte(creatureType); // creature type + msg.addByte(SPEECHBUBBLE_NONE); // speechbubble + msg.addByte(0xFF); // MARK_UNMARKED + msg.add(0x00); // helpers + msg.addByte(0); // walkThrough +} + +void ProtocolSpectator::syncKnownCreatureSets() +{ + const auto& casterKnownCreatures = client->getKnownCreatures(); + const auto playerPos = player->getPosition(); + const auto tile = player->getTile(); + + if (!tile || !tile->getGround()) { + disconnectSpectator("A sync error has occured."); + return; + } + sendEmptyTileOnPlayerPos(tile, playerPos); + + bool known; + uint32_t removedKnown; + for (const auto creatureID : casterKnownCreatures) { + if (knownCreatureSet.find(creatureID) != knownCreatureSet.end()) { + continue; + } + + NetworkMessage msg; + const auto creature = g_game.getCreatureByID(creatureID); + if (creature && !creature->isRemoved()) { + msg.addByte(0x6A); + msg.addPosition(playerPos); + msg.addByte(1); //stackpos + checkCreatureAsKnown(creature->getID(), known, removedKnown); + AddCreature(msg, creature, known, removedKnown); + RemoveTileThing(msg, playerPos, 1); + } else if (operatingSystem <= CLIENTOS_FLASH) { // otclient freeze with huge amount of creature add, but do not debug if there are unknown creatures, best solution for now :( + addDummyCreature(msg, creatureID, playerPos); + RemoveTileThing(msg, playerPos, 1); + } + writeToOutputBuffer(msg); + } + + sendUpdateTile(tile, playerPos); +} + +void ProtocolSpectator::syncChatChannels() +{ + const auto channels = g_chat->getChannelList(*player); + for (const auto channel : channels) { + const auto& channelUsers = channel->getUsers(); + if (channelUsers.find(player->getID()) != channelUsers.end()) { + sendChannel(channel->getId(), channel->getName(), &channelUsers, channel->getInvitedUsers()); + } + } + sendChannel(CHANNEL_CAST, LIVE_CAST_CHAT_NAME, nullptr, nullptr); +} + +void ProtocolSpectator::syncOpenContainers() +{ + const auto& openContainers = player->getOpenContainers(); + for (const auto& it : openContainers) { + auto openContainer = it.second; + auto container = openContainer.container; + sendContainer(it.first, container, container->hasParent(), openContainer.index); + } +} + +void ProtocolSpectator::login(const std::string& liveCastName, const std::string& password) +{ + //dispatcher thread + auto _player = g_game.getPlayerByName(liveCastName); + if (!_player || _player->isRemoved()) { + disconnectSpectator("Live cast no longer exists. Please relogin to refresh the list."); + return; + } + + const auto liveCasterProtocol = ProtocolGame::getLiveCast(_player); + if (!liveCasterProtocol) { + disconnectSpectator("Live cast no longer exists. Please relogin to refresh the list."); + return; + } + + const auto& liveCastPassword = liveCasterProtocol->getLiveCastPassword(); + if (liveCasterProtocol->isLiveCaster()) { + if (!liveCastPassword.empty() && password != liveCastPassword) { + disconnectSpectator("Wrong live cast password."); + return; + } + + player = _player; + player->incrementReferenceCounter(); + eventConnect = 0; + client = liveCasterProtocol; + acceptPackets = true; + OutputMessagePool::getInstance().addProtocolToAutosend(shared_from_this()); + sendAddCreature(player, player->getPosition(), 0, false); + syncKnownCreatureSets(); + syncChatChannels(); + syncOpenContainers(); + + liveCasterProtocol->addSpectator(std::static_pointer_cast(shared_from_this())); + } else { + disconnectSpectator("Live cast no longer exists. Please relogin to refresh the list."); + } +} + +void ProtocolSpectator::logout() +{ + acceptPackets = false; + if (client && player) { + client->removeSpectator(std::static_pointer_cast(shared_from_this())); + player->decrementReferenceCounter(); + player = nullptr; + } + disconnect(); +} + +void ProtocolSpectator::parsePacket(NetworkMessage& msg) +{ + if (!acceptPackets || g_game.getGameState() == GAME_STATE_SHUTDOWN || msg.getLength() <= 0) { + return; + } + + uint8_t recvbyte = msg.getByte(); + + if (!player) { + if (recvbyte == 0x0F) { + disconnect(); + } + + return; + } + + //a dead player can not perform actions + if (player->isRemoved() || player->getHealth() <= 0) { + disconnect(); + return; + } + + switch (recvbyte) { + case 0x14: g_dispatcher.addTask(createTask(std::bind(&ProtocolSpectator::logout, getThis()))); break; + case 0x1D: g_dispatcher.addTask(createTask(std::bind(&ProtocolSpectator::sendPingBack, getThis()))); break; + case 0x1E: g_dispatcher.addTask(createTask(std::bind(&ProtocolSpectator::sendPing, getThis()))); break; + //Reset viewed position/direction if the spectator tries to move in any way + case 0x64: case 0x65: case 0x66: case 0x67: case 0x68: case 0x6A: case 0x6B: case 0x6C: case 0x6D: case 0x6F: case 0x70: case 0x71: + case 0x72: g_dispatcher.addTask(createTask(std::bind(&ProtocolSpectator::sendCancelWalk, getThis()))); break; + case 0x96: parseSpectatorSay(msg); break; + default: + break; + } + + if (msg.isOverrun()) { + disconnect(); + } +} + +void ProtocolSpectator::parseSpectatorSay(NetworkMessage& msg) +{ + SpeakClasses type = (SpeakClasses)msg.getByte(); + uint16_t channelId = 0; + + if (type == TALKTYPE_CHANNEL_Y) { + channelId = msg.get(); + } else { + return; + } + + const std::string text = msg.getString(); + + if (text.length() > 255 || channelId != CHANNEL_CAST || !client) { + return; + } + + if (client) { + g_dispatcher.addTask(createTask(std::bind(&ProtocolGame::broadcastSpectatorMessage, client, text))); + } +} + +void ProtocolSpectator::release() +{ + //dispatcher + if (client && player) { + client->removeSpectator(std::static_pointer_cast(shared_from_this())); + player->decrementReferenceCounter(); + player = nullptr; + } + Protocol::release(); + OutputMessagePool::getInstance().removeProtocolFromAutosend(shared_from_this()); +} + +void ProtocolSpectator::writeToOutputBuffer(const NetworkMessage& msg, bool broadcast) +{ + OutputMessage_ptr out = getOutputBuffer(msg.getLength()); + out->append(msg); +} + +void ProtocolSpectator::onLiveCastStop() +{ + //dispatcher + if (player) { + player->decrementReferenceCounter(); + player = nullptr; + } + disconnect(); +} \ No newline at end of file diff --git a/src/protocolspectator.h b/src/protocolspectator.h new file mode 100644 index 0000000..67b38b2 --- /dev/null +++ b/src/protocolspectator.h @@ -0,0 +1,66 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2014 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_PROTOCOLSPECTATOR_H_67DF1C70909A3D52458F250DF5CE2492 +#define FS_PROTOCOLSPECTATOR_H_67DF1C70909A3D52458F250DF5CE2492 + +#include "protocolgamebase.h" + +class ProtocolGame; +class ProtocolSpectator; +typedef std::shared_ptr ProtocolGame_ptr; +typedef std::shared_ptr ProtocolSpectator_ptr; + +class ProtocolSpectator final : public ProtocolGameBase +{ + public: + static const char* protocol_name() { + return "spectator protocol"; + } + + ProtocolSpectator(Connection_ptr connection); + void onLiveCastStop(); + private: + ProtocolSpectator_ptr getThis() { + return std::static_pointer_cast(shared_from_this()); + } + ProtocolGame_ptr client; + OperatingSystem_t operatingSystem; + + void login(const std::string& liveCastName, const std::string& password); + void logout(); + + void disconnectSpectator(const std::string& message) const; + void writeToOutputBuffer(const NetworkMessage& msg, bool broadcast = true) final; + + void syncKnownCreatureSets(); + void syncChatChannels(); + void syncOpenContainers(); + void sendEmptyTileOnPlayerPos(const Tile* tile, const Position& playerPos); + + void release() final; + + void parsePacket(NetworkMessage& msg) final; + void onRecvFirstMessage(NetworkMessage& msg) final; + + void parseSpectatorSay(NetworkMessage& msg); + void addDummyCreature(NetworkMessage& msg, const uint32_t& creatureID, const Position& playerPos); +}; + +#endif \ No newline at end of file diff --git a/src/protocolstatus.cpp b/src/protocolstatus.cpp new file mode 100644 index 0000000..aba1bb2 --- /dev/null +++ b/src/protocolstatus.cpp @@ -0,0 +1,228 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "protocolstatus.h" +#include "configmanager.h" +#include "game.h" +#include "outputmessage.h" + +extern ConfigManager g_config; +extern Game g_game; + +std::map ProtocolStatus::ipConnectMap; +const uint64_t ProtocolStatus::start = OTSYS_TIME(); + +enum RequestedInfo_t : uint16_t { + REQUEST_BASIC_SERVER_INFO = 1 << 0, + REQUEST_OWNER_SERVER_INFO = 1 << 1, + REQUEST_MISC_SERVER_INFO = 1 << 2, + REQUEST_PLAYERS_INFO = 1 << 3, + REQUEST_MAP_INFO = 1 << 4, + REQUEST_EXT_PLAYERS_INFO = 1 << 5, + REQUEST_PLAYER_STATUS_INFO = 1 << 6, + REQUEST_SERVER_SOFTWARE_INFO = 1 << 7, +}; + +void ProtocolStatus::onRecvFirstMessage(NetworkMessage& msg) +{ + uint32_t ip = getIP(); + if (ip != 0x0100007F) { + std::string ipStr = convertIPToString(ip); + if (ipStr != g_config.getString(ConfigManager::IP)) { + std::map::const_iterator it = ipConnectMap.find(ip); + if (it != ipConnectMap.end() && (OTSYS_TIME() < (it->second + g_config.getNumber(ConfigManager::STATUSQUERY_TIMEOUT)))) { + disconnect(); + return; + } + } + } + + ipConnectMap[ip] = OTSYS_TIME(); + + switch (msg.getByte()) { + //XML info protocol + case 0xFF: { + if (msg.getString(4) == "info") { + g_dispatcher.addTask(createTask(std::bind(&ProtocolStatus::sendStatusString, + std::static_pointer_cast(shared_from_this())))); + return; + } + break; + } + + //Another ServerInfo protocol + case 0x01: { + uint16_t requestedInfo = msg.get(); // only a Byte is necessary, though we could add new info here + std::string characterName; + if (requestedInfo & REQUEST_PLAYER_STATUS_INFO) { + characterName = msg.getString(); + } + g_dispatcher.addTask(createTask(std::bind(&ProtocolStatus::sendInfo, std::static_pointer_cast(shared_from_this()), + requestedInfo, characterName))); + return; + } + + default: + break; + } + disconnect(); +} + +void ProtocolStatus::sendStatusString() +{ + auto output = OutputMessagePool::getOutputMessage(); + + setRawMessages(true); + + pugi::xml_document doc; + + pugi::xml_node decl = doc.prepend_child(pugi::node_declaration); + decl.append_attribute("version") = "1.0"; + + pugi::xml_node tsqp = doc.append_child("tsqp"); + tsqp.append_attribute("version") = "1.0"; + + pugi::xml_node serverinfo = tsqp.append_child("serverinfo"); + uint64_t uptime = (OTSYS_TIME() - ProtocolStatus::start) / 1000; + serverinfo.append_attribute("uptime") = std::to_string(uptime).c_str(); + serverinfo.append_attribute("ip") = g_config.getString(ConfigManager::IP).c_str(); + serverinfo.append_attribute("servername") = g_config.getString(ConfigManager::SERVER_NAME).c_str(); + serverinfo.append_attribute("port") = std::to_string(g_config.getNumber(ConfigManager::LOGIN_PORT)).c_str(); + serverinfo.append_attribute("location") = g_config.getString(ConfigManager::LOCATION).c_str(); + serverinfo.append_attribute("url") = g_config.getString(ConfigManager::URL).c_str(); + serverinfo.append_attribute("server") = STATUS_SERVER_NAME; + serverinfo.append_attribute("version") = STATUS_SERVER_VERSION; + serverinfo.append_attribute("client") = CLIENT_VERSION_STR; + + pugi::xml_node owner = tsqp.append_child("owner"); + owner.append_attribute("name") = g_config.getString(ConfigManager::OWNER_NAME).c_str(); + owner.append_attribute("email") = g_config.getString(ConfigManager::OWNER_EMAIL).c_str(); + + pugi::xml_node players = tsqp.append_child("players"); + players.append_attribute("online") = std::to_string(g_game.getPlayersOnline()).c_str(); + players.append_attribute("max") = std::to_string(g_config.getNumber(ConfigManager::MAX_PLAYERS)).c_str(); + players.append_attribute("peak") = std::to_string(g_game.getPlayersRecord()).c_str(); + + pugi::xml_node monsters = tsqp.append_child("monsters"); + monsters.append_attribute("total") = std::to_string(g_game.getMonstersOnline()).c_str(); + + pugi::xml_node npcs = tsqp.append_child("npcs"); + npcs.append_attribute("total") = std::to_string(g_game.getNpcsOnline()).c_str(); + + pugi::xml_node rates = tsqp.append_child("rates"); + rates.append_attribute("experience") = std::to_string(g_config.getNumber(ConfigManager::RATE_EXPERIENCE)).c_str(); + rates.append_attribute("skill") = std::to_string(g_config.getNumber(ConfigManager::RATE_SKILL)).c_str(); + rates.append_attribute("loot") = std::to_string(g_config.getNumber(ConfigManager::RATE_LOOT)).c_str(); + rates.append_attribute("magic") = std::to_string(g_config.getNumber(ConfigManager::RATE_MAGIC)).c_str(); + rates.append_attribute("spawn") = std::to_string(g_config.getNumber(ConfigManager::RATE_SPAWN)).c_str(); + + pugi::xml_node map = tsqp.append_child("map"); + map.append_attribute("name") = g_config.getString(ConfigManager::MAP_NAME).c_str(); + map.append_attribute("author") = g_config.getString(ConfigManager::MAP_AUTHOR).c_str(); + + uint32_t mapWidth, mapHeight; + g_game.getMapDimensions(mapWidth, mapHeight); + map.append_attribute("width") = std::to_string(mapWidth).c_str(); + map.append_attribute("height") = std::to_string(mapHeight).c_str(); + + pugi::xml_node motd = tsqp.append_child("motd"); + motd.text() = g_config.getString(ConfigManager::MOTD).c_str(); + + std::ostringstream ss; + doc.save(ss, "", pugi::format_raw); + + std::string data = ss.str(); + output->addBytes(data.c_str(), data.size()); + send(output); + disconnect(); +} + +void ProtocolStatus::sendInfo(uint16_t requestedInfo, const std::string& characterName) +{ + auto output = OutputMessagePool::getOutputMessage(); + + if (requestedInfo & REQUEST_BASIC_SERVER_INFO) { + output->addByte(0x10); + output->addString(g_config.getString(ConfigManager::SERVER_NAME)); + output->addString(g_config.getString(ConfigManager::IP)); + output->addString(std::to_string(g_config.getNumber(ConfigManager::LOGIN_PORT))); + } + + if (requestedInfo & REQUEST_OWNER_SERVER_INFO) { + output->addByte(0x11); + output->addString(g_config.getString(ConfigManager::OWNER_NAME)); + output->addString(g_config.getString(ConfigManager::OWNER_EMAIL)); + } + + if (requestedInfo & REQUEST_MISC_SERVER_INFO) { + output->addByte(0x12); + output->addString(g_config.getString(ConfigManager::MOTD)); + output->addString(g_config.getString(ConfigManager::LOCATION)); + output->addString(g_config.getString(ConfigManager::URL)); + output->add((OTSYS_TIME() - ProtocolStatus::start) / 1000); + } + + if (requestedInfo & REQUEST_PLAYERS_INFO) { + output->addByte(0x20); + output->add(g_game.getPlayersOnline()); + output->add(g_config.getNumber(ConfigManager::MAX_PLAYERS)); + output->add(g_game.getPlayersRecord()); + } + + if (requestedInfo & REQUEST_MAP_INFO) { + output->addByte(0x30); + output->addString(g_config.getString(ConfigManager::MAP_NAME)); + output->addString(g_config.getString(ConfigManager::MAP_AUTHOR)); + uint32_t mapWidth, mapHeight; + g_game.getMapDimensions(mapWidth, mapHeight); + output->add(mapWidth); + output->add(mapHeight); + } + + if (requestedInfo & REQUEST_EXT_PLAYERS_INFO) { + output->addByte(0x21); // players info - online players list + + const auto& players = g_game.getPlayers(); + output->add(players.size()); + for (const auto& it : players) { + output->addString(it.second->getName()); + output->add(it.second->getLevel()); + } + } + + if (requestedInfo & REQUEST_PLAYER_STATUS_INFO) { + output->addByte(0x22); // players info - online status info of a player + if (g_game.getPlayerByName(characterName) != nullptr) { + output->addByte(0x01); + } else { + output->addByte(0x00); + } + } + + if (requestedInfo & REQUEST_SERVER_SOFTWARE_INFO) { + output->addByte(0x23); // server software info + output->addString(STATUS_SERVER_NAME); + output->addString(STATUS_SERVER_VERSION); + output->addString(CLIENT_VERSION_STR); + } + send(output); + disconnect(); +} diff --git a/src/protocolstatus.h b/src/protocolstatus.h new file mode 100644 index 0000000..5df99e4 --- /dev/null +++ b/src/protocolstatus.h @@ -0,0 +1,50 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_STATUS_H_8B28B354D65B4C0483E37AD1CA316EB4 +#define FS_STATUS_H_8B28B354D65B4C0483E37AD1CA316EB4 + +#include "networkmessage.h" +#include "protocol.h" + +class ProtocolStatus final : public Protocol +{ + public: + // static protocol information + enum {server_sends_first = false}; + enum {protocol_identifier = 0xFF}; + enum {use_checksum = false}; + static const char* protocol_name() { + return "status protocol"; + } + + explicit ProtocolStatus(Connection_ptr connection) : Protocol(connection) {} + + void onRecvFirstMessage(NetworkMessage& msg) override; + + void sendStatusString(); + void sendInfo(uint16_t requestedInfo, const std::string& characterName); + + static const uint64_t start; + + private: + static std::map ipConnectMap; +}; + +#endif diff --git a/src/pugicast.h b/src/pugicast.h new file mode 100644 index 0000000..9b188ed --- /dev/null +++ b/src/pugicast.h @@ -0,0 +1,39 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_PUGICAST_H_07810DF7954D411EB14A16C3ED2A7548 +#define FS_PUGICAST_H_07810DF7954D411EB14A16C3ED2A7548 + +#include + +namespace pugi { + template + T cast(const pugi::char_t* str) + { + T value; + try { + value = boost::lexical_cast(str); + } catch (boost::bad_lexical_cast&) { + value = T(); + } + return value; + } +} + +#endif diff --git a/src/quests.cpp b/src/quests.cpp new file mode 100644 index 0000000..2e4f552 --- /dev/null +++ b/src/quests.cpp @@ -0,0 +1,228 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "quests.h" + +#include "pugicast.h" + +std::string Mission::getDescription(Player* player) const +{ + int32_t value; + player->getStorageValue(storageID, value); + + if (!mainDescription.empty()) { + std::string desc = mainDescription; + replaceString(desc, "|STATE|", std::to_string(value)); + replaceString(desc, "\\n", "\n"); + return desc; + } + + if (ignoreEndValue) { + for (int32_t current = endValue; current >= startValue; current--) { + if (value >= current) { + auto sit = descriptions.find(current); + if (sit != descriptions.end()) { + return sit->second; + } + } + } + } else { + for (int32_t current = endValue; current >= startValue; current--) { + if (value == current) { + auto sit = descriptions.find(current); + if (sit != descriptions.end()) { + return sit->second; + } + } + } + } + return "An error has occurred, please contact a gamemaster."; +} + +bool Mission::isStarted(Player* player) const +{ + if (!player) { + return false; + } + + int32_t value; + if (!player->getStorageValue(storageID, value)) { + return false; + } + + if (value < startValue) { + return false; + } + + if (!ignoreEndValue && value > endValue) { + return false; + } + + return true; +} + +bool Mission::isCompleted(Player* player) const +{ + if (!player) { + return false; + } + + int32_t value; + if (!player->getStorageValue(storageID, value)) { + return false; + } + + if (ignoreEndValue) { + return value >= endValue; + } + + return value == endValue; +} + +std::string Mission::getName(Player* player) const +{ + if (isCompleted(player)) { + return name + " (completed)"; + } + return name; +} + +uint16_t Quest::getMissionsCount(Player* player) const +{ + uint16_t count = 0; + for (const Mission& mission : missions) { + if (mission.isStarted(player)) { + count++; + } + } + return count; +} + +bool Quest::isCompleted(Player* player) const +{ + for (const Mission& mission : missions) { + if (!mission.isCompleted(player)) { + return false; + } + } + return true; +} + +bool Quest::isStarted(Player* player) const +{ + if (!player) { + return false; + } + + int32_t value; + if (!player->getStorageValue(startStorageID, value) || value < startStorageValue) { + return false; + } + + return true; +} + +bool Quests::reload() +{ + quests.clear(); + return loadFromXml(); +} + +bool Quests::loadFromXml() +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/XML/quests.xml"); + if (!result) { + printXMLError("Error - Quests::loadFromXml", "data/XML/quests.xml", result); + return false; + } + + uint16_t id = 0; + for (auto questNode : doc.child("quests").children()) { + quests.emplace_back( + questNode.attribute("name").as_string(), + ++id, + pugi::cast(questNode.attribute("startstorageid").value()), + pugi::cast(questNode.attribute("startstoragevalue").value()) + ); + Quest& quest = quests.back(); + + for (auto missionNode : questNode.children()) { + std::string mainDescription = missionNode.attribute("description").as_string(); + + quest.missions.emplace_back( + missionNode.attribute("name").as_string(), + pugi::cast(missionNode.attribute("storageid").value()), + pugi::cast(missionNode.attribute("startvalue").value()), + pugi::cast(missionNode.attribute("endvalue").value()), + missionNode.attribute("ignoreendvalue").as_bool() + ); + Mission& mission = quest.missions.back(); + + if (mainDescription.empty()) { + for (auto missionStateNode : missionNode.children()) { + int32_t missionId = pugi::cast(missionStateNode.attribute("id").value()); + mission.descriptions.emplace(missionId, missionStateNode.attribute("description").as_string()); + } + } else { + mission.mainDescription = mainDescription; + } + } + } + return true; +} + +Quest* Quests::getQuestByID(uint16_t id) +{ + for (Quest& quest : quests) { + if (quest.id == id) { + return ? + } + } + return nullptr; +} + +uint16_t Quests::getQuestsCount(Player* player) const +{ + uint16_t count = 0; + for (const Quest& quest : quests) { + if (quest.isStarted(player)) { + count++; + } + } + return count; +} + +bool Quests::isQuestStorage(const uint32_t key, const int32_t value, const int32_t oldValue) const +{ + for (const Quest& quest : quests) { + if (quest.getStartStorageId() == key && quest.getStartStorageValue() == value) { + return true; + } + + for (const Mission& mission : quest.getMissions()) { + if (mission.getStorageId() == key && value >= mission.getStartStorageValue() && value <= mission.getEndStorageValue()) { + return mission.mainDescription.empty() || oldValue < mission.getStartStorageValue() || oldValue > mission.getEndStorageValue(); + } + } + } + return false; +} diff --git a/src/quests.h b/src/quests.h new file mode 100644 index 0000000..3ccd180 --- /dev/null +++ b/src/quests.h @@ -0,0 +1,119 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_QUESTS_H_16E44051F23547BE8097F8EA9FCAACA0 +#define FS_QUESTS_H_16E44051F23547BE8097F8EA9FCAACA0 + +#include "player.h" +#include "networkmessage.h" + +class Mission; +class Quest; + +using MissionsList = std::list; +using QuestsList = std::list; + +class Mission +{ + public: + Mission(std::string name, int32_t storageID, int32_t startValue, int32_t endValue, bool ignoreEndValue) : + name(std::move(name)), storageID(storageID), startValue(startValue), endValue(endValue), ignoreEndValue(ignoreEndValue) {} + + bool isCompleted(Player* player) const; + bool isStarted(Player* player) const; + std::string getName(Player* player) const; + std::string getDescription(Player* player) const; + + uint32_t getStorageId() const { + return storageID; + } + int32_t getStartStorageValue() const { + return startValue; + } + int32_t getEndStorageValue() const { + return endValue; + } + + std::map descriptions; + std::string mainDescription; + + private: + std::string name; + uint32_t storageID; + int32_t startValue, endValue; + bool ignoreEndValue; +}; + +class Quest +{ + public: + Quest(std::string name, uint16_t id, int32_t startStorageID, int32_t startStorageValue) : + name(std::move(name)), startStorageID(startStorageID), startStorageValue(startStorageValue), id(id) {} + + bool isCompleted(Player* player) const; + bool isStarted(Player* player) const; + uint16_t getID() const { + return id; + } + std::string getName() const { + return name; + } + uint16_t getMissionsCount(Player* player) const; + + uint32_t getStartStorageId() const { + return startStorageID; + } + int32_t getStartStorageValue() const { + return startStorageValue; + } + + const MissionsList& getMissions() const { + return missions; + } + + private: + std::string name; + + uint32_t startStorageID; + int32_t startStorageValue; + uint16_t id; + + MissionsList missions; + + friend class Quests; +}; + +class Quests +{ + public: + const QuestsList& getQuests() const { + return quests; + } + + bool loadFromXml(); + Quest* getQuestByID(uint16_t id); + bool isQuestStorage(const uint32_t key, const int32_t value, const int32_t oldValue) const; + uint16_t getQuestsCount(Player* player) const; + bool reload(); + + private: + QuestsList quests; +}; + +#endif diff --git a/src/raids.cpp b/src/raids.cpp new file mode 100644 index 0000000..d831bdc --- /dev/null +++ b/src/raids.cpp @@ -0,0 +1,589 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "raids.h" + +#include "pugicast.h" + +#include "game.h" +#include "configmanager.h" +#include "scheduler.h" +#include "monster.h" + +extern Game g_game; +extern ConfigManager g_config; + +Raids::Raids() +{ + scriptInterface.initState(); +} + +Raids::~Raids() +{ + for (Raid* raid : raidList) { + delete raid; + } +} + +bool Raids::loadFromXml() +{ + if (isLoaded()) { + return true; + } + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/raids/raids.xml"); + if (!result) { + printXMLError("Error - Raids::loadFromXml", "data/raids/raids.xml", result); + return false; + } + + for (auto raidNode : doc.child("raids").children()) { + std::string name, file; + uint32_t interval, margin; + + pugi::xml_attribute attr; + if ((attr = raidNode.attribute("name"))) { + name = attr.as_string(); + } else { + std::cout << "[Error - Raids::loadFromXml] Name tag missing for raid" << std::endl; + continue; + } + + if ((attr = raidNode.attribute("file"))) { + file = attr.as_string(); + } else { + std::ostringstream ss; + ss << "raids/" << name << ".xml"; + file = ss.str(); + std::cout << "[Warning - Raids::loadFromXml] File tag missing for raid " << name << ". Using default: " << file << std::endl; + } + + interval = pugi::cast(raidNode.attribute("interval2").value()) * 60; + if (interval == 0) { + std::cout << "[Error - Raids::loadFromXml] interval2 tag missing or zero (would divide by 0) for raid: " << name << std::endl; + continue; + } + + if ((attr = raidNode.attribute("margin"))) { + margin = pugi::cast(attr.value()) * 60 * 1000; + } else { + std::cout << "[Warning - Raids::loadFromXml] margin tag missing for raid: " << name << std::endl; + margin = 0; + } + + bool repeat; + if ((attr = raidNode.attribute("repeat"))) { + repeat = booleanString(attr.as_string()); + } else { + repeat = false; + } + + Raid* newRaid = new Raid(name, interval, margin, repeat); + if (newRaid->loadFromXml("data/raids/" + file)) { + raidList.push_back(newRaid); + } else { + std::cout << "[Error - Raids::loadFromXml] Failed to load raid: " << name << std::endl; + delete newRaid; + } + } + + loaded = true; + return true; +} + +static constexpr int32_t MAX_RAND_RANGE = 10000000; + +bool Raids::startup() +{ + if (!isLoaded() || isStarted()) { + return false; + } + + setLastRaidEnd(OTSYS_TIME()); + + checkRaidsEvent = g_scheduler.addEvent(createSchedulerTask(CHECK_RAIDS_INTERVAL * 1000, std::bind(&Raids::checkRaids, this))); + + started = true; + return started; +} + +void Raids::checkRaids() +{ + if (!getRunning()) { + uint64_t now = OTSYS_TIME(); + + for (auto it = raidList.begin(), end = raidList.end(); it != end; ++it) { + Raid* raid = *it; + if (now >= (getLastRaidEnd() + raid->getMargin())) { + if (((MAX_RAND_RANGE * CHECK_RAIDS_INTERVAL) / raid->getInterval()) >= static_cast(uniform_random(0, MAX_RAND_RANGE))) { + setRunning(raid); + raid->startRaid(); + + if (!raid->canBeRepeated()) { + raidList.erase(it); + } + break; + } + } + } + } + + checkRaidsEvent = g_scheduler.addEvent(createSchedulerTask(CHECK_RAIDS_INTERVAL * 1000, std::bind(&Raids::checkRaids, this))); +} + +void Raids::clear() +{ + g_scheduler.stopEvent(checkRaidsEvent); + checkRaidsEvent = 0; + + for (Raid* raid : raidList) { + raid->stopEvents(); + delete raid; + } + raidList.clear(); + + loaded = false; + started = false; + running = nullptr; + lastRaidEnd = 0; + + scriptInterface.reInitState(); +} + +bool Raids::reload() +{ + clear(); + return loadFromXml(); +} + +Raid* Raids::getRaidByName(const std::string& name) +{ + for (Raid* raid : raidList) { + if (strcasecmp(raid->getName().c_str(), name.c_str()) == 0) { + return raid; + } + } + return nullptr; +} + +Raid::~Raid() +{ + for (RaidEvent* raidEvent : raidEvents) { + delete raidEvent; + } +} + +bool Raid::loadFromXml(const std::string& filename) +{ + if (isLoaded()) { + return true; + } + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(filename.c_str()); + if (!result) { + printXMLError("Error - Raid::loadFromXml", filename, result); + return false; + } + + for (auto eventNode : doc.child("raid").children()) { + RaidEvent* event; + if (strcasecmp(eventNode.name(), "announce") == 0) { + event = new AnnounceEvent(); + } else if (strcasecmp(eventNode.name(), "singlespawn") == 0) { + event = new SingleSpawnEvent(); + } else if (strcasecmp(eventNode.name(), "areaspawn") == 0) { + event = new AreaSpawnEvent(); + } else if (strcasecmp(eventNode.name(), "script") == 0) { + event = new ScriptEvent(&g_game.raids.getScriptInterface()); + } else { + continue; + } + + if (event->configureRaidEvent(eventNode)) { + raidEvents.push_back(event); + } else { + std::cout << "[Error - Raid::loadFromXml] In file (" << filename << "), eventNode: " << eventNode.name() << std::endl; + delete event; + } + } + + //sort by delay time + std::sort(raidEvents.begin(), raidEvents.end(), [](const RaidEvent* lhs, const RaidEvent* rhs) { + return lhs->getDelay() < rhs->getDelay(); + }); + + loaded = true; + return true; +} + +void Raid::startRaid() +{ + RaidEvent* raidEvent = getNextRaidEvent(); + if (raidEvent) { + state = RAIDSTATE_EXECUTING; + nextEventEvent = g_scheduler.addEvent(createSchedulerTask(raidEvent->getDelay(), std::bind(&Raid::executeRaidEvent, this, raidEvent))); + } +} + +void Raid::executeRaidEvent(RaidEvent* raidEvent) +{ + if (raidEvent->executeEvent()) { + nextEvent++; + RaidEvent* newRaidEvent = getNextRaidEvent(); + + if (newRaidEvent) { + uint32_t ticks = static_cast(std::max(RAID_MINTICKS, newRaidEvent->getDelay() - raidEvent->getDelay())); + nextEventEvent = g_scheduler.addEvent(createSchedulerTask(ticks, std::bind(&Raid::executeRaidEvent, this, newRaidEvent))); + } else { + resetRaid(); + } + } else { + resetRaid(); + } +} + +void Raid::resetRaid() +{ + nextEvent = 0; + state = RAIDSTATE_IDLE; + g_game.raids.setRunning(nullptr); + g_game.raids.setLastRaidEnd(OTSYS_TIME()); +} + +void Raid::stopEvents() +{ + if (nextEventEvent != 0) { + g_scheduler.stopEvent(nextEventEvent); + nextEventEvent = 0; + } +} + +RaidEvent* Raid::getNextRaidEvent() +{ + if (nextEvent < raidEvents.size()) { + return raidEvents[nextEvent]; + } else { + return nullptr; + } +} + +bool RaidEvent::configureRaidEvent(const pugi::xml_node& eventNode) +{ + pugi::xml_attribute delayAttribute = eventNode.attribute("delay"); + if (!delayAttribute) { + std::cout << "[Error] Raid: delay tag missing." << std::endl; + return false; + } + + delay = std::max(RAID_MINTICKS, pugi::cast(delayAttribute.value())); + return true; +} + +bool AnnounceEvent::configureRaidEvent(const pugi::xml_node& eventNode) +{ + if (!RaidEvent::configureRaidEvent(eventNode)) { + return false; + } + + pugi::xml_attribute messageAttribute = eventNode.attribute("message"); + if (!messageAttribute) { + std::cout << "[Error] Raid: message tag missing for announce event." << std::endl; + return false; + } + message = messageAttribute.as_string(); + + pugi::xml_attribute typeAttribute = eventNode.attribute("type"); + if (typeAttribute) { + std::string tmpStrValue = asLowerCaseString(typeAttribute.as_string()); + if (tmpStrValue == "warning") { + messageType = MESSAGE_STATUS_WARNING; + } else if (tmpStrValue == "event") { + messageType = MESSAGE_EVENT_ADVANCE; + } else if (tmpStrValue == "default") { + messageType = MESSAGE_EVENT_DEFAULT; + } else if (tmpStrValue == "description") { + messageType = MESSAGE_INFO_DESCR; + } else if (tmpStrValue == "smallstatus") { + messageType = MESSAGE_STATUS_SMALL; + } else if (tmpStrValue == "blueconsole") { + messageType = MESSAGE_STATUS_CONSOLE_BLUE; + } else if (tmpStrValue == "redconsole") { + messageType = MESSAGE_STATUS_CONSOLE_RED; + } else { + std::cout << "[Notice] Raid: Unknown type tag missing for announce event. Using default: " << static_cast(messageType) << std::endl; + } + } else { + messageType = MESSAGE_EVENT_ADVANCE; + std::cout << "[Notice] Raid: type tag missing for announce event. Using default: " << static_cast(messageType) << std::endl; + } + return true; +} + +bool AnnounceEvent::executeEvent() +{ + g_game.broadcastMessage(message, messageType); + return true; +} + +bool SingleSpawnEvent::configureRaidEvent(const pugi::xml_node& eventNode) +{ + if (!RaidEvent::configureRaidEvent(eventNode)) { + return false; + } + + pugi::xml_attribute attr; + if ((attr = eventNode.attribute("name"))) { + monsterName = attr.as_string(); + } else { + std::cout << "[Error] Raid: name tag missing for singlespawn event." << std::endl; + return false; + } + + if ((attr = eventNode.attribute("x"))) { + position.x = pugi::cast(attr.value()); + } else { + std::cout << "[Error] Raid: x tag missing for singlespawn event." << std::endl; + return false; + } + + if ((attr = eventNode.attribute("y"))) { + position.y = pugi::cast(attr.value()); + } else { + std::cout << "[Error] Raid: y tag missing for singlespawn event." << std::endl; + return false; + } + + if ((attr = eventNode.attribute("z"))) { + position.z = pugi::cast(attr.value()); + } else { + std::cout << "[Error] Raid: z tag missing for singlespawn event." << std::endl; + return false; + } + return true; +} + +bool SingleSpawnEvent::executeEvent() +{ + Monster* monster = Monster::createMonster(monsterName); + if (!monster) { + std::cout << "[Error] Raids: Cant create monster " << monsterName << std::endl; + return false; + } + + if (!g_game.placeCreature(monster, position, false, true)) { + delete monster; + std::cout << "[Error] Raids: Cant place monster " << monsterName << std::endl; + return false; + } + return true; +} + +bool AreaSpawnEvent::configureRaidEvent(const pugi::xml_node& eventNode) +{ + if (!RaidEvent::configureRaidEvent(eventNode)) { + return false; + } + + pugi::xml_attribute attr; + if ((attr = eventNode.attribute("radius"))) { + int32_t radius = pugi::cast(attr.value()); + Position centerPos; + + if ((attr = eventNode.attribute("centerx"))) { + centerPos.x = pugi::cast(attr.value()); + } else { + std::cout << "[Error] Raid: centerx tag missing for areaspawn event." << std::endl; + return false; + } + + if ((attr = eventNode.attribute("centery"))) { + centerPos.y = pugi::cast(attr.value()); + } else { + std::cout << "[Error] Raid: centery tag missing for areaspawn event." << std::endl; + return false; + } + + if ((attr = eventNode.attribute("centerz"))) { + centerPos.z = pugi::cast(attr.value()); + } else { + std::cout << "[Error] Raid: centerz tag missing for areaspawn event." << std::endl; + return false; + } + + fromPos.x = std::max(0, centerPos.getX() - radius); + fromPos.y = std::max(0, centerPos.getY() - radius); + fromPos.z = centerPos.z; + + toPos.x = std::min(0xFFFF, centerPos.getX() + radius); + toPos.y = std::min(0xFFFF, centerPos.getY() + radius); + toPos.z = centerPos.z; + } else { + if ((attr = eventNode.attribute("fromx"))) { + fromPos.x = pugi::cast(attr.value()); + } else { + std::cout << "[Error] Raid: fromx tag missing for areaspawn event." << std::endl; + return false; + } + + if ((attr = eventNode.attribute("fromy"))) { + fromPos.y = pugi::cast(attr.value()); + } else { + std::cout << "[Error] Raid: fromy tag missing for areaspawn event." << std::endl; + return false; + } + + if ((attr = eventNode.attribute("fromz"))) { + fromPos.z = pugi::cast(attr.value()); + } else { + std::cout << "[Error] Raid: fromz tag missing for areaspawn event." << std::endl; + return false; + } + + if ((attr = eventNode.attribute("tox"))) { + toPos.x = pugi::cast(attr.value()); + } else { + std::cout << "[Error] Raid: tox tag missing for areaspawn event." << std::endl; + return false; + } + + if ((attr = eventNode.attribute("toy"))) { + toPos.y = pugi::cast(attr.value()); + } else { + std::cout << "[Error] Raid: toy tag missing for areaspawn event." << std::endl; + return false; + } + + if ((attr = eventNode.attribute("toz"))) { + toPos.z = pugi::cast(attr.value()); + } else { + std::cout << "[Error] Raid: toz tag missing for areaspawn event." << std::endl; + return false; + } + } + + for (auto monsterNode : eventNode.children()) { + const char* name; + + if ((attr = monsterNode.attribute("name"))) { + name = attr.value(); + } else { + std::cout << "[Error] Raid: name tag missing for monster node." << std::endl; + return false; + } + + uint32_t minAmount; + if ((attr = monsterNode.attribute("minamount"))) { + minAmount = pugi::cast(attr.value()); + } else { + minAmount = 0; + } + + uint32_t maxAmount; + if ((attr = monsterNode.attribute("maxamount"))) { + maxAmount = pugi::cast(attr.value()); + } else { + maxAmount = 0; + } + + if (maxAmount == 0 && minAmount == 0) { + if ((attr = monsterNode.attribute("amount"))) { + minAmount = pugi::cast(attr.value()); + maxAmount = minAmount; + } else { + std::cout << "[Error] Raid: amount tag missing for monster node." << std::endl; + return false; + } + } + + spawnList.emplace_back(name, minAmount, maxAmount); + } + return true; +} + +bool AreaSpawnEvent::executeEvent() +{ + for (const MonsterSpawn& spawn : spawnList) { + uint32_t amount = uniform_random(spawn.minAmount, spawn.maxAmount); + for (uint32_t i = 0; i < amount; ++i) { + Monster* monster = Monster::createMonster(spawn.name); + if (!monster) { + std::cout << "[Error - AreaSpawnEvent::executeEvent] Can't create monster " << spawn.name << std::endl; + return false; + } + + bool success = false; + for (int32_t tries = 0; tries < MAXIMUM_TRIES_PER_MONSTER; tries++) { + Tile* tile = g_game.map.getTile(uniform_random(fromPos.x, toPos.x), uniform_random(fromPos.y, toPos.y), uniform_random(fromPos.z, toPos.z)); + if (tile && !tile->isMoveableBlocking() && !tile->hasFlag(TILESTATE_PROTECTIONZONE) && tile->getTopCreature() == nullptr && g_game.placeCreature(monster, tile->getPosition(), false, true)) { + success = true; + break; + } + } + + if (!success) { + delete monster; + } + } + } + return true; +} + +bool ScriptEvent::configureRaidEvent(const pugi::xml_node& eventNode) +{ + if (!RaidEvent::configureRaidEvent(eventNode)) { + return false; + } + + pugi::xml_attribute scriptAttribute = eventNode.attribute("script"); + if (!scriptAttribute) { + std::cout << "Error: [ScriptEvent::configureRaidEvent] No script file found for raid" << std::endl; + return false; + } + + if (!loadScript("data/raids/scripts/" + std::string(scriptAttribute.as_string()))) { + std::cout << "Error: [ScriptEvent::configureRaidEvent] Can not load raid script." << std::endl; + return false; + } + return true; +} + +std::string ScriptEvent::getScriptEventName() const +{ + return "onRaid"; +} + +bool ScriptEvent::executeEvent() +{ + //onRaid() + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - ScriptEvent::onRaid] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + scriptInterface->pushFunction(scriptId); + + return scriptInterface->callFunction(0); +} diff --git a/src/raids.h b/src/raids.h new file mode 100644 index 0000000..bb6d75d --- /dev/null +++ b/src/raids.h @@ -0,0 +1,228 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_RAIDS_H_3583C7C054584881856D55765DEDAFA9 +#define FS_RAIDS_H_3583C7C054584881856D55765DEDAFA9 + +#include "const.h" +#include "position.h" +#include "baseevents.h" + +enum RaidState_t { + RAIDSTATE_IDLE, + RAIDSTATE_EXECUTING, +}; + +struct MonsterSpawn { + MonsterSpawn(std::string name, uint32_t minAmount, uint32_t maxAmount) : + name(std::move(name)), minAmount(minAmount), maxAmount(maxAmount) {} + + std::string name; + uint32_t minAmount; + uint32_t maxAmount; +}; + +//How many times it will try to find a tile to add the monster to before giving up +static constexpr int32_t MAXIMUM_TRIES_PER_MONSTER = 10; +static constexpr int32_t CHECK_RAIDS_INTERVAL = 60; +static constexpr int32_t RAID_MINTICKS = 1000; + +class Raid; +class RaidEvent; + +class Raids +{ + public: + Raids(); + ~Raids(); + + // non-copyable + Raids(const Raids&) = delete; + Raids& operator=(const Raids&) = delete; + + bool loadFromXml(); + bool startup(); + + void clear(); + bool reload(); + + bool isLoaded() const { + return loaded; + } + bool isStarted() const { + return started; + } + + Raid* getRunning() { + return running; + } + void setRunning(Raid* newRunning) { + running = newRunning; + } + + Raid* getRaidByName(const std::string& name); + + uint64_t getLastRaidEnd() const { + return lastRaidEnd; + } + void setLastRaidEnd(uint64_t newLastRaidEnd) { + lastRaidEnd = newLastRaidEnd; + } + + void checkRaids(); + + LuaScriptInterface& getScriptInterface() { + return scriptInterface; + } + + private: + LuaScriptInterface scriptInterface{"Raid Interface"}; + + std::list raidList; + Raid* running = nullptr; + uint64_t lastRaidEnd = 0; + uint32_t checkRaidsEvent = 0; + bool loaded = false; + bool started = false; +}; + +class Raid +{ + public: + Raid(std::string name, uint32_t interval, uint32_t marginTime, bool repeat) : + name(std::move(name)), interval(interval), margin(marginTime), repeat(repeat) {} + ~Raid(); + + // non-copyable + Raid(const Raid&) = delete; + Raid& operator=(const Raid&) = delete; + + bool loadFromXml(const std::string& filename); + + void startRaid(); + + void executeRaidEvent(RaidEvent* raidEvent); + void resetRaid(); + + RaidEvent* getNextRaidEvent(); + void setState(RaidState_t newState) { + state = newState; + } + const std::string& getName() const { + return name; + } + + bool isLoaded() const { + return loaded; + } + uint64_t getMargin() const { + return margin; + } + uint32_t getInterval() const { + return interval; + } + bool canBeRepeated() const { + return repeat; + } + + void stopEvents(); + + private: + std::vector raidEvents; + std::string name; + uint32_t interval; + uint32_t nextEvent = 0; + uint64_t margin; + RaidState_t state = RAIDSTATE_IDLE; + uint32_t nextEventEvent = 0; + bool loaded = false; + bool repeat; +}; + +class RaidEvent +{ + public: + virtual ~RaidEvent() = default; + + virtual bool configureRaidEvent(const pugi::xml_node& eventNode); + + virtual bool executeEvent() = 0; + uint32_t getDelay() const { + return delay; + } + + private: + uint32_t delay; +}; + +class AnnounceEvent final : public RaidEvent +{ + public: + AnnounceEvent() = default; + + bool configureRaidEvent(const pugi::xml_node& eventNode) override; + + bool executeEvent() override; + + private: + std::string message; + MessageClasses messageType = MESSAGE_EVENT_ADVANCE; +}; + +class SingleSpawnEvent final : public RaidEvent +{ + public: + bool configureRaidEvent(const pugi::xml_node& eventNode) override; + + bool executeEvent() override; + + private: + std::string monsterName; + Position position; +}; + +class AreaSpawnEvent final : public RaidEvent +{ + public: + bool configureRaidEvent(const pugi::xml_node& eventNode) override; + + bool executeEvent() override; + + private: + std::list spawnList; + Position fromPos, toPos; +}; + +class ScriptEvent final : public RaidEvent, public Event +{ + public: + explicit ScriptEvent(LuaScriptInterface* interface) : Event(interface) {} + + bool configureRaidEvent(const pugi::xml_node& eventNode) override; + bool configureEvent(const pugi::xml_node&) override { + return false; + } + + bool executeEvent() override; + + private: + std::string getScriptEventName() const override; +}; + +#endif diff --git a/src/rsa.cpp b/src/rsa.cpp new file mode 100644 index 0000000..0b754a7 --- /dev/null +++ b/src/rsa.cpp @@ -0,0 +1,83 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "rsa.h" + +#include +#include + +#include +#include + +static CryptoPP::AutoSeededRandomPool prng; + +void RSA::decrypt(char* msg) const +{ + try { + CryptoPP::Integer m{reinterpret_cast(msg), 128}; + auto c = pk.CalculateInverse(prng, m); + c.Encode(reinterpret_cast(msg), 128); + } catch (const CryptoPP::Exception& e) { + std::cout << e.what() << '\n'; + } +} + +static const std::string header = "-----BEGIN RSA PRIVATE KEY-----"; +static const std::string footer = "-----END RSA PRIVATE KEY-----"; + +void RSA::loadPEM(const std::string& filename) +{ + std::ifstream file{filename}; + + if (!file.is_open()) { + throw std::runtime_error("Missing file " + filename + "."); + } + + std::ostringstream oss; + for (std::string line; std::getline(file, line); oss << line); + std::string key = oss.str(); + + if (key.substr(0, header.size()) != header) { + throw std::runtime_error("Missing RSA private key header."); + } + + if (key.substr(key.size() - footer.size(), footer.size()) != footer) { + throw std::runtime_error("Missing RSA private key footer."); + } + + key = key.substr(header.size(), key.size() - footer.size()); + + CryptoPP::ByteQueue queue; + CryptoPP::Base64Decoder decoder; + decoder.Attach(new CryptoPP::Redirector(queue)); + decoder.Put(reinterpret_cast(key.c_str()), key.size()); + decoder.MessageEnd(); + + try { + pk.BERDecodePrivateKey(queue, false, queue.MaxRetrievable()); + + if (!pk.Validate(prng, 3)) { + throw std::runtime_error("RSA private key is not valid."); + } + } catch (const CryptoPP::Exception& e) { + std::cout << e.what() << '\n'; + } +} diff --git a/src/rsa.h b/src/rsa.h new file mode 100644 index 0000000..888e3b6 --- /dev/null +++ b/src/rsa.h @@ -0,0 +1,43 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_RSA_H_C4E277DA8E884B578DDBF0566F504E91 +#define FS_RSA_H_C4E277DA8E884B578DDBF0566F504E91 + +#include + +#include + +class RSA +{ + public: + RSA() = default; + + // non-copyable + RSA(const RSA&) = delete; + RSA& operator=(const RSA&) = delete; + + void loadPEM(const std::string& filename); + void decrypt(char* msg) const; + + private: + CryptoPP::RSA::PrivateKey pk; +}; + +#endif diff --git a/src/scheduler.cpp b/src/scheduler.cpp new file mode 100644 index 0000000..f489f61 --- /dev/null +++ b/src/scheduler.cpp @@ -0,0 +1,138 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "scheduler.h" + +void Scheduler::threadMain() +{ + std::unique_lock eventLockUnique(eventLock, std::defer_lock); + while (getState() != THREAD_STATE_TERMINATED) { + std::cv_status ret = std::cv_status::no_timeout; + + eventLockUnique.lock(); + if (eventList.empty()) { + eventSignal.wait(eventLockUnique); + } else { + ret = eventSignal.wait_until(eventLockUnique, eventList.top()->getCycle()); + } + + // the mutex is locked again now... + if (ret == std::cv_status::timeout && !eventList.empty()) { + // ok we had a timeout, so there has to be an event we have to execute... + SchedulerTask* task = eventList.top(); + eventList.pop(); + + // check if the event was stopped + auto it = eventIds.find(task->getEventId()); + if (it == eventIds.end()) { + eventLockUnique.unlock(); + delete task; + continue; + } + eventIds.erase(it); + eventLockUnique.unlock(); + + task->setDontExpire(); + g_dispatcher.addTask(task, true); + } else { + eventLockUnique.unlock(); + } + } +} + +uint32_t Scheduler::addEvent(SchedulerTask* task) +{ + eventLock.lock(); + + if (getState() != THREAD_STATE_RUNNING) { + eventLock.unlock(); + delete task; + return 0; + } + + // check if the event has a valid id + if (task->getEventId() == 0) { + // if not generate one + if (++lastEventId == 0) { + lastEventId = 1; + } + + task->setEventId(lastEventId); + } + + // insert the event id in the list of active events + uint32_t eventId = task->getEventId(); + eventIds.insert(eventId); + + // add the event to the queue + eventList.push(task); + + // if the list was empty or this event is the top in the list + // we have to signal it + bool do_signal = (task == eventList.top()); + + eventLock.unlock(); + + if (do_signal) { + eventSignal.notify_one(); + } + + return eventId; +} + +bool Scheduler::stopEvent(uint32_t eventId) +{ + if (eventId == 0) { + return false; + } + + std::lock_guard lockClass(eventLock); + + // search the event id.. + auto it = eventIds.find(eventId); + if (it == eventIds.end()) { + return false; + } + + eventIds.erase(it); + return true; +} + +void Scheduler::shutdown() +{ + setState(THREAD_STATE_TERMINATED); + eventLock.lock(); + + //this list should already be empty + while (!eventList.empty()) { + delete eventList.top(); + eventList.pop(); + } + + eventIds.clear(); + eventLock.unlock(); + eventSignal.notify_one(); +} + +SchedulerTask* createSchedulerTask(uint32_t delay, std::function f) +{ + return new SchedulerTask(delay, std::move(f)); +} diff --git a/src/scheduler.h b/src/scheduler.h new file mode 100644 index 0000000..e42f59f --- /dev/null +++ b/src/scheduler.h @@ -0,0 +1,83 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_SCHEDULER_H_2905B3D5EAB34B4BA8830167262D2DC1 +#define FS_SCHEDULER_H_2905B3D5EAB34B4BA8830167262D2DC1 + +#include "tasks.h" +#include +#include + +#include "thread_holder_base.h" + +static constexpr int32_t SCHEDULER_MINTICKS = 50; + +class SchedulerTask : public Task +{ + public: + void setEventId(uint32_t id) { + eventId = id; + } + uint32_t getEventId() const { + return eventId; + } + + std::chrono::system_clock::time_point getCycle() const { + return expiration; + } + + private: + SchedulerTask(uint32_t delay, std::function&& f) : Task(delay, std::move(f)) {} + + uint32_t eventId = 0; + + friend SchedulerTask* createSchedulerTask(uint32_t, std::function); +}; + +SchedulerTask* createSchedulerTask(uint32_t delay, std::function f); + +struct TaskComparator { + bool operator()(const SchedulerTask* lhs, const SchedulerTask* rhs) const { + return lhs->getCycle() > rhs->getCycle(); + } +}; + +class Scheduler : public ThreadHolder +{ + public: + uint32_t addEvent(SchedulerTask* task); + bool stopEvent(uint32_t eventId); + + void shutdown(); + + void threadMain(); + + private: + std::thread thread; + std::mutex eventLock; + std::condition_variable eventSignal; + + uint32_t lastEventId {0}; + std::priority_queue, TaskComparator> eventList; + std::unordered_set eventIds; +}; + +extern Scheduler g_scheduler; + +#endif diff --git a/src/script.cpp b/src/script.cpp new file mode 100644 index 0000000..144d9d2 --- /dev/null +++ b/src/script.cpp @@ -0,0 +1,99 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "script.h" +#include +#include "configmanager.h" + +extern LuaEnvironment g_luaEnvironment; +extern ConfigManager g_config; + +Scripts::Scripts() : + scriptInterface("Scripts Interface") +{ + scriptInterface.initState(); +} + +Scripts::~Scripts() +{ + scriptInterface.reInitState(); +} + +bool Scripts::loadScripts(std::string folderName, bool isLib, bool reload) +{ + namespace fs = boost::filesystem; + + const auto dir = fs::current_path() / "data" / folderName; + if(!fs::exists(dir) || !fs::is_directory(dir)) { + std::cout << "[Warning - Scripts::loadScripts] Can not load folder '" << folderName << "'." << std::endl; + return false; + } + + fs::recursive_directory_iterator endit; + std::vector v; + std::string disable = ("#"); + for(fs::recursive_directory_iterator it(dir); it != endit; ++it) { + auto fn = it->path().parent_path().filename(); + if ((fn == "lib" && !isLib) || fn == "events") { + continue; + } + if(fs::is_regular_file(*it) && it->path().extension() == ".lua") { + size_t found = it->path().filename().string().find(disable); + if (found != std::string::npos) { + if (g_config.getBoolean(ConfigManager::SCRIPTS_CONSOLE_LOGS)) { + std::cout << "> " << it->path().filename().string() << " [disabled]" << std::endl; + } + continue; + } + v.push_back(it->path()); + } + } + sort(v.begin(), v.end()); + std::string redir; + for (auto it = v.begin(); it != v.end(); ++it) { + const std::string scriptFile = it->string(); + if (!isLib) { + if (redir.empty() || redir != it->parent_path().string()) { + auto p = fs::path(it->relative_path()); + if (g_config.getBoolean(ConfigManager::SCRIPTS_CONSOLE_LOGS)) { + std::cout << ">> [" << p.parent_path().filename() << "]" << std::endl; + } + redir = it->parent_path().string(); + } + } + + if(scriptInterface.loadFile(scriptFile) == -1) { + std::cout << "> " << it->filename().string() << " [error]" << std::endl; + std::cout << "^ " << scriptInterface.getLastLuaError() << std::endl; + continue; + } + + if (g_config.getBoolean(ConfigManager::SCRIPTS_CONSOLE_LOGS)) { + if (!reload) { + std::cout << "> " << it->filename().string() << " [loaded]" << std::endl; + } else { + std::cout << "> " << it->filename().string() << " [reloaded]" << std::endl; + } + } + } + + return true; +} diff --git a/src/script.h b/src/script.h new file mode 100644 index 0000000..1a23cb3 --- /dev/null +++ b/src/script.h @@ -0,0 +1,40 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_SCRIPTS_H +#define FS_SCRIPTS_H + +#include "luascript.h" +#include "enums.h" + +class Scripts +{ + public: + Scripts(); + ~Scripts(); + + bool loadScripts(std::string folderName, bool isLib, bool reload); + LuaScriptInterface& getScriptInterface() { + return scriptInterface; + } + private: + LuaScriptInterface scriptInterface; +}; + +#endif diff --git a/src/scriptmanager.cpp b/src/scriptmanager.cpp new file mode 100644 index 0000000..b166da7 --- /dev/null +++ b/src/scriptmanager.cpp @@ -0,0 +1,127 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "scriptmanager.h" + +#include "actions.h" +#include "chat.h" +#include "talkaction.h" +#include "spells.h" +#include "movement.h" +#include "weapons.h" +#include "globalevent.h" +#include "events.h" +#include "script.h" + +Actions* g_actions = nullptr; +CreatureEvents* g_creatureEvents = nullptr; +Chat* g_chat = nullptr; +Events* g_events = nullptr; +GlobalEvents* g_globalEvents = nullptr; +Spells* g_spells = nullptr; +TalkActions* g_talkActions = nullptr; +MoveEvents* g_moveEvents = nullptr; +Weapons* g_weapons = nullptr; +Scripts* g_scripts = nullptr; + +extern LuaEnvironment g_luaEnvironment; + +ScriptingManager::~ScriptingManager() +{ + delete g_events; + delete g_weapons; + delete g_spells; + delete g_actions; + delete g_talkActions; + delete g_moveEvents; + delete g_chat; + delete g_creatureEvents; + delete g_globalEvents; + delete g_scripts; +} + +bool ScriptingManager::loadScriptSystems() +{ + if (g_luaEnvironment.loadFile("data/global.lua") == -1) { + std::cout << "[Warning - ScriptingManager::loadScriptSystems] Can not load data/global.lua" << std::endl; + } + + g_scripts = new Scripts(); + std::cout << ">> Loading lua libs" << std::endl; + if (!g_scripts->loadScripts("scripts/lib", true, false)) { + std::cout << "> ERROR: Unable to load lua libs!" << std::endl; + return false; + } + + g_chat = new Chat(); + + g_weapons = new Weapons(); + if (!g_weapons->loadFromXml()) { + std::cout << "> ERROR: Unable to load weapons!" << std::endl; + return false; + } + + g_weapons->loadDefaults(); + + g_spells = new Spells(); + if (!g_spells->loadFromXml()) { + std::cout << "> ERROR: Unable to load spells!" << std::endl; + return false; + } + + g_actions = new Actions(); + if (!g_actions->loadFromXml()) { + std::cout << "> ERROR: Unable to load actions!" << std::endl; + return false; + } + + g_talkActions = new TalkActions(); + if (!g_talkActions->loadFromXml()) { + std::cout << "> ERROR: Unable to load talk actions!" << std::endl; + return false; + } + + g_moveEvents = new MoveEvents(); + if (!g_moveEvents->loadFromXml()) { + std::cout << "> ERROR: Unable to load move events!" << std::endl; + return false; + } + + g_creatureEvents = new CreatureEvents(); + if (!g_creatureEvents->loadFromXml()) { + std::cout << "> ERROR: Unable to load creature events!" << std::endl; + return false; + } + + g_globalEvents = new GlobalEvents(); + if (!g_globalEvents->loadFromXml()) { + std::cout << "> ERROR: Unable to load global events!" << std::endl; + return false; + } + + g_events = new Events(); + if (!g_events->load()) { + std::cout << "> ERROR: Unable to load events!" << std::endl; + return false; + } + + return true; +} diff --git a/src/scriptmanager.h b/src/scriptmanager.h new file mode 100644 index 0000000..d3b52e4 --- /dev/null +++ b/src/scriptmanager.h @@ -0,0 +1,41 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_SCRIPTMANAGER_H_F9428B7803A44FB88EB1A915CFD37F8B +#define FS_SCRIPTMANAGER_H_F9428B7803A44FB88EB1A915CFD37F8B + +class ScriptingManager +{ + public: + ScriptingManager() = default; + ~ScriptingManager(); + + // non-copyable + ScriptingManager(const ScriptingManager&) = delete; + ScriptingManager& operator=(const ScriptingManager&) = delete; + + static ScriptingManager& getInstance() { + static ScriptingManager instance; + return instance; + } + + bool loadScriptSystems(); +}; + +#endif diff --git a/src/server.cpp b/src/server.cpp new file mode 100644 index 0000000..7d58986 --- /dev/null +++ b/src/server.cpp @@ -0,0 +1,206 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "outputmessage.h" +#include "server.h" +#include "scheduler.h" +#include "configmanager.h" +#include "ban.h" + +extern ConfigManager g_config; +Ban g_bans; + +ServiceManager::~ServiceManager() +{ + stop(); +} + +void ServiceManager::die() +{ + io_service.stop(); +} + +void ServiceManager::run() +{ + assert(!running); + running = true; + io_service.run(); +} + +void ServiceManager::stop() +{ + if (!running) { + return; + } + + running = false; + + for (auto& servicePortIt : acceptors) { + try { + io_service.post(std::bind(&ServicePort::onStopServer, servicePortIt.second)); + } catch (boost::system::system_error& e) { + std::cout << "[ServiceManager::stop] Network Error: " << e.what() << std::endl; + } + } + + acceptors.clear(); + + death_timer.expires_from_now(boost::posix_time::seconds(3)); + death_timer.async_wait(std::bind(&ServiceManager::die, this)); +} + +ServicePort::~ServicePort() +{ + close(); +} + +bool ServicePort::is_single_socket() const +{ + return !services.empty() && services.front()->is_single_socket(); +} + +std::string ServicePort::get_protocol_names() const +{ + if (services.empty()) { + return std::string(); + } + + std::string str = services.front()->get_protocol_name(); + for (size_t i = 1; i < services.size(); ++i) { + str.push_back(','); + str.push_back(' '); + str.append(services[i]->get_protocol_name()); + } + return str; +} + +void ServicePort::accept() +{ + if (!acceptor) { + return; + } + + auto connection = ConnectionManager::getInstance().createConnection(io_service, shared_from_this()); + acceptor->async_accept(connection->getSocket(), std::bind(&ServicePort::onAccept, shared_from_this(), connection, std::placeholders::_1)); +} + +void ServicePort::onAccept(Connection_ptr connection, const boost::system::error_code& error) +{ + if (!error) { + if (services.empty()) { + return; + } + + auto remote_ip = connection->getIP(); + if (remote_ip != 0 && g_bans.acceptConnection(remote_ip)) { + Service_ptr service = services.front(); + if (service->is_single_socket()) { + connection->accept(service->make_protocol(connection)); + } else { + connection->accept(); + } + } else { + connection->close(Connection::FORCE_CLOSE); + } + + accept(); + } else if (error != boost::asio::error::operation_aborted) { + if (!pendingStart) { + close(); + pendingStart = true; + g_scheduler.addEvent(createSchedulerTask(15000, + std::bind(&ServicePort::openAcceptor, std::weak_ptr(shared_from_this()), serverPort))); + } + } +} + +Protocol_ptr ServicePort::make_protocol(bool checksummed, NetworkMessage& msg, const Connection_ptr& connection) const +{ + uint8_t protocolID = msg.getByte(); + for (auto& service : services) { + if (protocolID != service->get_protocol_identifier()) { + continue; + } + + if ((checksummed && service->is_checksummed()) || !service->is_checksummed()) { + return service->make_protocol(connection); + } + } + return nullptr; +} + +void ServicePort::onStopServer() +{ + close(); +} + +void ServicePort::openAcceptor(std::weak_ptr weak_service, uint16_t port) +{ + if (auto service = weak_service.lock()) { + service->open(port); + } +} + +void ServicePort::open(uint16_t port) +{ + close(); + + serverPort = port; + pendingStart = false; + + try { + if (g_config.getBoolean(ConfigManager::BIND_ONLY_GLOBAL_ADDRESS)) { + acceptor.reset(new boost::asio::ip::tcp::acceptor(io_service, boost::asio::ip::tcp::endpoint( + boost::asio::ip::address(boost::asio::ip::address_v4::from_string(g_config.getString(ConfigManager::IP))), serverPort))); + } else { + acceptor.reset(new boost::asio::ip::tcp::acceptor(io_service, boost::asio::ip::tcp::endpoint( + boost::asio::ip::address(boost::asio::ip::address_v4(INADDR_ANY)), serverPort))); + } + + acceptor->set_option(boost::asio::ip::tcp::no_delay(true)); + + accept(); + } catch (boost::system::system_error& e) { + std::cout << "[ServicePort::open] Error: " << e.what() << std::endl; + + pendingStart = true; + g_scheduler.addEvent(createSchedulerTask(15000, + std::bind(&ServicePort::openAcceptor, std::weak_ptr(shared_from_this()), port))); + } +} + +void ServicePort::close() +{ + if (acceptor && acceptor->is_open()) { + boost::system::error_code error; + acceptor->close(error); + } +} + +bool ServicePort::add_service(const Service_ptr& new_svc) +{ + if (std::any_of(services.begin(), services.end(), [](const Service_ptr& svc) {return svc->is_single_socket();})) { + return false; + } + + services.push_back(new_svc); + return true; +} diff --git a/src/server.h b/src/server.h new file mode 100644 index 0000000..02cb373 --- /dev/null +++ b/src/server.h @@ -0,0 +1,156 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_SERVER_H_984DA68ABF744127850F90CC710F281B +#define FS_SERVER_H_984DA68ABF744127850F90CC710F281B + +#include "connection.h" +#include "signals.h" +#include + +class Protocol; + +class ServiceBase +{ + public: + virtual bool is_single_socket() const = 0; + virtual bool is_checksummed() const = 0; + virtual uint8_t get_protocol_identifier() const = 0; + virtual const char* get_protocol_name() const = 0; + + virtual Protocol_ptr make_protocol(const Connection_ptr& c) const = 0; +}; + +template +class Service final : public ServiceBase +{ + public: + bool is_single_socket() const override { + return ProtocolType::server_sends_first; + } + bool is_checksummed() const override { + return ProtocolType::use_checksum; + } + uint8_t get_protocol_identifier() const override { + return ProtocolType::protocol_identifier; + } + const char* get_protocol_name() const override { + return ProtocolType::protocol_name(); + } + + Protocol_ptr make_protocol(const Connection_ptr& c) const override { + return std::make_shared(c); + } +}; + +class ServicePort : public std::enable_shared_from_this +{ + public: + explicit ServicePort(boost::asio::io_service& io_service) : io_service(io_service) {} + ~ServicePort(); + + // non-copyable + ServicePort(const ServicePort&) = delete; + ServicePort& operator=(const ServicePort&) = delete; + + static void openAcceptor(std::weak_ptr weak_service, uint16_t port); + void open(uint16_t port); + void close(); + bool is_single_socket() const; + std::string get_protocol_names() const; + + bool add_service(const Service_ptr& new_svc); + Protocol_ptr make_protocol(bool checksummed, NetworkMessage& msg, const Connection_ptr& connection) const; + + void onStopServer(); + void onAccept(Connection_ptr connection, const boost::system::error_code& error); + + private: + void accept(); + + boost::asio::io_service& io_service; + std::unique_ptr acceptor; + std::vector services; + + uint16_t serverPort = 0; + bool pendingStart = false; +}; + +class ServiceManager +{ + public: + ServiceManager() = default; + ~ServiceManager(); + + // non-copyable + ServiceManager(const ServiceManager&) = delete; + ServiceManager& operator=(const ServiceManager&) = delete; + + void run(); + void stop(); + + template + bool add(uint16_t port); + + bool is_running() const { + return acceptors.empty() == false; + } + + private: + void die(); + + std::unordered_map acceptors; + + boost::asio::io_service io_service; + Signals signals{io_service}; + boost::asio::deadline_timer death_timer { io_service }; + bool running = false; +}; + +template +bool ServiceManager::add(uint16_t port) +{ + if (port == 0) { + std::cout << "ERROR: No port provided for service " << ProtocolType::protocol_name() << ". Service disabled." << std::endl; + return false; + } + + ServicePort_ptr service_port; + + auto foundServicePort = acceptors.find(port); + + if (foundServicePort == acceptors.end()) { + service_port = std::make_shared(io_service); + service_port->open(port); + acceptors[port] = service_port; + } else { + service_port = foundServicePort->second; + + if (service_port->is_single_socket() || ProtocolType::server_sends_first) { + std::cout << "ERROR: " << ProtocolType::protocol_name() << + " and " << service_port->get_protocol_names() << + " cannot use the same port " << port << '.' << std::endl; + return false; + } + } + + return service_port->add_service(std::make_shared>()); +} + +#endif diff --git a/src/signals.cpp b/src/signals.cpp new file mode 100644 index 0000000..9ed5277 --- /dev/null +++ b/src/signals.cpp @@ -0,0 +1,212 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" +#include + +#include "signals.h" +#include "tasks.h" +#include "game.h" +#include "actions.h" +#include "configmanager.h" +#include "spells.h" +#include "talkaction.h" +#include "movement.h" +#include "weapons.h" +#include "raids.h" +#include "quests.h" +#include "mounts.h" +#include "globalevent.h" +#include "monster.h" +#include "events.h" +#include "scheduler.h" +#include "databasetasks.h" + + +extern Scheduler g_scheduler; +extern DatabaseTasks g_databaseTasks; +extern Dispatcher g_dispatcher; + +extern ConfigManager g_config; +extern Actions* g_actions; +extern Monsters g_monsters; +extern TalkActions* g_talkActions; +extern MoveEvents* g_moveEvents; +extern Spells* g_spells; +extern Weapons* g_weapons; +extern Game g_game; +extern CreatureEvents* g_creatureEvents; +extern GlobalEvents* g_globalEvents; +extern Events* g_events; +extern Chat* g_chat; +extern LuaEnvironment g_luaEnvironment; + +using ErrorCode = boost::system::error_code; + +Signals::Signals(boost::asio::io_service& service) : + set(service) +{ + set.add(SIGINT); + set.add(SIGTERM); +#ifndef _WIN32 + set.add(SIGUSR1); + set.add(SIGHUP); +#else + // This must be a blocking call as Windows calls it in a new thread and terminates + // the process when the handler returns (or after 5 seconds, whichever is earlier). + // On Windows it is called in a new thread. + signal(SIGBREAK, dispatchSignalHandler); +#endif + + asyncWait(); +} + +void Signals::asyncWait() +{ + set.async_wait([this] (ErrorCode err, int signal) { + if (err) { + std::cerr << "Signal handling error: " << err.message() << std::endl; + return; + } + dispatchSignalHandler(signal); + asyncWait(); + }); +} + +// On Windows this function does not need to be signal-safe, +// as it is called in a new thread. +// https://github.com/otland/forgottenserver/pull/2473 +void Signals::dispatchSignalHandler(int signal) +{ + switch(signal) { + case SIGINT: //Shuts the server down + g_dispatcher.addTask(createTask(sigintHandler)); + break; + case SIGTERM: //Shuts the server down + g_dispatcher.addTask(createTask(sigtermHandler)); + break; +#ifndef _WIN32 + case SIGHUP: //Reload config/data + g_dispatcher.addTask(createTask(sighupHandler)); + break; + case SIGUSR1: //Saves game state + g_dispatcher.addTask(createTask(sigusr1Handler)); + break; +#else + case SIGBREAK: //Shuts the server down + g_dispatcher.addTask(createTask(sigbreakHandler)); + // hold the thread until other threads end + g_scheduler.join(); + g_databaseTasks.join(); + g_dispatcher.join(); + break; +#endif + default: + break; + } +} + +void Signals::sigbreakHandler() +{ + //Dispatcher thread + std::cout << "SIGBREAK received, shutting game server down..." << std::endl; + g_game.setGameState(GAME_STATE_SHUTDOWN); +} + +void Signals::sigtermHandler() +{ + //Dispatcher thread + std::cout << "SIGTERM received, shutting game server down..." << std::endl; + g_game.setGameState(GAME_STATE_SHUTDOWN); +} + +void Signals::sigusr1Handler() +{ + //Dispatcher thread + std::cout << "SIGUSR1 received, saving the game state..." << std::endl; + g_game.saveGameState(); +} + +void Signals::sighupHandler() +{ + //Dispatcher thread + std::cout << "SIGHUP received, reloading config files..." << std::endl; + + g_actions->reload(); + std::cout << "Reloaded actions." << std::endl; + + g_config.reload(); + std::cout << "Reloaded config." << std::endl; + + g_creatureEvents->reload(); + std::cout << "Reloaded creature scripts." << std::endl; + + g_moveEvents->reload(); + std::cout << "Reloaded movements." << std::endl; + + Npcs::reload(); + std::cout << "Reloaded npcs." << std::endl; + + g_game.raids.reload(); + g_game.raids.startup(); + std::cout << "Reloaded raids." << std::endl; + + g_spells->reload(); + std::cout << "Reloaded monsters." << std::endl; + + g_monsters.reload(); + std::cout << "Reloaded spells." << std::endl; + + g_talkActions->reload(); + std::cout << "Reloaded talk actions." << std::endl; + + Item::items.reload(); + std::cout << "Reloaded items." << std::endl; + + g_weapons->reload(); + g_weapons->loadDefaults(); + std::cout << "Reloaded weapons." << std::endl; + + g_game.quests.reload(); + std::cout << "Reloaded quests." << std::endl; + + g_game.mounts.reload(); + std::cout << "Reloaded mounts." << std::endl; + + g_globalEvents->reload(); + std::cout << "Reloaded globalevents." << std::endl; + + g_events->load(); + std::cout << "Reloaded events." << std::endl; + + g_chat->load(); + std::cout << "Reloaded chatchannels." << std::endl; + + g_luaEnvironment.loadFile("data/global.lua"); + std::cout << "Reloaded global.lua." << std::endl; + + lua_gc(g_luaEnvironment.getLuaState(), LUA_GCCOLLECT, 0); +} + +void Signals::sigintHandler() +{ + //Dispatcher thread + std::cout << "SIGINT received, shutting game server down..." << std::endl; + g_game.setGameState(GAME_STATE_SHUTDOWN); +} diff --git a/src/signals.h b/src/signals.h new file mode 100644 index 0000000..5306852 --- /dev/null +++ b/src/signals.h @@ -0,0 +1,42 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_SIGNALHANDLINGTHREAD_H_01C6BF08B0EFE9E200175D108CF0B35F +#define FS_SIGNALHANDLINGTHREAD_H_01C6BF08B0EFE9E200175D108CF0B35F + +#include + +class Signals +{ + boost::asio::signal_set set; + public: + explicit Signals(boost::asio::io_service& service); + + private: + void asyncWait(); + static void dispatchSignalHandler(int signal); + + static void sigbreakHandler(); + static void sigintHandler(); + static void sighupHandler(); + static void sigtermHandler(); + static void sigusr1Handler(); +}; + +#endif diff --git a/src/spawn.cpp b/src/spawn.cpp new file mode 100644 index 0000000..cfcd0be --- /dev/null +++ b/src/spawn.cpp @@ -0,0 +1,332 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "spawn.h" +#include "game.h" +#include "monster.h" +#include "configmanager.h" +#include "scheduler.h" + +#include "pugicast.h" +#include "events.h" + +extern ConfigManager g_config; +extern Monsters g_monsters; +extern Game g_game; +extern Events* g_events; + +static constexpr int32_t MINSPAWN_INTERVAL = 1000; + +bool Spawns::loadFromXml(const std::string& filename) +{ + if (loaded) { + return true; + } + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(filename.c_str()); + if (!result) { + printXMLError("Error - Spawns::loadFromXml", filename, result); + return false; + } + + this->filename = filename; + loaded = true; + + for (auto spawnNode : doc.child("spawns").children()) { + Position centerPos( + pugi::cast(spawnNode.attribute("centerx").value()), + pugi::cast(spawnNode.attribute("centery").value()), + pugi::cast(spawnNode.attribute("centerz").value()) + ); + + int32_t radius; + pugi::xml_attribute radiusAttribute = spawnNode.attribute("radius"); + if (radiusAttribute) { + radius = pugi::cast(radiusAttribute.value()); + } else { + radius = -1; + } + + spawnList.emplace_front(centerPos, radius); + Spawn& spawn = spawnList.front(); + + for (auto childNode : spawnNode.children()) { + if (strcasecmp(childNode.name(), "monster") == 0) { + pugi::xml_attribute nameAttribute = childNode.attribute("name"); + if (!nameAttribute) { + continue; + } + + Direction dir; + + pugi::xml_attribute directionAttribute = childNode.attribute("direction"); + if (directionAttribute) { + dir = static_cast(pugi::cast(directionAttribute.value())); + } else { + dir = DIRECTION_NORTH; + } + + Position pos( + centerPos.x + pugi::cast(childNode.attribute("x").value()), + centerPos.y + pugi::cast(childNode.attribute("y").value()), + centerPos.z + ); + uint32_t interval = pugi::cast(childNode.attribute("spawntime").value()) * 1000; + if (interval > MINSPAWN_INTERVAL) { + spawn.addMonster(nameAttribute.as_string(), pos, dir, interval); + } else { + std::cout << "[Warning - Spawns::loadFromXml] " << nameAttribute.as_string() << ' ' << pos << " spawntime can not be less than " << MINSPAWN_INTERVAL / 1000 << " seconds." << std::endl; + } + } else if (strcasecmp(childNode.name(), "npc") == 0) { + pugi::xml_attribute nameAttribute = childNode.attribute("name"); + if (!nameAttribute) { + continue; + } + + Npc* npc = Npc::createNpc(nameAttribute.as_string()); + if (!npc) { + continue; + } + + pugi::xml_attribute directionAttribute = childNode.attribute("direction"); + if (directionAttribute) { + npc->setDirection(static_cast(pugi::cast(directionAttribute.value()))); + } + + npc->setMasterPos(Position( + centerPos.x + pugi::cast(childNode.attribute("x").value()), + centerPos.y + pugi::cast(childNode.attribute("y").value()), + centerPos.z + ), radius); + npcList.push_front(npc); + } + } + } + return true; +} + +void Spawns::startup() +{ + if (!loaded || isStarted()) { + return; + } + + for (Npc* npc : npcList) { + g_game.placeCreature(npc, npc->getMasterPos(), false, true); + } + npcList.clear(); + + for (Spawn& spawn : spawnList) { + spawn.startup(); + } + + started = true; +} + +void Spawns::clear() +{ + for (Spawn& spawn : spawnList) { + spawn.stopEvent(); + } + spawnList.clear(); + + loaded = false; + started = false; + filename.clear(); +} + +bool Spawns::isInZone(const Position& centerPos, int32_t radius, const Position& pos) +{ + if (radius == -1) { + return true; + } + + return ((pos.getX() >= centerPos.getX() - radius) && (pos.getX() <= centerPos.getX() + radius) && + (pos.getY() >= centerPos.getY() - radius) && (pos.getY() <= centerPos.getY() + radius)); +} + +void Spawn::startSpawnCheck() +{ + if (checkSpawnEvent == 0) { + checkSpawnEvent = g_scheduler.addEvent(createSchedulerTask(getInterval(), std::bind(&Spawn::checkSpawn, this))); + } +} + +Spawn::~Spawn() +{ + for (const auto& it : spawnedMap) { + Monster* monster = it.second; + monster->setSpawn(nullptr); + monster->decrementReferenceCounter(); + } +} + +bool Spawn::findPlayer(const Position& pos) +{ + SpectatorVec spectators; + g_game.map.getSpectators(spectators, pos, false, true); + for (Creature* spectator : spectators) { + if (!spectator->getPlayer()->hasFlag(PlayerFlag_IgnoredByMonsters)) { + return true; + } + } + return false; +} + +bool Spawn::isInSpawnZone(const Position& pos) +{ + return Spawns::isInZone(centerPos, radius, pos); +} + +bool Spawn::spawnMonster(uint32_t spawnId, MonsterType* mType, const Position& pos, Direction dir, bool startup /*= false*/) +{ + std::unique_ptr monster_ptr(new Monster(mType)); + if (!g_events->eventMonsterOnSpawn(monster_ptr.get(), pos, startup, false)) { + return false; + } + + if (startup) { + //No need to send out events to the surrounding since there is no one out there to listen! + if (!g_game.internalPlaceCreature(monster_ptr.get(), pos, true)) { + return false; + } + } else { + if (!g_game.placeCreature(monster_ptr.get(), pos, false, true)) { + return false; + } + } + + Monster* monster = monster_ptr.release(); + monster->setDirection(dir); + monster->setSpawn(this); + monster->setMasterPos(pos); + monster->incrementReferenceCounter(); + + spawnedMap.insert(spawned_pair(spawnId, monster)); + spawnMap[spawnId].lastSpawn = OTSYS_TIME(); + return true; +} + +void Spawn::startup() +{ + for (const auto& it : spawnMap) { + uint32_t spawnId = it.first; + const spawnBlock_t& sb = it.second; + spawnMonster(spawnId, sb.mType, sb.pos, sb.direction, true); + } +} + +void Spawn::checkSpawn() +{ + checkSpawnEvent = 0; + + cleanup(); + + uint32_t spawnCount = 0; + + for (auto& it : spawnMap) { + uint32_t spawnId = it.first; + if (spawnedMap.find(spawnId) != spawnedMap.end()) { + continue; + } + + spawnBlock_t& sb = it.second; + if (OTSYS_TIME() >= sb.lastSpawn + sb.interval) { + if (findPlayer(sb.pos)) { + sb.lastSpawn = OTSYS_TIME(); + continue; + } + + spawnMonster(spawnId, sb.mType, sb.pos, sb.direction); + if (++spawnCount >= static_cast(g_config.getNumber(ConfigManager::RATE_SPAWN))) { + break; + } + } + } + + if (spawnedMap.size() < spawnMap.size()) { + checkSpawnEvent = g_scheduler.addEvent(createSchedulerTask(getInterval(), std::bind(&Spawn::checkSpawn, this))); + } +} + +void Spawn::cleanup() +{ + auto it = spawnedMap.begin(); + while (it != spawnedMap.end()) { + uint32_t spawnId = it->first; + Monster* monster = it->second; + if (monster->isRemoved()) { + if (spawnId != 0) { + spawnMap[spawnId].lastSpawn = OTSYS_TIME(); + } + + monster->decrementReferenceCounter(); + it = spawnedMap.erase(it); + } else if (!isInSpawnZone(monster->getPosition()) && spawnId != 0) { + spawnedMap.insert(spawned_pair(0, monster)); + it = spawnedMap.erase(it); + } else { + ++it; + } + } +} + +bool Spawn::addMonster(const std::string& name, const Position& pos, Direction dir, uint32_t interval) +{ + MonsterType* mType = g_monsters.getMonsterType(name); + if (!mType) { + std::cout << "[Spawn::addMonster] Can not find " << name << std::endl; + return false; + } + + this->interval = std::min(this->interval, interval); + + spawnBlock_t sb; + sb.mType = mType; + sb.pos = pos; + sb.direction = dir; + sb.interval = interval; + sb.lastSpawn = 0; + + uint32_t spawnId = spawnMap.size() + 1; + spawnMap[spawnId] = sb; + return true; +} + +void Spawn::removeMonster(Monster* monster) +{ + for (auto it = spawnedMap.begin(), end = spawnedMap.end(); it != end; ++it) { + if (it->second == monster) { + monster->decrementReferenceCounter(); + spawnedMap.erase(it); + break; + } + } +} + +void Spawn::stopEvent() +{ + if (checkSpawnEvent != 0) { + g_scheduler.stopEvent(checkSpawnEvent); + checkSpawnEvent = 0; + } +} diff --git a/src/spawn.h b/src/spawn.h new file mode 100644 index 0000000..15657e6 --- /dev/null +++ b/src/spawn.h @@ -0,0 +1,103 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_SPAWN_H_1A86089E080846A9AE53ED12E7AE863B +#define FS_SPAWN_H_1A86089E080846A9AE53ED12E7AE863B + +#include "tile.h" +#include "position.h" + +class Monster; +class MonsterType; +class Npc; + +struct spawnBlock_t { + Position pos; + MonsterType* mType; + int64_t lastSpawn; + uint32_t interval; + Direction direction; +}; + +class Spawn +{ + public: + Spawn(Position pos, int32_t radius) : centerPos(std::move(pos)), radius(radius) {} + ~Spawn(); + + // non-copyable + Spawn(const Spawn&) = delete; + Spawn& operator=(const Spawn&) = delete; + + bool addMonster(const std::string& name, const Position& pos, Direction dir, uint32_t interval); + void removeMonster(Monster* monster); + + uint32_t getInterval() const { + return interval; + } + void startup(); + + void startSpawnCheck(); + void stopEvent(); + + bool isInSpawnZone(const Position& pos); + void cleanup(); + + private: + //map of the spawned creatures + using SpawnedMap = std::multimap; + using spawned_pair = SpawnedMap::value_type; + SpawnedMap spawnedMap; + + //map of creatures in the spawn + std::map spawnMap; + + Position centerPos; + int32_t radius; + + uint32_t interval = 60000; + uint32_t checkSpawnEvent = 0; + + static bool findPlayer(const Position& pos); + bool spawnMonster(uint32_t spawnId, MonsterType* mType, const Position& pos, Direction dir, bool startup = false); + void checkSpawn(); +}; + +class Spawns +{ + public: + static bool isInZone(const Position& centerPos, int32_t radius, const Position& pos); + + bool loadFromXml(const std::string& filename); + void startup(); + void clear(); + + bool isStarted() const { + return started; + } + + private: + std::forward_list npcList; + std::forward_list spawnList; + std::string filename; + bool loaded = false; + bool started = false; +}; + +#endif diff --git a/src/spectators.h b/src/spectators.h new file mode 100644 index 0000000..990d957 --- /dev/null +++ b/src/spectators.h @@ -0,0 +1,83 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_SPECTATORS_H_D78A7CCB7080406E8CAA6B1D31D3DA71 +#define FS_SPECTATORS_H_D78A7CCB7080406E8CAA6B1D31D3DA71 + +#include + +class Creature; + +class SpectatorVec +{ + using Vec = std::vector; + using Iterator = Vec::iterator; + using ConstIterator = Vec::const_iterator; +public: + SpectatorVec() { + vec.reserve(32); + } + + void addSpectators(const SpectatorVec& spectators) { + const size_t size = vec.size(); + for (Creature* spectator : spectators.vec) { + bool duplicate = false; + for (size_t i = 0; i < size; ++i) { + if (vec[i] == spectator) { + duplicate = true; + break; + } + } + + if (!duplicate) { + vec.emplace_back(spectator); + } + } + } + + void erase(Creature* spectator) { + for (size_t i = 0, len = vec.size(); i < len; i++) { + if (vec[i] == spectator) { + Creature* tmp = vec[len - 1]; + vec[len - 1] = vec[i]; + vec[i] = tmp; + vec.pop_back(); + break; + } + } + } + + inline size_t size() const { return vec.size(); } + inline bool empty() const { return vec.empty(); } + inline Iterator begin() { return vec.begin(); } + inline ConstIterator begin() const { return vec.begin(); } + inline ConstIterator cbegin() const { return vec.cbegin(); } + inline Iterator end() { return vec.end(); } + inline ConstIterator end() const { return vec.end(); } + inline ConstIterator cend() const { return vec.cend(); } + inline void emplace_back(Creature* c) { return vec.emplace_back(c); } + + template + inline void insert(Iterator pos, InputIterator first, InputIterator last) { vec.insert(pos, first, last); } + +private: + Vec vec; +}; + +#endif diff --git a/src/spells.cpp b/src/spells.cpp new file mode 100644 index 0000000..16bf95e --- /dev/null +++ b/src/spells.cpp @@ -0,0 +1,1301 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "combat.h" +#include "configmanager.h" +#include "game.h" +#include "monster.h" +#include "pugicast.h" +#include "spells.h" + +extern Game g_game; +extern Spells* g_spells; +extern Monsters g_monsters; +extern Vocations g_vocations; +extern ConfigManager g_config; +extern LuaEnvironment g_luaEnvironment; + +Spells::Spells() +{ + scriptInterface.initState(); +} + +Spells::~Spells() +{ + clear(false); +} + +TalkActionResult_t Spells::playerSaySpell(Player* player, std::string& words) +{ + std::string str_words = words; + + //strip trailing spaces + trimString(str_words); + + InstantSpell* instantSpell = getInstantSpell(str_words); + if (!instantSpell) { + return TALKACTION_CONTINUE; + } + + std::string param; + + if (instantSpell->getHasParam()) { + size_t spellLen = instantSpell->getWords().length(); + size_t paramLen = str_words.length() - spellLen; + std::string paramText = str_words.substr(spellLen, paramLen); + if (!paramText.empty() && paramText.front() == ' ') { + size_t loc1 = paramText.find('"', 1); + if (loc1 != std::string::npos) { + size_t loc2 = paramText.find('"', loc1 + 1); + if (loc2 == std::string::npos) { + loc2 = paramText.length(); + } else if (paramText.find_last_not_of(' ') != loc2) { + return TALKACTION_CONTINUE; + } + + param = paramText.substr(loc1 + 1, loc2 - loc1 - 1); + } else { + trimString(paramText); + loc1 = paramText.find(' ', 0); + if (loc1 == std::string::npos) { + param = paramText; + } else { + return TALKACTION_CONTINUE; + } + } + } + } + + if (instantSpell->playerCastInstant(player, param)) { + words = instantSpell->getWords(); + + if (instantSpell->getHasParam() && !param.empty()) { + words += " \"" + param + "\""; + } + + return TALKACTION_BREAK; + } + + return TALKACTION_FAILED; +} + +void Spells::clearMaps(bool fromLua) +{ + for (auto instant = instants.begin(); instant != instants.end(); ) { + if (fromLua == instant->second.fromLua) { + instant = instants.erase(instant); + } else { + ++instant; + } + } + + for (auto rune = runes.begin(); rune != runes.end(); ) { + if (fromLua == rune->second.fromLua) { + rune = runes.erase(rune); + } else { + ++rune; + } + } +} + +void Spells::clear(bool fromLua) +{ + clearMaps(fromLua); + + reInitState(fromLua); +} + +LuaScriptInterface& Spells::getScriptInterface() +{ + return scriptInterface; +} + +std::string Spells::getScriptBaseName() const +{ + return "spells"; +} + +Event_ptr Spells::getEvent(const std::string& nodeName) +{ + if (strcasecmp(nodeName.c_str(), "rune") == 0) { + return Event_ptr(new RuneSpell(&scriptInterface)); + } else if (strcasecmp(nodeName.c_str(), "instant") == 0) { + return Event_ptr(new InstantSpell(&scriptInterface)); + } + return nullptr; +} + +bool Spells::registerEvent(Event_ptr event, const pugi::xml_node&) +{ + InstantSpell* instant = dynamic_cast(event.get()); + if (instant) { + auto result = instants.emplace(instant->getWords(), std::move(*instant)); + if (!result.second) { + std::cout << "[Warning - Spells::registerEvent] Duplicate registered instant spell with words: " << instant->getWords() << std::endl; + } + return result.second; + } + + RuneSpell* rune = dynamic_cast(event.get()); + if (rune) { + auto result = runes.emplace(rune->getRuneItemId(), std::move(*rune)); + if (!result.second) { + std::cout << "[Warning - Spells::registerEvent] Duplicate registered rune with id: " << rune->getRuneItemId() << std::endl; + } + return result.second; + } + + return false; +} + +bool Spells::registerInstantLuaEvent(InstantSpell* event) +{ + InstantSpell_ptr instant { event }; + if (instant) { + std::string words = instant->getWords(); + auto result = instants.emplace(instant->getWords(), std::move(*instant)); + if (!result.second) { + std::cout << "[Warning - Spells::registerInstantLuaEvent] Duplicate registered instant spell with words: " << words << std::endl; + } + return result.second; + } + + return false; +} + +bool Spells::registerRuneLuaEvent(RuneSpell* event) +{ + RuneSpell_ptr rune { event }; + if (rune) { + uint16_t id = rune->getRuneItemId(); + auto result = runes.emplace(rune->getRuneItemId(), std::move(*rune)); + if (!result.second) { + std::cout << "[Warning - Spells::registerRuneLuaEvent] Duplicate registered rune with id: " << id << std::endl; + } + return result.second; + } + + return false; +} + +Spell* Spells::getSpellByName(const std::string& name) +{ + Spell* spell = getRuneSpellByName(name); + if (!spell) { + spell = getInstantSpellByName(name); + } + return spell; +} + +RuneSpell* Spells::getRuneSpell(uint32_t id) +{ + auto it = runes.find(id); + if (it == runes.end()) { + for (auto& rune : runes) { + if (rune.second.getId() == id) { + return &rune.second; + } + } + return nullptr; + } + return &it->second; +} + +RuneSpell* Spells::getRuneSpellByName(const std::string& name) +{ + for (auto& it : runes) { + if (strcasecmp(it.second.getName().c_str(), name.c_str()) == 0) { + return &it.second; + } + } + return nullptr; +} + +InstantSpell* Spells::getInstantSpell(const std::string& words) +{ + InstantSpell* result = nullptr; + + for (auto& it : instants) { + const std::string& instantSpellWords = it.second.getWords(); + size_t spellLen = instantSpellWords.length(); + if (strncasecmp(instantSpellWords.c_str(), words.c_str(), spellLen) == 0) { + if (!result || spellLen > result->getWords().length()) { + result = &it.second; + if (words.length() == spellLen) { + break; + } + } + } + } + + if (result) { + const std::string& resultWords = result->getWords(); + if (words.length() > resultWords.length()) { + if (!result->getHasParam()) { + return nullptr; + } + + size_t spellLen = resultWords.length(); + size_t paramLen = words.length() - spellLen; + if (paramLen < 2 || words[spellLen] != ' ') { + return nullptr; + } + } + return result; + } + return nullptr; +} + +InstantSpell* Spells::getInstantSpellById(uint32_t spellId) +{ + for (auto& it : instants) { + if (it.second.getId() == spellId) { + return &it.second; + } + } + return nullptr; +} + +InstantSpell* Spells::getInstantSpellByName(const std::string& name) +{ + for (auto& it : instants) { + if (strcasecmp(it.second.getName().c_str(), name.c_str()) == 0) { + return &it.second; + } + } + return nullptr; +} + +Position Spells::getCasterPosition(Creature* creature, Direction dir) +{ + return getNextPosition(dir, creature->getPosition()); +} + +CombatSpell::CombatSpell(Combat* combat, bool needTarget, bool needDirection) : + Event(&g_spells->getScriptInterface()), + combat(combat), + needDirection(needDirection), + needTarget(needTarget) +{} + +CombatSpell::~CombatSpell() +{ + if (!scripted) { + delete combat; + } +} + +bool CombatSpell::loadScriptCombat() +{ + combat = g_luaEnvironment.getCombatObject(g_luaEnvironment.lastCombatId); + return combat != nullptr; +} + +bool CombatSpell::castSpell(Creature* creature) +{ + if (scripted) { + LuaVariant var; + var.type = VARIANT_POSITION; + + if (needDirection) { + var.pos = Spells::getCasterPosition(creature, creature->getDirection()); + } else { + var.pos = creature->getPosition(); + } + + return executeCastSpell(creature, var); + } + + Position pos; + if (needDirection) { + pos = Spells::getCasterPosition(creature, creature->getDirection()); + } else { + pos = creature->getPosition(); + } + + combat->doCombat(creature, pos); + return true; +} + +bool CombatSpell::castSpell(Creature* creature, Creature* target) +{ + if (scripted) { + LuaVariant var; + + if (combat->hasArea()) { + var.type = VARIANT_POSITION; + + if (needTarget) { + var.pos = target->getPosition(); + } else if (needDirection) { + var.pos = Spells::getCasterPosition(creature, creature->getDirection()); + } else { + var.pos = creature->getPosition(); + } + } else { + var.type = VARIANT_NUMBER; + var.number = target->getID(); + } + return executeCastSpell(creature, var); + } + + if (combat->hasArea()) { + if (needTarget) { + combat->doCombat(creature, target->getPosition()); + } else { + return castSpell(creature); + } + } else { + combat->doCombat(creature, target); + } + return true; +} + +bool CombatSpell::executeCastSpell(Creature* creature, const LuaVariant& var) +{ + //onCastSpell(creature, var) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - CombatSpell::executeCastSpell] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + LuaScriptInterface::pushVariant(L, var); + + return scriptInterface->callFunction(2); +} + +bool Spell::configureSpell(const pugi::xml_node& node) +{ + pugi::xml_attribute nameAttribute = node.attribute("name"); + if (!nameAttribute) { + std::cout << "[Error - Spell::configureSpell] Spell without name" << std::endl; + return false; + } + + name = nameAttribute.as_string(); + + static const char* reservedList[] = { + "melee", + "physical", + "poison", + "fire", + "energy", + "drown", + "lifedrain", + "manadrain", + "healing", + "speed", + "outfit", + "invisible", + "drunk", + "firefield", + "poisonfield", + "energyfield", + "firecondition", + "poisoncondition", + "energycondition", + "drowncondition", + "freezecondition", + "cursecondition", + "dazzlecondition" + }; + + //static size_t size = sizeof(reservedList) / sizeof(const char*); + //for (size_t i = 0; i < size; ++i) { + for (const char* reserved : reservedList) { + if (strcasecmp(reserved, name.c_str()) == 0) { + std::cout << "[Error - Spell::configureSpell] Spell is using a reserved name: " << reserved << std::endl; + return false; + } + } + + pugi::xml_attribute attr; + if ((attr = node.attribute("spellid"))) { + spellId = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("group"))) { + std::string tmpStr = asLowerCaseString(attr.as_string()); + if (tmpStr == "none" || tmpStr == "0") { + group = SPELLGROUP_NONE; + } else if (tmpStr == "attack" || tmpStr == "1") { + group = SPELLGROUP_ATTACK; + } else if (tmpStr == "healing" || tmpStr == "2") { + group = SPELLGROUP_HEALING; + } else if (tmpStr == "support" || tmpStr == "3") { + group = SPELLGROUP_SUPPORT; + } else if (tmpStr == "special" || tmpStr == "4") { + group = SPELLGROUP_SPECIAL; + } else { + std::cout << "[Warning - Spell::configureSpell] Unknown group: " << attr.as_string() << std::endl; + } + } + + if ((attr = node.attribute("groupcooldown"))) { + groupCooldown = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("secondarygroup"))) { + std::string tmpStr = asLowerCaseString(attr.as_string()); + if (tmpStr == "none" || tmpStr == "0") { + secondaryGroup = SPELLGROUP_NONE; + } else if (tmpStr == "attack" || tmpStr == "1") { + secondaryGroup = SPELLGROUP_ATTACK; + } else if (tmpStr == "healing" || tmpStr == "2") { + secondaryGroup = SPELLGROUP_HEALING; + } else if (tmpStr == "support" || tmpStr == "3") { + secondaryGroup = SPELLGROUP_SUPPORT; + } else if (tmpStr == "special" || tmpStr == "4") { + secondaryGroup = SPELLGROUP_SPECIAL; + } else { + std::cout << "[Warning - Spell::configureSpell] Unknown secondarygroup: " << attr.as_string() << std::endl; + } + } + + if ((attr = node.attribute("secondarygroupcooldown"))) { + secondaryGroupCooldown = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("level")) || (attr = node.attribute("lvl"))) { + level = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("magiclevel")) || (attr = node.attribute("maglv"))) { + magLevel = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("mana"))) { + mana = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("manapercent"))) { + manaPercent = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("soul"))) { + soul = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("range"))) { + range = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("cooldown")) || (attr = node.attribute("exhaustion"))) { + cooldown = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("premium")) || (attr = node.attribute("prem"))) { + premium = attr.as_bool(); + } + + if ((attr = node.attribute("enabled"))) { + enabled = attr.as_bool(); + } + + if ((attr = node.attribute("needtarget"))) { + needTarget = attr.as_bool(); + } + + if ((attr = node.attribute("needweapon"))) { + needWeapon = attr.as_bool(); + } + + if ((attr = node.attribute("selftarget"))) { + selfTarget = attr.as_bool(); + } + + if ((attr = node.attribute("needlearn"))) { + learnable = attr.as_bool(); + } + + if ((attr = node.attribute("blocking"))) { + blockingSolid = attr.as_bool(); + blockingCreature = blockingSolid; + } + + if ((attr = node.attribute("blocktype"))) { + std::string tmpStrValue = asLowerCaseString(attr.as_string()); + if (tmpStrValue == "all") { + blockingSolid = true; + blockingCreature = true; + } else if (tmpStrValue == "solid") { + blockingSolid = true; + } else if (tmpStrValue == "creature") { + blockingCreature = true; + } else { + std::cout << "[Warning - Spell::configureSpell] Blocktype \"" << attr.as_string() << "\" does not exist." << std::endl; + } + } + + if ((attr = node.attribute("pzlock"))) { + pzLock = booleanString(attr.as_string()); + } + + if ((attr = node.attribute("aggressive"))) { + aggressive = booleanString(attr.as_string()); + } + + if (group == SPELLGROUP_NONE) { + group = (aggressive ? SPELLGROUP_ATTACK : SPELLGROUP_HEALING); + } + + for (auto vocationNode : node.children()) { + if (!(attr = vocationNode.attribute("name"))) { + continue; + } + + int32_t vocationId = g_vocations.getVocationId(attr.as_string()); + if (vocationId != -1) { + attr = vocationNode.attribute("showInDescription"); + vocSpellMap[vocationId] = !attr || attr.as_bool(); + } else { + std::cout << "[Warning - Spell::configureSpell] Wrong vocation name: " << attr.as_string() << std::endl; + } + } + return true; +} + +bool Spell::playerSpellCheck(Player* player) const +{ + if (player->hasFlag(PlayerFlag_CannotUseSpells)) { + return false; + } + + if (player->hasFlag(PlayerFlag_IgnoreSpellCheck)) { + return true; + } + + if (!enabled) { + return false; + } + + if ((aggressive || pzLock) && (range < 1 || (range > 0 && !player->getAttackedCreature())) && player->getSkull() == SKULL_BLACK) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + return false; + } + + if ((aggressive || pzLock) && player->hasCondition(CONDITION_PACIFIED)) { + player->sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + if ((aggressive || pzLock) && !player->hasFlag(PlayerFlag_IgnoreProtectionZone) && player->getZone() == ZONE_PROTECTION) { + player->sendCancelMessage(RETURNVALUE_ACTIONNOTPERMITTEDINPROTECTIONZONE); + return false; + } + + if (player->hasCondition(CONDITION_SPELLGROUPCOOLDOWN, group) || player->hasCondition(CONDITION_SPELLCOOLDOWN, spellId) || (secondaryGroup != SPELLGROUP_NONE && player->hasCondition(CONDITION_SPELLGROUPCOOLDOWN, secondaryGroup))) { + player->sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED); + + if (isInstant()) { + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + } + + return false; + } + + if (player->getLevel() < level) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHLEVEL); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + if (player->getMagicLevel() < magLevel) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHMAGICLEVEL); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + if (player->getMana() < getManaCost(player) && !player->hasFlag(PlayerFlag_HasInfiniteMana)) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHMANA); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + if (player->getSoul() < soul && !player->hasFlag(PlayerFlag_HasInfiniteSoul)) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHSOUL); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + if (isInstant() && isLearnable()) { + if (!player->hasLearnedInstantSpell(getName())) { + player->sendCancelMessage(RETURNVALUE_YOUNEEDTOLEARNTHISSPELL); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + } else if (!vocSpellMap.empty() && vocSpellMap.find(player->getVocationId()) == vocSpellMap.end()) { + player->sendCancelMessage(RETURNVALUE_YOURVOCATIONCANNOTUSETHISSPELL); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + if (needWeapon) { + switch (player->getWeaponType()) { + case WEAPON_SWORD: + case WEAPON_CLUB: + case WEAPON_AXE: + break; + + default: { + player->sendCancelMessage(RETURNVALUE_YOUNEEDAWEAPONTOUSETHISSPELL); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + } + } + + if (isPremium() && !player->isPremium()) { + player->sendCancelMessage(RETURNVALUE_YOUNEEDPREMIUMACCOUNT); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + return true; +} + +bool Spell::playerInstantSpellCheck(Player* player, const Position& toPos) +{ + if (toPos.x == 0xFFFF) { + return true; + } + + const Position& playerPos = player->getPosition(); + if (playerPos.z > toPos.z) { + player->sendCancelMessage(RETURNVALUE_FIRSTGOUPSTAIRS); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } else if (playerPos.z < toPos.z) { + player->sendCancelMessage(RETURNVALUE_FIRSTGODOWNSTAIRS); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + Tile* tile = g_game.map.getTile(toPos); + if (!tile) { + tile = new StaticTile(toPos.x, toPos.y, toPos.z); + g_game.map.setTile(toPos, tile); + } + + ReturnValue ret = Combat::canDoCombat(player, tile, aggressive); + if (ret != RETURNVALUE_NOERROR) { + player->sendCancelMessage(ret); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + if (blockingCreature && tile->getBottomVisibleCreature(player) != nullptr) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHROOM); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + if (blockingSolid && tile->hasFlag(TILESTATE_BLOCKSOLID)) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHROOM); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + return true; +} + +bool Spell::playerRuneSpellCheck(Player* player, const Position& toPos) +{ + if (!playerSpellCheck(player)) { + return false; + } + + if (toPos.x == 0xFFFF) { + return true; + } + + const Position& playerPos = player->getPosition(); + if (playerPos.z > toPos.z) { + player->sendCancelMessage(RETURNVALUE_FIRSTGOUPSTAIRS); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } else if (playerPos.z < toPos.z) { + player->sendCancelMessage(RETURNVALUE_FIRSTGODOWNSTAIRS); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + Tile* tile = g_game.map.getTile(toPos); + if (!tile) { + player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + if (range != -1 && !g_game.canThrowObjectTo(playerPos, toPos, true, range, range)) { + player->sendCancelMessage(RETURNVALUE_DESTINATIONOUTOFREACH); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + ReturnValue ret = Combat::canDoCombat(player, tile, aggressive); + if (ret != RETURNVALUE_NOERROR) { + player->sendCancelMessage(ret); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + const Creature* topVisibleCreature = tile->getBottomVisibleCreature(player); + if (blockingCreature && topVisibleCreature) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHROOM); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } else if (blockingSolid && tile->hasFlag(TILESTATE_BLOCKSOLID)) { + player->sendCancelMessage(RETURNVALUE_NOTENOUGHROOM); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + if (needTarget && !topVisibleCreature) { + player->sendCancelMessage(RETURNVALUE_CANONLYUSETHISRUNEONCREATURES); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + if (aggressive && needTarget && topVisibleCreature && player->hasSecureMode()) { + const Player* targetPlayer = topVisibleCreature->getPlayer(); + if (targetPlayer && targetPlayer != player && player->getSkullClient(targetPlayer) == SKULL_NONE && !Combat::isInPvpZone(player, targetPlayer)) { + player->sendCancelMessage(RETURNVALUE_TURNSECUREMODETOATTACKUNMARKEDPLAYERS); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + } + return true; +} + +void Spell::postCastSpell(Player* player, bool finishedCast /*= true*/, bool payCost /*= true*/) const +{ + if (finishedCast) { + if (!player->hasFlag(PlayerFlag_HasNoExhaustion)) { + if (cooldown > 0) { + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLCOOLDOWN, cooldown, 0, false, spellId); + player->addCondition(condition); + } + + if (groupCooldown > 0) { + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLGROUPCOOLDOWN, groupCooldown, 0, false, group); + player->addCondition(condition); + } + + if (secondaryGroupCooldown > 0) { + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLGROUPCOOLDOWN, secondaryGroupCooldown, 0, false, secondaryGroup); + player->addCondition(condition); + } + } + + if (aggressive) { + player->addInFightTicks(); + } + } + + if (payCost) { + Spell::postCastSpell(player, getManaCost(player), getSoulCost()); + } +} + +void Spell::postCastSpell(Player* player, uint32_t manaCost, uint32_t soulCost) +{ + if (manaCost > 0) { + player->addManaSpent(manaCost); + player->changeMana(-static_cast(manaCost)); + } + + if (!player->hasFlag(PlayerFlag_HasInfiniteSoul)) { + if (soulCost > 0) { + player->changeSoul(-static_cast(soulCost)); + } + } +} + +uint32_t Spell::getManaCost(const Player* player) const +{ + if (mana != 0) { + return mana; + } + + if (manaPercent != 0) { + uint32_t maxMana = player->getMaxMana(); + uint32_t manaCost = (maxMana * manaPercent) / 100; + return manaCost; + } + + return 0; +} + +std::string InstantSpell::getScriptEventName() const +{ + return "onCastSpell"; +} + +bool InstantSpell::configureEvent(const pugi::xml_node& node) +{ + if (!Spell::configureSpell(node)) { + return false; + } + + if (!TalkAction::configureEvent(node)) { + return false; + } + + spellType = SPELL_INSTANT; + + pugi::xml_attribute attr; + if ((attr = node.attribute("params"))) { + hasParam = attr.as_bool(); + } + + if ((attr = node.attribute("playernameparam"))) { + hasPlayerNameParam = attr.as_bool(); + } + + if ((attr = node.attribute("direction"))) { + needDirection = attr.as_bool(); + } else if ((attr = node.attribute("casterTargetOrDirection"))) { + casterTargetOrDirection = attr.as_bool(); + } + + if ((attr = node.attribute("blockwalls"))) { + checkLineOfSight = attr.as_bool(); + } + return true; +} + +bool InstantSpell::playerCastInstant(Player* player, std::string& param) +{ + if (!playerSpellCheck(player)) { + return false; + } + + LuaVariant var; + + if (selfTarget) { + var.type = VARIANT_NUMBER; + var.number = player->getID(); + } else if (needTarget || casterTargetOrDirection) { + Creature* target = nullptr; + bool useDirection = false; + + if (hasParam) { + Player* playerTarget = nullptr; + ReturnValue ret = g_game.getPlayerByNameWildcard(param, playerTarget); + + if (playerTarget && playerTarget->isAccessPlayer() && !player->isAccessPlayer()) { + playerTarget = nullptr; + } + + target = playerTarget; + if (!target || target->getHealth() <= 0) { + if (!casterTargetOrDirection) { + if (cooldown > 0) { + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLCOOLDOWN, cooldown, 0, false, spellId); + player->addCondition(condition); + } + + if (groupCooldown > 0) { + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLGROUPCOOLDOWN, groupCooldown, 0, false, group); + player->addCondition(condition); + } + + if (secondaryGroupCooldown > 0) { + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLGROUPCOOLDOWN, secondaryGroupCooldown, 0, false, secondaryGroup); + player->addCondition(condition); + } + + player->sendCancelMessage(ret); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + useDirection = true; + } + + if (playerTarget) { + param = playerTarget->getName(); + } + } else { + target = player->getAttackedCreature(); + if (!target || target->getHealth() <= 0) { + if (!casterTargetOrDirection) { + player->sendCancelMessage(RETURNVALUE_YOUCANONLYUSEITONCREATURES); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + useDirection = true; + } + } + + if (!useDirection) { + if (!canThrowSpell(player, target)) { + player->sendCancelMessage(RETURNVALUE_CREATUREISNOTREACHABLE); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + var.type = VARIANT_NUMBER; + var.number = target->getID(); + } else { + var.type = VARIANT_POSITION; + var.pos = Spells::getCasterPosition(player, player->getDirection()); + + if (!playerInstantSpellCheck(player, var.pos)) { + return false; + } + } + } else if (hasParam) { + var.type = VARIANT_STRING; + + if (getHasPlayerNameParam()) { + Player* playerTarget = nullptr; + ReturnValue ret = g_game.getPlayerByNameWildcard(param, playerTarget); + + if (ret != RETURNVALUE_NOERROR) { + if (cooldown > 0) { + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLCOOLDOWN, cooldown, 0, false, spellId); + player->addCondition(condition); + } + + if (groupCooldown > 0) { + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLGROUPCOOLDOWN, groupCooldown, 0, false, group); + player->addCondition(condition); + } + + if (secondaryGroupCooldown > 0) { + Condition* condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLGROUPCOOLDOWN, secondaryGroupCooldown, 0, false, secondaryGroup); + player->addCondition(condition); + } + + player->sendCancelMessage(ret); + g_game.addMagicEffect(player->getPosition(), CONST_ME_POFF); + return false; + } + + if (playerTarget && (!playerTarget->isAccessPlayer() || player->isAccessPlayer())) { + param = playerTarget->getName(); + } + } + + var.text = param; + } else { + var.type = VARIANT_POSITION; + + if (needDirection) { + var.pos = Spells::getCasterPosition(player, player->getDirection()); + } else { + var.pos = player->getPosition(); + } + + if (!playerInstantSpellCheck(player, var.pos)) { + return false; + } + } + + bool result = internalCastSpell(player, var); + if (result) { + postCastSpell(player); + } + + return result; +} + +bool InstantSpell::canThrowSpell(const Creature* creature, const Creature* target) const +{ + const Position& fromPos = creature->getPosition(); + const Position& toPos = target->getPosition(); + if (fromPos.z != toPos.z || + (range == -1 && !g_game.canThrowObjectTo(fromPos, toPos, checkLineOfSight)) || + (range != -1 && !g_game.canThrowObjectTo(fromPos, toPos, checkLineOfSight, range, range))) { + return false; + } + return true; +} + +bool InstantSpell::castSpell(Creature* creature) +{ + LuaVariant var; + + if (casterTargetOrDirection) { + Creature* target = creature->getAttackedCreature(); + if (target && target->getHealth() > 0) { + if (!canThrowSpell(creature, target)) { + return false; + } + + var.type = VARIANT_NUMBER; + var.number = target->getID(); + return internalCastSpell(creature, var); + } + + return false; + } else if (needDirection) { + var.type = VARIANT_POSITION; + var.pos = Spells::getCasterPosition(creature, creature->getDirection()); + } else { + var.type = VARIANT_POSITION; + var.pos = creature->getPosition(); + } + + return internalCastSpell(creature, var); +} + +bool InstantSpell::castSpell(Creature* creature, Creature* target) +{ + if (needTarget) { + LuaVariant var; + var.type = VARIANT_NUMBER; + var.number = target->getID(); + return internalCastSpell(creature, var); + } else { + return castSpell(creature); + } +} + +bool InstantSpell::internalCastSpell(Creature* creature, const LuaVariant& var) +{ + return executeCastSpell(creature, var); +} + +bool InstantSpell::executeCastSpell(Creature* creature, const LuaVariant& var) +{ + //onCastSpell(creature, var) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - InstantSpell::executeCastSpell] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + LuaScriptInterface::pushVariant(L, var); + + return scriptInterface->callFunction(2); +} + +bool InstantSpell::canCast(const Player* player) const +{ + if (player->hasFlag(PlayerFlag_CannotUseSpells)) { + return false; + } + + if (player->hasFlag(PlayerFlag_IgnoreSpellCheck)) { + return true; + } + + if (isLearnable()) { + if (player->hasLearnedInstantSpell(getName())) { + return true; + } + } else { + if (vocSpellMap.empty() || vocSpellMap.find(player->getVocationId()) != vocSpellMap.end()) { + return true; + } + } + + return false; +} + +std::string RuneSpell::getScriptEventName() const +{ + return "onCastSpell"; +} + +bool RuneSpell::configureEvent(const pugi::xml_node& node) +{ + if (!Spell::configureSpell(node)) { + return false; + } + + if (!Action::configureEvent(node)) { + return false; + } + + spellType = SPELL_RUNE; + + pugi::xml_attribute attr; + if (!(attr = node.attribute("id"))) { + std::cout << "[Error - RuneSpell::configureSpell] Rune spell without id." << std::endl; + return false; + } + runeId = pugi::cast(attr.value()); + + if ((attr = node.attribute("charges"))) { + charges = pugi::cast(attr.value()); + } else { + charges = 0; + } + + hasCharges = (charges > 0); + if (magLevel != 0 || level != 0) { + //Change information in the ItemType to get accurate description + ItemType& iType = Item::items.getItemType(runeId); + iType.runeMagLevel = magLevel; + iType.runeLevel = level; + iType.charges = charges; + } + + return true; +} + +ReturnValue RuneSpell::canExecuteAction(const Player* player, const Position& toPos) +{ + if (player->hasFlag(PlayerFlag_CannotUseSpells)) { + return RETURNVALUE_CANNOTUSETHISOBJECT; + } + + ReturnValue ret = Action::canExecuteAction(player, toPos); + if (ret != RETURNVALUE_NOERROR) { + return ret; + } + + if (toPos.x == 0xFFFF) { + if (needTarget) { + return RETURNVALUE_CANONLYUSETHISRUNEONCREATURES; + } else if (!selfTarget) { + return RETURNVALUE_NOTENOUGHROOM; + } + } + + return RETURNVALUE_NOERROR; +} + +bool RuneSpell::executeUse(Player* player, Item* item, const Position&, Thing* target, const Position& toPosition, bool isHotkey) +{ + if (!playerRuneSpellCheck(player, toPosition)) { + return false; + } + + if (!scripted) { + return false; + } + + LuaVariant var; + + if (needTarget) { + var.type = VARIANT_NUMBER; + + if (target == nullptr) { + Tile* toTile = g_game.map.getTile(toPosition); + if (toTile) { + const Creature* visibleCreature = toTile->getBottomVisibleCreature(player); + if (visibleCreature) { + var.number = visibleCreature->getID(); + } + } + } else { + var.number = target->getCreature()->getID(); + } + } else { + var.type = VARIANT_POSITION; + var.pos = toPosition; + } + + if (!internalCastSpell(player, var, isHotkey)) { + return false; + } + + postCastSpell(player); + + target = g_game.getCreatureByID(var.number); + if (getPzLock() && target) { + player->onAttackedCreature(target->getCreature()); + } + + if (hasCharges && item && g_config.getBoolean(ConfigManager::REMOVE_RUNE_CHARGES)) { + int32_t newCount = std::max(0, item->getItemCount() - 1); + g_game.transformItem(item, item->getID(), newCount); + } + return true; +} + +bool RuneSpell::castSpell(Creature* creature) +{ + LuaVariant var; + var.type = VARIANT_NUMBER; + var.number = creature->getID(); + return internalCastSpell(creature, var, false); +} + +bool RuneSpell::castSpell(Creature* creature, Creature* target) +{ + LuaVariant var; + var.type = VARIANT_NUMBER; + var.number = target->getID(); + return internalCastSpell(creature, var, false); +} + +bool RuneSpell::internalCastSpell(Creature* creature, const LuaVariant& var, bool isHotkey) +{ + bool result; + if (scripted) { + result = executeCastSpell(creature, var, isHotkey); + } else { + result = false; + } + return result; +} + +bool RuneSpell::executeCastSpell(Creature* creature, const LuaVariant& var, bool isHotkey) +{ + //onCastSpell(creature, var, isHotkey) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - RuneSpell::executeCastSpell] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + + LuaScriptInterface::pushUserdata(L, creature); + LuaScriptInterface::setCreatureMetatable(L, -1, creature); + + LuaScriptInterface::pushVariant(L, var); + + LuaScriptInterface::pushBoolean(L, isHotkey); + + return scriptInterface->callFunction(3); +} diff --git a/src/spells.h b/src/spells.h new file mode 100644 index 0000000..f0febf4 --- /dev/null +++ b/src/spells.h @@ -0,0 +1,457 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_SPELLS_H_D78A7CCB7080406E8CAA6B1D31D3DA71 +#define FS_SPELLS_H_D78A7CCB7080406E8CAA6B1D31D3DA71 + +#include "luascript.h" +#include "player.h" +#include "actions.h" +#include "talkaction.h" +#include "baseevents.h" + +class InstantSpell; +class RuneSpell; +class Spell; + +using VocSpellMap = std::map; +using InstantSpell_ptr = std::unique_ptr; +using RuneSpell_ptr = std::unique_ptr; + +class Spells final : public BaseEvents +{ + public: + Spells(); + ~Spells(); + + // non-copyable + Spells(const Spells&) = delete; + Spells& operator=(const Spells&) = delete; + + Spell* getSpellByName(const std::string& name); + RuneSpell* getRuneSpell(uint32_t id); + RuneSpell* getRuneSpellByName(const std::string& name); + + InstantSpell* getInstantSpell(const std::string& words); + InstantSpell* getInstantSpellByName(const std::string& name); + + InstantSpell* getInstantSpellById(uint32_t spellId); + + TalkActionResult_t playerSaySpell(Player* player, std::string& words); + + static Position getCasterPosition(Creature* creature, Direction dir); + std::string getScriptBaseName() const override; + + const std::map& getInstantSpells() const { + return instants; + }; + + void clearMaps(bool fromLua); + void clear(bool fromLua) override final; + bool registerInstantLuaEvent(InstantSpell* event); + bool registerRuneLuaEvent(RuneSpell* event); + + private: + LuaScriptInterface& getScriptInterface() override; + Event_ptr getEvent(const std::string& nodeName) override; + bool registerEvent(Event_ptr event, const pugi::xml_node& node) override; + + std::map runes; + std::map instants; + + friend class CombatSpell; + LuaScriptInterface scriptInterface { "Spell Interface" }; +}; + +using RuneSpellFunction = std::function; + +class BaseSpell +{ + public: + constexpr BaseSpell() = default; + virtual ~BaseSpell() = default; + + virtual bool castSpell(Creature* creature) = 0; + virtual bool castSpell(Creature* creature, Creature* target) = 0; +}; + +class CombatSpell final : public Event, public BaseSpell +{ + public: + CombatSpell(Combat* combat, bool needTarget, bool needDirection); + ~CombatSpell(); + + // non-copyable + CombatSpell(const CombatSpell&) = delete; + CombatSpell& operator=(const CombatSpell&) = delete; + + bool castSpell(Creature* creature) override; + bool castSpell(Creature* creature, Creature* target) override; + bool configureEvent(const pugi::xml_node&) override { + return true; + } + + //scripting + bool executeCastSpell(Creature* creature, const LuaVariant& var); + + bool loadScriptCombat(); + Combat* getCombat() { + return combat; + } + + private: + std::string getScriptEventName() const override { + return "onCastSpell"; + } + + Combat* combat; + + bool needDirection; + bool needTarget; +}; + +class Spell : public BaseSpell +{ + public: + Spell() = default; + + bool configureSpell(const pugi::xml_node& node); + const std::string& getName() const { + return name; + } + void setName(std::string n) { + name = n; + } + uint8_t getId() const { + return spellId; + } + void setId(uint8_t id) { + spellId = id; + } + + void postCastSpell(Player* player, bool finishedCast = true, bool payCost = true) const; + static void postCastSpell(Player* player, uint32_t manaCost, uint32_t soulCost); + + uint32_t getManaCost(const Player* player) const; + uint32_t getSoulCost() const { + return soul; + } + void setSoulCost(uint32_t s) { + soul = s; + } + uint32_t getLevel() const { + return level; + } + void setLevel(uint32_t lvl) { + level = lvl; + } + uint32_t getMagicLevel() const { + return magLevel; + } + void setMagicLevel(uint32_t lvl) { + magLevel = lvl; + } + uint32_t getMana() const { + return mana; + } + void setMana(uint32_t m) { + mana = m; + } + uint32_t getManaPercent() const { + return manaPercent; + } + void setManaPercent(uint32_t m) { + manaPercent = m; + } + bool isPremium() const { + return premium; + } + void setPremium(bool p) { + premium = p; + } + bool isEnabled() const { + return enabled; + } + void setEnabled(bool e) { + enabled = e; + } + + virtual bool isInstant() const = 0; + bool isLearnable() const { + return learnable; + } + void setLearnable(bool l) { + learnable = l; + } + + const VocSpellMap& getVocMap() const { + return vocSpellMap; + } + void addVocMap(uint16_t n, bool b) { + vocSpellMap[n] = b; + } + + const SpellGroup_t getGroup() const { + return group; + } + void setGroup(SpellGroup_t g) { + group = g; + } + const SpellGroup_t getSecondaryGroup() const { + return secondaryGroup; + } + void setSecondaryGroup(SpellGroup_t g) { + secondaryGroup = g; + } + + uint32_t getCooldown() const { + return cooldown; + } + void setCooldown(uint32_t cd) { + cooldown = cd; + } + uint32_t getSecondaryCooldown() const { + return secondaryGroupCooldown; + } + void setSecondaryCooldown(uint32_t cd) { + secondaryGroupCooldown = cd; + } + uint32_t getGroupCooldown() const { + return groupCooldown; + } + void setGroupCooldown(uint32_t cd) { + groupCooldown = cd; + } + + int32_t getRange() const { + return range; + } + void setRange(int32_t r) { + range = r; + } + + bool getNeedTarget() const { + return needTarget; + } + void setNeedTarget(bool n) { + needTarget = n; + } + bool getNeedWeapon() const { + return needWeapon; + } + void setNeedWeapon(bool n) { + needWeapon = n; + } + bool getNeedLearn() const { + return learnable; + } + void setNeedLearn(bool n) { + learnable = n; + } + bool getSelfTarget() const { + return selfTarget; + } + void setSelfTarget(bool s) { + selfTarget = s; + } + bool getBlockingSolid() const { + return blockingSolid; + } + void setBlockingSolid(bool b) { + blockingSolid = b; + } + bool getBlockingCreature() const { + return blockingCreature; + } + void setBlockingCreature(bool b) { + blockingCreature = b; + } + bool getAggressive() const { + return aggressive; + } + void setAggressive(bool a) { + aggressive = a; + } + bool getPzLock() const { + return pzLock; + } + void setPzLock(bool pzLock) { + this->pzLock = pzLock; + } + + SpellType_t spellType = SPELL_UNDEFINED; + + protected: + bool playerSpellCheck(Player* player) const; + bool playerInstantSpellCheck(Player* player, const Position& toPos); + bool playerRuneSpellCheck(Player* player, const Position& toPos); + + VocSpellMap vocSpellMap; + + SpellGroup_t group = SPELLGROUP_NONE; + SpellGroup_t secondaryGroup = SPELLGROUP_NONE; + + uint32_t cooldown = 1000; + uint32_t groupCooldown = 1000; + uint32_t secondaryGroupCooldown = 0; + uint32_t level = 0; + uint32_t magLevel = 0; + int32_t range = -1; + + uint8_t spellId = 0; + + bool selfTarget = false; + bool needTarget = false; + + private: + + uint32_t mana = 0; + uint32_t manaPercent = 0; + uint32_t soul = 0; + + bool needWeapon = false; + bool blockingSolid = false; + bool blockingCreature = false; + bool aggressive = true; + bool pzLock = false; + bool learnable = false; + bool enabled = true; + bool premium = false; + + + private: + std::string name; +}; + +class InstantSpell final : public TalkAction, public Spell +{ + public: + explicit InstantSpell(LuaScriptInterface* interface) : TalkAction(interface) {} + + bool configureEvent(const pugi::xml_node& node) override; + + virtual bool playerCastInstant(Player* player, std::string& param); + + bool castSpell(Creature* creature) override; + bool castSpell(Creature* creature, Creature* target) override; + + //scripting + bool executeCastSpell(Creature* creature, const LuaVariant& var); + + bool isInstant() const override { + return true; + } + bool getHasParam() const { + return hasParam; + } + void setHasParam(bool p) { + hasParam = p; + } + bool getHasPlayerNameParam() const { + return hasPlayerNameParam; + } + void setHasPlayerNameParam(bool p) { + hasPlayerNameParam = p; + } + bool getNeedDirection() const { + return needDirection; + } + void setNeedDirection(bool n) { + needDirection = n; + } + bool getNeedCasterTargetOrDirection() const { + return casterTargetOrDirection; + } + void setNeedCasterTargetOrDirection(bool d) { + casterTargetOrDirection = d; + } + bool getBlockWalls() const { + return checkLineOfSight; + } + void setBlockWalls(bool w) { + checkLineOfSight = w; + } + bool canCast(const Player* player) const; + bool canThrowSpell(const Creature* creature, const Creature* target) const; + + private: + std::string getScriptEventName() const override; + + bool internalCastSpell(Creature* creature, const LuaVariant& var); + + bool needDirection = false; + bool hasParam = false; + bool hasPlayerNameParam = false; + bool checkLineOfSight = true; + bool casterTargetOrDirection = false; +}; + +class RuneSpell final : public Action, public Spell +{ + public: + explicit RuneSpell(LuaScriptInterface* interface) : Action(interface) {} + + bool configureEvent(const pugi::xml_node& node) override; + + ReturnValue canExecuteAction(const Player* player, const Position& toPos) override; + bool hasOwnErrorHandler() override { + return true; + } + Thing* getTarget(Player*, Creature* targetCreature, const Position&, uint8_t) const override { + return targetCreature; + } + + bool executeUse(Player* player, Item* item, const Position& fromPosition, Thing* target, const Position& toPosition, bool isHotkey) override; + + bool castSpell(Creature* creature) override; + bool castSpell(Creature* creature, Creature* target) override; + + //scripting + bool executeCastSpell(Creature* creature, const LuaVariant& var, bool isHotkey); + + bool isInstant() const override { + return false; + } + uint16_t getRuneItemId() const { + return runeId; + } + void setRuneItemId(uint16_t i) { + runeId = i; + } + uint32_t getCharges() const { + return charges; + } + void setCharges(uint32_t c) { + if (c > 0) { + hasCharges = true; + } + charges = c; + } + + private: + std::string getScriptEventName() const override; + + bool internalCastSpell(Creature* creature, const LuaVariant& var, bool isHotkey); + + uint16_t runeId = 0; + uint32_t charges = 0; + bool hasCharges = false; +}; + +#endif diff --git a/src/talkaction.cpp b/src/talkaction.cpp new file mode 100644 index 0000000..2a30410 --- /dev/null +++ b/src/talkaction.cpp @@ -0,0 +1,169 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "player.h" +#include "talkaction.h" +#include "pugicast.h" + +TalkActions::TalkActions() + : scriptInterface("TalkAction Interface") +{ + scriptInterface.initState(); +} + +TalkActions::~TalkActions() +{ + clear(false); +} + +void TalkActions::clear(bool fromLua) +{ + for (auto it = talkActions.begin(); it != talkActions.end(); ) { + if (fromLua == it->second.fromLua) { + it = talkActions.erase(it); + } else { + ++it; + } + } + + reInitState(fromLua); +} + +LuaScriptInterface& TalkActions::getScriptInterface() +{ + return scriptInterface; +} + +std::string TalkActions::getScriptBaseName() const +{ + return "talkactions"; +} + +Event_ptr TalkActions::getEvent(const std::string& nodeName) +{ + if (strcasecmp(nodeName.c_str(), "talkaction") != 0) { + return nullptr; + } + return Event_ptr(new TalkAction(&scriptInterface)); +} + +bool TalkActions::registerEvent(Event_ptr event, const pugi::xml_node&) +{ + TalkAction_ptr talkAction{static_cast(event.release())}; // event is guaranteed to be a TalkAction + talkActions.emplace(talkAction->getWords(), std::move(*talkAction)); + return true; +} + +bool TalkActions::registerLuaEvent(TalkAction* event) +{ + TalkAction_ptr talkAction{ event }; + talkActions.emplace(talkAction->getWords(), std::move(*talkAction)); + return true; +} + +TalkActionResult_t TalkActions::playerSaySpell(Player* player, SpeakClasses type, const std::string& words) const +{ + size_t wordsLength = words.length(); + for (auto it = talkActions.begin(); it != talkActions.end(); ) { + const std::string& talkactionWords = it->first; + size_t talkactionLength = talkactionWords.length(); + if (wordsLength < talkactionLength || strncasecmp(words.c_str(), talkactionWords.c_str(), talkactionLength) != 0) { + ++it; + continue; + } + + std::string param; + if (wordsLength != talkactionLength) { + param = words.substr(talkactionLength); + if (param.front() != ' ') { + ++it; + continue; + } + trim_left(param, ' '); + + std::string separator = it->second.getSeparator(); + if (separator != " ") { + if (!param.empty()) { + if (param != separator) { + ++it; + continue; + } else { + param.erase(param.begin()); + } + } + } + } + + if (it->second.executeSay(player, param, type)) { + return TALKACTION_CONTINUE; + } else { + return TALKACTION_BREAK; + } + } + return TALKACTION_CONTINUE; +} + +bool TalkAction::configureEvent(const pugi::xml_node& node) +{ + pugi::xml_attribute wordsAttribute = node.attribute("words"); + if (!wordsAttribute) { + std::cout << "[Error - TalkAction::configureEvent] Missing words for talk action or spell" << std::endl; + return false; + } + + pugi::xml_attribute separatorAttribute = node.attribute("separator"); + if (separatorAttribute) { + separator = pugi::cast(separatorAttribute.value()); + } + + words = wordsAttribute.as_string(); + return true; +} + +std::string TalkAction::getScriptEventName() const +{ + return "onSay"; +} + +bool TalkAction::executeSay(Player* player, const std::string& param, SpeakClasses type) const +{ + //onSay(player, words, param, type) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - TalkAction::executeSay] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + + LuaScriptInterface::pushString(L, words); + LuaScriptInterface::pushString(L, param); + lua_pushnumber(L, type); + + return scriptInterface->callFunction(4); +} diff --git a/src/talkaction.h b/src/talkaction.h new file mode 100644 index 0000000..686c38b --- /dev/null +++ b/src/talkaction.h @@ -0,0 +1,93 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_TALKACTION_H_E6AABAC0F89843469526ADF310F3131C +#define FS_TALKACTION_H_E6AABAC0F89843469526ADF310F3131C + +#include "luascript.h" +#include "baseevents.h" +#include "const.h" + +class TalkAction; +using TalkAction_ptr = std::unique_ptr; + +enum TalkActionResult_t { + TALKACTION_CONTINUE, + TALKACTION_BREAK, + TALKACTION_FAILED, +}; + +class TalkAction : public Event +{ + public: + explicit TalkAction(LuaScriptInterface* interface) : Event(interface) {} + + bool configureEvent(const pugi::xml_node& node) override; + + const std::string& getWords() const { + return words; + } + void setWords(std::string word) { + words = word; + } + std::string getSeparator() const { + return separator; + } + void setSeparator(std::string sep) { + separator = sep; + } + + //scripting + bool executeSay(Player* player, const std::string& param, SpeakClasses type) const; + // + + private: + std::string getScriptEventName() const override; + + std::string words; + std::string separator = "\""; +}; + +class TalkActions final : public BaseEvents +{ + public: + TalkActions(); + ~TalkActions(); + + // non-copyable + TalkActions(const TalkActions&) = delete; + TalkActions& operator=(const TalkActions&) = delete; + + TalkActionResult_t playerSaySpell(Player* player, SpeakClasses type, const std::string& words) const; + + bool registerLuaEvent(TalkAction* event); + void clear(bool fromLua) override final; + + private: + LuaScriptInterface& getScriptInterface() override; + std::string getScriptBaseName() const override; + Event_ptr getEvent(const std::string& nodeName) override; + bool registerEvent(Event_ptr event, const pugi::xml_node& node) override; + + std::map talkActions; + + LuaScriptInterface scriptInterface; +}; + +#endif diff --git a/src/tasks.cpp b/src/tasks.cpp new file mode 100644 index 0000000..58a82a0 --- /dev/null +++ b/src/tasks.cpp @@ -0,0 +1,106 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "tasks.h" +#include "game.h" + +extern Game g_game; + +Task* createTask(std::function f) +{ + return new Task(std::move(f)); +} + +Task* createTask(uint32_t expiration, std::function f) +{ + return new Task(expiration, std::move(f)); +} + +void Dispatcher::threadMain() +{ + // NOTE: second argument defer_lock is to prevent from immediate locking + std::unique_lock taskLockUnique(taskLock, std::defer_lock); + + while (getState() != THREAD_STATE_TERMINATED) { + // check if there are tasks waiting + taskLockUnique.lock(); + + if (taskList.empty()) { + //if the list is empty wait for signal + taskSignal.wait(taskLockUnique); + } + + if (!taskList.empty()) { + // take the first task + Task* task = taskList.front(); + taskList.pop_front(); + taskLockUnique.unlock(); + + if (!task->hasExpired()) { + ++dispatcherCycle; + // execute it + (*task)(); + } + delete task; + } else { + taskLockUnique.unlock(); + } + } +} + +void Dispatcher::addTask(Task* task, bool push_front /*= false*/) +{ + bool do_signal = false; + + taskLock.lock(); + + if (getState() == THREAD_STATE_RUNNING) { + do_signal = taskList.empty(); + + if (push_front) { + taskList.push_front(task); + } else { + taskList.push_back(task); + } + } else { + delete task; + } + + taskLock.unlock(); + + // send a signal if the list was empty + if (do_signal) { + taskSignal.notify_one(); + } +} + +void Dispatcher::shutdown() +{ + Task* task = createTask([this]() { + setState(THREAD_STATE_TERMINATED); + taskSignal.notify_one(); + }); + + std::lock_guard lockClass(taskLock); + taskList.push_back(task); + + taskSignal.notify_one(); +} diff --git a/src/tasks.h b/src/tasks.h new file mode 100644 index 0000000..6f6af57 --- /dev/null +++ b/src/tasks.h @@ -0,0 +1,90 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_TASKS_H_A66AC384766041E59DCA059DAB6E1976 +#define FS_TASKS_H_A66AC384766041E59DCA059DAB6E1976 + +#include +#include "thread_holder_base.h" +#include "enums.h" + +const int DISPATCHER_TASK_EXPIRATION = 2000; +const auto SYSTEM_TIME_ZERO = std::chrono::system_clock::time_point(std::chrono::milliseconds(0)); + +class Task +{ + public: + // DO NOT allocate this class on the stack + explicit Task(std::function&& f) : func(std::move(f)) {} + Task(uint32_t ms, std::function&& f) : + expiration(std::chrono::system_clock::now() + std::chrono::milliseconds(ms)), func(std::move(f)) {} + + virtual ~Task() = default; + void operator()() { + func(); + } + + void setDontExpire() { + expiration = SYSTEM_TIME_ZERO; + } + + bool hasExpired() const { + if (expiration == SYSTEM_TIME_ZERO) { + return false; + } + return expiration < std::chrono::system_clock::now(); + } + + protected: + std::chrono::system_clock::time_point expiration = SYSTEM_TIME_ZERO; + + private: + // Expiration has another meaning for scheduler tasks, + // then it is the time the task should be added to the + // dispatcher + std::function func; +}; + +Task* createTask(std::function f); +Task* createTask(uint32_t expiration, std::function f); + +class Dispatcher : public ThreadHolder { + public: + void addTask(Task* task, bool push_front = false); + + void shutdown(); + + uint64_t getDispatcherCycle() const { + return dispatcherCycle; + } + + void threadMain(); + + private: + std::thread thread; + std::mutex taskLock; + std::condition_variable taskSignal; + + std::list taskList; + uint64_t dispatcherCycle = 0; +}; + +extern Dispatcher g_dispatcher; + +#endif diff --git a/src/teleport.cpp b/src/teleport.cpp new file mode 100644 index 0000000..34994ff --- /dev/null +++ b/src/teleport.cpp @@ -0,0 +1,145 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "teleport.h" +#include "game.h" + +extern Game g_game; + +Attr_ReadValue Teleport::readAttr(AttrTypes_t attr, PropStream& propStream) +{ + if (attr == ATTR_TELE_DEST) { + if (!propStream.read(destPos.x) || !propStream.read(destPos.y) || !propStream.read(destPos.z)) { + return ATTR_READ_ERROR; + } + return ATTR_READ_CONTINUE; + } + return Item::readAttr(attr, propStream); +} + +void Teleport::serializeAttr(PropWriteStream& propWriteStream) const +{ + Item::serializeAttr(propWriteStream); + + propWriteStream.write(ATTR_TELE_DEST); + propWriteStream.write(destPos.x); + propWriteStream.write(destPos.y); + propWriteStream.write(destPos.z); +} + +ReturnValue Teleport::queryAdd(int32_t, const Thing&, uint32_t, uint32_t, Creature*) const +{ + return RETURNVALUE_NOTPOSSIBLE; +} + +ReturnValue Teleport::queryMaxCount(int32_t, const Thing&, uint32_t, uint32_t&, uint32_t) const +{ + return RETURNVALUE_NOTPOSSIBLE; +} + +ReturnValue Teleport::queryRemove(const Thing&, uint32_t, uint32_t) const +{ + return RETURNVALUE_NOERROR; +} + +Cylinder* Teleport::queryDestination(int32_t&, const Thing&, Item**, uint32_t&) +{ + return this; +} + +void Teleport::addThing(Thing* thing) +{ + return addThing(0, thing); +} + +bool Teleport::checkInfinityLoop(Tile* destTile) +{ + if (!destTile) { + return false; + } + + if (Teleport* teleport = destTile->getTeleportItem()) { + const Position& nextDestPos = teleport->getDestPos(); + if (getPosition() == nextDestPos) { + return true; + } + return checkInfinityLoop(g_game.map.getTile(nextDestPos)); + } + return false; +} + +void Teleport::addThing(int32_t, Thing* thing) +{ + Tile* destTile = g_game.map.getTile(destPos); + if (!destTile) { + return; + } + + // Prevent infinity loop + if (checkInfinityLoop(destTile)) { + const Position& pos = getPosition(); + std::cout << "Warning: infinity loop teleport. " << pos << std::endl; + return; + } + + const MagicEffectClasses effect = Item::items[id].magicEffect; + + if (Creature* creature = thing->getCreature()) { + Position origPos = creature->getPosition(); + g_game.internalCreatureTurn(creature, origPos.x > destPos.x ? DIRECTION_WEST : DIRECTION_EAST); + g_game.map.moveCreature(*creature, *destTile); + if (effect != CONST_ME_NONE) { + g_game.addMagicEffect(origPos, effect); + g_game.addMagicEffect(destTile->getPosition(), effect); + } + } else if (Item* item = thing->getItem()) { + if (effect != CONST_ME_NONE) { + g_game.addMagicEffect(destTile->getPosition(), effect); + g_game.addMagicEffect(item->getPosition(), effect); + } + g_game.internalMoveItem(getTile(), destTile, INDEX_WHEREEVER, item, item->getItemCount(), nullptr, FLAG_NOLIMIT); + } +} + +void Teleport::updateThing(Thing*, uint16_t, uint32_t) +{ + // +} + +void Teleport::replaceThing(uint32_t, Thing*) +{ + // +} + +void Teleport::removeThing(Thing*, uint32_t) +{ + // +} + +void Teleport::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t) +{ + getParent()->postAddNotification(thing, oldParent, index, LINK_PARENT); +} + +void Teleport::postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t) +{ + getParent()->postRemoveNotification(thing, newParent, index, LINK_PARENT); +} diff --git a/src/teleport.h b/src/teleport.h new file mode 100644 index 0000000..c2701c9 --- /dev/null +++ b/src/teleport.h @@ -0,0 +1,74 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_TELEPORT_H_873B7F7F1DB24101A7ACFB54B25E0ABC +#define FS_TELEPORT_H_873B7F7F1DB24101A7ACFB54B25E0ABC + +#include "tile.h" + +class Teleport final : public Item, public Cylinder +{ + public: + explicit Teleport(uint16_t type) : Item(type) {}; + + Teleport* getTeleport() override { + return this; + } + const Teleport* getTeleport() const override { + return this; + } + + //serialization + Attr_ReadValue readAttr(AttrTypes_t attr, PropStream& propStream) override; + void serializeAttr(PropWriteStream& propWriteStream) const override; + + const Position& getDestPos() const { + return destPos; + } + void setDestPos(Position pos) { + destPos = std::move(pos); + } + + bool checkInfinityLoop(Tile* destTile); + + //cylinder implementations + ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count, + uint32_t flags, Creature* actor = nullptr) const override; + ReturnValue queryMaxCount(int32_t index, const Thing& thing, uint32_t count, + uint32_t& maxQueryCount, uint32_t flags) const override; + ReturnValue queryRemove(const Thing& thing, uint32_t count, uint32_t flags) const override; + Cylinder* queryDestination(int32_t& index, const Thing& thing, Item** destItem, + uint32_t& flags) override; + + void addThing(Thing* thing) override; + void addThing(int32_t index, Thing* thing) override; + + void updateThing(Thing* thing, uint16_t itemId, uint32_t count) override; + void replaceThing(uint32_t index, Thing* thing) override; + + void removeThing(Thing* thing, uint32_t count) override; + + void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + + private: + Position destPos; +}; + +#endif diff --git a/src/thing.cpp b/src/thing.cpp new file mode 100644 index 0000000..abda31f --- /dev/null +++ b/src/thing.cpp @@ -0,0 +1,42 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "thing.h" +#include "tile.h" + +const Position& Thing::getPosition() const +{ + const Tile* tile = getTile(); + if (!tile) { + return Tile::nullptr_tile.getPosition(); + } + return tile->getPosition(); +} + +Tile* Thing::getTile() +{ + return dynamic_cast(this); +} + +const Tile* Thing::getTile() const +{ + return dynamic_cast(this); +} diff --git a/src/thing.h b/src/thing.h new file mode 100644 index 0000000..3fee9d2 --- /dev/null +++ b/src/thing.h @@ -0,0 +1,85 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_THING_H_6F16A8E566AF4ACEAE02CF32A7246144 +#define FS_THING_H_6F16A8E566AF4ACEAE02CF32A7246144 + +#include "position.h" + +class Tile; +class Cylinder; +class Item; +class Creature; +class Container; + +class Thing +{ + public: + constexpr Thing() = default; + virtual ~Thing() = default; + + // non-copyable + Thing(const Thing&) = delete; + Thing& operator=(const Thing&) = delete; + + virtual std::string getDescription(int32_t lookDistance) const = 0; + + virtual Cylinder* getParent() const { + return nullptr; + } + virtual Cylinder* getRealParent() const { + return getParent(); + } + + virtual void setParent(Cylinder*) { + // + } + + virtual Tile* getTile(); + virtual const Tile* getTile() const; + + virtual const Position& getPosition() const; + virtual int32_t getThrowRange() const = 0; + virtual bool isPushable() const = 0; + + virtual Container* getContainer() { + return nullptr; + } + virtual const Container* getContainer() const { + return nullptr; + } + virtual Item* getItem() { + return nullptr; + } + virtual const Item* getItem() const { + return nullptr; + } + virtual Creature* getCreature() { + return nullptr; + } + virtual const Creature* getCreature() const { + return nullptr; + } + + virtual bool isRemoved() const { + return true; + } +}; + +#endif diff --git a/src/thread_holder_base.h b/src/thread_holder_base.h new file mode 100644 index 0000000..1ab206b --- /dev/null +++ b/src/thread_holder_base.h @@ -0,0 +1,59 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_THREAD_HOLDER_H_BEB56FC46748E71D15A5BF0773ED2E67 +#define FS_THREAD_HOLDER_H_BEB56FC46748E71D15A5BF0773ED2E67 + +#include +#include +#include "enums.h" + +template +class ThreadHolder +{ + public: + ThreadHolder() {} + void start() { + setState(THREAD_STATE_RUNNING); + thread = std::thread(&Derived::threadMain, static_cast(this)); + } + + void stop() { + setState(THREAD_STATE_CLOSING); + } + + void join() { + if (thread.joinable()) { + thread.join(); + } + } + protected: + void setState(ThreadState newState) { + threadState.store(newState, std::memory_order_relaxed); + } + + ThreadState getState() const { + return threadState.load(std::memory_order_relaxed); + } + private: + std::atomic threadState{THREAD_STATE_TERMINATED}; + std::thread thread; +}; + +#endif diff --git a/src/tile.cpp b/src/tile.cpp new file mode 100644 index 0000000..e192e9f --- /dev/null +++ b/src/tile.cpp @@ -0,0 +1,1628 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include + +#include "tile.h" + +#include "creature.h" +#include "combat.h" +#include "game.h" +#include "mailbox.h" +#include "monster.h" +#include "movement.h" +#include "teleport.h" +#include "trashholder.h" + +extern Game g_game; +extern MoveEvents* g_moveEvents; + +StaticTile real_nullptr_tile(0xFFFF, 0xFFFF, 0xFF); +Tile& Tile::nullptr_tile = real_nullptr_tile; + +bool Tile::hasProperty(ITEMPROPERTY prop) const +{ + if (ground && ground->hasProperty(prop)) { + return true; + } + + if (const TileItemVector* items = getItemList()) { + for (const Item* item : *items) { + if (item->hasProperty(prop)) { + return true; + } + } + } + return false; +} + +bool Tile::hasProperty(const Item* exclude, ITEMPROPERTY prop) const +{ + assert(exclude); + + if (ground && exclude != ground && ground->hasProperty(prop)) { + return true; + } + + if (const TileItemVector* items = getItemList()) { + for (const Item* item : *items) { + if (item != exclude && item->hasProperty(prop)) { + return true; + } + } + } + + return false; +} + +bool Tile::hasHeight(uint32_t n) const +{ + uint32_t height = 0; + + if (ground) { + if (ground->hasProperty(CONST_PROP_HASHEIGHT)) { + ++height; + } + + if (n == height) { + return true; + } + } + + if (const TileItemVector* items = getItemList()) { + for (const Item* item : *items) { + if (item->hasProperty(CONST_PROP_HASHEIGHT)) { + ++height; + } + + if (n == height) { + return true; + } + } + } + return false; +} + +size_t Tile::getCreatureCount() const +{ + if (const CreatureVector* creatures = getCreatures()) { + return creatures->size(); + } + return 0; +} + +size_t Tile::getItemCount() const +{ + if (const TileItemVector* items = getItemList()) { + return items->size(); + } + return 0; +} + +uint32_t Tile::getTopItemCount() const +{ + if (const TileItemVector* items = getItemList()) { + return items->getTopItemCount(); + } + return 0; +} + +uint32_t Tile::getDownItemCount() const +{ + if (const TileItemVector* items = getItemList()) { + return items->getDownItemCount(); + } + return 0; +} + +std::string Tile::getDescription(int32_t) const +{ + return "You dont know why, but you cant see anything!"; +} + +Teleport* Tile::getTeleportItem() const +{ + if (!hasFlag(TILESTATE_TELEPORT)) { + return nullptr; + } + + if (const TileItemVector* items = getItemList()) { + for (auto it = items->rbegin(), end = items->rend(); it != end; ++it) { + if ((*it)->getTeleport()) { + return (*it)->getTeleport(); + } + } + } + return nullptr; +} + +MagicField* Tile::getFieldItem() const +{ + if (!hasFlag(TILESTATE_MAGICFIELD)) { + return nullptr; + } + + if (ground && ground->getMagicField()) { + return ground->getMagicField(); + } + + if (const TileItemVector* items = getItemList()) { + for (auto it = items->rbegin(), end = items->rend(); it != end; ++it) { + if ((*it)->getMagicField()) { + return (*it)->getMagicField(); + } + } + } + return nullptr; +} + +TrashHolder* Tile::getTrashHolder() const +{ + if (!hasFlag(TILESTATE_TRASHHOLDER)) { + return nullptr; + } + + if (ground && ground->getTrashHolder()) { + return ground->getTrashHolder(); + } + + if (const TileItemVector* items = getItemList()) { + for (auto it = items->rbegin(), end = items->rend(); it != end; ++it) { + if ((*it)->getTrashHolder()) { + return (*it)->getTrashHolder(); + } + } + } + return nullptr; +} + +Mailbox* Tile::getMailbox() const +{ + if (!hasFlag(TILESTATE_MAILBOX)) { + return nullptr; + } + + if (ground && ground->getMailbox()) { + return ground->getMailbox(); + } + + if (const TileItemVector* items = getItemList()) { + for (auto it = items->rbegin(), end = items->rend(); it != end; ++it) { + if ((*it)->getMailbox()) { + return (*it)->getMailbox(); + } + } + } + return nullptr; +} + +BedItem* Tile::getBedItem() const +{ + if (!hasFlag(TILESTATE_BED)) { + return nullptr; + } + + if (ground && ground->getBed()) { + return ground->getBed(); + } + + if (const TileItemVector* items = getItemList()) { + for (auto it = items->rbegin(), end = items->rend(); it != end; ++it) { + if ((*it)->getBed()) { + return (*it)->getBed(); + } + } + } + return nullptr; +} + +Creature* Tile::getTopCreature() const +{ + if (const CreatureVector* creatures = getCreatures()) { + if (!creatures->empty()) { + return *creatures->begin(); + } + } + return nullptr; +} + +const Creature* Tile::getBottomCreature() const +{ + if (const CreatureVector* creatures = getCreatures()) { + if (!creatures->empty()) { + return *creatures->rbegin(); + } + } + return nullptr; +} + +Creature* Tile::getTopVisibleCreature(const Creature* creature) const +{ + if (const CreatureVector* creatures = getCreatures()) { + if (creature) { + const Player* player = creature->getPlayer(); + if (player && player->isAccessPlayer()) { + return getTopCreature(); + } + + for (Creature* tileCreature : *creatures) { + if (creature->canSeeCreature(tileCreature)) { + return tileCreature; + } + } + } else { + for (Creature* tileCreature : *creatures) { + if (!tileCreature->isInvisible()) { + const Player* player = tileCreature->getPlayer(); + if (!player || !player->isInGhostMode()) { + return tileCreature; + } + } + } + } + } + return nullptr; +} + +const Creature* Tile::getBottomVisibleCreature(const Creature* creature) const +{ + if (const CreatureVector* creatures = getCreatures()) { + if (creature) { + const Player* player = creature->getPlayer(); + if (player && player->isAccessPlayer()) { + return getBottomCreature(); + } + + for (auto it = creatures->rbegin(), end = creatures->rend(); it != end; ++it) { + if (creature->canSeeCreature(*it)) { + return *it; + } + } + } else { + for (auto it = creatures->rbegin(), end = creatures->rend(); it != end; ++it) { + if (!(*it)->isInvisible()) { + const Player* player = (*it)->getPlayer(); + if (!player || !player->isInGhostMode()) { + return *it; + } + } + } + } + } + return nullptr; +} + +Item* Tile::getTopDownItem() const +{ + if (const TileItemVector* items = getItemList()) { + return items->getTopDownItem(); + } + return nullptr; +} + +Item* Tile::getTopTopItem() const +{ + if (const TileItemVector* items = getItemList()) { + return items->getTopTopItem(); + } + return nullptr; +} + +Item* Tile::getItemByTopOrder(int32_t topOrder) +{ + //topOrder: + //1: borders + //2: ladders, signs, splashes + //3: doors etc + //4: creatures + if (TileItemVector* items = getItemList()) { + for (auto it = ItemVector::const_reverse_iterator(items->getEndTopItem()), end = ItemVector::const_reverse_iterator(items->getBeginTopItem()); it != end; ++it) { + if (Item::items[(*it)->getID()].alwaysOnTopOrder == topOrder) { + return (*it); + } + } + } + return nullptr; +} + +Thing* Tile::getTopVisibleThing(const Creature* creature) +{ + Thing* thing = getTopVisibleCreature(creature); + if (thing) { + return thing; + } + + TileItemVector* items = getItemList(); + if (items) { + for (ItemVector::const_iterator it = items->getBeginDownItem(), end = items->getEndDownItem(); it != end; ++it) { + const ItemType& iit = Item::items[(*it)->getID()]; + if (!iit.lookThrough) { + return (*it); + } + } + + for (auto it = ItemVector::const_reverse_iterator(items->getEndTopItem()), end = ItemVector::const_reverse_iterator(items->getBeginTopItem()); it != end; ++it) { + const ItemType& iit = Item::items[(*it)->getID()]; + if (!iit.lookThrough) { + return (*it); + } + } + } + + return ground; +} + +void Tile::onAddTileItem(Item* item) +{ + if (item->hasProperty(CONST_PROP_MOVEABLE) || item->getContainer()) { + auto it = g_game.browseFields.find(this); + if (it != g_game.browseFields.end()) { + it->second->addItemBack(item); + item->setParent(this); + } + } + + setTileFlags(item); + + const Position& cylinderMapPos = getPosition(); + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, cylinderMapPos, true); + + //send to client + for (Creature* spectator : spectators) { + if (Player* tmpPlayer = spectator->getPlayer()) { + tmpPlayer->sendAddTileItem(this, cylinderMapPos, item); + } + } + + //event methods + for (Creature* spectator : spectators) { + spectator->onAddTileItem(this, cylinderMapPos); + } +} + +void Tile::onUpdateTileItem(Item* oldItem, const ItemType& oldType, Item* newItem, const ItemType& newType) +{ + if (newItem->hasProperty(CONST_PROP_MOVEABLE) || newItem->getContainer()) { + auto it = g_game.browseFields.find(this); + if (it != g_game.browseFields.end()) { + int32_t index = it->second->getThingIndex(oldItem); + if (index != -1) { + it->second->replaceThing(index, newItem); + newItem->setParent(this); + } + } + } else if (oldItem->hasProperty(CONST_PROP_MOVEABLE) || oldItem->getContainer()) { + auto it = g_game.browseFields.find(this); + if (it != g_game.browseFields.end()) { + Cylinder* oldParent = oldItem->getParent(); + it->second->removeThing(oldItem, oldItem->getItemCount()); + oldItem->setParent(oldParent); + } + } + + const Position& cylinderMapPos = getPosition(); + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, cylinderMapPos, true); + + //send to client + for (Creature* spectator : spectators) { + if (Player* tmpPlayer = spectator->getPlayer()) { + tmpPlayer->sendUpdateTileItem(this, cylinderMapPos, newItem); + } + } + + //event methods + for (Creature* spectator : spectators) { + spectator->onUpdateTileItem(this, cylinderMapPos, oldItem, oldType, newItem, newType); + } +} + +void Tile::onRemoveTileItem(const SpectatorVec& spectators, const std::vector& oldStackPosVector, Item* item) +{ + if (item->hasProperty(CONST_PROP_MOVEABLE) || item->getContainer()) { + auto it = g_game.browseFields.find(this); + if (it != g_game.browseFields.end()) { + it->second->removeThing(item, item->getItemCount()); + } + } + + resetTileFlags(item); + + const Position& cylinderMapPos = getPosition(); + const ItemType& iType = Item::items[item->getID()]; + + //send to client + size_t i = 0; + for (Creature* spectator : spectators) { + if (Player* tmpPlayer = spectator->getPlayer()) { + tmpPlayer->sendRemoveTileThing(cylinderMapPos, oldStackPosVector[i++]); + } + } + + //event methods + for (Creature* spectator : spectators) { + spectator->onRemoveTileItem(this, cylinderMapPos, iType, item); + } +} + +void Tile::onUpdateTile(const SpectatorVec& spectators) +{ + const Position& cylinderMapPos = getPosition(); + + //send to clients + for (Creature* spectator : spectators) { + spectator->getPlayer()->sendUpdateTile(this, cylinderMapPos); + } +} + +ReturnValue Tile::queryAdd(int32_t, const Thing& thing, uint32_t, uint32_t flags, Creature*) const +{ + if (const Creature* creature = thing.getCreature()) { + if (hasBitSet(FLAG_NOLIMIT, flags)) { + return RETURNVALUE_NOERROR; + } + + if (hasBitSet(FLAG_PATHFINDING, flags) && hasFlag(TILESTATE_FLOORCHANGE | TILESTATE_TELEPORT)) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (ground == nullptr) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (const Monster* monster = creature->getMonster()) { + if (hasFlag(TILESTATE_PROTECTIONZONE | TILESTATE_FLOORCHANGE | TILESTATE_TELEPORT)) { + return RETURNVALUE_NOTPOSSIBLE; + } + + const CreatureVector* creatures = getCreatures(); + if (monster->canPushCreatures() && !monster->isSummon()) { + if (creatures) { + for (Creature* tileCreature : *creatures) { + if (tileCreature->getPlayer() && tileCreature->getPlayer()->isInGhostMode()) { + continue; + } + + const Monster* creatureMonster = tileCreature->getMonster(); + if (!creatureMonster || !tileCreature->isPushable() || + (creatureMonster->isSummon() && creatureMonster->getMaster()->getPlayer())) { + return RETURNVALUE_NOTPOSSIBLE; + } + } + } + } else if (creatures && !creatures->empty()) { + for (const Creature* tileCreature : *creatures) { + if (!tileCreature->isInGhostMode()) { + return RETURNVALUE_NOTENOUGHROOM; + } + } + } + + if (hasFlag(TILESTATE_IMMOVABLEBLOCKSOLID)) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (hasBitSet(FLAG_PATHFINDING, flags) && hasFlag(TILESTATE_IMMOVABLENOFIELDBLOCKPATH)) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (hasFlag(TILESTATE_BLOCKSOLID) || (hasBitSet(FLAG_PATHFINDING, flags) && hasFlag(TILESTATE_NOFIELDBLOCKPATH))) { + if (!(monster->canPushItems() || hasBitSet(FLAG_IGNOREBLOCKITEM, flags))) { + return RETURNVALUE_NOTPOSSIBLE; + } + } + + MagicField* field = getFieldItem(); + if (!field || field->isBlocking() || field->getDamage() == 0) { + return RETURNVALUE_NOERROR; + } + + CombatType_t combatType = field->getCombatType(); + + //There is 3 options for a monster to enter a magic field + //1) Monster is immune + if (!monster->isImmune(combatType)) { + //1) Monster is able to walk over field type + //2) Being attacked while random stepping will make it ignore field damages + if (hasBitSet(FLAG_IGNOREFIELDDAMAGE, flags)) { + if (!(monster->canWalkOnFieldType(combatType) || monster->isIgnoringFieldDamage())) { + return RETURNVALUE_NOTPOSSIBLE; + } + } else { + return RETURNVALUE_NOTPOSSIBLE; + } + } + + return RETURNVALUE_NOERROR; + } + + const CreatureVector* creatures = getCreatures(); + if (const Player* player = creature->getPlayer()) { + if (creatures && !creatures->empty() && !hasBitSet(FLAG_IGNOREBLOCKCREATURE, flags) && !player->isAccessPlayer()) { + for (const Creature* tileCreature : *creatures) { + if (!player->canWalkthrough(tileCreature)) { + return RETURNVALUE_NOTPOSSIBLE; + } + } + } + + if (player->getParent() == nullptr && hasFlag(TILESTATE_NOLOGOUT)) { + //player is trying to login to a "no logout" tile + return RETURNVALUE_NOTPOSSIBLE; + } + + const Tile* playerTile = player->getTile(); + if (playerTile && player->isPzLocked()) { + if (!playerTile->hasFlag(TILESTATE_PVPZONE)) { + //player is trying to enter a pvp zone while being pz-locked + if (hasFlag(TILESTATE_PVPZONE)) { + return RETURNVALUE_PLAYERISPZLOCKEDENTERPVPZONE; + } + } else if (!hasFlag(TILESTATE_PVPZONE)) { + // player is trying to leave a pvp zone while being pz-locked + return RETURNVALUE_PLAYERISPZLOCKEDLEAVEPVPZONE; + } + + if ((!playerTile->hasFlag(TILESTATE_NOPVPZONE) && hasFlag(TILESTATE_NOPVPZONE)) || + (!playerTile->hasFlag(TILESTATE_PROTECTIONZONE) && hasFlag(TILESTATE_PROTECTIONZONE))) { + // player is trying to enter a non-pvp/protection zone while being pz-locked + return RETURNVALUE_PLAYERISPZLOCKED; + } + } + } else if (creatures && !creatures->empty() && !hasBitSet(FLAG_IGNOREBLOCKCREATURE, flags)) { + for (const Creature* tileCreature : *creatures) { + if (!tileCreature->isInGhostMode()) { + return RETURNVALUE_NOTENOUGHROOM; + } + } + } + + if (!hasBitSet(FLAG_IGNOREBLOCKITEM, flags)) { + //If the FLAG_IGNOREBLOCKITEM bit isn't set we dont have to iterate every single item + if (hasFlag(TILESTATE_BLOCKSOLID)) { + return RETURNVALUE_NOTENOUGHROOM; + } + } else { + //FLAG_IGNOREBLOCKITEM is set + if (ground) { + const ItemType& iiType = Item::items[ground->getID()]; + if (iiType.blockSolid && (!iiType.moveable || ground->hasAttribute(ITEM_ATTRIBUTE_UNIQUEID))) { + return RETURNVALUE_NOTPOSSIBLE; + } + } + + if (const auto items = getItemList()) { + for (const Item* item : *items) { + const ItemType& iiType = Item::items[item->getID()]; + if (iiType.blockSolid && (!iiType.moveable || item->hasAttribute(ITEM_ATTRIBUTE_UNIQUEID))) { + return RETURNVALUE_NOTPOSSIBLE; + } + } + } + } + } else if (const Item* item = thing.getItem()) { + const TileItemVector* items = getItemList(); + if (items && items->size() >= 0xFFFF) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (hasBitSet(FLAG_NOLIMIT, flags)) { + return RETURNVALUE_NOERROR; + } + + bool itemIsHangable = item->isHangable(); + if (ground == nullptr && !itemIsHangable) { + return RETURNVALUE_NOTPOSSIBLE; + } + + const CreatureVector* creatures = getCreatures(); + if (creatures && !creatures->empty() && item->isBlocking() && !hasBitSet(FLAG_IGNOREBLOCKCREATURE, flags)) { + for (const Creature* tileCreature : *creatures) { + if (!tileCreature->isInGhostMode()) { + return RETURNVALUE_NOTENOUGHROOM; + } + } + } + + if (itemIsHangable && hasFlag(TILESTATE_SUPPORTS_HANGABLE)) { + if (items) { + for (const Item* tileItem : *items) { + if (tileItem->isHangable()) { + return RETURNVALUE_NEEDEXCHANGE; + } + } + } + } else { + if (ground) { + const ItemType& iiType = Item::items[ground->getID()]; + if (iiType.blockSolid) { + if (!iiType.allowPickupable || item->isMagicField() || item->isBlocking()) { + if (!item->isPickupable()) { + return RETURNVALUE_NOTENOUGHROOM; + } + + if (!iiType.hasHeight || iiType.pickupable || iiType.isBed()) { + return RETURNVALUE_NOTENOUGHROOM; + } + } + } + } + + if (items) { + for (const Item* tileItem : *items) { + const ItemType& iiType = Item::items[tileItem->getID()]; + if (!iiType.blockSolid) { + continue; + } + + if (iiType.allowPickupable && !item->isMagicField() && !item->isBlocking()) { + continue; + } + + if (!item->isPickupable()) { + return RETURNVALUE_NOTENOUGHROOM; + } + + if (!iiType.hasHeight || iiType.pickupable || iiType.isBed()) { + return RETURNVALUE_NOTENOUGHROOM; + } + } + } + } + } + return RETURNVALUE_NOERROR; +} + +ReturnValue Tile::queryMaxCount(int32_t, const Thing&, uint32_t count, uint32_t& maxQueryCount, uint32_t) const +{ + maxQueryCount = std::max(1, count); + return RETURNVALUE_NOERROR; +} + +ReturnValue Tile::queryRemove(const Thing& thing, uint32_t count, uint32_t flags) const +{ + int32_t index = getThingIndex(&thing); + if (index == -1) { + return RETURNVALUE_NOTPOSSIBLE; + } + + const Item* item = thing.getItem(); + if (item == nullptr) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (count == 0 || (item->isStackable() && count > item->getItemCount())) { + return RETURNVALUE_NOTPOSSIBLE; + } + + if (!item->isMoveable() && !hasBitSet(FLAG_IGNORENOTMOVEABLE, flags)) { + return RETURNVALUE_NOTMOVEABLE; + } + + return RETURNVALUE_NOERROR; +} + +Tile* Tile::queryDestination(int32_t&, const Thing&, Item** destItem, uint32_t& flags) +{ + Tile* destTile = nullptr; + *destItem = nullptr; + + if (hasFlag(TILESTATE_FLOORCHANGE_DOWN)) { + uint16_t dx = tilePos.x; + uint16_t dy = tilePos.y; + uint8_t dz = tilePos.z + 1; + + Tile* southDownTile = g_game.map.getTile(dx, dy - 1, dz); + if (southDownTile && southDownTile->hasFlag(TILESTATE_FLOORCHANGE_SOUTH_ALT)) { + dy -= 2; + destTile = g_game.map.getTile(dx, dy, dz); + } else { + Tile* eastDownTile = g_game.map.getTile(dx - 1, dy, dz); + if (eastDownTile && eastDownTile->hasFlag(TILESTATE_FLOORCHANGE_EAST_ALT)) { + dx -= 2; + destTile = g_game.map.getTile(dx, dy, dz); + } else { + Tile* downTile = g_game.map.getTile(dx, dy, dz); + if (downTile) { + if (downTile->hasFlag(TILESTATE_FLOORCHANGE_NORTH)) { + ++dy; + } + + if (downTile->hasFlag(TILESTATE_FLOORCHANGE_SOUTH)) { + --dy; + } + + if (downTile->hasFlag(TILESTATE_FLOORCHANGE_SOUTH_ALT)) { + dy -= 2; + } + + if (downTile->hasFlag(TILESTATE_FLOORCHANGE_EAST)) { + --dx; + } + + if (downTile->hasFlag(TILESTATE_FLOORCHANGE_EAST_ALT)) { + dx -= 2; + } + + if (downTile->hasFlag(TILESTATE_FLOORCHANGE_WEST)) { + ++dx; + } + + destTile = g_game.map.getTile(dx, dy, dz); + } + } + } + } else if (hasFlag(TILESTATE_FLOORCHANGE)) { + uint16_t dx = tilePos.x; + uint16_t dy = tilePos.y; + uint8_t dz = tilePos.z - 1; + + if (hasFlag(TILESTATE_FLOORCHANGE_NORTH)) { + --dy; + } + + if (hasFlag(TILESTATE_FLOORCHANGE_SOUTH)) { + ++dy; + } + + if (hasFlag(TILESTATE_FLOORCHANGE_EAST)) { + ++dx; + } + + if (hasFlag(TILESTATE_FLOORCHANGE_WEST)) { + --dx; + } + + if (hasFlag(TILESTATE_FLOORCHANGE_SOUTH_ALT)) { + dy += 2; + } + + if (hasFlag(TILESTATE_FLOORCHANGE_EAST_ALT)) { + dx += 2; + } + + destTile = g_game.map.getTile(dx, dy, dz); + } + + if (destTile == nullptr) { + destTile = this; + } else { + flags |= FLAG_NOLIMIT; //Will ignore that there is blocking items/creatures + } + + if (destTile) { + Thing* destThing = destTile->getTopDownItem(); + if (destThing) { + *destItem = destThing->getItem(); + } + } + return destTile; +} + +void Tile::addThing(Thing* thing) +{ + addThing(0, thing); +} + +void Tile::addThing(int32_t, Thing* thing) +{ + Creature* creature = thing->getCreature(); + if (creature) { + g_game.map.clearSpectatorCache(); + if (creature->getPlayer()) { + g_game.map.clearPlayersSpectatorCache(); + } + + creature->setParent(this); + CreatureVector* creatures = makeCreatures(); + creatures->insert(creatures->begin(), creature); + } else { + Item* item = thing->getItem(); + if (item == nullptr) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + TileItemVector* items = getItemList(); + if (items && items->size() >= 0xFFFF) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + item->setParent(this); + + const ItemType& itemType = Item::items[item->getID()]; + if (itemType.isGroundTile()) { + if (ground == nullptr) { + ground = item; + onAddTileItem(item); + } else { + const ItemType& oldType = Item::items[ground->getID()]; + + Item* oldGround = ground; + ground->setParent(nullptr); + g_game.ReleaseItem(ground); + ground = item; + resetTileFlags(oldGround); + setTileFlags(item); + onUpdateTileItem(oldGround, oldType, item, itemType); + postRemoveNotification(oldGround, nullptr, 0); + } + } else if (itemType.alwaysOnTop) { + if (itemType.isSplash() && items) { + //remove old splash if exists + for (ItemVector::const_iterator it = items->getBeginTopItem(), end = items->getEndTopItem(); it != end; ++it) { + Item* oldSplash = *it; + if (!Item::items[oldSplash->getID()].isSplash()) { + continue; + } + + removeThing(oldSplash, 1); + oldSplash->setParent(nullptr); + g_game.ReleaseItem(oldSplash); + postRemoveNotification(oldSplash, nullptr, 0); + break; + } + } + + bool isInserted = false; + + if (items) { + for (auto it = items->getBeginTopItem(), end = items->getEndTopItem(); it != end; ++it) { + //Note: this is different from internalAddThing + if (itemType.alwaysOnTopOrder <= Item::items[(*it)->getID()].alwaysOnTopOrder) { + items->insert(it, item); + isInserted = true; + break; + } + } + } else { + items = makeItemList(); + } + + if (!isInserted) { + items->push_back(item); + } + + onAddTileItem(item); + } else { + if (itemType.isMagicField()) { + //remove old field item if exists + if (items) { + for (ItemVector::const_iterator it = items->getBeginDownItem(), end = items->getEndDownItem(); it != end; ++it) { + MagicField* oldField = (*it)->getMagicField(); + if (oldField) { + if (oldField->isReplaceable()) { + removeThing(oldField, 1); + + oldField->setParent(nullptr); + g_game.ReleaseItem(oldField); + postRemoveNotification(oldField, nullptr, 0); + break; + } else { + //This magic field cannot be replaced. + item->setParent(nullptr); + g_game.ReleaseItem(item); + return; + } + } + } + } + } + + items = makeItemList(); + items->insert(items->getBeginDownItem(), item); + items->addDownItemCount(1); + onAddTileItem(item); + } + } +} + +void Tile::updateThing(Thing* thing, uint16_t itemId, uint32_t count) +{ + int32_t index = getThingIndex(thing); + if (index == -1) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + Item* item = thing->getItem(); + if (item == nullptr) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + const ItemType& oldType = Item::items[item->getID()]; + const ItemType& newType = Item::items[itemId]; + resetTileFlags(item); + item->setID(itemId); + item->setSubType(count); + setTileFlags(item); + onUpdateTileItem(item, oldType, item, newType); +} + +void Tile::replaceThing(uint32_t index, Thing* thing) +{ + int32_t pos = index; + + Item* item = thing->getItem(); + if (item == nullptr) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + Item* oldItem = nullptr; + bool isInserted = false; + + if (ground) { + if (pos == 0) { + oldItem = ground; + ground = item; + isInserted = true; + } + + --pos; + } + + TileItemVector* items = getItemList(); + if (items && !isInserted) { + int32_t topItemSize = getTopItemCount(); + if (pos < topItemSize) { + auto it = items->getBeginTopItem(); + it += pos; + + oldItem = (*it); + it = items->erase(it); + items->insert(it, item); + isInserted = true; + } + + pos -= topItemSize; + } + + CreatureVector* creatures = getCreatures(); + if (creatures) { + if (!isInserted && pos < static_cast(creatures->size())) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + pos -= static_cast(creatures->size()); + } + + if (items && !isInserted) { + int32_t downItemSize = getDownItemCount(); + if (pos < downItemSize) { + auto it = items->getBeginDownItem() + pos; + oldItem = *it; + it = items->erase(it); + items->insert(it, item); + isInserted = true; + } + } + + if (isInserted) { + item->setParent(this); + + resetTileFlags(oldItem); + setTileFlags(item); + const ItemType& oldType = Item::items[oldItem->getID()]; + const ItemType& newType = Item::items[item->getID()]; + onUpdateTileItem(oldItem, oldType, item, newType); + + oldItem->setParent(nullptr); + return /*RETURNVALUE_NOERROR*/; + } +} + +void Tile::removeThing(Thing* thing, uint32_t count) +{ + Creature* creature = thing->getCreature(); + if (creature) { + CreatureVector* creatures = getCreatures(); + if (creatures) { + auto it = std::find(creatures->begin(), creatures->end(), thing); + if (it != creatures->end()) { + g_game.map.clearSpectatorCache(); + if (creature->getPlayer()) { + g_game.map.clearPlayersSpectatorCache(); + } + + creatures->erase(it); + } + } + return; + } + + Item* item = thing->getItem(); + if (!item) { + return; + } + + int32_t index = getThingIndex(item); + if (index == -1) { + return; + } + + if (item == ground) { + ground->setParent(nullptr); + ground = nullptr; + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, getPosition(), true); + onRemoveTileItem(spectators, std::vector(spectators.size(), 0), item); + return; + } + + TileItemVector* items = getItemList(); + if (!items) { + return; + } + + const ItemType& itemType = Item::items[item->getID()]; + if (itemType.alwaysOnTop) { + auto it = std::find(items->getBeginTopItem(), items->getEndTopItem(), item); + if (it == items->getEndTopItem()) { + return; + } + + std::vector oldStackPosVector; + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, getPosition(), true); + for (Creature* spectator : spectators) { + if (Player* tmpPlayer = spectator->getPlayer()) { + oldStackPosVector.push_back(getStackposOfItem(tmpPlayer, item)); + } + } + + item->setParent(nullptr); + items->erase(it); + onRemoveTileItem(spectators, oldStackPosVector, item); + } else { + auto it = std::find(items->getBeginDownItem(), items->getEndDownItem(), item); + if (it == items->getEndDownItem()) { + return; + } + + if (itemType.stackable && count != item->getItemCount()) { + uint8_t newCount = static_cast(std::max(0, static_cast(item->getItemCount() - count))); + item->setItemCount(newCount); + onUpdateTileItem(item, itemType, item, itemType); + } else { + std::vector oldStackPosVector; + + SpectatorVec spectators; + g_game.map.getSpectators(spectators, getPosition(), true); + for (Creature* spectator : spectators) { + if (Player* tmpPlayer = spectator->getPlayer()) { + oldStackPosVector.push_back(getStackposOfItem(tmpPlayer, item)); + } + } + + item->setParent(nullptr); + items->erase(it); + items->addDownItemCount(-1); + onRemoveTileItem(spectators, oldStackPosVector, item); + } + } +} + +void Tile::removeCreature(Creature* creature) +{ + g_game.map.getQTNode(tilePos.x, tilePos.y)->removeCreature(creature); + removeThing(creature, 0); +} + +int32_t Tile::getThingIndex(const Thing* thing) const +{ + int32_t n = -1; + if (ground) { + if (ground == thing) { + return 0; + } + ++n; + } + + const TileItemVector* items = getItemList(); + if (items) { + const Item* item = thing->getItem(); + if (item && item->isAlwaysOnTop()) { + for (auto it = items->getBeginTopItem(), end = items->getEndTopItem(); it != end; ++it) { + ++n; + if (*it == item) { + return n; + } + } + } else { + n += items->getTopItemCount(); + } + } + + if (const CreatureVector* creatures = getCreatures()) { + if (thing->getCreature()) { + for (Creature* creature : *creatures) { + ++n; + if (creature == thing) { + return n; + } + } + } else { + n += creatures->size(); + } + } + + if (items) { + const Item* item = thing->getItem(); + if (item && !item->isAlwaysOnTop()) { + for (auto it = items->getBeginDownItem(), end = items->getEndDownItem(); it != end; ++it) { + ++n; + if (*it == item) { + return n; + } + } + } + } + return -1; +} + +int32_t Tile::getClientIndexOfCreature(const Player* player, const Creature* creature) const +{ + int32_t n; + if (ground) { + n = 1; + } else { + n = 0; + } + + const TileItemVector* items = getItemList(); + if (items) { + n += items->getTopItemCount(); + } + + if (const CreatureVector* creatures = getCreatures()) { + for (const Creature* c : boost::adaptors::reverse(*creatures)) { + if (c == creature) { + return n; + } else if (player->canSeeCreature(c)) { + ++n; + } + } + } + return -1; +} + +int32_t Tile::getStackposOfCreature(const Player* player, const Creature* creature) const +{ + int32_t n; + if (ground) { + n = 1; + } else { + n = 0; + } + + const TileItemVector* items = getItemList(); + if (items) { + n += items->getTopItemCount(); + if (n >= 10) { + return -1; + } + } + + if (const CreatureVector* creatures = getCreatures()) { + for (const Creature* c : boost::adaptors::reverse(*creatures)) { + if (c == creature) { + return n; + } else if (player->canSeeCreature(c)) { + if (++n >= 10) { + return -1; + } + } + } + } + return -1; +} + +int32_t Tile::getStackposOfItem(const Player* player, const Item* item) const +{ + int32_t n = 0; + if (ground) { + if (ground == item) { + return n; + } + ++n; + } + + const TileItemVector* items = getItemList(); + if (items) { + if (item->isAlwaysOnTop()) { + for (auto it = items->getBeginTopItem(), end = items->getEndTopItem(); it != end; ++it) { + if (*it == item) { + return n; + } else if (++n == 10) { + return -1; + } + } + } else { + n += items->getTopItemCount(); + if (n >= 10) { + return -1; + } + } + } + + if (const CreatureVector* creatures = getCreatures()) { + for (const Creature* creature : *creatures) { + if (player->canSeeCreature(creature)) { + if (++n >= 10) { + return -1; + } + } + } + } + + if (items && !item->isAlwaysOnTop()) { + for (auto it = items->getBeginDownItem(), end = items->getEndDownItem(); it != end; ++it) { + if (*it == item) { + return n; + } else if (++n >= 10) { + return -1; + } + } + } + return -1; +} + +size_t Tile::getFirstIndex() const +{ + return 0; +} + +size_t Tile::getLastIndex() const +{ + return getThingCount(); +} + +uint32_t Tile::getItemTypeCount(uint16_t itemId, int32_t subType /*= -1*/) const +{ + uint32_t count = 0; + if (ground && ground->getID() == itemId) { + count += Item::countByType(ground, subType); + } + + const TileItemVector* items = getItemList(); + if (items) { + for (const Item* item : *items) { + if (item->getID() == itemId) { + count += Item::countByType(item, subType); + } + } + } + return count; +} + +Thing* Tile::getThing(size_t index) const +{ + if (ground) { + if (index == 0) { + return ground; + } + + --index; + } + + const TileItemVector* items = getItemList(); + if (items) { + uint32_t topItemSize = items->getTopItemCount(); + if (index < topItemSize) { + return items->at(items->getDownItemCount() + index); + } + index -= topItemSize; + } + + if (const CreatureVector* creatures = getCreatures()) { + if (index < creatures->size()) { + return (*creatures)[index]; + } + index -= creatures->size(); + } + + if (items && index < items->getDownItemCount()) { + return items->at(index); + } + return nullptr; +} + +void Tile::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link /*= LINK_OWNER*/) +{ + SpectatorVec spectators; + g_game.map.getSpectators(spectators, getPosition(), true, true); + for (Creature* spectator : spectators) { + spectator->getPlayer()->postAddNotification(thing, oldParent, index, LINK_NEAR); + } + + //add a reference to this item, it may be deleted after being added (mailbox for example) + Creature* creature = thing->getCreature(); + Item* item; + if (creature) { + creature->incrementReferenceCounter(); + item = nullptr; + } else { + item = thing->getItem(); + if (item) { + item->incrementReferenceCounter(); + } + } + + if (link == LINK_OWNER) { + if (hasFlag(TILESTATE_TELEPORT)) { + Teleport* teleport = getTeleportItem(); + if (teleport) { + teleport->addThing(thing); + } + } else if (hasFlag(TILESTATE_TRASHHOLDER)) { + TrashHolder* trashholder = getTrashHolder(); + if (trashholder) { + trashholder->addThing(thing); + } + } else if (hasFlag(TILESTATE_MAILBOX)) { + Mailbox* mailbox = getMailbox(); + if (mailbox) { + mailbox->addThing(thing); + } + } + + //calling movement scripts + if (creature) { + g_moveEvents->onCreatureMove(creature, this, MOVE_EVENT_STEP_IN); + } else if (item) { + g_moveEvents->onItemMove(item, this, true); + } + } + + //release the reference to this item onces we are finished + if (creature) { + g_game.ReleaseCreature(creature); + } else if (item) { + g_game.ReleaseItem(item); + } +} + +void Tile::postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t) +{ + SpectatorVec spectators; + g_game.map.getSpectators(spectators, getPosition(), true, true); + + if (getThingCount() > 8) { + onUpdateTile(spectators); + } + + for (Creature* spectator : spectators) { + spectator->getPlayer()->postRemoveNotification(thing, newParent, index, LINK_NEAR); + } + + //calling movement scripts + Creature* creature = thing->getCreature(); + if (creature) { + g_moveEvents->onCreatureMove(creature, this, MOVE_EVENT_STEP_OUT); + } else { + Item* item = thing->getItem(); + if (item) { + g_moveEvents->onItemMove(item, this, false); + } + } +} + +void Tile::internalAddThing(Thing* thing) +{ + internalAddThing(0, thing); +} + +void Tile::internalAddThing(uint32_t, Thing* thing) +{ + thing->setParent(this); + + Creature* creature = thing->getCreature(); + if (creature) { + g_game.map.clearSpectatorCache(); + if (creature->getPlayer()) { + g_game.map.clearPlayersSpectatorCache(); + } + + CreatureVector* creatures = makeCreatures(); + creatures->insert(creatures->begin(), creature); + } else { + Item* item = thing->getItem(); + if (item == nullptr) { + return; + } + + const ItemType& itemType = Item::items[item->getID()]; + if (itemType.isGroundTile()) { + if (ground == nullptr) { + ground = item; + setTileFlags(item); + } + return; + } + + TileItemVector* items = makeItemList(); + if (items->size() >= 0xFFFF) { + return /*RETURNVALUE_NOTPOSSIBLE*/; + } + + if (itemType.alwaysOnTop) { + bool isInserted = false; + for (auto it = items->getBeginTopItem(), end = items->getEndTopItem(); it != end; ++it) { + if (Item::items[(*it)->getID()].alwaysOnTopOrder > itemType.alwaysOnTopOrder) { + items->insert(it, item); + isInserted = true; + break; + } + } + + if (!isInserted) { + items->push_back(item); + } + } else { + items->insert(items->getBeginDownItem(), item); + items->addDownItemCount(1); + } + + setTileFlags(item); + } +} + +void Tile::setTileFlags(const Item* item) +{ + if (!hasFlag(TILESTATE_FLOORCHANGE)) { + const ItemType& it = Item::items[item->getID()]; + if (it.floorChange != 0) { + setFlag(it.floorChange); + } + } + + if (item->hasProperty(CONST_PROP_IMMOVABLEBLOCKSOLID)) { + setFlag(TILESTATE_IMMOVABLEBLOCKSOLID); + } + + if (item->hasProperty(CONST_PROP_BLOCKPATH)) { + setFlag(TILESTATE_BLOCKPATH); + } + + if (item->hasProperty(CONST_PROP_NOFIELDBLOCKPATH)) { + setFlag(TILESTATE_NOFIELDBLOCKPATH); + } + + if (item->hasProperty(CONST_PROP_IMMOVABLENOFIELDBLOCKPATH)) { + setFlag(TILESTATE_IMMOVABLENOFIELDBLOCKPATH); + } + + if (item->getTeleport()) { + setFlag(TILESTATE_TELEPORT); + } + + if (item->getMagicField()) { + setFlag(TILESTATE_MAGICFIELD); + } + + if (item->getMailbox()) { + setFlag(TILESTATE_MAILBOX); + } + + if (item->getTrashHolder()) { + setFlag(TILESTATE_TRASHHOLDER); + } + + if (item->hasProperty(CONST_PROP_BLOCKSOLID)) { + setFlag(TILESTATE_BLOCKSOLID); + } + + if (item->getBed()) { + setFlag(TILESTATE_BED); + } + + const Container* container = item->getContainer(); + if (container && container->getDepotLocker()) { + setFlag(TILESTATE_DEPOT); + } + + if (item->hasProperty(CONST_PROP_SUPPORTHANGABLE)) { + setFlag(TILESTATE_SUPPORTS_HANGABLE); + } +} + +void Tile::resetTileFlags(const Item* item) +{ + const ItemType& it = Item::items[item->getID()]; + if (it.floorChange != 0) { + resetFlag(TILESTATE_FLOORCHANGE); + } + + if (item->hasProperty(CONST_PROP_BLOCKSOLID) && !hasProperty(item, CONST_PROP_BLOCKSOLID)) { + resetFlag(TILESTATE_BLOCKSOLID); + } + + if (item->hasProperty(CONST_PROP_IMMOVABLEBLOCKSOLID) && !hasProperty(item, CONST_PROP_IMMOVABLEBLOCKSOLID)) { + resetFlag(TILESTATE_IMMOVABLEBLOCKSOLID); + } + + if (item->hasProperty(CONST_PROP_BLOCKPATH) && !hasProperty(item, CONST_PROP_BLOCKPATH)) { + resetFlag(TILESTATE_BLOCKPATH); + } + + if (item->hasProperty(CONST_PROP_NOFIELDBLOCKPATH) && !hasProperty(item, CONST_PROP_NOFIELDBLOCKPATH)) { + resetFlag(TILESTATE_NOFIELDBLOCKPATH); + } + + if (item->hasProperty(CONST_PROP_IMMOVABLEBLOCKPATH) && !hasProperty(item, CONST_PROP_IMMOVABLEBLOCKPATH)) { + resetFlag(TILESTATE_IMMOVABLEBLOCKPATH); + } + + if (item->hasProperty(CONST_PROP_IMMOVABLENOFIELDBLOCKPATH) && !hasProperty(item, CONST_PROP_IMMOVABLENOFIELDBLOCKPATH)) { + resetFlag(TILESTATE_IMMOVABLENOFIELDBLOCKPATH); + } + + if (item->getTeleport()) { + resetFlag(TILESTATE_TELEPORT); + } + + if (item->getMagicField()) { + resetFlag(TILESTATE_MAGICFIELD); + } + + if (item->getMailbox()) { + resetFlag(TILESTATE_MAILBOX); + } + + if (item->getTrashHolder()) { + resetFlag(TILESTATE_TRASHHOLDER); + } + + if (item->getBed()) { + resetFlag(TILESTATE_BED); + } + + const Container* container = item->getContainer(); + if (container && container->getDepotLocker()) { + resetFlag(TILESTATE_DEPOT); + } + + if (item->hasProperty(CONST_PROP_SUPPORTHANGABLE)) { + resetFlag(TILESTATE_SUPPORTS_HANGABLE); + } +} + +bool Tile::isMoveableBlocking() const +{ + return !ground || hasFlag(TILESTATE_BLOCKSOLID); +} + +Item* Tile::getUseItem(int32_t index) const +{ + const TileItemVector* items = getItemList(); + if (!items || items->size() == 0) { + return ground; + } + + if (Thing* thing = getThing(index)) { + return thing->getItem(); + } + + return nullptr; +} diff --git a/src/tile.h b/src/tile.h new file mode 100644 index 0000000..d04ecc7 --- /dev/null +++ b/src/tile.h @@ -0,0 +1,388 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_TILE_H_96C7EE7CF8CD48E59D5D554A181F0C56 +#define FS_TILE_H_96C7EE7CF8CD48E59D5D554A181F0C56 + +#include "cylinder.h" +#include "item.h" +#include "tools.h" +#include "spectators.h" + +class Creature; +class Teleport; +class TrashHolder; +class Mailbox; +class MagicField; +class QTreeLeafNode; +class BedItem; + +using CreatureVector = std::vector; +using ItemVector = std::vector; + +enum tileflags_t : uint32_t { + TILESTATE_NONE = 0, + + TILESTATE_FLOORCHANGE_DOWN = 1 << 0, + TILESTATE_FLOORCHANGE_NORTH = 1 << 1, + TILESTATE_FLOORCHANGE_SOUTH = 1 << 2, + TILESTATE_FLOORCHANGE_EAST = 1 << 3, + TILESTATE_FLOORCHANGE_WEST = 1 << 4, + TILESTATE_FLOORCHANGE_SOUTH_ALT = 1 << 5, + TILESTATE_FLOORCHANGE_EAST_ALT = 1 << 6, + TILESTATE_PROTECTIONZONE = 1 << 7, + TILESTATE_NOPVPZONE = 1 << 8, + TILESTATE_NOLOGOUT = 1 << 9, + TILESTATE_PVPZONE = 1 << 10, + TILESTATE_TELEPORT = 1 << 11, + TILESTATE_MAGICFIELD = 1 << 12, + TILESTATE_MAILBOX = 1 << 13, + TILESTATE_TRASHHOLDER = 1 << 14, + TILESTATE_BED = 1 << 15, + TILESTATE_DEPOT = 1 << 16, + TILESTATE_BLOCKSOLID = 1 << 17, + TILESTATE_BLOCKPATH = 1 << 18, + TILESTATE_IMMOVABLEBLOCKSOLID = 1 << 19, + TILESTATE_IMMOVABLEBLOCKPATH = 1 << 20, + TILESTATE_IMMOVABLENOFIELDBLOCKPATH = 1 << 21, + TILESTATE_NOFIELDBLOCKPATH = 1 << 22, + TILESTATE_SUPPORTS_HANGABLE = 1 << 23, + + TILESTATE_FLOORCHANGE = TILESTATE_FLOORCHANGE_DOWN | TILESTATE_FLOORCHANGE_NORTH | TILESTATE_FLOORCHANGE_SOUTH | TILESTATE_FLOORCHANGE_EAST | TILESTATE_FLOORCHANGE_WEST | TILESTATE_FLOORCHANGE_SOUTH_ALT | TILESTATE_FLOORCHANGE_EAST_ALT, +}; + +enum ZoneType_t { + ZONE_PROTECTION, + ZONE_NOPVP, + ZONE_PVP, + ZONE_NOLOGOUT, + ZONE_NORMAL, +}; + +class TileItemVector : private ItemVector +{ + public: + using ItemVector::begin; + using ItemVector::end; + using ItemVector::rbegin; + using ItemVector::rend; + using ItemVector::size; + using ItemVector::clear; + using ItemVector::at; + using ItemVector::insert; + using ItemVector::erase; + using ItemVector::push_back; + using ItemVector::value_type; + using ItemVector::iterator; + using ItemVector::const_iterator; + using ItemVector::reverse_iterator; + using ItemVector::const_reverse_iterator; + + iterator getBeginDownItem() { + return begin(); + } + const_iterator getBeginDownItem() const { + return begin(); + } + iterator getEndDownItem() { + return begin() + downItemCount; + } + const_iterator getEndDownItem() const { + return begin() + downItemCount; + } + iterator getBeginTopItem() { + return getEndDownItem(); + } + const_iterator getBeginTopItem() const { + return getEndDownItem(); + } + iterator getEndTopItem() { + return end(); + } + const_iterator getEndTopItem() const { + return end(); + } + + uint32_t getTopItemCount() const { + return size() - downItemCount; + } + uint32_t getDownItemCount() const { + return downItemCount; + } + inline Item* getTopTopItem() const { + if (getTopItemCount() == 0) { + return nullptr; + } + return *(getEndTopItem() - 1); + } + inline Item* getTopDownItem() const { + if (downItemCount == 0) { + return nullptr; + } + return *getBeginDownItem(); + } + void addDownItemCount(int32_t increment) { + downItemCount += increment; + } + + private: + uint16_t downItemCount = 0; +}; + +class Tile : public Cylinder +{ + public: + static Tile& nullptr_tile; + Tile(uint16_t x, uint16_t y, uint8_t z) : tilePos(x, y, z) {} + virtual ~Tile() { + delete ground; + }; + + // non-copyable + Tile(const Tile&) = delete; + Tile& operator=(const Tile&) = delete; + + virtual TileItemVector* getItemList() = 0; + virtual const TileItemVector* getItemList() const = 0; + virtual TileItemVector* makeItemList() = 0; + + virtual CreatureVector* getCreatures() = 0; + virtual const CreatureVector* getCreatures() const = 0; + virtual CreatureVector* makeCreatures() = 0; + + int32_t getThrowRange() const override final { + return 0; + } + bool isPushable() const override final { + return false; + } + + MagicField* getFieldItem() const; + Teleport* getTeleportItem() const; + TrashHolder* getTrashHolder() const; + Mailbox* getMailbox() const; + BedItem* getBedItem() const; + + Creature* getTopCreature() const; + const Creature* getBottomCreature() const; + Creature* getTopVisibleCreature(const Creature* creature) const; + const Creature* getBottomVisibleCreature(const Creature* creature) const; + Item* getTopTopItem() const; + Item* getTopDownItem() const; + bool isMoveableBlocking() const; + Thing* getTopVisibleThing(const Creature* creature); + Item* getItemByTopOrder(int32_t topOrder); + + size_t getThingCount() const { + size_t thingCount = getCreatureCount() + getItemCount(); + if (ground) { + thingCount++; + } + return thingCount; + } + // If these return != 0 the associated vectors are guaranteed to exists + size_t getCreatureCount() const; + size_t getItemCount() const; + uint32_t getTopItemCount() const; + uint32_t getDownItemCount() const; + + bool hasProperty(ITEMPROPERTY prop) const; + bool hasProperty(const Item* exclude, ITEMPROPERTY prop) const; + + bool hasFlag(uint32_t flag) const { + return hasBitSet(flag, this->flags); + } + void setFlag(uint32_t flag) { + this->flags |= flag; + } + void resetFlag(uint32_t flag) { + this->flags &= ~flag; + } + + ZoneType_t getZone() const { + if (hasFlag(TILESTATE_PROTECTIONZONE)) { + return ZONE_PROTECTION; + } else if (hasFlag(TILESTATE_NOPVPZONE)) { + return ZONE_NOPVP; + } else if (hasFlag(TILESTATE_PVPZONE)) { + return ZONE_PVP; + } else { + return ZONE_NORMAL; + } + } + + bool hasHeight(uint32_t n) const; + + std::string getDescription(int32_t lookDistance) const override final; + + int32_t getClientIndexOfCreature(const Player* player, const Creature* creature) const; + int32_t getStackposOfCreature(const Player* player, const Creature* creature) const; + int32_t getStackposOfItem(const Player* player, const Item* item) const; + + //cylinder implementations + ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count, + uint32_t flags, Creature* actor = nullptr) const override; + ReturnValue queryMaxCount(int32_t index, const Thing& thing, uint32_t count, + uint32_t& maxQueryCount, uint32_t flags) const override final; + ReturnValue queryRemove(const Thing& thing, uint32_t count, uint32_t flags) const override final; + Tile* queryDestination(int32_t& index, const Thing& thing, Item** destItem, uint32_t& flags) override; + + void addThing(Thing* thing) override final; + void addThing(int32_t index, Thing* thing) override; + + void updateThing(Thing* thing, uint16_t itemId, uint32_t count) override final; + void replaceThing(uint32_t index, Thing* thing) override final; + + void removeThing(Thing* thing, uint32_t count) override final; + + void removeCreature(Creature* creature); + + int32_t getThingIndex(const Thing* thing) const override final; + size_t getFirstIndex() const override final; + size_t getLastIndex() const override final; + uint32_t getItemTypeCount(uint16_t itemId, int32_t subType = -1) const override final; + Thing* getThing(size_t index) const override final; + + void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) override final; + void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) override final; + + void internalAddThing(Thing* thing) override final; + void internalAddThing(uint32_t index, Thing* thing) override; + + const Position& getPosition() const override final { + return tilePos; + } + + bool isRemoved() const override final { + return false; + } + + Item* getUseItem(int32_t index) const; + + Item* getGround() const { + return ground; + } + void setGround(Item* item) { + ground = item; + } + + private: + void onAddTileItem(Item* item); + void onUpdateTileItem(Item* oldItem, const ItemType& oldType, Item* newItem, const ItemType& newType); + void onRemoveTileItem(const SpectatorVec& spectators, const std::vector& oldStackPosVector, Item* item); + void onUpdateTile(const SpectatorVec& spectators); + + void setTileFlags(const Item* item); + void resetTileFlags(const Item* item); + + Item* ground = nullptr; + Position tilePos; + uint32_t flags = 0; +}; + +// Used for walkable tiles, where there is high likeliness of +// items being added/removed +class DynamicTile : public Tile +{ + // By allocating the vectors in-house, we avoid some memory fragmentation + TileItemVector items; + CreatureVector creatures; + + public: + DynamicTile(uint16_t x, uint16_t y, uint8_t z) : Tile(x, y, z) {} + ~DynamicTile() { + for (Item* item : items) { + item->decrementReferenceCounter(); + } + } + + // non-copyable + DynamicTile(const DynamicTile&) = delete; + DynamicTile& operator=(const DynamicTile&) = delete; + + TileItemVector* getItemList() override { + return &items; + } + const TileItemVector* getItemList() const override { + return &items; + } + TileItemVector* makeItemList() override { + return &items; + } + + CreatureVector* getCreatures() override { + return &creatures; + } + const CreatureVector* getCreatures() const override { + return &creatures; + } + CreatureVector* makeCreatures() override { + return &creatures; + } +}; + +// For blocking tiles, where we very rarely actually have items +class StaticTile final : public Tile +{ + // We very rarely even need the vectors, so don't keep them in memory + std::unique_ptr items; + std::unique_ptr creatures; + + public: + StaticTile(uint16_t x, uint16_t y, uint8_t z) : Tile(x, y, z) {} + ~StaticTile() { + if (items) { + for (Item* item : *items) { + item->decrementReferenceCounter(); + } + } + } + + // non-copyable + StaticTile(const StaticTile&) = delete; + StaticTile& operator=(const StaticTile&) = delete; + + TileItemVector* getItemList() override { + return items.get(); + } + const TileItemVector* getItemList() const override { + return items.get(); + } + TileItemVector* makeItemList() override { + if (!items) { + items.reset(new TileItemVector); + } + return items.get(); + } + + CreatureVector* getCreatures() override { + return creatures.get(); + } + const CreatureVector* getCreatures() const override { + return creatures.get(); + } + CreatureVector* makeCreatures() override { + if (!creatures) { + creatures.reset(new CreatureVector); + } + return creatures.get(); + } +}; + +#endif diff --git a/src/tools.cpp b/src/tools.cpp new file mode 100644 index 0000000..a4e123b --- /dev/null +++ b/src/tools.cpp @@ -0,0 +1,1249 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "tools.h" +#include "configmanager.h" + +extern ConfigManager g_config; + +void printXMLError(const std::string& where, const std::string& fileName, const pugi::xml_parse_result& result) +{ + std::cout << '[' << where << "] Failed to load " << fileName << ": " << result.description() << std::endl; + + FILE* file = fopen(fileName.c_str(), "rb"); + if (!file) { + return; + } + + char buffer[32768]; + uint32_t currentLine = 1; + std::string line; + + auto offset = static_cast(result.offset); + size_t lineOffsetPosition = 0; + size_t index = 0; + size_t bytes; + do { + bytes = fread(buffer, 1, 32768, file); + for (size_t i = 0; i < bytes; ++i) { + char ch = buffer[i]; + if (ch == '\n') { + if ((index + i) >= offset) { + lineOffsetPosition = line.length() - ((index + i) - offset); + bytes = 0; + break; + } + ++currentLine; + line.clear(); + } else { + line.push_back(ch); + } + } + index += bytes; + } while (bytes == 32768); + fclose(file); + + std::cout << "Line " << currentLine << ':' << std::endl; + std::cout << line << std::endl; + for (size_t i = 0; i < lineOffsetPosition; i++) { + if (line[i] == '\t') { + std::cout << '\t'; + } else { + std::cout << ' '; + } + } + std::cout << '^' << std::endl; +} + +static uint32_t circularShift(int bits, uint32_t value) +{ + return (value << bits) | (value >> (32 - bits)); +} + +static void processSHA1MessageBlock(const uint8_t* messageBlock, uint32_t* H) +{ + uint32_t W[80]; + for (int i = 0; i < 16; ++i) { + const size_t offset = i << 2; + W[i] = messageBlock[offset] << 24 | messageBlock[offset + 1] << 16 | messageBlock[offset + 2] << 8 | messageBlock[offset + 3]; + } + + for (int i = 16; i < 80; ++i) { + W[i] = circularShift(1, W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]); + } + + uint32_t A = H[0], B = H[1], C = H[2], D = H[3], E = H[4]; + + for (int i = 0; i < 20; ++i) { + const uint32_t tmp = circularShift(5, A) + ((B & C) | ((~B) & D)) + E + W[i] + 0x5A827999; + E = D; D = C; C = circularShift(30, B); B = A; A = tmp; + } + + for (int i = 20; i < 40; ++i) { + const uint32_t tmp = circularShift(5, A) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1; + E = D; D = C; C = circularShift(30, B); B = A; A = tmp; + } + + for (int i = 40; i < 60; ++i) { + const uint32_t tmp = circularShift(5, A) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC; + E = D; D = C; C = circularShift(30, B); B = A; A = tmp; + } + + for (int i = 60; i < 80; ++i) { + const uint32_t tmp = circularShift(5, A) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6; + E = D; D = C; C = circularShift(30, B); B = A; A = tmp; + } + + H[0] += A; + H[1] += B; + H[2] += C; + H[3] += D; + H[4] += E; +} + +std::string transformToSHA1(const std::string& input) +{ + uint32_t H[] = { + 0x67452301, + 0xEFCDAB89, + 0x98BADCFE, + 0x10325476, + 0xC3D2E1F0 + }; + + uint8_t messageBlock[64]; + size_t index = 0; + + uint32_t length_low = 0; + uint32_t length_high = 0; + for (char ch : input) { + messageBlock[index++] = ch; + + length_low += 8; + if (length_low == 0) { + length_high++; + } + + if (index == 64) { + processSHA1MessageBlock(messageBlock, H); + index = 0; + } + } + + messageBlock[index++] = 0x80; + + if (index > 56) { + while (index < 64) { + messageBlock[index++] = 0; + } + + processSHA1MessageBlock(messageBlock, H); + index = 0; + } + + while (index < 56) { + messageBlock[index++] = 0; + } + + messageBlock[56] = length_high >> 24; + messageBlock[57] = length_high >> 16; + messageBlock[58] = length_high >> 8; + messageBlock[59] = length_high; + + messageBlock[60] = length_low >> 24; + messageBlock[61] = length_low >> 16; + messageBlock[62] = length_low >> 8; + messageBlock[63] = length_low; + + processSHA1MessageBlock(messageBlock, H); + + char hexstring[41]; + static const char hexDigits[] = {"0123456789abcdef"}; + for (int hashByte = 20; --hashByte >= 0;) { + const uint8_t byte = H[hashByte >> 2] >> (((3 - hashByte) & 3) << 3); + index = hashByte << 1; + hexstring[index] = hexDigits[byte >> 4]; + hexstring[index + 1] = hexDigits[byte & 15]; + } + return std::string(hexstring, 40); +} + +std::string generateToken(const std::string& key, uint32_t ticks) +{ + // generate message from ticks + std::string message(8, 0); + for (uint8_t i = 8; --i; ticks >>= 8) { + message[i] = static_cast(ticks & 0xFF); + } + + // hmac key pad generation + std::string iKeyPad(64, 0x36), oKeyPad(64, 0x5C); + for (uint8_t i = 0; i < key.length(); ++i) { + iKeyPad[i] ^= key[i]; + oKeyPad[i] ^= key[i]; + } + + oKeyPad.reserve(84); + + // hmac concat inner pad with message + iKeyPad.append(message); + + // hmac first pass + message.assign(transformToSHA1(iKeyPad)); + + // hmac concat outer pad with message, conversion from hex to int needed + for (uint8_t i = 0; i < message.length(); i += 2) { + oKeyPad.push_back(static_cast(std::strtoul(message.substr(i, 2).c_str(), nullptr, 16))); + } + + // hmac second pass + message.assign(transformToSHA1(oKeyPad)); + + // calculate hmac offset + uint32_t offset = static_cast(std::strtoul(message.substr(39, 1).c_str(), nullptr, 16) & 0xF); + + // get truncated hash + uint32_t truncHash = static_cast(std::strtoul(message.substr(2 * offset, 8).c_str(), nullptr, 16)) & 0x7FFFFFFF; + message.assign(std::to_string(truncHash)); + + // return only last AUTHENTICATOR_DIGITS (default 6) digits, also asserts exactly 6 digits + uint32_t hashLen = message.length(); + message.assign(message.substr(hashLen - std::min(hashLen, AUTHENTICATOR_DIGITS))); + message.insert(0, AUTHENTICATOR_DIGITS - std::min(hashLen, AUTHENTICATOR_DIGITS), '0'); + return message; +} + +void replaceString(std::string& str, const std::string& sought, const std::string& replacement) +{ + size_t pos = 0; + size_t start = 0; + size_t soughtLen = sought.length(); + size_t replaceLen = replacement.length(); + + while ((pos = str.find(sought, start)) != std::string::npos) { + str = str.substr(0, pos) + replacement + str.substr(pos + soughtLen); + start = pos + replaceLen; + } +} + +void trim_right(std::string& source, char t) +{ + source.erase(source.find_last_not_of(t) + 1); +} + +void trim_left(std::string& source, char t) +{ + source.erase(0, source.find_first_not_of(t)); +} + +void toLowerCaseString(std::string& source) +{ + std::transform(source.begin(), source.end(), source.begin(), tolower); +} + +std::string asLowerCaseString(std::string source) +{ + toLowerCaseString(source); + return source; +} + +std::string asUpperCaseString(std::string source) +{ + std::transform(source.begin(), source.end(), source.begin(), toupper); + return source; +} + +StringVector explodeString(const std::string& inString, const std::string& separator, int32_t limit/* = -1*/) +{ + StringVector returnVector; + std::string::size_type start = 0, end = 0; + + while (--limit != -1 && (end = inString.find(separator, start)) != std::string::npos) { + returnVector.push_back(inString.substr(start, end - start)); + start = end + separator.size(); + } + + returnVector.push_back(inString.substr(start)); + return returnVector; +} + +IntegerVector vectorAtoi(const StringVector& stringVector) +{ + IntegerVector returnVector; + for (const auto& string : stringVector) { + returnVector.push_back(std::stoi(string)); + } + return returnVector; +} + +std::mt19937& getRandomGenerator() +{ + static std::random_device rd; + static std::mt19937 generator(rd()); + return generator; +} + +int32_t uniform_random(int32_t minNumber, int32_t maxNumber) +{ + static std::uniform_int_distribution uniformRand; + if (minNumber == maxNumber) { + return minNumber; + } else if (minNumber > maxNumber) { + std::swap(minNumber, maxNumber); + } + return uniformRand(getRandomGenerator(), std::uniform_int_distribution::param_type(minNumber, maxNumber)); +} + +int32_t normal_random(int32_t minNumber, int32_t maxNumber) +{ + static std::normal_distribution normalRand(0.5f, 0.25f); + if (minNumber == maxNumber) { + return minNumber; + } else if (minNumber > maxNumber) { + std::swap(minNumber, maxNumber); + } + + int32_t increment; + const int32_t diff = maxNumber - minNumber; + const float v = normalRand(getRandomGenerator()); + if (v < 0.0) { + increment = diff / 2; + } else if (v > 1.0) { + increment = (diff + 1) / 2; + } else { + increment = round(v * diff); + } + return minNumber + increment; +} + +bool boolean_random(double probability/* = 0.5*/) +{ + static std::bernoulli_distribution booleanRand; + return booleanRand(getRandomGenerator(), std::bernoulli_distribution::param_type(probability)); +} + +void trimString(std::string& str) +{ + str.erase(str.find_last_not_of(' ') + 1); + str.erase(0, str.find_first_not_of(' ')); +} + +std::string convertIPToString(uint32_t ip) +{ + char buffer[17]; + + int res = sprintf(buffer, "%u.%u.%u.%u", ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24)); + if (res < 0) { + return {}; + } + + return buffer; +} + +std::string formatDate(time_t time) +{ + const tm* tms = localtime(&time); + if (!tms) { + return {}; + } + + char buffer[20]; + int res = sprintf(buffer, "%02d/%02d/%04d %02d:%02d:%02d", tms->tm_mday, tms->tm_mon + 1, tms->tm_year + 1900, tms->tm_hour, tms->tm_min, tms->tm_sec); + if (res < 0) { + return {}; + } + return {buffer, 19}; +} + +std::string formatDateShort(time_t time) +{ + const tm* tms = localtime(&time); + if (!tms) { + return {}; + } + + char buffer[12]; + size_t res = strftime(buffer, 12, "%d %b %Y", tms); + if (res == 0) { + return {}; + } + return {buffer, 11}; +} + +Direction getDirection(const std::string& string) +{ + Direction direction = DIRECTION_NORTH; + + if (string == "north" || string == "n" || string == "0") { + direction = DIRECTION_NORTH; + } else if (string == "east" || string == "e" || string == "1") { + direction = DIRECTION_EAST; + } else if (string == "south" || string == "s" || string == "2") { + direction = DIRECTION_SOUTH; + } else if (string == "west" || string == "w" || string == "3") { + direction = DIRECTION_WEST; + } else if (string == "southwest" || string == "south west" || string == "south-west" || string == "sw" || string == "4") { + direction = DIRECTION_SOUTHWEST; + } else if (string == "southeast" || string == "south east" || string == "south-east" || string == "se" || string == "5") { + direction = DIRECTION_SOUTHEAST; + } else if (string == "northwest" || string == "north west" || string == "north-west" || string == "nw" || string == "6") { + direction = DIRECTION_NORTHWEST; + } else if (string == "northeast" || string == "north east" || string == "north-east" || string == "ne" || string == "7") { + direction = DIRECTION_NORTHEAST; + } + + return direction; +} + +Position getNextPosition(Direction direction, Position pos) +{ + switch (direction) { + case DIRECTION_NORTH: + pos.y--; + break; + + case DIRECTION_SOUTH: + pos.y++; + break; + + case DIRECTION_WEST: + pos.x--; + break; + + case DIRECTION_EAST: + pos.x++; + break; + + case DIRECTION_SOUTHWEST: + pos.x--; + pos.y++; + break; + + case DIRECTION_NORTHWEST: + pos.x--; + pos.y--; + break; + + case DIRECTION_NORTHEAST: + pos.x++; + pos.y--; + break; + + case DIRECTION_SOUTHEAST: + pos.x++; + pos.y++; + break; + + default: + break; + } + + return pos; +} + +Direction getDirectionTo(const Position& from, const Position& to) +{ + Direction dir; + + int32_t x_offset = Position::getOffsetX(from, to); + if (x_offset < 0) { + dir = DIRECTION_EAST; + x_offset = std::abs(x_offset); + } else { + dir = DIRECTION_WEST; + } + + int32_t y_offset = Position::getOffsetY(from, to); + if (y_offset >= 0) { + if (y_offset > x_offset) { + dir = DIRECTION_NORTH; + } else if (y_offset == x_offset) { + if (dir == DIRECTION_EAST) { + dir = DIRECTION_NORTHEAST; + } else { + dir = DIRECTION_NORTHWEST; + } + } + } else { + y_offset = std::abs(y_offset); + if (y_offset > x_offset) { + dir = DIRECTION_SOUTH; + } else if (y_offset == x_offset) { + if (dir == DIRECTION_EAST) { + dir = DIRECTION_SOUTHEAST; + } else { + dir = DIRECTION_SOUTHWEST; + } + } + } + return dir; +} + +using MagicEffectNames = std::unordered_map; +using ShootTypeNames = std::unordered_map; +using CombatTypeNames = std::unordered_map>; +using AmmoTypeNames = std::unordered_map; +using WeaponActionNames = std::unordered_map; +using SkullNames = std::unordered_map; + +MagicEffectNames magicEffectNames = { + {"redspark", CONST_ME_DRAWBLOOD}, + {"bluebubble", CONST_ME_LOSEENERGY}, + {"poff", CONST_ME_POFF}, + {"yellowspark", CONST_ME_BLOCKHIT}, + {"explosionarea", CONST_ME_EXPLOSIONAREA}, + {"explosion", CONST_ME_EXPLOSIONHIT}, + {"firearea", CONST_ME_FIREAREA}, + {"yellowbubble", CONST_ME_YELLOW_RINGS}, + {"greenbubble", CONST_ME_GREEN_RINGS}, + {"blackspark", CONST_ME_HITAREA}, + {"teleport", CONST_ME_TELEPORT}, + {"energy", CONST_ME_ENERGYHIT}, + {"blueshimmer", CONST_ME_MAGIC_BLUE}, + {"redshimmer", CONST_ME_MAGIC_RED}, + {"greenshimmer", CONST_ME_MAGIC_GREEN}, + {"fire", CONST_ME_HITBYFIRE}, + {"greenspark", CONST_ME_HITBYPOISON}, + {"mortarea", CONST_ME_MORTAREA}, + {"greennote", CONST_ME_SOUND_GREEN}, + {"rednote", CONST_ME_SOUND_RED}, + {"poison", CONST_ME_POISONAREA}, + {"yellownote", CONST_ME_SOUND_YELLOW}, + {"purplenote", CONST_ME_SOUND_PURPLE}, + {"bluenote", CONST_ME_SOUND_BLUE}, + {"whitenote", CONST_ME_SOUND_WHITE}, + {"bubbles", CONST_ME_BUBBLES}, + {"dice", CONST_ME_CRAPS}, + {"giftwraps", CONST_ME_GIFT_WRAPS}, + {"yellowfirework", CONST_ME_FIREWORK_YELLOW}, + {"redfirework", CONST_ME_FIREWORK_RED}, + {"bluefirework", CONST_ME_FIREWORK_BLUE}, + {"stun", CONST_ME_STUN}, + {"sleep", CONST_ME_SLEEP}, + {"watercreature", CONST_ME_WATERCREATURE}, + {"groundshaker", CONST_ME_GROUNDSHAKER}, + {"hearts", CONST_ME_HEARTS}, + {"fireattack", CONST_ME_FIREATTACK}, + {"energyarea", CONST_ME_ENERGYAREA}, + {"smallclouds", CONST_ME_SMALLCLOUDS}, + {"holydamage", CONST_ME_HOLYDAMAGE}, + {"bigclouds", CONST_ME_BIGCLOUDS}, + {"icearea", CONST_ME_ICEAREA}, + {"icetornado", CONST_ME_ICETORNADO}, + {"iceattack", CONST_ME_ICEATTACK}, + {"stones", CONST_ME_STONES}, + {"smallplants", CONST_ME_SMALLPLANTS}, + {"carniphila", CONST_ME_CARNIPHILA}, + {"purpleenergy", CONST_ME_PURPLEENERGY}, + {"yellowenergy", CONST_ME_YELLOWENERGY}, + {"holyarea", CONST_ME_HOLYAREA}, + {"bigplants", CONST_ME_BIGPLANTS}, + {"cake", CONST_ME_CAKE}, + {"giantice", CONST_ME_GIANTICE}, + {"watersplash", CONST_ME_WATERSPLASH}, + {"plantattack", CONST_ME_PLANTATTACK}, + {"tutorialarrow", CONST_ME_TUTORIALARROW}, + {"tutorialsquare", CONST_ME_TUTORIALSQUARE}, + {"mirrorhorizontal", CONST_ME_MIRRORHORIZONTAL}, + {"mirrorvertical", CONST_ME_MIRRORVERTICAL}, + {"skullhorizontal", CONST_ME_SKULLHORIZONTAL}, + {"skullvertical", CONST_ME_SKULLVERTICAL}, + {"assassin", CONST_ME_ASSASSIN}, + {"stepshorizontal", CONST_ME_STEPSHORIZONTAL}, + {"bloodysteps", CONST_ME_BLOODYSTEPS}, + {"stepsvertical", CONST_ME_STEPSVERTICAL}, + {"yalaharighost", CONST_ME_YALAHARIGHOST}, + {"bats", CONST_ME_BATS}, + {"smoke", CONST_ME_SMOKE}, + {"insects", CONST_ME_INSECTS}, + {"dragonhead", CONST_ME_DRAGONHEAD}, + {"orcshaman", CONST_ME_ORCSHAMAN}, + {"orcshamanfire", CONST_ME_ORCSHAMAN_FIRE}, + {"thunder", CONST_ME_THUNDER}, + {"ferumbras", CONST_ME_FERUMBRAS}, + {"confettihorizontal", CONST_ME_CONFETTI_HORIZONTAL}, + {"confettivertical", CONST_ME_CONFETTI_VERTICAL}, + {"blacksmoke", CONST_ME_BLACKSMOKE}, + {"redsmoke", CONST_ME_REDSMOKE}, + {"yellowsmoke", CONST_ME_YELLOWSMOKE}, + {"greensmoke", CONST_ME_GREENSMOKE}, + {"purplesmoke", CONST_ME_PURPLESMOKE}, +}; + +ShootTypeNames shootTypeNames = { + {"spear", CONST_ANI_SPEAR}, + {"bolt", CONST_ANI_BOLT}, + {"arrow", CONST_ANI_ARROW}, + {"fire", CONST_ANI_FIRE}, + {"energy", CONST_ANI_ENERGY}, + {"poisonarrow", CONST_ANI_POISONARROW}, + {"burstarrow", CONST_ANI_BURSTARROW}, + {"throwingstar", CONST_ANI_THROWINGSTAR}, + {"throwingknife", CONST_ANI_THROWINGKNIFE}, + {"smallstone", CONST_ANI_SMALLSTONE}, + {"death", CONST_ANI_DEATH}, + {"largerock", CONST_ANI_LARGEROCK}, + {"snowball", CONST_ANI_SNOWBALL}, + {"powerbolt", CONST_ANI_POWERBOLT}, + {"poison", CONST_ANI_POISON}, + {"infernalbolt", CONST_ANI_INFERNALBOLT}, + {"huntingspear", CONST_ANI_HUNTINGSPEAR}, + {"enchantedspear", CONST_ANI_ENCHANTEDSPEAR}, + {"redstar", CONST_ANI_REDSTAR}, + {"greenstar", CONST_ANI_GREENSTAR}, + {"royalspear", CONST_ANI_ROYALSPEAR}, + {"sniperarrow", CONST_ANI_SNIPERARROW}, + {"onyxarrow", CONST_ANI_ONYXARROW}, + {"piercingbolt", CONST_ANI_PIERCINGBOLT}, + {"whirlwindsword", CONST_ANI_WHIRLWINDSWORD}, + {"whirlwindaxe", CONST_ANI_WHIRLWINDAXE}, + {"whirlwindclub", CONST_ANI_WHIRLWINDCLUB}, + {"etherealspear", CONST_ANI_ETHEREALSPEAR}, + {"ice", CONST_ANI_ICE}, + {"earth", CONST_ANI_EARTH}, + {"holy", CONST_ANI_HOLY}, + {"suddendeath", CONST_ANI_SUDDENDEATH}, + {"flasharrow", CONST_ANI_FLASHARROW}, + {"flammingarrow", CONST_ANI_FLAMMINGARROW}, + {"shiverarrow", CONST_ANI_SHIVERARROW}, + {"energyball", CONST_ANI_ENERGYBALL}, + {"smallice", CONST_ANI_SMALLICE}, + {"smallholy", CONST_ANI_SMALLHOLY}, + {"smallearth", CONST_ANI_SMALLEARTH}, + {"eartharrow", CONST_ANI_EARTHARROW}, + {"explosion", CONST_ANI_EXPLOSION}, + {"cake", CONST_ANI_CAKE}, + {"tarsalarrow", CONST_ANI_TARSALARROW}, + {"vortexbolt", CONST_ANI_VORTEXBOLT}, + {"prismaticbolt", CONST_ANI_PRISMATICBOLT}, + {"crystallinearrow", CONST_ANI_CRYSTALLINEARROW}, + {"drillbolt", CONST_ANI_DRILLBOLT}, + {"envenomedarrow", CONST_ANI_ENVENOMEDARROW}, + {"gloothspear", CONST_ANI_GLOOTHSPEAR}, + {"simplearrow", CONST_ANI_SIMPLEARROW}, +}; + +CombatTypeNames combatTypeNames = { + {COMBAT_PHYSICALDAMAGE, "physical"}, + {COMBAT_ENERGYDAMAGE, "energy"}, + {COMBAT_EARTHDAMAGE, "earth"}, + {COMBAT_FIREDAMAGE, "fire"}, + {COMBAT_UNDEFINEDDAMAGE, "undefined"}, + {COMBAT_LIFEDRAIN, "lifedrain"}, + {COMBAT_MANADRAIN, "manadrain"}, + {COMBAT_HEALING, "healing"}, + {COMBAT_DROWNDAMAGE, "drown"}, + {COMBAT_ICEDAMAGE, "ice"}, + {COMBAT_HOLYDAMAGE, "holy"}, + {COMBAT_DEATHDAMAGE, "death"}, +}; + +AmmoTypeNames ammoTypeNames = { + {"spear", AMMO_SPEAR}, + {"bolt", AMMO_BOLT}, + {"arrow", AMMO_ARROW}, + {"poisonarrow", AMMO_ARROW}, + {"burstarrow", AMMO_ARROW}, + {"throwingstar", AMMO_THROWINGSTAR}, + {"throwingknife", AMMO_THROWINGKNIFE}, + {"smallstone", AMMO_STONE}, + {"largerock", AMMO_STONE}, + {"snowball", AMMO_SNOWBALL}, + {"powerbolt", AMMO_BOLT}, + {"infernalbolt", AMMO_BOLT}, + {"huntingspear", AMMO_SPEAR}, + {"enchantedspear", AMMO_SPEAR}, + {"royalspear", AMMO_SPEAR}, + {"sniperarrow", AMMO_ARROW}, + {"onyxarrow", AMMO_ARROW}, + {"piercingbolt", AMMO_BOLT}, + {"etherealspear", AMMO_SPEAR}, + {"flasharrow", AMMO_ARROW}, + {"flammingarrow", AMMO_ARROW}, + {"shiverarrow", AMMO_ARROW}, + {"eartharrow", AMMO_ARROW}, +}; + +WeaponActionNames weaponActionNames = { + {"move", WEAPONACTION_MOVE}, + {"removecharge", WEAPONACTION_REMOVECHARGE}, + {"removecount", WEAPONACTION_REMOVECOUNT}, +}; + +SkullNames skullNames = { + {"none", SKULL_NONE}, + {"yellow", SKULL_YELLOW}, + {"green", SKULL_GREEN}, + {"white", SKULL_WHITE}, + {"red", SKULL_RED}, + {"black", SKULL_BLACK}, + {"orange", SKULL_ORANGE}, +}; + +MagicEffectClasses getMagicEffect(const std::string& strValue) +{ + auto magicEffect = magicEffectNames.find(strValue); + if (magicEffect != magicEffectNames.end()) { + return magicEffect->second; + } + return CONST_ME_NONE; +} + +ShootType_t getShootType(const std::string& strValue) +{ + auto shootType = shootTypeNames.find(strValue); + if (shootType != shootTypeNames.end()) { + return shootType->second; + } + return CONST_ANI_NONE; +} + +std::string getCombatName(CombatType_t combatType) +{ + auto combatName = combatTypeNames.find(combatType); + if (combatName != combatTypeNames.end()) { + return combatName->second; + } + return "unknown"; +} + +Ammo_t getAmmoType(const std::string& strValue) +{ + auto ammoType = ammoTypeNames.find(strValue); + if (ammoType != ammoTypeNames.end()) { + return ammoType->second; + } + return AMMO_NONE; +} + +WeaponAction_t getWeaponAction(const std::string& strValue) +{ + auto weaponAction = weaponActionNames.find(strValue); + if (weaponAction != weaponActionNames.end()) { + return weaponAction->second; + } + return WEAPONACTION_NONE; +} + +Skulls_t getSkullType(const std::string& strValue) +{ + auto skullType = skullNames.find(strValue); + if (skullType != skullNames.end()) { + return skullType->second; + } + return SKULL_NONE; +} + +std::string getSpecialSkillName(uint8_t skillid) +{ + switch (skillid) { + case SPECIALSKILL_CRITICALHITCHANCE: + return "critical hit chance"; + + case SPECIALSKILL_CRITICALHITAMOUNT: + return "critical extra damage"; + + case SPECIALSKILL_LIFELEECHCHANCE: + return "hitpoints leech chance"; + + case SPECIALSKILL_LIFELEECHAMOUNT: + return "hitpoints leech amount"; + + case SPECIALSKILL_MANALEECHCHANCE: + return "manapoints leech chance"; + + case SPECIALSKILL_MANALEECHAMOUNT: + return "mana points leech amount"; + + default: + return "unknown"; + } +} + +std::string getSkillName(uint8_t skillid) +{ + switch (skillid) { + case SKILL_FIST: + return "fist fighting"; + + case SKILL_CLUB: + return "club fighting"; + + case SKILL_SWORD: + return "sword fighting"; + + case SKILL_AXE: + return "axe fighting"; + + case SKILL_DISTANCE: + return "distance fighting"; + + case SKILL_SHIELD: + return "shielding"; + + case SKILL_FISHING: + return "fishing"; + + case SKILL_MAGLEVEL: + return "magic level"; + + case SKILL_LEVEL: + return "level"; + + default: + return "unknown"; + } +} + +uint32_t adlerChecksum(const uint8_t* data, size_t length) +{ + if (length > NETWORKMESSAGE_MAXSIZE) { + return 0; + } + + const uint16_t adler = 65521; + + uint32_t a = 1, b = 0; + + while (length > 0) { + size_t tmp = length > 5552 ? 5552 : length; + length -= tmp; + + do { + a += *data++; + b += a; + } while (--tmp); + + a %= adler; + b %= adler; + } + + return (b << 16) | a; +} + +std::string ucfirst(std::string str) +{ + for (char& i : str) { + if (i != ' ') { + i = toupper(i); + break; + } + } + return str; +} + +std::string ucwords(std::string str) +{ + size_t strLength = str.length(); + if (strLength == 0) { + return str; + } + + str[0] = toupper(str.front()); + for (size_t i = 1; i < strLength; ++i) { + if (str[i - 1] == ' ') { + str[i] = toupper(str[i]); + } + } + + return str; +} + +bool booleanString(const std::string& str) +{ + if (str.empty()) { + return false; + } + + char ch = tolower(str.front()); + return ch != 'f' && ch != 'n' && ch != '0'; +} + +std::string getWeaponName(WeaponType_t weaponType) +{ + switch (weaponType) { + case WEAPON_SWORD: return "sword"; + case WEAPON_CLUB: return "club"; + case WEAPON_AXE: return "axe"; + case WEAPON_DISTANCE: return "distance"; + case WEAPON_WAND: return "wand"; + case WEAPON_AMMO: return "ammunition"; + default: return std::string(); + } +} + +size_t combatTypeToIndex(CombatType_t combatType) +{ + switch (combatType) { + case COMBAT_PHYSICALDAMAGE: + return 0; + case COMBAT_ENERGYDAMAGE: + return 1; + case COMBAT_EARTHDAMAGE: + return 2; + case COMBAT_FIREDAMAGE: + return 3; + case COMBAT_UNDEFINEDDAMAGE: + return 4; + case COMBAT_LIFEDRAIN: + return 5; + case COMBAT_MANADRAIN: + return 6; + case COMBAT_HEALING: + return 7; + case COMBAT_DROWNDAMAGE: + return 8; + case COMBAT_ICEDAMAGE: + return 9; + case COMBAT_HOLYDAMAGE: + return 10; + case COMBAT_DEATHDAMAGE: + return 11; + default: + return 0; + } +} + +CombatType_t indexToCombatType(size_t v) +{ + return static_cast(1 << v); +} + +uint8_t serverFluidToClient(uint8_t serverFluid) +{ + uint8_t size = sizeof(clientToServerFluidMap) / sizeof(uint8_t); + for (uint8_t i = 0; i < size; ++i) { + if (clientToServerFluidMap[i] == serverFluid) { + return i; + } + } + return 0; +} + +uint8_t clientFluidToServer(uint8_t clientFluid) +{ + uint8_t size = sizeof(clientToServerFluidMap) / sizeof(uint8_t); + if (clientFluid >= size) { + return 0; + } + return clientToServerFluidMap[clientFluid]; +} + +itemAttrTypes stringToItemAttribute(const std::string& str) +{ + if (str == "aid") { + return ITEM_ATTRIBUTE_ACTIONID; + } else if (str == "uid") { + return ITEM_ATTRIBUTE_UNIQUEID; + } else if (str == "description") { + return ITEM_ATTRIBUTE_DESCRIPTION; + } else if (str == "text") { + return ITEM_ATTRIBUTE_TEXT; + } else if (str == "date") { + return ITEM_ATTRIBUTE_DATE; + } else if (str == "writer") { + return ITEM_ATTRIBUTE_WRITER; + } else if (str == "name") { + return ITEM_ATTRIBUTE_NAME; + } else if (str == "article") { + return ITEM_ATTRIBUTE_ARTICLE; + } else if (str == "pluralname") { + return ITEM_ATTRIBUTE_PLURALNAME; + } else if (str == "weight") { + return ITEM_ATTRIBUTE_WEIGHT; + } else if (str == "attack") { + return ITEM_ATTRIBUTE_ATTACK; + } else if (str == "defense") { + return ITEM_ATTRIBUTE_DEFENSE; + } else if (str == "extradefense") { + return ITEM_ATTRIBUTE_EXTRADEFENSE; + } else if (str == "armor") { + return ITEM_ATTRIBUTE_ARMOR; + } else if (str == "hitchance") { + return ITEM_ATTRIBUTE_HITCHANCE; + } else if (str == "shootrange") { + return ITEM_ATTRIBUTE_SHOOTRANGE; + } else if (str == "owner") { + return ITEM_ATTRIBUTE_OWNER; + } else if (str == "duration") { + return ITEM_ATTRIBUTE_DURATION; + } else if (str == "decaystate") { + return ITEM_ATTRIBUTE_DECAYSTATE; + } else if (str == "corpseowner") { + return ITEM_ATTRIBUTE_CORPSEOWNER; + } else if (str == "charges") { + return ITEM_ATTRIBUTE_CHARGES; + } else if (str == "fluidtype") { + return ITEM_ATTRIBUTE_FLUIDTYPE; + } else if (str == "doorid") { + return ITEM_ATTRIBUTE_DOORID; + } else if (str == "wrapid") { + return ITEM_ATTRIBUTE_WRAPID; + } + return ITEM_ATTRIBUTE_NONE; +} + +std::string getFirstLine(const std::string& str) +{ + std::string firstLine; + firstLine.reserve(str.length()); + for (const char c : str) { + if (c == '\n') { + break; + } + firstLine.push_back(c); + } + return firstLine; +} + +const char* getReturnMessage(ReturnValue value) +{ + switch (value) { + case RETURNVALUE_DESTINATIONOUTOFREACH: + return "Destination is out of range."; + + case RETURNVALUE_NOTMOVEABLE: + return "You cannot move this object."; + + case RETURNVALUE_DROPTWOHANDEDITEM: + return "Drop the double-handed object first."; + + case RETURNVALUE_BOTHHANDSNEEDTOBEFREE: + return "Both hands need to be free."; + + case RETURNVALUE_CANNOTBEDRESSED: + return "You cannot dress this object there."; + + case RETURNVALUE_PUTTHISOBJECTINYOURHAND: + return "Put this object in your hand."; + + case RETURNVALUE_PUTTHISOBJECTINBOTHHANDS: + return "Put this object in both hands."; + + case RETURNVALUE_CANONLYUSEONEWEAPON: + return "You may only use one weapon."; + + case RETURNVALUE_TOOFARAWAY: + return "You are too far away."; + + case RETURNVALUE_FIRSTGODOWNSTAIRS: + return "First go downstairs."; + + case RETURNVALUE_FIRSTGOUPSTAIRS: + return "First go upstairs."; + + case RETURNVALUE_NOTENOUGHCAPACITY: + return "This object is too heavy for you to carry."; + + case RETURNVALUE_CONTAINERNOTENOUGHROOM: + return "You cannot put more objects in this container."; + + case RETURNVALUE_NEEDEXCHANGE: + case RETURNVALUE_NOTENOUGHROOM: + return "There is not enough room."; + + case RETURNVALUE_CANNOTPICKUP: + return "You cannot take this object."; + + case RETURNVALUE_CANNOTTHROW: + return "You cannot throw there."; + + case RETURNVALUE_THEREISNOWAY: + return "There is no way."; + + case RETURNVALUE_THISISIMPOSSIBLE: + return "This is impossible."; + + case RETURNVALUE_PLAYERISPZLOCKED: + return "You can not enter a protection zone after attacking another player."; + + case RETURNVALUE_PLAYERISNOTINVITED: + return "You are not invited."; + + case RETURNVALUE_CREATUREDOESNOTEXIST: + return "Creature does not exist."; + + case RETURNVALUE_DEPOTISFULL: + return "You cannot put more items in this depot."; + + case RETURNVALUE_CANNOTUSETHISOBJECT: + return "You cannot use this object."; + + case RETURNVALUE_PLAYERWITHTHISNAMEISNOTONLINE: + return "A player with this name is not online."; + + case RETURNVALUE_NOTREQUIREDLEVELTOUSERUNE: + return "You do not have the required magic level to use this rune."; + + case RETURNVALUE_YOUAREALREADYTRADING: + return "You are already trading."; + + case RETURNVALUE_THISPLAYERISALREADYTRADING: + return "This player is already trading."; + + case RETURNVALUE_YOUMAYNOTLOGOUTDURINGAFIGHT: + return "You may not logout during or immediately after a fight!"; + + case RETURNVALUE_DIRECTPLAYERSHOOT: + return "You are not allowed to shoot directly on players."; + + case RETURNVALUE_NOTENOUGHLEVEL: + return "Your level is too low."; + + case RETURNVALUE_NOTENOUGHMAGICLEVEL: + return "You do not have enough magic level."; + + case RETURNVALUE_NOTENOUGHMANA: + return "You do not have enough mana."; + + case RETURNVALUE_NOTENOUGHSOUL: + return "You do not have enough soul."; + + case RETURNVALUE_YOUAREEXHAUSTED: + return "You are exhausted."; + + case RETURNVALUE_YOUCANNOTUSEOBJECTSTHATFAST: + return "You cannot use objects that fast."; + + case RETURNVALUE_CANONLYUSETHISRUNEONCREATURES: + return "You can only use it on creatures."; + + case RETURNVALUE_PLAYERISNOTREACHABLE: + return "Player is not reachable."; + + case RETURNVALUE_CREATUREISNOTREACHABLE: + return "Creature is not reachable."; + + case RETURNVALUE_ACTIONNOTPERMITTEDINPROTECTIONZONE: + return "This action is not permitted in a protection zone."; + + case RETURNVALUE_YOUMAYNOTATTACKTHISPLAYER: + return "You may not attack this person."; + + case RETURNVALUE_YOUMAYNOTATTACKTHISCREATURE: + return "You may not attack this creature."; + + case RETURNVALUE_YOUMAYNOTATTACKAPERSONINPROTECTIONZONE: + return "You may not attack a person in a protection zone."; + + case RETURNVALUE_YOUMAYNOTATTACKAPERSONWHILEINPROTECTIONZONE: + return "You may not attack a person while you are in a protection zone."; + + case RETURNVALUE_YOUCANONLYUSEITONCREATURES: + return "You can only use it on creatures."; + + case RETURNVALUE_TURNSECUREMODETOATTACKUNMARKEDPLAYERS: + return "Turn secure mode off if you really want to attack unmarked players."; + + case RETURNVALUE_YOUNEEDPREMIUMACCOUNT: + return "You need a premium account."; + + case RETURNVALUE_YOUNEEDTOLEARNTHISSPELL: + return "You must learn this spell first."; + + case RETURNVALUE_YOURVOCATIONCANNOTUSETHISSPELL: + return "You have the wrong vocation to cast this spell."; + + case RETURNVALUE_YOUNEEDAWEAPONTOUSETHISSPELL: + return "You need to equip a weapon to use this spell."; + + case RETURNVALUE_PLAYERISPZLOCKEDLEAVEPVPZONE: + return "You can not leave a pvp zone after attacking another player."; + + case RETURNVALUE_PLAYERISPZLOCKEDENTERPVPZONE: + return "You can not enter a pvp zone after attacking another player."; + + case RETURNVALUE_ACTIONNOTPERMITTEDINANOPVPZONE: + return "This action is not permitted in a non pvp zone."; + + case RETURNVALUE_YOUCANNOTLOGOUTHERE: + return "You can not logout here."; + + case RETURNVALUE_YOUNEEDAMAGICITEMTOCASTSPELL: + return "You need a magic item to cast this spell."; + + case RETURNVALUE_CANNOTCONJUREITEMHERE: + return "You cannot conjure items here."; + + case RETURNVALUE_YOUNEEDTOSPLITYOURSPEARS: + return "You need to split your spears first."; + + case RETURNVALUE_NAMEISTOOAMBIGUOUS: + return "Player name is ambiguous."; + + case RETURNVALUE_CANONLYUSEONESHIELD: + return "You may use only one shield."; + + case RETURNVALUE_NOPARTYMEMBERSINRANGE: + return "No party members in range."; + + case RETURNVALUE_YOUARENOTTHEOWNER: + return "You are not the owner."; + + case RETURNVALUE_NOSUCHRAIDEXISTS: + return "No such raid exists."; + + case RETURNVALUE_ANOTHERRAIDISALREADYEXECUTING: + return "Another raid is already executing."; + + case RETURNVALUE_TRADEPLAYERFARAWAY: + return "Trade player is too far away."; + + case RETURNVALUE_YOUDONTOWNTHISHOUSE: + return "You don't own this house."; + + case RETURNVALUE_TRADEPLAYERALREADYOWNSAHOUSE: + return "Trade player already owns a house."; + + case RETURNVALUE_TRADEPLAYERHIGHESTBIDDER: + return "Trade player is currently the highest bidder of an auctioned house."; + + case RETURNVALUE_YOUCANNOTTRADETHISHOUSE: + return "You can not trade this house."; + + case RETURNVALUE_YOUDONTHAVEREQUIREDPROFESSION: + return "You don't have the required profession."; + + default: // RETURNVALUE_NOTPOSSIBLE, etc + return "Sorry, not possible."; + } +} + +int64_t OTSYS_TIME() +{ + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); +} + +SpellGroup_t stringToSpellGroup(const std::string& value) +{ + std::string tmpStr = asLowerCaseString(value); + if (tmpStr == "attack" || tmpStr == "1") { + return SPELLGROUP_ATTACK; + } else if (tmpStr == "healing" || tmpStr == "2") { + return SPELLGROUP_HEALING; + } else if (tmpStr == "support" || tmpStr == "3") { + return SPELLGROUP_SUPPORT; + } else if (tmpStr == "special" || tmpStr == "4") { + return SPELLGROUP_SPECIAL; + } + + return SPELLGROUP_NONE; +} diff --git a/src/tools.h b/src/tools.h new file mode 100644 index 0000000..042bb01 --- /dev/null +++ b/src/tools.h @@ -0,0 +1,99 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_TOOLS_H_5F9A9742DA194628830AA1C64909AE43 +#define FS_TOOLS_H_5F9A9742DA194628830AA1C64909AE43 + +#include + +#include "position.h" +#include "const.h" +#include "enums.h" + +void printXMLError(const std::string& where, const std::string& fileName, const pugi::xml_parse_result& result); + +std::string transformToSHA1(const std::string& input); +std::string generateToken(const std::string& key, uint32_t ticks); + +void replaceString(std::string& str, const std::string& sought, const std::string& replacement); +void trim_right(std::string& source, char t); +void trim_left(std::string& source, char t); +void toLowerCaseString(std::string& source); +std::string asLowerCaseString(std::string source); +std::string asUpperCaseString(std::string source); + +using StringVector = std::vector; +using IntegerVector = std::vector; + +StringVector explodeString(const std::string& inString, const std::string& separator, int32_t limit = -1); +IntegerVector vectorAtoi(const StringVector& stringVector); +constexpr bool hasBitSet(uint32_t flag, uint32_t flags) { + return (flags & flag) != 0; +} + +std::mt19937& getRandomGenerator(); +int32_t uniform_random(int32_t minNumber, int32_t maxNumber); +int32_t normal_random(int32_t minNumber, int32_t maxNumber); +bool boolean_random(double probability = 0.5); + +Direction getDirection(const std::string& string); +Position getNextPosition(Direction direction, Position pos); +Direction getDirectionTo(const Position& from, const Position& to); + +std::string getFirstLine(const std::string& str); + +std::string formatDate(time_t time); +std::string formatDateShort(time_t time); +std::string convertIPToString(uint32_t ip); + +void trimString(std::string& str); + +MagicEffectClasses getMagicEffect(const std::string& strValue); +ShootType_t getShootType(const std::string& strValue); +Ammo_t getAmmoType(const std::string& strValue); +WeaponAction_t getWeaponAction(const std::string& strValue); +Skulls_t getSkullType(const std::string& strValue); +std::string getCombatName(CombatType_t combatType); + +std::string getSpecialSkillName(uint8_t skillid); +std::string getSkillName(uint8_t skillid); + +uint32_t adlerChecksum(const uint8_t* data, size_t length); + +std::string ucfirst(std::string str); +std::string ucwords(std::string str); +bool booleanString(const std::string& str); + +std::string getWeaponName(WeaponType_t weaponType); + +size_t combatTypeToIndex(CombatType_t combatType); +CombatType_t indexToCombatType(size_t v); + +uint8_t serverFluidToClient(uint8_t serverFluid); +uint8_t clientFluidToServer(uint8_t clientFluid); + +itemAttrTypes stringToItemAttribute(const std::string& str); + +const char* getReturnMessage(ReturnValue value); + +int64_t OTSYS_TIME(); + +SpellGroup_t stringToSpellGroup(const std::string& value); + +#endif diff --git a/src/town.h b/src/town.h new file mode 100644 index 0000000..be3c9e9 --- /dev/null +++ b/src/town.h @@ -0,0 +1,98 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_TOWN_H_3BE21D2293B44AA4A3D22D25BE1B9350 +#define FS_TOWN_H_3BE21D2293B44AA4A3D22D25BE1B9350 + +#include "position.h" + +class Town +{ + public: + explicit Town(uint32_t id) : id(id) {} + + const Position& getTemplePosition() const { + return templePosition; + } + const std::string& getName() const { + return name; + } + + void setTemplePos(Position pos) { + templePosition = pos; + } + void setName(std::string name) { + this->name = std::move(name); + } + uint32_t getID() const { + return id; + } + + private: + uint32_t id; + std::string name; + Position templePosition; +}; + +using TownMap = std::map; + +class Towns +{ + public: + Towns() = default; + ~Towns() { + for (const auto& it : townMap) { + delete it.second; + } + } + + // non-copyable + Towns(const Towns&) = delete; + Towns& operator=(const Towns&) = delete; + + bool addTown(uint32_t townId, Town* town) { + return townMap.emplace(townId, town).second; + } + + Town* getTown(const std::string& townName) const { + for (const auto& it : townMap) { + if (strcasecmp(townName.c_str(), it.second->getName().c_str()) == 0) { + return it.second; + } + } + return nullptr; + } + + Town* getTown(uint32_t townId) const { + auto it = townMap.find(townId); + if (it == townMap.end()) { + return nullptr; + } + return it->second; + } + + const TownMap& getTowns() const { + return townMap; + } + + private: + TownMap townMap; +}; + +#endif diff --git a/src/trashholder.cpp b/src/trashholder.cpp new file mode 100644 index 0000000..5d90758 --- /dev/null +++ b/src/trashholder.cpp @@ -0,0 +1,102 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "trashholder.h" +#include "game.h" + +extern Game g_game; + +ReturnValue TrashHolder::queryAdd(int32_t, const Thing&, uint32_t, uint32_t, Creature*) const +{ + return RETURNVALUE_NOERROR; +} + +ReturnValue TrashHolder::queryMaxCount(int32_t, const Thing&, uint32_t count, uint32_t& maxQueryCount, uint32_t) const +{ + maxQueryCount = std::max(1, count); + return RETURNVALUE_NOERROR; +} + +ReturnValue TrashHolder::queryRemove(const Thing&, uint32_t, uint32_t) const +{ + return RETURNVALUE_NOTPOSSIBLE; +} + +Cylinder* TrashHolder::queryDestination(int32_t&, const Thing&, Item**, uint32_t&) +{ + return this; +} + +void TrashHolder::addThing(Thing* thing) +{ + return addThing(0, thing); +} + +void TrashHolder::addThing(int32_t, Thing* thing) +{ + Item* item = thing->getItem(); + if (!item) { + return; + } + + if (item == this || !item->hasProperty(CONST_PROP_MOVEABLE)) { + return; + } + + const ItemType& it = Item::items[id]; + if (item->isHangable() && it.isGroundTile()) { + Tile* tile = dynamic_cast(getParent()); + if (tile && tile->hasFlag(TILESTATE_SUPPORTS_HANGABLE)) { + return; + } + } + + g_game.internalRemoveItem(item); + + if (it.magicEffect != CONST_ME_NONE) { + g_game.addMagicEffect(getPosition(), it.magicEffect); + } +} + +void TrashHolder::updateThing(Thing*, uint16_t, uint32_t) +{ + // +} + +void TrashHolder::replaceThing(uint32_t, Thing*) +{ + // +} + +void TrashHolder::removeThing(Thing*, uint32_t) +{ + // +} + +void TrashHolder::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t) +{ + getParent()->postAddNotification(thing, oldParent, index, LINK_PARENT); +} + +void TrashHolder::postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t) +{ + getParent()->postRemoveNotification(thing, newParent, index, LINK_PARENT); +} diff --git a/src/trashholder.h b/src/trashholder.h new file mode 100644 index 0000000..a8d1f46 --- /dev/null +++ b/src/trashholder.h @@ -0,0 +1,57 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_TRASHHOLDER_H_BA162024D67B4D388147F5EE06F33098 +#define FS_TRASHHOLDER_H_BA162024D67B4D388147F5EE06F33098 + +#include "item.h" +#include "cylinder.h" +#include "const.h" + +class TrashHolder final : public Item, public Cylinder +{ + public: + explicit TrashHolder(uint16_t itemId) : Item(itemId) {} + + TrashHolder* getTrashHolder() override { + return this; + } + const TrashHolder* getTrashHolder() const override { + return this; + } + + //cylinder implementations + ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count, uint32_t flags, Creature* actor = nullptr) const override; + ReturnValue queryMaxCount(int32_t index, const Thing& thing, uint32_t count, uint32_t& maxQueryCount, uint32_t flags) const override; + ReturnValue queryRemove(const Thing& thing, uint32_t count, uint32_t flags) const override; + Cylinder* queryDestination(int32_t& index, const Thing& thing, Item** destItem, uint32_t& flags) override; + + void addThing(Thing* thing) override; + void addThing(int32_t index, Thing* thing) override; + + void updateThing(Thing* thing, uint16_t itemId, uint32_t count) override; + void replaceThing(uint32_t index, Thing* thing) override; + + void removeThing(Thing* thing, uint32_t count) override; + + void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; + void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) override; +}; + +#endif diff --git a/src/vocation.cpp b/src/vocation.cpp new file mode 100644 index 0000000..ba80250 --- /dev/null +++ b/src/vocation.cpp @@ -0,0 +1,210 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "vocation.h" + +#include "pugicast.h" +#include "tools.h" + +bool Vocations::loadFromXml() +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/XML/vocations.xml"); + if (!result) { + printXMLError("Error - Vocations::loadFromXml", "data/XML/vocations.xml", result); + return false; + } + + for (auto vocationNode : doc.child("vocations").children()) { + pugi::xml_attribute attr; + if (!(attr = vocationNode.attribute("id"))) { + std::cout << "[Warning - Vocations::loadFromXml] Missing vocation id" << std::endl; + continue; + } + + uint16_t id = pugi::cast(attr.value()); + + auto res = vocationsMap.emplace(std::piecewise_construct, + std::forward_as_tuple(id), std::forward_as_tuple(id)); + Vocation& voc = res.first->second; + + if ((attr = vocationNode.attribute("name"))) { + voc.name = attr.as_string(); + } + + if ((attr = vocationNode.attribute("clientid"))) { + voc.clientId = pugi::cast(attr.value()); + } + + if ((attr = vocationNode.attribute("description"))) { + voc.description = attr.as_string(); + } + + if ((attr = vocationNode.attribute("gaincap"))) { + voc.gainCap = pugi::cast(attr.value()) * 100; + } + + if ((attr = vocationNode.attribute("gainhp"))) { + voc.gainHP = pugi::cast(attr.value()); + } + + if ((attr = vocationNode.attribute("gainmana"))) { + voc.gainMana = pugi::cast(attr.value()); + } + + if ((attr = vocationNode.attribute("gainhpticks"))) { + voc.gainHealthTicks = pugi::cast(attr.value()); + } + + if ((attr = vocationNode.attribute("gainhpamount"))) { + voc.gainHealthAmount = pugi::cast(attr.value()); + } + + if ((attr = vocationNode.attribute("gainmanaticks"))) { + voc.gainManaTicks = pugi::cast(attr.value()); + } + + if ((attr = vocationNode.attribute("gainmanaamount"))) { + voc.gainManaAmount = pugi::cast(attr.value()); + } + + if ((attr = vocationNode.attribute("manamultiplier"))) { + voc.manaMultiplier = pugi::cast(attr.value()); + } + + if ((attr = vocationNode.attribute("attackspeed"))) { + voc.attackSpeed = pugi::cast(attr.value()); + } + + if ((attr = vocationNode.attribute("basespeed"))) { + voc.baseSpeed = pugi::cast(attr.value()); + } + + if ((attr = vocationNode.attribute("soulmax"))) { + voc.soulMax = pugi::cast(attr.value()); + } + + if ((attr = vocationNode.attribute("gainsoulticks"))) { + voc.gainSoulTicks = pugi::cast(attr.value()); + } + + if ((attr = vocationNode.attribute("fromvoc"))) { + voc.fromVocation = pugi::cast(attr.value()); + } + + for (auto childNode : vocationNode.children()) { + if (strcasecmp(childNode.name(), "skill") == 0) { + pugi::xml_attribute skillIdAttribute = childNode.attribute("id"); + if (skillIdAttribute) { + uint16_t skill_id = pugi::cast(skillIdAttribute.value()); + if (skill_id <= SKILL_LAST) { + voc.skillMultipliers[skill_id] = pugi::cast(childNode.attribute("multiplier").value()); + } else { + std::cout << "[Notice - Vocations::loadFromXml] No valid skill id: " << skill_id << " for vocation: " << voc.id << std::endl; + } + } else { + std::cout << "[Notice - Vocations::loadFromXml] Missing skill id for vocation: " << voc.id << std::endl; + } + } else if (strcasecmp(childNode.name(), "formula") == 0) { + pugi::xml_attribute meleeDamageAttribute = childNode.attribute("meleeDamage"); + if (meleeDamageAttribute) { + voc.meleeDamageMultiplier = pugi::cast(meleeDamageAttribute.value()); + } + + pugi::xml_attribute distDamageAttribute = childNode.attribute("distDamage"); + if (distDamageAttribute) { + voc.distDamageMultiplier = pugi::cast(distDamageAttribute.value()); + } + + pugi::xml_attribute defenseAttribute = childNode.attribute("defense"); + if (defenseAttribute) { + voc.defenseMultiplier = pugi::cast(defenseAttribute.value()); + } + + pugi::xml_attribute armorAttribute = childNode.attribute("armor"); + if (armorAttribute) { + voc.armorMultiplier = pugi::cast(armorAttribute.value()); + } + } + } + } + return true; +} + +Vocation* Vocations::getVocation(uint16_t id) +{ + auto it = vocationsMap.find(id); + if (it == vocationsMap.end()) { + std::cout << "[Warning - Vocations::getVocation] Vocation " << id << " not found." << std::endl; + return nullptr; + } + return &it->second; +} + +int32_t Vocations::getVocationId(const std::string& name) const +{ + for (const auto& it : vocationsMap) { + if (strcasecmp(it.second.name.c_str(), name.c_str()) == 0) { + return it.first; + } + } + return -1; +} + +uint16_t Vocations::getPromotedVocation(uint16_t vocationId) const +{ + for (const auto& it : vocationsMap) { + if (it.second.fromVocation == vocationId && it.first != vocationId) { + return it.first; + } + } + return VOCATION_NONE; +} + +uint32_t Vocation::skillBase[SKILL_LAST + 1] = {50, 50, 50, 50, 30, 100, 20}; + +uint64_t Vocation::getReqSkillTries(uint8_t skill, uint16_t level) +{ + if (skill > SKILL_LAST) { + return 0; + } + + auto it = cacheSkill[skill].find(level); + if (it != cacheSkill[skill].end()) { + return it->second; + } + + uint64_t tries = static_cast(skillBase[skill] * std::pow(static_cast(skillMultipliers[skill]), level - 11)); + cacheSkill[skill][level] = tries; + return tries; +} + +uint64_t Vocation::getReqMana(uint32_t magLevel) +{ + auto it = cacheMana.find(magLevel); + if (it != cacheMana.end()) { + return it->second; + } + + uint64_t reqMana = std::floor(1600 * std::pow(manaMultiplier, static_cast(magLevel) - 1)); + cacheMana[magLevel] = reqMana; + return reqMana; +} diff --git a/src/vocation.h b/src/vocation.h new file mode 100644 index 0000000..757d45a --- /dev/null +++ b/src/vocation.h @@ -0,0 +1,139 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_VOCATION_H_ADCAA356C0DB44CEBA994A0D678EC92D +#define FS_VOCATION_H_ADCAA356C0DB44CEBA994A0D678EC92D + +#include "enums.h" +#include "item.h" + +class Vocation +{ + public: + explicit Vocation(uint16_t id) : id(id) {} + + const std::string& getVocName() const { + return name; + } + const std::string& getVocDescription() const { + return description; + } + uint64_t getReqSkillTries(uint8_t skill, uint16_t level); + uint64_t getReqMana(uint32_t magLevel); + + uint16_t getId() const { + return id; + } + + uint8_t getClientId() const { + return clientId; + } + + uint32_t getHPGain() const { + return gainHP; + } + uint32_t getManaGain() const { + return gainMana; + } + uint32_t getCapGain() const { + return gainCap; + } + + uint32_t getManaGainTicks() const { + return gainManaTicks; + } + uint32_t getManaGainAmount() const { + return gainManaAmount; + } + uint32_t getHealthGainTicks() const { + return gainHealthTicks; + } + uint32_t getHealthGainAmount() const { + return gainHealthAmount; + } + + uint8_t getSoulMax() const { + return soulMax; + } + uint16_t getSoulGainTicks() const { + return gainSoulTicks; + } + + uint32_t getAttackSpeed() const { + return attackSpeed; + } + uint32_t getBaseSpeed() const { + return baseSpeed; + } + + uint32_t getFromVocation() const { + return fromVocation; + } + + float meleeDamageMultiplier = 1.0f; + float distDamageMultiplier = 1.0f; + float defenseMultiplier = 1.0f; + float armorMultiplier = 1.0f; + + private: + friend class Vocations; + + std::map cacheMana; + std::map cacheSkill[SKILL_LAST + 1]; + + std::string name = "none"; + std::string description; + + float skillMultipliers[SKILL_LAST + 1] = {1.5f, 2.0f, 2.0f, 2.0f, 2.0f, 1.5f, 1.1f}; + float manaMultiplier = 4.0f; + + uint32_t gainHealthTicks = 6; + uint32_t gainHealthAmount = 1; + uint32_t gainManaTicks = 6; + uint32_t gainManaAmount = 1; + uint32_t gainCap = 500; + uint32_t gainMana = 5; + uint32_t gainHP = 5; + uint32_t fromVocation = VOCATION_NONE; + uint32_t attackSpeed = 1500; + uint32_t baseSpeed = 220; + uint16_t id; + + uint16_t gainSoulTicks = 120; + + uint8_t soulMax = 100; + uint8_t clientId = 0; + + static uint32_t skillBase[SKILL_LAST + 1]; +}; + +class Vocations +{ + public: + bool loadFromXml(); + + Vocation* getVocation(uint16_t id); + int32_t getVocationId(const std::string& name) const; + uint16_t getPromotedVocation(uint16_t vocationId) const; + + private: + std::map vocationsMap; +}; + +#endif diff --git a/src/waitlist.cpp b/src/waitlist.cpp new file mode 100644 index 0000000..df8d038 --- /dev/null +++ b/src/waitlist.cpp @@ -0,0 +1,158 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "configmanager.h" +#include "game.h" +#include "waitlist.h" + +extern ConfigManager g_config; +extern Game g_game; + + +namespace { + +struct Wait +{ + constexpr Wait(std::size_t timeout, uint32_t playerGUID) : + timeout(timeout), playerGUID(playerGUID) {} + + std::size_t timeout; + uint32_t playerGUID; +}; + +using WaitList = std::list; + +void cleanupList(WaitList& list) +{ + int64_t time = OTSYS_TIME(); + + auto it = list.begin(), end = list.end(); + while (it != end) { + if ((it->timeout - time) <= 0) { + it = list.erase(it); + } else { + ++it; + } + } +} + +std::size_t getTimeout(std::size_t slot) +{ + //timeout is set to 15 seconds longer than expected retry attempt + return WaitingList::getTime(slot) + 15; +} + +} // namespace + +struct WaitListInfo +{ + WaitList priorityWaitList; + WaitList waitList; + + std::pair findClient(const Player *player) { + std::size_t slot = 1; + for (auto it = priorityWaitList.begin(), end = priorityWaitList.end(); it != end; ++it, ++slot) { + if (it->playerGUID == player->getGUID()) { + return {it, slot}; + } + } + + for (auto it = waitList.begin(), end = waitList.end(); it != end; ++it, ++slot) { + if (it->playerGUID == player->getGUID()) { + return {it, slot}; + } + } + return {waitList.end(), slot}; + } +}; + +WaitingList& WaitingList::getInstance() +{ + static WaitingList waitingList; + return waitingList; +} + +std::size_t WaitingList::getTime(std::size_t slot) +{ + if (slot < 5) { + return 5; + } else if (slot < 10) { + return 10; + } else if (slot < 20) { + return 20; + } else if (slot < 50) { + return 60; + } else { + return 120; + } +} + +bool WaitingList::clientLogin(const Player* player) +{ + if (player->hasFlag(PlayerFlag_CanAlwaysLogin) || player->getAccountType() >= ACCOUNT_TYPE_GAMEMASTER) { + return true; + } + + auto maxPlayers = static_cast(g_config.getNumber(ConfigManager::MAX_PLAYERS)); + if (maxPlayers == 0 || (info->priorityWaitList.empty() && info->waitList.empty() && g_game.getPlayersOnline() < maxPlayers)) { + return true; + } + + cleanupList(info->priorityWaitList); + cleanupList(info->waitList); + + WaitList::iterator it; + WaitList::size_type slot; + std::tie(it, slot) = info->findClient(player); + if (it != info->waitList.end()) { + if ((g_game.getPlayersOnline() + slot) <= maxPlayers) { + //should be able to login now + info->waitList.erase(it); + return true; + } + + //let them wait a bit longer + it->timeout = OTSYS_TIME() + (getTimeout(slot) * 1000); + return false; + } + + slot = info->priorityWaitList.size(); + if (player->isPremium()) { + info->priorityWaitList.emplace_back(OTSYS_TIME() + (getTimeout(slot + 1) * 1000), player->getGUID()); + } else { + slot += info->waitList.size(); + info->waitList.emplace_back(OTSYS_TIME() + (getTimeout(slot + 1) * 1000), player->getGUID()); + } + return false; +} + +std::size_t WaitingList::getClientSlot(const Player* player) +{ + WaitList::iterator it; + WaitList::size_type slot; + std::tie(it, slot) = info->findClient(player); + if (it == info->waitList.end()) { + return 0; + } + return slot; +} + +WaitingList::WaitingList() : info(new WaitListInfo) {} diff --git a/src/waitlist.h b/src/waitlist.h new file mode 100644 index 0000000..661e7b9 --- /dev/null +++ b/src/waitlist.h @@ -0,0 +1,42 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_WAITLIST_H_7E4299E552E44F10BC4F4E50BF3D7241 +#define FS_WAITLIST_H_7E4299E552E44F10BC4F4E50BF3D7241 + +#include "player.h" + +struct WaitListInfo; + +class WaitingList +{ + public: + static WaitingList& getInstance(); + + bool clientLogin(const Player* player); + std::size_t getClientSlot(const Player* player); + static std::size_t getTime(std::size_t slot); + + private: + WaitingList(); + + std::unique_ptr info; +}; + +#endif diff --git a/src/weapons.cpp b/src/weapons.cpp new file mode 100644 index 0000000..15418bc --- /dev/null +++ b/src/weapons.cpp @@ -0,0 +1,946 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "combat.h" +#include "configmanager.h" +#include "game.h" +#include "pugicast.h" +#include "weapons.h" + +extern Game g_game; +extern Vocations g_vocations; +extern ConfigManager g_config; +extern Weapons* g_weapons; + +Weapons::Weapons() +{ + scriptInterface.initState(); +} + +Weapons::~Weapons() +{ + clear(false); +} + +const Weapon* Weapons::getWeapon(const Item* item) const +{ + if (!item) { + return nullptr; + } + + auto it = weapons.find(item->getID()); + if (it == weapons.end()) { + return nullptr; + } + return it->second; +} + +void Weapons::clear(bool fromLua) +{ + for (auto it = weapons.begin(); it != weapons.end(); ) { + if (fromLua == it->second->fromLua) { + it = weapons.erase(it); + } else { + ++it; + } + } + + reInitState(fromLua); +} + +LuaScriptInterface& Weapons::getScriptInterface() +{ + return scriptInterface; +} + +std::string Weapons::getScriptBaseName() const +{ + return "weapons"; +} + +void Weapons::loadDefaults() +{ + for (size_t i = 100, size = Item::items.size(); i < size; ++i) { + const ItemType& it = Item::items.getItemType(i); + if (it.id == 0 || weapons.find(i) != weapons.end()) { + continue; + } + + switch (it.weaponType) { + case WEAPON_AXE: + case WEAPON_SWORD: + case WEAPON_CLUB: { + WeaponMelee* weapon = new WeaponMelee(&scriptInterface); + weapon->configureWeapon(it); + weapons[i] = weapon; + break; + } + + case WEAPON_AMMO: + case WEAPON_DISTANCE: { + if (it.weaponType == WEAPON_DISTANCE && it.ammoType != AMMO_NONE) { + continue; + } + + WeaponDistance* weapon = new WeaponDistance(&scriptInterface); + weapon->configureWeapon(it); + weapons[i] = weapon; + break; + } + + default: + break; + } + } +} + +Event_ptr Weapons::getEvent(const std::string& nodeName) +{ + if (strcasecmp(nodeName.c_str(), "melee") == 0) { + return Event_ptr(new WeaponMelee(&scriptInterface)); + } else if (strcasecmp(nodeName.c_str(), "distance") == 0) { + return Event_ptr(new WeaponDistance(&scriptInterface)); + } else if (strcasecmp(nodeName.c_str(), "wand") == 0) { + return Event_ptr(new WeaponWand(&scriptInterface)); + } + return nullptr; +} + +bool Weapons::registerEvent(Event_ptr event, const pugi::xml_node&) +{ + Weapon* weapon = static_cast(event.release()); //event is guaranteed to be a Weapon + + auto result = weapons.emplace(weapon->getID(), weapon); + if (!result.second) { + std::cout << "[Warning - Weapons::registerEvent] Duplicate registered item with id: " << weapon->getID() << std::endl; + } + return result.second; +} + +bool Weapons::registerLuaEvent(Weapon* event) +{ + Weapon_ptr weapon{ event }; + weapons[weapon->getID()] = weapon.release(); + + return true; +} + +//monsters +int32_t Weapons::getMaxMeleeDamage(int32_t attackSkill, int32_t attackValue) +{ + return static_cast(std::ceil((attackSkill * (attackValue * 0.05)) + (attackValue * 0.5))); +} + +//players +int32_t Weapons::getMaxWeaponDamage(uint32_t level, int32_t attackSkill, int32_t attackValue, float attackFactor) +{ + return static_cast(std::round((level / 5) + (((((attackSkill / 4.) + 1) * (attackValue / 3.)) * 1.03) / attackFactor))); +} + +bool Weapon::configureEvent(const pugi::xml_node& node) +{ + pugi::xml_attribute attr; + if (!(attr = node.attribute("id"))) { + std::cout << "[Error - Weapon::configureEvent] Weapon without id." << std::endl; + return false; + } + id = pugi::cast(attr.value()); + + if ((attr = node.attribute("level"))) { + level = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("maglv")) || (attr = node.attribute("maglevel"))) { + magLevel = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("mana"))) { + mana = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("manapercent"))) { + manaPercent = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("soul"))) { + soul = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("prem"))) { + premium = attr.as_bool(); + } + + if ((attr = node.attribute("breakchance"))) { + breakChance = std::min(100, pugi::cast(attr.value())); + } + + if ((attr = node.attribute("action"))) { + action = getWeaponAction(asLowerCaseString(attr.as_string())); + if (action == WEAPONACTION_NONE) { + std::cout << "[Warning - Weapon::configureEvent] Unknown action " << attr.as_string() << std::endl; + } + } + + if ((attr = node.attribute("enabled"))) { + enabled = attr.as_bool(); + } + + if ((attr = node.attribute("unproperly"))) { + wieldUnproperly = attr.as_bool(); + } + + std::list vocStringList; + for (auto vocationNode : node.children()) { + if (!(attr = vocationNode.attribute("name"))) { + continue; + } + + int32_t vocationId = g_vocations.getVocationId(attr.as_string()); + if (vocationId != -1) { + vocWeaponMap[vocationId] = true; + int32_t promotedVocation = g_vocations.getPromotedVocation(vocationId); + if (promotedVocation != VOCATION_NONE) { + vocWeaponMap[promotedVocation] = true; + } + + if (vocationNode.attribute("showInDescription").as_bool(true)) { + vocStringList.push_back(asLowerCaseString(attr.as_string())); + } + } + } + + std::string vocationString; + for (const std::string& str : vocStringList) { + if (!vocationString.empty()) { + if (str != vocStringList.back()) { + vocationString.push_back(','); + vocationString.push_back(' '); + } else { + vocationString += " and "; + } + } + + vocationString += str; + vocationString.push_back('s'); + } + + uint32_t wieldInfo = 0; + if (getReqLevel() > 0) { + wieldInfo |= WIELDINFO_LEVEL; + } + + if (getReqMagLv() > 0) { + wieldInfo |= WIELDINFO_MAGLV; + } + + if (!vocationString.empty()) { + wieldInfo |= WIELDINFO_VOCREQ; + } + + if (isPremium()) { + wieldInfo |= WIELDINFO_PREMIUM; + } + + if (wieldInfo != 0) { + ItemType& it = Item::items.getItemType(id); + it.wieldInfo = wieldInfo; + it.vocationString = vocationString; + it.minReqLevel = getReqLevel(); + it.minReqMagicLevel = getReqMagLv(); + } + + configureWeapon(Item::items[id]); + return true; +} + +void Weapon::configureWeapon(const ItemType& it) +{ + id = it.id; +} + +std::string Weapon::getScriptEventName() const +{ + return "onUseWeapon"; +} + +int32_t Weapon::playerWeaponCheck(Player* player, Creature* target, uint8_t shootRange) const +{ + const Position& playerPos = player->getPosition(); + const Position& targetPos = target->getPosition(); + if (playerPos.z != targetPos.z) { + return 0; + } + + if (std::max(Position::getDistanceX(playerPos, targetPos), Position::getDistanceY(playerPos, targetPos)) > shootRange) { + return 0; + } + + if (!player->hasFlag(PlayerFlag_IgnoreWeaponCheck)) { + if (!enabled) { + return 0; + } + + if (player->getMana() < getManaCost(player)) { + return 0; + } + + if (player->getHealth() < getHealthCost(player)) { + return 0; + } + + if (player->getSoul() < soul) { + return 0; + } + + if (isPremium() && !player->isPremium()) { + return 0; + } + + if (!vocWeaponMap.empty()) { + if (vocWeaponMap.find(player->getVocationId()) == vocWeaponMap.end()) { + return 0; + } + } + + int32_t damageModifier = 100; + if (player->getLevel() < getReqLevel()) { + damageModifier = (isWieldedUnproperly() ? damageModifier / 2 : 0); + } + + if (player->getMagicLevel() < getReqMagLv()) { + damageModifier = (isWieldedUnproperly() ? damageModifier / 2 : 0); + } + return damageModifier; + } + + return 100; +} + +bool Weapon::useWeapon(Player* player, Item* item, Creature* target) const +{ + int32_t damageModifier = playerWeaponCheck(player, target, item->getShootRange()); + if (damageModifier == 0) { + return false; + } + + internalUseWeapon(player, item, target, damageModifier); + return true; +} + +bool Weapon::useFist(Player* player, Creature* target) +{ + if (!Position::areInRange<1, 1>(player->getPosition(), target->getPosition())) { + return false; + } + + float attackFactor = player->getAttackFactor(); + int32_t attackSkill = player->getSkillLevel(SKILL_FIST); + int32_t attackValue = 7; + + int32_t maxDamage = Weapons::getMaxWeaponDamage(player->getLevel(), attackSkill, attackValue, attackFactor); + + CombatParams params; + params.combatType = COMBAT_PHYSICALDAMAGE; + params.blockedByArmor = true; + params.blockedByShield = true; + + CombatDamage damage; + damage.origin = ORIGIN_MELEE; + damage.primary.type = params.combatType; + damage.primary.value = -normal_random(0, maxDamage); + + Combat::doTargetCombat(player, target, damage, params); + if (!player->hasFlag(PlayerFlag_NotGainSkill) && player->getAddAttackSkill()) { + player->addSkillAdvance(SKILL_FIST, 1); + } + + return true; +} + +void Weapon::internalUseWeapon(Player* player, Item* item, Creature* target, int32_t damageModifier) const +{ + if (scripted) { + LuaVariant var; + var.type = VARIANT_NUMBER; + var.number = target->getID(); + executeUseWeapon(player, var); + } else { + CombatDamage damage; + WeaponType_t weaponType = item->getWeaponType(); + if (weaponType == WEAPON_AMMO || weaponType == WEAPON_DISTANCE) { + damage.origin = ORIGIN_RANGED; + } else { + damage.origin = ORIGIN_MELEE; + } + damage.primary.type = params.combatType; + damage.primary.value = (getWeaponDamage(player, target, item) * damageModifier) / 100; + damage.secondary.type = getElementType(); + damage.secondary.value = getElementDamage(player, target, item); + Combat::doTargetCombat(player, target, damage, params); + } + + onUsedWeapon(player, item, target->getTile()); +} + +void Weapon::internalUseWeapon(Player* player, Item* item, Tile* tile) const +{ + if (scripted) { + LuaVariant var; + var.type = VARIANT_TARGETPOSITION; + var.pos = tile->getPosition(); + executeUseWeapon(player, var); + } else { + Combat::postCombatEffects(player, tile->getPosition(), params); + g_game.addMagicEffect(tile->getPosition(), CONST_ME_POFF); + } + + onUsedWeapon(player, item, tile); +} + +void Weapon::onUsedWeapon(Player* player, Item* item, Tile* destTile) const +{ + if (!player->hasFlag(PlayerFlag_NotGainSkill)) { + skills_t skillType; + uint32_t skillPoint; + if (getSkillType(player, item, skillType, skillPoint)) { + player->addSkillAdvance(skillType, skillPoint); + } + } + + uint32_t manaCost = getManaCost(player); + if (manaCost != 0) { + player->addManaSpent(manaCost); + player->changeMana(-static_cast(manaCost)); + } + + uint32_t healthCost = getHealthCost(player); + if (healthCost != 0) { + player->changeHealth(-static_cast(healthCost)); + } + + if (!player->hasFlag(PlayerFlag_HasInfiniteSoul) && soul > 0) { + player->changeSoul(-static_cast(soul)); + } + + if (breakChance != 0 && uniform_random(1, 100) <= breakChance) { + Weapon::decrementItemCount(item); + return; + } + + switch (action) { + case WEAPONACTION_REMOVECOUNT: + if (g_config.getBoolean(ConfigManager::REMOVE_WEAPON_AMMO)) { + Weapon::decrementItemCount(item); + } + break; + + case WEAPONACTION_REMOVECHARGE: { + uint16_t charges = item->getCharges(); + if (charges != 0 && g_config.getBoolean(ConfigManager::REMOVE_WEAPON_CHARGES)) { + g_game.transformItem(item, item->getID(), charges - 1); + } + break; + } + + case WEAPONACTION_MOVE: + g_game.internalMoveItem(item->getParent(), destTile, INDEX_WHEREEVER, item, 1, nullptr, FLAG_NOLIMIT); + break; + + default: + break; + } +} + +uint32_t Weapon::getManaCost(const Player* player) const +{ + if (mana != 0) { + return mana; + } + + if (manaPercent == 0) { + return 0; + } + + return (player->getMaxMana() * manaPercent) / 100; +} + +int32_t Weapon::getHealthCost(const Player* player) const +{ + if (health != 0) { + return health; + } + + if (healthPercent == 0) { + return 0; + } + + return (player->getMaxHealth() * healthPercent) / 100; +} + +bool Weapon::executeUseWeapon(Player* player, const LuaVariant& var) const +{ + //onUseWeapon(player, var) + if (!scriptInterface->reserveScriptEnv()) { + std::cout << "[Error - Weapon::executeUseWeapon] Call stack overflow" << std::endl; + return false; + } + + ScriptEnvironment* env = scriptInterface->getScriptEnv(); + env->setScriptId(scriptId, scriptInterface); + + lua_State* L = scriptInterface->getLuaState(); + + scriptInterface->pushFunction(scriptId); + LuaScriptInterface::pushUserdata(L, player); + LuaScriptInterface::setMetatable(L, -1, "Player"); + scriptInterface->pushVariant(L, var); + + return scriptInterface->callFunction(2); +} + +void Weapon::decrementItemCount(Item* item) +{ + uint16_t count = item->getItemCount(); + if (count > 1) { + g_game.transformItem(item, item->getID(), count - 1); + } else { + g_game.internalRemoveItem(item); + } +} + +WeaponMelee::WeaponMelee(LuaScriptInterface* interface) : + Weapon(interface) +{ + params.blockedByArmor = true; + params.blockedByShield = true; + params.combatType = COMBAT_PHYSICALDAMAGE; +} + +void WeaponMelee::configureWeapon(const ItemType& it) +{ + if (it.abilities) { + elementType = it.abilities->elementType; + elementDamage = it.abilities->elementDamage; + params.aggressive = true; + params.useCharges = true; + } else { + elementType = COMBAT_NONE; + elementDamage = 0; + } + Weapon::configureWeapon(it); +} + +bool WeaponMelee::useWeapon(Player* player, Item* item, Creature* target) const +{ + int32_t damageModifier = playerWeaponCheck(player, target, item->getShootRange()); + if (damageModifier == 0) { + return false; + } + + internalUseWeapon(player, item, target, damageModifier); + return true; +} + +bool WeaponMelee::getSkillType(const Player* player, const Item* item, + skills_t& skill, uint32_t& skillpoint) const +{ + if (player->getAddAttackSkill() && player->getLastAttackBlockType() != BLOCK_IMMUNITY) { + skillpoint = 1; + } else { + skillpoint = 0; + } + + WeaponType_t weaponType = item->getWeaponType(); + switch (weaponType) { + case WEAPON_SWORD: { + skill = SKILL_SWORD; + return true; + } + + case WEAPON_CLUB: { + skill = SKILL_CLUB; + return true; + } + + case WEAPON_AXE: { + skill = SKILL_AXE; + return true; + } + + default: + break; + } + return false; +} + +int32_t WeaponMelee::getElementDamage(const Player* player, const Creature*, const Item* item) const +{ + if (elementType == COMBAT_NONE) { + return 0; + } + + int32_t attackSkill = player->getWeaponSkill(item); + int32_t attackValue = elementDamage; + float attackFactor = player->getAttackFactor(); + + int32_t maxValue = Weapons::getMaxWeaponDamage(player->getLevel(), attackSkill, attackValue, attackFactor); + return -normal_random(0, static_cast(maxValue * player->getVocation()->meleeDamageMultiplier)); +} + +int32_t WeaponMelee::getWeaponDamage(const Player* player, const Creature*, const Item* item, bool maxDamage /*= false*/) const +{ + int32_t attackSkill = player->getWeaponSkill(item); + int32_t attackValue = std::max(0, item->getAttack()); + float attackFactor = player->getAttackFactor(); + + int32_t maxValue = static_cast(Weapons::getMaxWeaponDamage(player->getLevel(), attackSkill, attackValue, attackFactor) * player->getVocation()->meleeDamageMultiplier); + if (maxDamage) { + return -maxValue; + } + + return -normal_random(0, maxValue); +} + +WeaponDistance::WeaponDistance(LuaScriptInterface* interface) : + Weapon(interface) +{ + params.blockedByArmor = true; + params.combatType = COMBAT_PHYSICALDAMAGE; +} + +void WeaponDistance::configureWeapon(const ItemType& it) +{ + params.distanceEffect = it.shootType; + + if (it.abilities) { + elementType = it.abilities->elementType; + elementDamage = it.abilities->elementDamage; + params.aggressive = true; + params.useCharges = true; + } else { + elementType = COMBAT_NONE; + elementDamage = 0; + } + + Weapon::configureWeapon(it); +} + +bool WeaponDistance::useWeapon(Player* player, Item* item, Creature* target) const +{ + int32_t damageModifier = 0; + const ItemType& it = Item::items[id]; + if (it.weaponType == WEAPON_AMMO) { + Item* mainWeaponItem = player->getWeapon(true); + const Weapon* mainWeapon = g_weapons->getWeapon(mainWeaponItem); + if (mainWeapon) { + damageModifier = mainWeapon->playerWeaponCheck(player, target, mainWeaponItem->getShootRange()); + } else if (mainWeaponItem) { + damageModifier = playerWeaponCheck(player, target, mainWeaponItem->getShootRange()); + } + } else { + damageModifier = playerWeaponCheck(player, target, item->getShootRange()); + } + + if (damageModifier == 0) { + return false; + } + + int32_t chance; + if (it.hitChance == 0) { + //hit chance is based on distance to target and distance skill + uint32_t skill = player->getSkillLevel(SKILL_DISTANCE); + const Position& playerPos = player->getPosition(); + const Position& targetPos = target->getPosition(); + uint32_t distance = std::max(Position::getDistanceX(playerPos, targetPos), Position::getDistanceY(playerPos, targetPos)); + + uint32_t maxHitChance; + if (it.maxHitChance != -1) { + maxHitChance = it.maxHitChance; + } else if (it.ammoType != AMMO_NONE) { + //hit chance on two-handed weapons is limited to 90% + maxHitChance = 90; + } else { + //one-handed is set to 75% + maxHitChance = 75; + } + + if (maxHitChance == 75) { + //chance for one-handed weapons + switch (distance) { + case 1: + case 5: + chance = std::min(skill, 74) + 1; + break; + case 2: + chance = static_cast(std::min(skill, 28) * 2.40f) + 8; + break; + case 3: + chance = static_cast(std::min(skill, 45) * 1.55f) + 6; + break; + case 4: + chance = static_cast(std::min(skill, 58) * 1.25f) + 3; + break; + case 6: + chance = static_cast(std::min(skill, 90) * 0.80f) + 3; + break; + case 7: + chance = static_cast(std::min(skill, 104) * 0.70f) + 2; + break; + default: + chance = it.hitChance; + break; + } + } else if (maxHitChance == 90) { + //formula for two-handed weapons + switch (distance) { + case 1: + case 5: + chance = static_cast(std::min(skill, 74) * 1.20f) + 1; + break; + case 2: + chance = static_cast(std::min(skill, 28) * 3.20f); + break; + case 3: + chance = std::min(skill, 45) * 2; + break; + case 4: + chance = static_cast(std::min(skill, 58) * 1.55f); + break; + case 6: + case 7: + chance = std::min(skill, 90); + break; + default: + chance = it.hitChance; + break; + } + } else if (maxHitChance == 100) { + switch (distance) { + case 1: + case 5: + chance = static_cast(std::min(skill, 73) * 1.35f) + 1; + break; + case 2: + chance = static_cast(std::min(skill, 30) * 3.20f) + 4; + break; + case 3: + chance = static_cast(std::min(skill, 48) * 2.05f) + 2; + break; + case 4: + chance = static_cast(std::min(skill, 65) * 1.50f) + 2; + break; + case 6: + chance = static_cast(std::min(skill, 87) * 1.20f) - 4; + break; + case 7: + chance = static_cast(std::min(skill, 90) * 1.10f) + 1; + break; + default: + chance = it.hitChance; + break; + } + } else { + chance = maxHitChance; + } + } else { + chance = it.hitChance; + } + + if (item->getWeaponType() == WEAPON_AMMO) { + Item* bow = player->getWeapon(true); + if (bow && bow->getHitChance() != 0) { + chance += bow->getHitChance(); + } + } + + if (chance >= uniform_random(1, 100)) { + Weapon::internalUseWeapon(player, item, target, damageModifier); + } else { + //miss target + Tile* destTile = target->getTile(); + + if (!Position::areInRange<1, 1, 0>(player->getPosition(), target->getPosition())) { + static std::vector> destList { + {-1, -1}, {0, -1}, {1, -1}, + {-1, 0}, {0, 0}, {1, 0}, + {-1, 1}, {0, 1}, {1, 1} + }; + std::shuffle(destList.begin(), destList.end(), getRandomGenerator()); + + Position destPos = target->getPosition(); + + for (const auto& dir : destList) { + // Blocking tiles or tiles without ground ain't valid targets for spears + Tile* tmpTile = g_game.map.getTile(destPos.x + dir.first, destPos.y + dir.second, destPos.z); + if (tmpTile && !tmpTile->hasFlag(TILESTATE_IMMOVABLEBLOCKSOLID) && tmpTile->getGround() != nullptr) { + destTile = tmpTile; + break; + } + } + } + + Weapon::internalUseWeapon(player, item, destTile); + } + return true; +} + +int32_t WeaponDistance::getElementDamage(const Player* player, const Creature* target, const Item* item) const +{ + if (elementType == COMBAT_NONE) { + return 0; + } + + int32_t attackValue = elementDamage; + if (item->getWeaponType() == WEAPON_AMMO) { + Item* weapon = player->getWeapon(true); + if (weapon) { + attackValue += weapon->getAttack(); + } + } + + int32_t attackSkill = player->getSkillLevel(SKILL_DISTANCE); + float attackFactor = player->getAttackFactor(); + + int32_t minValue = 0; + int32_t maxValue = Weapons::getMaxWeaponDamage(player->getLevel(), attackSkill, attackValue, attackFactor); + if (target) { + if (target->getPlayer()) { + minValue = static_cast(std::ceil(player->getLevel() * 0.1)); + } else { + minValue = static_cast(std::ceil(player->getLevel() * 0.2)); + } + } + + return -normal_random(minValue, static_cast(maxValue * player->getVocation()->distDamageMultiplier)); +} + +int32_t WeaponDistance::getWeaponDamage(const Player* player, const Creature* target, const Item* item, bool maxDamage /*= false*/) const +{ + int32_t attackValue = item->getAttack(); + + if (item->getWeaponType() == WEAPON_AMMO) { + Item* weapon = player->getWeapon(true); + if (weapon) { + attackValue += weapon->getAttack(); + } + } + + int32_t attackSkill = player->getSkillLevel(SKILL_DISTANCE); + float attackFactor = player->getAttackFactor(); + + int32_t maxValue = static_cast(Weapons::getMaxWeaponDamage(player->getLevel(), attackSkill, attackValue, attackFactor) * player->getVocation()->distDamageMultiplier); + if (maxDamage) { + return -maxValue; + } + + int32_t minValue; + if (target) { + if (target->getPlayer()) { + minValue = static_cast(std::ceil(player->getLevel() * 0.1)); + } else { + minValue = static_cast(std::ceil(player->getLevel() * 0.2)); + } + } else { + minValue = 0; + } + return -normal_random(minValue, maxValue); +} + +bool WeaponDistance::getSkillType(const Player* player, const Item*, skills_t& skill, uint32_t& skillpoint) const +{ + skill = SKILL_DISTANCE; + + if (player->getAddAttackSkill()) { + switch (player->getLastAttackBlockType()) { + case BLOCK_NONE: { + skillpoint = 2; + break; + } + + case BLOCK_DEFENSE: + case BLOCK_ARMOR: { + skillpoint = 1; + break; + } + + default: + skillpoint = 0; + break; + } + } else { + skillpoint = 0; + } + return true; +} + +bool WeaponWand::configureEvent(const pugi::xml_node& node) +{ + if (!Weapon::configureEvent(node)) { + return false; + } + + pugi::xml_attribute attr; + if ((attr = node.attribute("min"))) { + minChange = pugi::cast(attr.value()); + } + + if ((attr = node.attribute("max"))) { + maxChange = pugi::cast(attr.value()); + } + + attr = node.attribute("type"); + if (!attr) { + return true; + } + + std::string tmpStrValue = asLowerCaseString(attr.as_string()); + if (tmpStrValue == "earth") { + params.combatType = COMBAT_EARTHDAMAGE; + } else if (tmpStrValue == "ice") { + params.combatType = COMBAT_ICEDAMAGE; + } else if (tmpStrValue == "energy") { + params.combatType = COMBAT_ENERGYDAMAGE; + } else if (tmpStrValue == "fire") { + params.combatType = COMBAT_FIREDAMAGE; + } else if (tmpStrValue == "death") { + params.combatType = COMBAT_DEATHDAMAGE; + } else if (tmpStrValue == "holy") { + params.combatType = COMBAT_HOLYDAMAGE; + } else { + std::cout << "[Warning - WeaponWand::configureEvent] Type \"" << attr.as_string() << "\" does not exist." << std::endl; + } + return true; +} + +void WeaponWand::configureWeapon(const ItemType& it) +{ + params.distanceEffect = it.shootType; + + Weapon::configureWeapon(it); +} + +int32_t WeaponWand::getWeaponDamage(const Player*, const Creature*, const Item*, bool maxDamage /*= false*/) const +{ + if (maxDamage) { + return -maxChange; + } + return -normal_random(minChange, maxChange); +} diff --git a/src/weapons.h b/src/weapons.h new file mode 100644 index 0000000..e889784 --- /dev/null +++ b/src/weapons.h @@ -0,0 +1,311 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_WEAPONS_H_69D1993478AA42948E24C0B90B8F5BF5 +#define FS_WEAPONS_H_69D1993478AA42948E24C0B90B8F5BF5 + +#include "luascript.h" +#include "player.h" +#include "baseevents.h" +#include "combat.h" +#include "const.h" +#include "vocation.h" + +extern Vocations g_vocations; + +class Weapon; +class WeaponMelee; +class WeaponDistance; +class WeaponWand; + +using Weapon_ptr = std::unique_ptr; + +class Weapons final : public BaseEvents +{ + public: + Weapons(); + ~Weapons(); + + // non-copyable + Weapons(const Weapons&) = delete; + Weapons& operator=(const Weapons&) = delete; + + void loadDefaults(); + const Weapon* getWeapon(const Item* item) const; + + static int32_t getMaxMeleeDamage(int32_t attackSkill, int32_t attackValue); + static int32_t getMaxWeaponDamage(uint32_t level, int32_t attackSkill, int32_t attackValue, float attackFactor); + + bool registerLuaEvent(Weapon* event); + void clear(bool fromLua) override final; + + private: + LuaScriptInterface& getScriptInterface() override; + std::string getScriptBaseName() const override; + Event_ptr getEvent(const std::string& nodeName) override; + bool registerEvent(Event_ptr event, const pugi::xml_node& node) override; + + std::map weapons; + + LuaScriptInterface scriptInterface { "Weapon Interface" }; +}; + +class Weapon : public Event +{ + public: + explicit Weapon(LuaScriptInterface* interface) : Event(interface) {} + + bool configureEvent(const pugi::xml_node& node) override; + bool loadFunction(const pugi::xml_attribute&, bool) final { + return true; + } + virtual void configureWeapon(const ItemType& it); + virtual bool interruptSwing() const { + return false; + } + + int32_t playerWeaponCheck(Player* player, Creature* target, uint8_t shootRange) const; + static bool useFist(Player* player, Creature* target); + virtual bool useWeapon(Player* player, Item* item, Creature* target) const; + + virtual int32_t getWeaponDamage(const Player* player, const Creature* target, const Item* item, bool maxDamage = false) const = 0; + virtual int32_t getElementDamage(const Player* player, const Creature* target, const Item* item) const = 0; + virtual CombatType_t getElementType() const = 0; + + uint16_t getID() const { + return id; + } + void setID(uint16_t newId) { + id = newId; + } + + uint32_t getReqLevel() const { + return level; + } + void setRequiredLevel(uint32_t reqlvl) { + level = reqlvl; + } + + uint32_t getReqMagLv() const { + return magLevel; + } + void setRequiredMagLevel(uint32_t reqlvl) { + magLevel = reqlvl; + } + + bool isPremium() const { + return premium; + } + void setNeedPremium(bool prem) { + premium = prem; + } + + bool isWieldedUnproperly() const { + return wieldUnproperly; + } + void setWieldUnproperly(bool unproperly) { + wieldUnproperly = unproperly; + } + + uint32_t getMana() const { + return mana; + } + void setMana(uint32_t m) { + mana = m; + } + + uint32_t getManaPercent() const { + return manaPercent; + } + void setManaPercent(uint32_t m) { + manaPercent = m; + } + + int32_t getHealth() const { + return health; + } + void setHealth(int32_t h) { + health = h; + } + + uint32_t getHealthPercent() const { + return healthPercent; + } + void setHealthPercent(uint32_t m) { + healthPercent = m; + } + + uint32_t getSoul() const { + return soul; + } + void setSoul(uint32_t s) { + soul = s; + } + + uint8_t getBreakChance() const { + return breakChance; + } + void setBreakChance(uint8_t b) { + breakChance = b; + } + + bool isEnabled() const { + return enabled; + } + void setIsEnabled(bool e) { + enabled = e; + } + + uint32_t getWieldInfo() const { + return wieldInfo; + } + void setWieldInfo(uint32_t info) { + wieldInfo |= info; + } + + void addVocWeaponMap(std::string vocName) { + int32_t vocationId = g_vocations.getVocationId(vocName); + if (vocationId != -1) { + vocWeaponMap[vocationId] = true; + } + } + + const std::string& getVocationString() const { + return vocationString; + } + void setVocationString(const std::string& str) { + vocationString = str; + } + + WeaponAction_t action = WEAPONACTION_NONE; + CombatParams params; + WeaponType_t weaponType; + std::map vocWeaponMap; + + protected: + void internalUseWeapon(Player* player, Item* item, Creature* target, int32_t damageModifier) const; + void internalUseWeapon(Player* player, Item* item, Tile* tile) const; + + uint16_t id = 0; + + private: + virtual bool getSkillType(const Player*, const Item*, skills_t&, uint32_t&) const { + return false; + } + + uint32_t getManaCost(const Player* player) const; + int32_t getHealthCost(const Player* player) const; + + uint32_t level = 0; + uint32_t magLevel = 0; + uint32_t mana = 0; + uint32_t manaPercent = 0; + uint32_t health = 0; + uint32_t healthPercent = 0; + uint32_t soul = 0; + uint32_t wieldInfo = WIELDINFO_NONE; + uint8_t breakChance = 0; + bool enabled = true; + bool premium = false; + bool wieldUnproperly = false; + std::string vocationString = ""; + + std::string getScriptEventName() const override final; + + bool executeUseWeapon(Player* player, const LuaVariant& var) const; + void onUsedWeapon(Player* player, Item* item, Tile* destTile) const; + + static void decrementItemCount(Item* item); + + friend class Combat; +}; + +class WeaponMelee final : public Weapon +{ + public: + explicit WeaponMelee(LuaScriptInterface* interface); + + void configureWeapon(const ItemType& it) override; + + bool useWeapon(Player* player, Item* item, Creature* target) const override; + + int32_t getWeaponDamage(const Player* player, const Creature* target, const Item* item, bool maxDamage = false) const override; + int32_t getElementDamage(const Player* player, const Creature* target, const Item* item) const override; + CombatType_t getElementType() const override { return elementType; } + + private: + bool getSkillType(const Player* player, const Item* item, skills_t& skill, uint32_t& skillpoint) const override; + + CombatType_t elementType = COMBAT_NONE; + uint16_t elementDamage = 0; +}; + +class WeaponDistance final : public Weapon +{ + public: + explicit WeaponDistance(LuaScriptInterface* interface); + + void configureWeapon(const ItemType& it) override; + bool interruptSwing() const override { + return true; + } + + bool useWeapon(Player* player, Item* item, Creature* target) const override; + + int32_t getWeaponDamage(const Player* player, const Creature* target, const Item* item, bool maxDamage = false) const override; + int32_t getElementDamage(const Player* player, const Creature* target, const Item* item) const override; + CombatType_t getElementType() const override { return elementType; } + + private: + bool getSkillType(const Player* player, const Item* item, skills_t& skill, uint32_t& skillpoint) const override; + + CombatType_t elementType = COMBAT_NONE; + uint16_t elementDamage = 0; +}; + +class WeaponWand final : public Weapon +{ + public: + explicit WeaponWand(LuaScriptInterface* interface) : Weapon(interface) {} + + bool configureEvent(const pugi::xml_node& node) override; + void configureWeapon(const ItemType& it) override; + + int32_t getWeaponDamage(const Player* player, const Creature* target, const Item* item, bool maxDamage = false) const override; + int32_t getElementDamage(const Player*, const Creature*, const Item*) const override { return 0; } + CombatType_t getElementType() const override { return COMBAT_NONE; } + + void setMinChange(int32_t change) { + minChange = change; + } + + void setMaxChange(int32_t change) { + maxChange = change; + } + + private: + bool getSkillType(const Player*, const Item*, skills_t&, uint32_t&) const override { + return false; + } + + int32_t minChange = 0; + int32_t maxChange = 0; +}; + +#endif diff --git a/src/wildcardtree.cpp b/src/wildcardtree.cpp new file mode 100644 index 0000000..ec182b6 --- /dev/null +++ b/src/wildcardtree.cpp @@ -0,0 +1,129 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include + +#include "wildcardtree.h" + +WildcardTreeNode* WildcardTreeNode::getChild(char ch) +{ + auto it = children.find(ch); + if (it == children.end()) { + return nullptr; + } + return &it->second; +} + +const WildcardTreeNode* WildcardTreeNode::getChild(char ch) const +{ + auto it = children.find(ch); + if (it == children.end()) { + return nullptr; + } + return &it->second; +} + +WildcardTreeNode* WildcardTreeNode::addChild(char ch, bool breakpoint) +{ + WildcardTreeNode* child = getChild(ch); + if (child) { + if (breakpoint && !child->breakpoint) { + child->breakpoint = true; + } + } else { + auto pair = children.emplace(std::piecewise_construct, + std::forward_as_tuple(ch), std::forward_as_tuple(breakpoint)); + child = &pair.first->second; + } + return child; +} + +void WildcardTreeNode::insert(const std::string& str) +{ + WildcardTreeNode* cur = this; + + size_t length = str.length() - 1; + for (size_t pos = 0; pos < length; ++pos) { + cur = cur->addChild(str[pos], false); + } + + cur->addChild(str[length], true); +} + +void WildcardTreeNode::remove(const std::string& str) +{ + WildcardTreeNode* cur = this; + + std::stack path; + path.push(cur); + size_t len = str.length(); + for (size_t pos = 0; pos < len; ++pos) { + cur = cur->getChild(str[pos]); + if (!cur) { + return; + } + path.push(cur); + } + + cur->breakpoint = false; + + do { + cur = path.top(); + path.pop(); + + if (!cur->children.empty() || cur->breakpoint || path.empty()) { + break; + } + + cur = path.top(); + + auto it = cur->children.find(str[--len]); + if (it != cur->children.end()) { + cur->children.erase(it); + } + } while (true); +} + +ReturnValue WildcardTreeNode::findOne(const std::string& query, std::string& result) const +{ + const WildcardTreeNode* cur = this; + for (char pos : query) { + cur = cur->getChild(pos); + if (!cur) { + return RETURNVALUE_PLAYERWITHTHISNAMEISNOTONLINE; + } + } + + result = query; + + do { + size_t size = cur->children.size(); + if (size == 0) { + return RETURNVALUE_NOERROR; + } else if (size > 1 || cur->breakpoint) { + return RETURNVALUE_NAMEISTOOAMBIGUOUS; + } + + auto it = cur->children.begin(); + result += it->first; + cur = &it->second; + } while (true); +} diff --git a/src/wildcardtree.h b/src/wildcardtree.h new file mode 100644 index 0000000..38aa7ed --- /dev/null +++ b/src/wildcardtree.h @@ -0,0 +1,49 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef FS_WILDCARDTREE_H_054C9BA46A1D4EA4B7C77ECE60ED4DEB +#define FS_WILDCARDTREE_H_054C9BA46A1D4EA4B7C77ECE60ED4DEB + +#include "enums.h" + +class WildcardTreeNode +{ + public: + explicit WildcardTreeNode(bool breakpoint) : breakpoint(breakpoint) {} + WildcardTreeNode(WildcardTreeNode&& other) = default; + + // non-copyable + WildcardTreeNode(const WildcardTreeNode&) = delete; + WildcardTreeNode& operator=(const WildcardTreeNode&) = delete; + + WildcardTreeNode* getChild(char ch); + const WildcardTreeNode* getChild(char ch) const; + WildcardTreeNode* addChild(char ch, bool breakpoint); + + void insert(const std::string& str); + void remove(const std::string& str); + + ReturnValue findOne(const std::string& query, std::string& result) const; + + private: + std::map children; + bool breakpoint; +}; + +#endif diff --git a/src/xtea.cpp b/src/xtea.cpp new file mode 100644 index 0000000..30f2e1f --- /dev/null +++ b/src/xtea.cpp @@ -0,0 +1,75 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2020 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "otpch.h" + +#include "xtea.h" + +#include +#include + +namespace xtea { + +namespace { + +constexpr uint32_t delta = 0x9E3779B9; + +template +void apply_rounds(uint8_t* data, size_t length, Round round) +{ + for (auto j = 0u; j < length; j += 8) { + uint32_t left = data[j+0] | data[j+1] << 8u | data[j+2] << 16u | data[j+3] << 24u, + right = data[j+4] | data[j+5] << 8u | data[j+6] << 16u | data[j+7] << 24u; + + round(left, right); + + data[j] = static_cast(left); + data[j+1] = static_cast(left >> 8u); + data[j+2] = static_cast(left >> 16u); + data[j+3] = static_cast(left >> 24u); + data[j+4] = static_cast(right); + data[j+5] = static_cast(right >> 8u); + data[j+6] = static_cast(right >> 16u); + data[j+7] = static_cast(right >> 24u); + } +} + +} + +void encrypt(uint8_t* data, size_t length, const key& k) +{ + for (uint32_t i = 0, sum = 0, next_sum = sum + delta; i < 32; ++i, sum = next_sum, next_sum += delta) { + apply_rounds(data, length, [&](uint32_t& left, uint32_t& right) { + left += ((right << 4 ^ right >> 5) + right) ^ (sum + k[sum & 3]); + right += ((left << 4 ^ left >> 5) + left) ^ (next_sum + k[(next_sum >> 11) & 3]); + }); + }; +} + +void decrypt(uint8_t* data, size_t length, const key& k) +{ + for (uint32_t i = 0, sum = delta << 5, next_sum = sum - delta; i < 32; ++i, sum = next_sum, next_sum -= delta) { + apply_rounds(data, length, [&](uint32_t& left, uint32_t& right) { + right -= ((left << 4 ^ left >> 5) + left) ^ (sum + k[(sum >> 11) & 3]); + left -= ((right << 4 ^ right >> 5) + right) ^ (next_sum + k[next_sum & 3]); + }); + }; +} + +} // namespace xtea diff --git a/src/xtea.h b/src/xtea.h new file mode 100644 index 0000000..6f8f555 --- /dev/null +++ b/src/xtea.h @@ -0,0 +1,32 @@ +/** + * The Forgotten Server - a free and open-source MMORPG server emulator + * Copyright (C) 2019 Mark Samman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef TFS_XTEA_H +#define TFS_XTEA_H + +namespace xtea { + +using key = std::array; + +void encrypt(uint8_t* data, size_t length, const key& k); +void decrypt(uint8_t* data, size_t length, const key& k); + +} // namespace xtea + +#endif // TFS_XTEA_H diff --git a/vc14/arch32.props b/vc14/arch32.props new file mode 100644 index 0000000..9ef4cdc --- /dev/null +++ b/vc14/arch32.props @@ -0,0 +1,12 @@ + + + + + + + + true + + + + \ No newline at end of file diff --git a/vc14/arch64.props b/vc14/arch64.props new file mode 100644 index 0000000..beef72d --- /dev/null +++ b/vc14/arch64.props @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vc14/debug.props b/vc14/debug.props new file mode 100644 index 0000000..5288791 --- /dev/null +++ b/vc14/debug.props @@ -0,0 +1,19 @@ + + + + + + true + + + + false + false + EnableFastChecks + MultiThreadedDebugDLL + $(IntDir)\obj_d\ + + + + + diff --git a/vc14/release.props b/vc14/release.props new file mode 100644 index 0000000..1a6520d --- /dev/null +++ b/vc14/release.props @@ -0,0 +1,19 @@ + + + + + + false + + + + Full + $(IntDir)\obj_r\ + + + UseLinkTimeCodeGeneration + + + + + diff --git a/vc14/settings.props b/vc14/settings.props new file mode 100644 index 0000000..e6862ee --- /dev/null +++ b/vc14/settings.props @@ -0,0 +1,32 @@ + + + + + _CRT_SECURE_NO_WARNINGS; + + + false + + + + Level3 + true + true + Use + otpch.h + MultiThreadedDLL + + + Default + + + $(PREPROCESSOR_DEFS) + + + + + $(PREPROCESSOR_DEFS) + true + + + diff --git a/vc14/theforgottenserver.sln b/vc14/theforgottenserver.sln new file mode 100644 index 0000000..b51d0f9 --- /dev/null +++ b/vc14/theforgottenserver.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2035 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "theforgottenserver", "theforgottenserver.vcxproj", "{A10F9657-129F-0FEF-14CB-CEE0B0E5AA3E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A10F9657-129F-0FEF-14CB-CEE0B0E5AA3E}.Debug|Win32.ActiveCfg = Debug|Win32 + {A10F9657-129F-0FEF-14CB-CEE0B0E5AA3E}.Debug|Win32.Build.0 = Debug|Win32 + {A10F9657-129F-0FEF-14CB-CEE0B0E5AA3E}.Debug|x64.ActiveCfg = Debug|x64 + {A10F9657-129F-0FEF-14CB-CEE0B0E5AA3E}.Debug|x64.Build.0 = Debug|x64 + {A10F9657-129F-0FEF-14CB-CEE0B0E5AA3E}.Release|Win32.ActiveCfg = Release|Win32 + {A10F9657-129F-0FEF-14CB-CEE0B0E5AA3E}.Release|Win32.Build.0 = Release|Win32 + {A10F9657-129F-0FEF-14CB-CEE0B0E5AA3E}.Release|x64.ActiveCfg = Release|x64 + {A10F9657-129F-0FEF-14CB-CEE0B0E5AA3E}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {81EB239E-6AEB-4015-9915-256C2C90D3FD} + EndGlobalSection +EndGlobal diff --git a/vc14/theforgottenserver.vcxproj b/vc14/theforgottenserver.vcxproj new file mode 100644 index 0000000..9152b6a --- /dev/null +++ b/vc14/theforgottenserver.vcxproj @@ -0,0 +1,336 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + Win32Proj + {A10F9657-129F-0FEF-14CB-CEE0B0E5AA3E} + 10.0.18362.0 + + + + Application + true + v142 + + + Application + true + v142 + + + Application + false + v142 + + + Application + false + v141 + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(ProjectName)-$(Platform) + $(VC_IncludePath);$(WindowsSDK_IncludePath); + + + $(ProjectName)-$(Platform) + $(VC_IncludePath);$(WindowsSDK_IncludePath); + + + $(ProjectName)-$(Platform) + $(VC_IncludePath);$(WindowsSDK_IncludePath); + + + $(ProjectName)-$(Platform) + $(VC_IncludePath);$(WindowsSDK_IncludePath); + + + + _CONSOLE;$(PREPROCESSOR_DEFS);%(PreprocessorDefinitions) + ProgramDatabase + Disabled + true + AdvancedVectorExtensions + $(VcpkgRoot)include\luajit;%(AdditionalIncludeDirectories) + + + MachineX86 + true + Console + + + + + _CONSOLE;$(PREPROCESSOR_DEFS);%(PreprocessorDefinitions) + ProgramDatabase + true + AdvancedVectorExtensions + $(VcpkgRoot)include\luajit;%(AdditionalIncludeDirectories) + + + true + Console + + + + + NDEBUG;_CONSOLE;$(PREPROCESSOR_DEFS);%(PreprocessorDefinitions) + MultiThreadedDLL + ProgramDatabase + MaxSpeed + true + true + true + AdvancedVectorExtensions + $(VcpkgRoot)include\luajit;%(AdditionalIncludeDirectories) + + + MachineX86 + true + Console + true + true + + + + + NDEBUG;_CONSOLE;$(PREPROCESSOR_DEFS);%(PreprocessorDefinitions) + MultiThreadedDLL + ProgramDatabase + Level4 + MaxSpeed + true + true + true + AdvancedVectorExtensions + $(VcpkgRoot)include\luajit;%(AdditionalIncludeDirectories) + + + true + Console + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + otpch.h + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file