diff --git a/.clang-format b/.clang-format index f1b2f74d0..b77507cbd 100644 --- a/.clang-format +++ b/.clang-format @@ -1,21 +1,23 @@ # Generated from CLion C/C++ Code Style settings BasedOnStyle: LLVM AccessModifierOffset: -4 -AlignAfterOpenBracket: Align +AlignAfterOpenBracket: BlockIndent AlignConsecutiveAssignments: None AlignEscapedNewlines: Left AlignOperands: Align -AllowAllArgumentsOnNextLine: false -AllowAllConstructorInitializersOnNextLine: false -AllowAllParametersOfDeclarationOnNextLine: false +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: Always -AllowShortCaseLabelsOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: true AllowShortFunctionsOnASingleLine: All -AllowShortIfStatementsOnASingleLine: Always +AllowShortIfStatementsOnASingleLine: AllIfsAndElse AllowShortLambdasOnASingleLine: Empty AllowShortLoopsOnASingleLine: true AlwaysBreakAfterReturnType: None -AlwaysBreakTemplateDeclarations: Yes +AlwaysBreakTemplateDeclarations: Leave +BinPackArguments: true +BinPackParameters: true BreakBeforeBraces: Custom BraceWrapping: AfterCaseLabel: true @@ -35,23 +37,24 @@ BraceWrapping: SplitEmptyFunction: false SplitEmptyRecord: true SplitEmptyNamespace: true +BracedInitializerIndentWidth: 4 BreakBeforeBinaryOperators: None BreakBeforeTernaryOperators: true BreakConstructorInitializers: BeforeColon BreakInheritanceList: BeforeColon -Cpp11BracedListStyle: false +Cpp11BracedListStyle: true ColumnLimit: 0 CompactNamespaces: false -ContinuationIndentWidth: 4 +ContinuationIndentWidth: 0 IndentAccessModifiers: false IndentCaseLabels: true IndentPPDirectives: BeforeHash IndentWidth: 4 InsertNewlineAtEOF: true KeepEmptyLinesAtTheStartOfBlocks: true -LambdaBodyIndentation: Signature +LambdaBodyIndentation: OuterScope MaxEmptyLinesToKeep: 1 -NamespaceIndentation: All +NamespaceIndentation: None ObjCSpaceAfterProperty: false ObjCSpaceBeforeProtocolList: true PointerAlignment: Left @@ -64,7 +67,7 @@ SpaceBeforeCpp11BracedList: false SpaceBeforeCtorInitializerColon: true SpaceBeforeInheritanceColon: true SpaceBeforeParens: ControlStatements -SpaceBeforeRangeBasedForLoopColon: false +SpaceBeforeRangeBasedForLoopColon: true SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false diff --git a/.dockerignore b/.dockerignore index a99bb6a56..a520b11ab 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,9 @@ dependencies/v8 .git -build*/ -cmake-*/ +build +cmake-build-*/ +vcpkg/ +docker/ +dependencies/fc/ *.tar +*.diff diff --git a/.editorconfig b/.editorconfig index 3b464f4a6..4b329d914 100644 --- a/.editorconfig +++ b/.editorconfig @@ -448,7 +448,7 @@ ij_c_indent_implementation_members = 0 ij_c_indent_inside_code_block = 4 ij_c_indent_interface_members = 0 ij_c_indent_interface_members_except_ivars_block = false -ij_c_indent_namespace_members = 4 +ij_c_indent_namespace_members = 0 ij_c_indent_preprocessor_directive = 0 ij_c_indent_visibility_keywords = 4 ij_c_insert_override = true @@ -979,34 +979,37 @@ ij_toml_keep_indents_on_empty_lines = false # Visual C++ Code Style settings -cpp_generate_documentation_comments = xml +cpp_generate_documentation_comments = doxygen_triple_slash # Visual C++ Formatting settings +indent_style = tab +indent_size = 4 +tab_width= 4 cpp_indent_braces = false -cpp_indent_multi_line_relative_to = innermost_parenthesis +cpp_indent_multi_line_relative_to = outermost_parenthesis cpp_indent_within_parentheses = indent -cpp_indent_preserve_within_parentheses = true +cpp_indent_preserve_within_parentheses = false cpp_indent_case_contents = true -cpp_indent_case_labels = false +cpp_indent_case_labels = true cpp_indent_case_contents_when_block = false -cpp_indent_lambda_braces_when_parameter = true +cpp_indent_lambda_braces_when_parameter = false cpp_indent_goto_labels = one_left cpp_indent_preprocessor = leftmost_column cpp_indent_access_specifiers = false -cpp_indent_namespace_contents = true +cpp_indent_namespace_contents = false cpp_indent_preserve_comments = false -cpp_new_line_before_open_brace_namespace = ignore -cpp_new_line_before_open_brace_type = ignore -cpp_new_line_before_open_brace_function = ignore -cpp_new_line_before_open_brace_block = ignore -cpp_new_line_before_open_brace_lambda = ignore -cpp_new_line_scope_braces_on_separate_lines = false +cpp_new_line_before_open_brace_namespace = new_line +cpp_new_line_before_open_brace_type = new_line +cpp_new_line_before_open_brace_function = new_line +cpp_new_line_before_open_brace_block = new_line +cpp_new_line_before_open_brace_lambda = new_line +cpp_new_line_scope_braces_on_separate_lines = true cpp_new_line_close_brace_same_line_empty_type = false cpp_new_line_close_brace_same_line_empty_function = false cpp_new_line_before_catch = true cpp_new_line_before_else = true -cpp_new_line_before_while_in_do_while = false +cpp_new_line_before_while_in_do_while = true cpp_space_before_function_open_parenthesis = remove cpp_space_within_parameter_list_parentheses = false cpp_space_between_empty_parameter_list_parentheses = false @@ -1043,10 +1046,13 @@ cpp_space_around_ternary_operator = insert cpp_use_unreal_engine_macro_formatting = true cpp_wrap_preserve_blocks = one_liners -# Visual C++ Inlcude Cleanup settings +# Visual C++ Include Cleanup settings +cpp_include_cleanup_alternate_files = utilities/std/generator.h:generator,generator.h:generator cpp_include_cleanup_add_missing_error_tag_type = suggestion +cpp_include_cleanup_excluded_files = corecrt.h,Windows.h,tomcrypt.h cpp_include_cleanup_remove_unused_error_tag_type = dimmed +cpp_include_cleanup_replacement_files = generator:utilities/std/generator.h cpp_include_cleanup_optimize_unused_error_tag_type = suggestion cpp_include_cleanup_sort_after_edits = false cpp_sort_includes_error_tag_type = none diff --git a/.gitattributes b/.gitattributes index f2d9aac3e..87c4091bc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,6 @@ -* test eol=lf -bin/servers/* binary +* text=auto +*.cpp text +*.h text +*.exe binary +bin/servers/** binary +.gitignore text diff --git a/.gitignore b/.gitignore index 2d27dda9c..205d6aaea 100644 --- a/.gitignore +++ b/.gitignore @@ -10,10 +10,12 @@ bin/*.bin bin/*.dat bin/*.dll bin/*.exe +bin/*.idb +bin/*.ilk +bin/*.obj bin/*.pdb -bin/*.zip bin/*.txt -bin/*.ilk +bin/*.zip bin/GS2Emu.* bin/gs2emu* bin/servers/* @@ -24,7 +26,7 @@ build*/* cmake-build-*/ dependencies/.cipd/ dependencies/.gclient* -dependencies/v8* +dependencies/fc/ lib/ out/ packages/ diff --git a/.gitmodules b/.gitmodules index 923c7324b..e69de29bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,18 +0,0 @@ -[submodule "dependencies/miniupnp"] - path = dependencies/miniupnp - url = https://github.com/miniupnp/miniupnp.git -[submodule "dependencies/depot_tools"] - path = dependencies/depot_tools - url = https://chromium.googlesource.com/chromium/tools/depot_tools.git -[submodule "dependencies/gs2lib"] - path = dependencies/gs2lib - url = https://xtjoeytx@bitbucket.org/xtjoeytx/gs2lib.git -[submodule "dependencies/gs2compiler"] - path = dependencies/gs2compiler - url = https://github.com/xtjoeytx/gs2-parser.git -[submodule "cmake/nuget"] - path = cmake/nuget - url = https://github.com/katusk/CMakeNuGetTools -[submodule "vcpkg"] - path = vcpkg - url = https://github.com/microsoft/vcpkg.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 12c57031e..99927daff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,89 +19,122 @@ # along with GS2Emu. If not, see . # -cmake_minimum_required(VERSION 3.0.0) - -# Make it easier for Windows users to get the required packages. -# This needs to go before the project() call. -if(WIN32) - set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake" CACHE STRING "Vcpkg toolchain file") - set(VCPKG_TARGET_TRIPLET "x64-windows-static") - if(V8NPCSERVER) - set(VCPKG_MANIFEST_FEATURES "npcserver") - endif() +cmake_minimum_required(VERSION 3.28) + +# Add custom CMake modules path for external dependencies. +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_SOURCE_DIR}/dependencies") + +# Includes. +include(FetchContent) +include(utility) + +# Set build type. +set(default_build_type "RelWithDebInfo") +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "Setting build type to '${default_build_type}' as none was specified.") + set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE + STRING "Choose the type of build." FORCE) + + # Set the possible values of build type for cmake-gui + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() -project(GS2Emu VERSION 3.0.9 DESCRIPTION "Graal Online v1.411 to v6.037 compatible server" LANGUAGES C CXX) - -set(CMAKE_DEBUG_POSTFIX _d) - -set(BIN_DIR "bin" CACHE STRING "Binary output directory") - -# specify the C++ standard -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED True) - -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/${BIN_DIR}) -set(CPACK_PACKAGE_DIRECTORY ${PROJECT_SOURCE_DIR}/dist) +# Enable Hot Reload for MSVC compilers if supported. +if(POLICY CMP0141) + cmake_policy(SET CMP0141 NEW) # CMP0141: MSVC debug information format. + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$,$>,$<$:EditAndContinue>$<$:ProgramDatabase>,$<$:ProgramDatabase>>" + CACHE STRING "MSVC debug format." FORCE) +endif() +# Policies. +cmake_policy(SET CMP0054 NEW) # CMP0054: Only interpret if() arguments as variables or keywords when unquoted. +cmake_policy(SET CMP0074 NEW) # CMP0074: find_package uses PackageName_ROOT variables. +cmake_policy(SET CMP0077 NEW) # CMP0077: option() honors normal variables. +cmake_policy(SET CMP0091 NEW) # CMP0091: MSVC runtime library flags are selected by an abstraction. +cmake_policy(SET CMP0095 NEW) # CMP0095: RPATH entries are properly escaped in the intermediary CMake install script. +cmake_policy(SET CMP0144 NEW) # CMP0144: Also search _ROOT as well as _ROOT. +if(POLICY CMP0177) + cmake_policy(SET CMP0177 NEW) # CMP0177: install() DESTINATION paths are normalized. +endif() +if(POLICY CMP0156) + cmake_policy(SET CMP0156 NEW) # CMP0156: De-duplicate libraries on link lines based on linker capabilities +endif() +if(POLICY CMP0179) + cmake_policy(SET CMP0179 NEW) # CMP0179: De-duplication of static libraries on link lines keeps first occurrence +endif() -# Second, for multi-config builds (e.g. msvc) -foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} ) - string( TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG ) - set( CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${PROJECT_SOURCE_DIR}/lib ) - set( CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${PROJECT_SOURCE_DIR}/lib ) - set( CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${PROJECT_SOURCE_DIR}/${BIN_DIR} ) -endforeach( OUTPUTCONFIG CMAKE_CONFIGURATION_TYPES ) +# Get submodules. +find_package(Git QUIET) +if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") + # Update submodules as needed + option(GIT_SUBMODULE "Check submodules during build" ON) + if (GIT_SUBMODULE) + message(STATUS "Submodule update") + execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE GIT_SUBMOD_RESULT) + if (NOT GIT_SUBMOD_RESULT EQUAL "0") + message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}, please checkout submodules") + endif() + endif() +endif() -link_directories(${PROJECT_SOURCE_DIR}/lib) +# vcpkg configuration. +set(VCPKG_TARGET_TRIPLET "x64-windows-static" CACHE STRING "vcpkg triplet") +set(VCPKG_INSTALLED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/build/vcpkg_installed") +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(CMAKE_EXECUTABLE_SUFFIX ".exe") +endif() -# Adhere to GNU filesystem layout conventions -include(GNUInstallDirs) +# GS2 project. +project(GS2Emu VERSION 4.0.0 DESCRIPTION "Graal Online v1.380 to v6.037 compatible server" LANGUAGES C CXX) -# Lowercase project name for binaries and packaging +# Lowercase project name for binaries and packaging. string(TOLOWER ${PROJECT_NAME} PROJECT_NAME_LOWER) -# Additional CMake modules -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/cmake) +# Adhere to GNU filesystem layout conventions. +include(GNUInstallDirs) -# Version number in format X.YY.ZZ -string(REPLACE "." ";" VERSION_LIST ${PROJECT_VERSION}) -list(GET VERSION_LIST 0 VER_X) -list(GET VERSION_LIST 1 VER_Y) -list(GET VERSION_LIST 2 VER_Z) -set(VER_EXTRA "-beta" CACHE STRING "Extra version") +# Debug build postfix. +set(CMAKE_DEBUG_POSTFIX _d) -# Build date Information -string(TIMESTAMP VER_YEAR "%Y") -string(TIMESTAMP VER_MONTH "%m") -string(TIMESTAMP VER_DAY "%d") -string(TIMESTAMP VER_HOUR "%H") -string(TIMESTAMP VER_MINUTE "%M") +# Directories. +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/build/lib") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/build/lib") +set(CPACK_PACKAGE_DIRECTORY ${PROJECT_SOURCE_DIR}/dist) -set(VER_EXTRA "${VER_EXTRA} (${VER_YEAR}-${VER_MONTH}-${VER_DAY} ${VER_HOUR}:${VER_MINUTE})") +# Dependency and FetchContent setup. +set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) +set(DEPENDENCY_DIRECTORY "${PROJECT_SOURCE_DIR}/dependencies") +set(FETCHCONTENT_BASE_DIR "${DEPENDENCY_DIRECTORY}/fc") +set(FETCHCONTENT_TRY_FIND_PACKAGE_MODE ALWAYS) +# System defines. +if(MINGW) + message(STATUS "Compiling with MinGW") + add_compile_definitions(__GXX_TYPEINFO_EQUALITY_INLINE=1) +endif() -set(VER_FULL "${VER_X}.${VER_Y}.${VER_Z}${VER_EXTRA}") +# Set up version data +setup_versioning_data() -set(APP_CREDITS "Joey, Nalin, Codr, and Cadavre") -set(APP_VENDOR "OpenGraal") +# Print version data +message(STATUS "[${PROJECT_NAME}] Configuring version ${VER_FULL}") -STRING(REGEX REPLACE " " "-" VER_CPACK ${VER_FULL}) -STRING(REGEX REPLACE "[\(]" "" VER_CPACK ${VER_CPACK}) -STRING(REGEX REPLACE "[\)]" "" VER_CPACK ${VER_CPACK}) -STRING(REGEX REPLACE "(-[0-9]+:[0-9]+)" "" VER_CPACK ${VER_CPACK}) +# Generate the IConfig.h file. +generate_iconfig() -# Generate version header from the above -configure_file( - ${PROJECT_SOURCE_DIR}/server/include/IConfig.h.in - ${PROJECT_BINARY_DIR}/server/include/IConfig.h -) +# Unit testing +option(TESTS "Compile tests" OFF) +# Static runtime. option(STATIC "Compile as a static runtime." ON) if(STATIC) - message("Compile as static runtime") + message(STATUS "[${PROJECT_NAME}] Compile as static runtime") add_definitions(-DSTATICLIB) set(BUILD_SHARED_LIBS FALSE CACHE BOOL "-" FORCE) else() @@ -109,53 +142,44 @@ else() set(BUILD_SHARED_LIBS TRUE CACHE BOOL "-" FORCE) endif() -option(GRALATNPC "Compile with Gralat NPC support, only works when V8NPCSERVER is enabled." ON) -if(GRALATNPC) - message("Compile with Gralat NPC support") - add_definitions(-DGRALATNPC) -else() - message("Don't compile with Gralat NPC support") -endif() +# Dependency: Threads. +if(NOT MSVC) + # Prefer static linkage. + if(STATIC) + set(SUFFIXES_ORIG ${CMAKE_FIND_LIBRARY_SUFFIXES}) + set(CMAKE_FIND_LIBRARY_SUFFIXES .a ${CMAKE_FIND_LIBRARY_SUFFIXES}) + endif() -option(V8NPCSERVER "Compile built-in V8 NPC-Server" OFF) -if(V8NPCSERVER) - message("Enabling built-in V8 NPC-Server") - add_definitions(-DV8NPCSERVER) -else() - message("Disabling built-in V8 NPC-Server") + find_package(Threads REQUIRED) + + # Restore library suffixes. + if(STATIC) + set(CMAKE_FIND_LIBRARY_SUFFIXES ${SUFFIXES_ORIG}) + endif() endif() -option(TESTS "Compile tests" ON) +# Server. +add_subdirectory(${PROJECT_SOURCE_DIR}/bin) +add_subdirectory(${PROJECT_SOURCE_DIR}/server) -option(UPNP "Compile with UPNP support" OFF) -if(UPNP) - message("Enabling UPNP support") - add_definitions(-DUPNP) -else() - message("Disabling UPNP support") -endif() +###-----------------------### -# Packaging -if(APPLE) - set(CPACK_GENERATOR DragNDrop) +# Packaging. +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(CPACK_GENERATOR "DragNDrop") set(CPACK_DMG_VOLUME_NAME "${PROJECT_NAME} ${VER_FULL}") - set( - CPACK_DMG_DS_STORE_SETUP_SCRIPT - ${PROJECT_SOURCE_DIR}/resources/packaging/osx/DMGSetup.scpt - ) - set( - CPACK_DMG_BACKGROUND_IMAGE - ${PROJECT_SOURCE_DIR}/resources/packaging/osx/DMGBackground.tif - ) -elseif(WIN32) + set(CPACK_DMG_DS_STORE_SETUP_SCRIPT ${PROJECT_SOURCE_DIR}/resources/packaging/osx/DMGSetup.scpt) + set(CPACK_DMG_BACKGROUND_IMAGE ${PROJECT_SOURCE_DIR}/resources/packaging/osx/DMGBackground.tif) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") set(CPACK_GENERATOR "ZIP") else() - set(CPACK_GENERATOR TGZ) + set(CPACK_GENERATOR "TGZ") endif() -set(CPACK_PACKAGE_NAME ${PROJECT_NAME_LOWER}) -set(CPACK_PACKAGE_VENDOR "OpenGraal Team") -set(CPACK_PACKAGE_CONTACT "opengraal") +# More packaging. +set(CPACK_PACKAGE_NAME "${PROJECT_NAME_LOWER}") +set(CPACK_PACKAGE_VENDOR "${APP_VENDOR} Team") +set(CPACK_PACKAGE_CONTACT "preagonal.net") set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${VER_CPACK}") set(CPACK_PACKAGE_VERSION_MAJOR ${VER_X}) set(CPACK_PACKAGE_VERSION_MINOR ${VER_Y}) @@ -167,166 +191,11 @@ set(CPACK_RESOURCE_FILE_LICENSE ${PROJECT_SOURCE_DIR}/LICENSE.md) set(CPACK_SOURCE_GENERATOR TGZ) set(CPACK_SOURCE_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${VER_CPACK}-src") set(CPACK_SOURCE_IGNORE_FILES "/build/;/.bzr/;~$;${CPACK_SOURCE_IGNORE_FILES}") -include(CPack) - -# Generate version header from the above -configure_file( - ${PROJECT_SOURCE_DIR}/server/include/IConfig.h.in - ${PROJECT_BINARY_DIR}/server/include/IConfig.h -) - -if(APPLE) - set(MAKE_TESTS OFF) - # Set variables for generating the Info.plist file - set(MACOSX_BUNDLE_BUNDLE_VERSION "${VER_FULL}") - set(MACOSX_BUNDLE_EXECUTABLE ${PROJECT_NAME}) - set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.OpenGraal.GS2Emu") - set(MACOSX_BUNDLE_NSMAIN_NIB_FILE "Application") - set(MACOSX_BUNDLE_ICON_FILE "carton") - set(MACOSX_BUNDLE_NAME ${PROJECT_NAME}) - set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${VER_FULL}") -elseif(WIN32) - set(MAKE_TESTS OFF) - # Visual C++ Compiler options - if(MSVC) - set(MAKE_TESTS ON) - - # Suppress secure string function warnings - add_definitions(-D_CRT_SECURE_NO_WARNINGS) - - # Supress fmtlib warnings - add_definitions(-D_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING) - - include(CheckCXXCompilerFlag) - CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) - if (_cpp_latest_flag_supported) - set(CMAKE_CXX_STANDARD 23) - #add_compile_options("/std:c++latest") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:CONSOLE") - endif() +set(CPACK_INSTALL_EXECUTABLES "${PROJECT_NAME_LOWER}${CMAKE_EXECUTABLE_SUFFIX}") - # Enable parallel compilation - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") - - # Enable preprocessor conformance mode. - string(APPEND CMAKE_CXX_FLAGS " /Zc:preprocessor") - - # Enable static linkage of the Microsoft Visual C/C++ Runtime - set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /MTd") - set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /MT") - set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} /MTd") - set(CMAKE_C_FLAGS_MINSIZEREL "${CMAKE_C_FLAGS_MINSIZEREL} /MT") - set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd") - set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") - set( - CMAKE_CXX_FLAGS_RELWITHDEBINFO - "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /MTd" - ) - set(CMAKE_CXX_FLAGS_MINSIZEREL "${CMAKE_CXX_FLAGS_MINSIZEREL} /MT") - cmake_policy(SET CMP0043 NEW) - - # hack to fix clion + msvc together - #if (CLIONHAX) - set(CompilerFlags - CMAKE_CXX_FLAGS - CMAKE_CXX_FLAGS_DEBUG - CMAKE_CXX_FLAGS_RELEASE - CMAKE_C_FLAGS - CMAKE_C_FLAGS_DEBUG - CMAKE_C_FLAGS_RELEASE - ) - foreach(CompilerFlag ${CompilerFlags}) - string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}") - endforeach() - #endif() - endif() - - if( MINGW ) - add_definitions(-D__STDC_FORMAT_MACROS) - add_definitions(-D__USE_MINGW_ANSI_STDIO=1 -D_BSD_SOURCE=1) - add_compile_options(-Wno-deprecated-declarations) - endif() - - # Prevent Windows.h from clashing with the Standard Template Library so we - # can use std::min/std::max (see https://support.microsoft.com/kb/143208) - add_definitions(-DNOMINMAX) -else() - set(MAKE_TESTS ON) - if(STATIC) - # SET(CMAKE_EXE_LINKER_FLAGS "-static") - endif() - - add_definitions(-D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=1 -DNDEBUG=1 -DENABLE_SCRIPTENV_DEBUG=1) - add_compile_options(-Wno-error -Wno-dev) -endif() - -# Prefer static linkage -if(STATIC) - set(SUFFIXES_ORIG ${CMAKE_FIND_LIBRARY_SUFFIXES}) - set(CMAKE_FIND_LIBRARY_SUFFIXES .a ${CMAKE_FIND_LIBRARY_SUFFIXES}) -endif() - -if(V8NPCSERVER) - if(NOT WIN32 AND NOT APPLE) - find_package(zstd) - endif() - find_package(V8 REQUIRED) - include(FetchContent) - set(HTTPLIB_USE_BROTLI_IF_AVAILABLE FALSE CACHE BOOL "-" FORCE) - FetchContent_Declare(httplib GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git - GIT_TAG a609330e4c6374f741d3b369269f7848255e1954) - FetchContent_MakeAvailable(httplib) - find_package(OpenSSL REQUIRED) - - - if(NOT V8_FOUND) - message("v8 not found in system") - endif() -endif() - -if(UPNP) - find_package(Miniupnpc) - - if(NOT MINIUPNPC_FOUND) - message("MiniUPNPC not found in system. Compiling it ourselves.") - - if(STATIC) - add_definitions(-DMINIUPNP_STATICLIB) - set(UPNPC_BUILD_STATIC TRUE CACHE BOOL "-" FORCE) - set(UPNPC_BUILD_SHARED FALSE CACHE BOOL "-" FORCE) - else() - set(UPNPC_BUILD_STATIC FALSE CACHE BOOL "-" FORCE) - set(UPNPC_BUILD_SHARED TRUE CACHE BOOL "-" FORCE) - endif() - - set(UPNPC_BUILD_TESTS FALSE CACHE BOOL "-" FORCE) - set(UPNPC_BUILD_SAMPLE FALSE CACHE BOOL "-" FORCE) - set(NO_GETADDRINFO FALSE CACHE BOOL "-" FORCE) - set(UPNPC_NO_INSTALL TRUE CACHE BOOL "-" FORCE) - - add_subdirectory(${PROJECT_SOURCE_DIR}/dependencies/miniupnp/miniupnpc EXCLUDE_FROM_ALL) - endif() -endif() - -if(NOT WIN32) - find_package(Threads REQUIRED) -endif() - -# Restore library suffixes -if(STATIC) - set(CMAKE_FIND_LIBRARY_SUFFIXES ${SUFFIXES_ORIG}) -endif() - -add_subdirectory(${PROJECT_SOURCE_DIR}/dependencies/gs2lib EXCLUDE_FROM_ALL) -add_subdirectory(${PROJECT_SOURCE_DIR}/dependencies/gs2compiler EXCLUDE_FROM_ALL) - -add_subdirectory(${PROJECT_SOURCE_DIR}/bin) -add_subdirectory(${PROJECT_SOURCE_DIR}/server) +include(CPack) if (TESTS) enable_testing() add_subdirectory(${PROJECT_SOURCE_DIR}/Catch_tests) endif() - -set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${PROJECT_NAME}) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..e4413203f --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,167 @@ +{ + "version": 5, + "configurePresets": [ + { + "name": "Release", + "hidden": true, + "binaryDir": "${sourceDir}/build", + "architecture": { + "value": "x64", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "MinSizeRel", + "CMAKE_TOOLCHAIN_FILE": { + "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "type": "FILEPATH" + }, + "CMAKE_SUPPRESS_DEVELOPER_WARNINGS": true, + "CMAKE_BUILD_WITH_INSTALL_RPATH": false, + "PACKET_LOGGING": false + } + }, + { + "name": "Debug", + "hidden": true, + "binaryDir": "${sourceDir}/build", + "architecture": { + "value": "x64", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_TOOLCHAIN_FILE": { + "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "type": "FILEPATH" + }, + "CMAKE_SUPPRESS_DEVELOPER_WARNINGS": true, + "CMAKE_BUILD_WITH_INSTALL_RPATH": false, + "CMAKE_VERBOSE_MAKEFILE": true, + "PACKET_LOGGING": false + } + }, + { + "name": "Release Debug Info", + "inherits": "Release", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } + }, + { + "name": "MSVC SegmentHeap", + "hidden": true, + "condition": { + "type": "allOf", + "conditions": [ + { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + { + "type": "notEquals", + "lhs": "$env{VSINSTALLDIR}", + "rhs": "" + } + ] + }, + "cacheVariables": { + "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "$env{VSINSTALLDIR}Common7/IDE/CommonExtensions/Microsoft/CMake/cmake/Microsoft/SegmentHeap.cmake" + } + }, + { + "name": "Target Windows", + "hidden": true, + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x64-windows-static" + } + }, + { + "name": "Target Linux", + "hidden": true, + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x64-linux" + } + }, + { + "name": "Windows Release", + "inherits": [ "Release", "Target Windows", "MSVC SegmentHeap" ] + }, + { + "name": "Windows Debug", + "inherits": [ "Debug", "Target Windows", "MSVC SegmentHeap" ] + }, + { + "name": "Windows Debug (Packet Logging)", + "inherits": "Windows Debug", + "cacheVariables": { + "PACKET_LOGGING": true + } + }, + { + "name": "Linux Release", + "inherits": [ "Release", "Target Linux" ] + }, + { + "name": "Linux Debug", + "inherits": [ "Debug", "Target Linux" ] + }, + { + "name": "Release MINGW", + "inherits": "Release", + "cacheVariables": { + "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": { + "value": "${sourceDir}/cmake/crosscompile/x86_64-mingw64.cmake", + "type": "FILEPATH" + }, + "CMAKE_BUILD_WITH_INSTALL_RPATH": true + } + }, + { + "name": "Linux Remote Release Debug Info", + "inherits": "Linux Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + }, + "vendor": { + "microsoft.com/VisualStudioSettings/CMake/1.0": { + "intelliSenseMode": "linux-gcc-x64" + }, + "microsoft.com/VisualStudioRemoteSettings/CMake/1.0": { + "rsyncCommandArgs": [ "-t", "--delete", "--delete-excluded" ], + "copySourcesOptions": { + "exclusionList": [ + ".vs", + ".git", + "out", + "build", + "dependencies/fc" + ] + } + } + } + }, + { + "name": "Linux Remote Debug", + "inherits": "Linux Debug", + "vendor": { + "microsoft.com/VisualStudioSettings/CMake/1.0": { + "intelliSenseMode": "linux-gcc-x64" + }, + "microsoft.com/VisualStudioRemoteSettings/CMake/1.0": { + "rsyncCommandArgs": [ "-t", "--delete", "--delete-excluded" ], + "copySourcesOptions": { + "exclusionList": [ + ".vs", + ".git", + "out", + "build", + "dependencies/fc" + ] + } + } + } + } + ] +} diff --git a/Catch_tests/CMakeLists.txt b/Catch_tests/CMakeLists.txt index e8d95c94b..076a2a617 100644 --- a/Catch_tests/CMakeLists.txt +++ b/Catch_tests/CMakeLists.txt @@ -1,17 +1,9 @@ cmake_minimum_required(VERSION 3.22) -if(MAKE_TESTS) - include(FetchContent) - include(AddTest) - include(SubdirList) +if(TESTS) + include(utility) - FetchContent_Declare( - Catch2 - GIT_REPOSITORY https://github.com/catchorg/Catch2.git - GIT_TAG v3.4.0 # or a later release - ) - - FetchContent_MakeAvailable(Catch2) + find_package(Catch2 CONFIG REQUIRED) subdir_list(SUBDIRS ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/Catch_tests/CString/CString_tests.cpp b/Catch_tests/CString/CString_tests.cpp index 98f3d81c3..429f63d93 100644 --- a/Catch_tests/CString/CString_tests.cpp +++ b/Catch_tests/CString/CString_tests.cpp @@ -59,7 +59,7 @@ SCENARIO( "CString", "[string]" ) { } WHEN("escape string") { - CString originalStr = CString("Test string's, hope it works out \\\\, \\, \", \', \"\" - / lol"); + auto originalStr = CString("Test string's, hope it works out \\\\, \\, \", \', \"\" - / lol"); CString stringEscaped = originalStr.escape(); THEN("check escaped result") { @@ -225,7 +225,7 @@ SCENARIO( "CString", "[string]" ) { test << "hello world"; CString test2 = "HeLlO wOrLd"; - int retVal = test.comparei(test2); + bool retVal = test.comparei(test2); THEN( "return should be true") { REQUIRE( retVal == true ); @@ -236,11 +236,11 @@ SCENARIO( "CString", "[string]" ) { test << "hello world"; CString test2 = "HeLlO wOrZd"; - int retVal = test.comparei(test2); + bool retVal = test.comparei(test2); THEN( "return should not be true") { REQUIRE( retVal != true ); } } } -} \ No newline at end of file +} diff --git a/Catch_tests/Player/Player_tests.cpp b/Catch_tests/Player/Player_tests.cpp index 1ba2e549a..34e3878e0 100644 --- a/Catch_tests/Player/Player_tests.cpp +++ b/Catch_tests/Player/Player_tests.cpp @@ -1,8 +1,13 @@ #define CATCH_CONFIG_MAIN #include "catch2/catch_all.hpp" +#include "player/PlayerClient.h" + #include -#include #include +#include + +using namespace preagonal; +using preagonal::Player; SCENARIO( "Player", "[object]" ) { @@ -11,7 +16,7 @@ SCENARIO( "Player", "[object]" ) { auto* server = BabyDI_PROVIDE(Server, new Server("test")); auto* socket = new CSocket(); - auto* player = new Player((CSocket*)socket, id); + const auto* player = new Player(socket, id); WHEN( "getting player id" ) { THEN( "id should be " << id ) { diff --git a/Catch_tests/Utilities/FilePermission_tests.cpp b/Catch_tests/Utilities/FilePermission_tests.cpp index c55ed3641..52dc1da8a 100644 --- a/Catch_tests/Utilities/FilePermission_tests.cpp +++ b/Catch_tests/Utilities/FilePermission_tests.cpp @@ -2,6 +2,8 @@ #include "catch2/catch_all.hpp" #include +using namespace preagonal; + SCENARIO("FilePermissions") { GIVEN("Some folder rights") { diff --git a/Catch_tests/Utilities/IdGenerator_tests.cpp b/Catch_tests/Utilities/IdGenerator_tests.cpp index cbc1c3744..2dd87c21a 100644 --- a/Catch_tests/Utilities/IdGenerator_tests.cpp +++ b/Catch_tests/Utilities/IdGenerator_tests.cpp @@ -1,6 +1,8 @@ #define CATCH_CONFIG_MAIN #include "catch2/catch_all.hpp" -#include +#include + +using namespace preagonal; SCENARIO("IdGenerator") { @@ -34,7 +36,8 @@ SCENARIO("IdGenerator") AND_WHEN("reseting back to defaults") { - generator.resetAndSetNext(0); + generator.reset(); + generator.createSegment(0); THEN("peeking next id should be 0") { REQUIRE(generator.peekNextId() == 0); @@ -77,7 +80,8 @@ SCENARIO("IdGenerator") AND_WHEN("reseting back to defaults") { - generator.resetAndSetNext(10001); + generator.reset(); + generator.createSegment(10001); THEN("peeking next id should be 10001") { REQUIRE(generator.peekNextId() == 10001); diff --git a/JenkinsEnv.json b/JenkinsEnv.json index f3069ce74..543f3df45 100644 --- a/JenkinsEnv.json +++ b/JenkinsEnv.json @@ -1,94 +1,49 @@ { - "builds": [ - { - "Title": "Docker Linux Alpine x86_64", - "Description": "", - "Type": "docker", - "OS": "linux", - "Config": { - "DockerRoot": "xtjoeytx", - "DockerImage": "gserver-v2", - "Tag": "", - "Dockerfile": "docker/gserver-x86_64-linux-musl.dockerfile", - "BuildIfSuccessful": "image", - "BuildEnv": "--build-arg NPCSERVER=on", - "RunTests": false - } - }, - { - "Title": "Docker Linux Alpine x86_64 - No npc-server", - "Description": "", - "Type": "docker", - "OS": "linux", - "Config": { - "DockerRoot": "xtjoeytx", - "DockerImage": "gserver-v2", - "Tag": "-no-npcserver", - "Dockerfile": "docker/gserver-x86_64-linux-musl.dockerfile", - "BuildIfSuccessful": "image", - "BuildEnv": "--build-arg NPCSERVER=off", - "RunTests": false - } - }, - { - "Title": "Linux x86_64", - "Description": "", - "Type": "docker", - "OS": "linux", - "Config": { - "DockerRoot": "xtjoeytx", - "DockerImage": "gserver-v2", - "Tag": "-linux-x86_64", - "Dockerfile": "docker/gserver-x86_64-linux-gnu.dockerfile", - "BuildIfSuccessful": "artifact", - "BuildEnv": "--build-arg NPCSERVER=on", - "RunTests": true - } - }, - { - "Title": "Linux x86_64 - No npc-server", - "Description": "", - "Type": "docker", - "OS": "linux", - "Config": { - "DockerRoot": "xtjoeytx", - "DockerImage": "gserver-v2", - "Tag": "-linux-x86_64-no-npcserver", - "Dockerfile": "docker/gserver-x86_64-linux-gnu.dockerfile", - "BuildIfSuccessful": "artifact", - "BuildEnv": "--build-arg NPCSERVER=off", - "RunTests": true - } - }, - { - "Title": "Windows x86_64", - "Description": "", - "Type": "docker", - "OS": "linux", - "Config": { - "DockerRoot": "xtjoeytx", - "DockerImage": "gserver-v2", - "Tag": "-win64", - "Dockerfile": "docker/gserver-x86_64-w64-mingw.dockerfile", - "BuildIfSuccessful": "artifact", - "BuildEnv": "--build-arg NPCSERVER=on", - "RunTests": false - } - }, - { - "Title": "Windows x86_64 - No npc-server", - "Description": "", - "Type": "docker", - "OS": "linux", - "Config": { - "DockerRoot": "xtjoeytx", - "DockerImage": "gserver-v2", - "Tag": "-win64-no-npcserver", - "Dockerfile": "docker/gserver-x86_64-w64-mingw.dockerfile", - "BuildIfSuccessful": "artifact", - "BuildEnv": "--build-arg NPCSERVER=off", - "RunTests": false - } - } - ] + "builds": [ + { + "Title": "Docker Linux Alpine", + "Description": "", + "Type": "docker", + "OS": "linux", + "Config": { + "DockerRoot": "xtjoeytx", + "DockerImage": "gserver-v2", + "Tag": "-linux64", + "Dockerfile": "docker/linux-musl.dockerfile", + "BuildIfSuccessful": "image", + "BuildEnv": "", + "RunTests": false + } + }, + { + "Title": "Linux GNU", + "Description": "", + "Type": "docker", + "OS": "linux", + "Config": { + "DockerRoot": "xtjoeytx", + "DockerImage": "gserver-v2", + "Tag": "-linux64-gnu", + "Dockerfile": "docker/linux-gnu.dockerfile", + "BuildIfSuccessful": "artifact", + "BuildEnv": "", + "RunTests": false + } + }, + { + "Title": "Windows", + "Description": "", + "Type": "docker", + "OS": "linux", + "Config": { + "DockerRoot": "xtjoeytx", + "DockerImage": "gserver-v2", + "Tag": "-win64", + "Dockerfile": "docker/windows-mingw.dockerfile", + "BuildIfSuccessful": "artifact", + "BuildEnv": "", + "RunTests": false + } + } + ] } diff --git a/Jenkinsfile b/Jenkinsfile index 4ab4030ce..aa690f406 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,5 @@ def notify(status){ - emailext ( + emailext( body: '$DEFAULT_CONTENT', recipientProviders: [ [$class: 'CulpritsRecipientProvider'], @@ -36,6 +36,7 @@ def killall_jobs() { if (killnums != "") { discordSend description: "in favor of #${buildnum}, ignore following failed builds for ${killnums}", footer: "", link: env.BUILD_URL, result: "ABORTED", title: "[${split_job_name[0]}] Killing task(s) ${fixed_job_name} ${killnums}", webhookURL: env.GS2EMU_WEBHOOK; } + echo "Done killing" } @@ -64,7 +65,7 @@ def buildStepDocker(config) { PUSH_ARTIFACT = BUILD_NEXT.equals('both'); } - if(env.TAG_NAME) { + if (env.TAG_NAME) { sh(returnStdout: true, script: "echo '```' > RELEASE_DESCRIPTION.txt"); env.RELEASE_DESCRIPTION = sh(returnStdout: true, script: "git tag -l --format='%(contents)' ${env.TAG_NAME} >> RELEASE_DESCRIPTION.txt"); sh(returnStdout: true, script: "echo '```' >> RELEASE_DESCRIPTION.txt"); @@ -93,7 +94,7 @@ def buildStepDocker(config) { customImage = docker.build("${DOCKER_ROOT}/${DOCKERIMAGE}:${tag}", "--build-arg BUILDENV=${buildenv} ${EXTRA_VER} ${BUILDENV} --network=host --pull -f ${DOCKERFILE} ."); } - def archive_date = sh ( + def archive_date = sh( script: 'date +"-%Y%m%d-%H%M"', returnStdout: true ).trim(); @@ -112,7 +113,7 @@ def buildStepDocker(config) { if (RUN_TESTS) { stage("Run tests...") { customImage.inside("") { - try{ + try { sh "cd /tmp/gserver/build && ctest -T test --no-compress-output --output-on-failure" } catch(err) { currentBuild.result = 'FAILURE' @@ -161,11 +162,11 @@ def buildStepDocker(config) { discordSend description: "Docker Image: ${DOCKER_ROOT}/${DOCKERIMAGE}:${tag}", footer: "", link: env.BUILD_URL, result: currentBuild.currentResult, title: "[${split_job_name[0]}] Artifact Successful: ${fixed_job_name} #${env.BUILD_NUMBER}", webhookURL: env.GS2EMU_WEBHOOK; } } + def dockerImageRef = docker.image("amigadev/docker-base:latest"); dockerImageRef.pull(); dockerImageRef.inside("") { - stage("Github Release") { withCredentials([string(credentialsId: 'PREAGONAL_GITHUB_TOKEN', variable: 'GITHUB_TOKEN')]) { dir("./dist") { @@ -179,19 +180,17 @@ def buildStepDocker(config) { release_type_tag = 'nightly'; } - if (!env.TAG_NAME) { sh(returnStdout: true, script: "echo -e '${release_type_tag} releases' > ../RELEASE_DESCRIPTION.txt"); } def files = sh(returnStdout: true, script: 'find . -name "*.zip" -o -name "*.tar.gz"'); - files = sh (script: "basename ${files}",returnStdout:true).trim() + files = sh(script: "basename ${files}",returnStdout:true).trim() try { sh "cat ../RELEASE_DESCRIPTION.txt | github-release release --user xtjoeytx --repo GServer-v2 --tag ${release_type_tag} --name \"GS2Emu ${release_type_tag}\" ${pre_release} --description -" - } catch(err) { + } catch(err) {} - } sh "github-release upload --user xtjoeytx --repo GServer-v2 --tag ${release_type_tag} --name \"${files}\" --file ${files} --replace" } } @@ -207,7 +206,6 @@ def buildStepDocker(config) { currentBuild.result = 'FAILURE' discordSend description: "", footer: "", link: env.BUILD_URL, result: currentBuild.currentResult, title: "[${split_job_name[0]}] Build Failed: ${fixed_job_name} #${env.BUILD_NUMBER}", webhookURL: env.GS2EMU_WEBHOOK - notify("Build Failed: ${fixed_job_name} #${env.BUILD_NUMBER}") throw err } @@ -219,7 +217,7 @@ node('master') { def fixed_job_name = split_job_name[1].replace('%2F',' '); checkout(scm); - env.COMMIT_MSG = sh ( + env.COMMIT_MSG = sh( script: 'git log -1 --pretty=%B ${GIT_COMMIT}', returnStdout: true ).trim(); diff --git a/README.md b/README.md index 55c1e7878..8a7c66ac9 100644 --- a/README.md +++ b/README.md @@ -8,200 +8,44 @@ For their additional work on the old gserver, special thanks go to: ## Building -### On *nix machines -just run the build-v8-(mac/linux) script: -``` -cd dependencies/ -./build-v8-linux -``` +### Required dependencies +- C++23 compiler (min supported: GCC 14, Clang 18, MSVC 2022 17.7) +- Java JRE +- CMake 3.28 +- Ninja build system +- vcpkg (https://vcpkg.io/en/getting-started) -### On Windows, using GCC -Currently, v8 is too much of a pain to build for this method to be viable. We suggest you use MSVC/MSBuild and nuget instead. +Linux users can install `openjdk-21-jre` or similar via their package manager, Windows users can install from [Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download) or via `winget`. -### On Windows, using MSVC/MSBuild with VSCode -#### Setup the environment: -Install [Visual Studio 2022](https://learn.microsoft.com/en-us/visualstudio/install/install-visual-studio?view=vs-2022). +vcpkg needs to be installed and the `VCPKG_ROOT` environment variable needs to be set to the location where it exists. +The directory should be writable by the user running the build (unless you want to spend extra time reading documentation and configuring the software). -- Install the "Desktop development with C++" workload. -- Install the "C++ CMake tools for Windows" component. -- Install the "NuGet package manager" component. -- Install the "Windows SDK" component. +### Build with CMake -#### Building the server with NPC-server support enabled -A folder called GServer-v2 will be created. Remember to update the Visual Studio installation path to your Visual Studio installation path. +If using command-line cmake, you would start a build like so: +``` +cmake --preset "Windows Release" -G Ninja -B build +cmake --build build ``` -git clone https://github.com/xtjoeytx/GServer-v2 -cd GServer-v2 -git submodule update --init --recursive -.\vcpkg\bootstrap-vcpkg.bat +On a Linux build, you would use `Linux Release` as the preset. -"%programfiles(x86)%\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 - -cmake .. -G "Visual Studio 17 2022" -A x64 -DV8NPCSERVER=TRUE -cmake --build . -``` +IDEs such as Visual Studio and CLion have CMake support built-in and can be used to easily build and configure the projects. ## Quick Start Instructions How-to setup a server: -1) Under the accounts folder, rename the text file 'YOURACCOUNT.txt' to your account name. For example: 'KuJi.txt' -2) Modify defaultaccount.txt to your liking. This is the default settings new players will start with. It can also be modified via RC. -3) Open config/serveroptions.txt and edit it to your liking. Be sure to modify the settings under "Private server options". Help for what these options do are available on the forums and in the file itself. -4) Find the line that starts with "staff=" in config/serveroptions.txt. Replace YOURACCOUNT with your account name. Anybody who needs RC access must be added to this line with their account names separated by commas. Additionally, RC users must have their IP range changed to at least *.*.*.* in their account to connect. -5) Port forward if needed. (Many threads on this topic exist in the forums. If you are having trouble, seek them out. Try the tutorials forum.) Basically, if you are behind a router and your computer isn't set to be the "DMZ", you will need to port forward. -6) Run gserver2.exe -- enjoy. -7) Report any bugs on http://www.graal.in/ - -## servers.txt - -The gserver can run multiple servers at once without needing to spawn separate processes. This is accomplished by the servers.txt file. This file will tell the gserver how many servers to run and where they are located, as well as some optional ip and port overrides. - -The file looks like this: -``` - servercount = 1 - server_1 = default - server_1_ip = myserver.com - server_1_port = 12345 - server_1_localip = 127.0.0.1 - server_1_interface = 192.168.2.1 -``` -servercount specifies the number of servers. In the default file, that is 1 server. -**server_#** specifies the directory the server is under. -**server_#_ip** specifies an optional ip address override. -**server_#_port** specifies an optional port override. -**server_#_localip** specifies an optional localip override. -**server_#_interface** specifies an optional interface override. - -All of the optional overrides will take precedence over the options defined in **serveroptions.txt**. - -## Special Graal Reborn NPC commands | - - -The Graal Reborn gserver has a couple special NPC commands built in. - -join somefile; - Much like official Graal's server-side join command, this command searches for somefile.txt and appends the contents to the end of the NPC script. - -singleplayer - This command is like the sparringzone command. When placed by itself with no semi-colon inside an NPC, it signifies that the level is "singleplayer." (SEE: Singleplayer Levels). - -## Singleplayer Levels - -The Graal Reborn gserver has the ability to toggle a level as "singleplayer." In this mode, the user cannot see any other player in the level. Any changes they make to the level are not replicated to other users. They are, in essence, in a level by themselves. - -To activate singleplayer mode, add an NPC to the level and add the single command "singleplayer" to the level, much like how the "sparringzone" command works. - -## Group Maps - -Like singleplayer levels, group maps allow only players in a group to see each other in a level. Player groups can be managed via the gr.setgroup and gr.setlevelgroup triggeractions (SEE: Graal Reborn special triggeractions). - -Individual levels cannot be set as group levels; instead, an entire map must be specified as a group map. The "groupmaps" server option specifies a comma-delimited list of maps that can contain groups. - -## Graal Reborn special client flags - -There are a few special client flags built into the gserver. These are: -gr.x -gr.y -gr.z - -These flags are used by the -gr_movement weapon included in the server weapons folder to simulate the smooth movement as found in the Graal clients 2.3 and up. - -If you don't want the gserver to recognize these flags, set the flaghack_movement setting to false in serveroptions.txt. - -Also, if flaghack_ip is enabled in the serveroptions.txt file, you can gain access to the following: -gr.ip - -## Graal Reborn special triggeractions - -The Graal Reborn gserver has a couple unique triggeractions built into it. They can be enabled/disabled by altering the setting that controls their group in serveroptions.txt. They are as follows: - -### Controlled by the setting triggerhack_weapons: - triggeraction 0,0,gr.addweapon,weapon1,weapon2,weapon3; - Adds weapon1, weapon2, and weapon3 to the player's account. - - triggeraction 0,0,gr.deleteweapon,weapon1,weapon2,weapon3; - Removes weapon1, weapon2, and weapon3 from the player's account. - -### Controlled by the setting triggerhack_guilds: - triggeraction 0,0,gr.addguildmember,guild,account,nickname; - Adds a player to the specified guild. Nickname is optional. - - triggeraction 0,0,gr.removeguildmember,guild,account; - Removes a player from the specified guild. - - triggeraction 0,0,gr.removeguild,guild; - Removes the guild from the server. - - triggeraction 0,0,gr.setguild,guild,account; - Sets the player's guild tag to the specified guild. - -### Controlled by the setting triggerhack_groups: - triggeraction 0,0,gr.setgroup,group; - Adds the player to the specified group. - - triggeraction 0,0,gr.setlevelgroup,group; - Adds all the players in the level to the specified group. - - triggeraction 0,0,gr.setplayergroup,account,group; - Adds the specified player to the specified group. - -### Controlled by the setting triggerhack_files: - triggeraction 0,0,gr.appendfile,filename,text; - Opens the file specified, located in the server's logs directory, and appends a line of text. - - triggeraction 0,0,gr.writefile,filename,text; - Opens the file specified, located in the server's logs directory, erases all of its contents, and writes a line of text. - - triggeraction 0,0,gr.readfile,filename,line_pos; - Opens the file specified, located in the server's logs directory, reads the given line number, and returns the contents to the player. - File contents are returned on the following flags: - gr.fileerror: String list. First index is a random number, subsequent indexes are error values. Error 1 = line_pos was outside of range. In this case, the next value is the line number returned. - gr.filedata: The file data. - -### Controlled by the setting triggerhack_rc: - triggeraction 0,0,gr.rcchat,Some chat text; - Sends some chat text to any logged in RC's. - -### Controlled by the setting triggerhack_execscript: - triggeraction 0,0,gr.es_clear; - Clears the execscript parameter list. - - triggeraction 0,0,gr.es_set,param1,param2,...; - Sets the execscript parameter list. - - triggeraction 0,0,gr.es_append,phrase; - Appends phrase directly to the end of the set parameter list. - - triggeraction 0,0,gr.es,account,script_name; - Sends the execscript to the specified account, or everybody if ALLPLAYERS was specified. - View the execscript/readme.txt file for more information. - -### Controlled by the setting triggerhack_props: - triggeraction 0,0,gr.attr1,data; - Sets data on the specified attribute. gr.attr1 - gr.attr30 work. - - triggeraction 0,0,gr.fullhearts,amount; - Sets the player's fullhearts to the specified amount. - -### Controlled by the setting triggerhack_levels: - triggeraction 0,0,gr.updatelevel; - Updates the current level. - - triggeraction 0,0,gr.updatelevel,levelname; - Updates the specified level. - -### Not controlled by any option: - triggeraction 0,0,gr.npc.move,id,dx,dy,duration,options; - Creates a serverside move command for the specified NPC. +1. Under the accounts folder, rename the text file `YOURACCOUNT.txt` to your account name. For example: `KuJi.txt` +2. Modify `defaultaccount.txt` to your liking. This is the default settings new players will start with. It can also be modified via RC. +3. Open `config/serveroptions.txt` and edit it to your liking (like selecting a server generation). Be sure to modify the settings under "Private server options". +4. Find the line that starts with `staff=` in `config/serveroptions.txt`. Replace `YOURACCOUNT` with your account name. Anybody who needs RC access must be added to this line with their account names separated by commas. Additionally, RC users must have their IP range changed to at least `*.*.*.*` in their account to connect. +5. Run `gs2emu.exe` -- enjoy. - triggeraction 0,0,gr.npc.setpos,id,x,y; - Sets an NPC's position. +Next steps: -## Weapon bytecode +[Start here](bin/docs/start-here.md) for more detailed instructions on how to set up and configure your server. -Place weapon bytecode in the weapon_bytecode/ folder. Inside each weapon file in weapons/, add the following: -BYTECODE name_of_file +## Documentation -The gserver will load weapon_bytecode/name_of_file and use the bytecode contained there-in. +Documentation is available in the `/docs/` folder. diff --git a/bin/CMakeLists.txt b/bin/CMakeLists.txt index c364ba0d9..8b498cdfd 100644 --- a/bin/CMakeLists.txt +++ b/bin/CMakeLists.txt @@ -20,11 +20,11 @@ # if(V8NPCSERVER) - file(GLOB DOCS - ../docs/npcserver.txt + file(GLOB V8DOCS + ../docs/v8-npcserver.txt ) - install(FILES ${DOCS} DESTINATION "./docs") + install(FILES ${V8DOCS} DESTINATION "./docs") file(GLOB SERVERS_DEFAULT_NPCS servers/default/npcs/npcControl-NPC.txt @@ -42,94 +42,100 @@ if(V8NPCSERVER) install(DIRECTORY DESTINATION "./servers/default/scripts") endif() +# Base directory file(GLOB TEXT changelog.txt miniupnp_licence.txt - servers.txt readme.txt + startupserver.txt ) - install(FILES ${TEXT} DESTINATION ".") +# docs/ +install(DIRECTORY "./docs" + DESTINATION "." + FILES_MATCHING PATTERN "*.md") + +# servers/default/ file(GLOB SERVERS_DEFAULT servers/default/serverflags.txt ) - install(FILES ${SERVERS_DEFAULT} DESTINATION "./servers/default") +# servers/default/accounts/ file(GLOB SERVERS_DEFAULT_ACCOUNTS + "servers/default/accounts/(npcserver).txt" servers/default/accounts/defaultaccount.txt servers/default/accounts/YOURACCOUNT.txt ) - install(FILES ${SERVERS_DEFAULT_ACCOUNTS} DESTINATION "./servers/default/accounts") +# servers/default/config/ file(GLOB SERVERS_DEFAULT_CONFIG - servers/default/config/rcmessage.txt servers/default/config/adminconfig.txt - servers/default/config/serveroptions.txt - servers/default/config/foldersconfig.txt servers/default/config/allowedversions.txt - servers/default/config/rules.txt + servers/default/config/foldersconfig.txt servers/default/config/ipbans.txt servers/default/config/rchelp.txt + servers/default/config/rcmessage.txt servers/default/config/rules.example.txt + servers/default/config/rules.txt servers/default/config/servermessage.html + servers/default/config/serveroptions.txt ) - install(FILES ${SERVERS_DEFAULT_CONFIG} DESTINATION "./servers/default/config") +# servers/default/documents/ file(GLOB SERVERS_DEFAULT_DOCUMENTS servers/default/documents/docu_wordfilter.txt servers/default/documents/rules.txt ) - install(FILES ${SERVERS_DEFAULT_DOCUMENTS} DESTINATION "./servers/default/documents") +# servers/default/execscripts/ file(GLOB SERVERS_DEFAULT_EXECSCRIPTS servers/default/execscripts/readme.txt ) - install(FILES ${SERVERS_DEFAULT_EXECSCRIPTS} DESTINATION "./servers/default/execscripts") +# servers/default/guilds/ file(GLOB SERVERS_DEFAULT_GUILDS servers/default/guilds/guildExample.txt ) - install(FILES ${SERVERS_DEFAULT_GUILDS} DESTINATION "./servers/default/guilds") +# servers/default/logs/ file(GLOB SERVERS_DEFAULT_LOGS servers/default/logs/rclog.txt servers/default/logs/serverlog.txt ) - install(FILES ${SERVERS_DEFAULT_LOGS} DESTINATION "./servers/default/logs") +# servers/default/npcs/ +file(GLOB SERVERS_DEFAULT_NPCS + servers/default/npcs/npcControl-NPC.txt + ) +install(FILES ${SERVERS_DEFAULT_NPCS} DESTINATION "./servers/default/npcs") -file(GLOB SERVERS_DEFAULT_TRANSLATIONS - servers/default/translations/english.po - servers/default/translations/deutsch.po - servers/default/translations/italiano.po - servers/default/translations/nederlands.po - servers/default/translations/norsk.po - servers/default/translations/svenska.po - ) - -install(FILES ${SERVERS_DEFAULT_TRANSLATIONS} DESTINATION "./servers/default/translations") +# servers/default/translations/ +install(DIRECTORY "./servers/default/translations" + DESTINATION "./servers/default/" + FILES_MATCHING PATTERN "*.po") +# servers/default/weapons/ file(GLOB SERVERS_DEFAULT_WEAPONS servers/default/weapons/weapon-gr_movement.txt ) - install(FILES ${SERVERS_DEFAULT_WEAPONS} DESTINATION "./servers/default/weapons") +# servers/default/world/ file(GLOB SERVERS_DEFAULT_WORLD - servers/default/world/readme.txt servers/default/world/onlinestartlocal.nw + servers/default/world/readme.txt ) - install(FILES ${SERVERS_DEFAULT_WORLD} DESTINATION "./servers/default/world") +# servers/default/world/* install(DIRECTORY DESTINATION "./servers/default/world/bodies") install(DIRECTORY DESTINATION "./servers/default/world/ganis") install(DIRECTORY DESTINATION "./servers/default/world/hats") @@ -138,10 +144,10 @@ install(DIRECTORY DESTINATION "./servers/default/world/images") install(DIRECTORY DESTINATION "./servers/default/world/shields") install(DIRECTORY DESTINATION "./servers/default/world/sounds") install(DIRECTORY DESTINATION "./servers/default/world/swords") -install(DIRECTORY DESTINATION "./servers/default/world/levels") +# servers/default/world/global/* install(DIRECTORY DESTINATION "./servers/default/world/global") install(DIRECTORY DESTINATION "./servers/default/world/global/bodies") install(DIRECTORY DESTINATION "./servers/default/world/global/heads") install(DIRECTORY DESTINATION "./servers/default/world/global/shields") -install(DIRECTORY DESTINATION "./servers/default/world/global/swords") \ No newline at end of file +install(DIRECTORY DESTINATION "./servers/default/world/global/swords") diff --git a/bin/changelog.txt b/bin/changelog.txt index a448e70d2..47be019bb 100644 --- a/bin/changelog.txt +++ b/bin/changelog.txt @@ -1,3 +1,46 @@ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Changelog for: + v4.0.0-beta + + * bug fix, + new feature, o other +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ++ GS1 NPC-server support with serverside scripting and events ++ Full, proper gmap support ++ Added support for heightmap-based gmap terrain ++ Serverside NPC showimg, projectiles / shoot tracking, and move commands ++ Scripted items for all item types, not just "gralats" ++ New internal filesystem keeps track of file changes and automatically reloads modified files, letting you change levels on the fly when locally hosting ++ Introduced "server generations", which simplifies server setup and will help segment incompatible client changes in the future ++ allowedversions.txt altered to support server generations ++ If the current working directory is set to a player world, that world will be loaded ++ Reintroduced UPNP support ++ Added support for 'GR-V1.05' .graal levels. ++ Tons of new server options (too many to list, see serveroptions.txt) ++ Added the ability to adjust the item drop rates for various item types ++ Classic translation system (old .po based system is not yet supported) +* Fixed bugs with carryable NPCs +* Fixed "update level" so it won't reset the X/Y locations of players in the level +* Fixed a bug that would cause players to momentarily become invisible when crossing level boundaries +* Fixed problems where players would get out of sync with baddies +* Many bug fixes around NPC props +* Many bug fixes around bigmaps and gmaps +* Fixed a problem where updating scripts via NC wouldn't cause the client to reload the script +o Removed the v8 NPC-server +o Removed multiple server support +o Removed servers.txt and altered how the startup server is found +o Replaced server logging with a new, better system +o Changed how files are written to disk to be more reliable +o Changed how files are named to avoid code page issues (we match official and use %000 to encode special characters) +o Many NC functions have been improved to get more immediate feedback (like adding/removing weapons) +o Refactored and rewrote a lot of the source code +o Changed how IDs are assigned to players and NPCs +o Added a lot of new documentation to the "docs" directory +o Any 2x2 tile change is respawned +o Baddy names and item numbers can be used in .nw files +o Improved server stability + +> + NPCs in adjacent levels on bigmaps are loaded (maybe?) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Changelog for: v3.0.9 diff --git a/bin/docs/extension-clientsidejoin.md b/bin/docs/extension-clientsidejoin.md new file mode 100644 index 000000000..4719e3a18 --- /dev/null +++ b/bin/docs/extension-clientsidejoin.md @@ -0,0 +1,49 @@ +# GS2Emu Clientside Join + +## What is it? + +The clientside join extension allows original and clientside classic mode servers to use the `join` command in clientside scripts. + +This extension is disabled if an NPC-Server is being used (you can add clientside script to a class, which makes this hack unnecessary). + +## How to enable + +Enable the following option in your `serveroptions.txt` file: + + clientsidejoins = true + +Then, use the `join` command in your clientside scripts: + + if (created) join myClass; + +The server will look for a `myClass.txt` file in the file system and append the contents of the file to the script. +Make sure the `.txt` file is located in a directory listed in `foldersconfig.txt`. +NOTE: You MUST include the semi-colon at the end of the command. This is NOT optional. + +## Implementation Considerations + +The code looks for the `join` keyword and tests to see if it is the first word in the line, or if it was immediately preceeded by a `;`, `{`, or `)`. +If that check fails, it will not process the `join` command. +It will then also check if there is a `#b` message code within the same line as the `join` command. +If so, it will assume that it is the word "join" is within a `say2` command and will not process the `join` command. + +Based on these checks, the following code will FAIL. + + say2 This#b + is a test#b + join things; + +The clientside hack is not intelligent enough to determine that it is within the `say2` command. + +Instead, you can use one of these workarounds: + + say2 This + #bis a test + #bjoin things; + + say2 This#b + is a test#b + join things; // #b + +In the first example, the `join` command is NOT the first word in the line and it will not be processed as a `join` command. +In the second example, a `#b` message code is technically within the line (but commented out), so it will not be processed. diff --git a/bin/docs/extension-execscript.md b/bin/docs/extension-execscript.md new file mode 100644 index 000000000..aa5532ae1 --- /dev/null +++ b/bin/docs/extension-execscript.md @@ -0,0 +1,32 @@ +# GS2Emu Execscripts + +## What are they? + +Execscripts are a feature that allows bespoke weapons to be added to a player to execute scripts. +The weapon script has replacement tokens that allow a customized version to be added to each client. + +## How to enable + +Enable the hack in the `serveroptions.txt` file: + + triggerhack_execscript = true + +## How to use + +Put weapon scripts in the `execscripts/` folder with the file name of `weaponname.txt`. +The weapon script should provide tokens for replacements: + + if (created) { + setplayerprop #c,*PARM0; + destroy; + } + +Tokens are given the format of `*PARM#`, where the `#` is the triggeraction parameter number. +For the above weapon (given the file name of `forcesay.txt`), you would use the following triggeraction: + + if (playerchats && startswith(/sayhi,#c)) { + tokenize #c; + triggeraction 0,0,gr.es_clear,; + triggeraction 0,0,gr.es_set,Hi; + triggeraction 0,0,gr.es,#t(1),forcesay; + } diff --git a/bin/docs/extension-flags.md b/bin/docs/extension-flags.md new file mode 100644 index 000000000..03f360278 --- /dev/null +++ b/bin/docs/extension-flags.md @@ -0,0 +1,31 @@ +# GS2Emu Special Flags + +## Smooth movement + +If the `flaghack_movement` option is enabled in the `serveroptions.txt` file, clients will be given a hidden weapon (named `-gr_movement`) that passes half-tile position updates to the server via client flags. +Clients older than version 2.3 would only send updated position props on whole tile increments, +which resulted in choppy movements for other players. + +Used flags: + + gr.x + gr.y + gr.z + +## File reading + +When the `triggerhack_files` option is enabled in the `serveroptions.txt` file, +the following flags will be used to return file contents to the client: + + gr.fileerror + gr.filedata + +The `gr.fileerror` flag will be set to `0` on no error, or `1,last line number` (e.g., `1,13`). +The `gr.filedata` flag will contain the contents of the requested line in the file. + +## IP + +When the `flaghack_ip` option is enabled in the `serveroptions.txt` file, +the following flag will be set with the player's remote IP address: + + gr.ip diff --git a/bin/docs/extension-level-groupmap.md b/bin/docs/extension-level-groupmap.md new file mode 100644 index 000000000..91ec9803b --- /dev/null +++ b/bin/docs/extension-level-groupmap.md @@ -0,0 +1,35 @@ +# GS2Emu Level Group Maps + +## What are they? + +Group maps provides a way for a group of players to play on their own private copy of a level. +All players will only see and interact with other players who have the same group name assigned to them. + +## How to enable + +Enable the triggeraction extension in the `serveroptions.txt` file: + + triggerhack_groups = true + +Then, list all the gmaps and levels that are group maps in the `serveroptions.txt` file: + + groupmaps = grouplevels_*.nw,groupgmap.gmap + +Finally, assign each player the same group name. + + triggeraction 0,0,gr.setgroup,some unique group name; + +## How to manage groups + +Use the following triggeractions to manage groups: + + gr.setgroup,group + Assigns the group name to a player. + gr.setlevelgroup,group + Assigns the group name to all current players in the level. + gr.setplayergroup,account,group + Assigns a group name to a specific player. + +Setting the group name to an empty string will remove the player from any group map and put them back in the main level. + + triggeraction 0,0,gr.setgroup,; diff --git a/bin/docs/extension-level-singleplayer.md b/bin/docs/extension-level-singleplayer.md new file mode 100644 index 000000000..d383e0a67 --- /dev/null +++ b/bin/docs/extension-level-singleplayer.md @@ -0,0 +1,14 @@ +# GS2Emu Level Singleplayer + +NOTE: Currently disabled. You can simulate it by using a group map with only one player in the group. + +## What are they? + +Singleplayer levels are levels where each player has their own private copy of the level. +They will not see any other players in the level. + +## How to enable + +In the level, add an NPC with the following script: + + singleplayer diff --git a/bin/docs/extension-triggeraction.md b/bin/docs/extension-triggeraction.md new file mode 100644 index 000000000..1ddc4235e --- /dev/null +++ b/bin/docs/extension-triggeraction.md @@ -0,0 +1,130 @@ +# GS2Emu Triggeraction Extensions + +## What are they? + +The server can process special triggeractions received from the client. +They have a wide variety of uses from writing files or enabling smooth movement for earlier clients. + +## How to enable + +Triggeraction extensions are currently enabled in the `serveroptions.txt` file in your playerworld. + +Options: + + triggerhack_weapons = false + triggerhack_guilds = false + triggerhack_groups = false + triggerhack_files = false + triggerhack_rc = false + triggerhack_execscript = false + triggerhack_props = false + triggerhack_levels = false + +## Always enabled + +The following actions are always enabled: + + gr.serverlist + Returns the server list to the player via a clientside triggeraction to the -Serverlist_v4 weapon. + +## triggerhack_weapons + +Enables the following actions: + + gr.addweapon,weapon1,weapon2,... + Adds weapons to the player's inventory. + gr.deleteweapon,weapon1,weapon2,... + Removes weapons from the player's inventory. + +## triggerhack_guilds + +Enables the following actions: + + gr.addguildmember,guild,nickname,account + Adds a member to a guild, creating it if it doesn't exist. + gr.removeguildmember,guild,account + Removes a member from a guild. + gr.removeguild,guild + Removes a guild and all its members. + gr.setguild,guild,account + Sets the guild of a player, but does not create the guild or add them to it. + +## triggerhack_groups + +Controls the "group map" extension feature of the server which lets a group of players have their own private copy of a level. +All players will only see and interact with other players who have the same group name. + +See: [extension-level-groupmap.md](extension-level-groupmap.md) for more information. + +Enables the following actions: + + gr.setgroup,group + Assigns the group name to a player. + gr.setlevelgroup,group + Assigns the group name to all current players in the level. + gr.setplayergroup,account,group + Assigns a group name to a specific player. + +## triggerhack_files + +Allows reading and writing to files in the `logs/` directory. + +Enables the following actions: + + gr.appendfile,filename,content + Appends content to a file. + gr.writefile,filename,content + Writes content to a file, overwriting any existing content. + gr.readfile,filename,line number + Reads the content of the specified line in a file. + +The `gr.readfile` action will write data to the following player flags: + + gr.fileerror + gr.filedata + +The `gr.fileerror` flag will be set to `0` on no error, or `1,last line number` (e.g., `1,13`). + +## triggerhack_rc + +Enables the following actions: + + gr.rcchat,content + Sends a message to the RC chat. + +## triggerhack_execscript + +Controls the "execscript" extension feature of the server which allows executing scripts on the player with custom parameters. + +See: [extension-execscript.md](extension-execscript.md) for more information. + +Enables the following actions: + + gr.es_clear + Clears the execscript parameters. + gr.es_set,param1,param2,... + Sets the execscript parameters. + gr.es_append,param1,param2,... + Appends the parameters to the execscript parameter list. + gr.es,account,script name + Executes the execscript on the player and clears the parameter list. + +## triggerhack_props + +Enables the following actions: + + gr.attr#,value + Sets the player's specified gani attribute. gr.attr1 - gr.attr30 + gr.fullhearts,heart count + Sets the player's full hearts count. + +## triggerhack_levels + +Enables the following actions: + + gr.updatelevel,level + Updates a level. + gr.npc.move,id,dx,dy,duration,options + Executes a "move" command on the specified NPC. + gr.npc.setpos,id,x,y + Sets the position of an NPC. diff --git a/bin/docs/npcserver-gs1.md b/bin/docs/npcserver-gs1.md new file mode 100644 index 000000000..ae4f035e4 --- /dev/null +++ b/bin/docs/npcserver-gs1.md @@ -0,0 +1,656 @@ +# GS2Emu GS1 NPC-Server + +For generic NPC-Server information, see: [npcserver.md](npcserver.md) + +## Generation + +The GS1 npc-server has only been tested with the "classic" generation. +It is possible that it will work with "newmain" and "modern", but there are no guarantees at this point. +Buyer beware. + +## Considerations + +Please review the lists below of implemented events, flags, functions, commands, and message codes. + +--- +## Events + + * - Existed on official, but not serverside. + [GR] - Never existed on official. + +#### Implemented + + compusdied + created + exploded + initialized + movementfinished + npcwarped + playerchats + playerdies + playerenters + playerhurt + playerhurted + playerlaysitem + playerleaves + playerlogin + playerlogout + * playertouchesme + * playertouchesother + playertouchsme + playertouchsother + pm + timeout + triggeraction events + warped + washit (hitobjects) + wasshot + wasshooted + wasthrown + +#### Not implemented + + serverlistconnect + washit (anything not hitobjects) + waspelt + +#### Clientside only + + firedonhorse + keypressed + mousedown + mouseup + mousewheel + weaponfired + +--- +## Flags + + * - Existed on official, but not serverside. + [GR] - Never existed on official. + +#### Implemented + + canspin + carrying + carriesblackstone + carriesbush + carriesnpc + carriessign + carriesstone + carriesvase + compsdead + isleader + isonmap + issparringzone + isweapon + nopkzone + playerattached + playerisfemale + playerismale + playeronhorse + playeronline + playerpaused + playerswimming + shotbybaddy + * shotbynpc + shotbyplayer + visible + weaponsenabled + +#### Not implemented + + * peltwithblackstone + * peltwithbush + * peltwithnpc + * peltwithsign + * peltwithstone + * peltwithvase + playertrial + +#### Clientside only + + followsplayer + isfocused + leftmousebutton + lighteffectsenabled + middlemousebutton + playermap + playerreading + rightmousebutton + +--- +## Variables + + * - Existed on official, but not serverside. + [GR] - Never existed on official. + +#### Implemented + + actionplayer + allplayers[] + allplayerscount + board[] + gravity + groundheights[] + levelorgx + levelorgy + npcscount + nwday + nwhour + nwmin + nwmonth + nwtime + nwweek + nwweekday + nwyear + paramscount + players[] + playerscount + tiles[x,y] + timevar + timevar2 + tokenscount + weaponscount + + npcs[] + .ap + .bombs + .darts + .dir + .glovepower + .gralats + .headset + .hearts + .height + .hp + .hurtdx + .hurtdy + * .hurtpower + .id + .rupees + .save[] + .shieldpower + .sprite + .swordpower + .timeout + .width + .x + .y + .z + npcs[] shorthand: e.g., hearts + + players[] + .ap + .attachid + .attachtype + .bombs + * .carrysprite + .darts + .deaths + .dir + .fullhearts + .glovepower + .gralats + .headset + .hearts + .hp + .hurtpower + .id + .kills + .lastdead + .logintime + .maxhp + .mp + .rating + .ratingd + .rupees + .saysnumber + .shieldpower + .shootpower + .sprite + .swordpower + .upgradestatus + .x + .y + .z + players[] shorthand: e.g., playerhearts + + * arrowscount + * arrows[] + .x + .y + .dx + .dy + .dir + .type + .from + + * compuscount + * compus[] + .x + .y + .type + .dir + .headdir + .power + .mode + + * bombscount + * bombs[] + .x + .y + .power + .time + + * itemscount + * items[] + .x + .y + .type + .time + + * exploscount + * explos[] + .x + .y + .power + .time + .dir + + * horsescount + * horses[] + .x + .y + .dir + .bushes + .bombs (always 0, client never sends) + .bombpower (always 0, client never sends) + .type + + signscount + signs[] + .x + .y + +#### Clientside only + + downloadpos + downloadsize + focusx + focusy + graalversion + mousebuttons + mousescreenx + mousescreeny + mousewheeldelta + mousex + mousey + musicpos + musiclen + playerfreezetime + screenheight + screenwidth + selectedsword + selectedweapon + waterheight + + npcs[].anistep + + players[] + .anistep + +--- +## Commands + + * - Existed on official, but not serverside. + [GR] - Never existed on official. + +#### Implemented + + addguildmember + addstring + addweapon + attachplayertoobj + blockagain + callnpc + canbecarried + canbepulled + canbepushed + cannotbecarried + cannotbepulled + cannotbepushed + cannotwarp + canwarp + canwarp2 + * carryobject + changeimgcolors + * changeimgmode + changeimgpart + changeimgvis + changeimgzoom + copylevel + copystrings + deletelevel + deletestring + destroy + detachplayer + * disabledamagereactions [GR] + disableweapons + dontblock + drawoverplayer + * drawovertrees + drawunderplayer + * enabledamagereactions [GR] + enableweapons + * explodebomb + freezeplayer2 + hide + hideimg + * hideimgs + * hitnpc + hitobjects + * hitplayer + * hurt + insertstring + join + * lay + * lay2 + message + move + noplayeronwall + putbomb + * putcomp + putexplosion + putexplosion2 + * puthorse + * putnewcomp + * putnpc + putnpc2 + * removebomb + * removecompus + removeguild + removeguildmember + * removehorse + * removeitem + removestring + removeweapon + replacestring + savelog + savelog2 + say + say2 + sendpm + sendrpgmessage + sendtonc + sendtorc + serverwarp + set + setani + setarray + setbeltcolor + * setbody + setcharani + setchargender + setcharprop + setcoatcolor + setgender + setgif + setgifpart + * sethead + setimg + setimgpart + * setlevel + setlevel2 + * setmap + * setminimap + * setplayerdir + setplayerprop + setpm + setshape + * setshield + setshoecolor + setshootparams + setskincolor + setsleevecolor + setstring + * setsword + shoot + * shootarrow + * shootball (gr extension - dir parameter) + * shootfireball + * shootfireblast + * shootnuke + show + showani + showani2 + showcharacter + showimg + showimg2 + showpoly + showpoly2 + showtext + showtext2 + sleep + * spyfire + * take + * take2 + * takehorse + * takeplayercarry + * takeplayerhorse + * throwcarry + * timershow + tokenize + tokenize2 + toweapons + triggeraction + unfreezeplayer + unset + updateboard + updateboard2 + warpto + +#### Not implemented + + * hitcompu + * savelevel + * showstats + +#### Clientside only + + addtiledef + addtiledef2 + blockagainlocal + callweapon + disabledefmovement + disablemap + disablepause + disableselectweapons + dontblocklocal + drawaslight + enabledefmovement + enablefeatures + enablemap + enablepause + enableselectweapons + explodebomb + followplayer + freezeplayer + hidelocal + hideplayer + hidesword + loadmap + noplayerkilling + openurl + openurl2 + play + play2 + playlooped + putleaps + putobject + reflectarrow + removetiledefs + replaceani + resetfocus + setbackpal + setbacktile + setbacktile2 + setcoloreffect + setcursor + setcursor2 + seteffect + seteffectmode + setfocus + setletters + setmusicvolume + setshape2 + setspritesimage + setstatusimage + seturllevel + setzoomeffect + setz + showfile + showlocal + stopmidi + stopsound + timereverywhere + toinventory + updateterrain + wraptext + wraptext2 + +#### Won't implement + + * saveinfo (internal command to control an official database, won't be implemented) + +--- +## Functions + + * - Existed on official, but not serverside. + [GR] - Never existed on official. + +#### Implemented + + abs(value) + * aindexof(value, array) + arctan(value) + arraylen(array) + * ascii(string) + base64decode(string) + base64encode(string) + cos(value) + findnearestplayer(x, y) + getangle(dx, dy) + * getareanpcs(x, y, width, height) + * getdir(dx, dy) + getflagkeys(prefix) + * getnearestplayer(x, y) + * getnearestplayers(x, y, flag) + getnpc(name) + getplayer(account) + getz(x, y) + hasweapon(name) + indexof(substring, string) + int(value) + * keycode(key) + lindexof(string, list) + * log(base, value) + * max(value1, value2) + * min(value1, value2) + onmapx(level) + onmapy(level) + onwall(x, y) + onwall2(x, y, width, height) + onwater(x, y) + onwater2(x, y, width, height) + * playersays(index, text) + * playersays(text) + * playersays2(index, text) + * playersays2(text) + random(min, max) + sarraylen(list) + sin(value) + startswith(prefix, string) + strcontains(string, substring) + strequals(string1, string2) + strlen(string) + strtofloat(string) + * testbomb(x, y) + * testcompu(x, y) + * testexplo(x, y) + * testhorse(x, y) + * testitem(x, y) + testnpc(x, y) + testplayer(x, y) + testsign(x, y) + * tiletype(x, y) + vecx(dir) + vecy(dir) + +#### Not implemented + + exp(r,r) + findnearestplayers(x, y) + +#### Clientside only + + imgheight(file) + imgwidth(file) + keydown(key) + keydown2(keycode, ignorecase) + screenx(x, y) + screeny(x, y) + textheight(zoom, font, style) + textwidth(zoom, font, style, text) + worldx(x, y) + worldy(x, y) + +--- +## Message codes + +#### Implemented + + #1 | #1(index) [RW] - Sword image + #2 | #2(index) [RW] - Shield image + #3 | #3(index) [RW] - Head image + #5 | #5(index) [RW] - Horse image + #6 | #6(index) [R] - Carried NPC image + #7 | #7(index) [RW] - Bow image (1.x) + #8 | #8(index) [RW] - Body image (2.x+) + #a | #a(index) [R] - Player account name + #c | #c(index) [RW] - Chat text + #g | #g(index) [R] - Guild name + #m | #m(index) [RW] - Animation + #n | #n(index) [RW] - Nickname + #N | #N(index) [R] - Database NPC name + #f | #f(index) [R] - NPC image + #W(index) [R] - Weapon image + #w(index) [R] - Weapon name + #p(index) [RW] - Action parameter (triggeraction) + #t(index) [RW] - Token (tokenize) + #F [R] - Level of the player + #L [R] - Level of the source NPC + #C0 - #C4(index) [RW] - Body colors + #C5 - #C7(index) [RW] - Newworld body colors (NW only) + #P1 - #P30(index) [RW] - Gani attributes + #Q(guild_name, account_name) [R] - Nickname of a guild member + #G | #G(index) [R] - Player's account level (e.g., gold, classic, trial, etc.) + #e(start_index, length, string) - Extracts a substring from the given string + #I(string_list, index) - Returns the string at the given index from the string list + #K(ascii_number) - The character represented by the given ASCII code + #R(string_list) - Randomly selects a string from the given string list + #s(identifier) - The string value of a variable + #T(string) - Trims the string + #U(string) - Replaces the string with a translated version of it + #v(identifier) - The value of an number variable as a string + #b - Line break + +#### Clientside only + + #D | #D(filename) - Current file being downloaded | The download position of the specified file + #E - The current emoticon character being displayed by the player + #i(image) | #i(image, x, y, width, height) - Displays an image or part of an image when used in a sign + #k(key_index) - The description of the specified key (in client language/key assignments) + #W - The image of the player's currently selected weapon + #w - The name of the player's currently selected weapon + #S - The player's currently selected sword (Newworld only) diff --git a/bin/docs/npcserver.md b/bin/docs/npcserver.md new file mode 100644 index 000000000..ba7d30edc --- /dev/null +++ b/bin/docs/npcserver.md @@ -0,0 +1,167 @@ +# GS2Emu NPC-Server + +## What is it? + +The NPC-Server manages NPCs and executes serverside scripts. +When an NPC-Server is enabled, many clientside features are disabled. +The game becomes more server-authoritative. + +## How to enable + +Set the following option in the `serveroptions.txt` file: + + serverside = true + +## Requirements + +Original servers may not use an NPC-Server. Only classic servers and beyond are supported. +Make sure the server generation is set appropriately in the `serveroptions.txt` file: + + generation = classic + +See: [server.md](server.md#server-generation) for more information about server generations. + +## Script languages + +The following languages are supported: + + gs1 + [Clientside] Clients version 5.007 and earlier. + [Serverside] Any client version. + gs2 + [Clientside] Clients version 4 and later. + [Serverside] N/A + +- [GS1 reference](npcserver-gs1.md) +- GS2 reference + +## Client behavior changes + +When the NPC-Server is enabled, the client will no longer send NPC property updates. + +## Server behavior changes + +The server will take over processing of level signs and links. +It will also start to keep track of level objects, such as arrows, bombs, items, etc, for use in serverside scripts. +Various client requests will also be rejected. + +The following changes will be made to how the server responds to client requests: + +- NPC property edits are ignored. +- New NPCs (via `putnpc`) are ignored. +- Deleting NPCs is ignored. +- Only `client.` flags will be accepted (`server.` flag changes will be ignored). +- Items a player spawns will be taken from the player's inventory (under the assumption the player dropped the item). +- Clientside `toweapons` is ignored. +- Various player property updates from the client are ignored (`maxpower`, `rupees`, `glovepower`, `swordpower`, `shieldpower`, `mp`, `ap`, `kills`, `deaths`, `rating`). + +Any of the above should be handled by serverside scripts. + +## Options + +The following options are available in the `serveroptions.txt` file: + + clientsidesigns = false + Disables serverside handling of signs if true. + clientsidelinks = false + Disables serverside handling of level links if true. + runallscriptevents = false + By default, NPCs will only respond to script events referenced in their scripts. If true, the NPC will respond to all events. + sleepwhennoplayers = true + If true, the NPC-Server will not process scripts when there are no players connected. + +--- +# Serverside NPC programming + +Inside a script, anything above the `//#CLIENTSIDE` separator is serverside. +The serverside script does not have access to all of the commands and events that clientside scripts can use. +Things like `putleaps` or `seteffectmode` are only visible on the client, so you need to issue those commands from the clientside script, same with events like `weaponfired`. + +### Communicating with a serverside script + +In order to communicate with the serverside portion of a script, you must make use of the `triggeraction` command. + + triggeraction x,y,dig,; + triggeraction 0,0,serverside,Shovel,; + triggeraction 0,0,servernpc,DatabaseNPCName,; + triggeraction 0,0,serverwhatever,; + +In the first case, a "dig" action will be triggered at the (x, y) location of the level. Any NPC at those coordinates will get an `actiondig` event, including the NPC itself that issued the action. + +In the second case, the serverside script of the "Shovel" weapon will get triggered. The shovel weapon will get an `actionserverside` event. + +In the third case, the serverside script of the database NPC with the name "DatabaseNPCName" will get triggered. The NPC will get an `actionserverside` event. + +In the fourth case, the Control-NPC will get an `actionserverwhatever` event. + +So, for a level NPC without a name, you would use the first case to trigger its own serverside script. +To talk to the Control-NPC, you would use the fourth case; any action that starts with `server` and is not `serverside` or `servernpc` will be sent to the Control-NPC. + +### Communicating with a clientside script + +It is not easy to communicate with an NPC's clientside script. +The client will not process any `triggeraction` events that did not originate from the client itself, so you can't trigger the NPC's clientside script directly. +Hacks can be done using the `shoot` command, but that is not recommended; instead, you should rewrite scripts to communicate in the other direction. + +One exception to this is the ability to `triggeraction` a client's weapon script: + + triggeraction 0,0,clientside,weapon name,params; + +The weapon's clientside code can listen for the `actionclientside` event. +This was introduced in the 2.21 client, so it won't work for any versions prior to that. + +### Collision detection + +In order for serverside collision detection to work, you must provide an NPC with a shape: + + if (created) { setshape 1,32,32; } + +That will set the width and height of the NPC to 32 pixels. This will enable collision detection and let events such as `playertouchsme` and `exploded` work. +NPCs that use the `showcharacter;` command will get a normal player's collision box, although you may call `setshape` again to overwrite it. +Triggeractions will also hit within the collision bounds of an NPC rather than at the exact coordinates. + +### Script classes + +Script classes are used to modularize code. +By issuing a `join` command in the serverside script, the class is joined to the script. +Events are called on the NPC or weapon script, then the event is passed to all joined classes. +Since the joined classes are separate script execution contexts, unprefixed variables (like `i`) are local only to the class. +If you want to make a variable accessible to both the NPC's main script and the class, use the `this.` prefix to assign the variable to the NPC itself. + +### Level item NPCs + +If a class exists that matches the name of any default item (with the exception of "gralats" which matches green, blue, red, and gold gralats), +then the server will prevent the item from dropping and instead create a new level NPC that joins against the class. + +Gralats, arrows, bombs, darts, and hearts will stack with nearby NPCs of the same type. + +> Item class names: `gralats`, `bombs`, `darts`, `heart`, `glove1`, `bow`, `bomb`, `shield`, `sword`, `fullheart`, `superbomb`, `battleaxe`, `goldensword`, `mirrorshield`, `glove2`, `lizardshield`, `lizardsword`, `fireball`, `fireblast`, `nukeshot`, `joltbomb`, and `spinattack`. + +### Timeouts + +Serverside timeouts are limited to 0.1 seconds, unlike clientside timeouts which have 0.05 second resolution. + +### Variables + +A `flag` is a string value that is set by `set` or `setstring`. +A `var` is a numeric value that is assigned by normal variable assignment. +Clients and serverside scripts only share `flags` that are prefixed with `client.`, `clientr.`, and `serverr.` + +Additionally, there are three special prefixes: `thiso.`, `cliento.`, and `clientro.` +When using the `with()` statement to access a different NPC or player in serverside script, the `this.`, `client.`, and `clientr.` variables point to the accessed NPC or player. +The `thiso.`, `cliento.`, and `clientro.` prefixes will let you access variables linked to the original NPC or player. + +Variable prefixes: + + client.flag - stored on the player account and writable by the client + clientr.flag - stored on the player account and read-only by the client + local.flag - stored on the npc + local.var - stored on the npc + server.flag - stored on the server and NOT sent to the client (this differs from classic mode) + serverr.flag - stored on the server and read-only by the client + level.flag - stored on the level + level.var - stored on the level + this.flag - stored on the npc + this.var - stored on the npc + flag - stored on the player account + var - stored in the script execution context (not visible to joined classes) diff --git a/bin/docs/server-unimplemented.md b/bin/docs/server-unimplemented.md new file mode 100644 index 000000000..8f3cce182 --- /dev/null +++ b/bin/docs/server-unimplemented.md @@ -0,0 +1,36 @@ +# GS2Emu Unimplemented Features + +## Original era +###### 1.x + +Support for client versions below 1.38 is not supported and may not work. + +Clients 1.38 - 1.411 are fully supported with no known issues. + +## Classic era +###### 2.x / 3.x + +Client authoritative: +- Full support with official with no known limitations. + +Server authoritative (NPC-server): +- No support for trial accounts or ghost mode. +- Translations for NPC messages in GS1 scripts are not supported. +- Lacking some script commands / features that may or may not have been implemented on official servers (lack of documentation to make full judgement). + +## Newmain era +###### 4.x - 5.007 + +Clientside GS2 scripts do not work on 4.x clients. +There are opcode differences that still need to be figured out. + +Clientside GS2 on the 5.007 client works and there is no known limitations. + +Serverside GS2 scripts are not supported. + +Translations using the modern PO (Portable Object) file format are not supported. + +## Modern era +###### 5.1 - 6.037 + +Same limitations as the Newmain era. diff --git a/bin/docs/server.md b/bin/docs/server.md new file mode 100644 index 000000000..66a8f325c --- /dev/null +++ b/bin/docs/server.md @@ -0,0 +1,93 @@ +# GS2Emu Server + +## Supported clients + +The server supports clients from version 1.38 to 6.037. + +## Server generation + +In order to better support such a wide range of clients, a specific generation must be set on the server. +The generation will control how the server behaves, what features are available, how data is sent to clients, etc. + +There are currently four identified generations: + + original - 1.x + classic - 2.x/3.x + newmain - 4.x to 5.007 + modern - 5.1+ + +The generation is set in the `serveroptions.txt` file in your playerworld: + + generation = classic + +## Configuration + +### accounts/(npcserver).txt + +This is the account for the NPC-Server player. + +### accounts/defaultaccount.txt + +Whenever a new player joins the server, a new account is created for them by cloning this file. +Configure where the player should start, which weapons they should have, and any other defaults with this account. + +### config/adminconfig.txt + +Controls how the server is displayed in the server list. + +### config/allowedversions.txt + +This file lists which client versions are allowed in the server. + +If the file starts with `[generation-range]`, then each generation has a list of accept client ranges. +Each generation contains a comma-separated list of allowed versions. +Putting a colon (:) between two versions creates a range. +See the examples already in the file. + +If the file does not have the generation indicator, then it uses an older format where each line lists a client version to allow. +Putting a colon (:) between two versions creates a range. + +### config/foldersconfig.txt + +Controls where types of files can be found on the file system. + +Format: `filetype folder` + +Valid file types: +- `file`: generic files +- `level`: level files +- `head`: head images +- `body`: body images +- `sword`: sword images +- `shield`: shield images +- ~~`sound`: sound files~~ (not supported, use `file`) + +The folder supports wildcards. + +Example: `file images/*.png` + +### config/ipbans.txt + +A list of IP addresses that are banned from connecting to the server. +IP addresses are listed one per line and wildcards are supported. + +### config/rchelp.txt + +Contains the help text that gets sent to a player when they issue the `/help` command in RC Chat. + +### config/rcmessage.txt + +Contains the message that gets sent to a player when they join RC Chat. + +### config/rules.txt + +Chat filter rules. See `rules.example.txt` for examples. + +### config/servermessage.html + +Contains the welcome message that gets sent to a player when they join the server. +The server message gets cached on the client so they will not see it again until it gets updated (or they visit a different server). + +### config/serveroptions.txt + +Controls various server options. See the file itself for details. diff --git a/bin/docs/start-here.md b/bin/docs/start-here.md new file mode 100644 index 000000000..7feb3289a --- /dev/null +++ b/bin/docs/start-here.md @@ -0,0 +1,37 @@ +# Steps to setting up a server + +## Set up your initial player account + +1. Under the accounts folder, rename the text file `YOURACCOUNT.txt` to your account name. For example: `KuJi.txt` +1. Edit the `config/serveroptions.txt` file, find the line that starts with `staff=`, and replace `YOURACCOUNT` with your account name. + +## Configure your server + +1. Modify the `config/serveroptions.txt` file: + 1. Set the `name` and `description` of your server. Servers must have unique names or they will be rejected from the server list. + 1. Set the `generation` of your server. This controls how the server behaves and which clients can connect. See: [Generations](server.md#server-generation). + 1. Set the `serverside` setting to `true` if you want to run an NPC-Server. Only the `original` and `classic` generations can run without one. +1. If you wish to change which clients are allowed, modify the `allowedverions.txt` file to adjust which client versions are allowed for your chosen generation. +1. Modify the `config/foldersconfig.txt` to adjust where the server looks for various files. Make sure to add any changes to your account file's `FOLDERRIGHT` entries if you want to use RC to upload files. + +# FAQ + +## Making a private, hidden server + +1. Modify the `config/adminconfig.txt` file and set `hq_level` to `0`. +1. Modify the `config/serveroptions.txt` file and set `onlystaff` to `true`. + +## Adding new staff accounts + +1. Edit the `config/serveroptions.txt` file (or use RC) and add the new account name to the `staff=` line, separating multiple accounts with commas. +1. Add or edit the player's account file in the `accounts` folder (or use RC) and: + 1. Adjust the `IPRANGE` setting. A value of `*.*.*.*` would allow the account to connect from any IP address. + 1. Set the `LOCALRIGHTS` setting. A value of `1040383` would give all rights. It is easiest to use RC to selectively give rights. + 1. Add `FOLDERRIGHT` entries for RC File Browser access. It is easiest to copy from your own staff account and modify as necessary. + +# Further reading + +- [Server](server.md) - Learn about server generations, client versions, and other server options. +- [Unimplemented Features](unimplemented.md) - See which features are not yet implemented on the new server. +- [NPC-Server](npcserver.md) - Learn about the NPC-Server, which is required for some generations and optional for others. +- Everything else in the `docs` directory. diff --git a/bin/docs/translation-classic.md b/bin/docs/translation-classic.md new file mode 100644 index 000000000..52ab12f60 --- /dev/null +++ b/bin/docs/translation-classic.md @@ -0,0 +1,53 @@ +# Translation - Classic Mode + +## Overview + +Classic translation mode is enabled when the [server Generation](server.md#server-generation) is not set to `modern`. +Translations are simple direct text replacements, without any advanced features. + +## File format + +File name: `slanguageDomain.txt` + +The language domain is one of the built-in languages of the older clients: +- Deutsch +- English +- Español +- Français +- Italiano +- Nederlands +- Norsk +- Português +- Svenska + +The special domain `Original` is used for the original text. + +The file consists of an MD5 hash of the text, followed by the translated text within quotes (with quotes and backslashes escaped). + +`12345678901234567890123456: "Translated \"Text\""` + +## RC commands + +- `/synctranslation [language]`: +Synchronizes an existing translation files with the `Original` domain. +If the language is not specified, all loaded languages are synchronized. +The language must be one of the endonyms listed above. +You can also use the ISO 639 language code (e.g. `en` for `English`, `de` for `Deutsch`, etc), +or an ISO Language Name (e.g. `English`, `German`, `Spanish`, etc). +- `/generatetranslationstubs`: +Generates translation files for all supported languages. + +Any unused translations (they no longer exist in the `Original` domain) are written to `slanguageDomain.unused` and removed from the language file during all operations. + +## Coverage + +Currently, signs (including both `say` and `say2`), RPG messages, the `#U(...)` message code, +and server generated PMs (jail message, `sendpm` command, and `setpm` replies) are translated. + +Translation currently happens AFTER strings are fully processed. + +## Missing features + +For full compliance: +- Translation needs to happen BEFORE strings are fully processed (so unprocessed message codes are included). +- NPC chat/messages need to be translated. diff --git a/bin/docs/translation-modern.md b/bin/docs/translation-modern.md new file mode 100644 index 000000000..c71c90cd8 --- /dev/null +++ b/bin/docs/translation-modern.md @@ -0,0 +1,11 @@ +# Translation - Modern Mode + +## Overview + +Modern translation mode is enabled when the [server Generation](server.md#server-generation) is set to `modern`. + +It uses the PO (Portable Object) file format, which is a more advanced format that supports context, plural forms, and comments. + +It is currently not yet implemented. + +See: [GNU gettext documentation](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) for more information about the PO file format. diff --git a/bin/readme.txt b/bin/readme.txt index 4f4150ea3..ce3ff4704 100644 --- a/bin/readme.txt +++ b/bin/readme.txt @@ -1,189 +1,25 @@ -Graal Reborn GServer -Created by: Joey, Nalin, dufresnep, Codr, Marlon. -Based off the original work by 39ster. -For their additional work on the old gserver, special thanks go to: - Agret, Beholder, Joey, Marlon, Nalin, and Pac. - ----------------------------- -| Quick Start Instructions | ----------------------------- - -How-to setup a server: - -1) Under the accounts folder, rename the text file 'YOURACCOUNT.txt' to your account name. For example: 'KuJi.txt' -2) Modify defaultaccount.txt to your liking. This is the default settings new players will start with. It can also be modified via RC. -3) Open config/serveroptions.txt and edit it to your liking. Be sure to modify the settings under "Private server options". Help for what these options do are available on the forums and in the file itself. -4) Find the line that starts with "staff=" in config/serveroptions.txt. Replace YOURACCOUNT with your account name. Anybody who needs RC access must be added to this line with their account names separated by commas. Additionally, RC users must have their IP range changed to at least *.*.*.* in their account to connect. -5) Port forward if needed. (Many threads on this topic exist in the forums. If you are having trouble, seek them out. Try the tutorials forum.) Basically, if you are behind a router and your computer isn't set to be the "DMZ", you will need to port forward. -6) Run gserver2.exe -- enjoy. -7) Report any bugs on http://www.graal.in/ - - ---------------- -| servers.txt | ---------------- - -The gserver can run multiple servers at once without needing to spawn separate processes. This is accomplished by the servers.txt file. This file will tell the gserver how many servers to run and where they are located, as well as some optional ip and port overrides. - -The file looks like this: - servercount = 1 - server_1 = default - server_1_ip = myserver.com - server_1_port = 12345 - server_1_localip = 127.0.0.1 - server_1_interface = 192.168.2.1 - -servercount specifies the number of servers. In the default file, that is 1 server. -server_# specifies the directory the server is under. -server_#_ip specifies an optional ip address override. -server_#_port specifies an optional port override. -server_#_localip specifies an optional localip override. -server_#_interface specifies an optional interface override. - -All of the optional overrides will take precedence over the options defined in serveroptions.txt. - - -------------------------------------- -| Special Graal Reborn NPC commands | -------------------------------------- - -The Graal Reborn gserver has a couple special NPC commands built in. - -join somefile; - Much like official Graal's server-side join command, this command searches for somefile.txt and appends the contents to the end of the NPC script. - -singleplayer - This command is like the sparringzone command. When placed by itself with no semi-colon inside an NPC, it signifies that the level is "singleplayer." (SEE: Singleplayer Levels). - - ------------------------ -| Singleplayer Levels | ------------------------ - -The Graal Reborn gserver has the ability to toggle a level as "singleplayer." In this mode, the user cannot see any other player in the level. Any changes they make to the level are not replicated to other users. They are, in essence, in a level by themselves. - -To activate singleplayer mode, add an NPC to the level and add the single command "singleplayer" to the level, much like how the "sparringzone" command works. - - ----------------- -| Group Maps | ----------------- - -Like singleplayer levels, group maps allow only players in a group to see each other in a level. Player groups can be managed via the gr.setgroup and gr.setlevelgroup triggeractions (SEE: Graal Reborn special triggeractions). - -Individual levels cannot be set as group levels; instead, an entire map must be specified as a group map. The "groupmaps" server option specifies a comma-delimited list of maps that can contain groups. - - -------------------------------------- -| Graal Reborn special client flags | -------------------------------------- - -There are a few special client flags built into the gserver. These are: -gr.x -gr.y -gr.z - -These flags are used by the -gr_movement weapon included in the server weapons folder to simulate the smooth movement as found in the Graal clients 2.3 and up. - -If you don't want the gserver to recognize these flags, set the flaghack_movement setting to false in serveroptions.txt. - -Also, if flaghack_ip is enabled in the serveroptions.txt file, you can gain access to the following: -gr.ip - - ---------------------------------------- -| Graal Reborn special triggeractions | ---------------------------------------- - -The Graal Reborn gserver has a couple unique triggeractions built into it. They can be enabled/disabled by altering the setting that controls their group in serveroptions.txt. They are as follows: - -Controlled by the setting triggerhack_weapons: - triggeraction 0,0,gr.addweapon,weapon1,weapon2,weapon3; - Adds weapon1, weapon2, and weapon3 to the player's account. - - triggeraction 0,0,gr.deleteweapon,weapon1,weapon2,weapon3; - Removes weapon1, weapon2, and weapon3 from the player's account. - -Controlled by the setting triggerhack_guilds: - triggeraction 0,0,gr.addguildmember,guild,account,nickname; - Adds a player to the specified guild. Nickname is optional. - - triggeraction 0,0,gr.removeguildmember,guild,account; - Removes a player from the specified guild. - - triggeraction 0,0,gr.removeguild,guild; - Removes the guild from the server. - - triggeraction 0,0,gr.setguild,guild,account; - Sets the player's guild tag to the specified guild. - -Controlled by the setting triggerhack_groups: - triggeraction 0,0,gr.setgroup,group; - Adds the player to the specified group. - - triggeraction 0,0,gr.setlevelgroup,group; - Adds all the players in the level to the specified group. - - triggeraction 0,0,gr.setplayergroup,account,group; - Adds the specified player to the specified group. - -Controlled by the setting triggerhack_files: - triggeraction 0,0,gr.appendfile,filename,text; - Opens the file specified, located in the server's logs directory, and appends a line of text. - - triggeraction 0,0,gr.writefile,filename,text; - Opens the file specified, located in the server's logs directory, erases all of its contents, and writes a line of text. - - triggeraction 0,0,gr.readfile,filename,line_pos; - Opens the file specified, located in the server's logs directory, reads the given line number, and returns the contents to the player. - File contents are returned on the following flags: - gr.fileerror: String list. First index is a random number, subsequent indexes are error values. Error 1 = line_pos was outside of range. In this case, the next value is the line number returned. - gr.filedata: The file data. - -Controlled by the setting triggerhack_rc: - triggeraction 0,0,gr.rcchat,Some chat text; - Sends some chat text to any logged in RC's. - -Controlled by the setting triggerhack_execscript: - triggeraction 0,0,gr.es_clear; - Clears the execscript parameter list. - - triggeraction 0,0,gr.es_set,param1,param2,...; - Sets the execscript parameter list. - - triggeraction 0,0,gr.es_append,phrase; - Appends phrase directly to the end of the set parameter list. - - triggeraction 0,0,gr.es,account,script_name; - Sends the execscript to the specified account, or everybody if ALLPLAYERS was specified. - View the execscript/readme.txt file for more information. - -Controlled by the setting triggerhack_props: - triggeraction 0,0,gr.attr1,data; - Sets data on the specified attribute. gr.attr1 - gr.attr30 work. - - triggeraction 0,0,gr.fullhearts,amount; - Sets the player's fullhearts to the specified amount. - -Controlled by the setting triggerhack_levels: - triggeraction 0,0,gr.updatelevel; - Updates the current level. - - triggeraction 0,0,gr.updatelevel,levelname; - Updates the specified level. - -Not controlled by any option: - triggeraction 0,0,gr.npc.move,id,dx,dy,duration,options; - Creates a serverside move command for the specified NPC. - - triggeraction 0,0,gr.npc.setpos,id,x,y; - Sets an NPC's position. - - -------------------- -| Weapon bytecode | -------------------- -Place weapon bytecode in the weapon_bytecode/ folder. Inside each weapon file in weapons/, add the following: -BYTECODE name_of_file - -The gserver will load weapon_bytecode/name_of_file and use the bytecode contained there-in. \ No newline at end of file +Graal Reborn GServer +Created by: Joey, Nalin, dufresnep, Codr, Marlon. +Based off the original work by 39ster. +For their additional work on the old gserver, special thanks go to: + Agret, Beholder, Joey, Marlon, Nalin, and Pac. + +---------------------------- +| Quick Start Instructions | +---------------------------- + +How-to setup a server: + +1) Under the accounts folder, rename the text file 'YOURACCOUNT.txt' to your account name. For example: 'KuJi.txt' +2) Modify defaultaccount.txt to your liking. This is the default settings new players will start with. It can also be modified via RC. +3) Open config/serveroptions.txt and edit it to your liking. Be sure to modify the settings under "Private server options". Help for what these options do are available on the forums and in the file itself. +4) Find the line that starts with "staff=" in config/serveroptions.txt. Replace YOURACCOUNT with your account name. Anybody who needs RC access must be added to this line with their account names separated by commas. Additionally, RC users must have their IP range changed to at least *.*.*.* in their account to connect. +5) Port forward if needed. (Many threads on this topic exist in the forums. If you are having trouble, seek them out. Try the tutorials forum.) Basically, if you are behind a router and your computer isn't set to be the "DMZ", you will need to port forward. +6) Run gserver2.exe -- enjoy. +7) Report any bugs on http://www.graal.in/ + +---------------------------- +| Documentation | +---------------------------- + +Extensive documentation can be found in the /docs/ folder. diff --git a/bin/servers/default/accounts/(npcserver).txt b/bin/servers/default/accounts/(npcserver).txt index d550ab3d4..ced93f667 100644 --- a/bin/servers/default/accounts/(npcserver).txt +++ b/bin/servers/default/accounts/(npcserver).txt @@ -1,47 +1,47 @@ -GRACC001 -NAME (npcserver) -NICK NPC-Server (Server) -COMMUNITYNAME (npcserver) -LEVEL -X 30.00 -Y 30.50 -Z 0.00 -MAXHP 3 -HP 3.00 -RUPEES 0 -ANI idle -ARROWS 10 -BOMBS 5 -GLOVEP 1 -SHIELDP 1 -SWORDP 1 -BOWP 1 -BOW -HEAD head25.png -BODY body.png -SWORD sword1.png -SHIELD shield1.png -COLORS 2,0,10,4,18 -SPRITE 2 -STATUS 20 -MP 0 -AP 50 -APCOUNTER 60 -ONSECS 0 -IP -2115681208 -LANGUAGE English -KILLS 0 -DEATHS 0 -RATING 1500.00 -DEVIATION 350.00 -LASTSPARTIME 0 - -BANNED 0 -BANREASON -BANLENGTH -COMMENTS -EMAIL -LOCALRIGHTS 0 -IPRANGE 0.0.0.0 -LOADONLY 0 -LASTFOLDER +GRACC001 +NAME (npcserver) +NICK NPC-Server (Server) +COMMUNITYNAME (npcserver) +LEVEL +X 30.00 +Y 30.50 +Z 0.00 +MAXHP 3 +HP 3.00 +RUPEES 0 +ANI idle +ARROWS 10 +BOMBS 5 +GLOVEP 1 +SHIELDP 1 +SWORDP 1 +BOWP 1 +BOW +HEAD head25.png +BODY body.png +SWORD sword1.png +SHIELD shield1.png +COLORS 2,0,10,4,18 +SPRITE 2 +STATUS 20 +MP 0 +AP 50 +APCOUNTER 60 +ONSECS 0 +IP -2115681208 +LANGUAGE English +KILLS 0 +DEATHS 0 +RATING 1500.00 +DEVIATION 350.00 +LASTSPARTIME 0 + +BANNED 0 +BANREASON +BANLENGTH +COMMENTS +EMAIL +LOCALRIGHTS 0 +IPRANGE 0.0.0.0 +LOADONLY 0 +LASTFOLDER diff --git a/bin/servers/default/accounts/defaultaccount.txt b/bin/servers/default/accounts/defaultaccount.txt index c8859c106..ed2bac7cc 100644 --- a/bin/servers/default/accounts/defaultaccount.txt +++ b/bin/servers/default/accounts/defaultaccount.txt @@ -1,7 +1,7 @@ GRACC001 NAME default NICK default -COMMUNITYNAME default +COMMUNITYNAME LEVEL onlinestartlocal.nw X 30.00 Y 30.50 diff --git a/bin/servers/default/bootstrap.js b/bin/servers/default/bootstrap.js deleted file mode 100644 index 8814d9cda..000000000 --- a/bin/servers/default/bootstrap.js +++ /dev/null @@ -1,343 +0,0 @@ -'use strict'; - -(function (env) { - /** - * Events -> onCreated(npc, args...) - */ - env.setCallBack("npc.created", function (npc, ...args) { - try { - // Set whether the npc supports these events - env.setNpcEvents(npc, - (npc.onCreated && 1 << 0) | - (npc.onTimeout && 1 << 1) | - (npc.onPlayerChats && 1 << 2) | - (npc.onPlayerEnters && 1 << 3) | - (npc.onPlayerLeaves && 1 << 4) | - (npc.onPlayerTouchsMe && 1 << 5) | - (npc.onPlayerLogin && 1 << 6) | - (npc.onPlayerLogout && 1 << 7) | - (npc.onNpcWarped && 1 << 8) - ); - - if (npc.onCreated) - npc.onCreated.apply(npc, args); - } catch (e) { - env.reportException("NPC Exception at " + npc.levelname + "," + npc.x + "," + npc.y + ": " + e.name + " - " + e.message); - } - }); - - /** - * Event -> onPlayerChats(npc, player, message) - */ - env.setCallBack("npc.playerchats", function (npc, player, message) { - try { - if (npc.onPlayerChats) - npc.onPlayerChats(player, message); - } catch (e) { - env.reportException("NPC Exception at " + npc.levelname + "," + npc.x + "," + npc.y + ": " + e.name + " - " + e.message); - } - }); - - /** - * Event -> onPlayerEnters(npc, player) - */ - env.setCallBack("npc.playerenters", function (npc, player) { - try { - if (npc.onPlayerEnters) - npc.onPlayerEnters(player); - } catch (e) { - env.reportException("NPC Exception at " + npc.levelname + "," + npc.x + "," + npc.y + ": " + e.name + " - " + e.message); - } - }); - - /** - * Event -> onPlayerLeaves(npc, player) - */ - env.setCallBack("npc.playerleaves", function (npc, player) { - try { - if (npc.onPlayerLeaves) - npc.onPlayerLeaves(player); - } catch (e) { - env.reportException("NPC Exception at " + npc.levelname + "," + npc.x + "," + npc.y + ": " + e.name + " - " + e.message); - } - }); - - /** - * Event -> onPlayerTouchsMe(npc, player) - */ - env.setCallBack("npc.playertouchsme", function (npc, player) { - try { - if (npc.onPlayerTouchsMe) - npc.onPlayerTouchsMe(player); - } catch (e) { - env.reportException("NPC Exception at " + npc.levelname + "," + npc.x + "," + npc.y + ": " + e.name + " - " + e.message); - } - }); - - /** - * Event -> onPlayerLogin(npc, player) - */ - env.setCallBack("npc.playerlogin", function (npc, player) { - try { - if (npc.onPlayerLogin) - npc.onPlayerLogin(player); - } catch (e) { - env.reportException(npc.name + " Exception at onPlayerLogin: " + e.name + " - " + e.message); - } - }); - - /** - * Event -> onPlayerLogout(npc, player) - */ - env.setCallBack("npc.playerlogout", function (npc, player) { - try { - if (npc.onPlayerLogout) - npc.onPlayerLogout(player); - } catch (e) { - env.reportException(npc.name + " Exception at onPlayerLogout: " + e.name + " - " + e.message); - } - }); - - /** - * Event -> onTimeout(npc, args...) - */ - env.setCallBack("npc.timeout", function (npc, ...args) { - try { - if (npc.onTimeout) - npc.onTimeout.apply(npc, args); - } catch (e) { - env.reportException("NPC Timeout Exception at " + npc.levelname + "," + npc.x + "," + npc.y + ": " + e.name + " - " + e.message); - } - }); - - /** - * Event -> onNpcWarped(npc) - */ - env.setCallBack("npc.warped", function (npc, ...args) { - try { - if (npc.onNpcWarped) - npc.onNpcWarped.apply(npc, args); - } catch (e) { - env.reportException("NPC Warped Exception at " + npc.levelname + "," + npc.x + "," + npc.y + ": " + e.name + " - " + e.message); - } - }); - - /* - * Event -> Triggeractions - */ - env.setCallBack("npc.trigger", function (npc, func, player, data) { - try { - const params = tokenize(data, ','); - func.call(npc, player, params); - } catch (e) { - env.reportException("NPC Trigger Exception at " + npc.levelname + "," + npc.x + "," + npc.y + ": " + e.name + " - " + e.message); - } - }); - - /** - * Events -> weapon.onCreated(npc, args...) - */ - env.setCallBack("weapon.created", function (weapon, ...args) { - if (weapon.onCreated) - weapon.onCreated.apply(weapon, args); - }); - - /* - * Event -> weapon.onActionServerSide(player, data) - */ - env.setCallBack("weapon.serverside", function (weapon, player, data) { - if (weapon.onActionServerSide) { - const params = tokenize(data, ','); - weapon.onActionServerSide(player, params); - } - }); - - /* - * Global Function -> Tokenize - */ - env.global.tokenize = function(string, sep = ' ') { - let separator = sep[0]; - let insideQuote = false; - let stringList = []; - let currentString = ""; - - let stringLength = string.length; - for (let i = 0; i < stringLength; i++) { - switch (string[i]) { - case separator: { - if (!insideQuote) { - stringList.push(currentString); - currentString = ""; - } - else currentString += string[i]; - - break; - } - - case '\"': { - insideQuote = !insideQuote; - break; - } - - case '\\': { - if (i + 1 < stringLength) { - switch (string[i+1]) { - case '"': - case '\\': - i++; - - default: - currentString += string[i]; - break; - } - } - else currentString += string[i]; - break; - } - - default: { - currentString += string[i]; - break; - } - } - } - - stringList.push(currentString); - return stringList; - }; - - // Math helper functions - (function() { - const _intVecX = [0, -1, 0, 1]; - const _intVecY = [-1, 0, 1, 0]; - - env.global.vecx = function(dir) { - return _intVecX[dir % 4]; - }; - - env.global.vecy = function(dir) { - return _intVecY[dir % 4]; - }; - - env.global.random = function(min, max) { - return Math.random() * (max - min) + min; - }; - - env.global.abs = function(num) { - return Math.abs(num); - }; - - env.global.arctan = function (angle) { - return Math.atan(angle); - }; - - env.global.char = function (code) { - return String.fromCharCode(code); - }; - - env.global.cos = function (angle) { - return Math.cos(angle); - }; - - env.global.exp = function (x) { - return Math.exp(x); - }; - - env.global.float = function(num) { - return parseFloat(num) || 0; - }; - - env.global.getangle = function (dx, dy) { - return Math.atan2(dx, dy); - }; - - env.global.int = function(num) { - return parseInt(num) || 0; - }; - - env.global.log = function (base, x) { - return Math.log(x) / Math.log(base); - }; - - env.global.max = function(n1, n2) { - return Math.max(n1, n2); - }; - - env.global.min = function(n1, n2) { - return Math.min(n1, n2); - }; - - env.global.sin = function (angle) { - return Math.sin(angle); - }; - })(); - - // Server functions - (function() { - env.global.createlevel = function(...args) { - return server.createlevel(...args); - }; - - env.global.httpget = function(...args) { - return server.httpget(...args); - }; - - env.global.httppost = function(...args) { - return server.httppost(...args); - }; - - env.global.findlevel = function(...args) { - return server.findlevel(...args); - }; - - env.global.findnpc = function(...args) { - return server.findnpc(...args); - }; - - env.global.findplayer = function(...args) { - return server.findplayer(...args); - }; - - env.global.setshootparams = function(...args) { - return server.setshootparams(...args); - }; - - env.global.savelog = function(...args) { - return server.savelog(...args); - }; - - env.global.sendtorc = function(...args) { - return server.sendtorc(...args); - }; - - env.global.sendtonc = function(...args) { - return server.sendtonc(...args); - }; - - env.global.loadstring = function (...args) { - return server.loadstring(...args); - }; - - env.global.savestring = function (...args) { - return server.savestring(...args); - }; - - Object.defineProperty(env.global, 'allplayers', { - get: function() { - return server.players; - } - }); - - Object.defineProperty(env.global, 'timevar', { - get: function() { - return server.timevar; - } - }); - - Object.defineProperty(env.global, 'timevar2', { - get: function() { - return server.timevar2; - } - }); - })(); -}); diff --git a/bin/servers/default/config/adminconfig.txt b/bin/servers/default/config/adminconfig.txt index 28543e776..779c10230 100644 --- a/bin/servers/default/config/adminconfig.txt +++ b/bin/servers/default/config/adminconfig.txt @@ -11,4 +11,4 @@ hq_password = hq_level = 1 # NPC-Server address (to send to RC's, should be same as gserver) -ns_ip = 127.0.0.1 \ No newline at end of file +ns_ip = AUTO diff --git a/bin/servers/default/config/allowedversions.txt b/bin/servers/default/config/allowedversions.txt index 8ec88e620..a53af3560 100644 --- a/bin/servers/default/config/allowedversions.txt +++ b/bin/servers/default/config/allowedversions.txt @@ -1,43 +1,44 @@ -// List of version strings. -// Uncomment versions you want to use. -// Use a colon to specify a range of versions. -// For example, to allow 2.171 through 2.31, use: GNW22122:GNW28015 - -//GNW13110 // 1.41r1 -//GNW31101 // 2.1.0.x -//GNW01012 // 2.1.2.x -//GNW23012 // 2.1.3.x -//GNW30042 // 2.1.4.x -//GNW19052 // 2.1.5.0 -//GNW20052 // 2.1.5.1 / 2.1.5.2 -//GNW12102 // 2.1.6.x - -//GNW22122 // 2.1.7.x - -//GNW21033 // 2.1.8.x -//GNW15053 // 2.1.9.x -//GNW28063 // 2.2.0.0 -//GNW01113 // 2.2.1.1 - -GNW03014 // 2.2.2.0 - -//GNW14015 // 2.3.0.0 -//GNW28015 // 2.3.1.0 - -//G3D16053 // 3.0.0.0 -//G3D27063 // 3.0.1.0 -//G3D03014 // 3.0.4.1 - -//G3D28095 // 4.0.2.11 -//G3D09125 // 4.0.3.4 -//G3D17026 // 4.0.4.2 -//G3D26076 // 4.1.1.0 -//G3D20126 // 4.2.0.8 - -//G3D22067 // 5.0.0.7 -//G3D14097 // 5.1.2.0 -//G3D3007A // 6.0.0.7 -//G3D3007A // 6.0.1.5 -//G3D2505C // 6.0.3.4 -//G3D0311C // 6.0.3.7 (Windows) -//G3D0511C // 6.0.3.7 (Linux) +[generation-range] + +original = GNW13110 // 1.41r1 +classic = GNW03014:GNW28015 // 2.22 - 2.31 +newmain = G3D22067 // 5.007 +modern = G3D14097:G3D0511C // 5.12 - 6.037 + +// Specify the range of versions that are allowed for each server generation. +// Entries can be comma-separated or a range can be specified by using a colon between two versions. + +//GNW13110 // 1.41r1 + +//GNW31101 // 2.1.0.x +//GNW01012 // 2.1.2.x +//GNW23012 // 2.1.3.x +//GNW30042 // 2.1.4.x +//GNW19052 // 2.1.5.0 +//GNW20052 // 2.1.5.1 / 2.1.5.2 +//GNW12102 // 2.1.6.x +//GNW22122 // 2.1.7.x - 2.171 +//GNW21033 // 2.1.8.x +//GNW15053 // 2.1.9.x +//GNW28063 // 2.2.0.0 +//GNW01113 // 2.2.1.1 +//GNW03014 // 2.2.2.0 - 2.22 +//GNW14015 // 2.3.0.0 +//GNW28015 // 2.3.1.0 - 2.31 +//G3D16053 // 3.0.0.0 +//G3D27063 // 3.0.1.0 +//G3D03014 // 3.0.4.1 + +//G3D28095 // 4.0.2.11 +//G3D09125 // 4.0.3.4 +//G3D17026 // 4.0.4.2 +//G3D26076 // 4.1.1.0 +//G3D20126 // 4.2.0.8 +//G3D22067 // 5.0.0.7 - 5.007 + +//G3D14097 // 5.1.2.0 - 5.12 +//G3D3007A // 6.0.0.7 +//G3D3007A // 6.0.1.5 +//G3D2505C // 6.0.3.4 +//G3D0311C // 6.0.3.7 (Windows) +//G3D0511C // 6.0.3.7 (Linux) diff --git a/bin/servers/default/config/rchelp.txt b/bin/servers/default/config/rchelp.txt index 4078ff2ca..b31a0c852 100644 --- a/bin/servers/default/config/rchelp.txt +++ b/bin/servers/default/config/rchelp.txt @@ -5,17 +5,18 @@ Available commands for GServer: /open accountname: Opens the player's attributes window. /openacc accountname: Opens the player's account window. /opencomments accountname: Opens the player's comments window. +/openaccess accountname: ? /openban accountname: Opens the player's ban info. /openrights accountname: Opens the player's rights window. /reset accountname: Resets the account. -/refreshservermessage: Reloads servermessage.html. -/refreshfilesystem: Reloads the server's known file list. /updatelevel level[,level]: Reloads levels from hard disk. /updatelevelall: updates all loaded levels. /restartserver: Restarts the server, kicking all joined players. /reloadserver: Reloads the server configuration files. /updateserverhq: Sends the ServerHQ information to the serverlist. -/reloadwordfilter: Reloads the word filter rules. -/reloadipbans: Reloads the ip bans. -/reloadweapons: Reloads the weapons from disk. -/find file: Finds a file. Accepts wildcards. \ No newline at end of file +/serveruptime: Displays the server uptime. +/savenpcs: Saves all database NPCs. +/stats: Displays NPC script execution statistics. +/find file: Finds a file. Accepts wildcards. +/synctranslation [language]: Scans for any new translation entries. +/generatetranslationstubs: Generates translation files for all languages. diff --git a/bin/servers/default/config/serveroptions.txt b/bin/servers/default/config/serveroptions.txt index 1e10adbe2..708e12f07 100644 --- a/bin/servers/default/config/serveroptions.txt +++ b/bin/servers/default/config/serveroptions.txt @@ -11,7 +11,10 @@ unstickmex = 30 unstickmey = 30.5 unstickmetime = 30 -# Players in these levels can''t warp out nor can they PM other players. +# List of weapon names (comma separated) that will be given to the player each time they connect. +protectedweapons = + +# Players in these levels can`t warp out nor can they PM other players. jaillevels = police2.graal,police4.graal # Enable/disable explosions. @@ -24,7 +27,10 @@ setshieldallowed = true setswordallowed = true setcolorsallowed = true -# Defines the amount of Gralats a player drops with they die. +# Disables the showadmins chat command. +disableshowadmins = false + +# Defines the amount of Gralats a player drops when they die. mindeathgralats = 1 maxdeathgralats = 50 @@ -40,13 +46,23 @@ staff = (Manager),YOURACCOUNT # Enables/disables item dropping from various sources. # bushitems also affects certain tiles other than bushes. -# tiledroprate affects bushitems only. # If making a 1.41 server, set bushitems, vasesdrop, and baddyitems to false as the 1.41 client generates items. bushitems = true vasesdrop = true baddyitems = false -dropitemsdead = true -tiledroprate = 50 + +# The type of items that are dropped by bushes or death. +# Allowed bush items: greenrupee, bluerupee, bombs, heart. GR extension: redrupee, goldrupee, darts. +# Allowed death items: greenrupee, bluerupee, redrupee, goldrupee, bombs, darts. +# bushitemtypes = greenrupee,bluerupee,heart,bombs +# deathitemtypes = greenrupee,bluerupee,redrupee,goldrupee,bombs,darts + +# Value from 0-100 that determines the percent chance the item will drop from bushes. +# Will check in order of the bushitemtypes list, so if the sum adds up to over 100, it will eventually stop early. +# spawnrategreenrupee = 10 +# spawnratebluerupee = 5 +# spawnrateheart = 5 +# spawnratebombs = 5 # If enabled, it will allow negative power swords which will heal players when used. healswords = false @@ -57,14 +73,15 @@ respawntime = 15 horselifetime = 30 baddyrespawntime = 60 -# Allows any player to use the warpto command. +# Controls who can use the warpto command. +# warptoforall - Lets everybody, including players, use warpto. warptoforall = false # Alters the possible status options in the player list. playerlisticons = Online,Away,DND,Eating,Hiding,No PMs,RPing,Sparring,PKing -# Selects what is displayed in the player''s profile. -# Name:=variable, where variable can also be a flag on the player''s account. +# Selects what is displayed in the player`s profile. +# Name:=variable, where variable can also be a flag on the player`s account. profilevars = Kills:=playerkills,Deaths:=playerdeaths,Maxpower:=playerfullhearts,Rating:=playerrating,Alignment:=playerap,Gralat:=playerrupees,Swordpower:=playerswordpower,Spin Attack:=canspin # Global guild settings. @@ -88,32 +105,51 @@ heartlimit = 3 swordlimit = 3 shieldlimit = 3 -# Enables or disables the putnpc script command. -putnpcenabled = true +# Determines if the Z value is ignored or not for serverside touch events (by default touches require Z < 3). +playertouchsmenoz = false -# If true, disable the ability. -dontchangekills = false +# Disables the player z-axis. Locks player.z to undefined. Z values outside of |-50, 170| is undefined. +lockplayerz = false + +# If true, the ratingd (deviation) value does not change. +dontupdateratingd = false # Flag options. # If dontaddserverflags is true, any server. flag changes sent by the client are rejected. -# If cropflags is true, any client and server flags will be cropped to 223 characters. -# The flag name and equal sign are INCLUSIVE! -# It is recommended to not turn this off unless you know the repercussions of doing so. dontaddserverflags = false -cropflags = true # If true, idle players are removed after maxnomovement seconds. disconnectifnotmoved = true maxnomovement = 1200 -# If true, moved push/pull blocks aren''t sent to other players. -clientsidepushpull = true - # If false, it will prevent the player from obtaining items like bomb, bow, superbomb, etc. defaultweapons = true -# List of weapon names (comma separated) that will be given to the player each time they connect. -protectedweapons = +# If true, player deaths and kills cannot be changed via serverside scripting. +dontchangekills = false + +# Enables or disables the putnpc script command. +putnpcenabled = true + +# Enables or disables puthorse. +puthorseenabled = true + +# Allows player triggeractions to be sent to other players. +sendplayertriggers = true + +# Disables player item dropping. +disableitemdropping = false + +# If true, each time an item is dropped, it will be blocked. +# Instead, the Control-NPC will receive an onItemDrop(level,x,y,itemname) action. +# For GS1, it will be `if (itemdrop)` with #p(0)-#p(3) as the arguments. +itemdropevents = false + +# Says if onItemDrop should only be used for gralats, but otherwise spawn classic items. +itemdropeventsonlyforgralats = false + +# Enables serverside onwall detection for shoot projectiles. +projectilesstoponwall = true # List of bigmap.txt type maps used by the server. It lets the server know the level layout # so you can see players move and talk in adjacent levels. @@ -122,13 +158,6 @@ maps = # List of gmaps to be used by the server. gmaps = -# List of group instanced maps used by the server. -# Use full filenames, even for gmaps. -groupmaps = - -# The head used by RCs on the server. -staffhead = head25.png - # Sets the bigmap and minimap to use. # Setting bigmap will break gmaps. # bigmap = maptext,mapimage,defaultx,defaulty @@ -139,27 +168,97 @@ staffhead = head25.png bigmap = minimap = +# Radius, in tiles, in which players receive updates. +# By default it only works on gmaps, but syncbydistanceinside can allow in non-gmap levels. +# NOTE: Not fully implemented yet. To avoid weird problems, don`t use distances less than 64. +# syncdistancex = 192 +# syncdistancey = 192 +# syncbydistanceinside = false + +# Controls if the NPC-Server processes scripts when there are no players. +# sleepwhennoplayers = true + +# Lets NPC-Server scripts permanently alter levels. +# savelevels = false + +# If true, and savelevels is true, then updateboard2 commands are automatically saved to the level file. +# levelsautosave = true + +# Controls if levels use the new tileset layout server-side, or a match list of levels that will (file-globbing). +# newtilesets = false +# newtilesetlevels = + +# Enables extended v6 body colors (disable v5 clients if using this). +# enableexbodycolors = true/false + +# -------------------------------------------------------------------------------------- +# --- Public Graal Reborn non-standard extensions. --- +# -------------------------------------------------------------------------------------- + +# If true, moved push/pull blocks aren`t sent to other players. +clientsidepushpull = true + +# List of group instanced maps used by the server. +# Comma-delimited list of level names in glob format. +# Make sure your glob doesn't match both the gmap and its level files. +# groupmaps = + +# The head used by RCs on the server. +staffhead = head25.png + +# If cropflags is true, any client and server flags will be cropped to 223 characters. +# The flag name and equal sign are INCLUSIVE! +# It is recommended to not turn this off unless you know the repercussions of doing so. +cropflags = true + +# The distance that an NPC listens for serverside events, in tiles. +# The client has a hardcoded distance of 10 tiles for trigger events. +# eventdistance = 64 +# triggerdistance = 10 + +# If true, serverside scripted log commands will use the classic format. +classicstylelogs = false + +# Time in seconds after which inactive levels (levels without players) are unloaded from the server. +# If 0, inactive levels are not unloaded. Levels are unloaded during maintenance cycles (every 5 minutes), so this may not be exact. +unloadinactiveleveltime = 600 + # -------------------------------------------------------------------------------------- # --- Private server options. The changestaffacct right is required to alter these. --- # -------------------------------------------------------------------------------------- + # The server details seen from the server list. name = My Server description = My Server url = http://www.graal.in/ +# Sets the server generation. +# The server version controls certain aspects of the server, like how data is sent and saved. +# This option will restrict which clients can join the server, taking precedence over allowedversions.txt. +# GS2 is only usable on newmain and modern. +# original - 1.x +# classic - 2.x/3.x +# newmain - 4.x to 5.007 +# modern - 5.1+ +generation = classic + +# The NPC-Server nickname. +nickname = NPC-Server + # The information of the computer hosting the gserver. This gets sent to people wanting to connect. # If myip is set to AUTO, it uses the IP address exposed to the list server. serverip = AUTO serverport = 14802 serverinterface = AUTO -# The local IP address of the computer. Helps you connect to your server if your router can't route on +# The local IP address of the computer. Helps you connect to your server if your router can`t route on # its WAN-side IP address. Leave it as AUTO unless you know what you are doing. # If you have a Linux server, you will want to change this, though. localip = AUTO +upnp = true # Specifies the location of the list server. -# DON'T CHANGE IF YOU DON'T KNOW WHAT YOU ARE DOING. +# DON`T CHANGE IF YOU DON`T KNOW WHAT YOU ARE DOING. listip = listserver.graal.in listport = 14900 @@ -172,28 +271,9 @@ onlystaff = false # Set to true to disable the folder configuration. nofoldersconfig = false -# Determines whether or not to use the old "if (created)" style. -# In the old style, "if (created)" is called for each player that enters the level for their first time. -oldcreated = true - # Determines whether the server handles certain things like signs and links. -# Don't set to true. serverside = false -# Enables triggeraction hacks. -triggerhack_weapons = false # gr.addweapon, gr.deleteweapon -triggerhack_guilds = false # gr.addguildmember, gr.removeguildmember, gr.removeguild, gr.setguild -triggerhack_groups = true # gr.setgroup, gr.setlevelgroup -triggerhack_files = false # gr.appendfile, gr.writefile -triggerhack_rc = false # gr.rcchat -triggerhack_execscript = false # gr.es_clear, gr.es_set, gr.es_append, gr.es -triggerhack_props = false # gr.attr1-gr.attr30 -triggerhack_levels = false # gr.updatelevel - -# Enables flag hacks. -flaghack_movement = true # -gr_movement weapon. -flaghack_ip = false # gr.ip - # If folders config is disabled, put additional search directories besides "world" here. # Comma delimited array. sharefolder = @@ -201,6 +281,209 @@ sharefolder = # Sets the language. Currently not implemented. language = English +# If true, DB NPCs can`t be deleted via `destroy`. Does not affect putnpc2 NPCs. +# protectdbnpcs = true + +# -------------------------------------------------------------------------------------- +# --- Private Graal Reborn non-standard extensions. --- +# -------------------------------------------------------------------------------------- + +# If true, level signs are sent to players (only takes effect if serverside is true). +clientsidesigns = false + +# If true, level links are sent to players (only takes effect if serverside is true). +clientsidelinks = false + +# If true, emulates joins on clientside code. +# When true, the server looks for a .txt file with the same name as the join. +# Does not function when serverside is true. +clientsidejoins = false + +# Enables triggeraction hacks. +triggerhack_weapons = false # gr.addweapon, gr.deleteweapon +triggerhack_guilds = false # gr.addguildmember, gr.removeguildmember, gr.removeguild, gr.setguild +triggerhack_groups = false # gr.setgroup, gr.setlevelgroup +triggerhack_files = false # gr.appendfile, gr.writefile +triggerhack_rc = false # gr.rcchat +triggerhack_execscript = false # gr.es_clear, gr.es_set, gr.es_append, gr.es +triggerhack_props = false # gr.attr1-gr.attr30 +triggerhack_levels = false # gr.updatelevel + +# Enables flag hacks. +flaghack_movement = true # -gr_movement weapon. +flaghack_ip = false # gr.ip + +# If true, the server will process all script events, even if the NPC doesn`t check for the event flag. +runallscriptevents = false + # Scripting gs2default = false -nickname = + +# -------------------------------------------------------------------------------------- +# --- Not implemented options. --- +# -------------------------------------------------------------------------------------- + +# Controls who can use the warpto command. +# warptoforlowadmins - Allows Global Admin Level 1 to warp (not implemented). +# warpto - Enables or disables warpto for all staff, including globals (not implemented). +# ignorewarpto - If true, "warpto" chat text is ignored and not processed (not implemented). +# warptoforlowadmins = true +# warpto = true +# ignorewarpto = false + +# Controls ghost mode (not implemented). +# ghostmodeenabled = false +# ghostmodefornotstaff = false + +# Selects whether to disable adding of Bombs, Arrows, and Explosions for Trial accounts. +# limitfreeplayers2 = false + +# How much AP new accounts are given (just update the defaultaccount). +# startap = 50 + +# Loads translations into the server for server-side translations. +# translatedlanguages = en,dn + +# The default language of the server. +# defaultlanguage = en + +# The order in which weapons will be sent to the player. +# weaponorder = + +# Enables or disables the checking of account rights for NC usage. +# npcrights = true + +# Percentage of data packets over an internal limit a player is allowed to send before being suspected of hacking. +# speedhacktolerance = 90 + +# Reports to RC if a level is saved. +# savelevelsmessage = true + +# Lets the NPC-Server log to logs/scriptfunctionslog.txt. +# scriptlogfunctions = write player.nick,write player.guild,call sendtonc,call sendtorc + +# Logs script errors to logs/scripterrors.txt instead of the RC chat. +# logscripterrorstofile = false + +# List of player attributes to send to all players and not just nearby players. +# The attribute numbers are the ones from #P / player.attr[]. Like 1, 2, 3, etc. +# sendtoallattr = + +# Use optimized storage for putnpc2 npcs, activated by setting this.savelocally = true in the NPC script. +# newnpcstorage = true + +# If true, sends the results of the echo() command to all RC users and not just ones with NC rights. +# sendechotorc = false + +# List of SQLite database files allowed to be used with the requestsql2() function. +# database = + +# Controls if server-side gralats are logged. +# disablegralatlog = false + +# If true, hideplayer() only works while a player is under a bush. +# nohidewithoutbush = false + +# Lets an RC without a staff guild tag send mass PMs to players who ignore mass PMs. +# rcofftagoverridesignore = false + +# Arrow types can be `all` or a comma separated list of arrow powers (1,2,3,4). +# If the filter is enabled, attempts to spawn a restricted arrow will be logged to logs/arrowfilter.txt. +# arrowallowedtypes = all +# arrowfilterlog = true + +# Enables or disables putbomb. +# Bomb types can be `all` or a comma separated list of bomb powers (1,2,3). +# If the filter is enabled, attempts to spawn a restricted bomb will be logged to logs/bombfilter.txt. +# putbombenabled = true +# bomballowedtypes = all +# bombfilterlog = true + +# Controls which showimg types are allowed. +# The type can be `all` or ani,img,text. +# The gani/images are a comma separated list (e.g., `idle,walk,sit` or `block.png,block2.png`) +# If the filter is enabled, attempts to show a restricted showimg will be logged to logs/showimgsfilter.txt. +# showimgstypes = all +# showimgsallowedganis = all +# showimgsallowedimages = all +# showimgsfilterlog = true + +# Controls which player attr[] values are allowed. +# The type can be `all` or a comma separated list of strings. +# If the filter is enabled, attempts to set a restricted value will be logged to logs/attrfilter.txt. +# attrallowedganis = all +# attrallowedimages = all +# attrallowedmisc = all +# attrfilterlog = true + +# Controls if ganis can only be used in player attr[] values (and not set on the player themselves). +# If the filter is set, attempts to use a restricted gani will be logged to logs/ganifilter.txt. +# ganionlyattr = +# ganifilterlog = true + +# Distance of players to which projectiles are sent. +# syncdistanceprojectiles = 256 + +# Dynamically adjusts sync distance by player count. +# start-range,end-range,syncdistance, ... repeated +# e.g., syncfactors=0,0,100,75,125,80,175,225,60 +# syncfactors = 0,0,192 + +# Controls if inactive players (hasn`t moved in 10 seconds) are counted for the `syncfactors` command. +# syncfactorinactive = true + +# Enables horse fire and the spyfire command. +# horsefireenabled = true + +# Disables the saving of player attributes. +# dontsaveattributes = false + +# Verifies the arrow/bomb count on server-side. +# arrowcountserverside = true/false +# bombcountserverside = true/false + +# Commands typed in IRC tabs are forwarded to script event onIRCCommand +# forwardirccommands=true/false + +# projectilemaxdistance = number - disallows projectiles if the player is not close to the projectile being sent +# projectilemaxspeed = maximum speed of projectiles +# projectileallowedanis = all or list of ganis which can be used for projectiles +# projectilemaxpersecond = number - maximum number of projectiles per second per player +# projectileaccountparam = number - which parameter is containing the account name, can be verified using this +# projectilefilterlog = true/false - logs to logs/projectilefilter.txt if the player has sent a disallowed projectile +# projectilelogdistance = true/false - says if projectiles which exceed the maxdistance should be logged +# explosionallowedtypes = all or list of allowed explosionpowers, e.g. 1,2 +# explosionmaxpersecond = number - maximum number of explosions per second per player +# explosionmaxdistance = number - disallows explosions if the player is not close to the epxlosion +# explosionmaxradius = number - maximum radius of explosions +# explosiondisallowedlevels = list of levels where explosions are not allowed +# explosiondisallowedinsparringzone = true/false - says if explosions are disallowed in sparring levels +# explosionlogdistance = true/false - says if explosions which exceed the maxdistance should be logged +# explosionfilterlog = true/false - logs to logs/explosionfilter.txt if the player has sent a disallowed explosion + +# Allows player nicknames to be set clientside. +# enableclientsidenick = true + +# Controls if the player`s level is saved to their account. +# saveplayerlevel = true + +# Disables global playerlist, reduces lag on servers with thousands of players. +# noserverlistercpp = true/false + +# List of levels where you don't see other players. +# singleplayerlevels = + +# When the player drops an item, `player.onItemDrop(x,y,itemname)` will be called and the item will not drop. +# itemdropevents2 = true/false + +# Logs attempts of modifying read-only variables to logs/scripterrors.txt +# scriptlogwritetoreadonly = true/false + +# Forces saving all npcs on level exit, can be disabled for optimization. +# savenpcsonlevelcache = true/false + +# Limits how many gralats can be stacked on the ground. +# maxgralatvalue = 0 or number + +# Enables use of //#GS3 code. +# enablegs3 = true/false diff --git a/bin/servers/default/npcs/npcControl-NPC.txt b/bin/servers/default/npcs/npcControl-NPC.txt index fa7b84b43..6dbf655a9 100644 --- a/bin/servers/default/npcs/npcControl-NPC.txt +++ b/bin/servers/default/npcs/npcControl-NPC.txt @@ -30,20 +30,8 @@ SHAPETYPE 0 SHAPE 32 48 SAVEARR 0,0,0,0,0,0,0,0,0,0 NPCSCRIPT -function onCreated() { - server.sendtorc("Script Server Initialized!"); +if (playerlogin) { + sendpm Welcome to your brand new server!; + sendtorc #a has logged in!; } - -function onPlayerLogin(player) { - server.sendtorc(player.account + " has logged in!") -} - -function onPlayerLogout(player) { - server.sendtorc(player.account + " has logged off!") -} - -function onPlayerMessage(player, message) { - // not implemented yet -} - NPCSCRIPTEND diff --git a/bin/servers/default/translations/deutsch.po b/bin/servers/default/translations/.empty similarity index 100% rename from bin/servers/default/translations/deutsch.po rename to bin/servers/default/translations/.empty diff --git "a/bin/servers/default/translations/espa\303\261ol.po" "b/bin/servers/default/translations/espa\303\261ol.po" deleted file mode 100644 index e69de29bb..000000000 diff --git "a/bin/servers/default/translations/fran\303\247ais.po" "b/bin/servers/default/translations/fran\303\247ais.po" deleted file mode 100644 index e69de29bb..000000000 diff --git a/bin/servers/default/translations/italiano.po b/bin/servers/default/translations/italiano.po deleted file mode 100644 index e69de29bb..000000000 diff --git a/bin/servers/default/translations/nederlands.po b/bin/servers/default/translations/nederlands.po deleted file mode 100644 index e69de29bb..000000000 diff --git a/bin/servers/default/translations/norsk.po b/bin/servers/default/translations/norsk.po deleted file mode 100644 index e69de29bb..000000000 diff --git "a/bin/servers/default/translations/portugu\303\252s.po" "b/bin/servers/default/translations/portugu\303\252s.po" deleted file mode 100644 index e69de29bb..000000000 diff --git a/bin/servers/default/translations/svenska.po b/bin/servers/default/translations/svenska.po deleted file mode 100644 index e69de29bb..000000000 diff --git a/bin/servers/default/weapons/weapon-gr_movement.txt b/bin/servers/default/weapons/weapon%045gr%095movement.txt similarity index 100% rename from bin/servers/default/weapons/weapon-gr_movement.txt rename to bin/servers/default/weapons/weapon%045gr%095movement.txt diff --git a/bin/servers/default/world/onlinestartlocal.nw b/bin/servers/default/world/onlinestartlocal.nw index 112a8776c..d00ae76dc 100644 --- a/bin/servers/default/world/onlinestartlocal.nw +++ b/bin/servers/default/world/onlinestartlocal.nw @@ -1,68 +1,68 @@ -GLEVNW01 -BOARD 0 0 64 0 DADBALAMANAOAAABAYAZAaAbAcAdAeAff/BIBJBKBLBMBNBOBJBKBLBMBNBOBPf/AAABAoApAqArAsAtAuAvf/f/+4+5f/f/f/AAABf/B4B5B6DQAZAaAbAcAdAeAZAa -BOARD 0 1 64 0 AZAaAbAcAdAeAff/AoApAqArAsAtAuAv0RBYBZBaBbBcBdBeBZBaBbBcBdBeBfAAABAQA4A5A6A7A8A9A+A/f/f//I/J5+5/ACADACADf/CJCKDgApAqArAsAtAuApAq -BOARD 0 2 64 0 ApAqArAsAtAuAvf/A4A5A6A7A8A9A+A/f/BoBpBqBrDADBALAMDCDDDADBALAMANAOf/BIBJBKBLBMBNBOBPAAABf/f/6O6PASATASATf/f/CaA4A5A6A7A8A9A+A5A6 -BOARD 0 3 64 0 A5A6A7A8A9A+A/AQBIBJBKBLBMBNBOBPf/B4B5B6DQAZAaAbAcAdAeAZAaAbAcAdAeAfBYBZBaBbBcBdBeBfDmDnAQAAABACADKlKmH/DsDt0RBIBJBKBLBMBNBOBJBK -BOARD 0 4 64 0 BJBKBLBMBNBOBPf/BYBZBaBbBcBdBeBfACADCJCKDgApAqArAsAtAuApAqArAsAtAuAvBoBpBqBrBsBtBuBvD2D3Dnf/f/ASATK1K2DsD8D9f/BYBZBaBbBcBdBeBZBa -BOARD 0 5 64 0 BZBaBbBcBdBeBff/BoBpBqBrBsBtBuBvASATf/CaA4A5A6A7A8A9A+A5A6A7A8A9A+A/B4B5B6B7B8B9B+B/CmCnD3CpCpCpCpCpCpD8CsCtf/BoBpBqBrDADBALAMDC -BOARD 0 6 64 0 AMDCDDBsBtBuBvf/B4B5B6B7B8B9B+B/ACADKlKmBIBJBKBLBMBNBOBJBKBLBMBNBOBP0RCJCKCLCMCNCOf/C2C3C4C5C6C5C6C5C6C7C8C9f/B4B5B6DQAZAaAbAcAd -BOARD 0 7 64 0 AcAdAeDRB9B+B/f/0RCJCKCLCMCNCOf/ASATK1K2BYBZBaBbBcBdBeBZBaBbBcBdBeBf5+5/CaCbCcCdACAD0RDHDIDJDKDJDKDJDKDLDMAAABf/CJCKDgApAqArAsAt -BOARD 0 8 64 0 AsAtAuDhCNCOKlKmACADCaCbCcCdf/ACADAQAAABBoBpBqBrDADBALAMDCDDBsBtBuBv6O6Pf/f/ACADASATf/DXDYDZDaDZDaDZDaDbDcf/AiAjf/CaA4A5A6A7A8A9 -BOARD 0 9 64 0 A8A9A+A/Cdf/K1K2ASAT5+5/f/f/f/ASATAAABf/B4B5B6DQAZAaAbAcAdAeDRB9B+B/AAABf/f/ASATACADAAABDoDpDqDpDqDpDqDrACADAyAz5+5/BIBJBKBLBMBN -BOARD 0 10 64 0 BMBNBOBP+4+5f/f/f/f/6O6Pf/f/f/f/f/f/f/ACADCJCKDgApAqArAsAtAuDhCNCO0g0hf/f/f/KlKmASATACADD4D5D6D5D6D5D6D7ASATACAD6O6PBYBZBaBbBcBd -BOARD 0 11 64 0 BcBdBeBf/I/Jf/f/f/AAABAQf/f/AQAAABf/f/ASATf/CaA4A5A6A7A8A9A+A/Cd0R0w0xf/f/f/K1K2f/f/ASATf/f/AAABACADACADKlKmASATAAABBoBpBqBrBsBt -BOARD 0 12 64 0 BsBtBuBv0RAAABf/f/f/AQf/AQAQf/AQf/f/f/f/f/+4+5BIBJBKBLBMBNBOBP0Rf/f/AQf/f/f/AAABf/f/f/AAABf/f/f/ASATASATK1K2f/AAABf/B4B5B6B7B8B9 -BOARD 0 13 64 0 B8B9B+B/f/f/f/f/f/f/f/AQAEAFAGAHf/AQAAABf//I/JBYBZBaBbBcBdBeBfAAABf/f/f/f/AiAjAAABAiAjAiAjf/f/AAABACADACADAAABf/f/f/0RCJCKCLCMCN -BOARD 0 14 64 0 CMCNCOACADf/f/f/f/f/AQAQAUAVAWAXAQf/f/f/f/AAABBoBpBqBrBsBtBuBvACADf/AAABf/AyAzAiAjAyAzAyAzf/f/f/f/ASATASATf/f/f/f/f/ACADCaCbCcCd -BOARD 0 15 64 0 CcCdf/ASATf/f/AAABf/f/AQAkAlAmAnAQAQf/f/f/AQf/B4B5B6B7B8B9B+B/ASATf/f/f/f/AiAjAyAzf/AAABf/AiAjf/AAABf/f/f/f/f/f/AAABASATACADACAD -BOARD 0 16 64 0 ACADACADAAABf/f/AAABf/AQA0A1A2A3AQf/f/f/AAABACADCJCKCLCMCNCOACADAAABf/f/f/AyAzf/f/f/f/f/f/AyAzf/f/f/f/f/f/f/f/f/f/f/f/f/ASATASAT -BOARD 0 17 64 0 ASATASATf/f/f/f/f/f/f/f/AQf/AQAAABf/f/f/f/f/ASATf/CaCbCcCdf/ASATf/f/f/f/AAABf/f/f/f/f/AAABf/AAABf/f/AAABf/AAABf/f/f/f/AQf/0RKlKm -BOARD 0 18 64 0 ACADAAABf/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/AAABf/f/AAABf/f/f/f/AAABf/f/f/f/f/f/f/f/AQf/f/f/f/+4+5f/f/f/KpKqf/AAABf/f/f/K1K2 -BOARD 0 19 64 0 ASATf/f/AQf/AAABf/KpKqf/f/f/f/f/f/f/AAABf/f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/f//I/Jf/f/f/K5K6f/f/f/f/f/f/f/f/ -BOARD 0 20 64 0 f/f/f/f/f/f/f/AAABK5K6f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/ARBgARf/f/f/f/AAABf/f/ -BOARD 0 21 64 0 AAABf/f/f/f/f/f/ARBgARf/f/K3f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/AAABf/f/f/f/f/OLf/f/f/f/f/f/f/f/AhBwAhARAQf/AAABf/f/AA -BOARD 0 22 64 0 AQf/f/f/f/f/f/ARAhBwAhf/f/f/f/f/AAABf/f/f/f/AAABf/f/f/f/f/f/f/f/f/K3f/f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/f/AhARf/f/f/f/f/f/ -BOARD 0 23 64 0 f/f/f/AAABf/ARAhf/f/f/f/f/f/f/f/f/f/f/Knf/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/Knf/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/AgKpKqf/f/f/f/ -BOARD 0 24 64 0 f/f/f/f/KpKqAgf/f/f/AAABf/f/f/f/K4f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/ARK5K6AAABf/f/ -BOARD 0 25 64 0 f/f/f/f/K5K6ARf/f/f/f/f/AwAxAwAxf/f/f/AwAxAwAxAwAxf/f/f/f/AwAxAwAxAwAxAAABf/AwAxAwAxAwAxf/f/f/AwAxf/f/f/f/f/f/f/f/Ahf/f/f/f/f/f/ -BOARD 0 26 64 0 f/f/f/f/f/f/Ahf/f/f/AwAxAwAxAwAxAwAxf/AwAxAwAxAwAxAwAxf/f/AwAxAwAxAwAxf/f/f/AwAxAwAxAwAxAAABf/AwAxf/f/Knf/f/f/f/f/AAABf/f/f/f/f/ -BOARD 0 27 64 0 AAABf/f/f/f/f/f/AAABAwAxf/f/f/f/AwAxf/AwAxf/OLf/K3AwAxf/AwAxf/f/AAABAwAxf/AwAxf/f/f/f/AwAxAAABAwAxf/f/f/f/f/f/K4f/f/f/f/f/f/AQf/ -BOARD 0 28 64 0 f/AQf/f/f/f/f/K4f/AwAxf/f/f/AAABf/f/f/AwAxAAABf/f/AwAxf/AwAxK3f/f/f/AwAxf/AwAxAAABf/f/AwAxf/f/AwAxf/f/f/AAABf/f/f/f/f/AAABf/f/f/ -BOARD 0 29 64 0 f/f/f/AAABf/f/f/f/AwAxf/f/AAABf/OLf/f/AwAxf/AAABf/AwAxf/AwAxf/AAABf/AwAxf/AwAxf/f/Knf/AwAxf/f/AwAxf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/ -BOARD 0 30 64 0 f/f/f/f/f/f/f/f/f/AwAxf/Knf/f/f/f/AAABAwAxf/f/f/f/AwAxf/AwAxAAABf/f/AwAxf/AwAxK3f/f/f/AwAxf/f/AwAxf/f/f/f/f/f/f/f/f/f/AQf/f/f/f/ -BOARD 0 31 64 0 f/f/f/f/f/f/AAABf/AwAxf/f/f/K3f/f/f/f/AwAxAwAxAwAxAwAxf/AwAxAwAxAwAxAwAxf/AwAxAwAxAwAxAwAxf/f/AwAxAAABK3f/OLf/f/f/f/AAABf/f/f/AA -BOARD 0 32 64 0 f/f/f/f/f/f/f/f/f/AwAxf/f/f/f/f/AwAxf/AwAxAwAxAwAxf/AAABAwAxAwAxAwAxAwAxf/AwAxAwAxAwAxAwAxf/K3AwAxf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/ -BOARD 0 33 64 0 f/AAABf/f/f/f/f/f/AwAxAAABf/AwAxAwAxf/AwAxf/f/f/AwAxf/f/AwAxf/f/f/f/AwAxf/AwAxf/f/f/f/AwAxf/f/AwAxf/f/f/f/f/f/AAABf/f/f/f/f/f/f/ -BOARD 0 34 64 0 f/f/f/f/AQf/f/f/f/AwAxf/f/f/AwAxAwAxf/AwAxf/AAABAwAxf/f/AwAxf/f/AAABAwAxf/AwAxf/f/f/K4AwAxf/f/AwAxf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/ -BOARD 0 35 64 0 f/f/f/f/AAABf/AAABAwAxf/f/f/f/f/AwAxf/AwAxf/Knf/f/AwAxf/AwAxK4f/OLf/AwAxf/AwAxf/AAABf/AwAxOLf/AwAxf/f/AAABKnf/f/f/f/f/f/f/f/f/f/ -BOARD 0 36 64 0 f/f/f/f/f/f/K4f/AAABAwAxf/K4AAABAwAxf/AwAxf/f/f/f/AwAxf/AwAxf/f/f/f/AwAxf/AwAxf/f/f/f/AwAxf/f/AwAxf/f/f/f/f/f/f/f/AAABf/f/f/AAAB -BOARD 0 37 64 0 f/AQf/f/f/f/ARf/f/f/AwAxAwAxAwAxAwAxf/AwAxf/f/f/K4AwAxf/AwAxAAABf/K3AwAxf/AwAxK3f/AAABAwAxf/f/AwAxAwAxAwAxAwAxf/f/ARf/f/f/AAABAQ -BOARD 0 38 64 0 AAABf/f/KpKqAgAAABf/K3f/AwAxAwAxf/f/f/AwAxf/f/f/f/AwAxf/AwAxf/f/f/f/AwAxf/AwAxf/f/f/f/AwAxf/f/AwAxAwAxAwAxAwAxf/f/AgKpKqf/f/f/f/ -BOARD 0 39 64 0 f/f/f/f/K5K6ARf/f/f/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/Knf/f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/ARK5K6f/f/f/f/ -BOARD 0 40 64 0 f/f/f/AAABf/AhARf/f/f/Knf/f/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/K3f/f/f/f/f/f/f/f/f/f/f/f/f/ARAhf/f/f/f/f/f/ -BOARD 0 41 64 0 f/f/f/f/f/f/f/AhARBgARf/f/OLf/f/f/f/AAABf/f/f/AAABf/f/K4f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/OLf/f/AAABARBgARAhAAABf/f/f/f/f/ -BOARD 0 42 64 0 f/f/f/f/f/AQf/f/AhBwAhABf/f/f/K4f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/OLf/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/f/AhBwAhf/f/AAABf/f/f/f/ -BOARD 0 43 64 0 AAABf/f/AAABf/f/f/KpKqAAABf/f/f/f/f/f/AAABf/f/f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/AQKpKqf/f/f/f/AQf/f/f/ -BOARD 0 44 64 0 f/f/f/AAABAAABf/f/K5K6f/f/f/f/f/f/f/f/f/KpKqf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/AAABf/f/f/f/f/K5K6f/f/f/f/AAABf/AA -BOARD 0 45 64 0 f/f/f/f/AAABAAABf/f/f/f/f/f/f/f/AQf/f/f/K5K6f/f/f/f/f/AQf/f/f/AAABf/f/0R0Rf/f/f/f/f/FKFLAAABf/AQf/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/ -BOARD 0 46 64 0 f/f/ACADf/AAABf/AQf/f/f/f/f/f/f/f/AAABLILJLKKrf/f/f/f/AAABf/f/f/f/f/f/f/0g0h0Rf/f/f/FaFbFKFLAAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/AAAB -BOARD 0 47 64 0 AQf/ASATACADf/AAABf/f/f/f/f/f/f/f/KpKqLYLZLaK7KpKqf/f/f/f/f/f/f/f/f/f/f/0w0xFKFLf/f/f/f/FaFbf/f/G2HFHXH/f/f/f/f/AAABf/f/f/f/f/AA -BOARD 0 48 64 0 f/AAAB0RASATACADf/f/f/f/f/f/f/f/f/K5K6LYLZLaK7K5K6f/f/f/f/f/f/f/H/f/AAAB0Rf/FaFbf/AAABf/f/f/f/f/HkGOHnHXf/f/f/f/f/AQACAD+4+5f/f/ -BOARD 0 49 64 0 f/f/f/f/ACADASATf/f/AAABf/f/f/f/AAABf/LoLpLqLLf/AAABf/f/f/f/f/f/f/f/f/f/f/AAABf/0Rf/f/G2HEHFHFHFHGH1GOG7AAABf/f/ACADASAT/I/Jf/f/ -BOARD 0 50 64 0 HEHXf/f/ASATAAABf/f/f/f/f/f/f/+4+5f/f/f/KpKqf/f/f/ACADf/f/f/ACADf/AQf/f/f/f/f/f/f/f/G2HGGOGOGOH1H0GOG3HHf/AAABf/ASATf/5+5/f/f/f/ -BOARD 0 51 64 0 H1HnHEHXf/f/f/f/f/f/f/f/f/AQf//I/Jf/f/f/K5K6f/f/f/ASATf/AAABASATf/f/f/f/f/G2HEHFHXf/HkGOH1H1H0GOG3H2HHf/f/f/f/f/f/AAAB6O6Pf/G2HG -BOARD 0 52 64 0 GOGOGOHnHEHFHEHFHXf/f/f/f/f/f/AAABf/f/f/AAABf/f/AAABf/AAAB5+5/f/f/f/G2HEHFHGH1GOHnHXGmGWGOGOG3H2HHf/G2HEHFHEHFHEHXf/f/f/AAABHUH1 -BOARD 0 53 64 0 H6GOGOGOGOH1GOH1HnHEHXAAABf/f/f/f/f/f/f/AQf/f/f/f/f/f/f/f/6O6Pf/f/G2HGGOGOGOGOGOH0G7H/GmH2H3HHH/G2HEHGGOGOH0H1GOHnHFHXf/f/f/HkGO -BOARD 0 54 64 0 FCFCFCFCH6GOGOGOGOH0HnHEHFHXf/f/f/f/G2HEHFHEHFHXf/f/f/f/f/f/AAABf/HkH1GOGOHIFCH6GOG7f/AQf/G2HEHFHGGOGOGOH1GOGOGOGOGOG7f/AQG2HGGO -BOARD 0 55 64 0 FDFEFCFCFCH6GOGOH1GOGOGOGOHnHXG2HEHFHGGOH1GOGOHnHEHXf/f/AQf/f/f/f/HkGOH1GOFCFCFCGOHnHFHEHFHGGOGOGOGOGOGOGOGOHIFCH6GOHnHEHFHGGOHI -BOARD 0 56 64 0 FCFCFCFCFCFDFEFCH6GOGOGOH1GOHnHGGOGOH0GOGOGOGOGOGOHnHXf/f/f/G2HEHFHGGOGOHIFCFCFCGOGOGOGOH1GOGOGOHIFCFCFCFCFCFCFCFCGOH1GOGOH0GOFC -BOARD 0 57 64 0 HVHWFCFCFCFCFCFCFCFCFCH6GOGOGOGOH1GOGOHIFCFCFCH6H1GOG7f/G2HEHGGOGOGOGOHIFCFCFDFEH6GOH1GOGOGOHIFCFCFCFCFCFCFCFCFCFCH6GOGOGOGOHIFC -BOARD 0 58 64 0 HlHmFCFSFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCH6H1G7G2HGH0GOH1GOHIFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFSFCFCFDFEFCFCGOGOHIFCFCFC -BOARD 0 59 64 0 FCFCFCFCFCFCHVHWFCFSFCFCFCFCFCFDFEFCFCFCFCFCFSFCFCGOHnHGH1GOHIFCFCFSFCFCFCFCFCFCFCFCFCFSFCFCFCFCFCFCFCFCFCFCFCFCFCFCH6HIFCFDFEFC -BOARD 0 60 64 0 FDFEFCFCFCFCHlHmFCFCFCFCFCHVHWFCFCFCFCHVHWFCFCFCFCH6GOGOGOHIFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCHVHWFCFCFCFCFCFCFCFCFCFCFCFCFCHV -BOARD 0 61 64 0 FCFCFCFCFCFCFDFEFCFCFCFCFCHlHmFCFCFCFCHlHmFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCHVHWFCFCFC4c484dFCFCHlHmFCFCFCFCFCFCFCFCFCHVHWFCFCHl -BOARD 0 62 64 0 FCFCFCFSFCFC4c4848484dFCFCFCFCFSFCFCFDFEFCHVHWFCFCFDFEFCFCFCFCFCFCFCFCFCFCFSHlHmFCFC4c484848FCFCFCFDFEHVHWFCFCFSFCHVHWHlHmFCFCFS -BOARD 0 63 64 0 FCHVHWFCFC4c4848484848484dFCFCFCFCFCFCFCFCHlHmFCFCFCFCFCFCFCFCFCFDFEFCFCFCFCFCFS4c48484848484dFCFCFCFCHlHmFCFCFCFCHlHmFDFEFCFCFC -CHEST 20 24 greenrupee 0 -CHEST 24 33 spinattack 0 -CHEST 15 32 fullheart 0 +GLEVNW01 +BOARD 0 0 64 0 DADBALAMANAOAAABAYAZAaAbAcAdAeAff/BIBJBKBLBMBNBOBJBKBLBMBNBOBPf/AAABAoApAqArAsAtAuAvf/f/+4+5f/f/f/AAABf/B4B5B6DQAZAaAbAcAdAeAZAa +BOARD 0 1 64 0 AZAaAbAcAdAeAff/AoApAqArAsAtAuAv0RBYBZBaBbBcBdBeBZBaBbBcBdBeBfAAABAQA4A5A6A7A8A9A+A/f/f//I/J5+5/ACADACADf/CJCKDgApAqArAsAtAuApAq +BOARD 0 2 64 0 ApAqArAsAtAuAvf/A4A5A6A7A8A9A+A/f/BoBpBqBrDADBALAMDCDDDADBALAMANAOf/BIBJBKBLBMBNBOBPAAABf/f/6O6PASATASATf/f/CaA4A5A6A7A8A9A+A5A6 +BOARD 0 3 64 0 A5A6A7A8A9A+A/AQBIBJBKBLBMBNBOBPf/B4B5B6DQAZAaAbAcAdAeAZAaAbAcAdAeAfBYBZBaBbBcBdBeBfDmDnAQAAABACADKlKmH/DsDt0RBIBJBKBLBMBNBOBJBK +BOARD 0 4 64 0 BJBKBLBMBNBOBPf/BYBZBaBbBcBdBeBfACADCJCKDgApAqArAsAtAuApAqArAsAtAuAvBoBpBqBrBsBtBuBvD2D3Dnf/f/ASATK1K2DsD8D9f/BYBZBaBbBcBdBeBZBa +BOARD 0 5 64 0 BZBaBbBcBdBeBff/BoBpBqBrBsBtBuBvASATf/CaA4A5A6A7A8A9A+A5A6A7A8A9A+A/B4B5B6B7B8B9B+B/CmCnD3CpCpCpCpCpCpD8CsCtf/BoBpBqBrDADBALAMDC +BOARD 0 6 64 0 AMDCDDBsBtBuBvf/B4B5B6B7B8B9B+B/ACADKlKmBIBJBKBLBMBNBOBJBKBLBMBNBOBP0RCJCKCLCMCNCOf/C2C3C4C5C6C5C6C5C6C7C8C9f/B4B5B6DQAZAaAbAcAd +BOARD 0 7 64 0 AcAdAeDRB9B+B/f/0RCJCKCLCMCNCOf/ASATK1K2BYBZBaBbBcBdBeBZBaBbBcBdBeBf5+5/CaCbCcCdACAD0RDHDIDJDKDJDKDJDKDLDMAAABf/CJCKDgApAqArAsAt +BOARD 0 8 64 0 AsAtAuDhCNCOKlKmACADCaCbCcCdf/ACADAQAAABBoBpBqBrDADBALAMDCDDBsBtBuBv6O6Pf/f/ACADASATf/DXDYDZDaDZDaDZDaDbDcf/AiAjf/CaA4A5A6A7A8A9 +BOARD 0 9 64 0 A8A9A+A/Cdf/K1K2ASAT5+5/f/f/f/ASATAAABf/B4B5B6DQAZAaAbAcAdAeDRB9B+B/AAABf/f/ASATACADAAABDoDpDqDpDqDpDqDrACADAyAz5+5/BIBJBKBLBMBN +BOARD 0 10 64 0 BMBNBOBP+4+5f/f/f/f/6O6Pf/f/f/f/f/f/f/ACADCJCKDgApAqArAsAtAuDhCNCO0g0hf/f/f/KlKmASATACADD4D5D6D5D6D5D6D7ASATACAD6O6PBYBZBaBbBcBd +BOARD 0 11 64 0 BcBdBeBf/I/Jf/f/f/AAABAQf/f/AQAAABf/f/ASATf/CaA4A5A6A7A8A9A+A/Cd0R0w0xf/f/f/K1K2f/f/ASATf/f/AAABACADACADKlKmASATAAABBoBpBqBrBsBt +BOARD 0 12 64 0 BsBtBuBv0RAAABf/f/f/AQf/AQAQf/AQf/f/f/f/f/+4+5BIBJBKBLBMBNBOBP0Rf/f/AQf/f/f/AAABf/f/f/AAABf/f/f/ASATASATK1K2f/AAABf/B4B5B6B7B8B9 +BOARD 0 13 64 0 B8B9B+B/f/f/f/f/f/f/f/AQAEAFAGAHf/AQAAABf//I/JBYBZBaBbBcBdBeBfAAABf/f/f/f/AiAjAAABAiAjAiAjf/f/AAABACADACADAAABf/f/f/0RCJCKCLCMCN +BOARD 0 14 64 0 CMCNCOACADf/f/f/f/f/AQAQAUAVAWAXAQf/f/f/f/AAABBoBpBqBrBsBtBuBvACADf/AAABf/AyAzAiAjAyAzAyAzf/f/f/f/ASATASATf/f/f/f/f/ACADCaCbCcCd +BOARD 0 15 64 0 CcCdf/ASATf/f/AAABf/f/AQAkAlAmAnAQAQf/f/f/AQf/B4B5B6B7B8B9B+B/ASATf/f/f/f/AiAjAyAzf/AAABf/AiAjf/AAABf/f/f/f/f/f/AAABASATACADACAD +BOARD 0 16 64 0 ACADACADAAABf/f/AAABf/AQA0A1A2A3AQf/f/f/AAABACADCJCKCLCMCNCOACADAAABf/f/f/AyAzf/f/f/f/f/f/AyAzf/f/f/f/f/f/f/f/f/f/f/f/f/ASATASAT +BOARD 0 17 64 0 ASATASATf/f/f/f/f/f/f/f/AQf/AQAAABf/f/f/f/f/ASATf/CaCbCcCdf/ASATf/f/f/f/AAABf/f/f/f/f/AAABf/AAABf/f/AAABf/AAABf/f/f/f/AQf/0RKlKm +BOARD 0 18 64 0 ACADAAABf/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/AAABf/f/AAABf/f/f/f/AAABf/f/f/f/f/f/f/f/AQf/f/f/f/+4+5f/f/f/KpKqf/AAABf/f/f/K1K2 +BOARD 0 19 64 0 ASATf/f/AQf/AAABf/KpKqf/f/f/f/f/f/f/AAABf/f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/f//I/Jf/f/f/K5K6f/f/f/f/f/f/f/f/ +BOARD 0 20 64 0 f/f/f/f/f/f/f/AAABK5K6f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/ARBgARf/f/f/f/AAABf/f/ +BOARD 0 21 64 0 AAABf/f/f/f/f/f/ARBgARf/f/K3f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/AAABf/f/f/f/f/OLf/f/f/f/f/f/f/f/AhBwAhARAQf/AAABf/f/AA +BOARD 0 22 64 0 AQf/f/f/f/f/f/ARAhBwAhf/f/f/f/f/AAABf/f/f/f/AAABf/f/f/f/f/f/f/f/f/K3f/f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/f/AhARf/f/f/f/f/f/ +BOARD 0 23 64 0 f/f/f/AAABf/ARAhf/f/f/f/f/f/f/f/f/f/f/Knf/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/Knf/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/AgKpKqf/f/f/f/ +BOARD 0 24 64 0 f/f/f/f/KpKqAgf/f/f/AAABf/f/f/f/K4f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/ARK5K6AAABf/f/ +BOARD 0 25 64 0 f/f/f/f/K5K6ARf/f/f/f/f/AwAxAwAxf/f/f/AwAxAwAxAwAxf/f/f/f/AwAxAwAxAwAxAAABf/AwAxAwAxAwAxf/f/f/AwAxf/f/f/f/f/f/f/f/Ahf/f/f/f/f/f/ +BOARD 0 26 64 0 f/f/f/f/f/f/Ahf/f/f/AwAxAwAxAwAxAwAxf/AwAxAwAxAwAxAwAxf/f/AwAxAwAxAwAxf/f/f/AwAxAwAxAwAxAAABf/AwAxf/f/Knf/f/f/f/f/AAABf/f/f/f/f/ +BOARD 0 27 64 0 AAABf/f/f/f/f/f/AAABAwAxf/f/f/f/AwAxf/AwAxf/OLf/K3AwAxf/AwAxf/f/AAABAwAxf/AwAxf/f/f/f/AwAxAAABAwAxf/f/f/f/f/f/K4f/f/f/f/f/f/AQf/ +BOARD 0 28 64 0 f/AQf/f/f/f/f/K4f/AwAxf/f/f/AAABf/f/f/AwAxAAABf/f/AwAxf/AwAxK3f/f/f/AwAxf/AwAxAAABf/f/AwAxf/f/AwAxf/f/f/AAABf/f/f/f/f/AAABf/f/f/ +BOARD 0 29 64 0 f/f/f/AAABf/f/f/f/AwAxf/f/AAABf/OLf/f/AwAxf/AAABf/AwAxf/AwAxf/AAABf/AwAxf/AwAxf/f/Knf/AwAxf/f/AwAxf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/ +BOARD 0 30 64 0 f/f/f/f/f/f/f/f/f/AwAxf/Knf/f/f/f/AAABAwAxf/f/f/f/AwAxf/AwAxAAABf/f/AwAxf/AwAxK3f/f/f/AwAxf/f/AwAxf/f/f/f/f/f/f/f/f/f/AQf/f/f/f/ +BOARD 0 31 64 0 f/f/f/f/f/f/AAABf/AwAxf/f/f/K3f/f/f/f/AwAxAwAxAwAxAwAxf/AwAxAwAxAwAxAwAxf/AwAxAwAxAwAxAwAxf/f/AwAxAAABK3f/OLf/f/f/f/AAABf/f/f/AA +BOARD 0 32 64 0 f/f/f/f/f/f/f/f/f/AwAxf/f/f/f/f/AwAxf/AwAxAwAxAwAxf/AAABAwAxAwAxAwAxAwAxf/AwAxAwAxAwAxAwAxf/K3AwAxf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/ +BOARD 0 33 64 0 f/AAABf/f/f/f/f/f/AwAxAAABf/AwAxAwAxf/AwAxf/f/f/AwAxf/f/AwAxf/f/f/f/AwAxf/AwAxf/f/f/f/AwAxf/f/AwAxf/f/f/f/f/f/AAABf/f/f/f/f/f/f/ +BOARD 0 34 64 0 f/f/f/f/AQf/f/f/f/AwAxf/f/f/AwAxAwAxf/AwAxf/AAABAwAxf/f/AwAxf/f/AAABAwAxf/AwAxf/f/f/K4AwAxf/f/AwAxf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/ +BOARD 0 35 64 0 f/f/f/f/AAABf/AAABAwAxf/f/f/f/f/AwAxf/AwAxf/Knf/f/AwAxf/AwAxK4f/OLf/AwAxf/AwAxf/AAABf/AwAxOLf/AwAxf/f/AAABKnf/f/f/f/f/f/f/f/f/f/ +BOARD 0 36 64 0 f/f/f/f/f/f/K4f/AAABAwAxf/K4AAABAwAxf/AwAxf/f/f/f/AwAxf/AwAxf/f/f/f/AwAxf/AwAxf/f/f/f/AwAxf/f/AwAxf/f/f/f/f/f/f/f/AAABf/f/f/AAAB +BOARD 0 37 64 0 f/AQf/f/f/f/ARf/f/f/AwAxAwAxAwAxAwAxf/AwAxf/f/f/K4AwAxf/AwAxAAABf/K3AwAxf/AwAxK3f/AAABAwAxf/f/AwAxAwAxAwAxAwAxf/f/ARf/f/f/AAABAQ +BOARD 0 38 64 0 AAABf/f/KpKqAgAAABf/K3f/AwAxAwAxf/f/f/AwAxf/f/f/f/AwAxf/AwAxf/f/f/f/AwAxf/AwAxf/f/f/f/AwAxf/f/AwAxAwAxAwAxAwAxf/f/AgKpKqf/f/f/f/ +BOARD 0 39 64 0 f/f/f/f/K5K6ARf/f/f/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/Knf/f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/ARK5K6f/f/f/f/ +BOARD 0 40 64 0 f/f/f/AAABf/AhARf/f/f/Knf/f/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/K3f/f/f/f/f/f/f/f/f/f/f/f/f/ARAhf/f/f/f/f/f/ +BOARD 0 41 64 0 f/f/f/f/f/f/f/AhARBgARf/f/OLf/f/f/f/AAABf/f/f/AAABf/f/K4f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/OLf/f/AAABARBgARAhAAABf/f/f/f/f/ +BOARD 0 42 64 0 f/f/f/f/f/AQf/f/AhBwAhABf/f/f/K4f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/OLf/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/f/AhBwAhf/f/AAABf/f/f/f/ +BOARD 0 43 64 0 AAABf/f/AAABf/f/f/KpKqAAABf/f/f/f/f/f/AAABf/f/f/f/AAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/f/AQKpKqf/f/f/f/AQf/f/f/ +BOARD 0 44 64 0 f/f/f/AAABAAABf/f/K5K6f/f/f/f/f/f/f/f/f/KpKqf/f/f/f/f/f/f/f/f/f/f/f/f/f/f/AAABf/f/f/f/f/f/f/f/AAABf/f/f/f/f/K5K6f/f/f/f/AAABf/AA +BOARD 0 45 64 0 f/f/f/f/AAABAAABf/f/f/f/f/f/f/f/AQf/f/f/K5K6f/f/f/f/f/AQf/f/f/AAABf/f/0R0Rf/f/f/f/f/FKFLAAABf/AQf/f/f/f/f/AAABf/f/f/f/f/f/f/f/f/ +BOARD 0 46 64 0 f/f/ACADf/AAABf/AQf/f/f/f/f/f/f/f/AAABLILJLKKrf/f/f/f/AAABf/f/f/f/f/f/f/0g0h0Rf/f/f/FaFbFKFLAAABf/f/f/f/f/f/f/f/f/f/f/f/f/f/AAAB +BOARD 0 47 64 0 AQf/ASATACADf/AAABf/f/f/f/f/f/f/f/KpKqLYLZLaK7KpKqf/f/f/f/f/f/f/f/f/f/f/0w0xFKFLf/f/f/f/FaFbf/f/G2HFHXH/f/f/f/f/AAABf/f/f/f/f/AA +BOARD 0 48 64 0 f/AAAB0RASATACADf/f/f/f/f/f/f/f/f/K5K6LYLZLaK7K5K6f/f/f/f/f/f/f/H/f/AAAB0Rf/FaFbf/AAABf/f/f/f/f/HkGOHnHXf/f/f/f/f/AQACAD+4+5f/f/ +BOARD 0 49 64 0 f/f/f/f/ACADASATf/f/AAABf/f/f/f/AAABf/LoLpLqLLf/AAABf/f/f/f/f/f/f/f/f/f/f/AAABf/0Rf/f/G2HEHFHFHFHGH1GOG7AAABf/f/ACADASAT/I/Jf/f/ +BOARD 0 50 64 0 HEHXf/f/ASATAAABf/f/f/f/f/f/f/+4+5f/f/f/KpKqf/f/f/ACADf/f/f/ACADf/AQf/f/f/f/f/f/f/f/G2HGGOGOGOH1H0GOG3HHf/AAABf/ASATf/5+5/f/f/f/ +BOARD 0 51 64 0 H1HnHEHXf/f/f/f/f/f/f/f/f/AQf//I/Jf/f/f/K5K6f/f/f/ASATf/AAABASATf/f/f/f/f/G2HEHFHXf/HkGOH1H1H0GOG3H2HHf/f/f/f/f/f/AAAB6O6Pf/G2HG +BOARD 0 52 64 0 GOGOGOHnHEHFHEHFHXf/f/f/f/f/f/AAABf/f/f/AAABf/f/AAABf/AAAB5+5/f/f/f/G2HEHFHGH1GOHnHXGmGWGOGOG3H2HHf/G2HEHFHEHFHEHXf/f/f/AAABHUH1 +BOARD 0 53 64 0 H6GOGOGOGOH1GOH1HnHEHXAAABf/f/f/f/f/f/f/AQf/f/f/f/f/f/f/f/6O6Pf/f/G2HGGOGOGOGOGOH0G7H/GmH2H3HHH/G2HEHGGOGOH0H1GOHnHFHXf/f/f/HkGO +BOARD 0 54 64 0 FCFCFCFCH6GOGOGOGOH0HnHEHFHXf/f/f/f/G2HEHFHEHFHXf/f/f/f/f/f/AAABf/HkH1GOGOHIFCH6GOG7f/AQf/G2HEHFHGGOGOGOH1GOGOGOGOGOG7f/AQG2HGGO +BOARD 0 55 64 0 FDFEFCFCFCH6GOGOH1GOGOGOGOHnHXG2HEHFHGGOH1GOGOHnHEHXf/f/AQf/f/f/f/HkGOH1GOFCFCFCGOHnHFHEHFHGGOGOGOGOGOGOGOGOHIFCH6GOHnHEHFHGGOHI +BOARD 0 56 64 0 FCFCFCFCFCFDFEFCH6GOGOGOH1GOHnHGGOGOH0GOGOGOGOGOGOHnHXf/f/f/G2HEHFHGGOGOHIFCFCFCGOGOGOGOH1GOGOGOHIFCFCFCFCFCFCFCFCGOH1GOGOH0GOFC +BOARD 0 57 64 0 HVHWFCFCFCFCFCFCFCFCFCH6GOGOGOGOH1GOGOHIFCFCFCH6H1GOG7f/G2HEHGGOGOGOGOHIFCFCFDFEH6GOH1GOGOGOHIFCFCFCFCFCFCFCFCFCFCH6GOGOGOGOHIFC +BOARD 0 58 64 0 HlHmFCFSFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCH6H1G7G2HGH0GOH1GOHIFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFSFCFCFDFEFCFCGOGOHIFCFCFC +BOARD 0 59 64 0 FCFCFCFCFCFCHVHWFCFSFCFCFCFCFCFDFEFCFCFCFCFCFSFCFCGOHnHGH1GOHIFCFCFSFCFCFCFCFCFCFCFCFCFSFCFCFCFCFCFCFCFCFCFCFCFCFCFCH6HIFCFDFEFC +BOARD 0 60 64 0 FDFEFCFCFCFCHlHmFCFCFCFCFCHVHWFCFCFCFCHVHWFCFCFCFCH6GOGOGOHIFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCHVHWFCFCFCFCFCFCFCFCFCFCFCFCFCHV +BOARD 0 61 64 0 FCFCFCFCFCFCFDFEFCFCFCFCFCHlHmFCFCFCFCHlHmFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCHVHWFCFCFC4c484dFCFCHlHmFCFCFCFCFCFCFCFCFCHVHWFCFCHl +BOARD 0 62 64 0 FCFCFCFSFCFC4c4848484dFCFCFCFCFSFCFCFDFEFCHVHWFCFCFDFEFCFCFCFCFCFCFCFCFCFCFSHlHmFCFC4c484848FCFCFCFDFEHVHWFCFCFSFCHVHWHlHmFCFCFS +BOARD 0 63 64 0 FCHVHWFCFC4c4848484848484dFCFCFCFCFCFCFCFCHlHmFCFCFCFCFCFCFCFCFCFDFEFCFCFCFCFCFS4c48484848484dFCFCFCFCHlHmFCFCFCFCHlHmFDFEFCFCFC +CHEST 20 24 greenrupee 0 +CHEST 24 33 spinattack 0 +CHEST 15 32 fullheart 0 diff --git a/cmake/AddTest.cmake b/cmake/AddTest.cmake deleted file mode 100644 index 72dee5baa..000000000 --- a/cmake/AddTest.cmake +++ /dev/null @@ -1,40 +0,0 @@ -function(add_test_og TARGET_NAME TARGET_PATH) - cmake_minimum_required(VERSION 3.22) - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) - - file(GLOB_RECURSE TESTS "${TARGET_PATH}/${TARGET_NAME}/*.cpp") - - add_executable(${TARGET_NAME} ${TESTS}) - target_link_libraries(${TARGET_NAME} PRIVATE ${APP_LIBRARY_NAME} Catch2::Catch2WithMain) - if(STATIC) - target_link_options(${TARGET_NAME} PRIVATE -static -fstack-protector) - target_link_libraries(${TARGET_NAME} PUBLIC -static-libgcc -static-libstdc++) - endif() - - target_include_directories(${TARGET_NAME} PUBLIC ${GS2LIB_INCLUDE_DIRECTORY}) - - target_include_directories(${TARGET_NAME} PUBLIC ${GS2COMPILER_INCLUDE_DIRECTORY}) - - target_include_directories(${TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/server/include) - target_include_directories(${TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/server/include/level) - target_include_directories(${TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/server/include/scripting) - target_include_directories(${TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/server/include/scripting/v8) - target_include_directories(${TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/server/include/scripting/interface) - target_include_directories(${TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/server/include/misc) - target_include_directories(${TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/server/include/utilities) - target_include_directories(${TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/server/include/animation) - - add_dependencies(${TARGET_NAME} ${APP_LIBRARY_NAME}) - - if(V8NPCSERVER) - include_directories(${V8_INCLUDE_DIR}) - endif() - - list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) - - include(CTest) - include(Catch) - catch_discover_tests(${TARGET_NAME}) - - message(STATUS "Added test ${TARGET_NAME}") -endfunction() diff --git a/cmake/FindMiniupnpc.cmake b/cmake/FindMiniupnpc.cmake deleted file mode 100644 index ad2004afc..000000000 --- a/cmake/FindMiniupnpc.cmake +++ /dev/null @@ -1,59 +0,0 @@ -# --------------------------------- FindMiniupnpc Start --------------------------------- -# Locate miniupnp library -# This module defines -# MINIUPNP_FOUND, if false, do not try to link to miniupnp -# MINIUPNP_LIBRARY, the miniupnp variant -# MINIUPNP_INCLUDE_DIR, where to find miniupnpc.h and family) -# MINIUPNPC_VERSION_1_7_OR_HIGHER, set if we detect the version of miniupnpc is 1.7 or higher -# -# Note that the expected include convention is -# #include "miniupnpc.h" -# and not -# #include -# This is because, the miniupnpc location is not standardized and may exist -# in locations other than miniupnpc/ - -if (MINIUPNP_INCLUDE_DIR AND MINIUPNP_LIBRARY) - # Already in cache, be silent - set(MINIUPNP_FIND_QUIETLY TRUE) -endif () - -find_path(MINIUPNP_INCLUDE_DIR miniupnpc.h - HINTS $ENV{MINIUPNP_INCLUDE_DIR} - PATH_SUFFIXES miniupnpc -) - -find_library(MINIUPNP_LIBRARY miniupnpc - HINTS $ENV{MINIUPNP_LIBRARY} -) - -find_library(MINIUPNP_STATIC_LIBRARY libminiupnpc.a - HINTS $ENV{MINIUPNP_STATIC_LIBRARY} -) - -set(MINIUPNP_INCLUDE_DIRS ${MINIUPNP_INCLUDE_DIR}) -set(MINIUPNP_LIBRARIES ${MINIUPNP_LIBRARY}) -set(MINIUPNP_STATIC_LIBRARIES ${MINIUPNP_STATIC_LIBRARY}) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - MiniUPnPc DEFAULT_MSG - MINIUPNP_INCLUDE_DIR - MINIUPNP_LIBRARY -) - -IF(MINIUPNPC_FOUND) - file(STRINGS "${MINIUPNP_INCLUDE_DIR}/miniupnpc.h" MINIUPNPC_API_VERSION_STR REGEX "^#define[\t ]+MINIUPNPC_API_VERSION[\t ]+[0-9]+") - if(MINIUPNPC_API_VERSION_STR MATCHES "^#define[\t ]+MINIUPNPC_API_VERSION[\t ]+([0-9]+)") - set(MINIUPNPC_API_VERSION "${CMAKE_MATCH_1}") - if (${MINIUPNPC_API_VERSION} GREATER "10" OR ${MINIUPNPC_API_VERSION} EQUAL "10") - message(STATUS "Found miniupnpc API version " ${MINIUPNPC_API_VERSION}) - set(MINIUPNP_FOUND true) - set(MINIUPNPC_VERSION_1_7_OR_HIGHER true) - endif() - endif() - -ENDIF() - -mark_as_advanced(MINIUPNP_INCLUDE_DIR MINIUPNP_LIBRARY MINIUPNP_STATIC_LIBRARY) -# --------------------------------- FindMiniupnpc End --------------------------------- diff --git a/cmake/FindV8.cmake b/cmake/FindV8.cmake deleted file mode 100644 index 9c6e780c8..000000000 --- a/cmake/FindV8.cmake +++ /dev/null @@ -1,220 +0,0 @@ -# Courtesy of: https://raw.githubusercontent.com/gwaldron/osgearth/master/CMakeModules/FindV8.cmake -# -# Locate V8 -# This module defines -# V8_LIBRARY -# V8_FOUND, if false, do not try to link to V8 -# V8_INCLUDE_DIR, where to find the headers - -message("Looking for monolithic v8...") - -IF (NOT $ENV{V8_DIR} STREQUAL "") - SET(V8_DIR $ENV{V8_DIR}) -ENDIF() - -set(V8_NUGET_NAME "v8-v142-x64") -set(V8_NUGET_REDIST "v8.redist-v142-x64") -set(V8_NUGET_VER "9.1.269.9") -set(V8_NUGET_VERSION "${V8_NUGET_NAME}.${V8_NUGET_VER}") - -SET(V8_LIBRARY_SEARCH_PATHS - ${V8_DIR}/ - ${V8_DIR}/lib/ - ${V8_DIR}/build/Release/lib/ - ${V8_DIR}/build/Release/lib/third_party/icu/ - ${V8_DIR}/build/Release/obj/ - ${V8_DIR}/build/Release/obj/third_party/icu/ - ${V8_DIR}/out/ia32.release/lib.target/ - ${V8_DIR}/out/ia32.release/lib.target/third_party/icu/ - ${V8_DIR}/out/ia32.release/obj/ - ${V8_DIR}/out/ia32.release/obj/third_party/icu/ - ${V8_DIR}/out.gn/ia32.release/lib.target/ - ${V8_DIR}/out.gn/ia32.release/lib.target/third_party/icu/ - ${V8_DIR}/out.gn/ia32.release/obj/ - ${V8_DIR}/out.gn/ia32.release/obj/third_party/icu/ - ${V8_DIR}/out/x64.release/lib.target/ - ${V8_DIR}/out/x64.release/lib.target/third_party/icu/ - ${V8_DIR}/out/x64.release/obj/ - ${V8_DIR}/out/x64.release/obj/third_party/icu/ - ${V8_DIR}/out.gn/x64.release/lib.target/ - ${V8_DIR}/out.gn/x64.release/lib.target/third_party/icu/ - ${V8_DIR}/out.gn/x64.release/obj/ - ${V8_DIR}/out.gn/x64.release/obj/third_party/icu/ - ${V8_DIR}/out.gn/x64.release.sample/lib.target/ - ${V8_DIR}/out.gn/x64.release.sample/lib.target/third_party/icu/ - ${V8_DIR}/out.gn/x64.release.sample/obj/ - ${V8_DIR}/out.gn/x64.release.sample/obj/third_party/icu/ - ${V8_DIR}/out.gn/arm64.release/lib.target/ - ${V8_DIR}/out.gn/arm64.release/lib.target/third_party/icu/ - ${V8_DIR}/out.gn/arm64.release/obj/ - ${V8_DIR}/out.gn/arm64.release/obj/third_party/icu/ - ${V8_DIR}/out.gn/arm64.release.sample/lib.target/ - ${V8_DIR}/out.gn/arm64.release.sample/lib.target/third_party/icu/ - ${V8_DIR}/out.gn/arm64.release.sample/obj/ - ${V8_DIR}/out.gn/arm64.release.sample/obj/third_party/icu/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/x64.release/lib.target/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/x64.release/lib.target/third_party/icu/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/x64.release/obj/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/x64.release/obj/third_party/icu/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/x64.release.sample/lib.target/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/x64.release.sample/lib.target/third_party/icu/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/x64.release.sample/obj/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/x64.release.sample/obj/third_party/icu/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm.release/lib.target/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm.release/lib.target/third_party/icu/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm.release/obj/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm.release/obj/third_party/icu/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm.release.sample/lib.target/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm.release.sample/lib.target/third_party/icu/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm.release.sample/obj/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm.release.sample/obj/third_party/icu/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm64.release/lib.target/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm64.release/lib.target/third_party/icu/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm64.release/obj/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm64.release/obj/third_party/icu/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm64.release.sample/lib.target/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm64.release.sample/lib.target/third_party/icu/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm64.release.sample/obj/ - ${PROJECT_SOURCE_DIR}/dependencies/v8/out.gn/arm64.release.sample/obj/third_party/icu/ - ~/Library/Frameworks - /Library/Frameworks - /usr/local/lib - /usr/lib - /sw/lib - /opt/local/lib - /opt/csw/lib - /opt/lib - /usr/freeware/lib64 - ${TOOLCHAIN_PREFIX}/lib - ${TOOLCHAIN_PREFIX}/usr/lib -) - -FIND_PATH(V8_INCLUDE_DIR v8.h - ${V8_DIR} - ${V8_DIR}/include - ${PROJECT_SOURCE_DIR}/dependencies/v8 - ${PROJECT_SOURCE_DIR}/dependencies/v8/include - ~/Library/Frameworks - /Library/Frameworks - /usr/local/include - /usr/include - /sw/include # Fink - /opt/local/include # DarwinPorts - /opt/csw/include # Blastwave - /opt/include - /usr/freeware/include - /devel -) - -FIND_LIBRARY(V8_LIBRARY - NAMES libv8_monolith.a v8_monolith.a v8_monolith libv8_monolith - PATHS ${V8_LIBRARY_SEARCH_PATHS} -) - -if(NOT V8_LIBRARY OR NOT V8_INCLUDE_DIR) - if(NOT V8_LIBRARY) - message("Couldn't find v8 library.") - endif() - if(NOT V8_INCLUDE_DIR) - message("Couldn't find v8 include dir.") - endif() - - message("Monolith search failed, looking for mingw package") - - find_library(V8_MAIN_LIBRARY libv8.dll.a - PATHS ${V8_LIBRARY_SEARCH_PATHS} - ) - find_library(V8_BASE_LIBRARY libv8_libbase.dll.a - PATHS ${V8_LIBRARY_SEARCH_PATHS} - ) - find_library(V8_PLATFORM_LIBRARY libv8_libplatform.dll.a - PATHS ${V8_LIBRARY_SEARCH_PATHS} - ) - if(V8_MAIN_LIBRARY OR V8_BASE_LIBRARY OR V8_PLATFORM_LIBRARY) - find_file(V8_REDIST_LIB libv8.dll - PATHS ${V8_LIBRARY_SEARCH_PATHS} - ) - get_filename_component(V8_REDIST_DIR ${V8_REDIST_LIB} DIRECTORY CACHE) - endif() - - if(NOT V8_MAIN_LIBRARY OR NOT V8_BASE_LIBRARY OR NOT V8_PLATFORM_LIBRARY) - message("Mingw package search failed, looking for nuget package") - - if (NOT V8_LIBRARY AND NOT V8_INCLUDE_DIR AND MSVC) - file(DOWNLOAD https://dist.nuget.org/win-x86-commandline/latest/nuget.exe "${CMAKE_BINARY_DIR}/nuget.exe") - set(NUGET_COMMAND "${CMAKE_BINARY_DIR}/nuget.exe") - include("${CMAKE_SOURCE_DIR}/cmake/nuget/cmake/NuGetTools.cmake") - # Call this once before any other nuget_* calls. - nuget_initialize() - - # NuGet install icu and flatbuffers packages, and import their CMake export files. - nuget_add_dependencies( - PACKAGE ${V8_NUGET_NAME} VERSION ${V8_NUGET_VER} CMAKE_PREFIX_PATHS installed/x64-windows - ) - endif() - - if(CMAKE_BUILD_TYPE STREQUAL "Release") - message("Searching for Release libraries as chosen") - set(V8_LIBRARY_SEARCH_PATHS - ${CMAKE_SOURCE_DIR}/packages/${V8_NUGET_NAME}/lib/Release - ${V8_DIR}/Release - ${V8_DIR}/lib/Release - ) - FIND_PATH(V8_INCLUDE_DIR v8.h - ${CMAKE_SOURCE_DIR}/packages/${V8_NUGET_NAME}/include - ${V8_DIR}/include - ) - set(V8_REDIST_DIR ${CMAKE_SOURCE_DIR}/packages/${V8_NUGET_REDIST}/lib/Release) - else() - if(NOT CMAKE_BUILD_TYPE) - message("Searching for Debug libraries by default") - elseif(CMAKE_BUILD_TYPE STREQUAL "Debug") - message("Searching for Debug library as chosen") - else() - message("Build type not recognized, searching for Debug libraries") - endif() - set(V8_LIBRARY_SEARCH_PATHS - ${CMAKE_SOURCE_DIR}/packages/${V8_NUGET_NAME}/lib/Debug - ${V8_DIR}/Debug - ${V8_DIR}/lib/Debug - ) - FIND_PATH(V8_INCLUDE_DIR v8.h - ${CMAKE_SOURCE_DIR}/packages/${V8_NUGET_NAME}/include - ${V8_DIR}/include - ) - set(V8_REDIST_DIR ${CMAKE_SOURCE_DIR}/packages/${V8_NUGET_REDIST}/lib/Debug) - endif() - - find_library(V8_MAIN_LIBRARY v8.dll.lib - PATHS ${V8_LIBRARY_SEARCH_PATHS} - ) - find_library(V8_BASE_LIBRARY v8_libbase.dll.lib - PATHS ${V8_LIBRARY_SEARCH_PATHS} - ) - find_library(V8_PLATFORM_LIBRARY v8_libplatform.dll.lib - PATHS ${V8_LIBRARY_SEARCH_PATHS} - ) - endif() - - if(NOT V8_MAIN_LIBRARY OR NOT V8_BASE_LIBRARY OR NOT V8_PLATFORM_LIBRARY) - message("Couldn't find v8 libraries as nuget package") - else() - file(GLOB V8_REDIST_LIBS "${V8_REDIST_DIR}/*.dll" "${V8_REDIST_DIR}/snapshot_blob.bin") - foreach(V8_REDIST_LIB ${V8_REDIST_LIBS}) - get_filename_component(V8_REDIST_LIB_FILE "${V8_REDIST_LIB}" NAME) - file(COPY_FILE ${V8_REDIST_LIB} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${V8_REDIST_LIB_FILE}) - endforeach() - endif() - if(NOT V8_INCLUDE_DIR) - message("Couldn't find v8 include dir as nuget package") - endif() - -endif() - -IF (V8_INCLUDE_DIR AND ((V8_LIBRARY) OR (V8_MAIN_LIBRARY AND V8_BASE_LIBRARY AND V8_PLATFORM_LIBRARY))) - message("V8 found!") - set(V8_FOUND TRUE) -ELSE() - message("V8 lookup failed.") -ENDIF() - diff --git a/cmake/SubdirList.cmake b/cmake/SubdirList.cmake deleted file mode 100644 index 57b62a0ab..000000000 --- a/cmake/SubdirList.cmake +++ /dev/null @@ -1,10 +0,0 @@ -MACRO(subdir_list result curdir) - FILE(GLOB children RELATIVE ${curdir} ${curdir}/*) - SET(dirlist "") - FOREACH(child ${children}) - IF(IS_DIRECTORY ${curdir}/${child}) - LIST(APPEND dirlist ${child}) - ENDIF() - ENDFOREACH() - SET(${result} ${dirlist}) -ENDMACRO() \ No newline at end of file diff --git a/cmake/bin2h.cmake b/cmake/bin2h.cmake deleted file mode 100644 index 0f1464b26..000000000 --- a/cmake/bin2h.cmake +++ /dev/null @@ -1,84 +0,0 @@ -include(CMakeParseArguments) - -# Function to wrap a given string into multiple lines at the given column position. -# Parameters: -# VARIABLE - The name of the CMake variable holding the string. -# AT_COLUMN - The column position at which string will be wrapped. -function(WRAP_STRING) - set(oneValueArgs VARIABLE AT_COLUMN) - cmake_parse_arguments(WRAP_STRING "${options}" "${oneValueArgs}" "" ${ARGN}) - - string(LENGTH ${${WRAP_STRING_VARIABLE}} stringLength) - math(EXPR offset "0") - - while(stringLength GREATER 0) - - if(stringLength GREATER ${WRAP_STRING_AT_COLUMN}) - math(EXPR length "${WRAP_STRING_AT_COLUMN}") - else() - math(EXPR length "${stringLength}") - endif() - - string(SUBSTRING ${${WRAP_STRING_VARIABLE}} ${offset} ${length} line) - set(lines "${lines}\n${line}") - - math(EXPR stringLength "${stringLength} - ${length}") - math(EXPR offset "${offset} + ${length}") - endwhile() - - set(${WRAP_STRING_VARIABLE} "${lines}" PARENT_SCOPE) -endfunction() - -# Function to embed contents of a file as byte array in C/C++ header file(.h). The header file -# will contain a byte array and integer variable holding the size of the array. -# Parameters -# SOURCE_FILE - The path of source file whose contents will be embedded in the header file. -# VARIABLE_NAME - The name of the variable for the byte array. The string "_SIZE" will be append -# to this name and will be used a variable name for size variable. -# HEADER_FILE - The path of header file. -# APPEND - If specified appends to the header file instead of overwriting it -# NULL_TERMINATE - If specified a null byte(zero) will be append to the byte array. This will be -# useful if the source file is a text file and we want to use the file contents -# as string. But the size variable holds size of the byte array without this -# null byte. -# Usage: -# bin2h(SOURCE_FILE "Logo.png" HEADER_FILE "Logo.h" VARIABLE_NAME "LOGO_PNG") -function(BIN2H) - set(options APPEND NULL_TERMINATE) - set(oneValueArgs SOURCE_FILE VARIABLE_NAME HEADER_FILE) - cmake_parse_arguments(BIN2H "${options}" "${oneValueArgs}" "" ${ARGN}) - - # reads source file contents as hex string - file(READ ${BIN2H_SOURCE_FILE} hexString HEX) - string(LENGTH ${hexString} hexStringLength) - - # appends null byte if asked - if(BIN2H_NULL_TERMINATE) - set(hexString "${hexString}00") - endif() - - # wraps the hex string into multiple lines at column 32(i.e. 16 bytes per line) - wrap_string(VARIABLE hexString AT_COLUMN 32) - math(EXPR arraySize "${hexStringLength} / 2") - - # adds '0x' prefix and comma suffix before and after every byte respectively - string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1, " arrayValues ${hexString}) - # removes trailing comma - string(REGEX REPLACE ", $" "" arrayValues ${arrayValues}) - - # converts the variable name into proper C identifier - string(MAKE_C_IDENTIFIER "${BIN2H_VARIABLE_NAME}" BIN2H_VARIABLE_NAME) - string(TOUPPER "${BIN2H_VARIABLE_NAME}" BIN2H_VARIABLE_NAME) - - # declares byte array and the length variables - set(includeDefinition "#include ") - set(arrayDefinition "const unsigned char ${BIN2H_VARIABLE_NAME}[] = { ${arrayValues} };") - set(arraySizeDefinition "const size_t ${BIN2H_VARIABLE_NAME}_SIZE = ${arraySize};") - - set(declarations "${includeDefinition}\n\n${arrayDefinition}\n\n${arraySizeDefinition}\n\n") - if(BIN2H_APPEND) - file(APPEND ${BIN2H_HEADER_FILE} "${declarations}") - else() - file(WRITE ${BIN2H_HEADER_FILE} "${declarations}") - endif() -endfunction() diff --git a/cmake/crosscompile/x86_64-mingw64.cmake b/cmake/crosscompile/x86_64-mingw64.cmake index 14f5988d7..df5a5b11a 100644 --- a/cmake/crosscompile/x86_64-mingw64.cmake +++ b/cmake/crosscompile/x86_64-mingw64.cmake @@ -1,5 +1,10 @@ # the name of the target operating system SET(CMAKE_SYSTEM_NAME Windows) +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) + +set(VCPKG_CMAKE_SYSTEM_NAME MinGW) # which compilers to use for C and C++ SET(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc-posix) @@ -13,6 +18,5 @@ SET(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32/ /usr/i686-w64-mingw32/) # search headers and libraries in the target environment, search # programs in the host environment set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) -set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) -set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) - +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH) diff --git a/cmake/generate_header_file.cmake b/cmake/generate_header_file.cmake deleted file mode 100644 index e3136f196..000000000 --- a/cmake/generate_header_file.cmake +++ /dev/null @@ -1,2 +0,0 @@ -include("${CMAKE_SOURCE_DIR}/cmake/bin2h.cmake") -bin2h(SOURCE_FILE ${SOURCE_FILE} HEADER_FILE ${HEADER_FILE} VARIABLE_NAME ${VARIABLE_NAME} NULL_TERMINATE) \ No newline at end of file diff --git a/cmake/nuget b/cmake/nuget deleted file mode 160000 index 010c0d32c..000000000 --- a/cmake/nuget +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 010c0d32c3ef1f5a2efe31c6955a09373db638af diff --git a/cmake/utility.cmake b/cmake/utility.cmake new file mode 100644 index 000000000..4cc81750d --- /dev/null +++ b/cmake/utility.cmake @@ -0,0 +1,184 @@ +function(set_default_compiler_options target ISTESTTARGET) + + # --- Configure compiler ----------------------------------------------------------- + + set(CMAKE_CXX_STANDARD 23) + set(CMAKE_CXX_STANDARD_REQUIRED True) + target_compile_features(${target} PUBLIC cxx_std_23) + set_target_properties(${target} PROPERTIES CXX_EXTENSIONS OFF) + + if (NOT CONFIG MATCHES "RelWithDebInfo") + set_target_properties(${target} PROPERTIES INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) + endif() + + if(MSVC) + if(MSVC_VERSION GREATER_EQUAL 1910) + target_compile_options(${target} PUBLIC "/permissive-") + endif() + if(MSVC_VERSION GREATER_EQUAL 1914) + target_compile_options(${target} PUBLIC "/Zc:__cplusplus") + endif() + if(MSVC_VERSION GREATER_EQUAL 1925) + target_compile_options(${target} PUBLIC "/Zc:preprocessor") + endif() + + target_compile_options(${target} PUBLIC + "$<$:/guard:cf>" # Control Flow Guard + "$<$:/Qspectre>" # Spectre Mitigation + "$<$:/dynamicdeopt>" # Dynamic Deoptimization + ) + target_link_options(${target} PUBLIC + "$<$:/dynamicdeopt>" # Dynamic Deoptimization + ) + endif() + + # GCC ignore attribute warnings. + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + target_compile_options(${target} PRIVATE + -Wno-attributes + -Wno-narrowing + -Wno-switch + ) + endif() + + # Clang ignore attribute warnings. + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(${target} PRIVATE + -Wno-unknown-attributes + -Wno-narrowing + -Wno-switch + ) + endif() + + # GCC static compile. + if(STATIC AND CMAKE_CXX_COMPILER_ID MATCHES "GNU") + target_compile_options(${target} PUBLIC -static-libstdc++) + endif() + + # MinGW links. + if(MINGW) + target_compile_options(${target} PUBLIC "-mthreads") + target_link_options(${target} PUBLIC "-mthreads") + endif() + + # --- Defines ---------------------------------------------------------------------- + + # Unicode in Windows. + if(MSVC OR MINGW) + target_compile_definitions(${target} PUBLIC UNICODE _UNICODE) + endif() + + # MinGW definitions. + if(MINGW) + target_compile_definitions(${target} PUBLIC -D__STDC_FORMAT_MACROS -D_DEFAULT_SOURCE=1) + endif() + + # Test definitions. + if(TESTS AND ISTESTTARGET) + target_compile_definitions(${target} PUBLIC NOMAIN _NOMAIN) + endif() + + # Debug definitions. + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_definitions(${target} PUBLIC DEBUG _DEBUG) + else() + target_compile_definitions(${target} PUBLIC NDEBUG _NDEBUG) + endif() + + # If windows, set the standard defines. + if(WIN32) + target_compile_definitions(${target} PUBLIC _WIN32 WIN32 _WINDOWS NOMINMAX WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x600) + + # If 64-bit windows... + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + target_compile_definitions(${target} PUBLIC _WIN64 WIN64) + endif() + endif() + + # Platform defines. + if(WIN32) + target_compile_definitions(${target} PUBLIC PLATFORM_WINDOWS) + elseif(UNIX) + target_compile_definitions(${target} PUBLIC PLATFORM_UNIX) + elseif(APPLE) + target_compile_definitions(${target} PUBLIC PLATFORM_APPLE) + endif() + +endfunction() + +MACRO(setup_versioning_data) + # Version number in format X.YY.ZZ + string(REPLACE "." ";" VERSION_LIST ${PROJECT_VERSION}) + list(GET VERSION_LIST 0 VER_X) + list(GET VERSION_LIST 1 VER_Y) + list(GET VERSION_LIST 2 VER_Z) + set(VER_EXTRA "-beta" CACHE STRING "Extra version") + + # Build date Information + string(TIMESTAMP VER_YEAR "%Y") + string(TIMESTAMP VER_MONTH "%m") + string(TIMESTAMP VER_DAY "%d") + string(TIMESTAMP VER_HOUR "%H") + string(TIMESTAMP VER_MINUTE "%M") + + set(VER_EXTRA "${VER_EXTRA} (${VER_YEAR}-${VER_MONTH}-${VER_DAY} ${VER_HOUR}:${VER_MINUTE})") + set(VER_FULL "${VER_X}.${VER_Y}.${VER_Z}${VER_EXTRA}") + + set(APP_CREDITS "Joey, Nalin, Codr, and Cadavre") + set(APP_VENDOR "Preagonal") + + STRING(REGEX REPLACE " " "-" VER_CPACK ${VER_FULL}) + STRING(REGEX REPLACE "[\(]" "" VER_CPACK ${VER_CPACK}) + STRING(REGEX REPLACE "[\)]" "" VER_CPACK ${VER_CPACK}) + STRING(REGEX REPLACE "(-[0-9]+:[0-9]+)" "" VER_CPACK ${VER_CPACK}) +ENDMACRO() + +function(generate_iconfig) + # Generate version header from the above + message(STATUS "[${PROJECT_NAME}] Generating IConfig.h") + configure_file( + ${PROJECT_SOURCE_DIR}/server/include/IConfig.h.in + ${PROJECT_BINARY_DIR}/server/include/IConfig.h + ) +endfunction() + +MACRO(subdir_list result curdir) + FILE(GLOB children RELATIVE ${curdir} ${curdir}/*) + SET(dirlist "") + FOREACH(child ${children}) + IF(IS_DIRECTORY ${curdir}/${child}) + LIST(APPEND dirlist ${child}) + ENDIF() + ENDFOREACH() + SET(${result} ${dirlist}) +ENDMACRO() + +function(add_test_og TARGET_NAME TARGET_PATH) + cmake_minimum_required(VERSION 3.22) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) + + file(GLOB_RECURSE TESTS "${TARGET_PATH}/${TARGET_NAME}/*.cpp") + + add_executable(${TARGET_NAME} ${TESTS}) + target_compile_features(${TARGET_NAME} PUBLIC cxx_std_23) + set_target_properties(${TARGET_NAME} PROPERTIES CXX_EXTENSIONS OFF) + target_link_libraries(${TARGET_NAME} PRIVATE gs2lib ${APP_LIBRARY_NAME_TESTREF} Catch2::Catch2WithMain) + add_dependencies(${TARGET_NAME} gs2lib ${APP_LIBRARY_NAME_TESTREF}) + target_include_directories(${TARGET_NAME} PRIVATE "${gs2lib_SOURCE_DIR}/include") + target_include_directories(${APP_LIBRARY_NAME_TESTREF} PRIVATE "${gs2lib_SOURCE_DIR}/include") + target_link_options(${TARGET_NAME} PRIVATE -static -fstack-protector) + + target_include_directories(${TARGET_NAME} PUBLIC ${GS2LIB_INCLUDE_DIRECTORY}) + target_include_directories(${TARGET_NAME} PUBLIC ${GS2COMPILER_INCLUDE_DIRECTORY}) + target_include_directories(${TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/server/include) + + add_dependencies(${TARGET_NAME} gs2lib ${APP_LIBRARY_NAME_TESTREF}) + + list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) + + include(CTest) + include(Catch) + catch_discover_tests(${TARGET_NAME}) + + message(STATUS "Added test ${TARGET_NAME}") +endfunction() diff --git a/cmake/v8/CMakeLists.txt b/cmake/v8/CMakeLists.txt deleted file mode 100644 index e0dd59ae0..000000000 --- a/cmake/v8/CMakeLists.txt +++ /dev/null @@ -1,121 +0,0 @@ - -set(DEPS ${CMAKE_SOURCE_DIR}/dependencies) -set(BASE ${CMAKE_SOURCE_DIR}/dependencies/v8) -set(BASE_TOOLS ${CMAKE_SOURCE_DIR}/dependencies/depot_tools) -project(v8) - -if(NOT EXISTS "${DEPS}/.gclient") - add_custom_command( - COMMAND - ${BASE_TOOLS}/fetch - v8 - WORKING_DIRECTORY - ${BASE}/../ - DEPENDS - ${BASE_TOOLS}/fetch - OUTPUT - ${BASE}/output-fetch1.txt - ) - - add_custom_command( - COMMAND - ${BASE_TOOLS}/gclient sync -D - WORKING_DIRECTORY - ${BASE}/ - DEPENDS - ${BASE_TOOLS}/gclient - ${BASE}/output-fetch1.txt - OUTPUT - ${BASE}/output-fetch.txt - ) -else() - add_custom_command( - COMMAND - ${BASE_TOOLS}/gclient sync -D - WORKING_DIRECTORY - ${BASE}/ - DEPENDS - ${BASE_TOOLS}/gclient - OUTPUT - ${BASE}/output-fetch.txt - ) -endif() - -add_custom_command( - COMMAND - git - checkout - refs/tags/7.4.288.26 - WORKING_DIRECTORY - ${BASE}/ - DEPENDS - ${BASE}/output-fetch.txt - OUTPUT - ${BASE}/output-gclient1.txt -) - -add_custom_command( - COMMAND - ${BASE_TOOLS}/gclient sync -D - WORKING_DIRECTORY - ${BASE}/ - DEPENDS - ${BASE}/output-gclient1.txt - OUTPUT - ${BASE}/output-gclient2.txt -) - -if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm") - set(V8_TARGET_CPU "arm") -else() - set(V8_TARGET_CPU "x64") -endif() - -if(APPLE) - set(V8_TARGET_OS "mac") -elseif(WIN32) - set(V8_TARGET_OS "win") -else() - set(V8_TARGET_OS "linux") -endif() - -add_custom_command( - COMMAND - ${BASE}/build/linux/sysroot_scripts/install-sysroot.py --arch=${V8_TARGET_CPU} - WORKING_DIRECTORY - ${BASE}/ - DEPENDS - ${BASE}/output-gclient2.txt - OUTPUT - ${BASE}/output-gclient3.txt -) - -add_custom_command( - COMMAND - ${BASE_TOOLS}/gn - gen - out.gn/${V8_TARGET_CPU}-${V8_TARGET_OS}.release.sample - --args='is_component_build=false is_debug=false target_cpu="${V8_TARGET_CPU}" use_custom_libcxx=false v8_monolithic=true v8_use_external_startup_data=false' - WORKING_DIRECTORY - ${BASE}/ - DEPENDS - ${BASE}/output-gclient3.txt - OUTPUT - ${BASE}/output-v8gen.txt -) - -add_custom_target( - v8 - COMMAND - ${BASE_TOOLS}/ninja -j 8 - -C out.gn/${V8_TARGET_CPU}-${V8_TARGET_OS}.release.sample - v8_monolith - WORKING_DIRECTORY - ${BASE}/ - DEPENDS - ${BASE}/output-v8gen.txt -) - -set(V8_DIR ${BASE}) -set(V8_INCLUDE_DIR "${BASE}/include" PARENT_SCOPE) -set(V8_LIBRARY "${BASE}/out.gn/${V8_TARGET_CPU}-${V8_TARGET_OS}.release.sample/obj/libv8_monolith.a" PARENT_SCOPE) diff --git a/cpp.hint b/cpp.hint index badb272f1..e700f985d 100644 --- a/cpp.hint +++ b/cpp.hint @@ -1,2 +1,10 @@ #define BabyDI_INJECT(c,v) #define BabyDI_PROVIDE(c,v) +#define SCRIPTENV_D(x, y) +#define SETPROP_RETURN_ERROR +#define FOR_LIST_OF_NPC_PROPS(DO) +#define FOR_LIST_OF_PLAYER_PROPS(DO) +#define DO_PACKETLOG(LOG) +#define assert(expression) +#define RECOVERABLE_PARSE_ERROR(MESSAGE, RETVAL) +#define DEBUGPRINT(...) diff --git a/dependencies/build-v8-linux b/dependencies/build-v8-linux deleted file mode 100755 index 7285410d6..000000000 --- a/dependencies/build-v8-linux +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash -export PATH="$PWD/depot_tools:$PATH" -export V8_VERSION=9.1.269.9 - -if [[ -z ${BUILDARCH} ]]; then - echo "Setting buildarch" - export BUILDARCH=$(uname -m) -fi - -if [[ ${BUILDARCH} == "x86_64" ]]; then - BUILDARCH=x64 -elif [[ ${BUILDARCH} == *"arm"* ]]; then - BUILDARCH=arm -fi - -echo "Buildarch ${BUILDARCH}" - -if [[ -d "v8/out.gn/${BUILDARCH}.release" ]]; then - echo "Skipping v8 build" - exit -fi - -if [[ -d "v8" ]]; then - rm -rf v8 -fi - -if [[ -f ".gclient" ]]; then - rm -f .gclient -fi - -fetch v8 \ -&& cd v8 \ -&& git checkout ${V8_VERSION} \ -&& gclient sync -D \ -&& ./tools/dev/v8gen.py ${BUILDARCH}.release -- \ -is_component_build=false \ -is_debug=false \ -use_custom_libcxx=false \ -v8_monolithic=true \ -v8_use_external_startup_data=false \ -target_os=\"linux\" \ -target_cpu=\"${BUILDARCH}\" \ -v8_target_cpu=\"${BUILDARCH}\" \ -v8_enable_future=true \ -is_official_build=false \ -is_cfi=false \ -is_clang=false \ -use_custom_libcxx=false \ -use_sysroot=false \ -use_gold=false \ -treat_warnings_as_errors=false \ -symbol_level=0 \ -strip_debug_info=true \ -v8_use_external_startup_data=false \ -v8_enable_i18n_support=false \ -v8_enable_gdbjit=false \ -v8_static_library=true \ -v8_enable_pointer_compression=false \ -&& ninja -C out.gn/${BUILDARCH}.release -j $(getconf _NPROCESSORS_ONLN) diff --git a/dependencies/build-v8-mac b/dependencies/build-v8-mac deleted file mode 100755 index deadd2489..000000000 --- a/dependencies/build-v8-mac +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash -export PATH="$PWD/depot_tools:$PATH" -export V8_VERSION=9.1.269.9 - -if [[ -z ${BUILDARCH} ]]; then - echo "Setting buildarch" - export BUILDARCH=$(uname -m) -fi - -if [[ ${BUILDARCH} == "x86_64" ]]; then - BUILDARCH=x64 -elif [[ ${BUILDARCH} == *"arm"* ]]; then - BUILDARCH=arm -fi - -echo "Buildarch ${BUILDARCH}" - -if [[ -d "v8/out.gn/${BUILDARCH}.release" ]]; then - echo "Skipping v8 build" - exit -fi - -if [[ -d "v8" ]]; then - rm -rf v8 -fi - -if [[ -f ".gclient" ]]; then - rm -f .gclient -fi - -fetch v8 \ -&& cd v8 \ -&& git checkout ${V8_VERSION} \ -&& gclient sync -D \ -&& ./tools/dev/v8gen.py ${BUILDARCH}.release -- \ -is_component_build=false \ -is_debug=false \ -use_custom_libcxx=false \ -v8_monolithic=true \ -v8_use_external_startup_data=false \ -binutils_path=\"/usr/bin\" \ -target_os=\"mac\" \ -target_cpu=\"x64\" \ -v8_target_cpu=\"x64\" \ -v8_enable_future=true \ -is_official_build=false \ -is_cfi=false \ -is_clang=true \ -use_custom_libcxx=false \ -use_sysroot=false \ -use_gold=false \ -treat_warnings_as_errors=false \ -symbol_level=0 \ -strip_debug_info=true \ -v8_use_external_startup_data=false \ -v8_enable_i18n_support=false \ -v8_enable_gdbjit=false \ -v8_static_library=false \ -v8_enable_pointer_compression=false \ -&& ninja -C out.gn/${BUILDARCH}.release -j $(getconf _NPROCESSORS_ONLN) diff --git a/dependencies/depot_tools b/dependencies/depot_tools deleted file mode 160000 index 90e930e2d..000000000 --- a/dependencies/depot_tools +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 90e930e2da74f78601bb968224ed3b15be2e36f5 diff --git a/dependencies/gs2compiler b/dependencies/gs2compiler deleted file mode 160000 index f9f729f34..000000000 --- a/dependencies/gs2compiler +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f9f729f342e63e90821abf789e22aa13d5ea6ddd diff --git a/dependencies/gs2lib b/dependencies/gs2lib deleted file mode 160000 index b83910652..000000000 --- a/dependencies/gs2lib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b83910652213436780eee87df25978ac081cfd7c diff --git a/dependencies/miniupnp b/dependencies/miniupnp deleted file mode 160000 index 51f185fd1..000000000 --- a/dependencies/miniupnp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 51f185fd13bca5f10b9b80ebd4ad4eac5e22fd11 diff --git a/docker/gserver-x86_64-linux-gnu.dockerfile b/docker/gserver-x86_64-linux-gnu.dockerfile deleted file mode 100644 index ef00811ae..000000000 --- a/docker/gserver-x86_64-linux-gnu.dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -ARG NPCSERVER=on -ARG VER_EXTRA="" - -FROM xtjoeytx/v8:9.1.269.9-gnu AS local-v8 - -# GServer Build Environment -FROM amigadev/crosstools:x86_64-linux AS build-env-npcserver-on -COPY --chown=1001:1001 --from=local-v8 /tmp/v8 /tmp/v8 - -FROM amigadev/crosstools:x86_64-linux AS build-env-npcserver-off - -FROM build-env-npcserver-${NPCSERVER} AS build-env -ARG NPCSERVER -ARG VER_EXTRA - -USER 0 - -RUN apt update && \ - apt install -y libssl-dev libzstd-dev - -USER 1001 -COPY --chown=1001:1001 ./ /tmp/gserver - -RUN cd /tmp/gserver \ - && ln -s /tmp/v8 /tmp/gserver/dependencies/v8 \ - && cmake -GNinja -S/tmp/gserver -B/tmp/gserver/build -DCMAKE_BUILD_TYPE=Release -DSTATIC=ON -DV8NPCSERVER=${NPCSERVER} -DVER_EXTRA=${VER_EXTRA} -DWOLFSSL=ON -DUPNP=OFF -DCMAKE_CXX_FLAGS_RELEASE="-O3 -ffast-math" \ - && cmake --build /tmp/gserver/build --target clean \ - && cmake --build /tmp/gserver/build --target package --parallel $(getconf _NPROCESSORS_ONLN) \ - && chmod 777 -R /tmp/gserver/dist \ - && chmod 777 -R /tmp/gserver/build \ - && rm -rf /tmp/gserver/dist/_CPack_Packages - -# GServer Run Environment -FROM alpine:3.20 -ARG CACHE_DATE=2021-07-25 -COPY --from=build-env /tmp/gserver/dist /dist -COPY --from=build-env /tmp/gserver/build /tmp/gserver/build -RUN apk add --update libstdc++ libatomic cmake -USER 1001 -WORKDIR /gserver - diff --git a/docker/gserver-x86_64-linux-musl.dockerfile b/docker/gserver-x86_64-linux-musl.dockerfile deleted file mode 100644 index 438ab7925..000000000 --- a/docker/gserver-x86_64-linux-musl.dockerfile +++ /dev/null @@ -1,54 +0,0 @@ -ARG NPCSERVER=on -ARG VER_EXTRA="" - -FROM xtjoeytx/v8:9.1.269.9 as local-v8 -ARG NPCSERVER -ARG VER_EXTRA - -# GServer Build Environment -FROM alpine:3.20 AS build-env-npcserver-on -COPY --chown=1001:1001 --from=local-v8 /tmp/v8 /tmp/gserver/dependencies/v8 - -FROM alpine:3.20 AS build-env-npcserver-off - -FROM build-env-npcserver-${NPCSERVER} AS build-env -ARG NPCSERVER -ARG VER_EXTRA -COPY --chown=1001:1001 ./ /tmp/gserver - -RUN apk add --update --virtual .gserver-build-dependencies \ - cmake \ - gcc \ - g++ \ - bison \ - flex \ - bash \ - make \ - git \ - automake \ - autoconf \ - ninja \ - openssl-dev \ - openssl-libs-static \ - && cd /tmp/gserver \ - && cmake -GNinja -S/tmp/gserver -B/tmp/gserver/build -DCMAKE_BUILD_TYPE=Release -DV8NPCSERVER=${NPCSERVER} -DVER_EXTRA=${VER_EXTRA} -DTESTS=ON -DWOLFSSL=ON -DUPNP=OFF -DSTATIC=ON -DCMAKE_CXX_FLAGS_RELEASE="-O3 -ffast-math" \ - && cmake --build /tmp/gserver/build --config Release --target clean \ - && cmake --build /tmp/gserver/build --config Release --target package --parallel $(getconf _NPROCESSORS_ONLN) \ - && chmod 777 -R /tmp/gserver/dist \ - && rm -rf /tmp/gserver/dist/_CPack_Packages \ - && chown 1001:1001 -R /tmp/gserver \ - && chmod 777 -R /tmp/gserver/build \ - && apk del --purge .gserver-build-dependencies - -USER 1001 - -# GServer Run Environment -FROM alpine:3.20 -ARG CACHE_DATE=2024-06-07 -COPY --from=build-env /tmp/gserver/bin /gserver -COPY entrypoint.sh /gserver/ -RUN apk add --update libstdc++ libatomic -WORKDIR /gserver -VOLUME /gserver/servers -ENTRYPOINT ["/gserver/entrypoint.sh"] -CMD ["/gserver/gs2emu"] \ No newline at end of file diff --git a/docker/gserver-x86_64-w64-mingw.dockerfile b/docker/gserver-x86_64-w64-mingw.dockerfile deleted file mode 100644 index 45976b868..000000000 --- a/docker/gserver-x86_64-w64-mingw.dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -ARG NPCSERVER=on -ARG VER_EXTRA="" - -FROM xtjoeytx/v8:9.1.269.9-mingw AS v8 - -# GServer Build Environment -FROM amigadev/crosstools:x86_64-w64-mingw32 AS build-env -ARG NPCSERVER -ARG VER_EXTRA -COPY --from=v8 /tmp/v8 /usr/x86_64-w64-mingw32/usr -RUN ln -s /usr/x86_64-w64-mingw32/include/wincrypt.h /usr/x86_64-w64-mingw32/include/Wincrypt.h - -USER 1001 -COPY --chown=1001:1001 ./ /tmp/gserver - -RUN cd /tmp/gserver \ - && cmake -GNinja -S/tmp/gserver -B/tmp/gserver/build -DCMAKE_BUILD_TYPE=Release -DSTATIC=ON -DV8NPCSERVER=${NPCSERVER} -DVER_EXTRA=${VER_EXTRA} -DWOLFSSL=ON -DUPNP=OFF -DCMAKE_CXX_FLAGS_RELEASE="-O3 -ffast-math" \ - && cmake --build /tmp/gserver/build --target clean \ - && cmake --build /tmp/gserver/build --target package --parallel $(getconf _NPROCESSORS_ONLN) \ - && chmod 777 -R /tmp/gserver/dist \ - && rm -rf /tmp/gserver/dist/_CPack_Packages - -# GServer Run Environment -FROM alpine:3.14 -ARG CACHE_DATE=2021-07-25 -COPY --from=build-env /tmp/gserver/dist /dist -USER 1001 -WORKDIR /gserver - diff --git a/docker/linux-gnu.dockerfile b/docker/linux-gnu.dockerfile new file mode 100644 index 000000000..6b0453fbf --- /dev/null +++ b/docker/linux-gnu.dockerfile @@ -0,0 +1,38 @@ +FROM amigadev/crosstools:x86_64-linux AS build-env +ARG VER_EXTRA +ARG TARGETARCH + +# - ROOT - +USER 0 + +ENV VCPKG_ROOT=/tmp/gserver/vcpkg +ENV VCPKG_FORCE_SYSTEM_BINARIES=1 +ENV VCPKG_DISABLE_METRICS=1 + +COPY --chown=1001:1001 ./ /tmp/gserver + +RUN ARCH=`echo $TARGETARCH| sed "s/amd64/x64/g" | sed "s/aarch64/arm64/g"` \ + && apt update \ + && apt install -y libssl-dev libzstd-dev cmake git ninja-build openjdk-21-jre gcc g++ \ + && git clone https://github.com/microsoft/vcpkg $VCPKG_ROOT \ + && cd $VCPKG_ROOT \ + && sh bootstrap-vcpkg.sh -disableMetrics \ + && chmod 777 -R /tmp/gserver \ + && cd /tmp/gserver \ + && cmake -GNinja -S/tmp/gserver -B/tmp/gserver/build --preset "Linux Release" -DVCPKG_TARGET_TRIPLET:STRING=${ARCH}-linux -DSTATIC=ON -DVER_EXTRA=${VER_EXTRA} -DWOLFSSL=ON -DUPNP=OFF -DCMAKE_CXX_FLAGS_RELEASE="-O3 -ffast-math" \ + && cmake --build /tmp/gserver/build --target clean \ + && cmake --build /tmp/gserver/build --target package --parallel $(getconf _NPROCESSORS_ONLN) \ + && rm -rf /tmp/gserver/dist/_CPack_Packages \ + && chown 1001:1001 -R /tmp/gserver \ + && chmod 777 -R /tmp/gserver/dist \ + && chmod 777 -R /tmp/gserver/build \ + && apt purge -y libssl-dev libzstd-dev cmake git ninja-build openjdk-21-jre + +# GServer Run Environment +FROM alpine:3.22 +ARG CACHE_DATE=2025-07-08 +COPY --from=build-env --chown=1001:1001 /tmp/gserver/dist /dist +COPY --from=build-env --chown=1001:1001 /tmp/gserver/build /tmp/gserver/build +RUN apk add --update libstdc++ libatomic cmake +USER 1001 +WORKDIR /gserver diff --git a/docker/linux-musl.dockerfile b/docker/linux-musl.dockerfile new file mode 100644 index 000000000..50df08df4 --- /dev/null +++ b/docker/linux-musl.dockerfile @@ -0,0 +1,62 @@ +ARG VER_EXTRA="" + +# GServer Build Environment +FROM alpine:3.22 AS build-env +ARG VER_EXTRA +ARG TARGETARCH + +# - ROOT - +USER 0 + +ENV VCPKG_ROOT=/tmp/gserver/vcpkg +ENV VCPKG_FORCE_SYSTEM_BINARIES=1 +ENV VCPKG_DISABLE_METRICS=1 + +COPY --chown=1001:1001 ./ /tmp/gserver + +RUN ARCH=`echo $TARGETARCH| sed "s/amd64/x64/g" | sed "s/aarch64/arm64/g"` \ + && apk add --update --virtual .gserver-build-dependencies \ + cmake \ + gcc \ + g++ \ + bison \ + flex \ + bash \ + make \ + git \ + curl \ + automake \ + autoconf \ + openjdk21-jdk \ + zip \ + ninja-build \ + ninja-is-really-ninja \ + openssl-dev \ + openssl-libs-static \ + python3 \ + && git clone https://github.com/microsoft/vcpkg $VCPKG_ROOT \ + && cd $VCPKG_ROOT \ + && sh bootstrap-vcpkg.sh -disableMetrics \ + && chmod 777 -R /tmp/gserver \ + && cd /tmp/gserver \ + && cmake -GNinja -S/tmp/gserver -B/tmp/gserver/build --preset "Linux Release" -DMUSL=ON -DVCPKG_TARGET_TRIPLET:STRING=${ARCH}-linux -DVER_EXTRA=${VER_EXTRA} -DWOLFSSL=ON -DUPNP=OFF -DCMAKE_CXX_FLAGS_RELEASE="-O3 -ffast-math" \ + && cmake --build /tmp/gserver/build --config Release --target clean \ + && cmake --build /tmp/gserver/build --config Release --target package --parallel $(getconf _NPROCESSORS_ONLN) \ + && chmod 777 -R /tmp/gserver/dist \ + && rm -rf /tmp/gserver/dist/_CPack_Packages \ + && chown 1001:1001 -R /tmp/gserver \ + && chmod 777 -R /tmp/gserver/build \ + && apk del --purge .gserver-build-dependencies + +# GServer Run Environment +FROM alpine:3.22 +USER 0 +ARG CACHE_DATE=2025-07-08 +COPY --from=build-env --chown=1001:1001 /tmp/gserver/bin /gserver +COPY entrypoint.sh /gserver/ +RUN apk add --update libstdc++ libatomic +USER 1001 +WORKDIR /gserver +VOLUME /gserver/servers +ENTRYPOINT ["/gserver/entrypoint.sh"] +CMD ["/gserver/gs2emu"] diff --git a/docker/v8-x86_64-linux-gnu.dockerfile b/docker/v8-x86_64-linux-gnu.dockerfile deleted file mode 100644 index 3dec981e2..000000000 --- a/docker/v8-x86_64-linux-gnu.dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -# Google V8 Build Environment -FROM amigadev/crosstools:x86_64-linux as v8 -COPY dependencies/build-v8-linux /tmp/ - -RUN cd /tmp/ && \ - git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git /tmp/depot_tools && \ - ln -s /usr/bin/python2.7 /tmp/depot_tools/python && \ - ./build-v8-linux - -FROM alpine:3.16 as v8-final -COPY --from=v8 /tmp/v8 /tmp/v8 diff --git a/docker/v8-x86_64-linux-musl.dockerfile b/docker/v8-x86_64-linux-musl.dockerfile deleted file mode 100644 index ede057657..000000000 --- a/docker/v8-x86_64-linux-musl.dockerfile +++ /dev/null @@ -1,99 +0,0 @@ -FROM alpine:3.17 as gn-builder -ARG GN_COMMIT=82d673acb802cee21534c796a59f8cdf26500f53 -RUN apk add --update --virtual .gn-build-dependencies \ - alpine-sdk \ - binutils-gold \ - clang \ - curl \ - git \ - llvm9 \ - ninja \ - python3 \ - tar \ - xz \ - && PATH=$PATH:/usr/lib/llvm4/bin \ - && cp -f /usr/bin/ld.gold /usr/bin/ld \ - && git clone https://gn.googlesource.com/gn /tmp/gn \ - && git -C /tmp/gn checkout ${GN_COMMIT} \ - && cd /tmp/gn \ - && python3 build/gen.py \ - && ninja -C out \ - && cp -f /tmp/gn/out/gn /usr/local/bin/gn \ - && apk del .gn-build-dependencies \ - && rm -rf /tmp/* /var/tmp/* /var/cache/apk/* - -# Google V8 Clone Environment -# gclient does NOT work with Alpine -FROM debian:buster-slim as source -ARG V8_VERSION=9.1.269.9 -RUN set -x && \ - apt-get update && \ - apt-get install -y \ - git \ - curl \ - python3 && \ - git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git /tmp/depot_tools && \ - PATH=$PATH:/tmp/depot_tools && \ - cd /tmp && \ - fetch v8 && \ - cd /tmp/v8 && \ - git checkout ${V8_VERSION} && \ - gclient sync -D && \ - apt-get remove --purge -y \ - git \ - curl \ - python3 && \ - apt-get autoremove -y && \ - rm -rf /var/lib/apt/lists/* - -# Google V8 Build Environment -FROM alpine:3.17 as v8 -COPY --from=source /tmp/v8 /tmp/v8 -COPY --from=gn-builder /usr/local/bin/gn /tmp/v8/buildtools/linux64/gn -RUN \ - apk add --update --virtual .v8-build-dependencies \ - curl \ - g++ \ - gcc \ - glib-dev \ - icu-dev \ - libstdc++ \ - linux-headers \ - make \ - ninja \ - python2 \ - tar \ - xz \ - && cd /tmp/v8 && \ - python2 ./tools/dev/v8gen.py x64.release -- \ - is_component_build=false \ - is_debug=false \ - use_custom_libcxx=false \ - v8_monolithic=true \ - v8_use_external_startup_data=false \ - binutils_path=\"/usr/bin\" \ - target_os=\"linux\" \ - target_cpu=\"x64\" \ - v8_target_cpu=\"x64\" \ - v8_enable_future=true \ - is_official_build=false \ - is_cfi=false \ - is_clang=false \ - use_custom_libcxx=false \ - use_sysroot=false \ - use_gold=false \ - treat_warnings_as_errors=false \ - symbol_level=0 \ - strip_debug_info=true \ - v8_use_external_startup_data=false \ - v8_enable_i18n_support=false \ - v8_enable_gdbjit=false \ - v8_static_library=true \ - v8_enable_pointer_compression=false \ - && ninja -C out.gn/x64.release -j $(getconf _NPROCESSORS_ONLN) \ - && find /tmp/v8/out.gn/x64.release -name '*.a' \ - && rm -rf /tmp/v8/third_party /tmp/v8/test \ - && apk del --purge .v8-build-dependencies - -FROM alpine:3.17 as v8-final -COPY --from=v8 /tmp/v8 /tmp/v8 diff --git a/docker/v8-x86_64-w64-mingw.dockerfile b/docker/v8-x86_64-w64-mingw.dockerfile deleted file mode 100644 index c1428f0e7..000000000 --- a/docker/v8-x86_64-w64-mingw.dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# Google V8 Build Environment -FROM alpine:3.16 AS v8 -USER root - -RUN \ - apk add --update --virtual .v8-build-dependencies \ - tar \ - zstd \ - xz \ - && cd /tmp \ - && wget https://mirror.msys2.org/mingw/mingw64/mingw-w64-x86_64-v8-9.1.269.39-5-any.pkg.tar.zst -O /tmp/mingw-w64-v8.tar.zst \ - && wget https://mirror.msys2.org/mingw/mingw64/mingw-w64-x86_64-icu-72.1-1-any.pkg.tar.zst -O /tmp/mingw-w64-icu.tar.zst \ - && wget https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-zlib-1.2.13-2-any.pkg.tar.zst -O /tmp/mingw-w64-zlib.tar.zst \ - && wget https://mirror.msys2.org/mingw/mingw64/mingw-w64-x86_64-gcc-libs-12.2.0-6-any.pkg.tar.zst -O /tmp/mingw-w64-gcc-libs.tar.zst \ - && wget https://mirror.msys2.org/mingw/mingw64/mingw-w64-x86_64-openssl-3.1.4-1-any.pkg.tar.zst -O /tmp/mingw-w64-openssl-devel.tar.zst \ - && tar xvf /tmp/mingw-w64-v8.tar.zst \ - && tar xvf /tmp/mingw-w64-icu.tar.zst \ - && tar xvf /tmp/mingw-w64-zlib.tar.zst \ - && tar xvf /tmp/mingw-w64-gcc-libs.tar.zst \ - && tar xvf /tmp/mingw-w64-openssl-devel.tar.zst \ - && mv -fv mingw64 v8 \ - && rm -rf *.zst \ - && apk del --purge .v8-build-dependencies diff --git a/docker/windows-mingw.dockerfile b/docker/windows-mingw.dockerfile new file mode 100644 index 000000000..977026365 --- /dev/null +++ b/docker/windows-mingw.dockerfile @@ -0,0 +1,50 @@ +ARG VER_EXTRA="" + +# GServer Build Environment +FROM amigadev/crosstools:x86_64-w64-mingw32 AS build-env +ARG VER_EXTRA +ARG TARGETARCH + +# - ROOT - +USER 0 + +ENV VCPKG_ROOT=/tmp/gserver/vcpkg +ENV VCPKG_FORCE_SYSTEM_BINARIES=1 +ENV VCPKG_DISABLE_METRICS=1 + +# Something is preventing this from being set via CMakePresets.json, so just force it. +ENV CMAKE_TOOLCHAIN_FILE=/tmp/gserver/vcpkg/scripts/buildsystems/vcpkg.cmake + +COPY --chown=1001:1001 ./ /tmp/gserver + + +RUN ARCH=`echo $TARGETARCH| sed "s/amd64/x64/g" | sed "s/aarch64/arm64/g"` \ + && apt update \ + && apt install -y wget \ + && wget -q https://github.com/berkley4/icu-74-debian/releases/download/74.2-2/libicu74_74.2-2_amd64.deb \ + && wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.5.2/powershell_7.5.2-1.deb_amd64.deb \ + && apt -y install ./libicu74_74.2-2_amd64.deb \ + && apt -y install ./powershell_7.5.2-1.deb_amd64.deb \ + && rm ./libicu74_74.2-2_amd64.deb \ + && rm ./powershell_7.5.2-1.deb_amd64.deb \ + && apt update \ + && apt install -y powershell libssl-dev libzstd-dev cmake git ninja-build openjdk-21-jre \ + && ln -s /usr/x86_64-w64-mingw32/include/wincrypt.h /usr/x86_64-w64-mingw32/include/Wincrypt.h \ + && git clone https://github.com/microsoft/vcpkg $VCPKG_ROOT \ + && cd $VCPKG_ROOT \ + && sh bootstrap-vcpkg.sh -disableMetrics \ + && chmod 777 -R /tmp/gserver \ + && cd /tmp/gserver \ + && cmake -GNinja -S/tmp/gserver -B/tmp/gserver/build --preset "Release MINGW" -DVCPKG_TARGET_TRIPLET:STRING=${ARCH}-mingw-static -DSTATIC=ON -DVER_EXTRA=${VER_EXTRA} -DWOLFSSL=ON -DCMAKE_CXX_FLAGS_RELEASE="-O3 -ffast-math" \ + && cmake --build /tmp/gserver/build --target clean \ + && cmake --build /tmp/gserver/build --target package --parallel $(getconf _NPROCESSORS_ONLN) \ + && chmod 777 -R /tmp/gserver/dist \ + && rm -rf /tmp/gserver/dist/_CPack_Packages \ + && apt purge -y libssl-dev libzstd-dev cmake git ninja-build openjdk-21-jre + +# GServer Run Environment +FROM alpine:3.22 +ARG CACHE_DATE=2025-07-08 +COPY --from=build-env --chown=1001:1001 /tmp/gserver/dist /dist +USER 1001 +WORKDIR /dist diff --git a/docs/graal_requesttext_sendtext_info.txt b/docs/graal_requesttext_sendtext_info.txt index aaf94d404..e0a620543 100644 --- a/docs/graal_requesttext_sendtext_info.txt +++ b/docs/graal_requesttext_sendtext_info.txt @@ -46,6 +46,7 @@ sendText( "disconnect", int playerid, str disconnectreason ); - disconnects the sendText( "lister", "getbanhistory", str accountname ); - gets the ban history of a players account sendText( "lister", "staffactivity", str accountname ); - gets the staff activity of a players account sendText( "lister", "getbanbyid", str id ); - gets the ban history of a players account +sendText( "lister", "getprofile", str accountmame ); - gets a player's profile sendText( "resetnpc", str npcname/int npcid, NULL ); - resets the given DB NPC (Client-RC) sendText( "deleteweapon", str weaponname, NULL ); - deletes the given weapon (Client-RC) diff --git a/docs/tiles-0/bed-lower.dat b/docs/tiles-0/bed-lower.dat new file mode 100644 index 000000000..b4b688d68 Binary files /dev/null and b/docs/tiles-0/bed-lower.dat differ diff --git a/docs/tiles-0/bed-upper.dat b/docs/tiles-0/bed-upper.dat new file mode 100644 index 000000000..4ba2b691b Binary files /dev/null and b/docs/tiles-0/bed-upper.dat differ diff --git a/docs/tiles-0/chair.dat b/docs/tiles-0/chair.dat new file mode 100644 index 000000000..8181b866b Binary files /dev/null and b/docs/tiles-0/chair.dat differ diff --git a/docs/tiles-0/hurt-underground.dat b/docs/tiles-0/hurt-underground.dat new file mode 100644 index 000000000..73c749687 Binary files /dev/null and b/docs/tiles-0/hurt-underground.dat differ diff --git a/docs/tiles-0/jump-stone.dat b/docs/tiles-0/jump-stone.dat new file mode 100644 index 000000000..9b87d65f6 Binary files /dev/null and b/docs/tiles-0/jump-stone.dat differ diff --git a/docs/tiles-0/lava-swamp.dat b/docs/tiles-0/lava-swamp.dat new file mode 100644 index 000000000..8ed55e1d9 Binary files /dev/null and b/docs/tiles-0/lava-swamp.dat differ diff --git a/docs/tiles-0/lava.dat b/docs/tiles-0/lava.dat new file mode 100644 index 000000000..bbaa75da0 Binary files /dev/null and b/docs/tiles-0/lava.dat differ diff --git a/docs/tiles-0/near-water.dat b/docs/tiles-0/near-water.dat new file mode 100644 index 000000000..0ed7e056f Binary files /dev/null and b/docs/tiles-0/near-water.dat differ diff --git a/docs/tiles-0/no-wall.dat b/docs/tiles-0/no-wall.dat new file mode 100644 index 000000000..26f097449 Binary files /dev/null and b/docs/tiles-0/no-wall.dat differ diff --git a/docs/tiles-0/readme.txt b/docs/tiles-0/readme.txt new file mode 100644 index 000000000..330354b59 --- /dev/null +++ b/docs/tiles-0/readme.txt @@ -0,0 +1,4 @@ +Pulled from arrays.dat. +Data is stored little-endian in 4-byte ints. +First int is how many elements are stored after it. +If the first int has bit 16 set, it is a multi-dimensional array. \ No newline at end of file diff --git a/docs/tiles-0/swamp.dat b/docs/tiles-0/swamp.dat new file mode 100644 index 000000000..2efc86d4c Binary files /dev/null and b/docs/tiles-0/swamp.dat differ diff --git a/docs/tiles-0/throw-through.dat b/docs/tiles-0/throw-through.dat new file mode 100644 index 000000000..f7b4923f2 Binary files /dev/null and b/docs/tiles-0/throw-through.dat differ diff --git a/docs/tiles-0/water.dat b/docs/tiles-0/water.dat new file mode 100644 index 000000000..65e2ec93a Binary files /dev/null and b/docs/tiles-0/water.dat differ diff --git a/docs/npcserver.txt b/docs/v8-npcserver.txt similarity index 100% rename from docs/npcserver.txt rename to docs/v8-npcserver.txt diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 97d7315b3..506675e33 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -19,199 +19,371 @@ # along with GS2Emu. If not, see . # -include(CheckFunctionExists) -include(CheckSymbolExists) - set( SOURCES - "src/Account.cpp" - "src/FileSystem.cpp" + "${PROJECT_BINARY_DIR}/server/include/IConfig.h" + "include/Account.h" + "include/BabyDI.h" + "include/IConfig.h.in" + "include/main.h" + "include/Server.h" + "include/ServerList.h" + "include/UpdatePackage.h" + "include/windresrc.h.in" + "include/animation/GameAni.h" + "include/filesystem/File.h" + "include/filesystem/FileSystem.h" + "include/filesystem/FileSystemTypes.h" + "include/filesystem/watch/FileWatch.h" + "include/level/Level.h" + "include/level/LevelArrow.h" + "include/level/LevelBaddy.h" + "include/level/LevelBoardChange.h" + "include/level/LevelBomb.h" + "include/level/LevelChest.h" + "include/level/LevelExplosion.h" + "include/level/LevelHorse.h" + "include/level/LevelItem.h" + "include/level/LevelLink.h" + "include/level/LevelShoot.h" + "include/level/LevelSign.h" + "include/level/LevelTerrain.h" + "include/level/LevelTiles.h" + "include/level/LevelTileTypes.h" + "include/level/Map.h" + "include/loader/IAccountLoader.h" + "include/loader/INPCLoader.h" + "include/loader/LevelLoader.h" + "include/loader/flatfile/FlatFileAccountLoader.h" + "include/loader/flatfile/FlatFileNPCLoader.h" + "include/misc/UPNP.h" + "include/misc/WordFilter.h" + "include/network/IPacketHandler.h" + "include/npcserver/NPCServer.h" + "include/npcserver/PlayerNPCServer.h" + "include/object/Character.h" + "include/object/NPC.h" + "include/object/Player.h" + "include/object/ShowImg.h" + "include/object/Weapon.h" + "include/player/PlayerClient.h" + "include/player/PlayerClientOriginal.h" + "include/player/PlayerLogin.h" + "include/player/PlayerNC.h" + "include/player/PlayerProps.h" + "include/player/PlayerRC.h" + "include/scripting/GS2ScriptManager.h" + "include/scripting/IScriptEngine.h" + "include/scripting/ScriptClass.h" + "include/scripting/ScriptContainers.h" + "include/scripting/ScriptSystem.h" + "include/scripting/ScriptTypes.h" + "include/scripting/Script.h" + "include/scripting/gs1/GS1Commands.h" + "include/scripting/gs1/GS1ErrorListener.h" + "include/scripting/gs1/GS1Flags.h" + "include/scripting/gs1/GS1Functions.h" + "include/scripting/gs1/GS1MessageCodes.h" + "include/scripting/gs1/GS1Variables.h" + "include/scripting/gs1/GS1Visitor.h" + "include/scripting/gs1/ScriptEngineGS1.h" + "include/scripting/gs2/ScriptEngineGS2.h" + "include/utilities/CommandDispatcher.h" + "include/utilities/CommonTypes.h" + "include/utilities/Events.h" + "include/utilities/Extents.h" + "include/utilities/FilePermissions.h" + "include/utilities/Log.h" + "include/utilities/PropertySerializers.h" + "include/utilities/Random.h" + "include/utilities/Settings.h" + "include/utilities/StringUtils.h" + "include/utilities/generator/IdGenerator.h" + "include/utilities/generator/TimeoutGenerator.h" + "include/utilities/manager/GuildManager.h" + "include/utilities/manager/ITranslationManager.h" + "include/utilities/manager/ResourceManager.h" + "include/utilities/manager/TranslationManagerClassic.h" + "include/utilities/manager/TranslationManagerModern.h" + "include/utilities/std/generator.h" + "include/utilities/std/inplace_vector.h" + "include/utilities/std/views_concat.h" "src/main.cpp" - "src/NPC.cpp" "src/Server.cpp" "src/ServerList.cpp" "src/TriggerCommandHandlers.cpp" "src/UpdatePackage.cpp" - "src/Weapon.cpp" + "src/versions.txt" + "src/animation/GameAni.cpp" + "src/filesystem/File.cpp" + "src/filesystem/FileSystem.cpp" + "src/filesystem/watch/FileWatchOS_Linux.cpp" + "src/filesystem/watch/FileWatchOS_OSX.cpp" + "src/filesystem/watch/FileWatchOS_Poll.cpp" + "src/filesystem/watch/FileWatchOS_Windows.cpp" + "src/loader/LevelLoader.cpp" + "src/loader/flatfile/FlatFileAccountLoader.cpp" + "src/loader/flatfile/FlatFileNPCLoader.cpp" + "src/level/Level.cpp" + "src/level/LevelBaddy.cpp" + "src/level/LevelBoardChange.cpp" + "src/level/LevelItem.cpp" + "src/level/LevelLink.cpp" + "src/level/LevelSign.cpp" + "src/level/LevelTerrain.cpp" + "src/level/Map.cpp" + "src/misc/UPNP.cpp" + "src/misc/WordFilter.cpp" + "src/npcserver/NPCServer.cpp" + "src/npcserver/PlayerNPCServer.cpp" + "src/object/NPC.cpp" + "src/object/ShowImg.cpp" + "src/object/Weapon.cpp" + "src/player/Player.cpp" + "src/player/PlayerClient.cpp" + "src/player/PlayerClientOriginal.cpp" + "src/player/PlayerExternalPlayers.cpp" + "src/player/PlayerLogin.cpp" + "src/player/PlayerNC.cpp" + "src/player/PlayerProps.cpp" + "src/player/PlayerRC.cpp" + "src/player/PlayerRequestText.cpp" + "src/player/packets/PlayerClientPackets.cpp" + "src/player/packets/PlayerNCPackets.cpp" + "src/player/packets/PlayerRCPackets.cpp" "src/scripting/GS2ScriptManager.cpp" "src/scripting/ScriptClass.cpp" - "${PROJECT_SOURCE_DIR}/bin/servers/default/bootstrap.js" + "src/scripting/ScriptContainers.cpp" + "src/scripting/ScriptSystem.cpp" + "src/scripting/Script.cpp" + "src/scripting/gs1/GS1Commands.cpp" + "src/scripting/gs1/GS1Flags.cpp" + "src/scripting/gs1/GS1Functions.cpp" + "src/scripting/gs1/GS1MessageCodes.cpp" + "src/scripting/gs1/GS1Variables.cpp" + "src/scripting/gs1/GS1Visitor.cpp" + "src/scripting/gs1/ScriptEngineGS1.cpp" + "src/scripting/gs2/ScriptEngineGS2.cpp" + "src/utilities/Events.cpp" + "src/utilities/FilePermissions.cpp" + "src/utilities/Log.cpp" + "src/utilities/PropertySerializers.cpp" + "src/utilities/Settings.cpp" + "src/utilities/StringUtils.cpp" + "src/utilities/manager/GuildManager.cpp" + "src/utilities/manager/TranslationManagerClassic.cpp" + "src/utilities/manager/TranslationManagerModern.cpp" ) -file(GLOB PLAYER_SOURCES src/player/**.cpp) -list(APPEND SOURCES ${PLAYER_SOURCES}) - -file(GLOB LEVEL_SOURCES src/level/**.cpp) -list(APPEND SOURCES ${LEVEL_SOURCES}) - -file(GLOB MISC_SOURCES src/misc/**.cpp) -list(APPEND SOURCES ${MISC_SOURCES}) - -file(GLOB UTILITIES_SOURCES src/utilities/**.cpp) -list(APPEND SOURCES ${UTILITIES_SOURCES}) - -file(GLOB ANIMATION_SOURCES src/animation/**.cpp) -list(APPEND SOURCES ${ANIMATION_SOURCES}) +# ---------------------------- +# Create the executable target. +set(TARGET_NAME "${PROJECT_NAME_LOWER}") +add_executable(${TARGET_NAME}) +set_target_properties(${TARGET_NAME} PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX}) +target_link_directories(${TARGET_NAME} PRIVATE ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) + +# Add the sources. +target_sources(${TARGET_NAME} PUBLIC ${SOURCES}) +target_sources(${TARGET_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS "include" "${PROJECT_BINARY_DIR}/server/include") + +# ---------------------------- +# Configure our compiler options. +set_default_compiler_options(${TARGET_NAME} FALSE) + +# ---------------------------- +# Tests +if (TESTS) + set(APP_LIBRARY_NAME "lib${TARGET_NAME}") + set(APP_LIBRARY_NAME_TESTREF "${APP_LIBRARY_NAME}" PARENT_SCOPE) + add_library(${APP_LIBRARY_NAME}) + set_target_properties(${APP_LIBRARY_NAME} PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX}) + target_link_directories(${APP_LIBRARY_NAME} PRIVATE ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) + + # Add the sources. + target_sources(${APP_LIBRARY_NAME} PUBLIC ${SOURCES}) + target_sources(${APP_LIBRARY_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS "include" "${PROJECT_BINARY_DIR}/server/include") + + # Configure our compiler options. + set_default_compiler_options(${APP_LIBRARY_NAME} TRUE) +endif() -include(bin2h) +# ---------------------------- +# Packet logging. +option(PACKET_LOGGING "Enable debug packet logging." OFF) +if(PACKET_LOGGING) + target_compile_definitions(${TARGET_NAME} PUBLIC PACKETLOGGING) +endif() -set(EXE_HEADERS "") -set( - HEADERS - "${PROJECT_BINARY_DIR}/server/include/IConfig.h" - "include/FileSystem.h" - "include/main.h" - "include/Account.h" - "include/NPC.h" - "include/Player.h" - "include/Server.h" - "include/ServerList.h" - "include/UpdatePackage.h" - "include/Weapon.h" - "include/scripting/GS2ScriptManager.h" - "include/scripting/ScriptClass.h" - "include/scripting/ScriptOrigin.h" - "include/scripting/SourceCode.h" +# ---------------------------- +# Dependency: gs2parser. +FetchContent_Declare(gs2parser + GIT_REPOSITORY https://github.com/xtjoeytx/gs2-parser.git + GIT_TAG 73e7ea8d6ed88112967547c5e6941bfa035fec6c # main 2026-07-02 ) - -file(GLOB LEVEL_HEADERS include/level/**.h) -list(APPEND HEADERS ${LEVEL_HEADERS}) - -file(GLOB MISC_HEADERS include/misc/**.h) -list(APPEND HEADERS ${MISC_HEADERS}) - -file(GLOB UTILITIES_HEADERS include/utilities/**.h) -list(APPEND HEADERS ${UTILITIES_HEADERS}) - -file(GLOB ANIMATION_HEADERS include/animation/**.h) -list(APPEND HEADERS ${ANIMATION_HEADERS}) - -if(V8NPCSERVER) - # Headers for script library interface - file(GLOB SCRIPT_INTERFACE_HEADERS include/scripting/interface/**.h) - list(APPEND HEADERS ${SCRIPT_INTERFACE_HEADERS}) - - # Headers for script library v8 implementation - file(GLOB SCRIPT_V8_HEADERS include/scripting/v8/**.h) - list(APPEND HEADERS ${SCRIPT_V8_HEADERS}) - - # Source for script library v8 implementation - list( - APPEND - SOURCES - src/scripting/v8/V8ScriptEnv.cpp - ) - - # GServer specific headers for implementation - list( - APPEND - HEADERS - ${PROJECT_BINARY_DIR}/server/include/EmbeddedBootstrapScript.h - include/scripting/ScriptEngine.h - include/scripting/ScriptAction.h - include/scripting/ScriptExecutionContext.h - include/scripting/ScriptFactory.h - include/scripting/v8/V8ScriptWrappers.h - ) - - # GServer specific source for implementation - list( - APPEND - SOURCES - "src/scripting/ScriptEngine.cpp" - "src/scripting/v8/V8EnvironmentImpl.cpp" - "src/scripting/v8/V8FunctionsImpl.cpp" - "src/scripting/v8/V8LevelImpl.cpp" - "src/scripting/v8/V8LevelLinkImpl.cpp" - "src/scripting/v8/V8LevelSignImpl.cpp" - "src/scripting/v8/V8LevelChestImpl.cpp" - "src/scripting/v8/V8NPCImpl.cpp" - "src/scripting/v8/V8PlayerImpl.cpp" - "src/scripting/v8/V8ScriptEnv.cpp" - "src/scripting/v8/V8ServerImpl.cpp" - "src/scripting/v8/V8WeaponImpl.cpp" - ) +FetchContent_MakeAvailable(gs2parser) +target_link_libraries(${TARGET_NAME} PUBLIC gs2parser::gs2compiler) + +# ---------------------------- +# Dependency: zlib. +find_package(ZLIB REQUIRED) +target_link_libraries(${TARGET_NAME} PRIVATE ZLIB::ZLIB) + +# ---------------------------- +# Dependency: bzip2 +find_package(BZip2 REQUIRED) +target_link_libraries(${TARGET_NAME} PRIVATE BZip2::BZip2) + +# ---------------------------- +# Dependency: antlr4. +find_package(antlr4-runtime CONFIG REQUIRED) +find_package(antlr4-generator CONFIG REQUIRED) +target_link_directories(${TARGET_NAME} PRIVATE ${ANTLR4_LIBRARY_DIR}) +target_include_directories(${TARGET_NAME} PRIVATE ${ANTLR4_INCLUDE_DIR}) +target_link_libraries(${TARGET_NAME} PRIVATE antlr4_static Threads::Threads) +if(TESTS) + target_link_directories(${APP_LIBRARY_NAME} PUBLIC ${ANTLR4_LIBRARY_DIR}) + target_include_directories(${APP_LIBRARY_NAME} PUBLIC ${ANTLR4_INCLUDE_DIR}) + target_link_libraries(${APP_LIBRARY_NAME} PUBLIC antlr4_static) endif() - -include_directories( - # Include the CMake-generated version header from the build directory - ${PROJECT_BINARY_DIR}/server/include - ${PROJECT_SOURCE_DIR}/server/include - ${PROJECT_SOURCE_DIR}/server/include/misc - ${PROJECT_SOURCE_DIR}/server/include/level - ${PROJECT_SOURCE_DIR}/server/include/utilities - ${PROJECT_SOURCE_DIR}/server/include/scripting - ${PROJECT_SOURCE_DIR}/server/include/scripting/interface - ${PROJECT_SOURCE_DIR}/server/include/scripting/v8 +FetchContent_Declare(AntlrJar + URL https://www.antlr.org/download/antlr-4.13.2-complete.jar + DOWNLOAD_DIR "${FETCHCONTENT_BASE_DIR}/antlr-jar" + DOWNLOAD_NAME "antlr.jar" + DOWNLOAD_NO_EXTRACT TRUE ) +FetchContent_MakeAvailable(AntlrJar) +set(ANTLR4_JAR_LOCATION "${FETCHCONTENT_BASE_DIR}/antlr-jar/antlr.jar") -# Set target names for the executables -if(APPLE OR WIN32) - # OS X and Windows get a mixed-case binary name - set(TARGET_NAME ${PROJECT_NAME}) -elseif(EMSCRIPTEN) - set(TARGET_NAME ${PROJECT_NAME_LOWER}) -else() - # Linux/other UNIX get a lower-case binary name - set(TARGET_NAME ${PROJECT_NAME_LOWER}) +# ---------------------------- +# Dependency: miniupnpc. +option(UPNP "Enable UPNP support." ON) +if(UPNP) + find_package(miniupnpc CONFIG REQUIRED) + target_compile_definitions(${TARGET_NAME} PUBLIC ENABLE_UPNP) + target_link_libraries(${TARGET_NAME} PRIVATE miniupnpc::miniupnpc miniupnpc::miniupnpc-private) endif() -set(TARGET_NAME_OLD ${TARGET_NAME}) -set(TARGET_NAME "lib${TARGET_NAME}") -if(STATIC) - set(LIBRARY_TYPE STATIC) -else() - set(LIBRARY_TYPE SHARED) +# ---------------------------- +# Dependency: gs2lib. +FetchContent_Declare(gs2lib + GIT_REPOSITORY https://xtjoeytx@bitbucket.org/xtjoeytx/gs2lib.git + GIT_TAG 63b1ae96491c188905b50c6b61c8532c601a2122 + #URL "${PROJECT_SOURCE_DIR}/../gs2lib" +) +FetchContent_MakeAvailable(gs2lib) +add_dependencies(${TARGET_NAME} gs2lib) +target_link_libraries(${TARGET_NAME} PUBLIC gs2lib) +target_include_directories(${TARGET_NAME} PUBLIC "${gs2lib_SOURCE_DIR}/include") + +if(TESTS) + target_link_libraries(${APP_LIBRARY_NAME} PUBLIC gs2lib) + target_include_directories(${APP_LIBRARY_NAME} PUBLIC "${gs2lib_SOURCE_DIR}/include") +endif () + +# ---------------------------- +# Dependency: libtommath +FetchContent_Declare(libtommath + GIT_REPOSITORY https://github.com/libtom/libtommath.git + GIT_TAG develop + EXCLUDE_FROM_ALL +) +#FetchContent_GetProperties(libtommath) +#if(NOT libtommath_POPULATED) +# FetchContent_Populate(libtommath) +# add_subdirectory(${libtommath_SOURCE_DIR} ${libtommath_BINARY_DIR} EXCLUDE_FROM_ALL) +#endif() +FetchContent_MakeAvailable(libtommath) +target_include_directories(${TARGET_NAME} PUBLIC "${libtommath_SOURCE_DIR}") + +# ---------------------------- +# Dependency: libtomcrypt-preagonal. +FetchContent_Declare(tomcrypt + GIT_REPOSITORY https://github.com/Preagonal/libtomcrypt.git + GIT_TAG cipher-desgr + EXCLUDE_FROM_ALL + FIND_PACKAGE_ARGS CONFIG +) +#FetchContent_GetProperties(tomcrypt) +#if(NOT tomcrypt_POPULATED) +# FetchContent_Populate(tomcrypt) +# add_subdirectory(${tomcrypt_SOURCE_DIR} ${tomcrypt_BINARY_DIR} EXCLUDE_FROM_ALL) +#endif() +FetchContent_MakeAvailable(tomcrypt) +target_compile_definitions(libtomcrypt PUBLIC LTC_NOTHING LTC_MD5 LTC_DES LTC_DESGR LTC_BASE64) +target_compile_definitions(libtomcrypt PUBLIC LTC_ECB_MODE) # Need to define this to avoid a broken header. +set_target_properties(libtomcrypt PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX}) +target_link_libraries(${TARGET_NAME} PUBLIC tomcrypt$<$:${CMAKE_DEBUG_POSTFIX}>) +target_include_directories(${TARGET_NAME} PUBLIC "${libtomcrypt_SOURCE_DIR}/src/headers") +add_dependencies(libtomcrypt libtommath) +add_dependencies(${TARGET_NAME} libtomcrypt) + +# ---------------------------- +# Generate grammar. +antlr4_generate(GS1Lexer "${CMAKE_CURRENT_SOURCE_DIR}/src/scripting/gs1/grammar/GS1Lexer.g4" LEXER + FALSE # 4: Generate listener + FALSE # 5: Generate visitor + "preagonal::gs1::grammar" # 6: Namespace +) +message("${CMAKE_CURRENT_SOURCE_DIR}/src/scripting/gs1/GS1Parser.g4") +antlr4_generate(GS1Parser "${CMAKE_CURRENT_SOURCE_DIR}/src/scripting/gs1/grammar/GS1Parser.g4" PARSER + FALSE # 4: Generate listener + TRUE # 5: Generate visitor + "preagonal::gs1::grammar" # 6: Namespace + "${ANTLR4_TOKEN_FILES_GS1Lexer}" # 7: Dependency - Token files + "${ANTLR4_TOKEN_DIRECTORY_GS1Lexer}" # 8: Location of grammars - Token directory +) +target_include_directories(${TARGET_NAME} PRIVATE ${ANTLR4_INCLUDE_DIR_GS1Lexer} ${ANTLR4_INCLUDE_DIR_GS1Parser}) +target_sources(${TARGET_NAME} PRIVATE ${ANTLR4_SRC_FILES_GS1Lexer} ${ANTLR4_SRC_FILES_GS1Parser}) +if(TESTS) + target_include_directories(${APP_LIBRARY_NAME} PUBLIC ${ANTLR4_INCLUDE_DIR_GS1Lexer} ${ANTLR4_INCLUDE_DIR_GS1Parser}) + target_sources(${APP_LIBRARY_NAME} PUBLIC ${ANTLR4_SRC_FILES_GS1Lexer} ${ANTLR4_SRC_FILES_GS1Parser}) endif() -if(UPNP AND NOT MINIUPNPC_FOUND) - include_directories(${PROJECT_SOURCE_DIR}/dependencies/miniupnp ${PROJECT_SOURCE_DIR}/dependencies/miniupnp/miniupnpc) +# ---------------------------- +# Windows libraries. +if(WIN32) + target_link_libraries(${TARGET_NAME} PRIVATE ws2_32 wsock32 iphlpapi) endif() - -include_directories(${PROJECT_SOURCE_DIR}/dependencies/gs2lib/include) - +# ---------------------------- +# macOS specific settings if(APPLE) - add_library(${TARGET_NAME} ${LIBRARY_TYPE} ${SOURCES} ${HEADERS}) - # Enable ARC (automatic reference counting) for OS X build set_property( TARGET ${TARGET_NAME} APPEND_STRING PROPERTY COMPILE_FLAGS "-fobjc-arc" ) -elseif(WIN32) +endif() + +# ---------------------------- +# Windows and MinGW specific settings +if(WIN32) if(MINGW) # Generate version header from the above configure_file( - ${PROJECT_SOURCE_DIR}/server/include/windresrc.h.in - ${PROJECT_BINARY_DIR}/windresrc.h + ${PROJECT_SOURCE_DIR}/server/include/windresrc.h.in + ${PROJECT_BINARY_DIR}/windresrc.h ) configure_file( - ${PROJECT_SOURCE_DIR}/my.rc.in - ${PROJECT_BINARY_DIR}/main.rc + ${PROJECT_SOURCE_DIR}/my.rc.in + ${PROJECT_BINARY_DIR}/main.rc ) file(COPY - ${PROJECT_SOURCE_DIR}/gs2emu.ico - DESTINATION - ${PROJECT_BINARY_DIR}/ - ) - list(APPEND EXE_HEADERS - ${PROJECT_BINARY_DIR}/windresrc.h - ${PROJECT_BINARY_DIR}/main.rc - ) + ${PROJECT_SOURCE_DIR}/gs2emu.ico + DESTINATION + ${PROJECT_BINARY_DIR}/ + ) + target_sources(${TARGET_NAME} + PUBLIC + ${PROJECT_BINARY_DIR}/windresrc.h + ${PROJECT_BINARY_DIR}/main.rc + ) + set(CMAKE_RC_COMPILER_INIT windres) ENABLE_LANGUAGE(RC) - SET(CMAKE_RC_COMPILE_OBJECT - " -O coff -i -o ") + SET(CMAKE_RC_COMPILE_OBJECT " -O coff -i -o ") endif() - add_library(${TARGET_NAME} ${LIBRARY_TYPE} ${SOURCES} ${HEADERS}) - - if(MINGW) - if(V8NPCSERVER) - set_target_properties(${TARGET_NAME} PROPERTIES COMPILE_FLAGS "-DV8_COMPRESS_POINTERS -DV8_31BIT_SMIS_ON_64BIT_ARCH") - endif() - endif() if(MSVC) set_target_properties(${TARGET_NAME} PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE") set_target_properties(${TARGET_NAME} PROPERTIES COMPILE_DEFINITIONS_DEBUG "_CONSOLE") @@ -220,174 +392,74 @@ elseif(WIN32) set_target_properties(${TARGET_NAME} PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:CONSOLE") set_target_properties(${TARGET_NAME} PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:CONSOLE") set_target_properties(${TARGET_NAME} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/bin") - - if(V8NPCSERVER) - set_target_properties(${TARGET_NAME} PROPERTIES COMPILE_FLAGS "-DV8_COMPRESS_POINTERS -DV8_31BIT_SMIS_ON_64BIT_ARCH") - - # Using MSVC now looks for V8 as a nuget package first - # Looking in lib folders if not found - if(NOT V8_FOUND) - message("Findv8 failed, looking for v8 in supplied folders") - target_link_libraries(${TARGET_NAME} v8.dll.lib v8_libbase.dll.lib v8_libplatform.dll.lib) - endif() - endif() - endif() -elseif(EMSCRIPTEN) - add_library(${TARGET_NAME} ${LIBRARY_TYPE} ${SOURCES} ${HEADERS}) -else() - add_library(${TARGET_NAME} ${LIBRARY_TYPE} ${SOURCES} ${HEADERS}) -endif() - -set_target_properties(${TARGET_NAME} PROPERTIES PREFIX "") -target_compile_definitions(${TARGET_NAME} PRIVATE NOMAIN) - -add_executable(${TARGET_NAME_OLD} src/main.cpp ${EXE_HEADERS}) -target_link_libraries(${TARGET_NAME_OLD} PRIVATE ${TARGET_NAME}) - -target_link_libraries(${TARGET_NAME} ${CMAKE_THREAD_LIBS_INIT}) - -target_include_directories(${TARGET_NAME} PUBLIC ${GS2LIB_INCLUDE_DIRECTORY}) - -target_include_directories(${TARGET_NAME} PUBLIC ${GS2COMPILER_INCLUDE_DIRECTORY}) - -add_dependencies(${TARGET_NAME} gs2compiler) -target_link_libraries(${TARGET_NAME} gs2compiler) -add_dependencies(${TARGET_NAME} gs2lib) -target_link_libraries(${TARGET_NAME} gs2lib) - -if(UPNP) - if(NOT MINIUPNPC_FOUND) - if(NOT STATIC) - add_dependencies(${TARGET_NAME} libminiupnpc-shared) - target_link_libraries(${TARGET_NAME} libminiupnpc-shared) - else() - add_dependencies(${TARGET_NAME} libminiupnpc-static) - target_link_libraries(${TARGET_NAME} libminiupnpc-static) - endif() - else() - target_link_libraries(${TARGET_NAME} ${MINIUPNP_LIBRARIES}) endif() endif() +# ---------------------------- +# More MinGW if (MINGW) - if(STATIC) - target_link_options(${TARGET_NAME} PRIVATE -static -fstack-protector) - target_link_libraries(${TARGET_NAME} -static-libgcc -static-libstdc++) - target_link_options(${TARGET_NAME_OLD} PRIVATE -static -fstack-protector) - target_link_libraries(${TARGET_NAME_OLD} PUBLIC -static-libgcc -static-libstdc++) + # Erase the INTERFACE_LINK_LIBRARIES from the miniupnpc::miniupnpc-private target. + # They are linked normally and the cmake configuration for miniupnpc causes bad compiler flags. + if(UPNP) + set_target_properties(miniupnpc::miniupnpc-private PROPERTIES INTERFACE_LINK_LIBRARIES "") endif() - install(CODE "set(MY_EXE \"${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET_NAME_OLD}.exe\")") + target_link_options(${TARGET_NAME} PRIVATE -static -fstack-protector) + target_link_libraries(${TARGET_NAME} PRIVATE -static-libgcc) + + install(CODE "set(MY_EXE \"${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET_NAME}.exe\")") # Transfer the value of ${MY_DEPENDENCY_PATHS} into the install script install(CODE "set(MY_DEPENDENCY_PATHS \"${CMAKE_FIND_DLL_PATH}\")") install(CODE [[ - set(CMAKE_GET_RUNTIME_DEPENDENCIES_PLATFORM "windows+pe") - set(CMAKE_GET_RUNTIME_DEPENDENCIES_TOOL "objdump") - set(CMAKE_GET_RUNTIME_DEPENDENCIES_COMMAND "./objdump_unix2dos.sh") - - write_file("./objdump_unix2dos.sh" "${CMAKE_OBJDUMP} $@ | unix2dos") - file(CHMOD "./objdump_unix2dos.sh" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) - - function(install_library_with_deps LIBRARY) - message("Getting dependencies for ${LIBRARY}") - - file(INSTALL - DESTINATION "${CMAKE_INSTALL_PREFIX}/" - TYPE SHARED_LIBRARY - FOLLOW_SYMLINK_CHAIN - FILES "${LIBRARY}" - ) + set(CMAKE_GET_RUNTIME_DEPENDENCIES_PLATFORM "windows+pe") + set(CMAKE_GET_RUNTIME_DEPENDENCIES_TOOL "objdump") + set(CMAKE_GET_RUNTIME_DEPENDENCIES_COMMAND "./objdump_unix2dos.sh") + + write_file("./objdump_unix2dos.sh" "${CMAKE_OBJDUMP} $@ | unix2dos") + file(CHMOD "./objdump_unix2dos.sh" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) + + function(install_library_with_deps LIBRARY) + message("Getting dependencies for ${LIBRARY}") + + file(INSTALL + DESTINATION "${CMAKE_INSTALL_PREFIX}/" + TYPE SHARED_LIBRARY + FOLLOW_SYMLINK_CHAIN + FILES "${LIBRARY}" + ) + file(GET_RUNTIME_DEPENDENCIES + LIBRARIES ${LIBRARY} + RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS + UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS + DIRECTORIES ${MY_DEPENDENCY_PATHS} + ) + foreach(FILE ${RESOLVED_DEPS}) + if(NOT IS_SYMLINK ${FILE}) + install_library_with_deps(${FILE}) + else() + message( "Symlink ${LIBRARY}: ${FILE}") + endif() + endforeach() + foreach(FILE ${UNRESOLVED_DEPS}) + message( "Unresolved from ${LIBRARY}: ${FILE}") + endforeach() + endfunction() + message("Getting dependencies for ${MY_EXE}") file(GET_RUNTIME_DEPENDENCIES - LIBRARIES ${LIBRARY} - RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS - UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS - DIRECTORIES ${MY_DEPENDENCY_PATHS} + EXECUTABLES ${MY_EXE} + RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS + UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS + DIRECTORIES ${MY_DEPENDENCY_PATHS} ) foreach(FILE ${RESOLVED_DEPS}) - if(NOT IS_SYMLINK ${FILE}) install_library_with_deps(${FILE}) - else() - message( "Symlink ${LIBRARY}: ${FILE}") - endif() endforeach() foreach(FILE ${UNRESOLVED_DEPS}) - message( "Unresolved from ${LIBRARY}: ${FILE}") + message( "Unresolved: ${FILE}") endforeach() - endfunction() - message("Getting dependencies for ${MY_EXE}") - file(GET_RUNTIME_DEPENDENCIES - EXECUTABLES ${MY_EXE} - RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS - UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS - DIRECTORIES ${MY_DEPENDENCY_PATHS} - ) - foreach(FILE ${RESOLVED_DEPS}) - install_library_with_deps(${FILE}) - endforeach() - foreach(FILE ${UNRESOLVED_DEPS}) - message( "Unresolved: ${FILE}") - endforeach() ]]) endif() -if(V8NPCSERVER) - if(NOT V8_FOUND) - add_dependencies(${TARGET_NAME} v8) - target_link_libraries(${TARGET_NAME} ${V8_LIBRARY}) - else() - if(V8_LIBRARY) - target_link_libraries(${TARGET_NAME} ${V8_LIBRARY}) - else() - target_link_libraries(${TARGET_NAME} ${V8_MAIN_LIBRARY} ${V8_BASE_LIBRARY} ${V8_PLATFORM_LIBRARY}) - set(INSTALL_DEST .) - install(FILES ${V8_REDIST_LIBS} DESTINATION ${INSTALL_DEST}) - endif() - endif() - - message("V8 include: ${V8_INCLUDE_DIR}") - include_directories(${V8_INCLUDE_DIR}) - - target_link_libraries(${TARGET_NAME} httplib::httplib OpenSSL::SSL OpenSSL::Crypto) - - if(zstd_FOUND) - target_link_libraries(${TARGET_NAME} zstd) - endif() - - add_custom_command( - OUTPUT ${PROJECT_BINARY_DIR}/server/include/EmbeddedBootstrapScript.h - COMMAND ${CMAKE_COMMAND} - -DSOURCE_FILE=${PROJECT_SOURCE_DIR}/bin/servers/default/bootstrap.js - -DHEADER_FILE=${PROJECT_BINARY_DIR}/server/include/EmbeddedBootstrapScript.h - -DVARIABLE_NAME=JSBOOTSTRAPSCRIPT - -P "${CMAKE_SOURCE_DIR}/cmake/generate_header_file.cmake" - COMMENT "Generating \"bootstrap.js\" header file..." - WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" - DEPENDS "${PROJECT_SOURCE_DIR}/bin/servers/default/bootstrap.js" - VERBATIM - ) - - add_custom_target(bootstrap_js_to_h DEPENDS ${PROJECT_BINARY_DIR}/server/include/EmbeddedBootstrapScript.h) - add_dependencies(${TARGET_NAME} bootstrap_js_to_h) -endif() - -if(WIN32) - target_link_libraries(${TARGET_NAME} ws2_32 wsock32 iphlpapi) -endif() - -set(APP_LIBRARY_NAME - "${TARGET_NAME}" - PARENT_SCOPE) - -file(GLOB TEXT - "${PROJECT_NAME_LOWER}.wasm" -) - -set(INSTALL_DEST ".") - -install(FILES ${TEXT} DESTINATION ${INSTALL_DEST}) - set(INSTALL_DEST .) - -install(TARGETS ${TARGET_NAME_OLD} DESTINATION ${INSTALL_DEST}) - +install(TARGETS ${TARGET_NAME} RUNTIME DESTINATION ${INSTALL_DEST}) diff --git a/server/include/Account.h b/server/include/Account.h index 80f0a9213..5acbb1d6c 100644 --- a/server/include/Account.h +++ b/server/include/Account.h @@ -1,19 +1,31 @@ -#ifndef TACCOUNT_H -#define TACCOUNT_H +#ifndef ACCOUNT_H +#define ACCOUNT_H +#include +#include +#include #include +#include +#include #include #include +#include #include -#include -#include "BabyDI.h" +#include +#include +#include +#include +#include -#include "animation/Character.h" -#include "level/LevelChest.h" -#include "utilities/FilePermissions.h" +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class Player; -enum +enum PlayerPermissions { PLPERM_WARPTO = 0x00001, PLPERM_WARPTOPLAYER = 0x00002, @@ -39,302 +51,74 @@ enum PLPERM_ANYRIGHT = 0xFFFFFF }; -enum +struct SavedChest { - PLPROP_NICKNAME = 0, - PLPROP_MAXPOWER = 1, - PLPROP_CURPOWER = 2, - PLPROP_RUPEESCOUNT = 3, - PLPROP_ARROWSCOUNT = 4, - PLPROP_BOMBSCOUNT = 5, - PLPROP_GLOVEPOWER = 6, - PLPROP_BOMBPOWER = 7, - PLPROP_SWORDPOWER = 8, - PLPROP_SHIELDPOWER = 9, - PLPROP_GANI = 10, // PLPROP_BOWGIF in pre-2.x - PLPROP_HEADGIF = 11, - PLPROP_CURCHAT = 12, - PLPROP_COLORS = 13, - PLPROP_ID = 14, - PLPROP_X = 15, - PLPROP_Y = 16, - PLPROP_SPRITE = 17, - PLPROP_STATUS = 18, - PLPROP_CARRYSPRITE = 19, - PLPROP_CURLEVEL = 20, - PLPROP_HORSEGIF = 21, - PLPROP_HORSEBUSHES = 22, - PLPROP_EFFECTCOLORS = 23, - PLPROP_CARRYNPC = 24, - PLPROP_APCOUNTER = 25, - PLPROP_MAGICPOINTS = 26, - PLPROP_KILLSCOUNT = 27, - PLPROP_DEATHSCOUNT = 28, - PLPROP_ONLINESECS = 29, - PLPROP_IPADDR = 30, - PLPROP_UDPPORT = 31, - PLPROP_ALIGNMENT = 32, - PLPROP_ADDITFLAGS = 33, - PLPROP_ACCOUNTNAME = 34, - PLPROP_BODYIMG = 35, - PLPROP_RATING = 36, - PLPROP_GATTRIB1 = 37, - PLPROP_GATTRIB2 = 38, - PLPROP_GATTRIB3 = 39, - PLPROP_GATTRIB4 = 40, - PLPROP_GATTRIB5 = 41, - PLPROP_ATTACHNPC = 42, - PLPROP_GMAPLEVELX = 43, - PLPROP_GMAPLEVELY = 44, - PLPROP_Z = 45, - PLPROP_GATTRIB6 = 46, - PLPROP_GATTRIB7 = 47, - PLPROP_GATTRIB8 = 48, - PLPROP_GATTRIB9 = 49, - PLPROP_JOINLEAVELVL = 50, - PLPROP_PCONNECTED = 51, - PLPROP_PLANGUAGE = 52, - PLPROP_PSTATUSMSG = 53, - PLPROP_GATTRIB10 = 54, - PLPROP_GATTRIB11 = 55, - PLPROP_GATTRIB12 = 56, - PLPROP_GATTRIB13 = 57, - PLPROP_GATTRIB14 = 58, - PLPROP_GATTRIB15 = 59, - PLPROP_GATTRIB16 = 60, - PLPROP_GATTRIB17 = 61, - PLPROP_GATTRIB18 = 62, - PLPROP_GATTRIB19 = 63, - PLPROP_GATTRIB20 = 64, - PLPROP_GATTRIB21 = 65, - PLPROP_GATTRIB22 = 66, - PLPROP_GATTRIB23 = 67, - PLPROP_GATTRIB24 = 68, - PLPROP_GATTRIB25 = 69, - PLPROP_GATTRIB26 = 70, - PLPROP_GATTRIB27 = 71, - PLPROP_GATTRIB28 = 72, - PLPROP_GATTRIB29 = 73, - PLPROP_GATTRIB30 = 74, - PLPROP_OSTYPE = 75, // 2.19+ - PLPROP_TEXTCODEPAGE = 76, // 2.19+ - PLPROP_UNKNOWN77 = 77, - PLPROP_X2 = 78, - PLPROP_Y2 = 79, - PLPROP_Z2 = 80, - PLPROP_UNKNOWN81 = 81, // {GCHAR flag} - flag 0 places in playerlist, flag 1 places in servers tab, flag 3 places in channels tab (unconfirmed) - - // In Graal v5, where players have the Graal######## accounts, this is their chosen account alias (community name.) - PLPROP_COMMUNITYNAME = 82, + int8_t x; + int8_t y; + std::string level; }; -#define propscount 83 -class Server; -class Account +struct Account { -public: - // Constructor - Deconstructor - Account(); - ~Account(); - - static bool meetsConditions(CString fileName, CString conditions); - - // Load/Save Account - void reset(); - bool loadAccount(const CString& pAccount, bool ignoreNickname = false); - bool saveAccount(); - - // Attribute-Managing - bool hasChest(const CString& pChest); - bool hasWeapon(const CString& pWeapon); - - // Flag-Managing - CString getFlag(const std::string& pFlagName) const; - void setFlag(CString pFlag); - void setFlag(const std::string& pFlagName, const CString& pFlagValue); - void deleteFlag(const std::string& pFlagName); - - CString translate(const CString& pKey) const; - bool hasRight(int mask) const { return (m_adminRights & mask) ? true : false; } - - // get functions - bool getGuest() const { return m_isGuest; } - float getX() const { return m_x / 16.0f; } - float getY() const { return m_y / 16.0f; } - float getZ() const { return m_z / 16.0f; } - int16_t getPixelX() const { return m_x; } - int16_t getPixelY() const { return m_y; } - int16_t getPixelZ() const { return m_z; } - float getPower() const { return m_character.hitpoints; } - int getAlignment() const { return m_character.ap; } - int getArrowCount() const { return m_character.arrows; } - int getBombCount() const { return m_character.bombs; } - int getGlovePower() const { return m_character.glovePower; } - int getMagicPower() const { return m_mp; } - int getMaxPower() const { return m_maxHitpoints; } - int getRupees() const { return m_character.gralats; } - int getSwordPower() const { return m_character.swordPower; } - int getShieldPower() const { return m_character.shieldPower; } - int getSprite() const { return m_character.sprite; } - int getStatus() const { return m_status; } - int getOnlineTime() const { return m_onlineTime; } - int getKills() const { return m_kills; } - int getDeaths() const { return m_deaths; } - int getAdminRights() const { return m_adminRights; } - int64_t getDeviceId() const { return m_deviceId; } - bool getBanned() const { return m_isBanned; } - bool getLoadOnly() const { return m_isLoadOnly; } - unsigned char getColorId(unsigned int idx) const; - unsigned int getAttachedNPC() const { return m_attachNPC; } - - const CString& getAccountName() const { return m_accountName; } - const std::string& getNickname() const { return m_character.nickName; } - const CString& getLevelName() const { return m_levelName; } - const CString& getBodyImage() const { return m_character.bodyImage; } - const CString& getHeadImage() const { return m_character.headImage; } - //Level * getLevel() { return nullptr; } - const CString& getShieldImage() const { return m_character.shieldImage; } - const CString& getSwordImage() const { return m_character.swordImage; } - const CString& getAnimation() const { return m_character.gani; } - const CString& getAdminIp() const { return m_adminIp; } - const CString& getBanReason() const { return m_banReason; } - const CString& getBanLength() const { return m_banLength; } - const CString& getChatMsg() const { return m_character.chatMessage; } - const CString& getEmail() const { return m_email; } - const CString& getIpStr() const { return m_accountIpStr; } - const CString& getComments() const { return m_accountComments; } - std::unordered_map& getFlagList() { return m_flagList; } - const std::vector& getFolderList() const { return m_folderList; } - const FilePermissions& getFolderRights() const { return m_folderRights; } - std::vector& getWeaponList() { return m_weaponList; } - - // set functions - void setX(float val) { m_x = static_cast(val * 16); } - void setY(float val) { m_y = static_cast(val * 16); } - void setZ(float val) { m_z = static_cast(val * 16); } - void setPixelX(int16_t val) { m_x = val; } - void setPixelY(int16_t val) { m_y = val; } - void setPixelZ(int16_t val) { m_z = val; } - void setDeviceId(int64_t newDeviceId) { m_deviceId = newDeviceId; } - void setLastSparTime(time_t newTime) { m_lastSparTime = newTime; } - void setApCounter(int newTime) { m_apCounter = newTime; } - void setKills(int newKills) { m_kills = newKills; } - void setRating(int newRate, int newDeviate) - { - m_eloRating = (float)newRate; - m_eloDeviation = (float)newDeviate; - } - void setAccountName(CString account) { m_accountName = account; } - void setExternal(bool external) { m_isExternal = external; } - void setBanned(bool banned) { m_isBanned = banned; } - void setBanReason(CString reason) { m_banReason = reason; } - void setBanLength(CString length) { m_banLength = length; } - void setLoadOnly(bool loadOnly) { m_isLoadOnly = loadOnly; } - void setEmail(CString email) { m_email = email; } - void setAdminRights(int rights) { m_adminRights = rights; } - void setAdminIp(CString ip) { m_adminIp = ip; } - void setComments(CString comments) { m_accountComments = comments; } - - void setBodyImage(const CString& newImage); - void setHeadImage(const CString& newImage); - void setMaxPower(int newMaxPower); - void setPower(float newPower); - void setShieldImage(const CString& newImage); - void setShieldPower(int newPower); - void setSwordImage(const CString& newImage); - void setSwordPower(int newPower); - void setGani(const CString& newGani); - void setFolderRights(const std::vector& folderRights); - -protected: - BabyDI_INJECT(Server, m_server); - - // Player-Account - bool m_isBanned = false; - bool m_isLoadOnly = false; - bool m_isGuest = false; - bool m_isExternal = false; - CString m_adminIp{ "0.0.0.0" }; - CString m_accountComments, m_accountName, m_communityName, m_banReason, m_banLength, m_lastFolder, m_email; - CString m_accountIpStr; - unsigned long m_accountIp = 0; - int m_adminRights = 0; - int64_t m_deviceId = 0; - - // Player-Attributes - Character m_character; - - CString m_language{ "English" }; - CString m_levelName; - int16_t m_x = 0, m_y = 0, m_z = 0; - float m_eloDeviation = 350.0f; - float m_eloRating = 1500.0f; - uint8_t m_maxHitpoints = 3; - uint8_t m_mp = 0; - uint8_t m_apCounter = 0; - uint8_t m_horseBombCount = 0; - uint32_t m_kills = 0; - uint32_t m_deaths = 0; - uint8_t m_carrySprite = -1; - int m_additionalFlags = 0; - int m_onlineTime = 0; - int m_status = 20; - int m_udpport = 0; - time_t m_lastSparTime = 0; - uint32_t m_attachNPC = 0; - uint8_t m_statusMsg = 0; - std::unordered_map m_flagList; - std::vector m_chestList, m_folderList, m_weaponList, m_privateMessageServerList; - FilePermissions m_folderRights; + std::string name; + std::string communityName; + std::string level; + Character character; + uint8_t maxHitpoints = 3; + uint8_t status = 20; + uint16_t apCounter = 0; // GR only? + uint32_t onlineSeconds = 0; // GR only? + std::string ipAddress; + std::string language{ "English" }; + std::string groupName; + uint32_t kills = 0; + uint32_t deaths = 0; + float eloRating = 1500.0f; + float eloDeviation = 350.0f; + std::chrono::system_clock::time_point lastSparTime; + std::array ganiAttributes; + std::vector weapons; + std::unordered_multimap savedChests; + GameVariableStore variables; + bool banned = false; + std::string banReason; + std::string banLength; + std::string comments; + std::string email; + uint32_t adminRights = 0; + std::vector adminIpRange; + bool loadOnly = false; + FilePermissions folderRights; + std::vector folderList; + std::string lastFolderAccessed; + + [[inline]] bool hasRight(uint32_t right) const; + [[inline]] bool hasChest(std::string_view level, const LocalWholeTilePosition& position) const; + [[inline]] bool hasWeapon(std::string_view weapon) const; }; -inline CString Account::getFlag(const std::string& pFlagName) const +inline bool Account::hasRight(uint32_t right) const { - auto it = m_flagList.find(pFlagName); - if (it != m_flagList.end()) - return it->second; - return ""; + return (adminRights & right); } -inline void Account::deleteFlag(const std::string& pFlagName) +inline bool Account::hasChest(std::string_view level, const LocalWholeTilePosition& position) const { - m_flagList.erase(pFlagName); -} - -inline unsigned char Account::getColorId(unsigned int idx) const -{ - if (idx < 5) return m_character.colors[idx]; - return 0; -} - -inline void Account::setPower(float newPower) -{ - m_character.hitpoints = clip(newPower, 0.0f, (float)m_maxHitpoints); -} - -inline void Account::setShieldImage(const CString& newImage) -{ - m_character.shieldImage = newImage.subString(0, 223); -} - -inline void Account::setSwordImage(const CString& newImage) -{ - m_character.swordImage = newImage.subString(0, 223); -} - -inline void Account::setGani(const CString& newGani) -{ - m_character.gani = newGani.subString(0, 223); + auto range = savedChests.equal_range(level.data()); + for (auto& i = range.first; i != range.second; ++i) + { + if (i->second == position) + return true; + } + return false; } -inline void Account::setBodyImage(const CString& newImage) +inline bool Account::hasWeapon(std::string_view weapon) const { - m_character.bodyImage = newImage.subString(0, 223); + return std::ranges::find_if(weapons, [&weapon](const auto& w) { return string::equalsi(w, weapon); }) != std::ranges::end(weapons); } -inline void Account::setHeadImage(const CString& newImage) -{ - m_character.headImage = newImage.subString(0, 123); -} +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal -#endif // TACCOUNT_H +#endif // ACCOUNT_H diff --git a/server/include/BabyDI.h b/server/include/BabyDI.h index 9b0cea278..812d5b0ae 100644 --- a/server/include/BabyDI.h +++ b/server/include/BabyDI.h @@ -1,10 +1,8 @@ #ifndef BABYDI_H #define BABYDI_H -#include -#include #include -#include +#include #include #include @@ -93,6 +91,7 @@ namespace BabyDI m_provisions.erase(itr); } + /* template static void AssertAllProvided(F&& assertCallback) { @@ -108,6 +107,7 @@ namespace BabyDI assertCallback(interfaceNames); } } + */ template static T* Get(size_t hash) @@ -131,7 +131,7 @@ namespace BabyDI template struct InjectMeta : public InjectMetaBase { - constexpr InjectMeta(T** injectAddress, const char* interfaceName) : m_injectAddress(injectAddress), m_interfaceName(interfaceName) + constexpr InjectMeta(T** injectAddress, const char* interfaceName) : m_interfaceName(interfaceName), m_injectAddress(injectAddress) { ::BabyDI::InjectionRepository::AddInjectMeta(this); } @@ -169,11 +169,13 @@ namespace BabyDI T** const m_injectAddress; }; + /* template static void AssertAllProvided(F&& assertCallback) { InjectionRepository::AssertAllProvided(assertCallback); } + */ /** * Gets an injected implementation, returning null if it's unprovided. @@ -185,6 +187,7 @@ namespace BabyDI } #ifndef BABYDI_EMBEDDED + /* static void AssertAllProvided() { AssertAllProvided([](const std::vector& interfaceNames) @@ -199,6 +202,7 @@ namespace BabyDI std::terminate(); }); } + */ #endif }; // namespace BabyDI diff --git a/server/include/FileSystem.h b/server/include/FileSystem.h deleted file mode 100644 index abb2fc3f7..000000000 --- a/server/include/FileSystem.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef CFILESYSTEM_H -#define CFILESYSTEM_H - -#include -#include - -#include -#include "BabyDI.h" - -class Server; -class FileSystem -{ -#if defined(_WIN32) || defined(_WIN64) - static const char fSep = '\\'; - static const char fSep_O = '/'; -#else - static const char fSep = '/'; - static const char fSep_O = '\\'; -#endif - -public: - FileSystem(); - ~FileSystem(); - void clear(); - - void addDir(const CString& dir, const CString& wildcard = "*", bool forceRecursive = false); - void removeDir(const CString& dir); - void addFile(CString file); - void removeFile(const CString& file); - void resync(); - - CString find(const CString& file) const; - CString findi(const CString& file) const; - CString fileExistsAs(const CString& file) const; - CString load(const CString& file) const; - time_t getModTime(const CString& file) const; - bool setModTime(const CString& file, time_t modTime) const; - int getFileSize(const CString& file) const; - std::map& getFileList() { return m_fileList; } - std::vector* getDirList() { return &m_directoryList; } - CString getDirByExtension(const std::string& extension) const; - - mutable std::recursive_mutex* m_preventChange; - - static constexpr char getPathSeparator(); - static void fixPathSeparators(CString& pPath); - -private: - void loadAllDirectories(const CString& directory, bool recursive = false); - - BabyDI_INJECT(Server, m_server); - - CString m_basedir; - std::map m_fileList; - std::vector m_directoryList; -}; - -inline void FileSystem::fixPathSeparators(CString& pPath) -{ - pPath.replaceAllI(fSep_O, fSep); -} - -constexpr char FileSystem::getPathSeparator() -{ - return fSep; -} - -#endif diff --git a/server/include/NPC.h b/server/include/NPC.h deleted file mode 100644 index dfa792312..000000000 --- a/server/include/NPC.h +++ /dev/null @@ -1,696 +0,0 @@ -#ifndef TNPC_H -#define TNPC_H - -#include -#include -#include - -#include "BabyDI.h" -#include -#include - -#include "animation/Character.h" -#include "scripting/SourceCode.h" - -#ifdef V8NPCSERVER - #include - #include - #include - - #include "scripting/ScriptAction.h" - #include "scripting/ScriptExecutionContext.h" - #include "scripting/interface/ScriptBindings.h" -#endif - -enum -{ - NPCPROP_IMAGE = 0, - NPCPROP_SCRIPT = 1, - NPCPROP_X = 2, - NPCPROP_Y = 3, - NPCPROP_POWER = 4, - NPCPROP_RUPEES = 5, - NPCPROP_ARROWS = 6, - NPCPROP_BOMBS = 7, - NPCPROP_GLOVEPOWER = 8, - NPCPROP_BOMBPOWER = 9, - NPCPROP_SWORDIMAGE = 10, - NPCPROP_SHIELDIMAGE = 11, - NPCPROP_GANI = 12, // NPCPROP_BOWGIF in pre-2.x - NPCPROP_VISFLAGS = 13, - NPCPROP_BLOCKFLAGS = 14, - NPCPROP_MESSAGE = 15, - NPCPROP_HURTDXDY = 16, - NPCPROP_ID = 17, - NPCPROP_SPRITE = 18, - NPCPROP_COLORS = 19, - NPCPROP_NICKNAME = 20, - NPCPROP_HORSEIMAGE = 21, - NPCPROP_HEADIMAGE = 22, - NPCPROP_SAVE0 = 23, - NPCPROP_SAVE1 = 24, - NPCPROP_SAVE2 = 25, - NPCPROP_SAVE3 = 26, - NPCPROP_SAVE4 = 27, - NPCPROP_SAVE5 = 28, - NPCPROP_SAVE6 = 29, - NPCPROP_SAVE7 = 30, - NPCPROP_SAVE8 = 31, - NPCPROP_SAVE9 = 32, - NPCPROP_ALIGNMENT = 33, - NPCPROP_IMAGEPART = 34, - NPCPROP_BODYIMAGE = 35, - NPCPROP_GATTRIB1 = 36, - NPCPROP_GATTRIB2 = 37, - NPCPROP_GATTRIB3 = 38, - NPCPROP_GATTRIB4 = 39, - NPCPROP_GATTRIB5 = 40, - NPCPROP_GMAPLEVELX = 41, - NPCPROP_GMAPLEVELY = 42, - - NPCPROP_Z = 43, - - NPCPROP_GATTRIB6 = 44, - NPCPROP_GATTRIB7 = 45, - NPCPROP_GATTRIB8 = 46, - NPCPROP_GATTRIB9 = 47, - - NPCPROP_UNKNOWN48 = 48, - NPCPROP_SCRIPTER = 49, // My guess is UNKNOWN48 or this is the scripter's name - NPCPROP_NAME = 50, - NPCPROP_TYPE = 51, - NPCPROP_CURLEVEL = 52, - - NPCPROP_GATTRIB10 = 53, - NPCPROP_GATTRIB11 = 54, - NPCPROP_GATTRIB12 = 55, - NPCPROP_GATTRIB13 = 56, - NPCPROP_GATTRIB14 = 57, - NPCPROP_GATTRIB15 = 58, - NPCPROP_GATTRIB16 = 59, - NPCPROP_GATTRIB17 = 60, - NPCPROP_GATTRIB18 = 61, - NPCPROP_GATTRIB19 = 62, - NPCPROP_GATTRIB20 = 63, - NPCPROP_GATTRIB21 = 64, - NPCPROP_GATTRIB22 = 65, - NPCPROP_GATTRIB23 = 66, - NPCPROP_GATTRIB24 = 67, - NPCPROP_GATTRIB25 = 68, - NPCPROP_GATTRIB26 = 69, - NPCPROP_GATTRIB27 = 70, - NPCPROP_GATTRIB28 = 71, - NPCPROP_GATTRIB29 = 72, - NPCPROP_GATTRIB30 = 73, - - NPCPROP_CLASS = 74, // NPC-Server class. Possibly also join scripts. - NPCPROP_X2 = 75, - NPCPROP_Y2 = 76, - NPCPROP_Z2 = 77, - - NPCPROP_COUNT -}; - -//! NPCPROP_VISFLAGS values. -enum -{ - NPCVISFLAG_VISIBLE = 0x01, - NPCVISFLAG_DRAWOVERPLAYER = 0x02, - NPCVISFLAG_DRAWUNDERPLAYER = 0x04, -}; - -//! NPCPROP_BLOCKFLAGS values. -enum -{ - NPCBLOCKFLAG_BLOCK = 0x00, - NPCBLOCKFLAG_NOBLOCK = 0x01, -}; - -//! NPCMOVE_FLAGS values -enum -{ - NPCMOVEFLAG_NOCACHE = 0x00, - NPCMOVEFLAG_CACHE = 0x01, - NPCMOVEFLAG_APPEND = 0x02, - NPCMOVEFLAG_BLOCKCHECK = 0x04, - NPCMOVEFLAG_EVENTWHENDONE = 0x08, - NPCMOVEFLAG_APPLYDIR = 0x10 -}; - -enum class NPCType -{ - LEVELNPC, // npcs found in a level - PUTNPC, // npcs created via script (putnpc) - DBNPC // npcs created in RC (Database-NPCs) -}; - -#ifdef V8NPCSERVER - -enum class NPCEventResponse -{ - NoEvents, - PendingEvents, - Delete -}; - -enum class NPCWarpType -{ - None, - AllLinks, - OverworldLinks -}; - -//! NPC Event Flags -enum -{ - NPCEVENTFLAG_CREATED = (int)(1 << 0), - NPCEVENTFLAG_TIMEOUT = (int)(1 << 1), - NPCEVENTFLAG_PLAYERCHATS = (int)(1 << 2), - NPCEVENTFLAG_PLAYERENTERS = (int)(1 << 3), - NPCEVENTFLAG_PLAYERLEAVES = (int)(1 << 4), - NPCEVENTFLAG_PLAYERTOUCHSME = (int)(1 << 5), - NPCEVENTFLAG_PLAYERLOGIN = (int)(1 << 6), - NPCEVENTFLAG_PLAYERLOGOUT = (int)(1 << 7), - NPCEVENTFLAG_NPCWARPED = (int)(1 << 8), -}; - -struct ScriptEventTimer -{ - unsigned int timer; - ScriptAction action; -}; - -#endif - -class Server; -class Level; -class Player; -class ScriptClass; -class NPC -{ -public: - NPC(NPCType type); - NPC(const CString& pImage, std::string pScript, float pX, float pY, std::shared_ptr pLevel, NPCType type); - ~NPC(); - - void setScriptCode(std::string pScript); - - // prop functions - CString getProp(unsigned char pId, int clientVersion = CLVER_2_17) const; - CString getProps(time_t newTime, int clientVersion = CLVER_2_17) const; - CString setProps(CString& pProps, int clientVersion = CLVER_2_17, bool pForward = false); - void setPropModTime(unsigned char pid, time_t time); - - // NPCPROP functions begin - - const CString& getChat() const; - void setChat(const CString& msg); - - const CString& getGani() const; - void setGani(const CString& gani); - - const std::string& getImage() const; - void setImage(const std::string& image); - void setImage(const std::string& image, int offsetx, int offsety, int widt, int height); - - const std::string& getNickname() const; - void setNickname(const std::string& nick); - - unsigned char getSave(unsigned int idx) const; - void setSave(unsigned int idx, unsigned char val); - - int getRupees() const; - void setRupees(int val); - - int getDarts() const; - void setDarts(int val); - - const CString& getBodyImage() const; - void setBodyImage(const std::string& pBodyImage); - - const CString& getHeadImage() const; - void setHeadImage(const std::string& pHeadImage); - - const CString& getHorseImage() const; - void setHorseImage(const std::string& pHeadImage); - - const CString& getShieldImage() const; - void setShieldImage(const std::string& pShieldImage); - - const CString& getSwordImage() const; - void setSwordImage(const std::string& pSwordImage); - - // NPCPROP functions end - - // set functions - void setId(unsigned int pId) { m_id = pId; } - void setLevel(std::shared_ptr pLevel) { m_curlevel = pLevel; } - void setX(float val) { m_x = static_cast(val * 16); } - void setY(float val) { m_y = static_cast(val * 16); } - void setZ(float val) { m_z = static_cast(val * 16); } - void setPixelX(int16_t val) { m_x = val; } - void setPixelY(int16_t val) { m_y = val; } - void setPixelZ(int16_t val) { m_z = val; } - void setHeight(int val) { m_height = val; } - void setWidth(int val) { m_width = val; } - void setName(const std::string& name) { m_npcName = name; } - void setScripter(const CString& name) { m_npcScripter = name; } - void setScriptType(const CString& type) { m_npcScriptType = type; } - void setBlockingFlags(int val) { m_blockFlags = val; } - void setVisibleFlags(int val) { m_visFlags = val; } - void setColorId(unsigned int idx, unsigned char val); - void setSprite(int val) { m_character.sprite = val; } - - // get functions - unsigned int getId() const { return m_id; } - NPCType getType() const { return m_npcType; } - float getX() const { return m_x / 16.0f; } - float getY() const { return m_y / 16.0f; } - float getZ() const { return m_z / 16.0f; } - int16_t getPixelX() const { return m_x; } - int16_t getPixelY() const { return m_y; } - int16_t getPixelZ() const { return m_z; } - int getHeight() const { return m_height; } - int getWidth() const { return m_width; } - unsigned char getSprite() const { return m_character.sprite; } - int getBlockFlags() const { return m_blockFlags; } - int getVisibleFlags() const { return m_visFlags; } - int getTimeout() const { return m_timeout; } - - const SourceCode& getSource() const { return m_npcScript; } - const std::string& getName() const { return m_npcName; } - const CString& getScriptType() const { return m_npcScriptType; } - const CString& getScripter() const { return m_npcScripter; } - const CString& getWeaponName() const { return m_weaponName; } - std::shared_ptr getLevel() const; - time_t getPropModTime(unsigned char pId) const; - unsigned char getColorId(unsigned int idx) const; - - const CString& getByteCode() const - { - return m_npcBytecode; - } - -#ifdef V8NPCSERVER - bool getIsNpcDeleteRequested() const { return m_npcDeleteRequested; } - - bool joinedClass(const std::string& name) - { - auto it = m_classMap.find(name); // std::find(m_classMap.begin(), m_classMap.end(), name); - return (it != m_classMap.end()); - } - - ScriptClass* joinClass(const std::string& className); - void setTimeout(int val); - void updatePropModTime(unsigned char propId); - - // - bool hasScriptEvent(int flag) const; - void setScriptEvents(int mask); - - ScriptExecutionContext& getExecutionContext(); - IScriptObject* getScriptObject() const; - void setScriptObject(std::unique_ptr> object); - - // -- flags - CString getFlag(const std::string& pFlagName) const; - void setFlag(const std::string& pFlagName, const CString& pFlagValue); - void deleteFlag(const std::string& pFlagName); - std::unordered_map& getFlagList() { return m_flagList; } - - bool deleteNPC(); - void reloadNPC(); - void resetNPC(); - - bool isWarpable() const; - void allowNpcWarping(NPCWarpType m_canWarp); - void moveNPC(int dx, int dy, double time, int options); - void warpNPC(std::shared_ptr pLevel, int pX, int pY); - - // file - bool loadNPC(const CString& fileName); - void saveNPC(); - - void queueNpcAction(const std::string& action, Player* player = nullptr, bool registerAction = true); - void queueNpcTrigger(const std::string& action, Player* player = nullptr, const std::string& data = ""); - - template - void queueNpcEvent(const std::string& action, bool registerAction, Args&&... An); - - void registerNpcUpdates(); - void registerTriggerAction(const std::string& action, IScriptFunction* cbFunc); - void scheduleEvent(unsigned int timeout, ScriptAction& action); - - bool runScriptTimer(); - NPCEventResponse runScriptEvents(); - - CString getVariableDump(); - -#endif - -private: - BabyDI_INJECT(Server, m_server); - - NPCType m_npcType; - SourceCode m_npcScript; - - unsigned int m_id = 0; - int16_t m_x = (30 * 16); - int16_t m_y = (30.5 * 16); - int16_t m_z = 0; - unsigned char m_visFlags = 1; - unsigned char m_blockFlags = 0; - float m_hurtX = 32.0f; - float m_hurtY = 32.0f; - unsigned char m_saves[10]; - int m_timeout = 0; - time_t m_modTime[NPCPROP_COUNT]; - - Character m_character; - bool m_isCharacter = false; - - std::string m_image; - int m_width = 32; - int m_height = 32; - CString m_imagePart; - - bool m_blockPositionUpdates = false; - std::weak_ptr m_curlevel; - CString m_weaponName; - - CString m_npcScripter, m_npcScriptType; - std::string m_npcName; - std::string m_clientScriptFormatted; - - CString m_npcBytecode; - -#ifdef V8NPCSERVER - bool hasTimerUpdates() const; - void freeScriptResources(); - void testTouch(); - void testForLinks(); - void updateClientCode(); - - std::map m_classMap; - std::unordered_set m_propModified; - std::vector m_joinedClasses; - - // Defaults - CString m_origImage, m_origLevel; - int16_t m_origX; - int16_t m_origY; - int16_t m_origZ; - - // npc-server - NPCWarpType m_canWarp = NPCWarpType::None; - bool m_npcDeleteRequested = false; - std::unordered_map m_flagList; - - unsigned int m_scriptEventsMask = 0xFF; - std::unique_ptr> m_scriptObject; - ScriptExecutionContext m_scriptExecutionContext; - std::unordered_map m_triggerActions; - std::vector m_scriptTimers; -#endif -}; - -using NPCPtr = std::shared_ptr; -using NPCWeakPtr = std::weak_ptr; - -inline time_t NPC::getPropModTime(unsigned char pId) const -{ - if (pId < NPCPROP_COUNT) return m_modTime[pId]; - return 0; -} - -inline void NPC::setPropModTime(unsigned char pId, time_t time) -{ - if (pId < NPCPROP_COUNT) - m_modTime[pId] = time; -} - -inline unsigned char NPC::getColorId(unsigned int idx) const -{ - if (idx < 5) return m_character.colors[idx]; - return 0; -} - -inline void NPC::setColorId(unsigned int idx, unsigned char val) -{ - if (idx < 5) m_character.colors[idx] = val; -} - -inline unsigned char NPC::getSave(unsigned int idx) const -{ - if (idx < 10) return m_saves[idx]; - return 0; -} - -inline void NPC::setSave(unsigned int idx, unsigned char val) -{ - if (idx < 10) m_saves[idx] = val; -} - -////////// - -inline const CString& NPC::getChat() const -{ - return m_character.chatMessage; -} - -inline void NPC::setChat(const CString& msg) -{ - m_character.chatMessage = msg.subString(0, std::min(msg.length(), 223)); -} - -////////// - -inline const CString& NPC::getGani() const -{ - return m_character.gani; -} - -inline void NPC::setGani(const CString& gani) -{ - m_character.gani = gani.subString(0, std::min(gani.length(), 223)); -} - -////////// - -inline int NPC::getRupees() const -{ - return m_character.gralats; -} - -inline void NPC::setRupees(int val) -{ - m_character.gralats = val; -} - -////////// - -inline int NPC::getDarts() const -{ - return m_character.arrows; -} - -inline void NPC::setDarts(int val) -{ - setProps(CString() >> (char)NPCPROP_ARROWS >> (char)clip(val, 0, 99), CLVER_2_17, true); -} - -///////// - -inline const std::string& NPC::getImage() const -{ - return m_image; -} - -inline void NPC::setImage(const std::string& pImage) -{ - m_image = pImage.substr(0, std::min(pImage.length(), 223)); -} - -inline void NPC::setImage(const std::string& pImage, int offsetx, int offsety, int pwidth, int pheight) -{ - setImage(pImage); - m_imagePart.clear(); - m_imagePart.writeGShort(offsetx); - m_imagePart.writeGShort(offsety); - m_imagePart.writeGChar(pwidth); - m_imagePart.writeGChar(pheight); -} - -////////// - -inline const std::string& NPC::getNickname() const -{ - return m_character.nickName; -} - -inline void NPC::setNickname(const std::string& pNick) -{ - m_character.nickName = pNick.substr(0, std::min(pNick.length(), 223)); -} - -////////// - -inline const CString& NPC::getBodyImage() const -{ - return m_character.bodyImage; -} - -inline void NPC::setBodyImage(const std::string& pBodyImage) -{ - m_character.bodyImage = pBodyImage.substr(0, 200); -} - -////////// - -inline const CString& NPC::getHeadImage() const -{ - return m_character.headImage; -} - -inline void NPC::setHeadImage(const std::string& pHeadImage) -{ - m_character.headImage = pHeadImage.substr(0, 123); -} - -////////// - -inline const CString& NPC::getHorseImage() const -{ - return m_character.horseImage; -} - -inline void NPC::setHorseImage(const std::string& pHorseImage) -{ - m_character.horseImage = pHorseImage.substr(0, 200); -} - -////////// - -inline const CString& NPC::getShieldImage() const -{ - return m_character.shieldImage; -} - -inline void NPC::setShieldImage(const std::string& pShieldImage) -{ - m_character.shieldImage = pShieldImage.substr(0, 200); -} - -////////// - -inline const CString& NPC::getSwordImage() const -{ - return m_character.swordImage; -} - -inline void NPC::setSwordImage(const std::string& pSwordImage) -{ - m_character.swordImage = pSwordImage.substr(0, 120); -} - -#ifdef V8NPCSERVER - -inline void NPC::updatePropModTime(unsigned char propId) -{ - if (propId < NPCPROP_COUNT) - { - m_propModified.insert(propId); - registerNpcUpdates(); - } -} - -inline bool NPC::isWarpable() const -{ - return m_canWarp != NPCWarpType::None; -} - -inline void NPC::allowNpcWarping(NPCWarpType m_canWarp) -{ - if (m_npcType != NPCType::LEVELNPC) - this->m_canWarp = m_canWarp; -} - -/** - * Script Engine - */ -inline bool NPC::hasTimerUpdates() const -{ - return (m_timeout > 0 || !m_scriptTimers.empty()); -} - -inline bool NPC::hasScriptEvent(int flag) const -{ - return ((m_scriptEventsMask & flag) == flag); -} - -inline void NPC::setScriptEvents(int mask) -{ - m_scriptEventsMask = mask; -} - -inline ScriptExecutionContext& NPC::getExecutionContext() -{ - return m_scriptExecutionContext; -} - -inline IScriptObject* NPC::getScriptObject() const -{ - return m_scriptObject.get(); -} - -inline void NPC::setScriptObject(std::unique_ptr> object) -{ - m_scriptObject = std::move(object); -} - -inline CString NPC::getFlag(const std::string& pFlagName) const -{ - auto it = m_flagList.find(pFlagName); - if (it != m_flagList.end()) - return it->second; - return ""; -} - -inline void NPC::setFlag(const std::string& pFlagName, const CString& pFlagValue) -{ - m_flagList[pFlagName] = pFlagValue; -} - -inline void NPC::deleteFlag(const std::string& pFlagName) -{ - m_flagList.erase(pFlagName); -} - - // TODO(joey): hm - #include "Server.h" - -template -inline void NPC::queueNpcEvent(const std::string& action, bool registerAction, Args&&... An) -{ - ScriptEngine* scriptEngine = m_server->getScriptEngine(); - ScriptAction scriptAction = scriptEngine->createAction(action, getScriptObject(), std::forward(An)...); - - m_scriptExecutionContext.addAction(scriptAction); - if (registerAction) - scriptEngine->registerNpcUpdate(this); -} - -inline void NPC::registerNpcUpdates() -{ - ScriptEngine* scriptEngine = m_server->getScriptEngine(); - scriptEngine->registerNpcUpdate(this); -} - -inline void NPC::scheduleEvent(unsigned int timeout, ScriptAction& action) -{ - m_scriptTimers.push_back({ timeout, std::move(action) }); -} - -#endif - -#endif diff --git a/server/include/Player.h b/server/include/Player.h deleted file mode 100644 index da57cc1d2..000000000 --- a/server/include/Player.h +++ /dev/null @@ -1,460 +0,0 @@ -#ifndef TPLAYER_H -#define TPLAYER_H - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include "BabyDI.h" - -#include "Account.h" -#include "Server.h" -#include "utilities/IdGenerator.h" - -#ifdef V8NPCSERVER - #include "scripting/interface/ScriptBindings.h" -#endif - -class Level; -class Map; -class Weapon; - -constexpr uint16_t EXTERNALPLAYERID_INIT = 16000; - -enum class LevelItemType; - -enum -{ - PLSETPROPS_SETBYPLAYER = 0x01, // if set, do serverside checks to prevent attributes from being changed - PLSETPROPS_FORWARD = 0x02, // forward data to other players - PLSETPROPS_FORWARDSELF = 0x04, // forward data back to the player -}; - -struct ShootPacketNew -{ - // shoot(float x, float y, float z, float angle, float zangle, float strength, str ani, str aniparams) - int16_t pixelx; - int16_t pixely; - int16_t pixelz; - int8_t offsetx; - int8_t offsety; - int8_t sangle; - int8_t sanglez; - int8_t speed; - int8_t gravity; - CString gani; - CString ganiArgs; - CString shootParams; - - CString constructShootV1() const; - CString constructShootV2() const; - - void debug(); -}; - -struct CachedLevel -{ - CachedLevel(std::weak_ptr pLevel, time_t pModTime) : level(pLevel), modTime(pModTime) {} - std::weak_ptr level; - time_t modTime; -}; - -class Player : public Account, public CSocketStub, public std::enable_shared_from_this -{ -public: - // Required by CSocketStub. - bool onRecv(); - bool onSend(); - bool onRegister() { return true; } - void onUnregister(); - SOCKET getSocketHandle() { return m_playerSock->getHandle(); } - bool canRecv(); - bool canSend(); - - // Constructor - Deconstructor - Player(CSocket* pSocket, uint16_t pId); - ~Player(); - void cleanup(); - - // Manage Account - bool isLoggedIn() const; - bool sendLogin(); - - // Get Properties - CSocket* getSocket() { return m_playerSock; } - std::shared_ptr getLevel() const; - std::weak_ptr getMap() { return m_pmap; } - CString getGroup() { return m_levelGroup; } - uint16_t getId() const; - time_t getLastData() const { return m_lastData; } - CString getGuild() const { return m_guild; } - int getVersion() const { return m_versionId; } - CString getVersionStr() const { return m_version; } - bool isUsingFileBrowser() const { return m_isFtp; } - CString getServerName() const { return m_serverName; } - const CString& getPlatform() const { return m_os; } - std::pair getMapPosition() const; - - // Set Properties - void setChat(const CString& pChat); - void setNick(CString pNickName, bool force = false); - void setId(uint16_t pId); - void setLoaded(bool loaded) { this->m_loaded = loaded; } - void setGroup(CString group) { m_levelGroup = group; } - void deleteFlag(const std::string& pFlagName, bool sendToPlayer = false); - void setFlag(const std::string& pFlagName, const CString& pFlagValue, bool sendToPlayer = false); - void setMap(std::shared_ptr map) { m_pmap = map; } - void setServerName(CString& tmpServerName) { m_serverName = tmpServerName; } - - // Level manipulation - bool warp(const CString& pLevelName, float pX, float pY, time_t modTime = 0); - bool setLevel(const CString& pLevelName, time_t modTime = 0); - bool sendLevel(std::shared_ptr pLevel, time_t modTime, bool fromAdjacent = false); - bool sendLevel141(std::shared_ptr pLevel, time_t modTime, bool fromAdjacent = false); - bool leaveLevel(bool resetCache = false); - time_t getCachedLevelModTime(const Level* level) const; - void resetLevelCache(const Level* level); - - // Prop-Manipulation - inline CString getProp(int pPropId) const; - void getProp(CString& buffer, int pPropId) const; - - CString getProps(const bool* pProps, int pCount); - CString getPropsRC(); - void setProps(CString& pPacket, uint8_t options, Player* rc = 0); - void sendProps(const bool* pProps, int pCount); - void setPropsRC(CString& pPacket, Player* rc); - - // Socket-Functions - bool doMain(); - void sendPacket(CString pPacket, bool appendNL = true); - bool sendFile(const CString& pFile); - bool sendFile(const CString& pPath, const CString& pFile); - - // Type of player - bool isAdminIp(); - bool isStaff(); - bool isNC() const { return (m_type & PLTYPE_ANYNC) != 0; } - bool isRC() const { return (m_type & PLTYPE_ANYRC) != 0; } - bool isClient() const { return (m_type & PLTYPE_ANYCLIENT) != 0; } - bool isNPCServer() const { return (m_type & PLTYPE_NPCSERVER) != 0; } - bool isControlClient() const { return (m_type & PLTYPE_ANYCONTROL) != 0; } - bool isHiddenClient() const { return (m_type & PLTYPE_NONITERABLE) != 0; } - bool isLoaded() const { return m_loaded; } - int getType() const { return m_type; } - void setType(int val) { m_type = val; } - - // Misc functions. - bool doTimedEvents(); - void disconnect(); - bool processChat(CString pChat); - bool addWeapon(LevelItemType defaultWeapon); - bool addWeapon(const std::string& name); - bool addWeapon(std::shared_ptr weapon); - bool deleteWeapon(LevelItemType defaultWeapon); - bool deleteWeapon(const std::string& name); - bool deleteWeapon(std::shared_ptr weapon); - void disableWeapons(); - void enableWeapons(); - void freezePlayer(); - void unfreezePlayer(); - void sendRPGMessage(const CString& message); - void sendSignMessage(const CString& message); - void setAni(CString gani); - - bool hasSeenFile(const std::string& file) const; - bool addPMServer(CString& option); - bool remPMServer(CString& option); - bool inChatChannel(const std::string& channel) const; - bool addChatChannel(const std::string& channel); - bool removeChatChannel(const std::string& channel); - bool updatePMPlayers(CString& servername, CString& players); - bool pmExternalPlayer(CString servername, CString account, CString& pmMessage); - std::vector getPMServerList(); - std::shared_ptr getExternalPlayer(const uint16_t id, bool includeRC = true) const; - std::shared_ptr getExternalPlayer(const CString& account, bool includeRC = true) const; - -#ifdef V8NPCSERVER - bool isProcessed() const { return m_processRemoval; } - void setProcessed() { m_processRemoval = true; } - - // NPC-Server Functionality - void sendNCAddr(); - - inline IScriptObject* getScriptObject() const - { - return m_scriptObject.get(); - } - - inline void setScriptObject(std::unique_ptr> object) - { - m_scriptObject = std::move(object); - } -#endif - - // Packet-Functions - static bool created; - static void createFunctions(); - - bool msgPLI_NULL(CString& pPacket); - bool msgPLI_LOGIN(CString& pPacket); - - bool msgPLI_LEVELWARP(CString& pPacket); - bool msgPLI_BOARDMODIFY(CString& pPacket); - bool msgPLI_REQUESTUPDATEBOARD(CString& pPacket); - bool msgPLI_PLAYERPROPS(CString& pPacket); - bool msgPLI_NPCPROPS(CString& pPacket); - bool msgPLI_BOMBADD(CString& pPacket); - bool msgPLI_BOMBDEL(CString& pPacket); - bool msgPLI_TOALL(CString& pPacket); - bool msgPLI_HORSEADD(CString& pPacket); - bool msgPLI_HORSEDEL(CString& pPacket); - bool msgPLI_ARROWADD(CString& pPacket); - bool msgPLI_FIRESPY(CString& pPacket); - bool msgPLI_THROWCARRIED(CString& pPacket); - bool msgPLI_ITEMADD(CString& pPacket); - bool msgPLI_ITEMDEL(CString& pPacket); - bool msgPLI_CLAIMPKER(CString& pPacket); - bool msgPLI_BADDYPROPS(CString& pPacket); - bool msgPLI_BADDYHURT(CString& pPacket); - bool msgPLI_BADDYADD(CString& pPacket); - bool msgPLI_FLAGSET(CString& pPacket); - bool msgPLI_FLAGDEL(CString& pPacket); - bool msgPLI_OPENCHEST(CString& pPacket); - bool msgPLI_PUTNPC(CString& pPacket); - bool msgPLI_NPCDEL(CString& pPacket); - bool msgPLI_WANTFILE(CString& pPacket); - bool msgPLI_SHOWIMG(CString& pPacket); - // PLI_UNKNOWN25 - bool msgPLI_HURTPLAYER(CString& pPacket); - bool msgPLI_EXPLOSION(CString& pPacket); - bool msgPLI_PRIVATEMESSAGE(CString& pPacket); - bool msgPLI_NPCWEAPONDEL(CString& pPacket); - bool msgPLI_PACKETCOUNT(CString& pPacket); - bool msgPLI_WEAPONADD(CString& pPacket); - bool msgPLI_UPDATEFILE(CString& pPacket); - bool msgPLI_ADJACENTLEVEL(CString& pPacket); - bool msgPLI_HITOBJECTS(CString& pPacket); - bool msgPLI_LANGUAGE(CString& pPacket); - bool msgPLI_TRIGGERACTION(CString& pPacket); - bool msgPLI_MAPINFO(CString& pPacket); - bool msgPLI_SHOOT(CString& pPacket); - bool msgPLI_SHOOT2(CString& pPacket); - bool msgPLI_SERVERWARP(CString& pPacket); - bool msgPLI_PROCESSLIST(CString& pPacket); - bool msgPLI_UNKNOWN46(CString& pPacket); - bool msgPLI_VERIFYWANTSEND(CString& pPacket); - bool msgPLI_UPDATECLASS(CString& pPacket); - bool msgPLI_RAWDATA(CString& pPacket); - - bool msgPLI_RC_SERVEROPTIONSGET(CString& pPacket); - bool msgPLI_RC_SERVEROPTIONSSET(CString& pPacket); - bool msgPLI_RC_FOLDERCONFIGGET(CString& pPacket); - bool msgPLI_RC_FOLDERCONFIGSET(CString& pPacket); - bool msgPLI_RC_RESPAWNSET(CString& pPacket); - bool msgPLI_RC_HORSELIFESET(CString& pPacket); - bool msgPLI_RC_APINCREMENTSET(CString& pPacket); - bool msgPLI_RC_BADDYRESPAWNSET(CString& pPacket); - bool msgPLI_RC_PLAYERPROPSGET(CString& pPacket); - bool msgPLI_RC_PLAYERPROPSSET(CString& pPacket); - bool msgPLI_RC_DISCONNECTPLAYER(CString& pPacket); - bool msgPLI_RC_UPDATELEVELS(CString& pPacket); - bool msgPLI_RC_ADMINMESSAGE(CString& pPacket); - bool msgPLI_RC_PRIVADMINMESSAGE(CString& pPacket); - bool msgPLI_RC_LISTRCS(CString& pPacket); - bool msgPLI_RC_DISCONNECTRC(CString& pPacket); - bool msgPLI_RC_APPLYREASON(CString& pPacket); - bool msgPLI_RC_SERVERFLAGSGET(CString& pPacket); - bool msgPLI_RC_SERVERFLAGSSET(CString& pPacket); - bool msgPLI_RC_ACCOUNTADD(CString& pPacket); - bool msgPLI_RC_ACCOUNTDEL(CString& pPacket); - bool msgPLI_RC_ACCOUNTLISTGET(CString& pPacket); - bool msgPLI_RC_PLAYERPROPSGET2(CString& pPacket); - bool msgPLI_RC_PLAYERPROPSGET3(CString& pPacket); - bool msgPLI_RC_PLAYERPROPSRESET(CString& pPacket); - bool msgPLI_RC_PLAYERPROPSSET2(CString& pPacket); - bool msgPLI_RC_ACCOUNTGET(CString& pPacket); - bool msgPLI_RC_ACCOUNTSET(CString& pPacket); - bool msgPLI_RC_CHAT(CString& pPacket); - bool msgPLI_PROFILEGET(CString& pPacket); - bool msgPLI_PROFILESET(CString& pPacket); - bool msgPLI_RC_WARPPLAYER(CString& pPacket); - bool msgPLI_RC_PLAYERRIGHTSGET(CString& pPacket); - bool msgPLI_RC_PLAYERRIGHTSSET(CString& pPacket); - bool msgPLI_RC_PLAYERCOMMENTSGET(CString& pPacket); - bool msgPLI_RC_PLAYERCOMMENTSSET(CString& pPacket); - bool msgPLI_RC_PLAYERBANGET(CString& pPacket); - bool msgPLI_RC_PLAYERBANSET(CString& pPacket); - bool msgPLI_RC_FILEBROWSER_START(CString& pPacket); - bool msgPLI_RC_FILEBROWSER_CD(CString& pPacket); - bool msgPLI_RC_FILEBROWSER_END(CString& pPacket); - bool msgPLI_RC_FILEBROWSER_DOWN(CString& pPacket); - bool msgPLI_RC_FILEBROWSER_UP(CString& pPacket); - bool msgPLI_NPCSERVERQUERY(CString& pPacket); - bool msgPLI_RC_FILEBROWSER_MOVE(CString& pPacket); - bool msgPLI_RC_FILEBROWSER_DELETE(CString& pPacket); - bool msgPLI_RC_FILEBROWSER_RENAME(CString& pPacket); - bool msgPLI_RC_LARGEFILESTART(CString& pPacket); - bool msgPLI_RC_LARGEFILEEND(CString& pPacket); - bool msgPLI_RC_FOLDERDELETE(CString& pPacket); - -#ifdef V8NPCSERVER - bool msgPLI_NC_NPCGET(CString& pPacket); - bool msgPLI_NC_NPCDELETE(CString& pPacket); - bool msgPLI_NC_NPCRESET(CString& pPacket); - bool msgPLI_NC_NPCSCRIPTGET(CString& pPacket); - bool msgPLI_NC_NPCWARP(CString& pPacket); - bool msgPLI_NC_NPCFLAGSGET(CString& pPacket); - bool msgPLI_NC_NPCSCRIPTSET(CString& pPacket); - bool msgPLI_NC_NPCFLAGSSET(CString& pPacket); - bool msgPLI_NC_NPCADD(CString& pPacket); - bool msgPLI_NC_CLASSEDIT(CString& pPacket); - bool msgPLI_NC_CLASSADD(CString& pPacket); - bool msgPLI_NC_LOCALNPCSGET(CString& pPacket); - bool msgPLI_NC_WEAPONLISTGET(CString& pPacket); - bool msgPLI_NC_WEAPONGET(CString& pPacket); - bool msgPLI_NC_WEAPONADD(CString& pPacket); - bool msgPLI_NC_WEAPONDELETE(CString& pPacket); - bool msgPLI_NC_CLASSDELETE(CString& pPacket); - bool msgPLI_NC_LEVELLISTGET(CString& pPacket); -#endif - - bool msgPLI_REQUESTTEXT(CString& pPacket); - bool msgPLI_SENDTEXT(CString& pPacket); - - bool msgPLI_UPDATEGANI(CString& pPacket); - bool msgPLI_UPDATESCRIPT(CString& pPacket); - bool msgPLI_UPDATEPACKAGEREQUESTFILE(CString& pPacket); - bool msgPLI_RC_UNKNOWN162(CString& pPacket); - -private: - // Login functions. - bool sendLoginClient(); - bool sendLoginNC(); - bool sendLoginRC(); - - // Packet functions. - bool parsePacket(CString& pPacket); - void decryptPacket(CString& pPacket); - - // Collision detection stuff. - bool testSign(); - void testTouch(); - - // Misc. - void dropItemsOnDeath(); - bool spawnLevelItem(CString& pPacket, bool playerDrop = true); - bool removeItem(LevelItemType itemType); - - // Socket Variables - CSocket* m_playerSock; - CString m_recvBuffer; - - // Encryption - unsigned char m_encryptionKey = 0; - CEncryption m_encryptionCodecIn; - - // Variables - CString m_version; - CString m_os{ "wind" }; - CString m_serverName; - uint16_t m_id = 0; - int m_envCodePage = 1252; - int m_type = PLTYPE_AWAIT; - int m_versionId = CLVER_UNKNOWN; - time_t m_lastData, m_lastMovement, m_lastChat, m_lastNick, m_lastMessage, m_lastSave, m_last1m; - std::vector> m_cachedLevels; - std::map m_rcLargeFiles; - std::map> m_singleplayerLevels; - std::set m_channelList; - std::unordered_set m_knownFiles; - std::weak_ptr m_pmap; - std::weak_ptr m_currentLevel; - - std::unordered_map> m_externalPlayers; - IdGenerator m_externalPlayerIdGenerator{ EXTERNALPLAYERID_INIT }; - - unsigned int m_carryNpcId = 0; - bool m_carryNpcThrown = false; - bool m_loaded = false; - bool m_nextIsRaw = false; - int m_rawPacketSize = 0; - bool m_isFtp = false; - bool m_grMovementUpdated = false; - bool m_firstLevel = true; - CString m_grMovementPackets; - CString m_npcserverPort; - int m_packetCount = 0; - int m_invalidPackets = 0; - CString m_guild; - CString m_levelGroup; - - CString m_grExecParameterList; - - // File queue. - CFileQueue m_fileQueue; - -#ifdef V8NPCSERVER - bool m_processRemoval = false; - std::unique_ptr> m_scriptObject; -#endif - - int getVersionIDByVersion(const CString& versionInput) const; -}; - -using PlayerPtr = std::shared_ptr; -using PlayerWeakPtr = std::weak_ptr; - -inline bool Player::isLoggedIn() const -{ - return (m_type != PLTYPE_AWAIT && m_id > 0); -} - -inline uint16_t Player::getId() const -{ - return m_id; -} - -inline void Player::setId(uint16_t pId) -{ - m_id = pId; -} - -inline bool Player::hasSeenFile(const std::string& file) const -{ - return m_knownFiles.find(file) != m_knownFiles.end(); -} - -inline bool Player::inChatChannel(const std::string& channel) const -{ - return m_channelList.find(channel) != m_channelList.end(); -} - -inline bool Player::addChatChannel(const std::string& channel) -{ - auto res = m_channelList.insert(channel); - return res.second; -} - -inline bool Player::removeChatChannel(const std::string& channel) -{ - m_channelList.erase(channel); - return false; -} - -inline CString Player::getProp(int pPropId) const -{ - CString packet; - getProp(packet, pPropId); - return packet; -} - -#endif // TPLAYER_H diff --git a/server/include/Server.h b/server/include/Server.h index 19c850eb6..17d5bff3a 100644 --- a/server/include/Server.h +++ b/server/include/Server.h @@ -1,54 +1,68 @@ -#ifndef TSERVER_H -#define TSERVER_H +#ifndef SERVER_H +#define SERVER_H +#include #include #include +#include #include #include -#include +#include +#include +#include #include +#include +#include #include +#include #include #include #include -#include +#include #include -#include -#include #include + #include -#include #include -#include "FileSystem.h" -#include "ServerList.h" -#include "misc/WordFilter.h" -#include "utilities/CommandDispatcher.h" -#include "utilities/IdGenerator.h" - -#ifdef UPNP - #include "misc/UPNP.h" -#endif - -#ifdef V8NPCSERVER - #include "scripting/ScriptEngine.h" -#endif - -#include "scripting/GS2ScriptManager.h" -#include "scripting/ScriptClass.h" - -// Resources -#include "animation/GameAni.h" -#include "utilities/ResourceManager.h" -#include "UpdatePackage.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// -class Player; -class Level; -class NPC; +class PlayerClient; class ScriptClass; class Map; class Weapon; +class NPCServer; enum // Socket Type { @@ -66,22 +80,101 @@ enum FS_SWORD = 5, FS_SHIELD = 6, }; -#define FS_COUNT 7 +constexpr int FS_COUNT = 7; + +enum class ServerGeneration +{ + // 1.x + ORIGINAL = 0, + + // 2.x/3.x + CLASSIC, + + // 4.x to 5.007 + NEWMAIN, + + // 5.1 and up + MODERN +}; + +inline constexpr std::array ServerGenerationNames{ + "original", + "classic", + "newmain", + "modern" +}; -// Player ids 0 and 1 break things. NPC id 0 breaks things. -// Don't allow anything to have one of those ids. -// Player ids 16000 and up is used for players on other servers and "IRC"-channels. -// The players from other servers should be unique lists for each player as they are fetched depending on -// what the player chooses to see (buddies, "global guilds" tab, "other servers" tab) -constexpr uint16_t PLAYERID_INIT = 2; -constexpr uint32_t NPCID_INIT = 10001; +/// @brief Cached settings directly queried from the server by other classes. +struct ExternalServerCachedSettings +{ + SettingCache maxPlayers{"maxplayers", 128}; + SettingCache sleepWhenNoPlayers{"sleepwhennoplayers", true}; + SettingCache unstickMeLevel{"unstickmelevel", "onlinestartlocal.nw"}; + std::array, 2> unstickMeTile{{{"unstickmex", 30.0f}, {"unstickmey", 30.5f}}}; + SettingCache unstickMeSeconds{"unstickmetime", 30}; + SettingCache enableBushItemDrops{"bushitems", true}; + SettingCache enableVaseItemDrops{"vasesdrop", true}; + SettingCache disableItemDropping{"disableitemdropping", false}; + SettingCache enableInsideSyncDistance{"syncbydistanceinside", false}; + std::array, 2> syncDistance{{{"syncdistancex", 192}, {"syncdistancey", 192}}}; + SettingCache eventDistance{"eventdistance", 64}; + SettingCache triggerDistance{"triggerdistance", 10}; + SettingCache sendTriggerActionsToPlayers{"sendplayertriggers", true}; + SettingCache enableFlagCropping{"cropflags", true}; + SettingCache disableExplosions{"noexplosions", false}; + SettingCache enableClientsidePushPull{"clientsidepushpull", true}; + SettingCache tileRespawnTime{"respawntime", 15}; + SettingCache enableIdleDisconnect{"disconnectifnotmoved", true}; + SettingCache idleTimeoutSeconds{"maxnomovement", 1200}; + SettingCache enablePermanentTileChanges{"savelevels", false}; + SettingCache saveTileChangesToLevelFile{"levelsautosave", false}; + // flag/triggerhacks + SettingCache enableFlaghackMovement{"flaghack_movement", true}; + SettingCache enableTriggerhackExecscript{"triggerhack_execscript", false}; + SettingCache enableTriggerhackFiles{"triggerhack_files", false}; + SettingCache enableTriggerhackGroups{"triggerhack_groups", true}; + SettingCache enableTriggerhackGuilds{"triggerhack_guilds", false}; + SettingCache enableTriggerhackLevels{"triggerhack_levels", false}; + SettingCache enableTriggerhackProps{"triggerhack_props", false}; + SettingCache enableTriggerhackRC{"triggerhack_rc", false}; + SettingCache enableTriggerhackWeapons{"triggerhack_weapons", false}; + // npc-server + SettingCache forceClientsideLinks{"clientsidelinks", false}; + SettingCache forceClientsideSigns{"clientsidesigns", false}; + SettingCache enableItemDropEvents{"itemdropevents", false}; + SettingCache itemDropEventsOnlyForGralats{"itemdropeventsonlyforgralats", false}; + SettingCache projectilesStopOnWall{"projectilesstoponwall", true}; + SettingCache runAllScriptEvents{"runallscriptevents", false}; + // security + SettingCache normalAdminsCanChangeGralats{"normaladminscanchangegralats", true}; + SettingCache> protectedWeapons{"protectedweapons", {}}; + SettingCache> jailLevels{"jaillevels", {"police2.graal", "police4.graal"}}; + // player + SettingCache enableDefaultWeapons{"defaultweapons", true}; + SettingCache maxHeartLimit{"heartlimit", 3}; + SettingCache swordPowerLimit{"swordlimit", 3}; + SettingCache shieldPowerLimit{"shieldlimit", 3}; + SettingCache enableHealingSwords{"healswords", false}; + SettingCache enableExBodyColors{"enableexbodycolors", false}; + SettingCache playerTouchesMeNoZ{"playertouchsmenoz", false}; + SettingCache lockPlayerZ{"lockplayerz", false}; + SettingCache enableAPSystem{"apsystem", true}; + std::array, 5> apSystemThresholdSeconds{{{"aptime0", 30}, {"aptime1", 90}, {"aptime2", 300}, {"aptime3", 600}, {"aptime4", 1200}}}; + SettingCache> playerProfileVariables{"profilevars", {"Kills:=playerkills", "Deaths:=playerdeaths", "Maxpower:=playerfullhearts", "Rating:=playerrating", "Alignment:=playerap", "Gralat:=playerrupees", "Swordpower:=playerswordpower", "Spin Attack:=canspin"}}; + SettingCache> playerStatusList{"playerlisticons", {"Online", "Away", "DND", "Eating", "Hiding", "No PMs", "RPing", "Sparring", "PKing"}}; + + void bind(Server* server); +}; using AnimationManager = ResourceManager; using PackageManager = ResourceManager; -using TriggerDispatcher = CommandDispatcher&>; +using TriggerDispatcher = CommandDispatcher&>; class Server : public CSocketStub { + friend class NPCServer; + friend class FlatFileNPCLoader; + public: // Required by CSocketStub. bool onRecv(); @@ -93,296 +186,364 @@ class Server : public CSocketStub bool canSend() { return false; } Server(const CString& pName); - ~Server(); + virtual ~Server(); void operator()(); void cleanup(); void restart(); - bool running; + bool running = false; - int init(const CString& serverip = "", const CString& serverport = "", const CString& localip = "", const CString& serverinterface = ""); + int init(std::string_view serverip, std::string_view serverport, std::string_view localip, std::string_view serverinterface); bool doMain(); - // Server Management + // Server Configuration int loadConfigFiles(); + void prepareSettings(); void loadSettings(); void loadAdminSettings(); void loadAllowedVersions(); - void loadFileSystem(); - void loadServerFlags(); + void loadServerFileSystem(); + void loadWorldFileSystem(); void loadServerMessage(); void loadIPBans(); - void loadClasses(bool print = false); - void loadWeapons(bool print = false); + void loadTranslations() const; + void loadWordFilter(); + void loadServerFlags(); + void loadGuilds(); void loadMaps(bool print = false); + int loadServerObjects(); + void loadWeapons(bool print = false); void loadMapLevels(); -#ifdef V8NPCSERVER - void loadNpcs(bool print = false); -#endif - void loadTranslations(); - void loadWordFilter(); + // Folder Configuration void loadAllFolders(); void loadFolderConfig(); + // NPC-Server + void loadNPCServer(); + +public: + // Save Functions void saveServerFlags(); void saveWeapons(); -#ifdef V8NPCSERVER - void saveNpcs(); - std::vector> calculateNpcStats(); -#endif - - void reportScriptException(const ScriptRunError& error); - void reportScriptException(const std::string& error_message); - - // Get functions. - const CString& getName() const { return m_name; } - FileSystem* getFileSystem(int c = 0) { return &(m_filesystem[c]); } - FileSystem* getAccountsFileSystem() { return &m_filesystemAccounts; } - CLog& getNPCLog() { return m_npcLog; } - CLog& getServerLog() { return m_serverLog; } - CLog& getRCLog() { return m_rcLog; } - CLog& getScriptLog() { return m_scriptLog; } - CSettings& getSettings() { return m_settings; } - CSettings& getAdminSettings() { return m_adminSettings; } - CSocketManager& getSocketManager() { return m_sockManager; } - CString getServerPath() const { return m_serverPath; } - CString getServerPath(const std::string& path) const; - const CString& getServerMessage() const { return m_serverMessage; } - const CString& getAllowedVersionString() const { return m_allowedVersionString; } - CTranslationManager& getTranslationManager() { return m_translationManager; } - WordFilter& getWordFilter() { return m_wordFilter; } - ServerList& getServerList() { return m_serverlist; } - AnimationManager& getAnimationManager() { return m_animationManager; } - PackageManager& getPackageManager() { return m_packageManager; } - unsigned int getNWTime() const { return m_serverTime; } - void calculateServerTime(); - - std::unordered_map>& getClassList() { return m_classList; } - std::unordered_map>& getNPCNameList() { return m_npcNameList; } - std::unordered_map& getServerFlags() { return m_serverFlags; } - std::unordered_map>& getWeaponList() { return m_weaponList; } - std::unordered_map>& getPlayerList() { return m_playerList; } - std::unordered_map>& getNPCList() { return m_npcList; } - std::vector>& getLevelList() { return m_levelList; } - const std::vector>& getMapList() const { return m_mapList; } - const std::vector& getStatusList() const { return m_statusList; } - const std::vector& getAllowedVersions() const { return m_allowedVersions; } - std::unordered_multimap>& getGroupLevels() { return m_groupLevels; } - -#ifdef V8NPCSERVER - ScriptEngine* getScriptEngine() { return &m_scriptEngine; } - int getNCPort() const { return m_ncPort; } - std::shared_ptr getNPCServer() const { return m_npcServer; } -#endif - - FileSystem* getFileSystemByType(CString& type); - CString getFlag(const std::string& pFlagName); - std::shared_ptr getLevel(const std::string& pLevel); - std::shared_ptr getNPC(const uint32_t id) const; - std::shared_ptr getPlayer(const uint16_t id) const; - std::shared_ptr getPlayer(const uint16_t id, int type) const; // = PLTYPE_ANYCLIENT) const; - std::shared_ptr getPlayer(const CString& account, int type) const; - -#ifdef V8NPCSERVER - void assignNPCName(std::shared_ptr npc, const std::string& name); - void removeNPCName(std::shared_ptr npc); - std::shared_ptr getNPCByName(const std::string& name) const; - std::shared_ptr addServerNpc(int npcId, float pX, float pY, std::shared_ptr pLevel, bool sendToPlayers = false); - - void handlePM(Player* player, const CString& message); - void setPMFunction(uint32_t npcId, IScriptFunction* function = nullptr); -#endif - std::shared_ptr addNPC(const CString& pImage, const CString& pScript, float pX, float pY, std::weak_ptr pLevel, bool pLevelNPC, bool sendToPlayers = false); + //void reportScriptException(const std::string& error_message); + +public: + const auto& getName() const { return m_name; } + const auto& getServerMessage() const { return m_serverMessage; } + const auto& getAllowedVersionString() const { return m_allowedVersionString; } + const auto& getNWTime() const { return m_serverTime; } + auto& getFileSystem() { return m_fsWorld; } + auto& getFileSystemServer() { return m_fsServer; } + auto& getAccountLoader() { return *m_accountLoader; } + auto& getAdminSettings() { return m_adminSettings; } + auto& getAnimationManager() { return m_animationManager; } + auto& getLevelList() { return m_levelList; } + auto& getGmapLevelList() { return m_gmapLevels; } + auto& getNPCList() { return m_npcList; } + auto& getNPCLoader() { return *m_npcLoader; } + auto& getPackageManager() { return m_packageManager; } + auto& getNPCIdGenerator() { return m_npcIdGenerator; } + auto& getPlayerIdGenerator() { return m_playerIdGenerator; } + auto& getPlayerList() { return m_playerList; } + auto& getServerList() { return m_serverlist; } + auto& getSettings() { return m_settings; } + auto& getSocketManager() { return m_sockManager; } + auto& getTriggerDispatcher() { return m_triggerActionDispatcher; } + auto& getWeaponList() { return m_weaponList; } + auto& getWordFilter() { return m_wordFilter; } + const auto& getAllowedVersions() const { return m_allowedVersions; } + const auto& getFrameStartTime() const { return m_frameStartTime; } + const auto& getFrameStartTimeHighPrecision() const { return m_frameStartTimeHighPrecision; } + const auto& getMapList() const { return m_mapList; } + const auto& getServerStartTime() const { return m_serverStartTime; } + +public: + /// @brief Gets a stubbed level with the given name (a stubbed level is not yet loaded). + /// @param levelName The name of the level. + /// @return A shared pointer to a Level. + std::shared_ptr getStubbedLevel(std::string_view levelName, std::string_view groupName = ""sv); + + /// @brief Gets a fully loaded level with the given name using no hinting. + /// @param levelName The name of the level. + /// @return A shared pointer to a Level. + std::shared_ptr getLoadedLevelNoHint(std::string_view levelName); + + /// @brief Gets a fully loaded level with the given name, using a player as a hint. + /// @param levelName The name of the level. + /// @param player The player to get the level for (since the level might be instanced for that player). + /// @return A shared pointer to a Level. + std::shared_ptr getLoadedLevel(std::string_view levelName, std::shared_ptr player); + + /// @brief Gets a fully loaded level with the given name, using the a level as a hint. + /// @param levelName The name of the level. + /// @param hintLevel The level to use as a hint (since it might be a group map). + /// @return A shared pointer to a Level. + std::shared_ptr getLoadedLevel(std::string_view levelName, std::shared_ptr hintLevel); + + /// @brief Gets the cached static data for the given level (tiles, links, signs, npcs, etc), loading it if it hasn't been loaded yet. + /// @param levelName The name of the level. + /// @return A shared pointer to a LevelStaticData. + std::shared_ptr getCachedLevelData(std::string_view levelName); + + /// @brief Finds a loaded map. + /// @param mapName The name of the map. + /// @return A shared pointer to a Map. + std::shared_ptr findMap(std::string_view mapName) const noexcept; + + /// @brief Finds the map that contains the given level. + /// @param levelName The name of the level. + /// @return A shared pointer to a Map. + std::shared_ptr findMapForLevel(std::string_view levelName) const noexcept; + + /// @brief Finds the map that contains the given level. + /// @param mapType Restricts the search to maps of the given type. + /// @param levelName The name of the level. + /// @return A shared pointer to a Map. + std::shared_ptr findMapForLevel(MapType mapType, std::string_view levelName) const noexcept; + + /// @brief Finds the appropriate gmap that contains the given level, given the level's name and taking into account singleplayer or group maps. + /// @param levelName The name of the level. + /// @return A shared pointer to a Level. + std::shared_ptr findGmapForLevel(std::string_view levelName, std::shared_ptr player) noexcept; + + /// @brief Gets the tileset type for the given level. + /// @param levelName The name of the level. + /// @return A TilesetType enum value. + tileset::TilesetType getTilesetTypeForLevel(std::shared_ptr level) const noexcept; + tileset::TilesetType getTilesetTypeForLevel(std::shared_ptr level) const noexcept; + + /// @brief Gets the tile type at the given index for the given tileset. + /// @param tileset The tileset type. + /// @param index The tile index. + /// @return The TileType enum value. + tileset::TileType getTileTypeForTile(tileset::TilesetType tileset, uint16_t tile) const noexcept; + +public: + LevelItemType rollBushItemDrop() const; + [[inline]] const auto& getAllowedDeathDrops() const noexcept; + +public: + std::shared_ptr getNPC(const NPCID id) const; + std::shared_ptr addNPC(std::string_view image, std::string_view script, float x, float y, std::weak_ptr level, NPCStorageType storageType, bool sendToPlayers = false, std::string_view type = {}); + std::shared_ptr addNPC(NPCPtr npc, bool sendToPlayers = false); bool deleteNPC(int id, bool eraseFromLevel = true); bool deleteNPC(std::shared_ptr npc, bool eraseFromLevel = true); - bool deleteClass(const std::string& className); - bool hasClass(const std::string& className) const; - ScriptClass* getClass(const std::string& className) const; - void updateClass(const std::string& className, const std::string& classCode); + +public: + template std::shared_ptr getPlayer(const PlayerID id) const; + template std::shared_ptr getPlayer(const PlayerID id, int type) const; + template std::shared_ptr getPlayer(const CString& account, int type) const; + + bool addPlayer(PlayerPtr player, PlayerID id = USHRT_MAX); + bool deletePlayer(PlayerPtr player); + bool swapPlayer(PlayerPtr old_player, PlayerPtr new_player); + void recordPlayerLoggedIn(PlayerPtr player); + bool warpPlayerToSafePlace(PlayerID playerId) const; + +public: + std::optional getFlag(std::string_view flagName) const; + bool deleteFlag(std::string_view flagName, bool sendToPlayers = true); + bool setFlag(std::string_view flagPair, bool sendToPlayers = true); + bool setFlag(std::string_view flagName, std::optional flagValue, bool sendToPlayers = true); + +public: + void calculateNWTime(); bool isIpBanned(const CString& ip); bool isStaff(const CString& accountName); - void logToFile(const std::string& fileName, const std::string& message); + [[inline]] bool isNewWorldMode() const noexcept; - bool deleteFlag(const std::string& pFlagName, bool pSendToPlayers = true); - bool setFlag(CString pFlag, bool pSendToPlayers = true); - bool setFlag(const std::string& pFlagName, const CString& pFlagValue, bool pSendToPlayers = true); +public: + void hitObjectsAtPoint(const TilePosition& pos, int8_t power, std::weak_ptr level, PlayerPtr source) const; + void hitObjectsAtPoint(const TilePosition& pos, int8_t power, std::weak_ptr level, NPCPtr source) const; + void hitPlayer(PlayerID playerId, int8_t power, float fromX, float fromY, std::shared_ptr source) const; - // Admin chat functions +public: + void logToFile(std::filesystem::path fileName, std::string_view message, bool writeTimestamp = true) const; + [[inline]] void logToFile(std::filesystem::path fileName, string::InputRangeNotString auto&& messages) const; + +public: void sendToRC(const CString& pMessage, std::weak_ptr pSender = {}) const; void sendToNC(const CString& pMessage, std::weak_ptr pSender = {}) const; + void sendTriggerAction(PlayerID toPlayerId, NPCID fromNpcId, const LocalPixelPosition& localPosition, std::string_view action, std::string_view params) const; + void sendTriggerAction(LevelPtr toLevel, NPCID fromNpcId, const PixelPosition& position, std::string_view action, std::string_view params) const; - // Packet sending. +public: using PlayerPredicate = std::function; - void sendPacketToAll(const CString& packet, const std::set& exclude = {}) const; - void sendPacketToLevelArea(const CString& packet, std::weak_ptr level, const std::set& exclude = {}, PlayerPredicate sendIf = nullptr) const; - void sendPacketToLevelArea(const CString& packet, std::weak_ptr player, const std::set& exclude = {}, PlayerPredicate sendIf = nullptr) const; - void sendPacketToLevelOnlyGmapArea(const CString& packet, std::weak_ptr level, const std::set& exclude = {}, PlayerPredicate sendIf = nullptr) const; - void sendPacketToLevelOnlyGmapArea(const CString& packet, std::weak_ptr player, const std::set& exclude = {}, PlayerPredicate sendIf = nullptr) const; - void sendPacketToOneLevel(const CString& packet, std::weak_ptr level, const std::set& exclude = {}) const; + void sendPacketToAll(const CString& packet, const std::set& exclude = {}, PlayerPredicate sendIf = nullptr) const; void sendPacketToType(int who, const CString& pPacket, std::weak_ptr pPlayer = {}) const; void sendPacketToType(int who, const CString& pPacket, Player* pPlayer) const; + void sendPacketToOneLevelPart(const CString& packet, const PixelPosition& position, LevelPtr level, const std::set& exclude = {}, PlayerPredicate sendIf = nullptr) const; + void sendPacketToOneLevelPart(const CString& packet, LevelPtr level, const MapPosition& mapPosition, const std::set& exclude = {}, PlayerPredicate sendIf = nullptr) const; + void sendPacketToNearby(const CString& packet, const PixelPosition& position, LevelPtr level, const std::set& exclude = {}, PlayerPredicate sendIf = nullptr) const; + void sendPacketToLevelAndPastVisitorsAfter(StaticLevelData* level, clock::time_point modTime, const CString& packet) const; - // Specific packet sending - void sendShootToOneLevel(const std::weak_ptr& sharedPtr, float x, float y, float z, float angle, float zangle, float strength, const std::string& ani, const std::string& aniArgs) const; - - // Player Management - bool addPlayer(std::shared_ptr player, uint16_t id = USHRT_MAX); - bool deletePlayer(std::shared_ptr player); - void playerLoggedIn(std::shared_ptr player); - bool warpPlayerToSafePlace(uint16_t playerId); - - // Translation Management - bool TS_Load(const CString& pLanguage, const CString& pFileName); - CString TS_Translate(const CString& pLanguage, const CString& pKey); - void TS_Reload(); - void TS_Save(); +public: + void sendShootToOneLevel(LevelShoot* shoot, std::shared_ptr level) const; +public: // Weapon Management - std::shared_ptr getWeapon(const std::string& name); + std::shared_ptr getWeapon(std::string_view name); bool NC_AddWeapon(std::shared_ptr pWeaponObj); - bool NC_DelWeapon(const std::string& pWeaponName); - void updateWeaponForPlayers(std::shared_ptr pWeapon); - void updateClassForPlayers(ScriptClass* pClass); - - /* - * GS2 Functionality - */ - void compileGS2Script(const std::string& source, GS2ScriptManager::user_callback_type cb); - void compileGS2Script(NPC* npc, GS2ScriptManager::user_callback_type cb); - void compileGS2Script(Weapon* weapon, GS2ScriptManager::user_callback_type cb); - void compileGS2Script(ScriptClass* cls, GS2ScriptManager::user_callback_type cb); - - std::time_t getServerStartTime() const - { - return m_serverStartTime; - } + bool NC_DelWeapon(std::string_view pWeaponName); + void updateWeaponForPlayers(Weapon* weapon); + void updateWeaponForPlayers(std::shared_ptr weapon); + void updateClassForPlayers(std::shared_ptr scriptClass); + +public: + bool hasNPCServer() const { return m_playerList.find(NPCServerPlayerID) != m_playerList.end(); } + std::shared_ptr getNPCServer() const { return m_npcServer; } - TriggerDispatcher getTriggerDispatcher() const + void queueNPCEventLocal(LevelPtr level, ScriptEventType type, ScriptObject source, auto&&... args) { - return m_triggerActionDispatcher; + if (!hasNPCServer()) return; + if (level == nullptr) return; + for (auto& npcid : level->getNPCs()) + { + if (auto npc = getNPC(npcid); npc) + npc->scripting.events.addEvent(type, source, std::forward(args)...); + } } - void setShootParams(const std::string& params) + void queueNPCEvent(LevelPtr level, const PixelPosition& position, ScriptEventType type, ScriptObject source, auto&&... args) { - m_shootParams = params; + if (!hasNPCServer()) return; + if (level == nullptr) return; + auto eventDistance = cached.eventDistance.getValue(); + for (auto& npcid : level->findInRangeNPCsByDistance(position, eventDistance)) + { + if (auto npc = getNPC(npcid); npc) + npc->scripting.events.addEvent(type, source, std::forward(args)...); + } } - const std::string& getShootParams() const +public: + [[inline]] const std::vector& getShootParams() const; + [[inline]] void setShootParams(std::vector&& params); + + void setShootParams(std::ranges::forward_range auto&& params) + requires std::same_as, std::string> { - return m_shootParams; + m_shootParams.clear(); + m_shootParams.assign(std::ranges::begin(params), std::ranges::end(params)); } -private: - GS2ScriptManager m_gs2ScriptManager; +public: + /// @brief Schedules a task to be executed after a specified delay. + /// @param delay The time duration to wait before executing the task. + /// @param task The function to execute after the delay. + void scheduleTask(precise_clock::duration delay, std::function task); - template - void compileScript(ScriptObjType& obj, GS2ScriptManager::user_callback_type& cb); +public: + ServerGeneration Generation{ServerGeneration::CLASSIC}; + ScriptContainer Scripting; - void handleGS2Errors(const std::vector& errors, const std::string& origin); + std::array groundHeights = {0.0, 3.0, 4.0, 5.0, 25.0, 55.0, 65.0}; + +public: + // Publicly visible settings. + ExternalServerCachedSettings cached; private: - bool doTimedEvents(); - void cleanupDeletedPlayers(); + bool doTimedEvents(int iterations); - bool m_doRestart; + bool m_doRestart = false; - FileSystem m_filesystem[FS_COUNT], m_filesystemAccounts; - CLog m_npcLog, m_rcLog, m_serverLog, m_scriptLog; //("logs/npclog|rclog|serverlog|scriptlog.txt"); - CSettings m_adminSettings, m_settings; + fs::FileSystem m_fsWorld, m_fsServer; CSocket m_playerSock; CSocketManager m_sockManager; - CTranslationManager m_translationManager; WordFilter m_wordFilter; AnimationManager m_animationManager; PackageManager m_packageManager; - CString m_allowedVersionString, m_name, m_serverMessage, m_serverPath; - CString m_overrideIp, m_overrideLocalIp, m_overridePort, m_overrideInterface; - - std::vector m_allowedVersions, m_foldersConfig, m_ipBans, m_statusList, m_staffList; - - std::unordered_map m_serverFlags; - std::unordered_map> m_weaponList; - std::unordered_map> m_classList; - - std::unordered_map> m_npcList; - std::unordered_map> m_npcNameList; - IdGenerator m_npcIdGenerator{ NPCID_INIT }; + CString m_allowedVersionString, m_name, m_serverMessage; + std::string m_overrideIp, m_overrideLocalIp, m_overridePort, m_overrideInterface; + std::vector m_allowedVersions, m_foldersConfig, m_ipBans; + std::vector> m_bushDrops; + std::vector m_deathDrops; + bool m_newWorldMode = false; + + Settings m_adminSettings; + Settings m_settings; + + SettingCache m_generationString{"generation", "classic"}; + SettingCache m_classicStyleLogs{"classicstylelogs", false}; + SettingCache m_dontAddServerFlags{"dontaddserverflags", false}; + SettingCache m_newTilesets{"newtilesets", false}; + SettingCache m_unloadInactiveLevelTime{"unloadinactiveleveltime", 600}; + SettingCache> m_newTilesetLevels{"newtilesetlevels", {}}; + SettingCache> m_staffList{"staff"}; + SettingCache> m_bushItemTypes{"bushitemtypes", {"greenrupee", "bluerupee", "heart", "bombs"}}; + SettingCache> m_deathItemTypes{"deathitemtypes", {"greenrupee", "bluerupee", "redrupee", "goldrupee", "bombs", "darts"}}; + SettingCache> m_gmaps{"gmaps", {}}; + SettingCache> m_bigmaps{"maps", {}}; + SettingCache> m_groupmaps{"groupmaps", {}}; + + std::unique_ptr m_accountLoader; + std::unique_ptr m_npcLoader; std::vector> m_mapList; - std::vector> m_levelList; - std::unordered_multimap> m_groupLevels; - - std::unordered_map> m_playerList; - std::unordered_set> m_deletedPlayers; - IdGenerator m_playerIdGenerator{ PLAYERID_INIT }; - - ServerList m_serverlist; - std::chrono::high_resolution_clock::time_point m_lastTimer, m_lastNewWorldTimer, m_last1mTimer, m_last5mTimer, m_last3mTimer; - std::time_t m_serverStartTime; - unsigned int m_serverTime; + string_map> m_cachedLevelDataList; + string_map> m_levelList; + string_multimap> m_gmapLevels; + + string_map> m_weaponList; + std::unordered_map> m_npcList; + IdGenerator m_npcIdGenerator{NPCID_GEN_DATABASE_LOCALN}; + + std::unordered_map> m_playerList; + IdGenerator m_playerIdGenerator{PLAYERID_GEN}; + + TimeoutGenerator m_timedEvents{1s, true}; + TimeoutGenerator m_timedNWTime{5s, true}; + TimeoutGenerator m_timedSave{1min, true}; + TimeoutGenerator m_timedMaintenance{5min, true}; + std::vector>> m_scheduledTasks; + clock::time_point m_serverStartTime; + clock::time_point m_frameStartTime; + precise_clock::time_point m_frameStartTimeHighPrecision; + uint32_t m_serverTime; // Trigger dispatcher TriggerDispatcher m_triggerActionDispatcher; void createTriggerCommands(TriggerDispatcher::Builder cmdBuilder); - std::string m_shootParams; + std::vector m_shootParams; -#ifdef V8NPCSERVER - ScriptEngine m_scriptEngine; - int m_ncPort; - std::shared_ptr m_npcServer; - std::shared_ptr m_pmHandlerNpc; -#endif + std::shared_ptr m_npcServer; + ServerList m_serverlist; -#ifdef UPNP - UPNP m_upnp; + std::unique_ptr m_upnp; std::thread m_upnpThread; -#endif }; -inline std::shared_ptr Server::getNPC(const uint32_t id) const +inline const auto& Server::getAllowedDeathDrops() const noexcept { - auto iter = m_npcList.find(id); - if (iter != std::end(m_npcList)) - return iter->second; - - return nullptr; + return m_deathDrops; } -inline bool Server::hasClass(const std::string& className) const +inline std::shared_ptr Server::getNPC(const NPCID id) const { - return m_classList.find(className) != m_classList.end(); -} - -inline ScriptClass* Server::getClass(const std::string& className) const -{ - auto classIter = m_classList.find(className); - if (classIter != m_classList.end()) - return classIter->second.get(); + auto iter = m_npcList.find(id); + if (iter != std::end(m_npcList)) + return iter->second; return nullptr; } -inline CString Server::getServerPath(const std::string& path) const +inline bool Server::isNewWorldMode() const noexcept { - return getServerPath() << std::filesystem::weakly_canonical(path).string(); + return m_newWorldMode; } -#ifdef V8NPCSERVER - -inline std::shared_ptr Server::getNPCByName(const std::string& name) const +inline void Server::logToFile(std::filesystem::path fileName, string::InputRangeNotString auto&& messages) const { - auto npcIter = m_npcNameList.find(name); - if (npcIter != m_npcNameList.end()) - return npcIter->second.lock(); - - return nullptr; + bool first = true; + for (const auto& message : messages) + { + logToFile(fileName, message, first); + first = false; + } } -#endif - -#include "IEnums.h" - inline void Server::sendToRC(const CString& pMessage, std::weak_ptr pSender) const { int len = pMessage.find("\n"); @@ -401,4 +562,62 @@ inline void Server::sendToNC(const CString& pMessage, std::weak_ptr pSen sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_RC_CHAT << pMessage.subString(0, len), pSender); } -#endif +template +inline std::shared_ptr Server::getPlayer(const PlayerID id) const +{ + auto iter = m_playerList.find(id); + if (iter == std::end(m_playerList)) + return nullptr; + + if constexpr (std::same_as) + return iter->second; + + return std::dynamic_pointer_cast(iter->second); +} + +template +inline std::shared_ptr Server::getPlayer(const PlayerID id, int type) const +{ + auto player = getPlayer(id); + if (player == nullptr || !(player->getType() & type)) + return nullptr; + + return player; +} + +template +inline std::shared_ptr Server::getPlayer(const CString& account, int type) const +{ + for (const auto& [id, player] : m_playerList) + { + // Check if its the type of player we are looking for + if (!player || !(player->getType() & type)) + continue; + + // Compare account names. + if (string::equalsi(player->account.name, account.toStringView())) + { + if constexpr (std::same_as) + return player; + + return std::dynamic_pointer_cast(player); + } + } + + return nullptr; +} + +inline const std::vector& Server::getShootParams() const +{ + return m_shootParams; +} + +inline void Server::setShootParams(std::vector&& params) +{ + m_shootParams = std::move(params); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // SERVER_H diff --git a/server/include/ServerList.h b/server/include/ServerList.h index 7745f85e2..e42bc5883 100644 --- a/server/include/ServerList.h +++ b/server/include/ServerList.h @@ -1,15 +1,22 @@ -#ifndef TSERVERLIST_H -#define TSERVERLIST_H +#ifndef SERVERLIST_H +#define SERVERLIST_H -#include +#include #include #include -#include +#include +#include -#include #include + +#include +#include #include -#include "BabyDI.h" + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// enum { @@ -118,4 +125,7 @@ class ServerList : public CSocketStub std::string m_serverRemoteIp{ "127.0.0.1" }; }; -#endif // TSERVERLIST_H +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // SERVERLIST_H diff --git a/server/include/UpdatePackage.h b/server/include/UpdatePackage.h index dd514969c..000f85d47 100644 --- a/server/include/UpdatePackage.h +++ b/server/include/UpdatePackage.h @@ -1,10 +1,16 @@ -#ifndef GS2EMU_UPDATEPACKAGE_H -#define GS2EMU_UPDATEPACKAGE_H +#ifndef UPDATEPACKAGE_H +#define UPDATEPACKAGE_H #include #include #include #include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// class Server; @@ -72,7 +78,7 @@ inline UpdatePackage::UpdatePackage(std::string packageName) inline UpdatePackage::UpdatePackage(UpdatePackage&& o) noexcept : m_packageName(std::move(o.m_packageName)), m_fileList(std::move(o.m_fileList)), - m_checksum(o.m_checksum), m_packageSize(o.m_packageSize) + m_checksum(o.m_checksum), m_packageSize(o.m_packageSize) { } @@ -105,4 +111,7 @@ inline bool UpdatePackage::compareChecksum(uint32_t check) const return m_checksum == check; } -#endif //GS2EMU_UPDATEPACKAGE_H +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // UPDATEPACKAGE_H diff --git a/server/include/Weapon.h b/server/include/Weapon.h deleted file mode 100644 index 27661fb13..000000000 --- a/server/include/Weapon.h +++ /dev/null @@ -1,114 +0,0 @@ -#ifndef TWEAPON_H -#define TWEAPON_H - -#include -#include -#include -#include - -#include -#include "BabyDI.h" - -#include "level/LevelItem.h" -#include "scripting/SourceCode.h" - -#ifdef V8NPCSERVER - #include "scripting/interface/ScriptBindings.h" - #include "scripting/ScriptExecutionContext.h" - -class Player; -#endif - -class Server; -class Weapon -{ -public: - // -- Constructor | Destructor -- // - Weapon(LevelItemType itemType); - Weapon(std::string pName, std::string pImage, std::string pScript, const time_t pModTime = 0, bool pSaveWeapon = false); - ~Weapon(); - - // -- Functions -- // - bool saveWeapon(); - void updateWeapon(std::string pImage, std::string pScript, const time_t pModTime = 0, bool pSaveWeapon = true); - - static std::shared_ptr loadWeapon(const CString& pWeapon); - - // Functions -> Inline Get-Functions - CString getWeaponPacket(int clientVersion) const; - bool isDefault() const { return (m_weaponDefault != LevelItemType::INVALID); } - bool hasBytecode() const { return (!m_bytecode.isEmpty()); } - LevelItemType getWeaponId() const { return m_weaponDefault; } - const SourceCode& getSource() const { return m_source; } - const CString& getByteCode() const { return m_bytecode; } - const std::string& getByteCodeFile() const { return m_bytecodeFile; } - const std::string& getImage() const { return m_weaponImage; } - const std::string& getName() const { return m_weaponName; } - const std::string& getFullScript() const { return m_source.getSource(); } - std::string_view getServerScript() const { return m_source.getServerSide(); } - time_t getModTime() const { return m_modTime; } - - // Functions -> Set Variables - void setModTime(time_t pModTime) { m_modTime = pModTime; } - -#ifdef V8NPCSERVER - ScriptExecutionContext& getExecutionContext(); - IScriptObject* getScriptObject() const; - - void freeScriptResources(); - void queueWeaponAction(Player* player, const std::string& args); - void runScriptEvents(); - void setScriptObject(std::unique_ptr> object); -#endif -protected: - BabyDI_INJECT(Server, m_server); - - void setClientScript(const CString& pScript); - - // Varaibles -> Weapon Data - LevelItemType m_weaponDefault; - time_t m_modTime; - - SourceCode m_source; - CString m_formattedClientGS1; - - CString m_bytecode; - std::string m_bytecodeFile; - - std::string m_weaponImage; - std::string m_weaponName; - std::vector m_joinedClasses; - -private: -#ifdef V8NPCSERVER - std::unique_ptr> m_scriptObject; - ScriptExecutionContext m_scriptExecutionContext; -#endif -}; -using TWeaponPtr = std::shared_ptr; - -#ifdef V8NPCSERVER - -inline ScriptExecutionContext& Weapon::getExecutionContext() -{ - return m_scriptExecutionContext; -} - -inline IScriptObject* Weapon::getScriptObject() const -{ - return m_scriptObject.get(); -} - -inline void Weapon::runScriptEvents() -{ - m_scriptExecutionContext.runExecution(); -} - -inline void Weapon::setScriptObject(std::unique_ptr> object) -{ - m_scriptObject = std::move(object); -} - -#endif - -#endif diff --git a/server/include/animation/Character.h b/server/include/animation/Character.h deleted file mode 100644 index 8461d41af..000000000 --- a/server/include/animation/Character.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef CHARACTER_H -#define CHARACTER_H - -#include -#include - -#include - -struct Character -{ - float hitpoints = 3; - uint8_t bombs = 10; - uint8_t arrows = 5; - uint8_t bombPower = 1; - uint8_t glovePower = 1; - uint8_t swordPower = 1; - uint8_t shieldPower = 1; - uint8_t bowPower = 1; - uint8_t sprite = 2; - uint8_t ap = 50; - uint8_t colors[5] = { 2, 0, 10, 4, 18 }; - uint32_t gralats = 0; - std::string nickName{ "default" }; - CString gani{ "idle" }; - CString chatMessage; - CString horseImage; - CString headImage{ "head0.png" }; - CString bodyImage{ "body.png" }; - CString swordImage{ "sword1.png" }; - CString shieldImage{ "shield1.png" }; - CString bowImage{ "bow1.png" }; - CString ganiAttributes[30]; -}; - -#endif // CHARACTER_H diff --git a/server/include/animation/GameAni.h b/server/include/animation/GameAni.h index e2299fd89..e9c37a07b 100644 --- a/server/include/animation/GameAni.h +++ b/server/include/animation/GameAni.h @@ -1,11 +1,18 @@ -#ifndef TGAMEANI_H -#define TGAMEANI_H +#ifndef GAMEANI_H +#define GAMEANI_H +#include #include #include +#include #include +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + class Server; class GameAni @@ -102,7 +109,7 @@ inline GameAni::GameAni(std::string aniName) inline GameAni::GameAni(GameAni&& o) noexcept : m_aniName(std::move(o.m_aniName)), m_script(std::move(o.m_script)), - m_setBackTo(std::move(o.m_setBackTo)), m_bytecode(std::move(o.m_bytecode)), m_aniFlags(o.m_aniFlags) + m_setBackTo(std::move(o.m_setBackTo)), m_bytecode(std::move(o.m_bytecode)), m_aniFlags(o.m_aniFlags) { } @@ -116,4 +123,7 @@ inline GameAni& GameAni::operator=(GameAni&& o) noexcept return *this; } -#endif +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // GAMEANI_H diff --git a/server/include/filesystem/File.h b/server/include/filesystem/File.h new file mode 100644 index 000000000..92d741539 --- /dev/null +++ b/server/include/filesystem/File.h @@ -0,0 +1,475 @@ +#ifndef FILE_H +#define FILE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace std::literals; + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Creates a string of the same native type as std::filesystem::path from a string literal using a user-defined literal operator. +/// @param chars Pointer to the character array representing the string literal. +/// @param length The length of the string literal. +/// @return A std::filesystem::path::string_type constructed from the given character array. +constexpr std::filesystem::path::string_type operator ""_pv(const char* chars, size_t length) +{ + return std::filesystem::path::string_type{ chars, chars + length }; +} + +/////////////////////////////////////////////////////////////////////////////// +} + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::fs +{ +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Returns the filename component of a filesystem path as an ANSI encoded std::string. +/// @param file The filesystem path from which to extract the filename. +/// @return A std::string containing the filename part of the path, encoded in ANSI. +std::string getANSIFileName(const std::filesystem::path& file); + +/// @brief Returns an HTML escaped version of the specified file name. +/// @param file The file to escape. +/// @return The escaped file name. +std::filesystem::path getHTMLEscapedFileName(const std::filesystem::path& file); + +/// @brief Returns an HTML unescaped version of the specified file name. +/// @param file The file to unescape. +/// @return The unescaped file name. +std::filesystem::path getHTMLUnescapedFileName(const std::filesystem::path& file); + +//---------------------------- + +class File +{ +public: + File() = default; + + File(const std::filesystem::path& file, std::unique_ptr&& stream) + : m_file(file), m_inputStreamHandle(std::move(stream)) + { + } + + File(const std::filesystem::path& file) : m_file(file) + { + open(); + } + + File(File&& other) noexcept + { + std::swap(m_file, other.m_file); + std::swap(m_inputStreamHandle, other.m_inputStreamHandle); + std::swap(m_inputStream, other.m_inputStream); + } + + virtual ~File() + { + close(); + } + +public: + File(const File& other) = delete; + File& operator=(const File& other) = delete; + bool operator==(const File& other) = delete; + +public: + virtual File& operator=(File&& other) noexcept + { + std::swap(m_file, other.m_file); + std::swap(m_inputStreamHandle, other.m_inputStreamHandle); + std::swap(m_inputStream, other.m_inputStream); + return *this; + } + +public: + /// @brief Converts directly into an istream. + virtual operator std::istream& () + { + return *m_inputStream; + } + + /// @brief Converts directly into a shared pointer to the istream. + virtual operator std::istream* () + { + return m_inputStream; + } + + /// @brief Returns if this is a valid file. + virtual operator bool() const + { + return opened(); + } + +public: + /// @brief Opens the file. + /// @return If the file was successfully opened. + virtual bool open(); + + /// @brief Closes the file. + virtual void close(); + + /// @brief Tells us if the file is opened. + /// @return If the file is opened or not. + virtual bool opened() const; + +public: + /// @brief Reads the position indicator of the file. + /// @return The position indicator. + virtual std::streampos getStreamPosition() const; + + /// @brief Sets the position indicator of the file. + /// @param position The position in the file. + virtual File& setStreamPosition(const std::streampos& position); + + /// @brief Sets the position indicator of the file. + /// @param offset The offset for our new read position. + /// @param origin Where we calculate the offset from. + virtual File& setStreamPosition(const std::streamoff& offset, const std::ios_base::seekdir origin = std::ios_base::beg); + + /// @brief Gets the file size. + /// @return The file size. + virtual uintmax_t size() const + { + return std::filesystem::file_size(m_file); + } + + /// @brief Gets the path to the file. + /// @return The path to the file. + const std::filesystem::path& filePath() const + { + return m_file; + } + + /// @brief Sets the file path for the current object, closing any previously opened file. + /// @param filePath The new file path to set. + void setFilePath(const std::filesystem::path& filePath) + { + if (opened()) close(); + m_file = filePath; + } + + /// @brief Gets the file modified time. + /// @return The file modified time. + virtual std::filesystem::file_time_type modifiedTime() const + { + try + { + return std::filesystem::last_write_time(m_file); + } + catch (...) + { + return std::filesystem::file_time_type::min(); + } + } + +public: + /// @brief Reads the full file. + /// @return The file contents. + virtual std::vector read(); + + /// @brief Reads part of a file. + /// @param count How many bytes to read. + /// @return The file contents. + virtual std::vector read(std::size_t count); + + /// @brief Reads part of a file as a string. + /// @param count How many bytes to read. + /// @return The file contents. + virtual std::string readChars(std::size_t count); + + /// @brief Reads a packed string (length + data). + /// @return The file contents. + virtual std::string readGString(); + + /// @brief Reads a packed integral value. + /// @tparam C The number of bytes to read. Valid values are 1, 2, 3, 4, 5, or 10. + /// @return An integral value whose type is deduced and corresponds to the requested size C. + template + [[inline]] auto readPackedIntegral(); + + /// @brief Reads an integral value. + /// @tparam C The number of bytes to read. + /// @return An integral value whose type is deduced and corresponds to the requested size C. + template + [[inline]] auto readIntegral(); + + /// @brief Reads from the file until it encounters the token. + virtual std::vector readUntil(std::string_view delimiter); + + /// @brief Reads the file into a string. + /// @return The file contents. + virtual std::string readAsString(); + + /// @brief Reads a line from the file. + /// @return A string containing a single line, excluding the line ending. + virtual std::string readLine(); + + /// @brief Reads the value of a configuration entry for a given key and seeks back to the beginning of the file. + /// @param key The key identifying the configuration entry to read. + /// @return The value associated with the specified key as a string, or a std::nullopt if it doesn't exist. + virtual std::optional readConfigLine(std::string_view key, std::string_view separator = "="sv); + + /// @brief Reads a configuration section between the specified start and end keys and seeks back to the beginning of the file. + /// @param startKey The key indicating the start of the configuration section to read. + /// @param endKey The key indicating the end of the configuration section to read. + /// @return An optional string containing the configuration section if found; std::nullopt if the section does not exist. + virtual std::optional readConfigSection(std::string_view startKey, std::string_view endKey); + + /// @brief Returns a generator that yields all lines as strings. + /// @return A generator that produces each line as a std::string. + std::generator readAllLines(); + + /// @brief Returns a generator that yields lines until it reaches the end key. + /// @param endToken A string that, when encountered at the start of a line, ends the read. + /// @return A generator that produces each line as a std::string. + std::generator readLinesUntilSectionEnd(std::string_view endKey); + + /// @brief Reads data into a buffer. + /// @param buffer The buffer to fill. + /// @param count How many bytes to read. + /// @return How much bytes were actually read. + virtual size_t readIntoBuffer(uint8_t* buffer, size_t count); + + /// @brief Tells us if we finished reading the file. + /// @return If we finished reading the file or not. + virtual bool finishedReading() const; + +protected: + std::filesystem::path m_file; + std::istream* m_inputStream{ nullptr }; + std::unique_ptr m_inputStreamHandle; +}; + +using FilePtr = std::shared_ptr; + +//---------------------------- + +template +inline auto File::readPackedIntegral() +{ + using Type = std::conditional_t>>>>; + + static_assert(C >= 1 && C <= 5, "Unsupported size for readPackedIntegral."); + + Type result = 0; + char byte = 0; + + if (!opened() || finishedReading()) + return result; + + auto readAndApply = [&](size_t N) + { + m_inputStream->read(&byte, 1); + result |= (static_cast(static_cast(byte - 32)) << ((N - 1) * 7)); + }; + + readAndApply(1); + + if constexpr (C <= 2) + { + readAndApply(2); + } + if constexpr (C <= 3) + { + readAndApply(3); + } + if constexpr (C <= 4) + { + readAndApply(4); + } + if constexpr (C <= 5) + { + readAndApply(5); + } + + return result; +} + +template +inline auto File::readIntegral() +{ + static_assert(C == 1 || C == 2 || C == 4 || C == 8, "Unsupported integral size for readIntegral."); + + using Type = std::conditional_t>>>; + + if (!opened() || finishedReading()) + return (Type)0; + + Type value = 0; + m_inputStream->read(reinterpret_cast(&value), sizeof(value)); + return value; +} + +//---------------------------- + +class FileIO : public File +{ +public: + FileIO() = default; + + FileIO(const std::filesystem::path& file, std::unique_ptr&& stream) + : m_outputStreamHandle(std::move(stream)) + { + m_file = file; + } + + FileIO(const std::filesystem::path& file) + { + m_file = file; + open(); + } + + FileIO(FileIO&& other) noexcept + { + std::swap(m_file, other.m_file); + std::swap(m_inputStream, other.m_inputStream); + std::swap(m_outputStreamHandle, other.m_outputStreamHandle); + } + + virtual ~FileIO() + { + close(); + } + +public: + FileIO(const FileIO& other) = delete; + FileIO& operator=(const FileIO& other) = delete; + bool operator==(const FileIO& other) = delete; + +public: + virtual File& operator=(File&& other) noexcept override + { + if (auto fileIO = dynamic_cast(&other)) + std::swap(m_outputStreamHandle, fileIO->m_outputStreamHandle); + return File::operator=(std::move(other)); + } + + FileIO& operator=(FileIO&& other) noexcept + { + std::swap(m_file, other.m_file); + std::swap(m_inputStream, other.m_inputStream); + std::swap(m_outputStreamHandle, other.m_outputStreamHandle); + return *this; + } + +public: + using File::operator std::istream&; + using File::operator std::istream*; + using File::operator bool; + + /// @brief Converts directly into an fstream. + operator std::fstream& () const + { + return *(m_outputStreamHandle.get()); + } + + /// @brief Converts directly into a shared pointer to the istream. + operator std::fstream* () const + { + return m_outputStreamHandle.get(); + } + +public: + /// @brief Opens the file. + /// @return If the file was successfully opened. + virtual bool open() override; + + /// @brief Closes the file. + virtual void close() override; + + /// @brief Tells us if the file is opened. + /// @return If the file is opened or not. + virtual bool opened() const override; + +public: + /// @brief Clears the contents of the file. + /// @return A reference to the FileIO object after clearing. + FileIO& clear(); + + /// @brief Writes the contents of the buffer to the file. + /// @param buffer A span containing the data to write to the file. + /// @return A reference to the FileIO object, allowing for method chaining. + FileIO& write(std::span buffer); + + /// @brief Writes the contents of the buffer to the file. + /// @param buffer A span containing the data to write to the file. + /// @return A reference to the FileIO object after writing. + FileIO& write(std::span buffer); + + /// @brief Writes a blank line to the file. + /// @return A reference to the FileIO object after writing the line. + FileIO& writeLine(); + + /// @brief Writes a line of text to the file. + /// @param line A span containing the characters to write as a line. + /// @return A reference to the FileIO object, allowing for method chaining. + FileIO& writeLine(std::span line); + + /// @brief Writes multiple lines to the file output stream. + /// @param lines A range of lines to write to the file. + /// @return A reference to the FileIO object after writing the lines. + FileIO& writeLines(std::ranges::input_range auto&& lines) + { + for (const auto& line : lines) + writeLine(line); + return *this; + } + + /// @brief Writes a configuration line consisting of a key, a separator, and a value to the file. + /// @param key A span containing the key to write. + /// @param value A span containing the value to write. + /// @param separator A span containing the separator to use between the key and value. Defaults to a single space. + /// @return A reference to the FileIO object after writing the configuration line. + FileIO& writeConfigLine(std::span key, std::span value, std::span separator = { " "sv }); + + /// @brief Writes a configuration section between specified start and end keys to a file. + /// @param startKey A span representing the starting key of the configuration section. + /// @param endKey A span representing the ending key of the configuration section. + /// @param section A span containing the configuration section data to write. + /// @return A reference to the FileIO object after writing the configuration section. + FileIO& writeConfigSection(std::span startKey, std::span section, std::span endKey); + + /// @brief Flushes the output buffer, ensuring all pending data is written to the file. + /// @return A reference to the FileIO object after flushing. + FileIO& flush(); + +protected: + bool open(bool truncate); + +protected: + std::filesystem::path m_tempFile; + std::unique_ptr m_outputStreamHandle; +}; + +using FileIOPtr = std::shared_ptr; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::fs + +#endif // FILE_H diff --git a/server/include/filesystem/FileSystem.h b/server/include/filesystem/FileSystem.h new file mode 100644 index 000000000..889e0f024 --- /dev/null +++ b/server/include/filesystem/FileSystem.h @@ -0,0 +1,341 @@ +#ifndef FILESYSTEM_H +#define FILESYSTEM_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::fs +{ +/////////////////////////////////////////////////////////////////////////////// + +struct FileData; + +/// @brief A callback function for file system events. +using FileEventCallback = std::function; + +//---------------------------- + +/// @brief Data describing a file. +struct FileData +{ + /// @brief The full path to the file. + std::filesystem::path file; + + /// @brief The size of the file. + uintmax_t fileSize = 0; + + /// @brief The categories this file belongs to. + FileCategoryCollection categories; + + /// @brief The file's modified time. + std::filesystem::file_time_type modifiedTime; + + /// @brief A callback function for handling file system events for a specific file. + FileEventCallback eventCallback; + + /// @brief Retrieves the last modification time of the file. + /// @return A time_point representing the last write time of the file. + clock::time_point getModTime() const + { + return getFileModTime(file); + } + + /// @brief Sets the last modification time of a file. + /// @param modTime The new modification time to set. + void setModTime(clock::time_point modTime) const + { + std::filesystem::last_write_time(file, toFileClock(modTime)); + } + + /// @brief Updates the modified time with the last modification time of the file. + void refreshModTime() + { + modifiedTime = std::filesystem::last_write_time(file); + } + + /// @brief Deletes the file associated with the object. + /// @return true if the file was successfully deleted; false otherwise. + bool deleteFile() const + { + return std::filesystem::remove(file); + } + + /// @brief Opens the file associated with the object. + /// @return A shared pointer to the opened file. + std::shared_ptr openFile() const + { + return std::make_shared(file); + } + + /// @brief Opens the file associated with the object for writing. + /// @return A shared pointer to the opened file. + std::shared_ptr openFileForWriting() const + { + return std::make_shared(file); + } +}; +using FileDataPtr = std::shared_ptr; +using FileDataWeakPtr = std::weak_ptr; + +//---------------------------- + +/// @brief Manages files within a directory. +class FileSystem +{ +public: + FileSystem() = default; + FileSystem(const std::filesystem::path& directory); + ~FileSystem(); + + FileSystem(const FileSystem& other) = delete; + FileSystem(FileSystem&& other) = delete; + FileSystem& operator=(const FileSystem& other) = delete; + FileSystem& operator=(FileSystem&& other) = delete; + +public: + /// @brief Resets the file system. + void reset(); + + /// @brief Sets the folders configuration. + void addFoldersConfigEntry(FileCategory category, const std::filesystem::path& glob); + + /// @brief Binds to a directory. + /// @param directory The directory to bind to. + void bind(const std::filesystem::path& directory); + + /// @brief Binds to multiple directories. + void bind(string::StringVariant auto... directories) + { + (..., bind(std::filesystem::path{ directories })); + } + + /// @brief Binds to multiple directories. + /// @param directories A range of directories of type std::filesystem::path. + void bind(std::ranges::input_range auto&& directories) + requires std::same_as, std::filesystem::path> + { + for (const auto& path : directories) + bind(path); + } + + /// @brief Binds to multiple directories. + /// @param directories A range of directories of type string::StringViewIshVariant. + void bind(std::ranges::input_range auto&& directories) + requires string::StringViewIshVariant> + { + for (const auto& path : directories) + bind(std::filesystem::path{ path }); + } + + /// @brief Checks for changes to the underlying OS filesystem. Call this every so often. + void update(); + +public: + /// @brief Checks if the filesystem is empty. + /// @return True if the filesystem is empty, false if not. + [[inline]] bool empty() const noexcept; + + /// @brief Checks if the file exists in the filesystem. + /// @param category The category the file must belong to. + /// @param file The file name to check for. + /// @return True if the file exists, false if not. + bool has(FileCategory category, const std::filesystem::path& file) const noexcept; + + /// @brief Checks if the file exists in the filesystem. + /// @param file The file name to check for. + /// @return True if the file exists, false if not. + bool has(const std::filesystem::path& file) const noexcept; + + /// @brief Checks if the file exists in the filesystem (case-insensitive). + /// @param category The category the file must belong to. + /// @param file The file name to check for. + /// @return True if the file exists, false if not. + bool hasi(FileCategory category, const std::filesystem::path& file) const noexcept; + + /// @brief Checks whether a folders configuration is present. + /// @return True if a folders configuration exists; otherwise, false. + [[inline]] bool hasFoldersConfig() const noexcept; + +public: + /// @brief Finds a file path corresponding to the specified file category and file. + /// @param category The category of the file to find. + /// @param file The file path to search for, provided as a reference to a std::filesystem::path object. + /// @return A std::filesystem::path representing the found file path corresponding to the given category and file. + std::filesystem::path find(FileCategory category, const std::filesystem::path& file) const noexcept; + + /// @brief Finds a file path corresponding to the specified file category and file (case-insensitive). + /// @param category The category of the file to find. + /// @param file The file path to search for, provided as a reference to a std::filesystem::path object. + /// @return A std::filesystem::path representing the found file path corresponding to the given category and file. + std::filesystem::path findi(FileCategory category, const std::filesystem::path& file) const noexcept; + +public: + /// @brief Returns information about the file. + /// @param category The category the file must belong to. + /// @return Information about the file. + FileData* info(FileCategory category, const std::filesystem::path& file) const; + + /// @brief Returns information about the file. + /// @return Information about the file. + std::vector info(const std::filesystem::path& file) const; + + /// @brief Gets a range of all files in a category. + std::vector info(FileCategory category) const; + + /// @brief Returns information about the file (case-insensitive). + /// @param category The category the file must belong to. + /// @return Information about the file. + FileData* infoi(FileCategory category, const std::filesystem::path& file) const; + + /// @brief Returns information about the file (case-insensitive). + /// @param category The category the file must belong to. + /// @return Information about the file. + std::vector infoi(const std::filesystem::path& file) const; + +public: + /// @brief Opens a file by name. + /// @param category The category the file must belong to. + /// @param file The file name to open. + /// @return A shared pointer to the file. + std::shared_ptr open(FileCategory category, const std::filesystem::path& file) const; + + /// @brief Opens multiple files by name. + /// @param file The file name to open. + /// @return A shared pointer to the file. + std::vector> open(const std::filesystem::path& file) const; + + /// @brief Opens a file from the file data. + /// @param fileData The file data of the file to open. + /// @return A shared pointer to the file. + std::shared_ptr open(const FileData& fileData) const; + + /// @brief Opens a file by name (case-insensitive). + /// @param category The category the file must belong to. + /// @param file The file name to open. + /// @return A shared pointer to the file. + std::shared_ptr openi(FileCategory category, const std::filesystem::path& file) const; + +public: + /// @brief Opens a file by name for writing. + /// @param category The category the file must belong to. + /// @param file The file name to open. + /// @param createNew If true, and the file does not exist, it creates a new file in the first directory of the specified category. + /// @return A shared pointer to the file. + std::shared_ptr openForWriting(FileCategory category, const std::filesystem::path& file, bool createNew = false) const; + + /// @brief Opens multiple files by name for writing. + /// @param file The file name to open. + /// @return A shared pointer to the file. + std::vector> openForWriting(const std::filesystem::path& file) const; + + /// @brief Opens a file from the file data for writing. + /// @param fileData The file data of the file to open. + /// @return A shared pointer to the file. + std::shared_ptr openForWriting(const FileData& fileData) const; + + /// @brief Opens a file by name for writing (case-insensitive). + /// @param category The category the file must belong to. + /// @param file The file name to open. + /// @param createNew If true, and the file does not exist, it creates a new file in the first directory of the specified category. + /// @return A shared pointer to the file. + std::shared_ptr openiForWriting(FileCategory category, const std::filesystem::path& file, bool createNew = false) const; + +public: + /// @brief Creates an entry for a file in the specified category. This is used to create an entry for a file we are creating, but don't want the file watcher to process an add event. + /// @param category The category the file must belong to. + /// @param file The file name to create a stub for. + void addExisting(FileCategory category, const std::filesystem::path& fullFilePath); + +public: + /// @brief Renames a file to a new file path. + /// @param fileData The data structure containing information about the file to be renamed. + /// @param newFilePath The new path for the file. + /// @return A pointer to the updated FileData structure if the rename was successful; otherwise, nullptr. + FileData* rename(const FileData& fileData, std::filesystem::path newFilePath); + +public: + /// @brief Returns a generator that yields references to the managed directories. + /// @return A generator that produces references to each managed directory. + std::generator getManagedDirectories() const; + std::generator getManagedDirectories(FileCategory category) const; + +public: + /// @brief Returns true if we are searching the filesystem. + [[inline]] bool isSearchingForFiles() const; + + /// @brief Blocks the thread until files have been fully searched. + [[inline]] void waitUntilFilesSearched(); + +public: + /// @brief An array that stores a callback function for each file category type. + std::array categoryEventCallback; + +private: + void assignCategoriesToFileData(FileData& fileData); + FileCategory categoryForDirectory(const std::filesystem::path& directory) const; + +private: + watch::FileWatch m_watcher; + std::atomic m_searching_files; + std::condition_variable m_searching_files_condition; + std::unordered_set m_directories; + std::unordered_set m_foldersConfig[FileCategoryTypeCount]; + std::unordered_multimap m_files; + + bool m_destructing = false; + mutable std::mutex m_file_mutex; +}; + +//---------------------------- + +inline bool FileSystem::empty() const noexcept +{ + return m_files.empty(); +} + +inline bool FileSystem::hasFoldersConfig() const noexcept +{ + return std::ranges::any_of(m_foldersConfig, [](const auto& cfg) + { + return !cfg.empty(); + }); +} + +inline bool FileSystem::isSearchingForFiles() const +{ + return m_searching_files; +} + +inline void FileSystem::waitUntilFilesSearched() +{ + if (!m_searching_files) + return; + + std::unique_lock guard(m_file_mutex); + m_searching_files_condition.wait(guard); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::fs + +#endif // FILESYSTEM_H diff --git a/server/include/filesystem/FileSystemTypes.h b/server/include/filesystem/FileSystemTypes.h new file mode 100644 index 000000000..f03b6e878 --- /dev/null +++ b/server/include/filesystem/FileSystemTypes.h @@ -0,0 +1,80 @@ +#ifndef FILESYSTEMTYPES_H +#define FILESYSTEMTYPES_H + +#include +#include +#include +#include + +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::fs +{ +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Retrieves the last modification time of a file as a clock::time_point. +/// @param file The path to the file whose modification time is to be retrieved. +/// @return The time point representing the last modification time of the specified file, using the clock type 'clock'. +inline clock::time_point getFileModTime(const std::filesystem::path& file) +{ + return toSystemClock(std::filesystem::last_write_time(file)); +} + +//---------------------------- + +/// @brief A category assigned to a file. +enum class FileCategory : uint8_t +{ + ALL = 0, + // + FILE, + LEVEL, + HEAD, + BODY, + SWORD, + SHIELD, + SOUND, + // + ACCOUNT, + CONFIG, + NPC, + SCRIPTCLASS, + TRANSLATION, + WEAPON, + // + COUNT +}; +inline constexpr uint8_t FileCategoryTypeCount = static_cast(FileCategory::COUNT); +using FileCategoryCollection = std::bitset; + +//---------------------------- + +/// @brief An event that occurs in the file system. +struct FileEvent +{ + static constexpr uint8_t Invalid = 0; + static constexpr uint8_t Added = 1; + static constexpr uint8_t Deleted = 2; + static constexpr uint8_t Modified = 3; + static constexpr uint8_t Renamed = 4; +}; + +inline constexpr size_t FileEventTypeCount = 5; +using FileEventCollection = std::bitset; + +//---------------------------- + +/// @brief Represents data related to file events. +struct FileEventData +{ + FileEventCollection events; + std::filesystem::path fileName; + std::filesystem::path oldFileName; + void* fsData = nullptr; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::fs + +#endif // FILESYSTEMTYPES_H diff --git a/server/include/filesystem/watch/FileWatch.h b/server/include/filesystem/watch/FileWatch.h new file mode 100644 index 000000000..e2266decc --- /dev/null +++ b/server/include/filesystem/watch/FileWatch.h @@ -0,0 +1,51 @@ +#ifndef FILEWATCH_H +#define FILEWATCH_H + +#include +#include +#include +#include +#include + +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::fs::watch +{ +/////////////////////////////////////////////////////////////////////////////// + +// id, dir, file, oldFile, events +using watch_cb = std::function; + +struct Watch; +struct WatchOS; +class FileWatch +{ +public: + FileWatch(); + ~FileWatch(); + + FileWatch(const FileWatch& other) = delete; + FileWatch(FileWatch&& other) = delete; + FileWatch& operator=(const FileWatch& other) = delete; + FileWatch& operator=(FileWatch&& other) = delete; + +public: + uint32_t add(const std::filesystem::path& directory, watch_cb callback, bool recursive = true); + void remove(const std::filesystem::path& directory); + void remove(uint32_t watch_id); + void removeAll(); + +public: + void update(); + +private: + std::unordered_map m_watchers; + std::unique_ptr m_watch_os; + uint32_t m_last_id; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::fs::watch + +#endif // FILEWATCH_H diff --git a/server/include/level/Level.h b/server/include/level/Level.h index 695233fed..922d99d16 100644 --- a/server/include/level/Level.h +++ b/server/include/level/Level.h @@ -1,356 +1,722 @@ -#ifndef TLEVEL_H -#define TLEVEL_H +#ifndef LEVEL_H +#define LEVEL_H +#include +#include #include -#include +#include #include #include -#include -#include +#include +#include +#include +#include #include #include -#include -#include "BabyDI.h" -#include "level/LevelBaddy.h" -#include "level/LevelBoardChange.h" -#include "level/LevelChest.h" -#include "level/LevelHorse.h" -#include "level/LevelItem.h" -#include "level/LevelLink.h" -#include "level/LevelSign.h" -#include "level/LevelTiles.h" -#include "utilities/IdGenerator.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// +class LevelLoader; +class NPC; +class Player; +class Server; -// Starting baddy id. Baddy id 0 breaks the client so always start here. -constexpr uint8_t BADDYID_INIT = 1; +//---------------------------- +/// @brief Stores the basic data for an NPC in a level. +struct LevelNPCTemplate +{ + std::string image; + LocalPixelPosition position; + Script script; +}; -class Server; -class Player; -class NPC; -class Map; +/// @brief Stores the static data of a level. +struct StaticLevelData; +struct StaticLevelData +{ + std::string levelName; + std::filesystem::path filePath; + clock::time_point modTime; + LevelTiles tiles; + std::vector links; + std::vector chests; + std::vector signs; + std::vector baddies; + std::vector npcs; + std::vector heights; + EventDispatcher> onDataRefreshed; + // + static void reload(std::shared_ptr staticData); + // + std::optional getChestFormattedForSave(LevelChest* chest) const; + void sendBoardToPlayer(std::shared_ptr player) const; + void sendBoardLayersToPlayer(std::shared_ptr player) const; + void sendBoardLayerToPlayer(std::shared_ptr player, size_t layer) const; + void sendChestsToPlayer(std::shared_ptr player) const; + void sendLinksToPlayer(std::shared_ptr player, bool onlyMapLinks) const; + void sendSignsToPlayer(std::shared_ptr player) const; +}; +using StaticLevelDataPtr = std::shared_ptr; + +//---------------------------- + +/// @brief Stores the data for a specific level tile on a map. +struct SubLevel +{ + std::weak_ptr parentLevel; + std::weak_ptr staticData; + std::optional mapPosition; + std::optional instancedTileUpdates; + std::optional scriptUpdatedTiles; + std::optional terrain; + std::vector baddies; + std::vector boardChanges; + bool isSparringZone = false; + bool isNoPkZone = false; + bool isOnGmap = false; + bool isOnBigMap = false; + EventHandle staticDataRefreshedHandle; + // + PixelRectangleArea clipRectangleToPart(const PixelRectangleArea& area) const noexcept; + WholeTileRectangleArea clipRectangleToPart(const WholeTileRectangleArea& area) const noexcept; + // + std::optional getTiles() noexcept; + std::optional getTiles() const noexcept; + std::optional getTiles(size_t layer) noexcept; + std::optional getTiles(size_t layer) const noexcept; + double getHeightAt(const LocalPixelPosition& position) const noexcept; + void sendBoardToPlayer(std::shared_ptr player) const; + void sendBoardLayersToPlayer(std::shared_ptr player) const; + void sendBoardLayerToPlayer(std::shared_ptr player, size_t layer) const; + void sendBoardHeightsToPlayer(std::shared_ptr player) const; + void sendBoardChangesToPlayer(std::shared_ptr player, std::optional time) const; +}; +using SubLevelPtr = std::shared_ptr; + +//---------------------------- class Level : public std::enable_shared_from_this { + friend class LevelLoader; + public: - //! Destructor. + Level(); ~Level(); - //! Finds a level with the specified level name and returns it. If not found, it tries to load it from the disk. - //! \param pLevelName The name of the level to search for. - //! \param server The server the level belongs to. - //! \return A pointer to the level found. - static std::shared_ptr findLevel(const CString& pLevelName, bool loadAbsolute = false); - static std::shared_ptr createLevel(short fillTile = 511, const std::string& levelName = ""); - - //! Re-loads the level. - //! \return True if it succeeds in re-loading the level. - bool reload(); - - void saveLevel(const std::string& filename); - - //! Returns a clone of the level. - std::shared_ptr clone() const; - - // get crafted packets - CString getBaddyPacket(int clientVersion = CLVER_2_17); - CString getBoardPacket(); - CString getLayerPacket(int i); - CString getBoardChangesPacket(time_t time); - CString getBoardChangesPacket2(time_t time); - CString getChestPacket(Player* pPlayer); - CString getHorsePacket(); - CString getLinksPacket(); - CString getNpcsPacket(time_t time, int clientVersion = CLVER_2_17); - CString getSignsPacket(Player* pPlayer); - - //! Gets the actual level name. - //! \return The actual level name. - CString getActualLevelName() const { return m_actualLevelName; } - - //! Gets the level name. - //! \return The level name. - CString getLevelName() const { return m_levelName; } - - //! Sets the level name. - //! \param pLevelName The new name of the level. - void setLevelName(CString pLevelName) { m_levelName = pLevelName; } - - //! Gets the raw level tile data. - //! \return A pointer to all 4096 raw level tiles. - LevelTiles& getTiles(int layer = 0) { return m_tiles[layer]; } - - //! Gets the level mod time. - //! \return The modified time of the level when it was first loaded from the disk. - time_t getModTime() const { return m_modTime; } - - //! Gets a vector full of all the level chests. - //! \return The level chests. - std::vector& getChests() { return m_chests; } - - //! Gets a vector full of the level npc ids. - //! \return The level npcs. - std::set& getNPCs() { return m_npcs; } - - //! Gets a vector full of the level signs. - //! \return The level signs. - std::vector& getSigns() { return m_signs; } - - //! Gets a vector full of the level links. - //! \return The level links. - std::vector& getLinks() { return m_links; } - - //! Gets the gmap this level belongs to. - //! \return The gmap this level belongs to. - std::shared_ptr getMap() const { return m_map.lock(); } - - //! Gets the map x of this level. - //! \return The map x of this level on the map - int getMapX() const { return m_mapX; } - - //! Gets the map y of this level. - //! \return The map y of this level on the map - int getMapY() const { return m_mapY; } - - //! Gets the gmap x of this level or 0 if it doesn't belong to a gmap. - //! \return The gmap x of this level on the map or 0 if it doesn't belong to a gmap. - int getGmapX() const; - - //! Gets the gmap y of this level or 0 if it doesn't belong to a gmap. - //! \return The gmap y of this level on the map or 0 if it doesn't belong to a gmap. - int getGmapY() const; - - //! Gets a vector full of the players on the level. - //! \return The players on the level. - std::deque& getPlayers() { return m_players; } - - //! Gets the tile data for all layers. - //! \return A map of all the layers and their tile data. - std::map getLayers() const { return m_tiles; } - - //! Gets the status on whether players are on the level. - //! \return The level has players. If true, the level has players on it. +public: + static std::shared_ptr createLevel(std::string_view levelName = ""sv); + static std::shared_ptr clone(LevelPtr level, std::string_view name); + +public: + bool loaded = false; + bool reload(std::string_view levelName); + bool reload(const MapPosition& position); + void reload(StaticLevelDataPtr staticData); + bool saveLevel(const MapPosition& mapPosition, std::string_view filename); + +public: + void doTimedEvents(); + void doFrameEvents(precise_clock::time_point time); + const auto& getLastFrameTime() const { return m_lastFrameTime; } + const auto& getFilePath() const { return m_filePath; } + +private: + precise_clock::time_point m_lastFrameTime = precise_clock::now(); + precise_clock::duration m_frameEventDuration = precise_clock::duration::zero(); + +public: + [[inline]] void setMap(std::shared_ptr map); + [[inline]] auto getMap() const noexcept; + bool isGmap() const noexcept; + [[inline]] bool isOnBigMap() const noexcept; + [[inline]] static constexpr Dimension tilesPerSubLevel() noexcept; + [[inline]] static constexpr Dimension pixelsPerTile() noexcept; + [[inline]] static constexpr Dimension pixelsPerSubLevel() noexcept; + [[inline]] Dimension sizeInSubLevels() const noexcept; + [[inline]] Dimension sizeInTiles() const noexcept; + [[inline]] Dimension sizeInPixels() const noexcept; + [[inline]] Rectangle getBoundingBox() const noexcept; + uint16_t* getMapTileForEditing(const TilePosition& position) noexcept; + [[inline]] std::string_view getLevelNameAtPosition(const PixelPosition& position) const noexcept; + +public: + [[inline]] std::optional getSubLevelIndex(std::string_view levelPart) const noexcept; + [[inline]] std::optional getSubLevelOrigin(SubLevelPtr part) const noexcept; + [[inline]] std::optional getSubLevelPositionInMap(std::string_view levelPart) const noexcept; + [[inline]] SubLevelPtr getSubLevelByName(std::string_view levelPart) const noexcept; + [[inline]] SubLevelPtr getSubLevelAtPosition(const PixelPosition& position) const noexcept; + [[inline]] SubLevelPtr getSubLevelAtPosition(const TilePosition& position) const noexcept; + [[inline]] SubLevelPtr getSubLevelAtPosition(const MapPosition& position) const noexcept; + [[inline]] StaticLevelDataPtr getStaticLevelDataByName(std::string_view levelPart) const noexcept; + [[inline]] StaticLevelDataPtr getStaticLevelDataAtPosition(const MapPosition& mapPosition) const noexcept; + [[inline]] std::pair getSubLevelAndStaticDataAtPosition(const MapPosition& position) const noexcept; + [[inline]] PixelPosition convertToMapPosition(std::string_view levelPart, const LocalPixelPosition& position) const noexcept; + [[inline]] PixelPosition convertToMapPosition(std::string_view levelPart, const LocalWholeTilePosition& position) const noexcept; + [[inline]] PixelPosition convertToMapPosition(const MapPosition& mapPosition, const LocalPixelPosition& position) const noexcept; + [[inline]] PixelPosition convertToMapPosition(const MapPosition& mapPosition, const LocalWholeTilePosition& position) const noexcept; + std::generator getSubLevelsInRectangle(const PixelRectangleArea& area) const noexcept; + std::generator getSubLevelsInRectangle(const WholeTileRectangleArea& area) const noexcept; + std::generator getNearbySubLevels(const PixelPosition& position, uint32_t tileDistance = 64) const noexcept; + +public: + [[inline]] auto& getPlayers() noexcept; + [[inline]] auto& getNPCs() noexcept; + [[inline]] auto& getArrows() noexcept; + [[inline]] auto& getBombs() noexcept; + [[inline]] auto& getExplosions() noexcept; + [[inline]] auto& getHorses() noexcept; + [[inline]] auto& getItems() noexcept; + + [[inline]] const auto& getPlayers() const noexcept; + [[inline]] const auto& getNPCs() const noexcept; + [[inline]] const auto& getArrows() const noexcept; + [[inline]] const auto& getBombs() const noexcept; + [[inline]] const auto& getExplosions() const noexcept; + [[inline]] const auto& getHorses() const noexcept; + [[inline]] const auto& getItems() const noexcept; + + std::generator getBaddies() const noexcept; + std::generator getChests() const noexcept; + std::generator getLinks() const noexcept; + std::generator getSigns() const noexcept; + std::generator> getSignPositions() const noexcept; + + size_t getBaddyCount() const noexcept; + size_t getChestCount() const noexcept; + size_t getLinkCount() const noexcept; + size_t getSignCount() const noexcept; + +public: + std::optional getTiles(const MapPosition& mapLevel, size_t layer = 0) noexcept; + std::optional getTiles(const MapPosition& mapLevel, size_t layer = 0) const noexcept; + std::optional getTiles(std::string_view levelPart, size_t layer = 0) noexcept; + std::optional getTiles(std::string_view levelPart, size_t layer = 0) const noexcept; + +public: + bool hasTerrain() const noexcept; + double getHeightAt(const PixelPosition& position) const noexcept; + +public: + void sendBoardToPlayer(std::shared_ptr player) const; + void sendBoardLayersToPlayer(std::shared_ptr player) const; + void sendBoardHeightsToPlayer(std::shared_ptr player) const; + void sendBoardChangesToPlayer(std::shared_ptr player, std::optional time) const; + // + void sendChestsToPlayer(std::shared_ptr player) const; + void sendLinksToPlayer(std::shared_ptr player, bool onlyMapLinks) const; + void sendSignsToPlayer(std::shared_ptr player) const; + // + void sendBaddiesToPlayer(std::shared_ptr player) const; + void sendHorsesToPlayer(std::shared_ptr player) const; + void sendNPCsToPlayer(std::shared_ptr player, std::optional time) const; + +public: bool hasPlayers() const { return !m_players.empty(); } + bool isPlayerLeader(PlayerID id) const; + bool hasLivingBaddies() const; + [[inline]] bool isSparringZone(const MapPosition& mapPosition) const noexcept; + [[inline]] bool isNoPkZone(const MapPosition& mapPosition) const noexcept; - //! Gets the sparring zone status of the level. - //! \return The sparring zone status. If true, the level is a sparring zone. - bool isSparringZone() const { return m_isSparringZone; } - - //! Sets the sparring zone status of the level. - //! \param pLevelSpar If true, the level becomes a sparring zone level. - void setSparringZone(bool pLevelSpar) { m_isSparringZone = pLevelSpar; } - - //! Gets the singleplayer status of the level. - //! \return The singleplayer status. If true, the level is singleplayer. - bool isSingleplayer() const { return m_isSingleplayer; } - - //! Sets the singleplayer status of the level. - //! \param pLevelSingleplayer If true, the level becomes a singleplayer level. - void setSingleplayer(bool pLevelSingleplayer) { m_isSingleplayer = pLevelSingleplayer; } - - //! Adds a board change to the level. - //! \param pTileData Linear array of Graal-packed tiles. Starts with the top-left tile, ends with the bottom-right. - //! \param pX X location of the top-left tile. - //! \param pY Y location of the top-left tile. - //! \param pWidth How many tiles wide we are altering. - //! \param pHeight How many tiles high we are altering. - //! \param player The player who initiated this board change. - //! \return True if it succeeds, false if it doesn't. - bool alterBoard(CString& pTileData, int pX, int pY, int pWidth, int pHeight, Player* player); - - //! Adds an item to the level. - //! \param pX X location of the item to add. - //! \param pY Y location of the item to add. - //! \param pItem The item we are adding. Use LevelItem::getItemId() to get the item type from an item name. - //! \return True if it succeeds, false if it doesn't. - bool addItem(float pX, float pY, LevelItemType pItem); - - //! Removes an item from the level. - //! \param pX X location of the item to remove. - //! \param pY Y location of the item to remove. - //! \return The type of item removed. Use LevelItem::getItemId() to get the item type from an item name. - LevelItemType removeItem(float pX, float pY); - - //! Adds a new horse to the level. - //! \param pImage The image of the horse. - //! \param pX X location of the horse. - //! \param pY Y location of the horse. - //! \param pDir The direction of the horse. - //! \param pBushes The bushes the horse has eaten. - //! \return Returns true if it succeeds. - bool addHorse(CString& pImage, float pX, float pY, char pDir, char pBushes); - - //! Removes a horse from the level. - //! \param pX X location of the horse to remove. - //! \param pY Y location of the horse to remove. - void removeHorse(float pX, float pY); - - //! Adds a baddy to the level. - //! \param pX X location of the baddy to add. - //! \param pY Y location of the baddy to add. - //! \param pType The type of baddy to add. - //! \return A pointer to the new LevelBaddy. - LevelBaddy* addBaddy(float pX, float pY, char pType); - - //! Removes a baddy from the level. - //! \param pId ID of the baddy to remove. - void removeBaddy(uint8_t pId); - - //! Finds a baddy by the specified id number. - //! \param pId The ID number of the baddy to find. - //! \return A pointer to the found LevelBaddy. - LevelBaddy* getBaddy(uint8_t id); - - //! Adds a player to the level. - //! \param player The player to add. - //! \return The id number of the player in the level. - int addPlayer(uint16_t id); - - //! Removes a player from the level. - //! \param player The player to remove. - void removePlayer(uint16_t id); - - //! Gets if the player is the current level leader. - //! \param id The player id to check. - //! \return True if the player is the leader. - bool isPlayerLeader(uint16_t id); - - //! Adds an NPC to the level. - //! \param npc NPC to add to the level. - //! \return True if the NPC was successfully added or false if it already exists in the level. +public: + int addPlayer(PlayerID id); + void removePlayer(PlayerID id); + +public: bool addNPC(std::shared_ptr npc); - bool addNPC(uint32_t npcId); - - //! Adds a level link to the level. - //! \return A pointer to the new LevelLink. - LevelLink* addLink(); - - //! Adds a level link to the level. - //! \param pLink link string to parse - //! \return A pointer to the new LevelLink. - LevelLink* addLink(const std::vector& pLink); - - //! Removes a level link from the level. - //! \param index link index to remove - //! \return true if removed, false otherwise - bool removeLink(uint32_t index); - - //! Adds a level sign to the level. - //! \param pX x position - //! \param pY y position - //! \param pSign sign text - //! \param encoded true if the sign text is encoded - //! \return A pointer to the new LevelSign. - LevelSign* addSign(const int pX, const int pY, const CString& pSign, bool encoded = false); - - //! Adds a level sign to the level. - //! \param index x position - //! \return true if removed, false otherwise. - bool removeSign(uint32_t index); - - //! Adds a level chest to the level. - //! \param pX x position - //! \param pY y position - //! \param itemType which type of item the chest contains - //! \param signIndex signIndex of sign to pop when chest is opened - //! \return A pointer to the new LevelChest. - LevelChest* addChest(const int pX, const int pY, const LevelItemType itemType, const int signIndex); - - //! Adds a level chest to the level. - //! \param index x position - //! \return true if removed, false otherwise. - bool removeChest(uint32_t index); - - //! Removes an NPC from the level. - //! \param npc The NPC to remove. + bool addNPC(NPCID npcId); void removeNPC(std::shared_ptr npc); - void removeNPC(uint32_t npcId); + void removeNPC(NPCID npcId); - //! Sets the map for the current level. - //! \param pMap Map the level is on. - //! \param pMapX X location on the map. - //! \param pMapY Y location on the map. - void setMap(std::weak_ptr pMap, int pMapX = 0, int pMapY = 0); +public: + bool alterBoard(CString& tileData, const WholeTileRectangleArea& area, Player* player, bool forceRespawn = false, bool allowRespawn = true, bool sendToPlayers = false); + void applyBoardChangeFromScriptTiles(const WholeTileRectangleArea& area, bool forceRespawn = false, bool allowRespawn = true); + void saveBoardChangeFromScriptTiles(const WholeTileRectangleArea& area); + void updateBoard(const TileRectangleArea& area) noexcept; + void updateBoard2(const TileRectangleArea& area) noexcept; - //! Does special events that should happen every second. - //! \return Currently, it always returns true. - bool doTimedEvents(); +public: + LevelArrow* addArrow(inform_client_t, const PixelPosition& position, const PixelPosition& speed, uint8_t direction, int8_t type, ScriptObject from); + LevelArrow* addArrow(const PixelPosition& position, const PixelPosition& speed, uint8_t direction, int8_t type, ScriptObject from); + bool removeArrow(uint8_t index); + std::optional getArrow(size_t index) noexcept; - bool isOnWall(int pX, int pY); - bool isOnWall2(int pX, int pY, int pWidth, int pHeight, uint8_t flags = 0); - bool isOnWater(int pX, int pY); - std::optional getChest(int x, int y) const; - std::optional getLink(int pX, int pY) const; - CString getChestStr(LevelChest* chest) const; +public: + LevelBaddy* addBaddy(const LocalPixelPosition& position, BaddyType type); + LevelBaddy* putNewBaddy(const LocalPixelPosition& position, BaddyType type); + LevelBaddy* putNewBaddy(const LocalPixelPosition& position, BaddyType type, uint8_t power, std::string_view image = {}); + bool removeBaddy(uint8_t pId); + bool removeAllBaddies(); + std::optional getBaddyById(uint8_t id) noexcept; + std::optional getAliveBaddyByIndex(size_t index) noexcept; -#ifdef V8NPCSERVER - std::vector findAreaNpcs(int pX, int pY, int pWidth, int pHeight); - std::vector testTouch(int pX, int pY); - NPC* isOnNPC(float pX, float pY, bool checkEventFlag = false); - void sendChatToLevel(const Player* player, const std::string& message); +public: + LevelBomb* addBomb(inform_client_t, const PixelPosition& position, uint8_t power); + LevelBomb* addBomb(const PixelPosition& position, uint8_t power); + LevelBomb* addBombFromClient(const PixelPosition& position, uint8_t power, PlayerID owner, std::chrono::milliseconds timeToExplode); + bool removeBomb(inform_client_t, size_t index); + bool removeBomb(size_t index); + bool removeBomb(const PixelPosition& position); + std::optional getBomb(size_t index) noexcept; - IScriptObject* getScriptObject() const; - void setScriptObject(std::unique_ptr> object); -#endif +public: + std::optional getChest(size_t index) const noexcept; + std::optional getChest(const WholeTilePosition& position) const noexcept; + std::optional getChest(const MapPosition& mapPosition, const LocalWholeTilePosition& position) const noexcept; - void modifyBoardDirect(uint32_t index, short tile); +public: + void addExplosion(inform_client_t, const PixelPosition& position, ScriptObject from, uint8_t radius, uint8_t power); + void addExplosion(const PixelPosition& position, ScriptObject from, uint8_t radius, uint8_t power); + void addSpyFire(const PixelPosition& position, ScriptObject from, uint8_t direction, uint8_t length, uint8_t power); + LevelExplosion* addExplosionPart(const PixelPosition& position, uint8_t direction, uint8_t power); + bool removeExplosion(size_t index); + bool removeExplosion(const PixelPosition& position); + std::optional getExplosion(size_t index) noexcept; -private: - Level(short fillTile = 0); +public: + LevelHorse* addHorse(inform_client_t, std::string_view image, const PixelPosition& position, uint8_t direction, uint8_t bushes); + LevelHorse* addHorse(std::string_view image, const PixelPosition& position, uint8_t direction, uint8_t bushes); + bool removeHorse(inform_client_t, size_t index); + bool removeHorse(size_t index); + bool removeHorse(const PixelPosition& position); + std::optional getHorse(size_t index) noexcept; - // level-loading functions - bool loadLevel(const CString& pLevelName); - bool detectLevelType(const CString& pLevelName); - bool loadGraal(const CString& pLevelName); - bool loadZelda(const CString& pLevelName); - bool loadNW(const CString& pLevelName); +public: + LevelItem* addItem(inform_client_t, const PixelPosition& position, LevelItemType item); + LevelItem* addItem(const PixelPosition& position, LevelItemType item); + bool removeItem(inform_client_t, size_t index); + bool removeItem(size_t index); + LevelItemType removeItem(const PixelPosition& position); + std::optional getItem(size_t index) noexcept; + +public: + std::optional getLink(size_t index) const noexcept; + std::optional getLink(std::string_view levelPart, const LocalWholeTilePosition& position, bool excludeOverworld = false) const noexcept; + std::optional getLink(const TilePosition& position, bool excludeOverworld = false) const noexcept; + +public: + LevelShoot* addShoot(LevelShoot* existingShoot); + LevelShoot* addShoot(inform_client_t, const PixelPosition& position, float angle, float zangle, uint8_t power, float gravity, const std::string& gani, ScriptObject from); + LevelShoot* addShoot(const PixelPosition& position, float angle, float zangle, uint8_t power, float gravity, const std::string& gani, ScriptObject from); + LevelShoot* addShoot(const PixelPosition& position, uint8_t angle, uint8_t zangle, uint8_t power, float gravity, const std::string& gani, ScriptObject from); + bool removeShoot(uint8_t index); + LevelShoot* getShoot(uint8_t index) const; + +public: + std::optional getSign(size_t index) const noexcept; + +public: + bool moveShoot(LevelShoot* shoot, int iterations); + bool moveArrow(LevelArrow* arrow, int iterations); + +public: + bool isOnWall(const WholeTilePosition& tilePosition) const noexcept; + bool isOnWall(const PixelPosition& position) const noexcept; + bool isOnWall2(const WholeTileRectangleArea& tileArea) const noexcept; + bool isOnWall2(const PixelRectangleArea& area) const noexcept; + bool isOnWater(const WholeTilePosition& tilePosition) const noexcept; + bool isOnWater(const PixelPosition& position) const noexcept; + bool isOnWater2(const WholeTileRectangleArea& tileArea) const noexcept; + bool isOnWater2(const PixelRectangleArea& area) const noexcept; + bool isOnPlayer(const PixelPosition& position) const noexcept; + bool isOnPlayer(const PixelRectangleArea& pixelArea) const noexcept; + tileset::TileType getTileTypeAt(const WholeTilePosition& tilePosition) const noexcept; + tileset::TileType getTileTypeAt(const PixelPosition& position) const noexcept; + +public: + std::generator findInRangePlayers(const PixelPosition& position, std::optional> range = std::nullopt) const noexcept; + std::generator findInRangePlayersForCommunication(const PixelPosition& position) const noexcept; + std::generator findPlayersInLevelPart(std::string_view levelPart) const noexcept; + std::generator findPlayersInLevelPart(const MapPosition& mapLevel) const noexcept; + std::generator findInRangeNPCs(const PixelPosition& position) const noexcept; + std::generator findInRangeNPCsByDistance(const PixelPosition& position, uint32_t tileDistance) const noexcept; + std::generator findIntersectingNPCs(const PixelPosition& position, bool includeInvisible = false) const noexcept; + std::generator findIntersectingNPCs(const PixelRectangleArea& area, bool includeInvisible = false) const noexcept; + std::generator findIntersectingNPCsForCollision(const PixelPosition& position) const noexcept; + std::generator findIntersectingNPCsForCollision(const PixelRectangleArea& area) const noexcept; + +public: + std::string levelName; + clock::time_point modTime; + std::optional timeSinceLastPlayerLeft; + ScriptContainer scripting; + +public: + bool isSinglePlayer = false; + bool isGroupMap = false; + std::string groupMapName; + +protected: + std::shared_ptr generateItemNPC(const PixelPosition& position, LevelItemType item); + size_t getMapIndexAtPosition(const MapPosition& mapLevel) const noexcept; private: - BabyDI_INJECT(Server, m_server); - - time_t m_modTime = 0; - bool m_isSparringZone = false; - bool m_isSingleplayer = false; - int m_mapX = 0; - int m_mapY = 0; - std::map m_tiles; - std::weak_ptr m_map; - CString m_fileName, m_fileVersion, m_actualLevelName, m_levelName; - - std::unordered_map m_baddies; - IdGenerator m_baddyIdGenerator{ BADDYID_INIT }; - - std::vector m_boardChanges; - std::vector m_chests; + Server* m_server; + std::filesystem::path m_filePath; + mutable std::shared_ptr m_map; + + std::vector m_levelParts; + + std::deque m_players; + + // TODO: Could be optimized with flat_set, whenever that becomes generally available. + std::unordered_set m_npcs; + + std::vector m_arrows; + std::vector m_bombs; + std::vector m_explosions; std::vector m_horses; std::vector m_items; - std::vector m_links; - std::vector m_signs; - std::set m_npcs; - std::deque m_players; - -#ifdef V8NPCSERVER - std::unique_ptr> m_scriptObject; -#endif + std::vector m_shoots; }; using LevelPtr = std::shared_ptr; -#ifdef V8NPCSERVER +//---------------------------- + +inline void Level::setMap(std::shared_ptr map) +{ + m_map = map; +} + +inline auto Level::getMap() const noexcept +{ + return m_map; +} + +inline bool Level::isOnBigMap() const noexcept +{ + return m_map != nullptr && m_map->isBigMap(); +} + +inline constexpr Dimension Level::tilesPerSubLevel() noexcept +{ + return { 64_ui8, 64_ui8 }; +} + +inline constexpr Dimension Level::pixelsPerTile() noexcept +{ + return { 16_ui8, 16_ui8 }; +} + +inline constexpr Dimension Level::pixelsPerSubLevel() noexcept +{ + return Dimension{ tilesPerSubLevel() } * pixelsPerTile(); +} + +inline Dimension Level::sizeInSubLevels() const noexcept +{ + if (m_map == nullptr) return { 1, 1 }; + return m_map->size; +} + +inline Dimension Level::sizeInTiles() const noexcept +{ + auto size = sizeInSubLevels(); + auto tileSize = tilesPerSubLevel(); + return { static_cast(size.width() * tileSize.width()), static_cast(size.height() * tileSize.height()) }; +} + +inline Dimension Level::sizeInPixels() const noexcept +{ + auto size = sizeInSubLevels(); + auto pixelSize = pixelsPerSubLevel(); + return { static_cast(size.width()) * pixelSize.width(), static_cast(size.height()) * pixelSize.height() }; +} + +inline Rectangle Level::getBoundingBox() const noexcept +{ + //return { getMapPixelOffset(), { 1024_ui16, 1024_ui16 } }; + return { { 0_ui32, 0_ui32 }, sizeInPixels() }; +} + +inline std::string_view Level::getLevelNameAtPosition(const PixelPosition& position) const noexcept +{ + if (!isGmap()) + return levelName; + + if (auto subLevel = getSubLevelAtPosition(position); subLevel != nullptr) + { + if (auto staticData = subLevel->staticData.lock(); staticData != nullptr) + return staticData->levelName; + } + + return levelName; +} + +//---------------------------- + +inline std::optional Level::getSubLevelIndex(std::string_view levelPart) const noexcept +{ + if (!isGmap()) + { + if (levelPart == levelName) + return 0; + return std::nullopt; + } + + if (auto pos = getSubLevelPositionInMap(levelPart); pos.has_value()) + return static_cast(pos.value().y()) * m_map->size.width() + pos.value().x(); + + return std::nullopt; +} + +inline std::optional Level::getSubLevelOrigin(SubLevelPtr part) const noexcept +{ + if (part == nullptr || !part->mapPosition.has_value()) + return std::nullopt; + + auto pixelPerPart = pixelsPerSubLevel(); + return PixelPosition{ static_cast(part->mapPosition.value().x()) * pixelPerPart.width(), static_cast(part->mapPosition.value().y()) * pixelPerPart.height(), 0}; +} -inline IScriptObject* Level::getScriptObject() const +inline std::optional Level::getSubLevelPositionInMap(std::string_view levelPart) const noexcept { - return m_scriptObject.get(); + if (!isGmap()) + { + if (levelPart == levelName) + return MapPosition{ 0, 0 }; + return std::nullopt; + } + + if (auto mapPosOpt = m_map->getLevelPosition(levelPart); mapPosOpt.has_value()) + return mapPosOpt.value(); + + return std::nullopt; } -inline void Level::setScriptObject(std::unique_ptr> object) +inline SubLevelPtr Level::getSubLevelByName(std::string_view levelPart) const noexcept { - m_scriptObject = std::move(object); + if (auto index = getSubLevelIndex(levelPart); index.has_value() && index.value() < m_levelParts.size()) + return m_levelParts[index.value()]; + + return nullptr; } -#endif +inline SubLevelPtr Level::getSubLevelAtPosition(const PixelPosition& position) const noexcept +{ + if (!isGmap()) + return m_levelParts.size() > 0 ? m_levelParts[0] : nullptr; + + auto mapPosition = position / 1024; + auto index = static_cast(mapPosition.y()) * m_map->size.width() + mapPosition.x(); + if (index < m_levelParts.size()) + return m_levelParts[index]; + return nullptr; +} + +inline SubLevelPtr Level::getSubLevelAtPosition(const TilePosition& position) const noexcept +{ + if (!isGmap()) + return m_levelParts.size() > 0 ? m_levelParts[0] : nullptr; + + auto mapPosition = position / 64; + auto index = static_cast(mapPosition.y()) * m_map->size.width() + static_cast(mapPosition.x()); + if (index < m_levelParts.size()) + return m_levelParts[index]; + return nullptr; +} + +inline SubLevelPtr Level::getSubLevelAtPosition(const MapPosition& mapPosition) const noexcept +{ + if (!isGmap()) + return m_levelParts.size() > 0 ? m_levelParts[0] : nullptr; + + auto index = static_cast(mapPosition.y()) * m_map->size.width() + static_cast(mapPosition.x()); + if (index < m_levelParts.size()) + return m_levelParts[index]; + return nullptr; +} + +inline StaticLevelDataPtr Level::getStaticLevelDataByName(std::string_view levelPart) const noexcept +{ + auto levelPartData = getSubLevelByName(levelPart); + if (levelPartData != nullptr) + return levelPartData->staticData.lock(); + return nullptr; +} + +inline StaticLevelDataPtr Level::getStaticLevelDataAtPosition(const MapPosition& mapPosition) const noexcept +{ + auto levelPartData = getSubLevelAtPosition(mapPosition); + if (levelPartData != nullptr) + return levelPartData->staticData.lock(); + return nullptr; +} + +inline std::pair Level::getSubLevelAndStaticDataAtPosition(const MapPosition& position) const noexcept +{ + auto subLevel = getSubLevelAtPosition(position); + if (subLevel != nullptr) + { + if (auto levelData = subLevel->staticData.lock(); levelData != nullptr) + return { subLevel, levelData }; + } + + return { nullptr, nullptr }; +} + +inline PixelPosition Level::convertToMapPosition(std::string_view levelPart, const LocalPixelPosition& position) const noexcept +{ + if (isGmap()) + { + auto mapPosition = getSubLevelPositionInMap(levelPart).value_or(MapPosition{}); + auto pixelPerPart = pixelsPerSubLevel(); + return { position.x() + static_cast(mapPosition.x()) * pixelPerPart.width(), position.y() + static_cast(mapPosition.y()) * pixelPerPart.height(), static_cast(position.z()) }; + } + return { position.x(), position.y(), position.z() }; +} + +inline PixelPosition Level::convertToMapPosition(std::string_view levelPart, const LocalWholeTilePosition& position) const noexcept +{ + auto mapPosition = getSubLevelPositionInMap(levelPart).value_or(MapPosition{}); + return convertToMapPosition(mapPosition, position); +} + +inline PixelPosition Level::convertToMapPosition(const MapPosition& mapPosition, const LocalPixelPosition& position) const noexcept +{ + if (isGmap()) + { + auto pixelPerPart = pixelsPerSubLevel(); + return { static_cast(position.x()) + (mapPosition.x() * pixelPerPart.width()), static_cast(position.y()) + (mapPosition.y() * pixelPerPart.height()), static_cast(position.z()) }; + } + return { static_cast(position.x()), static_cast(position.y()), static_cast(position.z()) }; +} + +inline PixelPosition Level::convertToMapPosition(const MapPosition& mapPosition, const LocalWholeTilePosition& position) const noexcept +{ + auto pixelPerTile = pixelsPerTile(); + + if (isGmap()) + { + auto pixelPerPart = pixelsPerSubLevel(); + return { (static_cast(position.x()) * pixelPerTile.width()) + (mapPosition.x() * pixelPerPart.width()), (static_cast(position.y()) * pixelPerTile.height()) + (mapPosition.y() * pixelPerPart.height()), static_cast(position.z()) * pixelPerTile.length() }; + } + return { static_cast(position.x() * pixelPerTile.width()), static_cast(position.y() * pixelPerTile.height()), static_cast(position.z() * pixelPerTile.length()) }; +} + +//---------------------------- + +inline auto& Level::getPlayers() noexcept +{ + return m_players; +} + +inline auto& Level::getNPCs() noexcept +{ + return m_npcs; +} + +inline auto& Level::getArrows() noexcept +{ + return m_arrows; +} + +inline auto& Level::getBombs() noexcept +{ + return m_bombs; +} + +inline auto& Level::getExplosions() noexcept +{ + return m_explosions; +} + +inline auto& Level::getHorses() noexcept +{ + return m_horses; +} + +inline auto& Level::getItems() noexcept +{ + return m_items; +} + +inline const auto& Level::getPlayers() const noexcept +{ + return m_players; +} + +inline const auto& Level::getNPCs() const noexcept +{ + return m_npcs; +} + +inline const auto& Level::getArrows() const noexcept +{ + return m_arrows; +} + +inline const auto& Level::getBombs() const noexcept +{ + return m_bombs; +} + +inline const auto& Level::getExplosions() const noexcept +{ + return m_explosions; +} + +inline const auto& Level::getHorses() const noexcept +{ + return m_horses; +} + +inline const auto& Level::getItems() const noexcept +{ + return m_items; +} + +inline bool Level::isSparringZone(const MapPosition& mapPosition) const noexcept +{ + if (auto subLevel = getSubLevelAtPosition(mapPosition); subLevel != nullptr) + return subLevel->isSparringZone; + return false; +} + +inline bool Level::isNoPkZone(const MapPosition& mapPosition) const noexcept +{ + if (auto subLevel = getSubLevelAtPosition(mapPosition); subLevel != nullptr) + return subLevel->isNoPkZone; + return false; +} + +//---------------------------- + +namespace source +{ +/// @brief Creates a ScriptObject from a Level by hashing the level's name. +ScriptObject FromLevel(LevelPtr level); +} // end namespace source + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal -#endif // TLEVEL_H +#endif // LEVEL_H diff --git a/server/include/level/LevelArrow.h b/server/include/level/LevelArrow.h new file mode 100644 index 000000000..dee6ceb94 --- /dev/null +++ b/server/include/level/LevelArrow.h @@ -0,0 +1,93 @@ +#ifndef LEVELARROW_H +#define LEVELARROW_H + +#include + +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +/* +(20, 30), shooting default arrow up: + dir=0 dx=0 dy=-1 dz=0 from=1 type=0 x=21.1875 y=29.0625 z=0 (+19 pixels, -16 pixels) + +(20, 30), shooting default arrow down: + dir=2 dx=0 dy=1 dz=0 from=1 type=0 x=21.1875 y=33.0625 z=0 (+19 pixels, +48 pixels) + +(20, 30), shooting default arrow left: + dir=1 dx=-1 dy=0 dz=0 from=1 type=0 x=18.5625 y=31.6875 z=0 (-24 pixels, +27 pixels) + +(20, 30), shooting default arrow right: + dir=3 dx=1 dy=0 dz=0 from=1 type=0 x=22.5625 y=31.6875 z=0 (+40 pixels, +27 pixels) + +type 0 = arrow arrow.x=21.5625 +type 1 = fireball arrow.x=21.5625 +type 2 = fireblast arrow.x=21.5625 +type 3 = nukeshot arrow.x=21.5625 +type -1 = ball (shot from center of the npc calculating dx/dy to travel towards the player) +*/ + +inline constexpr uint8_t arrowSpriteIndex = 107; +inline constexpr uint8_t ballSpriteIndex = 131; +inline constexpr uint8_t arrowTypeBall = 0; +inline constexpr uint8_t arrowTypeNormal = 1; +inline constexpr uint8_t arrowTypeFireball = 2; +inline constexpr uint8_t arrowTypeFireblast = 3; +inline constexpr uint8_t arrowTypeNukeshot = 4; +inline constexpr float arrowSpeedInTilesPer50ms = 2.0f; +inline constexpr int16_t arrowSpeedInPixelsPer50ms = 16; + +struct LevelArrow +{ + float getTileX() const { return position.x() / 16.0f; } + float getTileY() const { return position.y() / 16.0f; } + + PixelPosition startPosition; + PixelPosition position; + PixelPosition speed; + uint8_t direction; + int8_t type; + ScriptObject from; + + [[inline]] uint8_t getPacketFrom() const; + + [[inline]] void constructScriptParameters(); + string_map scriptParameters; +}; + +//---------------------------- + +inline uint8_t LevelArrow::getPacketFrom() const +{ + if (from.second == ScriptObjectType::PLAYER) + return (uint8_t)1; + return (uint8_t)0; +} + +inline void LevelArrow::constructScriptParameters() +{ + scriptParameters.try_emplace("x", set_temporary, "x", gameValueGetter([this]() { return position.x() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("y", set_temporary, "y", gameValueGetter([this]() { return position.y() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("dx", set_temporary, "dx", gameValueGetter([this]() { return speed.x() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("dy", set_temporary, "dy", gameValueGetter([this]() { return speed.y() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("dir", set_temporary, "dir", gameValueGetter(direction), GameValue::func_set{}); + scriptParameters.try_emplace("type", set_temporary, "type", gameValueGetter(type), GameValue::func_set{}); + scriptParameters.try_emplace("from", set_temporary, "from", + gameValueGetter([this]() + { + if (from.second == ScriptObjectType::PLAYER) + return 1.0; + return 0.0; + }), GameValue::func_set{}); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // LEVELARROW_H diff --git a/server/include/level/LevelBaddy.h b/server/include/level/LevelBaddy.h index 88f73533e..daf7abf95 100644 --- a/server/include/level/LevelBaddy.h +++ b/server/include/level/LevelBaddy.h @@ -1,93 +1,167 @@ -#ifndef TLEVELBADDY_H -#define TLEVELBADDY_H +#ifndef LEVELBADDY_H +#define LEVELBADDY_H +#include +#include #include +#include +#include #include #include -#include -#include -#include "BabyDI.h" -// Baddy props -enum +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Baddy types +enum class BaddyType : uint8_t { - BDPROP_ID = 0, - BDPROP_X = 1, - BDPROP_Y = 2, - BDPROP_TYPE = 3, - BDPROP_POWERIMAGE = 4, - BDPROP_MODE = 5, - BDPROP_ANI = 6, - BDPROP_DIR = 7, - BDPROP_VERSESIGHT = 8, - BDPROP_VERSEHURT = 9, - BDPROP_VERSEATTACK = 10, - BDPROP_COUNT + GRAYSOLDIER = 0, + BLUESOLDIER = 1, + REDSOLDIER = 2, + SHOOTINGSOLDIER = 3, + SWAMPSOLDIER = 4, + FROG = 5, + OCTOPUS = 6, + GOLDENWARRIOR = 7, + LIZARDON = 8, + DRAGON = 9, + COUNT }; +constexpr size_t BADDYTYPE_COUNT = static_cast(BaddyType::COUNT); -// Baddy modes -enum +/// @brief The names of the baddies. +inline constexpr std::array BaddyNames = { - BDMODE_WALK = 0, - BDMODE_LOOK = 1, - BDMODE_HUNT = 2, - BDMODE_HURT = 3, - BDMODE_BUMPED = 4, - BDMODE_DIE = 5, - BDMODE_SWAMPSHOT = 6, - BDMODE_HAREJUMP = 7, - BDMODE_OCTOSHOT = 8, - BDMODE_DEAD = 9, - BDMODE_COUNT + "graysoldier"sv, "bluesoldier"sv, "redsoldier"sv, "shootingsoldier"sv, "swampsoldier"sv, + "frog"sv, "octopus"sv, "goldenwarrior"sv, "lizardon"sv, "dragon"sv }; -class Server; +//---------------------------- + +/// @brief Baddy props +enum class BaddyProp : uint8_t +{ + ID = 0, + X = 1, + Y = 2, + TYPE = 3, + POWERIMAGE = 4, + MODE = 5, + ANI = 6, + DIR = 7, + VERSESIGHT = 8, + VERSEHURT = 9, + VERSEATTACK = 10, + COUNT +}; +constexpr size_t BADDYPROP_COUNT = static_cast(BaddyProp::COUNT); + +//---------------------------- + +/// @brief Baddy modes +enum class BaddyMode : uint8_t +{ + WALK = 0, + LOOK = 1, + HUNT = 2, + HURT = 3, + BUMPED = 4, + DIE = 5, + SWAMPSHOT = 6, + HAREJUMP = 7, + OCTOSHOT = 8, + DEAD = 9, + COUNT +}; +constexpr size_t BADDYMODE_COUNT = static_cast(BaddyMode::COUNT); + +//---------------------------- + class Level; +class Server; + class LevelBaddy { public: - LevelBaddy(const float pX, const float pY, const unsigned char pType, std::weak_ptr pLevel); + static BaddyType getBaddyTypeFromString(const std::string& type); + +public: + LevelBaddy(const LocalPixelPosition& position, BaddyType type, std::weak_ptr level); void reset(); - void dropItem(); - - // get functions - unsigned char getType() const { return m_type; } - char getId() const { return m_id; } - char getPower() const { return m_power; } - char getMode() const { return m_mode; } - char getAni() const { return m_ani; } - char getDir() const { return m_dir; } - float getX() const { return m_x; } - float getY() const { return m_y; } - float getStartX() const { return m_startX; } - float getStartY() const { return m_startY; } - CString getProp(const int propId, int clientVersion = CLVER_2_17) const; - CString getProps(int clientVersion = CLVER_2_17) const; - std::vector getVerses() const { return m_verses; }; - - // set functions - void setProps(CString& pProps); + void dropItem() const; + bool isAlive() const { return mode != BaddyMode::DEAD; } + bool canRespawn() const { return m_canRespawn; } + bool canBeReplaced() const { return !m_canRespawn && mode == BaddyMode::DEAD; } + +public: + CString getProp(BaddyProp propId) const; + CString getProps() const; + void setPropsFromPacket(CString& pProps); + +public: void setRespawn(const bool pRespawn) { m_canRespawn = pRespawn; } - void setId(const char pId) { m_id = pId; } + void setImage(std::string_view image); + [[inline]] void setLevel(LevelPtr level); - CTimeout timeout; +public: + float getTileX() const { return position.x() / 16.0f; } + float getTileY() const { return position.y() / 16.0f; } -private: - BabyDI_INJECT(Server, m_server); +public: + uint8_t id; + BaddyType type; + LocalPixelPosition position; + BaddyMode mode; + uint8_t power; + uint8_t animation; + uint8_t direction; + uint8_t headDirection; + std::string image; + std::vector verses; + + TimeoutGenerator timeout; +public: + [[inline]] void constructScriptParameters(); + string_map scriptParameters; + +private: + Server* m_server; std::weak_ptr m_level; - unsigned char m_type; - char m_id = 0; - char m_power, m_mode, m_ani, m_dir; - float m_x, m_y, m_startX, m_startY; - CString m_image; - std::vector m_verses; + LocalPixelPosition m_originalPosition; bool m_canRespawn = true; bool m_hasCustomImage = false; }; -using LevelBaddyPtr = std::unique_ptr; +//---------------------------- + +inline void LevelBaddy::setLevel(LevelPtr level) +{ + m_level = level; +} + +inline void LevelBaddy::constructScriptParameters() +{ + // TODO: headdir + scriptParameters.try_emplace("x", set_temporary, "x", gameValueGetter([this]() { return position.x() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("y", set_temporary, "y", gameValueGetter([this]() { return position.y() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("type", set_temporary, "type", gameValueGetter([this]() { return (double)type; }), GameValue::func_set{}); + scriptParameters.try_emplace("dir", set_temporary, "dir", gameValueGetter(direction), GameValue::func_set{}); + scriptParameters.try_emplace("headdir", set_temporary, "headdir", gameValueGetter(headDirection), GameValue::func_set{}); + scriptParameters.try_emplace("power", set_temporary, "power", gameValueGetter(power), GameValue::func_set{}); + scriptParameters.try_emplace("mode", set_temporary, "mode", gameValueGetter([this]() { return (double)mode; }), GameValue::func_set{}); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal -#endif // TLEVELBADDY_H +#endif // LEVELBADDY_H diff --git a/server/include/level/LevelBoardChange.h b/server/include/level/LevelBoardChange.h index 64a0eaa65..1b7246958 100644 --- a/server/include/level/LevelBoardChange.h +++ b/server/include/level/LevelBoardChange.h @@ -1,42 +1,57 @@ -#ifndef TLEVELBOARDCHANGE_H -#define TLEVELBOARDCHANGE_H +#ifndef LEVELBOARDCHANGE_H +#define LEVELBOARDCHANGE_H -#include -#include +#include +#include +#include +#include #include -#include + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class Level; +class Server; class LevelBoardChange { public: - // constructor - destructor - LevelBoardChange(const int pX, const int pY, const int pWidth, const int pHeight, - const CString& pTiles, const CString& pOldTiles, const int respawn = 15) - : m_x(pX), m_y(pY), m_width(pWidth), m_height(pHeight), - m_newTiles(pTiles), m_oldTiles(pOldTiles) { timeout.setTimeout(respawn); } - - // functions - CString getBoardStr() const; - void swapTiles(); + LevelBoardChange(std::shared_ptr level, const LocalWholeTileRectangleArea& area, const CString& tiles, const CString& oldTiles, std::chrono::seconds respawnTime = 15s); + LevelBoardChange(std::shared_ptr level, const MapPosition& mapPosition, const LocalWholeTileRectangleArea& area, const CString& tiles, const CString& oldTiles, std::chrono::seconds respawnTime = 15s); - // get private variables - int getX() const { return m_x; } - int getY() const { return m_y; } - int getWidth() const { return m_width; } - int getHeight() const { return m_height; } - CString getTiles() const { return m_newTiles; } - time_t getModTime() const { return m_modTime; } +public: + void update(const precise_clock::time_point& time); + void sendToPlayersOnLevel() const; - // set private variables - void setModTime(time_t ntime) { m_modTime = ntime; } +public: + CString getTiles() const { return m_newTiles; } + CString getPropsForSingleLevel() const; + CString getPropsForMapClassic() const; + //CString getPropsForMapNewMain() const; + void swapTiles(); + bool willRespawn() const { return m_timeout.isRunning(); } - CTimeout timeout; +public: + LocalWholeTileRectangleArea area; + uint8_t layer = 0; + clock::time_point modTime; private: - int m_x, m_y, m_width, m_height; + Server* m_server; + TimeoutGenerator m_timeout; + std::weak_ptr m_level; + std::optional m_mapPosition; CString m_newTiles, m_oldTiles; - time_t m_modTime = time(0); }; -#endif // TLEVELBOARDCHANGE_H +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // LEVELBOARDCHANGE_H diff --git a/server/include/level/LevelBomb.h b/server/include/level/LevelBomb.h new file mode 100644 index 000000000..79fbdcd4f --- /dev/null +++ b/server/include/level/LevelBomb.h @@ -0,0 +1,47 @@ +#ifndef LEVELBOMB_H +#define LEVELBOMB_H + +#include +#include + +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +struct LevelBomb +{ + float getTileX() const { return position.x() / 16.0f; } + float getTileY() const { return position.y() / 16.0f; } + + PixelPosition position; + uint8_t power; + ScriptObject owner; + TimeoutGenerator timeout; + + [[inline]] void constructScriptParameters(); + string_map scriptParameters; +}; + +//---------------------------- + +inline void LevelBomb::constructScriptParameters() +{ + scriptParameters.try_emplace("x", set_temporary, "x", gameValueGetter([this]() { return position.x() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("y", set_temporary, "y", gameValueGetter([this]() { return position.y() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("power", set_temporary, "power", gameValueGetter(power), GameValue::func_set{}); + scriptParameters.try_emplace("time", set_temporary, "time", + gameValueGetter([this]() { return std::chrono::duration_cast(timeout.getRemainingTime()).count(); }), + GameValue::func_set{}); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // LEVELBOMB_H diff --git a/server/include/level/LevelChest.h b/server/include/level/LevelChest.h index 39e5844d0..5e95b2d11 100644 --- a/server/include/level/LevelChest.h +++ b/server/include/level/LevelChest.h @@ -1,86 +1,24 @@ -#ifndef TLEVELCHEST_H -#define TLEVELCHEST_H +#ifndef LEVELCHEST_H +#define LEVELCHEST_H -#include -#include +#include -#include +#include +#include -#ifdef V8NPCSERVER - #include "scripting/interface/ScriptBindings.h" -#endif - -enum class LevelItemType; - -class LevelChest : public std::enable_shared_from_this +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal { -public: - LevelChest(char nx, char ny, LevelItemType itemIdx, char signIdx) - : m_itemIndex(itemIdx), m_signIndex(signIdx), m_x(nx), m_y(ny) - { - } - - LevelItemType getItemIndex() const - { - return m_itemIndex; - } - - int getSignIndex() const - { - return m_signIndex; - } - - int getX() const - { - return m_x; - } - - int getY() const - { - return m_y; - } - - void setItemIndex(int id) - { - m_itemIndex = (LevelItemType)id; - } +/////////////////////////////////////////////////////////////////////////////// - void setSignIndex(int id) - { - m_signIndex = id; - } - - void setX(int xVal = 0) - { - m_x = xVal; - } - - void setY(int yVal = 0) - { - m_y = yVal; - } - -#ifdef V8NPCSERVER - inline IScriptObject* getScriptObject() const - { - return m_scriptObject.get(); - } - - inline void setScriptObject(std::unique_ptr> object) - { - m_scriptObject = std::move(object); - } -#endif - -private: - LevelItemType m_itemIndex; - int m_signIndex, m_x, m_y; - -#ifdef V8NPCSERVER - std::unique_ptr> m_scriptObject; -#endif +struct LevelChest +{ + LocalWholeTilePosition position; + LevelItemType item; + uint8_t sign; }; -using LevelChestPtr = std::shared_ptr; +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal -#endif // TLEVELCHEST_H +#endif // LEVELCHEST_H diff --git a/server/include/level/LevelExplosion.h b/server/include/level/LevelExplosion.h new file mode 100644 index 000000000..2fdbc6c4f --- /dev/null +++ b/server/include/level/LevelExplosion.h @@ -0,0 +1,65 @@ +#ifndef LEVELEXPLOSION_H +#define LEVELEXPLOSION_H + +#include +#include + +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +/* Data: + +Indices of a power 2 explosion: + + 2 + 1 +4 3 0 7 8 + 5 + 6 + +Each explosion is 2x2 tiles in size and are tiled without overlap. + +*/ + +constexpr std::chrono::milliseconds ExplosionDuration = 300ms; + +struct LevelExplosion +{ + float getTileX() const { return position.x() / 16.0f; } + float getTileY() const { return position.y() / 16.0f; } + + PixelPosition position; + uint8_t power; + uint8_t direction; + ScriptObject from; + TimeoutGenerator timeout; + + [[inline]] void constructScriptParameters(); + string_map scriptParameters; +}; + +//---------------------------- + +inline void LevelExplosion::constructScriptParameters() +{ + scriptParameters.try_emplace("x", set_temporary, "x", gameValueGetter([this]() { return position.x() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("y", set_temporary, "y", gameValueGetter([this]() { return position.y() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("power", set_temporary, "power", gameValueGetter(power), GameValue::func_set{}); + scriptParameters.try_emplace("time", set_temporary, "time", + gameValueGetter([this]() { return std::chrono::duration_cast(timeout.getRemainingTime()).count(); }), + GameValue::func_set{}); + scriptParameters.try_emplace("dir", set_temporary, "dir", gameValueGetter(direction), GameValue::func_set{}); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // LEVELEXPLOSION_H diff --git a/server/include/level/LevelHorse.h b/server/include/level/LevelHorse.h index a0d99aaba..213596673 100644 --- a/server/include/level/LevelHorse.h +++ b/server/include/level/LevelHorse.h @@ -1,47 +1,61 @@ -#ifndef TLEVELHORSE_H -#define TLEVELHORSE_H +#ifndef LEVELHORSE_H +#define LEVELHORSE_H + +#include +#include #include -#include -class Server; -class LevelHorse +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal { -public: - LevelHorse(int horselife, const CString& pImage, float pX, float pY, char pDir = 0, char pBushes = 0) - : m_lifetime(horselife), m_image(pImage), m_x(pX), m_y(pY), m_dir(pDir), m_bushes(pBushes) - { - timeout.setTimeout(m_lifetime); - } +/////////////////////////////////////////////////////////////////////////////// - CString getHorseStr(); +constexpr uint8_t HORSETYPE_NORMAL = 0; +constexpr uint8_t HORSETYPE_BOAT = 1; - // get private variables - CString getImage() const { return m_image; } - float getX() const { return m_x; } - float getY() const { return m_y; } - char getDir() const { return m_dir; } - char getBushes() const { return m_bushes; } +struct LevelHorse +{ + float getTileX() const { return position.x() / 16.0f; } + float getTileY() const { return position.y() / 16.0f; } - CTimeout timeout; + CString getPacket() const + { + auto localPosition = toLocalPixelPosition(position); + char dir_bush = (bushes << 2) | (direction & 0x03); + return CString() >> (char)(localPosition.x() / 8) >> (char)(localPosition.y() / 8) >> (char)dir_bush << image; + } -private: - CString m_image; - CString m_horsePacket; - float m_x, m_y; - char m_dir, m_bushes; - int m_lifetime; + PixelPosition position; + std::string image; + uint8_t direction; + uint8_t bushes; + uint8_t type; + TimeoutGenerator timeout; + + [[inline]] void constructScriptParameters(); + string_map scriptParameters; }; -inline CString LevelHorse::getHorseStr() -{ - if (m_horsePacket.isEmpty()) - { - char dir_bush = (m_bushes << 2) | (m_dir & 0x03); - m_horsePacket = CString() << (char)(m_x * 2) >> (char)(m_y * 2) >> (char)dir_bush << m_image; - } +//---------------------------- - return m_horsePacket; +inline void LevelHorse::constructScriptParameters() +{ + scriptParameters.try_emplace("x", set_temporary, "x", gameValueGetter([this]() { return position.x() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("y", set_temporary, "y", gameValueGetter([this]() { return position.y() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("dir", set_temporary, "dir", gameValueGetter(direction), GameValue::func_set{}); + scriptParameters.try_emplace("bushes", set_temporary, "bushes", gameValueGetter(bushes), GameValue::func_set{}); + scriptParameters.try_emplace("bombs", set_temporary, "bombs", gameValueGetter([this]() { return 0.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("bombpower", set_temporary, "bombpower", gameValueGetter([this]() { return 0.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("type", set_temporary, "type", gameValueGetter([this]() { return static_cast(type); }), GameValue::func_set{}); } -#endif // TLEVELHORSE_H +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // LEVELHORSE_H diff --git a/server/include/level/LevelItem.h b/server/include/level/LevelItem.h index 3602de39e..966c82c81 100644 --- a/server/include/level/LevelItem.h +++ b/server/include/level/LevelItem.h @@ -1,10 +1,24 @@ -#ifndef TLEVELITEM_H -#define TLEVELITEM_H +#ifndef LEVELITEM_H +#define LEVELITEM_H -#include +#include +#include +#include +#include +#include +#include #include -#include + +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// enum class LevelItemType { @@ -37,44 +51,44 @@ enum class LevelItemType SPINATTACK = 24 }; -class Player; -class LevelItem +inline constexpr std::array ItemNames = { -public: - LevelItem(float pX, float pY, LevelItemType pItem) : m_x(pX), m_y(pY), m_item(pItem) - { - timeout.setTimeout(10); - } - - // Return the packet to be sent to the player. - CString getItemStr() const; - - // Get functions. - float getX() const { return m_x; } - float getY() const { return m_y; } - LevelItemType getItem() const { return m_item; } - time_t getModTime() const { return m_modTime; } + "greenrupee"sv, "bluerupee"sv, "redrupee"sv, "bombs"sv, "darts"sv, + "heart"sv, "glove1"sv, "bow"sv, "bomb"sv, "shield"sv, + "sword"sv, "fullheart"sv, "superbomb"sv, "battleaxe"sv, "goldensword"sv, + "mirrorshield"sv, "glove2"sv, "lizardshield"sv, "lizardsword"sv, "goldrupee"sv, + "fireball"sv, "fireblast"sv, "nukeshot"sv, "joltbomb"sv, "spinattack"sv +}; - CTimeout timeout; +/// @brief Level items disappear after 8.2 seconds; +constexpr clock::duration LevelItemTimeout = std::chrono::milliseconds(8200); - // Static functions. +class Player; +struct LevelItem +{ static LevelItemType getItemId(signed char itemId); static LevelItemType getItemId(const std::string& pItemName); static std::string getItemName(LevelItemType itemId); static CString getItemPlayerProp(LevelItemType itemType, Player* player); static CString getItemPlayerProp(const std::string& pItemName, Player* player); static constexpr auto getItemTypeId(LevelItemType val); - static bool isRupeeType(LevelItemType itemType); static uint16_t GetRupeeCount(LevelItemType type); -private: - float m_x; - float m_y; - LevelItemType m_item; - time_t m_modTime = time(0); + float getTileX() const { return position.x() / 16.0f; } + float getTileY() const { return position.y() / 16.0f; } + + PixelPosition position; + LevelItemType item; + clock::time_point modTime; + TimeoutGenerator timeout; + + [[inline]] void constructScriptParameters(); + string_map scriptParameters; }; +//---------------------------- + inline CString LevelItem::getItemPlayerProp(const std::string& pItemName, Player* player) { return getItemPlayerProp(LevelItem::getItemId(pItemName), player); @@ -107,4 +121,19 @@ inline bool LevelItem::isRupeeType(LevelItemType itemType) return GetRupeeCount(itemType) > 0; } -#endif // TLEVELITEM_H +//---------------------------- + +inline void LevelItem::constructScriptParameters() +{ + scriptParameters.try_emplace("x", set_temporary, "x", gameValueGetter([this]() { return position.x() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("y", set_temporary, "y", gameValueGetter([this]() { return position.y() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("type", set_temporary, "type", gameValueGetter([this]() { return (double)item; }), GameValue::func_set{}); + scriptParameters.try_emplace("time", set_temporary, "time", + gameValueGetter([this]() { return std::chrono::duration_cast(timeout.getRemainingTime()).count(); }), + GameValue::func_set{}); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // LEVELITEM_H diff --git a/server/include/level/LevelLink.h b/server/include/level/LevelLink.h index 2499dbd04..045c70d4b 100644 --- a/server/include/level/LevelLink.h +++ b/server/include/level/LevelLink.h @@ -1,144 +1,123 @@ -#ifndef TLEVELLINK_H -#define TLEVELLINK_H +#ifndef LEVELLINK_H +#define LEVELLINK_H -#include +#include +#include +#include +#include #include #include -#ifdef V8NPCSERVER - #include "scripting/interface/ScriptBindings.h" -#endif +#include +#include +#include -class LevelLink : public std::enable_shared_from_this +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class Server; + +class LevelLink { public: // constructor - destructor LevelLink() = default; LevelLink(const std::vector& pLink); + LevelLink(const Rectangle& coordinates, std::string_view destinationX, std::string_view destinationY, std::string_view destinationLevel); // functions CString getLinkStr() const; void parseLinkStr(const std::vector& pLink); - // get private variables - inline CString getNewLevel() const; - inline CString getNewX() const; - inline CString getNewY() const; - inline int getX() const; - inline int getY() const; - inline int getWidth() const; - inline int getHeight() const; - - // set private variables - inline void setNewLevel(const CString& _newLevel); - inline void setNewX(const CString& _newX); - inline void setNewY(const CString& _newY); - inline void setX(int posX = 0); - inline void setY(int posY = 0); - inline void setWidth(int _width = 0); - inline void setHeight(int _height = 0); - -#ifdef V8NPCSERVER - inline IScriptObject* getScriptObject() const - { - return m_scriptObject.get(); - } - - inline void setScriptObject(std::unique_ptr> object) - { - m_scriptObject = std::move(object); - } -#endif +public: + [[inline]] const Rectangle& getBoundingBox() const; + [[inline]] void setX(uint8_t posX = 0); + [[inline]] void setY(uint8_t posY = 0); + [[inline]] void setWidth(uint8_t width = 0); + [[inline]] void setHeight(uint8_t height = 0); -private: - CString m_newLevel, m_newX, m_newY; - int m_x = 0; - int m_y = 0; - int m_width = 0; - int m_height = 0; - -#ifdef V8NPCSERVER - std::unique_ptr> m_scriptObject; -#endif -}; +public: + LocalPixelPosition getDestinationForCharacter(Character& character, ScriptObject source) const; + [[inline]] const std::string& getDestinationLevel() const; + [[inline]] const std::string& getDestinationX() const; + [[inline]] const std::string& getDestinationY() const; + [[inline]] void setDestinationLevel(std::string_view level); + [[inline]] void setDestinationX(std::string_view newX); + [[inline]] void setDestinationY(std::string_view newY); -using LevelLinkPtr = std::shared_ptr; +public: + bool isProbableMapLink() const; -/* - LevelLink: Get Private Variables -*/ -inline CString LevelLink::getNewLevel() const -{ - return m_newLevel; -} +private: + Server* m_server = nullptr; + std::string m_destinationLevel, m_destinationX, m_destinationY; + Rectangle m_boundingBox; + bool m_constantX = false; + bool m_constantY = false; + std::array m_complex{false, false}; +}; -inline CString LevelLink::getNewX() const -{ - return m_newX; -} +//---------------------------- -inline CString LevelLink::getNewY() const +inline const std::string& LevelLink::getDestinationLevel() const { - return m_newY; + return m_destinationLevel; } -inline int LevelLink::getX() const +inline const std::string& LevelLink::getDestinationX() const { - return m_x; + return m_destinationX; } -inline int LevelLink::getY() const +inline const std::string& LevelLink::getDestinationY() const { - return m_y; + return m_destinationY; } -inline int LevelLink::getWidth() const +inline const Rectangle& LevelLink::getBoundingBox() const { - return m_width; + return m_boundingBox; } -inline int LevelLink::getHeight() const +inline void LevelLink::setDestinationLevel(std::string_view level) { - return m_height; + m_destinationLevel = level; } -/* - LevelLink: Set Private Variables -*/ -inline void LevelLink::setNewLevel(const CString& _newLevel) +inline void LevelLink::setDestinationX(std::string_view newX) { - m_newLevel = _newLevel; + m_destinationX = newX; } -inline void LevelLink::setNewX(const CString& _newX) +inline void LevelLink::setDestinationY(std::string_view newY) { - m_newX = _newX; + m_destinationY = newY; } -inline void LevelLink::setNewY(const CString& _newY) +inline void LevelLink::setX(uint8_t posX) { - m_newY = _newY; + m_boundingBox.position.x() = posX; } -inline void LevelLink::setX(int posX) +inline void LevelLink::setY(uint8_t posY) { - m_x = posX; + m_boundingBox.position.y() = posY; } -inline void LevelLink::setY(int posY) +inline void LevelLink::setWidth(uint8_t width) { - m_y = posY; + m_boundingBox.size.width() = width; } -inline void LevelLink::setWidth(int _width) +inline void LevelLink::setHeight(uint8_t height) { - m_width = _width; + m_boundingBox.size.height() = height; } -inline void LevelLink::setHeight(int _height) -{ - m_height = _height; -} +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal -#endif // TLEVELLINK_H +#endif // LEVELLINK_H diff --git a/server/include/level/LevelShoot.h b/server/include/level/LevelShoot.h new file mode 100644 index 000000000..fc6556d65 --- /dev/null +++ b/server/include/level/LevelShoot.h @@ -0,0 +1,84 @@ +#ifndef LEVELSHOOT_H +#define LEVELSHOOT_H + +#include +#include +#include +#include + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +/* +shoot x,y,z,angle,zangle,power,gani,ganiparams; + +angle: east = 0, north = pi/2, west = pi, south = 3*pi/2 +zangle: horizontal = 0, up = pi/2 +power: 0 = old projectile (no gravity, 20 tiles per second), otherwise + +z <= 3: + Collides with walls and NPCs + +When landing/hitting: + spawned from client: actionprojectile: #p(0) = x, #p(1) = y, #p(3+) = shootparams + spawned from server: actionsprojectile: #p(0) = x, #p(1) = y, #p(3+) = shootparams + triggers on players/npcs at the coordinate, and on the control-npc + clientside weapons: actionprojectile2 + serverside weapons: ??? + +On creation: + horzspeed = cos(zangle) * (power * 44) + vertspeed = sin(zangle) * (power * 44) + +Every second (but done spread out every 0.05ms): + vertspeed = vertspeed - gravity + newx = x + cos(angle) * horzspeed + newy = y - sin(angle) * horzspeed + newz = z + vertspeed +*/ + +struct LevelShoot +{ + TilePosition position; + float angle = 0.0f; + float zangle = 0.0f; + uint8_t powerIn44Pixels = 0; + std::string gani; + + float gravity = 2.0; + TilePosition movementPerFrame; + std::vector shootParams; + ScriptObject from; + + [[inline]] void calculateSpeeds(); + [[inline]] void move(); +}; + +//---------------------------- + +inline void LevelShoot::calculateSpeeds() +{ + float horizSpeed = std::cos(zangle) * (powerIn44Pixels / 44.0f); + float vertSpeed = std::sin(zangle) * (powerIn44Pixels / 44.0f); + movementPerFrame.x() = std::cos(angle) * horizSpeed; + movementPerFrame.y() = std::sin(angle) * horizSpeed; + movementPerFrame.z() = vertSpeed; +} + +inline void LevelShoot::move() +{ + movementPerFrame.z() -= gravity * 0.05f; + position.x() += movementPerFrame.x(); + position.y() -= movementPerFrame.y(); + position.z() += movementPerFrame.z(); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // LEVELSHOOT_H diff --git a/server/include/level/LevelSign.h b/server/include/level/LevelSign.h index e829e56bf..c5e8b77a0 100644 --- a/server/include/level/LevelSign.h +++ b/server/include/level/LevelSign.h @@ -1,58 +1,48 @@ -#ifndef TLEVELSIGN_H -#define TLEVELSIGN_H +#ifndef LEVELSIGN_H +#define LEVELSIGN_H -#include -#include +#include +#include #include -#ifdef V8NPCSERVER - #include "scripting/interface/ScriptBindings.h" -#endif +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// class Player; -class LevelSign : public std::enable_shared_from_this +struct LevelSign { -public: - LevelSign(const int pX, const int pY, const CString& pSign, bool encoded = false); - - // functions - CString getSignStr(Player* pPlayer = 0) const; - - // get private variables - int getX() const { return m_x; } - int getY() const { return m_y; } - CString getText() const { return m_text; } - CString getUText() const { return m_unformattedText; } - - void setX(int value = 0) { m_x = value; } - void setY(int value = 0) { m_y = value; } - void setText(const CString& value); - void setUText(const CString& value); - -#ifdef V8NPCSERVER - inline IScriptObject* getScriptObject() const - { - return m_scriptObject.get(); - } - - inline void setScriptObject(std::unique_ptr> object) - { - m_scriptObject = std::move(object); - } -#endif - -private: - int m_x, m_y; - CString m_text; - CString m_unformattedText; - -#ifdef V8NPCSERVER - std::unique_ptr> m_scriptObject; -#endif + LevelSign(const LocalWholeTilePosition& position, std::string_view signText, bool signTextIsEncoded = false); + CString getSignPacket(Player* player = nullptr) const; + void setText(std::string_view signText, bool signTextIsEncoded = false); + + float getTileX() const { return (float)position.x(); } + float getTileY() const { return (float)position.y(); } + + LocalWholeTilePosition position; + std::string text; + std::string encodedText; + + [[inline]] void constructScriptParameters(); + string_map scriptParameters; }; -using LevelSignPtr = std::shared_ptr; +//---------------------------- + +inline void LevelSign::constructScriptParameters() +{ + scriptParameters.try_emplace("x", set_temporary, "x", gameValueGetter([this]() { return position.x() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("y", set_temporary, "y", gameValueGetter([this]() { return position.y() / 16.0; }), GameValue::func_set{}); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal -#endif // TLEVELSIGN_H +#endif // LEVELSIGN_H diff --git a/server/include/level/LevelTerrain.h b/server/include/level/LevelTerrain.h new file mode 100644 index 000000000..22ca3ced6 --- /dev/null +++ b/server/include/level/LevelTerrain.h @@ -0,0 +1,44 @@ +#ifndef LEVELTERRAIN_H +#define LEVELTERRAIN_H + +#include +#include + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +struct LevelTerrain +{ + uint32_t levelSeed = 0; + double levelHeight = 4.0; + double levelChaos = 0.6; + std::vector heightmap; + std::vector levelHeightOverrides; +}; + +struct LevelTerrainWorker +{ + double levelHeight = 4.0; + double levelChaos = 0.6; + DelphiRandomDeviceReal random; + std::vector* heightmap = nullptr; + std::vector* levelHeightOverrides = nullptr; +}; + +void generateTerrain(LevelTerrain& levelTerrain, const MapTerrain& mapTerrain, const Position& mapPosition, const Dimension& gridDimension); +void floodFillHeights(LevelTerrainWorker& terrain, uint32_t rowWidth, double chaos, double height, size_t bottom, size_t right, size_t top, size_t left); +void floodFillQuadrant(LevelTerrainWorker& terrain, uint32_t rowWidth, double chaos, double height, size_t bottom, size_t right, size_t top, size_t left); + +void applyHeightOverrides(LevelTerrain& levelTerrain); +void applyHeightOverrideOnRow(LevelTerrain& levelTerrain, double deltaHeight, size_t tileX, size_t tileY); + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // LEVELTERRAIN_H diff --git a/server/include/level/LevelTileTypes.h b/server/include/level/LevelTileTypes.h new file mode 100644 index 000000000..43131a523 --- /dev/null +++ b/server/include/level/LevelTileTypes.h @@ -0,0 +1,569 @@ +#ifndef LEVELTILETYPES_H +#define LEVELTILETYPES_H + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::tileset +{ +/////////////////////////////////////////////////////////////////////////////// + +enum class TilesetType : uint8_t +{ + CLASSIC = 0, + NEWFORMAT = 1, + TERRAIN = 5 +}; + +inline constexpr std::array DefaultImages = +{ + "pics1.png", + "tiles_inside.png", + "picso.png" +}; + +enum class TileType : uint8_t +{ + NONBLOCKING = 0, + HURT_UNDERGROUND = 2, + CHAIR = 3, + BED_UPPER = 4, + BED_LOWER = 5, + SWAMP = 6, + LAVA_SWAMP = 7, + NEAR_WATER = 8, + WATER = 11, + LAVA = 12, + THROW_THROUGH = 20, + JUMP_STONE = 21, + BLOCKING = 22 +}; + +inline constexpr std::array Type0 = +{ + 0, 0, 20, 20, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, + 0, 20, 20, 20, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, + 20, 20, 20, 20, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, + 0, 0, 20, 20, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, + 22, 22, 22, 22, 0, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, + 20, 22, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, + 20, 22, 0, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, + 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, + 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, + 22, 22, 22, 22, 22, 22, 0, 22, 0, 0, 0, 0, 22, 0, 22, 22, + 22, 22, 22, 22, 22, 0, 0, 22, 22, 21, 21, 22, 22, 0, 21, 21, + 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, 22, 22, + 22, 22, 22, 22, 21, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 21, 22, 0, 0, 22, 22, 22, 22, 0, 0, 22, 22, + 0, 22, 22, 22, 22, 0, 0, 0, 22, 22, 22, 22, 0, 0, 22, 22, + 0, 22, 22, 22, 22, 0, 22, 22, 11, 11, 22, 0, 0, 0, 11, 11, + 0, 21, 22, 22, 22, 22, 22, 22, 11, 11, 22, 0, 0, 0, 11, 11, + 0, 21, 22, 22, 22, 22, 11, 11, 11, 22, 22, 0, 0, 0, 22, 22, + 22, 11, 11, 11, 0, 22, 22, 22, 0, 22, 22, 0, 0, 0, 0, 0, + 0, 0, 11, 11, 11, 22, 20, 20, 22, 22, 20, 20, 22, 22, 22, 22, + 0, 0, 11, 22, 22, 22, 20, 20, 22, 22, 20, 20, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 22, 0, 0, 22, + 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, 8, 11, + 22, 22, 22, 22, 22, 22, 0, 0, 11, 11, 22, 11, 22, 22, 0, 0, + 22, 22, 22, 22, 6, 6, 0, 0, 11, 22, 22, 11, 22, 22, 22, 22, + 22, 22, 22, 22, 6, 6, 0, 0, 11, 11, 22, 0, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 11, 11, 22, 22, 0, 11, 11, 22, + 22, 22, 22, 22, 0, 22, 22, 0, 11, 11, 0, 22, 22, 11, 11, 22, + 22, 22, 22, 22, 0, 11, 11, 0, 6, 6, 0, 22, 22, 0, 0, 0, + 22, 22, 22, 22, 8, 8, 0, 0, 6, 6, 11, 22, 22, 22, 22, 0, + 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 20, 20, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, + 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, + 20, 20, 22, 21, 11, 11, 11, 22, 22, 0, 22, 22, 22, 0, 11, 11, + 20, 20, 22, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 11, 11, + 20, 20, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 20, 20, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 22, 0, 0, 22, 22, 22, + 22, 22, 22, 22, 22, 0, 0, 0, 0, 3, 3, 22, 20, 20, 0, 0, + 22, 22, 22, 22, 22, 0, 0, 0, 0, 3, 3, 22, 20, 20, 22, 22, + 22, 0, 0, 22, 4, 4, 4, 4, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 0, 0, 22, 5, 5, 5, 5, 22, 22, 22, 0, 22, 22, 22, 22, + 22, 3, 3, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 3, 3, 22, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 20, 20, 0, + 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 20, 20, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, + 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, + 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 0, 0, 22, 0, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, + 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, 22, 11, 0, 0, + 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 11, 11, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 20, 20, + 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, 20, 20, + 0, 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 20, 20, 0, 0, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 20, 20, 0, 0, 22, 22, 22, + 22, 0, 22, 22, 22, 0, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 22, 22, 22, 22, 22, + 0, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 0, + 0, 22, 22, 22, 22, 22, 22, 0, 22, 0, 0, 22, 22, 22, 22, 22, + 0, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 0, + 0, 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 0, + 0, 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 0, + 0, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 0, 0, 0, 22, + 0, 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 0, 0, 0, 0, 22, + 0, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 0, 0, 0, 22, + 0, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 0, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 0, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 20, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 0, 20, 20, 22, 0, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 20, 20, 22, 22, 0, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 22, 22, 0, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 0, + 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 22, 22, 22, 22, 0, + 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 0, 22, 22, 22, 22, 0, + 22, 22, 22, 22, 20, 20, 3, 3, 3, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 20, 20, 3, 3, 3, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, 0, 22, 22, 0, + 0, 0, 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 0, 22, 22, 0, + 0, 0, 22, 22, 22, 22, 0, 0, 20, 20, 0, 0, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 20, 20, 0, 0, 22, 22, 22, 22, + 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, + 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, 22, + 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, 0, 0, 22, 22, 22, 22, + 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, 0, 0, 22, 22, 22, 22, + 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, 0, 0, 22, 22, 22, 22, + 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, 0, 0, 22, 22, 22, 22, + 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, 0, 0, 22, 22, 22, 22, + 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, + 20, 20, 0, 22, 22, 22, 22, 22, 22, 0, 0, 20, 20, 22, 22, 0, + 20, 20, 0, 22, 22, 22, 22, 22, 22, 0, 0, 20, 20, 22, 22, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 22, 22, 22, 0, 0, 0, 6, 6, 0, 0, 0, 0, 22, 22, + 22, 22, 22, 22, 22, 0, 8, 8, 6, 6, 0, 0, 0, 0, 22, 22, + 22, 22, 22, 22, 22, 22, 8, 8, 0, 0, 0, 0, 0, 0, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 22, 22, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 22, 0, 0, 0, 0, + 22, 0, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, + 22, 0, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 20, 20, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 20, 20, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 20, 20, + 22, 22, 22, 22, 22, 22, 0, 22, 22, 0, 22, 22, 22, 22, 20, 20, + 22, 22, 22, 22, 22, 22, 0, 0, 22, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 0, 0, 20, 20, 3, 3, 3, 3, 0, 22, 22, 22, 22, 22, 22, 0, + 0, 20, 20, 20, 3, 3, 3, 3, 22, 22, 22, 22, 22, 22, 22, 22, + 20, 20, 20, 20, 3, 3, 3, 3, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 20, 20, 3, 3, 3, 3, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, + 20, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, + 20, 22, 0, 22, 22, 22, 22, 22, 0, 22, 0, 0, 0, 0, 22, 0, + 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 0, + 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 0, 22, 22, + 0, 0, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 0, 0, 22, 22, + 0, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 22, 22, 22, 0, 22, 22, 12, 12, 22, 0, 0, 0, 12, 12, + 0, 22, 22, 22, 22, 22, 22, 22, 12, 12, 22, 0, 0, 0, 12, 12, + 0, 22, 22, 22, 22, 22, 12, 12, 12, 22, 22, 0, 0, 0, 22, 22, + 22, 12, 12, 12, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, + 0, 0, 12, 12, 12, 22, 20, 20, 22, 22, 20, 20, 22, 22, 22, 22, + 0, 0, 12, 22, 22, 22, 20, 20, 22, 22, 20, 20, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, 0, 12, + 22, 22, 22, 22, 22, 22, 0, 0, 12, 12, 22, 12, 22, 22, 22, 22, + 0, 0, 0, 0, 7, 7, 0, 0, 12, 22, 22, 12, 22, 22, 22, 22, + 0, 0, 0, 0, 7, 7, 0, 0, 12, 12, 22, 0, 22, 22, 22, 22, + 0, 0, 0, 0, 0, 0, 0, 0, 12, 12, 22, 22, 22, 12, 12, 22, + 0, 0, 0, 0, 0, 22, 22, 0, 12, 12, 0, 22, 22, 12, 12, 22, + 0, 0, 22, 22, 0, 12, 12, 0, 7, 7, 0, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 7, 7, 12, 22, 22, 22, 22, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, + 22, 22, 12, 12, 12, 22, 22, 22, 12, 12, 12, 22, 22, 22, 22, 22, + 22, 22, 12, 12, 12, 12, 12, 12, 12, 12, 22, 22, 22, 22, 22, 22, + 12, 12, 12, 12, 12, 12, 12, 12, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 20, 20, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, + 0, 0, 20, 20, 22, 20, 20, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 0, 22, 20, 20, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 0, 22, 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 0, 22, 22, 22, 22, 12, 12, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 0, 22, 22, 22, 22, 12, 12, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 12, 12, 22, 22, 12, 12, 0, 0, 0, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 20, 20, 20, 20, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 0, 0, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 0, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 0, 20, 20, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 20, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 20, 0, 22, 20, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 20, 0, 22, 20, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 22, 22, 20, 0, 22, 0, 22, 22, 22, 22, 22, + 22, 22, 0, 0, 0, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 0, 0, 22, 22, 22, 20, 0, 0, 20, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 20, 0, 0, 20, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 20, 0, 0, 20, 22, 22, 8, 8, 8, 8, + 22, 22, 22, 22, 22, 22, 20, 3, 3, 20, 22, 22, 8, 8, 8, 8, + 22, 22, 22, 22, 22, 0, 20, 3, 3, 20, 22, 22, 8, 8, 8, 22, + 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 8, 8, 8, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 22, 8, 8, 8, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, + 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, + 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, + 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 0, + 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 0, 0, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 0, 22, + 22, 22, 0, 0, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 0, 0, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 3, 3, 3, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 3, 3, 3, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 0, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 8, 22, 8, 8, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 8, 22, 8, 8, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 21, 22, 22, 21, + 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 21, 21, 21, 21, + 0, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 21, 21, 22, + 22, 0, 0, 22, 22, 0, 0, 22, 22, 0, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 22, 22, 0, 0, 22, 22, 0, 22, 22, 22, 22, 22, 22 +}; + +/////////////////////////////////////////////////////////////////////////////// + +inline constexpr std::array Type1 = +{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 6, 6, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 21, 21, 21, 21, 21, 21, 21, 21, + 22, 22, 22, 22, 22, 22, 22, 22, 21, 21, 21, 21, 21, 21, 21, 21, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22 +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::constants + +#endif // LEVELTILETYPES_H diff --git a/server/include/level/LevelTiles.h b/server/include/level/LevelTiles.h index 443488013..88abf2d4a 100644 --- a/server/include/level/LevelTiles.h +++ b/server/include/level/LevelTiles.h @@ -1,21 +1,167 @@ -#ifndef GS2EMU_TLEVELTILES_H -#define GS2EMU_TLEVELTILES_H +#ifndef LEVELTILES_H +#define LEVELTILES_H -#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +namespace constants +{ +inline constexpr uint16_t EmptyTileInBase = 0x0; +inline constexpr uint16_t EmptyTileInLayer = 0xFFFF; +inline constexpr uint16_t BasicGrassTilePics1 = 511; + +inline constexpr Dimension LevelPartSizeInTiles = Dimension{ 64, 64 }; +inline constexpr size_t LevelPartTileCount = static_cast(LevelPartSizeInTiles.width()) * static_cast(LevelPartSizeInTiles.height()); +} // end namespace constants + +/// @brief Gets the tile index at a specific local whole tile position. +/// @param position The local whole tile position. +/// @return The tile index. +[[nodiscard]] inline constexpr size_t GetTileIndexAtPosition(const LocalWholeTilePosition& position) noexcept +{ + return static_cast(position.y()) * 64 + static_cast(position.x()); +} + +/// @brief Contains the tile data for a specific part of a level on a map. class LevelTiles { public: - LevelTiles(short fillTile = 0x00) - { - memset(m_tiles, fillTile, sizeof(m_tiles)); - } + using TileArray = std::array; - short& operator[](uint32_t index) { return m_tiles[index]; } - explicit operator char*() const { return (char*)m_tiles; }; +public: + [[inline]] void reset(); + +public: + [[inline]] std::generator getUsedTileLayers() const noexcept; + [[inline]] TileArray* getOrCreateLayer(uint8_t layer) noexcept; + [[inline]] std::optional getLayer(uint8_t layer) noexcept; + [[inline]] std::optional getLayer(uint8_t layer) const noexcept; + +public: + [[inline]] void writeTiles(const LocalWholeTilePosition& position, uint8_t width, std::span sourceTiles, uint8_t layer = 0) noexcept; + +public: + [[inline]] void writeLayerToPacket(uint8_t layer, CString& packet) const noexcept; + [[inline]] void writeLayerToPacket(uint8_t layer, const LocalWholeTilePosition& position, uint8_t width, CString& packet) const noexcept; + +public: + uint16_t BaseLayerEmptyTile = constants::EmptyTileInBase; private: - short m_tiles[4096]; + std::map m_tiles; }; -#endif //GS2EMU_TLEVELTILES_H +//---------------------------- + +inline void LevelTiles::reset() +{ + m_tiles.clear(); +} + +inline std::generator LevelTiles::getUsedTileLayers() const noexcept +{ + for (const auto& [layer, tiles] : m_tiles) + co_yield layer; +} + +inline LevelTiles::TileArray* LevelTiles::getOrCreateLayer(uint8_t layer) noexcept +{ + auto it = m_tiles.find(layer); + if (it == m_tiles.end()) + { + m_tiles[layer] = TileArray{}; + m_tiles[layer].fill(layer == 0 ? BaseLayerEmptyTile : constants::EmptyTileInLayer); + return &m_tiles[layer]; + } + return &it->second; +} + +inline std::optional LevelTiles::getLayer(uint8_t layer) noexcept +{ + auto it = m_tiles.find(layer); + if (it != m_tiles.end()) + return &it->second; + return std::nullopt; +} + +inline std::optional LevelTiles::getLayer(uint8_t layer) const noexcept +{ + auto it = m_tiles.find(layer); + if (it != m_tiles.end()) + return &it->second; + return std::nullopt; +} + +inline void LevelTiles::writeTiles(const LocalWholeTilePosition& position, uint8_t width, std::span sourceTiles, uint8_t layer) noexcept +{ + auto it = m_tiles.find(layer); + if (it == m_tiles.end()) + { + m_tiles[layer] = TileArray{}; + m_tiles[layer].fill(layer == 0 ? BaseLayerEmptyTile : constants::EmptyTileInLayer); + } + + auto& tiles = m_tiles[layer]; + size_t startIndex = GetTileIndexAtPosition(position); + for (size_t y = 0; y < width; ++y) + { + for (size_t x = 0; x < width; ++x) + { + tiles[startIndex + (y * 64) + x] = sourceTiles[y * width + x]; + } + } +} + +inline void LevelTiles::writeLayerToPacket(uint8_t layer, CString& packet) const noexcept +{ + auto it = m_tiles.find(layer); + if (it == m_tiles.end()) + { + for (int i = 0; i < 4096; ++i) + packet.writeShort(layer == 0 ? BaseLayerEmptyTile : constants::EmptyTileInLayer); + return; + } + + const TileArray& tiles = it->second; + packet.write(reinterpret_cast(tiles.data()), sizeof(short) * 4096); +} + +inline void LevelTiles::writeLayerToPacket(uint8_t layer, const LocalWholeTilePosition& position, uint8_t width, CString& packet) const noexcept +{ + auto it = m_tiles.find(layer); + if (it == m_tiles.end()) + { + for (size_t y = 0; y < width; ++y) + { + for (size_t x = 0; x < width; ++x) + packet.writeShort(layer == 0 ? BaseLayerEmptyTile : constants::EmptyTileInLayer); + } + return; + } + + const TileArray& tiles = it->second; + size_t startIndex = GetTileIndexAtPosition(position); + for (size_t y = 0; y < width; ++y) + { + for (size_t x = 0; x < width; ++x) + packet.writeShort(tiles[startIndex + (y * 64) + x]); + } +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // LEVELTILES_H diff --git a/server/include/level/Map.h b/server/include/level/Map.h index 7a7764b1e..3fc5ee845 100644 --- a/server/include/level/Map.h +++ b/server/include/level/Map.h @@ -1,14 +1,30 @@ -#ifndef TGMAP_H -#define TGMAP_H - -#include -#include +#ifndef MAP_H +#define MAP_H + +#include +#include +#include +#include +#include #include -#include +#include #include -#include -#include "BabyDI.h" +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +struct is_bigmap_t { explicit is_bigmap_t() = default; }; +inline constexpr is_bigmap_t is_bigmap{}; + +struct is_gmap_t { explicit is_gmap_t() = default; }; +inline constexpr is_gmap_t is_gmap{}; enum class MapType { @@ -16,69 +32,117 @@ enum class MapType GMAP = 1, }; -struct MapLevel +class Server; +class StaticLevelData; + +struct MapTerrain { - MapLevel() = default; - MapLevel(int x, int y) : mapx(x), mapy(y) {} - MapLevel(const MapLevel& level) - { - mapx = level.mapx; - mapy = level.mapy; - } - - MapLevel& operator=(const MapLevel& level) - { - mapx = level.mapx; - mapy = level.mapy; - return *this; - } - - int mapx = -1; - int mapy = -1; -}; + /// @brief Seed for the whole map. + uint32_t mapSeed = 0; -class Server; + /// @brief The base terrain height for the map. + /// + /// Not used outside of initial map generation. + /// If evenBorders is false, the four corners of the map are set to the base height. + /// If evenBorders is true, the whole outline of the map is set to the base height. + double heightBase = 0; + + /// @brief If true, the whole outline of the map is set to the base height. + /// + /// Not used outside of initial map generation. + bool evenBorders = false; + + /// @brief Possible height variation. + double heightDeviation = 65.0; + + /// @brief Dampening multiplier for the height value as the vertices get generated. + double mapChaos = 0.6; + + /// @brief Base level height. + /// + /// Originally calculated with: pow(mapChaos, (width / 2.0)) * mapTerrainHeight + double levelHeightDeviation = 4.0; + + /// @brief Dampening multiplier for the height value as the level vertices get generated. + double levelChaos = 0.6; + + /// @brief A vector containing seed values for levels. + std::vector levelSeeds; + + std::vector gridBorderTileHeightsXAxis; + std::vector gridBorderTileHeightsYAxis; +}; class Map { public: - Map(MapType pType, bool pGroupMap = false); + Map(is_bigmap_t, const std::filesystem::path& fileName); + Map(is_gmap_t, const std::filesystem::path& fileName); - bool load(const CString& filename); +public: void loadMapLevels() const; + void setLevelDataLoaded(std::shared_ptr level); - bool isLevelOnMap(const std::string& level, int& mx, int& my) const; - const std::string& getLevelAt(int mx, int my) const; - //int getLevelX(const std::string& level) const; - //int getLevelY(const std::string& level) const; +public: + bool hasLevel(std::string_view levelName) const; + std::optional getLevelPosition(std::string_view levelName) const; + std::string getLevelNameAt(int x, int y) const; + std::shared_ptr getLevelDataAt(int x, int y) const; + std::shared_ptr getLevelDataAt(const PixelPosition& globalPosition) const; + std::generator, MapPosition>> getLevelDataInRange(const TilePosition& position, int syncTilesX, int syncTilesY) const noexcept; + std::generator, MapPosition>> getLevelDataInRectangle(const PixelRectangleArea& area) const noexcept; + std::generator, MapPosition>> getAllLevelData() const noexcept; - const std::string& getMapName() const { return m_mapName; } - MapType getType() const { return m_type; } - size_t getWidth() const { return m_width; } - size_t getHeight() const { return m_height; } - bool isBigMap() const { return m_type == MapType::BIGMAP; } - bool isGmap() const { return m_type == MapType::GMAP; } - bool isGroupMap() const { return m_groupMap; } +public: + [[inline]] std::string getMapName() const noexcept; + [[inline]] bool isGmap() const noexcept; + [[inline]] bool isBigMap() const noexcept; + [[inline]] bool hasTerrain() const noexcept; + +public: + const MapType mapType; + const std::filesystem::path fileName; + const std::string mapImage; + const std::string miniMapImage; + const Dimension size; + const bool keepAllLevelsLoaded = false; + const string_map levels; + const string_set levelsToKeepInMemory; + const MapTerrain terrain; private: - bool loadBigMap(const CString& pFileName); - bool loadGMap(const CString& pFileName); - - BabyDI_INJECT(Server, m_server); - - MapType m_type; - time_t m_modTime = 0; - size_t m_width = 0; - size_t m_height = 0; - bool m_groupMap; - bool m_loadFullMap = false; - std::string m_mapName; - std::string m_mapImage; - std::string m_miniMapImage; - - std::unordered_map m_levels; - std::vector m_levelList; - std::vector m_preloadLevelList; + void forceSetLevelDataLoaded(std::shared_ptr level) const noexcept; + std::shared_ptr getLevelDataPtr(std::string_view levelName, std::weak_ptr levelPtr) const noexcept; + +private: + Server* m_server; + mutable std::vector> levelDataByPosition; + mutable string_map> levelDataByName; }; -#endif +//---------------------------- + +inline std::string Map::getMapName() const noexcept +{ + return fs::getANSIFileName(fileName); +} + +inline bool Map::isGmap() const noexcept +{ + return mapType == MapType::GMAP; +} + +inline bool Map::isBigMap() const noexcept +{ + return mapType == MapType::BIGMAP; +} + +inline bool Map::hasTerrain() const noexcept +{ + return !terrain.gridBorderTileHeightsXAxis.empty(); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // MAP_H diff --git a/server/include/level/tiletypes.h b/server/include/level/tiletypes.h deleted file mode 100644 index 2fcf8a903..000000000 --- a/server/include/level/tiletypes.h +++ /dev/null @@ -1,349 +0,0 @@ -#ifndef GS2EMU_TILETYPES_H -#define GS2EMU_TILETYPES_H -static const unsigned char tiletypes[] = { - 0x00, 0x00, 0x14, 0x14, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x00, 0x14, 0x14, 0x14, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x14, 0x14, 0x14, 0x14, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x00, 0x00, 0x14, 0x14, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x14, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x14, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x00, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, - 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x00, 0x00, 0x16, 0x16, 0x15, 0x15, 0x16, 0x16, 0x00, 0x15, 0x15, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x15, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x15, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, - 0x0b, 0x0b, 0x16, 0x00, 0x00, 0x00, 0x0b, 0x0b, 0x00, 0x15, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x0b, 0x0b, 0x16, 0x00, 0x00, 0x00, 0x0b, 0x0b, - 0x00, 0x15, 0x16, 0x16, 0x16, 0x16, 0x0b, 0x0b, 0x0b, 0x16, 0x16, 0x00, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x0b, 0x0b, 0x0b, 0x00, 0x16, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x0b, - 0x0b, 0x16, 0x14, 0x14, 0x16, 0x16, 0x14, 0x14, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x0b, 0x16, 0x16, 0x16, 0x14, 0x14, 0x16, 0x16, 0x14, 0x14, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x08, 0x0b, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, - 0x0b, 0x0b, 0x16, 0x0b, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x06, 0x06, 0x00, 0x00, 0x0b, 0x16, 0x16, 0x0b, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x06, 0x06, 0x00, 0x00, 0x0b, 0x0b, 0x16, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, - 0x0b, 0x0b, 0x16, 0x16, 0x00, 0x0b, 0x0b, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x00, 0x0b, 0x0b, 0x00, 0x16, 0x16, 0x0b, 0x0b, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x0b, 0x0b, 0x00, 0x06, 0x06, 0x00, 0x16, - 0x16, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x08, 0x08, 0x00, 0x00, - 0x06, 0x06, 0x0b, 0x16, 0x16, 0x16, 0x16, 0x00, 0x14, 0x14, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x14, 0x14, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x16, 0x00, 0x00, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x14, 0x14, 0x16, 0x15, - 0x0b, 0x0b, 0x0b, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x00, 0x0b, 0x0b, - 0x14, 0x14, 0x16, 0x15, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x0b, 0x0b, 0x14, 0x14, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x14, 0x14, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x16, - 0x14, 0x14, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, - 0x00, 0x03, 0x03, 0x16, 0x14, 0x14, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, - 0x04, 0x04, 0x04, 0x04, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x00, 0x00, 0x16, 0x05, 0x05, 0x05, 0x05, 0x16, 0x16, 0x16, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x03, 0x03, 0x16, 0x16, 0x16, 0x00, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x03, 0x03, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x14, 0x14, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x14, 0x14, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x00, 0x00, - 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x0b, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x0b, 0x0b, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x14, 0x14, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x14, 0x14, - 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x14, 0x14, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x14, 0x14, 0x00, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x00, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x00, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x00, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x14, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x14, 0x14, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x14, 0x14, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x00, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x14, 0x14, 0x03, 0x03, - 0x03, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x14, 0x14, 0x03, 0x03, 0x03, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x00, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x14, 0x14, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x14, 0x14, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x14, 0x14, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x14, - 0x14, 0x16, 0x16, 0x00, 0x14, 0x14, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x00, 0x00, 0x14, 0x14, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x00, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x08, 0x08, 0x06, 0x06, 0x00, 0x00, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x08, 0x08, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, - 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x00, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x14, 0x14, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x14, 0x14, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x14, 0x14, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x14, 0x14, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x14, 0x14, 0x03, 0x03, 0x03, 0x03, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x14, 0x14, 0x14, - 0x03, 0x03, 0x03, 0x03, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x14, 0x14, 0x14, 0x14, 0x03, 0x03, 0x03, 0x03, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x14, 0x14, 0x03, 0x03, 0x03, 0x03, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, - 0x00, 0x00, 0x16, 0x16, 0x14, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x14, 0x16, 0x00, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, - 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, - 0x00, 0x00, 0x16, 0x00, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x00, 0x16, 0x16, 0x0c, 0x0c, 0x16, 0x00, 0x00, 0x00, 0x0c, 0x0c, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x0c, 0x0c, 0x16, 0x00, - 0x00, 0x00, 0x0c, 0x0c, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x0c, 0x0c, - 0x0c, 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x0c, 0x0c, 0x0c, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x0c, 0x0c, 0x0c, 0x16, 0x14, 0x14, 0x16, 0x16, 0x14, 0x14, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x0c, 0x16, 0x16, 0x16, 0x14, 0x14, - 0x16, 0x16, 0x14, 0x14, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x0c, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x0c, 0x0c, 0x16, 0x0c, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0x0c, 0x16, 0x16, 0x0c, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, - 0x0c, 0x0c, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c, 0x16, 0x16, 0x16, 0x0c, 0x0c, 0x16, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x00, 0x0c, 0x0c, 0x00, 0x16, - 0x16, 0x0c, 0x0c, 0x16, 0x00, 0x00, 0x16, 0x16, 0x00, 0x0c, 0x0c, 0x00, - 0x07, 0x07, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x00, 0x00, 0x07, 0x07, 0x0c, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x0c, 0x0c, - 0x0c, 0x16, 0x16, 0x16, 0x0c, 0x0c, 0x0c, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x14, 0x14, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x14, 0x14, 0x16, 0x14, 0x14, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x14, 0x14, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, - 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x0c, 0x0c, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x0c, 0x0c, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x00, 0x0c, 0x0c, 0x16, 0x16, 0x0c, 0x0c, 0x00, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x14, 0x14, 0x14, 0x14, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x14, 0x14, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x14, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x14, 0x00, 0x16, 0x14, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x14, - 0x00, 0x16, 0x14, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x16, 0x16, 0x14, 0x00, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x00, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x14, - 0x00, 0x00, 0x14, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x14, 0x00, 0x00, 0x14, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x14, 0x00, 0x00, 0x14, 0x16, 0x16, - 0x08, 0x08, 0x08, 0x08, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x14, 0x03, - 0x03, 0x14, 0x16, 0x16, 0x08, 0x08, 0x08, 0x08, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x00, 0x14, 0x03, 0x03, 0x14, 0x16, 0x16, 0x08, 0x08, 0x08, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x08, 0x08, 0x08, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x00, 0x00, 0x16, 0x08, 0x08, 0x08, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, - 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x00, 0x00, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x03, 0x03, 0x03, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x03, - 0x03, 0x03, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x08, 0x16, 0x08, 0x08, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x08, 0x16, 0x08, 0x08, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x15, 0x16, 0x16, 0x15, - 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x15, 0x15, 0x15, 0x15, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16, 0x16, 0x15, 0x15, 0x16, 0x16, 0x00, 0x00, 0x16, - 0x16, 0x00, 0x00, 0x16, 0x16, 0x00, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, - 0x00, 0x00, 0x00, 0x16, 0x16, 0x00, 0x00, 0x16, 0x16, 0x00, 0x16, 0x16, - 0x16, 0x16, 0x16, 0x16 -}; -static const unsigned int tiletypes_len = 4096; - -#endif //GS2EMU_TILETYPES_H diff --git a/server/include/loader/IAccountLoader.h b/server/include/loader/IAccountLoader.h new file mode 100644 index 000000000..56c709fa1 --- /dev/null +++ b/server/include/loader/IAccountLoader.h @@ -0,0 +1,31 @@ +#ifndef IACCOUNTLOADER_H +#define IACCOUNTLOADER_H + +#include +#include +#include + +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class IAccountLoader +{ +public: + virtual ~IAccountLoader() = default; + +public: + virtual bool loadAccount(std::string_view accountName, Account& account) = 0; + virtual bool saveAccount(const Account& account) = 0; + +public: + virtual bool checkSearchConditions(std::string_view account, const std::vector& searches) const = 0; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // IACCOUNTLOADER_H diff --git a/server/include/loader/INPCLoader.h b/server/include/loader/INPCLoader.h new file mode 100644 index 000000000..9ea86d16b --- /dev/null +++ b/server/include/loader/INPCLoader.h @@ -0,0 +1,25 @@ +#ifndef INPCLOADER_H +#define INPCLOADER_H + +#include + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class INPCLoader +{ +public: + virtual NPCPtr loadNPC(std::string_view npcName) noexcept = 0; + virtual NPCPtr loadNPC(const std::filesystem::path& filePath) noexcept = 0; + virtual bool saveNPC(NPCPtr npc) noexcept = 0; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // INPCLOADER_H diff --git a/server/include/loader/LevelLoader.h b/server/include/loader/LevelLoader.h new file mode 100644 index 000000000..1a8c32a0d --- /dev/null +++ b/server/include/loader/LevelLoader.h @@ -0,0 +1,74 @@ +#ifndef LEVELLOADER_H +#define LEVELLOADER_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class LevelLoader +{ +public: + /// @brief Loads a level from the specified file path. + /// @param levelName The filesystem path representing the name or location of the level to load. + /// @return A shared pointer to the loaded level object. + static LevelPtr loadLevel(const std::filesystem::path& levelName); + + /// @brief Loads a level from the specified file path into an existing level object. + /// @param levelName The filesystem path representing the name or location of the level to load. + /// @param level The shared pointer to the level object where the loaded data will be stored. + /// @return True if the level was loaded successfully, false otherwise. + static bool loadLevelInto(const std::filesystem::path& levelName, LevelPtr level); + +public: + /// @brief Loads the static data of a specified level using the given level name. + /// @param levelName The filesystem path representing the name or location of the level to load. + /// @return A shared pointer to the loaded static level data object. + static StaticLevelDataPtr loadStaticData(const std::filesystem::path& levelName); + + /// @brief Loads the static data of a specified level into an existing static level data object. + /// @param staticLevelData The pointer to the static level data object where the loaded data will be stored. + /// @return True if the static level data was loaded successfully, false otherwise. + static bool loadStaticDataInto(StaticLevelDataPtr staticLevelData); + + /// @brief Attaches static level data to a given level, returning a SubLevel that attaches it. + /// @param level The level to attach the static data to. + /// @param mapPosition The location within the map where the static data should be applied. + /// @param staticData The static data to attach to the level. + /// @return A new SubLevelPtr that represents the attached static data within the level. + static SubLevelPtr attachStaticDataToLevel(LevelPtr level, std::optional mapPosition, StaticLevelDataPtr staticData); + + /// @brief Loads the NPCs for a given level from the provided static level data. + /// @param level The shared pointer to the level for which NPCs are to be loaded. + /// @param mapPosition An optional map position indicating the specific sub-level location. + /// @param levelData A pointer to the static level data containing NPC information. + static void loadStaticDataNPCs(LevelPtr level, std::optional mapPosition, StaticLevelDataPtr staticData); + +private: + static void loadBinaryTiles(StaticLevelDataPtr levelData, fs::FilePtr& fileData, uint32_t bits, int layers); + static void loadBinaryLinks(StaticLevelDataPtr levelData, fs::FilePtr& fileData, fs::FileSystem& fileSystem); + static void loadBinaryBaddies(StaticLevelDataPtr levelData, fs::FilePtr& fileData, bool loadVerses); + static void loadBinaryNPCs(StaticLevelDataPtr levelData, fs::FilePtr& fileData); + static void loadBinaryChests(StaticLevelDataPtr levelData, fs::FilePtr& fileData); + static void loadBinarySigns(StaticLevelDataPtr levelData, fs::FilePtr& fileData); + +private: + static bool loadGraal(StaticLevelDataPtr levelData, std::string_view fileVersion, fs::FileSystem& fileSystem, fs::FilePtr& fileData); + static bool loadZelda(StaticLevelDataPtr levelData, std::string_view fileVersion, fs::FileSystem& fileSystem, fs::FilePtr& fileData); + static bool loadNW(StaticLevelDataPtr levelData, std::string_view fileVersion, fs::FileSystem& fileSystem, fs::FilePtr& fileData); +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // LEVELLOADER_H diff --git a/server/include/loader/flatfile/FlatFileAccountLoader.h b/server/include/loader/flatfile/FlatFileAccountLoader.h new file mode 100644 index 000000000..5db2ab0c9 --- /dev/null +++ b/server/include/loader/flatfile/FlatFileAccountLoader.h @@ -0,0 +1,39 @@ +#ifndef FLATFILEACCOUNTLOADER_H +#define FLATFILEACCOUNTLOADER_H + +#include +#include +#include +#include +#include + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +using flagPair = std::pair; +using chestPair = std::pair; + +class FlatFileAccountLoader : public IAccountLoader +{ +public: + bool loadAccount(std::string_view accountName, Account& account) override; + bool saveAccount(const Account& account) override; + +public: + bool checkSearchConditions(std::string_view account, const std::vector& searches) const override; + +protected: + flagPair decomposeFlag(const std::string& flag) const; + chestPair decomposeChest(const std::string& chest) const; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // FLATFILEACCOUNTLOADER_H diff --git a/server/include/loader/flatfile/FlatFileNPCLoader.h b/server/include/loader/flatfile/FlatFileNPCLoader.h new file mode 100644 index 000000000..937c272ed --- /dev/null +++ b/server/include/loader/flatfile/FlatFileNPCLoader.h @@ -0,0 +1,26 @@ +#ifndef FLATFILENPCLOADER_H +#define FLATFILENPCLOADER_H + +#include +#include + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class FlatFileNPCLoader : public INPCLoader +{ +public: + virtual NPCPtr loadNPC(std::string_view npcName) noexcept override; + virtual NPCPtr loadNPC(const std::filesystem::path& filePath) noexcept override; + virtual bool saveNPC(NPCPtr npc) noexcept override; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // FLATFILENPCLOADER_H diff --git a/server/include/main.h b/server/include/main.h index 859d8f77b..7586551e0 100644 --- a/server/include/main.h +++ b/server/include/main.h @@ -1,11 +1,11 @@ #ifndef MAIN_H #define MAIN_H -#include +#include bool parseArgs(int argc, char* argv[]); void printHelp(const char* pname); -std::string getBaseHomePath(); +std::filesystem::path getBaseHomePath(); void shutdownServer(int signal); #endif // MAIN_H diff --git a/server/include/misc/UPNP.h b/server/include/misc/UPNP.h index b4b43ea54..26bb26e28 100644 --- a/server/include/misc/UPNP.h +++ b/server/include/misc/UPNP.h @@ -1,19 +1,19 @@ -#ifndef CUPNP_H -#define CUPNP_H +#ifndef UPNP_H +#define UPNP_H -#ifdef UPNP +#include +#include +#include +#include - #include - #include - - #include - #include "BabyDI.h" - - #include - #include - #include +#ifdef ENABLE_UPNP +#include +#endif -class Server; +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// class UPNP { @@ -22,12 +22,12 @@ class UPNP void operator()() { discover(); - addPortForward(m_localIp, port); + addPortForward(m_localIp, m_port); } - void initialize(const char* m_localIp, const char* port) + void initialize(std::string_view localIp, std::string_view port) { - m_localIp = m_localIp; + m_localIp = localIp; m_port = port; } @@ -35,10 +35,10 @@ class UPNP void discover(); // Adds a port forward. - void addPortForward(const CString& addr, const CString& port); + void addPortForward(std::string_view address, std::string_view port); // Removes a port forward. - void removePortForward(const CString& port); + void removePortForward(std::string_view port); // Removes all the port forwards created by the addPortForward command. void removeAllForwardedPorts() @@ -49,20 +49,23 @@ class UPNP } // Returns true if the port was successfully forwarded. - bool wasPortForwarded(const CString& port) + bool wasPortForwarded(std::string_view port) { return m_portsForwarded.find(port) != m_portsForwarded.end(); } private: - BabyDI_INJECT(Server, m_server); + std::set> m_portsForwarded; + std::string m_localIp; + std::string m_port; - std::set m_portsForwarded; - CString m_localIp; - CString m_port; +#ifdef ENABLE_UPNP struct UPNPUrls m_urls; struct IGDdatas m_data; +#endif }; -#endif -#endif +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // UPNP_H diff --git a/server/include/misc/WordFilter.h b/server/include/misc/WordFilter.h index e50a7fe07..5e75a37b7 100644 --- a/server/include/misc/WordFilter.h +++ b/server/include/misc/WordFilter.h @@ -1,11 +1,16 @@ -#ifndef CWORDFILTER_H -#define CWORDFILTER_H +#ifndef WORDFILTER_H +#define WORDFILTER_H #include #include #include +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + enum { FILTER_CHECK_CHAT = 0x1, @@ -52,11 +57,14 @@ class WordFilter int apply(const Player* player, CString& chat, int check); private: - Server* m_server; + Server* m_server = nullptr; bool m_showWordsToRC = false; CString m_defaultWarnMessage; std::vector m_rules; }; -#endif +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // WORDFILTER_H diff --git a/server/include/network/IPacketHandler.h b/server/include/network/IPacketHandler.h new file mode 100644 index 000000000..0a3690813 --- /dev/null +++ b/server/include/network/IPacketHandler.h @@ -0,0 +1,378 @@ +#ifndef IPACKETHANDLER_H +#define IPACKETHANDLER_H + +#include +#include +#include + +#include +#include +#include + +#ifdef PACKETLOGGING +#include +#include +#include +#endif + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +#ifdef PACKETLOGGING +#define FOR_INPUT_PACKETS(DO) \ + DO(PLI_LEVELWARP) \ + DO(PLI_BOARDMODIFY) \ + DO(PLI_PLAYERPROPS) \ + DO(PLI_NPCPROPS) \ + DO(PLI_BOMBADD) \ + DO(PLI_BOMBDEL) \ + DO(PLI_TOALL) \ + DO(PLI_HORSEADD) \ + DO(PLI_HORSEDEL) \ + DO(PLI_ARROWADD) \ + DO(PLI_FIRESPY) \ + DO(PLI_THROWCARRIED) \ + DO(PLI_ITEMADD) \ + DO(PLI_ITEMDEL) \ + DO(PLI_CLAIMPKER) \ + DO(PLI_BADDYPROPS) \ + DO(PLI_BADDYHURT) \ + DO(PLI_BADDYADD) \ + DO(PLI_FLAGSET) \ + DO(PLI_FLAGDEL) \ + DO(PLI_OPENCHEST) \ + DO(PLI_PUTNPC) \ + DO(PLI_NPCDEL) \ + DO(PLI_WANTFILE) \ + DO(PLI_SHOWIMGPLAYER) \ + DO(PLI_UNKNOWN25) \ + DO(PLI_HURTPLAYER) \ + DO(PLI_EXPLOSION) \ + DO(PLI_PRIVATEMESSAGE) \ + DO(PLI_NPCWEAPONDEL) \ + DO(PLI_LEVELWARPMOD) \ + DO(PLI_PACKETCOUNT) \ + DO(PLI_ITEMTAKE) \ + DO(PLI_WEAPONADD) \ + DO(PLI_UPDATEFILE) \ + DO(PLI_ADJACENTLEVEL) \ + DO(PLI_HITOBJECTS) \ + DO(PLI_LANGUAGE) \ + DO(PLI_TRIGGERACTION) \ + DO(PLI_TAMPERCHECK) \ + DO(PLI_SHOOT) \ + DO(PLI_SERVERWARP) \ + DO(PLI_MUTEPLAYER) \ + DO(PLI_PROCESSLIST) \ + DO(PLI_ENTERLEVEL) \ + DO(PLI_VERIFYWANTSEND) \ + DO(PLI_SHOOT2) \ + DO(PLI_RAWDATA) \ + DO(PLI_RC_SERVEROPTIONSGET) \ + DO(PLI_RC_SERVEROPTIONSSET) \ + DO(PLI_RC_FOLDERCONFIGGET) \ + DO(PLI_RC_FOLDERCONFIGSET) \ + DO(PLI_RC_RESPAWNSET) \ + DO(PLI_RC_HORSELIFESET) \ + DO(PLI_RC_APINCREMENTSET) \ + DO(PLI_RC_BADDYRESPAWNSET) \ + DO(PLI_RC_PLAYERPROPSGET) \ + DO(PLI_RC_PLAYERPROPSSET) \ + DO(PLI_RC_DISCONNECTPLAYER) \ + DO(PLI_RC_UPDATELEVELS) \ + DO(PLI_RC_ADMINMESSAGE) \ + DO(PLI_RC_PRIVADMINMESSAGE) \ + DO(PLI_RC_LISTRCS) \ + DO(PLI_RC_DISCONNECTRC) \ + DO(PLI_RC_APPLYREASON) \ + DO(PLI_RC_SERVERFLAGSGET) \ + DO(PLI_RC_SERVERFLAGSSET) \ + DO(PLI_RC_ACCOUNTADD) \ + DO(PLI_RC_ACCOUNTDEL) \ + DO(PLI_RC_ACCOUNTLISTGET) \ + DO(PLI_RC_PLAYERPROPSGET2) \ + DO(PLI_RC_PLAYERPROPSGET3) \ + DO(PLI_RC_PLAYERPROPSRESET) \ + DO(PLI_RC_PLAYERPROPSSET2) \ + DO(PLI_RC_ACCOUNTGET) \ + DO(PLI_RC_ACCOUNTSET) \ + DO(PLI_RC_CHAT) \ + DO(PLI_PROFILEGET) \ + DO(PLI_PROFILESET) \ + DO(PLI_RC_WARPPLAYER) \ + DO(PLI_RC_PLAYERRIGHTSGET) \ + DO(PLI_RC_PLAYERRIGHTSSET) \ + DO(PLI_RC_PLAYERCOMMENTSGET) \ + DO(PLI_RC_PLAYERCOMMENTSSET) \ + DO(PLI_RC_PLAYERBANGET) \ + DO(PLI_RC_PLAYERBANSET) \ + DO(PLI_RC_FILEBROWSER_START) \ + DO(PLI_RC_FILEBROWSER_CD) \ + DO(PLI_RC_FILEBROWSER_END) \ + DO(PLI_RC_FILEBROWSER_DOWN) \ + DO(PLI_RC_FILEBROWSER_UP) \ + DO(PLI_NPCSERVERQUERY) \ + DO(PLI_RC_FILEBROWSER_MOVE) \ + DO(PLI_RC_FILEBROWSER_DELETE) \ + DO(PLI_RC_FILEBROWSER_RENAME) \ + DO(PLI_NC_NPCGET) \ + DO(PLI_NC_NPCDELETE) \ + DO(PLI_NC_NPCRESET) \ + DO(PLI_NC_NPCSCRIPTGET) \ + DO(PLI_NC_NPCWARP) \ + DO(PLI_NC_NPCFLAGSGET) \ + DO(PLI_NC_NPCSCRIPTSET) \ + DO(PLI_NC_NPCFLAGSSET) \ + DO(PLI_NC_NPCADD) \ + DO(PLI_NC_CLASSEDIT) \ + DO(PLI_NC_CLASSADD) \ + DO(PLI_NC_LOCALNPCSGET) \ + DO(PLI_NC_WEAPONLISTGET) \ + DO(PLI_NC_WEAPONGET) \ + DO(PLI_NC_WEAPONADD) \ + DO(PLI_NC_WEAPONDELETE) \ + DO(PLI_NC_CLASSDELETE) \ + DO(PLI_REQUESTUPDATEBOARD) \ + DO(PLI_NC_LEVELLISTGET) \ + DO(PLI_NC_LEVELLISTSET) \ + DO(PLI_REQUESTTEXT) \ + DO(PLI_SENDTEXT) \ + DO(PLI_RC_LARGEFILESTART) \ + DO(PLI_RC_LARGEFILEEND) \ + DO(PLI_UPDATEGANI) \ + DO(PLI_UPDATESCRIPT) \ + DO(PLI_UPDATEPACKAGEREQUESTFILE) \ + DO(PLI_RC_FOLDERDELETE) \ + DO(PLI_UPDATECLASS) \ + DO(PLI_RC_UNKNOWN162) \ + DO(PLI_SET_ENC_KEY) \ + DO(PLI_BUNDLE) +#define FILL_INPUT_ARRAY(name) names[(uint8_t)name] = #name; + +constexpr std::array FillInputPacketNamesArray() +{ + std::array names; + names.fill("(unknown packet)"); + FOR_INPUT_PACKETS(FILL_INPUT_ARRAY) + return names; +} + +inline std::array InputPacketNamesArray = FillInputPacketNamesArray(); +#endif + +/////////////////////////////////////////////////////////////////////////////// + +enum class HandlePacketResult +{ + Handled, + Bubble, + Failed, +}; + +enum class PacketHandleMode +{ + OLDPROTOCOL, + NEWPROTOCOL +}; + +class IPacketHandler +{ +public: + virtual ~IPacketHandler() = default; + +public: + void processBuffer(CString& buffer); + +protected: + std::optional retrievePacketBundle(CString& buffer) const; + void processPacketBundle(CString& packet); + void parsePacketsFromBundle(CString& packet); + void parseLoginPacket(CString& buffer); + virtual HandlePacketResult handlePacket(std::optional id, CString& packet) = 0; + virtual std::string_view whoAmI() const noexcept { return "(unknown);"sv; } + +public: + CEncryption Encryption; + uint32_t PacketCount = 0; + uint32_t InvalidPackets = 0; + +public: + PacketHandleMode HandleMode = PacketHandleMode::OLDPROTOCOL; + bool RemoveNewlinesFromRawPacket = false; + bool RemoveNewlineFromFileUpload = false; + +protected: + bool m_nextIsRaw = false; + size_t m_rawPacketSize = 0; +}; + +inline void IPacketHandler::processBuffer(CString& buffer) +{ + buffer.setRead(0); + while (buffer.length() > 2) + { + auto result = retrievePacketBundle(buffer); + if (result.has_value() == false) + break; + auto& bundle = result.value(); + if (bundle.isEmpty()) + break; + + // Process the packet bundle. + processPacketBundle(bundle); + + // Parse the packets. + if (PacketCount != 0) [[likely]] + parsePacketsFromBundle(bundle); + else + { + // Login packet should parse differently. + // We also break immediately after parsing it since we are going to create a new player. + parseLoginPacket(bundle); + break; + } + } +} + +inline std::optional IPacketHandler::retrievePacketBundle(CString& buffer) const +{ + uint16_t packetSize = static_cast(buffer.readShort()); + if (packetSize > buffer.length() - 2) + return std::nullopt; + + CString packet = buffer.readChars(packetSize); + buffer.removeI(0, packetSize + 2); + return std::make_optional(packet); +} + +inline void IPacketHandler::processPacketBundle(CString& bundle) +{ + // No encryption or compression. + if (Encryption.getGen() == ENCRYPT_GEN_1) + return; + + // Version 1.41 - 2.18 non-client. + // Not encrypted, but zlib compressed. + if (Encryption.getGen() == ENCRYPT_GEN_2) + { + bundle.zuncompressI(); + } + // Version 1.41 - 2.18 client encryption + // Compressed with zlib, individual packets in bundle encrypted. + else if (Encryption.getGen() == ENCRYPT_GEN_3) + { + bundle.zuncompressI(); + } + // Version 2.19+ encryption. + // Bundle compressed and then encrypted. Always BZ2 compressed. + else if (Encryption.getGen() == ENCRYPT_GEN_4) + { + // Decrypt the bundle. + Encryption.limitFromType(COMPRESS_BZ2); + Encryption.decrypt(bundle); + + // Uncompress bundle. + bundle.bzuncompressI(); + } + // Compressed and then encrypted. Encryption depends on the compression type. + else if (Encryption.getGen() >= ENCRYPT_GEN_5) + { + // Find the compression type and remove it. + int pType = bundle.readChar(); + bundle.removeI(0, 1); + + // Decrypt the bundle. + Encryption.limitFromType(pType); // Encryption is partially related to compression. + Encryption.decrypt(bundle); + + // Uncompress bundle + if (pType == COMPRESS_ZLIB) + bundle.zuncompressI(); + else if (pType == COMPRESS_BZ2) + bundle.bzuncompressI(); + else if (pType != COMPRESS_UNCOMPRESSED) + ; // log::printLine(log::server, "** [ERROR] Client gave incorrect packet compression type! [{}]", pType); + } +} + +inline void IPacketHandler::parsePacketsFromBundle(CString& bundle) +{ + while (bundle.bytesLeft() > 0) + { + // Grab a packet out of the input stream. + CString curPacket; + if (m_nextIsRaw) + { + m_nextIsRaw = false; + curPacket = bundle.readChars(m_rawPacketSize); + + // The client and RC versions above 1.1 append a \n to the end of the packet. + // Remove it now. + //if (isClient() || (isRC() && m_versionId > RCVER_1_1)) + if (RemoveNewlinesFromRawPacket) + { + if (curPacket[curPacket.length() - 1] == '\n') + curPacket.removeI(curPacket.length() - 1); + } + } + else + curPacket = bundle.readString("\n"); + + // Generation 3 encrypts individual packets so decrypt it now. + if (Encryption.getGen() == ENCRYPT_GEN_3) + Encryption.decrypt(curPacket); + + // Get the packet id. + unsigned char id = curPacket.readGUChar(); + ++PacketCount; + + // RC version 1.1 adds a "\n" string to the end of file uploads instead of a newline character. + // This causes issues because it messes with the packet order. + //if (isRC() && m_versionId == RCVER_1_1 && id == PLI_RC_FILEBROWSER_UP) + if (RemoveNewlineFromFileUpload && id == PLI_RC_FILEBROWSER_UP) + { + curPacket.removeI(curPacket.length() - 1); + curPacket.setRead(1); + bundle.readChar(); // Read out the \n that got left behind. + } + +#ifdef PACKETLOGGING + std::string_view who = whoAmI(); + log::printLine(log::networkdump, "> In Packet from {}: [{}] {} ({} bytes)", who, (uint32_t)id, InputPacketNamesArray[id], curPacket.length()); + log::print(log::networkdump, "{}", curPacket.text()); + if (curPacket[curPacket.length() - 1] != '\n') + log::print(log::networkdump, "\n"); + for (int i = 0; i < curPacket.length(); ++i) + log::print(log::networkdump, "{:02x} ", (unsigned char)((curPacket.text())[i])); + log::print(log::networkdump, "\n\n"); +#endif + + // Raw packet handling. + if (id == PLI_RAWDATA) + { + m_nextIsRaw = true; + m_rawPacketSize = curPacket.readGUInt(); + continue; + } + + // Call the function assigned to the packet id. + handlePacket(id, curPacket); + } +} + +inline void IPacketHandler::parseLoginPacket(CString& buffer) +{ + ++PacketCount; + + // Call the login packet handler function. + auto packet = buffer.readString("\n"); + handlePacket(std::nullopt, packet); +} + +/////////////////////////////////////////////////////////////////////////////// + +} // end namespace preagonal + +#endif // IPACKETHANDLER_H diff --git a/server/include/npcserver/NPCServer.h b/server/include/npcserver/NPCServer.h new file mode 100644 index 000000000..e5b9f12b4 --- /dev/null +++ b/server/include/npcserver/NPCServer.h @@ -0,0 +1,285 @@ +#ifndef NPCSERVER_H +#define NPCSERVER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class Server; +class NPC; +class Player; + +class NPCServer +{ +public: + NPCServer() = default; + NPCServer(const NPCServer&) = delete; + NPCServer(NPCServer&&) = delete; + NPCServer& operator=(const NPCServer&) = delete; + NPCServer& operator=(NPCServer&&) = delete; + +public: + void initialize(); + void setRemoteIp(std::string_view host); + void sendNCLoginToPlayer(std::shared_ptr player); + +public: + void update(TimeoutGenerator::time_point currentTime = precise_clock::now()); + +public: + template std::shared_ptr getPlayer(const PlayerID id) const; + template std::shared_ptr getPlayer(const PlayerID id, int type) const; + template std::shared_ptr getPlayer(const std::string& account, int type) const; + [[inline]] std::shared_ptr getPlayerNPCServer() const; + +public: + [[inline]] const auto& getGlobalNPCList() const noexcept; + [[inline]] const auto& getClassList() const noexcept; + [[inline]] auto& getPlayerList() noexcept; + +public: + [[inline]] void addEventToControlNPC(ScriptEventType type, ScriptObject source, string::NotInputRangeNotString auto&&... args); + [[inline]] void addEventToControlNPC(ScriptEventType type, ScriptObject source, string::InputRangeNotString auto&& range); + [[inline]] size_t addEventToLevelNPCsAtPosition(ScriptEventType type, ScriptObject source, std::weak_ptr level, PixelPosition pos, auto&& arg1, auto&&... args); + [[inline]] size_t addEventToLevelNPCsAtPosition(ScriptEventType type, ScriptObject source, std::weak_ptr level, PixelPosition pos, std::ranges::forward_range auto&& range); + +public: + void playerLogin(std::shared_ptr player); + void playerLogout(std::shared_ptr player); + +public: + std::weak_ptr getNPCByName(const std::string& name); + std::shared_ptr addNPC(std::string_view image, std::string_view script, std::shared_ptr level, const TilePosition& location, std::string_view type = NPCTYPE_LOCAL); + std::shared_ptr addNPC(std::string_view name, NPCID id, std::string_view type, std::string_view scripter, std::shared_ptr level, const TilePosition& location); + std::shared_ptr addNPCFromFile(const std::filesystem::path& filePath); + void deleteNPC(NPCID id); + void unloadNPC(NPCID id); + void saveNPCs(); + //std::vector> calculateNPCStats(); + +public: + bool hasClass(std::string_view name) const; + std::weak_ptr getClass(std::string_view name) const; + std::shared_ptr addClass(std::string_view className, std::string_view classCode); + std::shared_ptr loadClass(const std::filesystem::path& filePath); + bool deleteClass(std::string_view className); + void updateClass(std::string_view className, std::string_view classCode); + +public: + void showImage(std::shared_ptr npc, uint8_t index, const PixelPosition& position, std::string_view image) const; + void showText(std::shared_ptr npc, uint8_t index, const PixelPosition& position, std::string_view text, std::string_view font = {}, std::string_view style = {}) const; + void showGani(std::shared_ptr npc, uint8_t index, const PixelPosition& position, std::string_view animation, uint8_t direction) const; + void showPoly(std::shared_ptr npc, uint8_t index, const std::vector& points) const; + void changeShowImgColors(std::shared_ptr npc, uint8_t index, float red, float green, float blue, float alpha) const; + void changeShowImgMode(std::shared_ptr npc, uint8_t index, uint8_t drawMode) const; + void changeShowImgPart(std::shared_ptr npc, uint8_t index, const ImagePartRectangle& imagePart) const; + void changeShowImgLayer(std::shared_ptr npc, uint8_t index, uint8_t layer) const; + void changeShowImgZoom(std::shared_ptr npc, uint8_t index, float zoom) const; + void hideImages(std::shared_ptr npc, uint8_t index, std::optional endIndex = std::nullopt) const; + +public: + tileset::TileType getTileType(uint16_t tile, std::shared_ptr level) const noexcept; + +public: + ScriptSystem scripting; + +private: + void run(TimeoutGenerator::time_delta delta); + void processDeletedNPCs(); + void processUnloadedNPCs(); + void processDeletedPlayers(); + +private: + void loadClasses(); + void loadDatabaseNPCs(); + +private: + BabyDI_INJECT(Server, m_server); + + std::shared_ptr m_npcServerPlayer; + std::string m_ncHost; + uint16_t m_ncPort = 14900; + + clock::time_point m_frameStartTime; + + TimeoutGenerator m_runTimeout{ 100ms, true }; + TimeoutGenerator m_timedSave{ 5min, true }; + + bool m_sleeping = false; + bool m_firstNPCSave = true; + + std::unordered_map> m_globalNPCList; + std::unordered_set m_deletedNPCs; + std::unordered_set m_unloadedNPCs; + std::unordered_map> m_playerList; + std::unordered_set> m_deletedPlayers; + string_map> m_classList; +}; + +template +inline std::shared_ptr NPCServer::getPlayer(const PlayerID id) const +{ + auto iter = m_playerList.find(id); + if (iter == std::end(m_playerList)) + return nullptr; + + if constexpr (std::same_as) + return iter->second; + + return std::dynamic_pointer_cast(iter->second); +} + +template +inline std::shared_ptr NPCServer::getPlayer(const PlayerID id, int type) const +{ + auto player = getPlayer(id); + if (player == nullptr || !(player->getType() & type)) + return nullptr; + + return player; +} + +template +inline std::shared_ptr NPCServer::getPlayer(const std::string& account, int type) const +{ + for (const auto& [id, player] : m_playerList) + { + // Check if its the type of player we are looking for + if (!player || !(player->getType() & type)) + continue; + + // Compare account names. + if (string::equalsi(player->account.name, account)) + { + if constexpr (std::same_as) + return player; + + return std::dynamic_pointer_cast(player); + } + } + + return nullptr; +} + +inline std::shared_ptr NPCServer::getPlayerNPCServer() const +{ + return m_npcServerPlayer; +} + +inline const auto& NPCServer::getGlobalNPCList() const noexcept +{ + return m_globalNPCList; +} + +inline const auto& NPCServer::getClassList() const noexcept +{ + return m_classList; +} + +inline auto& NPCServer::getPlayerList() noexcept +{ + return m_playerList; +} + +inline void NPCServer::addEventToControlNPC(ScriptEventType type, ScriptObject source, string::NotInputRangeNotString auto&&... args) +{ + for (auto& [id, npcPtr] : m_globalNPCList) + { + if (auto npc = npcPtr.lock(); npc != nullptr && npc->scriptType == NPCTYPE_CONTROL) + npc->scripting.events.addEvent(type, source, args...); + } +} + +inline void NPCServer::addEventToControlNPC(ScriptEventType type, ScriptObject source, string::InputRangeNotString auto&& range) +{ + for (auto& [id, npcPtr] : m_globalNPCList) + { + if (auto npc = npcPtr.lock(); npc != nullptr && npc->scriptType == NPCTYPE_CONTROL) + npc->scripting.events.addEvent(type, source, std::forward(range)); + } +} + +inline size_t NPCServer::addEventToLevelNPCsAtPosition(ScriptEventType type, ScriptObject source, std::weak_ptr level, PixelPosition pos, auto&& arg1, auto&&... args) +{ + auto levelPtr = level.lock(); + if (levelPtr == nullptr) + return 0; + + auto eventDistance = m_server->cached.eventDistance.getValue(); + if (type == ScriptEventType::TRIGGERACTION) + eventDistance = m_server->cached.triggerDistance.getValue(); + + size_t count = 0; + for (const auto& id : levelPtr->findInRangeNPCsByDistance(pos, eventDistance)) + { + if (auto npc = m_server->getNPC(id); npc != nullptr) + { + LocalPixelRectangleArea npcRect = { npc->getLocalPosition(), npc->shape }; + if (positionInRectangle(pos, npcRect)) + { + ++count; + npc->scripting.events.addEvent(type, source, arg1, args...); + } + } + } + return count; +} + +inline size_t NPCServer::addEventToLevelNPCsAtPosition(ScriptEventType type, ScriptObject source, std::weak_ptr level, PixelPosition pos, std::ranges::forward_range auto&& range) +{ + auto levelPtr = level.lock(); + if (levelPtr == nullptr) + return 0; + + auto eventDistance = m_server->cached.eventDistance.getValue(); + if (type == ScriptEventType::TRIGGERACTION) + eventDistance = m_server->cached.triggerDistance.getValue(); + + size_t count = 0; + for (const auto& id : levelPtr->findInRangeNPCsByDistance(pos, eventDistance)) + { + if (auto npc = m_server->getNPC(id); npc != nullptr) + { + LocalPixelRectangleArea npcRect = { npc->getLocalPosition(), npc->shape }; + if (positionInRectangle(pos, npcRect)) + { + ++count; + npc->scripting.events.addEvent(type, source, std::forward(range)); + } + } + } + return count; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // NPCSERVER_H diff --git a/server/include/npcserver/PlayerNPCServer.h b/server/include/npcserver/PlayerNPCServer.h new file mode 100644 index 000000000..54193ba1c --- /dev/null +++ b/server/include/npcserver/PlayerNPCServer.h @@ -0,0 +1,47 @@ +#ifndef PLAYERNPCSERVER_H +#define PLAYERNPCSERVER_H + +#include +#include +#include + +#include + +#include + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class PlayerNPCServer : public Player +{ +public: + PlayerNPCServer(CSocket* pSocket, PlayerID pId); + virtual ~PlayerNPCServer() override; + +public: + virtual bool onRecv() override; + virtual void onUnregister() override; + +public: + virtual void sendPrivateMessage(PlayerID from, std::string_view message) override; + +public: + std::string privateMessage; + +protected: + virtual HandlePacketResult handlePacket(std::optional id, CString& packet) override; + +public: + //HandlePacketResult msgLoginPacket(CString& pPacket); +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // PLAYERNPCSERVER_H diff --git a/server/include/object/Character.h b/server/include/object/Character.h new file mode 100644 index 000000000..b6d22a2cc --- /dev/null +++ b/server/include/object/Character.h @@ -0,0 +1,460 @@ +#ifndef CHARACTER_H +#define CHARACTER_H + +#include +#include +#include +#include +#include +#include + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +struct Character +{ + int16_t localPixelX = 488; // 30.5 + int16_t localPixelY = 480; // 30 + int16_t localPixelZ = 0; + uint8_t mapX = 0; + uint8_t mapY = 0; + uint8_t ap = 50; + uint8_t mp = 0; + uint32_t gralats = 0; + uint8_t hitpointsInHalves = 6; + uint8_t hurtDeltaInHalves = 0; + uint8_t bombs = 10; + uint8_t arrows = 5; + uint8_t bombPower = 1; + uint8_t glovePower = 1; + int8_t swordPower = 1; + uint8_t shieldPower = 1; + uint8_t bowPower = 1; + uint8_t sprite = 2; + uint8_t direction = 2; // 0: up, 1: left, 2: down, 3: right + clock::time_point lastHurtTime = clock::time_point::min(); + std::array hurtPushDeltaInHalfPixels{ 0, 0 }; + std::array colors{ 2, 0, 10, 4, 18, 18, 18, 18 }; // 0-19 are ClassicColors, 20+ are HTMLColors + std::string nickName{ "default" }; + std::string gani{ "idle" }; + std::string chatMessage; + std::string horseImage; + std::string headImage{ "head0.png" }; + std::string bodyImage{ "body.png" }; + std::string swordImage{ "sword1.png" }; + std::string shieldImage{ "shield1.png" }; + std::string bowImage{ "bow1.png" }; + std::string ganiAttributes[30]; + + LocalPixelPosition getLocalPosition() const noexcept + { + return { localPixelX, localPixelY, localPixelZ }; + } + + PixelPosition getGlobalPosition() const noexcept + { + return { static_cast((mapX * 1024) + localPixelX), static_cast((mapY * 1024) + localPixelY), static_cast(localPixelZ) }; + } + + TilePosition getTilePosition() const noexcept + { + return { static_cast((mapX * 64) + (localPixelX / 16.0f)), static_cast((mapY * 64) + (localPixelY / 16.0f)), static_cast(localPixelZ / 16.0f) }; + } + + MapPosition getMapPosition() const noexcept + { + return { mapX, mapY, 0 }; + } +}; + +//---------------------------- + +enum class ClassicColors : uint8_t +{ + WHITE = 0, + YELLOW, + ORANGE, + PINK, + RED, + DARKRED, + LIGHTGREEN, + GREEN, + DARKGREEN, + LIGHTBLUE, + BLUE, + DARKBLUE, + BROWN, + CYNOBER, + PURPLE, + DARKPURPLE, + LIGHTGRAY, + GRAY, + BLACK, + TRANSPARENT, + + COUNT +}; +constexpr size_t CLASSICCOLORS_COUNT = static_cast(ClassicColors::COUNT); + +inline std::string_view getClassicColorName(ClassicColors color) +{ + static const std::unordered_map colorNames = + { + { ClassicColors::WHITE, "white"sv }, + { ClassicColors::YELLOW, "yellow"sv }, + { ClassicColors::ORANGE, "orange"sv }, + { ClassicColors::PINK, "pink"sv }, + { ClassicColors::RED, "red"sv }, + { ClassicColors::DARKRED, "darkred"sv }, + { ClassicColors::LIGHTGREEN, "lightgreen"sv }, + { ClassicColors::GREEN, "green"sv }, + { ClassicColors::DARKGREEN, "darkgreen"sv }, + { ClassicColors::LIGHTBLUE, "lightblue"sv }, + { ClassicColors::BLUE, "blue"sv }, + { ClassicColors::DARKBLUE, "darkblue"sv }, + { ClassicColors::BROWN, "brown"sv }, + { ClassicColors::CYNOBER, "cynober"sv }, + { ClassicColors::PURPLE, "purple"sv }, + { ClassicColors::DARKPURPLE, "darkpurple"sv }, + { ClassicColors::LIGHTGRAY, "lightgray"sv }, + { ClassicColors::GRAY, "gray"sv }, + { ClassicColors::BLACK, "black"sv }, + { ClassicColors::TRANSPARENT, "transparent"sv }, + }; + + if (colorNames.find(color) != colorNames.end()) + return colorNames.at(color); + + return {}; +} + +//---------------------------- + +enum class HTMLColors : uint8_t +{ + ALICEBLUE = 0, + ANTIQUEWHITE, + AQUA, + AQUAMARINE, + AZURE, + BEIGE, + BISQUE, + BLACK, + BLANCHEDALMOND, + BLUE, + BLUEVIOLET, + BROWN, + BURLYWOOD, + CADETBLUE, + CHARTREUSE, + CHOCOLATE, + CORAL, + CORNFLOWERBLUE, + CORNSILK, + CRIMSON, + CYAN, + DARKBLUE, + DARKCYAN, + DARKGOLDENROD, + DARKGRAY, + DARKGREEN, + DARKGREY, + DARKKHAKI, + DARKMAGENTA, + DARKOLIVEGREEN, + DARKORANGE, + DARKORCHID, + DARKRED, + DARKSALMON, + DARKSEAGREEN, + DARKSLATEBLUE, + DARKSLATEGRAY, + DARKSLATEGREY, + DARKTURQUOISE, + DARKVIOLET, + DEEPPINK, + DEEPSKYBLUE, + DIMGRAY, + DIMGREY, + DODGERBLUE, + FELDSPAR, + FIREBRICK, + FLORALWHITE, + FORESTGREEN, + FUCHSIA, + GAINSBORO, + GHOSTWHITE, + GOLD, + GOLDENROD, + GRAY, + GREEN, + GREENYELLOW, + GREY, + HONEYDEW, + HOTPINK, + INDIANRED, + INDIGO, + IVORY, + KHAKI, + LAVENDER, + LAVENDERBLUSH, + LAWNGREEN, + LEMONCHIFFON, + LIGHTBLUE, + LIGHTCORAL, + LIGHTCYAN, + LIGHTGOLDENRODYELLOW, + LIGHTGRAY, + LIGHTGREEN, + LIGHTGREY, + LIGHTPINK, + LIGHTSALMON, + LIGHTSEAGREEN, + LIGHTSKYBLUE, + LIGHTSLATEBLUE, + LIGHTSLATEGRAY, + LIGHTSLATEGREY, + LIGHTSTEELBLUE, + LIGHTYELLOW, + LIME, + LIMEGREEN, + LINEN, + MAGENTA, + MAROON, + MEDIUMAQUAMARINE, + MEDIUMBLUE, + MEDIUMORCHID, + MEDIUMPURPLE, + MEDIUMSEAGREEN, + MEDIUMSLATEBLUE, + MEDIUMSPRINGGREEN, + MEDIUMTURQUOISE, + MEDIUMVIOLETRED, + MIDNIGHTBLUE, + MINTCREAM, + MISTYROSE, + MOCCASIN, + NAVAJOWHITE, + NAVY, + OLDLACE, + OLIVE, + OLIVEDRAB, + ORANGE, + ORANGERED, + ORCHID, + PALEGOLDENROD, + PALEGREEN, + PALETURQUOISE, + PALEVIOLETRED, + PAPAYAWHIP, + PEACHPUFF, + PERU, + PINK, + PLUM, + POWDERBLUE, + PURPLE, + RED, + ROSYBROWN, + ROYALBLUE, + SADDLEBROWN, + SALMON, + SANDYBROWN, + SEAGREEN, + SEASHELL, + SIENNA, + SILVER, + SKYBLUE, + SLATEBLUE, + SLATEGRAY, + SLATEGREY, + SNOW, + SPRINGGREEN, + STEELBLUE, + TAN, + TEAL, + THISTLE, + TOMATO, + TURQUOISE, + VIOLET, + VIOLETRED, + WHEAT, + WHITE, + WHITESMOKE, + YELLOW, + YELLOWGREEN, + + COUNT +}; +constexpr size_t HTMLCOLORS_COUNT = static_cast(HTMLColors::COUNT); + +inline std::string_view getHTMLColorName(HTMLColors color) +{ + static const std::unordered_map colorNames = + { + { HTMLColors::ALICEBLUE, "aliceblue"sv }, + { HTMLColors::ANTIQUEWHITE, "antiquewhite"sv }, + { HTMLColors::AQUA, "aqua"sv }, + { HTMLColors::AQUAMARINE, "aquamarine"sv }, + { HTMLColors::AZURE, "azure"sv }, + { HTMLColors::BEIGE, "beige"sv }, + { HTMLColors::BISQUE, "bisque"sv }, + { HTMLColors::BLACK, "black"sv }, + { HTMLColors::BLANCHEDALMOND, "blanchedalmond"sv }, + { HTMLColors::BLUE, "blue"sv }, + { HTMLColors::BLUEVIOLET, "blueviolet"sv }, + { HTMLColors::BROWN, "brown"sv }, + { HTMLColors::BURLYWOOD, "burlywood"sv }, + { HTMLColors::CADETBLUE, "cadetblue"sv }, + { HTMLColors::CHARTREUSE, "chartreuse"sv }, + { HTMLColors::CHOCOLATE, "chocolate"sv }, + { HTMLColors::CORAL, "coral"sv }, + { HTMLColors::CORNFLOWERBLUE, "cornflowerblue"sv }, + { HTMLColors::CORNSILK, "cornsilk"sv }, + { HTMLColors::CRIMSON, "crimson"sv }, + { HTMLColors::CYAN, "cyan"sv }, + { HTMLColors::DARKBLUE, "darkblue"sv }, + { HTMLColors::DARKCYAN, "darkcyan"sv }, + { HTMLColors::DARKGOLDENROD, "darkgoldenrod"sv }, + { HTMLColors::DARKGRAY, "darkgray"sv }, + { HTMLColors::DARKGREEN, "darkgreen"sv }, + { HTMLColors::DARKGREY, "darkgrey"sv }, + { HTMLColors::DARKKHAKI, "darkkhaki"sv }, + { HTMLColors::DARKMAGENTA, "darkmagenta"sv }, + { HTMLColors::DARKOLIVEGREEN, "darkolivegreen"sv }, + { HTMLColors::DARKORANGE, "darkorange"sv }, + { HTMLColors::DARKORCHID, "darkorchid"sv }, + { HTMLColors::DARKRED, "darkred"sv }, + { HTMLColors::DARKSALMON, "darksalmon"sv }, + { HTMLColors::DARKSEAGREEN, "darkseagreen"sv }, + { HTMLColors::DARKSLATEBLUE, "darkslateblue"sv }, + { HTMLColors::DARKSLATEGRAY, "darkslategray"sv }, + { HTMLColors::DARKSLATEGREY, "darkslategrey"sv }, + { HTMLColors::DARKTURQUOISE, "darkturquoise"sv }, + { HTMLColors::DARKVIOLET, "darkviolet"sv }, + { HTMLColors::DEEPPINK, "deeppink"sv }, + { HTMLColors::DEEPSKYBLUE, "deepskyblue"sv }, + { HTMLColors::DIMGRAY, "dimgray"sv }, + { HTMLColors::DIMGREY, "dimgrey"sv }, + { HTMLColors::DODGERBLUE, "dodgerblue"sv }, + { HTMLColors::FELDSPAR, "feldspar"sv }, + { HTMLColors::FIREBRICK, "firebrick"sv }, + { HTMLColors::FLORALWHITE, "floralwhite"sv }, + { HTMLColors::FORESTGREEN, "forestgreen"sv }, + { HTMLColors::FUCHSIA, "fuchsia"sv }, + { HTMLColors::GAINSBORO, "gainsboro"sv }, + { HTMLColors::GHOSTWHITE, "ghostwhite"sv }, + { HTMLColors::GOLD, "gold"sv }, + { HTMLColors::GOLDENROD, "goldenrod"sv }, + { HTMLColors::GRAY, "gray"sv }, + { HTMLColors::GREEN, "green"sv }, + { HTMLColors::GREENYELLOW, "greenyellow"sv }, + { HTMLColors::GREY, "grey"sv }, + { HTMLColors::HONEYDEW, "honeydew"sv }, + { HTMLColors::HOTPINK, "hotpink"sv }, + { HTMLColors::INDIANRED, "indianred"sv }, + { HTMLColors::INDIGO, "indigo"sv }, + { HTMLColors::IVORY, "ivory"sv }, + { HTMLColors::KHAKI, "khaki"sv }, + { HTMLColors::LAVENDER, "lavender"sv }, + { HTMLColors::LAVENDERBLUSH, "lavenderblush"sv }, + { HTMLColors::LAWNGREEN, "lawngreen"sv }, + { HTMLColors::LEMONCHIFFON, "lemonchiffon"sv }, + { HTMLColors::LIGHTBLUE, "lightblue"sv }, + { HTMLColors::LIGHTCORAL, "lightcoral"sv }, + { HTMLColors::LIGHTCYAN, "lightcyan"sv }, + { HTMLColors::LIGHTGOLDENRODYELLOW, "lightgoldenrodyellow"sv }, + { HTMLColors::LIGHTGRAY, "lightgray"sv }, + { HTMLColors::LIGHTGREEN, "lightgreen"sv }, + { HTMLColors::LIGHTGREY, "lightgrey"sv }, + { HTMLColors::LIGHTPINK, "lightpink"sv }, + { HTMLColors::LIGHTSALMON, "lightsalmon"sv }, + { HTMLColors::LIGHTSEAGREEN, "lightseagreen"sv }, + { HTMLColors::LIGHTSKYBLUE, "lightskyblue"sv }, + { HTMLColors::LIGHTSLATEBLUE, "lightslateblue"sv }, + { HTMLColors::LIGHTSLATEGRAY, "lightslategray"sv }, + { HTMLColors::LIGHTSLATEGREY, "lightslategrey"sv }, + { HTMLColors::LIGHTSTEELBLUE, "lightsteelblue"sv }, + { HTMLColors::LIGHTYELLOW, "lightyellow"sv }, + { HTMLColors::LIME, "lime"sv }, + { HTMLColors::LIMEGREEN, "limegreen"sv }, + { HTMLColors::LINEN, "linen"sv }, + { HTMLColors::MAGENTA, "magenta"sv }, + { HTMLColors::MAROON, "maroon"sv }, + { HTMLColors::MEDIUMAQUAMARINE, "mediumaquamarine"sv }, + { HTMLColors::MEDIUMBLUE, "mediumblue"sv }, + { HTMLColors::MEDIUMORCHID, "mediumorchid"sv }, + { HTMLColors::MEDIUMPURPLE, "mediumpurple"sv }, + { HTMLColors::MEDIUMSEAGREEN, "mediumseagreen"sv }, + { HTMLColors::MEDIUMSLATEBLUE, "mediumslateblue"sv }, + { HTMLColors::MEDIUMSPRINGGREEN, "mediumspringgreen"sv }, + { HTMLColors::MEDIUMTURQUOISE, "mediumturquoise"sv }, + { HTMLColors::MEDIUMVIOLETRED, "mediumvioletred"sv }, + { HTMLColors::MIDNIGHTBLUE, "midnightblue"sv }, + { HTMLColors::MINTCREAM, "mintcream"sv }, + { HTMLColors::MISTYROSE, "mistyrose"sv }, + { HTMLColors::MOCCASIN, "moccasin"sv }, + { HTMLColors::NAVAJOWHITE, "navajowhite"sv }, + { HTMLColors::NAVY, "navy"sv }, + { HTMLColors::OLDLACE, "oldlace"sv }, + { HTMLColors::OLIVE, "olive"sv }, + { HTMLColors::OLIVEDRAB, "olivedrab"sv }, + { HTMLColors::ORANGE, "orange"sv }, + { HTMLColors::ORANGERED, "orangered"sv }, + { HTMLColors::ORCHID, "orchid"sv }, + { HTMLColors::PALEGOLDENROD, "palegoldenrod"sv }, + { HTMLColors::PALEGREEN, "palegreen"sv }, + { HTMLColors::PALETURQUOISE, "paleturquoise"sv }, + { HTMLColors::PALEVIOLETRED, "palevioletred"sv }, + { HTMLColors::PAPAYAWHIP, "papayawhip"sv }, + { HTMLColors::PEACHPUFF, "peachpuff"sv }, + { HTMLColors::PERU, "peru"sv }, + { HTMLColors::PINK, "pink"sv }, + { HTMLColors::PLUM, "plum"sv }, + { HTMLColors::POWDERBLUE, "powderblue"sv }, + { HTMLColors::PURPLE, "purple"sv }, + { HTMLColors::RED, "red"sv }, + { HTMLColors::ROSYBROWN, "rosybrown"sv }, + { HTMLColors::ROYALBLUE, "royalblue"sv }, + { HTMLColors::SADDLEBROWN, "saddlebrown"sv }, + { HTMLColors::SALMON, "salmon"sv }, + { HTMLColors::SANDYBROWN, "sandybrown"sv }, + { HTMLColors::SEAGREEN, "seagreen"sv }, + { HTMLColors::SEASHELL, "seashell"sv }, + { HTMLColors::SIENNA, "sienna"sv }, + { HTMLColors::SILVER, "silver"sv }, + { HTMLColors::SKYBLUE, "skyblue"sv }, + { HTMLColors::SLATEBLUE, "slateblue"sv }, + { HTMLColors::SLATEGRAY, "slategray"sv }, + { HTMLColors::SLATEGREY, "slategrey"sv }, + { HTMLColors::SNOW, "snow"sv }, + { HTMLColors::SPRINGGREEN, "springgreen"sv }, + { HTMLColors::STEELBLUE, "steelblue"sv }, + { HTMLColors::TAN, "tan"sv }, + { HTMLColors::TEAL, "teal"sv }, + { HTMLColors::THISTLE, "thistle"sv }, + { HTMLColors::TOMATO, "tomato"sv }, + { HTMLColors::TURQUOISE, "turquoise"sv }, + { HTMLColors::VIOLET, "violet"sv }, + { HTMLColors::VIOLETRED, "violetred"sv }, + { HTMLColors::WHEAT, "wheat"sv }, + { HTMLColors::WHITE, "white"sv }, + { HTMLColors::WHITESMOKE, "whitesmoke"sv }, + { HTMLColors::YELLOW, "yellow"sv }, + { HTMLColors::YELLOWGREEN, "yellowgreen"sv }, + }; + + if (colorNames.find(color) != colorNames.end()) + return colorNames.at(color); + + return {}; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // CHARACTER_H diff --git a/server/include/object/NPC.h b/server/include/object/NPC.h new file mode 100644 index 000000000..ce66ea005 --- /dev/null +++ b/server/include/object/NPC.h @@ -0,0 +1,691 @@ +#ifndef NPC_H +#define NPC_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace preagonal::props; + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +//////////////////////////////////////////////////////////////////////////////// + +inline constexpr std::array NPCGaniAttrPackets = { 36, 37, 38, 39, 40, 44, 45, 46, 47, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73 }; + +class FlatFileNPCLoader; +class Level; +class Player; +class ScriptClass; +class Server; + +using PlayerPtr = std::shared_ptr; + +enum class NPCProp : uint8_t +{ + IMAGE = 0, + SCRIPT = 1, + X = 2, + Y = 3, + POWER = 4, + RUPEES = 5, + ARROWS = 6, + BOMBS = 7, + GLOVEPOWER = 8, + BOMBPOWER = 9, + SWORDIMAGE = 10, + SHIELDIMAGE = 11, + GANI = 12, // NPCPROP_BOWGIF in pre-2.x + VISFLAGS = 13, + BLOCKFLAGS = 14, + MESSAGE = 15, + HURTDXDY = 16, + ID = 17, + SPRITE = 18, + COLORS = 19, + NICKNAME = 20, + HORSEIMAGE = 21, + HEADIMAGE = 22, + SAVE0 = 23, + SAVE1 = 24, + SAVE2 = 25, + SAVE3 = 26, + SAVE4 = 27, + SAVE5 = 28, + SAVE6 = 29, + SAVE7 = 30, + SAVE8 = 31, + SAVE9 = 32, + ALIGNMENT = 33, + IMAGEPART = 34, + BODYIMAGE = 35, + GATTRIB1 = 36, + GATTRIB2 = 37, + GATTRIB3 = 38, + GATTRIB4 = 39, + GATTRIB5 = 40, + GMAPLEVELX = 41, + GMAPLEVELY = 42, + + Z = 43, + + GATTRIB6 = 44, + GATTRIB7 = 45, + GATTRIB8 = 46, + GATTRIB9 = 47, + + // --- NOT HANDLED BY CLIENT, DO NOT SEND + UNKNOWN48 = 48, + SCRIPTER = 49, // My guess is UNKNOWN48 or this is the scripter's name + NAME = 50, + TYPE = 51, + CURLEVEL = 52, + // --- END NOT HANDLED BY CLIENT + + GATTRIB10 = 53, + GATTRIB11 = 54, + GATTRIB12 = 55, + GATTRIB13 = 56, + GATTRIB14 = 57, + GATTRIB15 = 58, + GATTRIB16 = 59, + GATTRIB17 = 60, + GATTRIB18 = 61, + GATTRIB19 = 62, + GATTRIB20 = 63, + GATTRIB21 = 64, + GATTRIB22 = 65, + GATTRIB23 = 66, + GATTRIB24 = 67, + GATTRIB25 = 68, + GATTRIB26 = 69, + GATTRIB27 = 70, + GATTRIB28 = 71, + GATTRIB29 = 72, + GATTRIB30 = 73, + + CLASS = 74, + X2 = 75, + Y2 = 76, + Z2 = 77, + + NPCPROP_COUNT +}; +constexpr int NPCPROP_COUNT = static_cast(NPCProp::NPCPROP_COUNT); + +inline constexpr std::array NPCSaveProps = { NPCProp::SAVE0, NPCProp::SAVE1, NPCProp::SAVE2, NPCProp::SAVE3, NPCProp::SAVE4, NPCProp::SAVE5, NPCProp::SAVE6, NPCProp::SAVE7, NPCProp::SAVE8, NPCProp::SAVE9 }; + +//! NPCPROP_VISFLAGS values. +enum class NPCVisFlags : uint8_t +{ + HIDDEN = 0b0000'0000, + + VISIBLE = 0b0000'0001, + DRAWOVERPLAYER = 0b0000'0010, + DRAWUNDERPLAYER = 0b0000'0100, + TIMERSHOW = 0b0000'1000, + CREATED = 0b0001'0000, + UNKNOWNBIT6 = 0b0010'0000, // layer gets set to 0 if neither UNKNOWNBIT6 | DRAWOVERPLAYER | DRAWUNDERPLAYER, maybe light layer? + MALE = 0b0100'0000, +}; + +//! NPCPROP_BLOCKFLAGS values. +enum class NPCBlockFlags : uint8_t +{ + BLOCK = 0b0000'0000, + NOBLOCK = 0b0000'0001, + CANBECARRIED = 0b0000'0010, + CANBEPULLED = 0b0000'0100, + CANBEPUSHED = 0b0000'1000, +}; + +//! NPCMOVE_FLAGS values +enum class NPCMoveFlags : uint8_t +{ + NOCACHE = 0b0000'0000, + CACHE = 0b0000'0001, + APPEND = 0b0000'0010, + BLOCKCHECK = 0b0000'0100, + EVENTWHENDONE = 0b0000'1000, + APPLYDIR = 0b0001'0000, +}; + +/// @brief NPC warp restrictions +/// +/// NPCs defaults to ALLOWED since classic mode NPCs could be carried between levels. +/// If an NPC-Server is present, the default is NOTALLOWED. +enum class NPCWarpRestrictions +{ + ALLOWED, + NOTALLOWED, + ONLYOVERWORLD, +}; + +/// @brief NPC storage types. +enum class NPCStorageType +{ + /// @brief NPC stored in the level. + LEVEL, + + /// @brief NPC stored in the database. + DATABASE +}; + +inline constexpr std::string_view NPCTYPE_LOCAL = "LOCALN"sv; +inline constexpr std::string_view NPCTYPE_OBJECT = "OBJECT"sv; +inline constexpr std::string_view NPCTYPE_ITEM = "ITEM"sv; +inline constexpr std::string_view NPCTYPE_CONTROL = "CONTROL"sv; + +//---------------------------- + +/// @brief Holds the results of a move operation. +struct NPCMove +{ + /// @brief The start position of the movement. + PixelPosition origin; + + /// @brief The end position of the movement. + PixelPosition destination; + + /// @brief How many milliseconds have elapsed since the start of the movement. + std::chrono::milliseconds elapsed = 0ms; + + /// @brief How many milliseconds the movement should take. + std::chrono::milliseconds duration = 0ms; + + /// @brief The time the movement was created. + clock::time_point modTime; + + /// @brief The set options for the movement. + std::bitset<5> options; + + /// @brief Callback function to execute when the movement is complete. + std::function onComplete; + + static const int cacheNearbyMovement = 0; // Value: 1 + static const int appendMovement = 1; // Value: 2 + static const int blockCheck = 2; // Value: 4 + static const int informWhenDone = 3; // Value: 8 + static const int applyDirection = 4; // Value: 16 + + /// @brief Returns the current interpolated pixel position based on elapsed time. + /// @return A PixelPosition representing the current position interpolated between the origin and destination, based on the elapsed time. + PixelPosition getCurrentPosition() const noexcept + { + if (elapsed >= duration) + return destination; + + double percent = static_cast(elapsed.count()) / duration.count(); + auto lerpX = std::lerp(origin.x(), destination.x(), percent); + auto lerpY = std::lerp(origin.y(), destination.y(), percent); + return { static_cast(lerpX), static_cast(lerpY) }; + } +}; + +//---------------------------- + +class NPC +{ + friend class FlatFileNPCLoader; + +public: + NPC(NPCID id, NPCStorageType storageType); + ~NPC(); + +public: + /// @brief Records the current state as the initial state of the NPC. + [[inline]] void recordInitialState(); + + /// @brief Resets the NPC to its initial state. + void resetToInitialState(); + +public: + bool warp(LevelPtr level, const PixelPosition& position); + void setLevel(LevelPtr level); + CString getShowImagesPacket(std::optional modTime = std::nullopt) const noexcept; + void sendShowImagesToPlayer(PlayerPtr player, std::optional modTime = std::nullopt) const noexcept; + void sendAllShowImagesToLevel(std::optional modTime = std::nullopt) const noexcept; + void addMoveToQueue(const LocalPixelPosition& moveDelta, float durationInSeconds, uint8_t options); + void processMoveQueue(std::chrono::milliseconds deltaTime); + std::pair getMoveQueuePacketData(std::optional modTime = std::nullopt) const noexcept; + void sendMoveQueueToPlayer(PlayerPtr player, std::optional modTime = std::nullopt) const noexcept; + void sendMoveQueueToLevel(LevelPtr level, std::optional modTime = std::nullopt) const noexcept; + void sendMoveQueueToLevel(LevelPtr level, const std::pair& queue) const noexcept; + void sendMoveQueueUpdatesToLevel(LevelPtr level) noexcept; + void refreshModTimes(clock::time_point modTime) noexcept; + +public: + void hurt(int8_t damageInHalves, std::optional damageEventType = std::nullopt, std::optional source = std::nullopt); + void hurtAndPush(int8_t damageInHalves, const PixelPosition& pushOrigin, std::optional damageEventType = std::nullopt, std::optional source = std::nullopt); + +public: + const std::string& getWeaponName() const noexcept { return m_weaponName; } + bool isCharacter() const noexcept { return image == "#c#"; } + bool hasShape() const noexcept { return shape.width() != 0 || shape.height() != 0 || isCharacter(); } + bool hasImage() const noexcept { return !image.empty() && image != "-"; } + [[inline]] Dimension getComputedShape() const noexcept; + [[inline]] PixelRectangleArea getBoundingBox() const noexcept; + [[inline]] PixelRectangleArea getCollisionBoundingBox() const noexcept; + [[inline]] PixelPosition getGlobalPosition() const noexcept; + [[inline]] LocalPixelPosition getLocalPosition() const noexcept; + [[inline]] TilePosition getTilePosition() const noexcept; + double getCalculatedTileZ() const noexcept; + std::string getLevelName() const; + std::shared_ptr getLevel() const; + std::vector getVariableDump() const; + +public: + void executeEvents(ScriptEventQueue& events, ScriptObject source) const; + void setScript(const Script& script); + void setScript(std::string_view script); + Script& getScript() noexcept { return m_script; } + const Script& getScript() const noexcept { return m_script; } + std::string getClientSideScript() const; + std::string getJoinedClassesList() const; + [[inline]] std::generator> getJoinedClasses(); + bool hasJoinedClass(std::string_view className) const; + void setJoinedClasses(std::string_view classes); + void joinClass(std::string_view className); + void leaveClass(std::string_view className); + void sendScriptUpdatesToLevel(clock::time_point when) const; + void constructScriptParameters(); + string_map scriptParameters; + +protected: + void updateScriptClass(ScriptClass* scriptClass); + +public: + /// @brief Records the current modification time of all properties. + [[inline]] void recordCurrentPropModTime(); + + /// @brief Constructs a PropertyContainer for NPCProp P with the given values. + /// @tparam P The NPCProp that determines the type of container to construct. + /// @param ...values The values to pass to the container's constructor. + /// @return A property container for the specified NPCProp P. + template + [[inline]] PropertyContainer auto constructPropFor(Args... values) const; + + /// @brief Constructs a PropertyContainer for NPCProp prop with the given values. + /// @param prop The NPCProp that determines the type of container to construct. + /// @return A shared pointer to the constructed property's base class. + std::shared_ptr constructPropFor(NPCProp prop) const; + + /// @brief Gets the property container for NPCProp P. + /// @tparam P The NPCProp that determines the type of container to get. + /// @return A property container for the specified NPCProp P. + template + [[inline]] PropertyContainer auto getProp() const; + + /// @brief Gets the property container for NPCProp P. + /// @param prop The NPCProp that determines the type of container to get. + /// @return A shared pointer to the constructed property's base class. + std::shared_ptr getProp(NPCProp prop) const; + + /// @brief Sets a property value for a player and returns the result of the operation. + /// @tparam P The type of the player property to set. + /// @param setBy Specifies who is setting the property. Defaults to SetBy::CLIENT. + /// @param prop A property container that contains the value to set. + /// @return A SetResults value indicating the outcome of the property set operation. + template + [[inline]] SetResults setProp(SetBy setBy, PropertyContainer auto prop); + + /// @brief Sets a property value for a player with the given values and returns the result of the operation. + /// @tparam P The NPCProp that determines the type of property to set. + /// @param setBy Specifies who is setting the property. Defaults to SetBy::CLIENT. + /// @param ...values The values to pass to the property container's constructor. + /// @return A SetResults value indicating the outcome of the property set operation. + template + [[inline]] SetResults setPropWith(SetBy setBy, Args... values); + + /// @brief Sets a property for a player and returns the result of the operation. + /// @param prop The player property to set. + /// @param setBy Indicates who is setting the property. Defaults to SetBy::CLIENT. + /// @param base A shared pointer to the base property value to assign. + /// @return A SetResults value indicating the outcome of the property set operation. + SetResults setProp(NPCProp prop, SetBy setBy, std::shared_ptr base); + + /// @brief Sends the results of setting a property across the network. + /// @param ...results A list of SetResults results to send. + template requires AllSameAs + [[inline]] void sendPropsFromResults(const Results&... results); + + /// @brief Sends the results of setting properties across the network. + /// @param results A range of SetResults results to send. + [[inline]] void sendPropsFromResults(std::ranges::forward_range auto&& results); + + /// @brief Sends the results of setting a property across the network. + /// @param ...results A list of SetResults results to send. + template requires AllSameAs + [[inline]] void sendPropsFromResults(PlayerPtr source, const Results&... results); + + /// @brief Sends the results of setting properties across the network. + /// @param results A range of SetResults results to send. + [[inline]] void sendPropsFromResults(PlayerPtr source, std::ranges::forward_range auto&& results); + +protected: + SetResults setProp(NPCProp prop, SetBy setBy, PropertyBase* base); + void sendPropsFromSendResults(PropertySendResults& results, PlayerPtr source = nullptr) const; + +public: + /// @brief Sets properties from a packet string. + /// @param packet A packet that contains property data. + /// @param source Indicates who is setting the properties. + void setPropsFromPacket(CString& packet, PlayerPtr source = nullptr); + + CString getModifiedPropsPacket() const; + CString getAllPropsPacket(std::optional newTime = std::nullopt) const; + + template + [[inline]] CString getPropsPacketFor() const; + +public: + void testForLinks(SetResults& result); + void testForTouch(SetResults& result); + +public: + const NPCID id; + const NPCStorageType storageType; + std::string level; + std::string groupName; + std::string name; + std::string scripter; + std::string scriptType; + std::string image; + Character character; + Dimension shape; + Rectangle imagePart; + uint8_t visFlags = PROPID(NPCVisFlags::VISIBLE); + uint8_t blockFlags = PROPID(NPCBlockFlags::BLOCK); + float hurtX = 0.0f; + float hurtY = 0.0f; + bool noPlayerOnWall = false; + bool allowServerDamageReactions = false; + std::array saves; + std::chrono::milliseconds timeout = 0ms; + NPCWarpRestrictions warpRestrictions = NPCWarpRestrictions::ALLOWED; + std::array, NPCPROP_COUNT> modTime; + clock::time_point lastUpdateTime; + clock::time_point lastSaveTime; + ScriptContainer scripting; + std::unordered_map showImgList; + std::deque moveQueue; + clock::time_point lastMoveQueueSentTime; + +private: + Server* m_server; + + std::array, NPCPROP_COUNT> m_savedModTime; + bool m_blockPositionUpdates = false; + mutable bool m_hadShowImgs = false; + + Script m_script; + mutable std::vector>> m_joinedClasses; + + std::string m_initialImage; + std::string m_initialLevel; + std::weak_ptr m_currentLevel; + Position m_initialMapPosition; + Character m_initialCharacter; + std::string m_weaponName; +}; + +using NPCPtr = std::shared_ptr; +using NPCWeakPtr = std::weak_ptr; + +//---------------------------- + +inline std::generator> NPC::getJoinedClasses() +{ + auto filter = m_joinedClasses + | std::views::transform([](const auto& pair) { return pair.second.lock(); }) + | std::views::filter([](const auto& scriptClass) { return scriptClass != nullptr; }); + for (auto scriptClass : filter) + co_yield scriptClass; +} + +inline void NPC::recordCurrentPropModTime() +{ + m_savedModTime = modTime; +} + +inline void NPC::recordInitialState() +{ + m_initialImage = image; + m_initialLevel = level; + m_initialCharacter = character; +} + +inline Dimension NPC::getComputedShape() const noexcept +{ + // Unless overridden, characters have a shape of 3 tiles in all directions. + if (isCharacter() && (shape.width() == 0 || shape.height() == 0)) + return { 48, 48, 48 }; + + return shape; +} + +inline PixelRectangleArea NPC::getBoundingBox() const noexcept +{ + return { getGlobalPosition(), getComputedShape() }; +} + +inline PixelRectangleArea NPC::getCollisionBoundingBox() const noexcept +{ + // Character NPCs have a specific bounding box. + // It is a 2x2 square centered on the character's feet, with a height of 3 tiles. + if (isCharacter() && (shape.width() == 0 || shape.height() == 0)) + return { getGlobalPosition().translate(8, 16), { 32, 32, 48 } }; + + return { getGlobalPosition(), shape }; +} + +inline PixelPosition NPC::getGlobalPosition() const noexcept +{ + auto pos = character.getGlobalPosition(); + pos.z() = static_cast(getCalculatedTileZ() * 16); + return pos; +} + +inline LocalPixelPosition NPC::getLocalPosition() const noexcept +{ + auto pos = character.getLocalPosition(); + pos.z() = static_cast(getCalculatedTileZ() * 16); + return pos; +} + +inline TilePosition NPC::getTilePosition() const noexcept +{ + auto pos = character.getTilePosition(); + pos.z() = static_cast(getCalculatedTileZ()); + return pos; +} + +//---------------------------- + +// Defines the mapping of NPCProp to PropertyContainer. +#define FOR_LIST_OF_NPC_PROPS(DO) \ + DO(NPCProp::IMAGE, PropertyString, image) \ + DO(NPCProp::SCRIPT, PropertyGS1Script, getClientSideScript()) \ + DO(NPCProp::X, PropertyTileCoordinate, character.localPixelX) \ + DO(NPCProp::Y, PropertyTileCoordinate, character.localPixelY) \ + DO(NPCProp::POWER, PropertyNumeric, character.hitpointsInHalves) \ + DO(NPCProp::RUPEES, PropertyNumeric, character.gralats) \ + DO(NPCProp::ARROWS, PropertyNumeric, character.arrows) \ + DO(NPCProp::BOMBS, PropertyNumeric, character.bombs) \ + DO(NPCProp::GLOVEPOWER, PropertyNumeric, character.glovePower) \ + DO(NPCProp::BOMBPOWER, PropertyNumeric, character.bombPower) \ + DO(NPCProp::SWORDIMAGE, PropertySwordPower, character.swordImage, character.swordPower) \ + DO(NPCProp::SHIELDIMAGE,PropertyShieldPower, character.shieldImage, character.shieldPower) \ + DO(NPCProp::GANI, PropertyGaniOrBowGif, character.gani, character.bowPower, character.bowImage) \ + DO(NPCProp::VISFLAGS, PropertyNumeric, visFlags) \ + DO(NPCProp::BLOCKFLAGS, PropertyNumeric, blockFlags) \ + DO(NPCProp::MESSAGE, PropertyString, character.chatMessage) \ + DO(NPCProp::HURTDXDY, PropertyHurtDxDy, character.hurtPushDeltaInHalfPixels[0], character.hurtPushDeltaInHalfPixels[1]) \ + DO(NPCProp::ID, PropertyNumeric, id) \ + DO(NPCProp::SPRITE, PropertySprite, character.sprite, character.direction) \ + DO(NPCProp::COLORS, PropertyColors, character.colors) \ + DO(NPCProp::NICKNAME, PropertyString, character.nickName) \ + DO(NPCProp::HORSEIMAGE, PropertyString, character.horseImage) \ + DO(NPCProp::HEADIMAGE, PropertyHeadGif, character.headImage) \ + DO(NPCProp::SAVE0, PropertyNumeric, saves[0]) \ + DO(NPCProp::SAVE1, PropertyNumeric, saves[1]) \ + DO(NPCProp::SAVE2, PropertyNumeric, saves[2]) \ + DO(NPCProp::SAVE3, PropertyNumeric, saves[3]) \ + DO(NPCProp::SAVE4, PropertyNumeric, saves[4]) \ + DO(NPCProp::SAVE5, PropertyNumeric, saves[5]) \ + DO(NPCProp::SAVE6, PropertyNumeric, saves[6]) \ + DO(NPCProp::SAVE7, PropertyNumeric, saves[7]) \ + DO(NPCProp::SAVE8, PropertyNumeric, saves[8]) \ + DO(NPCProp::SAVE9, PropertyNumeric, saves[9]) \ + DO(NPCProp::ALIGNMENT, PropertyNumeric, character.ap) \ + DO(NPCProp::IMAGEPART, PropertyImagePart, imagePart) \ + DO(NPCProp::BODYIMAGE, PropertyString, character.bodyImage) \ + DO(NPCProp::GATTRIB1, PropertyString, character.ganiAttributes[0]) \ + DO(NPCProp::GATTRIB2, PropertyString, character.ganiAttributes[1]) \ + DO(NPCProp::GATTRIB3, PropertyString, character.ganiAttributes[2]) \ + DO(NPCProp::GATTRIB4, PropertyString, character.ganiAttributes[3]) \ + DO(NPCProp::GATTRIB5, PropertyString, character.ganiAttributes[4]) \ + DO(NPCProp::GMAPLEVELX, PropertyNumeric, character.mapX) \ + DO(NPCProp::GMAPLEVELY, PropertyNumeric, character.mapY) \ + DO(NPCProp::Z, PropertyTileCoordinateZ, character.localPixelZ) \ + DO(NPCProp::GATTRIB6, PropertyString, character.ganiAttributes[5]) \ + DO(NPCProp::GATTRIB7, PropertyString, character.ganiAttributes[6]) \ + DO(NPCProp::GATTRIB8, PropertyString, character.ganiAttributes[7]) \ + DO(NPCProp::GATTRIB9, PropertyString, character.ganiAttributes[8]) \ + DO(NPCProp::UNKNOWN48, PropertyVoid) \ + DO(NPCProp::SCRIPTER, PropertyString, scripter) \ + DO(NPCProp::NAME, PropertyString, name) \ + DO(NPCProp::TYPE, PropertyString, scriptType) \ + DO(NPCProp::CURLEVEL, PropertyString, getLevelName()) \ + DO(NPCProp::GATTRIB10, PropertyString, character.ganiAttributes[9]) \ + DO(NPCProp::GATTRIB11, PropertyString, character.ganiAttributes[10]) \ + DO(NPCProp::GATTRIB12, PropertyString, character.ganiAttributes[11]) \ + DO(NPCProp::GATTRIB13, PropertyString, character.ganiAttributes[12]) \ + DO(NPCProp::GATTRIB14, PropertyString, character.ganiAttributes[13]) \ + DO(NPCProp::GATTRIB15, PropertyString, character.ganiAttributes[14]) \ + DO(NPCProp::GATTRIB16, PropertyString, character.ganiAttributes[15]) \ + DO(NPCProp::GATTRIB17, PropertyString, character.ganiAttributes[16]) \ + DO(NPCProp::GATTRIB18, PropertyString, character.ganiAttributes[17]) \ + DO(NPCProp::GATTRIB19, PropertyString, character.ganiAttributes[18]) \ + DO(NPCProp::GATTRIB20, PropertyString, character.ganiAttributes[19]) \ + DO(NPCProp::GATTRIB21, PropertyString, character.ganiAttributes[20]) \ + DO(NPCProp::GATTRIB22, PropertyString, character.ganiAttributes[21]) \ + DO(NPCProp::GATTRIB23, PropertyString, character.ganiAttributes[22]) \ + DO(NPCProp::GATTRIB24, PropertyString, character.ganiAttributes[23]) \ + DO(NPCProp::GATTRIB25, PropertyString, character.ganiAttributes[24]) \ + DO(NPCProp::GATTRIB26, PropertyString, character.ganiAttributes[25]) \ + DO(NPCProp::GATTRIB27, PropertyString, character.ganiAttributes[26]) \ + DO(NPCProp::GATTRIB28, PropertyString, character.ganiAttributes[27]) \ + DO(NPCProp::GATTRIB29, PropertyString, character.ganiAttributes[28]) \ + DO(NPCProp::GATTRIB30, PropertyString, character.ganiAttributes[29]) \ + DO(NPCProp::CLASS, PropertyLongString, getJoinedClassesList()) \ + DO(NPCProp::X2, PropertyPixelCoordinate, character.localPixelX) \ + DO(NPCProp::Y2, PropertyPixelCoordinate, character.localPixelY) \ + DO(NPCProp::Z2, PropertyPixelCoordinate, character.localPixelZ) + +//---------------------------- + +template +PropertyContainer auto NPC::constructPropFor(Args... values) const +{ +#define RETURN_CONSTRUCTPROPSFOR_CONSTEXPR(prop, type, ...) if constexpr (P == prop) return type{ values... }; + FOR_LIST_OF_NPC_PROPS(RETURN_CONSTRUCTPROPSFOR_CONSTEXPR); + + throw std::invalid_argument("Invalid NPCProp type in constructPropFor"); +} + +template +PropertyContainer auto NPC::getProp() const +{ +#define RETURN_GETPROP_CONSTEXPR(prop, type, ...) if constexpr (P == prop) return type{ __VA_ARGS__ }; + FOR_LIST_OF_NPC_PROPS(RETURN_GETPROP_CONSTEXPR); + + throw std::invalid_argument("Invalid NPCProp type in getProp"); +} + +template +SetResults NPC::setProp(SetBy setBy, PropertyContainer auto prop) +{ + return setProp(P, setBy, &prop); +} + +template +SetResults NPC::setPropWith(SetBy setBy, Args... values) +{ + return setProp

(setBy, constructPropFor

(values...)); +} + +template requires AllSameAs +void NPC::sendPropsFromResults(const Results&... results) +{ + PropertySendResults send_results; + (send_results.emplace_back(results, nullptr), ...); + sendPropsFromSendResults(send_results); +} + +void NPC::sendPropsFromResults(std::ranges::forward_range auto&& results) +{ + PropertySendResults send_results; + auto results_range = results | std::views::transform([](const SetResults& results) { return std::make_pair(results, nullptr); }); + for (const auto& r : results_range) + send_results.emplace_back(r); + + sendPropsFromSendResults(send_results); +} + +template requires AllSameAs +void NPC::sendPropsFromResults(PlayerPtr source, const Results&... results) +{ + PropertySendResults send_results; + (send_results.emplace_back(results, nullptr), ...); + sendPropsFromSendResults(send_results, source); +} + +void NPC::sendPropsFromResults(PlayerPtr source, std::ranges::forward_range auto&& results) +{ + PropertySendResults send_results; + auto results_range = results | std::views::transform([](const SetResults& results) { return std::make_pair(results, nullptr); }); + for (const auto& r : results_range) + send_results.emplace_back(r); + + sendPropsFromSendResults(send_results, source); +} + +template +inline CString NPC::getPropsPacketFor() const +{ + CString packet; + ((packet >> (char)Props << getProp().serialize()), ...); + return packet; +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // NPC_H diff --git a/server/include/object/Player.h b/server/include/object/Player.h new file mode 100644 index 000000000..4e8f3d069 --- /dev/null +++ b/server/include/object/Player.h @@ -0,0 +1,752 @@ +#ifndef PLAYER_H +#define PLAYER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace preagonal::props; + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class Level; +class SubLevel; +class StaticLevelData; +class Map; +class Weapon; + +//---------------------------- + +enum class LevelItemType; + +enum +{ + PLSETPROPS_SETBYPLAYER = 0x01, // if set, do serverside checks to prevent attributes from being changed + PLSETPROPS_FORWARD = 0x02, // forward data to other players + PLSETPROPS_FORWARDSELF = 0x04, // forward data back to the player +}; + +enum class CursorNumbers : uint8_t +{ + DEFAULT = 0, + HIDDEN = 1, + NORMAL = 2, + CROSS = 3, + TEXT = 4, + HIDDEN_2 = 5, + RESIZE_LL_UR = 6, + RESIZE_UD = 7, + RESIZE_UL_LR = 8, + RESIZE_LR = 9, + UP_ARROW = 10, + HOURGLASS = 11, + FILE = 12, + NOT_ALLOWED = 13, + BREAK_ADJUST_LR = 14, + BREAK_ADJUST_UD = 15, + MULTIPLE_FILES = 16, + SQL_HOURGLASS = 17, + NOT_ALLOWED_2 = 18, + MOUSE_HOURGLASS = 19, + MOUSE_QUESTION = 20, + POINTING_HAND = 21, + FOUR_DIR_ARROW = 22, + + COUNT +}; + +enum class GameFeatureFlags : uint32_t +{ + M_MAP = 0x0001, + P_PAUSE = 0x0002, + Q_WEAPONSELECT = 0x0004, + R_SHOWRATING = 0x0008, + SA_DROPITEM = 0x0010, + SD_SWITCHWEAPON = 0x0020, + TAB_CHAT = 0x0040, + CHATMESSAGE = 0x0080, + HEARTSOVERPLAYERS = 0x0100, + NICKNAMES = 0x0200, + TOALL_PM_BUBBLES = 0x0400, + OPEN_PROFILE = 0x0800, + EMOTICONS = 0x1000, + LEVELSNAPSHOTS = 0x2000, // ALT+5 + LEVELZOOMING = 0x4000, // ALT+8/9 + LOGFRAME = 0x8000, // F2 (savelog() / echo()) + ALLFEATURES = 0xFFFF +}; + +enum class GameStatsFlags : uint32_t +{ + ASD_KEYS = 0x0001, + ICONS = 0x0002, // (for gralats, bombs, arrows, etc.) + GRALAT_COUNT = 0x0004, + BOMB_COUNT = 0x0008, + ARROW_COUNT = 0x0010, + HEART_COUNT = 0x0020, + ALIGNMENT = 0x0040, + MAGIC = 0x0080, + MINIMAP = 0x0100, // ALT+3 + INVENTORY = 0x0200, + PLAYERS = 0x0400, + OPEN_PROFILE = 0x0800, + ALLSTATS = 0xFFFF +}; + +enum class CarryObjectSprite : uint8_t +{ + BOMB = 0, + BUSH = 1, + STONE = 3, + VASE = 5, + SIGN = 7, + SUPERBOMB = 61, + JOLTBOMB = 87, + HOTJOLTBOMB = 88, + HOTBOMB = 200, + BLACKSTONE = 201, + NPC = 251, + NONE = 255 +}; + +//---------------------------- + +struct ShootPacketWrapper +{ + NPCID source; + PixelPosition position; + int8_t offsetx; + int8_t offsety; + uint8_t sangle; + uint8_t sanglez; + uint8_t power; + uint8_t gravity; + std::string gani; + std::string shootParams; + + CString constructShootV1() const; + CString constructShootV2() const; +}; + +//---------------------------- + +class Server; +class Player : public CSocketStub, public IPacketHandler, public std::enable_shared_from_this +{ +public: + // Required by CSocketStub. + virtual bool onRecv() override; + virtual bool onSend() override; + virtual bool onRegister() override { return true; } + virtual void onUnregister() override; + virtual SOCKET getSocketHandle() override { return m_playerSock->getHandle(); } + virtual bool canRecv() override; + virtual bool canSend() override; + + // Constructor - Deconstructor + Player(CSocket* pSocket, PlayerID pId); + virtual ~Player(); + virtual void cleanup(); + + // Main methods. + virtual void doMain(); + virtual bool doTimedEvents(); + + // Manage Account + bool isLoggedIn() const; + virtual bool handleLogin(CString& pPacket); + virtual bool sendLogin(); + + // Get Properties + CSocket* getSocket() { return m_playerSock; } + [[inline]] PlayerID getId() const; + clock::time_point getLastData() const { return m_lastData; } + CString getGuild() const { return m_guild; } + int getVersion() const { return m_versionId; } + const std::string& getVersionStr() const { return m_version; } + const std::string& getServerName() const { return m_serverName; } + const std::string& getPlatform() const { return m_os; } + [[inline]] std::string_view getLanguage() const; + int64_t getDeviceId() const { return m_deviceId; } + NPCID getCarryNPC() const { return m_carryNPC; } + NPCID getAttachedNPC() const { return m_attachNPC; } + uint8_t getCarrySprite() const { return m_carrySprite; } + [[inline]] PixelRectangleArea getBoundingBox() const noexcept; + [[inline]] PixelRectangleArea getCollisionBoundingBox() const noexcept; + [[inline]] PixelPosition getGlobalPosition() const noexcept; + [[inline]] LocalPixelPosition getLocalPosition() const noexcept; + [[inline]] TilePosition getTilePosition() const noexcept; + [[inline]] PixelPosition getSubLevelOrigin() const noexcept; + [[inline]] MapPosition getMapPosition() const noexcept; + virtual double getCalculatedTileZ() const noexcept; + virtual std::string getLevelName() const { return account.level; } + + // Set Properties + void setNick(CString pNickName, bool force = false); + void setId(PlayerID pId); + void setLoaded(bool loaded) { this->m_loaded = loaded; } + void setServerName(CString& tmpServerName) { m_serverName = tmpServerName; } + void setChat(const CString& pChat); + void setDeviceId(int64_t newDeviceId) { m_deviceId = newDeviceId; } + void setCarryNPC(NPCID id) { m_carryNPC = id; } + +public: + /// @brief Records the current modification time of all properties. + [[inline]] void recordCurrentPropModTime(); + + /// @brief Constructs a PropertyContainer for PlayerProp P with the given values. + /// @tparam P The PlayerProp that determines the type of container to construct. + /// @param ...values The values to pass to the container's constructor. + /// @return A property container for the specified PlayerProp P. + template + [[inline]] PropertyContainer auto constructPropFor(Args... values) const; + + /// @brief Constructs a PropertyContainer for PlayerProp prop with the given values. + /// @param prop The PlayerProp that determines the type of container to construct. + /// @return A shared pointer to the constructed property's base class. + std::shared_ptr constructPropFor(PlayerProp prop) const; + + /// @brief Gets the property container for PlayerProp P. + /// @tparam P The PlayerProp that determines the type of container to get. + /// @return A property container for the specified PlayerProp P. + template + [[inline]] PropertyContainer auto getProp() const; + + /// @brief Gets the property container for PlayerProp P. + /// @param prop The PlayerProp that determines the type of container to get. + /// @return A shared pointer to the constructed property's base class. + std::shared_ptr getProp(PlayerProp prop) const; + + /// @brief Sets a property value for a player and returns the result of the operation. + /// @tparam P The type of the player property to set. + /// @param setBy Specifies who is setting the property. + /// @param prop A property container that contains the value to set. + /// @return A SetResults value indicating the outcome of the property set operation. + template + [[inline]] SetResults setProp(SetBy setBy, PropertyContainer auto prop); + + /// @brief Sets a property for a player and returns the result of the operation. + /// @param prop The player property to set. + /// @param base A shared pointer to the base property value to assign. + /// @param setBy Indicates who is setting the property. + /// @return A SetResults value indicating the outcome of the property set operation. + SetResults setProp(PlayerProp prop, SetBy setBy, std::shared_ptr base); + + /// @brief Sets a property value for a player with the given values and returns the result of the operation. + /// @tparam P The PlayerProp that determines the type of property to set. + /// @param setBy Specifies who is setting the property. + /// @param ...values The values to pass to the property container's constructor. + /// @return A SetResults value indicating the outcome of the property set operation. + template + [[inline]] SetResults setPropWith(SetBy setBy, Args... values); + + /// @brief Sends the results of setting a property across the network. + /// @param ...results A list of SetResults results to send. + template requires AllSameAs + [[inline]] void sendPropsFromResults(const Results&... results); + + /// @brief Sends the results of setting properties across the network. + /// @param results A range of SetResults results to send. + void sendPropsFromResults(std::ranges::forward_range auto&& results); + +public: + /// @brief Sets properties from a packet string. + /// @param packet A packet that contains property data. + /// @param setBy Indicates who is setting the properties, either the client or server. + /// @param originator Who is the originator of the property set request, if applicable. + void setPropsFromPacket(CString& packet, SetBy setBy, Player* originator = nullptr); + + /// @brief Sets properties from a remote control packet. + /// @param packet A packet that contains property data. + /// @param rc The remote control player, if applicable. + void setPropsFromRCPacket(CString& packet, Player* rc = nullptr); + + /// @brief Retrieves a packet containing properties from a list of properties. + /// @param props A list of properties to include in the packet. + /// @return A packet of properties. + CString getPropsPacketFromList(const PropList& props) const; + + /// @brief Gets a packet containing properties for remote control profile viewing. + /// @return A packet of properties. + CString getPropsForRCPacket(); + + /// @brief Exchanges the properties of the current player with other players. + void exchangeMyPropsWithOthers(); + + /// @brief Gets a packet containing modified properties. + /// @return A packet of modified properties. + CString getModifiedPropsPacket() const; + +public: + void constructScriptParameters(); + string_map scriptParameters; + +public: + bool deleteFlag(std::string_view flagName, bool sendToPlayer = false); + bool setFlag(std::string_view flagPair, bool sendToPlayers = false); + bool setFlag(std::string_view flagName, std::optional flagValue, bool sendToPlayer = false); + +public: + virtual bool warp(std::string_view levelName, const PixelPosition& position, std::optional clientCachedTime = std::nullopt); + virtual bool warp(std::shared_ptr level, const PixelPosition& position, std::optional clientCachedTime = std::nullopt); + virtual bool enterLevel(std::shared_ptr level, const PixelPosition& position, std::optional clientCachedTime = std::nullopt); + virtual bool enterLevel(std::shared_ptr level, const MapPosition& mapPosition, const LocalPixelPosition& position, std::optional clientCachedTime = std::nullopt); + virtual bool enterLevel(std::shared_ptr level, std::optional clientCachedTime = std::nullopt); + virtual bool leaveLevel(); + virtual bool leaveSubLevel(std::shared_ptr subLevel); + virtual bool sendStaticLevelData(std::shared_ptr staticLevelData, std::shared_ptr subLevel, std::optional clientCachedTime = std::nullopt); + virtual bool sendDynamicLevelData(std::shared_ptr level, std::optional clientCachedTime = std::nullopt); + virtual bool sendNearbyObjects(std::shared_ptr level); + +public: + // Socket-Functions + void sendPacket(CString pPacket, bool appendNL = true); + bool sendFile(const std::filesystem::path& file); + void setReceivedBuffer(const CString& buffer) { m_recvBuffer = buffer; } + + // Type of player + bool isAdminIp(); + bool isStaff(); + bool isJailed(); + bool isNC() const { return (m_type & PLTYPE_ANYNC) != 0; } + bool isRC() const { return (m_type & PLTYPE_ANYRC) != 0; } + bool isClient() const { return (m_type & PLTYPE_ANYCLIENT) != 0; } + bool isNPCServer() const { return (m_type & PLTYPE_NPCSERVER) != 0; } + bool isControlClient() const { return (m_type & PLTYPE_ANYCONTROL) != 0; } + bool isHiddenClient() const { return (m_type & PLTYPE_NONITERABLE) != 0; } + bool isLoaded() const { return m_loaded; } + bool isGuest() const { return account.loadOnly && account.communityName == "guest"; } + int getType() const { return m_type; } + void setType(int val) { m_type = val; } + void setExternal(bool val) { m_isExternal = val; } + + bool addWeapon(LevelItemType defaultWeapon); + bool addWeapon(std::string_view name); + bool addWeapon(std::shared_ptr weapon); + bool deleteWeapon(LevelItemType defaultWeapon); + bool deleteWeapon(std::string_view name); + bool deleteWeapon(std::shared_ptr weapon); + + std::string translate(std::string_view key) const; + + virtual void sendPrivateMessage(PlayerID from, std::string_view message); + + // Misc functions. + void disconnect(); + + bool addPMServer(CString& option); + bool remPMServer(CString& option); + bool inChatChannel(const std::string& channel) const; + bool addChatChannel(const std::string& channel); + bool removeChatChannel(const std::string& channel); + bool updatePMPlayers(CString& servername, CString& players); + bool pmExternalPlayer(CString servername, CString account, CString& pmMessage); + std::vector getPMServerList(); + std::shared_ptr getExternalPlayer(const PlayerID id, bool includeRC = true) const; + std::shared_ptr getExternalPlayer(const CString& account, bool includeRC = true) const; + +public: + Account account; + std::array, PLAYERPROP_COUNT> modTime; + uint32_t loginTime = 0; + uint32_t lastDeadTime = 0; + +protected: + SetResults setProp(PlayerProp prop, SetBy setBy, PropertyBase* base); + bool checkPropSetAccess(PlayerProp prop, SetBy setBy, Player* originator) const; + void sendPropsFromResults(PropertySendResults& results); + +protected: + virtual std::string_view whoAmI() const noexcept override { return account.name; } + virtual HandlePacketResult handlePacket(std::optional id, CString& packet) override; + +public: + // Packet-Functions + HandlePacketResult msgPLI_NULL(CString& pPacket); + HandlePacketResult msgPLI_LOGIN(CString& pPacket); + HandlePacketResult msgWebSocketInit(CString& pPacket); + + //HandlePacketResult msgPLI_LEVELWARP(CString& pPacket); + //HandlePacketResult msgPLI_BOARDMODIFY(CString& pPacket); + //HandlePacketResult msgPLI_REQUESTUPDATEBOARD(CString& pPacket); + HandlePacketResult msgPLI_PLAYERPROPS(CString& pPacket); + //HandlePacketResult msgPLI_NPCPROPS(CString& pPacket); + //HandlePacketResult msgPLI_BOMBADD(CString& pPacket); + //HandlePacketResult msgPLI_BOMBDEL(CString& pPacket); + HandlePacketResult msgPLI_TOALL(CString& pPacket); + //HandlePacketResult msgPLI_HORSEADD(CString& pPacket); + //HandlePacketResult msgPLI_HORSEDEL(CString& pPacket); + //HandlePacketResult msgPLI_ARROWADD(CString& pPacket); + //HandlePacketResult msgPLI_FIRESPY(CString& pPacket); + //HandlePacketResult msgPLI_THROWCARRIED(CString& pPacket); + //HandlePacketResult msgPLI_ITEMADD(CString& pPacket); + //HandlePacketResult msgPLI_ITEMDEL(CString& pPacket); + //HandlePacketResult msgPLI_CLAIMPKER(CString& pPacket); + //HandlePacketResult msgPLI_BADDYPROPS(CString& pPacket); + //HandlePacketResult msgPLI_BADDYHURT(CString& pPacket); + //HandlePacketResult msgPLI_BADDYADD(CString& pPacket); + //HandlePacketResult msgPLI_FLAGSET(CString& pPacket); + //HandlePacketResult msgPLI_FLAGDEL(CString& pPacket); + //HandlePacketResult msgPLI_OPENCHEST(CString& pPacket); + //HandlePacketResult msgPLI_PUTNPC(CString& pPacket); + //HandlePacketResult msgPLI_NPCDEL(CString& pPacket); + //HandlePacketResult msgPLI_WANTFILE(CString& pPacket); + //HandlePacketResult msgPLI_SHOWIMGPLAYER(CString& pPacket); + // PLI_UNKNOWN25 + //HandlePacketResult msgPLI_HURTPLAYER(CString& pPacket); + //HandlePacketResult msgPLI_EXPLOSION(CString& pPacket); + HandlePacketResult msgPLI_PRIVATEMESSAGE(CString& pPacket); + //HandlePacketResult msgPLI_NPCWEAPONDEL(CString& pPacket); + HandlePacketResult msgPLI_PACKETCOUNT(CString& pPacket); + //HandlePacketResult msgPLI_WEAPONADD(CString& pPacket); + //HandlePacketResult msgPLI_UPDATEFILE(CString& pPacket); + //HandlePacketResult msgPLI_ADJACENTLEVEL(CString& pPacket); + //HandlePacketResult msgPLI_HITOBJECTS(CString& pPacket); + HandlePacketResult msgPLI_LANGUAGE(CString& pPacket); + //HandlePacketResult msgPLI_TRIGGERACTION(CString& pPacket); + //HandlePacketResult msgPLI_TAMPERCHECK(CString& pPacket); + //HandlePacketResult msgPLI_SHOOT(CString& pPacket); + //HandlePacketResult msgPLI_SHOOT2(CString& pPacket); + //HandlePacketResult msgPLI_SERVERWARP(CString& pPacket); + //HandlePacketResult msgPLI_PROCESSLIST(CString& pPacket); + //HandlePacketResult msgPLI_ENTERLEVEL(CString& pPacket); + //HandlePacketResult msgPLI_VERIFYWANTSEND(CString& pPacket); + //HandlePacketResult msgPLI_UPDATECLASS(CString& pPacket); + //HandlePacketResult msgPLI_RAWDATA(CString& pPacket); + + HandlePacketResult msgPLI_PROFILEGET(CString& pPacket); + HandlePacketResult msgPLI_PROFILESET(CString& pPacket); + + //HandlePacketResult msgPLI_NPCSERVERQUERY(CString& pPacket); + + HandlePacketResult msgPLI_REQUESTTEXT(CString& pPacket); + HandlePacketResult msgPLI_SENDTEXT(CString& pPacket); + + //HandlePacketResult msgPLI_UPDATEGANI(CString& pPacket); + //HandlePacketResult msgPLI_UPDATESCRIPT(CString& pPacket); + //HandlePacketResult msgPLI_UPDATEPACKAGEREQUESTFILE(CString& pPacket); + +protected: + // Cyclic, we have to create in the constructor. + // BabyDI_INJECT(Server, m_server); + Server* m_server = nullptr; + + // Socket Variables + CSocket* m_playerSock; + CString m_recvBuffer; + + // Variables + PlayerID m_id = 0; + int m_type = PLTYPE_AWAIT; + int m_versionId = CLVER_UNKNOWN; + std::string m_version; + std::string m_os{ "wind" }; + std::string m_serverName; + uint8_t m_statusMsg = 0; + uint8_t m_additionalFlags = 0; + uint32_t m_envCodePage = 1252; + std::set m_channelList; + clock::time_point m_lastData; + uint8_t m_encryptionKey = 0; + int64_t m_accountIp = 0; + uint16_t m_udpport = 0; + int64_t m_deviceId = 0; + std::array, PLAYERPROP_COUNT> m_savedModTime; + + uint8_t m_horseBombCount = 0; + uint8_t m_carrySprite = 0xFF; + PlayerListCategory m_playerListCategory = PlayerListCategory::PLAYERLIST; + NPCID m_attachNPC = 0; + NPCID m_carryNPC = 0; + std::array m_effectColors{ 0, 0, 0, 0, 0 }; + + std::vector m_privateMessageServerList; + std::unordered_map> m_externalPlayers; + IdGenerator m_externalPlayerIdGenerator{ PLAYERID_GEN_EXTERNAL }; + + bool m_loaded = false; + bool m_isExternal = false; + CString m_npcserverPort; + CString m_guild; + + // File queue. + CFileQueue m_fileQueue; + + int getVersionIDByVersion(const CString& versionInput) const; +}; + +using PlayerPtr = std::shared_ptr; +using PlayerWeakPtr = std::weak_ptr; + +//---------------------------- + +template +concept DerivedFromPlayer = std::is_base_of_v; + +template +auto players_of_type(const std::unordered_map& range) +{ + using newpair = std::pair>; + return range + | std::views::filter([](auto& kvp) { return dynamic_cast(kvp.second.get()) != nullptr; }) + | std::views::transform([](auto& kvp) { return newpair(kvp.first, std::dynamic_pointer_cast

(kvp.second)); }); +} + +inline bool Player::isLoggedIn() const +{ + return (m_type != PLTYPE_AWAIT && m_id > 0); +} + +inline PlayerID Player::getId() const +{ + return m_id; +} + +inline void Player::setId(PlayerID pId) +{ + m_id = pId; +} + +inline PixelRectangleArea Player::getBoundingBox() const noexcept +{ + return { getGlobalPosition(), { 48, 48, 48 } }; +} + +inline PixelRectangleArea Player::getCollisionBoundingBox() const noexcept +{ + return { getGlobalPosition().translate(8, 16), { 32, 32, 48 }}; +} + +inline PixelPosition Player::getGlobalPosition() const noexcept +{ + auto pos = account.character.getGlobalPosition(); + pos.z() = static_cast(getCalculatedTileZ() * 16); + return pos; +} + +inline LocalPixelPosition Player::getLocalPosition() const noexcept +{ + auto pos = account.character.getLocalPosition(); + pos.z() = static_cast(getCalculatedTileZ() * 16); + return pos; +} + +inline TilePosition Player::getTilePosition() const noexcept +{ + auto pos = account.character.getTilePosition(); + pos.z() = getCalculatedTileZ(); + return pos; +} + +inline PixelPosition Player::getSubLevelOrigin() const noexcept +{ + return PixelPosition{ account.character.mapX * 1024, account.character.mapY * 1024, 0 }; +} + +inline MapPosition Player::getMapPosition() const noexcept +{ + return MapPosition{ account.character.mapX, account.character.mapY, 0 }; +} + +inline bool Player::inChatChannel(const std::string& channel) const +{ + return m_channelList.find(channel) != m_channelList.end(); +} + +inline bool Player::addChatChannel(const std::string& channel) +{ + auto res = m_channelList.insert(channel); + return res.second; +} + +inline bool Player::removeChatChannel(const std::string& channel) +{ + m_channelList.erase(channel); + return false; +} + +inline std::string_view Player::getLanguage() const +{ + return account.language; +} + +//---------------------------- + +inline void Player::recordCurrentPropModTime() +{ + m_savedModTime = modTime; +} + +//---------------------------- + +// Defines the mapping of PlayerProp to PropertyContainer. +#define FOR_LIST_OF_PLAYER_PROPS(DO) \ + DO(PlayerProp::NICKNAME, PropertyString, account.character.nickName) \ + DO(PlayerProp::MAXPOWER, PropertyNumeric, account.maxHitpoints) \ + DO(PlayerProp::CURPOWER, PropertyNumeric, account.character.hitpointsInHalves) \ + DO(PlayerProp::RUPEESCOUNT, PropertyNumeric, account.character.gralats) \ + DO(PlayerProp::ARROWSCOUNT, PropertyNumeric, account.character.arrows) \ + DO(PlayerProp::BOMBSCOUNT, PropertyNumeric, account.character.bombs) \ + DO(PlayerProp::GLOVEPOWER, PropertyNumeric, account.character.glovePower) \ + DO(PlayerProp::BOMBPOWER, PropertyNumeric, account.character.bombPower) \ + DO(PlayerProp::SWORDPOWER, PropertySwordPower, account.character.swordImage, account.character.swordPower) \ + DO(PlayerProp::SHIELDPOWER, PropertyShieldPower, account.character.shieldImage, account.character.shieldPower) \ + DO(PlayerProp::GANI, PropertyGaniOrBowGif, account.character.gani, account.character.bowPower, account.character.bowImage) \ + DO(PlayerProp::HEADGIF, PropertyHeadGif, account.character.headImage) \ + DO(PlayerProp::CURCHAT, PropertyString, account.character.chatMessage) \ + DO(PlayerProp::COLORS, PropertyColors, account.character.colors) \ + DO(PlayerProp::ID, PropertyNumeric, m_id) \ + DO(PlayerProp::X, PropertyTileCoordinate, account.character.localPixelX) \ + DO(PlayerProp::Y, PropertyTileCoordinate, account.character.localPixelY) \ + DO(PlayerProp::SPRITE, PropertySprite, account.character.sprite, account.character.direction) \ + DO(PlayerProp::STATUS, PropertyNumeric, account.status) \ + DO(PlayerProp::CARRYSPRITE, PropertyUnsafeByte, m_carrySprite) \ + DO(PlayerProp::CURLEVEL, PropertyString, getLevelName()) \ + DO(PlayerProp::HORSEGIF, PropertyString, account.character.horseImage) \ + DO(PlayerProp::HORSEBUSHES, PropertyNumeric, m_horseBombCount) \ + DO(PlayerProp::EFFECTCOLORS,PropertyEffectColors, m_effectColors) \ + DO(PlayerProp::CARRYNPC, PropertyNumeric, m_carryNPC) \ + DO(PlayerProp::APCOUNTER, PropertyNumeric, account.apCounter) \ + DO(PlayerProp::MAGICPOINTS, PropertyNumeric, account.character.mp) \ + DO(PlayerProp::KILLSCOUNT, PropertyNumeric, account.kills) \ + DO(PlayerProp::DEATHSCOUNT, PropertyNumeric, account.deaths) \ + DO(PlayerProp::ONLINESECS, PropertyNumeric, account.onlineSeconds) \ + DO(PlayerProp::IPADDR, PropertyNumeric, m_accountIp) \ + DO(PlayerProp::UDPPORT, PropertyNumeric, m_udpport) \ + DO(PlayerProp::ALIGNMENT, PropertyNumeric, account.character.ap) \ + DO(PlayerProp::ADDITFLAGS, PropertyNumeric, m_additionalFlags) \ + DO(PlayerProp::ACCOUNTNAME, PropertyString, account.name) \ + DO(PlayerProp::BODYIMG, PropertyString, account.character.bodyImage) \ + DO(PlayerProp::RATING, PropertyEloRating, account.eloRating, account.eloDeviation) \ + DO(PlayerProp::GATTRIB1, PropertyString, account.character.ganiAttributes[0]) \ + DO(PlayerProp::GATTRIB2, PropertyString, account.character.ganiAttributes[1]) \ + DO(PlayerProp::GATTRIB3, PropertyString, account.character.ganiAttributes[2]) \ + DO(PlayerProp::GATTRIB4, PropertyString, account.character.ganiAttributes[3]) \ + DO(PlayerProp::GATTRIB5, PropertyString, account.character.ganiAttributes[4]) \ + DO(PlayerProp::ATTACHNPC, PropertyAttachNPC, m_attachNPC) \ + DO(PlayerProp::GMAPLEVELX, PropertyNumeric, account.character.mapX) \ + DO(PlayerProp::GMAPLEVELY, PropertyNumeric, account.character.mapY) \ + DO(PlayerProp::Z, PropertyTileCoordinateZ, account.character.localPixelZ) \ + DO(PlayerProp::GATTRIB6, PropertyString, account.character.ganiAttributes[5]) \ + DO(PlayerProp::GATTRIB7, PropertyString, account.character.ganiAttributes[6]) \ + DO(PlayerProp::GATTRIB8, PropertyString, account.character.ganiAttributes[7]) \ + DO(PlayerProp::GATTRIB9, PropertyString, account.character.ganiAttributes[8]) \ + DO(PlayerProp::JOINLEAVELVL,PropertyNumeric, 1_ui8) \ + DO(PlayerProp::DISCONNECT, PropertyVoid) \ + DO(PlayerProp::LANGUAGE, PropertyString, account.language) \ + DO(PlayerProp::PLAYERLISTSTATUS, PropertyNumeric, m_statusMsg) \ + DO(PlayerProp::GATTRIB10, PropertyString, account.character.ganiAttributes[9]) \ + DO(PlayerProp::GATTRIB11, PropertyString, account.character.ganiAttributes[10]) \ + DO(PlayerProp::GATTRIB12, PropertyString, account.character.ganiAttributes[11]) \ + DO(PlayerProp::GATTRIB13, PropertyString, account.character.ganiAttributes[12]) \ + DO(PlayerProp::GATTRIB14, PropertyString, account.character.ganiAttributes[13]) \ + DO(PlayerProp::GATTRIB15, PropertyString, account.character.ganiAttributes[14]) \ + DO(PlayerProp::GATTRIB16, PropertyString, account.character.ganiAttributes[15]) \ + DO(PlayerProp::GATTRIB17, PropertyString, account.character.ganiAttributes[16]) \ + DO(PlayerProp::GATTRIB18, PropertyString, account.character.ganiAttributes[17]) \ + DO(PlayerProp::GATTRIB19, PropertyString, account.character.ganiAttributes[18]) \ + DO(PlayerProp::GATTRIB20, PropertyString, account.character.ganiAttributes[19]) \ + DO(PlayerProp::GATTRIB21, PropertyString, account.character.ganiAttributes[20]) \ + DO(PlayerProp::GATTRIB22, PropertyString, account.character.ganiAttributes[21]) \ + DO(PlayerProp::GATTRIB23, PropertyString, account.character.ganiAttributes[22]) \ + DO(PlayerProp::GATTRIB24, PropertyString, account.character.ganiAttributes[23]) \ + DO(PlayerProp::GATTRIB25, PropertyString, account.character.ganiAttributes[24]) \ + DO(PlayerProp::GATTRIB26, PropertyString, account.character.ganiAttributes[25]) \ + DO(PlayerProp::GATTRIB27, PropertyString, account.character.ganiAttributes[26]) \ + DO(PlayerProp::GATTRIB28, PropertyString, account.character.ganiAttributes[27]) \ + DO(PlayerProp::GATTRIB29, PropertyString, account.character.ganiAttributes[28]) \ + DO(PlayerProp::GATTRIB30, PropertyString, account.character.ganiAttributes[29]) \ + DO(PlayerProp::OSTYPE, PropertyString, m_os) \ + DO(PlayerProp::TEXTCODEPAGE,PropertyNumeric, m_envCodePage) \ + DO(PlayerProp::ONLINESECS2, PropertyNumeric) \ + DO(PlayerProp::X2, PropertyPixelCoordinate, account.character.localPixelX) \ + DO(PlayerProp::Y2, PropertyPixelCoordinate, account.character.localPixelY) \ + DO(PlayerProp::Z2, PropertyPixelCoordinate, account.character.localPixelZ) \ + DO(PlayerProp::PLAYERLISTCATEGORY, PropertyNumeric, (uint8_t)m_playerListCategory) \ + DO(PlayerProp::COMMUNITYNAME, PropertyString, account.communityName) + +//---------------------------- + +template +PropertyContainer auto Player::constructPropFor(Args... values) const +{ +#define RETURN_CONSTRUCTPROPSFOR_CONSTEXPR(prop, type, ...) if constexpr (P == prop) return type{ values... }; + FOR_LIST_OF_PLAYER_PROPS(RETURN_CONSTRUCTPROPSFOR_CONSTEXPR); + + throw std::invalid_argument("Invalid PlayerProp type in constructPropFor"); +} + +template +PropertyContainer auto Player::getProp() const +{ +#define RETURN_GETPROP_CONSTEXPR(prop, type, ...) if constexpr (P == prop) return type{ __VA_ARGS__ }; + FOR_LIST_OF_PLAYER_PROPS(RETURN_GETPROP_CONSTEXPR); + + throw std::invalid_argument("Invalid PlayerProp type in getProp"); +} + +template +SetResults Player::setProp(SetBy setBy, PropertyContainer auto prop) +{ + return setProp(P, setBy, &prop); +} + +template +SetResults Player::setPropWith(SetBy setBy, Args... values) +{ + return setProp

(setBy, constructPropFor

(values...)); +} + +template requires AllSameAs +void Player::sendPropsFromResults(const Results&... results) +{ + PropertySendResults send_results; + (send_results.emplace_back(results, nullptr), ...); + sendPropsFromResults(send_results); +} + +void Player::sendPropsFromResults(std::ranges::forward_range auto&& results) +{ + PropertySendResults send_results; + auto results_range = results | std::views::transform([](const SetResults& results) { return std::make_pair(results, nullptr); }); + for (auto&& r : results_range) + send_results.emplace_back(r); + + sendPropsFromResults(send_results); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // PLAYER_H diff --git a/server/include/object/ShowImg.h b/server/include/object/ShowImg.h new file mode 100644 index 000000000..6156ca8dc --- /dev/null +++ b/server/include/object/ShowImg.h @@ -0,0 +1,120 @@ +#ifndef SHOWIMG_H +#define SHOWIMG_H + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +//////////////////////////////////////////////////////////////////////////////// + +// index 0-199 = visible to everybody + +/* +showimg colors,layer, and zoom is sent to other players (even +when the layer is 4, then only values between 0 and 440 allowed) + +showing text with showimg +you can do showimg 1,@Hello!,x,y; do show text; setting +font and style is possible by doing showimg index,@font@style@text,x,y; +(style: characters from 'bicus', b-bold, i-italic, c-centered,u-underline, +s-strikeout); for change the view of the text use the commands +changeimgzoom,changeimgcolors +*/ + +/* + props: {GCHAR prop}{value} + - max of 9 props + + prop 0: {GSTRING image} + prop 1: {GCHAR x} + - if layer is 10, it is computed as val - 64 instead of val - 32, or something like that. + prop 2: {GCHAR y} + prop 3: {GCHAR layer} + - it is 1-indexed in the packet (val - 33) + - caps at 8 (9 in the packet) + prop 4: {GSHORT imagePartX}{GSHORT imagePartY}{GCHAR imagePartWidth}{GCHAR imagePartHeight} + prop 5: {GCHAR red}{GCHAR green}{GCHAR blue}{GCHAR alpha} + - value / 200.0, for 0..1 + prop 6: {GCHAR zoom} + - value / 10.0 + prop 7: {GCHAR z} + prop 8: {GCHAR drawMode} + - 0 = add, 1 = replace, 2 = subtract, 3 = daynight + + image prop: + If it starts with @, it is a showtext: @Font@Style@Text + If it starts with &, it is a showani: &dir,gani e.g.: &0,skip + If it starts with :, it is a showpoly: #2,x1,y1,...,xn,yn e.g.: #2,10,10,15,10,15,15,10,15 + Unknown what the 2 is for. + + layer prop: + 0 - under players + 1 - default, same layer as players + 2 - over players + 4 - GUI level +*/ + +enum class ShowImgProp : uint8_t +{ + IMAGE = 0, + X = 1, + Y = 2, + LAYER = 3, + IMAGEPART = 4, + COLORS = 5, + ZOOM = 6, + Z = 7, + DRAWMODE = 8, + + SHOWIMGPROP_COUNT +}; +inline constexpr size_t SHOWIMGPROP_COUNT = static_cast(ShowImgProp::SHOWIMGPROP_COUNT); + +struct ShowImg +{ + std::string image; + ImagePartRectangle imagePart; + PixelPosition position; + float zoom = 1.0f; + uint8_t drawMode = 0; + uint8_t layer = 1; + std::array colors = { 0.0f, 0.0f, 0.0f, 1.0f }; + std::array, SHOWIMGPROP_COUNT> modTime; + std::array, SHOWIMGPROP_COUNT> savedModTime; + + static ShowImg ConstructImage(clock::time_point modTime, const PixelPosition& position, std::string_view image) noexcept; + static ShowImg ConstructText(clock::time_point modTime, const PixelPosition& position, std::string_view text, std::string_view font = {}, std::string_view style = {}) noexcept; + static ShowImg ConstructGani(clock::time_point modTime, const PixelPosition& position, std::string_view animation, uint8_t direction) noexcept; + static ShowImg ConstructPoly(clock::time_point modTime, const std::vector& points) noexcept; + + void processProps(CString& props); + CString getPropPacket(ShowImgProp prop) const; + CString getAllPropsPacket(std::optional newTime = std::nullopt) const; + CString getModifiedPropsPacket() const; + + [[inline]] void recordCurrentPropModTime(); +}; + +// --------------------------- + +inline void ShowImg::recordCurrentPropModTime() +{ + savedModTime = modTime; +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // SHOWIMG_H diff --git a/server/include/object/Weapon.h b/server/include/object/Weapon.h new file mode 100644 index 000000000..5a74bebd4 --- /dev/null +++ b/server/include/object/Weapon.h @@ -0,0 +1,112 @@ +#ifndef WEAPON_H +#define WEAPON_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +// TODO: Weapon should probably just be inherited from NPC. +class Server; +class Player; + +class Weapon +{ +public: + Weapon(LevelItemType itemType); + Weapon(std::string_view name, std::string_view image, std::string_view script); + ~Weapon() = default; + +public: + static std::shared_ptr loadWeapon(const CString& pWeapon); + +public: + bool saveWeapon(); + Weapon& updateWeapon(std::string_view image, std::string_view script); + +public: + void registerWeaponWithPlayer(std::shared_ptr player) const; + void sendByteCodeToPlayer(std::shared_ptr player) const; + +public: + std::string getJoinedClassesList() const; + [[inline]] std::generator> getJoinedClasses(); + void setJoinedClasses(std::string_view classes); + void joinClass(std::string_view className); + void leaveClass(std::string_view className); + +protected: + std::string getClientSideScript() const; + void updateScriptClass(ScriptClass* scriptClass); + +public: + void executeEvents(ScriptEventQueue& events, ScriptObject source) const; + +public: + bool isDefault() const { return (m_weaponDefault != LevelItemType::INVALID); } + LevelItemType getWeaponId() const { return m_weaponDefault; } + Script& getScript() { return m_script; } + const Script& getScript() const { return m_script; } + +public: + const std::string name; + std::string image; + clock::time_point modTime; + ScriptContainer scripting; + +protected: + Server* m_server; + LevelItemType m_weaponDefault; + Script m_script; + uint32_t m_checksum; + std::string m_desKey; + std::string m_header; + std::string m_headerWithCRC; + + mutable std::vector>> m_joinedClasses; +}; +using TWeaponPtr = std::shared_ptr; + +//---------------------------- + +inline std::generator> Weapon::getJoinedClasses() +{ + auto filter = m_joinedClasses + | std::views::transform([](const auto& pair) { return pair.second.lock(); }) + | std::views::filter([](const auto& scriptClass) { return scriptClass != nullptr; }); + for (auto scriptClass : filter) + co_yield scriptClass; +} + +//---------------------------- + +namespace source +{ +/// @brief Creates a ScriptObject from a Weapon by hashing the weapon's name. +ScriptObject FromWeapon(WeaponPtr weapon); +} // end namespace source + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // WEAPON_H diff --git a/server/include/player/PlayerClient.h b/server/include/player/PlayerClient.h new file mode 100644 index 000000000..6c3fddd64 --- /dev/null +++ b/server/include/player/PlayerClient.h @@ -0,0 +1,248 @@ +#ifndef PLAYERCLIENT_H +#define PLAYERCLIENT_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std::literals::string_view_literals; + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +template +consteval auto join_arrays(Arrays... arrays) +{ + return std::apply([](auto... args) + { + return std::array{args...}; + }, std::tuple_cat(arrays...)); +} + +constexpr std::array DefaultGanis = { + "carried.gani", "carry.gani", "carrystill.gani", "carrypeople.gani", "dead.gani", "def.gani", "ghostani.gani", "grab.gani", "gralats.gani", "hatoff.gani", + "haton.gani", "hidden.gani", "hiddenstill.gani", "hurt.gani", "idle.gani", "kick.gani", "lava.gani", "lift.gani", "maps1.gani", "maps2.gani", + "maps3.gani", "pull.gani", "push.gani", "ride.gani", "rideeat.gani", "ridefire.gani", "ridehurt.gani", "ridejump.gani", "ridestill.gani", "ridesword.gani", + "shoot.gani", "sit.gani", "skip.gani", "sleep.gani", "spin.gani", "swim.gani", "sword.gani", "walk.gani", "walkslow.gani" +}; +constexpr std::array DefaultBodies = {"body.png", "body2.png", "body3.png"}; +constexpr std::array DefaultSwords = {"sword?.png", "sword?.gif"}; +constexpr std::array DefaultShields = {"shield?.png", "shield?.gif"}; +constexpr std::array DefaultWavs = { + "arrow.wav", "arrowon.wav", "axe.wav", "bomb.wav", "chest.wav", "compudead.wav", "crush.wav", "dead.wav", "extra.wav", "fire.wav", + "frog.wav", "frog2.wav", "goal.wav", "horse.wav", "horse2.wav", "item.wav", "item2.wav", "jump.wav", "lift.wav", "lift2.wav", + "nextpage.wav", "put.wav", "sign.wav", "steps.wav", "steps2.wav", "stonemove.wav", "sword.wav", "swordon.wav", "thunder.wav", "water.wav" +}; +constexpr std::array DefaultPngs = {"pics1.png"}; +constexpr std::array DefaultFiles = join_arrays(DefaultGanis, DefaultBodies, DefaultSwords, DefaultShields, DefaultWavs, DefaultPngs); + +//---------------------------- + +template +struct CachedLevel +{ + std::weak_ptr level; + clock::time_point lastEnteredTime; +}; + +//---------------------------- + +class PlayerClient : public Player +{ + // TODO: Need to refactor some Player functions like sendFile so this can be removed. + friend class Player; + +public: + PlayerClient(CSocket* pSocket, PlayerID pId); + virtual ~PlayerClient(); + virtual void cleanup() override; + +public: + std::shared_ptr self() { return std::dynamic_pointer_cast(shared_from_this()); } + +public: + // Main methods. + virtual void doMain() override; + virtual bool doTimedEvents() override; + virtual bool handleLogin(CString& pPacket) override; + virtual bool sendLogin() override; + + bool processChat(const CString& pChat); + + [[inline]] const std::string& getGroup() const; + void setGroup(std::string_view group); + + virtual double getCalculatedTileZ() const noexcept override; + + // Level manipulation + virtual std::string getLevelName() const override; + std::shared_ptr getLevel() const; + std::shared_ptr getSubLevel() const; + +public: + // Forcibly move a player (the client doesn't know it is transitioning levels). + virtual bool warp(std::string_view levelName, const PixelPosition& position, std::optional clientCachedTime = std::nullopt) override; + virtual bool warp(std::shared_ptr level, const PixelPosition& position, std::optional clientCachedTime = std::nullopt) override; + + // Place the player in a new level (the client knows it is transitioning levels). + virtual bool enterLevel(std::shared_ptr level, std::optional clientCachedTime = std::nullopt) override; + using Player::enterLevel; + + virtual bool leaveLevel() override; + virtual bool leaveSubLevel(std::shared_ptr subLevel) override; + + virtual bool sendStaticLevelData(std::shared_ptr staticLevelData, std::shared_ptr subLevel, std::optional clientCachedTime = std::nullopt) override; + virtual bool sendDynamicLevelData(std::shared_ptr level, std::optional clientCachedTime = std::nullopt) override; + + void checkAndInformIfLevelLeader(); + void informPlayerIsLevelLeader(); + +public: + std::optional getLevelLastEnteredTime(const StaticLevelData* level) const; + std::optional getLevelLastEnteredTime(const SubLevel* level, std::string_view group = ""sv) const; + void resetLevelCache(const StaticLevelData* level); + void resetLevelCache(const SubLevel* level, std::string_view group = ""sv); + void resetLevelCache(std::string_view group); + +public: + [[inline]] bool hasSeenFile(const std::string& file) const; + [[inline]] void setLastChatTime(clock::time_point time); + [[inline]] void setLastMovementTime(clock::time_point time); + void dropItemsOnDeath(); + +public: + void disableWeapons(); + void enableWeapons(); + void freezePlayer(); + void unfreezePlayer(); + void sendRPGMessage(std::string message); + void sendSignMessage(std::string message); + +public: + void testForTouch(SetResults& result, uint8_t movementDirection); + bool testForSigns(SetResults& result, uint8_t movementDirection); + bool testForLinks(SetResults& result, uint8_t movementDirection); + +protected: + virtual HandlePacketResult handlePacket(std::optional id, CString& packet) override; + +public: + HandlePacketResult msgPLI_LEVELWARP(CString& pPacket); + HandlePacketResult msgPLI_BOARDMODIFY(CString& pPacket); + HandlePacketResult msgPLI_REQUESTUPDATEBOARD(CString& pPacket); + HandlePacketResult msgPLI_NPCPROPS(CString& pPacket); + HandlePacketResult msgPLI_BOMBADD(CString& pPacket); + HandlePacketResult msgPLI_BOMBDEL(CString& pPacket); + HandlePacketResult msgPLI_HORSEADD(CString& pPacket); + HandlePacketResult msgPLI_HORSEDEL(CString& pPacket); + HandlePacketResult msgPLI_ARROWADD(CString& pPacket); + HandlePacketResult msgPLI_FIRESPY(CString& pPacket); + HandlePacketResult msgPLI_THROWCARRIED(CString& pPacket); + HandlePacketResult msgPLI_ITEMADD(CString& pPacket); + HandlePacketResult msgPLI_ITEMDEL(CString& pPacket); + HandlePacketResult msgPLI_CLAIMPKER(CString& pPacket); + HandlePacketResult msgPLI_BADDYPROPS(CString& pPacket); + HandlePacketResult msgPLI_BADDYHURT(CString& pPacket); + HandlePacketResult msgPLI_BADDYADD(CString& pPacket); + HandlePacketResult msgPLI_FLAGSET(CString& pPacket); + HandlePacketResult msgPLI_FLAGDEL(CString& pPacket); + HandlePacketResult msgPLI_OPENCHEST(CString& pPacket); + HandlePacketResult msgPLI_PUTNPC(CString& pPacket); + HandlePacketResult msgPLI_NPCDEL(CString& pPacket); + HandlePacketResult msgPLI_WANTFILE(CString& pPacket); + HandlePacketResult msgPLI_SHOWIMGPLAYER(CString& pPacket); + HandlePacketResult msgPLI_HURTPLAYER(CString& pPacket); + HandlePacketResult msgPLI_EXPLOSION(CString& pPacket); + HandlePacketResult msgPLI_PRIVATEMESSAGE(CString& pPacket); + HandlePacketResult msgPLI_NPCWEAPONDEL(CString& pPacket); + HandlePacketResult msgPLI_WEAPONADD(CString& pPacket); + HandlePacketResult msgPLI_UPDATEFILE(CString& pPacket); + HandlePacketResult msgPLI_ADJACENTLEVEL(CString& pPacket); + HandlePacketResult msgPLI_HITOBJECTS(CString& pPacket); + HandlePacketResult msgPLI_TRIGGERACTION(CString& pPacket); + HandlePacketResult msgPLI_TAMPERCHECK(CString& pPacket); + HandlePacketResult msgPLI_SHOOT(CString& pPacket); + HandlePacketResult msgPLI_SHOOT2(CString& pPacket); + HandlePacketResult msgPLI_SERVERWARP(CString& pPacket); + HandlePacketResult msgPLI_PROCESSLIST(CString& pPacket); + HandlePacketResult msgPLI_ENTERLEVEL(CString& pPacket); + HandlePacketResult msgPLI_UPDATECLASS(CString& pPacket); + HandlePacketResult msgPLI_UPDATEGANI(CString& pPacket); + HandlePacketResult msgPLI_UPDATESCRIPT(CString& pPacket); + HandlePacketResult msgPLI_VERIFYWANTSEND(CString& pPacket); + HandlePacketResult msgPLI_UPDATEPACKAGEREQUESTFILE(CString& pPacket); + +protected: + bool dropItem(const PixelPosition& position, LevelItemType item); + bool removeItem(LevelItemType itemType); + props::SetResults addItem(LevelItemType itemType, props::SetBy setBy = props::SetBy::SERVER); + void addItem(inform_client_t, LevelItemType itemType, props::SetBy setBy = props::SetBy::SERVER); + +protected: + clock::time_point m_lastMovement, m_lastSave, m_last1m; + clock::time_point m_lastChat; + clock::time_point m_lastMessage; + clock::time_point m_lastNick; + std::vector>> m_cachedStaticLevels; + string_map>>> m_cachedDynamicLevels; + std::map> m_singleplayerLevels; + std::weak_ptr m_currentLevel; + + std::unordered_set m_knownFiles; + + bool m_grMovementUpdated = false; + CString m_grMovementPackets; + CString m_grExecParameterList; +}; + +using PlayerClientPtr = std::shared_ptr; + +//---------------------------- + +inline const std::string& PlayerClient::getGroup() const +{ + return account.groupName; +} + +inline bool PlayerClient::hasSeenFile(const std::string& file) const +{ + return m_knownFiles.find(file) != m_knownFiles.end(); +} + +inline void PlayerClient::setLastChatTime(clock::time_point time) +{ + m_lastChat = time; +} + +inline void PlayerClient::setLastMovementTime(clock::time_point time) +{ + m_lastMovement = time; + m_grMovementUpdated = true; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // PLAYERCLIENT_H diff --git a/server/include/player/PlayerClientOriginal.h b/server/include/player/PlayerClientOriginal.h new file mode 100644 index 000000000..3c3a031f2 --- /dev/null +++ b/server/include/player/PlayerClientOriginal.h @@ -0,0 +1,49 @@ +#ifndef PLAYERCLIENTORIGINAL_H +#define PLAYERCLIENTORIGINAL_H + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +using namespace std::literals::string_view_literals; + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class PlayerClientOriginal : public PlayerClient +{ +public: + PlayerClientOriginal(CSocket* pSocket, PlayerID pId); + virtual ~PlayerClientOriginal(); + +public: + // Forcibly move a player (the client doesn't know it is transitioning levels). + virtual bool warp(std::shared_ptr level, const PixelPosition& position, std::optional clientCachedTime = std::nullopt) override; + + // Place the player in a new level (the client knows it is transitioning levels). + virtual bool enterLevel(std::shared_ptr level, std::optional clientCachedTime = std::nullopt) override; + using Player::enterLevel; + + virtual bool sendStaticLevelData(std::shared_ptr staticLevelData, std::shared_ptr subLevel, std::optional clientCachedTime = std::nullopt) override; + virtual bool sendDynamicLevelData(std::shared_ptr level, std::optional clientCachedTime = std::nullopt) override; + +protected: + bool m_firstLevel = false; +}; + +using PlayerClientOriginalPtr = std::shared_ptr; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // PLAYERCLIENTORIGINAL_H diff --git a/server/include/player/PlayerLogin.h b/server/include/player/PlayerLogin.h new file mode 100644 index 000000000..5f1642430 --- /dev/null +++ b/server/include/player/PlayerLogin.h @@ -0,0 +1,40 @@ +#ifndef PLAYERLOGIN_H +#define PLAYERLOGIN_H + +#include +#include + +#include + +#include + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class PlayerLogin : public Player +{ +public: + PlayerLogin(CSocket* pSocket, PlayerID pId); + virtual ~PlayerLogin() override; + +public: + virtual bool onRecv() override; + virtual void onUnregister() override {} + +protected: + virtual HandlePacketResult handlePacket(std::optional id, CString& packet) override; + +public: + HandlePacketResult msgLoginPacket(CString& pPacket); +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // PLAYERLOGIN_H diff --git a/server/include/player/PlayerNC.h b/server/include/player/PlayerNC.h new file mode 100644 index 000000000..6412827ee --- /dev/null +++ b/server/include/player/PlayerNC.h @@ -0,0 +1,61 @@ +#ifndef PLAYERNC_H +#define PLAYERNC_H + +#include +#include + +#include + +#include + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class PlayerNC : public Player +{ +public: + PlayerNC(CSocket* pSocket, PlayerID pId) : Player(pSocket, pId) {} + virtual ~PlayerNC() {} + //virtual void cleanup() override; + +public: + virtual bool handleLogin(CString& pPacket) override; + virtual bool sendLogin() override; + +protected: + virtual HandlePacketResult handlePacket(std::optional id, CString& packet) override; + +public: + HandlePacketResult msgPLI_RC_CHAT(CString& pPacket); + +public: + HandlePacketResult msgPLI_NC_NPCGET(CString& pPacket); + HandlePacketResult msgPLI_NC_NPCDELETE(CString& pPacket); + HandlePacketResult msgPLI_NC_NPCRESET(CString& pPacket); + HandlePacketResult msgPLI_NC_NPCSCRIPTGET(CString& pPacket); + HandlePacketResult msgPLI_NC_NPCWARP(CString& pPacket); + HandlePacketResult msgPLI_NC_NPCFLAGSGET(CString& pPacket); + HandlePacketResult msgPLI_NC_NPCSCRIPTSET(CString& pPacket); + HandlePacketResult msgPLI_NC_NPCFLAGSSET(CString& pPacket); + HandlePacketResult msgPLI_NC_NPCADD(CString& pPacket); + HandlePacketResult msgPLI_NC_CLASSEDIT(CString& pPacket); + HandlePacketResult msgPLI_NC_CLASSADD(CString& pPacket); + HandlePacketResult msgPLI_NC_LOCALNPCSGET(CString& pPacket); + HandlePacketResult msgPLI_NC_WEAPONLISTGET(CString& pPacket); + HandlePacketResult msgPLI_NC_WEAPONGET(CString& pPacket); + HandlePacketResult msgPLI_NC_WEAPONADD(CString& pPacket); + HandlePacketResult msgPLI_NC_WEAPONDELETE(CString& pPacket); + HandlePacketResult msgPLI_NC_CLASSDELETE(CString& pPacket); + HandlePacketResult msgPLI_NC_LEVELLISTGET(CString& pPacket); +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // PLAYERNC_H diff --git a/server/include/player/PlayerProps.h b/server/include/player/PlayerProps.h new file mode 100644 index 000000000..0dfc70877 --- /dev/null +++ b/server/include/player/PlayerProps.h @@ -0,0 +1,238 @@ +#ifndef PLAYERPROPS_H +#define PLAYERPROPS_H + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +enum class PlayerProp : uint8_t +{ + NICKNAME = 0, + MAXPOWER = 1, + CURPOWER = 2, + RUPEESCOUNT = 3, + ARROWSCOUNT = 4, + BOMBSCOUNT = 5, + GLOVEPOWER = 6, + BOMBPOWER = 7, + SWORDPOWER = 8, + SHIELDPOWER = 9, + GANI = 10, // PLPROP_BOWGIF in pre-2.x + HEADGIF = 11, + CURCHAT = 12, + COLORS = 13, + ID = 14, + X = 15, + Y = 16, + SPRITE = 17, + STATUS = 18, + CARRYSPRITE = 19, + CURLEVEL = 20, + HORSEGIF = 21, + HORSEBUSHES = 22, + EFFECTCOLORS = 23, + CARRYNPC = 24, + APCOUNTER = 25, + MAGICPOINTS = 26, + KILLSCOUNT = 27, + DEATHSCOUNT = 28, + ONLINESECS = 29, + IPADDR = 30, + UDPPORT = 31, + ALIGNMENT = 32, + ADDITFLAGS = 33, + ACCOUNTNAME = 34, + BODYIMG = 35, + RATING = 36, + GATTRIB1 = 37, + GATTRIB2 = 38, + GATTRIB3 = 39, + GATTRIB4 = 40, + GATTRIB5 = 41, + ATTACHNPC = 42, + GMAPLEVELX = 43, + GMAPLEVELY = 44, + Z = 45, + GATTRIB6 = 46, + GATTRIB7 = 47, + GATTRIB8 = 48, + GATTRIB9 = 49, + JOINLEAVELVL = 50, + DISCONNECT = 51, + LANGUAGE = 52, + PLAYERLISTSTATUS = 53, + GATTRIB10 = 54, + GATTRIB11 = 55, + GATTRIB12 = 56, + GATTRIB13 = 57, + GATTRIB14 = 58, + GATTRIB15 = 59, + GATTRIB16 = 60, + GATTRIB17 = 61, + GATTRIB18 = 62, + GATTRIB19 = 63, + GATTRIB20 = 64, + GATTRIB21 = 65, + GATTRIB22 = 66, + GATTRIB23 = 67, + GATTRIB24 = 68, + GATTRIB25 = 69, + GATTRIB26 = 70, + GATTRIB27 = 71, + GATTRIB28 = 72, + GATTRIB29 = 73, + GATTRIB30 = 74, + OSTYPE = 75, // 2.19+ + TEXTCODEPAGE = 76, // 2.19+ + ONLINESECS2 = 77, + X2 = 78, + Y2 = 79, + Z2 = 80, + PLAYERLISTCATEGORY = 81, // {GCHAR flag} - flag 0 places in playerlist, flag 1 places in servers tab, flag 3 places in channels tab (unconfirmed) + + // In Graal v5, where players have the Graal######## accounts, this is their chosen account alias (community name.) + COMMUNITYNAME = 82, + + // v6 will read a GBYTE5 and store it inside some variable, as long as the number is less than 1,000,000,000. + UNKNOWN83 = 83, + + PLAYERPROP_COUNT +}; +constexpr int PLAYERPROP_COUNT = static_cast(PlayerProp::PLAYERPROP_COUNT); + +enum class PlayerListCategory : uint8_t +{ + PLAYERLIST = 0b0000, + EXTERNAL = 0b0001, + CHANNEL = 0b0010, + CHANNELUSER = 0b0100, + CHANNELOPEN = 0b1010, +}; + +// Gani attributes in order of their property number. +inline constexpr std::array GaniAttributePropList = { 37, 38, 39, 40, 41, 46, 47, 48, 49, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74 }; + +using PropList = std::array; + +// Sent to the player on login. +inline constexpr PropList loginPropsClientSelf = +{ + false, true, true, true, true, true, // 0-5 + true, false, true, true, true, true, // 6-11 + false, true, false, false, false, true, // 12-17 + true, false, false, true, true, true, // 18-23 + false, true, true, false, false, false, // 24-29 + false, false, true, false, true, true, // 30-35 + true, true, true, true, true, true, // 36-41 + false, false, false, false, true, true, // 42-47 + true, true, false, false, false, false, // 48-53 + true, true, true, true, true, true, // 54-59 + true, true, true, true, true, true, // 60-65 + true, true, true, true, true, true, // 66-71 + true, true, true, false, false, false, // 72-77 + true, true, true, false, true, // 78-82 +}; + +// Sent to nearby players when a player logs in. +inline constexpr PropList loginPropsClientOthers = +{ + true, false, false, false, false, false, // 0-5 + false, false, true, true, true, true, // 6-11 + true, true, false, true, true, true, // 12-17 + true, true, true, true, false, false, // 18-23 + true, false, false, false, false, false, // 24-29 + true, true, true, false, true, true, // 30-35 + true, true, true, true, true, true, // 36-41 + false, true, true, true, true, true, // 42-47 + true, true, false, false, false, true, // 48-53 + true, true, true, true, true, true, // 54-59 + true, true, true, true, true, true, // 60-65 + true, true, true, true, true, true, // 66-71 + true, true, true, false, false, false, // 72-77 + true, true, true, false, true, // 78-82 +}; + +// Login props for NC that get sent to other players (currently unused, most likely incorrect). +inline constexpr PropList loginPropsNC = +{ + true, true, true, true, true, true, // 0-5 + true, true, true, true, true, true, // 6-11 + true, true, true, true, true, true, // 12-17 + true, true, true, true, true, true, // 18-23 + true, true, true, true, true, true, // 24-29 + true, false, true, true, true, true, // 30-35 + true, true, true, true, true, true, // 36-41 + false, true, true, true, true, true, // 42-47 + true, true, false, false, true, true, // 48-53 + true, true, true, true, true, true, // 54-59 + true, true, true, true, true, true, // 60-65 + true, true, true, true, true, true, // 66-71 + true, true, true, true, false, false, // 72-77 + true, true, true, false, false, // 78-82 +}; + +// Login props for RC that get sent to other players. +inline constexpr PropList loginPropsRC = +{ + true, false, false, false, false, false, // 0-5 + false, false, false, false, false, true, // 6-11 + false, false, false, false, false, false, // 12-17 + true, false, true, false, false, false, // 18-23 + false, false, false, false, false, false, // 24-29 + true, true, false, false, true, false, // 30-35 + false, false, false, false, false, false, // 36-41 + false, false, false, false, false, false, // 42-47 + false, false, false, false, false, true, // 48-53 + false, false, false, false, false, false, // 54-59 + false, false, false, false, false, false, // 60-65 + false, false, false, false, false, false, // 66-71 + false, false, false, false, false, false, // 72-77 + false, false, false, false, true, // 78-82 +}; + +// When one of these props change, they are sent to nearby players. +inline constexpr PropList clientPropsSharedLocal = +{ + true, false, true, false, false, false, // 0-5 + false, false, true, true, true, true, // 6-11 + true, true, false, true, true, true, // 12-17 + true, true, true, true, false, true, // 18-23 + true, false, false, false, false, false, // 24-29 + true, true, true, false, true, true, // 30-35 + true, true, true, true, true, true, // 36-41 + true, true, true, true, true, true, // 42-47 + true, true, false, false, false, true, // 48-53 + true, true, true, true, true, true, // 54-59 + true, true, true, true, true, true, // 60-65 + true, true, true, true, true, true, // 66-71 + true, true, true, false, false, false, // 72-77 + true, true, true, true, true, // 78-82 +}; + +// When the RC views a player's account, these props are sent. +inline constexpr PropList clientPropsForRCView = +{ + true, true, true, true, true, true, // 0-5 + true, false, true, true, true, true, // 6-11 + false, true, false, true, true, false, // 12-17 + true, false, true, false, false, false, // 18-23 + false, false, true, true, true, true, // 24-29 + true, false, true, false, true, true, // 30-35 + true, false, false, false, false, false, // 36-41 + false, false, false, false, false, false, // 42-47 + false, false, false, false, false, false, // 48-53 + false, false, false, false, false, false, // 54-59 + false, false, false, false, false, false, // 60-65 + false, false, false, false, false, false, // 66-71 + false, false, false, false, false, false, // 72-77 + false, false, false, false, false, // 78-82 +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // PLAYERPROPS_H diff --git a/server/include/player/PlayerRC.h b/server/include/player/PlayerRC.h new file mode 100644 index 000000000..3ae5bc3f8 --- /dev/null +++ b/server/include/player/PlayerRC.h @@ -0,0 +1,148 @@ +#ifndef PLAYERRC_H +#define PLAYERRC_H + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +// Admin-only server options. They are protected from being changed by people without the +// 'change staff account' right. +constexpr std::array AdminServerOptions = { + "name", "description", "url", "serverip", "serverport", "serverinterface", + "localip", "upnp", "listip", "listport", "maxplayers", "onlystaff", + "nofoldersconfig", "serverside", "sharefolder", "language", "protectdbnpcs", + // GR extensions + "triggerhack_weapons", "triggerhack_guilds", "triggerhack_groups", "triggerhack_files", "triggerhack_rc", "triggerhack_execscript", "triggerhack_props", "triggerhack_levels", + "flaghack_movement", "flaghack_ip", "generation", "runallscriptevents", "clientsidesigns", "clientsidelinks" +}; + +// Files that are protected from being downloaded by people without the +// 'change staff account' right. +constexpr std::array ProtectedFiles = { + "accounts/defaultaccount.txt", + "config/adminconfig.txt", + "config/allowedversions.txt", + "config/rchelp.txt", +}; + +// List of important files. +constexpr std::array ImportantFiles = { + "accounts/defaultaccount.txt", + "config/adminconfig.txt", + "config/allowedversions.txt", + "config/foldersconfig.txt", + "config/ipbans.txt", + "config/rchelp.txt", + "config/rcmessage.txt", + "config/rules.txt", + "config/servermessage.html", + "config/serveroptions.txt", +}; + +constexpr std::array ImportantFileRights = { + PLPERM_MODIFYSTAFFACCOUNT, + PLPERM_MODIFYSTAFFACCOUNT, + PLPERM_MODIFYSTAFFACCOUNT, + PLPERM_SETFOLDEROPTIONS, + PLPERM_MODIFYSTAFFACCOUNT, + PLPERM_MODIFYSTAFFACCOUNT, + PLPERM_MODIFYSTAFFACCOUNT, + PLPERM_MODIFYSTAFFACCOUNT, + PLPERM_SETSERVEROPTIONS, + PLPERM_SETSERVEROPTIONS, +}; + +class PlayerRC : public Player +{ +public: + PlayerRC(CSocket* pSocket, PlayerID pId) : Player(pSocket, pId) {} + virtual ~PlayerRC() {} + virtual void cleanup() override; + +public: + virtual bool handleLogin(CString& pPacket) override; + virtual bool sendLogin() override; + +public: + bool isUsingFileBrowser() const { return m_isFtp; } + +protected: + virtual HandlePacketResult handlePacket(std::optional id, CString& packet) override; + +public: + HandlePacketResult msgPLI_RC_SERVEROPTIONSGET(CString& pPacket); + HandlePacketResult msgPLI_RC_SERVEROPTIONSSET(CString& pPacket); + HandlePacketResult msgPLI_RC_FOLDERCONFIGGET(CString& pPacket); + HandlePacketResult msgPLI_RC_FOLDERCONFIGSET(CString& pPacket); + HandlePacketResult msgPLI_RC_RESPAWNSET(CString& pPacket); + HandlePacketResult msgPLI_RC_HORSELIFESET(CString& pPacket); + HandlePacketResult msgPLI_RC_APINCREMENTSET(CString& pPacket); + HandlePacketResult msgPLI_RC_BADDYRESPAWNSET(CString& pPacket); + HandlePacketResult msgPLI_RC_PLAYERPROPSGET(CString& pPacket); + HandlePacketResult msgPLI_RC_PLAYERPROPSSET(CString& pPacket); + HandlePacketResult msgPLI_RC_DISCONNECTPLAYER(CString& pPacket); + HandlePacketResult msgPLI_RC_UPDATELEVELS(CString& pPacket); + HandlePacketResult msgPLI_RC_ADMINMESSAGE(CString& pPacket); + HandlePacketResult msgPLI_RC_PRIVADMINMESSAGE(CString& pPacket); + HandlePacketResult msgPLI_RC_LISTRCS(CString& pPacket); + HandlePacketResult msgPLI_RC_DISCONNECTRC(CString& pPacket); + HandlePacketResult msgPLI_RC_APPLYREASON(CString& pPacket); + HandlePacketResult msgPLI_RC_SERVERFLAGSGET(CString& pPacket); + HandlePacketResult msgPLI_RC_SERVERFLAGSSET(CString& pPacket); + HandlePacketResult msgPLI_RC_ACCOUNTADD(CString& pPacket); + HandlePacketResult msgPLI_RC_ACCOUNTDEL(CString& pPacket); + HandlePacketResult msgPLI_RC_ACCOUNTLISTGET(CString& pPacket); + HandlePacketResult msgPLI_RC_PLAYERPROPSGET2(CString& pPacket); + HandlePacketResult msgPLI_RC_PLAYERPROPSGET3(CString& pPacket); + HandlePacketResult msgPLI_RC_PLAYERPROPSRESET(CString& pPacket); + HandlePacketResult msgPLI_RC_PLAYERPROPSSET2(CString& pPacket); + HandlePacketResult msgPLI_RC_ACCOUNTGET(CString& pPacket); + HandlePacketResult msgPLI_RC_ACCOUNTSET(CString& pPacket); + HandlePacketResult msgPLI_RC_CHAT(CString& pPacket); + HandlePacketResult msgPLI_RC_WARPPLAYER(CString& pPacket); + HandlePacketResult msgPLI_RC_PLAYERRIGHTSGET(CString& pPacket); + HandlePacketResult msgPLI_RC_PLAYERRIGHTSSET(CString& pPacket); + HandlePacketResult msgPLI_RC_PLAYERCOMMENTSGET(CString& pPacket); + HandlePacketResult msgPLI_RC_PLAYERCOMMENTSSET(CString& pPacket); + HandlePacketResult msgPLI_RC_PLAYERBANGET(CString& pPacket); + HandlePacketResult msgPLI_RC_PLAYERBANSET(CString& pPacket); + HandlePacketResult msgPLI_RC_FILEBROWSER_START(CString& pPacket); + HandlePacketResult msgPLI_RC_FILEBROWSER_CD(CString& pPacket); + HandlePacketResult msgPLI_RC_FILEBROWSER_END(CString& pPacket); + HandlePacketResult msgPLI_RC_FILEBROWSER_DOWN(CString& pPacket); + HandlePacketResult msgPLI_RC_FILEBROWSER_UP(CString& pPacket); + HandlePacketResult msgPLI_NPCSERVERQUERY(CString& pPacket); + HandlePacketResult msgPLI_RC_FILEBROWSER_MOVE(CString& pPacket); + HandlePacketResult msgPLI_RC_FILEBROWSER_DELETE(CString& pPacket); + HandlePacketResult msgPLI_RC_FILEBROWSER_RENAME(CString& pPacket); + HandlePacketResult msgPLI_RC_LARGEFILESTART(CString& pPacket); + HandlePacketResult msgPLI_RC_LARGEFILEEND(CString& pPacket); + HandlePacketResult msgPLI_RC_FOLDERDELETE(CString& pPacket); + HandlePacketResult msgPLI_RC_UNKNOWN162(CString& pPacket); + +protected: + bool m_isFtp = false; + std::map m_rcLargeFiles; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // PLAYERRC_H diff --git a/server/include/scripting/GS2ScriptManager.h b/server/include/scripting/GS2ScriptManager.h index 02ff75abb..3c0b9f1b6 100644 --- a/server/include/scripting/GS2ScriptManager.h +++ b/server/include/scripting/GS2ScriptManager.h @@ -1,21 +1,74 @@ #ifndef GS2SCRIPTMANAGER_H #define GS2SCRIPTMANAGER_H +#include +#include +#include #include #include +#include +#include +#include +#include -#include -#include +#include +#include #include -#include "scripting/interface/ScriptUtils.h" +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// -class GS2ScriptManager +// Custom thread pool callback that returns a future to a CompilerResponse, while also allowing a callback. +class CompiledWithCallbackThreadJob { - using BytecodeCache = std::unordered_map; +public: + struct job_result + { + CompilerResponse response; + }; + + struct thread_context + { + GS2Context gs2context; + }; + + using future_type = std::future; + using promise_type = std::promise; + using callback_type = std::function; +public: + CompiledWithCallbackThreadJob(callback_type callback) + : m_fn(std::move(callback)) + { + } + + void run(thread_context& th_context, promise_type& promise) + { + m_fn(th_context, promise); + } + + static void init(thread_context& th_context) {} + +private: + callback_type m_fn; +}; + +// The original compiler response cannot be shoved into a future. +struct BetterCompilerResponse +{ + bool success; + std::vector errors; + std::vector bytecode; + std::set joinedClasses; +}; + +// GS2 script management. +class GS2ScriptManager +{ // used for threadpool job queue - using CompilerThreadPool = CustomThreadPool; + using CompilerThreadPool = CustomThreadPool; using internal_callback_type = std::function; using queue_item_type = std::pair; @@ -25,22 +78,20 @@ class GS2ScriptManager GS2ScriptManager(); ~GS2ScriptManager() {} - void compileScript(const std::string& script, user_callback_type finishedCb); + std::future compileScript(const std::string& script, user_callback_type finishedCb = {}); void runQueue(); private: - // Async Compile - void queueCompileJob(const std::string& script, user_callback_type& finishedCb); + std::future queueCompileJob(const std::string& script, user_callback_type& finishedCb); - // Sync Compile GS2Context _context; - void syncCompileJob(const std::string& script, user_callback_type& finishedCb); - - BytecodeCache m_bytecodeCache; CompilerThreadPool m_compilerThreadPool; std::queue m_cbQueue; std::mutex m_cbQueueLock; }; -#endif +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // GS2SCRIPTMANAGER_H diff --git a/server/include/scripting/IScriptEngine.h b/server/include/scripting/IScriptEngine.h new file mode 100644 index 000000000..f31829d6a --- /dev/null +++ b/server/include/scripting/IScriptEngine.h @@ -0,0 +1,52 @@ +#ifndef ISCRIPTENGINE_H +#define ISCRIPTENGINE_H + +#include +#include + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +enum class ScriptEngineMode +{ + CALLBACK, + DIRECT +}; + +enum class ScriptExecutionType +{ + INTERPRETED, + COMPILED +}; + +class IScriptEngine +{ +public: + virtual ~IScriptEngine() {}; + +public: + virtual ScriptEngineMode getExecutionMode() = 0; + virtual ScriptExecutionType getExecutionType() = 0; + +public: + virtual CompiledScriptResult compileScript(std::string_view who, std::string_view script) = 0; + virtual bool reset() = 0; + +public: + virtual bool execute(ScriptEvent& event, ScriptObject source, CompiledScriptResultPtr context) = 0; + virtual bool executeFunction(std::string_view function, ScriptEvent& event, ScriptObject source, CompiledScriptResultPtr context) = 0; + +public: + virtual double processMathExpression(std::string_view expression, ScriptObject source) = 0; + virtual std::string processStringExpression(std::string_view expression, ScriptObject source) = 0; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // ISCRIPTENGINE_H diff --git a/server/include/scripting/Script.h b/server/include/scripting/Script.h new file mode 100644 index 000000000..15cb16189 --- /dev/null +++ b/server/include/scripting/Script.h @@ -0,0 +1,181 @@ +#ifndef SCRIPT_H +#define SCRIPT_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace std::literals; + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +using ScriptByteCode = std::vector; +using ScriptByteCodePtr = std::shared_ptr; + +class Script +{ +public: + Script() = default; + Script(std::string_view who, const std::string& src) noexcept : Script(who, std::move(std::string{ src })) {} + Script(std::string_view who, std::string_view src) noexcept : Script(who, std::move(std::string{ src })) {} + Script(const Script& o) noexcept { *this = o; } + Script(Script&& o) noexcept { *this = std::move(o); } + + Script(std::string_view who, std::string&& src) noexcept + { + setOriginalSource(who, std::move(src)); + } + + [[inline]] Script& operator=(const Script& o) noexcept; + [[inline]] Script& operator=(Script&& o) noexcept; + +public: + [[inline]] const size_t getHash() const noexcept; + [[inline]] const std::string& getOriginalSource() const noexcept; + [[inline]] const std::string& getModifiedSource() const noexcept; + [[inline]] std::string_view getClientSide() const noexcept; + [[inline]] std::string_view getServerSide() const noexcept; + const ScriptByteCode& getClientByteCode() const noexcept; + +public: + [[inline]] Script& setOriginalSource(std::string_view who, std::string&& source) noexcept; + [[inline]] Script& setOriginalSource(std::string_view who, const std::string& source) noexcept; + [[inline]] Script& setModifiedSource(const std::string& source) noexcept; + [[inline]] Script& setClientCompiledScript(CompiledScriptResultPtr script) noexcept; + [[inline]] Script& setServerCompiledScript(CompiledScriptResultPtr script) noexcept; + +public: + std::generator getServerJoinedClasses() const noexcept; + +public: + void executeEvents(ScriptContainer& container, ScriptObject source) const; + void executeEvents(ScriptEventQueue& events, ScriptObject source) const; + void executeEvents(clear_container_t, ScriptContainer& container, ScriptObject source) const; + void executeEvents(clear_container_t, ScriptEventQueue& events, ScriptObject source) const; + bool runUserDefinedFunction(std::string_view functionName, ScriptEvent& event, ScriptObject source) const; + +public: + static std::string minify(const std::string& src) noexcept; + +private: + std::string m_who; + std::string m_original_source; + std::string m_modified_source; + std::string_view m_clientside; + std::string_view m_serverside; + CompiledScriptResultPtr m_client_script; + CompiledScriptResultPtr m_server_script; + size_t m_hash = 0; + + void split(std::string& source) noexcept; + void compileScript() noexcept; +}; + +//---------------------------- + +inline const size_t Script::getHash() const noexcept +{ + return m_hash; +} + +inline Script& Script::operator=(const Script& o) noexcept +{ + m_who = o.m_who; + m_original_source = o.m_original_source; + m_modified_source = o.m_modified_source; + split(m_modified_source); + m_client_script = o.m_client_script; + m_server_script = o.m_server_script; + m_hash = o.m_hash; + return *this; +} + +inline Script& Script::operator=(Script&& o) noexcept +{ + m_who = std::move(o.m_who); + m_original_source = std::move(o.m_original_source); + m_modified_source = std::move(o.m_modified_source); + m_clientside = std::move(o.m_clientside); + m_serverside = std::move(o.m_serverside); + m_client_script = std::move(o.m_client_script); + m_server_script = std::move(o.m_server_script); + m_hash = o.m_hash; + return *this; +} + +inline const std::string& Script::getOriginalSource() const noexcept +{ + return m_original_source; +} + +inline const std::string& Script::getModifiedSource() const noexcept +{ + return m_modified_source; +} + +inline std::string_view Script::getClientSide() const noexcept +{ + return m_clientside; +} + +inline std::string_view Script::getServerSide() const noexcept +{ + return m_serverside; +} + +//---------------------------- + +inline Script& Script::setOriginalSource(std::string_view who, std::string&& source) noexcept +{ + m_who = who; + m_original_source = std::move(source); + m_hash = string::string_hash{}(m_original_source); + return setModifiedSource(m_original_source); +} + +inline Script& Script::setOriginalSource(std::string_view who, const std::string& source) noexcept +{ + m_who = who; + m_original_source = source; + m_hash = string::string_hash{}(m_original_source); + return setModifiedSource(m_original_source); +} + +inline Script& Script::setModifiedSource(const std::string& source) noexcept +{ + m_modified_source = std::move(minify(source)); + split(m_modified_source); + compileScript(); + return *this; +} + +inline Script& Script::setClientCompiledScript(CompiledScriptResultPtr script) noexcept +{ + m_client_script = script; + return *this; +} + +inline Script& Script::setServerCompiledScript(CompiledScriptResultPtr script) noexcept +{ + m_server_script = script; + return *this; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // SCRIPT_H diff --git a/server/include/scripting/ScriptAction.h b/server/include/scripting/ScriptAction.h deleted file mode 100644 index 32af6afe1..000000000 --- a/server/include/scripting/ScriptAction.h +++ /dev/null @@ -1,90 +0,0 @@ -#ifndef SCRIPTACTION_H -#define SCRIPTACTION_H - -#include -#include - -#include "scripting/interface/ScriptArguments.h" -#include "scripting/interface/ScriptFunction.h" - -class ScriptAction -{ -public: - ScriptAction() = default; - - explicit ScriptAction(IScriptFunction* function, IScriptArguments* args, const std::string& action = "") - : m_function(function), m_args(args), m_action(action) - { - m_function->increaseReference(); - } - - ScriptAction(const ScriptAction& o) = delete; - ScriptAction& operator=(const ScriptAction& o) = delete; - - ScriptAction(ScriptAction&& o) noexcept - { - m_action = std::move(o.m_action); - m_args = o.m_args; - m_function = o.m_function; - - o.m_args = nullptr; - o.m_function = nullptr; - } - - ScriptAction& operator=(ScriptAction&& o) noexcept - { - m_action = std::move(o.m_action); - m_args = o.m_args; - m_function = o.m_function; - - o.m_args = nullptr; - o.m_function = nullptr; - return *this; - } - - ~ScriptAction() - { - if (m_args) - { - delete m_args; - } - - if (m_function) - { - m_function->decreaseReference(); - if (!m_function->isReferenced()) - { - delete m_function; - } - } - } - - bool invoke() const - { - assert(m_args); - - return m_args->invoke(m_function, true); - } - - const std::string& getAction() const - { - return m_action; - } - - IScriptArguments* getArguments() const - { - return m_args; - } - - IScriptFunction* getFunction() const - { - return m_function; - } - -protected: - std::string m_action; - IScriptArguments* m_args = nullptr; - IScriptFunction* m_function = nullptr; -}; - -#endif diff --git a/server/include/scripting/ScriptClass.h b/server/include/scripting/ScriptClass.h index 35f495d35..3bc0ae3bc 100644 --- a/server/include/scripting/ScriptClass.h +++ b/server/include/scripting/ScriptClass.h @@ -1,42 +1,70 @@ -#ifndef TSCRIPTCLASS_H -#define TSCRIPTCLASS_H +#ifndef SCRIPTCLASS_H +#define SCRIPTCLASS_H +#include +#include +#include +#include #include #include -#include "scripting/SourceCode.h" +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// class ScriptClass { public: - ScriptClass(const std::string& className, const std::string& classSource); - ~ScriptClass(); + ScriptClass(std::string_view className, std::string_view classScript); + ~ScriptClass() = default; - // Functions -> Inline Get-Functions CString getClassPacket() const; - const std::string& getName() const - { - return m_className; - } + [[inline]] Script& getScript(); + [[inline]] const Script& getScript() const; + ScriptClass& setScript(std::string_view classScript); - const CString& getByteCode() const - { - return m_bytecode; - } + [[inline]] uint32_t getCheckSum() const; - const SourceCode& getSource() const - { - return m_source; - } +public: + const std::string name; + clock::time_point modTime; -private: - void parseScripts(const std::string& classSource); +public: + EventDispatcher onScriptModified; - std::string m_className; - SourceCode m_source; - CString m_bytecode; +private: + Script m_script; + uint32_t m_checksum; + std::string m_desKey; + std::string m_header; }; +using ScriptClassPtr = std::shared_ptr; + +//---------------------------- + +inline Script& ScriptClass::getScript() +{ + return m_script; +} + +inline const Script& ScriptClass::getScript() const +{ + return m_script; +} + +inline uint32_t ScriptClass::getCheckSum() const +{ + return m_checksum; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal -#endif +#endif // SCRIPTCLASS_H diff --git a/server/include/scripting/ScriptContainers.h b/server/include/scripting/ScriptContainers.h new file mode 100644 index 000000000..a12606dab --- /dev/null +++ b/server/include/scripting/ScriptContainers.h @@ -0,0 +1,1107 @@ +#ifndef SCRIPTCONTAINERS_H +#define SCRIPTCONTAINERS_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +//////////////////////////////////////////////////////////////////////////////// + +class Level; +class Weapon; +using LevelPtr = std::shared_ptr; +using WeaponPtr = std::shared_ptr; + + +//////////////////////////////////////////////////////////// +// GameValue +//////////////////////////////////////////////////////////// + +struct set_temporary_t { explicit set_temporary_t() = default; }; +inline constexpr set_temporary_t set_temporary{}; + +/// @brief Concept to ensure the value passed to `GameValue` is a valid type. +template +concept ValidGameValue = std::same_as, bool> + || std::same_as, double> + || std::same_as, std::string> + || std::same_as, std::vector> + || std::same_as, ScriptObject> + || std::same_as, std::vector>; + +/// @brief A variant of the types that can be stored in a GameValue, used by the getter/setter functions. +using GameValueVariant = std::variant*, std::optional*, std::optional*, std::optional>*, std::optional>*>; + +/// @brief A container that can hold one or more valid game value types. +/// +/// This is used to represent a single value that can be one or many of these types. +/// It provides methods to set and retrieve the value in a type-safe manner. +struct GameValue +{ + using func_get = std::function)>; + using func_set = std::function)>; + +public: + /// @brief Deserializes a variable. + /// @tparam T The data type of the variable. + /// @param identifier The identifier name of the variable. + /// @param data The data to deserialize. + /// @return A reference to this. + template + static GameValue deserialize(std::string identifier, const std::string_view data); + + /// @brief Deserializes a variable. + /// @param line The data to deserialize (should include the full data line, e.g.: VAR identifier=1,2,3). + /// @return A reference to this. + static std::optional deserialize(const std::string_view line); + +public: + GameValue() = default; + GameValue(ValidGameValue auto&& value) + { + insert(std::forward(value)); + } + GameValue(std::string_view identifier, ValidGameValue auto&& value) + : identifier(identifier) + { + insert(std::forward(value)); + } + GameValue(func_get getter, func_set setter) + : m_getter(getter), m_setter(setter) {} + GameValue(std::string_view identifier, func_get getter, func_set setter) + : identifier(identifier), m_getter(getter), m_setter(setter) {} + GameValue(std::string_view identifier, ValidGameValue auto&& value, func_get getter, func_set setter) + : identifier(identifier), m_getter(getter), m_setter(setter) + { + insert(std::forward(value)); + } + +public: + GameValue(set_temporary_t, ValidGameValue auto&& value) + : temporary(true) + { + insert(std::forward(value)); + } + GameValue(set_temporary_t, std::string_view identifier, ValidGameValue auto&& value) + : identifier(identifier), temporary(true) + { + insert(std::forward(value)); + } + GameValue(set_temporary_t, func_get getter, func_set setter) + : temporary(true), m_getter(getter), m_setter(setter) {} + GameValue(set_temporary_t, std::string_view identifier, func_get getter, func_set setter) + : identifier(identifier), temporary(true), m_getter(getter), m_setter(setter) {} + GameValue(set_temporary_t, std::string_view identifier, ValidGameValue auto&& value, func_get getter, func_set setter) + : identifier(identifier), temporary(true), m_getter(getter), m_setter(setter) + { + insert(std::forward(value)); + } + +public: + GameValue(const GameValue& other) + : identifier(other.identifier), temporary(other.temporary) + , m_boolean(other.m_boolean), m_number(other.m_number), m_text(other.m_text), m_array(other.m_array), m_source(other.m_source) + , m_getter(other.m_getter), m_setter(other.m_setter) + {} + GameValue(GameValue&& other) noexcept + : identifier(std::move(other.identifier)), temporary(other.temporary) + , m_boolean(std::move(other.m_boolean)), m_number(std::move(other.m_number)), m_text(std::move(other.m_text)), m_array(std::move(other.m_array)), m_source(std::move(other.m_source)) + , m_getter(other.m_getter), m_setter(other.m_setter) + {} + + GameValue& operator=(const GameValue& other) noexcept; + GameValue& operator=(GameValue&& other) noexcept; + bool operator==(const GameValue& other) noexcept; + explicit operator bool() const; + +public: + /// @brief The identifier of the variable. + std::string identifier; + + /// @brief Marks if the variable is temporary and should be cleared when appropriate. + bool temporary = false; + +public: + /// @brief Retrieves the stored value of the specified type, if present. + /// @tparam T The type of value to retrieve. Must satisfy the `ValidGameValue` constraint. + /// @param index An optional index to specify which element of the array to assign the value to, if applicable. + /// @return A reference to a 'std::optional{ T }' containing the stored value if it exists; otherwise, throws `std::bad_variant_access` if the type is not supported. + template + [[inline]] const std::optional get(std::optional index = std::nullopt) const; + + /// @brief Retrieves a pointer to the stored value of the specified type, if present. + /// @tparam T The type of value to retrieve. Must satisfy the `ValidGameValue` constraint. + /// @param index An optional index to specify which element of the array to assign the value to, if applicable. + /// @return A pointer to type T containing the stored value if it exists; otherwise, throws `std::bad_variant_access` if the type is not supported. + template + [[inline]] const T* get_unsafe(std::optional index = std::nullopt) const; + + /// @brief Sets the value of the GameValue object to the provided value, resetting any existing number, text, or array state. + /// @param value The new value to assign to the GameValue object. Must satisfy the ValidGameValue concept. + /// @param index An optional index to specify which element of the array to assign the value to, if applicable. + /// @return A reference to the modified GameValue object. + [[inline]] GameValue& set(ValidGameValue auto&& value, std::optional index = std::nullopt); + + /// @brief Assigns a value to the GameValue, overwriting the value of the passed type. Other types are not affected. + /// @param value The value to assign to the GameValue. Must satisfy the ValidGameValue concept. + /// @param index An optional index to specify which element of the array to assign the value to, if applicable. + /// @return A reference to the modified GameValue object. + [[inline]] GameValue& assign(ValidGameValue auto&& value, std::optional index = std::nullopt); + + /// @brief Assigns values from another GameValue object to this one, overwriting the values of the specified types. + /// @tparam ...Types A list of types to assign from the other GameValue. + /// @param other The other GameValue object from which to assign values. + /// @param index The index of the array to assign a value, if applicable. + /// @return A reference to the modified GameValue object. + template + GameValue& assign(const GameValue& other, std::optional index = std::nullopt) + { + (assign(other.get().value_or(Types{}), index), ...); + return *this; + } + + /// @brief Unassigns a value type from the GameValue. + /// @tparam Type The value type to unassign. + /// @return A reference to the modified GameValue object. + template + GameValue& unassign() + { + if constexpr (std::same_as) + m_boolean = std::nullopt; + if constexpr (std::same_as) + m_number = std::nullopt; + if constexpr (std::same_as) + m_text = std::nullopt; + if constexpr (std::same_as>) + m_array = std::nullopt; + if constexpr (std::same_as || std::same_as>) + m_source = std::nullopt; + return *this; + } + + /// @brief If the variable is an array, flattens it into a single value. + /// @return A reference to the modified GameValue object. + GameValue flatten(int64_t index) const noexcept; + + /// @brief Tests the GameValue as a flag check. + /// @return True if the GameValue has a boolean value or a non-empty string value, false otherwise. + bool testAsFlag() const; + + /// @brief Checks if the GameValue has a value of the specified type. + /// @tparam T The type to check for. Must satisfy the `ValidGameValue` constraint. + /// @return True if the GameValue has a value of the specified type; otherwise, false. + template + [[inline]] bool has() const; + + /// @brief Checks if the GameValue has multiple values. + /// @return True if the GameValue has multiple values; otherwise, false. + [[inline]] bool has_many() const; + + /// @brief Returns the stored getter function. + /// @return The getter function stored in the object. + func_get getGetter() const { return m_getter; } + + /// @brief Returns the stored setter function. + /// @return The setter function stored in the object. + func_set getSetter() const { return m_setter; } + + /// @brief Sets the getter function. + /// @param getter The function to be used as the getter. + void setGetter(func_get getter) { m_getter = getter; } + + /// @brief Sets the setter function for the object. + /// @param setter The function to be used as the setter. + void setSetter(func_set setter) { m_setter = setter; } + + /// @brief Serializes a variable for distribution. + /// @param name The name of the game variable to serialize. + /// @return An optional string that contains the serialized variable. + std::optional serializeModern(std::string_view name) const noexcept; + + /// @brief Serializes the variable for saving. + /// @return A serialized string for writing to disk. + template + [[inline]] std::string serialize() const; + +protected: + std::optional m_boolean; + std::optional m_number; + std::optional m_text; + std::optional> m_array; + std::optional> m_source; + func_get m_getter; + func_set m_setter; + + [[inline]] GameValue& insert(const ValidGameValue auto& value, std::optional index = std::nullopt); + [[inline]] GameValue& insert(ValidGameValue auto&& value, std::optional index = std::nullopt); +}; + +//---------------------------- + +template +inline const std::optional GameValue::get(std::optional index) const +{ + if constexpr (std::same_as) + { + if (m_getter) + { + std::optional val; + if (m_getter(&val, index); val.has_value()) + return val; + } + else if (m_array.has_value() && index.has_value()) + { + if (index.value() >= 0 && index.value() < (int64_t)m_array.value().size()) + return m_array.value().at(index.value()); + return 0.0; + } + if (m_number.has_value()) + return m_number.value(); + if (m_boolean.has_value()) + return m_boolean.value() ? 1.0 : 0.0; + return std::nullopt; + } + if constexpr (std::same_as) + { + if (m_getter) + { + std::optional val; + if (m_getter(&val, index); val.has_value()) + return val; + } + return m_text; + } + if constexpr (std::same_as>) + { + if (m_getter) + { + std::optional> val; + if (m_getter(&val, index); val.has_value()) + return val; + } + return m_array; + } + if constexpr (std::same_as) + { + if (m_getter) + { + std::optional val; + if (m_getter(&val, index); val.has_value()) + return val; + } + if (m_boolean.has_value()) + return m_boolean.value(); + if (m_number.has_value()) + return !DoubleIsZero(m_number.value()); + return std::nullopt; + } + if constexpr (std::same_as) + { + if (m_getter) + { + std::optional> val; + if (m_getter(&val, index); val.has_value()) + return val; + } + if (!m_source.has_value()) + return std::nullopt; + if (index.has_value()) + { + if (index.value() >= 0 && index.value() < (int64_t)m_source.value().size()) + return m_source.value().at(index.value()); + return m_source.value().at(0); + } + return m_source.value().at(0); + } + if constexpr (std::same_as>) + { + if (m_getter) + { + std::optional> val; + if (m_getter(&val, index); val.has_value()) + return val; + } + return m_source; + } + else throw std::bad_variant_access(); +} + +template +inline const T* GameValue::get_unsafe(std::optional index) const +{ + if constexpr (std::same_as) + { + if (m_getter) m_getter(const_cast*>(&m_number), index); + if (m_array.has_value() && index.has_value()) + { + if (index.value() >= 0 && index.value() < (int64_t)m_array.value().size()) + return &m_array.value().at(index.value()); + return nullptr; + } + if (!m_number.has_value()) return nullptr; + return &m_number.value(); + } + if constexpr (std::same_as) + { + if (m_getter) m_getter(const_cast*>(&m_text), index); + if (!m_text.has_value()) return nullptr; + return &m_text.value(); + } + if constexpr (std::same_as>) + { + if (m_getter) m_getter(const_cast>*>(&m_array), index); + if (!m_array.has_value()) return nullptr; + return &m_array.value(); + } + if constexpr (std::same_as) + { + if (m_getter) m_getter(const_cast*>(&m_boolean), index); + if (!m_boolean.has_value()) return nullptr; + return &m_boolean.value(); + } + if constexpr (std::same_as) + { + if (m_getter) m_getter(const_cast>*>(&m_source), index); + if (!m_source.has_value()) return nullptr; + if (index.has_value()) + { + if (index.value() >= 0 && index.value() < (int64_t)m_source.value().size()) + return &m_source.value().at(index.value()); + return &m_source.value().at(0); + } + return &m_source.value().at(0); + } + if constexpr (std::same_as>) + { + if (m_getter) m_getter(const_cast>*>(&m_source), index); + if (!m_source.has_value()) return nullptr; + return &m_source.value(); + } + else throw std::bad_variant_access(); +} + +inline GameValue& GameValue::set(ValidGameValue auto&& value, std::optional index) +{ + m_number = std::nullopt; + m_text = std::nullopt; + m_array = std::nullopt; + m_boolean = std::nullopt; + m_source = std::nullopt; + return insert(std::forward(value), index); +} + +inline GameValue& GameValue::assign(ValidGameValue auto&& value, std::optional index) +{ + return insert(std::forward(value), index); +} + +inline GameValue& GameValue::insert(const ValidGameValue auto& value, std::optional index) +{ + using V = std::remove_cvref_t; + if constexpr (std::same_as) + { + if (m_array.has_value() && index.has_value()) + { + if (index.value() >= 0 && index.value() < (int64_t)m_array.value().size()) + m_array.value().at(index.value()) = value; + if (m_setter) m_setter(&m_array, index); + } + else + { + m_number = value; + if (m_setter) m_setter(&m_number, index); + } + } + else if constexpr (std::same_as) + { + if (value.empty()) + m_text = std::nullopt; + else m_text = value; + if (m_setter) m_setter(&m_text, index); + } + else if constexpr (std::same_as>) + { + m_array = value; + if (m_setter) m_setter(&m_array, index); + } + else if constexpr (std::same_as) + { + m_boolean = value; + if (m_setter) m_setter(&m_boolean, index); + } + else if constexpr (std::same_as) + { + if (m_source.has_value() && index.has_value()) + { + if (index.value() >= 0 && index.value() < (int64_t)m_source.value().size()) + m_source.value().at(index.value()) = value; + } + else + { + m_source.value().clear(); + m_source.value().push_back(value); + } + if (m_setter) m_setter(&m_source, index); + } + else if constexpr (std::same_as>) + { + m_source = value; + if (m_setter) m_setter(&m_source, index); + } + else throw std::bad_variant_access(); + + return *this; +} + +inline GameValue& GameValue::insert(ValidGameValue auto&& value, std::optional index) +{ + using V = std::remove_cvref_t; + if constexpr (std::same_as) + { + if (m_array.has_value() && index.has_value()) + { + if (index.value() >= 0 && index.value() < (int64_t)m_array.value().size()) + m_array.value().at(index.value()) = value; + if (m_setter) m_setter(&m_array, index); + } + else + { + m_number = value; + if (m_setter) m_setter(&m_number, index); + } + } + else if constexpr (std::same_as) + { + if (value.empty()) + m_text = std::nullopt; + m_text = std::move(value); + if (m_setter) m_setter(&m_text, index); + } + else if constexpr (std::same_as>) + { + m_array = std::move(value); + if (m_setter) m_setter(&m_array, index); + } + else if constexpr (std::same_as) + { + m_boolean = value; + if (m_setter) m_setter(&m_boolean, index); + } + else if constexpr (std::same_as) + { + if (m_source.has_value() && index.has_value()) + { + if (index.value() >= 0 && index.value() < (int64_t)m_source.value().size()) + m_source.value().at(index.value()) = value; + } + else + { + m_source.value().clear(); + m_source.value().push_back(value); + } + if (m_setter) m_setter(&m_source, index); + } + else if constexpr (std::same_as>) + { + m_source = std::move(value); + if (m_setter) m_setter(&m_source, index); + } + else throw std::bad_variant_access(); + + return *this; +} + +template +inline bool GameValue::has() const +{ + if constexpr (std::same_as) + return m_number.has_value(); + else if constexpr (std::same_as) + return m_text.has_value(); + else if constexpr (std::same_as>) + return m_array.has_value(); + else if constexpr (std::same_as) + return m_boolean.has_value(); + else if constexpr (std::same_as || std::same_as>) + return m_source.has_value(); + return false; +} + +inline bool GameValue::has_many() const +{ + int count = 0; + if (m_number.has_value()) ++count; + if (m_text.has_value()) ++count; + if (m_array.has_value()) ++count; + if (m_boolean.has_value()) ++count; + if (m_source.has_value()) ++count; + return count > 1; +} + +//---------------------------- + +template +GameValue GameValue::deserialize(std::string identifier, const std::string_view data) +{ + if constexpr (std::same_as) + return GameValue{ identifier, true }; + if constexpr (std::same_as) + return GameValue{ identifier, string::toDouble(data) }; + if constexpr (std::same_as) + return GameValue{ identifier, std::string{ data } }; + if constexpr (std::same_as>) + { + std::vector array; + for (auto number : string::split(data, ","sv)) + array.emplace_back(string::toDouble(number)); + return GameValue{ identifier, std::move(array) }; + } + return GameValue{}; +} + +template +inline std::string GameValue::serialize() const +{ + if constexpr (std::same_as) + return {}; + if constexpr (std::same_as) + return std::format("{}", m_number.value_or(0.0)); + if constexpr (std::same_as) + return m_text.value_or(std::string{}); + if constexpr (std::same_as>) + { + std::string array; + if (m_array.has_value()) + { + for (size_t i = 0; i < m_array.value().size(); ++i) + { + array += std::format("{}", (m_array.value())[i]); + if (i != m_array.value().size() - 1) + array += ","; + } + } + return array; + } + return {}; +} + + +//////////////////////////////////////////////////////////// +// GameVariableStore +//////////////////////////////////////////////////////////// + +/// @brief Maintains a collection of game variables. +class GameVariableStore +{ +public: + virtual ~GameVariableStore() {} + +public: + /// @brief Adds a new game variable with the specified name and value. + /// @param name The name of the game variable to add. + /// @param value The value to assign to the new game variable (moved). + /// @return A weak pointer to the newly added GameValue. + virtual std::weak_ptr add(std::string_view name, GameValue&& value) noexcept; + + /// @brief Adds a new game variable. + /// @param variable The variable to add to the store (moved). + /// @return A weak pointer to the newly added GameValue. + virtual std::weak_ptr add(GameValue&& variable) noexcept; + + /// @brief Removes an item identified by the given name. + /// @param name The name of the item to remove. + /// @return true if the item was successfully removed; false otherwise. + virtual bool remove(std::string_view name) noexcept; + + /// @brief Checks if the specified name is contained within the object. + /// @param name The name to search for. + /// @return true if the name is found; otherwise, false. + virtual bool contains(std::string_view name) const noexcept; + + /// @brief Retrieves a weak pointer to a game variable by its name. + /// @param name The name of the game variable to retrieve. + /// @return A std::weak_ptr to the GameValue associated with the given name. If the variable does not exist, the returned weak pointer will be empty. + virtual std::weak_ptr get(std::string_view name) noexcept; + + /// @brief Retrieves a weak pointer to a game variable by its name. + /// @param name The name of the game variable to retrieve. + /// @return A weak pointer to the requested GameValue, or an empty weak pointer if not found. + virtual const std::weak_ptr get(std::string_view name) const noexcept; + + /// @brief Retrieves the value associated with the specified name, if it exists. + /// @tparam T The type to which the value should be converted. + /// @param name The name of the value to retrieve. + /// @return An optional containing the value of type GameValue if found; otherwise, an empty optional. + template + [[inline]] const std::optional getValue(const std::string_view name) const noexcept; + + /// @brief Retrieves a game variable by name, or adds it if it does not exist. + /// @param name The name of the game variable to retrieve or add. + /// @return A weak pointer to the retrieved or newly added GameValue. + virtual std::weak_ptr getOrAdd(std::string_view name) noexcept; + + /// @brief Retrieves a variable stub by name, or adds it if it does not exist. + /// @param name The name of the variable to retrieve or add. + /// @return A GameValue representing the variable with getters/setters to update the variable in storage. + virtual GameValue getOrStub(std::string_view name); + + /// @brief Clears all temporary variables from the store. + virtual void clearTemporary() noexcept; + + /// @brief Cleras temporary variables with a specific prefix from the store. + /// @param prefix The prefix to match for temporary variables to clear. + virtual void clearTemporary(std::string_view prefix) noexcept; + + /// @brief Serializes a variable for distribution. + /// @param name The name of the game variable to serialize. + /// @return An optional string that contains the serialized variable. + virtual std::optional serializeModern(std::string_view name) const noexcept; + + /// @brief Fully serializes a variable for writing to the disk. + /// @param name The name of a game variable to serialize. + /// @return A list of serialized data. + virtual std::vector serialize(std::string_view name) const noexcept; + +public: + /// @brief Marks the container as not accepting any new variables, nor deleting existing ones. + bool static_container = false; + + /// @brief The variable store map. + string_map> store; +}; + +//---------------------------- + +template +inline const std::optional GameVariableStore::getValue(const std::string_view name) const noexcept +{ + if (store.empty()) return std::nullopt; + auto it = store.find(name); + if (it == store.end()) return std::nullopt; + // it->second->update(); + return it->second->get(); +} + + +//////////////////////////////////////////////////////////// +// ScriptEventQueue +//////////////////////////////////////////////////////////// + +/// @brief Queued script events. +class ScriptEventQueue +{ +public: + /// @brief Gets the underlying queue of script events. + /// @return The queue of script events. + [[inline]] std::deque& queue(); + +public: + /// @brief Determines whether a specific event exists for a given initiator. + /// @param type The type of script event to check for. + /// @param initiator Who initiated the event. + /// @return True if the event exists for the given initiator; otherwise, false. + bool hasEvent(ScriptEventType type, ScriptObject initiator); + + /// @brief Adds an event to the queue with the specified type and initiator. + /// @param type The type of the script event to add. + /// @param initiator Who initiated the event. + void addEvent(ScriptEventType type, ScriptObject initiator); + + /// @brief Adds an event to the queue with the specified type, initiator, and additional arguments. + /// @param type The type of the script event to add. + /// @param initiator Who initiated the event. + /// @param ...args A list of additional arguments to be passed with the event. + [[inline]] void addEvent(ScriptEventType type, ScriptObject initiator, string::NotInputRangeNotString auto&&... args); + + /// @brief Adds an event to the queue with the specified type, initiator, and additional arguments. + /// @param type The type of the script event to add. + /// @param initiator Who initiated the event. + /// @param range A list of additional arguments to be passed with the event. + [[inline]] void addEvent(ScriptEventType type, ScriptObject initiator, string::InputRangeNotString auto&& range); + +private: + void addEvent(const ScriptEvent& event); + void addEvent(ScriptEvent&& event); + +private: + std::deque m_eventQueue; +}; + +//---------------------------- + +inline std::deque& ScriptEventQueue::queue() +{ + return m_eventQueue; +} + +inline void ScriptEventQueue::addEvent(ScriptEventType type, ScriptObject initiator, string::NotInputRangeNotString auto&&... args) +{ + ScriptEvent event{ .type = type, .initiator = initiator, .args = { std::forward(args)... } }; + addEvent(std::move(event)); +} + +inline void ScriptEventQueue::addEvent(ScriptEventType type, ScriptObject initiator, string::InputRangeNotString auto&& range) +{ + static_assert(!string::PointerToConstCharString, + "Don't use a const char* in the ranged variant of ScriptEventQueue::addEvent, pass in a std::string_view instead."); + + ScriptEvent event{ .type = type, .initiator = initiator }; + auto transformed = range | std::views::transform([](const auto& arg) -> std::any { return std::any{ arg }; }); + event.args.insert(event.args.end(), std::ranges::begin(transformed), std::ranges::end(transformed)); + addEvent(std::move(event)); +} + + +//////////////////////////////////////////////////////////// +// ScriptParameters +//////////////////////////////////////////////////////////// + +template +concept HasScriptParameters = requires(T t) +{ + { T::scriptParameters } -> std::convertible_to>; +}; + +template +concept HasConstructibleScriptParameters = requires(T t) +{ + { T::scriptParameters } -> std::convertible_to>; + { t.constructScriptParameters() } -> std::same_as; +}; + +template +inline std::optional getScriptParameter(T& source, std::string_view name) +{ + if constexpr (HasConstructibleScriptParameters) + { + if (source.scriptParameters.empty()) + source.constructScriptParameters(); + } + + auto it = source.scriptParameters.find(name); + if (it == source.scriptParameters.end()) + return std::nullopt; + return it->second; +} + + +//////////////////////////////////////////////////////////// +// ScriptContainer +//////////////////////////////////////////////////////////// + +/// @brief A container that holds a script event queue and a game variable store. +struct ScriptContainer +{ + ScriptEventQueue events; + GameVariableStore variables; +}; + + +//////////////////////////////////////////////////////////// +// Ranges +//////////////////////////////////////////////////////////// + +/// @brief Provides views that manipulate variables. +namespace variables +{ + +/// @brief A view that filters out temporary variables. +inline constexpr auto no_temporary = std::views::filter([](const decltype(GameVariableStore::store)::value_type& pair) -> bool { return !pair.second->temporary; }); + +/// @brief A view that filters out optional that don't have a value. +//inline constexpr auto with_value = std::views::filter([](const auto& opt) -> bool { return opt.has_value(); }); + +/// @brief Only gets variables that identify as flags. +inline constexpr auto only_flags = std::views::filter([](const decltype(GameVariableStore::store)::value_type& pair) -> bool { return pair.second->testAsFlag(); }); + +} // end namespace preagonal::variables + + +//////////////////////////////////////////////////////////// +// Concepts +//////////////////////////////////////////////////////////// + +template +concept ValidGameValueCallable = requires(T t) +{ + { t() } -> std::convertible_to; +}; + +template +concept ValidGameValueCallableWithIndex = requires(T t) +{ + { t(std::declval>()) } -> std::convertible_to; +}; + + +//////////////////////////////////////////////////////////// +// Functions +//////////////////////////////////////////////////////////// + +template +void copyToArrayAs(const auto& vec, auto& propvalue) +{ + size_t count = std::min(vec.size(), propvalue.size()); + auto it = std::begin(vec); + for (size_t i = 0; i < count; ++i, ++it) + { + propvalue[i] = static_cast(*it); + } +} + +inline void stupid_ide() +{ + auto not_transitive = std::format(""); +} + + +//////////////////////////////////////////////////////////// +// Prop helpers +//////////////////////////////////////////////////////////// + +/// @brief A getter function for a property that gets its results from another getter function. +GameValue::func_get gameValueGetter(ValidGameValueCallable auto getter) +{ + return [getter](GameValueVariant incoming, std::optional index) + { + GameValue value{ getter() }; + const auto picker = visit_functions + { + [&](std::optional* in) { *in = value.get(); }, + [&](std::optional* in) { *in = value.get(); }, + [&](std::optional* in) { *in = value.get(); }, + [&](std::optional>* in) { *in = value.get>(); }, + [&](std::optional>* in) { *in = value.get>(); } + }; + std::visit(picker, incoming); + }; +} + +/// @brief A getter function for a property that gets its results from another getter function. +inline GameValue::func_get gameValueGetter(ValidGameValueCallableWithIndex auto getter) +{ + return [getter](GameValueVariant incoming, std::optional index) + { + GameValue value{ getter(index) }; + const auto picker = visit_functions + { + [&](std::optional* in) { *in = value.get(); }, + [&](std::optional* in) { *in = value.get(); }, + [&](std::optional* in) { *in = value.get(); }, + [&](std::optional>* in) { *in = value.get>(); }, + [&](std::optional>* in) { *in = value.get>(); } + }; + std::visit(picker, incoming); + }; +} + +/// @brief A getter function for a property that gets its results from a property directly. +GameValue::func_get gameValueGetter(auto& value) +{ + using V = std::remove_cvref_t; + static_assert(std::integral || std::floating_point || string::StringVariant || std::ranges::forward_range || std::same_as || std::same_as>, + "gameValueGetter called with an unsupported type. Supported types are integral, floats, string, ranges, or ScriptObjectSources."); + + // Number. + if constexpr (std::integral || std::floating_point) + { + return [&value](GameValueVariant incoming, std::optional index) + { + if (auto var = std::get_if*>(&incoming); var != nullptr) + **var = value; + }; + } + // String. + else if constexpr (string::StringVariant) + { + return [&value](GameValueVariant incoming, std::optional index) + { + if (auto var = std::get_if*>(&incoming); var != nullptr) + **var = value; + }; + } + // ScriptObject (and array variant). + else if constexpr (std::same_as || std::same_as>) + { + return [&value](GameValueVariant incoming, std::optional index) + { + if (auto var = std::get_if>*>(&incoming); var != nullptr) + { + if constexpr (std::same_as) + { + (*var)->emplace(); + (*var)->value().push_back(value); + } + else **var = value; + } + }; + } + // Array. + else if constexpr (std::ranges::forward_range) + { + return [&value](GameValueVariant incoming, std::optional index) + { + if (auto var = std::get_if>*>(&incoming); var != nullptr) + { + // Transform the range to a vector of doubles. + **var = value | std::views::transform([](const auto& v) { return static_cast(v); }) | std::ranges::to>(); + } + }; + } + + throw std::invalid_argument("gameValueGetter called with an unsupported type."); +} + +/// @brief A setter function for a property that needs an additional setter function to write the values. +template +GameValue::func_set gameValueSetter(Who* who, std::optional prop, std::function)> setter) +{ + return [who, prop, setter](GameValueVariant incoming, std::optional index) + { + GameValue value; + const auto picker = visit_functions + { + [&](std::optional* in) { if (in->has_value()) value.set(in->value(), index); }, + [&](std::optional* in) { if (in->has_value()) value.set(in->value(), index); }, + [&](std::optional* in) { if (in->has_value()) value.set(in->value(), index); }, + [&](std::optional>* in) { if (in->has_value()) value.set(in->value(), index); }, + [&](std::optional>* in) { if (in->has_value()) value.set(in->value(), index); } + }; + std::visit(picker, incoming); + + // Call the setter function. + setter(value, index); + + // Record the modification time for the property. + if (prop.has_value() && who != nullptr) + who->modTime[PROPID(prop.value())] = currentTime(); + }; +} + +/// @brief A helper setter function that converts to a GameValue and passes to the next callback function. +inline GameValue::func_set gameValueSetter(std::function)> setter) +{ + return [setter](GameValueVariant incoming, std::optional index) + { + GameValue value; + const auto picker = visit_functions + { + [&](std::optional* in) { if (in->has_value()) value.set(in->value(), index); }, + [&](std::optional* in) { if (in->has_value()) value.set(in->value(), index); }, + [&](std::optional* in) { if (in->has_value()) value.set(in->value(), index); }, + [&](std::optional>* in) { if (in->has_value()) value.set(in->value(), index); }, + [&](std::optional>* in) { if (in->has_value()) value.set(in->value(), index); } + }; + std::visit(picker, incoming); + + // Call the setter function. + setter(value, index); + }; +} + +/// @brief A setter function for a property that can directly set to a value. +template +GameValue::func_set gameValueSetter(Who* who, std::optional prop, Value& propvalue) +{ + using V = std::remove_cvref_t; + static_assert(std::integral || std::floating_point || string::StringVariant || std::ranges::random_access_range, + "gameValueSetter called with an unsupported type. Supported types are integral, floats, string, or ranges."); + + // Number. + if constexpr (std::integral || std::floating_point) + { + return [who, prop, &propvalue](GameValueVariant incoming, std::optional index) + { + if (auto value = std::get_if*>(&incoming); value != nullptr) + propvalue = static_cast((*value)->value_or(V{})); + else if (auto value = std::get_if>*>(&incoming); value != nullptr && (*value)->has_value() && !(*value)->value().empty()) + { + auto& vec = (*value)->value(); + auto indexValue = index.value_or(0); + if (indexValue >= 0 && indexValue < (int64_t)vec.size()) + propvalue = static_cast(vec.at(indexValue)); + } + else if (auto value = std::get_if*>(&incoming); value != nullptr) + propvalue = static_cast((*value)->value_or(false) ? 1 : 0); + if (prop.has_value()) + who->modTime[PROPID(prop.value())] = currentTime(); + }; + } + // String. + else if constexpr (string::StringVariant) + { + return [who, prop, &propvalue](GameValueVariant incoming, std::optional index) + { + if (auto value = std::get_if*>(&incoming); value != nullptr) + propvalue = static_cast(**value); + if (prop.has_value()) + who->modTime[PROPID(prop.value())] = currentTime(); + }; + } + // Array. + else if constexpr (std::ranges::random_access_range) + { + return [who, prop, &propvalue](GameValueVariant incoming, std::optional index) + { + size_t propvalue_size = std::ranges::size(propvalue); + if (propvalue_size > 0) + { + using value_type = std::remove_cvref_t; + + // Setting an individual index in an array. + if (index.has_value() && index.value() >= 0 && index.value() < (int64_t)propvalue_size) + { + if (auto value = std::get_if>*>(&incoming); value != nullptr && (*value)->has_value()) + { + std::vector& darray = (**value).value(); + if (index.value() < (int64_t)darray.size()) + propvalue[index.value()] = static_cast(darray.at(index.value())); + else propvalue[index.value()] = value_type{}; + } + if (prop.has_value()) + who->modTime[PROPID(prop.value()) + index.value()] = currentTime(); + } + // Setting the whole array. + else if (!index.has_value()) + { + if (auto value = std::get_if>*>(&incoming); value != nullptr) + { + const std::vector vec = (*value)->value_or(std::vector{}); + copyToArrayAs(vec, propvalue); + } + if (prop.has_value()) + who->modTime[PROPID(prop.value())] = currentTime(); + } + } + }; + } + + throw std::invalid_argument("gameValueSetter called with an unsupported type."); +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // SCRIPTCONTAINERS_H diff --git a/server/include/scripting/ScriptEngine.h b/server/include/scripting/ScriptEngine.h deleted file mode 100644 index 00747d587..000000000 --- a/server/include/scripting/ScriptEngine.h +++ /dev/null @@ -1,250 +0,0 @@ -#ifndef CSCRIPTENGINE_H -#define CSCRIPTENGINE_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "BabyDI.h" - -#include "scripting/ScriptAction.h" -#include "scripting/ScriptFactory.h" -#include "scripting/SourceCode.h" -#include "scripting/interface/ScriptBindings.h" - -#ifdef V8NPCSERVER - #include "scripting/v8/V8ScriptWrappers.h" -#endif - -class IScriptEnv; -class IScriptFunction; - -class NPC; -class Server; -class Weapon; - -class ScriptEngine -{ -public: - ScriptEngine() = default; - ~ScriptEngine(); - - bool initialize(); - void cleanup(bool shutDown = false); - void runScripts(const std::chrono::high_resolution_clock::time_point& time); - - void scriptWatcher(); - void startScriptExecution(const std::chrono::high_resolution_clock::time_point& startTime); - bool stopScriptExecution(); - - Server* getServer() const; - IScriptEnv* getScriptEnv() const; - IScriptObject* getServerObject() const; - - bool executeNpc(NPC* npc); - bool executeWeapon(Weapon* weapon); - - void registerNpcTimer(NPC* npc); - void registerNpcUpdate(NPC* npc); - void registerWeaponUpdate(Weapon* weapon); - - void unregisterNpcTimer(NPC* npc); - void unregisterNpcUpdate(NPC* npc); - void unregisterWeaponUpdate(Weapon* weapon); - - // callbacks - IScriptFunction* getCallBack(const std::string& callback) const; - void removeCallBack(const std::string& callback); - void setCallBack(const std::string& callback, IScriptFunction* cbFunc); - - // Compile script into a ScriptFunction - IScriptFunction* compileCache(const std::string& code, bool referenceCount = true); - - // Clear cache for code - bool clearCache(const std::string& code); - - // Clear cache for code, with a WrapperScript of Type T - template - bool clearCache(const std::string& code); - - template - bool clearCache(const std::string_view& code); - - template - ScriptAction createAction(const std::string& action, Args... An); - - template - void wrapScriptObject(T* obj) const; - - const ScriptRunError& getScriptError() const; - - void reportScriptException(const ScriptRunError& error); - void reportScriptException(const std::string& error_message); - -private: - void runTimers(const std::chrono::high_resolution_clock::time_point& time); - - BabyDI_INJECT(Server, m_server); - - IScriptEnv* m_env = nullptr; - IScriptFunction* m_bootstrapFunction = nullptr; - std::unique_ptr> m_environmentObject; - std::unique_ptr> m_serverObject; - - std::chrono::high_resolution_clock::time_point m_lastScriptTimer = std::chrono::high_resolution_clock::now(); - std::chrono::nanoseconds m_accumulator = std::chrono::nanoseconds(0); - - // Script watcher - std::atomic m_scriptIsRunning = false; - std::atomic m_scriptWatcherRunning = false; - std::chrono::high_resolution_clock::time_point m_scriptStartTime; - std::mutex m_scriptWatcherLock; - std::thread m_scriptWatcherThread; - - std::unordered_map m_cachedScripts; - std::unordered_map m_callbacks; - std::unordered_set m_updateNpcs; - std::unordered_set m_updateNpcsTimer; - std::unordered_set m_updateWeapons; - std::unordered_set m_deletedCallbacks; -}; - -inline void ScriptEngine::startScriptExecution(const std::chrono::high_resolution_clock::time_point& startTime) -{ - { - std::lock_guard guard(m_scriptWatcherLock); - m_scriptStartTime = startTime; - } - m_scriptIsRunning.store(true); -} - -inline bool ScriptEngine::stopScriptExecution() -{ - bool res = m_scriptIsRunning.load(); - if (res) - m_scriptIsRunning.store(false); - return res; -} - -// Getters - -inline Server* ScriptEngine::getServer() const -{ - return m_server; -} - -inline IScriptEnv* ScriptEngine::getScriptEnv() const -{ - return m_env; -} - -inline IScriptObject* ScriptEngine::getServerObject() const -{ - return m_serverObject.get(); -} - -inline IScriptFunction* ScriptEngine::getCallBack(const std::string& callback) const -{ - auto it = m_callbacks.find(callback); - if (it != m_callbacks.end()) - return it->second; - - return nullptr; -} - -inline const ScriptRunError& ScriptEngine::getScriptError() const -{ - return m_env->getScriptError(); -} - -// Register scripts for processing - -inline void ScriptEngine::registerNpcTimer(NPC* npc) -{ - m_updateNpcsTimer.insert(npc); -} - -inline void ScriptEngine::registerNpcUpdate(NPC* npc) -{ - m_updateNpcs.insert(npc); -} - -inline void ScriptEngine::registerWeaponUpdate(Weapon* weapon) -{ - m_updateWeapons.insert(weapon); -} - -// Unregister scripts from processing - -inline void ScriptEngine::unregisterWeaponUpdate(Weapon* weapon) -{ - m_updateWeapons.erase(weapon); -} - -inline void ScriptEngine::unregisterNpcUpdate(NPC* npc) -{ - m_updateNpcs.erase(npc); -} - -inline void ScriptEngine::unregisterNpcTimer(NPC* npc) -{ - m_updateNpcsTimer.erase(npc); -} - -// - -template -ScriptAction ScriptEngine::createAction(const std::string& action, Args... An) -{ - constexpr size_t Argc = (sizeof...(Args)); - assert(Argc > 0); - - SCRIPTENV_D("Server_RegisterAction:\n"); - SCRIPTENV_D("\tAction: %s\n", action.c_str()); - SCRIPTENV_D("\tArguments: %zu\n", Argc); - - auto funcIt = m_callbacks.find(action); - if (funcIt == m_callbacks.end()) - { - SCRIPTENV_D("Global::Server_RegisterAction: Callback not registered for %s\n", action.c_str()); - return ScriptAction{}; - } - - // Create an arguments object, and pass it to ScriptAction - IScriptArguments* args = ScriptFactory::createArguments(m_env, std::forward(An)...); - assert(args); - - return ScriptAction(funcIt->second, args, action); -} - -template -inline void ScriptEngine::wrapScriptObject(T* obj) const -{ - SCRIPTENV_D("Begin Global::wrapScriptObject()\n"); - - // Wrap the object, and set the new script object on the original object - auto wrappedObject = ScriptFactory::wrapObject(m_env, ScriptConstructorId::result, obj); - obj->setScriptObject(std::move(wrappedObject)); - - SCRIPTENV_D("End Global::wrapScriptObject()\n\n"); -} - -template -inline bool ScriptEngine::clearCache(const std::string& code) -{ - return clearCache(wrapScript(code)); -} - -template -inline bool ScriptEngine::clearCache(const std::string_view& code) -{ - return clearCache(wrapScript(code)); -} - -#endif diff --git a/server/include/scripting/ScriptExecutionContext.h b/server/include/scripting/ScriptExecutionContext.h deleted file mode 100644 index ec77e7363..000000000 --- a/server/include/scripting/ScriptExecutionContext.h +++ /dev/null @@ -1,150 +0,0 @@ -#ifndef SCRIPTEXECUTION_H -#define SCRIPTEXECUTION_H - -#include -#include -#include - -#include "scripting/ScriptAction.h" -#include "scripting/ScriptEngine.h" -#include "scripting/interface/ScriptUtils.h" - -class ScriptExecutionContext -{ -public: - ScriptExecutionContext(ScriptEngine* scriptEngine) - : m_scriptEngine(scriptEngine) {} - - ~ScriptExecutionContext() { resetExecution(); } - - bool hasActions() const; - std::pair getExecutionData(); - - void addAction(ScriptAction& action); - void addAction(ScriptAction&& action); - void addExecutionSample(const ScriptTimeSample& sample); - void resetExecution(); - bool runExecution(); - -private: - ScriptEngine* m_scriptEngine; - std::vector m_actions; - std::vector m_scriptTimeSamples; -}; - -inline bool ScriptExecutionContext::hasActions() const -{ - return !m_actions.empty(); -} - -inline void ScriptExecutionContext::addExecutionSample(const ScriptTimeSample& sample) -{ -#ifndef NOSCRIPTPROFILING - m_scriptTimeSamples.push_back(sample); - - // Remove any script samples over a minute old - //if (m_scriptTimeSamples.size() > 1024) - { - auto curSampleTime = sample.sample_time; - while (!m_scriptTimeSamples.empty()) - { - auto oldSample = m_scriptTimeSamples.begin(); - auto sample_diff = std::chrono::duration_cast(curSampleTime - oldSample->sample_time); - if (sample_diff.count() < 1) - break; - - m_scriptTimeSamples.erase(oldSample); - } - } -#endif -} - -inline std::pair ScriptExecutionContext::getExecutionData() -{ - double exectime = 0.0; - unsigned int calls = 0; - -#ifndef NOSCRIPTPROFILING - auto time_now = std::chrono::high_resolution_clock::now(); - - for (auto it = m_scriptTimeSamples.begin(); it != m_scriptTimeSamples.end();) - { - auto sample_diff = std::chrono::duration_cast(time_now - (*it).sample_time); - if (sample_diff.count() >= 1) - { - it = m_scriptTimeSamples.erase(it); - continue; - } - - exectime += (*it).sample; - calls++; - ++it; - } -#endif - - return { calls, exectime }; -} - -inline void ScriptExecutionContext::addAction(ScriptAction& action) -{ - if (action.getFunction()) - { - m_actions.push_back(std::move(action)); - } -} - -inline void ScriptExecutionContext::addAction(ScriptAction&& action) -{ - if (action.getFunction()) - { - m_actions.push_back(std::move(action)); - } -} - -inline void ScriptExecutionContext::resetExecution() -{ - m_actions.clear(); - -#ifndef NOSCRIPTPROFILING - //m_scriptTimeSamples.clear(); -#endif -} - -inline bool ScriptExecutionContext::runExecution() -{ - // Take ownership of the queued actions, and clear them incase any scripts add actions. - std::vector iterateActions = std::move(m_actions); - m_actions.clear(); - - // Send start timer to engine - auto currentTimer = std::chrono::high_resolution_clock::now(); - m_scriptEngine->startScriptExecution(currentTimer); - - // iterate over queued actions - SCRIPTENV_D("Running %zd actions:\n", iterateActions.size()); - for (auto& action: iterateActions) - { - SCRIPTENV_D("Running action: %s\n", action.getAction().c_str()); - auto res = action.invoke(); - if (!res) - { - m_scriptEngine->reportScriptException(m_scriptEngine->getScriptError()); - } - } - - if (!m_scriptEngine->stopScriptExecution()) - { - // TODO(joey): Report to server? What should we do, hm. - printf("Oh no we were killed!!\n"); - } - -#ifndef NOSCRIPTPROFILING - auto endTimer = std::chrono::high_resolution_clock::now(); - auto time_diff = std::chrono::duration(endTimer - currentTimer); - addExecutionSample({ time_diff.count(), endTimer }); -#endif - - return hasActions(); -} - -#endif diff --git a/server/include/scripting/ScriptFactory.h b/server/include/scripting/ScriptFactory.h deleted file mode 100644 index 7515b4502..000000000 --- a/server/include/scripting/ScriptFactory.h +++ /dev/null @@ -1,105 +0,0 @@ -#ifndef SCRIPTFACTORY_H -#define SCRIPTFACTORY_H - -#define SCRIPTSYS_HASV8 - -template -struct ScriptConstructorId -{ - static constexpr auto result = ""; -}; - -#define SCRIPTFACTORY_CONSTRUCTOR(CLASS_NAME, KEY) \ - class CLASS_NAME; \ - template<> \ - struct ScriptConstructorId \ - { \ - static constexpr auto result = #KEY; \ - }; - -SCRIPTFACTORY_CONSTRUCTOR(Level, level) -SCRIPTFACTORY_CONSTRUCTOR(LevelLink, link) -SCRIPTFACTORY_CONSTRUCTOR(LevelSign, sign) -SCRIPTFACTORY_CONSTRUCTOR(LevelChest, chest) -SCRIPTFACTORY_CONSTRUCTOR(NPC, npc) -SCRIPTFACTORY_CONSTRUCTOR(Player, player) -SCRIPTFACTORY_CONSTRUCTOR(Weapon, weapon) - -#undef SCRIPTFACTORY_CONSTRUCTOR - -#ifdef SCRIPTSYS_HASV8 - #include "scripting/v8/V8ScriptArguments.h" -#endif - -#include - -struct ScriptFactory -{ - /* - Create Script Arguments - */ - - template - static inline ScriptArguments* createArguments(T* env, Args&&... An) - { - return nullptr; - } - - template - static inline ScriptArguments* createArguments(IScriptEnv* env, Args&&... An) - { - switch (env->getType()) - { -#ifdef SCRIPTSYS_HASV8 - case 1: // v8 - return createArguments(static_cast(env), std::forward(An)...); -#endif - - default: // couldn't deduce type - return nullptr; - } - } - -#ifdef SCRIPTSYS_HASV8 - template - static inline ScriptArguments* createArguments(V8ScriptEnv* env, Args&&... An) - { - return new V8ScriptArguments(std::forward(An)...); - } -#endif - - /* - Wrap Object - */ - - template - static inline std::unique_ptr> wrapObject(T* env, const std::string& ctor_name, Cls* obj) - { - return {}; - } - - template - static inline std::unique_ptr> wrapObject(IScriptEnv* env, const std::string& ctor_name, Cls* obj) - { - switch (env->getType()) - { -#ifdef SCRIPTSYS_HASV8 - case 1: // v8 - return wrapObject(static_cast(env), ctor_name, obj); -#endif - - default: // couldn't deduce type - return {}; - } - } - -#ifdef SCRIPTSYS_HASV8 - template - static inline std::unique_ptr> wrapObject(V8ScriptEnv* env, const std::string& ctor_name, Cls* obj) - { - return env->wrap(ctor_name, obj); - } -#endif -}; - -#endif diff --git a/server/include/scripting/ScriptOrigin.h b/server/include/scripting/ScriptOrigin.h deleted file mode 100644 index 2e70f33c9..000000000 --- a/server/include/scripting/ScriptOrigin.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef SCRIPTORIGIN_H -#define SCRIPTORIGIN_H - -#include -#include - -#include "NPC.h" -#include "Weapon.h" -#include "level/Level.h" -#include "scripting/ScriptClass.h" - -namespace scripting -{ - std::string getErrorOrigin(const NPC& npc) - { - std::string origin; - - switch (npc.getType()) - { - // Database npcs don't need to include their location, so we are returning here - // while the other two cases will append the level to the origin. - case NPCType::DBNPC: - return npc.getName(); - - case NPCType::LEVELNPC: - origin = "level npc"; - break; - - case NPCType::PUTNPC: - origin = "local npc"; - break; - } - - // Compiling before its assigned an npc id, so this requires some reworking to make work - // origin.append(std::format("[{}]", npc.getId())); - - auto level = npc.getLevel(); - if (level) - origin.append(std::format(" at {}, {:.2f}, {:.2f}", level->getLevelName().text(), npc.getX() / 16.0, npc.getY() / 16.0)); - - return origin; - } - - std::string getErrorOrigin(const ScriptClass& cls) - { - return std::format("Class {}", cls.getName()); - } - - std::string getErrorOrigin(const Weapon& npc) - { - return std::format("Weapon {}", npc.getName()); - } -} // namespace scripting - -#endif diff --git a/server/include/scripting/ScriptSystem.h b/server/include/scripting/ScriptSystem.h new file mode 100644 index 000000000..5d743ed35 --- /dev/null +++ b/server/include/scripting/ScriptSystem.h @@ -0,0 +1,67 @@ +#ifndef SCRIPTSYSTEM_H +#define SCRIPTSYSTEM_H + +#include +#include +#include +#include +#include +#include + +#include + +using namespace std::literals::string_view_literals; + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class IScriptEngine; +class ScriptClass; + +struct ScriptExecutionContext +{ + IScriptEngine* engine = nullptr; + std::shared_ptr script; + std::unordered_map> joinedClasses; +}; + +using CompiledScriptResult = std::variant; +using CompiledScriptResultPtr = std::shared_ptr; + +//---------------------------- + +class ScriptSystem +{ +public: + void registerScriptEngine(std::string_view name, std::shared_ptr engine); + +public: + // Gets the compiled client script. + // Forces the script to be compiled with the GS2 engine, as the client only understands GS2 bytecode. + CompiledScriptResultPtr getCompiledClientScript(std::string_view who, std::string_view source); + + // Gets the compiled server script. + CompiledScriptResultPtr getCompiledServerScript(std::string_view who, std::string_view source); + + /// @brief Gets a script engine by name. + /// @param name The name of the script engine to retrieve. + /// @return The script engine associated with the given name, or nullptr if no such engine is registered. + std::shared_ptr getScriptEngine(std::string_view name) const; + +public: + std::string defaultScriptEngine = "GS2"; + +private: + CompiledScriptResultPtr getCompiledScript(IScriptEngine* engine, std::string_view who, std::string_view source); + +private: + string_map> m_script_engines; + hash_map m_script_cache; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // SCRIPTSYSTEM_H diff --git a/server/include/scripting/ScriptTypes.h b/server/include/scripting/ScriptTypes.h new file mode 100644 index 000000000..120ffe45f --- /dev/null +++ b/server/include/scripting/ScriptTypes.h @@ -0,0 +1,132 @@ +#ifndef SCRIPTTYPES_H +#define SCRIPTTYPES_H + +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////// +// ScriptType +//////////////////////////////////////////////////////////// + +/// @brief The type of script. +enum class ScriptType +{ + NPC, + CLASS, + WEAPON, + SERVER, + + COUNT +}; +constexpr size_t SCRIPTTYPE_COUNT = static_cast(ScriptType::COUNT); + + +//////////////////////////////////////////////////////////// +// ScriptObject +//////////////////////////////////////////////////////////// + +/// @brief Identifies an object type that may be used by a scripting language. +enum class ScriptObjectType +{ + SERVER, + NPC, + PLAYER, + WEAPON, + LEVEL, + BADDY, + BOMB, + ARROW, + ITEM, + EXPLOSION, + HORSE, + SIGN, + + COUNT +}; + +/// @brief Binds a source object type with an identifier. +/// +/// The first element is the identifier, which may be an id or a hash. +using ScriptObject = std::pair; + +namespace source +{ +/// @brief Creates a ScriptObject for an NPC with the given id. +constexpr ScriptObject FromNPC(size_t id) +{ + return std::make_pair(id, ScriptObjectType::NPC); +} + +/// @brief Creates a ScriptObject for a player with the given id. +constexpr ScriptObject FromPlayer(size_t id) +{ + return std::make_pair(id, ScriptObjectType::PLAYER); +} + +/// @brief Creates a ScriptObject for the server. +constexpr ScriptObject FromServer() +{ + return std::make_pair(static_cast(0), ScriptObjectType::SERVER); +} + +// Weapon and Level in their respective headers. +} // end namespace source + + +//////////////////////////////////////////////////////////// +// ScriptEvent +//////////////////////////////////////////////////////////// + +/// @brief The script events known by the server. +enum class ScriptEventType : uint8_t +{ + CUSTOM = 0, + CREATED, + INITIALIZED, + PLAYERLOGIN, + PLAYERLOGOUT, + PLAYERENTERS, + PLAYERLEAVES, + PLAYERTOUCHSME, + PLAYERTOUCHSOTHER, + PLAYERLAYSITEM, + PLAYERCHATS, + PLAYERHURT, + PLAYERDIES, + COMPUSDIED, + NPCWARPED, + EXPLODED, + WASHIT, + WASSHOT, + WASPELT, + WASTHROWN, + TIMEOUT, + PRIVATEMESSAGE, + MOVEMENTFINISHED, + // + SERVERLISTCONNECT, + TRIGGERACTION, + + COUNT +}; +constexpr size_t SCRIPTEVENTTYPE_COUNT = static_cast(ScriptEventType::COUNT); + +/// @brief Represents an event in a scripting system, including its type, the source that initiated it, and any associated arguments. +struct ScriptEvent +{ + ScriptEventType type; + ScriptObject initiator; + std::vector args; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // SCRIPTTYPES_H diff --git a/server/include/scripting/SourceCode.h b/server/include/scripting/SourceCode.h deleted file mode 100644 index 5e0990587..000000000 --- a/server/include/scripting/SourceCode.h +++ /dev/null @@ -1,116 +0,0 @@ -#ifndef SOURCECODE_H -#define SOURCECODE_H - -#include -#include - -class SourceCode -{ -public: - SourceCode() = default; - SourceCode(std::string src, bool gs2default = false) : m_src(std::move(src)), m_gs2default(gs2default) { init(); } - SourceCode(SourceCode&& o) noexcept : m_src(std::move(o.m_src)), m_gs2default(o.m_gs2default) { init(); } - - SourceCode& operator=(SourceCode&& o) noexcept - { - m_gs2default = o.m_gs2default; - m_src = std::move(o.m_src); - init(); - return *this; - } - - explicit operator bool() const - { - return !empty(); - } - - bool empty() const - { - return m_src.empty(); - } - - const std::string& getSource() const - { - return m_src; - } - - std::string_view getServerSide() const - { - return m_serverside; - } - - std::string_view getClientSide() const - { - return m_clientside; - } - - std::string_view getClientGS1() const - { - return m_clientGS1; - } - - std::string_view getClientGS2() const - { - return m_clientGS2; - } - - void clearServerSide() - { - m_serverside = {}; - } - -private: - bool m_gs2default = false; - std::string m_src; - std::string_view m_clientside, m_serverside; - std::string_view m_clientGS1, m_clientGS2; - - void init() noexcept - { - m_clientside = m_serverside = m_clientGS1 = m_clientGS2 = {}; - -#ifdef V8NPCSERVER - auto clientSep = m_src.find("//#CLIENTSIDE"); - if (clientSep != std::string::npos) - { - // Separate clientside and serverside - m_clientside = std::string_view{ m_src }.substr(clientSep); - m_serverside = std::string_view{ m_src }.substr(0, clientSep); - } - else - m_serverside = std::string_view{ m_src }; -#else - // For non-npcserver builds all code is considered clientside - m_clientside = std::string_view{ m_src }; -#endif - - if (!m_clientside.empty()) - { - // Switch separator depending on if GS2 is set to default or not - const char* gs2sep_char; - if (m_gs2default) - gs2sep_char = "//#GS1"; - else - gs2sep_char = "//#GS2"; - - // Determine if this code is GS1 or GS2 - size_t codeSeparatorLoc = m_clientside.find(gs2sep_char); - if (codeSeparatorLoc != std::string::npos) - { - auto origCode = m_clientside.substr(0, codeSeparatorLoc); - auto otherCode = m_clientside.substr(codeSeparatorLoc); - m_clientGS2 = m_gs2default ? origCode : otherCode; - m_clientGS1 = m_gs2default ? otherCode : origCode; - } - else - { - if (m_gs2default) - m_clientGS2 = m_clientside; - else - m_clientGS1 = m_clientside; - } - } - } -}; - -#endif diff --git a/server/include/scripting/gs1/GS1Commands.h b/server/include/scripting/gs1/GS1Commands.h new file mode 100644 index 000000000..016702ae7 --- /dev/null +++ b/server/include/scripting/gs1/GS1Commands.h @@ -0,0 +1,233 @@ +#ifndef GS1COMMANDS_H +#define GS1COMMANDS_H + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1::grammar +{ +/////////////////////////////////////////////////////////////////////////////// + +constexpr std::array serverSideCommands = +{ + "addguildmember", + "addstring", + //"addtiledef ", + //"addtiledef2", + "addweapon", + "attachplayertoobj", + "blockagain", + //"blockagainlocal", + "callnpc", + //"callweapon", + "canbecarried", + "canbepulled", + "canbepushed", + "cannotbecarried", + "cannotbepulled", + "cannotbepushed", + "cannotwarp", + "canwarp", + "canwarp2", + "carryobject", + "changeimgcolors", + "changeimgmode", + "changeimgpart", + "changeimgvis", + "changeimgzoom", + "copylevel", + "copystrings", + "deletelevel", + "deletestring", + "destroy", + "detachplayer", + //"disabledefmovement", + //"disablemap", + //"disablepause", + //"disableselectweapons", + "disableweapons", + "dontblock", + //"dontblocklocal", + //"drawaslight", + "drawoverplayer", + "drawovertrees", // technically clientside, but can be supported + "drawunderplayer", + //"enabledefmovement", + //"enablefeatures", + //"enablemap", + //"enablepause", + //"enableselectweapons", + "enableweapons", + //"explodebomb", + //"followplayer", + //"freezeplayer", + "freezeplayer2", + "hide", + "hideimg", + "hideimgs", + //"hidelocal", + "hideplayer", + "hidesword", + "hitcompu", + "hitnpc", + "hitobjects", + "hitplayer", + "hurt ", + "insertstring", + "join", + "lay ", + "lay2", + //"loadmap", + "message", + "move", + //"noplayerkilling", + "noplayeronwall", + //"openurl ", + //"openurl2 ", + //"play ", + //"play2 ", + //"playlooped", + "putbomb", + "putcomp", + "putexplosion ", + "putexplosion2", + "puthorse", + //"putleaps", + "putnewcomp", + "putnpc", + "putnpc2", + //"putobject", + //"reflectarrow", + "removearrow", + "removebomb", + "removecompus", + "removeexplo", + "removeguild", + "removeguildmember", + "removehorse", + "removeitem", + "removestring", + //"removetiledefs", + "removeweapon", + //"replaceani", + "replacestring", + //"resetfocus", + "saveinfo", + "savelog", + "savelog2", + "say ", + "say2", + "sendpm", + "sendrpgmessage", + "sendtonc", + "sendtorc", + "serverwarp", + "set ", + "setani", + "setarray", + //"setbackpal", + //"setbacktile", + //"setbacktile2", + "setbeltcolor", + "setbody", + "setbow", + "setcharani", + "setchargender", + "setcharprop", + "setcoatcolor", + //"setcoloreffect", + //"setcursor ", + //"setcursor2", + //"seteffect ", + //"seteffectmode", + //"setfocus", + "setgender", + "setgif ", + "setgifpart", + "sethead", + "setimg", + "setimgpart", + //"setletters", + "setlevel ", + "setlevel2", + "setmap", + "setminimap", + //"setmusicvolume", + "setplayerdir", + "setplayerprop", + "setpm", + "setshape", + //"setshape2", + "setshield", + "setshoecolor", + "setshootparams ", + "setskincolor", + "setsleevecolor", + //"setspritesimage", + //"setstatusimage", + "setstring", + "setsword", + //"seturllevel", + //"setz ", + //"setzoomeffect", + "shoot ", + "shootarrow", + "shootball", + "shootfireball", + "shootfireblast", + "shootnuke", + "show", + "showani", + "showani2", + "showcharacter", + //"showfile", + "showimg", + "showimg2", + //"showlocal", + "showpoly", + "showpoly2", + "showstats", + "showtext", + "showtext2", + "sleep", + "spyfire", + //"stopmidi", + //"stopsound", + "take ", + "take2", + "takehorse", + "takeplayercarry", + "takeplayerhorse", + "throwcarry", + //"timereverywhere", + "timershow", + //"toinventory", + "tokenize ", + "tokenize2", + "toweapons", + "triggeraction", + "unfreezeplayer", + "unset ", + "updateboard ", + "updateboard2 ", + //"updateterrain", + "warpto", + //"wraptext", + //"wraptext2", +}; + +/* + triggeractions: + serverside - when received from client, triggers the weapon script of the weapon listed in first parameter. + server{EVENT} - invokes an event on the control-npc: if (actionserver{EVENT}) +*/ + +class GS1Visitor; +void processBuiltInCommand(GS1Visitor* visitor, antlr4::tree::ParseTree* node, std::string_view commandName); + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1::grammar + +#endif // GS1COMMANDS_H diff --git a/server/include/scripting/gs1/GS1ErrorListener.h b/server/include/scripting/gs1/GS1ErrorListener.h new file mode 100644 index 000000000..ad6e6b1c8 --- /dev/null +++ b/server/include/scripting/gs1/GS1ErrorListener.h @@ -0,0 +1,63 @@ +#ifndef GS1ERRORLISTENER_H +#define GS1ERRORLISTENER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1::grammar +{ +/////////////////////////////////////////////////////////////////////////////// + +class GS1ErrorListener : public antlr4::BaseErrorListener +{ +public: + GS1ErrorListener(std::string_view process, std::string_view name) : m_process(process), m_name(name) {} + + void syntaxError(antlr4::Recognizer* recognizer, antlr4::Token* offendingSymbol, size_t line, size_t charPositionInLine, const std::string& msg, std::exception_ptr e) override + { + std::vector> logbatch; + logbatch.emplace_back(0_ui8, std::format("* GS1 script {} failed for '{}':", m_process, m_name)); + logbatch.emplace_back(1_ui8, std::format("Line: {}, Column: {}", line, charPositionInLine + 1)); + + // If we have an offending token, log its details. + if (offendingSymbol != nullptr) + { + logbatch.emplace_back(1_ui8, std::format("Offending token: '{}'", offendingSymbol->getText())); + } + + // Log the error message. + logbatch.emplace_back(1_ui8, std::format("Error: {}", msg)); + + // Log the batch of messages. + log::batch(log::script, logbatch); + + // Send the log messages to the server. + auto server = BabyDI::Get(); + std::ranges::for_each(logbatch, [&server](const auto& kvp) { server->sendToNC(kvp.second); }); + } + +protected: + std::string m_process; + std::string m_name; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1::grammar + +#endif // GS1ERRORLISTENER_H diff --git a/server/include/scripting/gs1/GS1Flags.h b/server/include/scripting/gs1/GS1Flags.h new file mode 100644 index 000000000..37b88f0bf --- /dev/null +++ b/server/include/scripting/gs1/GS1Flags.h @@ -0,0 +1,25 @@ +#ifndef GS1FLAGS_H +#define GS1FLAGS_H + +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1 +{ +/////////////////////////////////////////////////////////////////////////////// + +void setEventFlags(ScriptEventType event, GameVariableStore& variableStore); +void setTriggerActionAndCustomEventFlags(ScriptEvent& event, GameVariableStore& variableStore); +void setPlayerFlags(GameVariableStore& variableStore, NPCPtr npc, PlayerClientPtr player); +void setNPCFlags(ScriptEvent& event, GameVariableStore& variableStore, NPCPtr npc); +void setLevelFlags(GameVariableStore& variableStore, NPCPtr npc, LevelPtr level); +void setWeaponFlags(ScriptEvent& event, ScriptObject source, GameVariableStore& variableStore); +void setOtherFlags(ScriptEvent& event, ScriptObject source, GameVariableStore& variableStore, NPCPtr npc, PlayerClientPtr player, LevelPtr level); + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1 + +#endif // GS1FLAGS_H diff --git a/server/include/scripting/gs1/GS1Functions.h b/server/include/scripting/gs1/GS1Functions.h new file mode 100644 index 000000000..b3a516c89 --- /dev/null +++ b/server/include/scripting/gs1/GS1Functions.h @@ -0,0 +1,38 @@ +#ifndef GS1FUNCTIONS_H +#define GS1FUNCTIONS_H + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1::grammar +{ +/////////////////////////////////////////////////////////////////////////////// + +/* +Possible extensions: + bodyexists(str) -> bool + headexists(str) -> bool + shieldexists(str) -> bool + swordexists(str) -> bool + checksum(str) -> float + extractfilebase(str) -> string + extractfileext(str) -> string + extractfilename(str) -> string + extractfilepath(str) -> string + fileexists(str) -> bool + filesize(str) -> integer + hasright(str, str) -> bool + lowercase(str) -> string + uppercase(str) -> string + isinclass(str) -> float +*/ + +class GS1Visitor; +GS1ScriptValue processBuiltInFunction(GS1Visitor* visitor, antlr4::tree::ParseTree* node, std::string_view functionName); + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1::grammar + +#endif // GS1FUNCTIONS_H diff --git a/server/include/scripting/gs1/GS1MessageCodes.h b/server/include/scripting/gs1/GS1MessageCodes.h new file mode 100644 index 000000000..e8a99b519 --- /dev/null +++ b/server/include/scripting/gs1/GS1MessageCodes.h @@ -0,0 +1,19 @@ +#ifndef GS1MESSAGECODES_H +#define GS1MESSAGECODES_H + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1::grammar +{ +/////////////////////////////////////////////////////////////////////////////// + +class GS1Visitor; +GS1ScriptValue processMessageCode(GS1Visitor* visitor, antlr4::tree::ParseTree* node, std::string_view messageCode); + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1::grammar + +#endif // GS1MESSAGECODES_H diff --git a/server/include/scripting/gs1/GS1Variables.h b/server/include/scripting/gs1/GS1Variables.h new file mode 100644 index 000000000..6e731a7be --- /dev/null +++ b/server/include/scripting/gs1/GS1Variables.h @@ -0,0 +1,90 @@ +#ifndef GS1VARIABLES_H +#define GS1VARIABLES_H + +#include + +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1 +{ +/////////////////////////////////////////////////////////////////////////////// + +// All variables set with setstring that don't have a special leading (this., thiso., server., serverr.) are added to the player (like in GS1) + +/* Variable prefixes +flag = setstring/set +var = numeric/array + +[->] transmits to the server +[<-] transmits to the client +[ ] does not transmit + +global = stored on the server (not saved) +server = stored on the server +level = stored on the level +player = stored on the player +npc = stored on the npc +class = stored on the script/class + +client.flag (client) [->] player + (server) [<-] player +clientr.flag (client) [ ] player + (server) [<-] player +local.flag (client) [ ] player + (server) [ ] npc +local.var (client) [ ] player + (server) [ ] npc +server.flag (client) [ ] player + (server) [ ] server +serverr.flag (client) [ ] player + (server) [<-] server +level.flag (client) [ ] player? + (server) [ ] level +level.var (client) [ ] player? + (server) [ ] level +this.flag (client) [ ] npc + (server) [ ] npc +this.var (client) [ ] npc + (server) [ ] npc +flag (client) [ ] player + (server) [ ] player +var (client) [ ] level + (server) [ ] (GS1) class (GS2) global + +In classic mode, all flags (except local.flags) are sent to the server from the client. +Global mode variables are not saved. + +unset flag; looks for flag and deletes if found. +setstring flag,; looks for flag= and deletes if found. + +set flag; +setstring flag,value; overwrites flag with flag=value. + +setstring flag,value; +set flag; does not erase flag's value, since it is technically already set. + +FLAG name +FLAG name=value +VAR name=value +VAR name={value,value} +*/ + +/* +players[index] On gmaps, it includes players in a 3x3 area around the player. Probably limited by syncdistancex / syncdistancey. +*/ + +void setGlobalVariables(GameVariableStore& variableStore); +void setNPCVariables(GameVariableStore& variableStore, std::weak_ptr npc); +void setPlayerVariables(GameVariableStore& variableStore, std::weak_ptr player); +void setLevelVariables(GameVariableStore& variableStore, std::weak_ptr level); +void setOtherVariables(GameVariableStore& variableStore, ScriptEvent& event); + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1 + +#endif // GS1VARIABLES_H diff --git a/server/include/scripting/gs1/GS1Visitor.h b/server/include/scripting/gs1/GS1Visitor.h new file mode 100644 index 000000000..daa23ba3a --- /dev/null +++ b/server/include/scripting/gs1/GS1Visitor.h @@ -0,0 +1,376 @@ +#ifndef GS1VISITOR_H +#define GS1VISITOR_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace preagonal +{ +class Level; +class SubLevel; +class StaticLevelData; +class Character; +} // namespace preagonal + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1::grammar +{ +/////////////////////////////////////////////////////////////////////////////// + +enum class StorageType : uint8_t +{ + THIS, + THISO, + CLIENT, + CLIENTR, + CLIENTO, + CLIENTRO, + SERVER, + SERVERR, + LEVEL, + LOCAL, + TEMP, +}; + +class GS1Visitor : public GS1ParserBaseVisitor +{ +public: + void execute(const ScriptEvent& event, ScriptObject source, GS1Parser& parser, ScriptExecutionContext& context, antlr4::tree::ParseTree* startNode); + void reportError(std::string_view message, antlr4::tree::ParseTree* node = nullptr, bool abort = true); + +public: + std::vector tokenizeTokens; + GameVariableStore flagStore; + GameVariableStore* builtInStore = nullptr; + ScriptExecutionContext* scriptContext = nullptr; + bool expectingFlag = false; + bool expectingTimeoutAsVariable = false; + std::string who; + +public: + [[inline]] const ScriptObject& getOriginalSource() const; + [[inline]] const ScriptObject& getInitiatingSource() const; + [[inline]] const ScriptObject& getCurrentSource(bool defaultToInitiator = false) const; + [[inline]] const ScriptObject& popSource(); + [[inline]] const void pushSource(ScriptObject source); + [[inline]] const ScriptEvent& getEvent() const; + [[inline]] auto sourceStack() const; + std::optional findNearestScriptObjectSourceFromStack(ScriptObjectType type) const; + std::shared_ptr findCurrentLevel() const; + std::tuple, std::shared_ptr, std::shared_ptr> findCurrentLevelData() const; + +public: + template + [[inline]] static T getGameValueAs(const GS1ScriptValue& value); + + template + [[inline]] T getReadOnlyGameValueFromAnyAs(const std::any& value); + + GameValue* getGameValueFromGS1ScriptValue(GS1ScriptValue& value); + std::optional getGameValueFromSource(const ScriptObject& source, std::string_view identifier); + GameValue getGameValueFromStorage(std::string_view identifier, std::optional type = std::nullopt); + +public: + [[inline]] static std::optional getStorageTypeFromIdentifier(std::string_view identifier, std::optional defaultValue = {}) noexcept; + [[inline]] static void applyStorageNameToIdentifier(std::optional storage, std::string& identifier) noexcept; + [[inline]] static void stripStorageNameFromIdentifier(std::string& identifier) noexcept; + +public: + GameVariableStore* getGameVariableStoreForStorageType(size_t type); + double getColorValueFromString(std::string_view colorString); + +public: + GS1ScriptValue translateSourceText(antlr4::tree::ParseTree* node, std::string_view language); + GS1ScriptValue translateSourceText(std::string_view sourceText, std::string_view language); + GS1ScriptValue processStringExpression(std::string_view expression); + GS1ScriptValue processMathExpression(std::string_view expression); + + template + [[inline]] T* walkToContext(antlr4::tree::ParseTree* node); + +protected: + std::any reparseExpression(std::string_view expression, std::string_view lexerMode, std::function node); + +public: + std::vector visitChildrenAndCollect(antlr4::tree::ParseTree* node); + +protected: + GS1Parser* m_parser = nullptr; + const ScriptEvent* m_event = nullptr; + ScriptObject m_originalSource; + GameVariableStore* m_serverStore = nullptr; + std::deque m_currentSource; + std::deque m_sleepCurrentSource; + std::vector> m_callStack; + std::vector> m_sleepCallStack; + bool m_reparsingStringExpression = false; + bool m_reparsingMathExpression = false; + +protected: + std::any safeVisit(antlr4::tree::ParseTree* node); + +protected: + GameVariableStore* findGameVariableStoreFromSourceStack(ScriptObjectType type, int skip = 0) const; + GS1GameVariable getGameVariableFromAny(std::any& value); + GameValue getReadOnlyGameValueFromGS1ScriptValue(const GS1ScriptValue& value); + GameValue getReadOnlyGameValueFromAny(const std::any& value); + std::optional getSourceFromGS1ScriptValue(GS1ScriptValue& value); + +protected: + void setCurrentPlayerVariables(std::optional source); + +public: + virtual std::any visitProgram(GS1Parser::ProgramContext* ctx) override; + virtual std::any visitBlock(GS1Parser::BlockContext* ctx) override; + // + virtual std::any visitStatementIf(GS1Parser::StatementIfContext* context) override; + virtual std::any visitStatementFor(GS1Parser::StatementForContext* context) override; + virtual std::any visitStatementWhile(GS1Parser::StatementWhileContext* context) override; + virtual std::any visitStatementWith(GS1Parser::StatementWithContext* context) override; + virtual std::any visitStatementFunctionDefinition(GS1Parser::StatementFunctionDefinitionContext* context) override; + virtual std::any visitStatementUserFunctionCall(GS1Parser::StatementUserFunctionCallContext* context) override; + virtual std::any visitStatementBuiltInCommand(GS1Parser::StatementBuiltInCommandContext* context) override; + virtual std::any visitStatementAssignment(GS1Parser::StatementAssignmentContext* context) override; + // + virtual std::any visitExpressionIn(GS1Parser::ExpressionInContext* context) override; + virtual std::any visitExpressionTernary(GS1Parser::ExpressionTernaryContext* context) override; + virtual std::any visitExpressionLogicOr(GS1Parser::ExpressionLogicOrContext* context) override; + virtual std::any visitExpressionLogicAnd(GS1Parser::ExpressionLogicAndContext* context) override; + virtual std::any visitExpressionEquality(GS1Parser::ExpressionEqualityContext* context) override; + virtual std::any visitExpressionRelational(GS1Parser::ExpressionRelationalContext* context) override; + virtual std::any visitExpressionAdditive(GS1Parser::ExpressionAdditiveContext* context) override; + virtual std::any visitExpressionMultiplicative(GS1Parser::ExpressionMultiplicativeContext* context) override; + virtual std::any visitExpressionExponentiation(GS1Parser::ExpressionExponentiationContext* context) override; + virtual std::any visitExpressionUnary(GS1Parser::ExpressionUnaryContext* context) override; + virtual std::any visitExpressionPostfix(GS1Parser::ExpressionPostfixContext* context) override; + // + virtual std::any visitBuiltInFunctionCall(GS1Parser::BuiltInFunctionCallContext* context) override; + virtual std::any visitIdentifierAccess(GS1Parser::IdentifierAccessContext* context) override; + virtual std::any visitIdentifierValue(GS1Parser::IdentifierValueContext* context) override; + virtual std::any visitCompoundIdentifier(GS1Parser::CompoundIdentifierContext* context) override; + virtual std::any visitCompoundString(GS1Parser::CompoundStringContext* context) override; + virtual std::any visitMessageCode(GS1Parser::MessageCodeContext* context) override; + // + virtual std::any visitFlowReturn(GS1Parser::FlowReturnContext* context) override; + virtual std::any visitFlowBreak(GS1Parser::FlowBreakContext* context) override; + virtual std::any visitFlowContinue(GS1Parser::FlowContinueContext* context) override; + // + virtual std::any visitLiteral(GS1Parser::LiteralContext* context) override; + virtual std::any visitRangeLiteral(GS1Parser::RangeLiteralContext* context) override; + virtual std::any visitArrayLiteral(GS1Parser::ArrayLiteralContext* context) override; + virtual std::any visitItemLiteral(GS1Parser::ItemLiteralContext* context) override; + virtual std::any visitCarryLiteral(GS1Parser::CarryLiteralContext* context) override; + virtual std::any visitDirectionLiteral(GS1Parser::DirectionLiteralContext* context) override; + virtual std::any visitGenderLiteral(GS1Parser::GenderLiteralContext* context) override; + virtual std::any visitColorLiteral(GS1Parser::ColorLiteralContext* context) override; + virtual std::any visitBaddyLiteral(GS1Parser::BaddyLiteralContext* context) override; + // + virtual std::any visitPrimaryExpression(GS1Parser::PrimaryExpressionContext* context) override; +}; + +//---------------------------- + +template +inline auto makeDefault() -> T +{ + if constexpr (std::is_same_v) + return 0.0; + else if constexpr (std::is_same_v) + return std::string{}; + else + return T{}; +} + +//---------------------------- + +inline const ScriptObject& GS1Visitor::getOriginalSource() const +{ + return m_originalSource; +} + +inline const ScriptObject& GS1Visitor::getInitiatingSource() const +{ + return m_event->initiator; +} + +inline const ScriptObject& GS1Visitor::getCurrentSource(bool defaultToInitiator) const +{ + if (m_event && m_event->initiator.second == ScriptObjectType::NPC) + defaultToInitiator = false; + return m_currentSource.empty() ? (defaultToInitiator && m_event ? m_event->initiator : m_originalSource) : m_currentSource.back(); +} + +inline const ScriptObject& GS1Visitor::popSource() +{ + m_currentSource.pop_back(); + return getCurrentSource(); +} + +inline const void GS1Visitor::pushSource(ScriptObject source) +{ + m_currentSource.emplace_back(std::move(source)); +} + +inline auto GS1Visitor::sourceStack() const +{ + // Save me C++26... + std::vector sources{m_currentSource.rbegin(), m_currentSource.rend()}; + if (m_event->initiator.second != ScriptObjectType::NPC) + sources.push_back(m_event->initiator); + sources.push_back(m_originalSource); + return sources; +} + +inline const ScriptEvent& GS1Visitor::getEvent() const +{ + return *m_event; +} + +template +inline T GS1Visitor::getGameValueAs(const GS1ScriptValue& value) +{ + if (const auto* gs1Pair = std::get_if(&value); gs1Pair != nullptr) + return gs1Pair->first.get(gs1Pair->second).value_or(makeDefault()); + else if (auto* gameValue = std::get_if(&value); gameValue != nullptr) + return gameValue->get().value_or(makeDefault()); + return makeDefault(); +} + +//---------------------------- + +template +inline T GS1Visitor::getReadOnlyGameValueFromAnyAs(const std::any& value) +{ + auto gameval = getReadOnlyGameValueFromAny(value); + return gameval.get().value_or(makeDefault()); +} + +//---------------------------- + +inline std::optional GS1Visitor::getStorageTypeFromIdentifier(std::string_view identifier, std::optional defaultValue) noexcept +{ + if (identifier.empty() || !identifier.contains('.')) + return defaultValue; + if (string::starts_withi(identifier, "this."sv)) + return ENUM(StorageType::THIS); + if (string::starts_withi(identifier, "thiso."sv)) + return ENUM(StorageType::THISO); + if (string::starts_withi(identifier, "client."sv)) + return ENUM(StorageType::CLIENT); + if (string::starts_withi(identifier, "clientr."sv)) + return ENUM(StorageType::CLIENTR); + if (string::starts_withi(identifier, "cliento."sv)) + return ENUM(StorageType::CLIENTO); + if (string::starts_withi(identifier, "clientro."sv)) + return ENUM(StorageType::CLIENTRO); + if (string::starts_withi(identifier, "server."sv)) + return ENUM(StorageType::SERVER); + if (string::starts_withi(identifier, "serverr."sv)) + return ENUM(StorageType::SERVERR); + if (string::starts_withi(identifier, "local."sv)) + return ENUM(StorageType::LOCAL); + if (string::starts_withi(identifier, "temp."sv)) + return ENUM(StorageType::TEMP); + + return defaultValue; +} + +inline void GS1Visitor::applyStorageNameToIdentifier(std::optional storage, std::string& identifier) noexcept +{ + if (!storage.has_value()) + return; + + switch (storage.value()) + { + case ENUM(StorageType::CLIENT): + case ENUM(StorageType::CLIENTO): + identifier = std::format("client.{}", identifier); + break; + case ENUM(StorageType::CLIENTR): + case ENUM(StorageType::CLIENTRO): + identifier = std::format("clientr.{}", identifier); + break; + case ENUM(StorageType::SERVER): + identifier = std::format("server.{}", identifier); + break; + case ENUM(StorageType::SERVERR): + identifier = std::format("serverr.{}", identifier); + break; + } +} + +inline void GS1Visitor::stripStorageNameFromIdentifier(std::string& identifier) noexcept +{ + auto storage = GS1Visitor::getStorageTypeFromIdentifier(identifier); + if (!storage.has_value()) return; + auto period = identifier.find('.'); + if (period == std::string::npos) return; + + switch (storage.value()) + { + // Erase the storage prefix from the identifier, leaving only the actual variable name. + case ENUM(StorageType::THIS): + case ENUM(StorageType::THISO): + case ENUM(StorageType::LOCAL): + case ENUM(StorageType::TEMP): + identifier.erase(0, period + 1); + break; + // Strip the "o" before the period for object storage types, leaving the "client." or "clientr." prefix. + case ENUM(StorageType::CLIENTO): + case ENUM(StorageType::CLIENTRO): + if (period > 1 && identifier[period - 1] == 'o') + identifier.erase(period - 1, 1); + break; + } +} + +//---------------------------- + +template +T* GS1Visitor::walkToContext(antlr4::tree::ParseTree* node) +{ + if (node == nullptr) return nullptr; + + int depthLimit = 50; // Arbitrary depth limit to prevent infinite recursion in malformed trees. + antlr4::tree::ParseTree* current = node; + while (depthLimit-- > 0) + { + if (auto* context = dynamic_cast(current); context != nullptr) + return context; + + if (current->children.size() != 1) + return nullptr; + + current = current->children[0]; + } + + return nullptr; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1::grammar + +#endif // GS1VISITOR_H diff --git a/server/include/scripting/gs1/ScriptEngineGS1.h b/server/include/scripting/gs1/ScriptEngineGS1.h new file mode 100644 index 000000000..3b07bfba1 --- /dev/null +++ b/server/include/scripting/gs1/ScriptEngineGS1.h @@ -0,0 +1,191 @@ +#ifndef SCRIPTENGINEGS1_H +#define SCRIPTENGINEGS1_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Forward declare. +namespace preagonal::gs1::grammar +{ +class GS1Visitor; +} + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1 +{ +/////////////////////////////////////////////////////////////////////////////// + +inline constexpr std::array colorNames = +{ + "white"sv, "yellow"sv, "orange"sv, "pink"sv, "red"sv, + "darkred"sv, "lightgreen"sv, "green"sv, "darkgreen"sv, "lightblue"sv, + "blue"sv, "darkblue"sv, "brown"sv, "cynober"sv, "purple"sv, + "darkpurple"sv, "lightgray"sv, "gray"sv, "black"sv, "transparent"sv +}; + +inline constexpr std::array directionNames = +{ + "up"sv, "left"sv, "down"sv, "right"sv +}; + +inline constexpr std::array genderNames = +{ + "male"sv, "female"sv +}; + +inline constexpr std::array carryNames = +{ + "bush"sv, "sign"sv, "vase"sv, "stone"sv, "blackstone"sv, + "bomb"sv, "hotbomb"sv, "superbomb"sv, "joltbomb"sv, "hotjoltbomb"sv, + "none"sv +}; + +inline constexpr std::array carrySprites = +{ + CarryObjectSprite::BUSH, CarryObjectSprite::SIGN, CarryObjectSprite::VASE, CarryObjectSprite::STONE, CarryObjectSprite::BLACKSTONE, + CarryObjectSprite::BOMB, CarryObjectSprite::HOTBOMB, CarryObjectSprite::SUPERBOMB, CarryObjectSprite::JOLTBOMB, CarryObjectSprite::HOTJOLTBOMB, + CarryObjectSprite::NONE +}; + +inline static const std::unordered_map eventFlagMap = +{ + {ScriptEventType::CREATED, "created"}, + {ScriptEventType::INITIALIZED, "initialized"}, + {ScriptEventType::PLAYERLOGIN, "playerlogin"}, + {ScriptEventType::PLAYERLOGOUT, "playerlogout"}, + {ScriptEventType::PLAYERENTERS, "playerenters"}, + {ScriptEventType::PLAYERLEAVES, "playerleaves"}, + {ScriptEventType::PLAYERTOUCHSME, "playertouchsme"}, + {ScriptEventType::PLAYERTOUCHSOTHER, "playertouchsother"}, + {ScriptEventType::PLAYERLAYSITEM, "playerlaysitem"}, + {ScriptEventType::PLAYERCHATS, "playerchats"}, + {ScriptEventType::PLAYERHURT, "playerhurt"}, + {ScriptEventType::PLAYERDIES, "playerdies"}, + {ScriptEventType::COMPUSDIED, "compusdied"}, + {ScriptEventType::NPCWARPED, "npcwarped"}, + {ScriptEventType::EXPLODED, "exploded"}, + {ScriptEventType::WASHIT, "washit"}, + {ScriptEventType::WASSHOT, "wasshot"}, + {ScriptEventType::WASPELT, "waspelt"}, + {ScriptEventType::WASTHROWN, "wasthrown"}, + {ScriptEventType::TIMEOUT, "timeout"}, + {ScriptEventType::PRIVATEMESSAGE, "pm"}, + {ScriptEventType::MOVEMENTFINISHED, "movementfinished"}, + // + {ScriptEventType::SERVERLISTCONNECT, "serverlistconnect"} +}; + +/////////////////////////////////////////////////////////////////////////////// + +struct unimplemented_error : public std::runtime_error +{ + using std::runtime_error::runtime_error; +}; + +struct sleep_exception : public std::exception +{ +}; +struct break_exception : public std::exception +{ +}; +struct continue_exception : public std::exception +{ +}; +struct return_exception : public std::exception +{ +}; + +/////////////////////////////////////////////////////////////////////////////// + +using PlayerOrNPC = std::optional>; + +PlayerPtr getPlayerFromSource(const ScriptObject& source, std::optional index = std::nullopt); +PlayerClientPtr getPlayerClientFromSource(const ScriptObject& source, std::optional index = std::nullopt); +NPCPtr getNPCFromSource(const ScriptObject& source, std::optional index = std::nullopt); +PlayerOrNPC getPlayerOrNPCFromSource(const ScriptObject& source, std::optional index = std::nullopt); +Character* getCharacterFromSource(const ScriptObject& source, std::optional index = std::nullopt); + +//---------------------------- + +/// @brief A GS1 variable pair of a GameValue and an index (for an array access). +using GS1GameVariable = std::pair>; + +/// @brief A GS1 script value used in the GS1 visitor pattern. +using GS1ScriptValue = std::variant; + +/// @brief A GS1 object source with an optional GameVariableStore. +using GS1ObjectSourceWithStore = std::pair; + +//---------------------------- + +/// @brief Wraps the GS1 script components needed for parsing and execution. +struct GS1ScriptWrapper +{ + GS1ScriptWrapper(std::string_view who, std::string_view script); + + std::shared_ptr errorListenerLexer; + std::shared_ptr errorListenerParser; + std::shared_ptr input; + std::shared_ptr tokens; + std::shared_ptr parser; + std::shared_ptr visitor; + grammar::GS1Parser::ProgramContext* program = nullptr; + GameVariableStore variables; +}; + +//---------------------------- + +class ScriptEngineGS1 : public IScriptEngine +{ +public: + ScriptEngineGS1(); + virtual ~ScriptEngineGS1() override {} + +public: + virtual ScriptEngineMode getExecutionMode() override { return ScriptEngineMode::DIRECT; } + virtual ScriptExecutionType getExecutionType() override { return ScriptExecutionType::INTERPRETED; } + +public: + virtual CompiledScriptResult compileScript(std::string_view who, std::string_view script) override; + virtual bool reset() override { return false; } + +public: + virtual bool execute(ScriptEvent& event, ScriptObject source, CompiledScriptResultPtr context) override; + virtual bool executeFunction(std::string_view function, ScriptEvent& event, ScriptObject source, CompiledScriptResultPtr context) override; + +public: + virtual double processMathExpression(std::string_view expression, ScriptObject source) override; + virtual std::string processStringExpression(std::string_view expression, ScriptObject source) override; + +protected: + bool prepare(GS1ScriptWrapper& wrapper, ScriptEvent& event, ScriptObject source, CompiledScriptResultPtr context, NPCPtr& npc, LevelPtr& level); + void cleanup(GS1ScriptWrapper& wrapper); +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1 + +#endif // SCRIPTENGINEGS1_H diff --git a/server/include/scripting/gs2/ScriptEngineGS2.h b/server/include/scripting/gs2/ScriptEngineGS2.h new file mode 100644 index 000000000..412ce4fc4 --- /dev/null +++ b/server/include/scripting/gs2/ScriptEngineGS2.h @@ -0,0 +1,58 @@ +#ifndef SCRIPTENGINEGS2_H +#define SCRIPTENGINEGS2_H + +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace preagonal +{ +class Server; +} + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs2 +{ +/////////////////////////////////////////////////////////////////////////////// + +class ScriptEngineGS2 : public IScriptEngine +{ +public: + virtual ~ScriptEngineGS2() override {} + +public: + virtual ScriptEngineMode getExecutionMode() override { return ScriptEngineMode::CALLBACK; } + virtual ScriptExecutionType getExecutionType() override { return ScriptExecutionType::COMPILED; } + +public: + virtual CompiledScriptResult compileScript(std::string_view who, std::string_view script) override; + virtual bool reset() override { return false; } + +public: + virtual bool execute(ScriptEvent& event, ScriptObject source, CompiledScriptResultPtr context) override { return false; } + virtual bool executeFunction(std::string_view function, ScriptEvent& event, ScriptObject source, CompiledScriptResultPtr context) override { return false; } + +public: + virtual double processMathExpression(std::string_view expression, ScriptObject source) override { return 0.0; } + virtual std::string processStringExpression(std::string_view expression, ScriptObject source) override { return {}; } + +protected: + BabyDI_INJECT(Server, m_server); + + std::string handleGS2Error(const GS2CompilerError& error); + void reportScriptException(const std::string& error_message); + + GS2ScriptManager m_scriptManager; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs2 + +#endif // SCRIPTENGINEGS2_H diff --git a/server/include/scripting/interface/ScriptArguments.h b/server/include/scripting/interface/ScriptArguments.h deleted file mode 100644 index 35d3218e1..000000000 --- a/server/include/scripting/interface/ScriptArguments.h +++ /dev/null @@ -1,107 +0,0 @@ -#ifndef SCRIPTARGUMENTS_H -#define SCRIPTARGUMENTS_H - -#include -#include -#include - -#include "scripting/interface/ScriptObject.h" - -class IScriptFunction; - -namespace detail -{ - template - inline void invalidateBinding(T val) - { - } - - template - inline void invalidateBinding(IScriptObject* val) - { - // Decrease reference for wrapped objects - val->decreaseReference(); - } - - template - inline void validateBinding(T val) - { - } - - template - inline void validateBinding(IScriptObject* val) - { - // Increase reference for wrapped objects - val->increaseReference(); - } -}; // namespace detail - -class IScriptArguments -{ -public: - IScriptArguments() = default; - virtual ~IScriptArguments() = default; - - virtual bool invoke(IScriptFunction* func, bool catchExceptions = false) = 0; -}; - -template -class ScriptArguments : public IScriptArguments -{ -public: - template - ScriptArguments(Args&&... An) - : IScriptArguments(), m_resolved(false), m_tuple(std::forward(An)...) - { - validateArgs(std::index_sequence_for{}); - } - - virtual ~ScriptArguments() - { - if (!m_resolved) - { - invalidateArgs(std::index_sequence_for{}); - } - } - - virtual bool invoke(IScriptFunction* func, bool catchExceptions = false) = 0; - - inline size_t count() const - { - return m_argc; - } - - inline const std::tuple& args() const - { - return m_tuple; - } - -protected: - static constexpr int m_argc = (sizeof...(Ts)); - - bool m_resolved; - std::tuple m_tuple; - -private: - template - inline void invalidateArgs(std::index_sequence) - { - if constexpr (sizeof...(Is) > 0) - { - int unused[] = { ((detail::invalidateBinding(std::get(m_tuple))), void(), 0)... }; - static_cast(unused); // Avoid warning for unused variable - } - } - - template - inline void validateArgs(std::index_sequence) - { - if constexpr (sizeof...(Is) > 0) - { - int unused[] = { ((detail::validateBinding(std::get(m_tuple))), void(), 0)... }; - static_cast(unused); // Avoid warning for unused variable - } - } -}; - -#endif diff --git a/server/include/scripting/interface/ScriptBindings.h b/server/include/scripting/interface/ScriptBindings.h deleted file mode 100644 index c4ad0e9e8..000000000 --- a/server/include/scripting/interface/ScriptBindings.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef SCRIPTBINDINGS_H -#define SCRIPTBINDINGS_H - -#ifdef NDEBUG - #ifdef ENABLE_SCRIPTENV_DEBUG - #define _SCRIPTENV_DEBUG - #endif -#endif - -#ifdef _SCRIPTENV_DEBUG - #define SCRIPTENV_D(...) printf(__VA_ARGS__) -#else - #define SCRIPTENV_D(...) \ - do { \ - } \ - while (0) -#endif - -#include "scripting/interface/ScriptArguments.h" -#include "scripting/interface/ScriptEnv.h" -#include "scripting/interface/ScriptFunction.h" -#include "scripting/interface/ScriptObject.h" -#include "scripting/interface/ScriptUtils.h" - -#endif diff --git a/server/include/scripting/interface/ScriptEnv.h b/server/include/scripting/interface/ScriptEnv.h deleted file mode 100644 index ca789641f..000000000 --- a/server/include/scripting/interface/ScriptEnv.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef SCRIPTENV_H -#define SCRIPTENV_H - -#include - -#include "scripting/interface/ScriptUtils.h" - -class IScriptFunction; - -class IScriptEnv -{ -public: - IScriptEnv() {} - virtual ~IScriptEnv() {} - - virtual int getType() const = 0; - - virtual void initialize() = 0; - virtual void cleanup(bool shutDown = false) = 0; - virtual IScriptFunction* compile(const std::string& name, const std::string& source) = 0; - virtual void callFunctionInScope(std::function function) = 0; - virtual void terminateExecution() = 0; - - const ScriptRunError& getScriptError() const - { - return m_lastScriptError; - } - -protected: - ScriptRunError m_lastScriptError; -}; - -#endif diff --git a/server/include/scripting/interface/ScriptFunction.h b/server/include/scripting/interface/ScriptFunction.h deleted file mode 100644 index 3114d1ccf..000000000 --- a/server/include/scripting/interface/ScriptFunction.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef SCRIPTFUNCTION_H -#define SCRIPTFUNCTION_H - -class IScriptArguments; - -class IScriptFunction -{ -public: - virtual ~IScriptFunction() = 0; - - inline bool isReferenced() const - { - return m_referenceCount > 0; - } - - inline int getReferenceCount() const - { - return m_referenceCount; - } - - inline void increaseReference() - { - m_referenceCount++; - } - - inline void decreaseReference() - { - m_referenceCount--; - } - -private: - int m_referenceCount = 0; -}; - -inline IScriptFunction::~IScriptFunction() = default; - -#endif diff --git a/server/include/scripting/interface/ScriptObject.h b/server/include/scripting/interface/ScriptObject.h deleted file mode 100644 index f588eccbc..000000000 --- a/server/include/scripting/interface/ScriptObject.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef SCRIPTOBJECT_H -#define SCRIPTOBJECT_H - -#include - -template -class IScriptObject -{ -public: - IScriptObject(T* object) - : m_object(object) - { - } - - virtual ~IScriptObject() - { - // This assert is triggered when updating levels quickly. The reason for this - // is because npcs may have actions queued up, and referenceCount doesn't decrease on destructor - // only when the action is invoked and the arguments are parsed. Will look into this, but shouldn't - // have any side effects anyway. - // joey (5/24/19) - believe this is fixed, but leaving the note and enabling the assert - assert(m_referenceCount == 0); - } - - T* object() const - { - return m_object; - } - - bool isReferenced() const - { - return m_referenceCount > 0; - } - - int getReferenceCount() const - { - return m_referenceCount; - } - - void increaseReference() - { - m_referenceCount++; - } - - void decreaseReference() - { - m_referenceCount--; - } - -protected: - T* m_object; - int m_referenceCount = 0; -}; - -#endif diff --git a/server/include/scripting/interface/ScriptUtils.h b/server/include/scripting/interface/ScriptUtils.h deleted file mode 100644 index b1f8c9980..000000000 --- a/server/include/scripting/interface/ScriptUtils.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef SCRIPTUTILS_H -#define SCRIPTUTILS_H - -#include -#include - -struct ScriptTimeSample -{ - double sample; - std::chrono::high_resolution_clock::time_point sample_time; -}; - -class ScriptRunError -{ -public: - std::string getErrorString() const - { - std::string error_message; // ("Error compiling: "); - error_message.append(error); - error_message.append(" at "); - error_message.append(error_line); - return error_message; - } - - std::string filename; - std::string error; - std::string error_line; - int startcol = 0; - int endcol = 0; - int lineno = 0; -}; - -#endif diff --git a/server/include/scripting/v8/V8ScriptArguments.h b/server/include/scripting/v8/V8ScriptArguments.h deleted file mode 100644 index e1427a12d..000000000 --- a/server/include/scripting/v8/V8ScriptArguments.h +++ /dev/null @@ -1,132 +0,0 @@ -#ifndef V8SCRIPTARGUMENTS_H -#define V8SCRIPTARGUMENTS_H - -#include -#include -#include -#include - -#include "scripting/interface/ScriptBindings.h" -#include "scripting/v8/V8ScriptEnv.h" -#include "scripting/v8/V8ScriptFunction.h" -#include "scripting/v8/V8ScriptObject.h" - -namespace detail -{ - inline v8::Handle toBinding(V8ScriptEnv* env, std::nullptr_t val) - { - return v8::Null(env->isolate()); - } - - inline v8::Handle toBinding(V8ScriptEnv* env, double val) - { - return v8::Number::New(env->isolate(), val); - } - - inline v8::Handle toBinding(V8ScriptEnv* env, int val) - { - return v8::Integer::New(env->isolate(), val); - } - - inline v8::Handle toBinding(V8ScriptEnv* env, const std::string& val) - { - return v8::String::NewFromUtf8(env->isolate(), val.c_str()).ToLocalChecked(); - } - - inline v8::Handle toBinding(V8ScriptEnv* env, const std::shared_ptr& object) - { - return object.get()->object(); - } - - inline v8::Handle toBinding(V8ScriptEnv* env, IScriptFunction* function) - { - return static_cast(function)->function(); - } - - template - inline v8::Handle toBinding(V8ScriptEnv* env, IScriptObject* val) - { - V8ScriptObject* wrappedVal = static_cast*>(val); - wrappedVal->decreaseReference(); - return wrappedVal->handle(env->isolate()); - } -}; // namespace detail - -template -class V8ScriptArguments : public ScriptArguments -{ - typedef ScriptArguments base; - -public: - template - explicit V8ScriptArguments(Args&&... An) - : ScriptArguments(std::forward(An)...) - { - } - - ~V8ScriptArguments() = default; - - virtual bool invoke(IScriptFunction* func, bool catchExceptions = false) override - { - assert(base::m_argc > 0); - SCRIPTENV_D("Invoke Script Argument: %d args\n", base::m_argc); - - if (!base::m_resolved) - { - V8ScriptFunction* v8_func = static_cast(func); - V8ScriptEnv* v8_env = static_cast(v8_func->env()); - - v8::Isolate* isolate = v8_env->isolate(); - v8::Local context = v8_env->context(); - - // get a v8 handle for the function to be executed - v8::Local cbFunc = v8_func->function(); - assert(!cbFunc.IsEmpty()); - - // sort arguments into array - resolveArgs(v8_env, std::index_sequence_for{}); - base::m_resolved = true; - - // TODO(joey): This will probably not stay like this. Needed the trycatch for executing - // new objects for the first time only. Will figure something out. - - // call function - if (catchExceptions) - { - v8::TryCatch try_catch(isolate); - v8::MaybeLocal ret = cbFunc->Call(context, m_args[0], base::m_argc, m_args); - static_cast(ret); - - if (try_catch.HasCaught()) - { - v8_env->parseErrors(&try_catch); - return false; - } - } - else - { - v8::MaybeLocal ret = cbFunc->Call(context, m_args[0], base::m_argc, m_args); // base::Argc - 1, m_args + 1); - static_cast(ret); - //ret.IsEmpty(); - } - } - - SCRIPTENV_D("Finish Script Argument\n"); - return true; - } - -private: - v8::Local m_args[base::m_argc]; - - template - inline void resolveArgs(V8ScriptEnv* env, std::index_sequence) - { - if constexpr (sizeof...(Is) > 0) - { - int unused[] = { ((m_args[Is] = detail::toBinding(env, std::get(base::m_tuple))), void(), 0)... }; - static_cast(unused); // Avoid warning for unused variable - } - } -}; - -#endif diff --git a/server/include/scripting/v8/V8ScriptBindings.h b/server/include/scripting/v8/V8ScriptBindings.h deleted file mode 100644 index a09599e94..000000000 --- a/server/include/scripting/v8/V8ScriptBindings.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef V8SCRIPTBINDINGS_H -#define V8SCRIPTBINDINGS_H - -#include "scripting/v8/V8ScriptArguments.h" -#include "scripting/v8/V8ScriptEnv.h" -#include "scripting/v8/V8ScriptFunction.h" -#include "scripting/v8/V8ScriptUtils.h" -#include "scripting/v8/V8ScriptWrappers.h" - -#endif diff --git a/server/include/scripting/v8/V8ScriptEnv.h b/server/include/scripting/v8/V8ScriptEnv.h deleted file mode 100644 index 4cd2e15d7..000000000 --- a/server/include/scripting/v8/V8ScriptEnv.h +++ /dev/null @@ -1,139 +0,0 @@ -#ifndef V8SCRIPTENV_H -#define V8SCRIPTENV_H - -#include -#include -#include - -#include "scripting/interface/ScriptBindings.h" -#include "scripting/v8/V8ScriptObject.h" -#include "scripting/v8/V8ScriptUtils.h" - -class IScriptFunction; - -class V8ScriptEnv : public IScriptEnv -{ -public: - V8ScriptEnv(); - virtual ~V8ScriptEnv(); - - int getType() const override { return 1; } - - void initialize() override; - void cleanup(bool shutDown = false) override; - - IScriptFunction* compile(const std::string& name, const std::string& source) override; - void callFunctionInScope(std::function function) override; - void terminateExecution() override; - - // Parse errors from a TryCatch into lastScriptError - bool parseErrors(v8::TryCatch* tryCatch); - - // -- - v8::Isolate* isolate() const; - v8::Local context() const; - v8::Local global() const; - v8::Local globalTemplate() const; - v8::Local getConstructor(const std::string& key) const; - - void setGlobal(v8::Local global); - void setGlobalTemplate(v8::Local global_tpl); - bool setConstructor(const std::string& key, v8::Local func_tpl); - - // -- - template - std::unique_ptr> wrap(const std::string& constructor_name, T* obj); - - template - T* unwrap(v8::Local value) const; - -private: - static int m_count; - static std::unique_ptr m_platform; - - bool m_initialized; - v8::Isolate::CreateParams m_createParams; - v8::Isolate* m_isolate; - v8::Persistent m_context; - v8::Persistent m_global; - v8::Persistent m_globalTpl; - std::unordered_map> m_constructorMap; -}; - -inline v8::Isolate* V8ScriptEnv::isolate() const -{ - return m_isolate; -} - -inline v8::Local V8ScriptEnv::context() const -{ - return persistentToLocal(isolate(), m_context); -} - -inline v8::Local V8ScriptEnv::global() const -{ - return persistentToLocal(isolate(), m_global); -} - -inline void V8ScriptEnv::setGlobal(v8::Local global) -{ - m_global.Reset(isolate(), global); -} - -inline v8::Local V8ScriptEnv::globalTemplate() const -{ - return persistentToLocal(isolate(), m_globalTpl); -} - -inline void V8ScriptEnv::setGlobalTemplate(v8::Local global_tpl) -{ - m_globalTpl.Reset(isolate(), global_tpl); -} - -inline v8::Local V8ScriptEnv::getConstructor(const std::string& key) const -{ - auto it = m_constructorMap.find(key); - if (it == m_constructorMap.end()) - return v8::Local(); - - return globalPersistentToLocal(isolate(), (*it).second); -} - -template -inline std::unique_ptr> V8ScriptEnv::wrap(const std::string& constructor_name, T* obj) -{ - // Fetch the v8 isolate and context - v8::Isolate* pisolate = isolate(); - v8::Local pcontext = context(); - assert(!pcontext.IsEmpty()); - - // Create a stack-allocated scope for v8 calls, and enter context - v8::Locker locker(pisolate); - v8::Isolate::Scope isolate_scope(pisolate); - v8::HandleScope handle_scope(pisolate); - v8::Context::Scope context_scope(pcontext); - - // Create an instance for the wrapped object - v8::Local ctor_tpl = getConstructor(constructor_name); - v8::Local obj_tpl = ctor_tpl->InstanceTemplate(); - v8::Local new_instance = obj_tpl->NewInstance(pcontext).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, obj); - - return std::make_unique>(obj, pisolate, new_instance); -} - -template -inline T* V8ScriptEnv::unwrap(v8::Local value) const -{ - T* obj = 0; - if (value->IsObject()) - { - v8::MaybeLocal handle = value->ToObject(context()); - if (!handle.IsEmpty()) - obj = static_cast(handle.ToLocalChecked()->GetAlignedPointerFromInternalField(0)); - } - - return obj; -} - -#endif diff --git a/server/include/scripting/v8/V8ScriptFunction.h b/server/include/scripting/v8/V8ScriptFunction.h deleted file mode 100644 index 3c4a239d6..000000000 --- a/server/include/scripting/v8/V8ScriptFunction.h +++ /dev/null @@ -1,67 +0,0 @@ -#ifndef V8SCRIPTFUNCTION_H -#define V8SCRIPTFUNCTION_H - -#include - -#include "scripting/interface/ScriptBindings.h" -#include "scripting/v8/V8ScriptEnv.h" -#include "scripting/v8/V8ScriptUtils.h" - -class V8ScriptData -{ -public: - V8ScriptData(V8ScriptEnv* env, v8::Local object) - : m_env(env) - { - assert(env != nullptr); - assert(object->IsObject()); - m_object.Reset(env->isolate(), object); - } - - ~V8ScriptData() - { - m_object.Reset(); - } - - v8::Local object() const - { - return persistentToLocal(m_env->isolate(), m_object); - } - -private: - V8ScriptEnv* m_env; - v8::Persistent m_object; -}; - -class V8ScriptFunction : public IScriptFunction -{ -public: - V8ScriptFunction(V8ScriptEnv* env, v8::Local function) - : IScriptFunction(), m_env(env) - { - assert(env != nullptr); - assert(function->IsFunction()); - m_function.Reset(env->isolate(), function); - } - - virtual ~V8ScriptFunction() - { - m_function.Reset(); - } - - inline v8::Local function() const - { - return persistentToLocal(m_env->isolate(), m_function); - } - - inline V8ScriptEnv* env() const - { - return m_env; - } - -private: - v8::Persistent m_function; - V8ScriptEnv* m_env; -}; - -#endif diff --git a/server/include/scripting/v8/V8ScriptObject.h b/server/include/scripting/v8/V8ScriptObject.h deleted file mode 100644 index d4ea64013..000000000 --- a/server/include/scripting/v8/V8ScriptObject.h +++ /dev/null @@ -1,89 +0,0 @@ -#ifndef V8SCRIPTWRAPPED_H -#define V8SCRIPTWRAPPED_H - -#include -#include - -#include "scripting/interface/ScriptBindings.h" -#include "scripting/v8/V8ScriptUtils.h" - -template -class V8ScriptObject : public IScriptObject -{ -public: - V8ScriptObject(T* object, v8::Isolate* isolate, v8::Local handle) - : IScriptObject(object), m_isolate(isolate) - { - m_handle.Reset(isolate, handle); - } - - ~V8ScriptObject() - { - // clear handle for children - for (auto it = m_children.begin(); it != m_children.end(); ++it) - { - v8::Local child = globalPersistentToLocal(m_isolate, it->second); - - child->SetAlignedPointerInInternalField(0, nullptr); - it->second.Reset(); - } - - // clear handle - v8::Local obj = handle(m_isolate); - obj->SetAlignedPointerInInternalField(0, nullptr); - m_handle.Reset(); - } - - void addChild(const std::string& prop, v8::Local handle) - { - removeChild(prop); - - v8::Global persist_child; - persist_child.Reset(m_isolate, handle); - m_children[prop] = std::move(persist_child); - } - - void removeChild(const std::string& prop) - { - auto it = m_children.find(prop); - if (it != m_children.end()) - { - v8::Local child = globalPersistentToLocal(m_isolate, it->second); - child->SetAlignedPointerInInternalField(0, nullptr); - it->second.Reset(); - - m_children.erase(it); - } - } - - v8::Local handle(v8::Isolate* isolate) const - { - return persistentToLocal(isolate, m_handle); - } - - v8::Persistent& persistent() - { - return m_handle; - } - - // TODO(joey): This is not implemented just yet. Protect / Unprotect objects from being garbage collected. - // May not be used because as of now there is no objects you can create in script so.. - //inline void protect() { - // m_object.ClearWeak(); - //} - - //inline void unprotect() { - // m_object.SetWeak(this, _V8WeakObjectCallback, v8::WeakCallbackType::kParameter); - // m_object.MarkIndependent(); - //} - - //inline static void v8WeakObjectCallback(const v8::WeakCallbackInfo& data) { - //} - -private: - v8::Isolate* m_isolate; - v8::Persistent m_handle; - std::unordered_map> m_children; -}; - -#endif diff --git a/server/include/scripting/v8/V8ScriptUtils.h b/server/include/scripting/v8/V8ScriptUtils.h deleted file mode 100644 index a4ffb36f5..000000000 --- a/server/include/scripting/v8/V8ScriptUtils.h +++ /dev/null @@ -1,90 +0,0 @@ -#ifndef V8SCRIPTUTILS_H -#define V8SCRIPTUTILS_H - -#include - -// Throw an exception if the function was called with new Function(); -#define V8ENV_THROW_CONSTRUCTOR(args, isolate) \ - if (args.IsConstructCall()) \ - { \ - isolate->ThrowException(v8::String::NewFromUtf8Literal(isolate, \ - "Cannot call function as a constructor.")); \ - return; \ - } - -// Throw an exception if a constructor was called with Function(); -#define V8ENV_THROW_METHOD(args, isolate) \ - if (!args.IsConstructCall()) \ - { \ - isolate->ThrowException(v8::String::NewFromUtf8Literal(isolate, \ - "Cannot call constructor as a function.")); \ - return; \ - } - -// Throw an exception if we didn't receive the correct amount of arguments -#define V8ENV_THROW_ARGCOUNT(args, isolate, required_args) \ - if (args.Length() != required_args) \ - { \ - isolate->ThrowException(v8::String::NewFromUtf8(isolate, \ - std::string("Cannot call function with ") \ - .append(std::to_string(args.Length())) \ - .append(" arguments, required " #required_args) \ - .c_str()) \ - .ToLocalChecked()); \ - return; \ - } - -// Throw an exception if we receive less than the minimum amount of arguments -#define V8ENV_THROW_MINARGCOUNT(args, isolate, required_args) \ - if (args.Length() < required_args) \ - { \ - isolate->ThrowException(v8::String::NewFromUtf8(isolate, \ - std::string("Cannot call function with ") \ - .append(std::to_string(args.Length())) \ - .append(" arguments, required " #required_args) \ - .c_str()) \ - .ToLocalChecked()); \ - return; \ - } - -// Unwrap an object, and validate the pointer -#define V8ENV_SAFE_UNWRAP(ARGS, TYPE, VAR_NAME) \ - TYPE* VAR_NAME = unwrapObject(ARGS.This()); \ - if (!VAR_NAME) \ - { \ - return; \ - } - -template -inline v8::Local persistentToLocal(v8::Isolate* isolate, const v8::Persistent& persistent) -{ - if (persistent.IsWeak()) - { - return v8::Local::New(isolate, persistent); - } - else - { - return *reinterpret_cast*>(const_cast*>(&persistent)); - } -} - -template -inline v8::Local globalPersistentToLocal(v8::Isolate* isolate, const v8::Global& persistent) -{ - if (persistent.IsWeak()) - { - return v8::Local::New(isolate, persistent); - } - else - { - return *reinterpret_cast*>(const_cast*>(&persistent)); - } -} - -template -inline Type* unwrapObject(v8::Local self) -{ - return static_cast(self->GetAlignedPointerFromInternalField(0)); -} - -#endif diff --git a/server/include/scripting/v8/V8ScriptWrappers.h b/server/include/scripting/v8/V8ScriptWrappers.h deleted file mode 100644 index 8a299212f..000000000 --- a/server/include/scripting/v8/V8ScriptWrappers.h +++ /dev/null @@ -1,145 +0,0 @@ -#ifndef V8SCRIPTWRAPPERS_H -#define V8SCRIPTWRAPPERS_H - -#include -#include -#include -#include - -const std::regex word_regex(R"((public[\s]+function[\s]+){1}(\w+)[\s]*\()"); - -//TODO: Move to CPP-file -inline std::string getPublicFunctions(const std::string_view& code) -{ - std::vector eventList; - - std::string s(code); - - auto words_begin = std::sregex_iterator(s.begin(), s.end(), word_regex); - auto words_end = std::sregex_iterator(); - - for (std::sregex_iterator i = words_begin; i != words_end; ++i) - { - std::smatch match = *i; - - std::string match_str = match.str(); - if (match.size() == 3) - eventList.push_back(match[2]); - } - - std::string varNames = ""; - std::string varNameQuotes = ""; - for (const auto eventName: eventList) - { - varNames += eventName + ","; - varNameQuotes += "\"" + eventName + "\"" + ","; - } - if (!varNames.empty()) - { - varNames.pop_back(); - varNameQuotes.pop_back(); - } - - std::string out; - if (!varNames.empty()) - { - out += "const __publicNames = [" + varNameQuotes + "];\n"; - out += "const __publicFuncs = [" + varNames + "];\n"; - out += R"( - for (let i = 0; i < __publicNames.length; ++i) { - if (__publicFuncs[i]) { - //print("Found fn ", __publicNames[i]); - self[__publicNames[i]] = __publicFuncs[i]; - } else { - //print("Not found fn ", __publicNames[i]); - } - } - )"; - } - - return out; -} - -template -inline std::string wrapScript(const std::string& code) -{ - return code; -} - -template -inline std::string wrapScript(const std::string_view& code) -{ - return code.data(); -} - -class NPC; -template<> -inline std::string wrapScript(const std::string_view& code) -{ - // self.onCreated || onCreated, for first declared to take precedence - // if (onCreated) for latest function to override - static const char* prefixString = "(function(npc) {" - "var onCreated, onTimeout, onNpcWarped, onPlayerChats, onPlayerEnters, onPlayerLeaves, onPlayerTouchsMe, onPlayerLogin, onPlayerLogout;" - "const self = npc;" - "if (onCreated) self.onCreated = onCreated;" - "if (onTimeout) self.onTimeout = onTimeout;" - "if (onNpcWarped) self.onNpcWarped = onNpcWarped;" - "if (onPlayerChats) self.onPlayerChats = onPlayerChats;" - "if (onPlayerEnters) self.onPlayerEnters = onPlayerEnters;" - "if (onPlayerLeaves) self.onPlayerLeaves = onPlayerLeaves;" - "if (onPlayerTouchsMe) self.onPlayerTouchsMe = onPlayerTouchsMe;" - "if (onPlayerLogin) self.onPlayerLogin = onPlayerLogin;" - "if (onPlayerLogout) self.onPlayerLogout = onPlayerLogout;" - "\n"; - - std::string wrappedCode = std::string(prefixString); - - std::string publicFunctions = getPublicFunctions(code); - std::string fixedCode = std::regex_replace(std::string(code), word_regex, "function $2("); - - wrappedCode.append(fixedCode); - wrappedCode.append(publicFunctions); - wrappedCode.append("\n});"); - return wrappedCode; -} - -class Player; -template<> -inline std::string wrapScript(const std::string_view& code) -{ - static const char* prefixString = "(function(player) {" - "const self = player;\n"; - - std::string wrappedCode = std::string(prefixString); - - std::string publicFunctions = getPublicFunctions(code); - std::string fixedCode = std::regex_replace(std::string(code), word_regex, "function $2("); - - wrappedCode.append(fixedCode); - wrappedCode.append(publicFunctions); - wrappedCode.append("\n});"); - return wrappedCode; -} - -class Weapon; -template<> -inline std::string wrapScript(const std::string_view& code) -{ - static const char* prefixString = "(function(weapon) {" - "var onCreated, onActionServerSide;" - "const self = weapon;" - "self.onCreated = onCreated;" - "self.onActionServerSide = onActionServerSide;\n"; - - std::string wrappedCode = std::string(prefixString); - - std::string publicFunctions = getPublicFunctions(code); - std::string fixedCode = std::regex_replace(std::string(code), word_regex, "function $2("); - - wrappedCode.append(fixedCode); - wrappedCode.append(publicFunctions); - wrappedCode.append("\n});"); - return wrappedCode; -} - -#endif diff --git a/server/include/utilities/CommandDispatcher.h b/server/include/utilities/CommandDispatcher.h index c40cc49af..a6b43fadb 100644 --- a/server/include/utilities/CommandDispatcher.h +++ b/server/include/utilities/CommandDispatcher.h @@ -1,10 +1,14 @@ -#ifndef GS2EMU_COMMANDDISPATCHER_H -#define GS2EMU_COMMANDDISPATCHER_H +#ifndef COMMANDDISPATCHER_H +#define COMMANDDISPATCHER_H #include -#include #include +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + template class CommandDispatcher { @@ -46,4 +50,7 @@ class CommandDispatcher cmd_map_type m_commands; }; -#endif +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // COMMANDDISPATCHER_H diff --git a/server/include/utilities/CommonTypes.h b/server/include/utilities/CommonTypes.h new file mode 100644 index 000000000..89983386d --- /dev/null +++ b/server/include/utilities/CommonTypes.h @@ -0,0 +1,366 @@ +#ifndef COMMONTYPES_H +#define COMMONTYPES_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace std::literals; + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +//////////////////////////////////////////////////////////////////////////////// + +//---------------------------- +// Concepts + +template +concept Pair = requires(P p) +{ + typename P::first_type; + typename P::second_type; + { p.first } -> std::same_as; + { p.second } -> std::same_as; +}; + +template +concept VariantContainsType = requires(Va v, Tp t) +{ + { std::holds_alternative(v) } -> std::same_as; +}; + +template +concept RangeOf = std::ranges::range && std::same_as, T>; + +template +concept AllSame = sizeof...(Ts) < 2 || + std::conjunction_v< + std::is_same>, Ts>... + >; + +template +concept AllSameAs = sizeof...(Ts) < 2 || + (std::conjunction_v>, Ts>...> + && std::same_as>>); + +template +concept IsEnum = std::is_enum_v; + +template +concept ContainerLike = std::ranges::range && std::ranges::sized_range; + +template +concept ContainerLikeNotString = std::ranges::range && std::ranges::sized_range && !string::StringViewIshVariant && !string::PointerToConstCharString; + +//---------------------------- +// Aliases + +template +using string_map = std::unordered_map; + +template +using string_multimap = std::unordered_multimap; + +template +using string_ordered_multimap = std::multimap>; + +template +using hash_map = std::unordered_map; + +using string_set = std::unordered_set; + +//---------------------------- +// ID types + +using PlayerID = uint16_t; +using NPCID = uint32_t; + +//---------------------------- +// ID constants + +inline constexpr PlayerID NPCServerPlayerID = 2; + +//---------------------------- +// ID start constants + +// Player IDs 0 and 1 break things, and 2 is reserved for the NPC server player. +inline constexpr PlayerID PLAYERID_GEN = 3; + +// Player IDs 16000 and up is used for players on other servers and "IRC"-channels. +// The players from other servers should be unique lists for each player as they are fetched depending on +// what the player chooses to see (buddies, "global guilds" tab, "other servers" tab) +inline constexpr PlayerID PLAYERID_GEN_EXTERNAL = 16000; + +// NPC IDs under 1000 can't be deleted, so require manual assignment. +inline constexpr NPCID NPCID_GEN_MANUAL = 3; +inline constexpr NPCID NPCID_GEN_LOCAL = 300; +inline constexpr NPCID NPCID_GEN_DATABASE = 10000; +inline constexpr NPCID NPCID_GEN_DATABASE_LOCALN = 100000; +inline constexpr uint8_t BADDYID_GEN = 1; + +//---------------------------- +// User-defined literals + +inline constexpr uint8_t operator""_ui8(unsigned long long val) +{ + return static_cast(val); +} + +inline constexpr int8_t operator""_i8(unsigned long long val) +{ + return static_cast(val); +} + +inline constexpr uint16_t operator""_ui16(unsigned long long val) +{ + return static_cast(val); +} + +inline constexpr int16_t operator""_i16(unsigned long long val) +{ + return static_cast(val); +} + +inline constexpr uint32_t operator""_ui32(unsigned long long val) +{ + return static_cast(val); +} + +inline constexpr int32_t operator""_i32(unsigned long long val) +{ + return static_cast(val); +} + +inline constexpr uint64_t operator""_ui64(unsigned long long val) +{ + return static_cast(val); +} + +inline constexpr int64_t operator""_i64(unsigned long long val) +{ + return static_cast(val); +} + +//---------------------------- +// Property helpers + +inline static constexpr uint8_t PROPID(auto prop) +{ + return static_cast(prop); +} + +template +inline static constexpr std::optional PROPOPT(T prop) +{ + return std::make_optional(prop); +} + +template +inline static constexpr std::optional PROPOPT(std::optional prop) +{ + return prop; +} + +inline static constexpr auto ENUM(IsEnum auto e) +{ + return static_cast>(e); +} + +template +inline static constexpr E ENUM(std::underlying_type_t value) +{ + return static_cast(value); +} + +//---------------------------- +// Time helpers + +namespace chrono = std::chrono; +using clock = std::chrono::system_clock; +using precise_clock = std::chrono::steady_clock; +using clock_duration_double = std::chrono::duration; +using duration_seconds_double = std::chrono::duration; +using duration_milli_double = std::chrono::duration; +using duration_nano_double = std::chrono::duration; + +inline clock::time_point currentTime() +{ + return clock::now(); +} + +inline clock::time_point convertFromTimeT(time_t time) +{ + return clock::from_time_t(time); +} + +/// @brief Calculates the absolute time difference between two time points. +/// @tparam T The duration type for the result. Defaults to std::chrono::seconds. +/// @param time1 The first time point. +/// @param time2 The second time point. +/// @return The absolute duration between the two time points, or the maximum duration value if either time point is uninitialized (minimum value). +template +inline T timeDifference(const clock::time_point& time1, const clock::time_point& time2) +{ + if (time1 == clock::time_point::min() || time2 == clock::time_point::min()) + return T::max(); + return std::chrono::duration_cast(time2 >= time1 ? time2 - time1 : time1 - time2); +} + +inline clock::time_point toSystemClock(const std::filesystem::file_time_type& fileTime) +{ +#if __cpp_lib_chrono < 201907L + // Clang doesn't support clock_cast, so convert to UTC, then the system clock. + return std::chrono::file_clock::to_sys(fileTime)); +#else + return std::chrono::clock_cast(fileTime); +#endif +} + +inline std::filesystem::file_time_type toFileClock(const clock::time_point& systemTime) +{ +#if __cpp_lib_chrono < 201907L + // Clang doesn't support clock_cast, so convert to UTC, then the system clock. + return std::chrono::file_clock::from_sys(systemTime); +#else + return std::chrono::clock_cast(systemTime); +#endif +} + +//---------------------------- +// Variant helpers + +template +struct visit_functions : Ts... +{ + using Ts::operator()...; +}; + +//---------------------------- +// Range helpers + +inline static auto toRange(AllSame auto&&... range) +{ + return std::array{ std::forward(range)... }; +} + +inline auto removeNulls = std::views::filter([](auto&& ptr) { return ptr != nullptr; }); + +inline auto toSharedPtr = std::views::transform([](auto&& ptr) { return ptr.lock(); }); + +//---------------------------- +// Floating point helpers + +inline static bool DoubleIsZero(double value) +{ + return std::abs(value) < std::numeric_limits::epsilon(); +} + +inline static bool DoublesAreSame(double left, double right) +{ + // Graal uses 0.0001 as the threshold for comparing doubles. + return std::abs(left - right) < 0.0001; + //return std::abs(left - right) < std::numeric_limits::epsilon(); +} + +template +inline static T DoubleAsIntegralFloor(double value) +{ + if (value < 0.0) + return static_cast(value - std::numeric_limits::epsilon()); + else + return static_cast(value + std::numeric_limits::epsilon()); +} + +//---------------------------- +// Pointer helpers + +template +inline auto toWeakPtr(std::shared_ptr& ptr) +{ + return std::weak_ptr(ptr); +} + +//---------------------------- +// Other helpers + +inline constexpr bool inRangeInclusive(std::integral auto value, std::integral auto min, std::integral auto max) +{ + return value >= min && value <= max; +} + +inline constexpr bool inRangeExclusive(std::integral auto value, std::integral auto min, std::integral auto max) +{ + return value > min && value < max; +} + +template +inline constexpr bool inList(C&& check, Pack&&... values) +{ + return ((check == values) || ...); +} + +//---------------------------- +// Tags + +struct inform_client_t { explicit inform_client_t() = default; }; +inline constexpr inform_client_t inform_client{}; + +struct clear_container_t { explicit clear_container_t() = default; }; +inline constexpr clear_container_t clear_container{}; + +//---------------------------- +// RAII structs + +template +struct SetAndRestore +{ + SetAndRestore(T& var, T newValue) + : m_var(var), m_oldValue(var) + { + var = newValue; + } + ~SetAndRestore() + { + m_var = m_oldValue; + } + +private: + T& m_var; + T m_oldValue; +}; + +//////////////////////////////////////////////////////////////////////////////// +}; // end namespace preagonal + + +//---------------------------- +// Macros + +#ifdef DEBUG +#include +#define DEBUGPRINT(...) do { log::printLine(log::server, __VA_ARGS__); } while(false) +#else +#define DEBUGPRINT(...) +#endif + +#endif // COMMONTYPES_H diff --git a/server/include/utilities/Events.h b/server/include/utilities/Events.h new file mode 100644 index 000000000..ce881042c --- /dev/null +++ b/server/include/utilities/Events.h @@ -0,0 +1,161 @@ +#ifndef EVENTS_H +#define EVENTS_H + +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +//////////////////////////////////////////////////////////////////////////////// + +class EventHandleBase; + +class EventDispatcherBase +{ +public: + EventDispatcherBase(); + virtual ~EventDispatcherBase(); + + /// @brief Unsubscribes a handler from the event dispatcher. + /// @param handle The handle to the event handler to unsubscribe. The handle should have been returned by a previous call to subscribe(). + /// @return True if the handler was successfully unsubscribed, false if the handler was not found or if the handle was invalid. + bool unsubscribe(EventHandleBase* handle); + + /// @brief Unsubscribes a handler from the event dispatcher. + /// @param handle The handle to the event handler to unsubscribe. The handle should have been returned by a previous call to subscribe(). + /// @return True if the handler was successfully unsubscribed, false if the handler was not found or if the handle was invalid. + bool unsubscribe(std::shared_ptr handle); + + /// @brief Unsubscribes all handlers from the event dispatcher. + void unsubscribeAll(); + +protected: + std::unordered_map> m_eventHandlers; +}; + +//---------------------------- + +class EventHandleBase +{ + friend class EventDispatcherBase; + +public: + virtual ~EventHandleBase(); + +protected: + EventHandleBase(EventDispatcherBase* dispatcher, size_t id); + + EventDispatcherBase* m_dispatcher; + size_t m_eventId; +}; + +template +class EventHandleImpl : public EventHandleBase +{ +public: + EventHandleImpl(EventDispatcherBase* dispatcher, size_t id, std::function callback) + : EventHandleBase(dispatcher, id), m_callback(callback) {}; + + ~EventHandleImpl() {}; + + /// @brief Dispatches the event to the subscribed handler. + /// @param ...args The arguments to pass to the event handler. + void dispatch(A... args) + { + if (m_callback) + m_callback(args...); + }; + +private: + std::function m_callback; +}; + +//---------------------------- + +/// @brief An event handle that can be used to unsubscribe from an event. +typedef std::shared_ptr EventHandle; + +//---------------------------- + +/// @brief Dispatches events to subscribed handlers. +/// @tparam ...A The types of the arguments that will be passed to the event handlers when an event is posted. +template +class EventDispatcher : public EventDispatcherBase +{ +public: + EventDispatcher() {}; + virtual ~EventDispatcher() {}; + + /// @brief Posts an event to all subscribed handlers. + /// @param ...args The arguments to pass to the event handlers. + void post(A... args) + { + m_isPosting = true; + + for (auto itr = m_eventHandlers.begin(); itr != m_eventHandlers.end();) + { + auto current = itr++; + auto basePtr = current->second.lock(); + if (!basePtr) + { + m_eventHandlers.erase(current); + continue; + } + + if (auto ptr = std::static_pointer_cast>(basePtr); ptr) + ptr->dispatch(args...); + } + + m_isPosting = false; + flushPendingSubscriptions(); + }; + + /// @brief Subscribes a handler to the event dispatcher. + /// @param callback The callback function to be called when an event is posted. The callback should take the same arguments as the event dispatcher. + /// @return A handle to the subscribed event. The handle can be used to unsubscribe from the event. + EventHandle subscribe(std::function callback) + { + // Static handle counter will give us unique ID's. + static size_t handleCounter = 0; + + // Get handle ID by incrementing the handle counter. + auto handleId = handleCounter++; + + // Create an event handle and store it with the handle ID as the key + std::shared_ptr> handle = std::make_shared>(this, handleId, callback); + + // Store a weak pointer to the event handler so we don't increment ref. count + auto weakHandle = std::weak_ptr(std::static_pointer_cast(handle)); + if (m_isPosting) + m_pendingEventHandlers.emplace_back(handleId, weakHandle); + else + m_eventHandlers[handleId] = weakHandle; + + return handle; + }; + +private: + /// @brief Flushes pending subscriptions that were added while an event was being posted. + void flushPendingSubscriptions() + { + for (auto& pendingHandler : m_pendingEventHandlers) + { + if (auto ptr = pendingHandler.second.lock(); ptr) + m_eventHandlers[pendingHandler.first] = pendingHandler.second; + } + + m_pendingEventHandlers.clear(); + } + + bool m_isPosting = false; + std::vector>> m_pendingEventHandlers; +}; + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // EVENTS_H diff --git a/server/include/utilities/Extents.h b/server/include/utilities/Extents.h new file mode 100644 index 000000000..1d2980d9e --- /dev/null +++ b/server/include/utilities/Extents.h @@ -0,0 +1,947 @@ +#ifndef EXTENTS_H +#define EXTENTS_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::literals; + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +//////////////////////////////////////////////////////////////////////////////// + +template +concept PixelBasedPosition = std::integral; + +template +concept TileBasedPosition = std::floating_point; + +//---------------------------- +// Position + +template +struct Position +{ + constexpr Position() : data{ T{}, T{}, T{} } {} + constexpr Position(T x, T y) : data{ x, y, T{} } {} + constexpr Position(T x, T y, T z) : data{ x, y, z } {} + constexpr Position(const Position& other) : data{ other.data } {} + + template + constexpr Position(const Position& other) : data{ T{ other.data[0] }, T{ other.data[1] }, T{ other.data[2] } } {} + + constexpr bool operator==(const Position& other) const + { + return data == other.data; + } + constexpr bool operator!=(const Position& other) const + { + return data != other.data; + } + + constexpr T& operator[](size_t index) + { + if (index >= 3) throw std::out_of_range("Index out of range for Position"); + return data[index]; + } + constexpr const T& operator[](size_t index) const + { + if (index >= 3) throw std::out_of_range("Index out of range for Position"); + return data[index]; + } + + constexpr T& x() { return data[0]; } + constexpr T& y() { return data[1]; } + constexpr T& z() { return data[2]; } + constexpr const T& x() const { return data[0]; } + constexpr const T& y() const { return data[1]; } + constexpr const T& z() const { return data[2]; } + + constexpr Position& translate(T dx, T dy) + { + data[0] += dx; + data[1] += dy; + return *this; + } + constexpr Position& translate(T dx, T dy, T dz) + { + data[0] += dx; + data[1] += dy; + data[2] += dz; + return *this; + } + + template + constexpr Position& translate(const Position& delta) + { + data[0] += static_cast(delta.data[0]); + data[1] += static_cast(delta.data[1]); + data[2] += static_cast(delta.data[2]); + return *this; + } + + Position translate(T dx, T dy) const + { + Position result{ *this }; + result.translate(dx, dy); + return result; + } + Position translate(T dx, T dy, T dz) const + { + Position result{ *this }; + result.translate(dx, dy, dz); + return result; + } + + template + Position translate(const Position& delta) const + { + Position result{ *this }; + result.translate(delta); + return result; + } + + T length2D() const + { + return static_cast(std::sqrt(data[0] * data[0] + data[1] * data[1])); + } + + T length3D() const + { + return static_cast(std::sqrt(data[0] * data[0] + data[1] * data[1] + data[2] * data[2])); + } + + template + Position& normalize2D(O length) + { + if (length == O{}) + throw std::invalid_argument("Cannot normalize a position with zero length."); + data[0] = static_cast(data[0] / length); + data[1] = static_cast(data[1] / length); + return *this; + } + + template + Position normalize2D(O length) const + { + if (length == O{}) + throw std::invalid_argument("Cannot normalize a position with zero length."); + Position result{ *this }; + result.data[0] = static_cast(result.data[0] / length); + result.data[1] = static_cast(result.data[1] / length); + return result; + } + + template + Position& normalize3D(O length) + { + if (length == O{}) + throw std::invalid_argument("Cannot normalize a position with zero length."); + data[0] = static_cast(data[0] / length); + data[1] = static_cast(data[1] / length); + data[2] = static_cast(data[2] / length); + return *this; + } + + template + Position normalize3D(O length) const + { + if (length == O{}) + throw std::invalid_argument("Cannot normalize a position with zero length."); + Position result{ *this }; + result.data[0] = static_cast(result.data[0] / length); + result.data[1] = static_cast(result.data[1] / length); + result.data[2] = static_cast(result.data[2] / length); + return result; + } + + std::array data; +}; + +using PixelPosition = Position; +using LocalPixelPosition = Position; + +using TilePosition = Position; +using WholeTilePosition = Position; +using LocalWholeTilePosition = Position; + +using MapPosition = Position; + +//---------------------------- +// Dimension + +template +struct Dimension +{ + constexpr Dimension() requires PixelBasedPosition : data{ T{}, T{}, T{48} } {} + constexpr Dimension() requires TileBasedPosition : data{ T{}, T{}, T{3} } {} + constexpr Dimension(T width, T height) requires PixelBasedPosition : data{ width, height, T{48} } {} + constexpr Dimension(T width, T height) requires TileBasedPosition : data{ width, height, T{3} } {} + constexpr Dimension(T width, T height, T length) : data{ width, height, length } {} + constexpr Dimension(const Dimension& other) : data{ other.data } {} + + template + constexpr Dimension(const Dimension& other) : data{ T{ other.data[0] }, T{ other.data[1] }, T{ other.data[2] } } {} + + constexpr bool operator==(const Dimension& other) const + { + return data == other.data; + } + constexpr bool operator!=(const Dimension& other) const + { + return data != other.data; + } + + constexpr T& operator[](size_t index) + { + if (index >= 3) throw std::out_of_range("Index out of range for Dimension"); + return data[index]; + } + constexpr const T& operator[](size_t index) const + { + if (index >= 3) throw std::out_of_range("Index out of range for Dimension"); + return data[index]; + } + + constexpr T& width() { return data[0]; } + constexpr T& height() { return data[1]; } + constexpr T& length() { return data[2]; } + constexpr const T& width() const { return data[0]; } + constexpr const T& height() const { return data[1]; } + constexpr const T& length() const { return data[2]; } + + std::array data; +}; + +//---------------------------- +// Rectangle (area) + +template +struct Rectangle +{ + constexpr Rectangle() {} + constexpr Rectangle(Position

position, Dimension size) : position(position), size(size) {} + + constexpr P left() const noexcept { return position.x(); } + constexpr P right() const noexcept { return position.x() + size.width(); } + constexpr P top() const noexcept { return position.y(); } + constexpr P bottom() const noexcept { return position.y() + size.height(); } + constexpr P ground() const noexcept { return position.z(); } + constexpr P sky() const noexcept { return position.z() + size.length(); } + constexpr Position

center() const noexcept { return { position.x() + static_cast

(size.width() / (S)2), position.y() + static_cast

(size.height() / (S)2) }; } + + Position

position{}; + Dimension size{}; +}; + +using PixelRectangleArea = Rectangle; +using LocalPixelRectangleArea = Rectangle; + +using TileRectangleArea = Rectangle; +using WholeTileRectangleArea = Rectangle; +using LocalWholeTileRectangleArea = Rectangle; + +using ImagePartRectangle = Rectangle; + +//---------------------------- +// Intersections + +template +inline constexpr bool positionInRectangle(const Position& pos, const Rectangle& rect) +{ + return pos.x() >= rect.left() && pos.x() <= rect.right() + && pos.y() >= rect.top() && pos.y() <= rect.bottom() + && pos.z() >= rect.ground() && pos.z() <= rect.sky(); +} + +template +inline constexpr bool rectanglesIntersect(const Rectangle& first, const Rectangle& second) +{ + return (first.right() < second.left() + || second.right() < first.left() + || first.bottom() < second.top() + || second.bottom() < first.top() + || first.sky() < second.ground() + || second.sky() < first.ground() + ) == false; +} + +template +inline constexpr bool rectangleContained(const Rectangle& child, const Rectangle& parent) +{ + return (child.left() >= parent.left() + && child.right() <= parent.right() + && child.top() >= parent.top() + && child.bottom() <= parent.bottom() + && child.ground() >= parent.ground() + && child.sky() <= parent.sky()); +} + +//---------------------------- +// Translations + +template +inline Position translatePosition(const Position& position, T x, T y) +{ + return position.translate(x, y); +} + +template +inline Position translatePosition(const Position& position, T x, T y, T z) +{ + return position.translate(x, y, z); +} + +template +inline Position translatePosition(const Position& position, const Position& delta) +{ + return position.translate(delta); +} + +//---------------------------- +// Conversions + +inline constexpr PixelPosition toPixelPosition(const PixelPosition& origin, std::floating_point auto x, std::floating_point auto y) +{ + // Enforce half tile increments. We will never have a float position that isn't a half tile. + int32_t halfTileX = static_cast(x * 2); + int32_t halfTileY = static_cast(y * 2); + return PixelPosition{ static_cast(origin.x() + (halfTileX * 8)), static_cast(origin.y() + (halfTileY * 8)), static_cast(origin.z()) }; +} + +inline constexpr PixelPosition toPixelPosition(const TilePosition& position) +{ + return PixelPosition{ static_cast(position.x() * 16), static_cast(position.y() * 16), static_cast(position.z() * 16) }; +} + +template +inline constexpr PixelPosition toPixelPosition(const PixelPosition& origin, const Position& position) +{ + // Same coordinates. + if constexpr (std::same_as) + { + return origin + position; + } + // Tiles to pixels. + else if constexpr (std::same_as || std::same_as) + { + return PixelPosition{ origin.x() + static_cast(position.x() * 16), origin.y() + static_cast(position.y() * 16), origin.z() + static_cast(position.z() * 16) }; + } + // Just convert the units. + else + { + return PixelPosition{ origin.x() + static_cast(position.x()), origin.y() + static_cast(position.y()), origin.z() + static_cast(position.z()) }; + } +} + +inline constexpr LocalPixelPosition toLocalPixelPosition(std::floating_point auto x, std::floating_point auto y) +{ + // Enforce half tile increments. We will never have a float position that isn't a half tile. + int16_t halfTileX = static_cast(x * 2); + int16_t halfTileY = static_cast(y * 2); + return LocalPixelPosition{ static_cast((halfTileX * 8) % 1024), static_cast((halfTileY * 8) % 1024) }; +} + +template +inline constexpr LocalPixelPosition toLocalPixelPosition(const Position& position) +{ + // Same coordinates. + if constexpr (std::same_as) + { + return position; + } + // Global pixel position to local. + else if constexpr (std::same_as) + { + return LocalPixelPosition{ static_cast(position.x() % 1024), static_cast(position.y() % 1024), static_cast(position.z()) }; + } + // Tiles to local pixels. + else if constexpr (std::same_as || std::same_as) + { + return LocalPixelPosition{ static_cast(static_cast(position.x() * 16) % 1024), static_cast(static_cast(position.y() * 16) % 1024), static_cast(position.z() * 16) }; + } + // Just convert the units. + else + { + return LocalPixelPosition{ static_cast(position.x()), static_cast(position.y()), static_cast(position.z()) }; + } +} + +template +inline constexpr TilePosition toTilePosition(const Position& position) +{ + // Pixels to tiles. + if constexpr (std::same_as || std::same_as) + { + return TilePosition{ static_cast(position.x()) / 16.0f, static_cast(position.y()) / 16.0f, static_cast(position.z()) / 16.0f }; + } + // Same coordinates. + else if constexpr (std::same_as) + { + return position; + } + // Just convert the units. + else + { + return TilePosition{ static_cast(position.x()), static_cast(position.y()), static_cast(position.z()) }; + } +} + +template +inline constexpr LocalWholeTilePosition toLocalWholeTilePosition(const Position& position) +{ + // Pixels to local whole tiles. + if constexpr (std::same_as || std::same_as) + { + auto x = static_cast((position.x() % 1024) / 16); + auto y = static_cast((position.y() % 1024) / 16); + auto z = static_cast(position.z() / 16); + return LocalWholeTilePosition{ static_cast(x), static_cast(y), static_cast(z) }; + } + // Same coordinates. + else if constexpr (std::same_as) + { + return position; + } + // Tiles to local whole tiles. + else if constexpr (std::same_as) + { + auto x = static_cast(position.x() + std::numeric_limits::epsilon()); + auto y = static_cast(position.y() + std::numeric_limits::epsilon()); + auto z = static_cast(position.z() + std::numeric_limits::epsilon()); + return LocalWholeTilePosition{ static_cast(x % 64), static_cast(y % 64), static_cast(z) }; + } + // Whole tiles to local whole tiles. + else if constexpr (std::same_as) + { + auto x = static_cast(position.x()); + auto y = static_cast(position.y()); + auto z = static_cast(position.z()); + return LocalWholeTilePosition{ static_cast(x % 64), static_cast(y % 64), static_cast(z) }; + } + // Just convert the units. + else + { + return LocalWholeTilePosition{ static_cast(position.x()), static_cast(position.y()), static_cast(position.z()) }; + } +} + +template +inline constexpr WholeTilePosition toWholeTilePosition(const Position& position) +{ + // Pixels to whole tiles. + if constexpr (std::same_as || std::same_as) + { + auto x = static_cast(position.x() / 16); + auto y = static_cast(position.y() / 16); + auto z = static_cast(position.z() / 16); + return WholeTilePosition{ static_cast(x), static_cast(y), static_cast(z) }; + } + // Same coordinates. + else if constexpr (std::same_as) + { + return position; + } + // Tiles to whole tiles. + else if constexpr (std::same_as) + { + auto x = static_cast(position.x() + std::numeric_limits::epsilon()); + auto y = static_cast(position.y() + std::numeric_limits::epsilon()); + auto z = static_cast(position.z() + std::numeric_limits::epsilon()); + return WholeTilePosition{ static_cast(x), static_cast(y), static_cast(z) }; + } + // Just convert the units. + else + { + return WholeTilePosition{ static_cast(position.x()), static_cast(position.y()), static_cast(position.z()) }; + } +} + +inline constexpr MapPosition toMapPosition(const PixelPosition& position) +{ + return { static_cast(position.x() / 1024), static_cast(position.y() / 1024) }; +} + +inline constexpr MapPosition toMapPosition(const TilePosition& position) +{ + return { static_cast((position.x() + std::numeric_limits::epsilon()) / 64), static_cast((position.y() + std::numeric_limits::epsilon()) / 64) }; +} + +inline constexpr MapPosition toMapPosition(const WholeTilePosition& position) +{ + return { static_cast(position.x() / 64), static_cast(position.y() / 64) }; +} + +//---------------------------- + +inline constexpr PixelRectangleArea toPixelRectangleArea(const TileRectangleArea& rect) +{ + Dimension size{ static_cast(rect.size.width() * 16), static_cast(rect.size.height() * 16), static_cast(rect.size.length() * 16) }; + return PixelRectangleArea{ toPixelPosition(rect.position), size }; +} + +template +inline constexpr PixelRectangleArea toPixelRectangleArea(const MapPosition& origin, const Rectangle& rect) +{ + // Same coordinates. + if constexpr (std::same_as) + { + Dimension size{ static_cast(rect.size.width()), static_cast(rect.size.height()), static_cast(rect.size.length()) }; + return PixelRectangleArea{ rect.position, size }; + } + // Tiles to pixels. + else if constexpr (std::same_as) + { + return toPixelRectangleArea(rect); + } + // Local tiles to pixels. + else if constexpr (std::same_as) + { + Dimension size{ static_cast(rect.size.width() * 16), static_cast(rect.size.height() * 16), static_cast(rect.size.length() * 16) }; + return PixelRectangleArea{ toPixelPosition(origin, rect.position), size }; + } + // Just convert the units. + else + { + Dimension size{ static_cast(rect.size.width()), static_cast(rect.size.height()), static_cast(rect.size.length()) }; + return PixelRectangleArea{ toPixelPosition(origin, rect.position), size }; + } +} + +template +inline constexpr LocalWholeTileRectangleArea toLocalWholeTileRectangleArea(const Rectangle& rect) +{ + // Same coordinates. + if constexpr (std::same_as) + { + Dimension size{ static_cast(rect.size.width()), static_cast(rect.size.height()), static_cast(rect.size.length()) }; + return LocalWholeTileRectangleArea{ rect.position, size }; + } + + int32_t x; + int32_t y; + int32_t z; + int32_t width; + int32_t height; + int32_t length; + + // Tiles to local whole tiles. + if constexpr (std::same_as) + { + x = static_cast(rect.position.x() + std::numeric_limits::epsilon()); + y = static_cast(rect.position.y() + std::numeric_limits::epsilon()); + z = static_cast(rect.position.z() + std::numeric_limits::epsilon()); + width = static_cast(rect.size.width() + std::numeric_limits::epsilon()); + height = static_cast(rect.size.height() + std::numeric_limits::epsilon()); + length = static_cast(rect.size.length() + std::numeric_limits::epsilon()); + } + // Whole tiles to local whole tiles. + else if constexpr (std::same_as) + { + x = static_cast(rect.position.x()); + y = static_cast(rect.position.y()); + z = static_cast(rect.position.z()); + width = static_cast(rect.size.width()); + height = static_cast(rect.size.height()); + length = static_cast(rect.size.length()); + } + // Pixels to local whole tiles. + else if constexpr (std::same_as || std::same_as) + { + x = static_cast(rect.position.x() / 16); + y = static_cast(rect.position.y() / 16); + z = static_cast(rect.position.z() / 16); + width = static_cast(rect.size.width() / 16); + height = static_cast(rect.size.height() / 16); + length = static_cast(rect.size.length() / 16); + } + // Just convert the units. + else + { + Position pos{ static_cast(rect.position.x()), static_cast(rect.position.y()), static_cast(rect.position.z()) }; + Dimension size{ static_cast(rect.size.width()), static_cast(rect.size.height()), static_cast(rect.size.length()) }; + return LocalWholeTileRectangleArea{ pos, size }; + } + + LocalWholeTilePosition pos{ static_cast(x % 64), static_cast(y % 64), static_cast(z) }; + Dimension size{ static_cast(width), static_cast(height), static_cast(length) }; + return LocalWholeTileRectangleArea{ pos, size }; +} + +template +inline constexpr LocalWholeTileRectangleArea clipLocalWholeTileRectangleArea(const MapPosition& origin, const Rectangle& rect) +{ + // Same coordinates. + if constexpr (std::same_as) + { + Dimension size{ static_cast(rect.size.width()), static_cast(rect.size.height()), static_cast(rect.size.length()) }; + return LocalWholeTileRectangleArea{ rect.position, size }; + } + + int32_t x; + int32_t y; + int32_t z; + int32_t width; + int32_t height; + int32_t length; + PixelPosition pixelOrigin = PixelPosition{ static_cast(origin.x()) * 1024, static_cast(origin.y()) * 1024, 0 }; + + // Tiles to local whole tiles. + if constexpr (std::same_as) + { + x = static_cast(rect.position.x() + std::numeric_limits::epsilon()); + y = static_cast(rect.position.y() + std::numeric_limits::epsilon()); + z = static_cast(rect.position.z() + std::numeric_limits::epsilon()); + width = static_cast(rect.size.width() + std::numeric_limits::epsilon()); + height = static_cast(rect.size.height() + std::numeric_limits::epsilon()); + length = static_cast(rect.size.length() + std::numeric_limits::epsilon()); + } + // Whole tiles to local whole tiles. + else if constexpr (std::same_as) + { + x = static_cast(rect.position.x()); + y = static_cast(rect.position.y()); + z = static_cast(rect.position.z()); + width = static_cast(rect.size.width()); + height = static_cast(rect.size.height()); + length = static_cast(rect.size.length()); + } + // Pixels to local whole tiles. + else if constexpr (std::same_as || std::same_as) + { + x = static_cast(rect.position.x() / 16); + y = static_cast(rect.position.y() / 16); + z = static_cast(rect.position.z() / 16); + width = static_cast(rect.size.width() / 16); + height = static_cast(rect.size.height() / 16); + length = static_cast(rect.size.length() / 16); + } + // Just convert the units. + else + { + Position pos{ static_cast(rect.position.x()), static_cast(rect.position.y()), static_cast(rect.position.z()) }; + Dimension size{ static_cast(rect.size.width()), static_cast(rect.size.height()), static_cast(rect.size.length()) }; + return LocalWholeTileRectangleArea{ pos, size }; + } + + // If the relative position to the origin is negative, we need to adjust it to be within the local level. + if (x * 16 < pixelOrigin.x()) + { + width -= (pixelOrigin.x() - (x * 16)) / 16; + x = 0; + } + if (y * 16 < pixelOrigin.y()) + { + height -= (pixelOrigin.y() - (y * 16)) / 16; + y = 0; + } + + // Adjust the position to fit within the local level. + x %= 64; + y %= 64; + + // If the boundaries are out of the local level, adjust them to fit. + if (x + width > 64) width = 64 - x; + if (y + height > 64) height = 64 - y; + + LocalWholeTilePosition pos{ static_cast(x), static_cast(y), static_cast(z) }; + Dimension size{ static_cast(width), static_cast(height), static_cast(length) }; + return LocalWholeTileRectangleArea{ pos, size }; +} + +inline constexpr WholeTileRectangleArea toWholeTileRectangleArea(const PixelRectangleArea& rect) +{ + return WholeTileRectangleArea{ toWholeTilePosition(rect.position), Dimension(static_cast(rect.size.width() / 16), static_cast(rect.size.height() / 16)) }; +} + +inline constexpr WholeTileRectangleArea toWholeTileRectangleArea(const TileRectangleArea& rect) +{ + auto width = static_cast(rect.size.width() + std::numeric_limits::epsilon()); + auto height = static_cast(rect.size.height() + std::numeric_limits::epsilon()); + return WholeTileRectangleArea{ toWholeTilePosition(rect.position), Dimension(width, height) }; +} + +//---------------------------- +// Math + +template +inline constexpr Position operator*(const Position& left, const OtherType& right) +{ + return Position{ static_cast(left.x() * right), static_cast(left.y() * right), static_cast(left.z() * right) }; +} + +template +inline constexpr Position operator+(const Position& left, const OtherType& right) +{ + return Position{ static_cast(left.x() + right), static_cast(left.y() + right), static_cast(left.z() + right) }; +} + +template +inline constexpr Position operator-(const Position& left, const OtherType& right) +{ + return Position{ static_cast(left.x() - right), static_cast(left.y() - right), static_cast(left.z() - right) }; +} + +template +inline constexpr Position operator/(const Position& left, const OtherType& right) +{ + return Position{ static_cast(left.x() / right), static_cast(left.y() / right), static_cast(left.z() / right) }; +} + +// + +template +inline constexpr Position operator*(const Position& left, const OtherType& right) +{ + return Position{ static_cast(left.x() * right), static_cast(left.y() * right), static_cast(left.z() * right) }; +} + +template +inline constexpr Position operator+(const Position& left, const OtherType& right) +{ + return Position{ static_cast(left.x() + right), static_cast(left.y() + right), static_cast(left.z() + right) }; +} + +template +inline constexpr Position operator-(const Position& left, const OtherType& right) +{ + return Position{ static_cast(left.x() - right), static_cast(left.y() - right), static_cast(left.z() - right) }; +} + +template +inline constexpr Position operator/(const Position& left, const OtherType& right) +{ + return Position{ static_cast(left.x() / right), static_cast(left.y() / right), static_cast(left.z() / right) }; +} + +// + +template +inline constexpr Position operator*(const Position& left, const Position& right) +{ + return Position{ static_cast(left.x() * right.x()), static_cast(left.y() * right.y()), static_cast(left.z() * right.z()) }; +} + +template +inline constexpr Position operator+(const Position& left, const Position& right) +{ + return Position{ static_cast(left.x() + right.x()), static_cast(left.y() + right.y()), static_cast(left.z() + right.z()) }; +} + +template +inline constexpr Position operator-(const Position& left, const Position& right) +{ + return Position{ static_cast(left.x() - right.x()), static_cast(left.y() - right.y()), static_cast(left.z() - right.z()) }; +} + +template +inline constexpr Position operator/(const Position& left, const Position& right) +{ + return Position{ static_cast(left.x() / right.x()), static_cast(left.y() / right.y()), static_cast(left.z() / right.z()) }; +} + +//---------------------------- + +template +inline constexpr Dimension operator*(const Dimension& left, const OtherType& right) +{ + return Dimension{ static_cast(left.width() * right), static_cast(left.height() * right), static_cast(left.length() * right) }; +} + +template +inline constexpr Dimension operator+(const Dimension& left, const OtherType& right) +{ + return Dimension{ static_cast(left.width() + right), static_cast(left.height() + right), static_cast(left.length() + right) }; +} + +template +inline constexpr Dimension operator-(const Dimension& left, const OtherType& right) +{ + return Dimension{ static_cast(left.width() - right), static_cast(left.height() - right), static_cast(left.length() - right) }; +} + +template +inline constexpr Dimension operator/(const Dimension& left, const OtherType& right) +{ + return Dimension{ static_cast(left.width() / right), static_cast(left.height() / right), static_cast(left.length() / right) }; +} + +// + +template +inline constexpr Dimension operator*(const Dimension& left, const OtherType& right) +{ + return Dimension{ static_cast(left.width() * right), static_cast(left.height() * right), static_cast(left.length() * right) }; +} + +template +inline constexpr Dimension operator+(const Dimension& left, const OtherType& right) +{ + return Dimension{ static_cast(left.width() + right), static_cast(left.height() + right), static_cast(left.length() + right) }; +} + +template +inline constexpr Dimension operator-(const Dimension& left, const OtherType& right) +{ + return Dimension{ static_cast(left.width() - right), static_cast(left.height() - right), static_cast(left.length() - right) }; +} + +template +inline constexpr Dimension operator/(const Dimension& left, const OtherType& right) +{ + return Dimension{ static_cast(left.width() / right), static_cast(left.height() / right), static_cast(left.length() / right) }; +} + +// + +template +inline constexpr Dimension operator*(const Dimension& left, const Dimension& right) +{ + return Dimension{ static_cast(left.width() * right.width()), static_cast(left.height() * right.height()), static_cast(left.length() * right.length()) }; +} + +template +inline constexpr Dimension operator+(const Dimension& left, const Dimension& right) +{ + return Dimension{ static_cast(left.width() + right.width()), static_cast(left.height() + right.height()), static_cast(left.length() + right.length()) }; +} + +template +inline constexpr Dimension operator-(const Dimension& left, const Dimension& right) +{ + return Dimension{ static_cast(left.width() - right.width()), static_cast(left.height() - right.height()), static_cast(left.length() - right.length()) }; +} + +template +inline constexpr Dimension operator/(const Dimension& left, const Dimension& right) +{ + return Dimension{ static_cast(left.width() / right.width()), static_cast(left.height() / right.height()), static_cast(left.length() / right.length()) }; +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +// Structured bindings support. +namespace std +{ +//////////////////////////////////////////////////////////////////////////////// + +// Position +template +class tuple_size> : public std::integral_constant {}; + +template +class tuple_element> { public: using type = T; }; + +// Dimension +template +class tuple_size> : public std::integral_constant {}; + +template +class tuple_element> { public: using type = T; }; + +// Rectangle +template +class tuple_size> : public std::integral_constant {}; + +template +class tuple_element> : conditional +{ + static_assert(I < 2, "Index out of bounds for tuple_element"); +}; + +//////////////////////////////////////////////////////////////////////////////// +} +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +//////////////////////////////////////////////////////////////////////////////// + +template +constexpr T& get(preagonal::Position& vec) { return vec.data[I]; } + +template +constexpr const T& get(const preagonal::Position& vec) { return vec.data[I]; } + +template +constexpr T& get(preagonal::Dimension& vec) { return vec.data[I]; } + +template +constexpr const T& get(const preagonal::Dimension& vec) { return vec.data[I]; } + +template +std::tuple_element_t>& get(preagonal::Rectangle& rect) +{ + static_assert(I < 2, "Index out of bounds for get"); + + if constexpr (I == 0) + return rect.position; + else if constexpr (I == 1) + return rect.size; +} + +template +std::tuple_element_t>&& get(const preagonal::Rectangle& rect) +{ + static_assert(I < 2, "Index out of bounds for get"); + + if constexpr (I == 0) + return rect.position; + else if constexpr (I == 1) + return rect.size; +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +////////////////////////////////////////////////// +// Printing +////////////////////////////////////////////////// + +template +struct std::formatter> : std::formatter +{ + auto format(const preagonal::Position& pos, std::format_context& ctx) const + { + return std::format_to(ctx.out(), "{},{},{}", pos.x(), pos.y(), pos.z()); + } +}; + +template +struct std::formatter> : std::formatter +{ + auto format(const preagonal::Position& pos, std::format_context& ctx) const + { + return std::format_to(ctx.out(), "{:04.2f},{:04.2f},{:04.2f}", pos.x(), pos.y(), pos.z()); + } +}; + +template +struct std::formatter> : std::formatter +{ + auto format(const preagonal::Dimension& dim, std::format_context& ctx) const + { + return std::format_to(ctx.out(), "{},{},{}", dim.width(), dim.height(), dim.length()); + } +}; + +template +struct std::formatter> : std::formatter +{ + auto format(const preagonal::Dimension& dim, std::format_context& ctx) const + { + return std::format_to(ctx.out(), "{:04.2f},{:04.2f},{:04.2f}", dim.width(), dim.height(), dim.length()); + } +}; + +#endif // EXTENTS_H diff --git a/server/include/utilities/FilePermissions.h b/server/include/utilities/FilePermissions.h index 764924dbb..62a043473 100644 --- a/server/include/utilities/FilePermissions.h +++ b/server/include/utilities/FilePermissions.h @@ -1,21 +1,23 @@ -#ifndef GS2EMU_FILEPERMISSIONS_H -#define GS2EMU_FILEPERMISSIONS_H - -#pragma once +#ifndef FILEPERMISSIONS_H +#define FILEPERMISSIONS_H #include +#include #include #include #include #include +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + class FilePermissions { public: - /** - * @enum Type - * @brief Defines the types of permissions available. - */ + /// @enum Type + /// @brief Defines the types of permissions available. enum Type : uint8_t { Read, @@ -23,34 +25,24 @@ class FilePermissions COUNT }; - /** - * @brief Adds a new permission to the manager. - * - * @param permissionString The permission string (e.g., "rw accounts/*"). - */ + /// @brief Adds a new permission to the manager. + /// @param permissionString The permission string (e.g., "rw accounts/*"). void addPermission(const std::string& permissionString); - /** - * @brief Checks if a given file path has the required permissions. - * - * @param path The path were checking for access - * @param Type The type of permission were checking for (e.g. Read or Write) - * @return true if the path has the required permission, false otherwise. - */ + /// @brief Checks if a given file path has the required permissions. + /// + /// @param path The path were checking for access + /// @param Type The type of permission were checking for (e.g. Read or Write) + /// @return true if the path has the required permission, false otherwise. bool hasPermission(const std::string& path, Type type) const; - /** - * @brief Loads permissions from a string input. - * - * @param input The string input containing permissions (e.g., "rw accounts/*\n-rw config/settings.php"). - */ + /// @brief Loads permissions from a string input. + /// @param input The string input containing permissions (e.g., "rw accounts/*\n-rw config/settings.php"). void loadPermissions(const std::string& permissionString); private: - /** - * @struct Permission - * @brief Represents a single permission rule. - */ + /// @struct Permission + /// @brief Represents a single permission rule. struct Permission { std::bitset flags; @@ -63,4 +55,7 @@ class FilePermissions static bool match(const std::string& path, const Permission& permission); }; -#endif //GS2EMU_FILEPERMISSIONS_H +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // FILEPERMISSIONS_H diff --git a/server/include/utilities/IdGenerator.h b/server/include/utilities/IdGenerator.h deleted file mode 100644 index 0d5da3149..000000000 --- a/server/include/utilities/IdGenerator.h +++ /dev/null @@ -1,56 +0,0 @@ -#ifndef UTILITIES_ID_GENERATOR_H -#define UTILITIES_ID_GENERATOR_H - -#include -#include - -template -class IdGenerator -{ -public: - IdGenerator() = default; - IdGenerator(T startId) : m_nextId(startId) {} - - // Generate a new ID - T getAvailableId() - { - if (!m_freeIds.empty()) - { - T id = *m_freeIds.begin(); - m_freeIds.erase(m_freeIds.begin()); - return id; - } - return m_nextId++; - } - - // Peeks the next ID - T peekNextId() const - { - return m_nextId; - } - - // Set the next ID - void setNextId(T id) - { - m_nextId = id; - } - - // Free an ID - void freeId(T id) - { - m_freeIds.insert(id); - } - - // Reset the free IDs and set the next ID - void resetAndSetNext(T nextId = 0) - { - m_freeIds.clear(); - m_nextId = nextId; - } - -protected: - T m_nextId = static_cast(0); - std::set m_freeIds; -}; - -#endif // UTILITIES_ID_GENERATOR_H diff --git a/server/include/utilities/Log.h b/server/include/utilities/Log.h new file mode 100644 index 000000000..246fa164b --- /dev/null +++ b/server/include/utilities/Log.h @@ -0,0 +1,345 @@ +#ifndef LOG_H +#define LOG_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +// Don't change this order. +// For some reason the compile will fail in Windows for some versions of MSVC. +#include +#include +#include +#include +// + +using namespace std::literals::string_view_literals; + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::log +{ +/////////////////////////////////////////////////////////////////////////////// + +/// @brief The timestamp mode for the log. +enum class TimestampMode +{ + /// @brief No timestamp. + None, + + /// @brief [HH:MM AM] (%I:%M %p) + Short, + + /// @brief [yyyy-MM-dd HH:MM:SS] (%F %T) + Long +}; + +/// @brief Timestamp format: short. Example: [12:34 PM] +inline constexpr std::string_view TimestampShort = "[{0:%I}:{0:%M} {0:%p}]"sv; + +/// @brief Timestamp format: long. Example: [2024-01-01 12:34:56] +inline constexpr std::string_view TimestampLong = "[{0:%F} {0:%T}]"sv; + +/////////////////////////////////////////////////////////////////////////////// + +struct IndentAbsolute_t { explicit IndentAbsolute_t() = default; }; + +/// @brief Absolute indentation tag for the Indent class. +inline constexpr IndentAbsolute_t IndentAbsolute{}; + +/// @brief Indentation helper for logging. +class Log; +struct Indent +{ + Indent(Log* log, uint8_t level); + Indent(IndentAbsolute_t is_absolute, Log* log, uint8_t level); + Indent(Indent&& other) noexcept; + ~Indent() noexcept; + +private: + Log* m_log = nullptr; + uint8_t m_old_level = 0; +}; + +/////////////////////////////////////////////////////////////////////////////// + +// Instance of a log file. +struct Log +{ + /// @brief The path to the log file. + std::filesystem::path filename; + + /// @brief A prefix to add at the start of each new line when indent level is 0. Useful for section headers. + std::string sectionPrefix; + + /// @brief The number of spaces to add for each indentation level. + uint8_t indentSpaces = 2; + + /// @brief The current indentation level. 0 means no indentation. + uint8_t indentLevel = 0; + + /// @brief Specifies the timestamp mode for file operations. + TimestampMode timestampFile = TimestampMode::Long; + + /// @brief Specifies the timestamp mode for console output. + TimestampMode timestampCli = TimestampMode::Short; + + /// @brief Mirror output to the console as well as the file. + bool mirrorToCli = true; + + /// @brief Indicates whether the next output is at the start of a new line. Used internally to determine when to add prefixes and indentation. + bool atLineStart = true; + + /// @brief A unique pointer to an output file stream. + std::unique_ptr file; + + /// @brief A mutex to synchronize access to the log file and related state. + std::recursive_mutex mutex; + + /// @brief Reloads the log file (after a filename change). + Log& reload(); + + /// @brief Closes the log file. + Log& close(); + + /// @brief Clears the log file. + Log& clear(); + + /// @brief Gets the output file stream. + /// @return A pointer to the output file stream. + std::ofstream* getFile(); + + /// @brief Creates an RAII indentation object. + /// @param levels The number of indentation levels to add. + Indent indent(uint8_t levels = 1) + { + return Indent(this, levels); + } + + /// @brief Creates an RAII indentation object on an absolute indentation level. + /// @param level The absolute indentation level. + Indent indent_absolute(uint8_t level) + { + return Indent(IndentAbsolute, this, level); + } + + /// @brief Creates a unique pointer to an RAII indentation object. + /// @param levels The number of indentation levels to add. + std::unique_ptr indent_ptr(uint8_t levels = 1) + { + return std::make_unique(this, levels); + } +}; + +/// @brief The serverlog.txt file. +inline Log server{ .filename = std::filesystem::path{ "logs" } / "serverlog.txt", .sectionPrefix = ":: "s }; + +/// @brief The rclog.txt file. +inline Log rc{ .filename = std::filesystem::path{ "logs" } / "rclog.txt" }; + +/// @brief The npclog.txt file. +inline Log npc{ .filename = std::filesystem::path{ "logs" } / "npclog.txt" }; + +/// @brief The scriptlog.txt file. +inline Log script{ .filename = std::filesystem::path{ "logs" } / "scriptlog.txt" }; + +/// @brief The networkdump.txt file. +inline Log networkdump{ .filename = std::filesystem::path{ "logs" } / "networkdump.txt", .mirrorToCli = false }; + +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Prints a message to the log file and console. +/// @tparam ...Args The types of the arguments to format. +/// @param log The log instance. +/// @param fmt The format string. +/// @param ...args The arguments to format. +template +void print(Log& log, std::string_view fmt, const Args&... args) +{ + std::lock_guard lock(log.mutex); + std::ostringstream text; + + // Add the section prefix. + if (log.atLineStart && log.indentLevel == 0 && !log.sectionPrefix.empty() && fmt.length() > 0 && fmt != "\n") + text << log.sectionPrefix; + + // Add the indentation whitespace. + uint8_t spaces = 0; + if (log.atLineStart && log.indentLevel != 0) + { + spaces = (log.indentSpaces * log.indentLevel); + if (!log.sectionPrefix.empty()) + spaces += log.sectionPrefix.length(); + text << std::string(spaces, ' '); + } + + // Output the message. + if constexpr(sizeof...(args) == 0) + text << fmt; + else + text << std::vformat(fmt, std::make_format_args(args...)); + + // Get the resultant string. + // If empty, don't log anything. + auto s = text.str(); + if (s.size() <= spaces) + return; + +#if __cpp_lib_chrono < 201907L + // Clang doesn't support timezones, so just use system_clock time (UTC) floored to seconds. + auto localtime = std::chrono::floor(std::chrono::system_clock::now()); +#else + // Get the current time, floored to seconds. + auto localtime = std::chrono::floor(std::chrono::current_zone()->to_local(std::chrono::system_clock::now())); +#endif + + // Output to file. + if (auto* logFile = log.getFile(); logFile && logFile->is_open()) + { + if (log.atLineStart && log.timestampFile == TimestampMode::Short) + *logFile << std::format(TimestampShort, localtime) << ' '; + else if (log.atLineStart && log.timestampFile == TimestampMode::Long) + *logFile << std::format(TimestampLong, localtime) << ' '; + + *logFile << s; + logFile->flush(); + } + + // Output to console. + if (log.mirrorToCli) + { + if (log.atLineStart && log.timestampCli == TimestampMode::Short) + std::cout << std::format(TimestampShort, localtime) << ' '; + else if (log.atLineStart && log.timestampCli == TimestampMode::Long) + std::cout << std::format(TimestampLong, localtime) << ' '; + std::cout << s << std::flush; + } + + log.atLineStart = s.back() == '\n'; +} + +/// @brief Prints a message to the log file and console and terminates the line. +/// @tparam ...Args The types of the arguments to format. +/// @param log The log instance. +/// @param fmt The format string. +/// @param ...args The arguments to format. +template +void printLine(Log& log, std::string_view fmt, const Args&... args) +{ + std::lock_guard lock(log.mutex); + print(log, fmt, args...); + print(log, "\n"sv); +} + +/// @brief Prints a message to the log file preventing trailing newlines from starting a new line. +/// @tparam ...Args The types of the arguments to format. +/// @param log The log instance. +/// @param fmt The format string. +/// @param ...args The arguments to format. +template +void printBlock(Log& log, std::string_view fmt, const Args&... args) +{ + std::lock_guard lock(log.mutex); + print(log, fmt, args...); + log.atLineStart = false; +} + +/// @brief Batches multiple log entries together to keep entries together when multiple threads are logging at the same time. +/// @param log The log instance. +/// @param range A range of pairs, where each pair consists of an indentation level and a log message. Each message will be printed with the specified indentation level. +void batch(Log& log, RangeOf> auto const& range) +{ + std::lock_guard lock(log.mutex); + for (auto& [indentation, text] : range) + { + auto indent = log.indent(indentation); + printLine(log, text); + } +} + +/// @brief Batches multiple log entries together to keep entries together when multiple threads are logging at the same time. +/// @param log The log instance. +/// @param range A range of pairs, where each pair consists of an indentation level and a log message. Each message will be printed with the specified indentation level. +void batch(Log& log, RangeOf> auto&& range) +{ + std::lock_guard lock(log.mutex); + for (auto& [indentation, text] : range) + { + auto indent = log.indent(indentation); + printLine(log, text); + } +} + +/// @brief Writes a message to the log if the condition is false, intended for debugging purposes where a failed assertion is not critical enough to throw an exception or terminate the program, but should still be logged for investigation. +/// @param condition The condition to check. +/// @param log The log instance. +/// @param message The message to log if the assertion fails. +/// @param location The source location of the assertion. Defaults to the current source location. +inline void debug_assert(bool condition, Log& log, std::string_view message, const std::source_location location = std::source_location::current()) +{ + if (!condition) + printLine(log, "[WARN][ASSERT] {} ({}:{})", message, location.file_name(), location.line()); +} + +/// @brief Writes a message to the log if the condition is false, intended for debugging purposes where a failed assertion is not critical enough to throw an exception or terminate the program, but should still be logged for investigation. Writes to log::server. +/// @param condition The condition to check. +/// @param message The message to log if the assertion fails. +/// @param location The source location of the assertion. Defaults to the current source location. +inline void debug_assert(bool condition, std::string_view message, const std::source_location location = std::source_location::current()) +{ + if (!condition) + printLine(log::server, "[WARN][ASSERT] {} ({}:{})", message, location.file_name(), location.line()); +} + +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Profiles the duration of a scope and logs it when the scope ends. +struct Profile +{ + /// @brief Constructs a Profile object that measures execution time. + /// @param log A reference to the Log object used for output. + /// @param message A description of the operation being profiled. + /// @param format The format string for the profiling output. + Profile(Log& log, std::string_view message, std::string_view format = "[Profile] {} took {:0.6} ms.") + : m_log(log), m_message(message), m_format(format), m_start(precise_clock::now()) + {} + + ~Profile() noexcept + { + auto end = precise_clock::now(); + auto duration_ns = duration_nano_double(end - m_start); + auto duration_ms = std::chrono::duration_cast(duration_ns); + printLine(m_log, m_format, m_message, duration_ms.count()); + } + + Log& m_log; + std::string m_message; + std::string m_format; + precise_clock::time_point m_start; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::log +/////////////////////////////////////////////////////////////////////////////// + +template <> +struct std::formatter : std::formatter +{ + auto format(const CString& str, std::format_context& ctx) const + { + return std::format_to(ctx.out(), "{}", std::string_view{ str.text(), static_cast(str.length()) }); + } +}; + +#endif // LOG_H diff --git a/server/include/utilities/PropertySerializers.h b/server/include/utilities/PropertySerializers.h new file mode 100644 index 000000000..226bb2358 --- /dev/null +++ b/server/include/utilities/PropertySerializers.h @@ -0,0 +1,796 @@ +#ifndef PROPSCONTAINER_H +#define PROPSCONTAINER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace preagonal +{ +class Player; +} + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal::props +{ +//////////////////////////////////////////////////////////////////////////////// + +using GBYTE1 = uint8_t; +using GBYTE2 = uint16_t; +using GBYTE3 = uint32_t; +using GBYTE5 = long long int; +using GBYTE1_signed = int8_t; +using GBYTE2_signed = int16_t; +using GBYTE3_signed = int32_t; + +////////////////////////////////////////////////// +// Helper Functions +////////////////////////////////////////////////// + +/// @brief Helper function to avoid #include'ing Server.h in this file. +/// @return ServerGeneration in integer format. +int getServerGeneration(); + +////////////////////////////////////////////////// +// SetBy and SetResults +////////////////////////////////////////////////// + +/// @brief Used to control how properties are forwarded to clients. +/// +/// A property that is set by the server is generally forwarded to the client, while a prop that is set by the client is not sent back. +enum class SetBy +{ + CLIENT, + SERVER +}; + +/// @brief Contains the results of setting a property. +struct SetResults +{ + using ResultFlagType = std::bitset<5>; + + /// @brief The ID of the property that was set. + uint8_t propId = 0; + + /// @brief The additional props to send back out as the result of setting this prop. + std::inplace_vector resultPropIds{}; + + /// @brief The results of the prop set. + ResultFlagType resultFlags{}; + + /// @brief Result Flag - Pass the prop changes to everybody. + static const size_t sendToAll = 0; + + /// @brief Result Flag - Pass the prop changes to the level. + static const size_t sendToLevel = 1; + + /// @brief Result Flag - Pass the prop changes back to the source. + static const size_t sendToSource = 2; + + /// @brief Result Flag - If this prop is being sent, a fresh copy should be acquired. + static const size_t getLatestOnSend = 3; + + /// @brief Result Flag - If true, the prop was invalid, so we should stop processing more props. + static const size_t wasInvalid = 4; +}; + +////////////////////////////////////////////////// +// Applying Limits +////////////////////////////////////////////////// + +/// @brief Apply length or value limits to a property value. +struct Limits +{ + static constexpr uint8_t HeadImageLength = 123; + static constexpr uint8_t BodyImageLength = 223; + static constexpr uint8_t SwordImageLength = 223; + static constexpr uint8_t ShieldImageLength = 223; + static constexpr uint8_t HorseImageLength = 119; + static constexpr uint8_t GaniLength = 223; + static constexpr uint8_t ChatMessageLength = 223; + static constexpr uint8_t MaxHitpoints = 20; + static constexpr uint8_t MaxArrows = 99; + static constexpr uint8_t MaxBombs = 99; + static constexpr uint8_t MaxMP = 100; + static constexpr uint8_t MaxAP = 100; + + /// @brief Sword, battleaxe, lizardsword, goldensword. + static constexpr uint8_t MaxSwordPower = 20; + + /// @brief Shield, mirrorshield, lizardshield. + static constexpr uint8_t MaxShieldPower = 3; + + /// @brief None?, ?, glove1, glove2. + static constexpr uint8_t MaxGlovePower = 3; + + /// @brief Bomb, joltbomb, superbomb. + static constexpr uint8_t MaxBombPower = 3; + + /// @brief Bow, fireball, fireblast, nukeshot. + static constexpr uint8_t MaxBowPower = 4; + + /// @brief Applies a limit to a value, clamping it between min and max. + /// @param value The input value to limit. + /// @param min The lower bounds of the limit. + /// @param max The upper bounds of the limit. + /// @return The clamped value. + static auto apply(std::integral auto value, std::integral auto min, std::integral auto max) -> decltype(value) + { + using T = decltype(value); + return std::clamp(value, static_cast(min), static_cast(max)); + } + + /// @brief Clamps an integral value to the range [0, max]. + /// @param value The integral value to clamp. + /// @param max The upper bound for the value. + /// @return The value clamped to the range between 0 and max (inclusive). + static auto apply(std::integral auto value, std::integral auto max) -> decltype(value) + { + using T = decltype(value); + return std::clamp(value, static_cast(0), static_cast(max)); + } + + /// @brief Truncates a string view to a specified maximum length. + /// @param value The input string view to be truncated if necessary. + /// @param maxLength The maximum allowed length for the returned string view. + /// @return A string view containing at most maxLength characters from the input. + static auto apply(std::string_view value, size_t maxLength) + { + if (value.length() > maxLength) + return value.substr(0, maxLength); + return value; + } + + /// @brief Applies the maximum hitpoints value (as determined by the server options) and returns the result. + /// @param maxHitpoints The maximum hitpoints value to apply. + /// @return The applied maximum hitpoints value, clamped to the maximum allowed. + static uint8_t applyMaxHitpoints(uint8_t maxHitpoints); + + /// @brief Applies the maximum sword power value (as determined by the server options) and returns the result. + /// @param swordPower The sword power value to apply. + /// @return The applied sword power value, clamped to the maximum allowed. + static int8_t applySwordPower(int8_t swordPower); + + /// @brief Applies the maximum shield power value (as determined by the server options) and returns the result. + /// @param shieldPower The shield power value to apply. + /// @return The applied shield power value, clamped to the maximum allowed. + static uint8_t applyShieldPower(uint8_t shieldPower); +}; + +////////////////////////////////////////////////// +// Property Containers +////////////////////////////////////////////////// + +struct PropertyBase +{ + virtual CString serialize() const = 0; + virtual void deserialize(CString& data) = 0; + virtual void apply(const GameValue& gameValue) = 0; + virtual std::format_context::iterator format(std::format_context& ctx) const = 0; +}; + +/// @brief A property that does not hold data. +struct PropertyVoid : public PropertyBase +{ + PropertyVoid() = default; + + virtual CString serialize() const override + { + return CString{}; // No data to serialize. + } + + virtual void deserialize(CString& data) override + { + // No data to read, so do nothing. + } + + virtual void apply(const GameValue& gameValue) override + { + // No data to apply, so do nothing. + } + + virtual std::format_context::iterator format(std::format_context& ctx) const override + { + return std::format_to(ctx.out(), "(void)"); + } +}; + +struct PropertyUnsafeByte : public PropertyBase +{ + explicit PropertyUnsafeByte(uint8_t value = 0) : value(value) {} + + virtual CString serialize() const override + { + CString result; + result.writeGCharUnsafe(value); + return result; + } + + virtual void deserialize(CString& data) override + { + data.readGInto(value); + } + + virtual void apply(const GameValue& gameValue) override + { + value = static_cast(gameValue.get().value_or(0.0)); + } + + virtual std::format_context::iterator format(std::format_context& ctx) const override + { + return std::format_to(ctx.out(), "value: {}", value); + } + + uint8_t value; +}; + +/// @brief A property that is encoded as a packed numeric value. +/// @tparam T The type of the numeric value, which must be an integral type that CString can easily serialize (1, 2, 3, or 5 bytes). +template +struct PropertyNumeric : public PropertyBase +{ + explicit PropertyNumeric(T value = T{}) : value(value) {} + + virtual CString serialize() const override + { + return CString() >> static_cast(value); + } + + virtual void deserialize(CString& data) override + { + data.readGInto(value); + } + + virtual void apply(const GameValue& gameValue) override + { + value = static_cast(gameValue.get().value_or(0)); + } + + virtual std::format_context::iterator format(std::format_context& ctx) const override + { + return std::format_to(ctx.out(), "value: {}", value); + } + + T value; +}; + +/// @brief A property that is encoded as a packed string value (length-prefixed). +struct PropertyString : public PropertyBase +{ + PropertyString() = default; + PropertyString(const char* value) : value(value) {} + PropertyString(std::string_view value) : value(value) {} + PropertyString(const std::string& value) : value(value) {} + PropertyString(std::string&& value) noexcept : value(std::move(value)) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + std::string value; +}; + +/// @brief A property that is encoded as a packed string value (length-prefixed) where the length is 2 bytes. +struct PropertyLongString : public PropertyString +{ + using PropertyString::PropertyString; + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; +}; + +/// @brief A property that combines sword power and sword image. +struct PropertySwordPower : public PropertyBase +{ + PropertySwordPower() = default; + PropertySwordPower(int8_t power) : power(power) {} + PropertySwordPower(const std::string& image) : image(image) {} + PropertySwordPower(const std::string& image, int8_t power) : image(image), power(power) {} + PropertySwordPower(std::string&& image) noexcept : image(std::move(image)) {} + PropertySwordPower(std::string&& image, int8_t power) noexcept : image(std::move(image)), power(power) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + std::string image; + std::optional power; +}; + +/// @brief A property that combines shield power and shield image. +struct PropertyShieldPower : public PropertyBase +{ + PropertyShieldPower() = default; + PropertyShieldPower(uint8_t power) : power(power) {} + PropertyShieldPower(const std::string& image) : image(image) {} + PropertyShieldPower(const std::string& image, uint8_t power) : image(image), power(power) {} + PropertyShieldPower(std::string&& image) noexcept : image(std::move(image)) {} + PropertyShieldPower(std::string&& image, uint8_t power) noexcept : image(std::move(image)), power(power) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + std::string image; + std::optional power; +}; + +/// @brief A property that handles the BOWGIF / GANI properties, which changed with the 2.x clients. +struct PropertyGaniOrBowGif : public PropertyBase +{ + PropertyGaniOrBowGif() = default; + PropertyGaniOrBowGif(std::string_view gani) : gani(gani) {} + PropertyGaniOrBowGif(uint8_t bowPower) + : bowGif(std::make_pair(std::string{}, bowPower)) {} + PropertyGaniOrBowGif(uint8_t bowPower, std::string_view bowGif) + : bowGif(std::make_pair(std::string{ bowGif }, bowPower)) {} + PropertyGaniOrBowGif(std::string_view gani, uint8_t bowPower, std::string_view bowGif) + : gani(gani), bowGif(std::make_pair(std::string{ bowGif }, bowPower)) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + std::optional gani; + std::optional> bowGif; +}; + +/// @brief A property that handles the HEADGIF property. +struct PropertyHeadGif : public PropertyBase +{ + PropertyHeadGif() = default; + PropertyHeadGif(uint8_t preset) : image(preset) {} + PropertyHeadGif(const std::string& image) : image(image) {} + PropertyHeadGif(std::string&& image) noexcept : image(std::move(image)) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + std::variant image; +}; + +/// @brief A property that handles multiple sequential values of the same type. +/// +/// If `stopIfFirstIsZero` is true, the deserialization will stop early if the first value is zero. This handles the EFFECTCOLORS property. +/// @tparam T The type of the values in the array. +/// @tparam N The number of values in the array. +/// @tparam StopIfFirstZero If true, the deserialization will stop early if the first value is zero. +template +struct PropertyArray : public PropertyBase +{ + using ValueType = T; + + PropertyArray() = default; + PropertyArray(const std::array& input) : values(input) {} + PropertyArray(std::array&& input) noexcept : values(std::move(input)) {} + PropertyArray(std::ranges::input_range auto&& input) noexcept + { + std::ranges::copy(input | std::views::take(N), values.begin()); + } + + virtual CString serialize() const override + { + CString result; + for (size_t i = 0; i < N; ++i) + { + result >> (T)values[i]; + if constexpr (StopIfFirstZero) + { + // If the first value is zero and we should stop, break early. + if (i == 0 && values[i] == 0) + break; + } + } + return result; + } + + virtual void deserialize(CString& data) override + { + for (size_t i = 0; i < N; ++i) + { + if (static_cast(data.bytesLeft()) < sizeof(T)) + throw std::runtime_error("Not enough data to deserialize PropertyArray."); + data.readGInto(values[i]); + + if constexpr (StopIfFirstZero) + { + // If the first value is zero and we should stop, break early. + if (i == 0 && values[i] == 0) + break; + } + } + } + + virtual void apply(const GameValue& gameValue) override + { + if (gameValue.get>().has_value()) + { + auto* vec = gameValue.get_unsafe>(); + if (vec == nullptr) + return; + + // Convert all values to type T and insert into the values array. + for (size_t i = 0; i < N && i < vec->size(); ++i) + { + if constexpr (std::is_integral_v) + { + values[i] = static_cast((*vec)[i]); + } + else + { + values[i] = T((*vec)[i]); + } + } + } + } + + virtual std::format_context::iterator format(std::format_context& ctx) const override + { + std::ostringstream out; + for (size_t i = 0; i < N; ++i) + { + out << std::format("{}", values[i]); + if constexpr (StopIfFirstZero) + { + if (i == 0 && values[i] == 0) + break; + } + + if (i < N - 1) + out << ", "; + } + + return std::format_to(ctx.out(), "values: [{}]", out.str()); + } + + std::array values{}; +}; + +/// @brief A property that stores an elo rating and its deviation. +struct PropertyEloRating : public PropertyBase +{ + PropertyEloRating() = default; + PropertyEloRating(float rating, float deviation) : rating(rating), deviation(deviation) {} + PropertyEloRating(uint32_t rating, uint32_t deviation) : rating(rating), deviation(deviation) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + float rating = 1500.0f; + float deviation = 350.0f; +}; + +/// @brief A property that stores an attachment to an NPC. +struct PropertyAttachNPC : public PropertyBase +{ + PropertyAttachNPC() = default; + PropertyAttachNPC(NPCID npcId) : npcId(npcId) {} + PropertyAttachNPC(NPCID npcId, uint8_t type) : type(type), npcId(npcId) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + uint8_t type = 0; + NPCID npcId = 0; +}; + +/// @brief A property that stores a pixel position. +struct PropertyPixelCoordinate : public PropertyBase +{ + PropertyPixelCoordinate() = default; + PropertyPixelCoordinate(int16_t pixelCoordinate) : pixelCoordinate(pixelCoordinate) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + int16_t pixelCoordinate = 0; +}; + +/// @brief A property that serializes a coordinate in the old style. +struct PropertyTileCoordinate : public PropertyBase +{ + PropertyTileCoordinate() = default; + PropertyTileCoordinate(int16_t pixelCoordinate) : pixelCoordinate(pixelCoordinate) {} + PropertyTileCoordinate(float tileCoordinate) : pixelCoordinate(static_cast(tileCoordinate * 16)) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + int16_t pixelCoordinate = 0; +}; + +/// @brief A property that serializes a Z coordinate in the old style (offset of 50). +struct PropertyTileCoordinateZ : public PropertyBase +{ + PropertyTileCoordinateZ() = default; + PropertyTileCoordinateZ(int16_t pixelCoordinate) : pixelCoordinate(pixelCoordinate) {} + PropertyTileCoordinateZ(float tileCoordinate) : pixelCoordinate(static_cast(tileCoordinate * 16)) {} + PropertyTileCoordinateZ(double tileCoordinate) : pixelCoordinate(static_cast(tileCoordinate * 16)) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + int16_t pixelCoordinate = 0; +}; + +/// @brief A property that stores an old GS1 script. +struct PropertyGS1Script : public PropertyBase +{ + PropertyGS1Script() = default; + PropertyGS1Script(std::string_view script) : script(script) {} + PropertyGS1Script(const std::string& script) : script(script) {} + PropertyGS1Script(std::string&& script) noexcept : script(std::move(script)) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + std::string script; +}; + +/// @brief A property that stores a hurt direction (dx, dy) for an NPC. +struct PropertyHurtDxDy : public PropertyBase +{ + PropertyHurtDxDy() = default; + + /// @brief Displacement from -1.0 to 1.0 (-9 tiles to 9 tiles). + explicit PropertyHurtDxDy(float dx, float dy); + + /// @brief Displacement from -32 to 32 (-9 tiles to 9 tiles). + explicit PropertyHurtDxDy(int8_t dx, int8_t dy); + + /// @brief Displacement in tiles from -9.0 to 9.0. + explicit PropertyHurtDxDy(const Position& displacement); + + /// @brief Displacement in pixel tiles from -144 to 144 (-9 tiles to 9 tiles). + explicit PropertyHurtDxDy(const Position& displacement); + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + std::pair getAsTiles() const; + + int8_t hurtDX = 0; + int8_t hurtDY = 0; +}; + +/// @brief A property that stores a rectangle for an image part. +struct PropertyImagePart : public PropertyBase +{ + PropertyImagePart() = default; + PropertyImagePart(uint16_t x, uint16_t y, uint8_t width, uint8_t height) + : imagePart({ x, y }, { width, height }) {} + PropertyImagePart(const ImagePartRectangle& imagePart) : imagePart(imagePart) {} + PropertyImagePart(ImagePartRectangle&& imagePart) noexcept : imagePart(std::move(imagePart)) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + ImagePartRectangle imagePart; +}; + +/// @brief A property that stores a sprite and its direction. +struct PropertySprite : public PropertyBase +{ + PropertySprite() = default; + PropertySprite(uint8_t sprite); + PropertySprite(uint8_t sprite, uint8_t direction) : sprite(sprite), direction(direction) {} + + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + + uint8_t sprite = 0; + uint8_t direction = 2; +}; + +/// @brief A property that stores an array of colors. +struct PropertyColors : public PropertyArray +{ + PropertyColors() = default; + PropertyColors(const std::array& input) : PropertyArray(input) {} + PropertyColors(std::array&& input) noexcept : PropertyArray(std::move(input)) {} + PropertyColors(std::ranges::input_range auto&& input) noexcept : PropertyArray(std::move(input)) {} + virtual CString serialize() const override; + virtual void deserialize(CString& data) override; + virtual void apply(const GameValue& gameValue) override; + virtual std::format_context::iterator format(std::format_context& ctx) const override; + int getColorCount() const noexcept; + size_t getMaxColorValue() const noexcept; +}; + +// Renames these properties so they can be used inside the X-macro. +using PropertyEffectColors = PropertyArray; + +////////////////////////////////////////////////// +// Concepts +////////////////////////////////////////////////// + +template +concept PropertyContainer = requires(T t, CString& c) +{ + std::is_base_of_v; + { t.serialize() } -> std::convertible_to; + t.deserialize(c); +}; + +////////////////////////////////////////////////// +// Sending Results +////////////////////////////////////////////////// + +using PropertySendResults = std::vector>>; +using PropertyContainerGetter = std::function(uint8_t, SetResults::ResultFlagType&)>; + +void collectPacketsFromResults(const PropertySendResults& results, CString& outAll, CString& outLevel, CString& outSource, PropertyContainerGetter getProp); + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::props + +////////////////////////////////////////////////// +// Printing +////////////////////////////////////////////////// + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyBase* prop, std::format_context& ctx) const { return prop->format(ctx); } +}; + +template <> +struct std::formatter> : std::formatter +{ + auto format(const std::shared_ptr& prop, std::format_context& ctx) const { return prop->format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyVoid& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template +struct std::formatter> : std::formatter +{ + auto format(const preagonal::props::PropertyNumeric& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyString& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyLongString& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertySwordPower& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyShieldPower& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyGaniOrBowGif& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyHeadGif& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template +struct std::formatter> : std::formatter +{ + auto format(const preagonal::props::PropertyArray& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyEloRating& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyAttachNPC& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyPixelCoordinate& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyTileCoordinate& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyTileCoordinateZ& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyGS1Script& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyHurtDxDy& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertyImagePart& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +template <> +struct std::formatter : std::formatter +{ + auto format(const preagonal::props::PropertySprite& prop, std::format_context& ctx) const { return prop.format(ctx); } +}; + +#endif // PROPSCONTAINER_H diff --git a/server/include/utilities/Random.h b/server/include/utilities/Random.h new file mode 100644 index 000000000..336017977 --- /dev/null +++ b/server/include/utilities/Random.h @@ -0,0 +1,79 @@ +#ifndef RANDOM_H +#define RANDOM_H + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class DelphiRandomDeviceIntegral +{ +public: + using result_type = uint32_t; + +public: + DelphiRandomDeviceIntegral() : m_seed(1) {} + DelphiRandomDeviceIntegral(result_type seed) : m_seed(seed) {} + + static constexpr result_type min() + { + return m_min; + } + + static constexpr result_type max() + { + return m_max; + } + +public: + result_type operator()() + { + m_seed = (m_seed * 0x8088405 + 1) & 0xFFFFFFFF; + return m_min + (m_seed % (m_max - m_min)); + } + +private: + static constexpr result_type m_min = 0; + static constexpr result_type m_max = 0xFFFFFFFF; + result_type m_seed; +}; + +class DelphiRandomDeviceReal +{ +public: + using result_type = long double; + +public: + DelphiRandomDeviceReal() : m_seed(1) {} + DelphiRandomDeviceReal(result_type seed) : m_seed(seed) {} + + static constexpr result_type min() + { + return m_min; + } + + static constexpr result_type max() + { + return m_max; + } + +public: + result_type operator()() + { + m_seed = (m_seed * 0x8088405 + 1) & 0xFFFFFFFF; + return std::ldexp((long double)m_seed, -32); + } + +private: + static constexpr result_type m_min = 0.0; + static constexpr result_type m_max = 1.0; + uint32_t m_seed; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // RANDOM_H diff --git a/server/include/utilities/Settings.h b/server/include/utilities/Settings.h new file mode 100644 index 000000000..8b3ed4d06 --- /dev/null +++ b/server/include/utilities/Settings.h @@ -0,0 +1,353 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +//////////////////////////////////////////////////////////////////////////////// + +template +class SettingCache; + +//---------------------------- + +/// @brief Stores settings. +class Settings +{ +public: + /// @brief Event dispatch that passes the old value. The event is posted when a setting is updated. + using SettingEventDispatch = EventDispatcher<>; + +public: + Settings() = default; + ~Settings() + { + m_settings.clear(); + for (auto& [_, dispatch] : m_settingUpdateEvents) + dispatch.unsubscribeAll(); + } + +public: + /// @brief Loads settings from a file. + /// @param file The path to the file to load settings from. + void load(const std::filesystem::path& file); + + /// @brief Clears all settings and posts update events for all settings. + void clear() + { + m_settings.clear(); + for (auto& [_, dispatch] : m_settingUpdateEvents) + dispatch.post(); + } + +public: + /// @brief Tracks a setting cache. The setting cache will be updated when the setting is updated, and when this function is first called. + /// @tparam ...T The contained types of the setting caches to track. + /// @param ...cache The setting caches to track. + template + void track(SettingCache&... cache) + { + (trackOne(cache), ...); + } + +public: + /// @brief Checks if a setting exists. + /// @param key The key of the setting to check for. + /// @return True if the setting exists, false otherwise. + bool exists(std::string_view key) const + { + return m_settings.contains(key); + } + +public: + /// @brief Sets a value to a setting. + /// @tparam T The type of the value to set. + /// @param key The key of the setting to set. + /// @param value The value to set. + template + void set(std::string_view key, const T& value) + { + if constexpr (std::same_as || std::same_as) + m_settings.emplace(key, value); + else if constexpr (std::same_as) + m_settings.emplace(key, value ? "true" : "false"); + else + m_settings.emplace(key, std::to_string(value)); + } + +public: + /// @brief Gets the value of a setting. If there are multiple values for the same key, the last value will be returned. + /// @tparam T The type of the value to get. + /// @param key The key of the setting to get. + /// @return The value of the setting, or std::nullopt if the setting does not exist. + template + std::optional get(std::string_view key) const + { + static_assert(false, "Settings::get called with a type that isn't handled. Make sure to provide a template specialization for the type you want to get."); + } + + /// @brief Gets the value of a setting. If there are multiple values for the same key, the last value will be returned. + /// @tparam T The type of the value to get. + /// @param key The key of the setting to get. + /// @return The value of the setting, or std::nullopt if the setting does not exist. + template + std::optional get(std::string_view key) const + { + auto range = m_settings.equal_range(key); + + // Not found. + if (range.first == std::end(m_settings)) + return std::nullopt; + + // One element. + if (range.second == std::end(m_settings)) + return range.first->second; + + // Many elements. + auto& last = range.second; + --last; + return last->second; + } + + /// @brief Gets a list of values for a setting. The values will be split by commas. If there are multiple values for the same key, all values will be returned. + /// @tparam T The type of the container to return. + /// @param key The key of the setting to get. + /// @return The container with the values of the setting, or std::nullopt if the setting does not exist. + template + std::optional get(std::string_view key) const + { + using value_type = std::ranges::range_value_t; + + if (!exists(key)) + return std::nullopt; + + T result{}; + + for (auto item : getList(key)) + { + if constexpr (std::integral) + result.emplace_back(string::toNumber(item)); + else if constexpr (std::same_as) + result.emplace_back(string::toFloat(item)); + else if constexpr (std::same_as) + result.emplace_back(string::toDouble(item)); + else if constexpr (std::same_as) + result.emplace_back(string::equalsi(item, "true"sv) ? true : false); + else if constexpr (string::StringViewIshVariant) + result.emplace_back(item); + else + static_assert(false, "Settings::get called with a container that contains a data type that isn't handled."); + } + + return result; + } + + /// @brief Gets the value of a setting. If there are multiple values for the same key, the last value will be returned. + /// @tparam T The type of the value to get. + /// @param key The key of the setting to get. + /// @return The value of the setting, or std::nullopt if the setting does not exist. + template + std::optional get(std::string_view key) const + { + auto value = get(key); + if (!value) + return std::nullopt; + + if constexpr (std::same_as) + { + std::string_view str = value.value(); + if (string::equalsi(str, "true"sv)) + return true; + if (string::equalsi(str, "false"sv)) + return false; + return std::nullopt; + } + else + { + return static_cast(string::toNumber(value.value())); + } + } + + /// @brief Gets the value of a setting. If there are multiple values for the same key, the last value will be returned. + /// @tparam T The type of the value to get. + /// @param key The key of the setting to get. + /// @return The value of the setting, or std::nullopt if the setting does not exist. + template + std::optional get(std::string_view key) const + { + auto value = get(key); + if (!value) + return std::nullopt; + + if constexpr (std::same_as) + return string::toFloat(value.value()); + else if constexpr (std::same_as) + return string::toDouble(value.value()); + else + static_assert(false, "Settings::get called with a floating point type that isn't handled."); + } + + /// @brief Gets a list of values for a setting. The values will be split by commas. If there are multiple values for the same key, all values will be returned. + /// @param key The key of the setting to get. + /// @return A generator that yields the values of the setting. + std::generator getList(std::string_view key) const + { + auto range = m_settings.equal_range(key); + if (range.first == std::end(m_settings)) + co_return; + + for (auto& it = range.first; it != range.second; ++it) + co_yield std::ranges::elements_of(string::split(it->second, ","sv, true)); + } + +private: + template + [[inline]] void trackOne(SettingCache& cache); + +private: + string_ordered_multimap m_settings; + string_map m_settingUpdateEvents; +}; + +//---------------------------- + +template +class SettingCache +{ +public: + friend class Settings; + +public: + /// @brief Creates a blank setting cache with the given key. + /// @param key The key of the setting to cache. + constexpr SettingCache(const std::string_view key) : key(key) {} + + /// @brief Creates a setting cache with the given key and default value. + /// @param key The key of the setting to cache. + /// @param defaultValue The default value of the setting. + constexpr SettingCache(const std::string_view key, const T& defaultValue) + : key(key), value(defaultValue) + { + onRequireDefaultValue = [this, defaultValue]() + { + value = defaultValue; + }; + } + + /// @brief Returns whether the setting cache has a value. + /// @return True if the setting cache has a value, false otherwise. + operator bool() const + { + return value.has_value(); + } + +public: + /// @brief Binds the setting cache to a settings instance. The setting cache will be updated when the setting is updated and the onUpdate handler will be called. + /// @param settings The settings instance to bind to. + void bind(Settings& settings) + { + settings.track(*this); + } + +public: + /// @brief Gets the value of the setting cache. + /// @return The value of the setting cache. + const std::optional& get() const + { + return value; + } + + /// @brief Gets the unwrapped value of the setting cache. If the value is std::nullopt, the behavior is undefined. Will throw if a default value was not given. + /// @return The unwrapped value of the setting cache. + const T& getValue() const + { + assert(onRequireDefaultValue); + return value.value(); + } + +public: + /// @brief The key of the setting to cache. + std::string key; + + /// @brief The value of the setting cache. + std::optional value = std::nullopt; + + /// @brief An event handler that is called when the setting is updated. The handler takes the new value and the old value as parameters. + std::function&, const std::optional&)> onUpdate; + +protected: + SettingCache() = delete; + + std::function onRequireDefaultValue; + + void update(std::optional&& newValue, const std::optional& oldValue) + { + if (onRequireDefaultValue && !newValue.has_value()) + { + onRequireDefaultValue(); + if (onUpdate) + onUpdate(value, oldValue); + } + else if (newValue != oldValue) + { + value = std::move(newValue); + if (onUpdate) + onUpdate(value, oldValue); + } + } + +protected: + EventHandle m_updateHandle; +}; + +//---------------------------- + +template +inline void Settings::trackOne(SettingCache& cache) +{ + auto updateFunction = [&cache, this]() + { + std::optional oldValueT = std::move(cache.value); + cache.update(get(cache.key), oldValueT); + }; + + SettingEventDispatch& dispatch = m_settingUpdateEvents[cache.key]; + cache.m_updateHandle = dispatch.subscribe(updateFunction); + + // Now that we are tracking this cache, update it with the current value of the setting. + // We are usually calling this function before we even load the settings, so if we have no settings yet, + // just call the update function with the existing value (most likely the default) so we aren't creating any unnecessary objects. + + if (m_settings.empty()) [[likely]] + { + if (cache.onUpdate) + cache.onUpdate(cache.value, std::nullopt); + } + else + { + updateFunction(); + } +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // SETTINGS_H diff --git a/server/include/utilities/StringUtils.h b/server/include/utilities/StringUtils.h index b9ba02b59..95f6bef1e 100644 --- a/server/include/utilities/StringUtils.h +++ b/server/include/utilities/StringUtils.h @@ -1,15 +1,1601 @@ -#ifndef UTILITIES_STRINGUTILS_H -#define UTILITIES_STRINGUTILS_H +#ifndef STRINGUTILS_H +#define STRINGUTILS_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include #include #include +#include + +using namespace std::literals::string_view_literals; + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::string +{ +/////////////////////////////////////////////////////////////////////////////// + +// A concept that checks if a type is a string. +template +concept StringVariant = std::same_as, std::string> || std::same_as, std::wstring>; + +// A concept that checks if a type is a string_view. +template +concept StringViewVariant = std::same_as, std::string_view> || std::same_as, std::wstring_view>; + +// A concept that checks if a type is a string or string_view. +template +concept StringViewIshVariant = StringVariant || StringViewVariant; + +/* +// A concept that checks if a type is a string or string_view. +template +concept StringViewVariantUnicode = StringViewIshVariant || std::same_as, std::u8string> || std::same_as, std::u8string_view>; + +// A concept that checks if a type is a string or string_view. +template +concept StringViewVariantNotUnicode = StringViewIshVariant && !StringViewVariantUnicode; +*/ + +// A concept that checks if a type is a pointer to a const char string (e.g. const char[], const char[N], const char*). +template +concept PointerToConstCharString = std::is_bounded_array_v> && std::is_same_v>, char>; + +// A concept that checks if a type is an input range, but not a string. +template +concept InputRangeNotString = std::ranges::input_range && !StringViewIshVariant && !PointerToConstCharString; + +template +concept NotInputRangeNotString = !InputRangeNotString; + +/////////////////////////////////////////////////////////////////////////////// + +/// A hash function for strings that can be used with heterogeneous lookups. +struct string_hash +{ + using hash_type = std::hash; + using is_transparent = void; + + [[nodiscard]] size_t operator()(const char* str) const noexcept + { + return hash_type{}(str); + } + [[nodiscard]] size_t operator()(const std::string_view& str) const noexcept + { + return hash_type{}(str); + } + [[nodiscard]] size_t operator()(const std::string& str) const noexcept + { + return hash_type{}(str); + } + [[nodiscard]] size_t operator()(const std::u8string_view& str) const noexcept + { + std::hash hasher{}; + return hasher(str); + } + [[nodiscard]] size_t operator()(const std::u8string& str) const noexcept + { + std::hash hasher{}; + return hasher(str); + } + [[nodiscard]] size_t operator()(const CString& str) const noexcept + { + return hash_type{}(str.toStringView()); + } + [[nodiscard]] size_t operator()(const size_t& hash) const noexcept + { + return hash; + } +}; + +/// A comparator function for strings that can be used with heterogeneous lookups. +struct string_hash_equal +{ + using is_transparent = void; + [[nodiscard]] bool operator()(const std::string& lhs, const std::string& rhs) const noexcept + { + return lhs == rhs; + } + // + [[nodiscard]] bool operator()(const char* lhs, const std::string& rhs) const noexcept + { + return lhs == rhs; + } + [[nodiscard]] bool operator()(const std::string_view& lhs, const std::string& rhs) const noexcept + { + return lhs == rhs; + } + [[nodiscard]] bool operator()(const CString& lhs, const std::string& rhs) const noexcept + { + return lhs == rhs; + } + [[nodiscard]] bool operator()(const size_t& lhs, const std::string& rhs) const noexcept + { + return lhs == string_hash{}(rhs); + } + // + [[nodiscard]] bool operator()(const std::string& lhs, const char* rhs) const noexcept + { + return lhs == rhs; + } + [[nodiscard]] bool operator()(const std::string& lhs, const std::string_view& rhs) const noexcept + { + return lhs == rhs; + } + [[nodiscard]] bool operator()(const std::string& lhs, const CString& rhs) const noexcept + { + return lhs == rhs; + } + [[nodiscard]] bool operator()(const std::string& lhs, const size_t& rhs) const noexcept + { + return string_hash{}(lhs) == rhs; + } +}; + +/// A comparator function for strings that can be used with heterogeneous lookups. +struct u8string_hash_equal +{ + using is_transparent = void; + [[nodiscard]] bool operator()(const std::u8string& lhs, const std::u8string& rhs) const noexcept + { + return lhs == rhs; + } + // + [[nodiscard]] bool operator()(const char8_t* lhs, const std::u8string& rhs) const noexcept + { + return lhs == rhs; + } + [[nodiscard]] bool operator()(const std::u8string_view& lhs, const std::u8string& rhs) const noexcept + { + return lhs == rhs; + } + [[nodiscard]] bool operator()(const CString& lhs, const std::u8string& rhs) const noexcept + { + auto sv = lhs.toStringView(); + std::u8string_view view{ reinterpret_cast(sv.data()), sv.length() }; + return view == rhs; + } + [[nodiscard]] bool operator()(const size_t& lhs, const std::u8string& rhs) const noexcept + { + return lhs == string_hash{}(rhs); + } + // + [[nodiscard]] bool operator()(const std::u8string& lhs, const char8_t* rhs) const noexcept + { + return lhs == rhs; + } + [[nodiscard]] bool operator()(const std::u8string& lhs, const std::u8string_view& rhs) const noexcept + { + return lhs == rhs; + } + [[nodiscard]] bool operator()(const std::u8string& lhs, const CString& rhs) const noexcept + { + auto sv = rhs.toStringView(); + std::u8string_view view{ reinterpret_cast(sv.data()), sv.length() }; + return lhs == view; + } + [[nodiscard]] bool operator()(const std::u8string& lhs, const size_t& rhs) const noexcept + { + return string_hash{}(lhs) == rhs; + } +}; + +/// A comparator function for hashes that can be used with heterogeneous lookups. +struct hash_string_equal +{ + using is_transparent = void; + [[nodiscard]] bool operator()(const size_t& lhs, const size_t& rhs) const noexcept + { + return lhs == rhs; + } + // + [[nodiscard]] bool operator()(const char* lhs, const size_t& rhs) const noexcept + { + return string_hash{}(lhs) == rhs; + } + [[nodiscard]] bool operator()(const std::string_view& lhs, const size_t& rhs) const noexcept + { + return string_hash{}(lhs) == rhs; + } + [[nodiscard]] bool operator()(const std::string& lhs, const size_t& rhs) const noexcept + { + return string_hash{}(lhs) == rhs; + } + [[nodiscard]] bool operator()(const CString& lhs, const size_t& rhs) const noexcept + { + return string_hash{}(lhs) == rhs; + } + // + [[nodiscard]] bool operator()(const size_t& lhs, const char* rhs) const noexcept + { + return lhs == string_hash{}(rhs); + } + [[nodiscard]] bool operator()(const size_t& lhs, const std::string_view& rhs) const noexcept + { + return lhs == string_hash{}(rhs); + } + [[nodiscard]] bool operator()(const size_t& lhs, const std::string& rhs) const noexcept + { + return lhs == string_hash{}(rhs); + } + [[nodiscard]] bool operator()(const size_t& lhs, const CString& rhs) const noexcept + { + return lhs == string_hash{}(rhs); + } +}; + +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Trims whitespace from the start of a string. +/// @param str A string or string_view to trim. +/// @return A string_view to the trimmed string. +std::string_view trimLeft(StringViewIshVariant auto const& str) +{ + std::string_view view{ str }; + auto size = str.size(); + for (size_t i = 0; i < size; ++i) + { + auto ch = view[i]; + if (!std::isspace(static_cast(ch)) && ch != '\xa7') + return view.substr(i, size - i); + } + return {}; +} + +/// @brief Trims whitespace from the end of a string. +/// @param str A string or string_view to trim. +/// @return A string_view to the trimmed string. +std::string_view trimRight(StringViewIshVariant auto const& str) +{ + std::string_view view{ str }; + for (size_t i = view.size(); i > 0; --i) + { + auto ch = view[i - 1]; + if (!std::isspace(static_cast(ch)) && ch != '\xa7') + return view.substr(0, i); + } + return {}; +} + +/// @brief Trims newlines (\\n and \\r) from the end of a string. +/// @param str A string or string_view to trim. +/// @return A string_view to the trimmed string. +std::string_view trimNewlines(StringViewIshVariant auto const& str) +{ + std::string_view view{ str }; + for (size_t i = view.size(); i > 0; --i) + { + auto ch = view[i - 1]; + if (ch != '\n' && ch != '\r' && ch != '\xa7') + return view.substr(0, i); + } + return {}; +} + +/// @brief Trims whitespace from the start and end of a string. +/// @param str A string or string_view to trim. +/// @return A string_view to the trimmed string. +std::string_view trim(StringViewIshVariant auto const& str) +{ + return trimLeft(trimRight(str)); +} + +/// @brief Trims whitespace from the start of a string, mutating it. +/// @param str A string to trim. +/// @return A reference to the trimmed string. +inline std::string& trimLeftMutate(std::string& str) +{ + if (str.empty()) return str; + + // Find first non-space. + const auto p = str.c_str(); + size_t idx = 0; + while (idx < str.length() && (std::isspace(int(p[idx])) || p[idx] == '\r' || p[idx] == '\n' || p[idx] == '\xa7')) + ++idx; + + // No whitespace. + if (idx == 0) + return str; + + // All whitespace. + if (idx == str.length()) + { + str.clear(); + return str; + } + + str = std::move(std::string{ str.begin() + idx, str.begin() + str.length()}); + return str; +} + +/// @brief Trims whitespace from the end of a string, mutating it. +/// @param str A string to trim. +/// @return A reference to the trimmed string. +inline std::string& trimRightMutate(std::string& str) +{ + if (str.empty()) return str; + + // Find last non-space. + const auto p = str.c_str(); + size_t idx = str.length(); + while (idx > 0 && (std::isspace(int(p[idx - 1])) || p[idx - 1] == '\r' || p[idx - 1] == '\n' || p[idx - 1] == '\xa7')) + --idx; + + // No whitespace. + if (idx == str.length()) + return str; + + // All whitespace. + if (idx < 0) + { + str.clear(); + return str; + } + + str.resize(idx); + return str; +} + +/// @brief Trims newlines (\\n and \\r) from the end of a string, mutating it. +/// @param str A string to trim. +/// @return A reference to the trimmed string. +inline std::string& trimNewlinesMutate(std::string& str) +{ + if (str.empty()) return str; + + // Find last non-space. + const auto p = str.c_str(); + size_t idx = str.length(); + while (idx > 0 && (p[idx - 1] == '\n' || p[idx - 1] == '\r' || p[idx - 1] == '\xa7')) + --idx; + + // No whitespace. + if (idx == str.length()) + return str; + + // All whitespace. + if (idx < 0) + { + str.clear(); + return str; + } + + str.resize(idx); + return str; +} + +/// @brief Trims whitespace from the start and end of a string, mutating it. +/// @param str A string to trim. +/// @return A reference to the trimmed string. +inline std::string& trimMutate(std::string& str) +{ + if (str.empty()) return str; + + // Find first and last non-space. + const auto p = str.c_str(); + size_t front = 0, back = str.length(); + while (front < str.length() && (std::isspace(static_cast(static_cast(p[front]))) || p[front] == '\r' || p[front] == '\n' || p[front] == '\xa7')) + ++front; + while (front < back && (std::isspace(static_cast(static_cast(p[back - 1]))) || p[back - 1] == '\r' || p[back - 1] == '\n' || p[back - 1] == '\xa7')) + --back; + + // No whitespace. + if (front == 0 && back == str.length()) + return str; + + // All whitespace. + if (back <= front) + { + str.clear(); + return str; + } + + str = std::move(std::string{ str.begin() + front, str.begin() + back }); + return str; +} + +/// @brief Trims whitespace from the start of a string, mutating it. +/// @param str A string to trim. +/// @return A reference to the trimmed string. +inline std::string&& trimLeftMutate(std::string&& str) +{ + return std::move(trimLeftMutate(str)); +} + +/// @brief Trims whitespace from the end of a string, mutating it. +/// @param str A string to trim. +/// @return A reference to the trimmed string. +inline std::string&& trimRightMutate(std::string&& str) +{ + return std::move(trimRightMutate(str)); +} + +/// @brief Trims whitespace from the start and end of a string, mutating it. +/// @param str A string to trim. +/// @return A reference to the trimmed string. +inline std::string&& trimMutate(std::string&& str) +{ + return std::move(trimMutate(str)); +} + +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Replaces all occurrences of a substring within a string with another substring. +/// @param in The input string to process. +/// @param from The substring to search for and replace. +/// @param to The substring to replace each occurrence of 'from' with. +/// @return A new string with all occurrences of 'from' replaced by 'to'. +inline std::string replace(std::string_view in, std::string_view from, std::string_view to) +{ + if (from.empty()) + return std::string{ in }; + + std::string out; + out.reserve(in.size() + 16); // Reserve some extra space for the new string. + size_t pos = 0; + while (true) + { + const auto next_pos = in.find(from, pos); + if (next_pos == std::string::npos) + { + out.append(in.substr(pos)); + break; + } + out.append(in.substr(pos, next_pos - pos)); + out.append(to); + pos = next_pos + from.size(); + } + return out; +} + +/// @brief Replaces all occurrences of a substring with another substring in a given string, modifying the original string. +/// @param in The string to perform replacements on. This string will be modified in place. +/// @param from The substring to search for and replace. +/// @param to The substring to replace each occurrence of 'from' with. +/// @return A reference to the modified input string after all replacements have been made. +inline std::string& replaceMutate(std::string& in, std::string_view from, std::string_view to) +{ + if (from.empty()) + return in; + + size_t pos = 0; + while ((pos = in.find(from, pos)) != std::string::npos) + { + in.replace(pos, from.size(), to); + pos += to.size(); + } + return in; +} + +/// @brief Removes all occurrences of specified characters from a string. +/// @param in The input string to process. +/// @param chars A string containing the characters to remove from the input. +/// @return A new string with all characters from 'chars' removed from the input string. +inline std::string eraseChars(std::string_view in, std::string_view chars) +{ + if (chars.empty()) + return std::string{ in }; + + auto filtered = in | std::views::filter([&chars](char c) { + return chars.find(c) == std::string_view::npos; + }); + std::string result(std::ranges::begin(filtered), std::ranges::end(filtered)); + + return result; +} + +/// @brief Removes all occurrences of specified characters from a string, modifying the original string. +/// @param in The string to be modified by removing specified characters. +/// @param chars A string view containing the characters to remove from the input string. +/// @return A reference to the modified input string with the specified characters removed. +inline std::string& eraseCharsMutate(std::string& in, std::string_view chars) +{ + if (chars.empty()) + return in; + + auto erased = std::ranges::remove_if(in, [&chars](char c) { return chars.find(c) != std::string_view::npos; }); + in.erase(erased.begin(), erased.end()); + + return in; +} + +inline auto removeExtension(StringViewIshVariant auto const& str) +{ + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + + std::basic_string_view view{ str }; + auto pos = view.rfind('.'); + if (pos == std::string_view::npos) + return view; + return view.substr(0, pos); +} + +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Escapes quotes in a string using a CSV-like format. +/// @param str The input string or string_view to escape quotes in. +/// @return A new string with quotes escaped. +auto escapeQuotes(StringViewIshVariant auto const& str) +{ + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + + std::basic_string ret{}; + ret.reserve(str.size() * 1.5); + for (const auto& c: str) + { + switch (c) + { + case '\\': + ret += "\\\\"; + break; + case '\"': + ret += "\"\""; + break; + case '\'': + ret += "\'\'"; + break; + default: + ret += c; + break; + } + } + return ret; +} + +/// @brief Unescapes quotes in a string that were escaped using a CSV-like format. +/// @param str The input string or string_view to unescape quotes in. +/// @return A new string with quotes unescaped. +auto unescapeQuotes(StringViewIshVariant auto const& str) +{ + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + + // The shortest an escaped character can be is 2 characters. + if (str.size() < 2) + { + if constexpr (StringVariant) + return str; + else + return std::basic_string{ str }; + } + + std::basic_string ret{}; + ret.reserve(str.size()); + size_t i = 0; + for (; i < str.size() - 1; ++i) + { + // If the current character is not an escape character, add it to the result and go to the next. + if (str[i] != '\\') + { + ret += str[i]; + continue; + } + + // We had an escape character, so check the next character. + // If it is a valid escape character, add the unescaped character to the result. + // Otherwise, keep both the escape character and the next character. + switch (str[i + 1]) + { + case '\\': + ret += '\\'; + break; + case '\"': + ret += '\"'; + break; + case '\'': + ret += '\''; + break; + default: + ret += '\\'; + ret += str[i + 1]; + break; + } + + // We skipped the next character, so increment the index. + ++i; + } + + // Catch the last character. + if (i == str.size() - 1) + ret += str[i]; + + return ret; +} + +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Splits a string into tokens based on a list of delimiters and returns them as a generator of string views. +/// @param str The input string to split. Can be any type compatible with string view. +/// @param delims A string containing delimiter characters used to split the input. +/// @param ignoreEmpty If true, empty tokens are ignored; if false, empty tokens are included in the output. +/// @return A generator yielding each token as a std::string_view. +auto split(StringViewVariant auto const& str, StringViewVariant auto delims, bool ignoreEmpty) -> std::generator> +{ + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + using StringViewType = std::basic_string_view; + StringViewType strview{ str }; + + size_t start = 0, end = 0; + while (start < str.length()) + { + // Find the next delimiter. + end = strview.find_first_of(delims, start); + + // None found, so add the rest of the string. + if (end == StringViewType::npos) + { + co_yield strview.substr(start); + break; + } + + // Add the token to the vector. + if (end > start) + co_yield strview.substr(start, end - start); + else if (!ignoreEmpty) + co_yield StringViewType{}; + + // If the delim was \r and the next character is \n, include it in the delim. + if (strview[end] == '\r' && end + 1 < strview.length() && strview[end + 1] == '\n') + ++end; + + start = end + 1; + } +} + +/// @brief Splits a string into tokens separated by a delimiting string and returns them as a generator of string views. +/// @param str The input string to split. Can be any type compatible with string view. +/// @param delim A string used to split the input. +/// @param ignoreEmpty If true, empty tokens are ignored; if false, empty tokens are included in the output. +/// @return A generator yielding each token as a std::string_view. +auto splitByString(StringViewVariant auto const& str, StringViewVariant auto delim, bool ignoreEmpty) -> std::generator> +{ + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + using StringViewType = std::basic_string_view; + StringViewType strview{ str }; + + size_t start = 0, end = 0; + while (start < str.length()) + { + // Find the next delimiter. + end = strview.find(delim, start); + + // None found, so add the rest of the string. + if (end == StringViewType::npos) + { + co_yield strview.substr(start); + break; + } + + // Add the token to the vector. + if (end > start) + co_yield strview.substr(start, end - start); + else if (!ignoreEmpty) + co_yield StringViewType{}; + + start = end + delim.length(); + } +} + +/// @brief Splits a string into a vector of tokens based on specified delimiters. +/// @param str The input string to split. +/// @param delims A string containing delimiter characters used to split the input string. +/// @param ignoreEmpty If true, empty tokens are ignored; if false, empty tokens are included in the result. +/// @return A vector of strings containing the tokens extracted from the input string. +auto splitToVector(StringViewIshVariant auto const& str, StringViewIshVariant auto const& delims, bool ignoreEmpty) +{ + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + using StringType = std::basic_string; + using StringViewType = std::basic_string_view; + StringViewType strview{ str }; + StringViewType delimview{ delims }; + + std::vector tokens; + for (const auto& token : split(strview, delimview, ignoreEmpty)) + tokens.emplace_back(token); + + return tokens; +} + +/// @brief Splits a string into a vector of tokens based on specified delimiters. +/// @param str The input string to split. +/// @param delims A string containing delimiter characters used to split the input string. +/// @param ignoreEmpty If true, empty tokens are ignored; if false, empty tokens are included in the result. +/// @return A vector of strings containing the tokens extracted from the input string. +auto splitToVectorView(StringViewIshVariant auto const& str, StringViewIshVariant auto const& delims, bool ignoreEmpty) +{ + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + using StringViewType = std::basic_string_view; + StringViewType strview{ str }; + StringViewType delimview{ delims }; + + std::vector tokens; + for (const auto& token : split(strview, delimview, ignoreEmpty)) + tokens.emplace_back(token); + + return tokens; +} + +/// @brief Splits a string into a vector of substrings using a specified delimiter. +/// @param str The input string to be split. +/// @param delim The delimiter string used to split the input. +/// @param ignoreEmpty If true, empty substrings are ignored; otherwise, they are included in the result. +/// @return A vector containing the substrings resulting from splitting the input string by the delimiter. +auto splitToVectorByString(StringViewIshVariant auto const& str, StringViewIshVariant auto const& delim, bool ignoreEmpty) +{ + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + using StringType = std::basic_string; + using StringViewType = std::basic_string_view; + StringViewType strview{ str }; + StringViewType delimview{ delim }; + + std::vector tokens; + for (const auto& token : splitByString(strview, delimview, ignoreEmpty)) + tokens.emplace_back(token); + + return tokens; +} + +//---------------------------- + +/// @brief Splits a string into tokens based on whitespace and returns them as a generator of string views, ignoring empty tokens. +/// @param str The input string to split. Can be any type compatible with string view. +/// @return A generator yielding each token as a std::string_view. +auto split(StringViewVariant auto str) -> std::generator +{ + for (const auto& item : split(str, " \t\n\r"sv, true)) + co_yield item; +} + +/// @brief Splits a string into tokens based on a list of delimiters and returns them as a generator of string views, ignoring empty tokens. +/// @param str The input string to split. Can be any type compatible with string view. +/// @param delims A string containing delimiter characters used to split the input. Defaults to whitespace characters. +/// @return A generator yielding each token as a std::string_view. +auto split(StringViewVariant auto str, StringViewVariant auto delims) -> std::generator +{ + for (const auto& item : split(str, delims, true)) + co_yield item; +} + +/// @brief Splits a string into tokens separated by a delimiting string and returns them as a generator of string views, ignoring empty tokens. +/// @param str The input string to split. Can be any type compatible with string view. +/// @param delim A string used to split the input. +/// @return A generator yielding each token as a std::string_view. +auto splitByString(StringViewVariant auto str, StringViewVariant auto delim) -> std::generator +{ + for (const auto& item : splitByString(str, delim, true)) + co_yield item; +} + +/// @brief Splits a string into a vector of tokens based on whitespace, ignoring empty tokens. +/// @param str The input string to split. +/// @return A vector of strings containing the tokens extracted from the input string. +auto splitToVector(StringViewIshVariant auto const& str) +{ + return splitToVector(str, " \t\n\r"sv, true); +} + +/// @brief Splits a string into a vector of tokens based on specified delimiters, ignoring empty tokens. +/// @param str The input string to split. +/// @param delims A string containing delimiter characters used to split the input string. +/// @return A vector of strings containing the tokens extracted from the input string. +auto splitToVector(StringViewIshVariant auto const& str, StringViewIshVariant auto const& delims) +{ + return splitToVector(str, delims, true); +} + +/// @brief Splits a string into a vector of tokens based on whitespace, ignoring empty tokens. +/// @param str The input string to split. +/// @return A vector of strings containing the tokens extracted from the input string. +auto splitToVectorView(StringViewIshVariant auto const& str) +{ + return splitToVectorView(str, " \t\n\r"sv, true); +} + +/// @brief Splits a string into a vector of tokens based on specified delimiters, ignoring empty tokens. +/// @param str The input string to split. +/// @param delims A string containing delimiter characters used to split the input string. +/// @return A vector of strings containing the tokens extracted from the input string. +auto splitToVectorView(StringViewIshVariant auto const& str, StringViewIshVariant auto const& delims) +{ + return splitToVectorView(str, delims, true); +} + +/// @brief Splits a string into a vector of substrings using a specified delimiter, ignoring empty tokens. +/// @param str The input string to be split. +/// @param delim The delimiter string used to split the input. +/// @return A vector containing the substrings resulting from splitting the input string by the delimiter. +auto splitToVectorByString(StringViewIshVariant auto const& str, StringViewIshVariant auto const& delim) +{ + return splitToVectorByString(str, delim, true); +} + +//---------------------------- + +/// @brief Splits a string into tokens based on a list of delimiters and returns them as a generator of string views. +/// @param str The input string to split. Can be any type compatible with string view. +/// @param delims A string containing delimiter characters used to split the input. +/// @param ignoreEmpty If true, empty tokens are ignored; if false, empty tokens are included in the output. +/// @return A generator yielding each token as a std::string_view. +inline std::generator split(const std::string& str, std::string_view delims, bool ignoreEmpty = true) +{ + for (const auto& item : split(std::string_view{ str }, delims, ignoreEmpty)) + co_yield item; +} +inline std::generator split(const std::wstring& str, std::wstring_view delims, bool ignoreEmpty = true) +{ + for (const auto& item : split(std::wstring_view{ str }, delims, ignoreEmpty)) + co_yield item; +} + +/// @brief Splits a string into tokens separated by a delimiting string and returns them as a generator of string views, ignoring empty tokens. +/// @param str The input string to split. Can be any type compatible with string view. +/// @param delim A string used to split the input. +/// @param ignoreEmpty If true, empty tokens are ignored; if false, empty tokens are included in the output. +/// @return A generator yielding each token as a std::string_view. +inline std::generator splitByString(const std::string& str, std::string_view delim, bool ignoreEmpty = true) +{ + for (const auto& item : splitByString(std::string_view{ str }, delim, ignoreEmpty)) + co_yield item; +} + +//---------------------------- + +/// @brief Joins the elements of a range into a single string, separated by a specified delimiter. +/// @param range An input range containing elements to join. The elements must be streamable to std::ostringstream. +/// @param delim The delimiter string to insert between elements. Defaults to ','. +/// @return A string containing the joined elements of the range, separated by the specified delimiter. +std::string join(std::ranges::input_range auto&& range, std::string_view delim = ",") +{ + std::ostringstream oss; + auto it = std::ranges::begin(range); + if (it != std::ranges::end(range)) + { + oss << *it; + ++it; + } + for (; it != std::ranges::end(range); ++it) + oss << delim << *it; + return oss.str(); +} + +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Converts a range of strings to a single CSV-formatted string, quoting fields as needed. +/// @param range An input range of string-like elements to be converted to CSV format. +/// @param force_quoted If true, all fields will be quoted regardless of content. Defaults to false. +/// @return A std::string containing the CSV-formatted representation of the input range, with fields separated by commas and quoted as necessary. +auto toCSV(InputRangeNotString auto&& range, bool force_quoted = false) +{ + constexpr std::array complexChars = { '"', ',', '\\' }; + std::ostringstream oss; + + for (const auto& wordFromRange : range) + { + std::string_view word{ wordFromRange }; + + // Check if the word contains any complex characters. + bool complex = std::ranges::any_of(word, + [&complexChars](const auto& c) { return std::ranges::find(complexChars, c) != complexChars.end(); }); + + // Output the word. + if (!complex && !force_quoted) + { + oss << word << ','; + continue; + } + + // This was a complex word, so we need to certain characters. + // For some reason we were doubling the backslash. I can't remember if that was intentional or not. + oss << '"'; + for (const char& c: word) + { + oss << c; + if (c == '"' || c == '\\') + oss << c; + } + + // Add the separator. + oss << "\","; + } + + // Remove the last comma. + auto result = oss.str(); + if (!result.empty()) + result.pop_back(); + + return result; +} + +/// @brief Converts a range of strings to a single CSV-formatted string, quoting fields as needed. +/// @param range An input range of string-like elements to be converted to CSV format. +/// @param force_quoted If true, all fields will be quoted regardless of content. Defaults to false. +/// @return A std::string containing the CSV-formatted representation of the input range, with fields separated by commas and quoted as necessary. +auto toCSV(const InputRangeNotString auto& range, bool force_quoted = false) +{ + return toCSV(std::move(range), force_quoted); +} + +/// @brief Converts a string or string-like object into CSV format, splitting it by a specified delimiter. +/// @param str The input string or string-like object to be converted to CSV. +/// @param delim The character used to split the input string into fields. Defaults to newline ('\n'). +/// @param force_quoted If true, all fields will be quoted in the resulting CSV. Defaults to false. +/// @return A CSV-formatted string constructed from the split fields of the input. +auto toCSV(StringViewIshVariant auto const& str, std::string_view delim = "\n"sv, bool force_quoted = false) +{ + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + using StringViewType = std::basic_string_view; + StringViewType strview{ str }; + + auto s = splitByString(strview, delim); + return toCSV(s, force_quoted); +} + +/// @brief Parses a CSV-formatted string into a vector of strings, handling quoted fields and optional leading whitespace. +/// @param str The input string or string view containing CSV data to parse. +/// @param ignoreLeadingWhitespace If true, leading spaces and tabs before each field are ignored. Defaults to false. +/// @return A vector of strings, each representing a parsed field from the CSV input. +std::vector fromCSV(StringViewIshVariant auto const& str, bool ignoreLeadingWhitespace = false) +{ + std::vector tokens{}; + auto token = std::string{}; + + bool wordStart = true; + bool wordQuoted = false; + for (size_t i = 0; i < str.length(); ++i) + { + const auto& c = str[i]; + + // Ignore whitespace at the start. + if (ignoreLeadingWhitespace) + { + if (wordStart && (c == ' ' || c == '\t')) + continue; + } + + // Check for a quoted word. + if (wordStart == true && c == '"') + { + wordStart = false; + wordQuoted = true; + continue; + } + + // Check for an escaped character. + if (wordQuoted) + { + std::optional next; + if (i + 1 < str.length()) + next = str[i + 1]; + + // Escaped backslash. + if (c == '\\' && next.has_value() && next.value() == '\\') + { + token += '\\'; + ++i; + } + // Escaped quote. + else if (c == '"' && next.has_value() && next.value() == '"') + { + token += '"'; + ++i; + } + // Quote that isn't escaped. + else if (c == '"') + { + // We reached the end of the quoted word. + // Store the current word and reset for the next one. + wordStart = true; + wordQuoted = false; + tokens.push_back(token); + token.clear(); + + // Advance to the comma, ignoring anything after the closing quote. + // Text after an unescaped quote is invalid, so just skip it. + auto nextcomma = str.find(',', i + 1); + if (nextcomma == std::string::npos) + break; + i = nextcomma; + } + else + { + // Add the character as is. + token += c; + } + } + else + { + if (c == ',') + { + // We reached the end of the quoted word. + // Store the current word and reset for the next one. + wordStart = true; + wordQuoted = false; + tokens.push_back(token); + token.clear(); + } + else + { + // Add the current character to the token. + token += c; + wordStart = false; + } + } + } + + // Push the last token if it exists. + if (!token.empty()) + tokens.push_back(token); + + return tokens; +} + +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Performs a case-insensitive comparison of two string-like objects. +/// @param str1 The first string-like object to compare. +/// @param str2 The second string-like object to compare. +/// @return An integer less than, equal to, or greater than zero if str1 is found, respectively, to be less than, to match, or be greater than str2 in a case-insensitive comparison. +int comparei(StringViewIshVariant auto const& str1, StringViewIshVariant auto const& str2) +{ + auto it1 = str1.begin(); + auto it2 = str2.begin(); + while (it1 != str1.end() && it2 != str2.end()) + { + if (std::tolower(*it1) != std::tolower(*it2)) + return std::tolower(*it1) - std::tolower(*it2); + ++it1; + ++it2; + } + return str1.size() - str2.size(); +} + +/// @brief Performs a case-insensitive equality check of two string-like objects. +/// @param str1 The first string-like object to compare. +/// @param str2 The second string-like object to compare. +/// @return true if the strings are equal (case-insensitive), false otherwise. +bool equalsi(StringViewIshVariant auto const& str1, StringViewIshVariant auto const& str2) +{ + return comparei(std::forward(str1), std::forward(str2)) == 0; +} + +/// @brief Finds the first occurrence of a substring within a string, ignoring case, starting from a specified position. +/// @param str The string to search within. +/// @param substr The substring to search for. +/// @param pos The position in the string to start the search from. Defaults to 0. +/// @return The index of the first occurrence of the substring (case-insensitive) in the string after the specified position, or std::string::npos if not found. +size_t findi(StringViewIshVariant auto const& str, StringViewIshVariant auto const& substr, size_t pos = 0) +{ + if (pos >= str.size()) + return std::string::npos; + + auto it = std::search(str.begin() + pos, str.end(), substr.begin(), substr.end(), + [](auto a, auto b) + { + return std::tolower(a) == std::tolower(b); + }); + + if (it == str.end()) + return std::string::npos; + + return std::distance(str.begin(), it); +} + +/// @brief Checks whether a string begins with a given prefix using a case-insensitive comparison. +/// @param str The string to check. +/// @param prefix The prefix to test for. +/// @return true if str starts with prefix (case-insensitive), otherwise false. +bool starts_withi(StringViewIshVariant auto const& str, StringViewIshVariant auto const& prefix) +{ + return findi(str, prefix, 0) == 0; +} + +/// @brief Returns true if str ends with suffix, performing a case-insensitive comparison. +/// @param str The string to test. +/// @param suffix The suffix to check for. If suffix.size() > str.size(), the function returns false. +/// @return true if str ends with suffix when compared case-insensitively; otherwise false. +bool ends_withi(StringViewIshVariant auto const& str, StringViewIshVariant auto const& suffix) +{ + if (suffix.size() > str.size()) + return false; + + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + using StringViewType = std::basic_string_view; + StringViewType strview{ str }; + + strview = strview.substr(strview.size() - suffix.size()); + return equalsi(strview, suffix); +} + +/// @brief Returns true if the input string is empty or consists solely of whitespace characters, using the current C locale (not locale-aware). +/// @param str The input string or string view to check. +/// @return true if the string is empty or consists solely of whitespace characters; otherwise false. +bool empty_or_whitespace(StringViewIshVariant auto const& str) +{ + return str.empty() || std::ranges::all_of(str, [](char c) { return std::isspace(static_cast(c)); }); +} + +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Converts all characters in the input string to uppercase, using the current C locale (not locale-aware). +/// @param str The input string or string view to convert to uppercase. Accepts any type compatible with StringViewIshVariant. +/// @return A new string with all characters converted to uppercase, preserving the original string's character type and traits. +auto toUpper(StringViewIshVariant auto const& str) +{ + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + + std::basic_string ret{}; + ret.reserve(str.size()); + + auto r = std::transform([](const Elem& c) { return static_cast(std::toupper(static_cast(c))); }); + std::ranges::copy(str | r, std::back_inserter(ret)); + return ret; +} + +/// @brief Converts all characters in the input string to lowercase, using the current C locale (not locale-aware). +/// @param str The input string or string view to be converted to lowercase. Accepts any type compatible with StringViewIshVariant. +/// @return A new string with all characters from the input converted to lowercase, preserving the original character and traits types. +auto toLower(StringViewIshVariant auto const& str) +{ + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + + std::basic_string ret{}; + ret.reserve(str.size()); + + auto r = std::views::transform([](const Elem& c) { return static_cast(std::tolower(static_cast(c))); }); + std::ranges::copy(str | r, std::back_inserter(ret)); + return ret; +} + +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Checks if a string represents a valid integral number, allowing for an optional leading sign. +/// @param str The input string to check. +/// @return true if the string represents a valid integral number; otherwise, false. +bool isIntegral(StringViewIshVariant auto const& str) +{ + if (str.empty()) + return false; + + // Allow a leading + or - sign. + auto startPos = 0; + if (str[0] == '+' || str[0] == '-') + startPos = 1; + + // Check that all remaining characters are digits. + return str.find_first_not_of("0123456789"sv, startPos) == std::string_view::npos; +} + +/// @brief Checks if a string represents a valid floating-point number, allowing for an optional leading sign and at most one decimal point. +/// @param str The input string to check. +/// @return true if the string represents a valid floating-point number; otherwise, false. +bool isFloat(StringViewIshVariant auto const& str) +{ + if (str.empty()) + return false; + + // Allow a leading + or - sign. + auto startPos = 0; + if (str[0] == '+' || str[0] == '-') + startPos = 1; + + // Check each character to ensure it's a digit or a decimal point, and that there is at most one decimal point. + bool decimalPointSeen = false; + for (size_t i = startPos; i < str.size(); ++i) + { + const char c = str[i]; + if (c == '.') + { + if (decimalPointSeen) + return false; + + decimalPointSeen = true; + } + else if (!std::isdigit(static_cast(c))) + return false; + } + return true; +} + +/// @brief Attempts to convert a string to a number of the specified integral type. +/// @tparam T The integral type to convert the string to. Defaults to int32_t. +/// @param str The string to convert to a number. +/// @param result Reference to a variable where the converted number will be stored if the conversion succeeds. +/// @return true if the conversion was successful; false otherwise. +template +bool toNumber(std::string_view str, T& result) +{ + try + { + char* p_end = nullptr; + const long num = std::strtol(str.data(), &p_end, 10); + if (p_end == str.data()) + return false; + + result = num; + return true; + } + catch (...) + { + result = 0; + return false; + } +} + +/// @brief Converts a string to a number of the specified integral type. +/// @tparam T The integral type to convert the string to. Defaults to int32_t. +/// @param str The string to convert to a number. +/// @return The converted number if the conversion succeeds; otherwise, returns 0 of the specified type. +template +T toNumber(std::string_view str) +{ + if (T result{}; toNumber(str, result)) + return result; + + return static_cast(0); +} + +/// @brief Attempts to convert a string to a float value. +/// @param str The input string to convert to a float. +/// @param result Reference to a float variable where the converted value will be stored if the conversion succeeds. +/// @return true if the conversion was successful and the result is stored in 'result'; false otherwise. +inline bool toFloat(std::string_view str, float& result) +{ + try + { + char* p_end = nullptr; + const float num = std::strtof(str.data(), &p_end); + if (p_end == str.data()) + return false; + + result = num; + return true; + } + catch (...) + { + result = 0.0f; + return false; + } +} + +/// @brief Converts a string to a float value. +/// @param str The string to convert to a float. +/// @return The float value represented by the string, or 0.0f if the conversion fails. +inline float toFloat(std::string_view str) +{ + if (float result; toFloat(str, result)) + return result; + + return 0.0f; +} + +/// @brief Attempts to convert a string to a double-precision floating-point number. +/// @param str The input string to convert. +/// @param result Reference to a double where the converted value will be stored if the conversion succeeds. +/// @return true if the conversion was successful and the result is stored in 'result'; false otherwise. +inline bool toDouble(std::string_view str, double& result) +{ + try + { + char* p_end = nullptr; + const double num = std::strtod(str.data(), &p_end); + if (p_end == str.data()) + return false; + + result = num; + return true; + } + catch (...) + { + result = 0.0; + return false; + } +} + +/// @brief Converts a string to a double-precision floating-point number. +/// @param str The string to convert to a double. +/// @return The converted double value if the conversion is successful; otherwise, returns 0.0. +inline double toDouble(std::string_view str) +{ + if (double result; toDouble(str, result)) + return result; + + return 0.0f; +} + +/// @brief Converts a string to its Base64 encoded representation. +/// @param str The string-like object to be encoded. +/// @return A Base64 encoded string. +inline std::string toBase64(std::span in) +{ + static constexpr std::array encodingTable = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '+', '/' + }; + + // Calculate the lengths of the input and output. + size_t in_len = in.size(); + size_t out_len = 4 * ((in_len + 2) / 3); + + // Create the output buffer. + std::string out(out_len, '\0'); + auto p = out.data(); + + // Encode the input string. + size_t i; + for (i = 0; i < in_len - 2; i += 3) + { + *p++ = encodingTable[(in[i] >> 2) & 0x3F]; + *p++ = encodingTable[(static_cast(in[i] & 0x3) << 4) | (static_cast(in[i + 1] & 0xF0) >> 4)]; + *p++ = encodingTable[(static_cast(in[i + 1] & 0xF) << 2) | (static_cast(in[i + 2] & 0xC0) >> 6)]; + *p++ = encodingTable[static_cast(in[i + 2] & 0x3F)]; + } + + // Handle padding for remaining bytes. + if (i < in_len) + { + *p++ = encodingTable[(in[i] >> 2) & 0x3F]; + if (i == (in_len - 1)) + { + *p++ = encodingTable[(static_cast(in[i] & 0x3) << 4)]; + *p++ = '='; + } + else + { + *p++ = encodingTable[(static_cast(in[i] & 0x3) << 4) | (static_cast(in[i + 1] & 0xF0) >> 4)]; + *p++ = encodingTable[(static_cast(in[i + 1] & 0xF) << 2)]; + } + *p++ = '='; + } + + return out; +} + +/// @brief Converts a Base64 encoded string back to its original representation. +/// @param str The Base64 encoded string-like object to be decoded. +/// @return A string containing the decoded data. If the input is not valid Base64, returns the input as is. +inline std::vector fromBase64(StringViewIshVariant auto const& str) +{ + static constexpr std::array decodingTable = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, + 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64, + 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 + }; + + // Check if our input is a valid length. + size_t in_len = str.length(); + if (in_len % 4 != 0) + return {}; + + // Calculate the output length. + size_t out_len = in_len / 4 * 3; + if (str[in_len - 1] == '=') out_len--; + if (str[in_len - 2] == '=') out_len--; + + // Prepare the output buffer. + std::vector out(out_len, static_cast(0)); + + // Decode. + for (size_t i = 0, j = 0; i < in_len;) + { + uint32_t a = str[i] == '=' ? 0 & i++ : decodingTable[static_cast(str[i++])]; + uint32_t b = str[i] == '=' ? 0 & i++ : decodingTable[static_cast(str[i++])]; + uint32_t c = str[i] == '=' ? 0 & i++ : decodingTable[static_cast(str[i++])]; + uint32_t d = str[i] == '=' ? 0 & i++ : decodingTable[static_cast(str[i++])]; + + uint32_t triple = (a << 3 * 6) + (b << 2 * 6) + (c << 1 * 6) + (d << 0 * 6); + + if (j < out_len) out[j++] = (triple >> 2 * 8) & 0xFF; + if (j < out_len) out[j++] = (triple >> 1 * 8) & 0xFF; + if (j < out_len) out[j++] = (triple >> 0 * 8) & 0xFF; + } + + return out; +} + +/// @brief Bring std::to_string into this namespace so we can overload it. +using std::to_string; + +/// @brief Converts a double value to a string with the specified number of decimal places. +/// @param value The double value to convert to a string. +/// @param precision The number of digits to display after the decimal point. +/// @return A string representation of the value with the specified precision. +inline auto to_string(double value, int precision) +{ + return std::format("{:0.{}f}", value, precision); +} + +/// @brief Converts a double value to a string with specified width and precision. +/// @param value The double value to convert to a string. +/// @param width The minimum width of the resulting string, including padding if necessary. +/// @param precision The number of digits to display after the decimal point. +/// @return A string representation of the value, formatted with the given width and precision. +inline auto to_string(double value, int width, int precision) +{ + return std::format("{:0{}.{}f}", value, width, precision); +} + +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Extracts the next line or substring from a string view, using a specified delimiter. +/// @param str A reference to the string view to extract from. This will be updated to remove the extracted line. +/// @param delim The delimiter character to use for splitting lines. Defaults to '\\n'. +/// @return A string containing the extracted line or substring up to the delimiter. If the delimiter is not found, returns the remainder of the string. +inline std::string extractLine(std::string_view& str, char delim = '\n') +{ + const auto pos = str.find(delim); + if (pos == std::string::npos) + { + std::string_view line = str; + str = {}; + return std::string(line); + } + + const auto line = str.substr(0, pos); + str.remove_prefix(pos + 1); + return std::string(line); +} + +/// @brief Splits a string into two trimmed parts using a specified delimiter. +/// @param str The input string to split. +/// @param delim The character used as the delimiter to split the string. Defaults to a space (' '). +/// @return A pair of std::string_view objects: the first is the trimmed substring before the delimiter, the second is the trimmed substring after the delimiter (or empty if the delimiter is not found). +inline std::pair extractConfigParts(StringViewIshVariant auto const& str, char delim = ' ') +{ + using StrType = std::remove_cvref_t; + + const auto pos = str.find(delim); + if (pos == StrType::npos) + return { string::trim(str), std::string_view{} }; + return { string::trim(str.substr(0, pos)), string::trim(str.substr(pos + 1)) }; +} + +/////////////////////////////////////////////////////////////////////////////// + +/// @brief Performs wildcard pattern matching on a string against a mask pattern. +/// @tparam ignoreCase If true, performs case-insensitive matching; if false, performs case-sensitive matching. Defaults to false. +/// @param str The string to match against the pattern. +/// @param mask The pattern mask containing wildcards ('*' for zero or more characters, '?' for exactly one character). +/// @return True if the string matches the mask pattern; otherwise, false. +template +inline bool match(StringViewIshVariant auto const& str, StringViewIshVariant auto const& mask) +{ + using str_value_type = std::remove_cvref_t::value_type; + using mask_value_type = std::remove_cvref_t::value_type; + + static_assert(std::same_as, "String and mask must have the same character type"); + + const str_value_type* curpos = str.data(); + const mask_value_type* maskpos = mask.data(); + const mask_value_type* laststarpos = nullptr; + while (*curpos != 0) + { + // Star (match any). + if (*maskpos == static_cast('*')) + { + if (!*++maskpos) + return true; + else + { + laststarpos = maskpos - 1; + + if constexpr (ignoreCase) + { + const auto m = std::tolower(static_cast(*maskpos)); + while (*curpos != 0 && std::tolower(static_cast(*curpos)) != m) + curpos++; + } + else + { + while (*curpos != 0 && *curpos != *maskpos) + curpos++; + } + } + } + + // Check for a character match. + bool match = false; + if constexpr (ignoreCase) + { + match = (std::tolower(static_cast(*maskpos)) == std::tolower(static_cast(*curpos))); + } + else + { + match = (*maskpos == *curpos); + } + + // Exact match or single character (match one). + if (match || (*maskpos == static_cast('?'))) + { + maskpos++; + curpos++; + } + else + { + // If we did not have a previous star, abort. + if (!laststarpos) + return false; + + // Otherwise, back up to it. + maskpos = laststarpos; + continue; + } + + // Reach the end of the string. + if (*curpos == 0 && *maskpos == 0) + return true; + + // Matchstring too short. + if (!*curpos && *maskpos && *maskpos != static_cast('*')) + return false; + } + + // Still match characters left. + return false; +} + +///////////////////////////////////////////////////////////////////// +} // end namespace preagonal::string + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::range +{ +/////////////////////////////////////////////////////////////////////////////// + +/// Transforms a range of std::string_view to std::string. +constexpr auto as_string = std::views::transform([](std::string_view s) { return std::string(s); }); + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::range + namespace utilities { std::string retokenizeArray(const std::vector& triggerData, int start_idx = 0); CString retokenizeCStringArray(const std::vector& triggerData, int start_idx = 0); } // namespace utilities -#endif +#endif // STRINGUTILS_H diff --git a/server/include/utilities/TimeUnits.h b/server/include/utilities/TimeUnits.h deleted file mode 100644 index 03a5bba55..000000000 --- a/server/include/utilities/TimeUnits.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef UTILITIES_TIMEUNITS_H -#define UTILITIES_TIMEUNITS_H - -#include -#include - -namespace utilities -{ - struct TimeUnits - { - std::time_t days; - uint8_t hours; - uint8_t minutes; - uint8_t seconds; - - TimeUnits(std::time_t time) - { - days = time / 86400; - hours = uint8_t((time / 3600) % 24); - minutes = uint8_t((time / 60) % 60); - seconds = uint8_t(time % 60); - } - - static auto calculate(std::time_t time) - { - return TimeUnits{ time }; - } - }; -} // namespace utilities - -#endif diff --git a/server/include/utilities/generator/IdGenerator.h b/server/include/utilities/generator/IdGenerator.h new file mode 100644 index 000000000..735481f77 --- /dev/null +++ b/server/include/utilities/generator/IdGenerator.h @@ -0,0 +1,253 @@ +#ifndef IDGENERATOR_H +#define IDGENERATOR_H + +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +template +class IdGenerator +{ +protected: + struct Segment + { + T startId = static_cast(0); + T nextId = static_cast(0); + std::optional endId; + std::set freeIds; + std::set manuallyUsedIds; + }; + + std::map m_segments; + +public: + IdGenerator() { createSegment(T(0)); } + IdGenerator(T startId) { createSegment(startId); } + + // Create a new segment starting at the specified ID. + bool createSegment(T startId) + { + Segment* previous = nullptr; + Segment* next = nullptr; + for (auto& [segmentStartId, segment] : m_segments) + { + // Find the segment previous to the startId. + if (segmentStartId < startId && (previous == nullptr || segmentStartId > previous->startId)) + previous = &segment; + // Find the segment next to the startId. + if (segmentStartId > startId && (next == nullptr || segmentStartId < next->startId)) + next = &segment; + } + + Segment newSegment{ .startId = startId, .nextId = startId }; + if (next != nullptr) + newSegment.endId = next->startId - 1; + + // If we found a segment, move any overlapping IDs to the new segment. + if (previous != nullptr) + { + previous->endId = startId - 1; + + // Check if the nextId of the previous segment is beyond our startId. + if (previous->nextId > startId) + { + newSegment.nextId = previous->nextId; + previous->nextId = startId; + } + // Move over the manually used ids. + for (auto id : previous->manuallyUsedIds) + { + if (id >= startId) + { + newSegment.manuallyUsedIds.insert(id); + previous->manuallyUsedIds.erase(id); + } + } + // Clear out any free ids. + for (auto id : previous->freeIds) + { + if (id >= startId) + previous->freeIds.erase(id); + } + condense(*previous); + } + + m_segments.emplace(std::make_pair(startId, std::move(newSegment))); + return true; + } + + // Generate a new ID + T getAvailableId(std::optional startId = {}) + { + for (auto& [segmentStartId, segment] : m_segments) + { + // Segment too early? Continue. + if (segmentStartId < startId.value_or(T{})) + continue; + + // If there is a free id, just use it. + if (!segment.freeIds.empty()) + { + T id = *segment.freeIds.begin(); + segment.freeIds.erase(segment.freeIds.begin()); + return id; + } + + // Find a free ID. + auto searchId = segment.nextId; + while (true) + { + if (!segment.manuallyUsedIds.contains(searchId)) + { + segment.nextId = searchId + 1; + return searchId; + } + ++searchId; + + if (segment.endId.has_value() && searchId > segment.endId.value()) + break; + } + } + + // Uh oh. + throw std::runtime_error("No available ID found in IdGenerator."); + } + + // Marks the ID as used + bool markAsUsed(T id) + { + if (auto segment = getSegmentForId(id); segment != nullptr) + { + auto p = segment->manuallyUsedIds.insert(id); + if (id == segment->nextId) + ++segment->nextId; + return p.second; + } + return false; + } + + // Checks if the ID is being used + bool isIdUsed(T id) const + { + if (auto segment = getSegmentForId(id); segment != nullptr) + return (segment->manuallyUsedIds.find(id) != segment->manuallyUsedIds.end()) || (id < segment->nextId && segment->freeIds.find(id) == segment->freeIds.end()); + return false; + } + + // Peeks the next ID + T peekNextId(std::optional startId = {}) const + { + const Segment* segment = &m_segments.begin()->second; + if (startId.has_value()) + { + if (const auto location = getSegmentForId(startId.value()); location != nullptr) + segment = location; + } + + return segment->nextId; + } + + // Free an ID + void freeId(T id) + { + if (auto segment = getSegmentForId(id); segment != nullptr) + { + // If the ID was manually used, and it was beyond our next ID, then don't add it to the free list. + if (segment->manuallyUsedIds.erase(id) != 0 && id >= segment->nextId) + return; + + segment->freeIds.insert(id); + condense(*segment); + } + } + + // Reset the free IDs and set the next ID + void reset() + { + for (auto& [segmentStartId, segment] : m_segments) + { + segment.freeIds.clear(); + segment.manuallyUsedIds.clear(); + segment.nextId = segmentStartId; + } + } + +protected: + void condense(Segment& segment) + { + // See if we can condense the free IDs. + if (!segment.freeIds.empty()) + { + auto searchId = segment.nextId; + auto it = segment.freeIds.rbegin(); + while (it != segment.freeIds.rend()) + { + if (*it == searchId - 1) + { + --searchId; + ++it; + continue; + } + break; + } + + // Erase the IDs. + if (searchId != segment.nextId) + { + if (it == segment.freeIds.rend()) + segment.freeIds.clear(); + else segment.freeIds.erase(*it.base()); + segment.nextId = searchId; + } + } + } + + Segment* getSegmentForId(T id) + { + for (auto& [segmentStartId, segment] : m_segments) + { + if (id < segmentStartId || (segment.endId.has_value() && segment.endId.value() < id)) + continue; + + // If the segment has free ids, pick it. + if (!segment.freeIds.empty()) + return &segment; + + // If there is space left, pick it. + if (!segment.endId.has_value() || segment.nextId < segment.endId.value()) + return &segment; + } + return nullptr; + } + + const Segment* getSegmentForId(T id) const + { + for (auto& [segmentStartId, segment] : m_segments) + { + if (id < segmentStartId || (segment.endId.has_value() && segment.endId.value() < id)) + continue; + + // If the segment has free ids, pick it. + if (!segment.freeIds.empty()) + return &segment; + + // If there is space left, pick it. + if (!segment.endId.has_value() || segment.nextId < segment.endId.value()) + return &segment; + } + return nullptr; + } +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // IDGENERATOR_H diff --git a/server/include/utilities/generator/TimeoutGenerator.h b/server/include/utilities/generator/TimeoutGenerator.h new file mode 100644 index 000000000..72353014c --- /dev/null +++ b/server/include/utilities/generator/TimeoutGenerator.h @@ -0,0 +1,130 @@ +#ifndef TIMEOUTGENERATOR_H +#define TIMEOUTGENERATOR_H + +#include +#include + +#include + +using namespace std::chrono_literals; + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +/// @brief A utility struct for generating periodic timeouts and invoking a callback after a specified interval. +struct TimeoutGenerator +{ + using time_point = precise_clock::time_point; + using time_delta = std::chrono::milliseconds; + + TimeoutGenerator() = default; + + template + TimeoutGenerator(std::chrono::duration timeout, bool repeated = false) + : timeout(std::chrono::duration_cast(timeout)), repeated(repeated) {} + +public: + time_delta timeout = 5ms; + bool repeated = true; + + /// @brief A callback function to be invoked with the current iteration index. DO NOT USE IF STORED IN A VECTOR THAT MAY REALLOCATE! + std::function callbackIterations = nullptr; + + /// @brief A callback function that takes a time duration as its parameter. DO NOT USE IF STORED IN A VECTOR THAT MAY REALLOCATE! + std::function callbackDuration = nullptr; + +public: + int update(time_point now = precise_clock::now()) + { + if (!m_running) + return 0; + + auto duration = now - m_lastTimeout; + int iterations = duration / timeout; + if (iterations > 0) + { + m_lastTimeout = now; + if (!repeated) + { + m_running = false; + iterations = 1; + } + + if (callbackIterations) + callbackIterations(iterations); + if (callbackDuration) + callbackDuration(std::chrono::duration_cast(duration)); + } + + return iterations; + } + + void setLastTimeout(time_point lastTimeout = precise_clock::now()) + { + m_lastTimeout = lastTimeout; + } + + void start() + { + m_running = true; + m_lastTimeout = precise_clock::now(); + } + + template + void startFor(Duration timeoutDuration) + { + timeout = std::chrono::duration_cast(timeoutDuration); + start(); + } + + template + void runOnceFor(Duration timeoutDuration) + { + repeated = false; + startFor(timeoutDuration); + } + + void resume() + { + m_running = true; + } + + void stop() + { + m_running = false; + } + + bool isRunning() const + { + return m_running; + } + + clock::duration getRemainingTime(time_point now = precise_clock::now()) const + { + if (!m_running) return clock::duration::zero(); + auto elapsed = now - m_lastTimeout; + auto remaining = timeout - std::chrono::duration_cast(elapsed); + return std::chrono::duration_cast(remaining > clock::duration::zero() ? remaining : clock::duration::zero()); + } + + size_t getRemainingTimeIn50msIncrements(time_point now = precise_clock::now()) const + { + if (!m_running) return 0; + auto elapsed = now - m_lastTimeout; + auto remaining = timeout - std::chrono::duration_cast(elapsed); + if (remaining > clock::duration::zero()) + return std::chrono::duration_cast(remaining).count() / 50; + return 0; + } + +protected: + bool m_running = false; + time_point m_lastTimeout = precise_clock::now(); +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // TIMEOUTGENERATOR_H diff --git a/server/include/utilities/manager/GuildManager.h b/server/include/utilities/manager/GuildManager.h new file mode 100644 index 000000000..8f5ad793b --- /dev/null +++ b/server/include/utilities/manager/GuildManager.h @@ -0,0 +1,71 @@ +#ifndef GUILDMANAGER_H +#define GUILDMANAGER_H + +#include +#include +#include +#include +#include +#include + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +struct Guild +{ + std::string name; + std::filesystem::path filePath; + string_multimap members; + bool modifiedSinceLastSave = false; +}; + +class GuildManager +{ +public: + using string_map_pair = std::pair::const_iterator, string_multimap::const_iterator>; + +public: + ~GuildManager(); + +public: + void loadGuilds(const std::filesystem::path& directory); + void saveGuilds(); + [[inline]] void update(); + +public: + bool guildExists(std::string_view guildName) const; + bool verifyPlayerInGuild(std::string_view guildName, std::string_view account, std::string_view nickName = {}) const; + std::optional getPlayerNicknamesForGuild(std::string_view guildName, std::string_view account) const; + +public: + bool createGuild(std::string_view guildName); + bool deleteGuild(std::string_view guildName); + bool saveGuild(std::string_view guildName); + bool addPlayerToGuild(std::string_view guildName, std::string_view account, std::string_view nickName = {}); + bool removePlayerFromGuild(std::string_view guildName, std::string_view account, std::string_view nickName = {}); + bool removePlayerEntirelyFromGuild(std::string_view guildName, std::string_view account); + +private: + Guild* loadGuild(const std::filesystem::path& filePath); + +private: + string_map m_guilds; + fs::FileSystem m_filesystem; +}; + +//---------------------------- + +inline void GuildManager::update() +{ + m_filesystem.update(); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // GUILDMANAGER_H diff --git a/server/include/utilities/manager/ITranslationManager.h b/server/include/utilities/manager/ITranslationManager.h new file mode 100644 index 000000000..7e03c7dae --- /dev/null +++ b/server/include/utilities/manager/ITranslationManager.h @@ -0,0 +1,123 @@ +#ifndef ITRANSLATIONMANAGER_H +#define ITRANSLATIONMANAGER_H + +#include +#include +#include +#include +#include +#include + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +inline namespace language +{ + +using namespace std::literals::string_literals; + +static std::unordered_map languageAliasesToClassic = +{ + { "de"s, "Deutsch"sv }, + { "german"s, "Deutsch"sv }, + // + { "en"s, "English"sv }, + { "english"s, "English"sv }, + // + { "es"s, "Espa\u00F1ol"sv }, + { "spanish"s, "Espa\u00F1ol"sv }, + { "espanol"s, "Espa\u00F1ol"sv }, + // + { "fr"s, "Fran\u00E7ais"sv }, + { "french"s, "Fran\u00E7ais"sv }, + { "francais"s, "Fran\u00E7ais"sv }, + // + { "it"s, "Italiano"sv }, + { "italian"s, "Italiano"sv }, + // + { "nl"s, "Nederlands"sv }, + { "dutch"s, "Nederlands"sv }, + // + { "no"s, "Norsk"sv }, + { "norwegian"s, "Norsk"sv }, + // + { "pt"s, "Portugu\u00EAs"sv }, + { "portuguese"s, "Portugu\u00EAs"sv }, + { "portugues"s, "Portugu\u00EAs"sv }, + // + { "sv"s, "Svenska"sv }, + { "swedish"s, "Svenska"sv }, +}; + +static std::unordered_map languageAliasesClassicToModern = +{ + { "Deutsch"s, "de"sv }, + { "English"s, "en"sv }, + { "Espa\u00F1ol"s, "es"sv }, + { "Fran\u00E7ais"s, "fr"sv }, + { "Italiano"s, "it"sv }, + { "Nederlands"s, "nl"sv }, + { "Norsk"s, "no"sv }, + { "Portugu\u00EAs"s, "pt"sv }, + { "Svenska"s, "sv"sv }, +}; + +inline static std::string_view mapToClassic(std::string_view language) noexcept +{ + if (auto it = languageAliasesToClassic.find(language); it != languageAliasesToClassic.end()) + return it->second; + return language; +} + +} // end namespace language + +//---------------------------- + +/// @brief Interface for managing translations. +class ITranslationManager +{ +public: + virtual ~ITranslationManager() {}; + +public: + /// @brief Loads all translations from a given directory. + /// @param directory The directory to load translations from. + virtual void loadTranslations(const std::filesystem::path& directory) = 0; + + /// @brief Reloads the translation data from the specified file. + /// @param fileName The path to the translation file to reload. + virtual void reloadTranslation(const std::filesystem::path& filePath) = 0; + + /// @brief Saves all translations in memory to the disk. + virtual void saveTranslations() = 0; + + /// @brief Synchronizes the specified language with the original. + /// @param language A view of the language string to synchronize. + /// @return A std::tuple containing the name of the language (0), how many entries were added (1), and how many were removed (2). + virtual std::tuple syncLanguageWithOriginal(std::string_view language) = 0; + + /// @brief Generator that, when iterated, synchronizes all loaded languages with the original. + /// @return A std::tuple containing the name of the language (0), how many entries were added (1), and how many were removed (2). + virtual std::generator> syncAllLanguagesWithOriginal() = 0; + + /// @brief Generates stubs for supported languages. + /// @return The number of language stubs generated. + virtual size_t generateAllLanguageStubs() = 0; + +public: + /// @brief Retrieves a localized text string for a given language and key. + /// @param language The language code specifying the desired localization. + /// @param key The key identifying the text string to retrieve. + /// @return A string view containing the localized text corresponding to the specified key and language. + virtual std::string_view getText(std::string_view language, std::string_view key) = 0; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // ITRANSLATIONMANAGER_H diff --git a/server/include/utilities/ResourceManager.h b/server/include/utilities/manager/ResourceManager.h similarity index 87% rename from server/include/utilities/ResourceManager.h rename to server/include/utilities/manager/ResourceManager.h index aea6e2309..1e7e22d66 100644 --- a/server/include/utilities/ResourceManager.h +++ b/server/include/utilities/manager/ResourceManager.h @@ -1,11 +1,16 @@ -#ifndef GS2EMU_RESOURCEMANAGER_H -#define GS2EMU_RESOURCEMANAGER_H +#ifndef RESOURCEMANAGER_H +#define RESOURCEMANAGER_H #include #include #include #include +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + template class ResourceManager { @@ -88,4 +93,7 @@ class ResourceManager ResourceMap m_resourceMap; }; -#endif +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // RESOURCEMANAGER_H diff --git a/server/include/utilities/manager/TranslationManagerClassic.h b/server/include/utilities/manager/TranslationManagerClassic.h new file mode 100644 index 000000000..115bbd1ac --- /dev/null +++ b/server/include/utilities/manager/TranslationManagerClassic.h @@ -0,0 +1,53 @@ +#ifndef TRANSLATIONMANAGERCLASSIC_H +#define TRANSLATIONMANAGERCLASSIC_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class TranslationManagerClassic : public ITranslationManager +{ + struct TranslationMap + { + std::filesystem::path filename; + string_map lines; + }; + +public: + virtual ~TranslationManagerClassic() override {} + +public: + virtual void loadTranslations(const std::filesystem::path& directory) override; + virtual void reloadTranslation(const std::filesystem::path& filePath) override; + virtual void saveTranslations() override; + virtual std::tuple syncLanguageWithOriginal(std::string_view language) override; + virtual std::generator> syncAllLanguagesWithOriginal() override; + virtual size_t generateAllLanguageStubs() override; + +protected: + void loadDomain(const std::filesystem::path& filePath); + std::string generateHash(std::string_view key) const; + +public: + virtual std::string_view getText(std::string_view language, std::string_view key) override; + +protected: + std::unordered_map m_domains; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // TRANSLATIONMANAGERCLASSIC_H diff --git a/server/include/utilities/manager/TranslationManagerModern.h b/server/include/utilities/manager/TranslationManagerModern.h new file mode 100644 index 000000000..6c940a3a2 --- /dev/null +++ b/server/include/utilities/manager/TranslationManagerModern.h @@ -0,0 +1,36 @@ +#ifndef TRANSLATIONMANAGERMODERN_H +#define TRANSLATIONMANAGERMODERN_H + +#include +#include +#include + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +class TranslationManagerModern : public ITranslationManager +{ +public: + virtual ~TranslationManagerModern() override {}; + +public: + virtual void loadTranslations(const std::filesystem::path& directory) override; + virtual void reloadTranslation(const std::filesystem::path& filePath) override; + virtual void saveTranslations() override; + virtual std::tuple syncLanguageWithOriginal(std::string_view language) override; + virtual std::generator> syncAllLanguagesWithOriginal() override; + virtual size_t generateAllLanguageStubs() override; + +public: + virtual std::string_view getText(std::string_view language, std::string_view key) override; +}; + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal + +#endif // TRANSLATIONMANAGERMODERN_H diff --git a/server/include/utilities/std/generator.h b/server/include/utilities/std/generator.h new file mode 100644 index 000000000..bc3d9357e --- /dev/null +++ b/server/include/utilities/std/generator.h @@ -0,0 +1,936 @@ +#ifndef __STD_GENERATOR_INCLUDED +#define __STD_GENERATOR_INCLUDED + +#include + +// If our STL supports the C++23 std::generator, just use that one. +#if defined(__cpp_lib_generator) +#include +#else + +/////////////////////////////////////////////////////////////////////////////// +// Reference implementation of std::generator proposal P2168. +// +// See https://wg21.link/P2168 for details. +// +/////////////////////////////////////////////////////////////////////////////// +// Copyright Lewis Baker, Corentin Jabot +// +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. +// (See accompanying file LICENSE or http://www.boost.org/LICENSE_1_0.txt) +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#if __has_include() +#include +#else +// Fallback for older experimental implementations of coroutines. +#include +namespace std +{ +using std::experimental::coroutine_handle; +using std::experimental::coroutine_traits; +using std::experimental::noop_coroutine; +using std::experimental::suspend_always; +using std::experimental::suspend_never; +} // namespace std +#endif + +#include +#include +#include +#include +#include +#include +#include + +#if __has_include() +# include +#else + +// Placeholder implementation of the bits we need from header +// when we don't have the header (e.g. Clang 12 and earlier). +namespace std +{ + +// Don't create naming conflicts with recent libc++ which defines std::iter_reference_t +// in but doesn't yet provide a header. +template +using __iter_reference_t = decltype(*std::declval<_T&>()); + +template +using iter_value_t = +typename std::iterator_traits>::value_type; + +namespace ranges +{ + +namespace __begin +{ +void begin(); + +struct _fn +{ + template + requires requires(_Range& __r) + { + __r.begin(); + } + auto operator()(_Range&& __r) const + noexcept(noexcept(__r.begin())) + -> decltype(__r.begin()) + { + return __r.begin(); + } + + template + requires + (!requires(_Range& __r) { __r.begin(); }) && + requires(_Range& __r) { begin(__r); } + auto operator()(_Range&& __r) const + noexcept(noexcept(begin(__r))) + -> decltype(begin(__r)) + { + return begin(__r); + } +}; + +} // namespace __begin + +inline namespace __begin_cpo +{ +inline constexpr __begin::_fn begin = {}; +} + +namespace __end +{ +void end(); + +struct _fn +{ + template + requires requires(_Range& __r) { __r.end(); } + auto operator()(_Range&& __r) const + noexcept(noexcept(__r.end())) + -> decltype(__r.end()) + { + return __r.end(); + } + + template + requires + (!requires(_Range& __r) { __r.end(); }) && + requires(_Range& __r) { end(__r); } + auto operator()(_Range&& __r) const + noexcept(noexcept(end(__r))) + -> decltype(end(__r)) + { + return end(__r); + } +}; +} // namespace __end + +inline namespace _end_cpo +{ +inline constexpr __end::_fn end = {}; +} + +template +using iterator_t = decltype(begin(std::declval<_Range>())); + +template +using sentinel_t = decltype(end(std::declval<_Range>())); + +template +using range_reference_t = __iter_reference_t>; + +template +using range_value_t = iter_value_t>; + +template +concept range = requires(_T & __t) +{ + ranges::begin(__t); + ranges::end(__t); +}; + +} // namespace ranges +} // namespace std + +#endif // !__has_include() + + +namespace std +{ + +template +class __manual_lifetime +{ +public: + __manual_lifetime() noexcept {} + ~__manual_lifetime() {} + + template + _T& construct(_Args&&... __args) noexcept(std::is_nothrow_constructible_v<_T, _Args...>) + { + return *::new (static_cast(std::addressof(__value_))) _T((_Args&&)__args...); + } + + void destruct() noexcept(std::is_nothrow_destructible_v<_T>) + { + __value_.~_T(); + } + + _T& get() & noexcept + { + return __value_; + } + _T&& get() && noexcept + { + return static_cast<_T&&>(__value_); + } + const _T& get() const& noexcept + { + return __value_; + } + const _T&& get() const&& noexcept + { + return static_cast(__value_); + } + +private: + union + { + std::remove_const_t<_T> __value_; + }; +}; + +template +class __manual_lifetime<_T&> +{ +public: + __manual_lifetime() noexcept : __value_(nullptr) {} + ~__manual_lifetime() {} + + _T& construct(_T& __value) noexcept + { + __value_ = std::addressof(__value); + return __value; + } + + void destruct() noexcept {} + + _T& get() const noexcept + { + return *__value_; + } + +private: + _T* __value_; +}; + +template +class __manual_lifetime<_T&&> +{ +public: + __manual_lifetime() noexcept : __value_(nullptr) {} + ~__manual_lifetime() {} + + _T&& construct(_T&& __value) noexcept + { + __value_ = std::addressof(__value); + return static_cast<_T&&>(__value); + } + + void destruct() noexcept {} + + _T&& get() const noexcept + { + return static_cast<_T&&>(*__value_); + } + +private: + _T* __value_; +}; + +struct use_allocator_arg {}; + +namespace ranges +{ + +template +struct elements_of +{ + explicit constexpr elements_of(_Rng&& __rng) noexcept + requires std::is_default_constructible_v<_Allocator> + : __range(static_cast<_Rng&&>(__rng)) + { + } + + constexpr elements_of(_Rng&& __rng, _Allocator&& __alloc) noexcept + : __range((_Rng&&)__rng), __alloc((_Allocator&&)__alloc) + { + } + + constexpr elements_of(elements_of&&) noexcept = default; + + constexpr elements_of(const elements_of&) = delete; + constexpr elements_of& operator=(const elements_of&) = delete; + constexpr elements_of& operator=(elements_of&&) = delete; + + constexpr _Rng&& get() noexcept + { + return static_cast<_Rng&&>(__range); + } + + constexpr _Allocator get_allocator() const noexcept + { + return __alloc; + } + +private: + [[no_unique_address]] _Allocator __alloc; // \expos + _Rng&& __range; // \expos +}; + +template +elements_of(_Rng&&) -> elements_of<_Rng>; + +template +elements_of(_Rng&&, Allocator&&) -> elements_of<_Rng, Allocator>; + +} // namespace ranges + +template +static constexpr bool __allocator_needs_to_be_stored = +!std::allocator_traits<_Alloc>::is_always_equal::value || +!std::is_default_constructible_v<_Alloc>; + +// Round s up to next multiple of a. +constexpr size_t __aligned_allocation_size(size_t s, size_t a) +{ + return (s + a - 1) & ~(a - 1); +} + + +template , + typename _Allocator = use_allocator_arg> +class generator; + +template +class __promise_base_alloc +{ + static constexpr std::size_t __offset_of_allocator(std::size_t __frameSize) noexcept + { + return __aligned_allocation_size(__frameSize, alignof(_Alloc)); + } + + static constexpr std::size_t __padded_frame_size(std::size_t __frameSize) noexcept + { + return __offset_of_allocator(__frameSize) + sizeof(_Alloc); + } + + static _Alloc& __get_allocator(void* __frame, std::size_t __frameSize) noexcept + { + return *reinterpret_cast<_Alloc*>( + static_cast(__frame) + __offset_of_allocator(__frameSize)); + } + +public: + template + static void* operator new(std::size_t __frameSize, std::allocator_arg_t, _Alloc __alloc, _Args&...) + { + void* __frame = __alloc.allocate(__padded_frame_size(__frameSize)); + + // Store allocator at end of the coroutine frame. + // Assuming the allocator's move constructor is non-throwing (a requirement for allocators) + ::new (static_cast(std::addressof(__get_allocator(__frame, __frameSize)))) _Alloc(std::move(__alloc)); + + return __frame; + } + + template + static void* operator new(std::size_t __frameSize, _This&, std::allocator_arg_t, _Alloc __alloc, _Args&...) + { + return __promise_base_alloc::operator new(__frameSize, std::allocator_arg, std::move(__alloc)); + } + + static void operator delete(void* __ptr, std::size_t __frameSize) noexcept + { + _Alloc& __alloc = __get_allocator(__ptr, __frameSize); + _Alloc __localAlloc(std::move(__alloc)); + __alloc.~Alloc(); + __localAlloc.deallocate(static_cast(__ptr), __padded_frame_size(__frameSize)); + } +}; + +template + requires (!__allocator_needs_to_be_stored<_Alloc>) +class __promise_base_alloc<_Alloc> +{ +public: + static void* operator new(std::size_t __size) + { + _Alloc __alloc; + return __alloc.allocate(__size); + } + + static void operator delete(void* __ptr, std::size_t __size) noexcept + { + _Alloc __alloc; + __alloc.deallocate(static_cast(__ptr), __size); + } +}; + +template +struct __generator_promise_base +{ + template + friend class generator; + + __generator_promise_base* __root_; + std::coroutine_handle<> __parentOrLeaf_; + // Note: Using manual_lifetime here to avoid extra calls to exception_ptr + // constructor/destructor in cases where it is not needed (i.e. where this + // generator coroutine is not used as a nested coroutine). + // This member is lazily constructed by the __yield_sequence_awaiter::await_suspend() + // method if this generator is used as a nested generator. + __manual_lifetime __exception_; + __manual_lifetime<_Ref> __value_; + + explicit __generator_promise_base(std::coroutine_handle<> thisCoro) noexcept + : __root_(this) + , __parentOrLeaf_(thisCoro) + { + } + + ~__generator_promise_base() + { + if (__root_ != this) + { + // This coroutine was used as a nested generator and so will + // have constructed its __exception_ member which needs to be + // destroyed here. + __exception_.destruct(); + } + } + + std::suspend_always initial_suspend() noexcept + { + return {}; + } + + void return_void() noexcept {} + + void unhandled_exception() + { + if (__root_ != this) + { + __exception_.get() = std::current_exception(); + } + else + { + throw; + } + } + + // Transfers control back to the parent of a nested coroutine + struct __final_awaiter + { + bool await_ready() noexcept + { + return false; + } + + template + std::coroutine_handle<> + await_suspend(std::coroutine_handle<_Promise> __h) noexcept + { + _Promise& __promise = __h.promise(); + __generator_promise_base& __root = *__promise.__root_; + if (&__root != &__promise) + { + auto __parent = __promise.__parentOrLeaf_; + __root.__parentOrLeaf_ = __parent; + return __parent; + } + return std::noop_coroutine(); + } + + void await_resume() noexcept {} + }; + + __final_awaiter final_suspend() noexcept + { + return {}; + } + + std::suspend_always yield_value(_Ref&& __x) + noexcept(std::is_nothrow_move_constructible_v<_Ref>) + { + __root_->__value_.construct((_Ref&&)__x); + return {}; + } + + template + requires + (!std::is_reference_v<_Ref>) && + std::is_convertible_v<_T, _Ref> + std::suspend_always yield_value(_T&& __x) + noexcept(std::is_nothrow_constructible_v<_Ref, _T>) + { + __root_->__value_.construct((_T&&)__x); + return {}; + } + + template + struct __yield_sequence_awaiter + { + _Gen __gen_; + + __yield_sequence_awaiter(_Gen&& __g) noexcept + // Taking ownership of the generator ensures frame are destroyed + // in the reverse order of their execution. + : __gen_((_Gen&&)__g) + { + } + + bool await_ready() noexcept + { + return false; + } + + // set the parent, root and exceptions pointer and + // resume the nested + template + std::coroutine_handle<> + await_suspend(std::coroutine_handle<_Promise> __h) noexcept + { + __generator_promise_base& __current = __h.promise(); + __generator_promise_base& __nested = *__gen_.__get_promise(); + __generator_promise_base& __root = *__current.__root_; + + __nested.__root_ = __current.__root_; + __nested.__parentOrLeaf_ = __h; + + // Lazily construct the __exception_ member here now that we + // know it will be used as a nested generator. This will be + // destroyed by the promise destructor. + __nested.__exception_.construct(); + __root.__parentOrLeaf_ = __gen_.__get_coro(); + + // Immediately resume the nested coroutine (nested generator) + return __gen_.__get_coro(); + } + + void await_resume() + { + __generator_promise_base& __nestedPromise = *__gen_.__get_promise(); + if (__nestedPromise.__exception_.get()) + { + std::rethrow_exception(std::move(__nestedPromise.__exception_.get())); + } + } + }; + + template + __yield_sequence_awaiter> + yield_value(std::ranges::elements_of> __g) noexcept + { + return std::move(__g).get(); + } + + template + __yield_sequence_awaiter, _Allocator>> + yield_value(std::ranges::elements_of<_Rng, _Allocator>&& __x) + { + return [](allocator_arg_t, _Allocator alloc, auto&& __rng) -> generator<_Ref, std::remove_cvref_t<_Ref>, _Allocator> + { + for (auto&& e : __rng) + co_yield static_cast(e); + }(std::allocator_arg, __x.get_allocator(), std::forward<_Rng>(__x.get())); + } + + void resume() + { + __parentOrLeaf_.resume(); + } + + // Disable use of co_await within this coroutine. + void await_transform() = delete; +}; + +template +struct __generator_promise; + +template +struct __generator_promise, _ByteAllocator, _ExplicitAllocator> final + : public __generator_promise_base<_Ref> + , public __promise_base_alloc<_ByteAllocator> +{ + __generator_promise() noexcept + : __generator_promise_base<_Ref>(std::coroutine_handle<__generator_promise>::from_promise(*this)) + { + } + + generator<_Ref, _Value, _Alloc> get_return_object() noexcept + { + return generator<_Ref, _Value, _Alloc>{ + std::coroutine_handle<__generator_promise>::from_promise(*this) + }; + } + + using __generator_promise_base<_Ref>::yield_value; + + template + typename __generator_promise_base<_Ref>::template __yield_sequence_awaiter> + yield_value(std::ranges::elements_of<_Rng>&& __x) + { + static_assert (!_ExplicitAllocator, + "This coroutine has an explicit allocator specified with std::allocator_arg so an allocator needs to be passed " + "explicitely to std::elements_of"); + return [](auto&& __rng) -> generator<_Ref, _Value, _Alloc> + { + for (auto&& e : __rng) + co_yield static_cast(e); + }(std::forward<_Rng>(__x.get())); + } +}; + +template +using __byte_allocator_t = typename std::allocator_traits>::template rebind_alloc; + + +// Type-erased allocator with default allocator behaviour. +template +struct coroutine_traits, _Args...> +{ + using promise_type = __generator_promise, std::allocator>; +}; + +// Type-erased allocator with std::allocator_arg parameter +template +struct coroutine_traits, allocator_arg_t, _Alloc, _Args...> +{ +private: + using __byte_allocator = __byte_allocator_t<_Alloc>; +public: + using promise_type = __generator_promise, __byte_allocator, true /*explicit Allocator*/>; +}; + +// Type-erased allocator with std::allocator_arg parameter (non-static member functions) +template +struct coroutine_traits, _This, allocator_arg_t, _Alloc, _Args...> +{ +private: + using __byte_allocator = __byte_allocator_t<_Alloc>; +public: + using promise_type = __generator_promise, __byte_allocator, true /*explicit Allocator*/>; +}; + +// Generator with specified allocator type +template +struct coroutine_traits, _Args...> +{ + using __byte_allocator = __byte_allocator_t<_Alloc>; +public: + using promise_type = __generator_promise, __byte_allocator>; +}; + + +// TODO : make layout compatible promise casts possible +template +class generator +{ + using __byte_allocator = __byte_allocator_t<_Alloc>; +public: + using promise_type = __generator_promise, __byte_allocator>; + friend promise_type; +private: + using __coroutine_handle = std::coroutine_handle; +public: + + generator() noexcept = default; + + generator(generator&& __other) noexcept + : __coro_(std::exchange(__other.__coro_, {})) + , __started_(std::exchange(__other.__started_, false)) + { + } + + ~generator() noexcept + { + if (__coro_) + { + if (__started_ && !__coro_.done()) + { + __coro_.promise().__value_.destruct(); + } + __coro_.destroy(); + } + } + + generator& operator=(generator&& g) noexcept + { + swap(g); + return *this; + } + + void swap(generator& __other) noexcept + { + std::swap(__coro_, __other.__coro_); + std::swap(__started_, __other.__started_); + } + + struct sentinel {}; + + class iterator + { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = _Value; + using reference = _Ref; + using pointer = std::add_pointer_t<_Ref>; + + iterator() noexcept = default; + iterator(const iterator&) = delete; + + iterator(iterator&& __other) noexcept + : __coro_(std::exchange(__other.__coro_, {})) + { + } + + iterator& operator=(iterator&& __other) + { + std::swap(__coro_, __other.__coro_); + return *this; + } + + ~iterator() + { + } + + friend bool operator==(const iterator& it, sentinel) noexcept + { + return it.__coro_.done(); + } + + iterator& operator++() + { + __coro_.promise().__value_.destruct(); + __coro_.promise().resume(); + return *this; + } + void operator++(int) + { + (void)operator++(); + } + + reference operator*() const noexcept + { + return static_cast(__coro_.promise().__value_.get()); + } + + private: + friend generator; + + explicit iterator(__coroutine_handle __coro) noexcept + : __coro_(__coro) + { + } + + __coroutine_handle __coro_; + }; + + iterator begin() + { + assert(__coro_); + assert(!__started_); + __started_ = true; + __coro_.resume(); + return iterator{ __coro_ }; + } + + sentinel end() noexcept + { + return {}; + } + +private: + explicit generator(__coroutine_handle __coro) noexcept + : __coro_(__coro) + { + } + +public: // to get around access restrictions for __yield_sequence_awaitable + std::coroutine_handle<> __get_coro() noexcept { return __coro_; } + promise_type* __get_promise() noexcept { return std::addressof(__coro_.promise()); } + +private: + __coroutine_handle __coro_; + bool __started_ = false; +}; + +// Specialisation for type-erased allocator implementation. +template +class generator<_Ref, _Value, use_allocator_arg> +{ + using __promise_base = __generator_promise_base<_Ref>; +public: + + generator() noexcept + : __promise_(nullptr) + , __coro_() + , __started_(false) + { + } + + generator(generator&& __other) noexcept + : __promise_(std::exchange(__other.__promise_, nullptr)) + , __coro_(std::exchange(__other.__coro_, {})) + , __started_(std::exchange(__other.__started_, false)) + { + } + + ~generator() noexcept + { + if (__coro_) + { + if (__started_ && !__coro_.done()) + { + __promise_->__value_.destruct(); + } + __coro_.destroy(); + } + } + + generator& operator=(generator g) noexcept + { + swap(g); + return *this; + } + + void swap(generator& __other) noexcept + { + std::swap(__promise_, __other.__promise_); + std::swap(__coro_, __other.__coro_); + std::swap(__started_, __other.__started_); + } + + struct sentinel {}; + + class iterator + { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = _Value; + using reference = _Ref; + using pointer = std::add_pointer_t<_Ref>; + + iterator() noexcept = default; + iterator(const iterator&) = delete; + + iterator(iterator&& __other) noexcept + : __promise_(std::exchange(__other.__promise_, nullptr)) + , __coro_(std::exchange(__other.__coro_, {})) + { + } + + iterator& operator=(iterator&& __other) + { + __promise_ = std::exchange(__other.__promise_, nullptr); + __coro_ = std::exchange(__other.__coro_, {}); + return *this; + } + + ~iterator() = default; + + friend bool operator==(const iterator& it, sentinel) noexcept + { + return it.__coro_.done(); + } + + iterator& operator++() + { + __promise_->__value_.destruct(); + __promise_->resume(); + return *this; + } + + void operator++(int) + { + (void)operator++(); + } + + reference operator*() const noexcept + { + return static_cast(__promise_->__value_.get()); + } + + private: + friend generator; + + explicit iterator(__promise_base* __promise, std::coroutine_handle<> __coro) noexcept + : __promise_(__promise) + , __coro_(__coro) + { + } + + __promise_base* __promise_; + std::coroutine_handle<> __coro_; + }; + + iterator begin() + { + assert(__coro_); + assert(!__started_); + __started_ = true; + __coro_.resume(); + return iterator{ __promise_, __coro_ }; + } + + sentinel end() noexcept + { + return {}; + } + +private: + template + friend struct __generator_promise; + + template + explicit generator(std::coroutine_handle<_Promise> __coro) noexcept + : __promise_(std::addressof(__coro.promise())) + , __coro_(__coro) + { + } + +public: // to get around access restrictions for __yield_sequence_awaitable + std::coroutine_handle<> __get_coro() noexcept { return __coro_; } + __promise_base* __get_promise() noexcept { return __promise_; } + +private: + __promise_base* __promise_; + std::coroutine_handle<> __coro_; + bool __started_ = false; +}; + +#if __has_include() +namespace ranges +{ + +template +constexpr inline bool enable_view> = true; + +} // namespace ranges +#endif + +} // namespace std + +#endif // !__cpp_lib_generator +#endif // __STD_GENERATOR_INCLUDED diff --git a/server/include/utilities/std/inplace_vector.h b/server/include/utilities/std/inplace_vector.h new file mode 100644 index 000000000..75fe29521 --- /dev/null +++ b/server/include/utilities/std/inplace_vector.h @@ -0,0 +1,706 @@ +#ifndef INPLACE_VECTOR_LOADER +#define INPLACE_VECTOR_LOADER + +#include + +// If our STL supports the C++26 std::inplace_vector, just use that one. +#if defined(__cpp_lib_inplace_vector) +#include +#else + +#ifndef INPLACE_VECTOR +#define INPLACE_VECTOR +/* + * SPDX-FileCopyrightText: Copyright (c) 2023 Gonzalo Brito Gadeschi. All rights reserved. + * SPDX-License-Identifier: MIT + * + * 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 Precondition, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include +#include // for rotate, equals, move_backwards, ... +#include // for lots... +#include // for size_t +#include // for fixed-width integer types +#include // for less and equal_to +#include // for reverse_iterator and iterator traits +#include // for numeric_limits +#include // for operator new +#include +#include // for length_error +#include // for std::unreachable +#include // for aligned_storage and all meta-functions +#include // for assertion diagnostics +#include // for abort +#include // for feature testing + +// Optimizer allowed to assume that EXPR evaluates to true +#define __IV_ASSUME(__EXPR) \ + static_cast((__EXPR) ? void(0) : std::unreachable()) + +// Assert pretty printer +#define __IV_ASSERT(...) \ + static_cast((__VA_ARGS__) \ + ? void(0) \ + : ::std::__iv_detail::__assert_failure( \ + static_cast(__FILE__), __LINE__, \ + "assertion failed: " #__VA_ARGS__)) + +// Assert in debug, assume in release. +#ifdef NDEBUG +#define __IV_EXPECT(__EXPR) __IV_ASSUME(__EXPR) +#else +#define __IV_EXPECT(__EXPR) __IV_ASSERT(__EXPR) +#endif + +// BUGBUG workaround for libstdc++ not providing from_range_t / from_range yet +namespace std { +#if !defined(__cpp_lib_containers_ranges) && !defined(__glibcxx_ranges_to_container) + struct from_range_t {}; + inline constexpr from_range_t from_range; + #endif +} // namespace std + +// Private utilites +namespace std::__iv_detail { + +template +[[noreturn]] +static constexpr void +__assert_failure(char const* __file, int __line, char const* __msg) { + //if consteval { + // throw __msg; // TODO: std lib implementor, do better here + //} else { + fprintf(stderr, "%s(%d): %s\n", __file, __line, __msg); + abort(); + //} +} + +// clang-format off +// Smallest unsigned integer that can represent values in [0, N]. +template +using __smallest_size_t += conditional_t<(__N < numeric_limits::max()), uint8_t, + conditional_t<(__N < numeric_limits::max()), uint16_t, + conditional_t<(__N < numeric_limits::max()), uint32_t, + conditional_t<(__N < numeric_limits::max()), uint64_t, + size_t>>>>; +// clang-format on + +// Index a random-access and sized range doing bound checks in debug builds +template +static constexpr decltype(auto) __index(__Rng&& __rng, __Index __i) noexcept + requires(ranges::sized_range<__Rng>) +{ + __IV_EXPECT(static_cast(__i) < static_cast(ranges::size(__rng))); + return begin(::std::forward<__Rng>(__rng))[::std::forward<__Index>(__i)]; +} + +// http://eel.is/c++draft/container.requirements.general#container.intro.reqmts-2 +template +concept __container_compatible_range + = ranges::input_range<__Rng> && convertible_to, __T>; + +template +concept __move_or_copy_insertable_from = requires(__Ptr __ptr, __T&& __value) { + { construct_at(__ptr, ::std::forward<__T&&>(__value)) } -> same_as<__Ptr>; +}; + +} // namespace std::__iv_detail + +// Types implementing the `inplace_vector`'s storage +namespace std::__iv_detail::__storage { + +// TODO: flesh out +template +struct __aligned_storage2 { + alignas(__T) byte __d[sizeof(__T) * __N]; + constexpr __T* __data(size_t __i) noexcept { + __IV_EXPECT(__i < __N); + return reinterpret_cast<__T*>(__d) + __i; + } + constexpr const __T* __data(size_t __i) const noexcept { + __IV_EXPECT(__i < __N); + return reinterpret_cast(__d) + __i; + } +}; + +// Storage for zero elements. +template +struct __zero_sized { + protected: + using __size_type = uint8_t; + static constexpr __T* __data() noexcept { return nullptr; } + static constexpr __size_type __size() noexcept { return 0; } + static constexpr void __unsafe_set_size(size_t __new_size) noexcept { + __IV_EXPECT(__new_size == 0 && "tried to change size of empty storage to non-zero value"); + } + public: + constexpr __zero_sized() = default; + constexpr __zero_sized(__zero_sized const&) = default; + constexpr __zero_sized& operator=(__zero_sized const&) = default; + constexpr __zero_sized(__zero_sized&&) = default; + constexpr __zero_sized& operator=(__zero_sized&&) = default; + constexpr ~__zero_sized() = default; +}; + +// Storage for trivial types. +template +struct __trivial { + static_assert(is_trivial_v<__T>, "storage::trivial requires Trivial"); + static_assert(__N != size_t{0}, "__N == 0, use __zero_sized"); + protected: + using __size_type = __smallest_size_t<__N>; + private: + // If value_type is const, then const array of non-const elements: + using __data_t = conditional_t< + !is_const_v<__T>, array<__T, __N>, + const array, __N> + >; + alignas(alignof(__T)) __data_t __data_{}; + __size_type __size_ = 0; + protected: + constexpr const __T* __data() const noexcept { return __data_.data(); } + constexpr __T* __data() noexcept { return __data_.data(); } + constexpr __size_type __size() const noexcept { return __size_; } + constexpr void __unsafe_set_size(size_t __new_size) noexcept { + __IV_EXPECT(__size_type(__new_size) <= __N && "new_size out-of-bounds [0, N]"); + __size_ = __size_type(__new_size); + } + public: + constexpr __trivial() noexcept = default; + constexpr __trivial(__trivial const&) noexcept = default; + constexpr __trivial& operator=(__trivial const&) noexcept = default; + constexpr __trivial(__trivial&&) noexcept = default; + constexpr __trivial& operator=(__trivial&&) noexcept = default; + constexpr ~__trivial() = default; +}; + +/// Storage for non-trivial elements. +template +struct __non_trivial { + static_assert(!is_trivial_v<__T>, "use storage::trivial for Trivial elements"); + static_assert(__N != size_t{0}, "use storage::zero for __N==0"); + protected: + using __size_type = __smallest_size_t<__N>; + private: + using __data_t = conditional_t< + !is_const_v<__T>, __aligned_storage2<__T, __N>, + const __aligned_storage2, __N>>; + __data_t __data_{}; // BUGBUG: test SIMD types + __size_type __size_ = 0; + protected: + constexpr const __T* __data() const noexcept { return __data_.__data(0); } + constexpr __T* __data() noexcept { return __data_.__data(0); } + constexpr __size_type __size() const noexcept { return __size_; } + constexpr void __unsafe_set_size(size_t __new_size) noexcept { + __IV_EXPECT(__size_type(__new_size) <= __N && "new_size out-of-bounds [0, __N)"); + __size_ = __size_type(__new_size); + } + public: + constexpr __non_trivial() noexcept = default; + constexpr __non_trivial(__non_trivial const&) noexcept = default; + constexpr __non_trivial& operator=(__non_trivial const&) noexcept = default; + constexpr __non_trivial(__non_trivial&&) noexcept = default; + constexpr __non_trivial& operator=(__non_trivial&&) noexcept = default; + constexpr ~__non_trivial() = default; +}; + +// Selects the vector storage. +template +using _t = conditional_t< + __N == 0, __zero_sized<__T>, + conditional_t, __trivial<__T, __N>, __non_trivial<__T, __N>>>; + +} // namespace std::iv_detail::storage + +namespace std { + +/// Dynamically-resizable fixed-__N vector with inplace storage. +template +struct inplace_vector : private __iv_detail::__storage::_t<__T, __N> { + private: + static_assert(is_nothrow_destructible_v<__T>, "T must be nothrow destructible"); + using __base_t = __iv_detail::__storage::_t<__T, __N>; + using __self = inplace_vector<__T, __N>; + using __base_t::__unsafe_set_size; + using __base_t::__data; + using __base_t::__size; + + public: + using value_type = __T; + using pointer = __T*; + using const_pointer = const __T*; + using reference = value_type&; + using const_reference = const value_type&; + using size_type = size_t; + using difference_type = ptrdiff_t; + using iterator = pointer; + using const_iterator = const_pointer; + using reverse_iterator = ::std::reverse_iterator; + using const_reverse_iterator = ::std::reverse_iterator; + + // [containers.sequences.inplace_vector.cons], construct/copy/destroy + constexpr inplace_vector() noexcept { __unsafe_set_size(0); } + // constexpr explicit inplace_vector(size_type __n); + // constexpr inplace_vector(size_type __n, const __T& __value); + // template // BUGBUG: why not model input_iterator? + // constexpr inplace_vector(__InputIterator __first, __InputIterator __last); + // template <__iv_detail::__container_compatible_range<__T> __R> + // constexpr inplace_vector(from_range_t, __R&& __rg); + // from base-class, trivial if is_trivially_copy_constructible_v: + // constexpr inplace_vector(const inplace_vector&); + // from base-class, trivial if is_trivially_move_constructible_v + // constexpr inplace_vector(inplace_vector&&) noexcept(__N == 0 || is_nothrow_move_constructible_v<__T>); + // constexpr inplace_vector(initializer_list<__T> __il); + // from base-class, trivial if is_trivially_destructible_v<__T> + // constexpr ~inplace_vector(); + // from base-class, trivial if is_trivially_destructible_v<__T> && is_trivially_copy_assignable_v<__T> + // constexpr inplace_vector& operator=(const inplace_vector& __other); + // from base-class, trivial if is_trivially_destructible_v<__T> && is_trivially_copy_assignable_v<__T> + // constexpr inplace_vector& operator=(inplace_vector&& __other) noexcept(__N == 0 || is_nothrow_move_assignable_v<__T>); + // template // BUGBUG: why not model input_iterator + // constexpr void assign(__InputIterator __first, __InputIterator l__ast); + // template<__iv_detail::__container_compatible_range<__T> __R> + // constexpr void assign_range(__R&& __rg); + // constexpr void assign(size_type __n, const __T& __u); + // constexpr void assign(initializer_list<__T> __il); + + // iterators + constexpr iterator begin() noexcept { return __data(); } + constexpr const_iterator begin() const noexcept { return __data(); } + constexpr iterator end() noexcept { return begin() + size(); } + constexpr const_iterator end() const noexcept { return begin() + size(); } + constexpr reverse_iterator rbegin() noexcept { return reverse_iterator(end()); } + constexpr const_reverse_iterator rbegin() const noexcept { return const_reverse_iterator(end()); } + constexpr reverse_iterator rend() noexcept { return reverse_iterator(begin()); } + constexpr const_reverse_iterator rend() const noexcept { return const_reverse_iterator(begin()); } + + constexpr const_iterator cbegin() const noexcept { return __data(); } + constexpr const_iterator cend() const noexcept { return cbegin() + size(); } + constexpr const_reverse_iterator crbegin() const noexcept { return const_reverse_iterator(cend()); } + constexpr const_reverse_iterator crend() const noexcept { return const_reverse_iterator(cbegin()); } + + [[nodiscard]] constexpr bool empty() const noexcept { return __size() == 0; }; + constexpr size_type size() const noexcept { return __size(); } + static constexpr size_type max_size() noexcept { return __N; } + static constexpr size_type capacity() noexcept { return __N; } + // constexpr void resize(size_type __sz); + // constexpr void resize(size_type __sz, const __T& __c); + constexpr void reserve(size_type __n) { if (__n > __N) [[unlikely]] throw bad_alloc(); } + constexpr void shrink_to_fit() {} + + // element access + constexpr reference operator[](size_type __n) { return __iv_detail::__index(*this, __n); } + constexpr const_reference operator[](size_type __n) const { return __iv_detail::__index(*this, __n); } + // constexpr const_reference at(size_type __n) const; + // constexpr reference at(size_type __n); + constexpr reference front() { return __iv_detail::__index(*this, size_type(0)); } + constexpr const_reference front() const { return __iv_detail::__index(*this, size_type(0)); } + constexpr reference back() { return __iv_detail::__index(*this, size() - size_type(1)); } + constexpr const_reference back() const { return __iv_detail::__index(*this, size() - size_type(1)); } + + // [containers.sequences.inplace_vector.data], data access + constexpr __T* data() noexcept { return __data(); } + constexpr const __T* data() const noexcept { return __data(); } + + // [containers.sequences.inplace_vector.modifiers], modifiers + // template + // constexpr __T& emplace_back(__Args&&... __args); + // constexpr __T& push_back(const __T& __x); + // constexpr __T& push_back(__T&& __x); + // template<__iv_detail::__container_compatible_range<__T> __R> + // constexpr void append_range(__R&& __rg); + // constexpr void pop_back(); + + // template + // constexpr __T* try_emplace_back(__Args&&... __args); + // constexpr __T* try_push_back(const __T& __value); + // constexpr __T* try_push_back(__T&& __value); + + // template + // constexpr __T& unchecked_emplace_back(__Args&&... __args); + // constexpr __T& unchecked_push_back(const __T& __value); + // constexpr __T& unchecked_push_back(__T&& __value); + + // template + // constexpr iterator emplace(const_iterator __position, __Args&&... __args); + // constexpr iterator insert(const_iterator __position, const __T& __x); + // constexpr iterator insert(const_iterator __position, __T&& __x); + // constexpr iterator insert(const_iterator __position, size_type __n, const __T& __x); + //template + // constexpr iterator insert(const_iterator __position, __InputIterator __first, __InputIterator __last); + // template<__iv_detail::__container_compatible_range<__T> __R> + // constexpr iterator insert_range(const_iterator __position, __R&& __rg); + // constexpr iterator insert(const_iterator __position, initializer_list<__T> __il); + // constexpr iterator erase(const_iterator __position); + // constexpr iterator erase(const_iterator __first, const_iterator __last); + // constexpr void swap(inplace_vector& __x) + // noexcept(__N == 0 || (is_nothrow_swappable_v<__T> && is_nothrow_move_constructible_v<__T>)); + //constexpr void clear() noexcept; + + constexpr friend bool operator==(const inplace_vector& __x, const inplace_vector& __y) { + return __x.size() == __y.size() && ::std::ranges::equal(__x, __y); + } + // constexpr friend auto /*synth-three-way-result*/ + // operator<=>(const inplace_vector& __x, const inplace_vector& __y); + constexpr friend void swap(inplace_vector& __x, inplace_vector& __y) + noexcept(__N == 0 || (is_nothrow_swappable_v<__T> && is_nothrow_move_constructible_v<__T>)) + { __x.swap(__y); } + + private: // Utilities + + constexpr void __assert_iterator_in_range(const_iterator __it) noexcept { + __IV_EXPECT(begin() <= __it && "iterator not in range"); + __IV_EXPECT(__it <= end() && "iterator not in range"); + } + constexpr void __assert_valid_iterator_pair(const_iterator __first, const_iterator __last) noexcept { + __IV_EXPECT(__first <= __last && "invalid iterator pair"); + } + constexpr void __assert_iterator_pair_in_range(const_iterator __first, const_iterator __last) noexcept { + __assert_iterator_in_range(__first); + __assert_iterator_in_range(__last); + __assert_valid_iterator_pair(__first, __last); + } + constexpr void __unsafe_destroy(__T* __first, __T* __last) noexcept(is_nothrow_destructible_v<__T>) { + __assert_iterator_pair_in_range(__first, __last); + if constexpr(__N > 0 && !is_trivial_v<__T>) { + for (; __first != __last; ++__first) __first->~__T(); + } + } + + public: + + // Implementation + + // [containers.sequences.inplace_vector.modifiers], modifiers + + template + constexpr __T& unchecked_emplace_back(__Args&&... __args) + requires(constructible_from<__T, __Args...>) + { + __IV_EXPECT(size() < capacity() && "inplace_vector out-of-memory"); + construct_at(end(), ::std::forward<__Args>(__args)...); + __unsafe_set_size(size() + size_type(1)); + return back(); + } + + template + constexpr __T* try_emplace_back(__Args&&... __args) { + if (size() == capacity()) [[unlikely]] return nullptr; + return &unchecked_emplace_back(::std::forward<__Args>(__args)...); + } + + template + constexpr void emplace_back(__Args&&... __args) + requires(constructible_from<__T, __Args...>) + { + if (!try_emplace_back(::std::forward<__Args>(__args)...)) [[unlikely]] throw bad_alloc(); + } + constexpr __T& push_back(const __T& __x) + requires(constructible_from<__T, const __T&>) + { + emplace_back(__x); + return back(); + } + constexpr __T& push_back(__T&& __x) + requires(constructible_from<__T, __T&&>) + { + emplace_back(::std::forward<__T&&>(__x)); + return back(); + } + + constexpr __T* try_push_back(const __T& __x) + requires(constructible_from<__T, const __T&>) + { + return try_emplace_back(__x); + } + constexpr __T* try_push_back(__T&& __x) + requires(constructible_from<__T, __T&&>) + { + return try_emplace_back(::std::forward<__T&&>(__x)); + } + + constexpr __T& unchecked_push_back(const __T& __x) + requires(constructible_from<__T, const __T&>) + { + return unchecked_emplace_back(__x); + } + constexpr __T& unchecked_push_back(__T&& __x) + requires(constructible_from<__T, __T&&>) + { + return unchecked_emplace_back(::std::forward<__T&&>(__x)); + } + + template<__iv_detail::__container_compatible_range<__T> __R> + constexpr void append_range(__R&& __rg) + requires(constructible_from<__T, ranges::range_reference_t<__R>>) + { + if constexpr(ranges::sized_range<__R>) { + if (size() + ranges::size(__rg) > capacity()) [[unlikely]] throw bad_alloc(); + } + for (auto&& __e : __rg) { + if (size() == capacity()) [[unlikely]] throw bad_alloc(); + emplace_back(::std::forward(__e)); + } + } + + template + constexpr iterator emplace(const_iterator __position, __Args&&... __args) + requires(constructible_from<__T, __Args...> && movable<__T>) + { + __assert_iterator_in_range(__position); + auto __b = end(); + emplace_back(std::forward<__Args>(__args)...); + auto __pos = begin() + (__position - begin()); + rotate(__pos, __b, end()); + return __pos; + } + + template + constexpr iterator insert(const_iterator __position, __InputIterator __first, __InputIterator __last) + requires(constructible_from<__T, iter_reference_t<__InputIterator>> && movable<__T>) + { + __assert_iterator_in_range(__position); + __assert_valid_iterator_pair(__first, __last); + if constexpr(random_access_iterator<__InputIterator>) { + if (size() + static_cast(distance(__first, __last)) > capacity()) [[unlikely]] throw bad_alloc{}; + } + auto __b = end(); + for (; __first != __last; ++__first) emplace_back(::std::move(*__first)); + auto __pos = begin() + (__position - begin()); + rotate(__pos, __b, end()); + return __pos; + } + + template<__iv_detail::__container_compatible_range<__T> __R> + constexpr iterator insert_range(const_iterator __position, __R&& __rg) + requires(constructible_from<__T, ranges::range_reference_t<__R>> && movable<__T>) + { + return insert(__position, ::std::begin(__rg), ::std::end(__rg)); + } + + constexpr iterator insert(const_iterator __position, initializer_list<__T> __il) + requires(constructible_from<__T, ranges::range_reference_t>> && movable<__T>) + { + return insert_range(__position, __il); + } + + constexpr iterator insert(const_iterator __position, size_type __n, const __T& __x) + requires(constructible_from<__T, const __T&> && copyable<__T>) + { + __assert_iterator_in_range(__position); + auto __b = end(); + for (size_type __i = 0; __i < __n; ++__i) emplace_back(__x); + auto __pos = begin() + (__position - begin()); + rotate(__pos, __b, end()); + return __pos; + } + + constexpr iterator insert(const_iterator __position, const __T& __x) + requires(constructible_from<__T, const __T&> && copyable<__T>) + { + return insert(__position, 1, __x); + } + + constexpr iterator insert(const_iterator __position, __T&& __x) + requires(constructible_from<__T, __T&&> && movable<__T>) + { + return emplace(__position, ::std::move(__x)); + } + + constexpr inplace_vector(initializer_list<__T> __il) + requires(constructible_from<__T, ranges::range_reference_t>> && movable<__T>) + { + insert(begin(), __il); + } + + constexpr inplace_vector(size_type __n, const __T& __value) + requires(constructible_from<__T, const __T&> && copyable<__T>) + { + insert(begin(), __n, __value); + } + + constexpr explicit inplace_vector(size_type __n) + requires(constructible_from<__T, __T&&> && default_initializable<__T>) + { + for (size_type __i = 0; __i < __n; ++__i) emplace_back(__T{}); + } + + template // BUGBUG: why not ranges::input_iterator? + constexpr inplace_vector(__InputIterator __first, __InputIterator __last) + requires(constructible_from<__T, iter_reference_t<__InputIterator>> && movable<__T>) + { + insert(begin(), __first, __last); + } + + template <__iv_detail::__container_compatible_range<__T> __R> + constexpr inplace_vector(from_range_t, __R&& __rg) + requires(constructible_from<__T, ranges::range_reference_t<__R>> && movable<__T>) + { + insert_range(begin(), std::forward<__R&&>(__rg)); + } + + constexpr iterator erase(const_iterator __first, const_iterator __last) + requires(movable<__T>) + { + __assert_iterator_pair_in_range(__first, __last); + iterator __f = begin() + (__first - begin()); + if (__first != __last) { + __unsafe_destroy(::std::move(__f + (__last - __first), end(), __f), end()); + __unsafe_set_size(size() - static_cast(__last - __first)); + } + return __f; + } + + constexpr iterator erase(const_iterator __position) requires(movable<__T>) + { return erase(__position, __position + 1); } + + constexpr void clear() noexcept { + __unsafe_destroy(begin(), end()); + __unsafe_set_size(0); + } + + constexpr void resize(size_type __sz, const __T& __c) + requires(constructible_from<__T, const __T&> && copyable<__T>) + { + if (__sz == size()) return; + else if (__sz > __N) [[unlikely]] throw bad_alloc{}; + else if (__sz > size()) insert(end(), __sz - size(), __c); + else { + __unsafe_destroy(begin() + __sz, end()); + __unsafe_set_size(__sz); + } + } + constexpr void resize(size_type __sz) + requires(constructible_from<__T, __T&&> && default_initializable<__T>) + { + if (__sz == size()) return; + else if (__sz > __N) [[unlikely]] throw bad_alloc{}; + else if (__sz > size()) while(size() != __sz) emplace_back(__T{}); + else { + __unsafe_destroy(begin() + __sz, end()); + __unsafe_set_size(__sz); + } + } + + constexpr reference at(size_type __pos) { + if (__pos >= size()) [[unlikely]] throw out_of_range("inplace_vector::at"); + return __iv_detail::__index(*this, __pos); + } + constexpr const_reference at(size_type __pos) const { + if (__pos >= size()) [[unlikely]] throw out_of_range("inplace_vector::at"); + return __iv_detail::__index(*this, __pos); + } + + constexpr void pop_back() + { + __IV_EXPECT(size() > 0 && "pop_back from empty inplace_vector!"); + __unsafe_destroy(end() - 1, end()); + __unsafe_set_size(size() - 1); + } + + constexpr inplace_vector(const inplace_vector& __x) + requires(copyable<__T>) + { + for (auto&& __e : __x) emplace_back(__e); + } + constexpr inplace_vector(inplace_vector&& __x) noexcept + requires(movable<__T>) + { + for (auto&& __e : __x) emplace_back(::std::move(__e)); + } + constexpr inplace_vector& operator=(const inplace_vector& __x) + requires(copyable<__T>) + { + clear(); + for (auto&& __e : __x) emplace_back(__e); + return *this; + } + constexpr inplace_vector& operator=(inplace_vector&& __x) noexcept + requires(movable<__T>) + { + clear(); + for (auto&& __e : __x) emplace_back(::std::move(__e)); + return *this; + } + + constexpr void swap(inplace_vector& __x) + noexcept(__N == 0 || (is_nothrow_swappable_v<__T> && is_nothrow_move_constructible_v<__T>)) + requires(movable<__T>) + { + auto tmp = ::std::move(__x); + __x = ::std::move(*this); + (*this) = ::std::move(tmp); + } + + template + constexpr void assign(__InputIterator __first, __InputIterator __last) + requires(constructible_from<__T, iter_reference_t<__InputIterator>> && movable<__T>) + { + clear(); + insert(begin(), __first, __last); + } + template<__iv_detail::__container_compatible_range<__T> __R> + constexpr void assign_range(__R&& __rg) + requires(constructible_from<__T, ranges::range_reference_t<__R>> && movable<__T>) + { + assign(begin(__rg), end(__rg)); + } + constexpr void assign(size_type __n, const __T& __u) + requires(constructible_from<__T, const __T&> && movable<__T>) + { + clear(); + insert(begin(), __n, __u); + } + constexpr void assign(initializer_list<__T> __il) + requires(constructible_from<__T, ranges::range_reference_t>> && movable<__T>) + { + clear(); + insert_range(begin(), __il); + } + + constexpr friend int /*synth-three-way-result*/ + operator<=>(const inplace_vector& __x, const inplace_vector& __y) + { + if (__x.size() < __y.size()) return -1; + if (__x.size() > __y.size()) return +1; + + bool __all_equal = true; + bool __all_less = true; + for (size_type __i = 0; __i < __x.size(); ++__i) { + if (__x[__i] < __y[__i]) __all_equal = false; + if (__x[__i] == __y[__i]) __all_less = false; + } + + if (__all_equal) return 0; + if (__all_less) return -1; + return 1; + } +}; + +} // namespace std + +// undefine all the internal macros +#undef __IV_ASSUME +#undef __IV_ASSERT +#undef __IV_EXPECT +#endif // INPLACE_VECTOR + +#endif // defined(__cpp_lib_inplace_vector) +#endif // INPLACE_VECTOR_LOADER diff --git a/server/include/utilities/std/views_concat.h b/server/include/utilities/std/views_concat.h new file mode 100644 index 000000000..6c217496f --- /dev/null +++ b/server/include/utilities/std/views_concat.h @@ -0,0 +1,978 @@ +#ifndef VIEWS_CONCAT_LOADER +#define VIEWS_CONCAT_LOADER + +#include + +// If our STL supports the C++26 std::views::concat, just use that one. +#if defined(__cpp_lib_ranges_concat) +#include +#else + +//===----------------------------------------------------------------------===// +// +// Under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +// Copyright (c) Hui Xie, S. Levent Yilmaz +#ifndef LIBCPP_RANGE_CONCAT_UTILS_HPP +#define LIBCPP_RANGE_CONCAT_UTILS_HPP +#include +#include +#include +#include + +namespace std::ranges::concat_detail +{ + +namespace tuple_or_pair_test +{ +template +auto test() -> std::pair; + +template + requires(sizeof...(Ts) != 2) auto test() -> std::tuple; +} // namespace tuple_or_pair_test + +// exposition only utilities from zip_view (zip_view is not implemented yet in libc++) +// http://eel.is/c++draft/ranges#range.zip.view (perhaps we can reuse in the spec) +// this paper proposed it to be moved out of zip_view +// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2374r3.html +template +using tuple_or_pair = decltype(tuple_or_pair_test::test()); + +template +constexpr auto tuple_transform(F&& f, Tuple&& tuple) +{ + return apply( + [&](Ts && ... elements) + { + return tuple_or_pair...>( + invoke(f, std::forward(elements))...); + }, + std::forward(tuple)); +} + + +} // namespace std::ranges::concat_detail + + + +// Exposition only utilities, normalize cross-compiler: +#ifdef _MSC_VER +namespace std::ranges +{ +template +using __maybe_const = _Maybe_const; + +template +concept __has_arrow = _Has_arrow; + +template +concept __simple_view = _Simple_view; +} // namespace std::ranges +#endif + +#if defined(__GNUC__) && !defined(_LIBCPP_VERSION) + +namespace std::ranges +{ + +template +using __maybe_const = __detail::__maybe_const_t; + +template +concept __has_arrow = __detail::__has_arrow; + +template +concept __simple_view = __detail::__simple_view; +} // namespace std::ranges + +#endif + +#if defined(_LIBCPP_VERSION) + +namespace std::ranges +{ + +template +concept __has_arrow = std::__has_arrow; + +} +#endif + +#endif + + +//===----------------------------------------------------------------------===// +// +// Under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +// Copyright (c) Hui Xie, S. Levent Yilmaz +#ifndef LIBCPP__RANGE_CONCAT_HPP +#define LIBCPP__RANGE_CONCAT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +// #include "utils.hpp" + +namespace std::ranges +{ + +namespace xo +{ // exposition only things (and persevering face) + +template +concept all_random_access = (random_access_range<__maybe_const> && + ...); +template +concept all_bidirectional = (bidirectional_range<__maybe_const> && + ...); +template +concept all_forward = (forward_range<__maybe_const> && ...); + +inline namespace not_to_spec +{ + +template +using concat_reference_t = common_reference_t...>; + +template +using concat_rvalue_reference_t = +common_reference_t...>; + +template +using concat_value_t = common_type_t...>; + +// clang-format off +template +concept concat_indirectly_readable_impl = requires (const It it) +{ + { *it } -> convertible_to; + { ranges::iter_move(it) } -> convertible_to; +}; + +template +concept concat_indirectly_readable = +common_reference_with&&, concat_value_t&>&& +common_reference_with&&, concat_rvalue_reference_t&&>&& +common_reference_with&&, concat_value_t const&> && +(concat_indirectly_readable_impl, concat_rvalue_reference_t, iterator_t> && ...); +// clang-format on + +} // namespace not_to_spec + +// clang-format off +template +concept concatable = requires { + typename concat_reference_t; + typename concat_value_t; + typename concat_rvalue_reference_t; +}&& concat_indirectly_readable; +// clang-format on + +static_assert(true); // clang-format badness + +inline namespace not_to_spec +{ + +template +using back = tuple_element_t>; + +template +consteval bool all_but_last_impl(std::index_sequence) +{ + return ((I == sizeof...(I) - 1 || b) && ...); +} + +template +constexpr bool all_but_last = +all_but_last_impl(make_index_sequence{}); + +template +consteval bool all_but_first_impl(std::index_sequence) +{ + return ((I == 0 || b) && ...); +} + +template +constexpr bool all_but_first = +all_but_first_impl(make_index_sequence{}); + +} // namespace not_to_spec + +template +concept concat_is_random_access = +(all_random_access && ...) && +(all_but_last>...>); + +template +concept concat_bidirectional = all_but_last>...>&& +all_bidirectional; + +static_assert(true); // clang-format badness + +inline namespace not_to_spec +{ + +// it is not defined in the standard and we can't refer to it. +template +concept has_member_arrow = requires(T it) +{ + it.operator->(); +}; + +// iterator_traits::pointer when present gives the result of arrow, or +// presumably the result of arrow is convertible to that. + +// when iterator_traits::pointer is not present for not-a-C++17-iterators, +// we should get the pointer type from the arrow expression: +template <__has_arrow It> +decltype(auto) get_arrow_result(It&& it) +{ + if constexpr (has_member_arrow) + { + return static_cast(it).operator->(); + } + else + { + return static_cast(it); + } +} +template +void get_arrow_result(It&&); + +template +struct PointerTrait +{ + using type = decltype(get_arrow_result(declval())); +}; + +template + requires requires { typename iterator_traits>::pointer; } +struct PointerTrait +{ + using type = typename iterator_traits>::pointer; +}; + +template +using range_pointer_t = typename PointerTrait>::type; + +template +using concat_pointer = common_type...>; +// using concat_pointer = common_type>::pointer...>; +// ^^ hard fails for not-a-c17iterator + +} // namespace not_to_spec + +template +using concat_pointer_t = typename concat_pointer::type; + +// clang-format off +template +concept concat_has_arrow = +(__has_arrow> && ...) && + requires { typename concat_pointer_t; } && +(convertible_to&, concat_pointer_t> && ...) +; +// clang-format on + +inline namespace not_to_spec +{ + +template +consteval auto iterator_concept_test() +{ + if constexpr (concat_is_random_access) + { + return random_access_iterator_tag{}; + } + else if constexpr (concat_bidirectional) + { + return bidirectional_iterator_tag{}; + } + else if constexpr ((forward_range<__maybe_const> && ...)) + { + return forward_iterator_tag{}; + } + else + { + return input_iterator_tag{}; + } +} + +// calls f(integral_constant{}) for a runtime idx in [0,N) +template +constexpr auto visit_i_impl(size_t idx, Var&& v, F&& f) +{ + assert(idx < N); + if constexpr (N > 1) + { + return idx == N - 1 + ? invoke(static_cast(f), integral_constant{}, + std::get(static_cast(v))) + : visit_i_impl(idx, static_cast(v), + static_cast(f)); + } + else + { + return invoke(static_cast(f), integral_constant{}, + std::get<0>(static_cast(v))); + } +} + +// calls f(integral_constant{}, get(v)) for idx == v.index(). +template +constexpr auto visit_i(Var&& v, F&& f) +{ + return visit_i_impl>>( + v.index(), static_cast(v), static_cast(f)); +} + +template +concept has_tag = +derived_from>::iterator_category>; + +template +consteval auto iter_cat_test() +{ + using reference = + common_reference_t>...>; + if constexpr (!is_reference_v) + { + return input_iterator_tag{}; + } + else if constexpr ((has_tag> && + ...) && + concat_is_random_access) + { + return random_access_iterator_tag{}; + } + else if constexpr ((has_tag> && + ...) && + concat_bidirectional) + { + return bidirectional_iterator_tag{}; + } + else if constexpr ((has_tag> && + ...)) + { + return forward_iterator_tag{}; + } + else + { + return input_iterator_tag{}; + } +} + +struct empty_ {}; + +template +struct iter_cat_base +{ + using iterator_category = decltype(iter_cat_test()); +}; + +template +consteval auto iter_cat_base_sel() -> empty_; + +template +consteval auto iter_cat_base_sel() -> iter_cat_base + requires((forward_range<__maybe_const> && ...)); + +template +using iter_cat_base_t = decltype(iter_cat_base_sel()); + +} // namespace not_to_spec + +} // namespace xo + +// clang-format off +// [TODO] constrain less and allow just a `view`? (i.e. including output_range in the mix - need an example) +template + requires (view&&...) && (sizeof...(Views) > 0) && xo::concatable +class concat_view : public view_interface> +{ + // clang-format on + tuple views_; // exposition only + + template + class iterator : public xo::iter_cat_base_t + { + public: + using value_type = xo::concat_value_t<__maybe_const...>; + using difference_type = + common_type_t>...>; + using iterator_concept = + decltype(xo::iterator_concept_test()); + + private: + using ParentView = __maybe_const; + using BaseIt = variant>...>; + + ParentView* parent_ = nullptr; + BaseIt it_; + + friend class iterator; + friend class concat_view; + + template + constexpr void satisfy() + { + if constexpr (N < (sizeof...(Views) - 1)) + { + if (get(it_) == ranges::end(get(parent_->views_))) + { + it_.template emplace( + ranges::begin(get(parent_->views_))); + satisfy(); + } + } + } + + template + constexpr void prev() + { + if constexpr (N == 0) + { + --get<0>(it_); + } + else + { + if (get(it_) == ranges::begin(get(parent_->views_))) + { + it_.template emplace( + ranges::end(get(parent_->views_))); + prev(); + } + else + { + --get(it_); + } + } + } + + template + constexpr void advance_fwd(difference_type current_offset, + difference_type steps) + { + using underlying_diff_type = + std::iter_difference_t>; + if constexpr (N == sizeof...(Views) - 1) + { + get(it_) += static_cast(steps); + } + else + { + static_assert( + std::ranges::common_range(parent_->views_))>); + auto n_size = ranges::distance(get(parent_->views_)); + if (current_offset + steps < n_size) + { + get(it_) += static_cast(steps); + } + else + { + it_.template emplace( + ranges::begin(get(parent_->views_))); + advance_fwd( + 0, current_offset + steps - n_size); + } + } + } + + template + constexpr void advance_bwd(difference_type current_offset, + difference_type steps) + { + using underlying_diff_type = + std::iter_difference_t>; + if constexpr (N == 0) + { + get(it_) -= static_cast(steps); + } + else + { + if (current_offset >= steps) + { + get(it_) -= static_cast(steps); + } + else + { + static_assert( + std::ranges::common_range(parent_->views_))>); + auto prev_size = ranges::distance(get(parent_->views_)); + it_.template emplace(ranges::end(get(parent_->views_))); + advance_bwd(prev_size, steps - current_offset); + } + } + } + + decltype(auto) get_parent_views() const { return (parent_->views_); } + + template + explicit constexpr iterator(ParentView* parent, Args&&... args) requires + constructible_from + : parent_{ parent }, it_{ static_cast(args)... } + { + } + + public: + iterator() = default; + + constexpr iterator(iterator i) requires Const && + (convertible_to, iterator_t>&&...) + // [TODO] noexcept specs? + : parent_{ i.parent_ }, it_{ xo::visit_i(std::move(i.it_), [](auto I, auto&& it) + { +return BaseIt(in_place_index, std::move(it)); +}) } + { + } + + constexpr decltype(auto) operator*() const + { + using reference = xo::concat_reference_t<__maybe_const...>; + return visit([](auto&& it) -> reference { return *it; }, it_); + } + + constexpr auto operator->() + const requires xo::concat_has_arrow<__maybe_const...> + { + return visit( + [](auto const& it) + -> xo::concat_pointer_t<__maybe_const...> + { + using It = remove_reference_t; + if constexpr (xo::has_member_arrow) + { + return it.operator->(); + } + else + { + static_assert(is_pointer_v); + return it; + } + }, + it_); + } + + constexpr iterator& operator++() + { + xo::visit_i(it_, [this](auto I, auto&& it) + { + ++it; + this->satisfy(); + }); + return *this; + } + + constexpr void operator++(int) { ++*this; } + + constexpr iterator operator++(int) requires( + forward_range<__maybe_const>&&...) + { + auto tmp = *this; + ++*this; + return tmp; + } + + constexpr iterator& operator--() requires + xo::concat_bidirectional + { + xo::visit_i(it_, [this](auto I, auto&&) { this->prev(); }); + return *this; + } + + constexpr iterator operator--( + int) requires xo::concat_bidirectional + { + auto tmp = *this; + --*this; + return tmp; + } + + constexpr iterator& operator+=(difference_type n) // + requires xo::concat_is_random_access + { + if (n > 0) + { + xo::visit_i(it_, [this, n](auto I, auto&& it) + { + this->advance_fwd(it - ranges::begin(get(parent_->views_)), n); + }); + } + else if (n < 0) + { + xo::visit_i(it_, [this, n](auto I, auto&& it) + { + this->advance_bwd(it - ranges::begin(get(parent_->views_)), -n); + }); + } + return *this; + } + + constexpr iterator& operator-=(difference_type n) // + requires xo::concat_is_random_access + { + *this += -n; + return *this; + } + + constexpr decltype(auto) operator[](difference_type n) const // + requires xo::concat_is_random_access + { + return *((*this) + n); + } + + friend constexpr bool + operator==(const iterator& it1, const iterator& it2) requires( + equality_comparable>>&&...) + { + return it1.it_ == it2.it_; + } + + friend constexpr bool operator==(const iterator& it, + const default_sentinel_t&) + { + constexpr auto LastIdx = sizeof...(Views) - 1; + return it.it_.index() == LastIdx && + get(it.it_) == + ranges::end(get(it.get_parent_views())); + } + + friend constexpr bool + operator<(const iterator& x, const iterator& y) requires( + random_access_range<__maybe_const>&&...) + { + return x.it_ < y.it_; + } + + friend constexpr bool + operator>(const iterator& x, const iterator& y) requires( + random_access_range<__maybe_const>&&...) + { + return y < x; + } + + friend constexpr bool + operator<=(const iterator& x, const iterator& y) requires( + random_access_range<__maybe_const>&&...) + { + return !(y < x); + } + + friend constexpr bool + operator>=(const iterator& x, const iterator& y) requires( + random_access_range<__maybe_const>&&...) + { + return !(x < y); + } + + friend constexpr auto + operator<=>(const iterator& x, const iterator& y) requires( + (random_access_range<__maybe_const>&& + three_way_comparable>>)&&...) + { + return x.it_ <=> y.it_; + } + + friend constexpr iterator operator+(const iterator& it, + difference_type n) requires + xo::concat_is_random_access + { + auto temp = it; + temp += n; + return temp; + } + + friend constexpr iterator operator+(difference_type n, + const iterator& it) requires + xo::concat_is_random_access + { + return it + n; + } + + friend constexpr iterator operator-(const iterator& it, + difference_type n) requires + xo::concat_is_random_access + { + auto temp = it; + temp -= n; + return temp; + } + + friend constexpr difference_type operator-(const iterator& x, + const iterator& y) requires + xo::concat_is_random_access + { + auto ix = x.it_.index(); + auto iy = y.it_.index(); + if (ix > iy) + { + // distance(y, yend) + size(ranges_in_between)... + distance(xbegin, x) + const auto all_sizes = std::apply( + [&](const auto&... views) + { + const auto getSize = [](const auto& view) + { + if constexpr (ranges::common_range>) + { + return ranges::distance(view); + } + else + { + return 0; // only the last range can be non common, and its + // value is not used + } + }; + return std::array{ + static_cast(getSize(views))... }; + }, + x.get_parent_views()); + auto in_between = + std::accumulate(all_sizes.data() + iy + 1, all_sizes.data() + ix, + difference_type(0)); + + auto y_to_end = xo::visit_i(y.it_, [&](auto I, auto&& it) + { + return ranges::distance(it, ranges::end(get(y.get_parent_views()))); + }); + + auto begin_to_x = xo::visit_i(x.it_, [&](auto I, auto&& it) + { + return it - ranges::begin(get(x.get_parent_views())); + }); + + return y_to_end + in_between + begin_to_x; + + } + else if (ix < iy) + { + return -(y - x); + } + else + { + return xo::visit_i(x.it_, [&](auto I, auto&&) + { + return get(x.it_) - get(y.it_); + }); + } + } + + friend constexpr difference_type + operator-(const iterator& it, default_sentinel_t) requires + (sized_sentinel_for>, + iterator_t<__maybe_const>> && ...) + && (xo::all_but_first>...>) + { + const auto idx = it.it_.index(); + const auto all_sizes = std::apply( + [&](const auto&... views) + { + return std::array{ + static_cast(ranges::distance(views))... }; + }, + it.get_parent_views()); + auto to_the_end = std::accumulate(all_sizes.begin() + idx + 1, + all_sizes.end(), difference_type(0)); + + auto i_to_idx_end = xo::visit_i(it.it_, [&](auto I, auto&& i) + { + return ranges::distance(i, ranges::end(get(it.get_parent_views()))); + }); + return -(i_to_idx_end + to_the_end); + } + + friend constexpr difference_type + operator-(default_sentinel_t, const iterator& it) requires + (sized_sentinel_for>, + iterator_t<__maybe_const>> && ...) + && (xo::all_but_first>...>) + { + return -(it - default_sentinel); + } + + friend constexpr decltype(auto) iter_move(iterator const& ii) noexcept( + ((std::is_nothrow_invocable_v< + decltype(ranges::iter_move), + const iterator_t<__maybe_const>&>&& + std::is_nothrow_convertible_v< + range_rvalue_reference_t<__maybe_const>, + xo::concat_rvalue_reference_t<__maybe_const...>>)&&...)) + { + return std::visit( + [](auto const& i) -> xo::concat_rvalue_reference_t< + __maybe_const...> + { // + return ranges::iter_move(i); + }, + ii.it_); + } + + friend constexpr void + iter_swap(const iterator& x, const iterator& y) requires + swappable_with, iter_reference_t> && + (...&& indirectly_swappable>>) + // todo: noexcept? + { + std::visit( + [&](const auto& it1, const auto& it2) + { + if constexpr (std::is_same_v) + { + ranges::iter_swap(it1, it2); + } + else + { + ranges::swap(*x, *y); + } + }, + x.it_, y.it_); + } + }; + +public: + constexpr concat_view() = default; + + constexpr explicit concat_view(Views... views) + : views_{ static_cast(views)... } + { + } + + constexpr iterator begin() requires(!(__simple_view && ...)) // + { + iterator it(this, in_place_index<0u>, ranges::begin(get<0>(views_))); + it.template satisfy<0>(); + return it; + } + + constexpr iterator begin() const + requires((range && ...) && + xo::concatable) // + { + iterator it(this, in_place_index<0u>, ranges::begin(get<0>(views_))); + it.template satisfy<0>(); + return it; + } + + constexpr auto end() requires(!(__simple_view && ...)) + { + using LastView = xo::back; + if constexpr (common_range) + { + constexpr auto N = sizeof...(Views); + return iterator(this, in_place_index, + ranges::end(get(views_))); + } + else + { + return default_sentinel; + } + } + + constexpr auto end() const requires((range&&...) && + xo::concatable) + { + using LastView = xo::back; + if constexpr (common_range) + { + constexpr auto N = sizeof...(Views); + return iterator(this, in_place_index, + ranges::end(get(views_))); + } + else + { + return default_sentinel; + } + } + + constexpr auto size() requires(sized_range&&...) + { + return apply( + [](auto... sizes) + { + using CT = make_unsigned_t>; + return (CT(sizes) + ...); + }, + concat_detail::tuple_transform(ranges::size, views_)); + } + + constexpr auto size() const requires(sized_range&&...) + { + return apply( + [](auto... sizes) + { + using CT = make_unsigned_t>; + return (CT{ 0 } + ... + CT{ sizes }); + }, + concat_detail::tuple_transform(ranges::size, views_)); + } +}; + +template +concat_view(R&&...) -> concat_view...>; + +// cpo: + +namespace views +{ +namespace xo +{ +class concat_fn +{ +public: + constexpr void operator()() const = delete; + + template + constexpr auto operator()(V&& v) const + noexcept(noexcept(std::views::all(static_cast(v)))) + { + return std::views::all(static_cast(v)); + } + + template + requires(sizeof...(V) > 1) && ranges::xo::concatable...> && + (viewable_range && ...) // + constexpr auto + operator()(V&&... v) + const + { // noexcept(noexcept(concat_view{static_cast(v)...})) { + return concat_view{ static_cast(v)... }; + } +}; +} // namespace xo + +inline constexpr xo::concat_fn concat; +} // namespace views + +} // namespace std::ranges + +#endif + + +namespace std::ranges::views +{ // HACK HACK +using namespace xo; +} + +#endif // defined(__cpp_lib_ranges_concat) +#endif // VIEWS_CONCAT_LOADER diff --git a/server/src/Account.cpp b/server/src/Account.cpp deleted file mode 100644 index c5e9e3eba..000000000 --- a/server/src/Account.cpp +++ /dev/null @@ -1,684 +0,0 @@ -#include - -#include -#include -#include - -#include "BabyDI.h" - -#include "Account.h" -#include "FileSystem.h" -#include "Server.h" - -/* - Account: Constructor - Deconstructor -*/ -Account::Account() = default; - -Account::~Account() = default; - -/* - Account: Load/Save Account -*/ -void Account::reset() -{ - if (!m_accountName.isEmpty()) - { - CString acc(m_accountName); - loadAccount("defaultaccount"); - m_accountName = acc; - saveAccount(); - } -} - -bool Account::loadAccount(const CString& pAccount, bool ignoreNickname) -{ - // Just in case this account was loaded offline through RC. - m_accountName = pAccount; - - bool loadedFromDefault = false; - FileSystem* accfs = m_server->getAccountsFileSystem(); - std::vector fileData; - - // Find the account in the file system. - CString accpath(accfs->findi(CString() << pAccount << ".txt")); - if (accpath.length() == 0) - { - accpath = m_server->getServerPath() << "accounts/defaultaccount.txt"; - FileSystem::fixPathSeparators(accpath); - loadedFromDefault = true; - } - - // Load file. - fileData = CString::loadToken(accpath, "\n"); - if (fileData.empty() || fileData[0].trim() != "GRACC001") - return false; - - // Clear Lists - for (auto& i: m_character.ganiAttributes) i.clear(); - m_chestList.clear(); - m_flagList.clear(); - m_weaponList.clear(); - m_privateMessageServerList.clear(); - std::vector folderList; - - // Parse File - for (auto& i: fileData) - { - // Trim Line - i.trimI(); - - // Declare Variables; - CString section, val; - int sep; - - // Seperate Section & Value - sep = i.find(' '); - section = i.subString(0, sep); - if (sep != -1) - val = i.subString(sep + 1); - - if (section == "NAME") continue; - else if (section == "NICK") - { - if (!ignoreNickname) m_character.nickName = val.subString(0, 223).toString(); - } - else if (section == "COMMUNITYNAME") - m_communityName = val; - else if (section == "LEVEL") - m_levelName = val; - else if (section == "X") { setX(strtofloat(val)); } - else if (section == "Y") { setY(strtofloat(val)); } - else if (section == "Z") { setZ(strtofloat(val)); } - else if (section == "MAXHP") - setMaxPower(strtoint(val)); - else if (section == "HP") - setPower((float)strtofloat(val)); - else if (section == "RUPEES") - m_character.gralats = strtoint(val); - else if (section == "ANI") - setGani(val); - else if (section == "ARROWS") - m_character.arrows = strtoint(val); - else if (section == "BOMBS") - m_character.bombs = strtoint(val); - else if (section == "GLOVEP") - m_character.glovePower = strtoint(val); - else if (section == "SHIELDP") - setShieldPower(strtoint(val)); - else if (section == "SWORDP") - setSwordPower(strtoint(val)); - else if (section == "BOWP") - m_character.bowPower = strtoint(val); - else if (section == "BOW") - m_character.bowImage = val; - else if (section == "HEAD") - setHeadImage(val); - else if (section == "BODY") - setBodyImage(val); - else if (section == "SWORD") - setSwordImage(val); - else if (section == "SHIELD") - setShieldImage(val); - else if (section == "COLORS") - { - std::vector t = val.tokenize(","); - for (int i = 0; i < (int)t.size() && i < 5; i++) m_character.colors[i] = (unsigned char)strtoint(t[i]); - } - else if (section == "SPRITE") - m_character.sprite = strtoint(val); - else if (section == "STATUS") - m_status = strtoint(val); - else if (section == "MP") - m_mp = strtoint(val); - else if (section == "AP") - m_character.ap = strtoint(val); - else if (section == "APCOUNTER") - m_apCounter = strtoint(val); - else if (section == "ONSECS") - m_onlineTime = strtoint(val); - else if (section == "IP") - { - if (m_accountIp == 0) m_accountIp = strtolong(val); - } - else if (section == "LANGUAGE") - { - m_language = val; - if (m_language.isEmpty()) m_language = "English"; - } - else if (section == "KILLS") - m_kills = strtoint(val); - else if (section == "DEATHS") - m_deaths = strtoint(val); - else if (section == "RATING") - m_eloRating = (float)strtofloat(val); - else if (section == "DEVIATION") - m_eloDeviation = (float)strtofloat(val); - else if (section == "LASTSPARTIME") - m_lastSparTime = strtolong(val); - else if (section == "FLAG") - setFlag(val); - else if (section == "ATTR1") - m_character.ganiAttributes[0] = val; - else if (section == "ATTR2") - m_character.ganiAttributes[1] = val; - else if (section == "ATTR3") - m_character.ganiAttributes[2] = val; - else if (section == "ATTR4") - m_character.ganiAttributes[3] = val; - else if (section == "ATTR5") - m_character.ganiAttributes[4] = val; - else if (section == "ATTR6") - m_character.ganiAttributes[5] = val; - else if (section == "ATTR7") - m_character.ganiAttributes[6] = val; - else if (section == "ATTR8") - m_character.ganiAttributes[7] = val; - else if (section == "ATTR9") - m_character.ganiAttributes[8] = val; - else if (section == "ATTR10") - m_character.ganiAttributes[9] = val; - else if (section == "ATTR11") - m_character.ganiAttributes[10] = val; - else if (section == "ATTR12") - m_character.ganiAttributes[11] = val; - else if (section == "ATTR13") - m_character.ganiAttributes[12] = val; - else if (section == "ATTR14") - m_character.ganiAttributes[13] = val; - else if (section == "ATTR15") - m_character.ganiAttributes[14] = val; - else if (section == "ATTR16") - m_character.ganiAttributes[15] = val; - else if (section == "ATTR17") - m_character.ganiAttributes[16] = val; - else if (section == "ATTR18") - m_character.ganiAttributes[17] = val; - else if (section == "ATTR19") - m_character.ganiAttributes[18] = val; - else if (section == "ATTR20") - m_character.ganiAttributes[19] = val; - else if (section == "ATTR21") - m_character.ganiAttributes[20] = val; - else if (section == "ATTR22") - m_character.ganiAttributes[21] = val; - else if (section == "ATTR23") - m_character.ganiAttributes[22] = val; - else if (section == "ATTR24") - m_character.ganiAttributes[23] = val; - else if (section == "ATTR25") - m_character.ganiAttributes[24] = val; - else if (section == "ATTR26") - m_character.ganiAttributes[25] = val; - else if (section == "ATTR27") - m_character.ganiAttributes[26] = val; - else if (section == "ATTR28") - m_character.ganiAttributes[27] = val; - else if (section == "ATTR29") - m_character.ganiAttributes[28] = val; - else if (section == "ATTR30") - m_character.ganiAttributes[29] = val; - else if (section == "WEAPON") - m_weaponList.push_back(val); - else if (section == "CHEST") - m_chestList.push_back(val); - else if (section == "BANNED") - m_isBanned = (strtoint(val) == 0 ? false : true); - else if (section == "BANREASON") - m_banReason = val; - else if (section == "BANLENGTH") - m_banLength = val; - else if (section == "COMMENTS") - m_accountComments = val; - else if (section == "EMAIL") - m_email = val; - else if (section == "LOCALRIGHTS") - m_adminRights = strtoint(val); - else if (section == "IPRANGE") - m_adminIp = val; - else if (section == "LOADONLY") - m_isLoadOnly = (strtoint(val) == 0 ? false : true); - else if (section == "FOLDERRIGHT") - folderList.push_back(val); - else if (section == "LASTFOLDER") - m_lastFolder = val; - } - - setFolderRights(folderList); - - // If this is a guest account, loadonly is set to true. - if (pAccount.toLower() == "guest") - { - m_isLoadOnly = true; - m_isGuest = true; - srand((unsigned int)time(0)); - - // Try to create a unique account number. - while (true) - { - int v = (rand() * rand()) % 9999999; - if (m_server->getPlayer("pc:" + CString(v).subString(0, 6), PLTYPE_ANYPLAYER) == 0) - { - m_communityName = "pc:" + CString(v).subString(0, 6); - break; - } - } - } - - // Comment out this line if you are actually going to use community names. - if (pAccount.toLower() == "guest") - { - // The PC:123123123 should only be sent to other players, the logged in player should see it as guest. - // Setting it back to only show as guest to everyone until that's fixed. - m_accountName = m_communityName; - m_communityName = "guest"; - } - else - m_communityName = m_accountName; - - // If we loaded from the default account... - if (loadedFromDefault) - { - auto& settings = m_server->getSettings(); - - // Check to see if we are overriding our start level and position. - if (settings.exists("startlevel")) - m_levelName = settings.getStr("startlevel", "onlinestartlocal.nw"); - if (settings.exists("startx")) - { - setX(settings.getFloat("startx", 30.0f)); - } - if (settings.exists("starty")) - { - setY(settings.getFloat("starty", 30.5f)); - } - - // Save our account now and add it to the file system. - if (!m_isLoadOnly) - { - saveAccount(); - accfs->addFile(CString() << "accounts/" << pAccount << ".txt"); - } - } - - return true; -} - -bool Account::saveAccount() -{ - // Don't save 'Load Only' or RC Accounts - if (m_isLoadOnly) - return false; - - CString newFile = "GRACC001\r\n"; - newFile << "NAME " << m_accountName << "\r\n"; - newFile << "NICK " << m_character.nickName << "\r\n"; - newFile << "COMMUNITYNAME " << m_accountName /*m_communityName*/ << "\r\n"; - newFile << "LEVEL " << m_levelName << "\r\n"; - newFile << "X " << CString(getX()) << "\r\n"; - newFile << "Y " << CString(getY()) << "\r\n"; - newFile << "Z " << CString(getZ()) << "\r\n"; - newFile << "MAXHP " << CString(m_maxHitpoints) << "\r\n"; - newFile << "HP " << CString(m_character.hitpoints) << "\r\n"; - newFile << "RUPEES " << CString(m_character.gralats) << "\r\n"; - newFile << "ANI " << m_character.gani << "\r\n"; - newFile << "ARROWS " << CString(m_character.arrows) << "\r\n"; - newFile << "BOMBS " << CString(m_character.bombs) << "\r\n"; - newFile << "GLOVEP " << CString(m_character.glovePower) << "\r\n"; - newFile << "SHIELDP " << CString(m_character.shieldPower) << "\r\n"; - newFile << "SWORDP " << CString(m_character.swordPower) << "\r\n"; - newFile << "BOWP " << CString(m_character.bowPower) << "\r\n"; - newFile << "BOW " << m_character.bowImage << "\r\n"; - newFile << "HEAD " << m_character.headImage << "\r\n"; - newFile << "BODY " << m_character.bodyImage << "\r\n"; - newFile << "SWORD " << m_character.swordImage << "\r\n"; - newFile << "SHIELD " << m_character.shieldImage << "\r\n"; - newFile << "COLORS " << CString(m_character.colors[0]) << "," << CString(m_character.colors[1]) << "," << CString(m_character.colors[2]) << "," << CString(m_character.colors[3]) << "," << CString(m_character.colors[4]) << "\r\n"; - newFile << "SPRITE " << CString(m_character.sprite) << "\r\n"; - newFile << "STATUS " << CString(m_status) << "\r\n"; - newFile << "MP " << CString(m_mp) << "\r\n"; - newFile << "AP " << CString(m_character.ap) << "\r\n"; - newFile << "APCOUNTER " << CString(m_apCounter) << "\r\n"; - newFile << "ONSECS " << CString(m_onlineTime) << "\r\n"; - newFile << "IP " << CString(m_accountIp) << "\r\n"; - newFile << "LANGUAGE " << m_language << "\r\n"; - newFile << "KILLS " << CString(m_kills) << "\r\n"; - newFile << "DEATHS " << CString(m_deaths) << "\r\n"; - newFile << "RATING " << CString(m_eloRating) << "\r\n"; - newFile << "DEVIATION " << CString(m_eloDeviation) << "\r\n"; - newFile << "LASTSPARTIME " << CString((unsigned long)m_lastSparTime) << "\r\n"; - - // Attributes - for (unsigned int i = 0; i < 30; i++) - { - if (m_character.ganiAttributes[i].length() > 0) - newFile << "ATTR" << CString(i + 1) << " " << m_character.ganiAttributes[i] << "\r\n"; - } - - // Chests - for (unsigned int i = 0; i < m_chestList.size(); i++) - newFile << "CHEST " << m_chestList[i] << "\r\n"; - - // Weapons - for (unsigned int i = 0; i < m_weaponList.size(); i++) - newFile << "WEAPON " << m_weaponList[i] << "\r\n"; - - // Flags - for (auto i = m_flagList.begin(); i != m_flagList.end(); ++i) - { - newFile << "FLAG " << i->first.c_str(); - if (!i->second.isEmpty()) newFile << "=" << i->second; - newFile << "\r\n"; - } - - // Account Settings - newFile << "\r\n"; - newFile << "BANNED " << CString((int)(m_isBanned == true ? 1 : 0)) << "\r\n"; - newFile << "BANREASON " << m_banReason << "\r\n"; - newFile << "BANLENGTH " << m_banLength << "\r\n"; - newFile << "COMMENTS " << m_accountComments << "\r\n"; - newFile << "EMAIL " << m_email << "\r\n"; - newFile << "LOCALRIGHTS " << CString(m_adminRights) << "\r\n"; - newFile << "IPRANGE " << m_adminIp << "\r\n"; - newFile << "LOADONLY " << CString((int)(m_isLoadOnly == true ? 1 : 0)) << "\r\n"; - - // Folder Rights - for (unsigned int i = 0; i < m_folderList.size(); i++) - newFile << "FOLDERRIGHT " << m_folderList[i] << "\r\n"; - newFile << "LASTFOLDER " << m_lastFolder << "\r\n"; - - // Get the file name for the account. - CString accountFileName = m_server->getAccountsFileSystem()->fileExistsAs(CString() << m_accountName << ".txt"); - if (accountFileName.isEmpty()) accountFileName = CString() << m_accountName << ".txt"; - - // Save the account now. - CString accpath = m_server->getServerPath() << "accounts/" << accountFileName; - FileSystem::fixPathSeparators(accpath); - if (!newFile.save(accpath)) - m_server->getRCLog().out("** Error saving account: %s\n", m_accountName.text()); - - return true; -} - -/* - Account: Account Management -*/ -bool Account::meetsConditions(CString fileName, CString conditions) -{ - const char* conditional[] = { ">=", "<=", "!=", "=", ">", "<" }; - - // Load and check if the file is valid. - std::vector file; - file = CString::loadToken(fileName, "\n", true); - if (file.size() == 0 || (file.size() != 0 && file[0] != "GRACC001")) - return false; - - // Load the conditions into a string list. - std::vector cond; - conditions.removeAllI("'"); - conditions.replaceAllI("%", "*"); - cond = conditions.tokenize(","); - bool* conditionsMet = new bool[cond.size()]; - memset((void*)conditionsMet, 0, sizeof(bool) * cond.size()); - - // Go through each line of the loaded file. - for (std::vector::iterator i = file.begin(); i != file.end(); ++i) - { - int sep = (*i).find(' '); - CString section = (*i).subString(0, sep); - CString val = (*i).subString(sep + 1).removeAll("\r"); - section.trimI(); - val.trimI(); - - // Check each line against the conditions specified. - for (unsigned int j = 0; j < cond.size(); ++j) - { - int cond_num = -1; - - // Read out the name and value. - cond[j].setRead(0); - - // Find out what conditional we are using. - for (int k = 0; k < 6; ++k) - { - if (cond[j].find(conditional[k]) != -1) - { - cond_num = k; - k = 6; - } - } - if (cond_num == -1) continue; - - CString cname = cond[j].readString(conditional[cond_num]); - CString cvalue = cond[j].readString(""); - cname.trimI(); - cvalue.trimI(); - cond[j].setRead(0); - - // Now, do a case-insensitive comparison of the section name. -#ifdef WIN32 - if (_stricmp(section.text(), cname.text()) == 0) -#else - if (strcasecmp(section.text(), cname.text()) == 0) -#endif - { - switch (cond_num) - { - case 0: - case 1: - { - // 0: >= - // 1: <= - // Check if it is a number. If so, do a number comparison. - bool condmet = false; - if (val.isNumber()) - { - double vNum[2] = { atof(val.text()), atof(cvalue.text()) }; - if (((cond_num == 1) ? (vNum[0] <= vNum[1]) : (vNum[0] >= vNum[1]))) - { - conditionsMet[j] = true; - condmet = true; - } - } - else - { - // If not a number, do a string comparison. - int ret = strcmp(val.text(), cvalue.text()); - if (((cond_num == 1) ? (ret <= 0) : (ret >= 0))) - { - conditionsMet[j] = true; - condmet = true; - } - } - - // No conditions met means we see if we can fail. - if (condmet == false) - { - CString cnameUp = cname.toUpper(); - if (!(cnameUp == "CHEST" || cnameUp == "WEAPON" || - cnameUp == "FLAG" || cnameUp == "FOLDERRIGHT")) - goto condAbort; - } - break; - } - - case 4: - case 5: - { - // 4: > - // 5: < - bool condmet = false; - if (val.isNumber()) - { - double vNum[2] = { atof(val.text()), atof(cvalue.text()) }; - if (((cond_num == 5) ? (vNum[0] < vNum[1]) : (vNum[0] > vNum[1]))) - { - conditionsMet[j] = true; - condmet = true; - } - } - else - { - int ret = strcmp(val.text(), cvalue.text()); - if (((cond_num == 5) ? (ret < 0) : (ret > 0))) - { - conditionsMet[j] = true; - condmet = true; - } - } - - if (condmet == false) - { - CString cnameUp = cname.toUpper(); - if (!(cnameUp == "CHEST" || cnameUp == "WEAPON" || - cnameUp == "FLAG" || cnameUp == "FOLDERRIGHT")) - goto condAbort; - } - break; - } - - case 2: - { - // 2: != - // If we find a match, return false. - if (val.isNumber()) - { - double vNum[2] = { atof(val.text()), atof(cvalue.text()) }; - if (vNum[0] == vNum[1]) goto condAbort; - conditionsMet[j] = true; - } - else - { - if (val.match(cvalue.text()) == true) goto condAbort; - conditionsMet[j] = true; - } - break; - } - - case 3: - default: - { - // 0 - equals - // If it returns false, don't include this account in the search. - bool condmet = false; - if (val.isNumber()) - { - double vNum[2] = { atof(val.text()), atof(cvalue.text()) }; - if (vNum[0] == vNum[1]) - { - conditionsMet[j] = true; - condmet = true; - } - } - else - { - if (val.match(cvalue.text()) == true) - { - conditionsMet[j] = true; - condmet = true; - } - } - - if (condmet == false) - { - CString cnameUp = cname.toUpper(); - if (!(cnameUp == "CHEST" || cnameUp == "WEAPON" || - cnameUp == "FLAG" || cnameUp == "FOLDERRIGHT")) - goto condAbort; - } - break; - } - } - } - } - } - - // Check if all the conditions were met. - for (unsigned int i = 0; i < cond.size(); ++i) - if (conditionsMet[i] == false) goto condAbort; - - // Clean up. - delete[] conditionsMet; - return true; - -condAbort: - delete[] conditionsMet; - return false; -} - -/* - Account: Attribute-Managing -*/ -bool Account::hasChest(const CString& pChest) -{ - auto it = std::find(m_chestList.begin(), m_chestList.end(), pChest); - return (it != m_chestList.end()); -} - -bool Account::hasWeapon(const CString& pWeapon) -{ - auto it = std::find(m_weaponList.begin(), m_weaponList.end(), pWeapon); - return (it != m_weaponList.end()); -} - -/* - Account: Flag Management -*/ -void Account::setFlag(CString pFlag) -{ - CString flagName = pFlag.readString("="); - CString flagValue = pFlag.readString(""); - this->setFlag(flagName.text(), flagValue); -} - -void Account::setFlag(const std::string& pFlagName, const CString& pFlagValue) -{ - if (m_server->getSettings().getBool("cropflags", true)) - { - int fixedLength = 223 - 1 - pFlagName.length(); - m_flagList[pFlagName] = pFlagValue.subString(0, fixedLength); - } - else - m_flagList[pFlagName] = pFlagValue; -} - -/* - Translation Functionality -*/ -CString Account::translate(const CString& pKey) const -{ - return m_server->TS_Translate(m_language, pKey); -} - -void Account::setMaxPower(int newMaxPower) -{ - const auto& settings = m_server->getSettings(); - - auto heartLimit = std::min(settings.getInt("heartlimit", 3), 20); - m_maxHitpoints = clip(newMaxPower, 0, heartLimit); -} - -void Account::setShieldPower(int newPower) -{ - const auto& settings = m_server->getSettings(); - - m_character.shieldPower = clip(newPower, 0, settings.getInt("shieldlimit", 3)); -} - -void Account::setSwordPower(int newPower) -{ - const auto& settings = m_server->getSettings(); - - m_character.swordPower = clip(newPower, ((settings.getBool("healswords", false) == true) ? -(settings.getInt("swordlimit", 3)) : 0), settings.getInt("swordlimit", 3)); -} - -void Account::setFolderRights(const std::vector& folderRights) -{ - m_folderList = folderRights; - m_folderRights = {}; - - for (const auto& folder : folderRights) - { - m_folderRights.addPermission(folder.text()); - } -} diff --git a/server/src/FileSystem.cpp b/server/src/FileSystem.cpp deleted file mode 100644 index 112a1d7d0..000000000 --- a/server/src/FileSystem.cpp +++ /dev/null @@ -1,300 +0,0 @@ -#include - -#include -#include -#if (defined(_WIN32) || defined(_WIN64)) && !defined(__GNUC__) - #include - #define _utime utime - #define _utimbuf utimbuf; -#else - #include - #include -#endif - -#include - -#include "FileSystem.h" -#include "Server.h" - -#if defined(_WIN32) || defined(_WIN64) - #ifndef __GNUC__ // rain - #include - #include - #endif -#endif - -FileSystem::FileSystem() -{ - m_preventChange = new std::recursive_mutex(); -} - -FileSystem::~FileSystem() -{ - clear(); - delete m_preventChange; -} - -void FileSystem::clear() -{ - m_fileList.clear(); - m_directoryList.clear(); -} - -void FileSystem::addDir(const CString& dir, const CString& wildcard, bool forceRecursive) -{ - std::lock_guard lock(*m_preventChange); - - if (m_server == nullptr) return; - - // Format the directory. - CString newDir(dir); - if (newDir[newDir.length() - 1] == '/' || newDir[newDir.length() - 1] == '\\') - FileSystem::fixPathSeparators(newDir); - else - { - newDir << fSep; - FileSystem::fixPathSeparators(newDir); - } - - // Add the directory to the directory list. - CString ndir = m_server->getServerPath() << newDir << wildcard; - if (vecSearch(m_directoryList, ndir) != -1) // Already exists? Resync. - resync(); - else - { - m_directoryList.push_back(ndir); - - // Load up the files in the directory. - loadAllDirectories(ndir, forceRecursive || m_server->getSettings().getBool("nofoldersconfig", false)); - } -} - -void FileSystem::addFile(CString file) -{ - std::lock_guard lock(*m_preventChange); - - // Grab the file name and directory. - FileSystem::fixPathSeparators(file); - CString filename(getFilename(file, fSep)); - CString directory(getPath(file, fSep)); - - // Fix directory path separators. - if (directory.find(m_server->getServerPath()) != -1) - directory.removeI(0, m_server->getServerPath().length()); - - // Add to the map. - m_fileList[filename] = m_server->getServerPath() << directory << filename; -} - -void FileSystem::removeFile(const CString& file) -{ - std::lock_guard lock(*m_preventChange); - - // Grab the file name and directory. - CString filename(file.subString(file.findl(fSep) + 1)); - CString directory(file.subString(0, file.find(filename))); - - // Fix directory path separators. - FileSystem::fixPathSeparators(directory); - - // Remove it from the map. - m_fileList.erase(filename); -} - -void FileSystem::resync() -{ - std::lock_guard lock(*m_preventChange); - - // Clear the file list. - m_fileList.clear(); - - // Iterate through all the directories, reloading their file list. - for (const auto& directory: m_directoryList) - loadAllDirectories(directory, m_server->getSettings().getBool("nofoldersconfig", false)); -} - -CString FileSystem::find(const CString& file) const -{ - std::lock_guard lock(*m_preventChange); - - auto fileIter = m_fileList.find(file); - if (fileIter == m_fileList.end()) return {}; - return { fileIter->second }; -} - -CString FileSystem::findi(const CString& file) const -{ - std::lock_guard lock(*m_preventChange); - - for (const auto& fileIter: m_fileList) - if (fileIter.first.comparei(file)) return { fileIter.second }; - return {}; -} - -CString FileSystem::fileExistsAs(const CString& file) const -{ - std::lock_guard lock(*m_preventChange); - - for (const auto& fileIter: m_fileList) - if (fileIter.first.comparei(file)) return { fileIter.first }; - return {}; -} - -#if (defined(_WIN32) || defined(_WIN64)) && !defined(__GNUC__) -void FileSystem::loadAllDirectories(const CString& directory, bool recursive) -{ - CString dir = CString() << directory.remove(directory.findl(fSep)) << fSep; - WIN32_FIND_DATAA filedata; - HANDLE hFind = FindFirstFileA(directory.text(), &filedata); - - if (hFind != INVALID_HANDLE_VALUE) - { - do - { - if (filedata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) - { - if (filedata.cFileName[0] != '.' && recursive) - { - // We need to add the directory to the directory list. - CString newDir = CString() << dir << filedata.cFileName << fSep; - newDir.removeI(0, m_server->getServerPath().length()); - addDir(newDir, "*", true); - } - } - else - { - // Grab the file name. - CString file((char*)filedata.cFileName); - m_fileList[file] = CString(dir) << filedata.cFileName; - } - } - while (FindNextFileA(hFind, &filedata)); - } - FindClose(hFind); -} -#else -void FileSystem::loadAllDirectories(const CString& directory, bool recursive) -{ - CString path = CString() << directory.remove(directory.findl(fSep)) << fSep; - CString wildcard = directory.subString(directory.findl(fSep) + 1); - DIR* dir; - struct stat statx - { - }; - struct dirent* ent; - - // Try to open the directory. - if ((dir = opendir(path.text())) == nullptr) - return; - - // Read everything in it now. - while ((ent = readdir(dir)) != 0) - { - if (ent->d_name[0] != '.') - { - CString dircheck = CString() << path << ent->d_name; - stat(dircheck.text(), &statx); - if ((statx.st_mode & S_IFDIR)) - { - if (recursive) - { - // We need to add the directory to the directory list. - CString newDir = CString() << path << ent->d_name << fSep; - newDir.removeI(0, m_server->getServerPath().length()); - addDir(newDir, "*", true); - } - continue; - } - } - else - continue; - - // Grab the file name. - CString file(ent->d_name); - if (file.match(wildcard)) - m_fileList[file] = CString(path) << file; - } - closedir(dir); -} -#endif - -CString FileSystem::load(const CString& file) const -{ - std::lock_guard lock(*m_preventChange); - - // Get the full path to the file. - CString fileName = find(file); - if (fileName.length() == 0) return CString(); - - // Load the file. - CString fileData; - fileData.load(fileName); - - return fileData; -} - -time_t FileSystem::getModTime(const CString& file) const -{ - std::lock_guard lock(*m_preventChange); - - // Get the full path to the file. - CString fileName = find(file); - if (fileName.length() == 0) return 0; - - struct stat fileStat - { - }; - if (stat(fileName.text(), &fileStat) != -1) - return (time_t)fileStat.st_mtime; - return 0; -} - -bool FileSystem::setModTime(const CString& file, time_t modTime) const -{ - std::lock_guard lock(*m_preventChange); - - // Get the full path to the file. - CString fileName = find(file); - if (fileName.length() == 0) return false; - - // Set the times. - struct utimbuf ut - { - }; - ut.actime = modTime; - ut.modtime = modTime; - - // Change the file. - return utime(fileName.text(), &ut) == 0; -} - -int FileSystem::getFileSize(const CString& file) const -{ - std::lock_guard lock(*m_preventChange); - - // Get the full path to the file. - CString fileName = find(file); - if (fileName.length() == 0) return 0; - - struct stat fileStat - { - }; - if (stat(fileName.text(), &fileStat) != -1) - return fileStat.st_size; - return 0; -} - -CString FileSystem::getDirByExtension(const std::string& extension) const -{ - std::lock_guard lock(*m_preventChange); - - for (const auto& directory: m_directoryList) - { - if (getExtension(directory) == extension) - { - return { getPath(directory) }; - } - } - - return {}; -} diff --git a/server/src/NPC.cpp b/server/src/NPC.cpp deleted file mode 100644 index d6b46d822..000000000 --- a/server/src/NPC.cpp +++ /dev/null @@ -1,1936 +0,0 @@ -#include - -#include -#include -#include - -#include - -#include "FileSystem.h" -#include "NPC.h" -#include "Server.h" -#include "level/Level.h" -#include "level/Map.h" -#include "scripting/SourceCode.h" - -#ifdef V8NPCSERVER - #include "scripting/ScriptEngine.h" - #include "Player.h" -#endif - -const char __nSavePackets[10] = { 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 }; -const char __nAttrPackets[30] = { 36, 37, 38, 39, 40, 44, 45, 46, 47, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73 }; - -static CString toWeaponName(const CString& code); -static CString doJoins(const CString& code, FileSystem* fs); - -std::string minifyClientCode(const CString& src) -{ - std::string minified; - if (!src.isEmpty()) - { - auto tmp = removeComments(src, "\n"); - - // Scripts should start with //#CLIENTSIDE since this is client code - if (tmp.find("//#CLIENTSIDE") != 0) - minified.append("//#CLIENTSIDE").append("\xa7"); - - std::vector codeLines = tmp.tokenize("\n"); - for (const auto& line: codeLines) - minified.append(line.trim().toString()).append("\xa7"); - } - - return minified; -} - -NPC::NPC(const CString& pImage, std::string pScript, float pX, float pY, std::shared_ptr pLevel, NPCType type) - : NPC(type) -{ - setX(pX); - setY(pY); - m_image = pImage.text(); - m_curlevel = pLevel; -#ifdef V8NPCSERVER - m_origImage = m_image; -#endif - - // Keep a copy of the original level for resets -#ifdef V8NPCSERVER - if (!m_curlevel.expired()) - { - m_origLevel = getLevel()->getLevelName(); - } -#endif - - // TODO: Create plugin hook so NPCServer can acquire/format code. - - // Needs to be called so it creates a script-object - //if (!pScript.isEmpty()) - setScriptCode(std::move(pScript)); -} - -NPC::NPC(NPCType type) - : m_npcType(type) -#ifdef V8NPCSERVER - , - m_scriptExecutionContext(m_server->getScriptEngine()), m_origX(m_x), m_origY(m_y), m_origZ(m_z) -#endif -{ - memset((void*)m_saves, 0, sizeof(m_saves)); - memset((void*)m_modTime, 0, sizeof(m_modTime)); - - // imagePart needs to be Graal-packed. - for (int i = 0; i < 6; i++) - m_imagePart.writeGChar(0); - - // We need to alter the modTime of the following props as they should be always sent. - // If we don't, they won't be sent until the prop gets modified. - m_modTime[NPCPROP_IMAGE] = m_modTime[NPCPROP_SCRIPT] = m_modTime[NPCPROP_X] = m_modTime[NPCPROP_Y] = m_modTime[NPCPROP_VISFLAGS] = m_modTime[NPCPROP_ID] = m_modTime[NPCPROP_SPRITE] = m_modTime[NPCPROP_MESSAGE] = m_modTime[NPCPROP_GMAPLEVELX] = m_modTime[NPCPROP_GMAPLEVELY] = m_modTime[NPCPROP_X2] = m_modTime[NPCPROP_Y2] = time(0); - - // Needs to be called so it creates a script-object - setScriptCode(""); -} - -NPC::~NPC() -{ -#ifdef V8NPCSERVER - freeScriptResources(); -#endif -} - -void NPC::setScriptCode(std::string pScript) -{ - bool firstExecution = m_npcScript.empty(); - -#ifdef V8NPCSERVER - // Clear any joined code - m_classMap.clear(); - - if (m_scriptObject) - freeScriptResources(); -#endif - bool gs2default = m_server->getSettings().getBool("gs2default", false); - - m_npcScript = SourceCode{ std::move(pScript), gs2default }; - - bool levelModificationNPCHack = false; - - // NOTE: since we are not removing comments from the source, any comments at the start of the script - // interferes with the starts_with check, so a temporary workaround is to check for it within the first 100 characters - - // All code is stored in clientside when building without an npc-server, and split as-expected with the npc-server -#ifdef V8NPCSERVER - std::string_view npcScriptSearch = m_npcScript.getServerSide(); -#else - std::string_view npcScriptSearch = m_npcScript.getClientSide(); -#endif - - // See if the NPC sets the level as a sparring zone. - if (auto level = getLevel(); level) - { - if (npcScriptSearch.starts_with("sparringzone") || npcScriptSearch.find("sparringzone\n") < 100) - { - level->setSparringZone(true); - levelModificationNPCHack = true; - } - // See if the NPC sets the level as singleplayer. - else if (npcScriptSearch.starts_with("singleplayer") || npcScriptSearch.find("singleplayer\n") < 100) - { - level->setSingleplayer(true); - levelModificationNPCHack = true; - } - } - - // Remove sparringzone / singleplayer from the server script - if (levelModificationNPCHack) - { - // Clearing the entire script - m_npcScript.clearServerSide(); - } - - // See if the NPC should block position updates from the level leader. -#ifdef V8NPCSERVER - m_blockPositionUpdates = true; -#else - if (m_npcScript.getClientGS1().find("//#BLOCKPOSITIONUPDATES") != std::string::npos) - m_blockPositionUpdates = true; -#endif - -#ifndef V8NPCSERVER - // Search for toweapons in the clientside code and extract the name of the weapon. - m_weaponName = toWeaponName(std::string{ m_npcScript.getClientGS1() }); -#endif - - // Remove comments and trim the code if specified. Also changes line-endings -#ifdef V8NPCSERVER - updateClientCode(); -#else - auto tmpScript = doJoins(std::string{ m_npcScript.getClientGS1() }, m_server->getFileSystem()); - m_clientScriptFormatted = minifyClientCode(tmpScript); - - // Just a little warning for people who don't know. - if (m_clientScriptFormatted.length() > 0x705F) - printf("WARNING: Clientside script of NPC (%s) exceeds the limit of 28767 bytes.\n", (m_weaponName.length() != 0 ? m_weaponName.text() : m_image.c_str())); -#endif - -#ifdef V8NPCSERVER - // Compile and execute the script. - bool executed = m_server->getScriptEngine()->executeNpc(this); - if (executed) - { - SCRIPTENV_D("SCRIPT COMPILED\n"); - this->queueNpcAction("npc.created"); - } - else - SCRIPTENV_D("Could not compile npc script\n"); - - // Delete old npc, and send npc to level. Currently only doing this for database npcs, everything else - // would need "update level" to take changes. - auto level = getLevel(); - if (!firstExecution && getType() != NPCType::LEVELNPC && level) - { - // this property forces showcharacter, preventing ganis to go back to images - m_modTime[NPCPROP_GANI] = 0; - m_modTime[NPCPROP_IMAGE] = time(0); - //m_image = ""; - - // TODO(joey): refactor - m_server->sendPacketToLevelArea(CString() >> (char)PLO_NPCDEL >> (int)getId(), level); - - CString packet = CString() >> (char)PLO_NPCPROPS >> (int)getId() << getProps(0); - m_server->sendPacketToLevelArea(packet, level); - } -#endif -} - -std::shared_ptr NPC::getLevel() const -{ - // TODO: Handle deleted level. - // Delete level NPCs. - - return m_curlevel.lock(); -} - -CString NPC::getProp(unsigned char pId, int clientVersion) const -{ - auto level = getLevel(); - switch (pId) - { - case NPCPROP_IMAGE: - return CString() >> (char)m_image.length() << m_image; - - case NPCPROP_SCRIPT: - // GS2 support - if (clientVersion >= CLVER_4_0211) - { - // GS1 was disabled after this client version - if (clientVersion > CLVER_5_07) - return CString() >> (short)0; - - // If we have bytecode, don't send gs1 script - if (!m_npcBytecode.isEmpty()) - return CString() >> (short)0; - } - - return CString() >> (short)(m_clientScriptFormatted.length() > 0x3FFF ? 0x3FFF : m_clientScriptFormatted.length()) << m_clientScriptFormatted.substr(0, 0x3FFF); - - case NPCPROP_X: - return CString() >> (char)(m_x / 8); - - case NPCPROP_Y: - return CString() >> (char)(m_y / 8); - - case NPCPROP_Z: - // range: -25 to 85 - return CString() >> (char)(std::min(85 * 2, std::max(-25 * 2, (m_z / 8))) + 50); - - case NPCPROP_POWER: - return CString() >> (char)(m_character.hitpoints * 2); - - case NPCPROP_RUPEES: - return CString() >> (int)m_character.gralats; - - case NPCPROP_ARROWS: - return CString() >> (char)m_character.arrows; - - case NPCPROP_BOMBS: - return CString() >> (char)m_character.bombs; - - case NPCPROP_GLOVEPOWER: - return CString() >> (char)m_character.glovePower; - - case NPCPROP_BOMBPOWER: - return CString() >> (char)m_character.bombPower; - - case NPCPROP_SWORDIMAGE: - if (m_character.swordPower == 0) - return CString() >> (char)0; - else - return CString() >> (char)(m_character.swordPower + 30) >> (char)m_character.swordImage.length() << m_character.swordImage; - - case NPCPROP_SHIELDIMAGE: - if (m_character.shieldPower + 10 > 10) - return CString() >> (char)(m_character.shieldPower + 10) >> (char)m_character.shieldImage.length() << m_character.shieldImage; - else - return CString() >> (char)0; - - case NPCPROP_GANI: - if (clientVersion < CLVER_2_1) - { - if (m_character.bowPower < 10) - return CString() >> (char)m_character.bowPower; - else - return CString() >> (char)(m_character.bowImage.length() + 10) << m_character.bowImage; - } - return CString() >> (char)m_character.gani.length() << m_character.gani; - - case NPCPROP_VISFLAGS: - return CString() >> (char)m_visFlags; - - case NPCPROP_BLOCKFLAGS: - return CString() >> (char)m_blockFlags; - - case NPCPROP_MESSAGE: - return CString() >> (char)m_character.chatMessage.length() << m_character.chatMessage; - - case NPCPROP_HURTDXDY: - return CString() >> (char)((m_hurtX * 32) + 32) >> (char)((m_hurtY * 32) + 32); - - case NPCPROP_ID: - return CString() >> (int)m_id; - - // Sprite is deprecated and has been replaced by def.gani. - // Sprite now holds the direction of the npc. sprite % 4 gives backwards compatibility. - case NPCPROP_SPRITE: - { - if (clientVersion < CLVER_2_1) - return CString() >> (char)m_character.sprite; - else - return CString() >> (char)(m_character.sprite % 4); - } - - case NPCPROP_COLORS: - return CString() >> (char)m_character.colors[0] >> (char)m_character.colors[1] >> (char)m_character.colors[2] >> (char)m_character.colors[3] >> (char)m_character.colors[4]; - - case NPCPROP_NICKNAME: - return CString() >> (char)m_character.nickName.length() << m_character.nickName; - - case NPCPROP_HORSEIMAGE: - return CString() >> (char)m_character.horseImage.length() << m_character.horseImage; - - case NPCPROP_HEADIMAGE: - return CString() >> (char)(m_character.headImage.length() + 100) << m_character.headImage; - - case NPCPROP_SAVE0: - case NPCPROP_SAVE1: - case NPCPROP_SAVE2: - case NPCPROP_SAVE3: - case NPCPROP_SAVE4: - case NPCPROP_SAVE5: - case NPCPROP_SAVE6: - case NPCPROP_SAVE7: - case NPCPROP_SAVE8: - case NPCPROP_SAVE9: - return CString() >> (char)m_saves[pId - NPCPROP_SAVE0]; - - case NPCPROP_ALIGNMENT: - return CString() >> (char)m_character.ap; - - case NPCPROP_IMAGEPART: - return CString() << m_imagePart; - - case NPCPROP_BODYIMAGE: - return CString() >> (char)m_character.bodyImage.length() << m_character.bodyImage; - - case NPCPROP_GMAPLEVELX: - return CString() >> (char)(level ? level->getGmapX() : 0); - - case NPCPROP_GMAPLEVELY: - return CString() >> (char)(level ? level->getGmapY() : 0); - -#ifdef V8NPCSERVER - case NPCPROP_SCRIPTER: - return CString() >> (char)m_npcScripter.length() << m_npcScripter; - - case NPCPROP_NAME: - return CString() >> (char)m_npcName.length() << m_npcName; - - case NPCPROP_TYPE: - return CString() >> (char)m_npcScriptType.length() << m_npcScriptType; - - case NPCPROP_CURLEVEL: - { - CString tmpLevelName = (level ? level->getLevelName() : ""); - return CString() >> (char)tmpLevelName.length() << tmpLevelName; - } -#endif - - case NPCPROP_CLASS: - { - CString classList; - -#ifdef V8NPCSERVER - for (const auto& it: m_classMap) - classList << it.first << ","; -#endif - - if (!classList.isEmpty()) - classList.removeI(classList.length() - 1); - return CString() >> (short)classList.length() << classList; - } - - case NPCPROP_X2: - { - uint16_t val = ((uint16_t)std::abs(m_x)) << 1; - if (m_x < 0) - val |= 0x0001; - return CString().writeGShort(val); - } - - case NPCPROP_Y2: - { - uint16_t val = ((uint16_t)std::abs(m_y)) << 1; - if (m_y < 0) - val |= 0x0001; - return CString().writeGShort(val); - } - - case NPCPROP_Z2: - { - // range: -25 to 85 - uint16_t val = std::min(85 * 16, std::max(-25 * 16, m_z)); - val = std::abs(val) << 1; - if (m_z < 0) - val |= 0x0001; - return CString().writeGShort(val); - } - } - - // Gani attributes. - if (inrange(pId, NPCPROP_GATTRIB1, NPCPROP_GATTRIB5) || inrange(pId, NPCPROP_GATTRIB6, NPCPROP_GATTRIB9) || inrange(pId, NPCPROP_GATTRIB10, NPCPROP_GATTRIB30)) - { - // TODO(joey): Are we really looping every single possible attribute to find the one we want....?? - for (unsigned int i = 0; i < sizeof(__nAttrPackets); i++) - { - if (__nAttrPackets[i] == pId) - return CString() >> (char)m_character.ganiAttributes[i].length() << m_character.ganiAttributes[i]; - } - } - - return CString(); -} - -CString NPC::getProps(time_t newTime, int clientVersion) const -{ - bool oldcreated = m_server->getSettings().getBool("oldcreated", "false"); - CString retVal; - int pmax = NPCPROP_COUNT; - if (clientVersion < CLVER_2_1) pmax = 36; - - for (int i = 0; i < pmax; i++) - { - if (m_modTime[i] != 0 && m_modTime[i] >= newTime) - { - if (oldcreated && i == NPCPROP_VISFLAGS && newTime == 0) - retVal >> (char)i >> (char)(m_visFlags | NPCVISFLAG_VISIBLE); - else - retVal >> (char)i << getProp(i, clientVersion); - } - } - if (clientVersion > CLVER_1_411) - { - if (m_modTime[NPCPROP_GANI] == 0 && m_image == "#c#") - retVal >> (char)NPCPROP_GANI >> (char)4 << "idle"; - } - - return retVal; -} - -CString NPC::setProps(CString& pProps, int clientVersion, bool pForward) -{ - bool hasMoved = false; - - // TODO(joey): Most of these props will eventually be ignored - - CString ret; - int len = 0; - while (pProps.bytesLeft() > 0) - { - unsigned char propId = pProps.readGUChar(); - CString oldProp = getProp(propId); - //printf( "propId: %d\n", propId ); - switch (propId) - { - case NPCPROP_IMAGE: - m_visFlags |= NPCVISFLAG_VISIBLE; - m_image = pProps.readChars(pProps.readGUChar()).text(); - if (!m_image.empty() && clientVersion < CLVER_2_1 && getExtension(m_image).isEmpty()) - m_image.append(".gif"); - break; - - case NPCPROP_SCRIPT: - pProps.readChars(pProps.readGUShort()); - - // TODO(joey): is this used for putnpcs? - //clientScript = pProps.readChars(pProps.readGUShort()); - break; - - case NPCPROP_X: - if (m_blockPositionUpdates) - { - pProps.readGChar(); - continue; - } - m_x = pProps.readGChar() * 8; - hasMoved = true; - break; - - case NPCPROP_Y: - if (m_blockPositionUpdates) - { - pProps.readGChar(); - continue; - } - m_y = pProps.readGChar() * 8; - hasMoved = true; - break; - - case NPCPROP_Z: - if (m_blockPositionUpdates) - { - pProps.readGChar(); - continue; - } - m_z = (pProps.readGChar() - 50) * 8; - hasMoved = true; - break; - - case NPCPROP_POWER: - m_character.hitpoints = (pProps.readGUChar() / 2.0f); - break; - - case NPCPROP_RUPEES: - m_character.gralats = pProps.readGUInt(); - break; - - case NPCPROP_ARROWS: - m_character.arrows = pProps.readGUChar(); - break; - - case NPCPROP_BOMBS: - m_character.bombs = pProps.readGUChar(); - break; - - case NPCPROP_GLOVEPOWER: - m_character.glovePower = pProps.readGUChar(); - break; - - case NPCPROP_BOMBPOWER: - m_character.bombPower = pProps.readGUChar(); - break; - - case NPCPROP_SWORDIMAGE: - { - int sp = pProps.readGUChar(); - if (sp <= 4) - m_character.swordImage = CString() << "sword" << CString(sp) << (clientVersion < CLVER_2_1 ? ".gif" : ".png"); - else - { - sp -= 30; - len = pProps.readGUChar(); - if (len > 0) - { - m_character.swordImage = pProps.readChars(len); - if (!m_character.swordImage.isEmpty() && clientVersion < CLVER_2_1 && getExtension(m_character.swordImage).isEmpty()) - m_character.swordImage << ".gif"; - } - else - m_character.swordImage = ""; - //m_character.swordPower = clip(sp, ((settings->getBool("healswords", false) == true) ? -(settings->getInt("swordlimit", 3)) : 0), settings->getInt("swordlimit", 3)); - } - m_character.swordPower = sp; - break; - } - - case NPCPROP_SHIELDIMAGE: - { - int sp = pProps.readGUChar(); - if (sp <= 3) - m_character.shieldImage = CString() << "shield" << CString(sp) << (clientVersion < CLVER_2_1 ? ".gif" : ".png"); - else - { - sp -= 10; - len = pProps.readGUChar(); - if (len > 0) - { - m_character.shieldImage = pProps.readChars(len); - if (!m_character.shieldImage.isEmpty() && clientVersion < CLVER_2_1 && getExtension(m_character.shieldImage).isEmpty()) - m_character.shieldImage << ".gif"; - } - else - m_character.shieldImage = ""; - } - m_character.shieldPower = std::min(sp, 3); - break; - } - - case NPCPROP_GANI: - { - if (clientVersion < CLVER_2_1) - { - // Older clients don't use ganis. This is the bow power and image instead. - m_character.bowPower = pProps.readGUChar(); - if (m_character.bowPower >= 10) - { - m_character.bowImage = pProps.readChars(m_character.bowPower - 10); - if (!m_character.bowImage.isEmpty() && clientVersion < CLVER_2_1 && getExtension(m_character.bowImage).isEmpty()) - m_character.bowImage << ".gif"; - } - break; - } - m_character.gani = pProps.readChars(pProps.readGUChar()).text(); - break; - } - - case NPCPROP_VISFLAGS: - m_visFlags = pProps.readGUChar(); - break; - - case NPCPROP_BLOCKFLAGS: - m_blockFlags = pProps.readGUChar(); - break; - - case NPCPROP_MESSAGE: - m_character.chatMessage = pProps.readChars(pProps.readGUChar()).text(); - break; - - case NPCPROP_HURTDXDY: - m_hurtX = ((float)(pProps.readGUChar() - 32)) / 32; - m_hurtY = ((float)(pProps.readGUChar() - 32)) / 32; - break; - - case NPCPROP_ID: - pProps.readGUInt(); - break; - - case NPCPROP_SPRITE: - m_character.sprite = pProps.readGUChar(); - break; - - case NPCPROP_COLORS: - for (int i = 0; i < 5; i++) - m_character.colors[i] = pProps.readGUChar(); - break; - - case NPCPROP_NICKNAME: - m_character.nickName = pProps.readChars(pProps.readGUChar()).text(); - break; - - case NPCPROP_HORSEIMAGE: - m_character.horseImage = pProps.readChars(pProps.readGUChar()); - if (!m_character.horseImage.isEmpty() && clientVersion < CLVER_2_1 && getExtension(m_character.horseImage).isEmpty()) - m_character.horseImage << ".gif"; - break; - - case NPCPROP_HEADIMAGE: - len = pProps.readGUChar(); - if (len < 100) - m_character.headImage = CString() << "head" << CString(len) << (clientVersion < CLVER_2_1 ? ".gif" : ".png"); - else - { - m_character.headImage = pProps.readChars(len - 100); - if (!m_character.headImage.isEmpty() && clientVersion < CLVER_2_1 && getExtension(m_character.headImage).isEmpty()) - m_character.headImage << ".gif"; - } - break; - - case NPCPROP_ALIGNMENT: - m_character.ap = pProps.readGUChar(); - m_character.ap = clip(m_character.ap, 0, 100); - break; - - case NPCPROP_IMAGEPART: - m_imagePart = pProps.readChars(6); - break; - - case NPCPROP_BODYIMAGE: - m_character.bodyImage = pProps.readChars(pProps.readGUChar()); - break; - - case NPCPROP_GMAPLEVELX: - pProps.readGUChar(); - break; - - case NPCPROP_GMAPLEVELY: - pProps.readGUChar(); - break; - - case NPCPROP_SCRIPTER: - m_npcScripter = pProps.readChars(pProps.readGUChar()); - break; - - case NPCPROP_NAME: - m_npcName = pProps.readChars(pProps.readGUChar()).text(); - break; - - case NPCPROP_TYPE: - m_npcScriptType = pProps.readChars(pProps.readGUChar()); - break; - - case NPCPROP_CURLEVEL: - pProps.readChars(pProps.readGUChar()); - break; - - case NPCPROP_CLASS: - pProps.readChars(pProps.readGShort()); - break; - - // Location, in pixels, of the npc on the level in 2.3+ clients. - // Bit 0x0001 controls if it is negative or not. - // Bits 0xFFFE are the actual value. - case NPCPROP_X2: - if (m_blockPositionUpdates) - { - pProps.readGUShort(); - continue; - } - - len = pProps.readGUShort(); - m_x = (len >> 1); - - // If the first bit is 1, our position is negative. - if ((uint16_t)len & 0x0001) - m_x = -m_x; - - hasMoved = true; - break; - - case NPCPROP_Y2: - if (m_blockPositionUpdates) - { - pProps.readGUShort(); - continue; - } - - len = pProps.readGUShort(); - m_y = (len >> 1); - - // If the first bit is 1, our position is negative. - if ((uint16_t)len & 0x0001) - m_y = -m_y; - - hasMoved = true; - break; - - case NPCPROP_Z2: - if (m_blockPositionUpdates) - { - pProps.readGUShort(); - continue; - } - - len = pProps.readGUShort(); - m_z = (len >> 1); - - // If the first bit is 1, our position is negative. - if ((uint16_t)len & 0x0001) - m_z = -m_z; - - hasMoved = true; - break; - - case NPCPROP_SAVE0: - m_saves[0] = pProps.readGUChar(); - break; - case NPCPROP_SAVE1: - m_saves[1] = pProps.readGUChar(); - break; - case NPCPROP_SAVE2: - m_saves[2] = pProps.readGUChar(); - break; - case NPCPROP_SAVE3: - m_saves[3] = pProps.readGUChar(); - break; - case NPCPROP_SAVE4: - m_saves[4] = pProps.readGUChar(); - break; - case NPCPROP_SAVE5: - m_saves[5] = pProps.readGUChar(); - break; - case NPCPROP_SAVE6: - m_saves[6] = pProps.readGUChar(); - break; - case NPCPROP_SAVE7: - m_saves[7] = pProps.readGUChar(); - break; - case NPCPROP_SAVE8: - m_saves[8] = pProps.readGUChar(); - break; - case NPCPROP_SAVE9: - m_saves[9] = pProps.readGUChar(); - break; - - case NPCPROP_GATTRIB1: - m_character.ganiAttributes[0] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB2: - m_character.ganiAttributes[1] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB3: - m_character.ganiAttributes[2] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB4: - m_character.ganiAttributes[3] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB5: - m_character.ganiAttributes[4] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB6: - m_character.ganiAttributes[5] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB7: - m_character.ganiAttributes[6] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB8: - m_character.ganiAttributes[7] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB9: - m_character.ganiAttributes[8] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB10: - m_character.ganiAttributes[9] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB11: - m_character.ganiAttributes[10] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB12: - m_character.ganiAttributes[11] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB13: - m_character.ganiAttributes[12] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB14: - m_character.ganiAttributes[13] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB15: - m_character.ganiAttributes[14] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB16: - m_character.ganiAttributes[15] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB17: - m_character.ganiAttributes[16] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB18: - m_character.ganiAttributes[17] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB19: - m_character.ganiAttributes[18] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB20: - m_character.ganiAttributes[19] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB21: - m_character.ganiAttributes[20] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB22: - m_character.ganiAttributes[21] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB23: - m_character.ganiAttributes[22] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB24: - m_character.ganiAttributes[23] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB25: - m_character.ganiAttributes[24] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB26: - m_character.ganiAttributes[25] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB27: - m_character.ganiAttributes[26] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB28: - m_character.ganiAttributes[27] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB29: - m_character.ganiAttributes[28] = pProps.readChars(pProps.readGUChar()); - break; - case NPCPROP_GATTRIB30: - m_character.ganiAttributes[29] = pProps.readChars(pProps.readGUChar()); - break; - - default: - { - printf("NPC %ud (%.2f, %.2f): ", m_id, (float)m_x / 16.0f, (float)m_y / 16.0f); - printf("Unknown prop: %ud, readPos: %d\n", propId, pProps.readPos()); - for (int i = 0; i < pProps.length(); ++i) - printf("%02x ", (unsigned char)pProps[i]); - printf("\n"); - return ret; - } - } - - // If a prop changed, adjust its mod time. - if (propId < NPCPROP_COUNT) - { - if (oldProp != getProp(propId)) - m_modTime[propId] = time(0); - } - - // Add to ret. - ret >> (char)propId << getProp(propId, clientVersion); - } - - if (pForward) - { - // Send the props. - m_server->sendPacketToLevelArea(CString() >> (char)PLO_NPCPROPS >> (int)m_id << ret, m_curlevel); - } - -#ifdef V8NPCSERVER - if (hasMoved) testTouch(); -#endif - - return ret; -} - -#ifdef V8NPCSERVER - -void NPC::testForLinks() -{ - auto level = getLevel(); - if (level == nullptr) return; - - // Overworld links - if (auto map = level->getMap(); map) - { - // Gmaps are treated as one large map, and so (local?) npcs can freely walk - // across maps without canwarp being enabled (source: post=1193766) - if (map->isGmap() || m_canWarp != NPCWarpType::None) - { - int gmapX = m_x + 1024 * level->getMapX(); - int gmapY = m_y + 1024 * level->getMapY(); - int mapx = gmapX / 1024; - int mapy = gmapY / 1024; - - if (level->getMapX() != mapx || level->getMapY() != mapy) - { - auto newLevel = m_server->getLevel(map->getLevelAt(mapx, mapy)); - if (newLevel != nullptr) - { - this->warpNPC(newLevel, gmapX % 1024, gmapY % 1024); - return; - } - } - } - } - - if (m_canWarp == NPCWarpType::AllLinks) - { - static const int touchtestd[] = { 2, 1, 0, 2, 2, 4, 3, 2 }; - - int dir = m_character.sprite % 4; - - auto linkTouched = level->getLink((int)(m_x / 16) + touchtestd[dir * 2], (int)(m_y / 16) + touchtestd[dir * 2 + 1]); - if (linkTouched) - { - auto newLevel = m_server->getLevel(linkTouched.value()->getNewLevel().toString()); - if (newLevel != 0) - { - int newX = (linkTouched.value()->getNewX() == "playerx" ? m_x : int(16.0 * strtofloat(linkTouched.value()->getNewX()))); - int newY = (linkTouched.value()->getNewY() == "playery" ? m_y : int(16.0 * strtofloat(linkTouched.value()->getNewY()))); - this->warpNPC(newLevel, newX, newY); - } - } - } -} - -void NPC::testTouch() -{ - if (m_curlevel.expired()) - return; - - testForLinks(); -} - -void NPC::freeScriptResources() -{ - ScriptEngine* scriptEngine = m_server->getScriptEngine(); - - // Clear cached script - if (!m_npcScript.getServerSide().empty()) - scriptEngine->clearCache(m_npcScript.getServerSide()); - - // Reset script execution - m_scriptExecutionContext.resetExecution(); - - // Clear any queued actions - scriptEngine->unregisterNpcUpdate(this); - - // Clear timeouts & scheduled events - scriptEngine->unregisterNpcTimer(this); - m_scriptTimers.clear(); - m_timeout = 0; - - // Clear triggeraction functions - for (auto& _triggerAction: m_triggerActions) - delete _triggerAction.second; - m_triggerActions.clear(); - - // Delete script object - if (m_scriptObject) - m_scriptObject.reset(); -} - -// Set callbacks for triggeractions! -void NPC::registerTriggerAction(const std::string& action, IScriptFunction* cbFunc) -{ - // clear old callback if it was set - auto triggerIter = m_triggerActions.find(action); - if (triggerIter != m_triggerActions.end()) - delete triggerIter->second; - - // register new trigger - m_triggerActions[action] = cbFunc; -} - -void NPC::queueNpcTrigger(const std::string& action, Player* player, const std::string& data) -{ - assert(m_scriptObject); - - // Check if we respond to this trigger - auto triggerIter = m_triggerActions.find(action); - if (triggerIter == m_triggerActions.end()) - return; - - ScriptEngine* scriptEngine = m_server->getScriptEngine(); - - IScriptObject* playerObject = nullptr; - if (player != nullptr) - playerObject = player->getScriptObject(); - - if (playerObject) - { - m_scriptExecutionContext.addAction(scriptEngine->createAction("npc.trigger", getScriptObject(), triggerIter->second, playerObject, data)); - } - else - { - m_scriptExecutionContext.addAction(scriptEngine->createAction("npc.trigger", getScriptObject(), triggerIter->second, nullptr, data)); - } - - scriptEngine->registerNpcUpdate(this); -} - -ScriptClass* NPC::joinClass(const std::string& className) -{ - auto found = m_classMap.find(className); - if (found != m_classMap.end()) - return nullptr; - - auto classObj = m_server->getClass(className); - if (!classObj) - return nullptr; - - m_classMap[className] = classObj->getSource().getClientGS1(); - updateClientCode(); - m_modTime[NPCPROP_CLASS] = time(0); - return classObj; -} - -void NPC::updateClientCode() -{ - // Skip servercode, and read client script - CString tmpScript = std::string{ m_npcScript.getClientGS1() }; - - // Iterate current classes, and add to end of code - for (auto& it: m_classMap) - tmpScript << "\n" - << it.second; - - // Remove comments and trim the code if specified. - m_clientScriptFormatted = minifyClientCode(tmpScript); - - // Just a little warning for people who don't know. - if (m_clientScriptFormatted.length() > 0x705F) - printf("WARNING: Clientside script of NPC (%s) exceeds the limit of 28767 bytes.\n", (m_weaponName.length() != 0 ? m_weaponName.text() : m_image.c_str())); - - // Compile gs2 - if (!m_npcScript.getClientGS2().empty()) - { - // Compile gs2 code - m_server->compileGS2Script(this, - [this](const CompilerResponse& response) - { - if (response.success) - { - auto& byteCode = response.bytecode; - m_npcBytecode.clear(byteCode.length()); - m_npcBytecode.write((const char*)byteCode.buffer(), (int)byteCode.length()); - } - }); - } - - // Update prop for players - this->updatePropModTime(NPCPROP_SCRIPT); -} - -void NPC::setTimeout(int newTimeout) -{ - m_timeout = newTimeout; - - if (hasTimerUpdates()) - m_server->getScriptEngine()->registerNpcTimer(this); - else - m_server->getScriptEngine()->unregisterNpcTimer(this); -} - -void NPC::queueNpcAction(const std::string& action, Player* player, bool registerAction) -{ - assert(m_scriptObject); - - ScriptEngine* scriptEngine = m_server->getScriptEngine(); - - IScriptObject* playerObject = nullptr; - if (player != nullptr) - playerObject = player->getScriptObject(); - - if (playerObject) - { - m_scriptExecutionContext.addAction(scriptEngine->createAction(action, getScriptObject(), playerObject)); - } - else - { - m_scriptExecutionContext.addAction(scriptEngine->createAction(action, getScriptObject())); - } - - if (registerAction) - scriptEngine->registerNpcUpdate(this); -} - -bool NPC::runScriptTimer() -{ - // TODO(joey): Scheduled events, pass in delta, use milliseconds as an integer - - if (m_timeout > 0) - { - m_timeout--; - if (m_timeout == 0) - queueNpcAction("npc.timeout", 0, true); - } - - // scheduled events - bool queued = false; - for (auto it = m_scriptTimers.begin(); it != m_scriptTimers.end();) - { - ScriptEventTimer& timer = *it; - timer.timer--; - if (timer.timer == 0) - { - m_scriptExecutionContext.addAction(timer.action); - it = m_scriptTimers.erase(it); - queued = true; - continue; - } - - ++it; - } - - // Register for npc updates - if (queued) - m_server->getScriptEngine()->registerNpcUpdate(this); - - // return value dictates if this gets unregistered from timer updates - return hasTimerUpdates(); -} - -NPCEventResponse NPC::runScriptEvents() -{ - bool hasActions = false; - if (!m_npcDeleteRequested) - hasActions = m_scriptExecutionContext.runExecution(); // Returns true if we still have actions to run - - // Send properties modified by scripts - if (!m_propModified.empty()) - { - if (m_propModified.contains(NPCPROP_X2) || m_propModified.contains(NPCPROP_Y2)) - { - testTouch(); - } - - time_t newModTime = time(0); - - CString propPacket = CString() >> (char)PLO_NPCPROPS >> (int)m_id; - for (unsigned char propId: m_propModified) - { - m_modTime[propId] = newModTime; - propPacket >> (char)propId << getProp(propId); - } - m_propModified.clear(); - - if (!m_curlevel.expired()) - m_server->sendPacketToLevelArea(propPacket, m_curlevel); - } - - if (m_npcDeleteRequested) - { - m_npcDeleteRequested = false; - return NPCEventResponse::Delete; - } - - return (hasActions ? NPCEventResponse::PendingEvents : NPCEventResponse::NoEvents); -} - -CString NPC::getVariableDump() -{ - static const char* const propNames[NPCPROP_COUNT] = { - "image", "script", "x", "y", "power", "rupees", - "arrows", "bombs", "glovepower", "bombpower", "sword", "shield", - "animation", "visibility flags", "blocking flags", "message", "hurtdxdy", - "id", "sprite", "colors", "nickname", "horse", "head", - "save[0]", "save[1]", "save[2]", "save[3]", "save[4]", "save[5]", - "save[6]", "save[7]", "save[8]", "save[9]", - "alignment", "imagepart", "body", - "ganiattr1", "ganiattr2", "ganiattr3", "ganiattr4", "ganiattr5", - "mapx", "mapy", "UNKNOWN43", "ganiattr6", "ganiattr7", "ganiattr8", - "ganiattr9", "UNKNOWN48", "scripter", "name", "type", "level", - "ganiattr10", "ganiattr11", "ganiattr12", "ganiattr13", "ganiattr14", - "ganiattr15", "ganiattr16", "ganiattr17", "ganiattr18", "ganiattr19", - "ganiattr20", "ganiattr21", "ganiattr22", "ganiattr23", "ganiattr24", - "ganiattr25", "ganiattr26", "ganiattr27", "ganiattr28", "ganiattr29", - "ganiattr30", "joinedclasses", "xprecise", "yprecise" - }; - - static const char propList[] = { - NPCPROP_ID, NPCPROP_IMAGE, NPCPROP_SCRIPT, NPCPROP_VISFLAGS, NPCPROP_BLOCKFLAGS, - NPCPROP_HEADIMAGE, NPCPROP_BODYIMAGE, NPCPROP_SWORDIMAGE, NPCPROP_SHIELDIMAGE, - NPCPROP_NICKNAME, NPCPROP_SPRITE, NPCPROP_GANI, - NPCPROP_GATTRIB1, NPCPROP_GATTRIB2, NPCPROP_GATTRIB3, NPCPROP_GATTRIB4, NPCPROP_GATTRIB5, - NPCPROP_GATTRIB6, NPCPROP_GATTRIB7, NPCPROP_GATTRIB8, NPCPROP_GATTRIB9, NPCPROP_GATTRIB10, - NPCPROP_GATTRIB11, NPCPROP_GATTRIB12, NPCPROP_GATTRIB13, NPCPROP_GATTRIB14, NPCPROP_GATTRIB15, - NPCPROP_GATTRIB16, NPCPROP_GATTRIB17, NPCPROP_GATTRIB18, NPCPROP_GATTRIB19, NPCPROP_GATTRIB20, - NPCPROP_GATTRIB21, NPCPROP_GATTRIB22, NPCPROP_GATTRIB23, NPCPROP_GATTRIB24, NPCPROP_GATTRIB25, - NPCPROP_GATTRIB26, NPCPROP_GATTRIB27, NPCPROP_GATTRIB28, NPCPROP_GATTRIB29, NPCPROP_GATTRIB30, - NPCPROP_SAVE0, NPCPROP_SAVE1, NPCPROP_SAVE2, NPCPROP_SAVE3, NPCPROP_SAVE4, - NPCPROP_SAVE5, NPCPROP_SAVE6, NPCPROP_SAVE7, NPCPROP_SAVE8, NPCPROP_SAVE9, - NPCPROP_GMAPLEVELX, NPCPROP_GMAPLEVELY, NPCPROP_X2, NPCPROP_Y2 - }; - - const int propsCount = sizeof(propList) / sizeof(char); - - // Create the npc dump... - CString npcDump; - CString npcNameStr = m_npcName; - if (npcNameStr.isEmpty()) - npcNameStr = CString() << "npcs[" << CString(m_id) << "]"; - - auto level = getLevel(); - - npcDump << "Variables dump from npc " << npcNameStr << "\n\n"; - if (!m_npcScriptType.isEmpty()) npcDump << npcNameStr << ".type: " << m_npcScriptType << "\n"; - if (!m_npcScripter.isEmpty()) npcDump << npcNameStr << ".scripter: " << m_npcScripter << "\n"; - if (level) npcDump << npcNameStr << ".level: " << level->getLevelName() << "\n"; - - npcDump << "\nAttributes:\n"; - for (int propId: propList) - { - CString prop = getProp(propId); - - switch (propId) - { - case NPCPROP_ID: - { - int id = prop.readGUInt(); - npcDump << npcNameStr << "." << propNames[propId] << ": " << CString((int)id) << "\n"; - break; - } - - case NPCPROP_SCRIPT: - { - int len = prop.readGUShort(); - if (len > 0) - npcDump << npcNameStr << "." << propNames[propId] << ": size: " << CString(len) << "\n"; - break; - } - - case NPCPROP_SWORDIMAGE: - { - int sp = prop.readGUChar(); - CString image; - if (sp > 30) - { - image = prop.readChars(prop.readGUChar()); - sp -= 30; - } - else if (sp > 0) - image = CString() << "sword" << CString(sp) << ".png"; - - if (!image.isEmpty()) - npcDump << npcNameStr << "." << propNames[propId] << ": " << image << " (" << CString(sp) << ")\n"; - - break; - } - - case NPCPROP_SHIELDIMAGE: - { - int sp = prop.readGUChar(); - CString image; - if (sp > 10) - { - image = prop.readChars(prop.readGUChar()); - sp -= 10; - } - else if (sp > 0) - image = CString() << "shield" << CString(sp) << ".png"; - - if (!image.isEmpty()) - npcDump << npcNameStr << "." << propNames[propId] << ": " << image << " (" << CString(sp) << ")\n"; - - break; - } - - case NPCPROP_VISFLAGS: - { - char value = prop.readGUChar(); - npcDump << npcNameStr << "." << propNames[propId] << ": "; - npcDump << ((value & NPCVISFLAG_VISIBLE) ? "visible" : "hidden"); - if (value & NPCVISFLAG_DRAWOVERPLAYER) - npcDump << ", drawoverplayer"; - if (value & NPCVISFLAG_DRAWUNDERPLAYER) - npcDump << ", drawunderplayer"; - npcDump << "\n"; - - break; - } - - case NPCPROP_BLOCKFLAGS: - { - char value = prop.readGUChar(); - if (value & NPCBLOCKFLAG_NOBLOCK) - npcDump << npcNameStr << "." << propNames[propId] << ": " - << "dontblock" - << "\n"; - break; - } - - case NPCPROP_SPRITE: - { - char value = prop.readGUChar(); - if (value > 0) - npcDump << npcNameStr << "." << propNames[propId] << ": " << CString((int)value) << "\n"; - break; - } - - case NPCPROP_GMAPLEVELX: - case NPCPROP_GMAPLEVELY: - { - char value = prop.readGUChar(); - if (level && level->getMap() != nullptr) - npcDump << npcNameStr << "." << propNames[propId] << ": " << CString((int)value) << "\n"; - break; - } - - case NPCPROP_IMAGE: - case NPCPROP_GANI: - case NPCPROP_MESSAGE: - case NPCPROP_NICKNAME: - case NPCPROP_HORSEIMAGE: - case NPCPROP_HEADIMAGE: - case NPCPROP_BODYIMAGE: - case NPCPROP_SCRIPTER: - case NPCPROP_NAME: - case NPCPROP_TYPE: - case NPCPROP_CURLEVEL: - case NPCPROP_GATTRIB1: - case NPCPROP_GATTRIB2: - case NPCPROP_GATTRIB3: - case NPCPROP_GATTRIB4: - case NPCPROP_GATTRIB5: - case NPCPROP_GATTRIB6: - case NPCPROP_GATTRIB7: - case NPCPROP_GATTRIB8: - case NPCPROP_GATTRIB9: - case NPCPROP_GATTRIB10: - case NPCPROP_GATTRIB11: - case NPCPROP_GATTRIB12: - case NPCPROP_GATTRIB13: - case NPCPROP_GATTRIB14: - case NPCPROP_GATTRIB15: - case NPCPROP_GATTRIB16: - case NPCPROP_GATTRIB17: - case NPCPROP_GATTRIB18: - case NPCPROP_GATTRIB19: - case NPCPROP_GATTRIB20: - case NPCPROP_GATTRIB21: - case NPCPROP_GATTRIB22: - case NPCPROP_GATTRIB23: - case NPCPROP_GATTRIB24: - case NPCPROP_GATTRIB25: - case NPCPROP_GATTRIB26: - case NPCPROP_GATTRIB27: - case NPCPROP_GATTRIB28: - case NPCPROP_GATTRIB29: - case NPCPROP_GATTRIB30: - { - int len = prop.readGUChar(); - if (len > 0) - { - CString value = prop.readChars(len).trim(); - if (!value.isEmpty()) - npcDump << npcNameStr << "." << propNames[propId] << ": " << value << "\n"; - } - break; - } - - case NPCPROP_SAVE0: - case NPCPROP_SAVE1: - case NPCPROP_SAVE2: - case NPCPROP_SAVE3: - case NPCPROP_SAVE4: - case NPCPROP_SAVE5: - case NPCPROP_SAVE6: - case NPCPROP_SAVE7: - case NPCPROP_SAVE8: - case NPCPROP_SAVE9: - { - unsigned char saveValue = prop.readGUChar(); - if (saveValue > 0) - npcDump << npcNameStr << "." << propNames[propId] << ": " << CString((int)saveValue) << "\n"; - break; - } - - case NPCPROP_X2: - case NPCPROP_Y2: - { - short pos = prop.readGUShort(); - pos = ((pos & 0x0001) ? -(pos >> 1) : pos >> 1); - npcDump << npcNameStr << "." << propNames[propId] << ": " << CString((double)pos / 16.0) << "\n"; - break; - } - - default: - continue; - } - } - - if (m_timeout > 0) - npcDump << npcNameStr << ".timeout: " << CString((float)(m_timeout * 0.05f)) << "\n"; - - std::pair executionData = m_scriptExecutionContext.getExecutionData(); - npcDump << npcNameStr << ".scripttime (in the last min): " << CString(executionData.second) << "\n"; - npcDump << npcNameStr << ".scriptcalls: " << CString(executionData.first) << "\n"; - - if (!m_flagList.empty()) - { - npcDump << "\nnpc.Flags:\n"; - for (auto it = m_flagList.begin(); it != m_flagList.end(); ++it) - npcDump << npcNameStr << ".flags[\"" << (*it).first << "\"]: " << (*it).second << "\n"; - } - - return npcDump; -} - -bool NPC::deleteNPC() -{ - if (getType() == NPCType::PUTNPC) - { - m_npcDeleteRequested = true; - registerNpcUpdates(); - } - - return m_npcDeleteRequested; -} - -void NPC::reloadNPC() -{ - setScriptCode(m_npcScript.getSource()); -} - -void NPC::resetNPC() -{ - // TODO(joey): reset script execution, clear flags.. unsure what else gets reset. TBD - m_canWarp = NPCWarpType::None; - m_modTime[NPCPROP_IMAGE] = m_modTime[NPCPROP_SCRIPT] = m_modTime[NPCPROP_X] = m_modTime[NPCPROP_Y] = m_modTime[NPCPROP_VISFLAGS] = m_modTime[NPCPROP_ID] = m_modTime[NPCPROP_SPRITE] = m_modTime[NPCPROP_MESSAGE] = m_modTime[NPCPROP_GMAPLEVELX] = m_modTime[NPCPROP_GMAPLEVELY] = m_modTime[NPCPROP_X2] = m_modTime[NPCPROP_Y2] = time(0); - m_character.headImage = ""; - m_character.bodyImage = ""; - m_image = ""; - m_character.gani = "idle"; - - // Reset script execution - setScriptCode(m_npcScript.getSource()); - - if (!m_origLevel.isEmpty()) - { - warpNPC(Level::findLevel(m_origLevel, m_server), m_origX, m_origY); - } -} - -void NPC::moveNPC(int dx, int dy, double time, int options) -{ - // TODO(joey): Implement options? Or does the client handle them? TBD - // - If we want function callbacks we will need to handle time, can schedule an event once that is implemented - - int start_x = ((uint16_t)std::abs(m_x) << 1) | (m_x < 0 ? 0x0001 : 0x0000); - int start_y = ((uint16_t)std::abs(m_y) << 1) | (m_y < 0 ? 0x0001 : 0x0000); - int delta_x = ((uint16_t)std::abs(dx) << 1) | (dx < 0 ? 0x0001 : 0x0000); - int delta_y = ((uint16_t)std::abs(dy) << 1) | (dy < 0 ? 0x0001 : 0x0000); - short itime = (short)(time / 0.05); - - setPixelX(m_x + dx); - setPixelY(m_y + dy); - - if (!m_curlevel.expired()) - m_server->sendPacketToLevelArea(CString() >> (char)PLO_MOVE2 >> (int)m_id >> (short)start_x >> (short)start_y >> (short)delta_x >> (short)delta_y >> (short)itime >> (char)options, m_curlevel); - - if (isWarpable()) - testTouch(); -} - -void NPC::warpNPC(std::shared_ptr pLevel, int pX, int pY) -{ - if (!pLevel) - return; - - auto level = getLevel(); - if (level != nullptr) - { - // TODO(joey): NPCMOVED needs to be sent to everyone who potentially has this level cached or else the npc - // will stay visible when you come back to the level. Should this just be sent to everyone on the server? We do - // such for PLO_NPCDEL - m_server->sendPacketToType(PLTYPE_ANYPLAYER, CString() >> (char)PLO_NPCMOVED >> (int)m_id); - - // Remove the npc from the old level - level->removeNPC(m_id); - } - - // Add to the new level - pLevel->addNPC(m_id); - level = pLevel; - - // Adjust the position of the npc - m_x = pX; - m_y = pY; - - updatePropModTime(NPCPROP_CURLEVEL); - updatePropModTime(NPCPROP_GMAPLEVELX); - updatePropModTime(NPCPROP_GMAPLEVELY); - updatePropModTime(NPCPROP_X2); - updatePropModTime(NPCPROP_Y2); - - // Send the properties to the players in the new level - m_server->sendPacketToLevelArea(CString() >> (char)PLO_NPCPROPS >> (int)m_id << getProps(0), level); - - if (!m_npcName.empty()) - m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_NC_NPCADD >> (int)m_id >> (char)NPCPROP_CURLEVEL << getProp(NPCPROP_CURLEVEL)); - - // Queue event - this->queueNpcAction("npc.warped"); -} - -void NPC::saveNPC() -{ - // TODO(joey): save localnpcs aka putnpcs to a localnpcs folder, as of now - // we are only saving database npcs. - - if (getType() != NPCType::DBNPC) - { - return; - } - - // TODO(joey): check if properties have been modified before deciding to save - // enumerate scriptObject variables, to save into file and load later..? - - // Clean up old samples - //m_scriptExecutionContext.getExecutionData(); - - /* - CString saveDir; - CString saveName; - if (getType() == NPCType::DBNPC) - { - saveDir = "npcs/"; - saveName = m_npcName; - } - else if (getType() == NPCType::PUTNPC) - { - saveDir = "npcprops/"; - saveName = CString("localnpc_"); - - if (level && level->getMap()) - { - saveName << removeExtension(m_origLevel) << "_" << level->getMapX() << "_" << level->getMapY(); - } - } - - // Level npcs shouldn't be saved - if (saveDir.isEmpty()) - { - return; - } - */ - - auto level = getLevel(); - - static const char* NL = "\r\n"; - CString fileName = m_server->getServerPath() << "npcs/npc" << m_npcName << ".txt"; - CString fileData = CString("GRNPC001") << NL; - fileData << "NAME " << m_npcName << NL; - fileData << "ID " << CString(m_id) << NL; - fileData << "TYPE " << m_npcScriptType << NL; - fileData << "SCRIPTER " << m_npcScripter << NL; - fileData << "IMAGE " << m_image << NL; - fileData << "STARTLEVEL " << m_origLevel << NL; - fileData << "STARTX " << CString((float)m_origX / 16.0f) << NL; - fileData << "STARTY " << CString((float)m_origY / 16.0f) << NL; - fileData << "STARTZ " << CString((float)m_origY / 16.0f) << NL; - if (level) - { - fileData << "LEVEL " << level->getLevelName() << NL; - fileData << "X " << CString(getX()) << NL; - fileData << "Y " << CString(getY()) << NL; - fileData << "Z " << CString(getZ()) << NL; - } - fileData << "NICK " << m_character.nickName << NL; - fileData << "ANI " << m_character.gani << NL; - fileData << "HP " << CString(m_character.hitpoints) << NL; - fileData << "GRALATS " << CString(m_character.gralats) << NL; - fileData << "ARROWS " << CString(m_character.arrows) << NL; - fileData << "BOMBS " << CString(m_character.bombs) << NL; - fileData << "GLOVEP " << CString(m_character.glovePower) << NL; - fileData << "SWORDP " << CString(m_character.swordPower) << NL; - fileData << "SHIELDP " << CString(m_character.shieldPower) << NL; - fileData << "BOWP" << CString(m_character.bowPower) << NL; - fileData << "BOW" << m_character.bowImage << NL; - fileData << "HEAD " << m_character.headImage << NL; - fileData << "BODY " << m_character.bodyImage << NL; - fileData << "SWORD " << m_character.swordImage << NL; - fileData << "SHIELD " << m_character.shieldImage << NL; - fileData << "HORSE " << m_character.horseImage << NL; - fileData << "COLORS " << CString((int)m_character.colors[0]) << "," << CString((int)m_character.colors[1]) << "," << CString((int)m_character.colors[2]) << "," << CString((int)m_character.colors[3]) << "," << CString((int)m_character.colors[4]) << NL; - fileData << "SPRITE " << CString(m_character.sprite) << NL; - fileData << "AP " << CString(m_character.ap) << NL; - fileData << "TIMEOUT " << CString(m_timeout / 20) << NL; - fileData << "LAYER 0" << NL; - fileData << "SHAPETYPE 0" << NL; - fileData << "SHAPE " << CString(m_width) << " " << CString(m_height) << NL; - - if (m_blockFlags & NPCBLOCKFLAG_NOBLOCK) - fileData << "DONTBLOCK 1" << NL; - - fileData << "SAVEARR " << CString((int)m_saves[0]) << "," << CString((int)m_saves[1]) << "," << CString((int)m_saves[2]) << "," - << CString((int)m_saves[3]) << "," << CString((int)m_saves[4]) << "," << CString((int)m_saves[5]) << "," - << CString((int)m_saves[6]) << "," << CString((int)m_saves[7]) << "," << CString((int)m_saves[8]) << "," - << CString((int)m_saves[9]) << NL; - - for (int i = 0; i < 30; i++) - { - if (!m_character.ganiAttributes[i].isEmpty()) - fileData << "ATTR" << std::to_string(i + 1) << " " << m_character.ganiAttributes[i] << NL; - } - - for (auto it = m_flagList.begin(); it != m_flagList.end(); ++it) - fileData << "FLAG " << (*it).first << "=" << (*it).second << NL; - - fileData << "NPCSCRIPT" << NL << CString(m_npcScript.getSource()).replaceAll("\n", NL); - if (fileData[fileData.length() - 1] != '\n') - fileData << NL; - fileData << "NPCSCRIPTEND" << NL; - fileData.save(fileName); -} - -bool NPC::loadNPC(const CString& fileName) -{ - // Load file - CString fileData; - if (!fileData.load(fileName)) - return false; - - fileData.removeAllI("\r"); - - CString headerLine = fileData.readString("\n"); - if (headerLine != "GRNPC001") - return false; - - // TODO(joey): implement - // JOINEDCLASSES staffblock (not really needed for us, so) - // DONTBLOCK 1 - // modtime is not being updated for these properties - - time_t updateTime = time(0); - CString npcLevel; - std::string script; - - CString propPacket; - - // Parse File - while (fileData.bytesLeft()) - { - CString curLine = fileData.readString("\n"); - - // Find Command - CString curCommand = curLine.readString(); - - // Parse Line - if (curCommand == "NAME") - { - m_npcName = curLine.readString("").text(); - m_modTime[NPCPROP_NAME] = updateTime; - } - else if (curCommand == "ID") - m_id = strtoint(curLine.readString("")); - else if (curCommand == "TYPE") - m_npcScriptType = curLine.readString(""); - else if (curCommand == "SCRIPTER") - { - m_npcScripter = curLine.readString(""); - m_modTime[NPCPROP_SCRIPTER] = updateTime; - } - else if (curCommand == "IMAGE") - { - m_image = curLine.readString("").text(); - m_modTime[NPCPROP_IMAGE] = updateTime; - } - else if (curCommand == "STARTLEVEL") - m_origLevel = curLine.readString(""); - else if (curCommand == "STARTX") - m_origX = int(strtofloat(curLine.readString("")) * 16); - else if (curCommand == "STARTY") - m_origY = int(strtofloat(curLine.readString("")) * 16); - else if (curCommand == "STARTZ") - m_origZ = int(strtofloat(curLine.readString("")) * 16); - else if (curCommand == "LEVEL") - npcLevel = curLine.readString(""); - else if (curCommand == "X") - setX(strtofloat(curLine.readString(""))); - else if (curCommand == "Y") - setY(strtofloat(curLine.readString(""))); - else if (curCommand == "Z") - setZ(strtofloat(curLine.readString(""))); - else if (curCommand == "MAPX") - { - //gmaplevelx = strtoint(curLine.readString("")); - m_modTime[NPCPROP_GMAPLEVELX] = updateTime; - } - else if (curCommand == "MAPY") - { - //gmaplevely = strtoint(curLine.readString("")); - m_modTime[NPCPROP_GMAPLEVELY] = updateTime; - } - else if (curCommand == "NICK") - { - m_character.nickName = curLine.readString("").text(); - m_modTime[NPCPROP_NICKNAME] = updateTime; - } - else if (curCommand == "ANI") - { - m_character.gani = curLine.readString("").text(); - m_modTime[NPCPROP_GANI] = updateTime; - } - else if (curCommand == "HP") - { - m_character.hitpoints = (int)strtofloat(curLine.readString("")); - m_modTime[NPCPROP_POWER] = updateTime; - } - else if (curCommand == "GRALATS") - { - m_character.gralats = strtoint(curLine.readString("")); - m_modTime[NPCPROP_RUPEES] = updateTime; - } - else if (curCommand == "ARROWS") - { - m_character.arrows = strtoint(curLine.readString("")); - m_modTime[NPCPROP_ARROWS] = updateTime; - } - else if (curCommand == "BOMBS") - { - m_character.bombs = strtoint(curLine.readString("")); - m_modTime[NPCPROP_BOMBS] = updateTime; - } - else if (curCommand == "GLOVEP") - { - m_character.glovePower = strtoint(curLine.readString("")); - m_modTime[NPCPROP_GLOVEPOWER] = updateTime; - } - else if (curCommand == "SWORDP") - { - m_character.swordPower = strtoint(curLine.readString("")); - m_modTime[NPCPROP_SWORDIMAGE] = updateTime; - } - else if (curCommand == "SHIELDP") - { - m_character.shieldPower = strtoint(curLine.readString("")); - m_modTime[NPCPROP_SHIELDIMAGE] = updateTime; - } - else if (curCommand == "BOWP") - { - m_character.bowPower = strtoint(curLine.readString("")); - m_modTime[NPCPROP_GANI] = updateTime; - } - else if (curCommand == "BOW") - { - m_character.bowImage = curLine.readString(""); - m_modTime[NPCPROP_GANI] = updateTime; - } - else if (curCommand == "HEAD") - { - m_character.headImage = curLine.readString(""); - m_modTime[NPCPROP_HEADIMAGE] = updateTime; - } - else if (curCommand == "BODY") - { - m_character.bodyImage = curLine.readString(""); - m_modTime[NPCPROP_BODYIMAGE] = updateTime; - } - else if (curCommand == "SWORD") - { - m_character.swordImage = curLine.readString(""); - m_modTime[NPCPROP_SWORDIMAGE] = updateTime; - } - else if (curCommand == "SHIELD") - { - m_character.shieldImage = curLine.readString(""); - m_modTime[NPCPROP_SHIELDIMAGE] = updateTime; - } - else if (curCommand == "HORSE") - { - m_character.horseImage = curLine.readString(""); - m_modTime[NPCPROP_HORSEIMAGE] = updateTime; - } - else if (curCommand == "SPRITE") - { - m_character.sprite = strtoint(curLine.readString("")); - m_modTime[NPCPROP_SPRITE] = updateTime; - } - else if (curCommand == "AP") - { - m_character.ap = strtoint(curLine.readString("")); - m_modTime[NPCPROP_ALIGNMENT] = updateTime; - } - else if (curCommand == "COLORS") - { - auto tokens = curLine.readString("").tokenize(","); - for (int idx = 0; idx < std::min((int)tokens.size(), 5); idx++) - m_character.colors[idx] = strtoint(tokens[idx]); - m_modTime[NPCPROP_COLORS] = updateTime; - } - else if (curCommand == "SAVEARR") - { - auto tokens = curLine.readString("").tokenize(","); - for (int idx = 0; idx < std::min(tokens.size(), sizeof(m_saves) / sizeof(unsigned char)); idx++) - { - m_saves[idx] = (unsigned char)strtoint(tokens[idx]); - m_modTime[NPCPROP_SAVE0 + idx] = updateTime; - } - } - else if (curCommand == "SHAPE") - { - m_width = strtoint(curLine.readString(" ")); - m_height = strtoint(curLine.readString(" ")); - } - else if (curCommand == "CANWARP") - { - m_canWarp = strtoint(curLine.readString("")) != 0 ? NPCWarpType::AllLinks : m_canWarp; - } - else if (curCommand == "CANWARP2") - { - m_canWarp = strtoint(curLine.readString("")) != 0 ? NPCWarpType::OverworldLinks : m_canWarp; - } - else if (curCommand == "TIMEOUT") - m_timeout = strtoint(curLine.readString("")) * 20; - else if (curCommand == "FLAG") - { - CString flagName = curLine.readString("="); - CString flagValue = curLine.readString(""); - setFlag(flagName.text(), flagValue); - } - else if (curCommand.subString(0, 4) == "ATTR") - { - CString attrIdStr = curCommand.subString(5); - int attrId = strtoint(attrIdStr); - if (attrId > 0 && attrId < 30) - { - int idx = attrId - 1; - m_character.ganiAttributes[idx] = curLine.readString(""); - m_modTime[__nAttrPackets[idx]] = updateTime; - } - } - else if (curCommand == "NPCSCRIPT") - { - do { - curLine = fileData.readString("\n"); - if (curLine == "NPCSCRIPTEND") - break; - - script.append(curLine.text(), curLine.length()).append(1, '\n'); - } - while (fileData.bytesLeft()); - - m_modTime[NPCPROP_SCRIPT] = updateTime; - } - } - - setScriptCode(std::move(script)); - - if (npcLevel.isEmpty()) - npcLevel = m_origLevel; - - if (!npcLevel.isEmpty()) - m_curlevel = Level::findLevel(npcLevel, m_server); - - return true; -} - -#endif - -CString toWeaponName(const CString& code) -{ - int name_start = code.find("toweapons "); - if (name_start == -1) return CString(); - name_start += 10; // 10 = strlen("toweapons ") - - int name_end[2] = { code.find(";", name_start), code.find("}", name_start) }; - if (name_end[0] == -1 && name_end[1] == -1) return CString(); - - int name_pos = -1; - if (name_end[0] == -1) name_pos = name_end[1]; - if (name_end[1] == -1) name_pos = name_end[0]; - if (name_pos == -1) name_pos = (name_end[0] < name_end[1]) ? name_end[0] : name_end[1]; - - return code.subString(name_start, name_pos - name_start).trim(); -} - -CString doJoins(const CString& code, FileSystem* fs) -{ - CString ret; - CString c(code); - std::vector joinList; - - // Parse out all the joins. - while (c.bytesLeft()) - { - ret << c.readString("join "); - - int pos = c.readPos(); - int loc = c.find(";", pos); - if (loc != -1) - { - CString spacecheck = c.subString(pos, loc - pos); - if (!spacecheck.contains(" \t") && c.bytesLeft()) - { - ret << ";\n"; - joinList.push_back(CString() << c.readString(";") << ".txt"); - } - } - } - - // Add the files now. - for (auto& fileName: joinList) - { - c = fs->load(fileName); - c.removeAllI("\r"); - ret << removeComments(c); - } - - return ret; -} diff --git a/server/src/Server.cpp b/server/src/Server.cpp index aa9e7eca9..694270774 100644 --- a/server/src/Server.cpp +++ b/server/src/Server.cpp @@ -1,39 +1,79 @@ -#include - +#include +#include #include +#include #include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include -#include +#include #include +#include #include -#include "main.h" - -#include "NPC.h" -#include "Player.h" -#include "Server.h" -#include "Weapon.h" -#include "level/Level.h" -#include "level/Map.h" -#include "scripting/ScriptOrigin.h" -#include "scripting/ScriptClass.h" - -static const char* const filesystemTypes[] = { - "all", - "file", - "level", - "head", - "body", - "sword", - "shield", - 0 -}; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// extern std::atomic_bool shutdownProgram; +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + template auto methodstub(T* t, R (T::*m)(Args...)) { @@ -44,73 +84,397 @@ auto methodstub(T* t, R (T::*m)(Args...)) } // I don't want to deal with adding this to the gs2lib. -CString operator<<(const CString& first, const CString& second) +[[maybe_unused]] static CString operator<<(const CString& first, const CString& second) { - CString result{ first }; + CString result{first}; return result << second; } +/////////////////////////////////////////////////////////////////////////////// + +void ExternalServerCachedSettings::bind(Server* server) +{ + auto& settings = server->getSettings(); + + settings.track(maxPlayers, sleepWhenNoPlayers); + settings.track(unstickMeLevel, unstickMeTile[0], unstickMeTile[1], unstickMeSeconds); + settings.track(enableBushItemDrops, enableVaseItemDrops, disableItemDropping); + settings.track(enableInsideSyncDistance, syncDistance[0], syncDistance[1]); + settings.track(eventDistance, triggerDistance, sendTriggerActionsToPlayers); + settings.track(enableFlagCropping, disableExplosions, enableClientsidePushPull, tileRespawnTime, enableIdleDisconnect, idleTimeoutSeconds); + settings.track(enablePermanentTileChanges, saveTileChangesToLevelFile); + + // triggerhacks + settings.track(enableFlaghackMovement, enableTriggerhackExecscript, enableTriggerhackFiles, enableTriggerhackGroups, enableTriggerhackGuilds, enableTriggerhackLevels, enableTriggerhackProps, enableTriggerhackRC, enableTriggerhackWeapons); + + // npc-server + settings.track(forceClientsideLinks, forceClientsideSigns, enableItemDropEvents, itemDropEventsOnlyForGralats, projectilesStopOnWall, runAllScriptEvents); + + // security + settings.track(normalAdminsCanChangeGralats, protectedWeapons, jailLevels); + + // player + settings.track(enableDefaultWeapons, maxHeartLimit, enableExBodyColors, playerTouchesMeNoZ, lockPlayerZ); + settings.track(enableAPSystem, apSystemThresholdSeconds[0], apSystemThresholdSeconds[1], apSystemThresholdSeconds[2], apSystemThresholdSeconds[3], apSystemThresholdSeconds[4]); + settings.track(playerProfileVariables, playerStatusList); +} + +/////////////////////////////////////////////////////////////////////////////// + Server::Server(const CString& pName) - : running(false), m_doRestart(false), m_name(pName), m_animationManager(this), m_packageManager(this), m_serverStartTime(0), + : m_animationManager(this), m_packageManager(this), m_name(pName), m_triggerActionDispatcher(methodstub(this, &Server::createTriggerCommands)) { - auto time_now = std::chrono::high_resolution_clock::now(); - m_lastTimer = m_lastNewWorldTimer = m_last1mTimer = m_last5mTimer = m_last3mTimer = time_now; - calculateServerTime(); - - // This has the full path to the server directory. - m_serverPath = CString() << getBaseHomePath() << "servers/" << m_name << "/"; - FileSystem::fixPathSeparators(m_serverPath); - - // Set up the log files. - CString logpath = m_serverPath.remove(0, static_cast(getBaseHomePath().length())); - CString npcPath = CString() << logpath << "logs/npclog.txt"; - CString rcPath = CString() << logpath << "logs/rclog.txt"; - CString serverPath = CString() << logpath << "logs/serverlog.txt"; - FileSystem::fixPathSeparators(npcPath); - FileSystem::fixPathSeparators(rcPath); - FileSystem::fixPathSeparators(serverPath); - m_npcLog.setFilename(npcPath); - m_rcLog.setFilename(rcPath); - m_serverLog.setFilename(serverPath); - -#ifdef V8NPCSERVER - CString scriptPath = CString() << logpath << "logs/scriptlog.txt"; - FileSystem::fixPathSeparators(scriptPath); - m_scriptLog.setFilename(scriptPath); -#endif + calculateNWTime(); + + m_npcIdGenerator.createSegment(NPCID_GEN_LOCAL); + m_npcIdGenerator.createSegment(NPCID_GEN_DATABASE); + m_npcIdGenerator.createSegment(NPCID_GEN_MANUAL); + //m_playerIdGenerator.createSegment(PLAYERID_GEN_EXTERNAL); + + m_accountLoader = std::make_unique(); + m_npcLoader = std::make_unique(); + + m_timedEvents.callbackIterations = std::bind(&Server::doTimedEvents, this, std::placeholders::_1); + m_timedSave.callbackIterations = [this](int) + { + saveServerFlags(); + auto guild = BabyDI::Get(); + guild->saveGuilds(); + }; + m_timedNWTime.callbackIterations = [this](int) + { + calculateNWTime(); + sendPacketToAll(CString() >> (char)PLO_NEWWORLDTIME << CString().writeGInt4(getNWTime())); + }; + m_timedMaintenance.callbackIterations = [this](int) + { + // Reload some server settings. + loadAllowedVersions(); + loadServerMessage(); + loadIPBans(); + + // Check if we need to unload any levels. + for (auto& [levelName, level] : m_levelList) + { + // TODO: Gmap sub-level (and maybe static level) unloading. Needs to follow Map::keepAllLevelsLoaded and levelsToKeepInMemory settings. + + // Skip if the level is currently active with players in it. + // We always do this so we can abort early. + if (!level->timeSinceLastPlayerLeft.has_value()) + continue; + + // Register that we will skip if we have the unload time set to 0 (which means never unload). + bool skip = (m_unloadInactiveLevelTime.getValue() == 0); + + // Give a 10 minute grace period after the last player leaves. + auto inactiveDuration = timeDifference(m_frameStartTime, level->timeSinceLastPlayerLeft.value()); + skip = skip || inactiveDuration < std::chrono::seconds(m_unloadInactiveLevelTime.getValue()); + + // Group maps will always unload after 10 minutes if the inactive time is not set. + if (level->isGroupMap && inactiveDuration > std::chrono::seconds(m_unloadInactiveLevelTime.get().value_or(600))) + skip = false; + + // Do the skip now. + if (skip) + continue; + + DEBUGPRINT("Unloading level '{}' due to inactivity.", levelName); + + // If we have an NPC-server, unload (or delete) our serverside NPCs. + if (hasNPCServer()) + { + if (level->isGroupMap) + { + for (const auto& id : level->getNPCs()) + m_npcServer->deleteNPC(id); + } + else + { + for (const auto& id : level->getNPCs()) + m_npcServer->unloadNPC(id); + } + } + + level = nullptr; + } + + std::erase_if(m_levelList, [](const auto& entry) + { + return entry.second == nullptr; + }); + }; + + m_fsServer.categoryEventCallback[ENUM(fs::FileCategory::CONFIG)] = [this](fs::FileEventCollection events, fs::FileData& file) + { + if (events.test(fs::FileEvent::Modified)) + { + auto fileName = fs::getANSIFileName(file.file); + if (fileName == "serveroptions.txt") + { + loadSettings(); + + // TODO: Map loading needs to be improved to deal with maps being added/removed, and to fix a level's link to a map. + // Levels have a shared_ptr to the map. Should it be switched to a weak_ptr? + //loadMaps(); + } + else if (fileName == "adminconfig.txt") + loadAdminSettings(); + else if (fileName == "allowedversions.txt") + loadAllowedVersions(); + else if (fileName == "foldersconfig.txt") + loadWorldFileSystem(); + else if (fileName == "serverflags.txt") + loadServerFlags(); + else if (fileName == "servermessage.html") + loadServerMessage(); + else if (fileName == "ipbans.txt") + loadIPBans(); + else if (fileName == "rules.txt") + loadWordFilter(); + } + }; + m_fsServer.categoryEventCallback[ENUM(fs::FileCategory::NPC)] = [this](fs::FileEventCollection events, fs::FileData& file) + { + if (!hasNPCServer()) + return; + + if (events.test(fs::FileEvent::Deleted)) + { + auto npcName = fs::getANSIFileName(fs::getHTMLUnescapedFileName(file.file)); + if (npcName.starts_with("npc") && npcName.ends_with(".txt")) + npcName = npcName.substr(3, npcName.size() - 7); // Remove npc and .txt + + if (auto npc = m_npcServer->getNPCByName(npcName).lock(); npc != nullptr) + { + log::printLine(log::server, "NPC deleted from filesystem: [{}] {}", npc->id, npc->name); + m_npcServer->deleteNPC(npc->id); + } + } + if (events.test(fs::FileEvent::Added)) + { + auto profile = log::Profile(log::server, "", " ({1:0.6} ms)"); + if (auto npc = m_npcServer->addNPCFromFile(file.file); npc != nullptr) + { + // TODO: Generic prop sending function NPCs. + CString packet = CString() >> (char)PLO_NPCPROPS >> (int)npc->id << npc->getAllPropsPacket(); + sendPacketToNearby(packet, npc->getGlobalPosition(), npc->getLevel()); + + log::printLine(log::server, "NPC added to filesystem: [{}] {}", npc->id, file.file.stem().generic_string()); + } + } + if (events.test(fs::FileEvent::Modified)) + { + fs::File npcFile{file.file}; + auto id = string::toNumber(npcFile.readConfigLine("ID", " ").value_or("0")); + if (id == 0) + return; + + auto npc = getNPC(id); + if (npc == nullptr || npc->lastSaveTime == file.getModTime()) return; + npc->lastSaveTime = file.getModTime(); + + // TODO: Ability to serialize all the attributes from the file and send changed ones. + + auto script = npcFile.readConfigSection("NPCSCRIPT", "NPCSCRIPTEND"); + if (script.has_value()) + { + npc->setScript(script.value()); + npc->scripting.events.addEvent(ScriptEventType::CREATED, source::FromServer()); + npc->sendScriptUpdatesToLevel(file.getModTime()); + + std::string logMsg = std::format("NPC script updated on filesystem: [{}] {}", npc->id, npc->name); + log::printLine(log::npc, logMsg); + sendToNC(logMsg); + } + } + }; + m_fsServer.categoryEventCallback[ENUM(fs::FileCategory::SCRIPTCLASS)] = [this](fs::FileEventCollection events, fs::FileData& file) + { + if (!hasNPCServer()) + return; + + auto className = file.file.stem().string(); + std::string logMsg; + + if (events.test(fs::FileEvent::Deleted)) + { + auto className = file.file.stem().string(); + if (m_npcServer->deleteClass(className)) + { + sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_NC_CLASSDELETE << className); + logMsg = std::format("Class deleted from filesystem: {}", className); + } + } + if (events.test(fs::FileEvent::Added)) + { + // Class already exists so it was added by NC. + if (auto existingClass = m_npcServer->getClass(className); !existingClass.expired()) + return; + + auto className = file.file.stem().string(); + if (auto scriptClass = m_npcServer->loadClass(file.file); scriptClass != nullptr) + { + sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_NC_CLASSADD << className); + logMsg = std::format("Class added to filesystem: {}", className); + } + } + if (events.test(fs::FileEvent::Modified)) + { + // Class mod time matches the file mod time? Then NC modified it, not the FS. + auto fileModTime = fs::getFileModTime(file.file); + if (auto existingClass = m_npcServer->getClass(className).lock(); existingClass && existingClass->modTime == fileModTime) + return; + + fs::File script{file.file}; + m_npcServer->updateClass(className, script.readAsString()); + logMsg = std::format("Class updated on filesystem: {}", className); + } + + if (!logMsg.empty()) + { + log::printLine(log::npc, logMsg); + sendToNC(logMsg); + } + }; + m_fsServer.categoryEventCallback[ENUM(fs::FileCategory::TRANSLATION)] = [this](fs::FileEventCollection events, fs::FileData& file) + { + if (events.test(fs::FileEvent::Modified)) + { + auto translationManager = BabyDI::Get(); + translationManager->reloadTranslation(file.file); + } + }; + m_fsServer.categoryEventCallback[ENUM(fs::FileCategory::WEAPON)] = [this](fs::FileEventCollection events, fs::FileData& file) + { + if (events.test(fs::FileEvent::Deleted)) + { + auto weaponName = fs::getANSIFileName(fs::getHTMLEscapedFileName(file.file.stem())).substr(6); + if (NC_DelWeapon(weaponName)) + { + auto logMsg = std::format("Weapon deleted from filesystem: {}", weaponName); + log::printLine(log::npc, logMsg); + sendToNC(logMsg); + } + } + if (events.test(fs::FileEvent::Modified)) + { + auto fileName = fs::getANSIFileName(file.file); + auto newWeapon = Weapon::loadWeapon(fileName); + if (auto weapon = getWeapon(newWeapon->name); weapon) + { + if (weapon->name != newWeapon->name) + { + log::printLine(log::server, "Weapon name mismatch ('{}' became '{}'), old weapon will be deleted.", weapon->name, newWeapon->name); + m_weaponList.erase(weapon->name); + sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_NPCWEAPONDEL << weapon->name); + } + else + { + updateWeaponForPlayers(newWeapon); + } + } + m_weaponList[newWeapon->name] = newWeapon; + } + }; + + m_fsWorld.categoryEventCallback[ENUM(fs::FileCategory::LEVEL)] = [this](fs::FileEventCollection events, fs::FileData& file) + { + if (events.test(fs::FileEvent::Deleted)) + { + // When the level gets deleted, players will be warped out. + m_levelList.erase(fs::getANSIFileName(file.file)); + } + if (events.test(fs::FileEvent::Modified)) + { + auto fileName = fs::getANSIFileName(file.file); + if (auto l = getCachedLevelData(fileName); l) + StaticLevelData::reload(l); + } + }; + m_fsWorld.categoryEventCallback[ENUM(fs::FileCategory::FILE)] = [this](fs::FileEventCollection events, fs::FileData& file) + { + auto fileName = fs::getANSIFileName(file.file); + auto ext = file.file.extension(); + if (events.test(fs::FileEvent::Deleted)) + { + if (ext == ".gupd") + m_packageManager.deleteResource(fileName); + } + if (events.test(fs::FileEvent::Modified)) + { + if (ext == ".gupd") + m_packageManager.findOrAddResource(fileName)->reload(this); + else if (Generation == ServerGeneration::NEWMAIN || Generation == ServerGeneration::MODERN) + { + // Ganis need to be recompiled on update + CString bytecodePacket; + if (ext == ".gani") + { + // delete the resource + m_animationManager.deleteResource(fileName); + + // reload the resource to compile the bytecode again + if (auto findAni = m_animationManager.findOrAddResource(fileName); findAni) + bytecodePacket << findAni->getBytecodePacket(); + } + + // Send the update packet to any v4+ clients that have seen this file + CString updatePacket = CString() >> (char)PLO_UPDATEPACKAGEISUPDATED << fileName; + for (const auto& [pid, pl] : players_of_type(m_playerList)) + { + if (pl->hasSeenFile(fileName)) + pl->sendPacket(updatePacket); + + // Send GS2 gani scripts + if (!bytecodePacket.isEmpty()) + pl->sendPacket(bytecodePacket); + } + } + } + }; } Server::~Server() { cleanup(); + + // Close our UPNP port forward. + // First, make sure the thread has completed already. + // This can cause an issue if the server is about to be deleted. +#ifdef ENABLE_UPNP + if (m_upnpThread.joinable()) + m_upnpThread.join(); + if (m_upnp) + m_upnp->removeAllForwardedPorts(); +#endif } -int Server::init(const CString& serverip, const CString& serverport, const CString& localip, const CString& serverinterface) +int Server::init(std::string_view serverip, std::string_view serverport, std::string_view localip, std::string_view serverinterface) { -#ifdef V8NPCSERVER - // Initialize the Script Engine - if (!m_scriptEngine.initialize()) - { - m_serverLog.out("** [Error] Could not initialize script engine.\n"); - // TODO(joey): new error number? log is probably enough - return ERR_SETTINGS; - } -#endif + // Register the server start time. + m_serverStartTime = std::chrono::system_clock::now(); // Load the config files. int ret = loadConfigFiles(); if (ret) return ret; + // Load the NPC-Server. + loadNPCServer(); + + // Load the server objects. + ret = loadServerObjects(); + if (ret) return ret; + // If an override serverip and serverport were specified, fix the options now. - if (!serverip.isEmpty()) - m_settings.addKey("serverip", serverip); - if (!serverport.isEmpty()) - m_settings.addKey("serverport", serverport); - if (!localip.isEmpty()) - m_settings.addKey("localip", localip); - if (!serverinterface.isEmpty()) - m_settings.addKey("serverinterface", serverinterface); + if (!serverip.empty()) + m_settings.set("serverip", serverip); + if (!serverport.empty()) + m_settings.set("serverport", serverport); + if (!localip.empty()) + m_settings.set("localip", localip); + if (!serverinterface.empty()) + m_settings.set("serverinterface", serverinterface); m_overrideIp = serverip; m_overridePort = serverport; @@ -118,11 +482,12 @@ int Server::init(const CString& serverip, const CString& serverport, const CStri m_overrideInterface = serverinterface; // Fix up the interface to work properly with CSocket. - CString oInter = m_overrideInterface; - if (m_overrideInterface.isEmpty()) - oInter = m_settings.getStr("serverinterface"); + std::string_view oInter = m_overrideInterface; + auto sinter = m_settings.get("serverinterface"); + if (m_overrideInterface.empty() && sinter.has_value()) + oInter = sinter.value(); if (oInter == "AUTO") - oInter.clear(); + oInter = std::string_view{}; // Initialize the player socket. m_playerSock.setType(SOCKET_TYPE_SERVER); @@ -130,52 +495,47 @@ int Server::init(const CString& serverip, const CString& serverport, const CStri m_playerSock.setDescription("playerSock"); // Start listening on the player socket. - m_serverLog.out(":: Initializing player listen socket.\n"); - if (m_playerSock.init((oInter.isEmpty() ? 0 : oInter.text()), m_settings.getStr("serverport").text())) + log::printLine(log::server, "Initializing player listen socket."); + if (m_playerSock.init((oInter.empty() ? 0 : oInter.data()), m_settings.get("serverport").value_or(""s).c_str())) { - m_serverLog.out("** [Error] Could not initialize listening socket...\n"); + log::printLine(log::server, "** [Error] Could not initialize listening socket."); return ERR_LISTEN; } if (m_playerSock.connect()) { - m_serverLog.out("** [Error] Could not connect listening socket...\n"); + log::printLine(log::server, "** [Error] Could not connect listening socket."); return ERR_LISTEN; } -#ifdef UPNP - // Start a UPNP thread. It will try to set a UPNP port forward in the background. - serverlog.out(":: Starting UPnP discovery thread.\n"); - m_upnp.initialize((oInter.isEmpty() ? m_playerSock.getLocalIp() : oInter.text()), m_settings.getStr("serverport").text()); - m_upnpThread = std::thread(std::ref(m_upnp)); -#endif + // Announce the ports. + { + auto indent = log::server.indent(); + log::printLine(log::server, "Listening on: {}:{}.", m_playerSock.getRemoteIp(), m_playerSock.getRemotePort()); + } -#ifdef V8NPCSERVER - // Setup NPC Control port - m_ncPort = strtoint(m_settings.getStr("serverport")); - - m_npcServer = std::make_shared(nullptr, 0); - m_npcServer->setType(PLTYPE_NPCSERVER); - m_npcServer->loadAccount("(npcserver)"); - m_npcServer->setHeadImage(m_settings.getStr("staffhead", "head25.png")); - m_npcServer->setLoaded(true); // can't guarantee this, so forcing it - - // TODO(joey): Update this when server options is changed? - // Set nickname, and append (Server) - required! - CString nickName = m_settings.getStr("nickname"); - if (nickName.isEmpty()) - nickName = "NPC-Server"; - nickName << " (Server)"; - m_npcServer->setNick(nickName, true); - - // Add npc-server to playerlist - addPlayer(m_npcServer); + // Start a UPNP thread. It will try to set a UPNP port forward in the background. +#ifdef ENABLE_UPNP + if (m_settings.get("upnp").value_or(true) && m_upnp == nullptr) + { + log::printLine(log::server, "Starting UPnP discovery thread."); + m_upnp = std::make_unique(); + m_upnp->initialize((oInter.empty() ? m_playerSock.getLocalIp() : oInter.data()), m_settings.get("serverport").value_or(""s).c_str()); + m_upnpThread = std::thread(std::ref(*m_upnp.get())); + } #endif // Register ourself with the socket manager. m_sockManager.registerSocket((CSocketStub*)this); - // Register the server start time. - m_serverStartTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + // Start the timers. + m_timedEvents.start(); + m_timedNWTime.start(); + m_timedSave.start(); + m_timedMaintenance.start(); + +#ifdef PACKETLOGGING + log::printLine(log::networkdump, "------------------------------ START ------------------------------"); +#endif return 0; } @@ -189,9 +549,6 @@ void Server::operator()() // Do a server loop. doMain(); - // Clean up deleted players here. - cleanupDeletedPlayers(); - // Check if we should do a restart. if (m_doRestart) { @@ -205,116 +562,49 @@ void Server::operator()() if (shutdownProgram) running = false; } - cleanup(); -} - -void Server::cleanupDeletedPlayers() -{ - if (m_deletedPlayers.empty()) return; - for (auto i = std::begin(m_deletedPlayers); i != std::end(m_deletedPlayers);) - { - // Value copy so the shared_ptr exists until the end. - PlayerPtr player = *i; - -#ifdef V8NPCSERVER - IScriptObject* playerObject = player->getScriptObject(); - if (playerObject != nullptr) - { - // Process last script events for this player - if (!player->isProcessed()) - { - // Leave the level now while the player object is still alive - if (player->getLevel() != nullptr) - player->leaveLevel(); - - // Send event to server that player is logging out - if (player->isLoaded() && (player->getType() & PLTYPE_ANYPLAYER)) - { - for (const auto& [npcName, npcPtr]: m_npcNameList) - { - if (auto npcObject = npcPtr.lock(); npcObject) - npcObject->queueNpcAction("npc.playerlogout", player.get()); - } - } - - // Set processed - player->setProcessed(); - } - - // If we just added events to the player, we will have to wait for them to run before removing player. - if (playerObject->isReferenced()) - { - SCRIPTENV_D("Reference count: %d\n", playerObject->getReferenceCount()); - ++i; - continue; - } - } -#endif - - // Get rid of the player now. - m_playerIdGenerator.freeId(player->getId()); - m_sockManager.unregisterSocket(player.get()); - m_playerList.erase(player->getId()); - player->cleanup(); - - i = m_deletedPlayers.erase(i); - } - //m_deletedPlayers.clear(); } void Server::cleanup() { - // Close our UPNP port forward. - // First, make sure the thread has completed already. - // This can cause an issue if the server is about to be deleted. -#ifdef UPNP - if (m_upnpThread.joinable()) - m_upnpThread.join(); - m_upnremoveAllForwardedPortsts(); -#endif - // Save translations. - this->TS_Save(); + auto translationManager = BabyDI::Get(); + translationManager->saveTranslations(); // Save server flags. saveServerFlags(); -#ifdef V8NPCSERVER - // Save npcs - saveNpcs(); + // Save NPC-Server NPCs. + if (hasNPCServer()) + m_npcServer->saveNPCs(); - // npc-server will be cleared from playerlist, so lets invalidate the pointer here - m_npcServer = nullptr; -#endif + m_npcList.clear(); + m_shootParams.clear(); - for (auto& [id, player]: m_playerList) + auto players = m_playerList | std::views::transform([](const auto& pair) { return pair.second; }); + std::vector deletePlayers{std::ranges::begin(players), std::ranges::end(players)}; + for (auto& player : deletePlayers) player->cleanup(); + m_npcServer.reset(); m_playerList.clear(); - m_deletedPlayers.clear(); - m_playerIdGenerator.resetAndSetNext(PLAYERID_INIT); m_levelList.clear(); m_mapList.clear(); - m_groupLevels.clear(); - - m_npcList.clear(); - m_npcNameList.clear(); - m_npcIdGenerator.resetAndSetNext(NPCID_INIT); saveWeapons(); m_weaponList.clear(); -#ifdef V8NPCSERVER - // Clean up the script engine - m_scriptEngine.cleanup(); -#endif + m_npcIdGenerator.reset(); + m_playerIdGenerator.reset(); m_playerSock.disconnect(); m_serverlist.getSocket().disconnect(); // Clean up the socket manager. Pass false so we don't cause a crash. m_sockManager.cleanup(false); + m_adminSettings.clear(); + + log::printLine(log::server, "-------------------------------------"); } void Server::restart() @@ -328,137 +618,94 @@ bool Server::doMain() m_sockManager.update(0, 5000); // 5ms // Current time - auto currentTimer = std::chrono::high_resolution_clock::now(); + auto oldTime = m_frameStartTimeHighPrecision; + m_frameStartTimeHighPrecision = precise_clock::now(); + m_frameStartTime = currentTime(); -#ifdef V8NPCSERVER - m_scriptEngine.runScripts(currentTimer); + // Update the NPC server. + if (hasNPCServer()) + m_npcServer->update(m_frameStartTimeHighPrecision); - // enable when we switch to async compiling - //m_gs2ScriptManager.runQueue(); -#endif + // Update our events. + m_timedEvents.update(m_frameStartTimeHighPrecision); + m_timedSave.update(m_frameStartTimeHighPrecision); + m_timedNWTime.update(m_frameStartTimeHighPrecision); + m_timedMaintenance.update(m_frameStartTimeHighPrecision); + + // Do level frame events. + for (auto& [name, level] : m_levelList) + { + if (level != nullptr) + level->doFrameEvents(m_frameStartTimeHighPrecision); + } - // Every second, do some events. - auto time_diff = std::chrono::duration_cast(currentTimer - m_lastTimer); - if (time_diff.count() >= 1000) + // Execute our scheduled tasks. + auto startingTasks = m_scheduledTasks.size(); + auto tasks = startingTasks; + auto diff = m_frameStartTimeHighPrecision - oldTime; + for (size_t i = 0; i < tasks;) { - m_lastTimer = currentTimer; - doTimedEvents(); + auto& task = m_scheduledTasks.at(i); + if (task.first < diff) + { + // Execute the task. + task.second(); + + // Swap the finished task with end, then decrement the number of tasks. + // We just want to erase the finished tasks at the end for efficiency. + if (i < tasks - 1) + std::swap(m_scheduledTasks.at(i), m_scheduledTasks.at(tasks - 1)); + + --tasks; + } + else + { + task.first -= diff; + ++i; + } } + m_scheduledTasks.erase(m_scheduledTasks.begin() + tasks, m_scheduledTasks.begin() + startingTasks); return true; } -bool Server::doTimedEvents() +bool Server::doTimedEvents(int) { + // File system events. + m_fsServer.update(); + m_fsWorld.update(); + if (auto guildManager = BabyDI::Get(); guildManager != nullptr) + guildManager->update(); + // Do serverlist events. m_serverlist.doTimedEvents(); // Do player events. { - for (auto& [id, player]: m_playerList) + std::vector deletePlayers; + for (auto& [id, player] : m_playerList) { assert(player); if (!player->isNPCServer()) { if (!player->doTimedEvents()) - this->deletePlayer(player); + deletePlayers.push_back(player); } } + std::ranges::for_each(deletePlayers, [this](PlayerPtr& player) + { + deletePlayer(player); + }); + deletePlayers.clear(); } // Do level events. { - for (auto& level: m_levelList) + for (auto& [name, level] : m_levelList) { assert(level); level->doTimedEvents(); } - - // Group levels. - for (auto& [group, levelPtr]: m_groupLevels) - { - if (auto level = levelPtr.lock(); level) - level->doTimedEvents(); - } - } - - // Send NW time. - auto time_diff = std::chrono::duration_cast(m_lastTimer - m_lastNewWorldTimer); - if (time_diff.count() >= 5) - { - calculateServerTime(); - - m_lastNewWorldTimer = m_lastTimer; - sendPacketToAll(CString() >> (char)PLO_NEWWORLDTIME << CString().writeGInt4(getNWTime())); - } - - // Stuff that happens every minute. - time_diff = std::chrono::duration_cast(m_lastTimer - m_last1mTimer); - if (time_diff.count() >= 60) - { - m_last1mTimer = m_lastTimer; - - // Save server flags. - this->saveServerFlags(); - } - - // Stuff that happens every 3 minutes. - time_diff = std::chrono::duration_cast(m_lastTimer - m_last3mTimer); - if (time_diff.count() >= 180) - { - m_last3mTimer = m_lastTimer; - - // TODO(joey): probably a better way to do this.. - - // Resynchronize the file systems. - m_filesystemAccounts.resync(); - for (auto& i: m_filesystem) - i.resync(); - } - - // Save stuff every 5 minutes. - time_diff = std::chrono::duration_cast(m_lastTimer - m_last5mTimer); - if (time_diff.count() >= 300) - { - m_last5mTimer = m_lastTimer; - - // Reload some server settings. - loadAllowedVersions(); - loadServerMessage(); - loadIPBans(); - - // Save some stuff. - // TODO(joey): Is this really needed? We save weapons to disk when it is updated or created anyway.. - saveWeapons(); -#ifdef V8NPCSERVER - saveNpcs(); -#endif - - // Check all of the instanced maps to see if the players have left. - if (!m_groupLevels.empty()) - { - std::unordered_set groupKeys; - std::for_each(std::begin(m_groupLevels), std::end(m_groupLevels), [&groupKeys](auto& pair) - { - groupKeys.insert(pair.first); - }); - - for (auto& groupName: groupKeys) - { - bool playersFound = false; - auto range = m_groupLevels.equal_range(groupName); - std::for_each(range.first, range.second, [&playersFound](decltype(m_groupLevels)::value_type& pair) - { - if (auto level = pair.second.lock(); level && !level->getPlayers().empty()) - playersFound = true; - }); - - if (!playersFound) - { - m_groupLevels.erase(groupName); - } - } - } } return true; @@ -472,7 +719,7 @@ bool Server::onRecv() return true; // Create the new player. - auto newPlayer = std::make_shared(newSock, 0); + auto newPlayer = std::make_shared(newSock, USHRT_MAX); // Add the player to the server if (!addPlayer(newPlayer)) @@ -488,25 +735,22 @@ bool Server::onRecv() void Server::loadAllFolders() { - for (auto& fs: m_filesystem) - fs.clear(); - - m_filesystem[0].addDir("world"); - if (m_settings.getStr("sharefolder").length() > 0) + m_fsWorld.reset(); + m_fsWorld.bind("world"); + if (auto sharefolder = m_settings.get("sharefolder"); sharefolder.has_value() && !sharefolder->empty()) { - std::vector folders = m_settings.getStr("sharefolder").tokenize(","); - for (auto& folder: folders) - m_filesystem[0].addDir(folder.trim()); + auto folders = string::split(sharefolder.value(), ","sv); + m_fsWorld.bind(folders); } } void Server::loadFolderConfig() { - for (auto& i: m_filesystem) - i.clear(); + auto indent = log::server.indent(); + m_fsWorld.reset(); - m_foldersConfig = CString::loadToken(CString() << m_serverPath << "config/foldersconfig.txt", "\n", true); - for (auto& configLine: m_foldersConfig) + m_foldersConfig = CString::loadToken(CString() << "config/foldersconfig.txt", "\n", true); + for (auto& configLine : m_foldersConfig) { // No comments. int cLoc = -1; @@ -516,123 +760,174 @@ void Server::loadFolderConfig() if (configLine.length() == 0) continue; // Parse the line. - CString type = configLine.readString(" "); - CString config = configLine.readString(""); - type.trimI(); - config.trimI(); - FileSystem::fixPathSeparators(config); - - // Get the directory. - CString dirNoWild; - int pos = -1; - if ((pos = config.findl(FileSystem::getPathSeparator())) != -1) - dirNoWild = config.remove(pos + 1); - CString dir = CString("world/") << dirNoWild; - CString wildcard = config.remove(0, dirNoWild.length()); - - // Find out which file system to add it to. - FileSystem* fs = getFileSystemByType(type); - - // Add it to the appropriate file system. - if (fs != nullptr) - { - fs->addDir(dir, wildcard); - m_serverLog.out(" adding %s [%s] to %s\n", dir.text(), wildcard.text(), type.text()); - } - m_filesystem[0].addDir(dir, wildcard); + std::string type = configLine.readString(" ").trimI().toString(); + auto world = std::filesystem::path{"world"}; + auto config = std::filesystem::path{configLine.readString("").trimI().toStringView()}; + + fs::FileCategory typeEnum = fs::FileCategory::ALL; + if (string::equalsi(type, "file"sv)) + typeEnum = fs::FileCategory::FILE; + else if (string::equalsi(type, "level"sv)) + typeEnum = fs::FileCategory::LEVEL; + else if (string::equalsi(type, "head"sv)) + typeEnum = fs::FileCategory::HEAD; + else if (string::equalsi(type, "body"sv)) + typeEnum = fs::FileCategory::BODY; + else if (string::equalsi(type, "sword"sv)) + typeEnum = fs::FileCategory::SWORD; + else if (string::equalsi(type, "shield"sv)) + typeEnum = fs::FileCategory::SHIELD; + else if (string::equalsi(type, "sound"sv)) + typeEnum = fs::FileCategory::SOUND; + + m_fsWorld.addFoldersConfigEntry(typeEnum, world / config); + log::printLine(log::server, "adding {}/ [{}] to {}", config.parent_path().generic_string(), fs::getANSIFileName(config), type); } + + m_fsWorld.bind("world"); } int Server::loadConfigFiles() { - // TODO(joey): /reloadconfig reloads this, but things like server flags, weapons and npcs probably shouldn't be reloaded. - // Move them out of here? - m_serverLog.out(":: Loading server configuration...\n"); + log::printLine(log::server, "Loading server configuration."); - // Load Settings - m_serverLog.out(" Loading settings...\n"); - loadSettings(); - - // Load Admin Settings - m_serverLog.out(" Loading admin settings...\n"); - loadAdminSettings(); - - // Load allowed versions. - m_serverLog.out(" Loading allowed client versions...\n"); - loadAllowedVersions(); - - // Load folders config and file system. - m_serverLog.out(" Folder config: "); - if (!m_settings.getBool("nofoldersconfig", false)) { - m_serverLog.append("ENABLED\n"); - } - else - m_serverLog.append("disabled\n"); + auto indent = log::server.indent(); + loadServerFileSystem(); + + // Load Settings + log::printLine(log::server, "Loading settings..."); + { + auto indentsettings = log::server.indent(); + prepareSettings(); + loadSettings(); + log::printLine(log::server, "Server generation: {}", ServerGenerationNames[(size_t)Generation]); + } - m_serverLog.out(" Loading file system...\n"); - loadFileSystem(); + // Load Admin Settings + log::printLine(log::server, "Loading admin settings."); + loadAdminSettings(); - // Load server flags. - m_serverLog.out(" Loading serverflags.txt...\n"); - loadServerFlags(); + // Load allowed versions. + log::printLine(log::server, "Loading allowed client versions."); + loadAllowedVersions(); - // Load server message. - m_serverLog.out(" Loading config/servermessage.html...\n"); - loadServerMessage(); + // Load folders config and file system. + log::print(log::server, "Folder config: "); + if (!m_settings.get("nofoldersconfig").value_or(false)) + log::printLine(log::server, "ENABLED"); + else + log::printLine(log::server, "disabled"); - // Load IP bans. - m_serverLog.out(" Loading config/ipbans.txt...\n"); - loadIPBans(); + log::printLine(log::server, "Loading file system..."); + loadWorldFileSystem(); - // Load weapons. - m_serverLog.out(" Loading weapons...\n"); - loadWeapons(true); + // Load server message. + log::printLine(log::server, "Loading config/servermessage.html."); + loadServerMessage(); - // Load classes. - m_serverLog.out(" Loading classes...\n"); - loadClasses(true); + // Load IP bans. + log::printLine(log::server, "Loading config/ipbans.txt."); + loadIPBans(); - // Load maps. - m_serverLog.out(" Loading maps...\n"); - loadMaps(true); + // Load translations. + loadTranslations(); -#ifdef V8NPCSERVER - // Load database npcs. - m_serverLog.out(" Loading npcs...\n"); - loadNpcs(true); -#endif + // Load word filter. + log::printLine(log::server, "Loading word filter."); + loadWordFilter(); - // Load map levels - doing this after db npcs are loaded incase - // some level scripts may require access to the databases. - loadMapLevels(); + // Load server flags. + log::printLine(log::server, "Loading serverflags.txt."); + loadServerFlags(); - // Load translations. - m_serverLog.out(" Loading translations...\n"); - loadTranslations(); + // Load guilds. + log::printLine(log::server, "Loading guilds..."); + loadGuilds(); - // Load word filter. - m_serverLog.out(" Loading word filter...\n"); - loadWordFilter(); + // Load maps. + log::printLine(log::server, "Loading maps..."); + loadMaps(true); + } return 0; } -void Server::loadSettings() +void Server::prepareSettings() { - if (!m_settings.isOpened()) + // Generation. + m_generationString.onUpdate = [this](const std::optional& newValue, const std::optional& oldValue) { - m_settings.setSeparator("="); - m_settings.loadFile(CString() << m_serverPath << "config/serveroptions.txt"); - if (!m_settings.isOpened()) - m_serverLog.out("** [Error] Could not open config/serveroptions.txt. Will use default config.\n"); - } + auto generation = string::toLower(newValue.value()); + if (auto it = std::ranges::find(ServerGenerationNames, generation); it != std::ranges::end(ServerGenerationNames)) + Generation = static_cast(std::distance(ServerGenerationNames.begin(), it)); + else + log::printLine(log::server, "** [Error] Invalid generation specified in settings: '{}'.", newValue.value_or("(blank)")); + }; + + // Bush drops. + m_bushItemTypes.onUpdate = [this](const std::optional>& newValue, const std::optional>& oldValue) + { + // greenrupee 10, bluerupee 5, bombs 5, heart 5 + static const std::array defaults = {10, 5, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + m_bushDrops.clear(); + for (const auto& curItem : newValue.value()) + { + std::string itemType = string::toLower(curItem); + string::trimMutate(itemType); + + LevelItemType item = LevelItem::getItemId(itemType); + if (item == LevelItemType::INVALID) + continue; + + auto spawnRate = m_settings.get(std::format("spawnrate{}", itemType)).value_or(defaults[static_cast(item)]); + m_bushDrops.emplace_back(item, spawnRate); + } + }; + + // Death drops. + m_deathItemTypes.onUpdate = [this](const std::optional>& newValue, const std::optional>& oldValue) + { + m_deathDrops.clear(); + for (const auto& curItem : newValue.value()) + { + std::string itemType = string::toLower(curItem); + string::trimMutate(itemType); + + LevelItemType item = LevelItem::getItemId(itemType); + if (item != LevelItemType::INVALID) + m_deathDrops.push_back(item); + } + }; + + // Gmaps. + m_gmaps.onUpdate = [this](const std::optional>& newValue, const std::optional>& oldValue) + { + if (running) + loadMaps(); + }; - // Load status list. - m_statusList = m_settings.getStr("playerlisticons", "Online,Away,DND,Eating,Hiding,No PMs,RPing,Sparring,PKing").tokenize(","); + // Bigmaps. + m_bigmaps.onUpdate = [this](const std::optional>& newValue, const std::optional>& oldValue) + { + if (running) + loadMaps(); + }; + + // Set the cache bindings before we load so our settings will get cached. + cached.bind(this); + m_settings.track(m_generationString, m_classicStyleLogs); + m_settings.track(m_dontAddServerFlags); + m_settings.track(m_newTilesets, m_newTilesetLevels); + m_settings.track(m_unloadInactiveLevelTime); + m_settings.track(m_staffList, m_bushItemTypes, m_deathItemTypes); + m_settings.track(m_gmaps, m_bigmaps, m_groupmaps); +} - // Load staff list - m_staffList = m_settings.getStr("staff").tokenize(","); +void Server::loadSettings() +{ + m_settings.load("serveroptions.txt"); // Send our ServerHQ info in case we got changed the staffonly setting. getServerList().sendServerHQ(); @@ -640,722 +935,825 @@ void Server::loadSettings() void Server::loadAdminSettings() { - m_adminSettings.setSeparator("="); - m_adminSettings.loadFile(CString() << m_serverPath << "config/adminconfig.txt"); - if (!m_adminSettings.isOpened()) - m_serverLog.out("** [Error] Could not open config/adminconfig.txt. Will use default config.\n"); - else - getServerList().sendServerHQ(); + m_adminSettings.load("adminconfig.txt"); + getServerList().sendServerHQ(); } void Server::loadAllowedVersions() { CString versions; - versions.load(CString() << m_serverPath << "config/allowedversions.txt"); + versions.load(CString() << "config/allowedversions.txt"); versions = removeComments(versions); versions.removeAllI("\r"); versions.removeAllI("\t"); versions.removeAllI(" "); - m_allowedVersions = versions.tokenize("\n"); + m_allowedVersions.clear(); m_allowedVersionString.clear(); - for (auto& allowedVersion: m_allowedVersions) + + // New version. + if (versions.contains("[generation-range]")) { - if (!m_allowedVersionString.isEmpty()) - m_allowedVersionString << ", "; + const auto& generation = ServerGenerationNames.at(static_cast(Generation)); + if (auto line = versions.find(generation); line != -1) + { + if (auto sep = versions.find("=", line); sep != -1) + { + versions.setRead(sep + 1); + std::string versionRange = string::trimMutate(versions.readString("\n").toString()); - int loc = allowedVersion.find(":"); - if (loc == -1) - m_allowedVersionString << getVersionString(allowedVersion, PLTYPE_ANYCLIENT); - else + std::vector formattedVersions; + for (const auto& version : string::split(versionRange, ","sv)) + { + m_allowedVersions.push_back(version); + auto rangeParts = string::splitToVector(version, ":"sv); + if (rangeParts.size() == 1) + { + formattedVersions.push_back(getVersionString(rangeParts[0], PLTYPE_ANYCLIENT)); + } + else if (rangeParts.size() == 2) + { + int startId = getVersionID(rangeParts[0]); + int endId = getVersionID(rangeParts[1]); + if (startId != 0 && endId != 0) + formattedVersions.emplace_back(std::format("{} - {}", getVersionString(rangeParts[0], PLTYPE_ANYCLIENT), getVersionString(rangeParts[1], PLTYPE_ANYCLIENT))); + } + } + m_allowedVersionString = string::join(formattedVersions, ", "sv); + } + } + + if (m_allowedVersions.empty()) + log::printLine(log::server, "** [Error] Could not find generation range for '{}' in allowedversions.txt.", generation); + } + // Old version. + else + { + m_allowedVersions = versions.tokenize("\n"); + for (auto& allowedVersion : m_allowedVersions) { - CString s = allowedVersion.subString(0, loc); - CString f = allowedVersion.subString(loc + 1); - int vid = getVersionID(s); - int vid2 = getVersionID(f); - if (vid != -1 && vid2 != -1) - m_allowedVersionString << getVersionString(s, PLTYPE_ANYCLIENT) << " - " << getVersionString(f, PLTYPE_ANYCLIENT); + if (!m_allowedVersionString.isEmpty()) + m_allowedVersionString << ", "; + + int loc = allowedVersion.find(":"); + if (loc == -1) + m_allowedVersionString << getVersionString(allowedVersion, PLTYPE_ANYCLIENT); + else + { + CString s = allowedVersion.subString(0, loc); + CString f = allowedVersion.subString(loc + 1); + int vid = getVersionID(s); + int vid2 = getVersionID(f); + if (vid != -1 && vid2 != -1) + m_allowedVersionString << getVersionString(s, PLTYPE_ANYCLIENT) << " - " << getVersionString(f, PLTYPE_ANYCLIENT); + } } } } -void Server::loadFileSystem() +void Server::loadServerFileSystem() { - for (auto& i: m_filesystem) - i.clear(); - m_filesystemAccounts.clear(); - m_filesystemAccounts.addDir("accounts", "*.txt"); - if (m_settings.getBool("nofoldersconfig", false)) + if (m_fsServer.empty()) + { + m_fsServer.addFoldersConfigEntry(fs::FileCategory::ACCOUNT, "accounts/*.txt"); + m_fsServer.addFoldersConfigEntry(fs::FileCategory::CONFIG, "config/*"); + m_fsServer.addFoldersConfigEntry(fs::FileCategory::NPC, "npcs/npc*.txt"); + m_fsServer.addFoldersConfigEntry(fs::FileCategory::SCRIPTCLASS, "scripts/*.txt"); + m_fsServer.addFoldersConfigEntry(fs::FileCategory::TRANSLATION, "translations/*"); + m_fsServer.addFoldersConfigEntry(fs::FileCategory::WEAPON, "weapons/weapon*.txt"); + m_fsServer.bind("accounts"s, "config"s, "npcs"s, "scripts"s, "translations"s, "weapons"s); + } +} + +void Server::loadWorldFileSystem() +{ + if (m_settings.get("nofoldersconfig").value_or(false)) loadAllFolders(); else loadFolderConfig(); - for (auto &[file, path] : m_filesystem[0].getFileList()) { - if (getExtension(file) == ".gupd") { - getPackageManager().findOrAddResource(file.toString())->reload(this); - } + for (auto fileData : m_fsWorld.info(fs::FileCategory::ALL) | toSharedPtr) + { + if (fileData == nullptr) continue; + if (fileData->file.extension() == ".gupd") + getPackageManager().findOrAddResource(fileData->file.string())->reload(this); } } void Server::loadServerFlags() { - std::vector lines = CString::loadToken(CString() << m_serverPath << "serverflags.txt", "\n", true); - for (auto& line: lines) + std::vector lines = CString::loadToken(CString() << "serverflags.txt", "\n", true); + for (auto& line : lines) this->setFlag(line, false); } +void Server::loadGuilds() +{ + auto guild = BabyDI::Get(); + guild->loadGuilds("guilds"); +} + void Server::loadServerMessage() { - m_serverMessage.load(CString() << m_serverPath << "config/servermessage.html"); + m_serverMessage.load(CString() << "config/servermessage.html"); m_serverMessage.removeAllI("\r"); m_serverMessage.replaceAllI("\n", " "); } void Server::loadIPBans() { - m_ipBans = CString::loadToken(CString() << m_serverPath << "config/ipbans.txt", "\n", true); + m_ipBans = CString::loadToken(CString() << "config/ipbans.txt", "\n", true); } -void Server::loadClasses(bool print) +void Server::loadTranslations() const { - FileSystem scriptFS; - scriptFS.addDir("scripts", "*.txt"); - const std::map& scriptFileList = scriptFS.getFileList(); - for (auto& scriptFile: scriptFileList) - { - std::string className = scriptFile.first.subString(0, scriptFile.first.length() - 4).text(); - - CString scriptData; - scriptData.load(scriptFile.second); - m_classList[className] = std::make_unique(className, scriptData.text()); + log::print(log::server, "Loading translations: "); - updateClassForPlayers(getClass(className)); - } -} + // If our translation manager already exists, save the translations first. + if (auto translationManager = BabyDI::Get(); translationManager != nullptr) + translationManager->saveTranslations(); -void Server::loadWeapons(bool print) -{ - FileSystem weaponFS; - weaponFS.addDir("weapons", "weapon*.txt"); - FileSystem bcweaponFS; - bcweaponFS.addDir("weapon_bytecode", "*"); + BabyDI_RELEASE(ITranslationManager); - auto& weaponFileList = weaponFS.getFileList(); - for (auto& weaponFile: weaponFileList) + // Create our translation manager. + ITranslationManager* manager = nullptr; + if (Generation == ServerGeneration::MODERN) { - auto weapon = Weapon::loadWeapon(weaponFile.first); - if (weapon == nullptr) continue; - if (weapon->getByteCodeFile().empty()) - weapon->setModTime(weaponFS.getModTime(weaponFile.first)); - else - weapon->setModTime(bcweaponFS.getModTime(weapon->getByteCodeFile())); - - // Check if the weapon exists. - if (m_weaponList.find(weapon->getName()) == m_weaponList.end()) - { - m_weaponList[weapon->getName()] = weapon; - if (print) m_serverLog.out(" %s\n", weapon->getName().c_str()); - } - else - { - // If the weapon exists, and the version on disk is newer, reload it. - auto& w = m_weaponList[weapon->getName()]; - if (w->getModTime() < weapon->getModTime()) - { - m_weaponList[weapon->getName()] = weapon; - updateWeaponForPlayers(weapon); - if (print) - { - m_serverLog.out(" %s [updated]\n", weapon->getName().c_str()); - Server::sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: Updated weapon " << weapon->getName() << " "); - } - } - else - { - // TODO(joey): even though were deleting the weapon because its skipped, its still queuing its script action - // and attempting to execute it. Technically the code needs to be run again though, will fix soon. - if (print) m_serverLog.out(" %s [skipped]\n", weapon->getName().c_str()); - } - } + log::printLine(log::server, "modern style."); + manager = BabyDI_PROVIDE(ITranslationManager, new TranslationManagerModern()); + } + else + { + log::printLine(log::server, "classic style."); + manager = BabyDI_PROVIDE(ITranslationManager, new TranslationManagerClassic()); } - // Add the default weapons. - if (!m_weaponList.contains("bow")) m_weaponList["bow"] = std::make_shared(LevelItem::getItemId("bow")); - if (!m_weaponList.contains("bomb")) m_weaponList["bomb"] = std::make_shared(LevelItem::getItemId("bomb")); - if (!m_weaponList.contains("superbomb")) m_weaponList["superbomb"] = std::make_shared(LevelItem::getItemId("superbomb")); - if (!m_weaponList.contains("fireball")) m_weaponList["fireball"] = std::make_shared(LevelItem::getItemId("fireball")); - if (!m_weaponList.contains("fireblast")) m_weaponList["fireblast"] = std::make_shared(LevelItem::getItemId("fireblast")); - if (!m_weaponList.contains("nukeshot")) m_weaponList["nukeshot"] = std::make_shared(LevelItem::getItemId("nukeshot")); - if (!m_weaponList.contains("joltbomb")) m_weaponList["joltbomb"] = std::make_shared(LevelItem::getItemId("joltbomb")); + // Load the translation files. + if (manager != nullptr) + manager->loadTranslations(std::filesystem::current_path() / "translations"); } -void Server::loadMapLevels() +void Server::loadWordFilter() { - // Load gmap levels based on options provided by the gmap file - for (const auto& map: m_mapList) - { - if (map->getType() == MapType::GMAP) - map->loadMapLevels(); - } + m_wordFilter.load(CString() << "config/rules.txt"); } void Server::loadMaps(bool print) { - // Remove players off all maps - for (auto& [id, player]: m_playerList) - player->setMap(nullptr); + assert(m_levelList.empty() && "Levels should be loaded after maps."); + + auto indent = log::server.indent(); // Remove existing maps. m_mapList.clear(); // Load gmaps. - std::vector gmaps = m_settings.getStr("gmaps").guntokenize().tokenize("\n"); - for (CString& gmapName: gmaps) + for (const auto& gmapName : m_gmaps.getValue()) { - // Check for blank lines. - if (gmapName == "\r") continue; - - // Gmaps in server options don't need the .gmap suffix, so we will add the suffix - if (gmapName.right(5) != ".gmap") + // Load the gmap. + try { - gmapName << ".gmap"; + auto mapName = string::trim(gmapName); + if (bool hasExtension = mapName.ends_with(".gmap"); hasExtension) + { + auto gmap = std::make_unique(is_gmap, mapName); + m_mapList.push_back(std::move(gmap)); + } + else + { + auto gmap = std::make_unique(is_gmap, std::format("{}.gmap", mapName)); + m_mapList.push_back(std::move(gmap)); + } + if (print) log::printLine(log::server, "[gmap] {}", mapName); } - - // Load the gmap. - auto gmap = std::make_unique(MapType::GMAP); - if (!gmap->load(CString() << gmapName)) + catch (...) { - if (print) m_serverLog.out(CString() << "[" << m_name << "] " - << "** [Error] Could not load " << gmapName - << "\n"); - continue; + auto inerr = log::server.indent_absolute(0); + if (print) log::printLine(log::server, "** [Error] Could not load {} (gmap).", string::trim(gmapName)); } - - if (print) m_serverLog.out(" [gmap] %s\n", gmapName.text()); - m_mapList.push_back(std::move(gmap)); } // Load bigmaps. - std::vector bigmaps = m_settings.getStr("maps").guntokenize().tokenize("\n"); - for (auto& i: bigmaps) + for (const auto& bigmapName : m_bigmaps.getValue()) { - // Check for blank lines. - if (i == "\r") continue; - // Load the bigmap. - auto bigmap = std::make_unique(MapType::BIGMAP); - if (!bigmap->load(i.trim())) + try { - if (print) m_serverLog.out(CString() << "[" << m_name << "] " - << "** [Error] Could not load " << i << "\n"); - continue; + auto mapName = string::trim(bigmapName); + auto bigmap = std::make_unique(is_bigmap, mapName); + if (print) log::printLine(log::server, "[bigmap] {}", mapName); + m_mapList.push_back(std::move(bigmap)); } - - if (print) m_serverLog.out(" [bigmap] %s\n", i.text()); - m_mapList.push_back(std::move(bigmap)); - } - - // Load group maps. - std::vector groupmaps = m_settings.getStr("groupmaps").guntokenize().tokenize("\n"); - for (auto& groupmap: groupmaps) - { - // Check for blank lines. - if (groupmap == "\r") continue; - - // Determine the type of map we are loading. - CString ext(getExtension(groupmap)); - ext.toLowerI(); - - // Create the new map based on the file extension. - std::unique_ptr gmap; - if (ext == ".txt") - gmap = std::make_unique(MapType::BIGMAP, true); - else if (ext == ".gmap") - gmap = std::make_unique(MapType::GMAP, true); - else - continue; - - // Load the map. - if (!gmap->load(CString() << groupmap)) + catch (...) { - if (print) m_serverLog.out(CString() << "[" << m_name << "] " - << "** [Error] Could not load " << groupmap << "\n"); - continue; + auto inerr = log::server.indent_absolute(0); + if (print) log::printLine(log::server, "** [Error] Could not load {} (bigmap).", string::trim(bigmapName)); } - - if (print) m_serverLog.out(" [group map] %s\n", groupmap.text()); - m_mapList.push_back(std::move(gmap)); } +} - // Update all map <--> level relationships - for (const auto& level: m_levelList) +void Server::loadNPCServer() +{ + if (m_settings.get("serverside").value_or(true)) { - bool found = false; - for (const auto& map: m_mapList) + log::printLine(log::server, "Loading NPC server."); { - int mx, my; - if (map->isLevelOnMap(level->getLevelName().toLower().text(), mx, my)) + auto indent = log::server.indent(); { - level->setMap(map, mx, my); - found = true; - break; + auto sectionProfile = log::Profile(log::server, "", "(Completed in {1:0.6} ms)"); + m_npcServer = std::make_shared(); + m_npcServer->initialize(); } } - - if (!found) - { - level->setMap({}); - } } } -#ifdef V8NPCSERVER -void Server::loadNpcs(bool print) +int Server::loadServerObjects() { - FileSystem npcFS; - npcFS.addDir("npcs", "npc*.txt"); + log::printLine(log::server, "Loading server objects."); + + auto indent = log::server.indent(); + { + // Load weapons. + log::printLine(log::server, "Loading weapons..."); + loadWeapons(true); + + // Load map levels - doing this after db npcs are loaded incase + // some level scripts may require access to the databases. + log::printLine(log::server, "Pre-loading map levels."); + loadMapLevels(); + } + + return 0; +} - auto& npcFileList = npcFS.getFileList(); - for (const auto& [npcName, fileName]: npcFileList) +void Server::loadWeapons(bool print) +{ + auto indent = log::server.indent(); { - bool loaded = false; + auto sectionProfile = log::Profile(log::server, "", "(Completed in {1:0.6} ms)"); - // Create the npc - auto newNPC = std::make_shared("", "", 30.f, 30.5f, nullptr, NPCType::DBNPC); - if (newNPC->loadNPC(fileName)) + for (auto weaponFile : m_fsServer.info(fs::FileCategory::WEAPON) | toSharedPtr) { - int npcId = newNPC->getId(); - if (npcId < 1000) - { - printf("Database npcs must be greater than 1000\n"); - } - else if (auto existing = m_npcList.find(npcId); existing != std::end(m_npcList)) + if (weaponFile == nullptr) continue; + + auto profile = log::Profile(log::server, "", " ({1:0.6} ms)"); + + auto fileName = fs::getANSIFileName(weaponFile->file); + auto weapon = Weapon::loadWeapon(fileName); + if (weapon == nullptr) continue; + + // Check if the weapon exists. + if (m_weaponList.find(weapon->name) == m_weaponList.end()) { - printf("Error creating database npc: Id is in use!\n"); + m_weaponList[weapon->name] = weapon; + if (print) log::print(log::server, weapon->name); } else { - m_npcList.insert(std::make_pair(npcId, newNPC)); - assignNPCName(newNPC, newNPC->getName()); - - // Add npc to level - if (auto level = newNPC->getLevel(); level) - level->addNPC(newNPC); - - loaded = true; + // If the weapon exists, and the version on disk is newer, reload it. + auto& w = m_weaponList[weapon->name]; + if (w->modTime < weapon->modTime) + { + m_weaponList[weapon->name] = weapon; + updateWeaponForPlayers(weapon); + if (print) + { + log::print(log::server, "{} [updated]", weapon->name); + Server::sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: Updated weapon " << weapon->name << " "); + } + } + else + { + // TODO(joey): even though were deleting the weapon because its skipped, its still queuing its script action + // and attempting to execute it. Technically the code needs to be run again though, will fix soon. + if (print) log::print(log::server, "{} [skipped]", weapon->name); + } } } - } -} -#endif -void Server::loadTranslations() -{ - this->TS_Reload(); + // Add the default weapons. + if (!m_weaponList.contains("bow")) m_weaponList["bow"] = std::make_shared(LevelItem::getItemId("bow")); + if (!m_weaponList.contains("bomb")) m_weaponList["bomb"] = std::make_shared(LevelItem::getItemId("bomb")); + if (!m_weaponList.contains("superbomb")) m_weaponList["superbomb"] = std::make_shared(LevelItem::getItemId("superbomb")); + if (!m_weaponList.contains("fireball")) m_weaponList["fireball"] = std::make_shared(LevelItem::getItemId("fireball")); + if (!m_weaponList.contains("fireblast")) m_weaponList["fireblast"] = std::make_shared(LevelItem::getItemId("fireblast")); + if (!m_weaponList.contains("nukeshot")) m_weaponList["nukeshot"] = std::make_shared(LevelItem::getItemId("nukeshot")); + if (!m_weaponList.contains("joltbomb")) m_weaponList["joltbomb"] = std::make_shared(LevelItem::getItemId("joltbomb")); + } } -void Server::loadWordFilter() +void Server::loadMapLevels() { - m_wordFilter.load(CString() << m_serverPath << "config/rules.txt"); + auto indent = log::server.indent(); + { + auto sectionProfile = log::Profile(log::server, "", "(Completed in {1:0.6} ms)"); + for (const auto& map : m_mapList) + map->loadMapLevels(); + } } void Server::saveServerFlags() { CString out; - for (auto& mServerFlag: m_serverFlags) - out << mServerFlag.first << "=" << mServerFlag.second << "\r\n"; - out.save(CString() << m_serverPath << "serverflags.txt"); + for (auto& [flag, value] : Scripting.variables.store) + { + if (auto serialized = value->serializeModern(flag); serialized.has_value()) + out << serialized.value() << "\r\n"; + } + out.save(CString() << "serverflags.txt"); } void Server::saveWeapons() { - FileSystem weaponFS; - weaponFS.addDir("weapons", "weapon*.txt"); - const std::map& weaponFileList = weaponFS.getFileList(); - - for (auto& [weaponName, weapon]: m_weaponList) + for (auto& [weaponName, weapon] : m_weaponList) { if (weapon->isDefault()) continue; - // TODO(joey): add a function to weapon to get the filename? - CString weaponFile = CString("weapon") << weaponName << ".txt"; - time_t mod = weaponFS.getModTime(weaponFile); - if (weapon->getModTime() > mod) + std::filesystem::path weaponFile{std::format("weapon{}.txt", weaponName)}; + clock::time_point mod{clock::time_point::min()}; + + auto fileData = m_fsServer.info(fs::FileCategory::WEAPON, weaponFile); + if (fileData != nullptr) + mod = fileData->getModTime(); + + if (weapon->modTime > mod) { // The weapon in memory is newer than the weapon on disk. Save it. weapon->saveWeapon(); - weaponFS.setModTime(weaponFS.find(weaponFile), weapon->getModTime()); + if (fileData != nullptr) + fileData->setModTime(weapon->modTime); } } } -#ifdef V8NPCSERVER -void Server::saveNpcs() -{ - for (const auto& [npcId, npc]: m_npcList) - { - if (npc->getType() != NPCType::LEVELNPC) - npc->saveNPC(); - } -} +///////////////////////////////////////////////////// -std::vector> Server::calculateNpcStats() +std::shared_ptr Server::getStubbedLevel(std::string_view levelName, std::string_view groupName) { - std::vector> script_profiles; + if (levelName.empty()) + return nullptr; - // Iterate npcs - for (const auto& [npcId, npc]: m_npcList) + // Get the computed level name, which includes the group name (if applicable). + std::string lowerCaseLevel; + if (!groupName.empty()) { - ScriptExecutionContext& context = npc->getExecutionContext(); - std::pair executionData = context.getExecutionData(); - if (executionData.second > 0.0) - { - std::string npcName = npc->getName(); - if (npcName.empty()) - npcName = "Level npc " + std::to_string(npc->getId()); - - auto npcLevel = npc->getLevel(); - if (npcLevel != nullptr) - { - npcName.append(" (in level ").append(npcLevel->getLevelName().text()).append(" at pos (").append(CString(npc->getY() / 16.0).text()).append(", ").append(CString(npc->getX() / 16.0).text()).append(")"); - } - - script_profiles.push_back(std::make_pair(executionData.second, npcName)); - } + lowerCaseLevel = groupName; + lowerCaseLevel += "."; } + lowerCaseLevel += string::toLower(levelName); - // Iterate weapons - for (const auto& [weaponName, weapon]: m_weaponList) - { - ScriptExecutionContext& context = weapon->getExecutionContext(); - std::pair executionData = context.getExecutionData(); + // Check to see if the level exists and has a value. + auto existing = m_levelList.find(lowerCaseLevel); + if (existing != m_levelList.end() && existing->second != nullptr) + return existing->second; - if (executionData.second > 0.0) - { - std::string weaponName("Weapon "); - weaponName.append(weaponName); - script_profiles.push_back(std::make_pair(executionData.second, weaponName)); - } - } + // Create a new stub level. + auto level = Level::createLevel(levelName); + if (!groupName.empty()) + level->groupMapName = groupName; + + // Add it to the list, or update the existing entry if it exists. + if (existing == m_levelList.end()) + m_levelList.insert(std::make_pair(lowerCaseLevel, level)); + else + existing->second = level; - std::sort(script_profiles.rbegin(), script_profiles.rend()); - return script_profiles; + return level; } -#endif -std::string transformString(const std::string& str) +std::shared_ptr Server::getLoadedLevelNoHint(std::string_view levelName) { - std::string newStr; - for (char ch: str) + if (levelName.empty()) + return nullptr; + + // Get the stub for the level. + LevelPtr level = getStubbedLevel(levelName); + if (level == nullptr) + return nullptr; + + // Level was already loaded. + if (level != nullptr && level->loaded) + return level; + + // Load the level. + if (LevelLoader::loadLevelInto(levelName, level)) { - if (ch == '"' || ch == '\\') - newStr += "\\"; - else if (ch == '%') - newStr += '%'; - newStr += ch; + m_levelList.insert(std::make_pair(string::toLower(levelName), level)); + return level; } - return newStr; + return nullptr; } -void Server::reportScriptException(const ScriptRunError& error) +std::shared_ptr Server::getLoadedLevel(std::string_view levelName, std::shared_ptr player) { - std::string error_message = transformString(error.getErrorString()); - sendToNC(error_message); - error_message += "\n"; - getScriptLog().out(error_message); -} + if (levelName.empty()) + return nullptr; -void Server::reportScriptException(const std::string& error_message) -{ - auto lines = CString{ error_message }.tokenize("\n"); + LevelPtr level = nullptr; - for (const auto& line: lines) + // Check if this level matches the list of group maps. + // If it does, and the player's group matches, try to load the group version of the level (or create it). + if (!player->account.groupName.empty()) { - sendToNC(line); - getScriptLog().out(line + "\n"); + for (const auto& groupmap : m_groupmaps.getValue()) + { + auto mask = string::trim(groupmap); + if (string::match(levelName, mask)) + { + // Check if this level already exists. + std::string groupMapName = string::toLower(std::format("{}.{}", player->account.groupName, levelName)); + if (level = findGmapForLevel(groupMapName, player); level != nullptr && level->loaded) + return level; + + // Level doesn't exist or isn't loaded, create it. + if (level == nullptr) + level = std::make_shared(); + + // Record that the level is going to be a group map, with the name of the group it belongs to. + // We do this now so level NPCs get added with the correct name. + level->isGroupMap = true; + level->groupMapName = player->account.groupName; + + // Load the level. + if (LevelLoader::loadLevelInto(levelName, level)) + { + m_levelList.insert(std::make_pair(groupMapName, level)); + return level; + } + } + } } -} - -///////////////////////////////////////////////////// -std::shared_ptr Server::getPlayer(unsigned short id) const -{ - auto iter = m_playerList.find(id); - if (iter == std::end(m_playerList)) - return nullptr; + // See if this level belongs to a gmap. + if (level = findGmapForLevel(levelName, player); level != nullptr) + { + if (!level->loaded && LevelLoader::loadLevelInto(level->levelName, level)) + m_levelList.insert(std::make_pair(string::toLower(level->levelName), level)); + return level; + } - return iter->second; + return getLoadedLevelNoHint(levelName); } -std::shared_ptr Server::getPlayer(unsigned short id, int type) const +std::shared_ptr Server::getLoadedLevel(std::string_view levelName, std::shared_ptr hintLevel) { - auto player = getPlayer(id); - if (player == nullptr || !(player->getType() & type)) + if (levelName.empty()) return nullptr; - return player; -} - -std::shared_ptr Server::getPlayer(const CString& account, int type) const -{ - for (auto& [id, player]: m_playerList) + // See if this level belongs to the hinted level. + if (hintLevel != nullptr && hintLevel->isGmap() && hintLevel->getSubLevelByName(levelName) != nullptr) { - // Check if its the type of player we are looking for - if (!player || !(player->getType() & type)) - continue; - - // Compare account names. - if (player->getAccountName().toLower() == account.toLower()) - return player; + if (!hintLevel->loaded && LevelLoader::loadLevelInto(hintLevel->levelName, hintLevel)) + m_levelList.insert(std::make_pair(string::toLower(hintLevel->levelName), hintLevel)); + return hintLevel; } - return nullptr; -} - -std::shared_ptr Server::getLevel(const std::string& pLevel) -{ - return Level::findLevel(pLevel); + return getLoadedLevelNoHint(levelName); } -std::shared_ptr Server::getWeapon(const std::string& name) +std::shared_ptr Server::getCachedLevelData(std::string_view levelName) { - auto iter = m_weaponList.find(name); - if (iter == std::end(m_weaponList)) + if (levelName.empty()) return nullptr; - return iter->second; -} -CString Server::getFlag(const std::string& pFlagName) -{ -#ifdef V8NPCSERVER - if (m_serverFlags.find(pFlagName) != m_serverFlags.end()) - return m_serverFlags[pFlagName]; - return ""; -#else - return m_serverFlags[pFlagName]; -#endif + std::string lowerCaseLevel = string::toLower(levelName); + if (auto it = m_cachedLevelDataList.find(lowerCaseLevel); it != m_cachedLevelDataList.end()) + return it->second; + + auto levelData = LevelLoader::loadStaticData(levelName); + m_cachedLevelDataList.insert(std::make_pair(lowerCaseLevel, levelData)); + return levelData; } -FileSystem* Server::getFileSystemByType(CString& type) +std::shared_ptr Server::findMap(std::string_view mapName) const noexcept { - // Find out the filesystem. - int fs = -1; - int j = 0; - while (filesystemTypes[j] != 0) + auto foundMap = std::ranges::find_if(m_mapList, [&mapName](const auto& map) { - if (type.comparei(CString(filesystemTypes[j]))) - { - fs = j; - break; - } - ++j; - } - - if (fs == -1) return 0; - return &m_filesystem[fs]; + return map->getMapName() == mapName; + }); + if (foundMap != std::ranges::end(m_mapList)) + return *foundMap; + return nullptr; } -#ifdef V8NPCSERVER -void Server::assignNPCName(std::shared_ptr npc, const std::string& name) +std::shared_ptr Server::findMapForLevel(std::string_view levelName) const noexcept { - std::string newName = name; - int num = 0; - while (m_npcNameList.find(newName) != m_npcNameList.end()) - newName = name + std::to_string(++num); - - npc->setName(newName); - m_npcNameList[newName] = npc; + auto foundMap = std::ranges::find_if(m_mapList, [&levelName](const auto& map) + { + return map->hasLevel(levelName); + }); + if (foundMap != std::ranges::end(m_mapList)) + return *foundMap; + return nullptr; } -void Server::removeNPCName(std::shared_ptr npc) +std::shared_ptr Server::findMapForLevel(MapType mapType, std::string_view levelName) const noexcept { - auto npcIter = m_npcNameList.find(npc->getName()); - if (npcIter != m_npcNameList.end()) - m_npcNameList.erase(npcIter); + auto foundMap = std::ranges::find_if(m_mapList, [&mapType, &levelName](const auto& map) + { + return map->mapType == mapType && map->hasLevel(levelName); + }); + if (foundMap != std::ranges::end(m_mapList)) + return *foundMap; + return nullptr; } -std::shared_ptr Server::addServerNpc(int npcId, float pX, float pY, std::shared_ptr pLevel, bool sendToPlayers) +std::shared_ptr Server::findGmapForLevel(std::string_view levelName, std::shared_ptr player) noexcept { - // Force database npc ids to be >= 1000 - if (npcId < 1000) - { - printf("Database npcs need to be greater than 1000\n"); - return nullptr; - } + LevelPtr returnLevel = nullptr; - // Make sure the npc id isn't in use - auto existing = m_npcList.find(npcId); - if (existing != std::end(m_npcList)) + // If this is the gmap itself, find it directly. + if (levelName.ends_with(".gmap"sv)) { - printf("Error creating database npc: Id is in use!\n"); - return nullptr; + if (auto it = m_levelList.find(levelName); it != m_levelList.end()) + return it->second; } - // Create the npc - auto newNPC = std::make_shared("", "", pX, pY, pLevel, NPCType::DBNPC); - newNPC->setId(npcId); - m_npcList.insert(std::make_pair(npcId, newNPC)); - - // Add the npc to the level - if (pLevel) + // Check if this level belongs to a gmap. + auto [iter, end] = m_gmapLevels.equal_range(levelName); + while (iter != end) { - pLevel->addNPC(npcId); - - // Send the NPC's props to everybody in range. - if (sendToPlayers) + if (auto level = iter->second.lock(); level != nullptr) { - CString packet = CString() >> (char)PLO_NPCPROPS >> (int)newNPC->getId() << newNPC->getProps(0); - sendPacketToLevelOnlyGmapArea(packet, pLevel); + // If this is a group map, and our group matches, use this one. + if (level->isGroupMap && player->account.groupName == level->groupMapName) + return level; + + // If this is a singleplayer map, and our account matches, use this one. + if (level->isSinglePlayer && player->account.name == level->groupMapName) + return level; + + // Otherwise, record this as the level we will return if we don't find anything. + if (!level->isGroupMap && !level->isSinglePlayer) + returnLevel = level; } + ++iter; } - return newNPC; + return returnLevel; } -void Server::handlePM(Player* player, const CString& message) +tileset::TilesetType Server::getTilesetTypeForLevel(std::shared_ptr level) const noexcept { - if (!m_pmHandlerNpc) + if (level == nullptr) + return tileset::TilesetType::CLASSIC; + + // Levels with terrain always use the terrain tileset. + if (level->hasTerrain()) + return tileset::TilesetType::TERRAIN; + + // Check for tileset type 1 (new tilesets). + for (const auto& newlevel : m_newTilesetLevels.getValue()) { - CString npcServerMsg; - npcServerMsg = "I am the npcserver for\nthis game server. Almost\nall npc actions are controlled\nby me."; - player->sendPacket(CString() >> (char)PLO_PRIVATEMESSAGE >> (short)m_npcServer->getId() << "\"\"," << npcServerMsg.gtokenize()); - return; + if (string::match(level->levelName, newlevel) || level->levelName.starts_with(newlevel)) + return tileset::TilesetType::NEWFORMAT; } - // TODO(joey): This sets the first argument as the npc object, so we can't use it here for now. - //m_pmHandlerNpc->queueNpcEvent("npcserver.playerpm", true, player->getScriptObject(), std::string(message.text())); + // If all levels are the new tileset, return that. + if (m_newTilesets.getValue()) + return tileset::TilesetType::NEWFORMAT; - m_pmHandlerNpc->getExecutionContext().addAction(m_scriptEngine.createAction("npcserver.playerpm", player->getScriptObject(), message.toString())); - m_scriptEngine.registerNpcUpdate(m_pmHandlerNpc.get()); + // Otherwise, return classic. + return tileset::TilesetType::CLASSIC; } -void Server::setPMFunction(uint32_t npcId, IScriptFunction* function) +tileset::TilesetType Server::getTilesetTypeForLevel(std::shared_ptr level) const noexcept { - auto npc = getNPC(npcId); - if (npc == nullptr || function == nullptr) - { - m_pmHandlerNpc = nullptr; - m_scriptEngine.removeCallBack("npcserver.playerpm"); - return; - } - - m_scriptEngine.setCallBack("npcserver.playerpm", function); - m_pmHandlerNpc = npc; + return getTilesetTypeForLevel(std::const_pointer_cast(level)); } -#endif -std::shared_ptr Server::addNPC(const CString& pImage, const CString& pScript, float pX, float pY, std::weak_ptr pLevel, bool pLevelNPC, bool sendToPlayers) +tileset::TileType Server::getTileTypeForTile(tileset::TilesetType tileset, uint16_t tile) const noexcept { - // New Npc - auto level = pLevel.lock(); - auto newNPC = std::make_shared(pImage, pScript.toString(), pX, pY, level, (pLevelNPC ? NPCType::LEVELNPC : NPCType::PUTNPC)); + // Terrain tileset uses non-blocking for all tiles. + if (tileset == tileset::TilesetType::TERRAIN) + return tileset::TileType::NONBLOCKING; - // Get available NPC Id. - uint32_t newId = m_npcIdGenerator.getAvailableId(); + // If tile is out of range, return blocking. + if (tile >= 4096) + return tileset::TileType::BLOCKING; - // Assign NPC Id and add to list. - newNPC->setId(newId); - m_npcList.insert(std::make_pair(newId, newNPC)); + // Classic (type 0). + if (tileset == tileset::TilesetType::CLASSIC) + return ENUM(tileset::Type0.at(tile)); - // Send the NPC's props to everybody in range. - if (sendToPlayers) + // New format (type 1). + if (tileset == tileset::TilesetType::NEWFORMAT) + return ENUM(tileset::Type1.at(tile)); + + // Default to blocking. + return tileset::TileType::BLOCKING; +} + +///////////////////////////////////////////////////// + +LevelItemType Server::rollBushItemDrop() const +{ + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution dist(1, 100); + int roll = dist(gen); + + for (const auto& [itemType, rate] : m_bushDrops) { - CString packet = CString() >> (char)PLO_NPCPROPS >> (int)newNPC->getId() << newNPC->getProps(0); - sendPacketToLevelOnlyGmapArea(packet, level); + if (roll <= rate) + return itemType; + roll -= rate; } - return newNPC; + return LevelItemType::INVALID; } -bool Server::deleteNPC(int id, bool eraseFromLevel) +///////////////////////////////////////////////////// + +std::shared_ptr Server::getWeapon(std::string_view name) { - auto npc = getNPC(id); - return deleteNPC(npc, eraseFromLevel); + auto iter = m_weaponList.find(name); + if (iter == std::end(m_weaponList)) + return nullptr; + return iter->second; } -bool Server::deleteNPC(std::shared_ptr npc, bool eraseFromLevel) +std::shared_ptr Server::addNPC(std::string_view image, std::string_view script, float x, float y, std::weak_ptr level, NPCStorageType storageType, bool sendToPlayers, std::string_view type) { - assert(npc); + LevelPtr levelPtr = level.lock(); + if (storageType == NPCStorageType::LEVEL && levelPtr == nullptr) + return nullptr; - // Erase NPC from the list. - m_npcList.erase(npc->getId()); + // Pick the ID range for the NPC ID. + auto startId = NPCID_GEN_LOCAL; + if (storageType == NPCStorageType::DATABASE) + startId = NPCID_GEN_DATABASE_LOCALN; - if (auto level = npc->getLevel(); level) - { - // Remove the NPC from the level - if (eraseFromLevel) - level->removeNPC(npc); + // Create the NPC. + NPCID newId = m_npcIdGenerator.getAvailableId(startId); + auto newNPC = std::make_shared(newId, storageType); - // Tell the clients to delete the NPC. - auto map = level->getMap(); - bool isOnMap = map != nullptr; - CString tmpLvlName = (isOnMap ? map->getMapName() : level->getLevelName()); + // Set the script type. + if (!type.empty()) + newNPC->scriptType = type; + + // Set NPC props. + auto localPixelPosition = toLocalPixelPosition(x, y); + newNPC->character.localPixelX = localPixelPosition.x(); + newNPC->character.localPixelY = localPixelPosition.y(); + newNPC->image = image; - for (auto& [pid, p]: m_playerList) + // Set the level details. + if (levelPtr != nullptr) + { + newNPC->setLevel(levelPtr); + + // If the level is a gmap, set the modTime on the map position props. + if (auto map = levelPtr->getMap(); map && map->isGmap()) { - if (p->isClient()) - { - if (isOnMap || p->getVersion() < CLVER_2_1) - p->sendPacket(CString() >> (char)PLO_NPCDEL >> (int)npc->getId()); - else - p->sendPacket(CString() >> (char)PLO_NPCDEL2 >> (char)tmpLvlName.length() << tmpLvlName >> (int)npc->getId()); - } + auto mapPosition = toMapPosition(TilePosition{x, y}); + newNPC->character.mapX = mapPosition.x(); + newNPC->character.mapY = mapPosition.y(); + newNPC->modTime[PROPID(NPCProp::GMAPLEVELX)] = m_frameStartTime; + newNPC->modTime[PROPID(NPCProp::GMAPLEVELY)] = m_frameStartTime; } } -#ifdef V8NPCSERVER - // TODO(joey): Need to deal with illegal characters - // TODO(joey): add putnpc storage - // If we persist this npc, delete the file [ maybe should add a parameter if we should remove the npc from disk ] - if (npc->getType() == NPCType::DBNPC) + // Set the script and record the initial state. + newNPC->setScript(script); + newNPC->recordInitialState(); + + // Finish adding the NPC. + return addNPC(newNPC, sendToPlayers); +} + +std::shared_ptr Server::addNPC(NPCPtr npc, bool sendToPlayers) +{ + // Add the NPC to the list. + m_npcList.insert(std::make_pair(npc->id, npc)); + + // Set the default warp type. + npc->warpRestrictions = hasNPCServer() ? NPCWarpRestrictions::NOTALLOWED : NPCWarpRestrictions::ALLOWED; + + // Set the default script type. + if (npc->scriptType.empty()) + { + if (npc->storageType == NPCStorageType::LEVEL) + npc->scriptType = NPCTYPE_LOCAL; + else npc->scriptType = NPCTYPE_OBJECT; + } + + // Add the NPC to the level. + auto level = npc->getLevel(); + if (level != nullptr) + { + level->addNPC(npc); + } + else if (!npc->level.empty()) { - CString filePath = getServerPath() << "npcs/npc" << npc->getName() << ".txt"; - FileSystem::fixPathSeparators(filePath); - remove(filePath.text()); + if (level = getStubbedLevel(npc->level, npc->groupName); level != nullptr) + level->addNPC(npc); } - if (npc->getType() == NPCType::DBNPC) + // Synchronize the group name of the NPC. + if (level != nullptr && level->isGroupMap && npc->groupName != level->groupMapName) + npc->groupName = level->groupMapName; + + // Set the NPC's name. + if (npc->name.empty()) { - // Remove npc name assignment - if (!npc->getName().empty()) - removeNPCName(npc); + std::string npcNamePrefix = std::format("{}_{}{}{}_{}_", + string::toLower(npc->scriptType), + level != nullptr && level->isGroupMap ? level->groupMapName : "", + level != nullptr && level->isGroupMap ? "." : "", + string::removeExtension(npc->level), m_serverTime + ); + auto count = std::ranges::count_if(m_npcList, [&npcNamePrefix](const auto& pair) + { + return pair.second->name.starts_with(npcNamePrefix); + }); - // If this is the npc that handles pms, clear it - if (m_pmHandlerNpc == npc) - m_pmHandlerNpc = nullptr; + npc->name = std::format("{}{}", npcNamePrefix, (count + 1)); + } + + // Created event. + if (hasNPCServer()) + { + npc->scripting.events.addEvent(ScriptEventType::CREATED, source::FromServer()); + } + +#ifdef DEBUG + if (running) + { + log::printLine(log::server, "Adding NPC [{}] '{}' at ({}, {})[{},{}] in level '{}'.", + npc->id, npc->name, npc->character.localPixelX, npc->character.localPixelY, npc->character.mapX, npc->character.mapY, npc->level); } #endif - return true; + // Record the current prop modification time. + npc->recordCurrentPropModTime(); + + // Send the NPC's props to everybody in range. + if (sendToPlayers) + { + CString packet = CString() >> (char)PLO_NPCPROPS >> (int)npc->id << npc->getAllPropsPacket(); + sendPacketToNearby(packet, npc->getGlobalPosition(), npc->getLevel()); + } + + return npc; } -bool Server::deleteClass(const std::string& className) +bool Server::deleteNPC(int id, bool eraseFromLevel) { - auto classIter = m_classList.find(className); - if (classIter == m_classList.end()) + auto npc = getNPC(id); + if (npc == nullptr) return false; - m_classList.erase(classIter); - CString filePath = getServerPath() << "scripts/" << className << ".txt"; - FileSystem::fixPathSeparators(filePath); - remove(filePath.text()); - - return true; + return deleteNPC(npc, eraseFromLevel); } -void Server::updateClass(const std::string& className, const std::string& classCode) +bool Server::deleteNPC(std::shared_ptr npc, bool eraseFromLevel) { - m_classList[className] = std::make_unique(className, classCode); + assert(npc); - CString filePath = getServerPath() << "scripts/" << className << ".txt"; - FileSystem::fixPathSeparators(filePath); + // Erase NPC from the list. + m_npcList.erase(npc->id); + m_npcIdGenerator.freeId(npc->id); + + if (auto level = npc->getLevel(); level) + { + // Get the sub-level the NPC is on. + auto [subLevel, levelData] = level->getSubLevelAndStaticDataAtPosition(MapPosition{npc->character.mapX, npc->character.mapY}); + + // Remove the NPC from the level + if (eraseFromLevel) + level->removeNPC(npc); - CString fileData(classCode); - fileData.save(filePath); + // Tell the clients to delete the NPC. + std::string levelName = npc->getLevelName(); + + auto lastLevelChange = npc->modTime[PROPID(NPCProp::CURLEVEL)]; + for (auto& [pid, p] : m_playerList) + { + std::optional lastEntered = std::nullopt; + auto playerClient = std::dynamic_pointer_cast(p); + if (playerClient != nullptr) + lastEntered = playerClient->getLevelLastEnteredTime(levelData.get()); + + if (playerClient != nullptr && (!lastEntered.has_value() || lastEntered.value() >= lastLevelChange || playerClient->getLevel() == level)) + { + if (playerClient->getLevelName() != levelName) + p->sendPacket(CString() >> (char)PLO_NPCDEL2 >> (char)levelName.length() << levelName >> (int)npc->id); + else p->sendPacket(CString() >> (char)PLO_NPCDEL >> (int)npc->id); + } + else if (p->isNC()) + { + p->sendPacket(CString() >> (char)PLO_NC_NPCDELETE >> (int)npc->id); + } + } + } + + return true; } -bool Server::addPlayer(PlayerPtr player, uint16_t id) +bool Server::addPlayer(PlayerPtr player, PlayerID id) { assert(player); @@ -1367,11 +1765,6 @@ bool Server::addPlayer(PlayerPtr player, uint16_t id) player->setId(id); m_playerList[id] = player; -#ifdef V8NPCSERVER - // Create script object for player - m_scriptEngine.wrapScriptObject(player.get()); -#endif - return true; } @@ -1380,56 +1773,89 @@ bool Server::deletePlayer(PlayerPtr player) if (player == nullptr) return true; - // Add the player to the set of players to delete. - if (m_deletedPlayers.insert(player).second) + if (player->isLoaded()) { - // Remove the player from the serverlist. + // If we have an NPC-Server, let it process the player first. + // TODO(NPCServer): Might need to check for remote NPC-Servers in the future here. + if (hasNPCServer() && player->isClient()) + m_npcServer->playerLogout(player); + + // Leave the level. + if (auto client = std::dynamic_pointer_cast(player); client != nullptr) + client->leaveLevel(); + + // Add the player to the set of players to delete. getServerList().deletePlayer(player); } + m_playerList.erase(player->getId()); + + // The ID will be freed in Player::cleanup. + // If we clear it now, then a player who presses F8 and reconnects will enter a race condition where they + // may get the same ID as their previous connection and then be immediately disconnected. + // m_playerIdGenerator.freeId(player->getId()); + return true; } -void Server::playerLoggedIn(PlayerPtr player) +bool Server::swapPlayer(PlayerPtr old_player, PlayerPtr new_player) { - // Tell the serverlist that the player connected. - getServerList().addPlayer(player); + if (old_player == nullptr || new_player == nullptr) + return false; -#ifdef V8NPCSERVER - // Send event to server that player is logging in - for (const auto& [npcName, npcPtr]: m_npcNameList) + auto id = old_player->getId(); + + // Swap the player in the player list. + m_playerList.erase(id); + m_playerList[id] = new_player; + + // Set the id on the new player. + new_player->setId(id); + + // Update the socket manager. + m_sockManager.unregisterSocket(old_player.get()); + m_sockManager.registerSocket(new_player.get()); + + // If we are an npc-server, fix our id. + if (new_player->isNPCServer() && id != NPCServerPlayerID) { - // TODO(joey): check if they have the event before queueing for them - if (auto npcObject = npcPtr.lock(); npcObject) - npcObject->queueNpcAction("npc.playerlogin", player.get()); + m_playerList.erase(id); + new_player->setId(NPCServerPlayerID); + m_playerList[NPCServerPlayerID] = new_player; } -#endif + + return true; } -bool Server::warpPlayerToSafePlace(uint16_t playerId) +void Server::recordPlayerLoggedIn(PlayerPtr player) { - auto player = getPlayer(playerId); + // Tell the serverlist that the player connected. + getServerList().addPlayer(player); +} + +bool Server::warpPlayerToSafePlace(PlayerID playerId) const +{ + auto player = getPlayer(playerId); if (player == nullptr) return false; // Try unstick me level. - CString unstickLevel = m_settings.getStr("unstickmelevel", "onlinestartlocal.nw"); - float unstickX = m_settings.getFloat("unstickmex", 30.0f); - float unstickY = m_settings.getFloat("unstickmey", 30.5f); - return player->warp(unstickLevel, unstickX, unstickY); + return player->warp(cached.unstickMeLevel.getValue(), {static_cast(cached.unstickMeTile[0].getValue() * 16.0f), static_cast(cached.unstickMeTile[1].getValue() * 16.0f)}); // TODO: Maybe try the default account level? } -void Server::calculateServerTime() +//---------------------------- + +void Server::calculateNWTime() { // Thu Feb 01 2001 17:33:34 GMT+0000 // this is likely the actual start time of timevar - m_serverTime = ((unsigned int)time(nullptr) - 981048814) / 5; + m_serverTime = static_cast((time(nullptr) - 981048814) / 5); } bool Server::isIpBanned(const CString& ip) { - for (const auto& ipBan: m_ipBans) + for (const auto& ipBan : m_ipBans) { if (ip.match(ipBan)) return true; @@ -1440,288 +1866,306 @@ bool Server::isIpBanned(const CString& ip) bool Server::isStaff(const CString& accountName) { - for (const auto& account: m_staffList) + const auto& staffList = m_staffList.get(); + if (!staffList.has_value()) + return false; + + for (const auto& account : staffList.value()) { - if (accountName.toLower() == account.trim().toLower()) + if (string::equalsi(accountName.toStringView(), account)) return true; } return false; } -void Server::logToFile(const std::string& fileName, const std::string& message) +void Server::logToFile(std::filesystem::path fileName, std::string_view message, bool writeTimestamp) const { - CString fileNamePath = CString() << getServerPath().remove(0, static_cast(getBaseHomePath().length())) << "logs/"; + std::filesystem::path logPath{"logs"}; - // Remove leading characters that may try to go up a directory - int idx = 0; - while (fileName[idx] == '.' || fileName[idx] == '/' || fileName[idx] == '\\') - idx++; - fileNamePath << fileName.substr(idx); + fs::FileIO file{logPath / fileName}; + if (!file.opened()) + return; - CLog logFile(fileNamePath, true); - logFile.open(); - logFile.out("\n%s\n", message.c_str()); + if (writeTimestamp) + { + if (m_classicStyleLogs.getValue()) + { + // Non-standard, but make it at least a LITTLE easier to read these dumb logs. + file.writeLine(); + + // std::ctime appends a newline, so issue a normal write. + char buffer[32]; + std::time_t curTime = std::time(nullptr); + std::strncpy(buffer, std::ctime(&curTime), 31); + buffer[31] = '\0'; + + file.write(std::string_view{buffer}); + } + else + { +#if __cpp_lib_chrono < 201907L + // Clang doesn't support timezones, so just use system_clock time (UTC) floored to seconds. + auto localtime = std::chrono::floor(std::chrono::system_clock::now()); +#else + // Get the current time, floored to seconds. + auto localtime = std::chrono::floor(std::chrono::current_zone()->to_local(std::chrono::system_clock::now())); +#endif + file.write(std::format(log::TimestampLong, localtime)); + file.write(" "sv); + } + } + + file.writeLine(message); } /* Server: Server Flag Management */ -bool Server::deleteFlag(const std::string& pFlagName, bool pSendToPlayers) + +std::optional Server::getFlag(std::string_view flagName) const { - if (m_settings.getBool("dontaddserverflags", false)) + auto flagVal = Scripting.variables.get(flagName); + if (auto flag = flagVal.lock(); flag != nullptr) + return flag->get(); + return std::nullopt; +} + +bool Server::deleteFlag(std::string_view flagName, bool sendToPlayers) +{ + if (m_dontAddServerFlags.getValue()) return false; - std::unordered_map::iterator mServerFlag; - if ((mServerFlag = m_serverFlags.find(pFlagName)) != m_serverFlags.end()) + if (Scripting.variables.remove(flagName)) { - m_serverFlags.erase(mServerFlag); - if (pSendToPlayers) - sendPacketToAll(CString() >> (char)PLO_FLAGDEL << pFlagName); + if (sendToPlayers) + sendPacketToAll(CString() >> (char)PLO_FLAGDEL << flagName); return true; } return false; } -bool Server::setFlag(CString pFlag, bool pSendToPlayers) +bool Server::setFlag(std::string_view flagPair, bool sendToPlayers) { - std::string flagName = pFlag.readString("=").text(); - CString flagValue = pFlag.readString(""); - return this->setFlag(flagName, (flagValue.isEmpty() ? "1" : flagValue), pSendToPlayers); + if (!flagPair.contains('=')) + return setFlag(flagPair, std::nullopt, sendToPlayers); + + auto separator = flagPair.find('='); + auto flagName = string::trim(flagPair.substr(0, separator)); + auto flagValue = string::trim(flagPair.substr(separator + 1)); + return setFlag(flagName, std::string{flagValue}, sendToPlayers); } -bool Server::setFlag(const std::string& pFlagName, const CString& pFlagValue, bool pSendToPlayers) +bool Server::setFlag(std::string_view flagName, std::optional flagValue, bool pSendToPlayers) { - if (m_settings.getBool("dontaddserverflags", false)) + if (m_dontAddServerFlags.getValue()) return false; - // delete flag - if (pFlagValue.isEmpty()) - return deleteFlag(pFlagName); - - // optimize - if (m_serverFlags[pFlagName] == pFlagValue) - return true; + // Function to crop flags. + auto cropFlag = [this, &flagName](std::string& value) + { + if (cached.enableFlagCropping.getValue()) + value.erase(std::min(value.length(), static_cast(223 - 1) - flagName.length())); + return value; + }; - // set flag - if (m_settings.getBool("cropflags", true)) + // Alter the flag if it exists. + auto existing = Scripting.variables.get(flagName).lock(); + if (existing != nullptr) { - int fixedLength = 223 - 1 - (int)pFlagName.length(); - m_serverFlags[pFlagName] = pFlagValue.subString(0, fixedLength); + //bool isFlag = existing->has() && !existing->has(); + bool isStringFlag = existing->has(); + + // No change. + if (!flagValue.has_value()) + return true; + + // If flag value is empty, delete. + if (isStringFlag && flagValue.value().empty()) + return deleteFlag(flagName); + + // Alter value. + existing->assign(cropFlag(flagValue.value())); } + // New flag. else - m_serverFlags[pFlagName] = pFlagValue; + { + if (!flagValue.has_value()) + Scripting.variables.add(flagName, GameValue{true}); + else Scripting.variables.add(flagName, GameValue{cropFlag(flagValue.value())}); + } + + // And share it. + if (pSendToPlayers && (!hasNPCServer() || flagName.starts_with("serverr."))) + { + if (!flagValue.has_value()) + sendPacketToAll(CString() >> (char)PLO_FLAGSET << flagName); + else sendPacketToAll(CString() >> (char)PLO_FLAGSET << flagName << "=" << flagValue.value()); + } - if (pSendToPlayers) - sendPacketToAll(CString() >> (char)PLO_FLAGSET << pFlagName << "=" << pFlagValue); return true; } -/* - Packet-Sending Functions -*/ - -void Server::sendPacketToAll(const CString& packet, const std::set& exclude) const +void Server::hitObjectsAtPoint(const TilePosition& pos, int8_t power, std::weak_ptr level, PlayerPtr source) const { - for (auto& [id, player]: m_playerList) - { - if (exclude.contains(id)) - continue; - if (player->isNPCServer()) - continue; + // Client ignores if not within 2 tiles in both X/Y. + sendPacketToNearby(CString() >> (char)PLO_HITOBJECTS >> (short)source->getId() >> (char)power >> (char)(pos.x() * 2) >> (char)(pos.y() * 2), toPixelPosition(pos), level.lock()); +} - player->sendPacket(packet); - } +void Server::hitObjectsAtPoint(const TilePosition& pos, int8_t power, std::weak_ptr level, NPCPtr source) const +{ + // Client ignores if not within 2 tiles in both X/Y. + sendPacketToNearby(CString() >> (char)PLO_HITOBJECTS >> (short)0 >> (char)power >> (char)(pos.x() * 2) >> (char)(pos.y() * 2) >> (int)source->id, toPixelPosition(pos), level.lock()); } -void Server::sendPacketToLevelArea(const CString& packet, std::weak_ptr level, const std::set& exclude, PlayerPredicate sendIf) const +void Server::hitPlayer(PlayerID playerId, int8_t power, float fromX, float fromY, std::shared_ptr source) const { - auto levelp = level.lock(); - if (!levelp) return; + auto player = getPlayer(playerId); + if (player == nullptr) + return; - // If we have no map, just send to the level players. - auto map = levelp->getMap(); - if (!map) - { - for (auto id: levelp->getPlayers()) - { - if (exclude.contains(id)) continue; - if (auto other = this->getPlayer(id); other->isClient() && (sendIf == nullptr || sendIf(other.get()))) - other->sendPacket(packet); - } - } - else - { - std::pair sgmap{ levelp->getMapX(), levelp->getMapY() }; + // Client ignores if PLO_DISABLECLASSICMODE was sent. + // Client ignores if the source wasn't within 10 tiles. - for (auto& [id, other]: m_playerList) - { - if (exclude.contains(id)) continue; - if (!other->isClient()) continue; - if (sendIf != nullptr && !sendIf(other.get())) continue; + auto tilePosition = player->getTilePosition(); + auto dx = tilePosition.x() - fromX; + auto dy = tilePosition.y() - fromY; - auto othermap = other->getMap().lock(); - if (!othermap || othermap != map) continue; + // Normalize the direction vector. + float length = std::sqrt(dx * dx + dy * dy); + if (!DoubleIsZero(dx)) + dx /= length; + if (!DoubleIsZero(dy)) + dy /= length; - // Check if they are nearby before sending the packet. - auto ogmap{ other->getMapPosition() }; - if (abs(ogmap.first - sgmap.first) < 2 && abs(ogmap.second - sgmap.second) < 2) - other->sendPacket(packet); - } - } -} + // Push out 4 tiles. + dx *= 4; + dy *= 4; -void Server::sendPacketToLevelArea(const CString& packet, std::weak_ptr player, const std::set& exclude, PlayerPredicate sendIf) const -{ - auto playerp = player.lock(); - if (!playerp) return; + // Pixel position. + auto encodedDX = static_cast(static_cast(dx * 16) + 64); + auto encodedDY = static_cast(static_cast(dy * 16) + 64); - auto level = playerp->getLevel(); - if (!level) return; + // Send the final packet. + player->sendPacket(CString() >> (char)PLO_HURTPLAYER >> (short)0 >> (char)(encodedDX) >> (char)(encodedDY) >> (char)power >> (int)source->id); +} - // If we have no map, just send to the level players. - auto map = level->getMap(); - if (!map) - { - for (auto id: level->getPlayers()) - { - if (exclude.contains(id)) continue; - if (auto other = this->getPlayer(id); other->isClient() && (sendIf == nullptr || sendIf(other.get()))) - other->sendPacket(packet); - } - } - else - { - auto isGroupMap = map->isGroupMap(); - auto sgmap{ playerp->getMapPosition() }; +void Server::sendTriggerAction(PlayerID toPlayerId, NPCID fromNpcId, const LocalPixelPosition& localPosition, std::string_view action, std::string_view params) const +{ + auto player = getPlayer(toPlayerId); + if (player == nullptr) + return; - for (auto& [id, other]: m_playerList) - { - if (exclude.contains(id)) continue; - if (!other->isClient()) continue; - if (sendIf != nullptr && !sendIf(other.get())) continue; - - auto othermap = other->getMap().lock(); - if (!othermap || othermap != map) continue; - if (isGroupMap && playerp->getGroup() != other->getGroup()) continue; - - // Check if they are nearby before sending the packet. - auto ogmap{ other->getMapPosition() }; - if (abs(ogmap.first - sgmap.first) < 2 && abs(ogmap.second - sgmap.second) < 2) - other->sendPacket(packet); - } - } + CString packet = CString() >> (char)PLO_TRIGGERACTION >> (short)0 >> (int)fromNpcId >> (char)(localPosition.x() / 8.0f) >> (char)(localPosition.y() / 8.0f) << action << "," << params; + player->sendPacket(packet); } -void Server::sendPacketToLevelOnlyGmapArea(const CString& packet, std::weak_ptr level, const std::set& exclude, PlayerPredicate sendIf) const +void Server::sendTriggerAction(LevelPtr toLevel, NPCID fromNpcId, const PixelPosition& position, std::string_view action, std::string_view params) const { - auto levelp = level.lock(); - if (!levelp) return; + if (toLevel == nullptr) + return; - // If we have no map, just send to the level players. - // If it we are on a bigmap, also just send to level players. - auto map = levelp->getMap(); - if (!map || map->getType() == MapType::BIGMAP) - { - for (auto id : levelp->getPlayers()) - { - if (exclude.contains(id)) continue; - if (auto other = this->getPlayer(id); other->isClient() && (sendIf == nullptr || sendIf(other.get()))) - other->sendPacket(packet); - } - } - else - { - std::pair sgmap{ levelp->getMapX(), levelp->getMapY() }; + auto localPosition = toLocalPixelPosition(position); + CString packet = CString() >> (char)PLO_TRIGGERACTION >> (short)0 >> (int)fromNpcId >> (char)(localPosition.x() / 8.0f) >> (char)(localPosition.y() / 8.0f) << action << "," << params; + sendPacketToNearby(packet, position, toLevel); +} - for (auto& [id, other] : m_playerList) - { - if (exclude.contains(id)) continue; - if (!other->isClient()) continue; - if (sendIf != nullptr && !sendIf(other.get())) continue; +/* + Packet-Sending Functions +*/ - auto othermap = other->getMap().lock(); - if (!othermap || othermap != map) continue; +void Server::sendPacketToAll(const CString& packet, const std::set& exclude, PlayerPredicate sendIf) const +{ + for (auto& [id, player] : m_playerList) + { + if (exclude.contains(id)) + continue; + if (player->isNPCServer()) + continue; + if (sendIf && !sendIf(player.get())) + continue; - // Check if they are nearby before sending the packet. - auto ogmap{ other->getMapPosition() }; - if (abs(ogmap.first - sgmap.first) < 2 && abs(ogmap.second - sgmap.second) < 2) - other->sendPacket(packet); - } + player->sendPacket(packet); } } -void Server::sendPacketToLevelOnlyGmapArea(const CString& packet, std::weak_ptr player, const std::set& exclude, PlayerPredicate sendIf) const +void Server::sendPacketToType(int who, const CString& pPacket, std::weak_ptr pPlayer) const { - auto playerp = player.lock(); - if (!playerp) return; + auto p = pPlayer.lock(); + if (!running) return; - auto level = playerp->getLevel(); - if (!level) return; + sendPacketToType(who, pPacket, p.get()); +} - // If we have no map, just send to the level players. - auto map = level->getMap(); - if (!map || map->getType() == MapType::BIGMAP) - { - for (auto id : level->getPlayers()) - { - if (exclude.contains(id)) continue; - if (auto other = this->getPlayer(id); other->isClient() && (sendIf == nullptr || sendIf(other.get()))) - other->sendPacket(packet); - } - } - else +void Server::sendPacketToType(int who, const CString& pPacket, Player* pPlayer) const +{ + if (!running) return; + for (auto& [id, player] : m_playerList) { - auto isGroupMap = map->isGroupMap(); - auto sgmap{ playerp->getMapPosition() }; - - for (auto& [id, other] : m_playerList) - { - if (exclude.contains(id)) continue; - if (!other->isClient()) continue; - if (sendIf != nullptr && !sendIf(other.get())) continue; - - auto othermap = other->getMap().lock(); - if (!othermap || othermap != map) continue; - if (isGroupMap && playerp->getGroup() != other->getGroup()) continue; - - // Check if they are nearby before sending the packet. - auto ogmap{ other->getMapPosition() }; - if (abs(ogmap.first - sgmap.first) < 2 && abs(ogmap.second - sgmap.second) < 2) - other->sendPacket(packet); - } + if ((player->getType() & who) && (!pPlayer || id != pPlayer->getId())) + player->sendPacket(pPacket); } } -void Server::sendPacketToOneLevel(const CString& packet, std::weak_ptr level, const std::set& exclude) const +void Server::sendPacketToOneLevelPart(const CString& packet, const PixelPosition& position, LevelPtr level, const std::set& exclude, PlayerPredicate sendIf) const { - auto levelp = level.lock(); - if (!levelp) return; + auto mapPosition = toMapPosition(position); + sendPacketToOneLevelPart(packet, level, mapPosition, exclude, sendIf); +} - for (auto id: levelp->getPlayers()) +void Server::sendPacketToOneLevelPart(const CString& packet, LevelPtr level, const MapPosition& mapPosition, const std::set& exclude, PlayerPredicate sendIf) const +{ + for (const auto& id : level->findPlayersInLevelPart(mapPosition)) { if (exclude.contains(id)) continue; - if (auto player = this->getPlayer(id); player->isClient()) + if (auto player = this->getPlayer(id); player && player->isClient() && (!sendIf || sendIf(player.get()))) player->sendPacket(packet); } } -void Server::sendPacketToType(int who, const CString& pPacket, std::weak_ptr pPlayer) const +void Server::sendPacketToNearby(const CString& packet, const PixelPosition& position, LevelPtr level, const std::set& exclude, PlayerPredicate sendIf) const { - auto p = pPlayer.lock(); - if (!running) return; + if (!running || level == nullptr) return; - sendPacketToType(who, pPacket, p.get()); + auto players = level->findInRangePlayersForCommunication(position); + for (const auto& playerId : players) + { + if (exclude.contains(playerId)) + continue; + if (auto player = getPlayer(playerId); player != nullptr && (!sendIf || sendIf(player.get()))) + { + // Are we on the same level? + // Levels on a gmap are the same level and thus this would be false. + bool sameLevel = level->levelName == player->getLevelName(); + + // TODO: Enable nearby data for bigmaps again. + // The current problem is that the NPC-Server will send modified NPC props to players when they don't know about the NPC yet, which breaks the NPCs. + // We need to add the ability to send the full NPC details of adjacent levels when entering a bigmap level before we can re-enable this. + if (!sameLevel) + continue; + + // TODO: Figure out when PLO_SETACTIVELEVEL was introduced. + //if (!sameLevel && player->getVersion() < CLVER_2_17) + // continue; + // + //if (!sameLevel) player->sendPacket(CString() >> (char)PLO_SETACTIVELEVEL << level->levelName); + player->sendPacket(packet); + //if (!sameLevel) player->sendPacket(CString() >> (char)PLO_SETACTIVELEVEL << player->getLevelName()); + } + } } -void Server::sendPacketToType(int who, const CString& pPacket, Player* pPlayer) const +void Server::sendPacketToLevelAndPastVisitorsAfter(StaticLevelData* level, clock::time_point modTime, const CString& packet) const { if (!running) return; - for (auto& [id, player]: m_playerList) + for (const auto& [id, player] : players_of_type(m_playerList)) { - if ((player->getType() & who) && (!pPlayer || id != pPlayer->getId())) - player->sendPacket(pPacket); + auto playerLevel = player->getLevel(); + auto levelData = playerLevel->getStaticLevelDataAtPosition(player->getMapPosition()); + auto lastEntered = player->getLevelLastEnteredTime(level); + if ((lastEntered.has_value() && lastEntered.value() > modTime) || (levelData != nullptr && levelData.get() == level)) + player->sendPacket(packet); } } @@ -1733,308 +2177,118 @@ bool Server::NC_AddWeapon(std::shared_ptr pWeaponObj) if (pWeaponObj == nullptr) return false; - m_weaponList[pWeaponObj->getName()] = pWeaponObj; + m_weaponList[pWeaponObj->name] = pWeaponObj; return true; } -bool Server::NC_DelWeapon(const std::string& pWeaponName) +bool Server::NC_DelWeapon(std::string_view pWeaponName) { // Definitions auto weaponObj = getWeapon(pWeaponName); if (!weaponObj || weaponObj->isDefault()) return false; - // Delete from File Browser + // Delete from the file system. CString name = pWeaponName; name.replaceAllI("\\", "_"); name.replaceAllI("/", "_"); name.replaceAllI("*", "@"); name.replaceAllI(":", ";"); name.replaceAllI("?", "!"); - CString filePath = getServerPath() << "weapons/weapon" << name << ".txt"; - FileSystem::fixPathSeparators(filePath); - remove(filePath.text()); + std::filesystem::path weaponFile{"weapons"}; + std::filesystem::remove(weaponFile / std::format("weapon{}.txt", name)); // Delete from Memory - m_weaponList.erase(pWeaponName); + m_weaponList.erase(std::string{pWeaponName}); // Delete from Players sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_NPCWEAPONDEL << pWeaponName); return true; } -void Server::updateWeaponForPlayers(std::shared_ptr pWeapon) +void Server::updateWeaponForPlayers(Weapon* weapon) { - // Update Weapons - for (auto& [id, player]: m_playerList) - { - if (!player->isClient()) - continue; - - if (player->hasWeapon(pWeapon->getName())) - { - player->sendPacket(CString() >> (char)PLO_NPCWEAPONDEL << pWeapon->getName()); - player->sendPacket(pWeapon->getWeaponPacket(player->getVersion())); - } - } -} + if (weapon == nullptr) + return; -void Server::updateClassForPlayers(ScriptClass* pClass) -{ // Update Weapons - for (auto& [id, player]: m_playerList) + for (auto& [id, player] : m_playerList) { if (!player->isClient()) continue; - if (player->getVersion() >= CLVER_4_0211) + if (player->account.hasWeapon(weapon->name)) { - if (pClass != nullptr) - { - CString out; - CString b = pClass->getByteCode(); - out >> (char)PLO_RAWDATA >> (int)b.length() << "\n"; - out >> (char)PLO_NPCWEAPONSCRIPT << b; - - player->sendPacket(out); - } + player->sendPacket(CString() >> (char)PLO_NPCWEAPONDEL << weapon->name); + weapon->registerWeaponWithPlayer(player); } } } -/* - GS2 Functionality -*/ -template -void Server::compileScript(ScriptObjType& scriptObject, GS2ScriptManager::user_callback_type& cb) +void Server::updateWeaponForPlayers(std::shared_ptr weapon) { - std::string script{ scriptObject.getSource().getClientGS2() }; - - m_gs2ScriptManager.compileScript(script, [cb, &scriptObject, this](const CompilerResponse& resp) - { - if (!resp.errors.empty()) - { - handleGS2Errors(resp.errors, scripting::getErrorOrigin(scriptObject)); - } - - // Compile any referenced joined classes, disabled for now as all classes should be compiled immediately - //if (resp.success) - //{ - // for (auto& joinedClass : resp.joinedClasses) - // { - // auto cls = getClass(joinedClass); - // if (cls && cls->getByteCode().isEmpty()) - // { - // GS2ScriptManager::user_callback_type fn = [](const auto& resp) {}; - // compileScript(*cls, fn); - // } - // } - //} - - if (cb) - { - cb(resp); - } - }); + updateWeaponForPlayers(weapon.get()); } -void Server::compileGS2Script(const std::string& source, GS2ScriptManager::user_callback_type cb) +// TODO(Nalin): This should probably be in the NPCServer class. +void Server::updateClassForPlayers(std::shared_ptr scriptClass) { - m_gs2ScriptManager.compileScript(source, cb); -} + CString classPacket = scriptClass->getClassPacket(); + if (classPacket.isEmpty()) + return; -void Server::compileGS2Script(NPC* scriptObject, GS2ScriptManager::user_callback_type cb) -{ - if (scriptObject) + // Update players. + for (auto& [id, player] : m_playerList) { - compileScript(*scriptObject, cb); - } -} + if (!player->isClient()) + continue; -void Server::compileGS2Script(ScriptClass* scriptObject, GS2ScriptManager::user_callback_type cb) -{ - if (scriptObject) - { - compileScript(*scriptObject, cb); + player->sendPacket(CString() >> (char)PLO_RAWDATA >> (int)classPacket.length()); + player->sendPacket(classPacket); } } -void Server::compileGS2Script(Weapon* scriptObject, GS2ScriptManager::user_callback_type cb) -{ - if (scriptObject) - { - compileScript(*scriptObject, cb); - } -} +//---------------------------- -void Server::handleGS2Errors(const std::vector& errors, const std::string& origin) +void Server::sendShootToOneLevel(LevelShoot* shoot, std::shared_ptr level) const { - std::string errorMsg; - for (auto& err: errors) - { - switch (err.level()) - { - case ErrorLevel::E_INFO: - errorMsg += std::format("info: {}\n", err.msg()); - break; - case ErrorLevel::E_WARNING: - errorMsg += std::format("warning: {}\n", err.msg()); - break; - default: - errorMsg += std::format("error: {}\n", err.msg()); - break; - } - } - - if (!errorMsg.empty()) - reportScriptException(std::format("Script compiler output for {}:\n{}", origin, errorMsg)); -} - -/* - Translation Functionality -*/ -bool Server::TS_Load(const CString& pLanguage, const CString& pFileName) -{ - // Load File - std::vector fileData = CString::loadToken(pFileName, "\n", true); - if (fileData.empty()) - return false; - - // Parse File - std::vector::const_iterator cur, next; - for (cur = fileData.begin(); cur != fileData.end(); ++cur) - { - if (cur->find("msgid") == 0) - { - CString msgId = cur->subString(7, cur->length() - 8); - CString msgStr = ""; - bool isStr = false; - - ++cur; - while (cur != fileData.end()) - { - // Make sure our string isn't empty. - if (cur->isEmpty()) - { - ++cur; - continue; - } - - if ((*cur)[0] == '"' && (*cur)[cur->length() - 1] == '"') - { - CString str('\n'); - str.write(cur->subString(1, cur->length() - 2)); - (isStr ? msgStr.write(str) : msgId.write(str)); - } - else if (cur->find("msgstr") == 0) - { - msgStr = cur->subString(8, cur->length() - 9); - isStr = true; - } - else - { - --cur; - break; - } - - ++cur; - } + if (shoot == nullptr || level == nullptr) + return; - m_translationManager.add(pLanguage.text(), msgId.text(), msgStr.text()); - } + float pi = std::numbers::pi_v; + float halfpi = pi / 2; - if (cur == fileData.end()) - break; - } + ShootPacketWrapper newPacket{}; + newPacket.source = (shoot->from.second == ScriptObjectType::NPC ? shoot->from.first : 0); + newPacket.position = toPixelPosition(shoot->position); + newPacket.offsetx = 0; + newPacket.offsety = 0; + newPacket.sangle = static_cast(220 * (std::clamp(shoot->angle, 0.0f, 2 * pi) / (2 * pi))); + newPacket.sanglez = std::clamp(110 + static_cast(110 * (std::clamp(shoot->zangle, -halfpi, halfpi) / halfpi)), 0, 220); + newPacket.power = shoot->powerIn44Pixels; + newPacket.gravity = static_cast(shoot->gravity * 16); + newPacket.gani = shoot->gani; + newPacket.shootParams = string::toCSV(getShootParams()); - return true; -} + CString oldPacketBuf = CString() >> (char)PLO_SHOOT >> (short)0 << newPacket.constructShootV1(); + CString newPacketBuf = CString() >> (char)PLO_SHOOT2 >> (short)0 << newPacket.constructShootV2(); -CString Server::TS_Translate(const CString& pLanguage, const CString& pKey) -{ - return m_translationManager.translate(pLanguage.toLower().text(), pKey.text()); + sendPacketToNearby(oldPacketBuf, newPacket.position, level, {0}, [](const auto pl) + { + return pl->getVersion() < CLVER_5_07; + }); + sendPacketToNearby(newPacketBuf, newPacket.position, level, {0}, [](const auto pl) + { + return pl->getVersion() >= CLVER_5_07; + }); } -void Server::TS_Reload() -{ - // Save Translations - this->TS_Save(); - - // Reset Translations - m_translationManager.reset(); - - // Load Translation Folder - FileSystem translationFS; - translationFS.addDir("translations", "*.po"); +//---------------------------- - // Load Each File - const std::map& temp = translationFS.getFileList(); - for (auto& i: temp) - this->TS_Load(removeExtension(i.first), i.second); -} - -void Server::TS_Save() +void Server::scheduleTask(precise_clock::duration delay, std::function task) { - // Grab Translations - std::map* languages = m_translationManager.getTranslationList(); - - // Iterate each Language - for (auto& language: *languages) - { - // Create Output - CString output; - - // Iterate each Translation - for (auto& lang: language.second) - { - output << "msgid "; - std::vector sign = CString(lang.first.c_str()).removeAll("\r").tokenize("\n"); - for (auto& s: sign) - output << "\"" << s << "\"\r\n"; - output << "msgstr "; - if (!lang.second.empty()) - { - std::vector lines = CString(lang.second.c_str()).removeAll("\r").tokenize("\n"); - for (auto& line: lines) - output << "\"" << line << "\"\r\n"; - } - else - output << "\"\"\r\n"; - - output << "\r\n"; - } - - // Save File - output.trimRight().save(getServerPath() << "translations/" << language.first.c_str() << ".po"); - } + m_scheduledTasks.emplace_back(delay, std::move(task)); } -void Server::sendShootToOneLevel(const std::weak_ptr& level, float x, float y, float z, float angle, float zangle, float strength, const std::string& ani, const std::string& aniArgs) const -{ - auto levelLock = level.lock(); - ShootPacketNew newPacket{}; - newPacket.pixelx = (int16_t)(x * 16); - newPacket.pixely = (int16_t)(y * 16); - newPacket.pixelz = (int16_t)(z * 16); - newPacket.offsetx = 0; - newPacket.offsety = 0; - newPacket.sangle = (int8_t)angle; - newPacket.sanglez = (int8_t)zangle; - newPacket.speed = (int8_t)strength; - newPacket.gravity = 8; - newPacket.gani = ani; - newPacket.ganiArgs = aniArgs; - newPacket.shootParams = getShootParams(); - - CString oldPacketBuf = CString() >> (char)PLO_SHOOT >> (short)0 << newPacket.constructShootV1(); - CString newPacketBuf = CString() >> (char)PLO_SHOOT2 >> (short)0 << newPacket.constructShootV2(); - - sendPacketToLevelArea(oldPacketBuf, levelLock, { 0 }, [](const auto pl) - { - return pl->getVersion() < CLVER_5_07; - }); - sendPacketToLevelArea(newPacketBuf, levelLock, { 0 }, [](const auto pl) - { - return pl->getVersion() >= CLVER_5_07; - }); -} +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/ServerList.cpp b/server/src/ServerList.cpp index 33abdda72..27c7a88ac 100644 --- a/server/src/ServerList.cpp +++ b/server/src/ServerList.cpp @@ -1,17 +1,42 @@ -#include - +#include +#include #include +#include +#include +#include +#include #include +#include +#include +#include +#include + +#include -#include +#include +#include #include #include -#include "IConfig.h" - -#include "Player.h" -#include "Server.h" -#include "ServerList.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// /* Pointer-Functions for Packets @@ -112,7 +137,7 @@ bool ServerList::canRecv() void ServerList::onUnregister() { - m_server->getServerLog().out(":: %s - Disconnected.\n", m_socket.getDescription()); + log::printLine(log::server, "{} - Disconnected.", m_socket.getDescription()); } bool ServerList::main() @@ -185,43 +210,41 @@ bool ServerList::connectServer() if (getConnected()) return true; - auto& serverLog = m_server->getServerLog(); - - serverLog.out(":: Initializing %s socket.\n", m_socket.getDescription()); + log::printLine(log::server, "Initializing {} socket.", m_socket.getDescription()); // Initialize the socket - if (m_socket.init(settings.getStr("listip").text(), settings.getStr("listport").text()) != 0) + if (m_socket.init(settings.get("listip").value_or(""s).c_str(), settings.get("listport").value_or(""s).c_str()) != 0) { - serverLog.out(":: [Error] Could not initialize %s socket.\n", m_socket.getDescription()); + log::printLine(log::server, "[Error] Could not initialize {} socket.", m_socket.getDescription()); return false; } // Connect to Server if (m_socket.connect() != 0) { - serverLog.out(":: [Error] Could not connect %s socket.\n", m_socket.getDescription()); + log::printLine(log::server, "[Error] Could not connect {} socket.", m_socket.getDescription()); return false; } m_server->getSocketManager().registerSocket((CSocketStub*)this); - serverLog.out(":: %s - Connected.\n", m_socket.getDescription()); + log::printLine(log::server, "{} - Connected to {}:{}.", m_socket.getDescription(), m_socket.getRemoteIp(), m_socket.getRemotePort()); // Get Some Stuff - CString name(settings.getStr("name")); - CString desc(settings.getStr("description")); - CString language(settings.getStr("language", "English")); + CString name(settings.get("name").value_or(""s)); + CString desc(settings.get("description").value_or(""s)); + CString language(settings.get("language").value_or("English"s)); CString version(APP_VERSION); - CString url(settings.getStr("url", "http://www.graal.in/")); - CString ip(settings.getStr("serverip", "AUTO")); - CString port(settings.getStr("serverport", "14900")); - CString localip(settings.getStr("localip")); + CString url(settings.get("url").value_or("http://www.graal.in/"s)); + CString ip(settings.get("serverip").value_or("AUTO"s)); + CString port(settings.get("serverport").value_or("14900"s)); + CString localip(settings.get("localip").value_or(""s)); // Grab the local ip. if (localip.isEmpty() || localip == "AUTO") localip = m_socket.getLocalIp(); if (localip == "127.0.1.1" || localip == "127.0.0.1") { - serverLog.out("** [WARNING] Socket returned %s for its local ip! Not sending local ip to serverlist.\n", localip.text()); + log::printLine(log::server, "** [WARNING] Socket returned {} for its local ip! Not sending local ip to serverlist.", localip); localip.clear(); } @@ -236,16 +259,16 @@ bool ServerList::connectServer() // Send before SVO_NEWSERVER or else we will get an incorrect name. auto& adminsettings = m_server->getAdminSettings(); - sendPacket(CString() >> (char)SVO_SERVERHQPASS << adminsettings.getStr("hq_password")); + sendPacket(CString() >> (char)SVO_SERVERHQPASS << adminsettings.get("hq_password").value_or(""s)); // Send server info. sendPacket(CString() >> (char)SVO_NEWSERVER >> (char)name.length() << name >> (char)desc.length() << desc >> (char)language.length() << language >> (char)version.length() << version >> (char)url.length() << url >> (char)ip.length() << ip >> (char)port.length() << port >> (char)localip.length() << localip); // Set the level now. - if (m_server->getSettings().getBool("onlystaff", false)) + if (m_server->getSettings().get("onlystaff").value_or(false)) sendPacket(CString() >> (char)SVO_SERVERHQLEVEL >> (char)0); else - sendPacket(CString() >> (char)SVO_SERVERHQLEVEL >> (char)adminsettings.getInt("hq_level", 1)); + sendPacket(CString() >> (char)SVO_SERVERHQLEVEL >> (char)adminsettings.get("hq_level").value_or(1)); sendVersionConfig(); @@ -264,7 +287,7 @@ void ServerList::sendVersionConfig() // Send allowed versions to the listserver CString versionNames; auto& versionList = m_server->getAllowedVersions(); - for (const auto& version: versionList) + for (const auto& version : versionList) { if (!versionNames.isEmpty()) versionNames << ","; @@ -296,23 +319,26 @@ void ServerList::sendPacket(CString& pPacket, bool sendNow) /* Altering Player Information */ -void ServerList::addPlayer(PlayerPtr player) +void ServerList::addPlayer(std::shared_ptr player) { assert(player != nullptr); + if (player->isNC() || player->isNPCServer()) + return; + CString dataPacket; dataPacket >> (char)SVO_PLYRADD >> (short)player->getId() >> (char)player->getType(); - dataPacket >> (char)PLPROP_ACCOUNTNAME << player->getProp(PLPROP_ACCOUNTNAME); - dataPacket >> (char)PLPROP_NICKNAME << player->getProp(PLPROP_NICKNAME); - dataPacket >> (char)PLPROP_CURLEVEL << player->getProp(PLPROP_CURLEVEL); - dataPacket >> (char)PLPROP_X << player->getProp(PLPROP_X); - dataPacket >> (char)PLPROP_Y << player->getProp(PLPROP_Y); - dataPacket >> (char)PLPROP_ALIGNMENT << player->getProp(PLPROP_ALIGNMENT); - dataPacket >> (char)PLPROP_IPADDR << player->getProp(PLPROP_IPADDR); + dataPacket >> (char)PlayerProp::ACCOUNTNAME << player->getProp().serialize(); + dataPacket >> (char)PlayerProp::NICKNAME << player->getProp().serialize(); + dataPacket >> (char)PlayerProp::CURLEVEL << player->getProp().serialize(); + dataPacket >> (char)PlayerProp::X << player->getProp().serialize(); + dataPacket >> (char)PlayerProp::Y << player->getProp().serialize(); + dataPacket >> (char)PlayerProp::ALIGNMENT << player->getProp().serialize(); + dataPacket >> (char)PlayerProp::IPADDR << player->getProp().serialize(); sendPacket(dataPacket); } -void ServerList::deletePlayer(PlayerPtr player) +void ServerList::deletePlayer(std::shared_ptr player) { assert(player != nullptr); @@ -326,10 +352,13 @@ void ServerList::sendPlayers() // Adds the players to the serverlist auto& playerList = m_server->getPlayerList(); - for (auto& [id, player]: playerList) + for (auto& [id, player] : playerList) { - if (!player->isNC()) + if (!player->isNC() && !player->isNPCServer()) + { addPlayer(player); + player->sendPacket(CString() >> (char)PLO_SERVERLISTCONNECTED); + } } } @@ -350,7 +379,7 @@ void ServerList::handleText(const CString& data) CString tmpData = CString(",irc,privmsg,") << params[3].gtokenize() << "," << params[4].gtokenize() << "," << params[5].gtokenize(); auto& playerList = m_server->getPlayerList(); - for (auto& [id, pl]: playerList) + for (auto& [id, pl] : playerList) { if (pl->inChatChannel(channel)) { @@ -366,6 +395,10 @@ void ServerList::handleText(const CString& data) if (params.size() == 3 && params[1] == "SetRemoteIp") { m_serverRemoteIp = params[2].text(); + log::printLine(log::server, "listserver - Remote IP identified as '{}'.", m_serverRemoteIp); + + if (m_server->hasNPCServer()) + m_server->getNPCServer()->setRemoteIp(m_serverRemoteIp); } else if (params.size() >= 4) { @@ -373,7 +406,7 @@ void ServerList::handleText(const CString& data) { std::string serverName = params[3].guntokenize().text(); - for (int i = 4; i < params.size(); i++) + for (size_t i = 4; i < params.size(); i++) { params[i].guntokenizeI(); while (params[i].bytesLeft()) @@ -411,12 +444,12 @@ void ServerList::sendText(const std::vector& stringList) { CString dataPacket; dataPacket.writeGChar(SVO_SENDTEXT); - for (const auto& string: stringList) + for (const auto& string : stringList) dataPacket << string.gtokenize(); sendPacket(dataPacket); } -void ServerList::sendTextForPlayer(PlayerPtr player, const CString& data) +void ServerList::sendTextForPlayer(std::shared_ptr player, const CString& data) { assert(player != nullptr); @@ -426,19 +459,19 @@ void ServerList::sendTextForPlayer(PlayerPtr player, const CString& data) sendPacket(dataPacket); } -void ServerList::sendLoginPacketForPlayer(PlayerPtr player, const CString& password, const CString& identity) +void ServerList::sendLoginPacketForPlayer(std::shared_ptr player, const CString& password, const CString& identity) { - sendPacket(CString() >> (char)SVO_VERIACC2 >> (char)player->getAccountName().length() << player->getAccountName() >> (char)password.length() << password >> (short)player->getId() >> (char)player->getType() >> (short)identity.length() << identity); + sendPacket(CString() >> (char)SVO_VERIACC2 >> (char)player->account.name.length() << player->account.name >> (char)password.length() << password >> (short)player->getId() >> (char)player->getType() >> (short)identity.length() << identity); } void ServerList::sendServerHQ() { auto& adminsettings = m_server->getAdminSettings(); - sendPacket(CString() >> (char)SVO_SERVERHQPASS << adminsettings.getStr("hq_password")); - if (m_server->getSettings().getBool("onlystaff", false)) + sendPacket(CString() >> (char)SVO_SERVERHQPASS << adminsettings.get("hq_password").value_or(""s)); + if (m_server->getSettings().get("onlystaff").value_or(false)) sendPacket(CString() >> (char)SVO_SERVERHQLEVEL >> (char)0); else - sendPacket(CString() >> (char)SVO_SERVERHQLEVEL >> (char)adminsettings.getInt("hq_level", 1)); + sendPacket(CString() >> (char)SVO_SERVERHQLEVEL >> (char)adminsettings.get("hq_level").value_or(1)); } /* @@ -470,12 +503,12 @@ bool ServerList::parsePacket(CString& pPacket) void ServerList::msgSVI_NULL(CString& pPacket) { pPacket.setRead(0); - m_server->getServerLog().out("Unknown Serverlist Packet: %i (%s)\n", pPacket.readGUChar(), pPacket.text() + 1); + log::printLine(log::server, "Unknown Serverlist Packet: %i (%s)\n", pPacket.readGUChar(), pPacket.text() + 1); } void ServerList::msgSVI_VERIACC(CString& pPacket) { - m_server->getServerLog().out("** SVI_VERIACC is deprecated. It should not be used.\n"); + log::printLine(log::server, "** SVI_VERIACC is deprecated. It should not be used.\n"); } void ServerList::msgSVI_VERIGUILD(CString& pPacket) @@ -487,7 +520,7 @@ void ServerList::msgSVI_VERIGUILD(CString& pPacket) if (p) { // Create the prop packet. - CString prop = CString() >> (char)PLPROP_NICKNAME >> (char)nickname.length() << nickname; + CString prop = CString() >> (char)PlayerProp::NICKNAME >> (char)nickname.length() << nickname; // Assign the nickname to the player. p->setNick(nickname, true); @@ -500,24 +533,23 @@ void ServerList::msgSVI_VERIGUILD(CString& pPacket) void ServerList::msgSVI_FILESTART(CString& pPacket) { - m_server->getServerLog().out("** SVI_FILESTART is deprecated. It should not be used.\n"); + log::printLine(log::server, "** SVI_FILESTART is deprecated. It should not be used."); } void ServerList::msgSVI_FILEEND(CString& pPacket) { - m_server->getServerLog().out("** SVI_FILEEND is deprecated. It should not be used.\n"); + log::printLine(log::server, "** SVI_FILEEND is deprecated. It should not be used."); } void ServerList::msgSVI_FILEDATA(CString& pPacket) { - m_server->getServerLog().out("** SVI_FILEDATA is deprecated. It should not be used.\n"); + log::printLine(log::server, "** SVI_FILEDATA is deprecated. It should not be used."); } void ServerList::msgSVI_VERSIONOLD(CString& pPacket) { - m_server->getServerLog().out(":: You are running an old version of %s %s.\n" - ":: An updated version is available online.\n", - APP_VENDOR, APP_NAME); + log::printLine(log::server, "You are running an old version of {} {}.", APP_VENDOR, APP_NAME); + log::printLine(log::server, "An updated version is available online."); } void ServerList::msgSVI_VERSIONCURRENT(CString& pPacket) @@ -540,13 +572,13 @@ void ServerList::msgSVI_PROFILE(CString& pPacket) // Start the profile string. CString profile; - profile << p2->getProp(PLPROP_ACCOUNTNAME) << pPacket.readString(""); + profile << p2->getProp().serialize() << pPacket.readString(""); // Add the time to the profile string. - int time = p2->getOnlineTime(); - CString line = CString() << CString((int)time / 3600) << " hrs " - << CString((int)(time / 60) % 60) << " mins " - << CString((int)time % 60) << " secs"; + auto time = p2->account.onlineSeconds; + CString line = CString() << CString((uint32_t)time / 3600) << " hrs " + << CString((uint32_t)(time / 60) % 60) << " mins " + << CString((uint32_t)time % 60) << " secs"; profile >> (char)line.length() << line; // Do the old profile method for the old clients. @@ -554,114 +586,105 @@ void ServerList::msgSVI_PROFILE(CString& pPacket) { CString val; - val = CString((int)p2->getKills()); + val = CString((int)p2->account.kills); profile >> (char)val.length() << val; - val = CString((int)p2->getDeaths()); + val = CString((int)p2->account.deaths); profile >> (char)val.length() << val; - val = CString((int)p2->getProp(PLPROP_MAXPOWER).readGUChar()); + val = CString((int)p2->getProp().value); profile >> (char)val.length() << val; - int rating = p2->getProp(PLPROP_RATING).readGUInt(); - val = CString((int)((rating >> 9) & 0xFFF)) << "/" << CString((int)(rating & 0x1FF)); + auto rating = p2->getProp(); + val = CString((int)rating.rating) << "/" << CString((int)rating.deviation); profile >> (char)val.length() << val; - val = CString((int)p2->getProp(PLPROP_ALIGNMENT).readGUChar()); + val = CString((int)p2->getProp().value); profile >> (char)val.length() << val; - val = CString((int)p2->getProp(PLPROP_RUPEESCOUNT).readGUInt()); + val = CString((int)p2->getProp().value); profile >> (char)val.length() << val; - val = CString((int)(p2->getProp(PLPROP_SWORDPOWER).readGUChar() - 30)); + val = CString((int)p2->getProp().power.value_or(1)); profile >> (char)val.length() << val; - bool canSpin = ((p2->getProp(PLPROP_STATUS).readGUChar() & PLSTATUS_HASSPIN) != 0 ? true : false); - if (canSpin) val = "true"; - else - val = "false"; + bool canSpin = ((p2->getProp().value & PLSTATUS_HASSPIN) != 0 ? true : false); + val = (canSpin) ? "true" : "false"; profile >> (char)val.length() << val; } else if (!p2->isNPCServer()) { // Add all the specified variables to the profile string. - CString profileVars = m_server->getSettings().getStr("profilevars"); - if (profileVars.length() != 0) + for (const auto& profilevar : m_server->cached.playerProfileVariables.value.value()) { - std::vector vars = profileVars.tokenize(","); - for (std::vector::iterator i = vars.begin(); i != vars.end(); ++i) + auto tokens = string::splitToVectorByString(profilevar, ":="sv); + if (tokens.size() != 2) + continue; + + CString name = string::trim(tokens[0]); + CString val = string::trim(tokens[1]); + + // Built-in values. + if (val == "playerkills") + val = CString(p2->account.kills); + else if (val == "playerdeaths") + val = CString(p2->account.deaths); + else if (val == "playerfullhearts") + val = CString(p2->getProp().value); + else if (val == "playerrating") { - CString name = i->readString(":=").trim(); - CString val = i->readString("").trim(); - - // Built-in values. - if (val == "playerkills") - val = CString((unsigned int)(p2->getKills())); - else if (val == "playerdeaths") - val = CString((unsigned int)(p2->getDeaths())); - else if (val == "playerfullhearts") - val = CString((int)p2->getProp(PLPROP_MAXPOWER).readGUChar()); - else if (val == "playerrating") - { - int rating = p2->getProp(PLPROP_RATING).readGUInt(); - val = CString((int)((rating >> 9) & 0xFFF)) << "/" << CString((int)(rating & 0x1FF)); - } - else if (val == "playerap") - val = CString((int)p2->getProp(PLPROP_ALIGNMENT).readGChar()); - else if (val == "playerrupees") - val = CString((int)p2->getProp(PLPROP_RUPEESCOUNT).readGUInt()); - else if (val == "playerswordpower") - { - char sp = p2->getProp(PLPROP_SWORDPOWER).readGChar(); - if (sp > 4) sp -= 30; - val = CString((int)sp); - } - else if (val == "canspin") - val = ((p2->getProp(PLPROP_STATUS).readGUChar() & PLSTATUS_HASSPIN) ? "true" : "false"); - else if (val == "playerhearts") - { - unsigned char power = p2->getProp(PLPROP_CURPOWER).readGUChar(); - val = CString((int)(power / 2)); - if (power % 2 == 1) val << ".5"; - } - else if (val == "playerdarts") - val = CString((int)p2->getProp(PLPROP_ARROWSCOUNT).readGUChar()); - else if (val == "playerbombs") - val = CString((int)p2->getProp(PLPROP_BOMBSCOUNT).readGUChar()); - else if (val == "playermp") - val = CString((int)p2->getProp(PLPROP_MAGICPOINTS).readGUChar()); - else if (val == "playershieldpower") - { - char sp = p2->getProp(PLPROP_SHIELDPOWER).readGChar(); - if (sp > 3) sp -= 10; - val = CString((int)sp); - } - else if (val == "playerglovepower") - val = CString((int)p2->getProp(PLPROP_GLOVEPOWER).readGUChar()); - else + auto rating = p2->getProp(); + val = CString(rating.rating) << "/" << CString(rating.deviation); + } + else if (val == "playerap") + val = CString(p2->getProp().value); + else if (val == "playerrupees") + val = CString(p2->getProp().value); + else if (val == "playerswordpower") + val = CString(p2->getProp().power.value_or(1)); + else if (val == "canspin") + val = ((p2->getProp().value & PLSTATUS_HASSPIN) ? "true" : "false"); + else if (val == "playerhearts") + { + auto power = p2->getProp().value; + val = CString(power / 2); + if (power % 2 == 1) val << ".5"; + } + else if (val == "playerdarts") + val = CString(p2->getProp().value); + else if (val == "playerbombs") + val = CString(p2->getProp().value); + else if (val == "playermp") + val = CString(p2->getProp().value); + else if (val == "playershieldpower") + val = CString(p2->getProp().power.value_or(1)); + else if (val == "playerglovepower") + val = CString(p2->getProp().value); + else + { + // Find if String-Array + int pos[3] = { 0, 0, 0 }; + pos[0] = val.findl('{'); + pos[1] = val.find('}', pos[0]); + pos[2] = (pos[0] >= 0 && pos[1] > 0 ? strtoint(val.subString(pos[0] + 1, pos[1] - 1)) : -1); + + // Find Flag Name / Value + CString flagName = val.subString(0, pos[0]); + auto flagMaybe = p2->account.variables.get(flagName.toStringView()); + if (auto flag = flagMaybe.lock(); flag != nullptr) + val = flag->get().value_or(std::string{}); + + // If String-Array, Get Index + if (pos[2] >= 0) { - // Find if String-Array - int pos[3] = { 0, 0, 0 }; - pos[0] = val.findl('{'); - pos[1] = val.find('}', pos[0]); - pos[2] = (pos[0] >= 0 && pos[1] > 0 ? strtoint(val.subString(pos[0] + 1, pos[1] - 1)) : -1); - - // Find Flag Name / Value - CString flagName = val.subString(0, pos[0]); - val = p2->getFlag(flagName.text()); - - // If String-Array, Get Index - if (pos[2] >= 0) - { - std::vector temp = val.guntokenize().tokenize("\n"); - if ((int)temp.size() > pos[2]) - val = temp[pos[2]]; - } + std::vector temp = val.guntokenize().tokenize("\n"); + if ((int)temp.size() > pos[2]) + val = temp[pos[2]]; } - - // Add it to the profile now. - profile >> (char)(name.length() + val.length() + 2) << name << ":=" << val; } + + // Add it to the profile now. + profile >> (char)(name.length() + val.length() + 2) << name << ":=" << val; } } @@ -671,14 +694,14 @@ void ServerList::msgSVI_PROFILE(CString& pPacket) void ServerList::msgSVI_ERRMSG(CString& pPacket) { - m_server->getServerLog().out(":: %s - [Error] %s\n", m_socket.getDescription(), pPacket.readString("").text()); + log::printLine(log::server, "{} - [Error] {}", m_socket.getDescription(), pPacket.readString("").text()); } void ServerList::msgSVI_VERIACC2(CString& pPacket) { CString account = pPacket.readChars(pPacket.readGUChar()); unsigned short id = pPacket.readGUShort(); - unsigned char type = pPacket.readGUChar(); + [[maybe_unused]] unsigned char type = pPacket.readGUChar(); CString message = pPacket.readString(""); // Get the player. @@ -686,13 +709,13 @@ void ServerList::msgSVI_VERIACC2(CString& pPacket) if (player == nullptr) return; // Overwrite the player's account name with the one from the listserver. - player->setAccountName(account); + player->account.name = account.toString(); // If we did not get the success message, inform the client of his failure. if (message != "SUCCESS") { player->sendPacket(CString() >> (char)PLO_DISCMESSAGE << message); - player->setLoadOnly(true); // Prevent saving of the account. + player->account.loadOnly = true; // Prevent saving of the account. player->disconnect(); return; } @@ -701,24 +724,24 @@ void ServerList::msgSVI_VERIACC2(CString& pPacket) if (player->sendLogin() == false) { //player->sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Failed to send login information."); - player->setLoadOnly(true); // Prevent saving of the account. + player->account.loadOnly = true; // Prevent saving of the account. player->disconnect(); } } void ServerList::msgSVI_FILESTART2(CString& pPacket) { - m_server->getServerLog().out("** SVI_FILESTART2 is deprecated. It should not be used.\n"); + log::printLine(log::server, "** SVI_FILESTART2 is deprecated. It should not be used.\n"); } void ServerList::msgSVI_FILEDATA2(CString& pPacket) { - m_server->getServerLog().out("** SVI_FILEDATA2 is deprecated. It should not be used.\n"); + log::printLine(log::server, "** SVI_FILEDATA2 is deprecated. It should not be used.\n"); } void ServerList::msgSVI_FILEEND2(CString& pPacket) { - m_server->getServerLog().out("** SVI_FILEEND2 is deprecated. It should not be used.\n"); + log::printLine(log::server, "** SVI_FILEEND2 is deprecated. It should not be used.\n"); } void ServerList::msgSVI_PING(CString& pPacket) @@ -736,37 +759,55 @@ void ServerList::msgSVI_RAWDATA(CString& pPacket) void ServerList::msgSVI_FILESTART3(CString& pPacket) { unsigned char pTy = pPacket.readGUChar(); - CString blank, filename = CString() << "world/global/"; + std::filesystem::path filename{ "world/global/" }; + CString blank; switch (pTy) { case SVF_HEAD: - filename << "heads/"; + filename /= "heads"; break; case SVF_BODY: - filename << "bodies/"; + filename /= "bodies"; break; case SVF_SWORD: - filename << "swords/"; + filename /= "swords"; break; case SVF_SHIELD: - filename << "shields/"; + filename /= "shields"; break; } - filename << pPacket.readChars(pPacket.readGUChar()); - FileSystem::fixPathSeparators(filename); - blank.save(CString() << m_server->getServerPath() << filename); - m_server->getFileSystem()->addFile(filename); + filename /= std::format("{}.partial", pPacket.readChars(pPacket.readGUChar()).toString()); + blank.save(filename.string()); } void ServerList::msgSVI_FILEDATA3(CString& pPacket) { - unsigned char pTy = pPacket.readGUChar(); - CString filename = m_server->getFileSystem()->find(pPacket.readChars(pPacket.readGUChar())); - if (filename.length() == 0) return; - CString filedata; - filedata.load(filename); - filedata << pPacket.readChars(pPacket.bytesLeft()); // Read the rest of the packet. - filedata.save(filename); + [[maybe_unused]] unsigned char pTy = pPacket.readGUChar(); + fs::FileCategory category = fs::FileCategory::ALL; + switch (pTy) + { + case SVF_HEAD: + category = fs::FileCategory::HEAD; + break; + case SVF_BODY: + category = fs::FileCategory::BODY; + break; + case SVF_SWORD: + category = fs::FileCategory::SWORD; + break; + case SVF_SHIELD: + category = fs::FileCategory::SHIELD; + break; + } + + auto filename = std::format("{}.partial", pPacket.readChars(pPacket.readGUChar()).toString()); + auto fileData = m_server->getFileSystem().info(category, filename); + if (fileData == nullptr) return; + + CString data; + data.load(fileData->file.string()); + data << pPacket.readChars(pPacket.bytesLeft()); // Read the rest of the packet. + data.save(fileData->file.string()); } void ServerList::msgSVI_FILEEND3(CString& pPacket) @@ -778,35 +819,27 @@ void ServerList::msgSVI_FILEEND3(CString& pPacket) unsigned int fileLength = pPacket.readGUInt5(); CString shortName = pPacket.readString(""); - // If we have folder config enabled, we need to add the file to the appropriate - // file system. - bool foldersconfig = !m_server->getSettings().getBool("nofoldersconfig", false); - FileSystem* fileSystem = 0; - CString typeString; + fs::FileCategory category = fs::FileCategory::ALL; switch (type) { case SVF_HEAD: - typeString = "heads/"; - if (foldersconfig) fileSystem = m_server->getFileSystem(FS_HEAD); + category = fs::FileCategory::HEAD; break; case SVF_BODY: - typeString = "bodies/"; - if (foldersconfig) fileSystem = m_server->getFileSystem(FS_BODY); + category = fs::FileCategory::BODY; break; case SVF_SWORD: - typeString = "swords/"; - if (foldersconfig) fileSystem = m_server->getFileSystem(FS_SWORD); + category = fs::FileCategory::SWORD; break; case SVF_SHIELD: - typeString = "shields/"; - if (foldersconfig) fileSystem = m_server->getFileSystem(FS_SHIELD); + category = fs::FileCategory::SHIELD; break; } - CString fileName = m_server->getFileSystem()->find(shortName); - // Add the file to the filesystem. - if (fileSystem) - fileSystem->addFile(CString() << "world/global/" << typeString << shortName); + auto fileName = std::format("{}.partial", shortName.toString()); + auto fileData = m_server->getFileSystem().info(category, fileName); + if (fileData == nullptr) + return; // Uncompress the file if compressed. if (doCompress == 1) @@ -818,38 +851,45 @@ void ServerList::msgSVI_FILEEND3(CString& pPacket) } // Set the file mod time. - if (m_server->getFileSystem()->setModTime(shortName, modTime) == false) - m_server->getServerLog().out("** [WARNING] Could not set modification time on file %s\n", fileName.text()); + fileData->setModTime(clock::from_time_t(modTime)); + + // Rename the file. + auto newFileName = shortName.toString(); + std::filesystem::rename(fileData->file, fileData->file.parent_path() / newFileName); // Set the player props. // TODO(joey): Confirm if we can use ANYCLIENT instead - auto p = m_server->getPlayer(pid, PLTYPE_ANYPLAYER); - if (p) + if (auto p = m_server->getPlayer(pid, PLTYPE_ANYPLAYER); p) { + props::SetResults result; switch (type) { case SVF_HEAD: - p->setProps(CString() >> (char)PLPROP_HEADGIF >> (char)(shortName.length() + 100) << shortName, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); + result = p->setPropWith(props::SetBy::SERVER, shortName.toString()); break; case SVF_BODY: - p->setProps(CString() >> (char)PLPROP_BODYIMG >> (char)shortName.length() << shortName, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); + result = p->setPropWith(props::SetBy::SERVER, shortName.toString()); break; case SVF_SWORD: - { - CString prop = p->getProp(PLPROP_SWORDPOWER); - p->setProps(CString() >> (char)PLPROP_SWORDPOWER >> (char)prop.readGUChar() >> (char)shortName.length() << shortName, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); + result = p->setPropWith(props::SetBy::SERVER, shortName.toString()); break; - } case SVF_SHIELD: - { - CString prop = p->getProp(PLPROP_SHIELDPOWER); - p->setProps(CString() >> (char)PLPROP_SHIELDPOWER >> (char)prop.readGUChar() >> (char)shortName.length() << shortName, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); + result = p->setPropWith(props::SetBy::SERVER, shortName.toString()); break; - } } + + // Send the prop. + uint8_t propId = result.resultPropIds.front(); + CString prop = p->getProp((PlayerProp)propId)->serialize(); + if (result.resultFlags.test(props::SetResults::sendToAll)) + m_server->sendPacketToAll(CString() >> (char)PLO_OTHERPLPROPS >> (short)pid >> (char)propId << prop); + if (auto player = std::dynamic_pointer_cast(p); p && result.resultFlags.test(props::SetResults::sendToLevel)) + m_server->sendPacketToNearby(CString() >> (char)PLO_OTHERPLPROPS >> (short)pid >> (char)propId << prop, player->account.character.getGlobalPosition(), player->getLevel(), { pid }); + if (result.resultFlags.test(props::SetResults::sendToSource)) + p->sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)propId << prop); } } @@ -942,7 +982,7 @@ void ServerList::msgSVI_REQUESTTEXT(CString& pPacket) } else { - //m_server->getServerLog().out("[OUT] [RequestText] %s\n", message.text()); + //log::printLine(log::server, "[OUT] [RequestText] %s\n", message.text()); if (player->getVersion() >= CLVER_4_0211 || player->getVersion() > RCVER_1_1) player->sendPacket(CString() >> (char)PLO_SERVERTEXT << message); @@ -971,10 +1011,7 @@ void ServerList::msgSVI_PMPLAYER(CString& pPacket) CString message2 = data.readString(""); CString message3 = message2.gtokenizeI(); - CString player = CString(CString() << account << "\n" - << nick << "\n") - .gtokenizeI() - << "\n"; + CString player = CString(CString() << account << "\n" << nick << "\n").gtokenizeI() << "\n"; CString pmMessageType("\"\","); pmMessageType << "\"Private message:\","; @@ -1003,3 +1040,6 @@ void ServerList::msgSVI_ASSIGNPCID(CString& pPacket) player->setDeviceId(std::stoll(pcId.text())); } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/TriggerCommandHandlers.cpp b/server/src/TriggerCommandHandlers.cpp index 39f520900..9c2ceb488 100644 --- a/server/src/TriggerCommandHandlers.cpp +++ b/server/src/TriggerCommandHandlers.cpp @@ -1,305 +1,297 @@ -#include - -#include "NPC.h" -#include "Player.h" -#include "Server.h" -#include "Weapon.h" -#include "level/Level.h" -#include "utilities/StringUtils.h" +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// void Server::createTriggerCommands(TriggerDispatcher::Builder builder) { - auto& dispatcher = m_triggerActionDispatcher; - -#ifdef V8NPCSERVER - builder.registerCommand("serverside", [&](Player* player, std::vector& triggerData) - { - if (triggerData.size() > 1) - { - auto weaponObject = this->getWeapon(triggerData[1].toString()); - if (weaponObject != nullptr) - weaponObject->queueWeaponAction(player, utilities::retokenizeArray(triggerData, 2)); - } - - return true; - }); - - builder.registerCommand("servernpc", [&](Player* player, std::vector& triggerData) - { - if (triggerData.size() > 2) - { - auto npcObject = this->getNPCByName(triggerData[1].toString()); - if (npcObject != nullptr) - npcObject->queueNpcTrigger(triggerData[2].toString(), player, utilities::retokenizeArray(triggerData, 3)); - } - - return true; - }); -#endif - - builder.registerCommand("gr.serverlist", [&](Player* player, std::vector& triggerData) - { - auto& listServer = getServerList(); - const auto& serverList = listServer.getServerList(); - - CString actionData("clientside,-Serverlist_v4,updateservers,"); - for (auto& serverData: serverList) - actionData << CString(serverData.first).gtokenize() << "," << CString(serverData.second) << ","; - - player->sendPacket(CString() >> (char)PLO_TRIGGERACTION >> (short)0 >> (int)0 >> (char)0 >> (char)0 << actionData); - return true; - }); + //auto& dispatcher = m_triggerActionDispatcher; + + builder.registerCommand("serverside", [&](Player* player, std::vector& triggerData) + { + if (!hasNPCServer()) + return false; + + if (triggerData.size() >= 2) + { + // triggeraction 0,0,serverside,weaponname,params...; + // Triggers on a player's weapons. + if (auto weapon = getWeapon(triggerData[1]); weapon != nullptr) + weapon->scripting.events.addEvent(ScriptEventType::TRIGGERACTION, source::FromPlayer(player->getId()), "serverside"s, string::toCSV(triggerData | std::views::drop(2))); + } + return true; + }); + + builder.registerCommand("servernpc", [&](Player* player, std::vector& triggerData) + { + if (!hasNPCServer()) + return false; + + if (triggerData.size() >= 2) + { + // triggeraction 0,0,servernpc,npcname,params...; + if (auto npcServer = getNPCServer(); npcServer != nullptr) + { + if (auto npc = npcServer->getNPCByName(triggerData[1]).lock(); npc != nullptr) + npc->scripting.events.addEvent(ScriptEventType::TRIGGERACTION, source::FromPlayer(player->getId()), "serverside"s, string::toCSV(triggerData | std::views::drop(2))); + } + } + return true; + }); + + builder.registerCommand("gr.serverlist", [&](Player* player, std::vector& triggerData) + { + auto& listServer = getServerList(); + const auto& serverList = listServer.getServerList(); + + CString actionData("clientside,-Serverlist_v4,updateservers,"); + for (auto& serverData : serverList) + actionData << CString(serverData.first).gtokenize() << "," << CString(serverData.second) << ","; + + player->sendPacket(CString() >> (char)PLO_TRIGGERACTION >> (short)0 >> (int)0 >> (char)0 >> (char)0 << actionData); + return true; + }); // Weapon management - builder.registerCommand("gr.addweapon", [&](Player* player, std::vector& triggerData) - { - if (getSettings().getBool("triggerhack_weapons", false)) - { - for (auto i = 1; i < triggerData.size(); ++i) - player->addWeapon(triggerData[i].trim().toString()); - } - - return true; - }); - - builder.registerCommand("gr.deleteweapon", [&](Player* player, std::vector& triggerData) - { - if (getSettings().getBool("triggerhack_weapons", false)) - { - for (auto i = 1; i < triggerData.size(); ++i) - player->deleteWeapon(triggerData[i].trim().toString()); - } - - return true; - }); + builder.registerCommand("gr.addweapon", [&](Player* player, std::vector& triggerData) + { + if (!cached.enableTriggerhackWeapons.getValue()) + return false; + + for (size_t i = 1; i < triggerData.size(); ++i) + player->addWeapon(string::trim(triggerData[i])); + return true; + }); + + builder.registerCommand("gr.deleteweapon", [&](Player* player, std::vector& triggerData) + { + if (!cached.enableTriggerhackWeapons.getValue()) + return false; + + for (size_t i = 1; i < triggerData.size(); ++i) + player->deleteWeapon(string::trim(triggerData[i])); + return true; + }); // Guild management - builder.registerCommand("gr.addguildmember", [&](Player* player, std::vector& triggerData) - { - if (getSettings().getBool("triggerhack_guilds", false)) - { - CString guild, account, nick; - if (triggerData.size() > 1) guild = triggerData[1]; - if (triggerData.size() > 2) account = triggerData[2]; - if (triggerData.size() > 3) nick = triggerData[3]; - - if (!guild.isEmpty() && !account.isEmpty()) - { - // Read the guild list. - FileSystem guildFS; - guildFS.addDir("guilds"); - CString guildList = guildFS.load(CString() << "guild" << guild << ".txt"); - - if (guildList.find(account) == -1) - { - if (guildList[guildList.length() - 1] != '\n') guildList << "\n"; - guildList << account; - if (!nick.isEmpty()) guildList << ":" << nick; - - guildList.save(CString() << getServerPath() << "guilds/guild" << guild << ".txt"); - } - } - } - - return true; - }); - - builder.registerCommand("gr.removeguildmember", [&](Player* player, std::vector& triggerData) - { - if (getSettings().getBool("triggerhack_guilds", false)) - { - CString guild, account; - if (triggerData.size() > 1) guild = triggerData[1]; - if (triggerData.size() > 2) account = triggerData[2]; - - if (!guild.isEmpty() && !account.isEmpty()) - { - // Read the guild list. - FileSystem guildFS; - guildFS.addDir("guilds"); - CString guildList = guildFS.load(CString() << "guild" << guild << ".txt"); - - if (guildList.find(account) != -1) - { - int pos = guildList.find(account); - int length = guildList.find("\n", pos) - pos; - if (length < 0) length = -1; - else - ++length; - - guildList.removeI(pos, length); - guildList.save(CString() << getServerPath() << "guilds/guild" << guild << ".txt"); - } - } - } - - return true; - }); - - builder.registerCommand("gr.removeguild", [&](Player* player, std::vector& triggerData) - { - if (getSettings().getBool("triggerhack_guilds", false)) - { - CString guild; - if (triggerData.size() > 1) guild = triggerData[1]; - - if (!guild.isEmpty()) - { - // Read the guild list. - FileSystem guildFS; - guildFS.addDir("guilds"); - CString path = guildFS.find(CString() << "guild" << guild << ".txt"); - - // Remove the guild. - remove(path.text()); - - // Remove the guild from all players. - for (auto& [pid, p]: getPlayerList()) - { - if (p->getGuild() == guild) - { - CString nick = p->getNickname(); - p->setNick(nick.readString("(").trimI()); - p->sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)PLPROP_NICKNAME << p->getProp(PLPROP_NICKNAME)); - sendPacketToAll(CString() >> (char)PLO_OTHERPLPROPS >> (short)p->getId() >> (char)PLPROP_NICKNAME << p->getProp(PLPROP_NICKNAME), { pid }); - } - } - } - } - - return true; - }); - - builder.registerCommand("gr.setguild", [&](Player* player, std::vector& triggerData) - { - if (getSettings().getBool("triggerhack_guilds", false)) - { - CString guild, account; - if (triggerData.size() > 1) guild = triggerData[1]; - if (triggerData.size() > 2) account = triggerData[2]; - - if (!guild.isEmpty()) - { - Player* p = player; - if (!account.isEmpty()) p = getPlayer(account, PLTYPE_ANYCLIENT).get(); - if (p) - { - CString nick = p->getNickname(); - p->setNick(CString() << nick.readString("(").trimI() << " (" << guild << ")", true); - p->sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)PLPROP_NICKNAME >> (char)p->getNickname().length() << p->getNickname()); - sendPacketToAll(CString() >> (char)PLO_OTHERPLPROPS >> (short)p->getId() >> (char)PLPROP_NICKNAME >> (char)p->getNickname().length() << p->getNickname(), { p->getId() }); - } - } - } - - return true; - }); + builder.registerCommand("gr.addguildmember", [&](Player* player, std::vector& triggerData) + { + if (!cached.enableTriggerhackGuilds.getValue()) + return false; + + CString guild, account, nick; + if (triggerData.size() > 1) guild = triggerData[1]; + if (triggerData.size() > 2) account = triggerData[2]; + if (triggerData.size() > 3) nick = triggerData[3]; + + if (!guild.isEmpty() && !account.isEmpty()) + { + auto guildManager = BabyDI::Get(); + guildManager->addPlayerToGuild(guild, account, nick); + } + + return true; + }); + + builder.registerCommand("gr.removeguildmember", [&](Player* player, std::vector& triggerData) + { + if (!cached.enableTriggerhackGuilds.getValue()) + return false; + + CString guild, account, nickName; + if (triggerData.size() > 1) guild = triggerData[1]; + if (triggerData.size() > 2) account = triggerData[2]; + if (triggerData.size() > 3) nickName = triggerData[3]; + + if (!guild.isEmpty() && !account.isEmpty()) + { + auto guildManager = BabyDI::Get(); + if (nickName.isEmpty()) + guildManager->removePlayerEntirelyFromGuild(guild, account); + else guildManager->removePlayerFromGuild(guild, account, nickName); + } + + return true; + }); + + builder.registerCommand("gr.removeguild", [&](Player* player, std::vector& triggerData) + { + if (!cached.enableTriggerhackGuilds.getValue()) + return false; + + CString guild; + if (triggerData.size() > 1) guild = triggerData[1]; + + if (!guild.isEmpty()) + { + auto guildManager = BabyDI::Get(); + guildManager->deleteGuild(guild); + + // Remove the guild from all players. + for (auto& [pid, p] : getPlayerList()) + { + if (p->getGuild() == guild) + { + CString nick = p->account.character.nickName; + p->setNick(nick.readString("(").trimI()); + p->sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)PlayerProp::NICKNAME << p->getProp().serialize()); + sendPacketToAll(CString() >> (char)PLO_OTHERPLPROPS >> (short)p->getId() >> (char)PlayerProp::NICKNAME << p->getProp().serialize(), { pid }); + } + } + } + return true; + }); + + builder.registerCommand("gr.setguild", [&](Player* player, std::vector& triggerData) + { + if (!cached.enableTriggerhackGuilds.getValue()) + return false; + + CString guild, account; + if (triggerData.size() > 1) guild = triggerData[1]; + if (triggerData.size() > 2) account = triggerData[2]; + + if (!guild.isEmpty()) + { + Player* p = player; + if (!account.isEmpty()) p = getPlayer(account, PLTYPE_ANYCLIENT).get(); + if (p) + { + CString nick = p->account.character.nickName; + p->setNick(CString() << nick.readString("(").trimI() << " (" << guild << ")", true); + p->sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)PlayerProp::NICKNAME >> (char)p->account.character.nickName.length() << p->account.character.nickName); + sendPacketToAll(CString() >> (char)PLO_OTHERPLPROPS >> (short)p->getId() >> (char)PlayerProp::NICKNAME >> (char)p->account.character.nickName.length() << p->account.character.nickName, { p->getId() }); + } + } + return true; + }); // Group levels - builder.registerCommand("gr.setgroup", [&](Player* player, std::vector& triggerData) - { - if (getSettings().getBool("triggerhack_groups", true) && triggerData.size() == 2) - { - player->setGroup(triggerData[1]); - } - - return true; - }); - - builder.registerCommand("gr.setlevelgroup", [&](Player* player, std::vector& triggerData) - { - if (getSettings().getBool("triggerhack_groups", true) && triggerData.size() == 2) - { - const auto& playerList = player->getLevel()->getPlayers(); - for (auto& id: playerList) - { - auto pl = getPlayer(id); - pl->setGroup(triggerData[1]); - } - } - - return true; - }); - - builder.registerCommand("gr.setplayergroup", [&](Player* player, std::vector& triggerData) - { - if (getSettings().getBool("triggerhack_groups", true) && triggerData.size() == 3) - { - auto player = getPlayer(triggerData[1], PLTYPE_ANYCLIENT); - player->setGroup(triggerData[2]); - } - - return true; - }); + builder.registerCommand("gr.setgroup", [&](Player* player, std::vector& triggerData) + { + if (auto client = dynamic_cast(player); cached.enableTriggerhackGroups.getValue() && client != nullptr) + { + client->setGroup(triggerData.size() >= 2 ? triggerData[1] : ""s); + return true; + } + return false; + }); + + builder.registerCommand("gr.setlevelgroup", [&](Player* player, std::vector& triggerData) + { + if (auto client = dynamic_cast(player); cached.enableTriggerhackGroups.getValue() && client != nullptr) + { + for (const auto& id : client->getLevel()->getPlayers()) + { + if (auto pl = getPlayer(id); pl != nullptr) + pl->setGroup(triggerData.size() >= 2 ? triggerData[1] : ""s); + } + return true; + } + return false; + }); + + builder.registerCommand("gr.setplayergroup", [&](Player* player, std::vector& triggerData) + { + if (cached.enableTriggerhackGroups.getValue() && triggerData.size() >= 2) + { + if (auto client = getPlayer(triggerData[1], PLTYPE_ANYCLIENT); client != nullptr) + client->setGroup(triggerData.size() >= 3 ? triggerData[2] : ""s); + return true; + } + return false; + }); // RC triggers - builder.registerCommand("gr.rcchat", [&](Player* player, std::vector& triggerData) - { - if (getSettings().getBool("triggerhack_rc", false)) - { - auto p = getPlayer(player->getId()); + builder.registerCommand("gr.rcchat", [&](Player* player, std::vector& triggerData) + { + if (!cached.enableTriggerhackRC.getValue()) + return false; - CString msg; - for (auto i = 1; i < triggerData.size(); ++i) - msg << triggerData[i] << ","; - sendToRC(msg, p); - } + auto p = getPlayer(player->getId()); - return true; - }); + CString msg; + for (size_t i = 1; i < triggerData.size(); ++i) + msg << triggerData[i] << ","; + sendToRC(msg, p); + return true; + }); // Level triggers - builder.registerCommand("gr.npc.move", [&](Player* player, std::vector& triggerData) - { - if (getSettings().getBool("triggerhack_levels", false) && triggerData.size() == 6) - { - unsigned int id = strtoint(triggerData[1]); - int dx = strtoint(triggerData[2]); - int dy = strtoint(triggerData[3]); - float duration = (float)strtofloat(triggerData[4]); - int options = strtoint(triggerData[5]); - - auto npc = getNPC(id); - if (npc) - { - CString packet; - packet >> (char)(npc->getX() / 8.0f) >> (char)(npc->getY() / 8.0f); - packet >> (char)((dx * 2) + 100) >> (char)((dy * 2) + 100); - packet >> (short)(duration / 0.05f); - packet >> (char)options; - sendPacketToLevelOnlyGmapArea(CString() >> (char)PLO_MOVE >> (int)id << packet, getPlayer(player->getId())); - - npc->setX(npc->getX() + dx * 16); - npc->setY(npc->getY() + dy * 16); - //npc->setProps(CString() >> (char)NPCPROP_X >> (char)((npc->getX() + dx) * 2) >> (char)NPCPROP_Y >> (char)((npc->getY() + dy) * 2)); - } - } - - return true; - }); - - builder.registerCommand("gr.npc.setpos", [&](Player* player, std::vector& triggerData) - { - if (getSettings().getBool("triggerhack_levels", false) && triggerData.size() == 4) - { - unsigned int id = strtoint(triggerData[1]); - float x = (float)strtofloat(triggerData[2]); - float y = (float)strtofloat(triggerData[3]); - - auto npc = getNPC(id); - if (npc) - { - npc->setX(int(x * 16.0)); - npc->setY(int(y * 16.0)); - - // Send the prop packet to the level. - CString packet; - packet >> (char)NPCPROP_X >> (char)(x * 2.0f); - packet >> (char)NPCPROP_Y >> (char)(y * 2.0f); - sendPacketToLevelOnlyGmapArea(CString() >> (char)PLO_NPCPROPS >> (int)id << packet, getPlayer(player->getId())); - } - } - - return true; - }); + builder.registerCommand("gr.npc.move", [&](Player* player, std::vector& triggerData) + { + if (!cached.enableTriggerhackLevels.getValue() || triggerData.size() != 6) + return false; + + unsigned int id = string::toNumber(triggerData[1]); + int dx = string::toNumber(triggerData[2]); + int dy = string::toNumber(triggerData[3]); + float duration = string::toFloat(triggerData[4]); + int options = string::toNumber(triggerData[5]); + + auto npc = getNPC(id); + if (npc) + { + CString packet; + packet >> (char)(npc->character.localPixelX / 8.0f) >> (char)(npc->character.localPixelY / 8.0f); + packet >> (char)((dx * 2) + 100) >> (char)((dy * 2) + 100); + packet >> (short)(duration / 0.05f); + packet >> (char)options; + sendPacketToNearby(CString() >> (char)PLO_MOVE >> (int)id << packet, npc->character.getGlobalPosition(), npc->getLevel()); + + npc->character.localPixelX += dx * 16; + npc->character.localPixelY += dy * 16; + //npc->setPropsFromPacket(CString() >> (char)NPCPROP_X >> (char)((npc->getX() + dx) * 2) >> (char)NPCPROP_Y >> (char)((npc->getY() + dy) * 2)); + } + return true; + }); + + builder.registerCommand("gr.npc.setpos", [&](Player* player, std::vector& triggerData) + { + if (!cached.enableTriggerhackLevels.getValue() || triggerData.size() != 4) + return false; + + unsigned int id = string::toNumber(triggerData[1]); + float x = string::toFloat(triggerData[2]); + float y = string::toFloat(triggerData[3]); + + auto npc = getNPC(id); + if (npc) + { + npc->character.localPixelX = static_cast(x * 16.0); + npc->character.localPixelY = static_cast(y * 16.0); + + // Send the prop packet to the level. + CString packet; + packet >> (char)NPCProp::X >> (char)(x * 2.0f); + packet >> (char)NPCProp::Y >> (char)(y * 2.0f); + sendPacketToNearby(CString() >> (char)PLO_NPCPROPS >> (int)id << packet, npc->character.getGlobalPosition(), npc->getLevel()); + } + return true; + }); } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/UpdatePackage.cpp b/server/src/UpdatePackage.cpp index 34b4e1897..5628dac11 100644 --- a/server/src/UpdatePackage.cpp +++ b/server/src/UpdatePackage.cpp @@ -1,18 +1,27 @@ -#include - +#include #include +#include +#include + +#include -#include "FileSystem.h" -#include "Server.h" -#include "UpdatePackage.h" +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// std::optional UpdatePackage::load(Server* const server, const std::string& name) { - auto fileSystem = server->getFileSystem(); + auto& fileSystem = server->getFileSystem(); // Search for the file in the filesystem, and load the contents - auto fileContents = fileSystem->load(name); - if (fileContents.isEmpty()) + auto fileData = fileSystem.info(fs::FileCategory::FILE, name); + if (fileData == nullptr) return std::nullopt; // Calculate the checksum for the gupd file @@ -28,18 +37,21 @@ void UpdatePackage::reload(Server* const server) this->m_packageSize = 0; this->m_fileList.clear(); - auto fileSystem = server->getFileSystem(); + auto& fileSystem = server->getFileSystem(); // Search for the file in the filesystem, and load the contents - auto fileContents = fileSystem->load(this->m_packageName.c_str()); - if (fileContents.isEmpty()) + auto fileData = fileSystem.info(fs::FileCategory::FILE, m_packageName); + if (fileData == nullptr) return; + CString fileContents; + fileContents.load(fileData->file.string()); + // Calculate the checksum for the gupd file this->m_checksum = calculateCrc32Checksum(fileContents); // Calculate the checksum and filesize for each file referenced in the package - for (const auto packageLines = fileContents.tokenize("\n"); const auto& line: packageLines) + for (const auto packageLines = fileContents.tokenize("\n"); const auto& line : packageLines) { // Line should be in the format of FILE levels/body.png if (const auto startPos = line.findi("FILE"); startPos == 0) @@ -47,22 +59,26 @@ void UpdatePackage::reload(Server* const server) std::string filePath = line.subString(4).trim().toString(); std::string baseFileName = std::filesystem::path(filePath).filename().string(); - CString updateFileData = fileSystem->load(baseFileName); - // File was not found in the filesystem - if (updateFileData.isEmpty()) + fileData = fileSystem.info(fs::FileCategory::FILE, baseFileName); + if (fileData == nullptr) { server->sendToRC(CString() << "[Server]: Unable to find file '" << baseFileName << "' in package '" << m_packageName << "'"); continue; } + CString updateFileData; + updateFileData.load(fileData->file.string()); uint32_t fileLength(updateFileData.length()); this->m_fileList.emplace(baseFileName, FileEntry{ - .size = fileLength, - .checksum = calculateCrc32Checksum(updateFileData)}); + .size = fileLength, + .checksum = calculateCrc32Checksum(updateFileData) }); this->m_packageSize += fileLength; } } } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/Weapon.cpp b/server/src/Weapon.cpp deleted file mode 100644 index 818bbfc43..000000000 --- a/server/src/Weapon.cpp +++ /dev/null @@ -1,332 +0,0 @@ -#include - -#include -#include - -// GS2 Compiler includes -#include - -#include "NPC.h" -#include "Server.h" -#include "Weapon.h" -#include "level/LevelItem.h" -#include "scripting/SourceCode.h" - -#ifdef V8NPCSERVER - #include "Player.h" -#endif - -// -- Constructor: Default Weapons -- // -Weapon::Weapon(LevelItemType pId) - : m_modTime(0), m_weaponDefault(pId) -#ifdef V8NPCSERVER - , - m_scriptExecutionContext(m_server->getScriptEngine()) -#endif -{ - m_weaponName = LevelItem::getItemName(m_weaponDefault); -} - -// -- Constructor: Weapon Script -- // -Weapon::Weapon(std::string pName, std::string pImage, std::string pScript, const time_t pModTime, bool pSaveWeapon) - : m_weaponName(std::move(pName)), m_modTime(pModTime), m_weaponDefault(LevelItemType::INVALID) -#ifdef V8NPCSERVER - , - m_scriptExecutionContext(m_server->getScriptEngine()) -#endif -{ - // Update Weapon - this->updateWeapon(std::move(pImage), std::move(pScript), pModTime, pSaveWeapon); -} - -Weapon::~Weapon() -{ -#ifdef V8NPCSERVER - freeScriptResources(); -#endif -} - -// -- Function: Load Weapon -- // -std::shared_ptr Weapon::loadWeapon(const CString& pWeapon) -{ - // File Path - CString fileName = m_server->getServerPath() << "weapons" << FileSystem::getPathSeparator() << pWeapon; - - // Load File - CString fileData; - if (!fileData.load(fileName)) - return nullptr; - - fileData.removeAllI("\r"); - - // Grab some information. - bool has_scriptend = fileData.find("SCRIPTEND") != -1; - bool found_scriptend = false; - - // Parse header - CString headerLine = fileData.readString("\n"); - if (headerLine != "GRAWP001") - return nullptr; - - // Definitions - CString byteCodeData; - std::string byteCodeFile, weaponImage, weaponName, weaponScript; - - // Parse File - while (fileData.bytesLeft()) - { - CString curLine = fileData.readString("\n"); - - // Find Command - CString curCommand = curLine.readString(); - - // Parse Line - if (curCommand == "REALNAME") - weaponName = curLine.readString("").toString(); - else if (curCommand == "IMAGE") - weaponImage = curLine.readString("").toString(); - else if (curCommand == "BYTECODE") - { - CString fileName = curLine.readString(""); - - byteCodeData.load(m_server->getServerPath() << "weapon_bytecode/" << fileName); - if (!byteCodeData.isEmpty()) - byteCodeFile = fileName.toString(); - } - else if (curCommand == "SCRIPT") - { - do { - curLine = fileData.readString("\n"); - if (curLine == "SCRIPTEND") - { - found_scriptend = true; - break; - } - - weaponScript.append(curLine.text()).append("\n"); - } - while (fileData.bytesLeft()); - } - } - - // Valid Weapon Name? - if (weaponName.empty()) - return nullptr; - - // Give a warning if our weapon was malformed. - if (has_scriptend && !found_scriptend) - { - m_server->getServerLog().out("WARNING: Weapon %s is malformed.\n", weaponName.c_str()); - m_server->getServerLog().out("SCRIPTEND needs to be on its own line.\n"); - } - - // Give a warning if both a script and a bytecode was found. - if (!weaponScript.empty() && !byteCodeData.isEmpty()) - { - m_server->getServerLog().out("WARNING: Weapon %s includes both script and bytecode. Using bytecode.\n", weaponName.c_str()); - weaponScript.clear(); - } - - auto weapon = std::make_shared(weaponName, weaponImage, weaponScript, 0); - if (!byteCodeData.isEmpty()) - { - weapon->m_bytecode = CString(std::move(byteCodeData)); - weapon->m_bytecodeFile = std::move(byteCodeFile); - } - - return weapon; -} - -// -- Function: Save Weapon -- // -bool Weapon::saveWeapon() -{ - // Don't save default weapons / empty weapons - if (this->isDefault() || m_weaponName.empty()) - return false; - - // If the bytecode filename is set, the weapon is treated as read-only so it can't be saved - if (!m_bytecodeFile.empty()) - return false; - - // Prevent the loading/saving of filenames with illegal characters. - CString name = m_weaponName; - name.replaceAllI("\\", "_"); - name.replaceAllI("/", "_"); - name.replaceAllI("*", "@"); - name.replaceAllI(":", ";"); - name.replaceAllI("?", "!"); - CString filename = m_server->getServerPath() << "weapons" << FileSystem::getPathSeparator() << "weapon" << name << ".txt"; - - // Write the File. - CString output = "GRAWP001\r\n"; - output << "REALNAME " << m_weaponName << "\r\n"; - output << "IMAGE " << m_weaponImage << "\r\n"; - - if (m_source) - { - output << "SCRIPT\r\n"; - output << CString(m_source.getSource()).replaceAll("\n", "\r\n"); - - // Append a new line to the end of the script if one doesn't exist. - if (m_source.getSource().back() != '\n') - output << "\r\n"; - - output << "SCRIPTEND\r\n"; - } - - // Save it. - return output.save(filename); -} - -// -- Function: Get Player Packet -- // -CString Weapon::getWeaponPacket(int clientVersion) const -{ - if (this->isDefault()) - return CString() >> (char)PLO_DEFAULTWEAPON >> (char)m_weaponDefault; - - CString weaponPacket; - weaponPacket >> (char)PLO_NPCWEAPONADD >> (char)m_weaponName.length() << m_weaponName >> (char)NPCPROP_IMAGE >> (char)m_weaponImage.length() << m_weaponImage; - - // GS2 is available for v4+ - if (clientVersion >= CLVER_4_0211) - { - if (!m_bytecode.isEmpty()) - { - weaponPacket >> (char)NPCPROP_CLASS >> (short)0 << "\n"; - - CString b = m_bytecode; - CString header = b.readChars(b.readGUShort()); - - // Get the mod time and send packet 197. - weaponPacket >> (char)PLO_UNKNOWN197 << header << "," >> (long long)time(0) << "\n"; - return weaponPacket; - } - - // GS1 is disabled for > 5.0.0.7 - if (clientVersion > CLVER_5_07) - return weaponPacket; - } - - weaponPacket >> (char)NPCPROP_SCRIPT >> (short)m_formattedClientGS1.length() << m_formattedClientGS1; - return weaponPacket; -} - -// -- Function: Update Weapon Image/Script -- // -void Weapon::updateWeapon(std::string pImage, std::string pCode, const time_t pModTime, bool pSaveWeapon) -{ -#ifdef V8NPCSERVER - // Clear script function - if (m_source || m_scriptExecutionContext.hasActions()) - freeScriptResources(); -#endif - - bool gs2default = m_server->getSettings().getBool("gs2default", false); - - m_source = SourceCode{ std::move(pCode), gs2default }; - m_weaponImage = std::move(pImage); - setModTime(pModTime == 0 ? time(0) : pModTime); - -#ifdef V8NPCSERVER - // Compile and execute the script. - ScriptEngine* scriptEngine = m_server->getScriptEngine(); - bool executed = scriptEngine->executeWeapon(this); - if (executed) - { - SCRIPTENV_D("WEAPON SCRIPT COMPILED\n"); - - if (!m_source.getServerSide().empty()) - { - m_scriptExecutionContext.addAction(scriptEngine->createAction("weapon.created", getScriptObject())); - scriptEngine->registerWeaponUpdate(this); - } - } - else - SCRIPTENV_D("Could not compile weapon script\n"); -#endif - - // Clear any GS1 scripts/GS2 bytecode - m_bytecode.clear(); - m_formattedClientGS1.clear(); - - // Compile GS2 code - if (!m_source.getClientGS2().empty()) - { - // Compile gs2 code - m_server->compileGS2Script(this, [this](const CompilerResponse& response) - { - if (response.success) - { - // these should be sent for compilation right after - m_joinedClasses = { response.joinedClasses.begin(), response.joinedClasses.end() }; - - auto bytecodeWithHeader = GS2Context::CreateHeader(response.bytecode, "weapon", m_weaponName, true); - m_bytecode.clear(bytecodeWithHeader.length()); - m_bytecode.write((const char*)bytecodeWithHeader.buffer(), static_cast(bytecodeWithHeader.length())); - } - }); - } - - auto gs1Script = m_source.getClientGS1(); - if (!gs1Script.empty()) - setClientScript(std::string{ gs1Script }); - - // Save Weapon - if (pSaveWeapon) - saveWeapon(); -} - -void Weapon::setClientScript(const CString& pScript) -{ - // Remove any comments in the code - CString formattedScript = removeComments(pScript); - - // Extra padding incase we need to add //#CLIENTSIDE to the script - m_formattedClientGS1.clear(static_cast(formattedScript.length()) + 14); - - if (formattedScript.find("//#CLIENTSIDE") != 0) - { - m_formattedClientGS1 << "//#CLIENTSIDE" - << "\xa7"; - } - - // Split code into tokens, trim each line, and use the clientside line ending '\xa7' - std::vector code = formattedScript.tokenize("\n"); - for (auto& it: code) - m_formattedClientGS1 << it.trim() << "\xa7"; -} - -#ifdef V8NPCSERVER - -void Weapon::freeScriptResources() -{ - ScriptEngine* scriptEngine = m_server->getScriptEngine(); - - scriptEngine->clearCache(m_source.getServerSide()); - - // Clear any queued actions - if (m_scriptExecutionContext.hasActions()) - { - // Unregister npc from any queued event calls - scriptEngine->unregisterWeaponUpdate(this); - - // Reset execution - m_scriptExecutionContext.resetExecution(); - } - - // Delete script object - if (m_scriptObject) - { - m_scriptObject.reset(); - } -} - -void Weapon::queueWeaponAction(Player* player, const std::string& args) -{ - ScriptEngine* scriptEngine = m_server->getScriptEngine(); - - ScriptAction scriptAction = scriptEngine->createAction("weapon.serverside", getScriptObject(), player->getScriptObject(), args); - m_scriptExecutionContext.addAction(scriptAction); - scriptEngine->registerWeaponUpdate(this); -} - -#endif diff --git a/server/src/animation/GameAni.cpp b/server/src/animation/GameAni.cpp index 75aba343f..ca6b0f5b1 100644 --- a/server/src/animation/GameAni.cpp +++ b/server/src/animation/GameAni.cpp @@ -1,21 +1,34 @@ -#include +#include +#include +#include +#include +#include +#include -#include +#include +#include -#include "animation/GameAni.h" -#include "Server.h" +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// std::optional GameAni::load(Server* const server, const std::string& name) { - auto fileSystem = server->getFileSystem(FS_FILE); + auto& fileSystem = server->getFileSystem(); // Search for the file in the filesystem - auto filePath = fileSystem->find(name); - if (filePath.isEmpty()) + auto filePath = fileSystem.find(fs::FileCategory::FILE, name); + if (filePath.empty()) return std::nullopt; // Load the animation file for parsing - std::vector fileData = CString::loadToken(filePath, "\n", true); + std::vector fileData = CString::loadToken(filePath.generic_string(), "\n", true); if (fileData.empty()) return std::nullopt; @@ -73,19 +86,19 @@ std::optional GameAni::load(Server* const server, const std::string& na } // Attempt to compile the script in GS2 - if (!gameAni.m_script.empty()) + gameAni.m_bytecode.clear(); + if (!gameAni.m_script.empty() && server->hasNPCServer()) { // Synchronous callback - server->compileGS2Script(gameAni.m_script, [&gameAni](const CompilerResponse& response) - { - if (response.success) - { - gameAni.m_bytecode.clear(response.bytecode.length()); - gameAni.m_bytecode.write((const char*)response.bytecode.buffer(), static_cast(response.bytecode.length())); - } - else - gameAni.m_bytecode.clear(); - }); + if (auto result = server->getNPCServer()->scripting.getCompiledClientScript(name, gameAni.m_script); result != nullptr) + { + auto bytecode = std::any_cast>(result->script.get()); + if (bytecode != nullptr) + { + gameAni.m_bytecode.clear(bytecode->size()); + gameAni.m_bytecode.write((const char*)bytecode->data(), static_cast(bytecode->size())); + } + } } return gameAni; @@ -107,3 +120,6 @@ CString GameAni::getBytecodePacket() const return out; } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/filesystem/File.cpp b/server/src/filesystem/File.cpp new file mode 100644 index 000000000..249d80448 --- /dev/null +++ b/server/src/filesystem/File.cpp @@ -0,0 +1,681 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifdef PLATFORM_WINDOWS +#include +#include +#endif + +#ifdef PLATFORM_UNIX +#include +#include +#endif + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::fs +{ +/////////////////////////////////////////////////////////////////////////////// + +template +auto readGPacked(std::istream& stream) -> std::conditional_t, uint32_t> +{ + using ReturnType = std::conditional_t, uint32_t>; + ReturnType result = 0; + for (size_t i = 0; i < C; ++i) + { + char byte = 0; + stream.read(&byte, 1); + result |= (static_cast(static_cast(byte - 32)) << (i * 7)); + } + return result; +} + +std::string getANSIFileName(const std::filesystem::path& file) +{ +#ifdef PLATFORM_WINDOWS + // Graal uses ANSI encoding for filenames, so convert so we don't mangle the filenames in Windows. + std::filesystem::path::string_type fileName = file.filename().native(); + + // Calculate the required buffer size for the conversion. + int bufferSize = WideCharToMultiByte(1252, 0, fileName.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (bufferSize == 0) + throw std::runtime_error("Failed to calculate buffer size for CP-1252 conversion."); + + // Allocate the string. + std::string result(bufferSize - 1, '\0'); + + // Convert to CP-1252. + int bytesWritten = WideCharToMultiByte(1252, 0, fileName.c_str(), -1, &result[0], bufferSize, nullptr, nullptr); + if (bytesWritten == 0) + throw std::runtime_error("Failed to convert file name to CP-1252."); + + return result; +#elifdef __clang__ + // Clang doesn't support std::codecvt_utf8, so just use the filename as-is. + return file.filename().string(); +#else + // Hacky version for Linux using deprecated C++. + // TODO: Link to ICU. + try + { + std::locale loc{}; + using wcvt = std::wstring_convert, wchar_t>; + auto wstr = wcvt{}.from_bytes(file.filename().string()); + std::string result(wstr.size(), '\0'); + std::use_facet>(loc).narrow(wstr.data(), wstr.data() + wstr.size(), '?', &result[0]); + return result; + } + catch (...) + { + return file.filename().string(); + } +#endif +} + +std::filesystem::path getHTMLEscapedFileName(const std::filesystem::path& file) +{ + using ST = std::filesystem::path::string_type; + using VT = std::filesystem::path::value_type; + + std::function findFirstNotOfAlphaNumeric; + std::function writeEncoded; + +#ifdef PLATFORM_WINDOWS + findFirstNotOfAlphaNumeric = [](const ST& native, size_t pos) -> size_t + { + return native.find_first_not_of(L"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.", pos); + }; + writeEncoded = [](ST& result, VT ch) + { + result += std::format(L"{:03}", (uint32_t)ch); + }; +#else + findFirstNotOfAlphaNumeric = [](const ST& native, size_t pos) -> size_t + { + return native.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.", pos); + }; + writeEncoded = [](ST& result, VT ch) + { + result += std::format("{:03}", (uint32_t)ch); + }; +#endif + + auto& native = file.native(); + + ST result; + size_t oldpos = 0, pos = 0; + while ((pos = findFirstNotOfAlphaNumeric(native, pos)) != ST::npos) + { + result.append(native.c_str() + oldpos, pos - oldpos); + result.append(1, (VT)'%'); + writeEncoded(result, native[pos]); + oldpos = ++pos; + } + if (oldpos < native.length()) + result.append(native.c_str() + oldpos); + + return result; +} + +std::filesystem::path getHTMLUnescapedFileName(const std::filesystem::path& file) +{ + using ST = std::filesystem::path::string_type; + using Elem = std::remove_cvref_t::value_type; + using Traits = std::remove_cvref_t::traits_type; + using SVT = std::basic_string_view; + + std::function findFirstEscaped; + std::function writeDecoded; + +#ifdef PLATFORM_WINDOWS + auto singleByte = [](const SVT& native) + { + // Calculate the required buffer size for the conversion. + int bufferSize = WideCharToMultiByte(1252, 0, native.data(), static_cast(native.size()), nullptr, 0, nullptr, nullptr); + if (bufferSize == 0) + throw std::runtime_error("Failed to calculate buffer size for CP-1252 conversion."); + + // Allocate the string. + std::string result(bufferSize - 1, '\0'); + + // Convert to CP-1252. + int bytesWritten = WideCharToMultiByte(1252, 0, native.data(), static_cast(native.size()), &result[0], bufferSize, nullptr, nullptr); + if (bytesWritten == 0) + throw std::runtime_error("Failed to convert file name to CP-1252."); + + return result; + }; + findFirstEscaped = [](const ST& native, size_t pos) -> size_t + { + return native.find_first_of(L"%", pos); + }; + writeDecoded = [&](ST& result, SVT code) + { + auto codeStr = singleByte(code); + result += string::toNumber(codeStr); + }; +#else + findFirstEscaped = [](const ST& native, size_t pos) -> size_t + { + return native.find_first_of("%", pos); + }; + writeDecoded = [](ST& result, SVT code) + { + result += string::toNumber(code); + }; +#endif + + auto& native = file.native(); + SVT code{native}; + + ST result; + size_t oldpos = 0, pos = 0; + while ((pos = findFirstEscaped(native, pos)) != ST::npos) + { + result.append(native.c_str() + oldpos, pos - oldpos); + writeDecoded(result, code.substr(pos + 1, 3)); + oldpos = pos + 4; + pos = oldpos; + } + if (oldpos < native.length()) + result.append(native.c_str() + oldpos); + + return result; +} + +//---------------------------- + +bool File::open() +{ + if (m_inputStreamHandle) + m_inputStreamHandle->close(); + + auto fstream = std::make_unique(); + fstream->open(m_file, std::ios::binary | std::ios::in); + + // Sometimes there can be very weird OS issues where the first attempt to open fails. + // No idea why. If the permission was denied, briefly sleep and try one more time. + if (!fstream->is_open() && errno == EACCES) + { + std::this_thread::sleep_for(1ms); + fstream->open(m_file, std::ios::binary | std::ios::in); + } + + // We failed to open the file. + if (!fstream->is_open()) + { + std::string error{ strerror(errno) }; + log::printLine(log::server, "** File '{}' read error: {}", m_file.string(), error); + } + + m_inputStreamHandle = std::move(fstream); + m_inputStream = dynamic_cast(m_inputStreamHandle.get()); + + return true; +} + +void File::close() +{ + m_inputStream = nullptr; + if (m_inputStreamHandle) + m_inputStreamHandle->close(); +} + +std::vector File::read() +{ + if (!opened() || finishedReading()) + return std::vector(); + + // Seek to the end and get the file size. + m_inputStream->seekg(0, std::ios::end); + auto size = m_inputStream->tellg(); + + // Seek to the start and read into the vector. + std::vector result(static_cast(size)); + m_inputStream->seekg(0); + m_inputStream->read(result.data(), size); + return result; +} + +std::vector File::read(std::size_t count) +{ + if (!opened() || finishedReading()) + return std::vector(); + + std::vector result(count); + m_inputStream->read(result.data(), count); + auto amount = m_inputStream->gcount(); + result.resize(static_cast(amount)); + return result; +} + +std::string File::readChars(std::size_t count) +{ + if (!opened() || finishedReading()) + return std::string{}; + + std::string result; + result.reserve(count); + + std::copy_n(std::istreambuf_iterator(*m_inputStream), count, std::back_inserter(result)); + + // For some reason it doesn't advance the last byte read, so do it manually. + m_inputStream->seekg(1, std::ios::cur); + + return result; +} + +std::string File::readGString() +{ + if (!opened() || finishedReading()) + return std::string{}; + + size_t length = readPackedIntegral<1>(); + return readChars(length); +} + +std::vector File::readUntil(std::string_view delimiter) +{ + if (delimiter.empty()) + return read(); + + std::vector result; + + if (opened() && !finishedReading()) + { + constexpr size_t bufferSize = 4096; + char buffer[bufferSize]; + char delim = delimiter[0]; + + do + { + // Read up to the start of the delimiter, or the full count of the buffer. + m_inputStream->get(buffer, bufferSize, delim); + + // Append what we read to the result. + auto count = m_inputStream->gcount(); + result.insert(result.end(), buffer, buffer + count); + + // If the next character is the start of our delimiter, check if the full delimiter is there. + if (!finishedReading() && m_inputStream->peek() == delim) + { + for (size_t i = 0; i < delimiter.length(); ++i) + { + buffer[i] = m_inputStream->get(); + if (finishedReading() || m_inputStream->peek() != delimiter[i + 1]) + { + // We didn't find the full delimiter, add what we read to the result and continue. + result.insert(result.end(), buffer, buffer + i + 1); + break; + } + } + + // We found the full delimiter so stop reading. + if (std::equal(buffer, buffer + delimiter.length(), delimiter.begin())) + break; + } + } + while (!finishedReading()); + } + + return result; +} + +std::string File::readAsString() +{ + if (!opened() || finishedReading()) + return std::string(); + + std::stringstream s; + s << m_inputStream->rdbuf(); + return s.str(); +} + +std::string File::readLine() +{ + if (!opened() || finishedReading()) + return std::string(); + + std::string result; + std::getline(*m_inputStream, result); + if (result.empty()) + return result; + + if (*result.crbegin() == '\r') + result.pop_back(); + + return result; +} + +std::optional File::readConfigLine(std::string_view key, std::string_view separator) +{ + if (!opened()) + return std::nullopt; + + setStreamPosition(0); + std::string line; + + while (!finishedReading()) + { + std::getline(*m_inputStream, line); + if (string::trimLeft(line).starts_with(key)) + { + auto sep = line.find(separator); + setStreamPosition(0); + + if (sep == std::string::npos) + return std::string{}; + + std::string value{ string::trim(line.substr(sep + separator.length())) }; + return value; + } + } + + setStreamPosition(0); + return std::nullopt; +} + +std::optional File::readConfigSection(std::string_view startKey, std::string_view endKey) +{ + if (!opened()) + return std::nullopt; + + setStreamPosition(0); + std::string section; + std::string line; + bool inSection = false; + + while (!finishedReading()) + { + std::getline(*m_inputStream, line); + if (string::trimLeft(line).starts_with(startKey)) + { + inSection = true; + continue; + } + + if (inSection && string::trimLeft(line).starts_with(endKey)) + break; + + if (inSection) + section += line + "\n"; + } + + setStreamPosition(0); + + if (section.empty()) + return std::nullopt; + return std::string{ section }; +} + +std::generator File::readAllLines() +{ + while (!finishedReading()) + { + co_yield readLine(); + } +} + +std::generator File::readLinesUntilSectionEnd(std::string_view endKey) +{ + std::string result; + while (!finishedReading()) + { + result = readLine(); + if (string::trim(result) == endKey) + break; + co_yield result; + } +} + +size_t File::readIntoBuffer(uint8_t* buffer, size_t count) +{ + if (!opened() || finishedReading()) + return 0; + + auto as_char = reinterpret_cast(buffer); + m_inputStream->read(as_char, count); + auto amount = m_inputStream->gcount(); + return static_cast(amount); +} + +std::streampos File::getStreamPosition() const +{ + return m_inputStream->tellg(); +} + +File& File::setStreamPosition(const std::streampos& position) +{ + if (opened()) + m_inputStream->seekg(position); + return *this; +} + +File& File::setStreamPosition(const std::streamoff& offset, const std::ios_base::seekdir origin) +{ + if (opened()) + m_inputStream->seekg(offset, origin); + return *this; +} + +bool File::opened() const +{ + if (auto inputStream = dynamic_cast(m_inputStream); inputStream != nullptr) + return inputStream->is_open(); + return false; +} + +bool File::finishedReading() const +{ + if (m_inputStream == nullptr || !opened()) + return true; + return m_inputStream->eof(); +} + +/////////////////////////////////////////////////////////////////////////////// + +static void removeNullTermination(std::span& input) +{ + if (input.size() != 0 && input.back() == '\0') + input = input.subspan(0, input.size() - 1); +} + +bool FileIO::open() +{ + return open(false); +} + +bool FileIO::open(bool truncate) +{ + // We want to write into a temp file and move it over the original when done, so record the file name of the temp file. + m_tempFile = m_file; + m_tempFile.concat(".partial"); + //m_tempFile.replace_extension(m_tempFile.extension().concat(".partial")); + + if (m_outputStreamHandle) + m_outputStreamHandle->close(); + + // Binary read/write mode. + // binary | in | out = open for read/write and create new if not exists. + // trunc = destroy contents. + // app | ate = append to end of file and seek to the end on open. + std::ios_base::openmode modeFlags = std::ios::binary | std::ios::in | std::ios::out; + modeFlags |= (truncate ? std::ios::trunc : (std::ios::app | std::ios::ate)); + + auto fstream = std::make_unique(); + fstream->open(m_tempFile, modeFlags); + + // Sometimes there can be very weird OS issues where the first attempt to open fails. + // No idea why (maybe virus scanners locking the file?) + // If the permission was denied, briefly sleep and try one more time. + if (!fstream->is_open() && errno == EACCES) + { + std::this_thread::sleep_for(1ms); + fstream->open(m_tempFile, modeFlags); + } + + // We failed to open the file. + if (!fstream->is_open()) + { + std::string error{ strerror(errno) }; + log::printLine(log::server, "** File '{}' read error: {}", m_tempFile.string(), error); + } + + m_outputStreamHandle = std::move(fstream); + auto outputStream = m_outputStreamHandle.get(); + m_inputStream = dynamic_cast(outputStream); + + return true; +} + +void FileIO::close() +{ + if (m_outputStreamHandle) + m_outputStreamHandle->close(); + + File::close(); + + // Move the temp file over the destination file. + if (!m_tempFile.empty() && std::filesystem::exists(m_tempFile)) + { + std::error_code ec; + std::filesystem::rename(m_tempFile, m_file, ec); + if (ec) + log::printLine(log::server, "** File '{}' write error: {}", m_file.string(), ec.message()); + + m_tempFile.clear(); + } +} + +bool FileIO::opened() const +{ + if (!m_outputStreamHandle) + return false; + return m_outputStreamHandle->is_open(); +} + +FileIO& FileIO::clear() +{ + open(true); + return *this; +} + +FileIO& FileIO::write(std::span buffer) +{ + if (!opened()) + return *this; + + m_outputStreamHandle->write(reinterpret_cast(buffer.data()), static_cast(buffer.size())); + return *this; +} + +FileIO& FileIO::write(std::span buffer) +{ + if (!opened()) + return *this; + + // Deal with char[] literals in the code. + removeNullTermination(buffer); + + m_outputStreamHandle->write(buffer.data(), static_cast(buffer.size())); + return *this; +} + +FileIO& FileIO::writeLine() +{ + if (!opened()) + return *this; + + m_outputStreamHandle->write("\n", 1); + return *this; +} + +FileIO& FileIO::writeLine(std::span line) +{ + if (!opened()) + return *this; + + // Deal with char[] literals in the code. + removeNullTermination(line); + + m_outputStreamHandle->write(line.data(), static_cast(line.size())); + m_outputStreamHandle->write("\n", 1); + return *this; +} + +FileIO& FileIO::writeConfigLine(std::span key, std::span value, std::span separator) +{ + if (!opened()) + return *this; + + // Deal with char[] literals in the code. + removeNullTermination(key); + removeNullTermination(value); + removeNullTermination(separator); + + m_outputStreamHandle->write(key.data(), static_cast(key.size())); + m_outputStreamHandle->write(separator.data(), static_cast(separator.size())); + m_outputStreamHandle->write(value.data(), static_cast(value.size())); + m_outputStreamHandle->write("\n", 1); + return *this; +} + +FileIO& FileIO::writeConfigSection(std::span startKey, std::span section, std::span endKey) +{ + if (!opened()) + return *this; + + // Deal with char[] literals in the code. + removeNullTermination(startKey); + removeNullTermination(section); + removeNullTermination(endKey); + + m_outputStreamHandle->write(startKey.data(), static_cast(startKey.size())); + m_outputStreamHandle->write("\n", 1); + + m_outputStreamHandle->write(section.data(), static_cast(section.size())); + if (section.back() != '\n') + m_outputStreamHandle->write("\n", 1); + + m_outputStreamHandle->write(endKey.data(), static_cast(endKey.size())); + m_outputStreamHandle->write("\n", 1); + return *this; +} + +FileIO& FileIO::flush() +{ + if (!opened()) + return *this; + + m_outputStreamHandle->flush(); + return *this; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::fs diff --git a/server/src/filesystem/FileSystem.cpp b/server/src/filesystem/FileSystem.cpp new file mode 100644 index 000000000..44f19cc91 --- /dev/null +++ b/server/src/filesystem/FileSystem.cpp @@ -0,0 +1,735 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::fs +{ +/////////////////////////////////////////////////////////////////////////////// + +FileSystem::FileSystem(const std::filesystem::path& directory) +{ + bind(directory); +} + +FileSystem::~FileSystem() +{ + // Wait for any searching to finish. + waitUntilFilesSearched(); + + // Stop watching all directories. + m_watcher.removeAll(); + + // Mark ourselves as destructing so we can avoid callbacks. + m_destructing = true; +} + +//---------------------------- + +void FileSystem::reset() +{ + m_watcher.removeAll(); + + // Clear our saved filesystem. + std::scoped_lock guard{ m_file_mutex }; + m_files.clear(); + m_directories.clear(); +} + +void FileSystem::addFoldersConfigEntry(FileCategory category, const std::filesystem::path& glob) +{ + std::scoped_lock guard{ m_file_mutex }; + + // Force into preferred format so we can do proper matching. + std::filesystem::path preferred{ glob }; + preferred.make_preferred(); + + // Add the glob to our folders config category. + size_t categoryIndex = static_cast(category); + m_foldersConfig[categoryIndex].insert(preferred); + + // Apply the glob to any tracked files. + for (auto& fileData : m_files) + { + if (string::match(fileData.second->file.native(), preferred.native())) + fileData.second->categories.set(categoryIndex); + } +} + +void FileSystem::bind(const std::filesystem::path& directory) +{ + std::scoped_lock guard{ m_file_mutex }; + + // If we are already watching this directory, abort. + if (std::ranges::contains(m_directories, directory)) + return; + + // We are starting our file search. + m_searching_files = true; + + // Create directories that don't exist. + std::filesystem::create_directories(directory); + + // Fill our filesystem with file information. + for (const auto& file : std::filesystem::recursive_directory_iterator(directory)) + { + // If it is not a regular file, abort. + if (!std::filesystem::is_regular_file(file.status())) + continue; + + const auto& path = file.path(); + auto entry = std::make_unique(); + + entry->file = path; + entry->file.make_preferred(); + + entry->fileSize = file.file_size(); + entry->modifiedTime = file.last_write_time(); + assignCategoriesToFileData(*entry); + + m_files.insert(std::make_pair(path.filename(), std::move(entry))); + } + + // We are done searching our file system. + m_searching_files = false; + m_searching_files_condition.notify_all(); + + m_directories.insert(directory); + m_watcher.add(directory, [this](uint32_t id, const std::filesystem::path& dir, const std::filesystem::path& file, const std::filesystem::path& oldFile, preagonal::fs::FileEventCollection e) + { + if (m_destructing || e.test(FileEvent::Invalid)) + return; + + FileData* eventFileData = nullptr; + FileData deletedData; + + DEBUGPRINT("[FS] Event: {} on file: {} in dir: {}", e.to_string(), file.string(), dir.string()); + + // Limit our lock to not include the event callbacks. + { + std::scoped_lock watchGuard{ m_file_mutex }; + auto iter = m_files.find(file.filename()); + + // Existing file. + if (iter != m_files.end()) + { + // The file got changed. + if (e.test(FileEvent::Modified) || e.test(FileEvent::Renamed)) + { + if (std::filesystem::exists(dir / file)) + { + // Check for no change in mod time. + // Sometimes a modify event can get spawned multiple times. + auto fileModTime = std::filesystem::last_write_time(dir / file); + if (iter->second->modifiedTime == fileModTime) + return; + + iter->second->modifiedTime = fileModTime; + + // If we got a rename event and overwrote an existing file, + // we want to make sure we also have a modify event since the file got changed. + if (!e.test(FileEvent::Modified)) + e.set(FileEvent::Modified); + + DEBUGPRINT("[FS] Existing file modified: {}", file.string()); + } + } + + // The file got deleted. + if (e.test(FileEvent::Deleted)) + { + // Make a copy of the data that is going to be deleted so we can pass it to the event callback. + deletedData = *iter->second; + eventFileData = &deletedData; + + m_files.erase(iter); + iter = m_files.end(); + + DEBUGPRINT("[FS] Existing file deleted: {}", file.string()); + } + + // If the file got renamed, make sure we remove the old one from the file system. + if (e.test(FileEvent::Renamed)) + { + DEBUGPRINT("[FS] Existing file renamed: {} -> {}", oldFile.string(), file.string()); + auto oldIter = m_files.find(oldFile.filename()); + if (oldIter != m_files.end()) + { + // Make a copy of the data that is going to be deleted so we can pass it to the event callback. + deletedData = *oldIter->second; + eventFileData = &deletedData; + m_files.erase(oldIter); + iter = m_files.find(file.filename()); + DEBUGPRINT("[FS] Old file deleted due to rename: {}", oldFile.string()); + } + + // If the old file was the same name as the new file, but with a .partial extension, then we ignore the renamed event. + if (oldFile.extension() == ".partial" && oldFile.stem() == file) + { + e.reset(FileEvent::Renamed); + DEBUGPRINT("[FS] Ignored renamed event for partial file: {} -> {}", oldFile.string(), file.string()); + } + } + } + else + { + // File got renamed and didn't overwrite an existing file, so update the file entry internals. + if (e.test(FileEvent::Renamed)) + { + // Found the old entry. + iter = m_files.find(oldFile.filename()); + if (iter != m_files.end()) + { + // Update the file entry. + iter->second->file = dir / file; + iter->second->file.make_preferred(); + iter->second->modifiedTime = std::filesystem::last_write_time(iter->second->file); + iter->second->categories.reset(); + assignCategoriesToFileData(*iter->second.get()); + + // Change the key in the map. + auto node = m_files.extract(iter); + node.key() = file.filename(); + iter = m_files.insert(std::move(node)); + + // Switch over to the added event since this is effectively a new file now. + e.reset(); + e.set(FileEvent::Added); + + DEBUGPRINT("[FS] New file renamed from old: {} -> {}", oldFile.string(), file.string()); + } + // Not found, so treat it as a new file. + else + { + e.reset(FileEvent::Renamed); + e.set(FileEvent::Added); + + DEBUGPRINT("[FS] New file added (old file not tracked): {} -> {}", oldFile.string(), file.string()); + } + } + + // New file. + if (iter == m_files.end() && e.test(FileEvent::Added) && std::filesystem::exists(dir / file)) + { + auto entry = std::make_unique(); + entry->file = dir / file; + entry->file.make_preferred(); + entry->fileSize = std::filesystem::file_size(entry->file); + entry->modifiedTime = std::filesystem::last_write_time(entry->file); + assignCategoriesToFileData(*entry); + + iter = m_files.insert(std::make_pair(file.filename(), std::move(entry))); + DEBUGPRINT("[FS] New file added: {}", file.string()); + } + } + + // Extract the event data for callbacks. + if (iter != m_files.end() && iter->second) + eventFileData = iter->second.get(); + } + + // Run events. + if (eventFileData != nullptr) + { + // File callbacks. + if (eventFileData->eventCallback) + eventFileData->eventCallback(e, *eventFileData); + + // Category callbacks. + for (size_t i = 0; i < FileCategoryTypeCount; ++i) + { + if (eventFileData->categories.test(i) && categoryEventCallback[i]) + categoryEventCallback[i](e, *eventFileData); + } + } + }); +} + +void FileSystem::update() +{ + m_watcher.update(); +} + +//---------------------------- + +bool FileSystem::has(FileCategory category, const std::filesystem::path& file) const noexcept +{ + if (std::filesystem::exists(file)) + return true; + + // If we don't have a folders config, skip the category test. + bool skipTest = !hasFoldersConfig(); + + // Check if our file is saved in the file system list. + { + std::scoped_lock guard{ m_file_mutex }; + auto iter = m_files.find(file); + if (iter != m_files.end()) + { + if (iter->second != nullptr && (skipTest || iter->second->categories.test((size_t)category))) + return true; + } + } + + return false; +} + +bool FileSystem::has(const std::filesystem::path& file) const noexcept +{ + if (std::filesystem::exists(file)) + return true; + + // Check if our file is saved in the file system list. + { + std::scoped_lock guard{ m_file_mutex }; + if (auto iter = m_files.find(file); iter != m_files.end()) + return true; + } + + return false; +} + +bool FileSystem::hasi(FileCategory category, const std::filesystem::path& file) const noexcept +{ + if (std::filesystem::exists(file)) + return true; + + bool skipTest = !hasFoldersConfig(); + auto fileName = file.string(); + + // Check if our file is saved in the file system list. + std::scoped_lock guard{ m_file_mutex }; + for (auto& [filePath, info] : m_files) + { + if (string::equalsi(filePath.string(), fileName) && (skipTest || info->categories.test((size_t)category))) + return true; + } + + return false; +} + +//---------------------------- + +std::filesystem::path FileSystem::find(FileCategory category, const std::filesystem::path& file) const noexcept +{ + if (auto fileInfo = info(category, file); fileInfo != nullptr) + return fileInfo->file; + + return std::filesystem::path{}; +} + +std::filesystem::path FileSystem::findi(FileCategory category, const std::filesystem::path& file) const noexcept +{ + std::scoped_lock guard{ m_file_mutex }; + + bool skipTest = !hasFoldersConfig(); + auto fileName = file.string(); + for (auto& [key, value] : m_files) + { + if ((skipTest || value->categories.test((size_t)category)) && string::equalsi(key.string(), fileName)) + return value->file; + } + return std::filesystem::path{}; +} + +//---------------------------- + +FileData* FileSystem::info(FileCategory category, const std::filesystem::path& file) const +{ + std::scoped_lock guard{ m_file_mutex }; + + bool skipTest = !hasFoldersConfig(); + auto iter = m_files.find(file); + while (iter != m_files.end()) + { + if (skipTest || iter->second->categories.test((size_t)category)) + return iter->second.get(); + ++iter; + } + return nullptr; +} + +std::vector FileSystem::info(const std::filesystem::path& file) const +{ + std::scoped_lock guard{ m_file_mutex }; + std::vector result; + + for (auto& [key, value] : m_files) + { + if (key == file) + result.push_back(value); + } + + return result; +}; + +std::vector FileSystem::info(FileCategory category) const +{ + std::scoped_lock guard{ m_file_mutex }; + std::vector result; + + bool skipTest = !hasFoldersConfig(); + for (auto& fileData : m_files) + { + if (skipTest || fileData.second->categories.test((size_t)category)) + result.push_back(fileData.second); + } + + return result; +} + +FileData* FileSystem::infoi(FileCategory category, const std::filesystem::path& file) const +{ + std::scoped_lock guard{ m_file_mutex }; + + bool skipTest = !hasFoldersConfig(); + auto fileName = file.string(); + for (auto& [key, value] : m_files) + { + if ((skipTest || value->categories.test((size_t)category)) && string::equalsi(key.string(), fileName)) + return value.get(); + } + return nullptr; +} + +std::vector FileSystem::infoi(const std::filesystem::path& file) const +{ + std::scoped_lock guard{ m_file_mutex }; + std::vector result; + + auto fileName = file.string(); + for (auto& [key, value] : m_files) + { + if (string::equalsi(key.string(), fileName)) + result.push_back(value); + } + + return result; +} + +//---------------------------- + +std::shared_ptr FileSystem::open(FileCategory category, const std::filesystem::path& file) const +{ + // Check if the file exists in the native file system and file is a direct path. + if (std::filesystem::exists(file)) + { + if (auto f = std::make_shared(file); f->opened()) + return f; + return nullptr; + } + + // Check if the file exists in the native file system and file is a filename we want to find. + if (auto fileData = info(category, file); fileData != nullptr) + { + if (auto f = std::make_shared(fileData->file); f->opened()) + return f; + } + + return nullptr; +} + +std::vector> FileSystem::open(const std::filesystem::path& file) const +{ + std::vector> result; + + // Check if the file exists in the native file system and file is a direct path. + if (std::filesystem::exists(file)) + { + auto f = std::make_shared(); + f->setFilePath(file); + result.push_back(f); + return result; + } + + for (auto& [fileName, fileData] : m_files) + { + auto f = std::make_shared(); + f->setFilePath(fileName); + result.push_back(f); + } + + return result; +} + +std::shared_ptr FileSystem::open(const FileData& fileData) const +{ + if (std::filesystem::exists(fileData.file)) + { + if (auto f = std::make_shared(fileData.file); f->opened()) + return f; + } + + return nullptr; +} + +std::shared_ptr FileSystem::openi(FileCategory category, const std::filesystem::path& file) const +{ + // Check if the file exists in the native file system and file is a direct path. + if (std::filesystem::exists(file)) + { + if (auto f = std::make_shared(file); f->opened()) + return f; + return nullptr; + } + + // Check if the file exists in the native file system and file is a filename we want to find. + if (auto fileData = infoi(category, file); fileData != nullptr) + { + if (auto f = std::make_shared(fileData->file); f->opened()) + return f; + } + + return nullptr; +} + +//---------------------------- + +std::shared_ptr FileSystem::openForWriting(FileCategory category, const std::filesystem::path& file, bool createNew) const +{ + FileIOPtr result = nullptr; + + // Check if the file exists in the native file system and file is a direct path. + if (std::filesystem::exists(file)) + result = std::make_shared(file); + + // Check if the file exists in the native file system and file is a filename we want to find. + if (result == nullptr) + { + if (auto fileData = info(category, file); fileData != nullptr) + result = std::make_shared(fileData->file); + } + + // If we have a file, return it. + if (result != nullptr) + { + if (result->opened()) + return result; + return nullptr; + } + if (!createNew) + return nullptr; + + // Create the new file. + auto directories = getManagedDirectories(category); + auto first = directories.begin(); + if (first == directories.end()) + return nullptr; + + return std::make_shared((*first) / file); +} + +std::vector> FileSystem::openForWriting(const std::filesystem::path& file) const +{ + std::vector> result; + + // Check if the file exists in the native file system and file is a direct path. + if (std::filesystem::exists(file)) + { + auto f = std::make_shared(); + f->setFilePath(file); + result.push_back(f); + return result; + } + + for (auto& [fileName, fileData] : m_files) + { + auto f = std::make_shared(); + f->setFilePath(fileName); + result.push_back(f); + } + + return result; +} + +std::shared_ptr FileSystem::openForWriting(const FileData& fileData) const +{ + if (std::filesystem::exists(fileData.file)) + { + if (auto f = std::make_shared(fileData.file); f->opened()) + return f; + } + + return nullptr; +} + +std::shared_ptr FileSystem::openiForWriting(FileCategory category, const std::filesystem::path& file, bool createNew) const +{ + FileIOPtr result = nullptr; + + // Check if the file exists in the native file system and file is a direct path. + if (std::filesystem::exists(file)) + result = std::make_shared(file); + + // Check if the file exists in the native file system and file is a filename we want to find. + if (result == nullptr) + { + if (auto fileData = infoi(category, file); fileData != nullptr) + result = std::make_shared(fileData->file); + } + + // If we have a file, return it. + if (result != nullptr) + { + if (result->opened()) + return result; + return nullptr; + } + if (!createNew) + return nullptr; + + // Create the new file. + auto directories = getManagedDirectories(category); + auto first = directories.begin(); + if (first == directories.end()) + return nullptr; + + return std::make_shared((*first) / file); +} + +//---------------------------- + +void FileSystem::addExisting(FileCategory category, const std::filesystem::path& fullFilePath) +{ + if (!std::filesystem::exists(fullFilePath)) + return; + + std::scoped_lock guard{m_file_mutex}; + + auto files = m_files.find(fullFilePath.filename()); + if (files != m_files.end()) + return; + + auto entry = std::make_unique(); + entry->file = fullFilePath; + entry->file.make_preferred(); + entry->fileSize = std::filesystem::file_size(entry->file); + entry->modifiedTime = std::filesystem::last_write_time(entry->file); + assignCategoriesToFileData(*entry); + + m_files.insert(std::make_pair(fullFilePath.filename(), std::move(entry))); +} + +//---------------------------- + +FileData* FileSystem::rename(const FileData& fileData, std::filesystem::path newFileName) +{ + if (!std::filesystem::exists(fileData.file)) + return nullptr; + + std::scoped_lock guard{ m_file_mutex }; + + auto files = m_files.find(fileData.file.filename()); + while (files != m_files.end()) + { + if (files->second->modifiedTime == fileData.modifiedTime && files->second->categories == fileData.categories) + { + auto newFilePath = fileData.file.parent_path() / newFileName; + newFilePath.make_preferred(); + + // Rename the file. + std::error_code ec; + std::filesystem::rename(fileData.file, newFilePath, ec); + if (ec) + { + log::printLine(log::server, "** Error renaming file [{}] to [{}]: {} **", fileData.file.filename().string(), newFileName.string(), ec.message()); + return nullptr; + } + + // Update the file data. + files->second->file = newFilePath; + files->second->refreshModTime(); + + // Reset the categories. + files->second->categories.reset(); + assignCategoriesToFileData(*files->second.get()); + + // Update the key. + auto node = m_files.extract(files); + node.key() = newFilePath.filename(); + files = m_files.insert(std::move(node)); + + // Return our new data. + return files->second.get(); + } + } + + return nullptr; +} + +//---------------------------- + +std::generator FileSystem::getManagedDirectories() const +{ + for (const auto& dir : m_directories) + co_yield dir; +} + +std::generator FileSystem::getManagedDirectories(FileCategory category) const +{ + bool skipTest = !hasFoldersConfig(); + if (skipTest || category == FileCategory::ALL) + co_yield std::ranges::elements_of(getManagedDirectories()); + + for (const auto& dir : m_directories) + { + if (categoryForDirectory(dir) == category) + co_yield dir; + } +} + +//---------------------------- + +void FileSystem::assignCategoriesToFileData(FileData& fileData) +{ + fileData.categories.set(ENUM(FileCategory::ALL)); + + for (size_t i = 0; i < FileCategoryTypeCount; ++i) + { + if (m_foldersConfig[i].empty()) + continue; + + for (const auto& glob : m_foldersConfig[i]) + { + if (string::match(fileData.file.native(), glob.native())) + { + fileData.categories.set(i); + break; + } + } + } +} + +FileCategory FileSystem::categoryForDirectory(const std::filesystem::path& directory) const +{ + for (size_t i = 0; i < FileCategoryTypeCount; ++i) + { + if (m_foldersConfig[i].empty()) + continue; + + for (const auto& glob : m_foldersConfig[i]) + { + if (string::match(directory.native(), glob.parent_path().native())) + return static_cast(i); + } + } + return FileCategory::ALL; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::fs diff --git a/server/src/filesystem/watch/FileWatchOS_Linux.cpp b/server/src/filesystem/watch/FileWatchOS_Linux.cpp new file mode 100644 index 000000000..2acd077ef --- /dev/null +++ b/server/src/filesystem/watch/FileWatchOS_Linux.cpp @@ -0,0 +1,211 @@ +#ifdef PLATFORM_UNIX + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define BUFF_SIZE ((sizeof(struct inotify_event)+FILENAME_MAX)*100) + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::fs::watch +{ +/////////////////////////////////////////////////////////////////////////////// + +struct Watch +{ + uint32_t watch_id; + std::filesystem::path dir; + watch_cb callback; +}; + +struct WatchOS +{ + int fd; + timeval timeout; + fd_set descriptor_set; +}; + +///////////////////////////// + +FileWatch::FileWatch() + : m_last_id(0) +{ + m_watch_os = std::make_unique(); + + m_watch_os->fd = inotify_init(); + if (m_watch_os->fd < 0) + fprintf(stderr, "Error: %s\n", strerror(errno)); + + m_watch_os->timeout.tv_sec = 0; + m_watch_os->timeout.tv_usec = 0; + + FD_ZERO(&m_watch_os->descriptor_set); +} + +FileWatch::~FileWatch() +{ + removeAll(); +} + +uint32_t FileWatch::add(const std::filesystem::path& directory, watch_cb callback, bool recursive) +{ + int wd = inotify_add_watch(m_watch_os->fd, directory.c_str(), + IN_CLOSE_WRITE | IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE); + + if (wd < 0) + return 0; + + Watch* watch = new Watch; + watch->watch_id = wd; + watch->dir = directory; + watch->callback = callback; + + m_watchers.insert(std::make_pair(wd, watch)); + return wd; +} + +void FileWatch::remove(const std::filesystem::path& directory) +{ + for (auto& w : m_watchers) + { + if (directory == w.second->dir) + { + remove(w.first); + return; + } + } +} + +void FileWatch::remove(uint32_t watch_id) +{ + auto i = m_watchers.find(watch_id); + if (i == m_watchers.end()) + return; + + Watch* watch = i->second; + m_watchers.erase(i); + + inotify_rm_watch(m_watch_os->fd, watch->watch_id); + delete watch; +} + +void FileWatch::removeAll() +{ + for (auto& w : m_watchers) + { + inotify_rm_watch(m_watch_os->fd, w.second->watch_id); + delete w.second; + } + + m_watchers.clear(); +} + +void FileWatch::update() +{ + FD_SET(m_watch_os->fd, &m_watch_os->descriptor_set); + + int ret = select(m_watch_os->fd + 1, &m_watch_os->descriptor_set, NULL, NULL, &m_watch_os->timeout); + if (ret < 0) + { + perror("select"); + } + else if (FD_ISSET(m_watch_os->fd, &m_watch_os->descriptor_set)) + { + ssize_t len, i = 0; + std::unordered_map fileEvents; + char notifyBuff[BUFF_SIZE] = { 0 }; + char fileBuff[FILENAME_MAX + 1] = { 0 }; + + len = read(m_watch_os->fd, notifyBuff, BUFF_SIZE); + + while (i < len) + { + struct inotify_event* pevent = (struct inotify_event*)¬ifyBuff[i]; + + auto iter = m_watchers.find(pevent->wd); + if (iter != m_watchers.end()) + { + Watch* watch = iter->second; + if (pevent->name[0] != '.') + { + if (IN_MOVED_FROM & pevent->mask) + { + std::strncpy(fileBuff, pevent->name, FILENAME_MAX); + fileBuff[FILENAME_MAX] = '\0'; + } + else + { + std::filesystem::path fileName{ pevent->name }; + + auto& data = fileEvents[fileName.filename()]; + data.fsData = watch; + data.fileName = fileName; + if (fileBuff[0] != '\0') + { + data.oldFileName = std::filesystem::path{ fileBuff }; + fileBuff[0] = '\0'; + } + + if (IN_CLOSE_WRITE & pevent->mask) + data.events.set(FileEvent::Modified); + if (IN_CREATE & pevent->mask) + data.events.set(FileEvent::Added); + if (IN_DELETE & pevent->mask) + data.events.set(FileEvent::Deleted); + if (IN_MOVED_TO & pevent->mask) + data.events.set(FileEvent::Renamed); + } + } + } + + i += sizeof(struct inotify_event) + pevent->len; + } + + // Check for rename events on overwritten files. + // When we save files, a temp file gets created and renamed over top of the original file. + // The temp file will get an Added event, and the original file will get a Deleted event + a Renamed event. + // We want the original file to have a single Modified event, while the original file should get a Deleted event. + for (auto& [file, data] : fileEvents) + { + auto* watch = (Watch*)data.fsData; + if (watch == nullptr) continue; + + // If we have both an added and deleted event, test if the file exists and only set the appropriate one. + if (data.events.test(FileEvent::Added) && data.events.test(FileEvent::Deleted)) + { + if (std::filesystem::exists(watch->dir / data.oldFileName)) + data.events.reset(FileEvent::Deleted); + else + data.events.reset(FileEvent::Added); + } + // If we have an added + modified event, clear the modified. + else if (data.events.test(FileEvent::Added) && data.events.test(FileEvent::Modified)) + { + data.events.reset(FileEvent::Modified); + } + } + + // Execute callbacks for our queued data. + for (auto& [file, data] : fileEvents) + { + if (data.events.test(FileEvent::Invalid)) + continue; + + if (auto* watch = (Watch*)data.fsData; watch != nullptr) + watch->callback(watch->watch_id, watch->dir, data.fileName, data.oldFileName, data.events); + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::fs::watch + +#endif // PLATFORM_UNIX diff --git a/server/src/filesystem/watch/FileWatchOS_OSX.cpp b/server/src/filesystem/watch/FileWatchOS_OSX.cpp new file mode 100644 index 000000000..916b4723b --- /dev/null +++ b/server/src/filesystem/watch/FileWatchOS_OSX.cpp @@ -0,0 +1,3 @@ +#ifdef PLATFORM_APPLE + +#endif // PLATFORM_APPLE diff --git a/bin/servers/default/translations/english.po b/server/src/filesystem/watch/FileWatchOS_Poll.cpp similarity index 100% rename from bin/servers/default/translations/english.po rename to server/src/filesystem/watch/FileWatchOS_Poll.cpp diff --git a/server/src/filesystem/watch/FileWatchOS_Windows.cpp b/server/src/filesystem/watch/FileWatchOS_Windows.cpp new file mode 100644 index 000000000..078be4843 --- /dev/null +++ b/server/src/filesystem/watch/FileWatchOS_Windows.cpp @@ -0,0 +1,339 @@ +#ifdef PLATFORM_WINDOWS + +#ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN +#endif + +#ifndef NOMINMAX + #define NOMINMAX +#endif + +#include +#include + +/* +#if defined(_MSC_VER) +#pragma comment(lib, "comctl32.lib") +#pragma comment(lib, "user32.lib") +#pragma comment(lib, "ole32.lib") +#pragma warning (disable: 4996) +#endif +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::fs::watch +{ +/////////////////////////////////////////////////////////////////////////////// + +struct Watch +{ + static constexpr size_t FileBufferLength = std::numeric_limits::max() + 1; + + Watch() + : dir_handle(0), notify_filter(0), watch_id(0), recursive(false), callback(nullptr), stop(false) + { + memset(&overlapped, 0, sizeof(OVERLAPPED)); + buffer[0] = '\0'; + } + + OVERLAPPED overlapped; + HANDLE dir_handle; + BYTE buffer[32 * 1024]; + DWORD notify_filter; + + uint32_t watch_id; + std::filesystem::path dir; + bool recursive; + watch_cb callback; + + std::atomic stop; +}; + +struct WatchOS +{ +}; + +///////////////////////////// + +static bool refreshWatch(Watch* watch, bool clear = false); +static void deleteWatch(Watch* watch); + +static FileEventCollection translateAction(FileEventCollection& event, DWORD action) +{ + switch (action) + { + case FILE_ACTION_ADDED: + event.set(FileEvent::Added); + break; + + case FILE_ACTION_REMOVED: + case FILE_ACTION_RENAMED_OLD_NAME: + event.set(FileEvent::Deleted); + break; + + case FILE_ACTION_MODIFIED: + event.set(FileEvent::Modified); + break; + + case FILE_ACTION_RENAMED_NEW_NAME: + event.set(FileEvent::Renamed); + break; + + default: + event.set(FileEvent::Invalid); + break; + } + return event; +} + +static void CALLBACK watchCallback(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped) +{ + thread_local TCHAR fileBuffer[Watch::FileBufferLength]; + thread_local TCHAR oldFileBuffer[Watch::FileBufferLength]; + + // Maximum path length on Windows is 32767. + PFILE_NOTIFY_INFORMATION pNotify; + Watch* pWatch = (Watch*)lpOverlapped; + size_t offset = 0; + + if (pWatch == nullptr || pWatch->callback == nullptr || dwNumberOfBytesTransfered == 0) + return; + + if (dwErrorCode == ERROR_SUCCESS) + { + std::unordered_map fileEvents; + fileBuffer[0] = TEXT('\0'); + oldFileBuffer[0] = TEXT('\0'); + + do + { + pNotify = (PFILE_NOTIFY_INFORMATION)&pWatch->buffer[offset]; + offset += pNotify->NextEntryOffset; + + // Set our filename buffer. + PTCHAR targetBuffer = fileBuffer; + if (pNotify->Action == FILE_ACTION_RENAMED_OLD_NAME) + targetBuffer = oldFileBuffer; + + *targetBuffer = TEXT('\0'); + +#if defined(UNICODE) + { + size_t fileNameCharacters = pNotify->FileNameLength / sizeof(WCHAR); + (void)StringCchCopyNW(targetBuffer, Watch::FileBufferLength, pNotify->FileName, fileNameCharacters); + } +#else + { + int count = WideCharToMultiByte(CP_ACP, 0, pNotify->FileName, + pNotify->FileNameLength / sizeof(WCHAR), + targetBuffer, Watch::FileBufferLength - 1, NULL, NULL); + targetBuffer[count] = TEXT('\0'); + } +#endif + + // If this is a rename event, and it is the old file, continue to the next entry. + // The old file was stored above. + if (pNotify->Action == FILE_ACTION_RENAMED_OLD_NAME) + continue; + + std::filesystem::path fileName{ fileBuffer }; + auto& data = fileEvents[fileName.filename()]; + + // Save information to the queued data. + translateAction(data.events, pNotify->Action); + data.fileName = fileName; + if (oldFileBuffer[0] != TEXT('\0')) + data.oldFileName = std::filesystem::path{ oldFileBuffer }; + } + while (pNotify->NextEntryOffset != 0); + + // Check for rename events on overwritten files. + // When we save files, a temp file gets created and renamed over top of the original file. + // The temp file will get an Added event, and the original file will get a Deleted event + a Renamed event. + // We want the original file to have a single Renamed event, while the temp file should be ignored. + for (auto& [file, data] : fileEvents) + { + if (data.events.test(FileEvent::Renamed)) + { + // Make sure we are only getting a rename event. + data.events.reset(); + data.events.set(FileEvent::Renamed); + + // If the old file exists in our list, ignore it. + // The filesystem logic will take care of removing it. + auto oldFileIter = fileEvents.find(data.oldFileName); + if (oldFileIter != fileEvents.end()) + { + auto& [oldFile, oldFileData] = *oldFileIter; + oldFileData.events.set(FileEvent::Invalid); + } + } + } + + // Execute callbacks for our queued data. + for (auto& [file, data] : fileEvents) + { + if (data.events.test(FileEvent::Invalid)) + continue; + + pWatch->callback(pWatch->watch_id, pWatch->dir, data.fileName, data.oldFileName, data.events); + } + } + + if (!pWatch->stop) + refreshWatch(pWatch); + else + deleteWatch(pWatch); +} + +bool refreshWatch(Watch* watch, bool clear) +{ + return ReadDirectoryChangesW( + watch->dir_handle, watch->buffer, sizeof(watch->buffer), watch->recursive, + watch->notify_filter, NULL, &watch->overlapped, clear ? 0 : watchCallback) != 0; +} + +void deleteWatch(Watch* watch) +{ + CloseHandle(watch->overlapped.hEvent); + CloseHandle(watch->dir_handle); + + delete watch; +} + +///////////////////////////// + +FileWatch::FileWatch() + : m_last_id(0) +{ +} + +FileWatch::~FileWatch() +{ + for (auto& w : m_watchers) + { + Watch* watch = w.second; + + CancelIo(watch->dir_handle); + refreshWatch(watch, true); + + if (!HasOverlappedIoCompleted(&watch->overlapped)) + SleepEx(5, TRUE); + + deleteWatch(watch); + } +} + +uint32_t FileWatch::add(const std::filesystem::path& directory, watch_cb callback, bool recursive) +{ + Watch* watch = new Watch; + + // Create the directory handle. + // FILE_FLAG_BACKUP_SEMANTICS lets us actually create a directory handle to get file change events. + // FILE_FLAG_OVERLAPPED lets us do async IO. + watch->dir_handle = CreateFile(directory.c_str(), FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL); + if (watch->dir_handle == INVALID_HANDLE_VALUE) + { + delete watch; + return 0; + } + + // Create our event. + // This is the important part that lets us do async IO. + // The callback was registered with ReadDirectoryChangesW(), and this event is passed in the overlapped data. + // That means that ReadDirectoryChangesW() will buffer events until the event gets signaled. + // The signal is kicked off by the MsgWaitForMultipleObjectsEx() function. + watch->overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + watch->notify_filter = FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_FILE_NAME; + watch->recursive = recursive; + + uint32_t id = ++m_last_id; + + watch->watch_id = id; + watch->dir = directory; + watch->callback = callback; + + if (!refreshWatch(watch)) + { + deleteWatch(watch); + return 0; + } + + m_watchers.insert(std::make_pair(id, watch)); + return id; +} + +void FileWatch::remove(const std::filesystem::path& directory) +{ + for (auto& w : m_watchers) + { + if (directory == w.second->dir) + { + remove(w.first); + return; + } + } +} + +void FileWatch::remove(uint32_t watch_id) +{ + auto i = m_watchers.find(watch_id); + if (i == m_watchers.end()) + return; + + Watch* watch = i->second; + m_watchers.erase(i); + + CancelIo(watch->dir_handle); + refreshWatch(watch); + + if (HasOverlappedIoCompleted(&watch->overlapped)) + { + deleteWatch(watch); + } +} + +void FileWatch::removeAll() +{ + for (auto& w : m_watchers) + { + Watch* watch = w.second; + + CancelIo(watch->dir_handle); + refreshWatch(watch); + + if (HasOverlappedIoCompleted(&watch->overlapped)) + { + deleteWatch(watch); + } + } + + m_watchers.clear(); +} + +void FileWatch::update() +{ + // Count is 0, so it waits only for an input event. + // No handles. + // 0ms timeout, which means this function will not block. + // QS_ALLINPUT means we want to wake up for all input events. + // Wait only for alertable input events. + MsgWaitForMultipleObjectsEx(0, NULL, 0, QS_ALLINPUT, MWMO_ALERTABLE); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::fs::watch + +#endif // PLATFORM_WINDOWS diff --git a/server/src/level/Level.cpp b/server/src/level/Level.cpp index f1adc0283..eb38e8d5e 100644 --- a/server/src/level/Level.cpp +++ b/server/src/level/Level.cpp @@ -1,1228 +1,1494 @@ +#include +#include +#include +#include #include +#include +#include +#include +#include +#include #include +#include #include -#include -#include - -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include #include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// -#include "NPC.h" -#include "Player.h" -#include "Server.h" -#include "level/Level.h" -#include "level/Map.h" - -/* - Global Variables -*/ -//CString base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; short respawningTiles[] = { - 0x1ff, - 0x3ff, - 0x2ac, - 0x002, - 0x200, - 0x022, - 0x3de, - 0x1a4, - 0x14a, - 0x674, - 0x72a, + 0x1ff, // grass + 0x3ff, // grass + 0x7ff, // grass + 0x2ac, // vase + 0x002, // bush + 0x200, // sign + 0x022, // stone + 0x3de, // blackstone + 0x1a4, // swamp + 0x14a, // stake 1 + 0x674, // stake 2 + 0x72a, // hole }; -constexpr int getBase64Position(char c) +//---------------------------- + +void StaticLevelData::reload(std::shared_ptr staticData) { - if (c >= 'a') - return 26 + (c - 'a'); - else if (c >= 'A') - return (c - 'A'); - else if (c >= '0' && c <= '9') - return 52 + (c - '0'); + // Clear our data. + staticData->tiles.reset(); + staticData->links.clear(); + staticData->chests.clear(); + staticData->signs.clear(); + staticData->baddies.clear(); + staticData->npcs.clear(); + staticData->heights.clear(); + + // Reload the data from disk. + LevelLoader::loadStaticDataInto(staticData); + + // Notify listeners that the data has been refreshed. + staticData->onDataRefreshed.post(staticData); +} - switch (c) - { - case '+': - return 52 + 10; - case '/': - return 52 + 11; - } +std::optional StaticLevelData::getChestFormattedForSave(LevelChest* chest) const +{ + if (chest == nullptr) + return std::nullopt; - return 0; + return std::format("{}:{}:{}", chest->position.x(), chest->position.y(), levelName); } -/* - Level: Constructor - Deconstructor -*/ -Level::Level(short fillTile) +void StaticLevelData::sendBoardToPlayer(std::shared_ptr player) const { - m_tiles[0] = LevelTiles(fillTile); + CString retVal; + retVal.writeGChar(PLO_BOARDPACKET); + tiles.writeLayerToPacket(0, retVal); + + player->sendPacket(CString() >> (char)PLO_RAWDATA >> (int)((1 + (64 * 64 * 2) + 1))); + player->sendPacket(retVal); } -Level::~Level() +void StaticLevelData::sendBoardLayersToPlayer(std::shared_ptr player) const { - // Delete NPCs. + for (auto layer : tiles.getUsedTileLayers()) { - // Remove every NPC in the level. - for (auto& levelNPC: m_npcs) - { - // TODO(joey): we need to delete putnpc's, and move db-npcs to a different level - if (auto npc = m_server->getNPC(levelNPC); npc && npc->getType() == NPCType::LEVELNPC) - m_server->deleteNPC(npc, false); - } - m_npcs.clear(); + if (layer == 0) continue; + sendBoardLayerToPlayer(player, layer); } +} - // Delete baddies. - m_baddies.clear(); - m_baddyIdGenerator.resetAndSetNext(BADDYID_INIT); +void StaticLevelData::sendBoardLayerToPlayer(std::shared_ptr player, size_t layer) const +{ + CString retVal; + retVal.writeGChar(PLO_BOARDLAYER); - // Delete chests. - m_chests.clear(); + // TODO: Only send the tiles that has been placed on the layer + retVal << (char)layer << (char)0 << (char)0 << (char)64 << (char)64; - // Delete links. - m_links.clear(); + tiles.writeLayerToPacket(layer, retVal); - // Delete signs. - m_signs.clear(); + // The +1 is the \n at the end of the packet. + player->sendPacket(CString() >> (char)PLO_RAWDATA >> (int)(retVal.length() + 1)); + player->sendPacket(retVal); +} - // Delete items. - for (auto& item: m_items) +void StaticLevelData::sendChestsToPlayer(std::shared_ptr player) const +{ + CString packet; + for (auto& chest : chests) { - CString packet = CString() >> (char)PLO_ITEMDEL >> (char)(item.getX() * 2) >> (char)(item.getY() * 2); - for (auto& player: m_players) - { - if (auto p = m_server->getPlayer(player); p) - p->sendPacket(packet); - } + bool hasChest = player->account.hasChest(levelName, chest.position); + + packet.clear(); + packet >> (char)PLO_LEVELCHEST >> (char)(hasChest ? 1 : 0) >> (char)chest.position.x() >> (char)chest.position.y(); + if (!hasChest) packet >> (char)chest.item >> (char)chest.sign; + player->sendPacket(packet); } - m_items.clear(); +} - // Delete board changes. - m_boardChanges.clear(); +void StaticLevelData::sendLinksToPlayer(std::shared_ptr player, bool onlyMapLinks) const +{ + CString packet; + for (const auto& link : links) + { + if (onlyMapLinks && !link.isProbableMapLink()) + continue; - // TODO: Warp players out? + packet.clear(); + packet >> (char)PLO_LEVELLINK << link.getLinkStr(); + player->sendPacket(packet); + } +} -#ifdef V8NPCSERVER - if (m_scriptObject) +void StaticLevelData::sendSignsToPlayer(std::shared_ptr player) const +{ + CString packet; + for (const auto& sign : signs) { - m_scriptObject.reset(); + packet.clear(); + packet >> (char)PLO_LEVELSIGN << sign.getSignPacket(player.get()); + player->sendPacket(packet); } -#endif } -/* - Level: Get Crafted Packets -*/ -CString Level::getBaddyPacket(int clientVersion) +//---------------------------- + +PixelRectangleArea SubLevel::clipRectangleToPart(const PixelRectangleArea& area) const noexcept { - CString retVal; - for (const auto& [id, baddy]: m_baddies) + PixelRectangleArea result{area}; + PixelRectangleArea localRect{{0, 0}, {1024, 1024}}; + if (mapPosition.has_value()) + localRect.position.translate(mapPosition.value().x() * 1024, mapPosition.value().y() * 1024); + + if (localRect.right() < area.left() || localRect.left() > area.right()) { - assert(baddy != nullptr); - if (baddy == nullptr) - continue; + result.position.x() = 0; + result.size.width() = 0; + } + else + { + if (localRect.left() > area.left()) + { + auto diff = localRect.left() - area.left(); + result.position.x() = localRect.position.x(); + result.size.width() = static_cast(area.size.width() - diff); + } + if (localRect.right() < area.right()) + { + auto diff = area.right() - localRect.right(); + result.position.x() = area.position.x(); + result.size.width() = static_cast(area.size.width() - diff); + } + } - //if (baddy->getProp(BDPROP_MODE).readGChar() != BDMODE_DIE) - retVal >> (char)PLO_BADDYPROPS >> (char)baddy->getId() << baddy->getProps(clientVersion) << "\n"; + if (localRect.bottom() < area.top() || localRect.top() > area.bottom()) + { + result.position.y() = 0; + result.size.height() = 0; + } + else + { + if (localRect.top() > area.top()) + { + auto diff = localRect.top() - area.top(); + result.position.y() = localRect.position.y(); + result.size.height() = static_cast(area.size.height() - diff); + } + if (localRect.bottom() < area.bottom()) + { + auto diff = area.bottom() - localRect.bottom(); + result.position.y() = area.position.y(); + result.size.height() = static_cast(area.size.height() - diff); + } } - return retVal; + + return result; } -CString Level::getBoardPacket() +WholeTileRectangleArea SubLevel::clipRectangleToPart(const WholeTileRectangleArea& area) const noexcept { - CString retVal; - retVal.writeGChar(PLO_BOARDPACKET); - retVal.write((char*)m_tiles[0], sizeof(short[4096])); - retVal << "\n"; + WholeTileRectangleArea result{area}; + WholeTileRectangleArea localRect{{0, 0}, {64, 64}}; + if (mapPosition.has_value()) + localRect.position.translate(mapPosition.value().x() * 64, mapPosition.value().y() * 64); + + if (localRect.right() < area.left() || localRect.left() > area.right()) + { + result.position.x() = 0; + result.size.width() = 0; + } + else + { + if (localRect.left() > area.left()) + { + auto diff = localRect.left() - area.left(); + result.position.x() = localRect.position.x(); + result.size.width() = static_cast(area.size.width() - diff); + } + if (localRect.right() < area.right()) + { + auto diff = area.right() - localRect.right(); + result.position.x() = area.position.x(); + result.size.width() = static_cast(area.size.width() - diff); + } + } + + if (localRect.bottom() < area.top() || localRect.top() > area.bottom()) + { + result.position.y() = 0; + result.size.height() = 0; + } + else + { + if (localRect.top() > area.top()) + { + auto diff = localRect.top() - area.top(); + result.position.y() = localRect.position.y(); + result.size.height() = static_cast(area.size.height() - diff); + } + if (localRect.bottom() < area.bottom()) + { + auto diff = area.bottom() - localRect.bottom(); + result.position.y() = area.position.y(); + result.size.height() = static_cast(area.size.height() - diff); + } + } - return retVal; + return result; } -CString Level::getLayerPacket(int layer) +std::optional SubLevel::getTiles() noexcept { - CString retVal; - retVal.writeGChar(PLO_BOARDLAYER); - - // TODO: Only send the tiles that has been placed on the layer - retVal << (char)layer << (char)0 << (char)0 << (char)64 << (char)64; - retVal.write((char*)m_tiles[layer], sizeof(short[4096])); - retVal << "\n"; + // Get the tiles. + LevelTiles* tiles = nullptr; + if (instancedTileUpdates.has_value()) + tiles = &instancedTileUpdates.value(); + else if (auto sdata = staticData.lock(); sdata != nullptr) + tiles = &sdata->tiles; + + // Make sure we found tiles. + if (tiles == nullptr) + return std::nullopt; + + return tiles; +} - return retVal; +std::optional SubLevel::getTiles() const noexcept +{ + // Get the tiles. + const LevelTiles* tiles = nullptr; + if (instancedTileUpdates.has_value()) + tiles = &instancedTileUpdates.value(); + else if (auto sdata = staticData.lock(); sdata != nullptr) + tiles = &sdata->tiles; + + // Make sure we found tiles. + if (tiles == nullptr) + return std::nullopt; + + return tiles; } -CString Level::getBoardChangesPacket(time_t time) +std::optional SubLevel::getTiles(size_t layer) noexcept { - CString retVal; - retVal >> (char)PLO_LEVELBOARD; - for (const auto& change: m_boardChanges) - { - if (change.getModTime() >= time) - retVal << change.getBoardStr(); - } - return retVal; + // Get the tiles. + auto tiles = getTiles(); + if (!tiles.has_value()) + return std::nullopt; + + // Try to get the tiles for the specified layer. + auto tileLayer = tiles.value()->getLayer(layer); + if (tileLayer.has_value() && tileLayer.value() != nullptr) + return tileLayer.value(); + + return std::nullopt; } -CString Level::getBoardChangesPacket2(time_t time) +std::optional SubLevel::getTiles(size_t layer) const noexcept { - CString retVal; - retVal >> (char)PLO_BOARDMODIFY; - for (const auto& change: m_boardChanges) - { - if (change.getModTime() >= time) - retVal << change.getBoardStr(); - } - return retVal; + // Get the tiles. + auto tiles = getTiles(); + if (!tiles.has_value()) + return std::nullopt; + + // Try to get the tiles for the specified layer. + auto tileLayer = tiles.value()->getLayer(layer); + if (tileLayer.has_value() && tileLayer.value() != nullptr) + return tileLayer.value(); + + return std::nullopt; } -CString Level::getChestPacket(Player* pPlayer) +double SubLevel::getHeightAt(const LocalPixelPosition& position) const noexcept { - CString retVal; + if (!terrain.has_value() || terrain.value().heightmap.empty()) + return 0.0; + + auto tilePosition = toTilePosition(position); + + // Determine the origin tile for our calculation. + // This will be the top-left tile within the quad we are calculating the height for. + LocalWholeTilePosition originTile = toLocalWholeTilePosition(tilePosition); + if (tilePosition.x() > 64) originTile.x() = 64; + if (tilePosition.y() > 64) originTile.y() = 64; - if (pPlayer) + auto heightAtPosition = [&](const TilePosition& pos) -> double { - for (auto& chest: m_chests) - { - bool hasChest = pPlayer->hasChest(getChestStr(chest.get())); + return terrain.value().heightmap[static_cast(pos.y()) * 65 + pos.x()]; + }; - retVal >> (char)PLO_LEVELCHEST >> (char)(hasChest ? 1 : 0) >> (char)chest->getX() >> (char)chest->getY(); - if (!hasChest) retVal >> (char)chest->getItemIndex() >> (char)chest->getSignIndex(); - retVal << "\n"; - } - } + // Generate 3D coordinates for our tiles. + TilePosition topLeft = toTilePosition(originTile); + TilePosition topRight = toTilePosition(translatePosition(originTile, 1_ui8, 0_ui8)); + TilePosition bottomLeft = toTilePosition(translatePosition(originTile, 0_ui8, 1_ui8)); + topLeft.z() = heightAtPosition(topLeft); + topRight.z() = heightAtPosition(topRight); + bottomLeft.z() = heightAtPosition(bottomLeft); + + // Calculate our direction vectors. + Position vecU = topRight - topLeft; + Position vecV = bottomLeft - topLeft; - return retVal; + // Determine our tile offset. + TilePosition offset{tilePosition.x() - topLeft.x(), tilePosition.y() - topLeft.y()}; + + // Calculate our point using the offset along the direction vectors. + TilePosition point = topLeft + (vecU * offset.x()) + (vecV * offset.y()); + + return point.z(); } -CString Level::getHorsePacket() +void SubLevel::sendBoardToPlayer(std::shared_ptr player) const { + auto tiles = getTiles(); + if (!tiles.has_value()) + return; + CString retVal; - for (auto& horse: m_horses) - { - retVal >> (char)PLO_HORSEADD << horse.getHorseStr() << "\n"; - } + retVal.writeGChar(PLO_BOARDPACKET); + tiles.value()->writeLayerToPacket(0, retVal); - return retVal; + player->sendPacket(CString() >> (char)PLO_RAWDATA >> (int)((1 + (64 * 64 * 2) + 1))); + player->sendPacket(retVal); } -CString Level::getLinksPacket() +void SubLevel::sendBoardLayersToPlayer(std::shared_ptr player) const { - CString retVal; - for (const auto& link: m_links) + auto tiles = getTiles(); + if (!tiles.has_value()) + return; + + for (auto layer : tiles.value()->getUsedTileLayers()) { - retVal >> (char)PLO_LEVELLINK << link->getLinkStr() << "\n"; + if (layer == 0) continue; + sendBoardLayerToPlayer(player, layer); } +} + +void SubLevel::sendBoardLayerToPlayer(std::shared_ptr player, size_t layer) const +{ + auto tiles = getTiles(); + if (!tiles.has_value()) + return; + + CString retVal; + retVal.writeGChar(PLO_BOARDLAYER); + + // TODO: Only send the tiles that has been placed on the layer + retVal << (char)layer << (char)0 << (char)0 << (char)64 << (char)64; + + tiles.value()->writeLayerToPacket(layer, retVal); - return retVal; + // The +1 is the \n at the end of the packet. + player->sendPacket(CString() >> (char)PLO_RAWDATA >> (int)(retVal.length() + 1)); + player->sendPacket(retVal); } -CString Level::getNpcsPacket(time_t time, int clientVersion) +void SubLevel::sendBoardHeightsToPlayer(std::shared_ptr player) const { + // We only need to send heights if there are level overrides. + if (!mapPosition.has_value() || !terrain.has_value() || terrain.value().levelHeightOverrides.empty()) + return; + CString retVal; - for (auto& npcId: m_npcs) - { - auto npc = m_server->getNPC(npcId); - if (!npc) continue; + retVal.writeGChar(PLO_BOARDHEIGHTS); + + // Maybe the map position? + retVal >> (char)mapPosition.value().x() >> (char)mapPosition.value().y(); + + // Starting x/y index of the heightmap block. + retVal >> (char)0 >> (char)0; - retVal >> (char)PLO_NPCPROPS >> (int)npc->getId() << npc->getProps(time, clientVersion) << "\n"; + // Width/height of the heightmap block. + // 0 indexed for some reason so use 8 instead of 9. + retVal >> (char)8 >> (char)8; - if (clientVersion >= CLVER_4_0211 && !npc->getByteCode().isEmpty()) + // The heightmap data. + for (size_t y = 0; y < 9; ++y) + { + for (size_t x = 0; x < 9; ++x) { - CString byteCodePacket = CString() >> (char)PLO_NPCBYTECODE >> (int)npc->getId() << npc->getByteCode(); - if (byteCodePacket[byteCodePacket.length() - 1] != '\n') - byteCodePacket << "\n"; + auto index = y * 9 + x; + auto height = terrain.value().levelHeightOverrides[index]; + + // The whole number and fractional part of the height are stored separately. + // The whole number is offset by 50, giving a range of -50 to +170. + // The fractional part is multiplied by 128 and stored as a byte. + double decimal = height - std::floor(height); + double whole = std::round(height - decimal); - retVal >> (char)PLO_RAWDATA >> (int)byteCodePacket.length() << "\n"; - retVal << byteCodePacket; + uint8_t wholePart = static_cast(whole + 50); + uint8_t decimalPart = static_cast(decimal * 128); + + retVal >> wholePart; + retVal >> decimalPart; } } - return retVal; + player->sendPacket(retVal); } -CString Level::getSignsPacket(Player* pPlayer = 0) +void SubLevel::sendBoardChangesToPlayer(std::shared_ptr player, std::optional time) const { - CString retVal; - for (const auto& sign: m_signs) + if (player == nullptr) + return; + + // Determine the style of board changes to send. + // 0 = PLO_BOARDMODIFY2 with pixel position (bad) + // 1 = PLO_BOARDMODIFY2 with map position + // 2 = PLO_LEVELBOARD with batched changes + // 3 = PLO_BOARDMODIFY + int style = 1; + if (player->getVersion() < CLVER_2_1) + style = 3; + else if (player->getVersion() < CLVER_4_0211) + style = mapPosition.has_value() ? 1 : 3; // 2; + + // The batched board changes seem to be sent when the player enters a level that it has cached. + // TODO: The current level sending implementation doesn't easily allow use to use this right now, so send individual changes (it won't hurt things). + if (style == 2) + { + CString retVal; + retVal >> (char)PLO_LEVELBOARD; + for (const auto& change : boardChanges) + { + if (!time.has_value() || change.modTime >= time.value()) + retVal << change.getPropsForSingleLevel(); + } + if (retVal.length() > 1) + player->sendPacket(retVal); + return; + } + + // Send all board changes. + for (const auto& change : boardChanges) { - retVal >> (char)PLO_LEVELSIGN << sign->getSignStr(pPlayer) << "\n"; + /* + if (style == 0) + player->sendPacket(CString() >> (char)PLO_BOARDMODIFY2 << change.getPropsForMapNewMain()); + else + */ + if (style == 1) + player->sendPacket(CString() >> (char)PLO_BOARDMODIFY2 << change.getPropsForMapClassic()); + else if (style == 3) + player->sendPacket(CString() >> (char)PLO_BOARDMODIFY << change.getPropsForSingleLevel()); } - return retVal; } -int Level::getGmapX() const -{ - if (auto map = m_map.lock(); map && map->isGmap()) - return m_mapX; - return 0; -} +//---------------------------- -int Level::getGmapY() const +Level::Level() { - if (auto map = m_map.lock(); map && map->isGmap()) - return m_mapY; - return 0; + m_server = BabyDI::Get(); + assert(m_server != nullptr); } -/* - Level: Level-Loading Functions -*/ -bool Level::reload() +Level::~Level() { // Delete NPCs. - // Don't delete NPCs if this level is on a gmap! If we are on a gmap, just set them - // back to their original positions. { // Remove every NPC in the level. - for (auto it = m_npcs.begin(); it != m_npcs.end();) + for (auto& levelNPC : m_npcs) { - auto npc = m_server->getNPC(*it); - if (!npc || npc->getType() == NPCType::LEVELNPC) - { + auto npc = m_server->getNPC(levelNPC); + if (!npc) continue; + if (npc->storageType == NPCStorageType::LEVEL) m_server->deleteNPC(npc, false); - it = m_npcs.erase(it); - } - else - { -#ifdef V8NPCSERVER - npc->reloadNPC(); -#endif - it++; - } } + m_npcs.clear(); } - // Delete baddies. - m_baddies.clear(); - m_baddyIdGenerator.resetAndSetNext(BADDYID_INIT); + // Erase our levels from the gmap level list. + if (isGmap()) + { + auto& gmapLevels = m_server->getGmapLevelList(); + using GT = std::remove_cvref_t::value_type; + std::erase_if(gmapLevels, [this](const GT& pair) + { + return getSubLevelIndex(pair.first).has_value() && pair.second.expired(); + }); - // Delete chests. - m_chests.clear(); + // Create stubs for our levels so they can be reloaded later if needed. + // We need to do this for map levels because we many things refer to the sublevels by name, so we need to link them to a gmap. + auto stub = m_server->getStubbedLevel(levelName, groupMapName); + for (const auto& [levelName, position] : m_map->levels) + gmapLevels.insert({levelName, stub}); + } - // Delete links. - m_links.clear(); + // Delete shoots. + m_shoots.clear(); - // Delete signs. - m_signs.clear(); + // Delete arrows. + m_arrows.clear(); // Delete items. - for (const auto& item: m_items) - { - CString packet = CString() >> (char)PLO_ITEMDEL >> (char)(item.getX() * 2) >> (char)(item.getY() * 2); - for (auto& playerId: m_players) - { - if (auto player = m_server->getPlayer(playerId); player) - player->sendPacket(packet); - } - } + for (size_t i = m_items.size(); i > 0; --i) + removeItem(inform_client, i - 1); m_items.clear(); - // Delete board changes. - m_boardChanges.clear(); - - // Clean up the rest. - m_isSparringZone = false; - m_isSingleplayer = false; - - // Remove all the players from the level. - std::deque oldplayers = m_players; - for (auto& id: oldplayers) + // Delete sub level data. + for (auto& subLevel : m_levelParts) { - if (auto p = m_server->getPlayer(id); p) - p->leaveLevel(true); + subLevel->boardChanges.clear(); + subLevel->instancedTileUpdates.reset(); + subLevel->scriptUpdatedTiles.reset(); + subLevel->isNoPkZone = false; + subLevel->isSparringZone = false; } - // Reset the level cache for all the players on the server. - auto& playerList = m_server->getPlayerList(); - for (auto& [id, p]: playerList) - { - p->resetLevelCache(this); - } + // Warp players out. + for (const auto& playerId : m_players) + m_server->warpPlayerToSafePlace(playerId); +} - // Re-load the level now. - bool ret = loadLevel(m_levelName); +//---------------------------- - // Warp all players back to the level (or to unstick me if loadLevel failed). - CString uLevel = m_server->getSettings().getStr("unstickmelevel", "onlinestartlocal.nw"); - float uX = m_server->getSettings().getFloat("unstickmex", 30.0f); - float uY = m_server->getSettings().getFloat("unstickmey", 35.0f); - for (auto& id: oldplayers) - { - if (auto p = m_server->getPlayer(id); p) - p->warp((ret ? m_levelName : uLevel), (ret ? p->getX() : uX), (ret ? p->getY() : uY)); - } +std::shared_ptr Level::createLevel(std::string_view levelName) +{ + auto server = BabyDI::Get(); - return ret; -} + auto level = std::shared_ptr(new Level()); + level->levelName = levelName; -std::shared_ptr Level::clone() const -{ - Level* level = new Level(); - if (!level->loadLevel(m_levelName)) + if (!levelName.empty()) { - delete level; - return nullptr; + auto& levelList = server->getLevelList(); + levelList.insert(std::make_pair(string::toLower(levelName), level)); } - return level->shared_from_this(); + return level; } -bool Level::loadLevel(const CString& pLevelName) +std::shared_ptr Level::clone(LevelPtr level, std::string_view name) { -#ifdef V8NPCSERVER - m_server->getScriptEngine()->wrapScriptObject(this); -#endif - - CString ext(getExtension(pLevelName)); - if (ext == ".nw") return loadNW(pLevelName); - else if (ext == ".graal") - return loadGraal(pLevelName); - else if (ext == ".zelda") - return loadZelda(pLevelName); - else - return detectLevelType(pLevelName); + if (level == nullptr) return nullptr; + /* + * TODO: The level needs to be stubbed, and the new name has to be set, without being overwritten. + * If not, then serverside NPCs are going to muck everything up when they try to register to the level. + auto server = BabyDI::Get(); + auto cloned = server->getStubbedLevel(name); + LevelLoader::loadLevelInto(cloned, std::filesystem::path{ level->levelName }); + cloned->levelName = name; + cloned->m_filePath = level->m_filePath.parent_path() / name; + */ + return nullptr; } -bool Level::detectLevelType(const CString& pLevelName) -{ - // Get the appropriate filesystem. - FileSystem* fileSystem = m_server->getFileSystem(); - if (!m_server->getSettings().getBool("nofoldersconfig", false)) - fileSystem = m_server->getFileSystem(FS_LEVEL); +//---------------------------- - // Load file - CString fileData; - if (!fileData.load(fileSystem->find(pLevelName))) +bool Level::reload(std::string_view levelName) +{ + auto staticData = getStaticLevelDataByName(levelName); + if (staticData == nullptr) return false; - // Grab file version. - m_fileVersion = fileData.readChars(8); - - // Determine the level type. - int v = -1; - if (m_fileVersion == "GLEVNW01") v = 0; - else if (m_fileVersion == "GR-V1.03" || m_fileVersion == "GR-V1.02" || m_fileVersion == "GR-V1.01") - v = 1; - else if (m_fileVersion == "Z3-V1.04" || m_fileVersion == "Z3-V1.03") - v = 2; - - // Not a level. - if (v == -1) return false; - - // Load the correct level. - if (v == 0) return loadNW(pLevelName); - if (v == 1) return loadGraal(pLevelName); - if (v == 2) return loadZelda(pLevelName); - return false; + StaticLevelData::reload(staticData); + return true; } -bool Level::loadZelda(const CString& pLevelName) +bool Level::reload(const MapPosition& position) { - // Get the appropriate filesystem. - FileSystem* fileSystem = m_server->getFileSystem(); - if (!m_server->getSettings().getBool("nofoldersconfig", false)) - fileSystem = m_server->getFileSystem(FS_LEVEL); - - // Path-To-File - m_actualLevelName = m_levelName = pLevelName; - m_fileName = fileSystem->find(pLevelName); - m_modTime = fileSystem->getModTime(pLevelName); - - // Load file - CString fileData; - if (!fileData.load(m_fileName)) return false; + auto staticData = getStaticLevelDataAtPosition(position); + if (staticData == nullptr) + return false; - // Grab file version. - m_fileVersion = fileData.readChars(8); + StaticLevelData::reload(staticData); + return true; +} - // Check if it is actually a .graal level. The 1.39-1.41r1 client actually - // saved .zelda as .graal. - if (m_fileVersion.subString(0, 2) == "GR") - return loadGraal(pLevelName); +void Level::reload(StaticLevelDataPtr staticData) +{ + if (staticData == nullptr) + return; - int v = -1; - if (m_fileVersion == "Z3-V1.03") v = 3; - else if (m_fileVersion == "Z3-V1.04") - v = 4; - if (v == -1) return false; + auto mapPosition = getSubLevelPositionInMap(staticData->levelName); + auto subLevelIndex = getMapIndexAtPosition(mapPosition.value_or(MapPosition{})); + auto oldSubLevel = m_levelParts.size() > subLevelIndex ? m_levelParts[subLevelIndex] : nullptr; - // Load tiles. + // Delete arrows. + for (auto it = m_arrows.begin(); it != m_arrows.end();) { - int bits = (v > 4 ? 13 : 12); - int read = 0; - unsigned int buffer = 0; - unsigned short code = 0; - short tiles[2] = { -1, -1 }; - int boardIndex = 0; - int count = 1; - bool doubleMode = false; - - // Read the tiles. - while (boardIndex < 64 * 64 && fileData.bytesLeft() != 0) + auto& arrow = *it; + if (toMapPosition(arrow.position) == mapPosition) { - // Every control code/tile is either 12 or 13 bits. WTF. - // Read in the bits. - while (read < bits) - { - buffer += ((unsigned char)fileData.readChar()) << read; - read += 8; - } - - // Pull out a single 12/13 bit code from the buffer. - code = buffer & (bits == 12 ? 0xFFF : 0x1FFF); - buffer >>= bits; - read -= bits; - - // See if we have an RLE control code. - // Control codes determine how the RLE scheme works. - if (code & (bits == 12 ? 0x800 : 0x1000)) - { - // If the 0x100 bit is set, we are in a double repeat mode. - // {double 4}56 = 56565656 - if (code & 0x100) doubleMode = true; - - // How many tiles do we count? - count = code & 0xFF; - continue; - } - - // If our count is 1, just read in a tile. This is the default mode. - if (count == 1) - { - m_tiles[0][boardIndex++] = (short)code; - continue; - } - - // If we reach here, we have an RLE scheme. - // See if we are in double repeat mode or not. - if (doubleMode) - { - // Read in our first tile. - if (tiles[0] == -1) - { - tiles[0] = (short)code; - continue; - } - - // Read in our second tile. - tiles[1] = (short)code; - - // Add the tiles now. - for (int i = 0; i < count && boardIndex < 64 * 64 - 1; ++i) - { - m_tiles[0][boardIndex++] = tiles[0]; - m_tiles[0][boardIndex++] = tiles[1]; - } + it = m_arrows.erase(it); + continue; + } + ++it; + } - // Clean up. - tiles[0] = tiles[1] = -1; - doubleMode = false; - count = 1; - } - // Regular RLE scheme. - else - { - for (int i = 0; i < count && boardIndex < 64 * 64; ++i) - m_tiles[0][boardIndex++] = (short)code; - count = 1; - } + // Delete bombs. + for (auto it = m_bombs.begin(); it != m_bombs.end();) + { + auto& bomb = *it; + if (toMapPosition(bomb.position) == mapPosition) + { + it = m_bombs.erase(it); + continue; } + ++it; } - // Load the links. + // Delete explosions. + for (auto it = m_explosions.begin(); it != m_explosions.end();) { - while (fileData.bytesLeft()) + auto& explosion = *it; + if (toMapPosition(explosion.position) == mapPosition) { - CString line = fileData.readString("\n"); - if (line.length() == 0 || line == "#") break; + it = m_explosions.erase(it); + continue; + } + ++it; + } - // Assemble the level string. - std::vector vline = line.tokenize(); - CString level = vline[0]; - if (vline.size() > 7) - { - for (size_t i = 0; i < vline.size() - 7; ++i) - level << " " << vline[1 + i]; - } + // Delete horses. + for (auto it = m_horses.begin(); it != m_horses.end();) + { + auto& horse = *it; + if (toMapPosition(horse.position) == mapPosition) + { + it = m_horses.erase(it); + continue; + } + ++it; + } - if (fileSystem->find(level).isEmpty()) - continue; + // Delete items. + for (size_t i = m_items.size(); i > 0; --i) + { + if (toMapPosition(m_items[i].position) == mapPosition) + removeItem(inform_client, i - 1); + } - addLink(vline); + // Delete shoots. + for (auto it = m_shoots.begin(); it != m_shoots.end();) + { + auto& shoot = *it; + if (toMapPosition(shoot.position) == mapPosition) + { + it = m_shoots.erase(it); + continue; } + ++it; } - // Load the baddies. + // Delete NPCs. + // We want to delete them while players are still in the level so they get the appropriate delete packets. + // Older clients (1.x) may crash if things don't happen in the right order. + for (auto iter = m_npcs.begin(); iter != m_npcs.end();) { - while (fileData.bytesLeft()) + if (auto npc = m_server->getNPC(*iter); npc == nullptr || npc->storageType == NPCStorageType::LEVEL) { - signed char x = fileData.readChar(); - signed char y = fileData.readChar(); - signed char type = fileData.readChar(); - - // Ends with an invalid baddy. - if (x == -1 && y == -1 && type == -1) + if (npc && (!mapPosition.has_value() || npc->character.getMapPosition() == mapPosition)) { - fileData.readString("\n"); // Empty verses. - break; - } - - // Add the baddy. - LevelBaddy* baddy = addBaddy((float)x, (float)y, type); - if (baddy == nullptr) + m_server->deleteNPC(npc, false); + iter = m_npcs.erase(iter); continue; - - // Only v1.04+ baddies have verses. - if (v > 3) - { - // Load the verses. - std::vector bverse = fileData.readString("\n").tokenize("\\"); - CString props; - for (char j = 0; j < (char)bverse.size(); ++j) - props >> (char)(BDPROP_VERSESIGHT + j) >> (char)bverse[j].length() << bverse[j]; - if (props.length() != 0) baddy->setProps(props); } } + ++iter; } - // Load signs. + // Remove all the players from the level. + std::deque oldplayers = m_players; + for (auto& id : oldplayers) { - while (fileData.bytesLeft()) + if (auto p = m_server->getPlayer(id); p) { - CString line = fileData.readString("\n"); - if (line.length() == 0) break; - - signed char x = line.readGChar(); - signed char y = line.readGChar(); - CString text = line.readString(""); - - addSign(x, y, text, true); + if (p->getMapPosition() == mapPosition) + p->leaveLevel(); } } - return true; -} + // Clear the level cache for all players on the server. + // Make sure this always gets called AFTER we leave the level. + auto& playerList = m_server->getPlayerList(); + for (const auto& [id, p] : players_of_type(playerList)) + { + p->resetLevelCache(staticData.get()); + if (oldSubLevel != nullptr) + p->resetLevelCache(oldSubLevel.get()); + } -bool Level::loadGraal(const CString& pLevelName) -{ - // Get the appropriate filesystem. - FileSystem* fileSystem = m_server->getFileSystem(); - if (!m_server->getSettings().getBool("nofoldersconfig", false)) - fileSystem = m_server->getFileSystem(FS_LEVEL); - - // Path-To-File - m_actualLevelName = m_levelName = pLevelName; - m_fileName = fileSystem->find(pLevelName); - m_modTime = fileSystem->getModTime(pLevelName); - - // Load file - CString fileData; - if (!fileData.load(m_fileName)) return false; - - // Grab file version. - m_fileVersion = fileData.readChars(8); - int v = -1; - if (m_fileVersion == "GR-V1.00") v = 0; - else if (m_fileVersion == "GR-V1.01") - v = 1; - else if (m_fileVersion == "GR-V1.02") - v = 2; - else if (m_fileVersion == "GR-V1.03") - v = 3; - if (v == -1) return false; - - // Load tiles. - { - int bits = (v > 0 ? 13 : 12); - int read = 0; - unsigned int buffer = 0; - unsigned short code = 0; - short tiles[2] = { -1, -1 }; - int boardIndex = 0; - int count = 1; - bool doubleMode = false; - - // Read the tiles. - while (boardIndex < 64 * 64 && fileData.bytesLeft() != 0) - { - // Every control code/tile is either 12 or 13 bits. WTF. - // Read in the bits. - while (read < bits) - { - buffer += ((unsigned char)fileData.readChar()) << read; - read += 8; - } - - // Pull out a single 12/13 bit code from the buffer. - code = buffer & (bits == 12 ? 0xFFF : 0x1FFF); - buffer >>= bits; - read -= bits; + // Attach the static data to the level part. + // This will remove any existing static data association. + auto subLevel = LevelLoader::attachStaticDataToLevel(shared_from_this(), mapPosition, staticData); + if (subLevel == nullptr) + return; - // See if we have an RLE control code. - // Control codes determine how the RLE scheme works. - if (code & (bits == 12 ? 0x800 : 0x1000)) - { - // If the 0x100 bit is set, we are in a double repeat mode. - // {double 4}56 = 56565656 - if (code & 0x100) doubleMode = true; + // Bind listeners for level data changes. + subLevel->staticDataRefreshedHandle = staticData->onDataRefreshed.subscribe([weakSelf = std::weak_ptr(shared_from_this())](StaticLevelDataPtr staticData) + { + if (auto self = weakSelf.lock(); self != nullptr) + self->reload(staticData); + }); - // How many tiles do we count? - count = code & 0xFF; - continue; - } + // Replace the sub-level with the new one. + m_levelParts[subLevelIndex] = subLevel; - // If our count is 1, just read in a tile. This is the default mode. - if (count == 1) - { - m_tiles[0][boardIndex++] = (short)code; - continue; - } + // Load NPCs for the sub-level. + LevelLoader::loadStaticDataNPCs(shared_from_this(), mapPosition, staticData); - // If we reach here, we have an RLE scheme. - // See if we are in double repeat mode or not. - if (doubleMode) - { - // Read in our first tile. - if (tiles[0] == -1) - { - tiles[0] = (short)code; - continue; - } + // Warp all players back to the level. + for (auto& id : oldplayers) + { + if (auto p = m_server->getPlayer(id); p) + p->warp(shared_from_this(), p->getGlobalPosition()); + } +} - // Read in our second tile. - tiles[1] = (short)code; +bool Level::saveLevel(const MapPosition& mapPosition, std::string_view filename) +{ + const auto& [subLevel, staticData] = getSubLevelAndStaticDataAtPosition(mapPosition); + if (subLevel == nullptr || staticData == nullptr) + return false; - // Add the tiles now. - for (int i = 0; i < count && boardIndex < 64 * 64 - 1; ++i) - { - m_tiles[0][boardIndex++] = tiles[0]; - m_tiles[0][boardIndex++] = tiles[1]; - } + auto& fileSystem = m_server->getFileSystem(); - // Clean up. - tiles[0] = tiles[1] = -1; - doubleMode = false; - count = 1; - } - // Regular RLE scheme. - else - { - for (int i = 0; i < count && boardIndex < 64 * 64; ++i) - m_tiles[0][boardIndex++] = (short)code; - count = 1; - } - } - } + auto actualFilename = getFilename(filename); + auto path = fileSystem.findi(fs::FileCategory::LEVEL, actualFilename.toStringView()); - // Load the links. + if (path.empty()) { - while (fileData.bytesLeft()) + auto dirs = fileSystem.getManagedDirectories(fs::FileCategory::LEVEL); + auto iter = dirs.begin(); + if (iter == dirs.end()) { - CString line = fileData.readString("\n"); - if (line.length() == 0 || line == "#") break; + log::printLine(log::server, "** Error saving level: {}. No level directories are configured.", actualFilename); + return false; + } - // Assemble the level string. - std::vector vline = line.tokenize(); - CString level = vline[0]; - if (vline.size() > 7) - { - for (size_t i = 0; i < vline.size() - 7; ++i) - level << " " << vline[1 + i]; - } + path = std::filesystem::path{*iter} / actualFilename.toStringView(); + } - if (fileSystem->find(level).isEmpty()) - continue; + std::ofstream fileStream(path); - addLink(vline); - } - } + fileStream << "GLEVNW01" << std::endl; - // Load the baddies. + // Write tiles. + if (auto tiles = subLevel->getTiles(); tiles.has_value()) { - while (fileData.bytesLeft()) + for (const auto& layerIndex : tiles.value()->getUsedTileLayers()) { - signed char x = fileData.readChar(); - signed char y = fileData.readChar(); - signed char type = fileData.readChar(); + auto layer = tiles.value()->getLayer(layerIndex).value_or(nullptr); + if (layer == nullptr) + continue; - // Ends with an invalid baddy. - if (x == -1 && y == -1 && type == -1) + std::string data; + std::list> chunks; + for (int y = 0; y < 64; ++y) { - fileData.readString("\n"); // Empty verses. - break; - } + data.clear(); + chunks.clear(); + int currentStart = 0; - // Add the baddy. - LevelBaddy* baddy = addBaddy((float)x, (float)y, type); - if (baddy == nullptr) - continue; + // Separate each row into chunks of actually non-transparent tiles. + for (int x = 0; x < 64; ++x) + { + auto tile = layer->at(x + static_cast(y) * 64); + if (tile == constants::EmptyTileInLayer) + { + if (!data.empty()) + { + chunks.emplace_back(currentStart, data); + currentStart = x; + data.clear(); + } + + // Skip transparent tile + currentStart++; + continue; + } + + // Swap to big-endian for storage. + if constexpr (std::endian::native == std::endian::little) + { + // We only store the first 12 bits of the tile, so shift by 4 and swap to big-endian. + tile <<= 4; + tile = std::byteswap(tile); + } + + // Append the base64 encoded tile data. + std::span tileData{reinterpret_cast(&tile), sizeof(decltype(tile))}; + data += string::toBase64(tileData).substr(0, 2); + } + + // Write any remaining data as a chunk. + if (!data.empty()) + chunks.emplace_back(currentStart, data); - // Load the verses. - std::vector bverse = fileData.readString("\n").tokenize("\\"); - CString props; - for (char j = 0; j < (char)bverse.size(); ++j) - props >> (char)(BDPROP_VERSESIGHT + j) >> (char)bverse[j].length() << bverse[j]; - if (props.length() != 0) baddy->setProps(props); + // Write one BOARD entry for each chunk so transparent tile-data is culled. + for (const auto& chunk : chunks) + { + // BOARD x y width layer data + fileStream << std::format("BOARD {} {} {} {} {}", chunk.first, y, chunk.second.length() / 2, layerIndex, chunk.second) << std::endl; + } + } } } - // Load NPCs. + for (const auto& link : staticData->links) { - while (fileData.bytesLeft()) - { - CString line = fileData.readString("\n"); - if (line.length() == 0 || line == "#") break; + auto& bbox = link.getBoundingBox(); + fileStream << std::format("LINK {} {} {} {} {} {} {}", link.getDestinationLevel(), bbox.position.x(), bbox.position.y(), bbox.size.width(), bbox.size.height(), link.getDestinationX(), link.getDestinationY()) << std::endl; + } - signed char x = line.readGChar(); - signed char y = line.readGChar(); - CString image = line.readString("#"); - CString code = line.readString("").replaceAll("\xa7", "\n"); + for (const auto& sign : staticData->signs) + { + fileStream << std::format("SIGN {} {}", sign.getTileX(), sign.getTileY()) << std::endl; + fileStream << sign.text << std::endl; + fileStream << "SIGNEND" << std::endl; + } - auto npc = m_server->addNPC(image, code, x, y, this->shared_from_this(), true, false); - m_npcs.insert(npc->getId()); - } + for (const auto& chest : staticData->chests) + { + fileStream << std::format("CHEST {} {} {} {}", chest.position.x(), chest.position.y(), LevelItem::getItemName(chest.item), chest.sign) << std::endl; } - // Load chests. - if (v > 0) + for (const auto& baddy : staticData->baddies) { - while (fileData.bytesLeft()) - { - CString line = fileData.readString("\n"); - if (line.length() == 0 || line == "#") break; + fileStream << std::format("BADDY {} {} {}", baddy.getTileX(), baddy.getTileY(), PROPID(baddy.type)) << std::endl; - char x = line.readGChar(); - char y = line.readGChar(); - char item = line.readGChar(); - char signindex = line.readGChar(); + for (const auto& verse : baddy.verses) + fileStream << verse << std::endl; - addChest(x, y, LevelItemType(item), signindex); - } + fileStream << "BADDYEND" << std::endl; } - // Load signs. + for (const auto& npcId : m_npcs) { - while (fileData.bytesLeft()) - { - CString line = fileData.readString("\n"); - if (line.length() == 0) break; + auto npc = m_server->getNPC(npcId); + if (npc == nullptr || npc->storageType != NPCStorageType::LEVEL) + continue; - signed char x = line.readGChar(); - signed char y = line.readGChar(); - CString text = line.readString(""); + // Only include NPCs for this sub-level. + if (npc->character.getMapPosition() != mapPosition) + continue; - addSign(x, y, text, true); - } + // Empty image and characters are stored as "-". + std::string_view image = npc->image; + if (image.empty() || image == "#c#"sv) + image = "-"sv; + + fileStream << std::format("NPC {} {} {}", image, (npc->character.localPixelX / 16.0f), (npc->character.localPixelY / 16.0f)) << std::endl; + fileStream << string::trim(npc->getScript().getOriginalSource()) << std::endl; + fileStream << "NPCEND" << std::endl; } return true; } -bool Level::loadNW(const CString& pLevelName) -{ - // Get the appropriate filesystem. - FileSystem* fileSystem = m_server->getFileSystem(); - if (!m_server->getSettings().getBool("nofoldersconfig", false)) - fileSystem = m_server->getFileSystem(FS_LEVEL); - - // Path-To-File - m_actualLevelName = m_levelName = getFilename(pLevelName); - m_fileName = fileSystem->find(m_actualLevelName); - m_modTime = fileSystem->getModTime(m_actualLevelName); +//---------------------------- - // Load File - std::vector fileData = CString::loadToken(m_fileName, "\n", true); - if (fileData.empty()) - return false; - - // Grab File Version - m_fileVersion = fileData[0]; +void Level::doTimedEvents() +{ + const auto& now = m_server->getFrameStartTimeHighPrecision(); - // Parse Level - for (auto i = fileData.begin(); i != fileData.end(); ++i) + // Run board change events. + for (auto& part : m_levelParts | removeNulls) { - // Tokenize - std::vector curLine = i->tokenize(); - if (curLine.empty()) - continue; + for (auto& change : part->boardChanges) + change.update(now); + } - // Parse Each Type - if (curLine[0] == "BOARD") + // Run bomb events. + for (auto& bomb : m_bombs) bomb.timeout.update(now); + std::erase_if(m_bombs, [this](const LevelBomb& bomb) + { + bool exploded = !bomb.timeout.isRunning(); + if (exploded) { - if (curLine.size() != 6) - continue; - - int x, y, w, layer; - x = strtoint(curLine[1]); - y = strtoint(curLine[2]); - w = strtoint(curLine[3]); - layer = strtoint(curLine[4]); - - if (!inrange(x, 0, 64) || !inrange(y, 0, 64) || w <= 0 || x + w > 64) - continue; - - if (curLine[5].length() >= w * 2) + // Generate bomb explosions. + // Don't send to players as they should see the bomb already. + if (bomb.power != 2) + addExplosion(bomb.position, bomb.owner, 1, bomb.power); + else { - for (int ii = x; ii < x + w; ii++) - { - char left = curLine[5].readChar(); - char top = curLine[5].readChar(); - short tile = getBase64Position(left) << 6; - tile += getBase64Position(top); - m_tiles[layer][ii + y * 64] = tile; - } + addExplosion(bomb.position, bomb.owner, 4, bomb.power); + addExplosion(translatePosition(bomb.position, -32, -32), bomb.owner, 4, bomb.power); + addExplosion(translatePosition(bomb.position, 32, -32), bomb.owner, 4, bomb.power); + addExplosion(translatePosition(bomb.position, -32, 32), bomb.owner, 4, bomb.power); + addExplosion(translatePosition(bomb.position, 32, 32), bomb.owner, 4, bomb.power); } } - else if (curLine[0] == "CHEST") - { - if (curLine.size() != 5) - continue; + return exploded; + }); - LevelItemType itemType = LevelItem::getItemId(curLine[3].toString()); - if (itemType != LevelItemType::INVALID) - { - char chestx = strtoint(curLine[1]); - char chesty = strtoint(curLine[2]); - char signidx = strtoint(curLine[4]); - addChest(chestx, chesty, itemType, signidx); - } - } - else if (curLine[0] == "LINK") - { - if (curLine.size() < 8) - continue; + // Run explosion events. + for (auto& explosion : m_explosions) explosion.timeout.update(now); + std::erase_if(m_explosions, [](const LevelExplosion& explosion) + { + return !explosion.timeout.isRunning(); + }); - // Get link string. - std::vector::iterator i = curLine.begin(); - std::vector link(++i, curLine.end()); + // Run item events. + for (auto& item : m_items) item.timeout.update(now); + std::erase_if(m_items, [](const LevelItem& item) + { + return !item.timeout.isRunning(); + }); - // Find the whole level name. - CString level(link[0]); - if (link.size() > 7) - { - for (size_t i = 0; i < link.size() - 7; ++i) - level << " " << link[i + 1]; - } + // Run horse events. + for (auto& horse : m_horses) horse.timeout.update(now); + std::erase_if(m_horses, [](const LevelHorse& horse) + { + return !horse.timeout.isRunning(); + }); - if (fileSystem->find(level).isEmpty()) - continue; + // Run baddy events. + if (auto subLevel = getSubLevelAtPosition(MapPosition{}); subLevel != nullptr && !isGmap()) + { + for (auto& baddy : subLevel->baddies) + baddy.timeout.update(now); + } +} - addLink(link); - } - else if (curLine[0] == "NPC") - { - unsigned int offset = 0; - if (curLine.size() < 4) - continue; +void Level::doFrameEvents(precise_clock::time_point time) +{ + // Don't bother with shoot and arrow processing if we don't have an npc-server. + if (!m_server->hasNPCServer()) + return; - // Grab the image properties. - CString image(curLine[1]); - if (curLine.size() > 4) - { - offset = (int)curLine.size() - 4; - for (size_t i = 0; i < offset; ++i) - image << " " << curLine[i + 2]; - } + // Determine elapsed time. + auto elapsed = time - m_lastFrameTime; + m_frameEventDuration += elapsed; + m_lastFrameTime = time; - // Grab the NPC location. - float x = (float)strtofloat(curLine[2 + offset]); - float y = (float)strtofloat(curLine[3 + offset]); + // Count the iterations. + int iterations = m_frameEventDuration / 50ms; + if (iterations == 0) + return; - // Grab the NPC code. - CString code; - ++i; - while (i != fileData.end()) - { - if (*i == "NPCEND") break; - code << *i << "\n"; - ++i; - } - //printf( "image: %s, x: %.2f, y: %.2f, code: %s\n", image.text(), x, y, code.text() ); - // Add the new NPC. - auto npc = m_server->addNPC(image, code, x, y, this->shared_from_this(), true, false); - m_npcs.insert(npc->getId()); - } - else if (curLine[0] == "SIGN") - { - if (curLine.size() != 3) - continue; + // Subtract our iterations. + m_frameEventDuration -= iterations * 50ms; - int x = strtoint(curLine[1]); - int y = strtoint(curLine[2]); + // Run shoot events. + std::vector deletedItems; + for (size_t i = 0; i < m_shoots.size(); ++i) + { + if (!moveShoot(&m_shoots[i], iterations)) + deletedItems.push_back(i); + } + std::erase_if(m_shoots, [this, &deletedItems](const LevelShoot& shoot) + { + return std::find(deletedItems.begin(), deletedItems.end(), &shoot - &m_shoots[0]) != deletedItems.end(); + }); - // Grab the sign code. - CString text; - ++i; - while (i != fileData.end()) - { - if (*i == "SIGNEND") break; - text << *i << "\n"; - ++i; - } + // Run arrow events. + deletedItems.clear(); + for (size_t i = 0; i < m_arrows.size(); ++i) + { + if (!moveArrow(&m_arrows[i], iterations)) + deletedItems.push_back(i); + } + std::erase_if(m_arrows, [this, &deletedItems](const LevelArrow& arrow) + { + return std::find(deletedItems.begin(), deletedItems.end(), &arrow - &m_arrows[0]) != deletedItems.end(); + }); +} - // Add the new sign. - addSign(x, y, text); - } - else if (curLine[0] == "BADDY") - { - if (curLine.size() != 4) - continue; +//---------------------------- - int x = strtoint(curLine[1]); - int y = strtoint(curLine[2]); - int type = strtoint(curLine[3]); +std::generator Level::getBaddies() const noexcept +{ + if (isGmap()) + co_return; - // Add the baddy. - LevelBaddy* baddy = addBaddy((float)x, (float)y, type); - if (baddy == nullptr) - continue; + if (auto subLevel = getSubLevelAtPosition(MapPosition{}); subLevel != nullptr) + co_yield std::ranges::elements_of(subLevel->baddies); +} - // Load the verses. - std::vector bverse; - ++i; - while (i != fileData.end()) - { - if (*i == "BADDYEND") break; - bverse.push_back(*i); - ++i; - } - CString props; - for (char j = 0; j < (char)bverse.size(); ++j) - props >> (char)(BDPROP_VERSESIGHT + j) >> (char)bverse[j].length() << bverse[j]; - if (props.length() != 0) baddy->setProps(props); +std::generator Level::getChests() const noexcept +{ + if (!isGmap()) + { + if (m_levelParts.empty() || m_levelParts.at(0) == nullptr) + co_return; + if (auto sdata = m_levelParts.at(0)->staticData.lock(); sdata != nullptr) + co_yield std::ranges::elements_of(sdata->chests); + } + else + { + for (const auto& levelPtr : m_levelParts | removeNulls) + { + if (auto sdata = levelPtr->staticData.lock(); sdata != nullptr) + co_yield std::ranges::elements_of(sdata->chests); } - if (i == fileData.end()) break; } - - return true; } -/* - Level: Find Level -*/ -std::shared_ptr Level::findLevel(const CString& pLevelName, bool loadAbsolute) +std::generator Level::getLinks() const noexcept { - auto& levelList = m_server->getLevelList(); - - // TODO(joey): Maybe its time for a hashmap, even if a duplicate level name occurs - // this is still going to break on the first occurrence. - - // Find Appropriate Level by Name - CString levelName = pLevelName.toLower(); - for (auto& it: levelList) + if (!isGmap()) { - if (it->getLevelName().toLower() == levelName) - return it; + if (m_levelParts.empty() || m_levelParts.at(0) == nullptr) + co_return; + if (auto sdata = m_levelParts.at(0)->staticData.lock(); sdata != nullptr) + co_yield std::ranges::elements_of(sdata->links); } - - if (loadAbsolute) + else { - FileSystem* fileSystem = m_server->getFileSystem(); - if (!m_server->getSettings().getBool("nofoldersconfig", false)) - fileSystem = m_server->getFileSystem(FS_LEVEL); - - if (fileSystem->find(pLevelName).trim().length() == 0) + for (const auto& levelPtr : m_levelParts | removeNulls) { - fileSystem->addFile(pLevelName); - fileSystem->addDir(getPath(pLevelName), "*", true); + if (auto sdata = levelPtr->staticData.lock(); sdata != nullptr) + co_yield std::ranges::elements_of(sdata->links); } } +} - // Load New Level - auto level = std::shared_ptr(new Level()); - if (!level->loadLevel(pLevelName)) - return nullptr; - - auto& mapList = m_server->getMapList(); - for (const auto& map: mapList) +std::generator Level::getSigns() const noexcept +{ + if (!isGmap()) { - int mx, my; - if (map->isLevelOnMap(levelName.text(), mx, my)) + if (m_levelParts.empty() || m_levelParts.at(0) == nullptr) + co_return; + if (auto sdata = m_levelParts.at(0)->staticData.lock(); sdata != nullptr) + co_yield std::ranges::elements_of(sdata->signs); + } + else + { + for (const auto& levelPtr : m_levelParts | removeNulls) { - level->setMap(map, mx, my); - break; + if (auto sdata = levelPtr->staticData.lock(); sdata != nullptr) + co_yield std::ranges::elements_of(sdata->signs); } } - - // Return Level - levelList.push_back(level); - return level; } -/* - Level: Create Level -*/ -std::shared_ptr Level::createLevel(short fillTile, const std::string& levelName) +std::generator> Level::getSignPositions() const noexcept { - auto& levelList = m_server->getLevelList(); - - // Load New Level - auto level = std::shared_ptr(new Level(fillTile)); - level->setLevelName(levelName); - -#ifdef V8NPCSERVER - m_server->getScriptEngine()->wrapScriptObject(level.get()); -#endif - - // Return Level - levelList.push_back(level); - return level; + if (!isGmap()) + { + if (m_levelParts.empty() || m_levelParts.at(0) == nullptr) + co_return; + if (auto sdata = m_levelParts.at(0)->staticData.lock(); sdata != nullptr) + { + for (const auto& sign : sdata->signs) + co_yield std::make_pair(&sign, WholeTilePosition{(uint16_t)sign.getTileX(), (uint16_t)sign.getTileY()}); + } + } + else + { + for (const auto& levelPtr : m_levelParts | removeNulls) + { + if (auto sdata = levelPtr->staticData.lock(); sdata != nullptr) + { + auto origin = toWholeTilePosition(getSubLevelOrigin(levelPtr).value_or(PixelPosition{})); + for (const auto& sign : sdata->signs) + co_yield std::make_pair(&sign, translatePosition(origin, (uint16_t)sign.getTileX(), (uint16_t)sign.getTileY())); + } + } + } } -/* - Level: Save Level -*/ -void Level::saveLevel(const std::string& filename) +size_t Level::getBaddyCount() const noexcept { - FileSystem* fileSystem = m_server->getFileSystem(); - if (!m_server->getSettings().getBool("nofoldersconfig", false)) - fileSystem = m_server->getFileSystem(FS_LEVEL); + if (isGmap()) + return 0; - auto actualFilename = getFilename(filename); + if (auto subLevel = getSubLevelAtPosition(MapPosition{}); subLevel != nullptr) + return subLevel->baddies.size(); - auto path = fileSystem->findi(actualFilename); + return 0; +} - if (path == "") +size_t Level::getChestCount() const noexcept +{ + if (!isGmap()) { - path << fileSystem->getDirByExtension(getExtension(actualFilename).text()); - path << actualFilename; - - fileSystem->addFile(path); + if (m_levelParts.empty() || m_levelParts.at(0) == nullptr) + return 0; + if (auto sdata = m_levelParts.at(0)->staticData.lock(); sdata != nullptr) + return sdata->chests.size(); } - - std::ofstream fileStream(path.text()); - - fileStream << "GLEVNW01" << std::endl; - - // white space separator - std::string s = " "; - // write tiles - for (int layer = 0; layer < getLayers().size(); layer++) + else { - auto& tiles = getTiles(layer); - for (int y = 0; y < 64 /*tiles.get_height()*/; y++) - { - std::string data; - // chunk start, chunk data pairs - std::list> chunks; - /* Separate each row into chunks of actually non-transparent tiles. - * Every time we encounter a transparent tile, flush the current data - * into the chunk list and clear it. If we never encounter a transparent - * tile, flush the entire data after the loop */ - int currentStart = 0; - for (int x = 0; x < 64 /*tiles.get_width()*/; x++) - { - auto tile = tiles[x + y * 64]; - if (tile == -2) - { - if (!data.empty()) - { - chunks.emplace_back(currentStart, data); - currentStart = x; - data.clear(); - } - - // Skip transparent tile - currentStart++; - continue; - } - - data += CString::formatBase64(tile); - } - if (!data.empty()) - chunks.emplace_back(currentStart, data); + size_t result = 0; - /* Draw one BOARD entry for each chunk so transparent tile-data is culled */ - for (const auto& chunk: chunks) - { - fileStream << "BOARD" << s << chunk.first << s << y << s << chunk.second.length() / 2 << s << layer // x, y, width, layer - << s << chunk.second << std::endl; - } + for (const auto& levelPtr : m_levelParts | removeNulls) + { + if (auto sdata = levelPtr->staticData.lock(); sdata != nullptr) + result += sdata->chests.size(); } + + return result; } - for (const auto& link: getLinks()) + return 0; +} + +size_t Level::getLinkCount() const noexcept +{ + if (!isGmap()) { - fileStream << "LINK" << s << link->getNewLevel().text() << s << link->getX() << s << link->getY() - << s << link->getWidth() << s << link->getHeight() << s << link->getNewX().text() - << s << link->getNewY().text() << std::endl; + if (m_levelParts.empty() || m_levelParts.at(0) == nullptr) + return 0; + if (auto sdata = m_levelParts.at(0)->staticData.lock(); sdata != nullptr) + return sdata->links.size(); } - - for (const auto& sign: getSigns()) + else { - fileStream << "SIGN" << s << sign->getX() << s << sign->getY() << std::endl; - fileStream << sign->getUText().text() << std::endl; - fileStream << "SIGNEND" << std::endl; + size_t result = 0; + + for (const auto& levelPtr : m_levelParts | removeNulls) + { + if (auto sdata = levelPtr->staticData.lock(); sdata != nullptr) + result += sdata->links.size(); + } + + return result; } - for (const auto& chest: getChests()) + return 0; +} + +size_t Level::getSignCount() const noexcept +{ + if (!isGmap()) { - fileStream << "CHEST" << s << chest->getX() << s << chest->getY() << s << LevelItem::getItemName(chest->getItemIndex()) << s << chest->getSignIndex() << std::endl; + if (m_levelParts.empty() || m_levelParts.at(0) == nullptr) + return 0; + if (auto sdata = m_levelParts.at(0)->staticData.lock(); sdata != nullptr) + return sdata->signs.size(); } - - for (const auto& baddy: m_baddies) + else { - fileStream << "BADDY" << s << baddy.second->getX() << s << baddy.second->getY() << s << baddy.second->getType() << std::endl; + size_t result = 0; - for (const auto& verse: baddy.second->getVerses()) + for (const auto& levelPtr : m_levelParts | removeNulls) { - fileStream << verse.text() << std::endl; + if (auto sdata = levelPtr->staticData.lock(); sdata != nullptr) + result += sdata->signs.size(); } - fileStream << "BADDYEND" << std::endl; + return result; } - for (const auto& npcId: getNPCs()) - { - auto npc = m_server->getNPC(npcId); - if (npc->getType() != NPCType::LEVELNPC) - continue; // Don't save PUTNPC's or DBNPC's in the level file - std::string image = npc->getImage(); + return 0; +} - if (image.empty()) - image = "-"; // No image is represented by "-" +//---------------------------- - fileStream << "NPC" << s << image << s << npc->getX() << s << npc->getY() << std::endl; - fileStream << npc->getSource().getSource() << std::endl; - fileStream << "NPCEND" << std::endl; - } +std::optional Level::getTiles(const MapPosition& mapLevel, size_t layer) noexcept +{ + if (auto part = getSubLevelAtPosition(mapLevel); part != nullptr) + return part->getTiles(layer); + return std::nullopt; +} + +std::optional Level::getTiles(const MapPosition& mapLevel, size_t layer) const noexcept +{ + if (auto part = getSubLevelAtPosition(mapLevel); part != nullptr) + return part->getTiles(layer); + return std::nullopt; +} + +std::optional Level::getTiles(std::string_view levelPart, size_t layer) noexcept +{ + if (auto mapPosition = getSubLevelPositionInMap(levelPart); mapPosition.has_value()) + return getTiles(mapPosition.value(), layer); + return std::nullopt; +} + +std::optional Level::getTiles(std::string_view levelPart, size_t layer) const noexcept +{ + if (auto mapPosition = getSubLevelPositionInMap(levelPart); mapPosition.has_value()) + return getTiles(mapPosition.value(), layer); + return std::nullopt; } -bool Level::alterBoard(CString& pTileData, int pX, int pY, int pWidth, int pHeight, Player* player) +//---------------------------- + +bool Level::hasTerrain() const noexcept { - if (pX < 0 || pY < 0 || pX > 63 || pY > 63 || - pWidth < 1 || pHeight < 1 || - pX + pWidth > 64 || pY + pHeight > 64) + if (m_map == nullptr || isOnBigMap()) return false; - auto& settings = m_server->getSettings(); + return !m_map->terrain.gridBorderTileHeightsXAxis.empty(); +} - // Do the check for the push-pull block. - if (pWidth == 4 && pHeight == 4 && settings.getBool("clientsidepushpull", true)) - { - // Try to find the top-left corner tile. - int i; - for (i = 0; i < 16; ++i) - { - short stoneCheck = pTileData.readGShort(); - if (stoneCheck == 0x06E4 || stoneCheck == 0x07CE) - break; - } +double Level::getHeightAt(const PixelPosition& position) const noexcept +{ + if (auto part = getSubLevelAtPosition(position); part != nullptr) + return part->getHeightAt(toLocalPixelPosition(position)); - // Check if we found a possible push-pull block. - if (i != 16 && i < 11) - { - // Go back one full short so the first readByte2() returns the top-left corner. - pTileData.setRead(i * 2); + return 0.0; +} - int foundCount = 0; - for (int j = 0; j < 6; ++j) - { - // Read a piece. - short stoneCheck = pTileData.readGShort(); +//---------------------------- - // A valid stone will have pieces at the following j locations. - if (j == 0 || j == 1 || j == 4 || j == 5) +void Level::sendBoardToPlayer(std::shared_ptr player) const +{ + if (auto subLevel = getSubLevelAtPosition(player->getMapPosition()); subLevel != nullptr) + subLevel->sendBoardToPlayer(player); +} + +void Level::sendBoardLayersToPlayer(std::shared_ptr player) const +{ + if (auto subLevel = getSubLevelAtPosition(player->getMapPosition()); subLevel != nullptr) + subLevel->sendBoardLayersToPlayer(player); +} + +void Level::sendBoardHeightsToPlayer(std::shared_ptr player) const +{ + if (auto subLevel = getSubLevelAtPosition(player->getMapPosition()); subLevel != nullptr) + subLevel->sendBoardHeightsToPlayer(player); +} + +void Level::sendBoardChangesToPlayer(std::shared_ptr player, std::optional time) const +{ + if (auto subLevel = getSubLevelAtPosition(player->getMapPosition()); subLevel != nullptr) + subLevel->sendBoardChangesToPlayer(player, time); +} + +void Level::sendChestsToPlayer(std::shared_ptr player) const +{ + if (auto staticData = getStaticLevelDataAtPosition(player->getMapPosition()); staticData != nullptr) + staticData->sendChestsToPlayer(player); +} + +void Level::sendLinksToPlayer(std::shared_ptr player, bool onlyMapLinks) const +{ + if (auto staticData = getStaticLevelDataAtPosition(player->getMapPosition()); staticData != nullptr) + staticData->sendLinksToPlayer(player, onlyMapLinks); +} + +void Level::sendSignsToPlayer(std::shared_ptr player) const +{ + if (auto staticData = getStaticLevelDataAtPosition(player->getMapPosition()); staticData != nullptr) + staticData->sendSignsToPlayer(player); +} + +void Level::sendBaddiesToPlayer(std::shared_ptr player) const +{ + auto subLevel = getSubLevelAtPosition(MapPosition{}); + if (isGmap() || subLevel == nullptr) + return; + + CString packet; + for (const auto& baddy : subLevel->baddies) + { + packet.clear(); + packet >> (char)PLO_BADDYPROPS >> (char)baddy.id << baddy.getProps(); + player->sendPacket(packet); + } +} + +void Level::sendHorsesToPlayer(std::shared_ptr player) const +{ + CString packet; + for (auto& horse : m_horses) + { + packet.clear(); + packet >> (char)PLO_HORSEADD << horse.getPacket(); + player->sendPacket(packet); + } +} + +// TODO: Replace with a function in server that sends npc props from a list of ids. +void Level::sendNPCsToPlayer(std::shared_ptr player, std::optional time) const +{ + for (const auto& npcId : m_npcs) + { + auto npc = m_server->getNPC(npcId); + if (!npc) continue; + + auto packet = npc->getAllPropsPacket(time); + if (!packet.isEmpty()) + { + player->sendPacket(CString() >> (char)PLO_NPCPROPS >> (int)npc->id << packet); + if (player->getVersion() >= CLVER_4_0211 && !npc->getScript().getClientByteCode().empty()) + { + CString byteCodePacket = CString() >> (char)PLO_NPCBYTECODE >> (int)npc->id; + byteCodePacket.write(reinterpret_cast(npc->getScript().getClientByteCode().data()), npc->getScript().getClientByteCode().size()); + player->sendPacket(CString() >> (char)PLO_RAWDATA >> (int)byteCodePacket.length()); + player->sendPacket(byteCodePacket); + } + } + + npc->sendShowImagesToPlayer(player, time); + npc->sendMoveQueueToPlayer(player, time); + } +} + +//---------------------------- + +bool Level::isPlayerLeader(PlayerID id) const +{ + if (m_players.empty()) + return false; + return m_players.front() == id; +} + +bool Level::hasLivingBaddies() const +{ + auto subLevel = getSubLevelAtPosition(MapPosition{}); + if (isGmap() || subLevel == nullptr) + return false; + + for (const auto& baddy : subLevel->baddies) + { + if (baddy.mode != BaddyMode::DEAD) + return true; + } + return false; +} + +//---------------------------- + +int Level::addPlayer(PlayerID id) +{ + log::debug_assert(std::ranges::contains(m_players, id) == false, "Player already in level"); + + m_players.push_back(id); + timeSinceLastPlayerLeft.reset(); + + // Set the player enters event on all the NPCs. + if (auto player = m_server->getPlayer(id); player != nullptr) + m_server->queueNPCEvent(shared_from_this(), player->getGlobalPosition(), ScriptEventType::PLAYERENTERS, source::FromPlayer(id)); + + return static_cast(m_players.size() - 1); +} + +void Level::removePlayer(PlayerID id) +{ + std::erase(m_players, id); + + // Set the player leaves event on all the NPCs. + if (auto player = m_server->getPlayer(id); player != nullptr) + m_server->queueNPCEvent(shared_from_this(), player->getGlobalPosition(), ScriptEventType::PLAYERLEAVES, source::FromPlayer(id)); + + // If there are no more players in the level, record the time so we can do level cleanup after a delay. + if (m_players.empty()) + timeSinceLastPlayerLeft = m_server->getFrameStartTime(); +} + +//---------------------------- + +bool Level::addNPC(std::shared_ptr npc) +{ + if (std::ranges::contains(m_npcs, npc->id)) + return false; + + m_npcs.insert(npc->id); + npc->setLevel(shared_from_this()); + + if (auto part = getSubLevelAtPosition(npc->getGlobalPosition()); part != nullptr) + { + auto script = string::trimLeft(npc->getScript().getClientSide()); + + if (script.starts_with("sparringzone")) + part->isSparringZone = true; + + if (script.starts_with("noplayerkilling")) + part->isNoPkZone = true; + + //if (script.starts_with("singleplayer")) + // isSingleplayer = true; + } + + return true; +} + +bool Level::addNPC(NPCID npcId) +{ + auto npc = m_server->getNPC(npcId); + return addNPC(npc); +} + +void Level::removeNPC(std::shared_ptr npc) +{ + if (npc == nullptr) + return; + + m_npcs.erase(npc->id); +} + +void Level::removeNPC(NPCID npcId) +{ + auto npc = m_server->getNPC(npcId); + removeNPC(npc); +} + +//---------------------------- + +bool Level::alterBoard(CString& tileData, const WholeTileRectangleArea& area, Player* player, bool forceRespawn, bool allowRespawn, bool sendToPlayers) +{ + // Do the check for the push-pull block. + if (area.position.z() == 0 && area.size.width() == 4 && area.size.height() == 4 && m_server->cached.enableClientsidePushPull.getValue()) + { + // Try to find the top-left corner tile. + int i; + for (i = 0; i < 16; ++i) + { + short stoneCheck = tileData.readGShort(); + if (stoneCheck == 0x06E4 || stoneCheck == 0x07CE) + break; + } + + // Check if we found a possible push-pull block. + if (i != 16 && i < 11) + { + // Go back one full short so the first readByte2() returns the top-left corner. + tileData.setRead(i * 2); + + int foundCount = 0; + for (int j = 0; j < 6; ++j) + { + // Read a piece. + short stoneCheck = tileData.readGShort(); + + // A valid stone will have pieces at the following j locations. + if (j == 0 || j == 1 || j == 4 || j == 5) { switch (stoneCheck) { @@ -1241,681 +1507,1659 @@ bool Level::alterBoard(CString& pTileData, int pX, int pY, int pWidth, int pHeig } } } - pTileData.setRead(0); + tileData.setRead(0); // Check if we found a full tile. If so, don't accept the change. - if (foundCount == 4) + if (foundCount == 4 && player != nullptr) { - player->sendPacket(CString() >> (char)PLO_BOARDMODIFY >> (char)pX >> (char)pY >> (char)pWidth >> (char)pHeight << pTileData); + player->sendPacket(CString() >> (char)PLO_BOARDMODIFY >> (char)area.position.x() >> (char)area.position.y() >> (char)area.size.width() >> (char)area.size.height() << tileData); return false; } } } - // Delete any existing changes within the same region. - for (auto i = m_boardChanges.begin(); i != m_boardChanges.end();) - { - LevelBoardChange& change = *i; - if ((change.getX() >= pX && change.getX() + change.getWidth() <= pX + pWidth) && - (change.getY() >= pY && change.getY() + change.getHeight() <= pY + pHeight)) - { - i = m_boardChanges.erase(i); - } - else - ++i; - } + // Any 2x2 tile change can respawn. + // The list of tiles is mostly for security checks and should be a list of allowed replacements. + // TODO: Develop a way to specify valid tile replacements. + auto respawnTime = m_server->cached.tileRespawnTime.getValue(); + bool doRespawn = allowRespawn && (forceRespawn || (area.size.width() == 2 && area.size.height() == 2)); + /* // Check if the tiles should be respawned. // Only tiles in the respawningTiles array are allowed to respawn. // These are things like signs, bushes, pots, etc. - int respawnTime = settings.getInt("respawntime", 15); + auto respawnTime = m_server->cached.tileRespawnTime.getValue(); bool doRespawn = false; - short testTile = m_tiles[0][pX + (pY * 64)]; + short testTile = m_tiles[0][area.position.x() + (static_cast(area.position.y()) * 64)]; int tileCount = sizeof(respawningTiles) / sizeof(short); for (int i = 0; i < tileCount; ++i) if (testTile == respawningTiles[i]) doRespawn = true; + */ + + // Split up the board change into level parts. + std::pair mapPartsX{area.left() / tilesPerSubLevel().width(), area.right() / tilesPerSubLevel().width()}; + std::pair mapPartsY{area.top() / tilesPerSubLevel().height(), area.bottom() / tilesPerSubLevel().height()}; + for (auto partY = mapPartsY.first; partY <= mapPartsY.second; ++partY) + { + for (auto partX = mapPartsX.first; partX <= mapPartsX.second; ++partX) + { + MapPosition mapPosition{partX, partY}; + + // Get the level part. + auto sourcePart = getSubLevelAtPosition(mapPosition); + if (sourcePart == nullptr) + continue; + + // Determine the area within the part. + auto localRect = clipLocalWholeTileRectangleArea(mapPosition, area); + + // Delete any existing changes contained within the same region. + std::erase_if(sourcePart->boardChanges, [&localRect](const LevelBoardChange& change) + { + return rectangleContained(change.area, localRect); + }); + + // Grab the old tiles for respawn. + CString oldTiles; + if (doRespawn) + { + auto tiles = sourcePart->getTiles(area.position.z()); + if (!tiles.has_value()) + continue; + + for (int j = localRect.position.y(); j < localRect.position.y() + localRect.size.height(); ++j) + { + for (int i = localRect.position.x(); i < localRect.position.x() + localRect.size.width(); ++i) + oldTiles.writeGShort((*tiles.value())[i + (static_cast(j) * 64)]); + } + } + + // Construct the tile data for this part. + CString partTileData; + for (int j = localRect.position.y(); j < localRect.position.y() + localRect.size.height(); ++j) + { + for (int i = localRect.position.x(); i < localRect.position.x() + localRect.size.width(); ++i) + { + // Determine the index in the full tileData. + int globalX = i + (static_cast(mapPosition.x()) * tilesPerSubLevel().width()); + int globalY = j + (static_cast(mapPosition.y()) * tilesPerSubLevel().height()); + int index = globalX - area.position.x() + ((globalY - area.position.y()) * area.size.width()); + + // Read the tile from the full tileData. + tileData.setRead(index * 2); + short tile = tileData.readGShort(); + partTileData.writeGShort(tile); + } + } + + // Apply the board update to this part. + sourcePart->boardChanges.push_back(LevelBoardChange{shared_from_this(), MapPosition{partX, partY}, localRect, partTileData, oldTiles, (doRespawn ? std::chrono::seconds(respawnTime) : 0s)}); + if (sendToPlayers) sourcePart->boardChanges.back().sendToPlayersOnLevel(); + } + } + + return true; +} + +void Level::applyBoardChangeFromScriptTiles(const WholeTileRectangleArea& area, bool forceRespawn, bool allowRespawn) +{ + // Prepare a tile array for the area. + std::vector tiles{0}; + tiles.resize(static_cast(area.size.width()) * area.size.height()); + + // Fill in the tile array with script updated tiles. + // The tiles are stored in each sub-level. + for (const auto& subLevel : getSubLevelsInRectangle(area)) + { + auto clippedArea = subLevel->clipRectangleToPart(area); + if (clippedArea.size.width() == 0 || clippedArea.size.height() == 0) + continue; + + if (!subLevel->scriptUpdatedTiles.has_value()) + subLevel->scriptUpdatedTiles = LevelTiles(); + + auto layer = subLevel->scriptUpdatedTiles.value().getLayer(0); + if (!layer.has_value() || layer.value() == nullptr) + continue; + + auto displacement = clippedArea.position - area.position; + auto localArea = toLocalWholeTileRectangleArea(clippedArea); + + // Collect the tiles. + auto& subTiles = layer.value(); + for (size_t y = 0; y < localArea.size.height(); ++y) + { + for (size_t x = 0; x < localArea.size.width(); ++x) + { + // Get the tile from the sub-level. + auto sourcePosition = Position(localArea.position.x() + x, localArea.position.y() + y); + auto tile = subTiles->at(sourcePosition.y() * 64 + sourcePosition.x()); + + // Set the tile in the destination array, calculating the position relative to the origin of the area. + // This will let us pick the appropriate index. + auto destPosition = Position(displacement.x() + x, displacement.y() + y); + tiles[destPosition.y() * area.size.width() + destPosition.x()] = tile; + } + } + } + + CString tileData; + for (auto& tile : tiles) + tileData.writeGShort(tile); + + // Apply the board update. + alterBoard(tileData, area, nullptr, forceRespawn, allowRespawn, true); +} + +void Level::saveBoardChangeFromScriptTiles(const WholeTileRectangleArea& area) +{ + for (const auto& levelPart : getSubLevelsInRectangle(area)) + { + // We need to have script updated tiles. + if (!levelPart->scriptUpdatedTiles.has_value()) + continue; + + // And we need to have updates in this layer. + auto updatedTiles = levelPart->scriptUpdatedTiles.value().getLayer(area.position.z()); + if (!updatedTiles.has_value() || updatedTiles.value() == nullptr) + continue; + + // And the updates need to be within this part. + auto localArea = toLocalWholeTileRectangleArea(levelPart->clipRectangleToPart(area)); + if (localArea.size.width() == 0 || localArea.size.height() == 0) + continue; + + // And we need to have static data. + auto sData = levelPart->staticData.lock(); + if (sData == nullptr) + continue; + + // Initialize the instanced tile updates if not already done. + if (!levelPart->instancedTileUpdates.has_value()) + levelPart->instancedTileUpdates = sData->tiles; + + // Get the level part's tiles for this layer. + if (auto tiles = levelPart->getTiles(area.position.z()); tiles.has_value()) + { + auto& destTiles = *tiles.value(); + auto& sourceTiles = *updatedTiles.value(); + for (int j = localArea.position.y(); j < localArea.position.y() + localArea.size.height(); ++j) + { + for (int i = localArea.position.x(); i < localArea.position.x() + localArea.size.width(); ++i) + { + auto index = i + (static_cast(j) * 64); + auto tile = sourceTiles.at(index); + if (tile != constants::EmptyTileInLayer) + destTiles.at(index) = tile; + } + } + } + } +} + +void Level::updateBoard(const TileRectangleArea& area) noexcept +{ + applyBoardChangeFromScriptTiles(toWholeTileRectangleArea(area), true); +} + +void Level::updateBoard2(const TileRectangleArea& area) noexcept +{ + // If we don't allow permanent tile modifications, just call updateBoard(). + if (m_server->cached.enablePermanentTileChanges.getValue() == false) + { + updateBoard(area); + return; + } + + auto wholeTileArea = toWholeTileRectangleArea(area); + applyBoardChangeFromScriptTiles(wholeTileArea, false, false); - // Grab old tiles for the respawn. - CString oldTiles; - if (doRespawn) + bool levelsAutoSave = m_server->cached.saveTileChangesToLevelFile.getValue(); + if (levelsAutoSave) { - for (int j = pY; j < pY + pHeight; ++j) + auto mapPosition = toMapPosition(area.position); + if (auto staticData = getStaticLevelDataAtPosition(mapPosition); staticData != nullptr) { - for (int i = pX; i < pX + pWidth; ++i) - oldTiles.writeGShort(m_tiles[0][i + (j * 64)]); + saveBoardChangeFromScriptTiles(wholeTileArea); + saveLevel(mapPosition, staticData->levelName); } } +} + +//---------------------------- + +LevelArrow* Level::addArrow(inform_client_t, const PixelPosition& position, const PixelPosition& speed, uint8_t direction, int8_t type, ScriptObject from) +{ + if (!m_server->hasNPCServer()) + return nullptr; + + auto result = addArrow(position, speed, direction, type, from); + if (result != nullptr) + { + auto localPosition = toLocalPixelPosition(result->position); + char x = static_cast(localPosition.x() / 8.0f); + char y = static_cast(localPosition.y() / 8.0f); + + // Get the sprite for the arrow. + uint8_t sprite = (result->type == 0 ? ballSpriteIndex : arrowSpriteIndex); + if (result->type != 0) + sprite += (result->direction & 0b11); + + uint8_t flags = (result->direction & 0b11) | (result->getPacketFrom() << 3); + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_ARROWADD >> (short)0 >> (char)x >> (char)y >> (char)flags >> (char)sprite >> (char)type, position, shared_from_this()); + } + return result; +} + +LevelArrow* Level::addArrow(const PixelPosition& position, const PixelPosition& speed, uint8_t direction, int8_t type, ScriptObject from) +{ + if (!m_server->hasNPCServer()) + return nullptr; + + LevelArrow newArrow{.startPosition = position, .position = position, .speed = speed, .direction = direction, .type = type, .from = from}; + m_arrows.emplace_back(std::move(newArrow)); + return &m_arrows.back(); +} - // TODO: old gserver didn't save the board change if oldTiles.length() == 0. - // Should we do it that way still? - m_boardChanges.push_back(LevelBoardChange(pX, pY, pWidth, pHeight, pTileData, oldTiles, (doRespawn ? respawnTime : -1))); +bool Level::removeArrow(uint8_t index) +{ + if (index >= m_arrows.size()) + return false; + + m_arrows.erase(m_arrows.begin() + index); return true; } -bool Level::addItem(float pX, float pY, LevelItemType pItem) +std::optional Level::getArrow(size_t index) noexcept +{ + if (index >= m_arrows.size()) + return std::nullopt; + return &m_arrows.at(index); +} + +//---------------------------- + +LevelBaddy* Level::addBaddy(const LocalPixelPosition& position, BaddyType type) +{ + auto subLevel = getSubLevelAtPosition(MapPosition{}); + if (isGmap() || subLevel == nullptr) + return nullptr; + + // Find the next available baddy that can be used. + size_t nextIndex = 0; + for (nextIndex = 0; nextIndex < subLevel->baddies.size(); ++nextIndex) + { + if (subLevel->baddies[nextIndex].canBeReplaced()) + break; + } + + // Limit of 50 baddies per level. + if (nextIndex >= 50) + return nullptr; + + // Clamp the index to the size of the baddy list, just in case. + nextIndex = std::clamp(nextIndex, static_cast(0), subLevel->baddies.size()); + + // New Baddy + LevelBaddy newBaddy{position, type, this->shared_from_this()}; + newBaddy.id = nextIndex + 1; + + if (nextIndex == subLevel->baddies.size()) + subLevel->baddies.emplace_back(std::move(newBaddy)); + else + subLevel->baddies[nextIndex] = std::move(newBaddy); + + return &subLevel->baddies[nextIndex]; +} + +LevelBaddy* Level::putNewBaddy(const LocalPixelPosition& position, BaddyType type) +{ + auto baddy = addBaddy(position, type); + if (baddy == nullptr) + return nullptr; + + CString packet = CString() >> (char)PLO_BADDYPROPS >> (char)baddy->id << baddy->getProps(); + for (auto& playerId : m_players) + { + if (auto player = m_server->getPlayer(playerId); player) + player->sendPacket(packet); + } + + return baddy; +} + +LevelBaddy* Level::putNewBaddy(const LocalPixelPosition& position, BaddyType type, uint8_t power, std::string_view image) +{ + auto baddy = addBaddy(position, type); + if (baddy == nullptr) + return nullptr; + + baddy->setImage(image); + baddy->power = power; + + CString packet = CString() >> (char)PLO_BADDYPROPS >> (char)baddy->id << baddy->getProps(); + for (auto& playerId : m_players) + { + if (auto player = m_server->getPlayer(playerId); player) + player->sendPacket(packet); + } + + return baddy; +} + +bool Level::removeBaddy(uint8_t pId) +{ + auto subLevel = getSubLevelAtPosition(MapPosition{}); + if (isGmap() || subLevel == nullptr) + return false; + + // Don't allow us to remove id 0 or any id over 50. + if (pId < 1 || pId > 50 || (pId > subLevel->baddies.size())) return false; + + // Find the baddy. + auto& baddy = subLevel->baddies.at(static_cast(pId) - 1); + if (baddy.mode == BaddyMode::DEAD) + return false; + + // Erase the baddy. + baddy.mode = BaddyMode::DEAD; + baddy.setRespawn(false); + + // Set the baddy as dead for all the other players in the level. + CString props = CString() >> (char)BaddyProp::MODE >> (char)BaddyMode::DEAD; + for (const auto& playerId : m_players) + { + if (auto player = m_server->getPlayer(playerId); player != nullptr) + player->sendPacket(CString() >> (char)PLO_BADDYPROPS >> (char)baddy.id << props); + } + return true; +} + +bool Level::removeAllBaddies() +{ + auto subLevel = getSubLevelAtPosition(MapPosition{}); + if (isGmap() || subLevel == nullptr) + return false; + + CString propsPacket; + for (auto& baddy : subLevel->baddies) + { + if (baddy.mode == BaddyMode::DEAD) + continue; + + baddy.mode = BaddyMode::DEAD; + baddy.setRespawn(false); + + // Set the baddy as dead for all the other players in the level. + propsPacket.clear(); + propsPacket >> (char)PLO_BADDYPROPS >> (char)baddy.id >> (char)BaddyProp::MODE >> (char)BaddyMode::DEAD; + for (const auto& playerId : m_players) + { + if (auto player = m_server->getPlayer(playerId); player != nullptr) + player->sendPacket(propsPacket); + } + } + return true; +} + +std::optional Level::getBaddyById(uint8_t id) noexcept +{ + auto subLevel = getSubLevelAtPosition(MapPosition{}); + if (isGmap() || subLevel == nullptr) + return std::nullopt; + + if (id > subLevel->baddies.size() || id == 0) + return std::nullopt; + return &subLevel->baddies.at(static_cast(id) - 1); +} + +std::optional Level::getAliveBaddyByIndex(size_t index) noexcept +{ + auto subLevel = getSubLevelAtPosition(MapPosition{}); + if (isGmap() || subLevel == nullptr) + return std::nullopt; + + if (index >= subLevel->baddies.size()) + return std::nullopt; + + size_t pos = 0; + for (auto& baddy : subLevel->baddies) + { + if (!baddy.isAlive()) + continue; + if (index == pos++) + return &baddy; + } + + return std::nullopt; +} + +//---------------------------- + +LevelBomb* Level::addBomb(inform_client_t, const PixelPosition& position, uint8_t power) +{ + if (!m_server->hasNPCServer()) + return nullptr; + + auto result = addBomb(position, power); + if (result != nullptr) + { + auto localPosition = toLocalPixelPosition(result->position); + char x = static_cast(localPosition.x() / 8.0f); + char y = static_cast(localPosition.y() / 8.0f); + uint8_t timeToExplode = static_cast(std::min(223, std::chrono::duration_cast(result->timeout.timeout).count() / 50)); + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_BOMBADD >> (short)0 >> (char)x >> (char)y >> (char)result->power >> (char)timeToExplode, position, shared_from_this()); + // PLO_BOMBADD might support a bomb image at the end of the packet. + } + return result; +} + +LevelBomb* Level::addBomb(const PixelPosition& position, uint8_t power) +{ + if (!m_server->hasNPCServer()) + return nullptr; + + LevelBomb newBomb{.position = position, .power = power}; + newBomb.timeout.runOnceFor(3s); + m_bombs.emplace_back(std::move(newBomb)); + return &m_bombs.back(); +} + +LevelBomb* Level::addBombFromClient(const PixelPosition& position, uint8_t power, PlayerID owner, std::chrono::milliseconds timeToExplode) +{ + if (!m_server->hasNPCServer()) + return nullptr; + + // If we generate an item NPC, remove the bomb from the level. + LevelItemType itemType = (power == 2 ? LevelItemType::SUPERBOMB : (power == 3 ? LevelItemType::JOLTBOMB : LevelItemType::BOMB)); + if (auto itemNPC = generateItemNPC(position, itemType); itemNPC != nullptr) + return nullptr; + + // Add the bomb to the level otherwise. + LevelBomb newBomb{.position = position, .power = power, .owner = source::FromPlayer(owner)}; + newBomb.timeout.runOnceFor(timeToExplode); + m_bombs.emplace_back(std::move(newBomb)); + return &m_bombs.back(); +} + +bool Level::removeBomb(inform_client_t, size_t index) +{ + if (index < m_bombs.size()) + { + auto mapPosition = toMapPosition(m_bombs[index].position); + auto localPosition = toLocalPixelPosition(m_bombs[index].position); + CString packet = CString() >> (char)PLO_BOMBDEL >> (char)(localPosition.x() / 8) >> (char)(localPosition.y() / 8); + m_server->sendPacketToOneLevelPart(packet, this->shared_from_this(), mapPosition); + } + return removeBomb(index); +} + +bool Level::removeBomb(size_t index) +{ + if (index >= m_bombs.size()) + return false; + + m_bombs.erase(m_bombs.begin() + index); + return true; +} + +bool Level::removeBomb(const PixelPosition& position) +{ + for (auto it = m_bombs.begin(); it != m_bombs.end(); ++it) + { + if (it->position == position) + { + m_bombs.erase(it); + return true; + } + } + return false; +} + +std::optional Level::getBomb(size_t index) noexcept +{ + if (index >= m_bombs.size()) + return std::nullopt; + return &m_bombs.at(index); +} + +//---------------------------- + +std::optional Level::getChest(size_t index) const noexcept +{ + auto objects = getChests(); + auto iter = objects.begin(); + std::ranges::advance(iter, index, objects.end()); + if (iter == objects.end()) + return std::nullopt; + return std::make_optional(&(*iter)); +} + +std::optional Level::getChest(const WholeTilePosition& position) const noexcept +{ + auto mapPosition = toMapPosition(position); + auto localPosition = toLocalWholeTilePosition(position); + return getChest(mapPosition, localPosition); +} + +std::optional Level::getChest(const MapPosition& mapPosition, const LocalWholeTilePosition& position) const noexcept +{ + auto index = getMapIndexAtPosition(mapPosition); + if (index >= m_levelParts.size()) + return std::nullopt; + + auto& part = m_levelParts.at(index); + if (part == nullptr) + return std::nullopt; + + if (auto sdata = part->staticData.lock(); sdata != nullptr) + { + for (auto& chest : sdata->chests) + { + if (chest.position == position) + return std::make_optional(&chest); + } + } + + return std::nullopt; +} + +//---------------------------- + +void Level::addExplosion(inform_client_t, const PixelPosition& position, ScriptObject from, uint8_t radius, uint8_t power) +{ + if (!m_server->hasNPCServer()) + return; + + addExplosion(position, from, radius, power); + + auto localPosition = toLocalPixelPosition(position); + CString packet = CString() >> (char)PLO_EXPLOSION >> (short)0 >> (char)radius >> (char)(localPosition.x() / 8) >> (char)(localPosition.y() / 8) >> (char)power; + m_server->sendPacketToOneLevelPart(packet, position, shared_from_this()); +} + +void Level::addExplosion(const PixelPosition& position, ScriptObject from, uint8_t radius, uint8_t power) +{ + if (!m_server->hasNPCServer()) + return; + + addExplosionPart(position, 2, power); + for (size_t i = 0; i < (static_cast(radius) * 4); ++i) + { + uint8_t dir = i / radius; + int16_t step = (((i % radius) + 1) * 2) * 16; + PixelPosition partPosition = position.translate( + (dir == 0 || dir == 2) ? 0 : (dir == 1 ? -step : step), + (dir == 1 || dir == 3) ? 0 : (dir == 0 ? -step : step) + ); + addExplosionPart(partPosition, dir, power); + } + + // Add exploded events to NPCs in the level. + if (m_server->hasNPCServer()) + { + PixelRectangleArea vertTest = {position.translate(0, -(radius * 32)), {static_cast(32), static_cast((1 + (radius * 2)) * 32)}}; + PixelRectangleArea horzTest = {position.translate(-(radius * 32), 0), {static_cast((1 + (radius * 2)) * 32), static_cast(32)}}; + auto center = vertTest.center(); + for (const NPCID& npcId : findIntersectingNPCsForCollision(vertTest)) + { + if (auto npc = m_server->getNPC(npcId); npc != nullptr) + npc->hurtAndPush(power, center, ScriptEventType::EXPLODED, from); + } + for (const NPCID& npcId : findIntersectingNPCsForCollision(horzTest)) + { + if (auto npc = m_server->getNPC(npcId); npc != nullptr) + npc->hurtAndPush(power, center, ScriptEventType::EXPLODED, from); + } + } +} + +void Level::addSpyFire(const PixelPosition& position, ScriptObject from, uint8_t direction, uint8_t length, uint8_t power) +{ + /* + spyfire 3,1; + + up: x+0.5,y-1.5 2 0 0 0 + down: x+0.5,y+2.2 0 2 2 2 + left: x-2.0,y+0.2 3 1 1 1 + right: x+3.0,y+0.2 1 3 3 3 + */ + + if (!m_server->hasNPCServer()) + return; + + const PixelPosition startingPosition = position.translate( + (direction == 0 || direction == 2) ? 8 : (direction == 1 ? -32 : 48), + (direction == 1 || direction == 3) ? 3 : (direction == 0 ? -24 : 35) + ); + + for (size_t i = 0; i < static_cast(length + 1); ++i) + { + uint8_t dir = (i != 0 ? direction : (direction + 2) % 4); + int16_t stepX = (direction == 0 || direction == 2) ? 0 : (direction == 1 ? -i * 32 : i * 32); + int16_t stepY = (direction == 1 || direction == 3) ? 0 : (direction == 0 ? -i * 32 : i * 32); + PixelPosition partPosition = startingPosition.translate(stepX, stepY); + addExplosionPart(partPosition, dir, power); + } + + // Add exploded events to NPCs in the level. + if (m_server->hasNPCServer()) + { + int16_t lengthInPixels = (length + 1) * 32; + PixelPosition testPosition = startingPosition.translate( + static_cast((direction == 1) ? -lengthInPixels : 0), + static_cast((direction == 0) ? -lengthInPixels : 0) + ); + Dimension testDimension{ + static_cast((direction == 0 || direction == 2) ? 32 : lengthInPixels), + static_cast((direction == 1 || direction == 3) ? 32 : lengthInPixels) + }; + + auto center = translatePosition(startingPosition, 16, 16); + for (const NPCID& npcId : findIntersectingNPCsForCollision({testPosition, testDimension})) + { + if (auto npc = m_server->getNPC(npcId); npc != nullptr) + npc->hurtAndPush(power, center, ScriptEventType::EXPLODED, from); + } + } +} + +LevelExplosion* Level::addExplosionPart(const PixelPosition& position, uint8_t direction, uint8_t power) +{ + if (!m_server->hasNPCServer()) + return nullptr; + + LevelExplosion explo{.position = position, .power = power, .direction = direction}; + explo.timeout.runOnceFor(ExplosionDuration); + m_explosions.emplace_back(std::move(explo)); + return &m_explosions.back(); +} + +bool Level::removeExplosion(size_t index) +{ + if (index >= m_explosions.size()) + return false; + + m_explosions.erase(m_explosions.begin() + index); + return true; +} + +bool Level::removeExplosion(const PixelPosition& position) +{ + for (size_t i = 0; i < m_explosions.size(); ++i) + { + LevelExplosion& explosion = m_explosions[i]; + if (explosion.position == position) + return removeExplosion(i); + } + return false; +} + +std::optional Level::getExplosion(size_t index) noexcept +{ + if (index >= m_explosions.size()) + return std::nullopt; + return &m_explosions.at(index); +} + +//---------------------------- + +LevelHorse* Level::addHorse(inform_client_t, std::string_view image, const PixelPosition& position, uint8_t direction, uint8_t bushes) +{ + auto result = addHorse(image, position, direction, bushes); + if (result != nullptr) + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_HORSEADD << result->getPacket(), position, shared_from_this()); + return result; +} + +LevelHorse* Level::addHorse(std::string_view image, const PixelPosition& position, uint8_t direction, uint8_t bushes) +{ + auto horseLife = m_server->getSettings().get("horselifetime").value_or(30); + + LevelHorse newHorse{.position = position, .image = std::string{image}, .direction = direction, .bushes = bushes, .timeout = TimeoutGenerator(std::chrono::seconds(horseLife))}; + if (isOnWater(position.translate(16, 32))) + newHorse.type = HORSETYPE_BOAT; + + newHorse.timeout.runOnceFor(std::chrono::seconds(horseLife)); + m_horses.emplace_back(std::move(newHorse)); + return &m_horses.back(); +} + +bool Level::removeHorse(inform_client_t, size_t index) +{ + if (index < m_horses.size()) + { + auto mapPosition = toMapPosition(m_horses[index].position); + auto localPosition = toLocalPixelPosition(m_horses[index].position); + CString packet = CString() >> (char)PLO_HORSEDEL >> (char)(localPosition.x() / 8) >> (char)(localPosition.y() / 8); + m_server->sendPacketToOneLevelPart(packet, this->shared_from_this(), mapPosition); + } + return removeHorse(index); +} + +bool Level::removeHorse(size_t index) +{ + if (index >= m_horses.size()) + return false; + + m_horses.erase(m_horses.begin() + index); + return true; +} + +bool Level::removeHorse(const PixelPosition& position) +{ + for (size_t i = 0; i < m_horses.size(); ++i) + { + LevelHorse& horse = m_horses[i]; + if (horse.position == position) + return removeHorse(i); + } + return false; +} + +std::optional Level::getHorse(size_t index) noexcept +{ + if (index >= m_horses.size()) + return std::nullopt; + return &m_horses.at(index); +} + +//---------------------------- + +LevelItem* Level::addItem(inform_client_t, const PixelPosition& position, LevelItemType item) +{ + auto result = addItem(position, item); + if (result != nullptr) + { + auto localPosition = toLocalPixelPosition(result->position); + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_ITEMADD >> (char)(localPosition.x() / 8) >> (char)(localPosition.y() / 8) >> (char)LevelItem::getItemTypeId(result->item), result->position, shared_from_this()); + } + return result; +} + +LevelItem* Level::addItem(const PixelPosition& position, LevelItemType item) +{ + if (m_server->hasNPCServer()) + { + // If we were able to generate the item NPC, don't add the item to the ground. + if (auto itemNPC = generateItemNPC(position, item); itemNPC != nullptr) + return nullptr; + } + + LevelItem newItem{.position = position, .item = item, .modTime = m_server->getFrameStartTime()}; + newItem.timeout.runOnceFor(LevelItemTimeout); + m_items.emplace_back(std::move(newItem)); + return &m_items.back(); +} + +bool Level::removeItem(inform_client_t, size_t index) +{ + if (index < m_items.size()) + { + auto mapPosition = toMapPosition(m_items[index].position); + auto localPosition = toLocalPixelPosition(m_items[index].position); + CString packet = CString() >> (char)PLO_ITEMDEL >> (char)(localPosition.x() / 8) >> (char)(localPosition.y() / 8); + m_server->sendPacketToOneLevelPart(packet, this->shared_from_this(), mapPosition); + } + return removeItem(index); +} + +bool Level::removeItem(size_t index) +{ + if (index >= m_items.size()) + return false; + + m_items.erase(m_items.begin() + index); + return true; +} + +LevelItemType Level::removeItem(const PixelPosition& position) +{ + for (auto i = m_items.begin(); i != m_items.end(); ++i) + { + LevelItem& item = *i; + if (item.position == position) + { + LevelItemType itemType = item.item; + m_items.erase(i); + return itemType; + } + } + + return LevelItemType::INVALID; +} + +std::optional Level::getItem(size_t index) noexcept +{ + if (index >= m_items.size()) + return std::nullopt; + return &m_items.at(index); +} + +//---------------------------- + +std::optional Level::getLink(size_t index) const noexcept +{ + auto objects = getLinks(); + auto iter = objects.begin(); + std::ranges::advance(iter, index, objects.end()); + if (iter == objects.end()) + return std::nullopt; + return std::make_optional(&(*iter)); +} + +std::optional Level::getLink(std::string_view levelPart, const LocalWholeTilePosition& position, bool excludeOverworld) const noexcept +{ + auto sdata = getStaticLevelDataByName(levelPart); + if (sdata == nullptr) + return std::nullopt; + + for (auto& link : sdata->links) + { + if (excludeOverworld && link.isProbableMapLink()) + continue; + + auto& bbox = link.getBoundingBox(); + if ((position.x() >= bbox.position.x() && position.x() <= bbox.position.x() + bbox.size.width()) && (position.y() >= bbox.position.y() && position.y() <= bbox.position.y() + bbox.size.height())) + { + return std::make_optional(&link); + } + } + + return std::nullopt; +} + +std::optional Level::getLink(const TilePosition& position, bool excludeOverworld) const noexcept +{ + auto part = getSubLevelAtPosition(position); + if (part == nullptr) + return std::nullopt; + auto sdata = part->staticData.lock(); + if (sdata == nullptr) + return std::nullopt; + + for (auto& link : sdata->links) + { + if (excludeOverworld && link.isProbableMapLink()) + continue; + + auto localPosition = toLocalWholeTilePosition(position); + auto& bbox = link.getBoundingBox(); + if ((localPosition.x() >= bbox.position.x() && localPosition.x() <= bbox.position.x() + bbox.size.width()) && (localPosition.y() >= bbox.position.y() && localPosition.y() <= bbox.position.y() + bbox.size.height())) + { + return std::make_optional(&link); + } + } + + return std::nullopt; +} + +//---------------------------- + +LevelShoot* Level::addShoot(LevelShoot* existingShoot) +{ + if (existingShoot == nullptr) + return nullptr; + + m_shoots.push_back(*existingShoot); + return &m_shoots.back(); +} + +LevelShoot* Level::addShoot(inform_client_t, const PixelPosition& position, float angle, float zangle, uint8_t power, float gravity, const std::string& gani, ScriptObject from) +{ + if (!m_server->hasNPCServer()) + return nullptr; + + auto result = addShoot(position, angle, zangle, power, gravity, gani, from); + if (result != nullptr) + m_server->sendShootToOneLevel(result, shared_from_this()); + + return result; +} + +LevelShoot* Level::addShoot(const PixelPosition& position, float angle, float zangle, uint8_t power, float gravity, const std::string& gani, ScriptObject from) +{ + if (!m_server->hasNPCServer()) + return nullptr; + + auto tilePosition = toTilePosition(position); + double ground = getHeightAt(position.translate(8, 16)); + tilePosition.translate(0, 0, ground); + + LevelShoot newShoot{.position = tilePosition, .angle = angle, .zangle = zangle, .powerIn44Pixels = power, .gani = gani, .gravity = gravity, .from = from}; + if (newShoot.gani.back() == ',') + newShoot.gani.pop_back(); + newShoot.calculateSpeeds(); + m_shoots.emplace_back(std::move(newShoot)); + return &m_shoots.back(); +} + +LevelShoot* Level::addShoot(const PixelPosition& position, uint8_t angle, uint8_t zangle, uint8_t power, float gravity, const std::string& gani, ScriptObject from) +{ + auto pi = std::numbers::pi_v; + return addShoot(position, (angle / 220.0f) * (2 * pi), ((zangle / 110.0f) - 1.0f) * (pi / 2), power, gravity, gani, from); +} + +bool Level::removeShoot(uint8_t index) +{ + if (index >= m_shoots.size()) + return false; + + m_shoots.erase(m_shoots.begin() + index); + return true; +} + +LevelShoot* Level::getShoot(uint8_t index) const +{ + if (index >= m_shoots.size()) + return nullptr; + return const_cast(&m_shoots[index]); +} + +//---------------------------- + +std::optional Level::getSign(size_t index) const noexcept +{ + auto objects = getSigns(); + auto iter = objects.begin(); + std::ranges::advance(iter, index, objects.end()); + if (iter == objects.end()) + return std::nullopt; + return std::make_optional(&(*iter)); +} + +//---------------------------- + +bool Level::moveShoot(LevelShoot* shoot, int iterations) { -#ifdef V8NPCSERVER - #ifdef GRALATNPC - if (LevelItem::isRupeeType(pItem)) + if (shoot == nullptr) + return false; + + for (int i = 0; i < iterations; ++i) { - if (m_server->getClass("gralats") == nullptr) - return true; + // If the shoot is out of bounds, delete it. + auto levelDimensions = sizeInTiles(); + if (shoot->position.x() < 0 || shoot->position.y() < 0 || shoot->position.x() >= levelDimensions.width() || shoot->position.y() >= levelDimensions.height()) + return false; + + // Move the shoot. + shoot->move(); + + bool collided = false; + std::string eventParams; + auto constructEventParams = [&collided, &eventParams, &shoot]() + { + if (!eventParams.empty()) return; + collided = true; + eventParams = std::format("{},{}", shoot->position.x(), shoot->position.y()); + if (!shoot->shootParams.empty()) + { + eventParams += ","; + eventParams += string::toCSV(shoot->shootParams); + } + }; - NPC* gralatNPC = nullptr; + // Determine our absolute projectile position in the world space. + // The Z location is relative from the starting Z. + PixelPosition pixelPosition = toPixelPosition(shoot->position).translate(8_i16, 16_i16); - // Find existing rupees, and add to the npc - auto pixelX = static_cast((pX - 0.5) * 16); - auto pixelY = static_cast((pY - 0.5) * 16); + // Determine the ground level at the shoot position. + auto currentGroundLevel = static_cast(getHeightAt(pixelPosition) * 16); - auto npcList = findAreaNpcs(pixelX, pixelY, 32, 32); - for (auto& npc: npcList) + // Check for NPC collisions. + //log::printLine(log::server, "Collision search pos: ({}), ground: {}", searchPosition / 16.0f, currentGroundLevel / 16.0f); + bool fromPlayer = (shoot->from.second == ScriptObjectType::PLAYER); + for (const auto& npc : findIntersectingNPCsForCollision({pixelPosition, {24_ui16, 24_ui16, 48_ui16}})) { - if (npc->joinedClass("gralats")) + if (shoot->from.second == ScriptObjectType::NPC && shoot->from.first == npc) + continue; + if (auto npcPtr = m_server->getNPC(npc); npcPtr != nullptr) { - gralatNPC = npc; - break; + //log::printLine(log::server, "Collision ({}) with NPC '{}' at ({})", searchPosition / 16.0f, npcPtr->name, npcPos); + constructEventParams(); + npcPtr->scripting.events.addEvent(ScriptEventType::TRIGGERACTION, shoot->from, (fromPlayer ? "projectile" : "sprojectile"), eventParams); } } - // Create a new gralat npc for these rupees - if (!gralatNPC) + // If we are within 3 tiles of the ground, and we aren't going up, check for walls and the ground. + int32_t groundDiff = pixelPosition.z() - currentGroundLevel; + if (!collided && groundDiff <= 48 && (DoubleIsZero(shoot->movementPerFrame.z()) || shoot->movementPerFrame.z() <= 0.0)) { - auto npc = m_server->addNPC("", "npc.join(\"gralats\");", pX, pY, shared_from_this(), false, true); - addNPC(npc); + // Check if we hit the ground. + if (pixelPosition.z() <= currentGroundLevel) + constructEventParams(); + + // Check for wall collisions. + bool onWallDetection = m_server->cached.projectilesStopOnWall.getValue() && groundDiff < 48; + if (!collided && onWallDetection && isOnWall2(WholeTileRectangleArea{toWholeTilePosition(pixelPosition), {1_ui8, 1_ui8}})) + constructEventParams(); + } - gralatNPC = npc.get(); - gralatNPC->setScriptType("LOCALN"); + // We collided, so tell the control-NPC and delete the shoot projectile. + if (collided) + { + m_server->getNPCServer()->addEventToControlNPC(ScriptEventType::TRIGGERACTION, shoot->from, (fromPlayer ? "projectile" : "sprojectile"), eventParams); + return false; } + } - // Update rupees - gralatNPC->setRupees(gralatNPC->getRupees() + LevelItem::GetRupeeCount(pItem)); - gralatNPC->updatePropModTime(NPCPROP_RUPEES); - gralatNPC->queueNpcTrigger("update", nullptr, ""); + return true; +} +bool Level::moveArrow(LevelArrow* arrow, int iterations) +{ + if (arrow == nullptr) return false; - } - //TODO: Make a super-class to handle all drops? - if (LevelItemType::DARTS == pItem) + for (int i = 0; i < iterations; ++i) { - if (m_server->getClass("darts") == nullptr) - return true; + // Move the arrow. + arrow->position.translate(arrow->speed.x(), arrow->speed.y()); - NPC* dartNPC = nullptr; + // If the arrow has gone out of bounds, delete it. + // TODO: Maybe just set a max range and make it behave like a shoot? Like the sync distance? Or 2 levels distance? + constexpr auto maxDistance = pixelsPerSubLevel().width() * 2; + if (std::abs(arrow->position.x() - arrow->startPosition.x()) > maxDistance || std::abs(arrow->position.y() - arrow->startPosition.y()) > maxDistance) + return false; - // Find existing rupees, and add to the npc - auto pixelX = static_cast((pX - 0.5) * 16); - auto pixelY = static_cast((pY - 0.5) * 16); + bool hitWall = false; - auto npcList = findAreaNpcs(pixelX, pixelY, 32, 32); - for (auto& npc: npcList) + // Check for NPC collision. + PixelRectangleArea searchBox = {translatePosition(arrow->position, 16_i32, -8_i32), {32_ui16, 32_ui16}}; + auto center = searchBox.center(); + int8_t arrowPower = arrow->type == arrowTypeFireball ? 2 : 1; + for (const auto& npc : findIntersectingNPCsForCollision(searchBox)) { - if (npc->joinedClass("darts")) - { - dartNPC = npc; - break; - } + if (arrow->from.second == ScriptObjectType::NPC && arrow->from.first == npc) + continue; + if (auto npcPtr = m_server->getNPC(npc); npcPtr != nullptr) + npcPtr->hurtAndPush(arrowPower, center, ScriptEventType::WASSHOT, arrow->from); + + hitWall = true; } - // Create a new darts npc for these darts - if (!dartNPC) + // If the arrow is a fireblast or nukeshot, check for walls. + if (!hitWall && (arrow->type == arrowTypeFireblast || arrow->type == arrowTypeNukeshot)) { - auto npc = m_server->addNPC("", "npc.join(\"darts\");", pX, pY, shared_from_this(), false, true); - addNPC(npc); - - dartNPC = npc.get(); - dartNPC->setScriptType("LOCALN"); + if (isOnWall(toWholeTilePosition(arrow->position).translate(1_ui8, 0_ui8))) + hitWall = true; } - // Update darts - dartNPC->setDarts(dartNPC->getDarts() + 1); - dartNPC->updatePropModTime(NPCPROP_ARROWS); - dartNPC->queueNpcTrigger("update", nullptr, ""); - - return false; + // We hit a wall (or an NPC), so destroy the arrow. + if (hitWall) + { + // If we are producing an explosion on hit, do it now. + if (arrow->type == arrowTypeFireblast || arrow->type == arrowTypeNukeshot) + addExplosion(arrow->position, arrow->from, 1_ui8, 1_ui8); + return false; + } } - #endif -#endif - m_items.push_back(LevelItem(pX, pY, pItem)); return true; } -LevelItemType Level::removeItem(float pX, float pY) +//---------------------------- + +bool Level::isOnWall(const WholeTilePosition& tilePosition) const noexcept { - for (auto i = m_items.begin(); i != m_items.end(); ++i) + auto tiletype = getTileTypeAt(tilePosition); + switch (tiletype) { - LevelItem& item = *i; - if (item.getX() == pX && item.getY() == pY) - { - LevelItemType itemType = item.getItem(); - m_items.erase(i); - return itemType; - } + case tileset::TileType::THROW_THROUGH: + case tileset::TileType::JUMP_STONE: + case tileset::TileType::BLOCKING: + return true; } - return LevelItemType::INVALID; + return false; } -bool Level::addHorse(CString& pImage, float pX, float pY, char pDir, char pBushes) +bool Level::isOnWall(const PixelPosition& position) const noexcept { - auto horseLife = m_server->getSettings().getInt("horselifetime", 30); - m_horses.push_back(LevelHorse(horseLife, pImage, pX, pY, pDir, pBushes)); - return true; + return isOnWall(toWholeTilePosition(position)); } -void Level::removeHorse(float pX, float pY) +bool Level::isOnWall2(const WholeTileRectangleArea& tileArea) const noexcept { - for (auto it = m_horses.begin(); it != m_horses.end(); ++it) + // TODO: Optimize this. + for (auto cy = tileArea.position.y(); cy < tileArea.position.y() + tileArea.size.height(); ++cy) { - LevelHorse& horse = *it; - if (horse.getX() == pX && horse.getY() == pY) + for (auto cx = tileArea.position.x(); cx < tileArea.position.x() + tileArea.size.width(); ++cx) { - m_horses.erase(it); - return; + if (isOnWall(WholeTilePosition{cx, cy})) + return true; } } + return false; } -LevelBaddy* Level::addBaddy(float pX, float pY, char pType) +bool Level::isOnWall2(const PixelRectangleArea& area) const noexcept { - // Limit of 50 baddies per level. - if (m_baddies.size() > 50) return nullptr; - - // New Baddy - auto newBaddy = std::make_unique(pX, pY, pType, this->shared_from_this()); - - // Get the next baddy id. - auto new_id = m_baddyIdGenerator.getAvailableId(); - - // Assign the new id. - newBaddy->setId(new_id); - - auto* baddy = newBaddy.get(); - m_baddies[new_id] = std::move(newBaddy); - - return baddy; + return isOnWall2(toWholeTileRectangleArea(area)); } -void Level::removeBaddy(uint8_t pId) +bool Level::isOnWater(const WholeTilePosition& tilePosition) const noexcept { - // Don't allow us to remove id 0 or any id over 50. - if (pId < 1 || pId > 50) return; - - // Find the baddy. - auto iter = m_baddies.find(pId); - if (iter == std::end(m_baddies)) return; - - // Erase the baddy. - auto id = iter->first; - m_baddyIdGenerator.freeId(id); - m_baddies.erase(iter); + auto tiletype = getTileTypeAt(tilePosition); + return tiletype == tileset::TileType::WATER; } -LevelBaddy* Level::getBaddy(uint8_t id) +bool Level::isOnWater(const PixelPosition& position) const noexcept { - auto iter = m_baddies.find(id); - if (iter == std::end(m_baddies)) - return nullptr; - - return iter->second.get(); + return isOnWater(toWholeTilePosition(position)); } -int Level::addPlayer(uint16_t id) +bool Level::isOnWater2(const WholeTileRectangleArea& tileArea) const noexcept { - m_players.push_back(id); - -#ifdef V8NPCSERVER - for (auto& npcId: m_npcs) + // TODO: Optimize this. + for (auto cy = tileArea.position.y(); cy < tileArea.position.y() + tileArea.size.height(); ++cy) { - auto npc = m_server->getNPC(npcId); - if (npc->hasScriptEvent(NPCEVENTFLAG_PLAYERENTERS)) + for (auto cx = tileArea.position.x(); cx < tileArea.position.x() + tileArea.size.width(); ++cx) { - auto player = m_server->getPlayer(id); - npc->queueNpcAction("npc.playerenters", player.get()); + if (isOnWater(WholeTilePosition{cx, cy})) + return true; } } -#endif - - return static_cast(m_players.size() - 1); + return false; } -void Level::removePlayer(uint16_t id) +bool Level::isOnWater2(const PixelRectangleArea& area) const noexcept { - std::erase(m_players, id); + return isOnWater2(toWholeTileRectangleArea(area)); +} -#ifdef V8NPCSERVER - for (auto& npcId: m_npcs) +bool Level::isOnPlayer(const PixelPosition& position) const noexcept +{ + for (const auto& playerId : findInRangePlayers(position)) { - auto npc = m_server->getNPC(npcId); - if (npc->hasScriptEvent(NPCEVENTFLAG_PLAYERLEAVES)) + if (auto player = m_server->getPlayer(playerId); player != nullptr) { - auto player = m_server->getPlayer(id); - npc->queueNpcAction("npc.playerleaves", player.get()); + if (positionInRectangle(position, player->getBoundingBox())) + return true; } } -#endif + return false; } -bool Level::isPlayerLeader(uint16_t id) +bool Level::isOnPlayer(const PixelRectangleArea& pixelArea) const noexcept { - if (m_players.empty()) - return false; - return m_players.front() == id; + for (const auto& playerId : findInRangePlayers(pixelArea.position)) + { + if (auto player = m_server->getPlayer(playerId); player != nullptr) + { + if (rectanglesIntersect(pixelArea, player->getBoundingBox())) + return true; + } + } + return false; } -bool Level::addNPC(std::shared_ptr npc) +tileset::TileType Level::getTileTypeAt(const WholeTilePosition& tilePosition) const noexcept { - [[maybe_unused]] auto [iter, inserted] = m_npcs.insert(npc->getId()); - return inserted; -} + using namespace tileset; -bool Level::addNPC(uint32_t npcId) -{ - [[maybe_unused]] auto [iter, inserted] = m_npcs.insert(npcId); - return inserted; + auto tileDimensions = sizeInTiles(); + if (tilePosition.x() >= tileDimensions.width() || tilePosition.y() >= tileDimensions.height()) + return TileType::BLOCKING; + + auto mapPosition = toMapPosition(tilePosition); + auto tiles = getTiles(mapPosition); + if (!tiles.has_value()) + return TileType::BLOCKING; + + auto localPosition = toLocalWholeTilePosition(tilePosition); + auto tile = tiles.value()->at(static_cast(localPosition.y()) * 64 + localPosition.x()); + + auto tileset = m_server->getTilesetTypeForLevel(shared_from_this()); + return m_server->getTileTypeForTile(tileset, tile); } -void Level::removeNPC(std::shared_ptr npc) +tileset::TileType Level::getTileTypeAt(const PixelPosition& position) const noexcept { - m_npcs.erase(npc->getId()); + return getTileTypeAt(toWholeTilePosition(position)); } -void Level::removeNPC(uint32_t npcId) +//---------------------------- + +bool Level::isGmap() const noexcept { - m_npcs.erase(npcId); + // Kind of a hacky way to determine if it's a gmap when the map is not loaded yet (stubbed). + // The server might try to save an NPC on a stubbed level that doesn't have its map set, so it won't write the map position to the file. + // TODO: Find a better way to handle this. + if (m_map == nullptr && levelName.ends_with(".gmap"sv)) + m_map = m_server->findMap(levelName); + + return m_map != nullptr && m_map->isGmap(); } -void Level::setMap(std::weak_ptr pMap, int pMapX, int pMapY) +uint16_t* Level::getMapTileForEditing(const TilePosition& position) noexcept { - m_map = pMap; - m_mapX = pMapX; - m_mapY = pMapY; + auto subLevel = getSubLevelAtPosition(position); + if (subLevel == nullptr) + return nullptr; + + auto localTilePos = toLocalWholeTilePosition(position); + if (!subLevel->scriptUpdatedTiles.has_value()) + subLevel->scriptUpdatedTiles = LevelTiles(); + + auto layer = subLevel->scriptUpdatedTiles.value().getOrCreateLayer(0); + if (layer == nullptr) + return nullptr; + + return &layer->at(static_cast(localTilePos.y()) * 64 + localTilePos.x()); } -bool Level::doTimedEvents() +std::generator Level::getSubLevelsInRectangle(const PixelRectangleArea& area) const noexcept { - // Check if we should revert any board changes. - for (auto& change: m_boardChanges) + std::pair mapPartsX{area.left() / pixelsPerSubLevel().width(), area.right() / pixelsPerSubLevel().width()}; + std::pair mapPartsY{area.top() / pixelsPerSubLevel().height(), area.bottom() / pixelsPerSubLevel().height()}; + for (auto partY = mapPartsY.first; partY <= mapPartsY.second; ++partY) { - int respawnTimer = change.timeout.doTimeout(); - if (respawnTimer == 0) + for (auto partX = mapPartsX.first; partX <= mapPartsX.second; ++partX) { - // Put the old data back in. DON'T DELETE THE CHANGE. - // The client remembers board changes and if we delete the - // change, the client won't get the new data. - change.swapTiles(); - change.setModTime(time(0)); - m_server->sendPacketToOneLevel(CString() >> (char)PLO_BOARDMODIFY << change.getBoardStr(), this->shared_from_this()); + if (auto sourcePart = getSubLevelAtPosition(MapPosition{partX, partY}); sourcePart != nullptr) + co_yield sourcePart; } } +} - // Check if any items have timed out. - // This allows us to delete items that have disappeared if nobody is in the level to send - // the PLI_ITEMDEL packet. - for (auto i = m_items.begin(); i != m_items.end();) +std::generator Level::getSubLevelsInRectangle(const WholeTileRectangleArea& area) const noexcept +{ + std::pair mapPartsX{area.left() / tilesPerSubLevel().width(), area.right() / tilesPerSubLevel().width()}; + std::pair mapPartsY{area.top() / tilesPerSubLevel().height(), area.bottom() / tilesPerSubLevel().height()}; + for (auto partY = mapPartsY.first; partY <= mapPartsY.second; ++partY) { - LevelItem& item = *i; - int deleteTimer = item.timeout.doTimeout(); - if (deleteTimer == 0) + for (auto partX = mapPartsX.first; partX <= mapPartsX.second; ++partX) { - i = m_items.erase(i); + if (auto sourcePart = getSubLevelAtPosition(MapPosition{partX, partY}); sourcePart != nullptr) + co_yield sourcePart; } - else - ++i; } +} - // Check if any horses need to be deleted. - for (auto i = m_horses.begin(); i != m_horses.end();) - { - LevelHorse& horse = *i; - int deleteTimer = horse.timeout.doTimeout(); - if (deleteTimer == 0) +std::generator Level::getNearbySubLevels(const PixelPosition& position, uint32_t tileDistance) const noexcept +{ + auto sourcePart = getSubLevelAtPosition(position); + if (sourcePart == nullptr) + co_return; + + // Always yield the source part first. + co_yield sourcePart; + + // Now yield parts in an expanding square around the source part. + auto levelPixelSize = pixelsPerSubLevel().width(); + uint32_t levelPartSize = tilesPerSubLevel().width(); + uint32_t distance = 1; + while (distance * levelPartSize <= tileDistance) + { + int32_t negDistance = -static_cast(distance); + std::pair offsets[] = { + // top + {negDistance, negDistance}, + {0, negDistance}, + {distance, negDistance}, + // middle + {negDistance, 0}, + {distance, 0}, + // bottom + {negDistance, distance}, + {0, distance}, + {distance, distance}, + }; + + for (const auto& offset : offsets) { - m_server->sendPacketToOneLevel(CString() >> (char)PLO_HORSEDEL >> (char)(horse.getX() * 2) >> (char)(horse.getY() * 2), this->shared_from_this()); - i = m_horses.erase(i); + auto part = getSubLevelAtPosition(translatePosition(position, offset.first * levelPixelSize, offset.second * levelPixelSize)); + if (part != nullptr && part != sourcePart) + co_yield part; } - else - ++i; + + ++distance; } +} + +//---------------------------- + +std::generator Level::findInRangePlayers(const PixelPosition& position, std::optional> range) const noexcept +{ + bool syncInside = m_server->cached.enableInsideSyncDistance.getValue(); + bool isInsideLevel = !isGmap(); - // Check if any baddies need to be marked as dead or respawned. - std::unordered_set set_dead; - for (auto i = m_baddies.begin(); i != m_baddies.end();) + // If this is not a gmap, and we aren't syncing by distance inside, return all level players. + if (isInsideLevel && !syncInside) { - auto& baddy = i->second; - if (baddy == nullptr) - { - i = m_baddies.erase(i); - continue; - } - ++i; + co_yield std::ranges::elements_of(m_players); + co_return; + } - // See if we can respawn him. - int respawnTimer = baddy->timeout.doTimeout(); - if (respawnTimer == 0) - { - if (baddy->getType() == 4 /*swamp arrow baddy*/ && baddy->getMode() == BDMODE_HURT) - { - if (baddy->getPower() == 1) - { - // Unset the hurt mode on the baddy. - CString props = CString() >> (char)BDPROP_MODE >> (char)BDMODE_SWAMPSHOT; - baddy->setProps(props); - for (unsigned int i = 1; i < m_players.size(); ++i) - { - auto player = m_server->getPlayer(m_players[i]); - player->sendPacket(CString() >> (char)PLO_BADDYPROPS >> (char)baddy->getId() << props); - } - } - } - else if (baddy->getMode() == BDMODE_DIE) - { - // Setting the baddy props could delete the baddy and invalidate our iterator. - // So, save a list of all the baddies we are setting as dead and do it after this loop. - set_dead.insert(baddy.get()); + auto syncx = m_server->cached.syncDistance[0].getValue(); + auto syncy = m_server->cached.syncDistance[1].getValue(); + auto mapSize = sizeInTiles(); + auto tilePosition = toTilePosition(position); - // Set the baddy as dead for all the other players in the level. - CString props = CString() >> (char)BDPROP_MODE >> (char)BDMODE_DEAD; - for (unsigned int i = 1; i < m_players.size(); ++i) - { - auto player = m_server->getPlayer(m_players[i]); - player->sendPacket(CString() >> (char)PLO_BADDYPROPS >> (char)baddy->getId() << props); - } - } - else - { - baddy->reset(); - for (auto p: m_players) - { - auto player = m_server->getPlayer(p); - player->sendPacket(CString() >> (char)PLO_BADDYPROPS >> (char)baddy->getId() << baddy->getProps(player->getVersion())); - } - } - } + if (range.has_value()) + { + syncx = range->first; + syncy = range->second; + } + + // If the sync distance is larger than the level, return all the level players. + if (syncx >= mapSize.width() && syncy >= mapSize.height()) + { + co_yield std::ranges::elements_of(m_players); + co_return; } - { // Mark all the baddies as dead now. - CString props = CString() >> (char)BDPROP_MODE >> (char)BDMODE_DEAD; - for (auto& baddy: set_dead) + + auto playerInRange = [&](const PlayerID& playerId) + { + if (auto player = m_server->getPlayer(playerId); player != nullptr) { - baddy->setProps(props); + auto otherTilePosition = player->getTilePosition(); + return std::abs(tilePosition.x() - otherTilePosition.x()) <= syncx && std::abs(tilePosition.y() - otherTilePosition.y()) <= syncy; } - } + return false; + }; - return true; + // Find all players in range. + for (const auto& playerId : m_players) + { + if (playerInRange(playerId)) + co_yield playerId; + } } -bool Level::isOnWall(int pX, int pY) +std::generator Level::findInRangePlayersForCommunication(const PixelPosition& position) const noexcept { - if (pX < 0 || pY < 0 || pX > 63 || pY > 63) + // If this is not a bigmap, use the default search. + if (!isOnBigMap()) { - return true; + co_yield std::ranges::elements_of(findInRangePlayers(position)); + co_return; } - return tiletypes[getTiles(0)[pY * 64 + pX]] >= 20; -} - -bool Level::isOnWall2(int pX, int pY, int pWidth, int pHeight, uint8_t flags) -{ - for (int cy = pY; cy < pY + pHeight; ++cy) + auto mapPositionOpt = m_map->getLevelPosition(levelName); + if (!mapPositionOpt.has_value()) { - for (int cx = pX; cx < pX + pWidth; ++cx) - { - if (isOnWall(cx, cy)) - { - return true; - } - } + co_return; } - return false; -} + auto& mapPosition = mapPositionOpt.value(); + int startX = mapPosition.x() - 1, endX = mapPosition.x() + 1; + int startY = mapPosition.y() - 1, endY = mapPosition.y() + 1; -bool Level::isOnWater(int pX, int pY) -{ - return (tiletypes[getTiles(0)[pY * 64 + pX]] == 11); -} + if (startX < 0) startX = 0; + if (startY < 0) startY = 0; + if (endX >= m_map->size.width()) endX = m_map->size.width() - 1; + if (endY >= m_map->size.height()) endY = m_map->size.height() - 1; -std::optional Level::getLink(int pX, int pY) const -{ - for (const auto& link: m_links) + for (int y = startY; y <= endY; ++y) { - if ((pX >= link->getX() && pX <= link->getX() + link->getWidth()) && - (pY >= link->getY() && pY <= link->getY() + link->getHeight())) + for (int x = startX; x <= endX; ++x) { - return std::make_optional(link.get()); + auto hintLevel = std::const_pointer_cast(shared_from_this()); + if (auto level = m_server->getLoadedLevel(m_map->getLevelNameAt(x, y), hintLevel); level != nullptr) + co_yield std::ranges::elements_of(level->m_players); } } +} - return std::nullopt; +std::generator Level::findPlayersInLevelPart(std::string_view levelPart) const noexcept +{ + auto position = getSubLevelPositionInMap(levelPart); + if (!position.has_value()) + co_return; + for (const auto& playerId : findPlayersInLevelPart(position.value())) + co_yield playerId; } -std::optional Level::getChest(int x, int y) const +std::generator Level::findPlayersInLevelPart(const MapPosition& mapLevel) const noexcept { - for (const auto& chest: m_chests) + for (const auto& playerId : m_players) { - if (chest->getX() == x && chest->getY() == y) + if (auto player = m_server->getPlayer(playerId); player != nullptr) { - return std::make_optional(chest.get()); + if (player->account.character.mapX == mapLevel.x() && player->account.character.mapY == mapLevel.y()) + co_yield playerId; } } - - return std::nullopt; } -CString Level::getChestStr(LevelChest* chest) const +std::generator Level::findInRangeNPCs(const PixelPosition& position) const noexcept { - static char retVal[500]; - sprintf(retVal, "%i:%i:%s", chest->getX(), chest->getY(), m_levelName.text()); - return retVal; -} + bool syncInside = m_server->cached.enableInsideSyncDistance.getValue(); + bool isInsideLevel = !isGmap(); -LevelLink* Level::addLink() -{ - // New level link - auto newLink = std::make_shared(); + // If this is an inside level and we aren't going to sync by distance inside, return all level NPCs. + if (isInsideLevel && !syncInside) + { + co_yield std::ranges::elements_of(m_npcs); + co_return; + } + + auto syncx = m_server->cached.syncDistance[0].getValue(); + auto syncy = m_server->cached.syncDistance[1].getValue(); + auto mapSize = sizeInTiles(); + auto tilePosition = toTilePosition(position); -#ifdef V8NPCSERVER - m_server->getScriptEngine()->wrapScriptObject(newLink.get()); -#endif + auto npcInRange = [&](const NPCID& npcId) + { + if (auto npc = m_server->getNPC(npcId); npc != nullptr) + { + auto otherTilePosition = npc->getTilePosition(); + return std::abs(tilePosition.x() - otherTilePosition.x()) <= syncx && std::abs(tilePosition.y() - otherTilePosition.y()) <= syncy; + } + return false; + }; - auto* link = newLink.get(); + // Inside level (or bigmap), so just check the NPCs for the level. + if (m_map == nullptr || m_map->isBigMap()) + { + // Sync is greater than the level bounds so return all the NPCs. + if (syncx >= mapSize.width() && syncy >= mapSize.height()) + { + co_yield std::ranges::elements_of(m_npcs); + co_return; + } - m_links.push_back(std::move(newLink)); + for (const auto& npcId : m_npcs) + { + if (npcInRange(npcId)) + co_yield npcId; + } + co_return; + } - return link; + // Gmaps. + // TODO: Optimize by only checking levels in range. + for (const auto& npcId : m_npcs) + { + if (npcInRange(npcId)) + co_yield npcId; + } } -LevelLink* Level::addLink(const std::vector& pLink) +std::generator Level::findInRangeNPCsByDistance(const PixelPosition& position, uint32_t tileDistance) const noexcept { - // New level link - auto newLink = std::make_unique(pLink); - -#ifdef V8NPCSERVER - m_server->getScriptEngine()->wrapScriptObject(newLink.get()); -#endif - - auto* link = newLink.get(); - - m_links.push_back(std::move(newLink)); + // If this is not a map level, return all level NPCs. + if (!isGmap()) + { + co_yield std::ranges::elements_of(m_npcs); + co_return; + } - return link; -} + auto tilePosition = toTilePosition(position); -bool Level::removeLink(uint32_t index) -{ - if (m_links.empty()) - return false; - if (index < 0 || index > m_links.size()) + auto npcInRange = [&](const NPCID& npcId) { + if (auto npc = m_server->getNPC(npcId); npc != nullptr) + { + auto otherTilePosition = npc->getTilePosition(); + auto distance = std::hypotf(tilePosition.x() - otherTilePosition.x(), tilePosition.y() - otherTilePosition.y()); + return distance <= tileDistance; + } return false; - } - else + }; + + // TODO: Optimize by only checking levels in range. + for (const auto& npcId : m_npcs) { - m_links.erase(m_links.begin() + index); - return true; + if (npcInRange(npcId)) + co_yield npcId; } - - return false; } -LevelSign* Level::addSign(const int pX, const int pY, const CString& pSign, bool encoded) +std::generator Level::findIntersectingNPCs(const PixelPosition& position, bool includeInvisible) const noexcept { - // New level link - auto newSign = std::make_unique(pX, pY, pSign, encoded); - -#ifdef V8NPCSERVER - m_server->getScriptEngine()->wrapScriptObject(newSign.get()); -#endif - - auto* sign = newSign.get(); - - m_signs.push_back(std::move(newSign)); - - return sign; + for (const auto& id : findIntersectingNPCs({position, {0, 0, 48}}, includeInvisible)) + co_yield id; } -bool Level::removeSign(uint32_t index) +std::generator Level::findIntersectingNPCs(const PixelRectangleArea& area, bool includeInvisible) const noexcept { - if (m_signs.empty()) - return false; - - if (index < 0 || index > getSigns().size()) - { - return false; - } - else + for (const auto& npcId : findInRangeNPCs(area.position)) { - getSigns().erase(getSigns().begin() + index); + if (auto npc = m_server->getNPC(npcId); npc != nullptr) + { + // If the NPC is invisible and we don't want to include invisible NPCs, skip it. + if (!includeInvisible && (npc->visFlags & PROPID(NPCVisFlags::VISIBLE)) == 0) + continue; - return true; + // Check if the NPC intersects with the area. + if (rectanglesIntersect(area, npc->getBoundingBox())) + co_yield npcId; + } } - - return false; } -LevelChest* Level::addChest(const int pX, const int pY, const LevelItemType itemType, const int signIndex) +std::generator Level::findIntersectingNPCsForCollision(const PixelPosition& position) const noexcept { - // New level link - auto newChest = std::make_unique(pX, pY, itemType, signIndex); - -#ifdef V8NPCSERVER - m_server->getScriptEngine()->wrapScriptObject(newChest.get()); -#endif - - auto* chest = newChest.get(); - - m_chests.push_back(std::move(newChest)); - - return chest; + for (const auto& id : findIntersectingNPCsForCollision({position, {0, 0, 48}})) + co_yield id; } -bool Level::removeChest(uint32_t index) +std::generator Level::findIntersectingNPCsForCollision(const PixelRectangleArea& area) const noexcept { - if (getChests().empty()) - return false; - - if (index < 0 || index > getChests().size()) + for (const auto& npcId : findInRangeNPCs(area.position)) { - return false; - } - else - { - getChests().erase(getChests().begin() + index); + if (auto npc = m_server->getNPC(npcId); npc != nullptr) + { + // If the NPC is invisible, skip it. + if ((npc->visFlags & PROPID(NPCVisFlags::VISIBLE)) == 0) + continue; - return true; + // Check if the NPC intersects with the area. + if (rectanglesIntersect(area, npc->getCollisionBoundingBox())) + co_yield npcId; + } } - - return false; } -#ifdef V8NPCSERVER +//---------------------------- -std::vector Level::findAreaNpcs(int pX, int pY, int pWidth, int pHeight) +std::shared_ptr Level::generateItemNPC(const PixelPosition& position, LevelItemType item) { - int testEndX = pX + pWidth; - int testEndY = pY + pHeight; + if (!m_server->hasNPCServer()) + return nullptr; - std::vector npcList; - for (const auto& npcId: m_npcs) - { - auto npc = m_server->getNPC(npcId); - if (pX < npc->getPixelX() + npc->getWidth() && testEndX > npc->getPixelX() && - pY < npc->getPixelY() + npc->getHeight() && testEndY > npc->getPixelY()) - { - npcList.push_back(npc.get()); - } - } + auto itemName = LevelItem::getItemName(item); + if (LevelItem::isRupeeType(item)) + itemName = "gralats"; - return npcList; -} + auto itemclass = m_server->getNPCServer()->getClass(itemName).lock(); + if (itemclass == nullptr) + return nullptr; -std::vector Level::testTouch(int pX, int pY) -{ - std::vector npcList; - for (const auto& npcId: m_npcs) - { - auto npc = m_server->getNPC(npcId); - if (npc->hasScriptEvent(NPCEVENTFLAG_PLAYERTOUCHSME) && (npc->getVisibleFlags() & NPCVISFLAG_VISIBLE) != 0) + static std::unordered_map stackableItems = { - if (npc->getPixelX() <= pX && npc->getPixelX() + npc->getWidth() >= pX && - npc->getPixelY() <= pY && npc->getPixelY() + npc->getHeight() >= pY) - { - npcList.push_back(npc.get()); - } + {LevelItemType::GREENRUPEE, NPCProp::RUPEES}, + {LevelItemType::BLUERUPEE, NPCProp::RUPEES}, + {LevelItemType::REDRUPEE, NPCProp::RUPEES}, + {LevelItemType::GOLDRUPEE, NPCProp::RUPEES}, + {LevelItemType::BOMBS, NPCProp::BOMBS}, + {LevelItemType::DARTS, NPCProp::ARROWS}, + {LevelItemType::HEART, NPCProp::POWER}, + }; + static std::unordered_map stackableCount = + { + {LevelItemType::GREENRUPEE, 1}, + {LevelItemType::BLUERUPEE, 5}, + {LevelItemType::REDRUPEE, 30}, + {LevelItemType::GOLDRUPEE, 100}, + {LevelItemType::BOMBS, 5}, + {LevelItemType::DARTS, 5}, + {LevelItemType::HEART, 2}, + }; + + std::shared_ptr itemNPC = nullptr; + + // Determine the NPC location. + TilePosition loc = toTilePosition(position).translate(-0.5f, -1.0f); + + auto stackable = stackableItems.find(item); + if (stackable != stackableItems.end()) + { + // Find existing items, and stack with the existing. + PixelRectangleArea searchArea{toPixelPosition(loc).translate(-2 * 16, -2 * 16), {6 * 16, 6 * 16}}; + auto npcList = findIntersectingNPCs(searchArea); + for (const auto& npcId : npcList) + { + if (auto npc = m_server->getNPC(npcId); npc != nullptr && npc->hasJoinedClass(itemName)) + itemNPC = npc; } } - return npcList; -} - -NPC* Level::isOnNPC(float pX, float pY, bool checkEventFlag) -{ - for (const auto& npcId: m_npcs) + // Create a new npc for this item. + bool isNew = !itemNPC; + if (isNew) { - auto npc = m_server->getNPC(npcId); - if (checkEventFlag && !npc->hasScriptEvent(NPCEVENTFLAG_PLAYERTOUCHSME)) - continue; + itemNPC = m_server->getNPCServer()->addNPC("", std::format("if (created) join {};", itemName), shared_from_this(), {loc[0], loc[1]}, NPCTYPE_ITEM); + itemNPC->character.gralats = itemNPC->character.arrows = itemNPC->character.bombs = itemNPC->character.hitpointsInHalves = 0; + } - //if (!npc->getImage().isEmpty()) + // If this NPC has stackable items, set the count. + if (stackable != stackableItems.end()) + { + uint8_t stackCount = stackableCount[item]; + props::SetResults results; + switch (stackable->second) { - if ((npc->getVisibleFlags() & 1) != 0) - { - if ((pX >= npc->getX() && pX <= npc->getX() + (float)(npc->getWidth() / 16.0f)) && - (pY >= npc->getY() && pY <= npc->getY() + (float)(npc->getHeight() / 16.0f))) - { - // what if it touches multiple npcs? hm. not sure how graal did it. - return npc.get(); - } - } + case NPCProp::RUPEES: + results = itemNPC->setPropWith(props::SetBy::SERVER, static_cast(itemNPC->getProp().value + stackCount)); + break; + case NPCProp::BOMBS: + results = itemNPC->setPropWith(props::SetBy::SERVER, static_cast(itemNPC->getProp().value + stackCount)); + break; + case NPCProp::ARROWS: + results = itemNPC->setPropWith(props::SetBy::SERVER, static_cast(itemNPC->getProp().value + stackCount)); + break; + case NPCProp::POWER: + results = itemNPC->setPropWith(props::SetBy::SERVER, static_cast(itemNPC->getProp().value + stackCount)); + break; } + itemNPC->sendPropsFromResults(results); } - return nullptr; -} - -void Level::sendChatToLevel(const Player* player, const std::string& message) -{ - for (const auto& npcId: m_npcs) - { - auto npc = m_server->getNPC(npcId); - if (npc->hasScriptEvent(NPCEVENTFLAG_PLAYERCHATS)) - npc->queueNpcEvent("npc.playerchats", true, player->getScriptObject(), message); - } + // Update the item. + itemNPC->scripting.events.addEvent(ScriptEventType::CUSTOM, source::FromNPC(itemNPC->id), "updategani"); + return itemNPC; } -void Level::modifyBoardDirect(uint32_t index, short tile) +size_t Level::getMapIndexAtPosition(const MapPosition& mapLevel) const noexcept { - int pX = index % 64; - int pY = index / 64; + if (m_map == nullptr) + return 0; - short oldTile = m_tiles[0][index]; - m_tiles[0][index] = tile; + size_t maxLevels = static_cast(m_map->size.width()) * m_map->size.height(); + return std::min(maxLevels, (static_cast(m_map->size.width()) * mapLevel.y()) + mapLevel.x()); +} - auto change = LevelBoardChange(pX, pY, 1, 1, CString() >> tile, CString() >> oldTile, -1); +//---------------------------- - m_boardChanges.push_back(change); - m_server->sendPacketToOneLevel(CString() >> (char)PLO_BOARDMODIFY << change.getBoardStr(), shared_from_this()); +ScriptObject source::FromLevel(LevelPtr level) +{ + size_t hash = string::string_hash{}(level->levelName); + return std::make_pair(hash, ScriptObjectType::LEVEL); } -#endif +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/level/LevelBaddy.cpp b/server/src/level/LevelBaddy.cpp index c32bd8b75..7f95e7aa9 100644 --- a/server/src/level/LevelBaddy.cpp +++ b/server/src/level/LevelBaddy.cpp @@ -1,47 +1,89 @@ -#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include #include -#include -#include "Server.h" -#include "level/Level.h" -#include "level/LevelBaddy.h" +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// -const int baddytypes = 10; -const char* baddyImages[baddytypes] = { +constexpr int baddytypes = 10; +constexpr const char* baddyImages[baddytypes] = { "baddygray.png", "baddyblue.png", "baddyred.png", "baddyblue.png", "baddygray.png", "baddyhare.png", "baddyoctopus.png", "baddygold.png", "baddylizardon.png", "baddydragon.png" }; -const char baddyStartMode[baddytypes] = { - BDMODE_WALK, BDMODE_WALK, BDMODE_WALK, BDMODE_WALK, BDMODE_SWAMPSHOT, - BDMODE_HAREJUMP, BDMODE_WALK, BDMODE_WALK, BDMODE_WALK, BDMODE_WALK +constexpr BaddyMode baddyStartMode[baddytypes] = { + BaddyMode::WALK, BaddyMode::WALK, BaddyMode::WALK, BaddyMode::WALK, BaddyMode::SWAMPSHOT, + BaddyMode::HAREJUMP, BaddyMode::WALK, BaddyMode::WALK, BaddyMode::WALK, BaddyMode::WALK }; -const int baddyPower[baddytypes] = { +constexpr const int baddyPower[baddytypes] = { 2, 3, 4, 3, 2, 1, 1, 6, 12, 8 }; -LevelBaddy::LevelBaddy(const float pX, const float pY, const unsigned char pType, std::weak_ptr pLevel) - : m_level(pLevel), m_type(pType), - m_startX(pX), m_startY(pY) +/////////////////////////////////////////////////////////////////////////////// + +BaddyType LevelBaddy::getBaddyTypeFromString(const std::string& type) +{ + // Try by name. + for (unsigned int i = 0; i < BaddyNames.size(); ++i) + { + if (string::equalsi(BaddyNames[i], type)) + return BaddyType(i); + } + + // Try by ID. + uint32_t itemId = 0; + if (string::toNumber(type, itemId) && itemId < BaddyNames.size()) + return BaddyType(itemId); + + // Bad. + return BaddyType::GRAYSOLDIER; +} + +/////////////////////////////////////////////////////////////////////////////// + +LevelBaddy::LevelBaddy(const LocalPixelPosition& position, BaddyType type, std::weak_ptr level) + : type(type), position(position), m_level(level), m_originalPosition(position) { - if (pType > baddytypes) m_type = 0; - m_verses.resize(3); + m_server = BabyDI::Get(); + assert(m_server != nullptr); + if (PROPID(type) > baddytypes) type = BaddyType::GRAYSOLDIER; + verses.resize(3); reset(); } void LevelBaddy::reset() { - m_mode = baddyStartMode[(int)m_type]; - m_x = m_startX; - m_y = m_startY; - m_power = baddyPower[(int)m_type]; - m_image = baddyImages[(int)m_type]; - m_dir = (2 << 2) | 2; // Both head/body direction is encoded in dir. - m_ani = 0; + mode = baddyStartMode[PROPID(type)]; + power = baddyPower[PROPID(type)]; + image = baddyImages[PROPID(type)]; + position = m_originalPosition; + direction = 2; + headDirection = 2; + animation = 0; m_hasCustomImage = false; } -void LevelBaddy::dropItem() +void LevelBaddy::dropItem() const { // 41.66...% chance of a green gralat. // 41.66...% chance of something else. @@ -69,53 +111,50 @@ void LevelBaddy::dropItem() if (itemType != LevelItemType::INVALID) { if (auto lvl = m_level.lock(); lvl) - { - if (lvl->addItem(this->m_x, this->m_y, itemType)) - m_server->sendPacketToOneLevel(CString() >> (char)PLO_ITEMADD >> (char)(this->m_x * 2) >> (char)(this->m_y * 2) >> (char)LevelItem::getItemTypeId(itemType), m_level); - } + lvl->addItem(inform_client, toPixelPosition({ 0, 0 }, position), itemType); } } -CString LevelBaddy::getProp(const int propId, int clientVersion) const +CString LevelBaddy::getProp(BaddyProp propId) const { switch (propId) { - case BDPROP_ID: - return CString() >> (char)m_id; + case BaddyProp::ID: + return CString() >> (char)id; - case BDPROP_X: - return CString() >> (char)(m_x * 2); + case BaddyProp::X: + return CString() >> (char)(position.x() / 8); - case BDPROP_Y: - return CString() >> (char)(m_y * 2); + case BaddyProp::Y: + return CString() >> (char)(position.y() / 8); - case BDPROP_TYPE: - return CString() >> (char)m_type; + case BaddyProp::TYPE: + return CString() >> (char)PROPID(type); - case BDPROP_POWERIMAGE: + case BaddyProp::POWERIMAGE: { - if (clientVersion < CLVER_2_1 && m_image == baddyImages[(int)m_type]) - return CString() >> (char)m_power >> (char)m_image.length() << m_image.replaceAll(".png", ".gif"); + if (m_server->Generation == ServerGeneration::ORIGINAL && image == baddyImages[PROPID(type)]) + return CString() >> (char)power >> (char)image.length() << string::replace(image, ".png", ".gif"); else - return CString() >> (char)m_power >> (char)m_image.length() << m_image; + return CString() >> (char)power >> (char)image.length() << image; } - case BDPROP_MODE: - return CString() >> (char)m_mode; + case BaddyProp::MODE: + return CString() >> (char)PROPID(mode); - case BDPROP_ANI: - return CString() >> (char)m_ani; + case BaddyProp::ANI: + return CString() >> (char)animation; - case BDPROP_DIR: - return CString() >> (char)m_dir; + case BaddyProp::DIR: + return CString() >> (char)(headDirection << 2 | direction); - case BDPROP_VERSESIGHT: - case BDPROP_VERSEHURT: - case BDPROP_VERSEATTACK: + case BaddyProp::VERSESIGHT: + case BaddyProp::VERSEHURT: + case BaddyProp::VERSEATTACK: { - unsigned int verseId = propId - BDPROP_VERSESIGHT; - if (verseId < m_verses.size()) - return CString() >> (char)m_verses[verseId].length() << m_verses[verseId]; + size_t verseId = PROPID(propId) - PROPID(BaddyProp::VERSESIGHT); + if (verseId < verses.size()) + return CString() >> (char)verses[verseId].length() << verses[verseId]; else return CString() >> (char)0; } @@ -123,49 +162,47 @@ CString LevelBaddy::getProp(const int propId, int clientVersion) const return CString(); } -CString LevelBaddy::getProps(int clientVersion) const +CString LevelBaddy::getProps() const { CString retVal; - for (int i = 1; i < BDPROP_COUNT; i++) - retVal >> (char)i << getProp(i, clientVersion); + for (size_t i = 1; i < BADDYPROP_COUNT; i++) + retVal >> (char)i << getProp(static_cast(i)); return retVal; } -void LevelBaddy::setProps(CString& pProps) +void LevelBaddy::setPropsFromPacket(CString& pProps) { int len = 0; while (pProps.bytesLeft()) { - unsigned char propId = pProps.readGUChar(); + BaddyProp propId = static_cast(pProps.readGUChar()); switch (propId) { - case BDPROP_ID: - m_id = pProps.readGChar(); + case BaddyProp::ID: + id = pProps.readGChar(); break; - case BDPROP_X: - m_x = (float)pProps.readGChar() / 2.0f; - m_x = clip(m_x, 0.0f, 63.5f); + case BaddyProp::X: + position.x() = static_cast(std::clamp(pProps.readGChar() * 8, 0, 1016)); // 0 - 63.5 break; - case BDPROP_Y: - m_y = (float)pProps.readGChar() / 2.0f; - m_y = clip(m_y, 0.0f, 63.5f); + case BaddyProp::Y: + position.y() = static_cast(std::clamp(pProps.readGChar() * 8, 0, 1016)); // 0 - 63.5 break; - case BDPROP_TYPE: - m_type = pProps.readGChar(); + case BaddyProp::TYPE: + type = static_cast(pProps.readGChar()); break; - case BDPROP_POWERIMAGE: + case BaddyProp::POWERIMAGE: { - m_power = pProps.readGChar(); + power = pProps.readGChar(); if (pProps.bytesLeft() != 0) { CString newImage = pProps.readChars(pProps.readGUChar()); if (newImage.isEmpty()) - m_image = baddyImages[(int)m_type]; + image = baddyImages[PROPID(type)]; else { // Why we need this I have no idea. @@ -173,62 +210,107 @@ void LevelBaddy::setProps(CString& pProps) if (m_hasCustomImage == false) { m_hasCustomImage = true; - m_image = newImage; + image = newImage; } } } } break; - case BDPROP_MODE: - m_mode = pProps.readGChar(); - if (m_type == 4 && m_mode == BDMODE_HURT) + case BaddyProp::MODE: + { + mode = static_cast(pProps.readGChar()); + + // Swamp soldiers can get stuck in a hurt animation and become invulnerable. + auto fixStuckSwampSoldier = [this](int) { - // Workaround for buggy client. In 2 seconds, set us back to BDMODE_SWAMPSHOT from - // inside Level.cpp. - timeout.setTimeout(2); - } - else if (m_mode == BDMODE_DIE) + if (power == 1) + { + mode = BaddyMode::SWAMPSHOT; + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_BADDYPROPS >> (char)id >> (char)BaddyProp::MODE >> (char)mode, { 0, 0 }, m_level.lock()); + } + }; + + // Reset and respawn baddies. + auto respawnBaddy = [this](int) { - // In 2 seconds, set our mode to BDMODE_DEAD inside Level.cpp. - timeout.setTimeout(2); + if (!canRespawn()) return; + reset(); + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_BADDYPROPS >> (char)id << getProps(), { 0, 0 }, m_level.lock()); + }; + // Set baddies to dead. + auto setDead = [this, respawnBaddy](int) + { + mode = BaddyMode::DEAD; + if (canRespawn()) + { + timeout.callbackIterations = respawnBaddy; + timeout.runOnceFor(std::chrono::seconds(m_server->getSettings().get("baddyrespawntime").value_or(60))); + } + + if (auto level = m_level.lock(); level != nullptr) + { + // Set the baddy as dead for all the other players in the level. + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_BADDYPROPS >> (char)id >> (char)BaddyProp::MODE >> (char)mode, { 0, 0 }, level); + + // TODO(Nalin): Record the last player who hit the baddy so we can record the source properly. + if (!level->hasLivingBaddies()) + m_server->queueNPCEventLocal(level, ScriptEventType::COMPUSDIED, source::FromLevel(level)); + } + }; + + if (type == BaddyType::SWAMPSOLDIER && mode == BaddyMode::HURT) + { + timeout.callbackIterations = fixStuckSwampSoldier; + timeout.runOnceFor(2s); + } + else if (mode == BaddyMode::DIE) + { // Drop items when dead. - if (m_server->getSettings().getBool("baddyitems", false) == true) + if (m_server->getSettings().get("baddyitems").value_or(false) == true) dropItem(); + + // Set the baddy to dead after 2 seconds. + timeout.callbackIterations = setDead; + timeout.runOnceFor(2s); } - else if (m_mode == BDMODE_DEAD) + else if (mode == BaddyMode::DEAD && m_canRespawn) { - if (m_canRespawn) - timeout.setTimeout(m_server->getSettings().getInt("baddyrespawntime", 60)); - else - { - if (auto lvl = m_level.lock(); lvl) - lvl->removeBaddy(m_id); - else - delete this; - return; - } + timeout.callbackIterations = respawnBaddy; + timeout.runOnceFor(std::chrono::seconds(m_server->getSettings().get("baddyrespawntime").value_or(60))); } break; + } - case BDPROP_ANI: - m_ani = pProps.readGChar(); + case BaddyProp::ANI: + animation = pProps.readGChar(); break; - case BDPROP_DIR: - m_dir = pProps.readGChar(); + case BaddyProp::DIR: + direction = pProps.readGChar(); + headDirection = direction >> 2; + direction &= 0b11; break; - case BDPROP_VERSESIGHT: - case BDPROP_VERSEHURT: - case BDPROP_VERSEATTACK: + case BaddyProp::VERSESIGHT: + case BaddyProp::VERSEHURT: + case BaddyProp::VERSEATTACK: { len = pProps.readGUChar(); - unsigned int verseId = propId - BDPROP_VERSESIGHT; - if (verseId < m_verses.size()) - m_verses[verseId] = pProps.readChars(len); + size_t verseId = PROPID(propId) - PROPID(BaddyProp::VERSESIGHT); + if (verseId < verses.size()) + verses[verseId] = pProps.readChars(len); } } } } + +void LevelBaddy::setImage(std::string_view image) +{ + image = image; + m_hasCustomImage = true; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/level/LevelBoardChange.cpp b/server/src/level/LevelBoardChange.cpp index c5a78cf40..994a80c8b 100644 --- a/server/src/level/LevelBoardChange.cpp +++ b/server/src/level/LevelBoardChange.cpp @@ -1,15 +1,116 @@ -#include +#include +#include +#include -#include "level/LevelBoardChange.h" +#include +#include -CString LevelBoardChange::getBoardStr() const +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +LevelBoardChange::LevelBoardChange(std::shared_ptr level, const LocalWholeTileRectangleArea& area, const CString& tiles, const CString& oldTiles, std::chrono::seconds respawnTime) + : area(area), m_level(level), m_newTiles(tiles), m_oldTiles(oldTiles) +{ + m_server = BabyDI::Get(); + assert(m_server != nullptr); + + modTime = m_server->getFrameStartTime(); + + if (respawnTime != 0s) + m_timeout.runOnceFor(respawnTime); +} + +LevelBoardChange::LevelBoardChange(std::shared_ptr level, const MapPosition& mapPosition, const LocalWholeTileRectangleArea& area, const CString& tiles, const CString& oldTiles, std::chrono::seconds respawnTime) + : LevelBoardChange(level, area, tiles, oldTiles, respawnTime) { - return CString() >> (char)m_x >> (char)m_y >> (char)m_width >> (char)m_height << m_newTiles; + m_mapPosition = mapPosition; +} + +void LevelBoardChange::update(const precise_clock::time_point& time) +{ + if (m_timeout.isRunning()) + { + m_timeout.update(time); + if (!m_timeout.isRunning()) + { + swapTiles(); + sendToPlayersOnLevel(); + } + } +} + +void LevelBoardChange::sendToPlayersOnLevel() const +{ + if (auto level = m_level.lock(); level != nullptr) + { + if (!level->isGmap()) + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_BOARDMODIFY << getPropsForSingleLevel(), { 0, 0 }, level); + else + { + m_server->sendPacketToNearby(CString() >> (char)PLO_BOARDMODIFY2 << getPropsForMapClassic(), toPixelPosition(m_mapPosition.value(), area.position), level, {}); + + /* + // Classic mode clients don't support board updates in adjacent levels, but still need the map position. + server->sendPacketToOneLevel(CString() >> (char)PLO_BOARDMODIFY2 << getPropsForMapClassic(), level, {}, [](const Player* player) { return player->getVersion() < CLVER_4_0211; }); + + // Newmain and up can see nearby level board changes. + server->sendPacketToNearby(CString() >> (char)PLO_BOARDMODIFY2 << getPropsForMapNewMain(), toPixelPosition(this->area.position), level, {}, [](const Player* player) { return player->getVersion() >= CLVER_4_0211; }); + */ + } + } } +CString LevelBoardChange::getPropsForSingleLevel() const +{ + // {7}{CHAR tileX}{CHAR tileY}{CHAR width}{CHAR height}{tiles} + // {7}{CHAR layer +64}{CHAR tileX}{CHAR tileY}{CHAR width}{CHAR height}{tiles} + if (layer == 0) [[likely]] + return CString() >> (char)area.position.x() >> (char)area.position.y() >> (char)area.size.width() >> (char)area.size.height() << m_newTiles; + + return CString() >> (char)(layer + 64) >> (char)area.position.x() >> (char)area.position.y() >> (char)area.size.width() >> (char)area.size.height() << m_newTiles; +} + +CString LevelBoardChange::getPropsForMapClassic() const +{ + // {186}{CHAR mapX}{CHAR mapY}{CHAR tileX}{CHAR tileY}{CHAR width}{CHAR height}{tiles} + // {186}{CHAR mapX}{CHAR mapY}{CHAR layer +64}{CHAR tileX}{CHAR tileY}{CHAR width}{CHAR height}{tiles} + + if (m_level.expired() || !m_mapPosition.has_value()) + return CString(); + + const auto& [mapX, mapY, _] = m_mapPosition.value(); + if (layer == 0) [[likely]] + return CString() >> (char)mapX >> (char)mapY >> (char)area.position.x() >> (char)area.position.y() >> (char)area.size.width() >> (char)area.size.height() << m_newTiles; + + return CString() >> (char)mapX >> (char)mapY >> (char)(layer + 64) >> (char)area.position.x() >> (char)area.position.y() >> (char)area.size.width() >> (char)area.size.height() << m_newTiles; +} + +/* +CString LevelBoardChange::getPropsForMapNewMain() const +{ + props::PropertyPixelCoordinate positionX{ static_cast(area.position.x()) }; + props::PropertyPixelCoordinate positionY{ static_cast(area.position.y()) }; + return CString() << positionX.serialize() << positionY.serialize() >> (char)area.size.width() >> (char)area.size.height() << m_newTiles; +} +*/ + void LevelBoardChange::swapTiles() { CString temp = m_newTiles; m_newTiles = m_oldTiles; m_oldTiles = temp; + + modTime = m_server->getFrameStartTime(); } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/level/LevelItem.cpp b/server/src/level/LevelItem.cpp index 799fb16f8..1c740a2df 100644 --- a/server/src/level/LevelItem.cpp +++ b/server/src/level/LevelItem.cpp @@ -1,47 +1,23 @@ -#include +#include +#include +#include + +#include #include -#include "Player.h" -#include "level/LevelItem.h" - -const char* __itemList[] = { - "greenrupee", // 0 - "bluerupee", // 1 - "redrupee", // 2 - "bombs", // 3 - "darts", // 4 - "heart", // 5 - "glove1", // 6 - "bow", // 7 - "bomb", // 8 - "shield", // 9 - "sword", // 10 - "fullheart", // 11 - "superbomb", // 12 - "battleaxe", // 13 - "goldensword", // 14 - "mirrorshield", // 15 - "glove2", // 16 - "lizardshield", // 17 - "lizardsword", // 18 - "goldrupee", // 19 - "fireball", // 20 - "fireblast", // 21 - "nukeshot", // 22 - "joltbomb", // 23 - "spinattack" // 24 -}; - -const int __itemCount = (sizeof(__itemList) / sizeof(const char*)); - -CString LevelItem::getItemStr() const +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal { - return CString() >> (char)PLO_ITEMADD >> (char)m_x >> (char)m_y >> (char)m_item; -} +/////////////////////////////////////////////////////////////////////////////// LevelItemType LevelItem::getItemId(signed char itemId) { - if (itemId < 0 || itemId >= __itemCount) + if (itemId < 0 || (size_t)itemId >= ItemNames.size()) return LevelItemType::INVALID; return LevelItemType(itemId); @@ -49,20 +25,27 @@ LevelItemType LevelItem::getItemId(signed char itemId) LevelItemType LevelItem::getItemId(const std::string& pItemName) { - for (unsigned int i = 0; i < __itemCount; ++i) + // Try by name. + for (unsigned int i = 0; i < ItemNames.size(); ++i) { - if (__itemList[i] == pItemName) + if (ItemNames[i] == pItemName) return LevelItemType(i); } + // Try by ID. + uint32_t itemId = 0; + if (string::toNumber(pItemName, itemId) && itemId < ItemNames.size()) + return LevelItemType(itemId); + + // Bad item. return LevelItemType::INVALID; } std::string LevelItem::getItemName(LevelItemType itemId) { - auto id = LevelItem::getItemTypeId(itemId); - if (id < 0 || id >= __itemCount) return {}; - return std::string(__itemList[id]); + size_t id = LevelItem::getItemTypeId(itemId); + if (id >= ItemNames.size()) return {}; + return std::string(ItemNames[id]); } CString LevelItem::getItemPlayerProp(LevelItemType itemType, Player* player) @@ -74,8 +57,9 @@ CString LevelItem::getItemPlayerProp(LevelItemType itemType, Player* player) case LevelItemType::REDRUPEE: // redrupee case LevelItemType::GOLDRUPEE: // goldrupee { - int rupeeCount = player->getRupees(); - if (itemType == LevelItemType::GOLDRUPEE) rupeeCount += 100; + int rupeeCount = player->account.character.gralats; + if (itemType == LevelItemType::GOLDRUPEE) + rupeeCount += 100; else if (itemType == LevelItemType::REDRUPEE) rupeeCount += 30; else if (itemType == LevelItemType::BLUERUPEE) @@ -83,38 +67,38 @@ CString LevelItem::getItemPlayerProp(LevelItemType itemType, Player* player) else rupeeCount += 1; - rupeeCount = clip(rupeeCount, 0, 9999999); - return CString() >> (char)PLPROP_RUPEESCOUNT >> (int)rupeeCount; + rupeeCount = std::clamp(rupeeCount, 0, 9999999); + return CString() >> (char)PlayerProp::RUPEESCOUNT >> (int)rupeeCount; } case LevelItemType::BOMBS: // bombs { - int bombCount = clip(player->getBombCount() + 5, 0, 99); - return CString() >> (char)PLPROP_BOMBSCOUNT >> (char)bombCount; + int bombCount = std::clamp(player->account.character.bombs + 5, 0, 99); + return CString() >> (char)PlayerProp::BOMBSCOUNT >> (char)bombCount; } case LevelItemType::DARTS: // darts { - int arrowCount = clip(player->getArrowCount() + 5, 0, 99); - return CString() >> (char)PLPROP_ARROWSCOUNT >> (char)arrowCount; + int arrowCount = std::clamp(player->account.character.arrows + 5, 0, 99); + return CString() >> (char)PlayerProp::ARROWSCOUNT >> (char)arrowCount; } case LevelItemType::HEART: // heart { - float newPower = clip(player->getPower() + 1.0f, 0.0f, player->getMaxPower() * 1.0f); - return CString() >> (char)PLPROP_CURPOWER >> (char)(newPower * 2.0f); + uint8_t newPower = std::clamp(player->account.character.hitpointsInHalves + 2, 0, player->account.maxHitpoints * 2); + return CString() >> (char)PlayerProp::CURPOWER >> (char)(newPower); } case LevelItemType::GLOVE1: // glove1 case LevelItemType::GLOVE2: // glove2 { - auto glovePower = player->getGlovePower(); + auto glovePower = player->account.character.glovePower; if (itemType == LevelItemType::GLOVE2) glovePower = 3; else if (glovePower < 2) glovePower = 2; - return CString() >> (char)PLPROP_GLOVEPOWER >> (char)glovePower; + return CString() >> (char)PlayerProp::GLOVEPOWER >> (char)glovePower; } case LevelItemType::BOW: // bow @@ -139,10 +123,10 @@ CString LevelItem::getItemPlayerProp(LevelItemType itemType, Player* player) else if (itemType == LevelItemType::MIRRORSHIELD) newShieldPower = 2; - if (player->getShieldPower() > newShieldPower) - newShieldPower = player->getShieldPower(); + if (player->account.character.shieldPower > newShieldPower) + newShieldPower = player->account.character.shieldPower; - return CString() >> (char)PLPROP_SHIELDPOWER >> (char)newShieldPower; + return CString() >> (char)PlayerProp::SHIELDPOWER >> (char)newShieldPower; } case LevelItemType::SWORD: // sword @@ -150,7 +134,7 @@ CString LevelItem::getItemPlayerProp(LevelItemType itemType, Player* player) case LevelItemType::LIZARDSWORD: // lizardsword case LevelItemType::GOLDENSWORD: // goldensword { - char swordPower = (char)player->getSwordPower(); + char swordPower = (char)player->account.character.swordPower; if (itemType == LevelItemType::GOLDENSWORD) swordPower = 4; else if (itemType == LevelItemType::LIZARDSWORD) swordPower = (swordPower < 3 ? 3 : swordPower); @@ -158,22 +142,21 @@ CString LevelItem::getItemPlayerProp(LevelItemType itemType, Player* player) swordPower = (swordPower < 2 ? 2 : swordPower); else swordPower = (swordPower < 1 ? 1 : swordPower); - return CString() >> (char)PLPROP_SWORDPOWER >> (char)swordPower; + return CString() >> (char)PlayerProp::SWORDPOWER >> (char)swordPower; } case LevelItemType::FULLHEART: // fullheart { - char heartMax = clip(player->getMaxPower() + 1, 0, 20); // Hard limit of 20 hearts. - return CString() >> (char)PLPROP_MAXPOWER >> (char)heartMax >> (char)PLPROP_CURPOWER >> (char)(heartMax * 2); + char heartMax = std::clamp(player->account.maxHitpoints + 1, 0, 20); // Hard limit of 20 hearts. + return CString() >> (char)PlayerProp::MAXPOWER >> (char)heartMax >> (char)PlayerProp::CURPOWER >> (char)(heartMax * 2); } case LevelItemType::SPINATTACK: // spinattack { - CString playerProp = player->getProp(PLPROP_STATUS); - char status = playerProp.readGChar(); + auto status = player->getProp().value; if (status & PLSTATUS_HASSPIN) return {}; status |= PLSTATUS_HASSPIN; - return CString() >> (char)PLPROP_STATUS >> (char)status; + return CString() >> (char)PlayerProp::STATUS >> (char)status; } default: @@ -182,3 +165,7 @@ CString LevelItem::getItemPlayerProp(LevelItemType itemType, Player* player) return {}; } + +/////////////////////////////////////////////////////////////////////////////// + +} // end namespace preagonal diff --git a/server/src/level/LevelLink.cpp b/server/src/level/LevelLink.cpp index 9b015eea2..a0fc3d1bd 100644 --- a/server/src/level/LevelLink.cpp +++ b/server/src/level/LevelLink.cpp @@ -1,24 +1,75 @@ -#include +#include +#include +#include +#include -#include "level/LevelLink.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +static bool checkIfComplex(std::string_view destination) +{ + if (destination != "playerx" && destination != "playery" && !string::isFloat(destination)) + return true; + return false; +} + +static int16_t getPixelDestination(std::string_view destination, const Character& character) +{ + if (destination == "playerx") + return character.localPixelX; + if (destination == "playery") + return character.localPixelY; + return static_cast(string::toFloat(destination) * 16); +} + +/////////////////////////////////////////////////////////////////////////////// -/* - LevelLink: Constructor - Deconstructor -*/ LevelLink::LevelLink(const std::vector& pLink) { parseLinkStr(pLink); + + m_server = BabyDI::Get(); + m_complex[0] = checkIfComplex(m_destinationX); + m_complex[1] = checkIfComplex(m_destinationY); } -/* - LevelLink: Functions -*/ +LevelLink::LevelLink(const Rectangle& coordinates, std::string_view destinationX, std::string_view destinationY, std::string_view destinationLevel) + : m_destinationLevel{destinationLevel}, m_destinationX{destinationX}, m_destinationY{destinationY}, m_boundingBox{coordinates} +{ + if (m_destinationX == "-1") + { + m_constantX = true; + m_destinationX = "playerx"; + } + + if (m_destinationY == "-1") + { + m_constantY = true; + m_destinationY = "playery"; + } + + m_server = BabyDI::Get(); + m_complex[0] = checkIfComplex(m_destinationX); + m_complex[1] = checkIfComplex(m_destinationY); +} CString LevelLink::getLinkStr() const { - static char retVal[500]; - sprintf(retVal, "%s %i %i %i %i %s %s", m_newLevel.text(), m_x, m_y, m_width, m_height, m_newX.text(), m_newY.text()); - return retVal; + return std::format("{} {} {} {} {} {} {}", m_destinationLevel, m_boundingBox.position.x(), m_boundingBox.position.y(), m_boundingBox.size.width(), m_boundingBox.size.height(), m_destinationX, m_destinationY); } void LevelLink::parseLinkStr(const std::vector& pLink) @@ -26,18 +77,75 @@ void LevelLink::parseLinkStr(const std::vector& pLink) size_t offset = 0; // Find the whole level name. - m_newLevel = pLink[0]; + m_destinationLevel = pLink[0]; if (pLink.size() > 7) { offset = pLink.size() - 7; for (size_t i = 0; i < offset; ++i) - m_newLevel << " " << pLink[1 + i]; + { + m_destinationLevel += " "; + m_destinationLevel += pLink[1 + i]; + } + } + + m_boundingBox = + { + {static_cast(string::toNumber(pLink[1 + offset].toString())), static_cast(string::toNumber(pLink[2 + offset].toString()))}, + {static_cast(string::toNumber(pLink[3 + offset].toString())), static_cast(string::toNumber(pLink[4 + offset].toString()))} + }; + m_destinationX = pLink[5 + offset].toString(); + m_destinationY = pLink[6 + offset].toString(); + + // TODO: We need better handling of ancient level links that don't use math. + + if (m_destinationX == "-1") + { + m_constantX = true; + m_destinationX = "playerx"; + } + + if (m_destinationY == "-1") + { + m_constantY = true; + m_destinationY = "playery"; + } +} + +//---------------------------- + +LocalPixelPosition LevelLink::getDestinationForCharacter(Character& character, ScriptObject source) const +{ + // If the link is complex and we have an NPC server, process the link via the scripting system. + if ((m_complex[0] || m_complex[1]) && m_server && m_server->hasNPCServer()) + { + const auto& npcServer = m_server->getNPCServer(); + if (auto gs1 = npcServer->scripting.getScriptEngine("GS1"); gs1 != nullptr) + { + auto x = m_complex[0] ? static_cast(gs1->processMathExpression(m_destinationX, source) * 16) : getPixelDestination(m_destinationX, character); + auto y = m_complex[1] ? static_cast(gs1->processMathExpression(m_destinationY, source) * 16) : getPixelDestination(m_destinationY, character); + return LocalPixelPosition{x, y}; + } } - m_x = strtoint(pLink[1 + offset]); - m_y = strtoint(pLink[2 + offset]); - m_width = strtoint(pLink[3 + offset]); - m_height = strtoint(pLink[4 + offset]); - m_newX = pLink[5 + offset]; - m_newY = pLink[6 + offset]; + // If not complex, or in classic mode, we can just return the result without doing any math or scripting. + LocalPixelPosition result = + { + getPixelDestination(m_destinationX, character), + getPixelDestination(m_destinationY, character) + }; + return result; +} + +//---------------------------- + +bool LevelLink::isProbableMapLink() const +{ + if ((m_boundingBox.position.x() == 0 || m_boundingBox.position.x() == 63) && m_boundingBox.size.width() == 1 && m_destinationY == "playery") + return true; + if ((m_boundingBox.position.y() == 0 || m_boundingBox.position.y() == 63) && m_boundingBox.size.height() == 1 && m_destinationX == "playerx") + return true; + return false; } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/level/LevelSign.cpp b/server/src/level/LevelSign.cpp index 7e0c472cf..1fdb39fec 100644 --- a/server/src/level/LevelSign.cpp +++ b/server/src/level/LevelSign.cpp @@ -1,14 +1,22 @@ -#include +#include -#include "Player.h" -#include "level/LevelSign.h" +#include + +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// static CString encodeSignCode(CString& pText); static CString encodeSign(const CString& pSignText); static CString decodeSignCode(CString pText); const CString signText = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - "0123456789!?-.,#>()#####\"####':/~&### <####;\n"; +"0123456789!?-.,#>()#####\"####':/~&### <####;\n"; const CString signSymbols = "ABXYudlrhxyz#4."; const int ctablen[] = { 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 2, 1 }; const int ctabindex[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 15, 17 }; @@ -111,40 +119,48 @@ CString encodeSign(const CString& pSignText) return retVal; } -LevelSign::LevelSign(const int pX, const int pY, const CString& pSign, bool encoded) - : m_x(pX), m_y(pY), m_unformattedText(pSign) +LevelSign::LevelSign(const LocalWholeTilePosition& position, std::string_view signText, bool signTextIsEncoded) + : position(position) { - if (encoded) + if (signTextIsEncoded) { - m_text = m_unformattedText; - m_unformattedText = decodeSignCode(m_unformattedText); + encodedText = signText; + text = decodeSignCode(signText); } else - m_text = encodeSign(m_unformattedText); + { + encodedText = encodeSign(signText); + text = signText; + } } -CString LevelSign::getSignStr(Player* pPlayer) const +CString LevelSign::getSignPacket(Player* pPlayer) const { CString outText; // Write the x and y location to the packet. - outText.writeGChar(m_x); - outText.writeGChar(m_y); + outText.writeGChar(position.x()); + outText.writeGChar(position.y()); // Write the text to the packet. - outText.write(pPlayer ? encodeSign(pPlayer->translate(m_unformattedText)) : m_text); + outText.write(pPlayer ? encodeSign(pPlayer->translate(text)) : encodedText); return outText; } -void LevelSign::setText(const CString& value) +void LevelSign::setText(std::string_view signText, bool signTextIsEncoded) { - m_text = value; - m_unformattedText = decodeSignCode(value); + if (signTextIsEncoded) + { + encodedText = signText; + text = decodeSignCode(signText); + } + else + { + encodedText = encodeSign(signText); + text = signText; + } } -void LevelSign::setUText(const CString& value) -{ - m_text = encodeSign(value); - m_unformattedText = value; -} +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/level/LevelTerrain.cpp b/server/src/level/LevelTerrain.cpp new file mode 100644 index 000000000..c878888b5 --- /dev/null +++ b/server/src/level/LevelTerrain.cpp @@ -0,0 +1,158 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +void generateTerrain(LevelTerrain& levelTerrain, const MapTerrain& mapTerrain, const Position& mapPosition, const Dimension& gridDimension) +{ + constexpr size_t numberOfHeights = static_cast(65) * 65; + constexpr size_t topRowStart = 0; + constexpr size_t bottomRowStart = static_cast(65) * 64; + + size_t column = mapPosition[0]; + size_t row = mapPosition[1]; + size_t gridWidth = gridDimension[0]; + [[maybe_unused]] size_t gridHeight = gridDimension[1]; + + auto& heightmap = levelTerrain.heightmap; + if (heightmap.size() != numberOfHeights) + heightmap.resize(numberOfHeights, std::nan("")); + + // Copy in the border heights for the tile. + for (size_t tile = 0; tile < 65; ++tile) + { + auto indexTop = (column * 64) + tile + ((gridWidth * 64 + 1) * row); + auto indexBottom = ((row + 1) * (gridWidth * 64 + 1)) + (column * 64) + tile; + auto indexLeft = ((row * 64 + tile) * (gridWidth + 1)) + column; + auto indexRight = ((row * 64 + tile) * (gridWidth + 1)) + column + 1; + + heightmap[topRowStart + tile] = mapTerrain.gridBorderTileHeightsXAxis[indexTop]; + heightmap[bottomRowStart + tile] = mapTerrain.gridBorderTileHeightsXAxis[indexBottom]; + heightmap[tile * 65] = mapTerrain.gridBorderTileHeightsYAxis[indexLeft]; + heightmap[tile * 65 + 64] = mapTerrain.gridBorderTileHeightsYAxis[indexRight]; + } + + LevelTerrainWorker worker{ + .levelHeight = levelTerrain.levelHeight, + .levelChaos = levelTerrain.levelChaos, + .random = DelphiRandomDeviceReal(mapTerrain.levelSeeds[gridWidth * row + column]), + .heightmap = &heightmap + }; + + // Flood fill the heights. + floodFillQuadrant(worker, 65, worker.levelChaos, worker.levelHeight * worker.levelChaos, 64, 64, 0, 0); + + // Apply terrain overrides. + if (!levelTerrain.levelHeightOverrides.empty()) + applyHeightOverrides(levelTerrain); +} + +void floodFillHeights(LevelTerrainWorker& terrain, uint32_t rowWidth, double chaos, double height, size_t bottom, size_t right, size_t top, size_t left) +{ + auto& heightmap = *terrain.heightmap; + if ((right - left) > 1 || (bottom - top) > 1) + { + size_t midpointX = static_cast(std::abs((left + right) / 2.0)); + size_t midpointY = static_cast(std::abs((top + bottom) / 2.0)); + + auto randomValue = terrain.random(); + + size_t index = midpointY * rowWidth + midpointX; + heightmap[index] = ((heightmap[top * rowWidth + left] + heightmap[bottom * rowWidth + right]) / 2.0) + + ((randomValue - 0.5) * 2.0 * height); + + floodFillHeights(terrain, rowWidth, chaos, height * chaos, midpointY, midpointX, top, left); + floodFillHeights(terrain, rowWidth, chaos, height * chaos, bottom, right, midpointY, midpointX); + } +} + +void floodFillQuadrant(LevelTerrainWorker& terrain, uint32_t rowWidth, double chaos, double height, size_t bottom, size_t right, size_t top, size_t left) +{ + if ((right - left) > 1 || (bottom - top) > 1) + { + size_t midpointX = static_cast(std::abs((left + right) / 2.0)); + size_t midpointY = static_cast(std::abs((top + bottom) / 2.0)); + + floodFillHeights(terrain, rowWidth, chaos, height, midpointY, right, midpointY, left); // middle row + floodFillHeights(terrain, rowWidth, chaos, height * chaos, midpointY, midpointX, top, midpointX); // middle column top-half + floodFillHeights(terrain, rowWidth, chaos, height * chaos, bottom, midpointX, midpointY, midpointX); // middle column bottom-half + + floodFillQuadrant(terrain, rowWidth, chaos, chaos * height, midpointY, midpointX, top, left); // top-left + floodFillQuadrant(terrain, rowWidth, chaos, chaos * height, midpointY, right, top, midpointX); // top-right + floodFillQuadrant(terrain, rowWidth, chaos, chaos * height, bottom, midpointX, midpointY, left); // bottom-left + floodFillQuadrant(terrain, rowWidth, chaos, chaos * height, bottom, right, midpointY, midpointX); // bottom-right + } +} + +//---------------------------- + +void applyHeightOverrides(LevelTerrain& levelTerrain) +{ + for (int64_t y = 0; y < 9; ++y) + { + for (int64_t x = 0; x < 9; ++x) + { + size_t tileIndex = (y * 8) * 65 + (x * 8); + auto overrideHeight = levelTerrain.levelHeightOverrides[y * 9 + x]; + if (!DoublesAreSame(levelTerrain.heightmap[tileIndex], overrideHeight)) + { + auto tileX = x * 8; + auto tileY = y * 8; + + // Calculate the base delta step. + // Each row away from the origin will have a smaller base change applied to it. + auto deltaStep = (overrideHeight - levelTerrain.heightmap[tileIndex]) / 8; + + // Constrain the limits to the level. + auto top = tileY; + auto bottom = tileY; + if (tileY >= 8) top -= 7; + if (tileY <= ((int64_t)64 - 8)) bottom += 7; + + // A height override affects a 15x15 area centered on the override tile. + // Determine how much change should be applied to each row, then adjust all the tiles in that row. + for (size_t row = top; row <= (size_t)bottom; ++row) + { + auto rowDelta = 8 - std::abs((int64_t)row - tileY); + applyHeightOverrideOnRow(levelTerrain, deltaStep * rowDelta, tileX, row); + } + } + } + } +} + +void applyHeightOverrideOnRow(LevelTerrain& levelTerrain, double deltaHeight, size_t tileX, size_t tileY) +{ + auto deltaStep = deltaHeight / 8.0; + + // Tiles to the left of the origin. + if (tileX >= 8) + { + for (size_t index = 1; index < 8; ++index) + levelTerrain.heightmap[tileY * 65 + (tileX - 8) + index] += (deltaStep * index); + } + + // The origin. + levelTerrain.heightmap[tileY * 65 + tileX] += deltaHeight; + + // Tiles to the right of the origin. + if (tileY < 64) + { + for (size_t index = 1; index < 8; ++index) + levelTerrain.heightmap[tileY * 65 + tileX + index] += (deltaStep * (8 - index)); + } +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/level/Map.cpp b/server/src/level/Map.cpp index 32d6bc413..e8967e43e 100644 --- a/server/src/level/Map.cpp +++ b/server/src/level/Map.cpp @@ -1,285 +1,566 @@ -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include - -#include "FileSystem.h" -#include "Server.h" -#include "level/Map.h" - -Map::Map(MapType pType, bool pGroupMap) - : m_type(pType), m_groupMap(pGroupMap) -{ -} - -bool Map::load(const CString& pFileName) -{ - if (m_type == MapType::BIGMAP) - return loadBigMap(pFileName); - else if (m_type == MapType::GMAP) - return loadGMap(pFileName); - return true; -} - -bool Map::isLevelOnMap(const std::string& level, int& mapx, int& mapy) const -{ - auto it = m_levels.find(level); - if (it != m_levels.end()) - { - mapx = it->second.mapx; - mapy = it->second.mapy; - return true; - } - - return false; -} - -const std::string& Map::getLevelAt(int mx, int my) const +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal { - static const std::string emptyStr; - - if (mx < m_width && my < m_height) - return m_levelList[mx + my * m_width]; +/////////////////////////////////////////////////////////////////////////////// - return emptyStr; -} - -bool Map::loadBigMap(const CString& pFileName) +Map::Map(is_bigmap_t, const std::filesystem::path& fileName) + : mapType(MapType::BIGMAP), fileName(fileName) { // Get the appropriate filesystem. - FileSystem* fileSystem = m_server->getFileSystem(); - if (!m_server->getSettings().getBool("nofoldersconfig", false)) - fileSystem = m_server->getFileSystem(FS_FILE); - - CString fileName = fileSystem->find(pFileName); - m_modTime = fileSystem->getModTime(pFileName); - m_mapName = pFileName.text(); + m_server = BabyDI::Get(); + assert(m_server != nullptr); + auto& fileSystem = m_server->getFileSystem(); + auto fullFilePath = fileSystem.find(fs::FileCategory::FILE, fileName); // Make sure the file exists. - if (fileName.length() == 0) return false; + if (fullFilePath.empty()) + throw std::runtime_error("Map file not found!"); - // Load the gmap. - std::vector fileData = CString::loadToken(fileName); - - // Parse it. - m_levels.clear(); - m_width = 0; - m_height = 0; - - std::vector> mapData; + // Stupid. + auto& constructSize = const_cast&>(size); + auto& constructLevels = const_cast>&>(levels); + Position currentPosition; - for (auto& line: fileData) + // Load the levels. + auto fileData = CString::loadToken(fullFilePath.string()); + for (auto& line : fileData) { line = line.removeAll("\r").trim(); if (line.isEmpty()) continue; - auto levelList = line.guntokenize().tokenize("\n", true); + auto levelList = string::fromCSV(line.toStringView()); int empty = 0; - for (const auto& lvl: levelList) - { - // dont calculate the width based on any extra padding - empty = (lvl.isEmpty() ? ++empty : 0); - } - - // calculate width/height - auto currentWidth = levelList.size() - empty; - m_height++; - if (m_width < currentWidth) - m_width = currentWidth; - - mapData.push_back(levelList); - } - - { - std::vector levelMap(m_width * m_height); - - for (size_t my = 0; my < mapData.size(); my++) + for (const auto& lvl : levelList) { - for (size_t mx = 0; mx < mapData[my].size(); mx++) + if (!lvl.empty()) { - if (mx < m_width) - { - std::string lcLevelName(mapData[my][mx].toLower().text()); - if (!lcLevelName.empty()) - { - levelMap[mx + my * m_width] = lcLevelName; - m_levels[lcLevelName] = MapLevel(static_cast(mx), static_cast(my)); - } - } + constructLevels.insert({string::toLower(lvl), currentPosition}); + levelDataByName.insert({string::toLower(lvl), std::weak_ptr()}); } + else ++empty; + + ++currentPosition.x(); } + currentPosition.x() = 0; + ++currentPosition.y(); - m_levelList = std::move(levelMap); + // Calculate width/height. + auto currentWidth = levelList.size() - empty; + ++constructSize.height(); + if (constructSize.width() < currentWidth) + constructSize.width() = currentWidth; } - return true; + // Size the positional storage. + levelDataByPosition.resize(static_cast(constructSize.width() * constructSize.height())); } -bool Map::loadGMap(const CString& pFileName) +Map::Map(is_gmap_t, const std::filesystem::path& fileName) + : mapType(MapType::GMAP), fileName(fileName) { // Get the appropriate filesystem. - FileSystem* fileSystem = m_server->getFileSystem(); - if (!m_server->getSettings().getBool("nofoldersconfig", false)) - fileSystem = m_server->getFileSystem(FS_LEVEL); - - CString fileName = fileSystem->find(pFileName); - m_modTime = fileSystem->getModTime(pFileName); - m_mapName = pFileName.text(); + m_server = BabyDI::Get(); + assert(m_server != nullptr); + auto& fileSystem = m_server->getFileSystem(); + auto fileInfo = fileSystem.infoi(fs::FileCategory::LEVEL, fileName); // Make sure the file exists. - if (fileName.length() == 0) return false; + if (fileInfo == nullptr) + return; - m_levels.clear(); - m_width = 0; - m_height = 0; + // Try to open the file. + auto file = fileInfo->openFile(); + if (file == nullptr) + return; - // Load the gmap. - std::vector fileData = CString::loadToken(fileName); + // Save for later. + std::string mapName{fs::getANSIFileName(fileName.stem())}; + + // Stupid. + auto& constructSize = const_cast&>(size); + auto& constructLevels = const_cast>&>(levels); + auto& constructPreload = const_cast(levelsToKeepInMemory); + auto& constructTerrain = const_cast(terrain); + Position currentPosition; - // Parse it. - for (auto it = fileData.begin(); it != fileData.end(); ++it) + std::string generatedLastLevel; + std::vector terrainGridHeights; + + // Load the gmap. + while (!file->finishedReading()) { - // Tokenize - std::vector curLine = it->removeAll("\r").tokenize(); - if (curLine.empty()) + auto line = file->readLine(); + auto lineView = string::trim(line); + if (lineView.empty() || lineView == "GRMAP001") continue; - // Parse Each Type - if (curLine[0] == "WIDTH") - { - if (curLine.size() != 2) - continue; + auto [key, value] = string::extractConfigParts(lineView); + if (key.empty()) + continue; - m_width = strtoint(curLine[1]); - } - else if (curLine[0] == "HEIGHT") + if (key == "WIDTH") { - if (curLine.size() != 2) - continue; - - m_height = strtoint(curLine[1]); + constructSize.width() = string::toNumber(std::string{value}); } - else if (curLine[0] == "GENERATED") + else if (key == "HEIGHT") { - if (curLine.size() != 2) - continue; - - // Not really needed. + constructSize.height() = string::toNumber(std::string{value}); } - else if (curLine[0] == "LEVELNAMES") + else if (key == "LEVELNAMES") { - ++it; - int gmapy = 0; - - std::vector levelMap(m_width * m_height); + currentPosition.y() = 0; - while (it != fileData.end()) + line = file->readLine(); + lineView = string::trim(line); + while (!lineView.starts_with("LEVELNAMESEND")) { - CString line = it->removeAll("\r").trim(); - if (line.length() == 0) + if (currentPosition.y() < constructSize.height()) { - ++it; - continue; - } - if (line == "LEVELNAMESEND") break; - - if (gmapy < m_height) - { - int gmapx = 0; - - // Untokenize the level names and put them into a vector for easy loading. - line.guntokenizeI(); - std::vector names = line.tokenize("\n"); - for (auto& levelName: names) + auto lines = string::fromCSV(lineView); + for (auto& levelName : lines) { - if (gmapx < m_width) + if (currentPosition.x() < constructSize.width()) { - // Check for blank levels. - if (levelName != "\r") - { - std::string lcLevelName(levelName.toLower().text()); - levelMap[gmapx + gmapy * m_width] = lcLevelName; - m_levels[lcLevelName] = MapLevel(gmapx, gmapy); - } - - ++gmapx; + if (!levelName.empty()) + constructLevels.insert({string::toLower(levelName), currentPosition}); + + ++currentPosition.x(); } } - ++gmapy; + currentPosition.x() = 0; + ++currentPosition.y(); } - ++it; + line = file->readLine(); + lineView = string::trim(line); } + } + else if (key == "MAPIMG") + { + const_cast(mapImage) = value; + } + else if (key == "MINIMAPIMG") + { + const_cast(miniMapImage) = value; + } + else if (key == "NOAUTOMAPPING") + { + // Clientside only. + } + else if (key == "LOADFULLMAP") + { + const_cast(keepAllLevelsLoaded) = true; + } + else if (key == "LOADATSTART") + { + const_cast(keepAllLevelsLoaded) = false; + + line = file->readLine(); + lineView = string::trim(line); + while (!lineView.starts_with("LOADATSTARTEND")) + { + auto lines = string::fromCSV(lineView); + for (auto& levelName : lines) + constructPreload.emplace(string::toLower(levelName)); - m_levelList = std::move(levelMap); + line = file->readLine(); + lineView = string::trim(line); + } + } + else if (key == "GENERATED") + { + generatedLastLevel = string::trim(value); } - else if (curLine[0] == "MAPIMG") + else if (key == "GENSEED") { - if (curLine.size() != 2) - continue; + constructTerrain.mapSeed = string::toNumber(std::string{value}); + } + else if (key == "GENBASE") + { + constructTerrain.heightBase = string::toDouble(std::string{value}); + } + else if (key == "GENEVENBORDERS") + { + constructTerrain.evenBorders = string::equalsi(value, "true"sv); + } + else if (key == "GENHEIGHT") + { + constructTerrain.heightDeviation = string::toDouble(std::string{value}); + } + else if (key == "GENCHAOS") + { + constructTerrain.mapChaos = string::toDouble(std::string{value}); + } + else if (key == "LEVHEIGHT") + { + constructTerrain.levelHeightDeviation = string::toDouble(std::string{value}); + } + else if (key == "LEVCHAOS") + { + constructTerrain.levelChaos = string::toDouble(std::string{value}); + } + else if (key == "HEIGHTMAP") + { + line = file->readLine(); + lineView = string::trim(line); + while (!lineView.starts_with("HEIGHTMAPEND")) + { + auto lines = string::fromCSV(lineView); + for (auto& height : lines) + terrainGridHeights.push_back(string::toDouble(height)); - m_mapImage = curLine[1].text(); + line = file->readLine(); + lineView = string::trim(line); + } } - else if (curLine[0] == "MINIMAPIMG") + else if (key == "RANDOMSEEDS") { - if (curLine.size() != 2) - continue; + line = file->readLine(); + lineView = string::trim(line); + while (!lineView.starts_with("RANDOMSEEDSEND")) + { + auto lines = string::fromCSV(lineView); + for (auto& seed : lines) + constructTerrain.levelSeeds.push_back(string::toNumber(seed)); - m_miniMapImage = curLine[1].text(); + line = file->readLine(); + lineView = string::trim(line); + } } - else if (curLine[0] == "NOAUTOMAPPING") + } + + // Size the positional storage. + levelDataByPosition.resize(static_cast(constructSize.width() * constructSize.height())); + + // If we don't have any levels, but we do have a generated level end, automatically create the levels. + if (constructLevels.empty() && !generatedLastLevel.empty()) + { + auto columnDigits = std::floor(std::log(constructSize.width()) / std::log(26)) + 1; + auto rowDigits = std::floor(std::log10(constructSize.height())) + 1; + + auto toColumnName = [](const size_t digits, size_t col) -> std::string { - // Clientside only. + std::string result(digits, 'a'); + auto iter = result.rbegin(); + while (col > 0 && iter != result.rend()) + { + auto remainder = col % 26; + *iter = 'a' + remainder; + col /= 26; + ++iter; + } + return result; + }; + + // Using the pattern of the generated level name, create all the levels. + // prefix|column|row.nw + // Example: mymap_a-1.nw or mymap_a1.nw + + // First, determine the separator between the prefix and the columns. + std::string_view genLevel{generatedLastLevel}; + std::string_view levelPrefix; + std::string_view columnSeparator = "_"sv; + std::string_view rowSeparator = "-"sv; + + // Generated level starts with the map name. + if (genLevel.starts_with(fileName.stem().string())) + { + levelPrefix = genLevel.substr(0, fileName.stem().string().size()); + genLevel = genLevel.substr(levelPrefix.length()); + } + // Search for a - or _ separator. + else if (auto sepPos = genLevel.find_first_of("-_"sv); sepPos != std::string_view::npos) + { + levelPrefix = genLevel.substr(0, sepPos); + genLevel = genLevel.substr(levelPrefix.length()); } - else if (curLine[0] == "LOADFULLMAP") + + // If we can't figure out the generated level prefix, just use the map file name. + if (levelPrefix.empty()) + { + log::printLine(log::server, "** Could not determine generated level prefix for map '{}', using map name.", mapName); + levelPrefix = mapName; + } + else { - m_loadFullMap = true; + // Find the first alphabetic character. + auto alphaPos = genLevel.find_first_of("abcdefghijklmnopqrstuvwxyz"sv); + if (alphaPos != std::string_view::npos) + { + columnSeparator = genLevel.substr(0, alphaPos); + genLevel = genLevel.substr(alphaPos); + + if (auto last = genLevel.find_first_not_of("abcdefghijklmnopqrstuvwxyz"sv); last != std::string_view::npos) + genLevel = genLevel.substr(last); + + // Find the first numeric character. + auto numericPos = genLevel.find_first_of("0123456789"sv); + if (numericPos != std::string_view::npos) + rowSeparator = genLevel.substr(0, numericPos); + } } - else if (curLine[0] == "LOADATSTART") + + std::string row; + for (size_t y = 0; y < constructSize.height(); ++y) { - m_loadFullMap = false; + row = std::format("{:0{}}", y + 1, static_cast(rowDigits)); + for (size_t x = 0; x < constructSize.width(); ++x) + { + auto levelName = std::format("{}{}{}{}{}.nw", levelPrefix, columnSeparator, toColumnName(columnDigits, x), rowSeparator, row); + constructLevels.insert({string::toLower(levelName), Position{static_cast(x), static_cast(y)}}); + } + } + } - ++it; - while (it != fileData.end()) + // If we have terrain heights, generate the row/column border heights. + if (!terrainGridHeights.empty()) + { + size_t gridWidth = constructSize.width(); + size_t gridHeight = constructSize.height(); + constructTerrain.gridBorderTileHeightsXAxis.resize((gridWidth * 64 + 1) * (gridHeight + 1)); + constructTerrain.gridBorderTileHeightsYAxis.resize((gridHeight * 64 + 1) * (gridWidth + 1)); + + // Get the corner heights for the map grid. + for (size_t column = 0; column <= gridWidth; ++column) + { + for (size_t row = 0; row <= gridHeight; ++row) { - CString line = it->removeAll("\r"); - if (line == "LOADATSTARTEND") break; + size_t index = (gridWidth + 1) * row + column; + size_t indexAxisX = ((gridWidth * 64 + 1) * row) + (column * 64); + size_t indexAxisY = (row * 64 * (gridWidth + 1)) + column; + + if (index >= terrainGridHeights.size() + || indexAxisX >= constructTerrain.gridBorderTileHeightsXAxis.size() + || indexAxisY >= constructTerrain.gridBorderTileHeightsYAxis.size()) + throw std::runtime_error("Invalid terrain height data in gmap file!"); + + auto heightValue = terrainGridHeights[index]; + constructTerrain.gridBorderTileHeightsXAxis[indexAxisX] = heightValue; + constructTerrain.gridBorderTileHeightsYAxis[indexAxisY] = heightValue; + } + } + + // Set our seed. + LevelTerrainWorker worker{ + .levelHeight = constructTerrain.levelHeightDeviation, + .levelChaos = constructTerrain.levelChaos, + .random = DelphiRandomDeviceReal(constructTerrain.mapSeed), + .heightmap = &constructTerrain.gridBorderTileHeightsXAxis + }; - line.guntokenizeI(); - std::vector names = line.tokenize("\n"); - for (auto& levelName: names) + // Fill in the border heights for the map grid. + for (size_t column = 0; column <= gridWidth; ++column) + { + for (size_t row = 0; row <= gridHeight; ++row) + { + if (column < gridWidth) + { + worker.heightmap = &constructTerrain.gridBorderTileHeightsXAxis; + floodFillHeights(worker, gridWidth * 64 + 1, worker.levelChaos, worker.levelHeight, row, (column + 1) * 64, row, column * 64); + } + + if (row < gridHeight) { - m_preloadLevelList.push_back(levelName.toLower().text()); + worker.heightmap = &constructTerrain.gridBorderTileHeightsYAxis; + floodFillHeights(worker, gridWidth + 1, worker.levelChaos, worker.levelHeight, (row + 1) * 64, column, row * 64, column); } } } - // TODO: 3D settings maybe? } - return true; + // Register all of our levels as being part of a gmap so we can fix any links or warps. + if (auto stub = m_server->getStubbedLevel(fileName.string()); stub != nullptr) + { + auto& gmapLevels = m_server->getGmapLevelList(); + for (const auto& [levelName, levelPos] : levels) + gmapLevels.insert({levelName, stub}); + } } +//---------------------------- + void Map::loadMapLevels() const { - if (m_loadFullMap) + if (keepAllLevelsLoaded) { - for (const auto& levelName: m_levelList) + for (const auto& [levelName, position] : levels) { - if (!levelName.empty()) + if (auto level = m_server->getCachedLevelData(levelName); level != nullptr) { - auto lvl = m_server->getLevel(levelName); - assert(lvl); + auto index = position.x() + position.y() * size.width(); + levelDataByName[levelName] = level; + levelDataByPosition[index] = level; } } } - else if (!m_preloadLevelList.empty()) + else if (!levelsToKeepInMemory.empty()) + { + for (const auto& levelName : levelsToKeepInMemory) + { + if (auto level = m_server->getCachedLevelData(levelName); level != nullptr) + { + auto levelIter = levels.find(levelName); + if (levelIter == levels.end()) + continue; + + auto index = levelIter->second.x() + levelIter->second.y() * size.width(); + levelDataByName[levelName] = level; + levelDataByPosition[index] = level; + } + } + } +} + +void Map::setLevelDataLoaded(std::shared_ptr level) +{ + forceSetLevelDataLoaded(level); +} + +//---------------------------- + +bool Map::hasLevel(std::string_view levelName) const +{ + auto it = levels.find(levelName); + return it != levels.end(); +} + +std::optional Map::getLevelPosition(std::string_view levelName) const +{ + auto it = levels.find(levelName); + if (it != levels.end()) + return it->second; + return std::nullopt; +} + +std::string Map::getLevelNameAt(int x, int y) const +{ + for (const auto& [levelName, levelPos] : levels) + { + if (levelPos.x() == x && levelPos.y() == y) + return levelName; + } + return std::string{}; +} + +std::shared_ptr Map::getLevelDataAt(int x, int y) const +{ + for (const auto& [levelName, levelPos] : levels) + { + if (levelPos.x() == x && levelPos.y() == y) + return getLevelDataPtr(levelName, levelDataByName[levelName]); + } + return nullptr; +} + +std::shared_ptr Map::getLevelDataAt(const PixelPosition& globalPosition) const +{ + int x = static_cast(std::floor(globalPosition.x() / 1024)); + int y = static_cast(std::floor(globalPosition.y() / 1024)); + return getLevelDataAt(x, y); +} + +std::generator, MapPosition>> Map::getLevelDataInRange(const TilePosition& position, int syncTilesX, int syncTilesY) const noexcept +{ + Position searchPos{static_cast(position.x() / 64), static_cast(position.y() / 64)}; + Dimension levelSyncDistance{static_cast(std::ceilf(syncTilesX / 64)), static_cast(std::ceilf(syncTilesY / 64))}; + Rectangle area{searchPos.translate(-levelSyncDistance.width(), -levelSyncDistance.height()), levelSyncDistance * 2}; + + for (const auto& [levelName, levelPos] : levels) + { + if (levelPos.x() >= area.left() && levelPos.x() <= area.right() && + levelPos.y() >= area.top() && levelPos.y() <= area.bottom()) + { + if (auto level = getLevelDataPtr(levelName, levelDataByName[levelName]); level != nullptr) + co_yield std::make_pair(level, levelPos); + } + } +} + +std::generator, MapPosition>> Map::getLevelDataInRectangle(const PixelRectangleArea& area) const noexcept +{ + for (const auto& [levelName, levelPos] : levels) { - for (auto& level: m_preloadLevelList) + PixelPosition levelOrigin{levelPos.x() * 1024, levelPos.y() * 1024}; + if (positionInRectangle(levelOrigin, area)) { - auto lvl = m_server->getLevel(level); - assert(lvl); + auto index = (levelPos.y() * size.width()) + levelPos.x(); + co_yield std::make_pair(getLevelDataPtr(levelName, levelDataByPosition[index]), levelPos); } } } + +std::generator, MapPosition>> Map::getAllLevelData() const noexcept +{ + for (const auto& [levelName, levelPos] : levels) + { + auto index = (levelPos.y() * size.width()) + levelPos.x(); + co_yield std::make_pair(getLevelDataPtr(levelName, levelDataByPosition[index]), levelPos); + } +} + +//---------------------------- + +void Map::forceSetLevelDataLoaded(std::shared_ptr level) const noexcept +{ + if (auto it = levelDataByName.find(level->levelName); it != levelDataByName.end()) + it->second = level; + + if (auto position = getLevelPosition(level->levelName); position.has_value()) + { + if (size_t index = position.value().x() + position.value().y() * size.width(); index < levelDataByPosition.size()) + levelDataByPosition[index] = level; + } +} + +std::shared_ptr Map::getLevelDataPtr(std::string_view levelName, std::weak_ptr levelPtr) const noexcept +{ + if (levelName.empty()) + return nullptr; + if (auto level = levelPtr.lock(); level != nullptr) + return level; + + // The level could not be locked, so ask the server to load it. + if (auto level = m_server->getCachedLevelData(levelName); level != nullptr) + { + //level->setMap(server->findMap(getMapName())); + forceSetLevelDataLoaded(level); + return level; + } + + return nullptr; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/loader/LevelLoader.cpp b/server/src/loader/LevelLoader.cpp new file mode 100644 index 000000000..253f84c1c --- /dev/null +++ b/server/src/loader/LevelLoader.cpp @@ -0,0 +1,871 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +/* +Z3-V1.03 (Zelda Online?) + 12 bit tiles + links + baddies (no verses) + signs + +Z3-V1.04 (Alpha 1) + 12 bit tiles + links + baddies + signs + +GR-V1.00 (Alpha 1 online?) + 12 bit tiles + links + baddies + npcs + signs + +GR-V1.01 (Alpha 2) + 12 bit tiles + links + baddies + npcs + chests + signs + +GR-V1.02 (Alpha 5) + 13 bit tiles + links + baddies + npcs + chests + signs + +GR-V1.03 (Alpha 7) + 13 bit tiles + links (using variables) + baddies + npcs + chests + signs + +GR-V1.04 + +GR-V1.05 + tile layers + +GLEVNW01 + +GWEBL001 + +GSERVL01 + saves modified npcs in 'levelnpcs/levelfilename.save` + index "prop string" + + REPLACENPCS + 1 "prop string" + REPLACENPCSEND +*/ + +constexpr int MAX_TILE_COUNT = 64 * 64; // 4096 tiles per level. + +static constexpr int getBase64Position(char c) +{ + if (c >= 'a') + return 26 + (c - 'a'); + else if (c >= 'A') + return (c - 'A'); + else if (c >= '0' && c <= '9') + return 52 + (c - '0'); + + switch (c) + { + case '+': + return 52 + 10; + case '/': + return 52 + 11; + } + + return 0; +} + +/////////////////////////////////////////////////////////////////////////////// + +LevelPtr LevelLoader::loadLevel(const std::filesystem::path& levelName) +{ + auto level = std::make_shared(); + if (loadLevelInto(levelName, level)) + return level; + + return nullptr; +} + +bool LevelLoader::loadLevelInto(const std::filesystem::path& levelName, LevelPtr level) +{ + auto server = BabyDI::Get(); + level->levelName = fs::getANSIFileName(levelName); + + // Normal level loading (just one sub-level). + bool isGmap = string::ends_withi(level->levelName, ".gmap"sv); + if (!isGmap) + { + auto levelData = server->getCachedLevelData(level->levelName); + if (levelData == nullptr) + return false; + + // Check if this level belongs to a bigmap. + if (auto map = server->findMapForLevel(MapType::BIGMAP, level->levelName); map != nullptr) + level->setMap(map); + + level->m_filePath = levelData->filePath; + level->modTime = levelData->modTime; + level->m_levelParts.push_back(attachStaticDataToLevel(level, std::nullopt, levelData)); + loadStaticDataNPCs(level, std::nullopt, levelData); + + // Bind listeners for level data changes. + auto handle = levelData->onDataRefreshed.subscribe([weakSelf = std::weak_ptr(level)](StaticLevelDataPtr staticData) + { + if (auto self = weakSelf.lock(); self != nullptr) + self->reload(staticData); + }); + level->m_levelParts.front()->staticDataRefreshedHandle = handle; + + level->loaded = true; + return true; + } + + // Check if the server generation supports gmaps. + if (server->Generation == ServerGeneration::ORIGINAL) + { + log::printLine(log::server, "[ERROR] Server generation does not support gmaps, refusing to load {}.", level->levelName); + return false; + } + + // Find the map for the gmap level. + auto map = server->findMap(level->levelName); + if (map == nullptr) + return false; + + // Set the map and make space for the level parts. + level->setMap(map); + level->m_levelParts.resize(static_cast(map->size.width()) * map->size.height()); + + // Load all the sub-levels for the gmap. + for (const auto& [levelData, levelPos] : map->getAllLevelData()) + { + if (levelData == nullptr) + continue; + + auto index = static_cast(levelPos.y()) * map->size.width() + levelPos.x(); + level->m_levelParts[index] = attachStaticDataToLevel(level, levelPos, levelData); + loadStaticDataNPCs(level, levelPos, levelData); + + // Bind listeners for level data changes. + auto handle = levelData->onDataRefreshed.subscribe([weakSelf = std::weak_ptr(level)](StaticLevelDataPtr staticData) + { + if (auto self = weakSelf.lock(); self != nullptr) + self->reload(staticData); + }); + level->m_levelParts[index]->staticDataRefreshedHandle = handle; + } + + level->loaded = true; + return true; +} + +//---------------------------- + +StaticLevelDataPtr LevelLoader::loadStaticData(const std::filesystem::path& levelName) +{ + auto data = std::make_shared(); + + // Find the level file. + auto levelString = fs::getANSIFileName(levelName); + data->levelName = levelString; + + // Load the data. + if (loadStaticDataInto(data)) + return data; + + return nullptr; +} + +bool LevelLoader::loadStaticDataInto(StaticLevelDataPtr staticLevelData) +{ + auto* server = BabyDI::Get(); + auto& fileSystem = server->getFileSystem(); + + auto fileInfo = fileSystem.infoi(fs::FileCategory::LEVEL, staticLevelData->levelName); + if (fileInfo == nullptr) + return false; + + // Open it for loading. + auto fileData = fileInfo->openFile(); + if (fileData == nullptr || !fileData->opened()) + return false; + + // Get the file version. + auto version = fileData->readChars(8); + + // Save some level details. + staticLevelData->filePath = fileInfo->file; + staticLevelData->modTime = fileInfo->getModTime(); + + // Load the level data. + if (version == "GLEVNW01") + loadNW(staticLevelData, version, fileSystem, fileData); + if (version.substr(0, 3) == "GR-") + loadGraal(staticLevelData, version, fileSystem, fileData); + if (version.substr(0, 3) == "Z3-") + loadZelda(staticLevelData, version, fileSystem, fileData); + + return true; +} + +SubLevelPtr LevelLoader::attachStaticDataToLevel(LevelPtr level, std::optional mapPosition, StaticLevelDataPtr staticData) +{ + auto subLevel = std::make_shared(); + subLevel->parentLevel = level; + subLevel->staticData = staticData; + subLevel->mapPosition = mapPosition; + + // Record if this sub-level is related to a level that is a gmap or on a bigmap. + if (level->isGmap()) + subLevel->isOnGmap = true; + if (level->isOnBigMap()) + subLevel->isOnBigMap = true; + + // Reserve space for baddies to avoid reallocations, which will destroy timeout callback pointers. + subLevel->baddies.reserve(0xFF); + + // Load baddies. + if (!level->isGmap()) + { + // Mark all existing baddies as dead and non-respawning. + for (auto& baddy : subLevel->baddies) + { + baddy.setRespawn(false); + baddy.mode = BaddyMode::DEAD; + } + + // Copy over the new baddies. + for (size_t i = 0; i < staticData->baddies.size() && i < 50; ++i) + { + if (i < subLevel->baddies.size()) + subLevel->baddies[i] = staticData->baddies[i]; + else + subLevel->baddies.push_back(staticData->baddies[i]); + + subLevel->baddies.back().setLevel(level); + } + } + + // Load heights. + if (level->isGmap()) + { + // Check for map terrain. + if (auto map = level->getMap(); map != nullptr && !map->terrain.levelSeeds.empty()) + { + auto& terrain = subLevel->terrain.emplace(); + auto seedIndex = mapPosition.value().y() * map->size.width() + mapPosition.value().x(); + terrain.levelSeed = map->terrain.levelSeeds[seedIndex]; + terrain.levelHeight = map->terrain.levelHeightDeviation; + terrain.levelChaos = map->terrain.levelChaos; + terrain.levelHeightOverrides = staticData->heights; + + generateTerrain(terrain, map->terrain, mapPosition.value(), map->size); + } + } + + return subLevel; +} + +void LevelLoader::loadStaticDataNPCs(LevelPtr level, std::optional mapPosition, StaticLevelDataPtr staticData) +{ + // The sub-level must exist before this method gets called. + + auto server = BabyDI::Get(); + + // Delete existing level NPCs. + auto& npcs = level->getNPCs(); + for (auto iter = npcs.begin(); iter != npcs.end();) + { + if (auto npc = server->getNPC(*iter); npc == nullptr || npc->storageType == NPCStorageType::LEVEL) + { + if (npc && (!mapPosition.has_value() || npc->character.getMapPosition() == mapPosition)) + { + server->deleteNPC(npc, false); + iter = npcs.erase(iter); + continue; + } + } + ++iter; + } + + // Add new NPCs. + for (const auto& npcData : staticData->npcs) + { + auto& gen = server->getNPCIdGenerator(); + auto npcId = gen.getAvailableId(NPCID_GEN_LOCAL); + auto npc = std::make_shared(npcId, NPCStorageType::LEVEL); + + // Cached data. + npc->character.localPixelX = npcData.position.x(); + npc->character.localPixelY = npcData.position.y(); + npc->image = npcData.image; + npc->setLevel(level); + + // Map position. + if (mapPosition.has_value()) + { + npc->character.mapX = mapPosition.value().x(); + npc->character.mapY = mapPosition.value().y(); + npc->modTime[PROPID(NPCProp::GMAPLEVELX)] = server->getFrameStartTime(); + npc->modTime[PROPID(NPCProp::GMAPLEVELY)] = server->getFrameStartTime(); + } + + // Script. + npc->setScript(npcData.script); + + // Add. + npc->recordInitialState(); + server->addNPC(npc, level->loaded); + } +} + +/////////////////////////////////////////////////////////////////////////////// + +bool LevelLoader::loadZelda(StaticLevelDataPtr levelData, std::string_view fileVersion, fs::FileSystem& fileSystem, fs::FilePtr& fileData) +{ + int version = -1; + if (fileVersion == "Z3-V1.03") + version = 3; + else if (fileVersion == "Z3-V1.04") + version = 4; + else return false; + + // Load tiles. + loadBinaryTiles(levelData, fileData, 12, 1); + + // Load links. + loadBinaryLinks(levelData, fileData, fileSystem); + + // Load the baddies. + loadBinaryBaddies(levelData, fileData, version > 3); + + // Load signs. + loadBinarySigns(levelData, fileData); + + return true; +} + +bool LevelLoader::loadGraal(StaticLevelDataPtr levelData, std::string_view fileVersion, fs::FileSystem& fileSystem, fs::FilePtr& fileData) +{ + // Grab file version. + int version = -1; + if (fileVersion == "GR-V1.00") + version = 0; + else if (fileVersion == "GR-V1.01") + version = 1; + else if (fileVersion == "GR-V1.02") + version = 2; + else if (fileVersion == "GR-V1.03") + version = 3; + else if (fileVersion == "GR-V1.05") + version = 5; + else return false; + + // Determine layer count. + uint8_t layers = 1; + if (version >= 5) + { + // Read the layer count. + layers = (uint8_t)fileData->readPackedIntegral<1>(); + } + + // Load tiles. + loadBinaryTiles(levelData, fileData, version > 1 ? 13 : 12, layers); + + // Load links. + loadBinaryLinks(levelData, fileData, fileSystem); + + // Load baddies. + loadBinaryBaddies(levelData, fileData, true); + + // Load NPCs. + loadBinaryNPCs(levelData, fileData); + + // Load chests. + if (version > 0) + { + loadBinaryChests(levelData, fileData); + } + + // Load signs. + loadBinarySigns(levelData, fileData); + + return true; +} + +void LevelLoader::loadBinaryTiles(StaticLevelDataPtr levelData, fs::FilePtr& fileData, uint32_t bits, int layers) +{ + for (uint8_t layer = 0; layer < layers; ++layer) + { + if (fileData->finishedReading()) + break; + + auto tiles = levelData->tiles.getOrCreateLayer(layer); + + uint32_t buffer = 0; + uint32_t read = 0; + uint16_t code = 0; + int tileReadAmount = 1; + int boardWriteIndex = 0; + bool isExtraLayer = layer != 0; + + bool doubleTileRLEMode = false; + int32_t rleTiles[2] = { -1, -1 }; + + // Read the tiles. + while (boardWriteIndex < MAX_TILE_COUNT && !fileData->finishedReading()) + { + // Every control code/tile is either 12 or 13 bits. WTF. + // Read in the bits. + while (read < bits) + { + buffer += fileData->readIntegral<1>() << read; + read += 8; + } + + // Pull out a single 12/13 bit code from the buffer. + code = buffer & (bits == 12 ? 0xFFF : 0x1FFF); + buffer >>= bits; + read -= bits; + + // See if we have an RLE control code. + // Control codes determine how the RLE scheme works. + if (code & ((bits == 12) ? 0x800 : 0x1000)) + { + // If the 0x100 bit is set, we are in a double repeat mode. + // {double 4}56 = 56565656 + if (code & 0x100) doubleTileRLEMode = true; + + // How many tiles do we count? + tileReadAmount = code & 0xFF; + continue; + } + + // If our count is 1, just read in a tile. This is the default mode. + if (tileReadAmount == 1) + { + // Extra layer tiles are 0xFFFF. + if (isExtraLayer && code == 0xFFF) + code = std::numeric_limits::max(); + + tiles->at(boardWriteIndex++) = code; + continue; + } + + // If we reach here, we have an RLE scheme. + // See if we are in double repeat mode or not. + if (doubleTileRLEMode) + { + // Read in our first tile. + if (rleTiles[0] == -1) + { + rleTiles[0] = code; + continue; + } + + // Read in our second tile. + rleTiles[1] = code; + + // Determine the actual tiles we are going to write. + // Tiles on additional layers are 0xFFFF if not set, so handle that. + uint16_t first = std::numeric_limits::max(); + uint16_t second = rleTiles[1]; + if (!isExtraLayer || rleTiles[0] != 0xFFF) + first = rleTiles[0]; + if (isExtraLayer && rleTiles[1] == 0xFFF) + second = std::numeric_limits::max(); + + // Add the tiles now. + for (int i = 0; i < tileReadAmount && boardWriteIndex < MAX_TILE_COUNT - 1; ++i) + { + tiles->at(boardWriteIndex++) = first; + tiles->at(boardWriteIndex++) = second; + } + + // Clean up. + rleTiles[0] = rleTiles[1] = -1; + doubleTileRLEMode = false; + tileReadAmount = 1; + } + // Regular RLE scheme. + else + { + for (int i = 0; i < tileReadAmount && boardWriteIndex < MAX_TILE_COUNT; ++i) + { + // Extra layer tiles are 0xFFFF. + if (isExtraLayer && code == 0xFFF) + code = std::numeric_limits::max(); + + tiles->at(boardWriteIndex++) = code; + } + tileReadAmount = 1; + } + } + } +} + +void LevelLoader::loadBinaryLinks(StaticLevelDataPtr levelData, fs::FilePtr& fileData, fs::FileSystem& fileSystem) +{ + while (!fileData->finishedReading()) + { + auto line = fileData->readLine(); + if (line.length() == 0 || line == "#") break; + + // Assemble the level string. + auto splitData = string::splitToVectorView(line, " "sv); + if (splitData.size() < 7) + continue; + + auto end = splitData.size(); + + // Get the positions and destinations of the link. + Rectangle rect; + rect.position[0] = string::toNumber(splitData[end - 6]); + rect.position[1] = string::toNumber(splitData[end - 5]); + rect.size[0] = string::toNumber(splitData[end - 4]); + rect.size[1] = string::toNumber(splitData[end - 3]); + auto destX = splitData[end - 2]; + auto destY = splitData[end - 1]; + + // Get the level name. + // Levels with spaces in the name are not supposed to be allowed, but we support it anyway. + // TODO: This will not work with levels with two+ spaces in a row. + auto destLevel = string::join(splitData | std::views::take(end - 6), " "sv); + + if (!fileSystem.has(fs::FileCategory::LEVEL, destLevel)) + continue; + + levelData->links.emplace_back(rect, destX, destY, destLevel); + } +} + +void LevelLoader::loadBinaryBaddies(StaticLevelDataPtr levelData, fs::FilePtr& fileData, bool loadVerses) +{ + while (!fileData->finishedReading()) + { + int8_t x = fileData->readIntegral<1>(); + int8_t y = fileData->readIntegral<1>(); + int8_t type = fileData->readIntegral<1>(); + + // Ends with an invalid baddy. + if (x == -1 && y == -1 && type == -1) + { + fileData->readLine(); // Empty verses. + break; + } + + // Add the baddy. + LevelBaddy baddy{ toLocalPixelPosition((float)x, (float)y), (BaddyType)type, {} }; + baddy.id = static_cast(levelData->baddies.size() + 1); + + // Load the verses. + if (loadVerses) + { + auto verseLine = fileData->readLine(); + auto verseParts = string::splitToVectorView(verseLine, "\\"sv); + for (size_t j = 0; j < std::min((size_t)3, verseParts.size()); ++j) + baddy.verses[j] = verseParts[j]; + } + + levelData->baddies.emplace_back(std::move(baddy)); + } +} + +void LevelLoader::loadBinaryNPCs(StaticLevelDataPtr levelData, fs::FilePtr& fileData) +{ + int index = 0; + while (!fileData->finishedReading()) + { + ++index; + + auto line = fileData->readLine(); + if (line.length() == 0 || line == "#") break; + + TilePosition position; + position[0] = line[0] - 32; + position[1] = line[1] - 32; + + std::string_view lineView{ line }; + lineView.remove_prefix(2); + + auto image = string::extractLine(lineView, '#'); + auto code = string::replace(lineView, "\xa7", "\n"); + + LevelNPCTemplate npc{ .image = image, .position = toLocalPixelPosition(position) }; + npc.script.setOriginalSource(std::format("{}.{}", levelData->levelName, index), code); + levelData->npcs.emplace_back(std::move(npc)); + } +} + +void LevelLoader::loadBinaryChests(StaticLevelDataPtr levelData, fs::FilePtr& fileData) +{ + while (!fileData->finishedReading()) + { + auto line = fileData->readLine(); + if (line.length() == 0 || line == "#") break; + + uint8_t x = line[0] - 32; + uint8_t y = line[1] - 32; + char item = line[2] - 32; + char signindex = line[3] - 32; + + LevelChest chest{ .position = LocalWholeTilePosition{ x, y }, .item = LevelItemType(item), .sign = (uint8_t)signindex }; + levelData->chests.emplace_back(std::move(chest)); + } +} + +void LevelLoader::loadBinarySigns(StaticLevelDataPtr levelData, fs::FilePtr& fileData) +{ + while (!fileData->finishedReading()) + { + auto line = fileData->readLine(); + if (line.length() == 0) break; + + uint8_t x = line[0] - 32; + uint8_t y = line[1] - 32; + std::string_view text{ line }; + text.remove_prefix(2); + + levelData->signs.emplace_back(LocalWholeTilePosition{ x, y }, text, true); + } +} + +//---------------------------- + +bool LevelLoader::loadNW(StaticLevelDataPtr levelData, std::string_view fileVersion, fs::FileSystem& fileSystem, fs::FilePtr& fileData) +{ + std::string curLine; + std::vector splitData; + int npcIndex = 0; + + while (!fileData->finishedReading()) + { + // Read the line. + curLine = fileData->readLine(); + std::string_view line{ string::trim(curLine) }; + if (line.empty()) + continue; + + // Get the line data. + auto [section, data] = string::extractConfigParts(line); + splitData = string::splitToVectorView(data); + + // Parse each line. + if (section == "BOARD") + { + if (splitData.size() != 5) + continue; + + uint8_t x = string::toNumber(splitData[0]); + uint8_t y = string::toNumber(splitData[1]); + uint8_t width = string::toNumber(splitData[2]); + uint8_t layer = string::toNumber(splitData[3]); + if (!inRangeInclusive(x, 0, 64) || !inRangeInclusive(y, 0, 64) || width <= 0 || x + width > 64) + continue; + if (splitData[4].length() < static_cast(width) * 2) + continue; + + auto tiles = levelData->tiles.getOrCreateLayer(layer); + for (size_t index = 0; index < width; ++index) + { + char left = splitData[4].at(index * 2); + char top = splitData[4].at(index * 2 + 1); + short tile = getBase64Position(left) << 6; + tile += getBase64Position(top); + if (tile == 0x3FFF) + tile = constants::EmptyTileInLayer; + + tiles->at(static_cast(x + index) + static_cast(y * 64)) = tile; + } + } + else if (section == "CHEST") + { + if (splitData.size() < 4) + continue; + + LevelItemType itemType = LevelItem::getItemId(std::string{ splitData[2] }); + if (itemType != LevelItemType::INVALID) + { + uint8_t chestx = string::toNumber(splitData[0]); + uint8_t chesty = string::toNumber(splitData[1]); + char signidx = string::toNumber(splitData[3]); + + LevelChest chest{ .position = LocalWholeTilePosition{ chestx, chesty }, .item = itemType, .sign = (uint8_t)signidx }; + levelData->chests.emplace_back(std::move(chest)); + } + } + else if (section == "LINK") + { + if (splitData.size() < 7) + continue; + + auto end = splitData.size(); + + // Get the positions and destinations of the link. + Rectangle rect; + rect.position[0] = string::toNumber(splitData[end - 6]); + rect.position[1] = string::toNumber(splitData[end - 5]); + rect.size[0] = string::toNumber(splitData[end - 4]); + rect.size[1] = string::toNumber(splitData[end - 3]); + auto destX = splitData[end - 2]; + auto destY = splitData[end - 1]; + + // Get the level name. + // Levels with spaces in the name are not supposed to be allowed, but we support it anyway. + // TODO: This will not work with levels with two+ spaces in a row. + auto destLevel = string::join(splitData | std::views::take(end - 6), " "sv); + + if (!fileSystem.has(fs::FileCategory::LEVEL, destLevel)) + continue; + + levelData->links.emplace_back(rect, destX, destY, destLevel); + } + else if (section == "SIGN") + { + if (splitData.size() != 2) + continue; + + uint8_t x = string::toNumber(splitData[0]); + uint8_t y = string::toNumber(splitData[1]); + + // Grab the sign code. + std::string text; + for (const auto& line : fileData->readLinesUntilSectionEnd("SIGNEND")) + { + text += line; + text += '\n'; + } + + // Erase the final newline. + if (text.back() == '\n') + text.pop_back(); + + // Add the new sign. + levelData->signs.emplace_back(LocalWholeTilePosition{ x, y }, text, false); + } + else if (section == "BADDY") + { + if (splitData.size() != 3) + continue; + + TilePosition position; + position[0] = string::toFloat(splitData[0]); + position[1] = string::toFloat(splitData[1]); + BaddyType type = LevelBaddy::getBaddyTypeFromString(std::string{ splitData[2] }); + + LevelBaddy baddy{ toLocalPixelPosition(position), type, {} }; + baddy.id = static_cast(levelData->baddies.size() + 1); + + int i = 0; + for (const auto& verse : fileData->readLinesUntilSectionEnd("BADDYEND")) + { + if (i < 3) baddy.verses[i] = verse; + ++i; + } + + levelData->baddies.emplace_back(std::move(baddy)); + } + else if (section == "NPC") + { + ++npcIndex; + + if (splitData.size() < 3) + continue; + + auto end = splitData.size(); + + TilePosition position; + position[0] = string::toFloat(splitData[end - 2]); + position[1] = string::toFloat(splitData[end - 1]); + + // Remove the back 2 entries from the split data. + splitData.erase(splitData.begin() + (end - 2), splitData.end()); + + // Combine all the rest. + std::string image = string::join(splitData, " "sv); + + // If the image is just a hyphen, clear it. + if (string::trim(image) == "-") + image.clear(); + + std::string code; + for (const auto& line : fileData->readLinesUntilSectionEnd("NPCEND")) + { + code += line; + code += '\n'; + } + + LevelNPCTemplate npc{ .image = image, .position = toLocalPixelPosition(position) }; + npc.script.setOriginalSource(std::format("{}.{}", levelData->levelName, npcIndex), code); + levelData->npcs.emplace_back(std::move(npc)); + } + else if (section == "HEIGHTS") + { + for (const auto& heights : fileData->readLinesUntilSectionEnd("HEIGHTSEND")) + { + auto values = string::splitToVectorView(heights, ","sv); + for (const auto& val : values) + levelData->heights.push_back(string::toDouble(string::trim(val))); + } + + if (levelData->heights.size() != 81) + { + log::printLine(log::server, "[WARNING] Level '{}' has an improper amount of heights. Expected: {}, found: {}.", levelData->levelName, 81, levelData->heights.size()); + levelData->heights.clear(); + } + } + else + { + log::printLine(log::server, "[WARNING] Level '{}' has unhandled section '{}'.", levelData->levelName, section); + } + } + + return true; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/loader/flatfile/FlatFileAccountLoader.cpp b/server/src/loader/flatfile/FlatFileAccountLoader.cpp new file mode 100644 index 000000000..fa763d55c --- /dev/null +++ b/server/src/loader/flatfile/FlatFileAccountLoader.cpp @@ -0,0 +1,533 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::string_view_literals; + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +// Helper to avoid having to write uint8_t everywhere. +const auto& toByte = static_cast(string::toNumber); + +static bool setIfEmpty(std::string& str, std::string_view value, std::string_view defaultValue = {}) +{ + if (!str.empty()) + return false; + str = value.empty() ? defaultValue : value; + return true; +} + +static void writeLine(std::string& output, const std::string& section, const auto& value) +{ + output += section + " " + std::format("{}", value) + "\r\n"; +} + +static void writeLine(std::string& output, const std::string& section, const auto& value, const auto& defaultValue) +{ + if (value != defaultValue) + writeLine(output, section, value); +} + +/////////////////////////////////////////////////////////////////////////////// + +flagPair FlatFileAccountLoader::decomposeFlag(const std::string& flag) const +{ + auto server = BabyDI::Get(); + flagPair result; + auto sep = flag.find('='); + result = (sep == std::string::npos) ? std::make_pair(flag, "") : std::make_pair(flag.substr(0, sep), flag.substr(sep + 1)); + if (server->cached.enableFlagCropping.getValue()) + { + // If cropflags is enabled, crop the flag to 223 characters. + // Subtract the length of the flag name and the = character from 223 to determine the space left for the flag value. + int fixedLength = result.first.length() < 223 ? static_cast(223 - 1) - result.first.length() : 0; + result.second = result.second.substr(0, fixedLength); + } + return result; +} + +chestPair FlatFileAccountLoader::decomposeChest(const std::string& chest) const +{ + chestPair result; + auto tokens = string::splitToVector(chest, ":"sv); + if (tokens.size() == 3) + { + result.second.x() = string::toNumber(tokens[0]); + result.second.y() = string::toNumber(tokens[1]); + result.first = string::trim(tokens[2]); + } + return result; +} + +bool FlatFileAccountLoader::loadAccount(std::string_view accountName, Account& account) +{ + auto server = BabyDI::Get(); + + // Find the account to load. + bool loadedFromDefault = false; + auto& accountFS = server->getFileSystemServer(); + auto path = accountFS.findi(fs::FileCategory::ACCOUNT, std::format("{}.txt", accountName)); + if (path.empty()) + { + path = "accounts/defaultaccount.txt"; + loadedFromDefault = true; + } + + // Load the account data. + auto fileData = CString::loadToken(path.string(), "\n"); + if (fileData.empty() || fileData[0].trim() != "GRACC001") + return false; + + // Set the account name. + account.name = accountName; + + // Parse File + for (auto& i : fileData) + { + // Trim Line + i.trimI(); + + // Get the section and value. + auto sep = i.find(' '); + std::string section = i.subString(0, sep).toString(); + std::string val; + if (sep != -1) + val = i.subString(sep + 1).toString(); + + if (section == "NAME") + continue; + else if (section == "NICK") + account.character.nickName = val.substr(0, 223); + else if (section == "COMMUNITYNAME") + account.communityName = val; + else if (section == "LEVEL") + account.level = val; + else if (section == "GROUPNAME") + account.groupName = val; + else if (section == "X") + account.character.localPixelX = static_cast(string::toFloat(val) * 16); + else if (section == "Y") + account.character.localPixelY = static_cast(string::toFloat(val) * 16); + else if (section == "Z") + account.character.localPixelZ = static_cast(string::toFloat(val) * 16); + else if (section == "MAPX") + account.character.mapX = toByte(val); + else if (section == "MAPY") + account.character.mapY = toByte(val); + else if (section == "MAXHP") + account.maxHitpoints = toByte(val); + else if (section == "HP") + account.character.hitpointsInHalves = static_cast(string::toFloat(val) * 2); + else if (section == "GRALATS" || section == "RUPEES") + account.character.gralats = string::toNumber(val); + else if (section == "ANI") + account.character.gani = val; + else if (section == "ARROWS") + account.character.arrows = toByte(val); + else if (section == "BOMBS") + account.character.bombs = toByte(val); + else if (section == "GLOVEP") + account.character.glovePower = toByte(val); + else if (section == "SHIELDP") + account.character.shieldPower = toByte(val); + else if (section == "SWORDP") + account.character.swordPower = toByte(val); + else if (section == "BOMBP") + account.character.bombPower = toByte(val); + else if (section == "BOWP") + account.character.bowPower = toByte(val); + else if (section == "BOW") + account.character.bowImage = val; + else if (section == "HEAD") + account.character.headImage = val; + else if (section == "BODY") + account.character.bodyImage = val; + else if (section == "SWORD") + account.character.swordImage = val; + else if (section == "SHIELD") + account.character.shieldImage = val; + else if (section == "COLORS") + { + auto tokensAsNumbers = string::split(val, ","sv) | std::views::take(8) | std::views::transform([](const std::string_view& token) { return toByte(std::string{ token }); }); + std::ranges::copy(tokensAsNumbers, account.character.colors.begin()); + } + else if (section == "SPRITE") + { + auto sprite = toByte(val); + account.character.sprite = sprite >> 2; + account.character.direction = sprite & 0b11; + } + else if (section == "STATUS") + account.status = toByte(val); + else if (section == "MP") + account.character.mp = toByte(val); + else if (section == "AP") + account.character.ap = toByte(val); + else if (section == "APCOUNTER") + account.apCounter = toByte(val); + else if (section == "ONSECS") + account.onlineSeconds = string::toNumber(val); + else if (section == "IP") + setIfEmpty(account.ipAddress, val); + else if (section == "LANGUAGE") + setIfEmpty(account.language, val, "English"sv); + else if (section == "KILLS") + account.kills = string::toNumber(val); + else if (section == "DEATHS") + account.deaths = string::toNumber(val); + else if (section == "RATING") + account.eloRating = string::toFloat(val); + else if (section == "DEVIATION") + account.eloDeviation = string::toFloat(val); + else if (section == "LASTSPARTIME") + account.lastSparTime = clock::from_time_t(string::toNumber(val)); + else if (section == "FLAG") + { + auto variable = GameValue::deserialize(i.toString()); + if (variable.has_value()) + account.variables.add(std::move(variable.value())); + } + else if (section.starts_with("ATTR")) + { + if (auto idx = toByte(section.substr(4)); idx > 0 && idx <= 30) + account.character.ganiAttributes[idx - 1] = val; + } + else if (section == "WEAPON") + account.weapons.push_back(val); + else if (section == "CHEST") + account.savedChests.insert(decomposeChest(val)); + else if (section == "BANNED") + account.banned = toByte(val) != 0; + else if (section == "BANREASON") + account.banReason = val; + else if (section == "BANLENGTH") + account.banLength = val; + else if (section == "COMMENTS") + account.comments = val; + else if (section == "EMAIL") + account.email = val; + else if (section == "LOCALRIGHTS") + account.adminRights = string::toNumber(val); + else if (section == "IPRANGE") + account.adminIpRange = string::splitToVector(val, ","sv); + else if (section == "LOADONLY") + account.loadOnly = toByte(val) != 0; + else if (section == "FOLDERRIGHT") + { + account.folderList.push_back(val); + account.folderRights.addPermission(val); + } + else if (section == "LASTFOLDER") + account.lastFolderAccessed = val; + } + + // If this is a guest account, loadonly is set to true. + if (string::equalsi(accountName, "guest"sv)) + { + account.loadOnly = true; + srand((unsigned int)time(0)); + + // Try to create a unique account number. + while (true) + { + int v = (rand() * rand()) % 9999999; + if (server->getPlayer("pc:" + CString(v).subString(0, 6), PLTYPE_ANYPLAYER) == 0) + { + account.name = std::format("pc:{:6}", v); + break; + } + } + + account.communityName = "guest"; + } + + // Default community name to account name if not set. + if (account.communityName.empty()) + account.communityName = account.name; + + // If we loaded from the default account, check if the settings is overriding the start level and position. + // Also, save the account and add it to the file system. + if (loadedFromDefault) + { + auto& settings = server->getSettings(); + + // Check to see if we are overriding our start level and position. + if (settings.exists("startlevel")) + account.level = settings.get("startlevel").value_or("onlinestartlocal.nw"); + + if (settings.exists("startx")) + account.character.localPixelX = static_cast(settings.get("startx").value_or(30.0f) * 16); + + if (settings.exists("starty")) + account.character.localPixelY = static_cast(settings.get("starty").value_or(30.5f) * 16); + + // Save our account now and add it to the file system. + if (!account.loadOnly) + saveAccount(account); + } + + return true; +} + +bool FlatFileAccountLoader::saveAccount(const Account& account) +{ + auto server = BabyDI::Get(); + + // Don't save 'Load Only' or RC accounts. + if (account.loadOnly) + return false; + +#ifdef DEBUG + assert(account.level.empty() == false); +#endif + + std::string colorStr = std::format("{},{},{},{},{}", account.character.colors[0], account.character.colors[1], account.character.colors[2], account.character.colors[3], account.character.colors[4]); + std::string colorStrEx = std::format("{},{},{},{}", colorStr, account.character.colors[5], account.character.colors[6], account.character.colors[7]); + std::string defaultColorStr = "2,0,10,4,18"; + std::string defaultColorStrEx = "2,0,10,4,18,18,18,18"; + + std::string newFile = "GRACC001\r\n"; + writeLine(newFile, "NAME", account.name); + writeLine(newFile, "NICK", account.character.nickName); + writeLine(newFile, "COMMUNITYNAME", account.communityName, account.name); + writeLine(newFile, "LEVEL", account.level); + + if (!account.groupName.empty()) + writeLine(newFile, "GROUPNAME", account.groupName); + + writeLine(newFile, "X", account.character.localPixelX / 16.0f); + writeLine(newFile, "Y", account.character.localPixelY / 16.0f); + writeLine(newFile, "Z", account.character.localPixelZ / 16.0f, 0.0f); + writeLine(newFile, "MAPX", account.character.mapX); + writeLine(newFile, "MAPY", account.character.mapY); + writeLine(newFile, "MAXHP", account.maxHitpoints); + writeLine(newFile, "HP", account.character.hitpointsInHalves / 2.0f); + writeLine(newFile, "ANI", account.character.gani); + writeLine(newFile, "SPRITE", (account.character.sprite << 2 | account.character.direction), 2); + writeLine(newFile, "GRALATS", account.character.gralats); + writeLine(newFile, "ARROWS", account.character.arrows); + writeLine(newFile, "BOMBS", account.character.bombs); + writeLine(newFile, "GLOVEP", account.character.glovePower); + writeLine(newFile, "SWORDP", account.character.swordPower); + writeLine(newFile, "SHIELDP", account.character.shieldPower); + writeLine(newFile, "BOMBP", account.character.bombPower, 1_ui8); + writeLine(newFile, "BOWP", account.character.bowPower, 1_ui8); + writeLine(newFile, "BOW", account.character.bowImage, ""); + writeLine(newFile, "HEAD", account.character.headImage); + writeLine(newFile, "BODY", account.character.bodyImage); + writeLine(newFile, "SWORD", account.character.swordImage); + writeLine(newFile, "SHIELD", account.character.shieldImage); + + if (server->isNewWorldMode()) + writeLine(newFile, "COLORS", colorStrEx, defaultColorStrEx); + else writeLine(newFile, "COLORS", colorStr, defaultColorStr); + + writeLine(newFile, "STATUS", account.status); + writeLine(newFile, "MP", account.character.mp, 0_ui8); + writeLine(newFile, "AP", account.character.ap); + writeLine(newFile, "APCOUNTER", account.apCounter, 0_ui8); + writeLine(newFile, "ONSECS", account.onlineSeconds, (uint32_t)0); + writeLine(newFile, "IP", account.ipAddress); + writeLine(newFile, "LANGUAGE", account.language, "English"sv); + writeLine(newFile, "KILLS", account.kills, (uint32_t)0); + writeLine(newFile, "DEATHS", account.deaths, (uint32_t)0); + writeLine(newFile, "RATING", account.eloRating, 1500.0f); + writeLine(newFile, "DEVIATION", account.eloDeviation, 350.0f); + writeLine(newFile, "LASTSPARTIME", clock::to_time_t(account.lastSparTime), (time_t)0); + + // Attributes + for (size_t i = 0; i < 30; i++) + writeLine(newFile, "ATTR" + std::to_string(i + 1), account.character.ganiAttributes[i], ""); + + // Chests + for (const auto& [level, pos] : account.savedChests) + writeLine(newFile, "CHEST", std::format("{}:{}:{}", pos.x(), pos.y(), level)); + + // Weapons + for (const auto& weapon : account.weapons) + writeLine(newFile, "WEAPON", weapon); + + // Flags + for (const auto& [variable, value] : account.variables.store | variables::no_temporary) + { + if (auto serialized = account.variables.serializeModern(variable); serialized.has_value()) + writeLine(newFile, "FLAG", serialized.value()); + } + + // Account Settings + newFile += "\r\n"; + writeLine(newFile, "BANNED", account.banned ? 1 : 0, 0); + writeLine(newFile, "BANREASON", account.banReason, ""); + writeLine(newFile, "BANLENGTH", account.banLength, ""); + writeLine(newFile, "COMMENTS", account.comments, ""); + writeLine(newFile, "EMAIL", account.email, ""); + writeLine(newFile, "LOCALRIGHTS", account.adminRights, (uint32_t)0); + writeLine(newFile, "IPRANGE", string::join(account.adminIpRange), ""); + writeLine(newFile, "LOADONLY", account.loadOnly ? 1 : 0, 0); + + // Folder Rights + for (const auto& perm : account.folderList) + writeLine(newFile, "FOLDERRIGHT", perm); + + // Last Folder Accessed + writeLine(newFile, "LASTFOLDER", account.lastFolderAccessed, ""); + + // Get the file name for the account. + auto accountFileName = std::format("{}.txt", account.name); + auto accountPath = server->getFileSystemServer().findi(fs::FileCategory::ACCOUNT, accountFileName); + if (accountPath.empty()) + accountPath = std::filesystem::path{ "accounts" } / accountFileName; + + // Save the account now. + if (!CString(newFile).save(accountPath.string())) + log::printLine(log::rc, "** Error saving account: {}", account.name); + + return true; +} + +bool FlatFileAccountLoader::checkSearchConditions(std::string_view account, const std::vector& searches) const +{ + constexpr std::array conditions = { ">=", "<=", "!=", "=", ">", "<" }; + + // Load the account data. + std::string file; + { + CString fileData; + fileData.load(account); + if (fileData.isEmpty() || fileData.subString(0, 8) != "GRACC001") + return false; + file = fileData.toString(); + } + + // Go through each search and check if the conditions are met. + for (const auto& search : searches) + { + // Find the condition. + size_t condition = std::numeric_limits::max(); + for (size_t i = 0; i < (size_t)conditions.size(); ++i) + { + if (search.find(conditions[i]) != std::string::npos) + { + condition = i; + break; + } + } + + // If we didn't find a condition, fail out completely. + if (condition == std::numeric_limits::max()) + return false; + + // Split the search up into the components. + std::string searchSection = search.substr(0, search.find(conditions[condition])); + std::string searchValue = search.substr(search.find(conditions[condition]) + conditions[condition].size()); + + // Check if the search value is a number. + float searchValueNumber = 0.0f; + bool searchValueIsNumber = string::toFloat(searchValue, searchValueNumber); + + // Search for all matching sections. + bool matched = false; + size_t pos = 0; + while (pos < file.length() && (pos = string::findi(file, searchSection, pos)) != std::string::npos) + { + // Get the value for this line. + auto start = file.find(' ', pos); + auto end = file.find('\n', start); + std::string fileValue; + { + std::string_view value_view(file.data() + start + 1, end - start - 1); + fileValue = string::trim(value_view); + } + + // Check if the value is a number. + float valueNum = 0.0f; + if (string::toFloat(fileValue, valueNum) && searchValueIsNumber) + { + switch (condition) + { + case 0: + matched |= valueNum >= searchValueNumber; + break; + case 1: + matched |= valueNum <= searchValueNumber; + break; + case 2: + matched |= valueNum != searchValueNumber; + break; + case 3: + matched |= valueNum == searchValueNumber; + break; + case 4: + matched |= valueNum > searchValueNumber; + break; + case 5: + matched |= valueNum < searchValueNumber; + break; + } + } + else + { + switch (condition) + { + case 0: + matched |= string::comparei(fileValue, searchValue) >= 0; + break; + case 1: + matched |= string::comparei(fileValue, searchValue) <= 0; + break; + case 2: + matched |= string::comparei(fileValue, searchValue) != 0; + break; + case 3: + matched |= string::comparei(fileValue, searchValue) == 0; + break; + case 4: + matched |= string::comparei(fileValue, searchValue) > 0; + break; + case 5: + matched |= string::comparei(fileValue, searchValue) < 0; + break; + } + } + + pos = end + 1; + } + + if (!matched) + return false; + } + + return true; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/loader/flatfile/FlatFileNPCLoader.cpp b/server/src/loader/flatfile/FlatFileNPCLoader.cpp new file mode 100644 index 000000000..86d10c52c --- /dev/null +++ b/server/src/loader/flatfile/FlatFileNPCLoader.cpp @@ -0,0 +1,640 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +static constexpr std::array attrPackets = { 36, 37, 38, 39, 40, 44, 45, 46, 47, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73 }; + +/////////////////////////////////////////////////////////////////////////////// + +NPCPtr FlatFileNPCLoader::loadNPC(std::string_view npcName) noexcept +{ + auto server = BabyDI::Get(); + auto fileInfo = server->getFileSystemServer().info(fs::FileCategory::NPC, std::format("npc{}.txt", npcName)); + if (fileInfo == nullptr) + return nullptr; + + return loadNPC(fileInfo->file); +} + +NPCPtr FlatFileNPCLoader::loadNPC(const std::filesystem::path& filePath) noexcept +{ + auto server = BabyDI::Get(); + + // Load file + auto file = server->getFileSystemServer().open(fs::FileCategory::NPC, filePath); + if (file == nullptr) + return nullptr; + + std::string header = string::trimMutate(file->readLine()); + if (header != "GRNPC001") + return nullptr; + + auto npcNameFromFile = filePath.stem().string(); + auto name = file->readConfigLine("NAME", " "sv).value_or(npcNameFromFile.substr(3, npcNameFromFile.length() - 7)); + + // Search for the ID of the NPC from the file data. + NPCID id = 0; + if (auto sectionId = file->readConfigLine("ID", " "sv); sectionId.has_value()) + { + id = string::toNumber(sectionId.value()); + if (id < NPCID_GEN_DATABASE) + { + id = 0; + log::printLine(log::server, "** NPC [{}] ID is less than {}, getting next available.", name, NPCID_GEN_DATABASE); + } + else if (server->m_npcIdGenerator.isIdUsed(id)) + { + id = 0; + log::printLine(log::server, "** NPC [{}] ID is already in use, getting next available.", name); + } + else server->m_npcIdGenerator.markAsUsed(id); + } + + if (id == 0) + id = server->m_npcIdGenerator.getAvailableId(NPCID_GEN_DATABASE); + + // Make the NPC. + auto npc = std::make_shared(id, NPCStorageType::DATABASE); + npc->lastSaveTime = fs::getFileModTime(filePath); + + // Set the default warp type. + auto warpRestriction = server->hasNPCServer() ? NPCWarpRestrictions::NOTALLOWED : NPCWarpRestrictions::ALLOWED; + + // Set some default values. + bool isMale = true; + npc->visFlags = PROPID(NPCVisFlags::VISIBLE) | PROPID(NPCVisFlags::CREATED); + + const auto& updateTime = server->getServerStartTime(); + std::string script; + std::vector joinedClasses; + + // Parse File + std::string line; + std::string command; + while (!file->finishedReading()) + { + line = string::trimMutate(file->readLine()); + + std::string_view lineView = line; + command = string::extractLine(lineView, ' '); + + // Parse Line + if (command == "NAME") + { + npc->name = lineView; + npc->modTime[PROPID(NPCProp::NAME)] = updateTime; + } + else if (command == "ID") + ; // npc->id = string::toNumber(std::string{ lineView }); + else if (command == "TYPE") + npc->scriptType = lineView; + else if (command == "SCRIPTER") + { + npc->scripter = lineView; + npc->modTime[PROPID(NPCProp::SCRIPTER)] = updateTime; + } + else if (command == "IMAGE") + { + npc->image = lineView; + npc->modTime[PROPID(NPCProp::IMAGE)] = updateTime; + } + else if (command == "IMGPART") + { + auto parts = string::splitToVectorView(lineView, " "sv); + if (parts.size() >= 4) + { + npc->imagePart.position = {string::toNumber(parts[0]), string::toNumber(parts[1])}; + npc->imagePart.size = {string::toNumber(parts[2]), string::toNumber(parts[3])}; + npc->modTime[PROPID(NPCProp::IMAGEPART)] = updateTime; + } + } + else if (command == "STARTLEVEL") + npc->m_initialLevel = lineView; + else if (command == "STARTX") + npc->m_initialCharacter.localPixelX = static_cast(string::toFloat(std::string{lineView}) * 16); + else if (command == "STARTY") + npc->m_initialCharacter.localPixelY = static_cast(string::toFloat(std::string{lineView}) * 16); + else if (command == "STARTZ") + npc->m_initialCharacter.localPixelZ = static_cast(string::toFloat(std::string{lineView}) * 16); + else if (command == "LEVEL") + npc->level = lineView; + else if (command == "GROUPNAME") + npc->groupName = lineView; + else if (command == "X") + { + npc->character.localPixelX = static_cast(string::toFloat(std::string{ lineView }) * 16); + npc->modTime[PROPID(NPCProp::X)] = updateTime; + npc->modTime[PROPID(NPCProp::X2)] = updateTime; + } + else if (command == "Y") + { + npc->character.localPixelY = static_cast(string::toFloat(std::string{ lineView }) * 16); + npc->modTime[PROPID(NPCProp::Y)] = updateTime; + npc->modTime[PROPID(NPCProp::Y2)] = updateTime; + } + else if (command == "Z") + { + npc->character.localPixelZ = static_cast(string::toFloat(std::string{ lineView }) * 16); + npc->modTime[PROPID(NPCProp::Z)] = updateTime; + npc->modTime[PROPID(NPCProp::Z2)] = updateTime; + } + else if (command == "MAPX") + { + npc->character.mapX = string::toNumber(std::string{ lineView }); + npc->modTime[PROPID(NPCProp::GMAPLEVELX)] = updateTime; + } + else if (command == "MAPY") + { + npc->character.mapY = string::toNumber(std::string{ lineView }); + npc->modTime[PROPID(NPCProp::GMAPLEVELY)] = updateTime; + } + else if (command == "NICK") + { + npc->character.nickName = lineView; + npc->modTime[PROPID(NPCProp::NICKNAME)] = updateTime; + } + else if (command == "ANI") + { + npc->character.gani = lineView; + npc->modTime[PROPID(NPCProp::GANI)] = updateTime; + } + else if (command == "HP") + { + npc->character.hitpointsInHalves = static_cast(2 * string::toFloat(std::string{ lineView })); + npc->modTime[PROPID(NPCProp::POWER)] = updateTime; + } + else if (command == "GRALATS") + { + npc->character.gralats = string::toNumber(std::string{ lineView }); + npc->modTime[PROPID(NPCProp::RUPEES)] = updateTime; + } + else if (command == "ARROWS") + { + npc->character.arrows = string::toNumber(std::string{ lineView }); + npc->modTime[PROPID(NPCProp::ARROWS)] = updateTime; + } + else if (command == "BOMBS") + { + npc->character.bombs = string::toNumber(std::string{ lineView }); + npc->modTime[PROPID(NPCProp::BOMBS)] = updateTime; + } + else if (command == "GLOVEP") + { + npc->character.glovePower = string::toNumber(std::string{ lineView }); + npc->modTime[PROPID(NPCProp::GLOVEPOWER)] = updateTime; + } + else if (command == "SWORDP") + { + npc->character.swordPower = string::toNumber(std::string{ lineView }); + npc->modTime[PROPID(NPCProp::SWORDIMAGE)] = updateTime; + } + else if (command == "SHIELDP") + { + npc->character.shieldPower = string::toNumber(std::string{ lineView }); + npc->modTime[PROPID(NPCProp::SHIELDIMAGE)] = updateTime; + } + else if (command == "BOWP") + { + npc->character.bowPower = string::toNumber(std::string{ lineView }); + npc->modTime[PROPID(NPCProp::GANI)] = updateTime; + } + else if (command == "BOW") + { + npc->character.bowImage = lineView; + npc->modTime[PROPID(NPCProp::GANI)] = updateTime; + } + else if (command == "HEAD") + { + npc->character.headImage = lineView; + npc->modTime[PROPID(NPCProp::HEADIMAGE)] = updateTime; + } + else if (command == "BODY") + { + npc->character.bodyImage = lineView; + npc->modTime[PROPID(NPCProp::BODYIMAGE)] = updateTime; + } + else if (command == "SWORD") + { + npc->character.swordImage = lineView; + npc->modTime[PROPID(NPCProp::SWORDIMAGE)] = updateTime; + } + else if (command == "SHIELD") + { + npc->character.shieldImage = lineView; + npc->modTime[PROPID(NPCProp::SHIELDIMAGE)] = updateTime; + } + else if (command == "HORSE") + { + npc->character.horseImage = lineView; + npc->modTime[PROPID(NPCProp::HORSEIMAGE)] = updateTime; + } + else if (command == "COLORS") + { + auto tokens = string::splitToVectorView(lineView, ","sv); + for (size_t idx = 0; idx < std::min(tokens.size(), (size_t)8); idx++) + npc->character.colors[idx] = string::toNumber(tokens[idx]); + npc->modTime[PROPID(NPCProp::COLORS)] = updateTime; + } + else if (command == "SPRITE") + { + auto sprite = string::toNumber(std::string{ lineView }); + npc->character.sprite = sprite >> 2; + npc->character.direction = sprite & 0b11; + npc->modTime[PROPID(NPCProp::SPRITE)] = updateTime; + } + else if (command == "AP") + { + npc->character.ap = string::toNumber(std::string{ lineView }); + npc->modTime[PROPID(NPCProp::ALIGNMENT)] = updateTime; + } + else if (command == "TIMEOUT") + { + npc->timeout = std::chrono::milliseconds(string::toNumber(std::string{ lineView }) * 20); + } + else if (command == "LAYER") + { + auto layer = string::toNumber(std::string{ lineView }); + if (layer == 0) + npc->visFlags |= PROPID(NPCVisFlags::DRAWUNDERPLAYER); + if (layer == 2) + npc->visFlags |= PROPID(NPCVisFlags::DRAWOVERPLAYER); + npc->modTime[PROPID(NPCProp::VISFLAGS)] = updateTime; + } + else if (command == "SHAPETYPE") + { + // Only shape type 1 is supported, but we just look at the dimension of the shape data. + } + else if (command == "SHAPE") + { + std::get<0>(npc->shape.data) = string::toNumber(string::extractLine(lineView, ' ')); + std::get<1>(npc->shape.data) = string::toNumber(std::string{ string::trim(lineView) }); + } + else if (command == "DONTBLOCK") + { + npc->blockFlags = string::toNumber(std::string{ lineView }); + npc->modTime[PROPID(NPCProp::BLOCKFLAGS)] = updateTime; + } + else if (command == "NOPLAYERONWALL") + { + npc->noPlayerOnWall = string::toNumber(std::string{ lineView }) != 0; + } + else if (command == "SAVEARR") + { + auto tokens = string::splitToVectorView(lineView, ","sv); + for (size_t idx = 0; idx < std::min(tokens.size(), npc->saves.size()); idx++) + { + npc->saves[idx] = string::toNumber(tokens[idx]); + npc->modTime[PROPID(NPCProp::SAVE0) + idx] = updateTime; + } + } + else if (command == "CANWARP") + { + warpRestriction = string::toNumber(std::string{ lineView }) != 0 ? NPCWarpRestrictions::ALLOWED : warpRestriction; + } + else if (command == "CANWARP2") + { + warpRestriction = string::toNumber(std::string{ lineView }) != 0 ? NPCWarpRestrictions::ONLYOVERWORLD : warpRestriction; + } + + // Official variables for these are unknown. + else if (command == "CANCARRY") + { + auto value = string::toNumber(std::string{ lineView }); + if (value != 0) + npc->blockFlags |= PROPID(NPCBlockFlags::CANBECARRIED); + } + else if (command == "CANPULL") + { + auto value = string::toNumber(std::string{ lineView }); + if (value != 0) + npc->blockFlags |= PROPID(NPCBlockFlags::CANBEPULLED); + } + else if (command == "CANPUSH") + { + auto value = string::toNumber(std::string{ lineView }); + if (value != 0) + npc->blockFlags |= PROPID(NPCBlockFlags::CANBEPUSHED); + } + else if (command == "VISIBLE") + { + auto value = string::toNumber(std::string{ lineView }); + if (value == 0) + npc->visFlags &= ~PROPID(NPCVisFlags::VISIBLE); + } + else if (command == "TIMERSHOW") + { + auto value = string::toNumber(std::string{ lineView }); + if (value != 0) + npc->visFlags |= PROPID(NPCVisFlags::TIMERSHOW); + } + else if (command == "MALE") + { + auto value = string::toNumber(std::string{ lineView }); + if (value == 0) + isMale = false; + } + //--- + + else if (command == "FLAG") + { + std::string flagName = string::trimMutate(string::extractLine(lineView, '=')); + std::string flagValue = std::string{ string::trim(lineView) }; + npc->scripting.variables.add(GameValue::deserialize(flagName, flagValue)); + } + else if (command.substr(0, 4) == "ATTR") + { + auto attrIdStr = command.substr(5); + int attrId = string::toNumber(attrIdStr); + if (attrId > 0 && attrId < 30) + { + int idx = attrId - 1; + npc->character.ganiAttributes[idx] = lineView; + npc->modTime[attrPackets[idx]] = updateTime; + } + } + else if (command == "JOINEDCLASSES") + { + joinedClasses = string::fromCSV(lineView); + } + else if (command == "NPCSCRIPT") + { + do { + line = string::trimNewlines(file->readLine()); + if (string::trim(line) == "NPCSCRIPTEND") + break; + + script.append(line).append(1, '\n'); + } while (!file->finishedReading()); + + npc->modTime[PROPID(NPCProp::SCRIPT)] = updateTime; + } + } + file->close(); + + // If the NPC is a character, set the gender prop. + // Also, set the gender. + if (npc->isCharacter() && isMale) + { + npc->visFlags |= PROPID(NPCVisFlags::MALE); + } + + // If the NPC has no image, make it invisible. + if (!npc->hasImage() && !npc->hasShape()) + { + npc->visFlags &= ~PROPID(NPCVisFlags::VISIBLE); + } + + // Set the script. + npc->setScript(script); + + // Join the classes. + for (const auto& className : joinedClasses) + { + if (!className.empty()) + npc->joinClass(className); + } + + // Add the NPC to the server. + server->addNPC(npc, false); + + // Set the warp restriction (do this after adding to the server since that will overwrite the restriction). + npc->warpRestrictions = warpRestriction; + + // Check if we need to rename the file. + auto expectedFileName = fs::getHTMLEscapedFileName(std::format("npc{}.txt", npc->name)).string(); + auto currentFileName = fs::getANSIFileName(filePath); + if (expectedFileName != currentFileName) + { + auto fileData = server->getFileSystemServer().infoi(fs::FileCategory::NPC, currentFileName); + if (fileData != nullptr) + { + auto indent = log::server.indent(); + if (server->getFileSystemServer().rename(*fileData, expectedFileName)) + log::printLine(log::server, "Renamed NPC file [{}] to [{}]", currentFileName, expectedFileName); + else + log::printLine(log::server, "** Failed to rename NPC file [{}] to [{}]", currentFileName, expectedFileName); + } + } + + return npc; +} + +bool FlatFileNPCLoader::saveNPC(NPCPtr npc) noexcept +{ + if (npc->storageType != NPCStorageType::DATABASE) + return false; + + // Open the file for writing. + auto server = BabyDI::Get(); + auto fileName = fs::getHTMLEscapedFileName(std::format("npc{}.txt", npc->name)); + auto file = server->getFileSystemServer().openiForWriting(fs::FileCategory::NPC , fileName, true); + if (!file) + return false; + + // Function to check for prop modification before writing. + auto writeProp = [&](NPCProp prop, std::string_view key, std::string_view value) + { + if (npc->modTime[PROPID(prop)] != clock::time_point::min()) + file->writeConfigLine(key, value); + }; + + auto level = npc->getLevel(); + + // Get the draw layer number. + int layer = 0; + if (npc->visFlags & PROPID(NPCVisFlags::DRAWUNDERPLAYER)) + layer = -1; + else if (npc->visFlags & PROPID(NPCVisFlags::DRAWOVERPLAYER)) + layer = 1; + + // Start the file. + file->clear(); + file->writeLine("GRNPC001"); + + // Write our data. + file->writeConfigLine("NAME", npc->name); + file->writeConfigLine("ID", string::to_string(npc->id)); + file->writeConfigLine("TYPE", npc->scriptType); + file->writeConfigLine("SCRIPTER", npc->scripter); + + file->writeConfigLine("IMAGE", npc->image); + if (npc->imagePart.size.width() > 0 && npc->imagePart.size.height() > 0) + { + file->writeConfigLine("IMGPART", std::format("{} {} {} {}", npc->imagePart.position.x(), npc->imagePart.position.y(), npc->imagePart.size.width(), npc->imagePart.size.height())); + } + + file->writeConfigLine("STARTLEVEL", npc->m_initialLevel); + file->writeConfigLine("STARTX", string::to_string(npc->m_initialCharacter.localPixelX / 16.0, 2)); + file->writeConfigLine("STARTY", string::to_string(npc->m_initialCharacter.localPixelY / 16.0, 2)); + file->writeConfigLine("STARTZ", string::to_string(npc->m_initialCharacter.localPixelZ / 16.0, 2)); + + if (!npc->level.empty()) + { + file->writeConfigLine("LEVEL", npc->getLevelName()); + if (!npc->groupName.empty()) + file->writeConfigLine("GROUPNAME", npc->groupName); + + file->writeConfigLine("X", string::to_string(npc->character.localPixelX / 16.0, 2)); + file->writeConfigLine("Y", string::to_string(npc->character.localPixelY / 16.0, 2)); + file->writeConfigLine("Z", string::to_string(npc->character.localPixelZ / 16.0, 2)); + if (npc->character.mapX != 0 || npc->character.mapY != 0) + { + file->writeConfigLine("MAPX", string::to_string(npc->character.mapX)); + file->writeConfigLine("MAPY", string::to_string(npc->character.mapY)); + } + } + + writeProp(NPCProp::NICKNAME, "NICK", npc->character.nickName); + + if (server->Generation != ServerGeneration::ORIGINAL) + writeProp(NPCProp::GANI, "ANI", npc->character.gani); + + writeProp(NPCProp::POWER, "HP", std::format("{:.2f}", npc->character.hitpointsInHalves / 2.0f)); + writeProp(NPCProp::RUPEES, "GRALATS", string::to_string(npc->character.gralats)); + writeProp(NPCProp::ARROWS, "ARROWS", string::to_string(npc->character.arrows)); + writeProp(NPCProp::BOMBS, "BOMBS", string::to_string(npc->character.bombs)); + writeProp(NPCProp::GLOVEPOWER, "GLOVEP", string::to_string(npc->character.glovePower)); + writeProp(NPCProp::SWORDIMAGE, "SWORDP", string::to_string(npc->character.swordPower)); + writeProp(NPCProp::SHIELDIMAGE, "SHIELDP", string::to_string(npc->character.shieldPower)); + + if (server->Generation == ServerGeneration::ORIGINAL) + { + writeProp(NPCProp::GANI, "BOWP", string::to_string(npc->character.bowPower)); + writeProp(NPCProp::GANI, "BOW", npc->character.bowImage); + } + + writeProp(NPCProp::HEADIMAGE, "HEAD", npc->character.headImage); + writeProp(NPCProp::BODYIMAGE, "BODY", npc->character.bodyImage); + writeProp(NPCProp::SWORDIMAGE, "SWORD", npc->character.swordImage); + writeProp(NPCProp::SHIELDIMAGE, "SHIELD", npc->character.shieldImage); + writeProp(NPCProp::HORSEIMAGE, "HORSE", npc->character.horseImage); + + if (server->isNewWorldMode()) + writeProp(NPCProp::COLORS, "COLORS", std::format("{},{},{},{},{},{},{},{}", npc->character.colors[0], npc->character.colors[1], npc->character.colors[2], npc->character.colors[3], npc->character.colors[4], npc->character.colors[5], npc->character.colors[6], npc->character.colors[7])); + else writeProp(NPCProp::COLORS, "COLORS", std::format("{},{},{},{},{}", npc->character.colors[0], npc->character.colors[1], npc->character.colors[2], npc->character.colors[3], npc->character.colors[4])); + + writeProp(NPCProp::SPRITE, "SPRITE", string::to_string(npc->character.sprite << 2 | npc->character.direction)); + writeProp(NPCProp::ALIGNMENT, "AP", string::to_string(npc->character.ap)); + + if (npc->timeout != 0ms) + file->writeConfigLine("TIMEOUT", string::to_string(static_cast(npc->timeout.count() * 0.05))); + + if (layer != 0) + file->writeConfigLine("LAYER", string::to_string(layer + 1)); + + if (npc->shape.width() != 0 || npc->shape.height() != 0) + { + file->writeConfigLine("SHAPETYPE", npc->shape.width() != 0 && npc->shape.height() != 0 ? "1" : "0"); + file->writeConfigLine("SHAPE", std::format("{} {}", npc->shape.width(), npc->shape.height())); + } + + if (npc->blockFlags & PROPID(NPCBlockFlags::NOBLOCK)) + file->writeLine("DONTBLOCK 1"); + if (npc->noPlayerOnWall) + file->writeLine("NOPLAYERONWALL 1"); + if (npc->warpRestrictions == NPCWarpRestrictions::NOTALLOWED) + file->writeLine("CANWARP"); + if (npc->warpRestrictions == NPCWarpRestrictions::ONLYOVERWORLD) + file->writeLine("CANWARP2"); + + // Official variables for these are unknown. + if (npc->blockFlags & PROPID(NPCBlockFlags::CANBECARRIED)) + file->writeLine("CANCARRY 1"); + if (npc->blockFlags & PROPID(NPCBlockFlags::CANBEPULLED)) + file->writeLine("CANPULL"); + if (npc->blockFlags & PROPID(NPCBlockFlags::CANBEPUSHED)) + file->writeLine("CANPUSH"); + if ((npc->visFlags & PROPID(NPCVisFlags::VISIBLE)) == 0) + file->writeLine("VISIBLE 0"); + if ((npc->visFlags & PROPID(NPCVisFlags::TIMERSHOW)) != 0) + file->writeLine("TIMERSHOW 1"); + if (npc->isCharacter() && (npc->visFlags & PROPID(NPCVisFlags::MALE)) == 0) + file->writeLine("MALE 0"); + // --- + + if (!std::ranges::empty(NPCSaveProps | std::views::filter([&npc](NPCProp prop) { return npc->modTime[PROPID(prop)] != clock::time_point::min(); }))) + file->writeConfigLine("SAVEARR", string::toCSV(npc->saves | std::views::transform([](uint8_t x) { return string::to_string(x); }))); + + for (int i = 0; i < 30; i++) + { + NPCProp prop = static_cast(NPCGaniAttrPackets[i]); + if (!npc->character.ganiAttributes[i].empty()) + writeProp(prop, std::format("ATTR{}", i + 1), npc->character.ganiAttributes[i]); + } + + for (auto& [flag, value] : npc->scripting.variables.store | variables::no_temporary) + { + // Ignore flags. + if (value->has() && !value->has()) continue; + + // Serialize the variable entirely. + if (server->Generation == ServerGeneration::MODERN) + { + auto var = npc->scripting.variables.serializeModern(flag); + if (var.has_value()) + file->writeConfigLine("FLAG", var.value()); + } + else + { + for (const auto& serialized : npc->scripting.variables.serialize(flag)) + file->writeLine(serialized); + } + } + + if (!npc->m_joinedClasses.empty()) + { + file->writeConfigLine("JOINEDCLASSES", npc->getJoinedClassesList()); + } + + file->writeConfigSection("NPCSCRIPT", npc->getScript().getOriginalSource(), "NPCSCRIPTEND"); + + // Finish up. + file->close(); + + // Update the NPC's last save time. + npc->lastSaveTime = toSystemClock(file->modifiedTime()); + + // If the NPC exists on the filesystem, refresh its mod time to avoid any modification events. + auto& fs = server->getFileSystemServer(); + if (auto info = fs.info(fs::FileCategory::NPC, file->filePath().filename()); info != nullptr) + info->refreshModTime(); + // Else if the NPC doesn't exist, we want to add it to the file system so the file watcher doesn't cause a reload. + else + { + fs.addExisting(fs::FileCategory::NPC, file->filePath()); + } + + return true; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/main.cpp b/server/src/main.cpp index 3b9cf8b81..df785d3a9 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -1,23 +1,30 @@ -#include - #include #include +#include #include +#include #include -#include -#include +#include +#include +#include +#include +#include -#include #include + +#include #include +#include #include -#include "BabyDI.h" -#include "IConfig.h" +#include +#include +#include +#include +#include +#include -#include "Account.h" -#include "Server.h" -#include "main.h" +using namespace preagonal; // Linux specific stuff. #if !(defined(_WIN32) || defined(_WIN64)) @@ -31,54 +38,49 @@ typedef void (*sighandler_t)(int); // Home path of the gserver. -CString homePath; -static void getBasePath(); -std::string getBaseHomePath() -{ - return homePath.text(); -} - -void getBasePath() +static CString getBasePath() { + CString homePath; #if defined(_WIN32) || defined(_WIN64) // Get the path. char path[MAX_PATH]; - GetCurrentDirectoryA(MAX_PATH, path); - - // Find the program exe and remove it from the path. - // Assign the path to homepath. - homePath = path; - homePath += "\\"; - int pos = homePath.findl('\\'); - if (pos == -1) homePath.clear(); - else if (pos != (homePath.length() - 1)) - homePath.removeI(++pos, homePath.length()); + DWORD length = GetModuleFileNameA(NULL, path, MAX_PATH); + std::string_view path_view{ path, length }; + if (auto pos = path_view.find_last_of("\\"); pos != std::string_view::npos) + path_view = path_view.substr(0, pos); + homePath = path_view; #elif __APPLE__ - char path[255]; - if (!getcwd(path, sizeof(path))) - printf("Error getting CWD\n"); - - homePath = path; - if (homePath[homePath.length() - 1] != '/') - homePath << '/'; + char path[1024]; + uint32_t size = sizeof(path); + int result = _NSGetExecutablePath(&path[0], &size); + if (result == -1) + printf("Error getting executable path\n"); + + std::string_view path_view{ path, size }; + homePath = path_view; #else // Get the path to the program. - char path[260]; - memset((void*)path, 0, 260); + char path[1024]; + memset((void*)path, 0, 1024); readlink("/proc/self/exe", path, sizeof(path)); // Assign the path to homepath. char* end = strrchr(path, '/'); if (end != 0) { - end++; - if (end != 0) *end = '\0'; + *end = '\0'; homePath = path; } #endif + printf("Calculated home path: %s\n", homePath.text()); + return homePath; +} +std::filesystem::path getBaseHomePath() +{ + static std::filesystem::path homePath{ getBasePath().toString() }; + return homePath; } -CLog serverlog("startuplog.txt"); CString overrideServer; CString overridePort; CString overrideServerIp = nullptr; @@ -95,12 +97,6 @@ int main(int argc, char* argv[]) if (parseArgs(argc, argv)) return 1; - #if (defined(_WIN32) || defined(_WIN64) || defined(WIN32) || defined(WIN64)) && defined(_MSC_VER) - #if defined(DEBUG) || defined(_DEBUG) - _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); - #endif - #endif - { // Shut down the server if we get a kill signal. signal(SIGINT, (sighandler_t)shutdownServer); @@ -111,62 +107,100 @@ int main(int argc, char* argv[]) // Seed the random number generator with the current time. srand((unsigned int)time(0)); - // Grab the base path to the server executable. - getBasePath(); + // Load Server Settings + std::string discovery_mode; + std::cout << "Determining the server to start... "; + std::filesystem::path cwd = std::filesystem::current_path(); - // Program announcements. - serverlog.out("%s %s version %s\n", APP_VENDOR, APP_NAME, APP_VERSION); - serverlog.out("Programmed by %s.\n\n", APP_CREDITS); + auto found_server = [&discovery_mode](const std::string& why, std::string_view server, std::filesystem::path& working_directory, const std::filesystem::path& test_directory) + { + if (!working_directory.empty() && !test_directory.empty()) + { + // Sanity check. + if (!std::filesystem::exists(working_directory / test_directory)) + { + std::cout << "FAILED! " << why << " (" << server << ")" << std::endl; + std::cerr << "Failed to start server: working directory does not exist." << std::endl; + return false; + } + std::filesystem::current_path(working_directory / test_directory); + working_directory = std::filesystem::current_path(); + } - // Load Server Settings - if (overrideServer.isEmpty()) + std::cout << "success! " << why << std::endl; + discovery_mode = why; + overrideServer = server; + return true; + }; + + // Environment variable / command line (do similar to startupserver.txt) + if (!overrideServer.isEmpty()) { - serverlog.out(":: Determining the server to start... "); + bool use_env = getenv("USE_ENV"); + if (!found_server(use_env ? "(environment variable)" : "(command line)", overrideServer.toStringView(), cwd, cwd / "servers" / overrideServer.text())) + return ERR_SETTINGS; + } - auto found_server = [](const std::string& why, const std::string& server) - { - serverlog.append("success! %s\n", why.c_str()); - overrideServer = server; - }; + // Current working directory. + if (overrideServer.isEmpty()) + { + if (std::filesystem::exists(cwd / "config" / "serveroptions.txt") && !found_server("(current working directory)", cwd.filename().generic_string(), cwd, {})) + return ERR_SETTINGS; + } - // startupserver.txt - { - CString startup; - startup.load(CString(homePath) << "startupserver.txt"); - if (!startup.isEmpty()) - found_server("(startupserver.txt)", std::string{ startup.text() }); - } + // startupserver.txt + if (overrideServer.isEmpty()) + { + CString startup; + startup.load("startupserver.txt"); + startup = startup.readString("\n").replaceAllI("\r", ""); - // Number of directories. - if (overrideServer.isEmpty()) - { - std::vector servers; + if (!startup.isEmpty() && !found_server("(startupserver.txt)", startup.text(), cwd, cwd / "servers" / startup.text())) + return ERR_SETTINGS; + } - std::filesystem::path base_dir{ homePath.text() }; - for (const auto& p: std::filesystem::directory_iterator{ base_dir / "servers" }) - { - if (p.is_directory()) - servers.push_back(p.path().filename()); - } + // Number of directories. + if (overrideServer.isEmpty()) + { + std::vector servers; - if (servers.size() == 1) - found_server("(directory search)", servers.front().string()); + for (const auto& p : std::filesystem::directory_iterator{ "servers" }) + { + if (p.is_directory()) + servers.push_back(p.path().filename()); } - // Failure. - if (overrideServer.isEmpty()) - { - serverlog.append("FAILED!\n"); + if (servers.size() == 1 && !found_server("(directory search)", servers.front().generic_string(), cwd, cwd / "servers" / servers.front())) return ERR_SETTINGS; - } } - // Initialize the server. + // Failure. + if (overrideServer.isEmpty() || !std::filesystem::exists(cwd / "config" / "serveroptions.txt")) + { + std::cout << "FAILED!" << std::endl; + std::cerr << "Failed to start server: no server specified and no default server found." << std::endl; + return ERR_SETTINGS; + } + + // Create the server. auto* server = BabyDI_PROVIDE(Server, new Server(overrideServer)); - serverlog.out(":: Starting server: %s.\n", overrideServer.text()); + [[maybe_unused]] auto* guilds = BabyDI_PROVIDE(GuildManager, new GuildManager()); + + // Program announcements. + log::printLine(log::server, "------------------------------ START ------------------------------"); + log::printLine(log::server, "{} {} version {}", APP_VENDOR, APP_NAME, APP_VERSION); + log::printLine(log::server, "Programmed by {}.", APP_CREDITS); + log::printLine(log::server, ""); + + // Initialize the server. + log::printLine(log::server, "Starting server: {}.", overrideServer); + { + auto indent = log::server.indent(); + log::printLine(log::server, "{}: {}", discovery_mode, std::filesystem::current_path().generic_string()); + } if (server->init(overrideServerIp, overridePort, overrideLocalIp, overrideServerInterface) != 0) { - serverlog.out("** [Error] Failed to start server: %s\n", overrideServer.text()); + log::printLine(log::server, "** [Error] Failed to start server: {}", overrideServer); return 1; } @@ -174,39 +208,34 @@ int main(int argc, char* argv[]) { auto& settings = server->getSettings(); - if (!overrideName.isEmpty()) - settings.addKey("name", overrideName); - if (!overrideStaff.isEmpty()) { if (!server->isStaff(overrideStaff)) { - auto staff = settings.getStr("staff"); - settings.addKey("staff", staff << "," << overrideStaff); + auto staff = settings.get("staff").value_or(""); + settings.set("staff", std::format("{},{}", staff, overrideStaff.toStringView())); } Account accfs; - accfs.loadAccount(overrideStaff, false); - if (accfs.getOnlineTime() == 0) + server->getAccountLoader().loadAccount(overrideStaff.toStringView(), accfs); + if (accfs.onlineSeconds == 0) { - accfs.loadAccount("YOURACCOUNT"); - accfs.setAccountName(overrideStaff); - accfs.saveAccount(); + server->getAccountLoader().loadAccount("YOURACCOUNT", accfs); + accfs.name = overrideStaff.toStringView(); + server->getAccountLoader().saveAccount(accfs); } } - - settings.saveFile(); - server->loadSettings(); } // Announce that the program is now running. - serverlog.out(":: Started server %s", server->getName().text()); + log::print(log::server, "Started server {}", server->getName()); if (server->getSettings().exists("name")) - serverlog.append(" (%s)\n", server->getSettings().getStr("name").text()); - else serverlog.append("\n"); + log::printLine(log::server, " ({})", server->getSettings().get("name").value_or("")); + else + log::printLine(log::server, ""); #if defined(WIN32) || defined(WIN64) - serverlog.out(":: Press CTRL+C to close the program. DO NOT CLICK THE X, you will LOSE data!\n"); + log::printLine(log::server, "Press CTRL+C to close the program. DO NOT CLICK THE X, you will LOSE data!"); #endif // Run the server. @@ -216,6 +245,8 @@ int main(int argc, char* argv[]) CSocket::socketSystemDestroy(); BabyDI_RELEASE(Server); + BabyDI_RELEASE(GuildManager); + BabyDI_RELEASE(ITranslationManager); } return ERR_SUCCESS; @@ -227,8 +258,7 @@ int main(int argc, char* argv[]) void shutdownServer(int signal) { - serverlog.out(":: The server is now shutting down...\n-------------------------------------\n\n"); - + log::printLine(log::server, "The server is now shutting down..."); shutdownProgram = true; } @@ -338,16 +368,16 @@ bool parseArgs(int argc, char* argv[]) void printHelp(const char* pname) { - serverlog.out("%s %s version %s\n", APP_VENDOR, APP_NAME, APP_VERSION); - serverlog.out("Programmed by %s.\n\n", APP_CREDITS); - serverlog.out("USAGE: %s [options]\n\n", pname); - serverlog.out("Commands:\n\n"); - serverlog.out(" -h, --help\t\tPrints out this help text.\n"); - serverlog.out(" -s, --server DIR\tOverride the servers.txt by specifying which server directory to use.\n"); - serverlog.out(" -p, --port PORT\tSpecify which port to use when using servers.txt override.\n"); - serverlog.out(" --localip IP\tSpecify which IP to retrieve when on the same network as the server.\n"); - serverlog.out(" --serverip IP\tSpecify which IP that the listserver should deliver to clients.\n"); - serverlog.out(" --interface IP\tSpecify which IP to bind the server to.\n"); - - serverlog.out("\n"); + printf("%s %s version %s\n", APP_VENDOR, APP_NAME, APP_VERSION); + printf("Programmed by %s.\n\n", APP_CREDITS); + printf("USAGE: %s [options]\n\n", pname); + printf("Commands:\n\n"); + printf(" -h, --help\t\tPrints out this help text.\n"); + printf(" -s, --server DIR\tOverride the servers.txt by specifying which server directory to use.\n"); + printf(" -p, --port PORT\tSpecify which port to use when using servers.txt override.\n"); + printf(" --localip IP\tSpecify which IP to retrieve when on the same network as the server.\n"); + printf(" --serverip IP\tSpecify which IP that the listserver should deliver to clients.\n"); + printf(" --interface IP\tSpecify which IP to bind the server to.\n"); + + printf("\n"); } diff --git a/server/src/misc/UPNP.cpp b/server/src/misc/UPNP.cpp index b6c250f0c..ac154ce79 100644 --- a/server/src/misc/UPNP.cpp +++ b/server/src/misc/UPNP.cpp @@ -1,47 +1,66 @@ -#ifdef UPNP - #define UPNPCOMMAND_CONFLICTING_MAPPING 718 +#define UPNPCOMMAND_CONFLICTING_MAPPING 718 - #if defined(_WIN32) || defined(_WIN64) - #ifndef WIN32_LEAN_AND_MEAN - #define WIN32_LEAN_AND_MEAN - #endif +#include +#include +#include +#include +#include +#include +#include +#include - #ifndef __GNUC__ // rain - #pragma comment(lib, "ws2_32.lib") - #endif +#include +#include +#include - #include - #endif - #include "Server.h" - #include "misc/UPNP.h" +#ifdef ENABLE_UPNP +#include +#include +#include +#if DEBUG +#include +#endif +#endif + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// void UPNP::discover() { +#ifdef ENABLE_UPNP struct UPNPDev* device_list; struct UPNPDev* device; char* xmlDescription; int xmlDescriptionSize = 0, responseCode = 0; - memset(&m_urls, 0, sizeof(UPNPUrls)); - memset(&m_data, 0, sizeof(IGDdatas)); + std::memset(&m_urls, 0, sizeof(UPNPUrls)); + std::memset(&m_data, 0, sizeof(IGDdatas)); + + std::vector> logbatch; + logbatch.emplace_back(0_ui8, "Discovering UPNP devices:"); - device_list = upnpDiscover(2000, 0, 0, 0, 0, 0, 0); + device_list = upnpDiscover(2000, 0, 0, UPNP_LOCAL_PORT_ANY, 0, 2, 0); if (device_list) { + std::vector gatewayDevices; + device = device_list; while (device) { + std::string_view device_view{ device->st }; + logbatch.emplace_back(1_ui8, std::format("{}{}", (device == device_list ? "* " : ""), device_view)); + // We are searching for our gateway device. If we found it, break out. - if (strstr(device->st, "InternetGatewayDevice")) - break; + if (device_view.contains("InternetGatewayDevice")) + gatewayDevices.push_back(device); + device = device->pNext; } - // If no valid device was found, default to the first device. - if (!device) - device = device_list; - - // m_server->getServerLog().out(":: [UPnP] Device desc: %s, st: %s\n", device->descURL, device->st); + // Get the first device in the list. + device = gatewayDevices.front(); // Get the XML description of the UPNP device. xmlDescription = (char*)miniwget(device->descURL, &xmlDescriptionSize, 0, &responseCode); @@ -59,51 +78,104 @@ void UPNP::discover() } else { - m_server->getServerLog().out("** [UPnP] No devices found.\n"); + logbatch.emplace_back(1_ui8, "** [UPnP] No devices found."); } + + log::batch(log::server, logbatch); +#endif } -void UPNP::addPortForward(const CString& addr, const CString& port) +void UPNP::addPortForward(std::string_view address, std::string_view port) { +#ifdef ENABLE_UPNP if (m_urls.controlURL == 0 || m_urls.controlURL[0] == '\0') return; - CLog& serverlog = m_server->getServerLog(); - int r = UPNP_AddPortMapping(m_urls.controlURL, m_data.first.servicetype, port.text(), port.text(), addr.text(), "Graal GServer", "TCP", 0, 0); + int r = UPNP_AddPortMapping(m_urls.controlURL, m_data.first.servicetype, port.data(), port.data(), address.data(), "Graal GServer", "TCP", "", 0); + if (r == UPNPCOMMAND_CONFLICTING_MAPPING) + { + // Check if this port map was likely created by us and was left behind. + char intClient[16] = { 0 }; + char intPort[6] = { 0 }; + char desc[80] = { 0 }; + int r2 = UPNP_GetSpecificPortMappingEntry(m_urls.controlURL, m_data.first.servicetype, port.data(), "TCP", "", intClient, intPort, desc, 0, 0); + if (r2 == 0 && std::string_view{ desc }.find("Graal GServer") != std::string_view::npos) + { + log::printLine(log::server, "[UPnP] Found existing port mapping on port {} likely created by us.", port); + m_portsForwarded.emplace(port); + return; + } +#if DEBUG + else + { + unsigned int entries; + if (UPNP_GetPortMappingNumberOfEntries(m_urls.controlURL, m_data.first.servicetype, &entries) == UPNPCOMMAND_SUCCESS) + { + char index[6] = { 0 }; + for (unsigned int i = 0; i < entries; ++i) + { + std::snprintf(index, 6, "%u", i); + UPNP_GetGenericPortMappingEntry(m_urls.controlURL, m_data.first.servicetype, index, 0, intClient, intPort, 0, desc, 0, 0, 0); + log::printLine(log::server, "[UPnP] Existing mapping #{}: {} -> {} ({})", i, intClient, intPort, desc); + } + } + else + { + PortMappingParserData parserData; + memset(&parserData, 0, sizeof(PortMappingParserData)); + if (UPNP_GetListOfPortMappings(m_urls.controlURL, m_data.first.servicetype, "1", "65535", "TCP", 0, &parserData) == UPNPCOMMAND_SUCCESS) + { + int i = 0; + for (auto pm = parserData.l_head; pm != nullptr; pm = pm->l_next) + { + log::printLine(log::server, "[UPnP] {}: {} {}", i, pm->description, pm->externalPort); + i++; + } + FreePortListing(&parserData); + } + } + } +#endif + } if (r != 0) { - serverlog.out("** [UPnP] Failed to forward port %s to %s: ", port.text(), addr.text()); + log::print(log::server, "** [UPnP] Failed to forward port {} to {}: ", port, address); switch (r) { case UPNPCOMMAND_INVALID_ARGS: - serverlog.out("Invalid arguments.\n"); + log::printLine(log::server, "Invalid arguments."); break; case UPNPCOMMAND_HTTP_ERROR: - serverlog.out("HTTP error.\n"); + log::printLine(log::server, "HTTP error."); break; case UPNPCOMMAND_CONFLICTING_MAPPING: - serverlog.out("Port mapping already exists.\n"); + log::printLine(log::server, "Port mapping already exists."); break; default: case UPNPCOMMAND_UNKNOWN_ERROR: - serverlog.out("Unknown error.\n"); + log::printLine(log::server, "Unknown error."); break; } } else { - m_server->getServerLog().out(":: [UPnP] Forwarded port %s to %s.\n", port.text(), addr.text()); - m_portsForwarded.insert(port); + log::printLine(log::server, "[UPnP] Forwarded port {} to {}.", port, address); + m_portsForwarded.emplace(port); } +#endif } -void UPNP::removePortForward(const CString& port) +void UPNP::removePortForward(std::string_view port) { +#ifdef ENABLE_UPNP if (m_urls.controlURL == 0 || m_urls.controlURL[0] == '\0') return; - UPNP_DeletePortMapping(m_urls.controlURL, m_data.first.servicetype, port.text(), "TCP", 0); - m_server->getServerLog().out(":: [UPnP] Removing forward on port %s.\n", port.text()); - m_portsForwarded.erase(port); -} + UPNP_DeletePortMapping(m_urls.controlURL, m_data.first.servicetype, port.data(), "TCP", 0); + log::printLine(log::server, "[UPnP] Removing forward on port {}.", port); + m_portsForwarded.erase(std::string{ port }); #endif +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/misc/WordFilter.cpp b/server/src/misc/WordFilter.cpp index fc223d83d..de2e3d0dd 100644 --- a/server/src/misc/WordFilter.cpp +++ b/server/src/misc/WordFilter.cpp @@ -1,10 +1,19 @@ -#include -#include +#include +#include +#include + +#include #include -#include "Player.h" -#include "Server.h" -#include "misc/WordFilter.h" +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// char bypass[] = { ' ', @@ -29,7 +38,7 @@ static char toLower(char c) return c; } -static char toUpper(char c) +[[maybe_unused]] static char toUpper(char c) { if (c >= 97 && c <= 122) return c - 32; @@ -164,7 +173,7 @@ int WordFilter::apply(const Player* player, CString& chat, int check) std::vector wordsFound; int actionsFound = 0; - for (WordFilterRulePtr& rule: m_rules) + for (WordFilterRulePtr& rule : m_rules) { // Check if we should use this rule. if ((check & rule->check) == 0) continue; @@ -253,7 +262,7 @@ int WordFilter::apply(const Player* player, CString& chat, int check) if (wordpos + chatpos == wordStart) { bool found = false; - for (int b = 0; b < sizeof(bypass); ++b) + for (size_t b = 0; b < sizeof(bypass); ++b) { if (chat[wordpos + chatpos] == bypass[b]) { @@ -269,7 +278,7 @@ int WordFilter::apply(const Player* player, CString& chat, int check) while (true) { bool found = false; - for (int b = 0; b < sizeof(bypass); ++b) + for (size_t b = 0; b < sizeof(bypass); ++b) { if (chat[wordpos + chatpos] == bypass[b]) { @@ -356,10 +365,7 @@ int WordFilter::apply(const Player* player, CString& chat, int check) // Apply an action based on the word. if (actionsFound & FILTER_ACTION_LOG) { - CLog wordfilter; - wordfilter.setFilename(m_server->getServerPath() << "logs/serverlog.txt"); - wordfilter.setEnabled(true); - wordfilter.out("[Word Filter] Player %s was caught using these words: %s\n", player->getAccountName().text(), badwords.text()); + log::printLine(log::server, "[Word Filter] Player {} was caught using these words: {}", player->account.name, badwords); } // Graal doesn't implement. Should we? @@ -375,7 +381,7 @@ int WordFilter::apply(const Player* player, CString& chat, int check) // Tell RC what happened. if (m_showWordsToRC || actionsFound & FILTER_ACTION_TELLRC) { - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Word Filter: Player " << player->getAccountName() << " was caught using these words: " << badwords); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Word Filter: Player " << player->account.name << " was caught using these words: " << badwords); } // If it is a warning rule, we are altering the message. @@ -391,3 +397,6 @@ int WordFilter::apply(const Player* player, CString& chat, int check) return actionsFound; } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/npcserver/NPCServer.cpp b/server/src/npcserver/NPCServer.cpp new file mode 100644 index 000000000..5c6a4bdd2 --- /dev/null +++ b/server/src/npcserver/NPCServer.cpp @@ -0,0 +1,739 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +void NPCServer::initialize() +{ + // TODO(Nalin): This needs to be an option somewhere. + scripting.defaultScriptEngine = "GS1"; + + // NC options. + m_ncHost = string::toLower(m_server->getAdminSettings().get("ns_ip").value_or("auto")); + m_ncPort = m_server->getSettings().get("serverport").value_or(14900); + if (m_ncHost == "auto") + m_ncHost = m_server->getServerList().getServerIP(); + + // Make the NPC server player. + m_npcServerPlayer = std::make_shared(nullptr, NPCServerPlayerID); + m_npcServerPlayer->setType(PLTYPE_NPCSERVER); + + auto& settings = m_server->getSettings(); + auto& account = m_npcServerPlayer->account; + + // TODO(Nalin): The settings manager sees `NICK ` nodes as valid, so it doesn't get a default! We need to redo settings. + auto nickname = settings.get("nickname").value_or("NPC-Server"); + if (nickname.empty()) + nickname = "NPC-Server"; + + // Load the npc-server account. + m_server->getAccountLoader().loadAccount("(npcserver)", account); + account.character.headImage = settings.get("staffhead").value_or("head25.png"); + account.character.nickName = std::format("{} (Server)", nickname); + account.level = ""; + m_npcServerPlayer->setLoaded(true); + + // Add the npc-server player to the player list. + m_server->addPlayer(m_npcServerPlayer, NPCServerPlayerID); + + // Load the GS1 and GS2 engines. + // They must always be loaded as the client will only accept GS1 or GS2 scripts. + scripting.registerScriptEngine("GS1", std::make_shared()); + scripting.registerScriptEngine("GS2", std::make_shared()); + + log::printLine(log::server, "Loading classes..."); + loadClasses(); + + log::printLine(log::server, "Loading Database NPCs..."); + loadDatabaseNPCs(); + + m_runTimeout.callbackDuration = std::bind(&NPCServer::run, this, std::placeholders::_1); + m_timedSave.callbackDuration = std::bind(&NPCServer::saveNPCs, this); + + // TODO(Nalin): Need an event system and this should be called after the Server sends an "all done loading" event. + m_runTimeout.start(); + m_timedSave.start(); + + // If we don't sleep, unset the first NPC save flag. + // We won't run into the problem where we immediately save on server start. + if (!m_server->cached.sleepWhenNoPlayers.getValue()) + m_firstNPCSave = false; +} + +void NPCServer::setRemoteIp(std::string_view host) +{ + if (string::equalsi(m_server->getAdminSettings().get("ns_ip").value_or("auto"), "auto"sv)) + m_ncHost = host; +} + +void NPCServer::sendNCLoginToPlayer(std::shared_ptr player) +{ + // RC's only! + if (!player->isRC() || !player->account.hasRight(PLPERM_NPCCONTROL)) + return; + + // Grab NPCServer & Send + // If the player is connecting from the same IP as the NPC server, use that IP. + std::string connectString = std::format("{},{}", (player->account.ipAddress == m_npcServerPlayer->getSocket()->getLocalIp() ? player->account.ipAddress : m_ncHost), m_ncPort); + log::printLine(log::server, "-- Sending NPC-Server connection info to '{}': {}", player->account.name, connectString); + + player->sendPacket(CString() >> (char)PLO_NPCSERVERADDR >> (short)m_npcServerPlayer->getId() << connectString); +} + +//---------------------------- + +void NPCServer::update(TimeoutGenerator::time_point currentTime) +{ + // If we are sleeping, don't process updates. + if (m_sleeping) + { + // Update the timeouts so they don't have huge deltas when we wake up. + m_runTimeout.setLastTimeout(currentTime); + m_timedSave.setLastTimeout(currentTime); + + processDeletedNPCs(); + processUnloadedNPCs(); + return; + } + + m_runTimeout.update(currentTime); + m_timedSave.update(currentTime); +} + +void NPCServer::run(TimeoutGenerator::time_delta delta) +{ + //auto profile = log::Profile(log::server, "NPCServer::run"); + m_frameStartTime = clock::now(); + + // Save all NPC mod times and update timeouts. + { + for (auto& [id, npc] : m_server->getNPCList()) + { + npc->recordCurrentPropModTime(); + + // TODO(Nalin): Replace with TimeoutGenerator. + if (npc->timeout.count() != 0) + { + if (delta < npc->timeout) + npc->timeout -= delta; + else + npc->timeout = -1ms; + + if (npc->timeout < std::chrono::milliseconds::zero()) + { + npc->timeout = 0ms; + npc->scripting.events.addEvent(ScriptEventType::TIMEOUT, source::FromNPC(id)); + } + } + } + } + + // Save all player prop mod times. + for (auto& [id, player] : m_playerList) + { + player->recordCurrentPropModTime(); + } + + // Run all weapon scripts. + for (auto& [name, weapon] : m_server->getWeaponList()) + { + // Copy the shared_ptr so if we "destroy" gets called, the weapon isn't immediately deleted while we are running the script. + WeaponPtr copy = weapon; + copy->executeEvents(weapon->scripting.events, source::FromWeapon(weapon)); + } + + // Process all NPC movements. + // Run all NPC scripts. + for (auto& [id, npc] : m_server->getNPCList()) + { + // Process movement. + npc->processMoveQueue(delta); + + // Process scripts. + npc->executeEvents(npc->scripting.events, source::FromNPC(id)); + } + + // Send all changed NPC props. + // Send all queued movements. + { + CString propsPacket; + for (auto& [id, npc] : m_server->getNPCList()) + { + if (auto level = npc->getLevel(); level != nullptr) + { + // Send props packet. + propsPacket.clear(); + propsPacket.writeGChar((char)PLO_NPCPROPS) >> (int)npc->id << npc->getModifiedPropsPacket(); + if (propsPacket.length() > 4) + m_server->sendPacketToNearby(propsPacket, npc->getGlobalPosition(), level); + + // Send movements. + npc->sendMoveQueueUpdatesToLevel(level); + } + } + } + + // Send all changed player props. + { + CString propsPacket; + for (auto& [id, player] : m_playerList) + { + auto playerClient = std::dynamic_pointer_cast(player); + if (playerClient == nullptr) continue; + + propsPacket.clear(); + propsPacket.write(player->getModifiedPropsPacket()); + if (propsPacket.isEmpty()) continue; + + player->sendPacket(CString() >> (char)PLO_PLAYERPROPS << propsPacket); + m_server->sendPacketToNearby(CString() >> (char)PLO_OTHERPLPROPS >> (short)player->getId() << propsPacket, playerClient->getGlobalPosition(), playerClient->getLevel(), { player->getId() }); + } + } + + // Process deleted NPCs and players. + processDeletedNPCs(); + processUnloadedNPCs(); + processDeletedPlayers(); + + // If we have no players, enter sleep mode. + // We do it this way to give the server time to process logouts, and to force an NPC save (since saves will be disabled while sleeping). + if (m_server->cached.sleepWhenNoPlayers.getValue() && m_playerList.empty()) + { + m_sleeping = true; + saveNPCs(); + } +} + +////////////////////////////////////////////////////////////////////////////// + +void NPCServer::loadClasses() +{ + auto indent = log::server.indent(); + + for (auto info : m_server->getFileSystemServer().info(fs::FileCategory::SCRIPTCLASS) | toSharedPtr) + { + if (info == nullptr) continue; + + auto profile = log::Profile(log::server, "", " ({1:0.6} ms)"); + std::string fileName = fs::getANSIFileName(info->file); + std::string className = fileName.substr(0, fileName.length() - 4); + + CString scriptData; + scriptData.load(info->file.string()); + + auto scriptClass = std::make_shared(className, scriptData.text()); + scriptClass->modTime = info->getModTime(); + m_classList[className] = scriptClass; + + log::print(log::server, "{}", className); + } +} + +void NPCServer::loadDatabaseNPCs() +{ + auto indent = log::server.indent(); + + for (auto info : m_server->getFileSystemServer().info(fs::FileCategory::NPC) | toSharedPtr) + { + if (info == nullptr) continue; + + auto profile = log::Profile(log::server, "", " ({1:0.6} ms)"); + if (auto npc = addNPCFromFile(info->file); npc != nullptr) + log::print(log::server, "[{}] {}", npc->id, npc->name); + } +} + +void NPCServer::saveNPCs() +{ + // Avoid saving NPCs immediately after the server starts. + if (m_firstNPCSave) + { + m_firstNPCSave = false; + return; + } + + log::printLine(log::server, "Saving NPCs."); + for (const auto& [npcId, npcPtr] : m_globalNPCList) + { + if (auto npc = npcPtr.lock(); npc != nullptr) + m_server->getNPCLoader().saveNPC(npc); + } +} + +////////////////////////////////////////////////////////////////////////////// + +void NPCServer::playerLogin(std::shared_ptr player) +{ + m_playerList[player->getId()] = player; + m_sleeping = false; +} + +void NPCServer::playerLogout(std::shared_ptr player) +{ + m_deletedPlayers.insert(player); + addEventToControlNPC(ScriptEventType::PLAYERLOGOUT, source::FromPlayer(player->getId())); +} + +void NPCServer::processDeletedPlayers() +{ + for (const auto& player : m_deletedPlayers) + m_playerList.erase(player->getId()); + + m_deletedPlayers.clear(); +} + +////////////////////////////////////////////////////////////////////////////// + +std::weak_ptr NPCServer::getNPCByName(const std::string& name) +{ + for (const auto& [_, npc] : m_globalNPCList) + { + if (npc.lock()->name == name) + return npc; + } + + return {}; +} + +std::shared_ptr NPCServer::addNPC(std::string_view image, std::string_view script, std::shared_ptr level, const TilePosition& location, std::string_view type) +{ + auto npc = m_server->addNPC(image, script, location.x(), location.y(), level, NPCStorageType::DATABASE, true, type); + m_globalNPCList[npc->id] = npc; + return npc; +} + +std::shared_ptr NPCServer::addNPC(std::string_view name, NPCID id, std::string_view type, std::string_view scripter, std::shared_ptr level, const TilePosition& location) +{ + NPCPtr npc = nullptr; + + if (type == NPCTYPE_LOCAL) + npc = std::make_shared(id, NPCStorageType::LEVEL); + else + npc = std::make_shared(id, NPCStorageType::DATABASE); + + auto pixelPosition = toPixelPosition(location); + auto localPixelPosition = toLocalPixelPosition(pixelPosition); + auto mapPosition = toMapPosition(pixelPosition); + + npc->name = name; + npc->level = level->levelName; + npc->setPropWith(SetBy::SERVER, type); + npc->setPropWith(SetBy::SERVER, scripter); + npc->setPropWith(SetBy::SERVER, localPixelPosition.x()); + npc->setPropWith(SetBy::SERVER, localPixelPosition.y()); + + if (level && level->isGmap()) + { + npc->setPropWith(SetBy::SERVER, mapPosition.x()); + npc->setPropWith(SetBy::SERVER, mapPosition.y()); + } + + m_server->addNPC(npc, true); + m_globalNPCList[npc->id] = npc; + + if (type != NPCTYPE_LOCAL) + { + CString props = npc->getPropsPacketFor(); + m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_NC_NPCADD >> (int)npc->id << props); + } + + return npc; +} + +std::shared_ptr NPCServer::addNPCFromFile(const std::filesystem::path& filePath) +{ + auto& npcLoader = m_server->getNPCLoader(); + auto npc = npcLoader.loadNPC(filePath); + if (npc) + { + auto fileName = fs::getANSIFileName(filePath); + auto npcName = fileName.substr(3, fileName.length() - 7); // Remove npc and .txt + + npc->scripting.events.addEvent(ScriptEventType::INITIALIZED, source::FromServer()); + if (npc->scriptType != NPCTYPE_LOCAL) + { + m_globalNPCList[npc->id] = npc; + + CString props = npc->getPropsPacketFor(); + m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_NC_NPCADD >> (int)npc->id << props); + } + } + return npc; +} + +void NPCServer::deleteNPC(NPCID id) +{ + m_deletedNPCs.insert(id); +} + +void NPCServer::unloadNPC(NPCID id) +{ + m_unloadedNPCs.insert(id); +} + +void NPCServer::processDeletedNPCs() +{ + if (m_deletedNPCs.empty()) + return; + + for (const auto& npcId : m_deletedNPCs) + { + auto npc = m_server->getNPC(npcId); + + // Remove from the global list. + m_globalNPCList.erase(npcId); + + // Remove from the server's NPC list. + m_server->deleteNPC(npcId, true); + + // Delete the NPC from the filesystem. + if (npc != nullptr) + std::filesystem::remove(std::filesystem::path{ "npcs" } / fs::getHTMLEscapedFileName(std::format("npc{}.txt", npc->name))); + } + m_deletedNPCs.clear(); +} + +void NPCServer::processUnloadedNPCs() +{ + if (m_unloadedNPCs.empty()) + return; + + auto& npcLoader = m_server->getNPCLoader(); + for (const auto& npcId : m_unloadedNPCs) + { + auto npc = m_server->getNPC(npcId); + + // Don't remove database NPCs. + // TODO: Make it so database NPCs can be loaded/unloaded on demand. + if (npc != nullptr && npc->storageType == NPCStorageType::DATABASE) + { + npcLoader.saveNPC(npc); + + // If the level no longer exists, stub the level so this NPC works again once it comes back. + if (npc->getLevel() == nullptr) + { + if (auto level = m_server->getStubbedLevel(npc->level, npc->groupName); level != nullptr) + level->addNPC(npc); + } + continue; + } + + // Remove from the global list. + m_globalNPCList.erase(npcId); + + // Remove from the server's NPC list. + m_server->deleteNPC(npcId, true); + } + m_unloadedNPCs.clear(); +} + +//---------------------------- + +bool NPCServer::hasClass(std::string_view name) const +{ + return m_classList.find(name) != m_classList.end(); +} + +std::weak_ptr NPCServer::getClass(std::string_view name) const +{ + auto classIter = m_classList.find(name); + if (classIter == m_classList.end()) + return {}; + + return classIter->second; +} + +bool NPCServer::deleteClass(std::string_view className) +{ + auto classIter = m_classList.find(className); + if (classIter == m_classList.end()) + return false; + + m_classList.erase(classIter); + std::filesystem::remove(std::filesystem::path{ "scripts" } / std::format("{}.txt", className)); + + // TODO: Send blank class? + + return true; +} + +std::shared_ptr NPCServer::addClass(std::string_view className, std::string_view classCode) +{ + auto file = m_server->getFileSystemServer().openiForWriting(fs::FileCategory::SCRIPTCLASS, std::format("{}.txt", className), true); + if (!file) return nullptr; + + const auto& filePath = file->filePath(); + file->clear(); + file->write(classCode); + file->close(); + + auto scriptClass = std::make_shared(className, classCode); + scriptClass->modTime = fs::getFileModTime(filePath); + m_classList[std::string{ className }] = scriptClass; + + m_server->updateClassForPlayers(scriptClass); + return scriptClass; +} + +std::shared_ptr NPCServer::loadClass(const std::filesystem::path& filePath) +{ + CString fileData; + fileData.load(filePath.string()); + + auto className = filePath.stem().string(); + auto scriptClass = std::make_shared(className, fileData.toStringView()); + scriptClass->modTime = fs::getFileModTime(filePath); + m_classList[std::string{ className }] = scriptClass; + + m_server->updateClassForPlayers(scriptClass); + return scriptClass; +} + +void NPCServer::updateClass(std::string_view className, std::string_view classCode) +{ + auto it = m_classList.find(className); + if (it == m_classList.end()) + return; + + auto& scriptClass = it->second; + scriptClass->setScript(classCode); + + auto file = m_server->getFileSystemServer().openiForWriting(fs::FileCategory::SCRIPTCLASS, std::format("{}.txt", className), true); + if (!file) return; + + const auto& filePath = file->filePath(); + file->clear(); + file->write(classCode); + file->close(); + + scriptClass->modTime = fs::getFileModTime(filePath); + + m_server->updateClassForPlayers(scriptClass); +} + +//---------------------------- + +void NPCServer::showImage(std::shared_ptr npc, uint8_t index, const PixelPosition& position, std::string_view image) const +{ + if (index > 199) + return; + + auto level = npc->getLevel(); + if (level == nullptr) + return; + + auto showimg = ShowImg::ConstructImage(m_frameStartTime, position, image); + m_server->sendPacketToNearby(CString() >> (char)PLO_SHOWIMGNPC >> (int)npc->id >> (char)(index + 10) << showimg.getAllPropsPacket(), npc->getGlobalPosition(), level); + npc->showImgList[index] = std::move(showimg); +} + +void NPCServer::showText(std::shared_ptr npc, uint8_t index, const PixelPosition& position, std::string_view text, std::string_view font, std::string_view style) const +{ + if (index > 199 || text.empty()) + return; + + auto level = npc->getLevel(); + if (level == nullptr) + return; + + auto showimg = ShowImg::ConstructText(m_frameStartTime, position, text, font, style); + m_server->sendPacketToNearby(CString() >> (char)PLO_SHOWIMGNPC >> (int)npc->id >> (char)(index + 10) << showimg.getAllPropsPacket(), npc->getGlobalPosition(), level); + npc->showImgList[index] = std::move(showimg); +} + +void NPCServer::showGani(std::shared_ptr npc, uint8_t index, const PixelPosition& position, std::string_view animation, uint8_t direction) const +{ + if (index > 199) + return; + + auto level = npc->getLevel(); + if (level == nullptr) + return; + + auto showimg = ShowImg::ConstructGani(m_frameStartTime, position, animation, direction); + m_server->sendPacketToNearby(CString() >> (char)PLO_SHOWIMGNPC >> (int)npc->id >> (char)(index + 10) << showimg.getAllPropsPacket(), npc->getGlobalPosition(), level); + npc->showImgList[index] = std::move(showimg); +} + +void NPCServer::showPoly(std::shared_ptr npc, uint8_t index, const std::vector& points) const +{ + if (index > 199) + return; + + auto level = npc->getLevel(); + if (level == nullptr) + return; + + auto showimg = ShowImg::ConstructPoly(m_frameStartTime, points); + m_server->sendPacketToNearby(CString() >> (char)PLO_SHOWIMGNPC >> (int)npc->id >> (char)(index + 10) << showimg.getAllPropsPacket(), npc->getGlobalPosition(), level); + npc->showImgList[index] = std::move(showimg); +} + +void NPCServer::changeShowImgColors(std::shared_ptr npc, uint8_t index, float red, float green, float blue, float alpha) const +{ + if (index > 199) + return; + + auto level = npc->getLevel(); + if (level == nullptr) + return; + + auto iter = npc->showImgList.find(index); + if (iter == std::end(npc->showImgList)) + return; + + ShowImg& showimg = iter->second; + showimg.colors[0] = std::clamp(red, 0.0f, 1.0f); + showimg.colors[1] = std::clamp(green, 0.0f, 1.0f); + showimg.colors[2] = std::clamp(blue, 0.0f, 1.0f); + showimg.colors[3] = std::clamp(alpha, 0.0f, 1.0f); + showimg.modTime[PROPID(ShowImgProp::COLORS)] = m_frameStartTime; + + m_server->sendPacketToNearby(CString() >> (char)PLO_SHOWIMGNPC >> (int)npc->id >> (char)(index + 10) << showimg.getAllPropsPacket(m_frameStartTime), npc->getGlobalPosition(), level); +} + +void NPCServer::changeShowImgMode(std::shared_ptr npc, uint8_t index, uint8_t drawMode) const +{ + if (index > 199) + return; + + auto level = npc->getLevel(); + if (level == nullptr) + return; + + auto iter = npc->showImgList.find(index); + if (iter == std::end(npc->showImgList)) + return; + + ShowImg& showimg = iter->second; + showimg.drawMode = drawMode; + showimg.modTime[PROPID(ShowImgProp::DRAWMODE)] = m_frameStartTime; + + m_server->sendPacketToNearby(CString() >> (char)PLO_SHOWIMGNPC >> (int)npc->id >> (char)(index + 10) << showimg.getAllPropsPacket(m_frameStartTime), npc->getGlobalPosition(), level); +} + +void NPCServer::changeShowImgPart(std::shared_ptr npc, uint8_t index, const ImagePartRectangle& imagePart) const +{ + if (index > 199) + return; + + auto level = npc->getLevel(); + if (level == nullptr) + return; + + auto iter = npc->showImgList.find(index); + if (iter == std::end(npc->showImgList)) + return; + + ShowImg& showimg = iter->second; + showimg.imagePart = imagePart; + showimg.modTime[PROPID(ShowImgProp::IMAGEPART)] = m_frameStartTime; + + m_server->sendPacketToNearby(CString() >> (char)PLO_SHOWIMGNPC >> (int)npc->id >> (char)(index + 10) << showimg.getAllPropsPacket(m_frameStartTime), npc->getGlobalPosition(), level); +} + +void NPCServer::changeShowImgLayer(std::shared_ptr npc, uint8_t index, uint8_t layer) const +{ + if (index > 199) + return; + + auto level = npc->getLevel(); + if (level == nullptr) + return; + + auto iter = npc->showImgList.find(index); + if (iter == std::end(npc->showImgList)) + return; + + ShowImg& showimg = iter->second; + showimg.layer = layer; + showimg.modTime[PROPID(ShowImgProp::LAYER)] = m_frameStartTime; + + m_server->sendPacketToNearby(CString() >> (char)PLO_SHOWIMGNPC >> (int)npc->id >> (char)(index + 10) << showimg.getAllPropsPacket(m_frameStartTime), npc->getGlobalPosition(), level); +} + +void NPCServer::changeShowImgZoom(std::shared_ptr npc, uint8_t index, float zoom) const +{ + if (index > 199) + return; + + auto level = npc->getLevel(); + if (level == nullptr) + return; + + auto iter = npc->showImgList.find(index); + if (iter == std::end(npc->showImgList)) + return; + + ShowImg& showimg = iter->second; + showimg.zoom = std::clamp(zoom, 0.0f, 22.0f); + showimg.modTime[PROPID(ShowImgProp::ZOOM)] = m_frameStartTime; + + m_server->sendPacketToNearby(CString() >> (char)PLO_SHOWIMGNPC >> (int)npc->id >> (char)(index + 10) << showimg.getAllPropsPacket(m_frameStartTime), npc->getGlobalPosition(), level); +} + +void NPCServer::hideImages(std::shared_ptr npc, uint8_t index, std::optional endIndex) const +{ + if (index > 199) + return; + + for (uint8_t i = index; i <= endIndex.value_or(index); ++i) + npc->showImgList.erase(i); + + npc->sendAllShowImagesToLevel(); +} + +/////////////////////////////////////////////////////////////////////////////// + +tileset::TileType NPCServer::getTileType(uint16_t tile, std::shared_ptr level) const noexcept +{ + auto tilesetType = m_server->getTilesetTypeForLevel(level); + return m_server->getTileTypeForTile(tilesetType, tile); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/npcserver/PlayerNPCServer.cpp b/server/src/npcserver/PlayerNPCServer.cpp new file mode 100644 index 000000000..58c44e895 --- /dev/null +++ b/server/src/npcserver/PlayerNPCServer.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +HandlePacketResult PlayerNPCServer::handlePacket(std::optional id, CString& packet) +{ + return HandlePacketResult::Failed; +} + +/////////////////////////////////////////////////////////////////////////////// + +PlayerNPCServer::PlayerNPCServer(CSocket* pSocket, PlayerID pId) + : Player(pSocket, pId) +{ +} + +PlayerNPCServer::~PlayerNPCServer() +{ +} + +/////////////////////////////////////////////////////////////////////////////// + +bool PlayerNPCServer::onRecv() +{ + return false; +} + +void PlayerNPCServer::onUnregister() +{ +} + +/////////////////////////////////////////////////////////////////////////////// + +void PlayerNPCServer::sendPrivateMessage(PlayerID from, std::string_view message) +{ + if (auto player = m_server->getPlayer(from); player != nullptr) + { + if (!privateMessage.empty()) + player->sendPacket(CString() >> (char)PLO_PRIVATEMESSAGE >> (short)m_id << player->translate(privateMessage)); + + m_server->getNPCServer()->addEventToControlNPC(ScriptEventType::PRIVATEMESSAGE, source::FromPlayer(from), "pm"s, player->account.name, std::string{ message }); + } +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/object/NPC.cpp b/server/src/object/NPC.cpp new file mode 100644 index 000000000..2ee9b0df0 --- /dev/null +++ b/server/src/object/NPC.cpp @@ -0,0 +1,2128 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +//////////////////////////////////////////////////////////////////////////////// + +static constexpr std::array savePackets = { 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 }; + +static std::string_view toWeaponName(std::string_view code); + +static bool canSendProp(NPCProp prop) +{ + static Server* server = nullptr; + if (server == nullptr) + server = BabyDI::Get(); + + if (server->Generation == ServerGeneration::ORIGINAL && PROPID(prop) > PROPID(NPCProp::BODYIMAGE)) + return false; + if (prop == NPCProp::SCRIPTER || prop == NPCProp::NAME || prop == NPCProp::TYPE) + return false; + if (prop == NPCProp::CLASS && (server->Generation == ServerGeneration::ORIGINAL || server->Generation == ServerGeneration::CLASSIC)) + return false; + + return true; +} + +//---------------------------- + +#ifdef PACKETLOGGING +#define DO_PACKETLOG(LOG) LOG +#else +#define DO_PACKETLOG(LOG) +#endif + +#define PRINT_NPCPROP(prop, ...) #prop ##sv, +constexpr std::array npcPropNames = +{ + FOR_LIST_OF_NPC_PROPS(PRINT_NPCPROP) +}; + +//---------------------------- + +NPC::NPC(NPCID id, NPCStorageType storageType) + : id(id), storageType(storageType), m_savedModTime() +{ + m_server = BabyDI::Get(); + assert(m_server != nullptr); + resetToInitialState(); +} + +NPC::~NPC() +{ +#ifdef DEBUG + log::printLine(log::server, "Destroying NPC [{}] '{}' in level '{}'.", id, name, level); +#endif +} + +//---------------------------- + +void NPC::resetToInitialState() +{ + groupName.clear(); + image = m_initialImage; + shape = {}; + imagePart = {}; + visFlags = PROPID(NPCVisFlags::VISIBLE); + blockFlags = 0; + hurtX = 0.0f; + hurtY = 0.0f; + noPlayerOnWall = false; + timeout = 0ms; + m_initialCharacter.nickName.clear(); + character = m_initialCharacter; + saves.fill(0); + + warpRestrictions = m_server->hasNPCServer() ? NPCWarpRestrictions::NOTALLOWED : NPCWarpRestrictions::ALLOWED; + + // We need to alter the modTime of the following props as they should be always sent. + // If we don't, they won't be sent until the prop gets modified. + auto props = std::to_array({ NPCProp::IMAGE, NPCProp::SCRIPT, NPCProp::X, NPCProp::Y, NPCProp::Z, NPCProp::VISFLAGS, NPCProp::ID, NPCProp::SPRITE, NPCProp::MESSAGE, NPCProp::X2, NPCProp::Y2, NPCProp::Z2 }); + std::ranges::for_each(props, [this, now = m_server->getFrameStartTime()](const NPCProp& prop) { modTime[PROPID(prop)] = now; }); + + m_savedModTime = modTime; + lastUpdateTime = m_server->getFrameStartTime(); + + // Clear the variables and queues. + scripting.variables.store.clear(); + moveQueue.clear(); + + // Warp. + if (auto initialLevel = m_server->getStubbedLevel(m_initialLevel); initialLevel != nullptr) + warp(initialLevel, character.getLocalPosition()); +} + +//---------------------------- + +bool NPC::warp(LevelPtr level, const PixelPosition& position) +{ + if (level == nullptr) + return false; + + std::inplace_vector warpResults; + auto localPosition = toLocalPixelPosition(position); + auto mapPosition = toMapPosition(position); + + // Clear the move queue since we are being forcibly moved. + moveQueue.clear(); + + // Set our new position. + warpResults.push_back(setPropWith(SetBy::SERVER, localPosition.x())); + warpResults.push_back(setPropWith(SetBy::SERVER, localPosition.y())); + + // If the level is a gmap, include the map position. + if (level->isGmap()) + { + warpResults.push_back(setPropWith(SetBy::SERVER, mapPosition.x())); + warpResults.push_back(setPropWith(SetBy::SERVER, mapPosition.y())); + } + + // If we are moving levels, change the current level. + // Do this last so our current position is passed on the warp. + if (level != m_currentLevel.lock()) + warpResults.push_back(setPropWith(SetBy::SERVER, level->levelName)); + + sendPropsFromResults(warpResults); + + return true; +} + +void NPC::setLevel(LevelPtr level) +{ + if (level == nullptr) + return; + + // Refresh our mod times. + refreshModTimes(m_server->getFrameStartTime()); + + this->level = level->levelName; + m_currentLevel = level; +} + +//---------------------------- + +CString NPC::getShowImagesPacket(std::optional modTime) const noexcept +{ + // Construct the packet. + // Index 9 will cause all of the showimgs to be erased on the client. + CString packet; + packet >> (char)PLO_SHOWIMGNPC >> (int)id >> (char)9; + + // Send all the showimgs. + for (const auto& [id, showimg] : showImgList) + packet >> (char)(id + 10) << showimg.getAllPropsPacket(modTime); + + return packet; +} + +void NPC::sendShowImagesToPlayer(PlayerPtr player, std::optional modTime) const noexcept +{ + // Only start sending showimg packets when the NPC gains showimgs. + if (!m_hadShowImgs && showImgList.size() == 0) + return; + + m_hadShowImgs = true; + + player->sendPacket(getShowImagesPacket(modTime)); +} + +void NPC::sendAllShowImagesToLevel(std::optional modTime) const noexcept +{ + // Only start sending showimg packets when the NPC gains showimgs. + if (!m_hadShowImgs && showImgList.size() == 0) + return; + + m_server->sendPacketToNearby(getShowImagesPacket(modTime), getGlobalPosition(), getLevel()); +} + +//---------------------------- + +void NPC::addMoveToQueue(const LocalPixelPosition& moveDelta, float durationInSeconds, uint8_t options) +{ + NPCMove move{ .duration = std::chrono::duration_cast(duration_seconds_double{ durationInSeconds }), .modTime = m_server->getFrameStartTime() }; + + if (options & (1 << NPCMove::cacheNearbyMovement)) + move.options.set(NPCMove::cacheNearbyMovement); + if (options & (1 << NPCMove::appendMovement)) + move.options.set(NPCMove::appendMovement); + if (options & (1 << NPCMove::blockCheck)) + move.options.set(NPCMove::blockCheck); + if (options & (1 << NPCMove::informWhenDone)) + move.options.set(NPCMove::informWhenDone); + if (options & (1 << NPCMove::applyDirection)) + move.options.set(NPCMove::applyDirection); + + // Determine our start and stop positions. + move.origin = moveQueue.empty() ? getGlobalPosition() : moveQueue.back().destination; + move.destination = translatePosition(move.origin, moveDelta); + + bool finishAllMovements = false; + + // If we are not caching or appending movement, and we have some in the queue, finish the queue. + if (!move.options.test(NPCMove::cacheNearbyMovement) && !move.options.test(NPCMove::appendMovement) && !moveQueue.empty()) + finishAllMovements = true; + else if (move.options.test(NPCMove::cacheNearbyMovement) && !moveQueue.empty()) + { + // If the distance to go from the current position to the end of our new movement is over 5, + // finish all the movements. + auto currentTilePosition = getTilePosition(); + auto destinationTilePosition = toTilePosition(move.destination); + auto distance = std::hypot(destinationTilePosition.x() - currentTilePosition.x(), destinationTilePosition.y() - currentTilePosition.y()); + finishAllMovements = distance > 5.0f; + } + + // If we are clearing the movement queue, pop all but the last movement in the queue + // and execute the last movement to the end (so any events get called). + if (finishAllMovements) + { + while (moveQueue.size() > 1) + { + auto& queue = moveQueue.front(); + if (queue.onComplete) + queue.onComplete(); + moveQueue.pop_front(); + } + if (!moveQueue.empty()) + processMoveQueue(moveQueue.front().duration); + } + + moveQueue.push_back(std::move(move)); +} + +void NPC::processMoveQueue(std::chrono::milliseconds deltaTime) +{ + if (moveQueue.empty()) + return; + + while (deltaTime != 0ms && !moveQueue.empty()) + { + NPCMove& move = moveQueue.front(); + + // If the move hasn't started yet, do the starting events. + if (move.elapsed == 0ms) + { + // Set the direction when moving. + if (move.options.test(NPCMove::applyDirection) && isCharacter()) + { + uint8_t dir = 0; + if (move.destination.x() > move.origin.x()) + dir = 3; + if (move.destination.y() > move.origin.y()) + dir = 2; + if (move.destination.x() < move.origin.x()) + dir = 1; + + setPropWith(SetBy::SERVER, character.sprite, dir); + } + } + + // Calculate our times. + auto timeRemaining = 0ms; + move.elapsed += deltaTime; + if (move.elapsed < move.duration) + { + // The duration was fully used up. + deltaTime = 0ms; + timeRemaining = move.duration - move.elapsed; + } + else + { + // We reached the end, so some duration is still remaining. + deltaTime = move.elapsed - move.duration; + move.elapsed = move.duration; + } + + // Determine where we will end up this frame. + PixelPosition currentPosition{ move.getCurrentPosition() }; + + // If the map position changed, set that now. + const auto& [mapX, mapY, _] = toMapPosition(currentPosition); + if (mapX != character.mapX) + setPropWith(SetBy::SERVER, mapX); + if (mapY != character.mapY) + setPropWith(SetBy::SERVER, mapY); + + // Set the new position. + auto localPosition = toLocalPixelPosition(currentPosition); + auto sendX = setPropWith(SetBy::SERVER, localPosition.x()); + auto sendY = setPropWith(SetBy::SERVER, localPosition.y()); + + // Adjust our saved mod times, just in case. + // We don't want the position to be accidentally sent. + m_savedModTime[PROPID(NPCProp::X)] = modTime[PROPID(NPCProp::X)]; + m_savedModTime[PROPID(NPCProp::Y)] = modTime[PROPID(NPCProp::Y)]; + m_savedModTime[PROPID(NPCProp::X2)] = modTime[PROPID(NPCProp::X2)]; + m_savedModTime[PROPID(NPCProp::Y2)] = modTime[PROPID(NPCProp::Y2)]; + + bool movementFinished = false; + + // If we are testing for walls, do that now. + if (move.options.test(NPCMove::blockCheck)) + { + if (auto levelPtr = getLevel(); levelPtr != nullptr) + { + // Do an onwall check at the destination. + // If we collide, then stop the movement. + auto boundingBox = getCollisionBoundingBox(); + boundingBox.position = currentPosition; + + // Fix offsets for characters. + if (isCharacter() && (shape.width() == 0 || shape.height() == 0)) + boundingBox.position.translate(8, 16); + + // Check for wall collision. + bool isOnWall = levelPtr->isOnWall2(boundingBox); + if (isOnWall) + movementFinished = true; + } + } + + // Check if our movement is done. + if (timeRemaining <= 0ms) + movementFinished = true; + + // If the movement is finished, terminate! + if (movementFinished) + { + // Queue the movement finished event. + if (move.options.test(NPCMove::informWhenDone)) + scripting.events.addEvent(ScriptEventType::MOVEMENTFINISHED, source::FromNPC(id)); + + // Finish callback. + if (move.onComplete) + move.onComplete(); + + // Pop the front movement. + moveQueue.pop_front(); + } + } +} + +std::pair NPC::getMoveQueuePacketData(std::optional modTime) const noexcept +{ + if (moveQueue.empty()) + return {}; + + std::pair result; + + // Append the whole move queue to the move packet. + for (const auto& move : moveQueue) + { + // Only send newer movements. + if (modTime.has_value() && move.modTime < modTime.value()) + continue; + + auto durationLeftInSeconds = std::chrono::duration_cast(move.duration - move.elapsed); + auto timeIn50msIncrements = static_cast(durationLeftInSeconds.count() / 0.05f); + + auto currentPosition = move.getCurrentPosition(); + auto dx = static_cast(move.destination.x() - currentPosition.x()); + auto dy = static_cast(move.destination.y() - currentPosition.y()); + auto localPosition = toLocalPixelPosition(currentPosition); + + // Client versions 2.3+ support the new move packet. + { + PropertyPixelCoordinate posX{ localPosition.x() }; + PropertyPixelCoordinate posY{ localPosition.y() }; + PropertyPixelCoordinate moveDX{ dx }; + PropertyPixelCoordinate moveDY{ dy }; + + result.second << posX.serialize() << posY.serialize(); + result.second << moveDX.serialize() << moveDY.serialize(); + result.second >> (short)timeIn50msIncrements; + result.second >> (char)move.options.to_ulong(); + } + { + uint8_t posX = static_cast(localPosition.x() / 8.0f); + uint8_t posY = static_cast(localPosition.y() / 8.0f); + auto moveDX = static_cast((dx / 8) + 100); + auto moveDY = static_cast((dy / 8) + 100); + + result.first >> (char)posX >> (char)posY; + result.first >> (char)moveDX >> (char)moveDY; + result.first >> (short)timeIn50msIncrements; + result.first >> (char)move.options.to_ulong(); + } + } + + return result; +} + +void NPC::sendMoveQueueToPlayer(PlayerPtr player, std::optional modTime) const noexcept +{ + if (moveQueue.empty()) + return; + + auto [move1, move2] = getMoveQueuePacketData(modTime); + if (move1.isEmpty()) + return; + + if (player->getVersion() < CLVER_2_3) + player->sendPacket(CString() >> (char)PLO_MOVE >> (int)id << move1); + else + player->sendPacket(CString() >> (char)PLO_MOVE2 >> (int)id << move2); +} + +void NPC::sendMoveQueueToLevel(LevelPtr level, std::optional modTime) const noexcept +{ + if (moveQueue.empty()) + return; + + sendMoveQueueToLevel(level, getMoveQueuePacketData(modTime)); +} + +void NPC::sendMoveQueueToLevel(LevelPtr level, const std::pair& queue) const noexcept +{ + if (queue.first.isEmpty()) + return; + + // Send them out. + m_server->sendPacketToNearby(CString() >> (char)PLO_MOVE2 >> (int)id << queue.second, character.getGlobalPosition(), level, {}, [](const Player* player) { return player->getVersion() >= CLVER_2_3; }); + m_server->sendPacketToNearby(CString() >> (char)PLO_MOVE >> (int)id << queue.first, character.getGlobalPosition(), level, {}, [](const Player* player) { return player->getVersion() < CLVER_2_3; }); +} + +void NPC::sendMoveQueueUpdatesToLevel(LevelPtr level) noexcept +{ + auto result = getMoveQueuePacketData(lastMoveQueueSentTime); + lastMoveQueueSentTime = m_server->getFrameStartTime(); + sendMoveQueueToLevel(level, result); +} + +void NPC::refreshModTimes(clock::time_point modTime) noexcept +{ + for (auto& time : this->modTime) + { + if (time.has_value()) + time = modTime; + } +} + +//---------------------------- + +double NPC::getCalculatedTileZ() const noexcept +{ + auto level = getLevel(); + if (level == nullptr || !level->hasTerrain()) + return character.localPixelZ / 16.0; + + PixelPosition testPosition = character.getGlobalPosition(); + if (isCharacter()) + testPosition.translate(24, 48); + + auto terrainHeight = level->getHeightAt(testPosition); + auto currentZ = character.localPixelZ / 16.0; + return std::max(terrainHeight, currentZ); +} + +//---------------------------- + +std::string NPC::getLevelName() const +{ + if (auto levelPtr = getLevel(); levelPtr != nullptr) + return levelPtr->levelName; + + return level; +} + +std::shared_ptr NPC::getLevel() const +{ + // If we are a control-NPC, our level constantly changes, so don't rely on our pointer. + if (scriptType == NPCTYPE_CONTROL) + return m_server->getLoadedLevelNoHint(level); + + return m_currentLevel.lock(); +} + +//---------------------------- + +void NPC::hurt(int8_t damageInHalves, std::optional damageEventType, std::optional source) +{ + // Adjust the NPC's HP. + if (allowServerDamageReactions && isCharacter()) + { + sendPropsFromResults( + setPropWith(SetBy::SERVER, static_cast(std::max(0, character.hitpointsInHalves - damageInHalves))) + ); + } + + // Queue the hurt event. + if (damageEventType.has_value()) + scripting.events.addEvent(damageEventType.value(), source.value_or(source::FromServer())); +} + +void NPC::hurtAndPush(int8_t damageInHalves, const PixelPosition& pushOrigin, std::optional damageEventType, std::optional source) +{ + if (allowServerDamageReactions && isCharacter()) + { + // Become invulerable for 1.6 seconds. + if (timeDifference(character.lastHurtTime, m_server->getFrameStartTime()) < 1600ms) + return; + + // Push the character away from the source of damage. + auto tileOrigin = toTilePosition(pushOrigin); + TilePosition pushVector{ character.getTilePosition().x() + 1.5f - tileOrigin.x(), character.getTilePosition().y() + 2.0f - tileOrigin.y() }; + pushVector.normalize2D(pushVector.length2D()); + pushVector = pushVector * 5.0f; + + // Set the hurt animation and force an X/Y prop update (to cancel any current movements), then clear the move queue. + // This will let us abort any movements in progress. + std::inplace_vector results; + results.push_back(setPropWith(SetBy::SERVER, character.localPixelX)); + results.push_back(setPropWith(SetBy::SERVER, character.localPixelY)); + results.push_back(setPropWith(SetBy::SERVER, "hurt")); + moveQueue.clear(); + + // Add our new movement to the queue and send it out. + addMoveToQueue(toLocalPixelPosition(pushVector), 0.5, ENUM(NPCMoveFlags::BLOCKCHECK)); + sendMoveQueueUpdatesToLevel(getLevel()); + + // Set up a task to fix the animation. + m_server->scheduleTask(500ms, [self = m_server->getNPC(id)]() + { + if (self != nullptr && self->character.gani == "hurt") + self->sendPropsFromResults(self->setPropWith(SetBy::SERVER, "idle")); + }); + + // Send prop changes. + sendPropsFromResults(results); + + // Set the last hurt time. + character.lastHurtTime = m_server->getFrameStartTime(); + } + + // Do the damage. + hurt(damageInHalves, damageEventType, source); +} + +//---------------------------- + +void NPC::executeEvents(ScriptEventQueue& events, ScriptObject source) const +{ + if (events.queue().empty()) + return; + + m_script.executeEvents(events, source); + + // Execute classes. + for (auto& [handle, scriptClassPtr] : m_joinedClasses) + { + if (auto scriptClass = scriptClassPtr.lock(); scriptClass != nullptr) + scriptClass->getScript().executeEvents(events, source); + } + + events.queue().clear(); +} + +void NPC::setScript(const Script& script) +{ + m_script = script; + + // TODO: Optimize this. We need a better way to track joined classes and to assign them to the NPC. + auto classes = string::join(m_script.getServerJoinedClasses() | std::views::transform([](const auto& pair) { return pair.first; })); + setJoinedClasses(classes); + + auto clientside = m_script.getClientSide(); + + // Check for position update blocking. + if (m_server->hasNPCServer() || clientside.contains("//#BLOCKPOSITIONUPDATES")) + m_blockPositionUpdates = true; + + // If we have no npc-server, we support toweapons, so extract the weapon name. + if (!m_server->hasNPCServer()) + m_weaponName = toWeaponName(clientside); + + // Just a little warning for people who don't know. + if (m_script.getClientByteCode().empty() && m_script.getClientSide().length() > 0x705F) + log::printLine(log::server, "WARNING: Clientside script of NPC ({}) exceeds the limit of 28767 bytes.", (image.length() != 0 ? image : std::to_string(id))); +} + +void NPC::setScript(std::string_view script) +{ + //auto profile = log::Profile(log::server, "NPC::setScript"); + + // Set the script. + setJoinedClasses(""); + m_script = std::move(Script{ name, script }); + modTime[PROPID(NPCProp::SCRIPT)] = m_server->getFrameStartTime(); + + // Check if we have joined classes already (due to a cached script). + for (const auto& [name, classPtr] : m_script.getServerJoinedClasses()) + { + if (auto scriptClass = classPtr.lock(); scriptClass != nullptr) + { + auto it = std::ranges::find_if(m_joinedClasses, [&scriptClass](const decltype(m_joinedClasses)::value_type& kvp) { return kvp.second.lock()->name == scriptClass->name; }); + if (it != m_joinedClasses.end()) + continue; + + auto handle = scriptClass->onScriptModified.subscribe(std::bind(&NPC::updateScriptClass, this, std::placeholders::_1)); +#ifdef DEBUG + log::printLine(log::server, "[DEBUG] NPC '{}' auto-joining class '{}' due to cached script.", name, scriptClass->name); +#endif + m_joinedClasses.emplace_back(handle, scriptClass); + } + } + + auto clientside = m_script.getClientSide(); + + // Check for position update blocking. + if (m_server->hasNPCServer() || clientside.contains("//#BLOCKPOSITIONUPDATES")) + m_blockPositionUpdates = true; + + // If we have no npc-server, we support toweapons, so extract the weapon name. + if (!m_server->hasNPCServer()) + m_weaponName = toWeaponName(clientside); + + // Just a little warning for people who don't know. + if (m_script.getClientByteCode().empty() && m_script.getClientSide().length() > 0x705F) + log::printLine(log::server, "WARNING: Clientside script of NPC ({}) exceeds the limit of 28767 bytes.", (image.length() != 0 ? image : std::to_string(id))); +} + +std::string NPC::getClientSideScript() const +{ + std::string result{ m_script.getClientSide() }; + for (const auto& [handle, classPtr] : m_joinedClasses) + { + if (auto scriptClass = classPtr.lock(); scriptClass != nullptr) + { + const auto& clientSide = scriptClass->getScript().getClientSide(); + if (!clientSide.empty()) + { + result += "\xa7"; + result += clientSide; + } + } + } + return result; +} + +std::string NPC::getJoinedClassesList() const +{ + bool hasExpired = false; + std::string result; + for (const auto& [handle, classPtr] : m_joinedClasses) + { + if (auto scriptClass = classPtr.lock(); scriptClass != nullptr) + { + result += scriptClass->name; + result += ","; + } + else hasExpired = true; + } + if (!result.empty()) + result.pop_back(); + + // If we have expired, clear them out. + if (hasExpired) + { + std::erase_if(m_joinedClasses, [this](const decltype(m_joinedClasses)::value_type& pair) { return pair.second.expired(); }); + } + + return result; +} + +bool NPC::hasJoinedClass(std::string_view className) const +{ + for (const auto& [handle, classPtr] : m_joinedClasses) + { + if (auto scriptClass = classPtr.lock(); scriptClass != nullptr && scriptClass->name == className) + return true; + } + return false; +} + +void NPC::setJoinedClasses(std::string_view classes) +{ + if (!m_server->hasNPCServer()) return; + + for (const auto& [handle, classPtr] : m_joinedClasses) + { + if (auto scriptClass = classPtr.lock(); scriptClass != nullptr) + scriptClass->onScriptModified.unsubscribe(handle); + } + + m_joinedClasses.clear(); + + bool sendToLevel = false; + while (!classes.empty()) + { + auto className = string::extractLine(classes, ','); + if (className.empty()) + continue; + + className = string::trim(className); + if (auto scriptClass = m_server->getNPCServer()->getClass(className).lock(); scriptClass != nullptr) + { + auto handle = scriptClass->onScriptModified.subscribe(std::bind(&NPC::updateScriptClass, this, std::placeholders::_1)); + m_joinedClasses.emplace_back(handle, scriptClass); + modTime[PROPID(NPCProp::CLASS)] = m_server->getFrameStartTime(); + lastUpdateTime = m_server->getFrameStartTime(); + + // If the joined script has clientside code, delete the NPC and resend the new code. + if (!scriptClass->getScript().getClientSide().empty()) + sendToLevel = true; + } + } + + if (sendToLevel) + sendScriptUpdatesToLevel(lastUpdateTime); +} + +void NPC::joinClass(std::string_view className) +{ + auto it = std::ranges::find_if(m_joinedClasses, [&className](const decltype(m_joinedClasses)::value_type& kvp) { return kvp.second.lock()->name == className; }); + if (it != m_joinedClasses.end()) + return; + + if (!m_server->hasNPCServer()) + return; + + if (auto scriptClass = m_server->getNPCServer()->getClass(className).lock(); scriptClass != nullptr) + { + auto handle = scriptClass->onScriptModified.subscribe(std::bind(&NPC::updateScriptClass, this, std::placeholders::_1)); + m_joinedClasses.emplace_back(handle, scriptClass); + modTime[PROPID(NPCProp::CLASS)] = m_server->getFrameStartTime(); + lastUpdateTime = m_server->getFrameStartTime(); + + // If the joined script has clientside code, delete the NPC and resend the new code. + if (!scriptClass->getScript().getClientSide().empty()) + sendScriptUpdatesToLevel(lastUpdateTime); + } + else + { + log::printLine(log::npc, "Error: NPC [{}] '{}' tried to join class '{}', but it does not exist.", id, name, className); + } +} + +void NPC::leaveClass(std::string_view className) +{ + auto it = std::ranges::find_if(m_joinedClasses, [&className](const decltype(m_joinedClasses)::value_type& kvp) { return kvp.second.lock()->name == className; }); + if (it == m_joinedClasses.end()) + return; + + if (!m_server->hasNPCServer()) + return; + + bool sendToLevel = false; + if (auto scriptClass = it->second.lock(); scriptClass != nullptr) + { + scriptClass->onScriptModified.unsubscribe(it->first); + modTime[PROPID(NPCProp::CLASS)] = m_server->getFrameStartTime(); + lastUpdateTime = m_server->getFrameStartTime(); + + // If the joined script has clientside code, delete the NPC and resend the new code. + if (!scriptClass->getScript().getClientSide().empty()) + sendToLevel = true; + } + + if (sendToLevel) + sendScriptUpdatesToLevel(lastUpdateTime); + + m_joinedClasses.erase(it); +} + +void NPC::sendScriptUpdatesToLevel(clock::time_point when) const +{ + if (auto npclevel = getLevel(); npclevel != nullptr) + { + if (auto levelData = npclevel->getStaticLevelDataAtPosition(character.getMapPosition()); levelData != nullptr) + { + const auto& levelName = npclevel->levelName; + + CString packet = CString() >> (char)PLO_NPCDEL2 >> (char)levelName.length() << levelName >> (int)id; + m_server->sendPacketToLevelAndPastVisitorsAfter(levelData.get(), when, packet); + m_server->sendPacketToNearby(CString() >> (char)PLO_NPCPROPS >> (int)id << getAllPropsPacket(), character.getGlobalPosition(), npclevel); + } + } +} + +void NPC::updateScriptClass(ScriptClass* scriptClass) +{ + if (scriptClass == nullptr || !m_server->hasNPCServer()) + return; + if (scriptClass->getScript().getClientSide().empty()) + return; + + sendScriptUpdatesToLevel(lastUpdateTime); + modTime[PROPID(NPCProp::SCRIPT)] = m_server->getFrameStartTime(); + lastUpdateTime = m_server->getFrameStartTime(); +} + +//---------------------------- + +std::shared_ptr NPC::constructPropFor(NPCProp prop) const +{ + switch (prop) + { +#define GENERATE_CONSTRUCTPROPFOR_CASE(prop, type, ...) case prop: return std::make_shared(); + FOR_LIST_OF_NPC_PROPS(GENERATE_CONSTRUCTPROPFOR_CASE); + } + throw std::invalid_argument("Invalid NPCProp type in constructPropFor"); +} + +//---------------------------- + +std::shared_ptr NPC::getProp(NPCProp prop) const +{ + switch (prop) + { +#define GENERATE_GETPROP_CASE(prop, type, ...) case prop: return std::make_shared( __VA_ARGS__ ); + FOR_LIST_OF_NPC_PROPS(GENERATE_GETPROP_CASE); + } + + throw std::invalid_argument("Invalid NPCProp type in getProp"); +} + +//---------------------------- + +SetResults NPC::setProp(NPCProp prop, SetBy setBy, std::shared_ptr base) +{ + PropertyBase* basePtr = base.get(); + if (basePtr != nullptr) + return setProp(prop, setBy, basePtr); + throw std::invalid_argument("setProp called with nullptr base pointer."); +} + +SetResults NPC::setProp(NPCProp prop, SetBy setBy, PropertyBase* base) +{ + auto levelPtr = getLevel(); + bool canUpdatePosition = !m_blockPositionUpdates || setBy == props::SetBy::SERVER; + + props::SetResults result{ .propId = { PROPID(prop) } }; + result.resultFlags.set(props::SetResults::sendToLevel, true); + result.resultFlags.set(props::SetResults::sendToSource, false); + + const auto& curTime = m_server->getFrameStartTime(); + auto oldTime = modTime[PROPID(prop)]; + auto oldLastUpdateTime = lastUpdateTime; + + modTime[PROPID(prop)] = curTime; + lastUpdateTime = curTime; + +#define SETPROP_RETURN_ERROR do { result.resultFlags.set(SetResults::wasInvalid); modTime[PROPID(prop)] = oldTime; lastUpdateTime = oldLastUpdateTime; return result; } while(false) + + switch (prop) + { + case NPCProp::IMAGE: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr || strProp->value == image) + SETPROP_RETURN_ERROR; + + // If we are changing to a character, set the gani to idle. + if (strProp->value == "#c#" && image != "#c") + { + visFlags |= PROPID(NPCVisFlags::MALE); + if (m_server->Generation != ServerGeneration::ORIGINAL) + { + character.gani = "idle"; + result.resultPropIds.push_back(PROPID(NPCProp::GANI)); + } + } + + image = strProp->value; + auto oldVisFlags = visFlags; + + // If the image is being set and it is empty or "-", and we don't have a shape, make us invisible. + // This will prevent the NPC from being seen as an obstacle in serverside checks. + if (!hasImage() && !hasShape()) + visFlags &= ~(uint8_t)NPCVisFlags::VISIBLE; + else + visFlags |= (uint8_t)NPCVisFlags::VISIBLE; + + // If we had a visibility change, send it. + if (visFlags != oldVisFlags) + result.resultPropIds.push_back(PROPID(NPCProp::VISFLAGS)); + break; + } + + case NPCProp::SCRIPT: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr || setBy != SetBy::SERVER) + SETPROP_RETURN_ERROR; + + setScript(strProp->value); + break; + } + + case NPCProp::X: + { + PropertyTileCoordinate* coordProp = dynamic_cast(base); + if (coordProp == nullptr || !canUpdatePosition) + SETPROP_RETURN_ERROR; + + character.localPixelX = coordProp->pixelCoordinate; + result.resultPropIds.push_back(PROPID(NPCProp::X2)); + + // Do collision testing. + testForTouch(result); + break; + } + + case NPCProp::Y: + { + PropertyTileCoordinate* coordProp = dynamic_cast(base); + if (coordProp == nullptr || !canUpdatePosition) + SETPROP_RETURN_ERROR; + + character.localPixelY = coordProp->pixelCoordinate; + result.resultPropIds.push_back(PROPID(NPCProp::Y2)); + + // Do collision testing. + testForTouch(result); + break; + } + + case NPCProp::Z: + { + PropertyTileCoordinateZ* zProp = dynamic_cast(base); + if (zProp == nullptr || !canUpdatePosition) + SETPROP_RETURN_ERROR; + + character.localPixelZ = zProp->pixelCoordinate; + result.resultPropIds.push_back(PROPID(NPCProp::Z2)); + + // No collision testing for Z movement. + break; + } + + case NPCProp::POWER: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + character.hurtDeltaInHalves = character.hitpointsInHalves - numProp->value; + character.hitpointsInHalves = numProp->value; + + if (character.hurtDeltaInHalves != 0) + character.lastHurtTime = curTime; + break; + } + + case NPCProp::RUPEES: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + character.gralats = numProp->value; + break; + } + + case NPCProp::ARROWS: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + character.arrows = numProp->value; + break; + } + + case NPCProp::BOMBS: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + character.bombs = numProp->value; + break; + } + + case NPCProp::GLOVEPOWER: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + character.glovePower = numProp->value; + break; + } + + case NPCProp::BOMBPOWER: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + character.bombPower = numProp->value; + break; + } + + case NPCProp::SWORDIMAGE: + { + PropertySwordPower* swordProp = dynamic_cast(base); + if (swordProp == nullptr) + SETPROP_RETURN_ERROR; + + if (swordProp->power.has_value()) + character.swordPower = props::Limits::applySwordPower(swordProp->power.value_or(1)); + + character.swordImage = props::Limits::apply(swordProp->image, props::Limits::SwordImageLength); + break; + } + + case NPCProp::SHIELDIMAGE: + { + PropertyShieldPower* shieldProp = dynamic_cast(base); + if (shieldProp == nullptr) + SETPROP_RETURN_ERROR; + + if (shieldProp->power.has_value()) + character.shieldPower = props::Limits::applyShieldPower(shieldProp->power.value_or(1)); + + character.shieldImage = props::Limits::apply(shieldProp->image, props::Limits::ShieldImageLength); + break; + } + + case NPCProp::GANI: + { + PropertyGaniOrBowGif* ganiProp = dynamic_cast(base); + if (ganiProp == nullptr) + SETPROP_RETURN_ERROR; + + // 1.x servers didn't have ganis. This prop was used for the bow instead. + if (m_server->Generation == ServerGeneration::ORIGINAL) + { + if (!ganiProp->bowGif.has_value()) + SETPROP_RETURN_ERROR; + + auto& [image, power] = ganiProp->bowGif.value(); + character.bowPower = props::Limits::apply(power, props::Limits::MaxBowPower); + character.bowImage = image; + if (!character.bowImage.empty() && !character.bowImage.contains('.')) + character.bowImage += ".gif"; + break; + } + + // Set the gani. + std::string gani = ganiProp->gani.value_or("idle"); + character.gani = props::Limits::apply(gani, props::Limits::GaniLength); + result.resultFlags.set(SetResults::getLatestOnSend); + + // If we aren't a character, do that now. + if (!isCharacter()) + { + image = "#c#"; + result.resultPropIds.push_back(PROPID(NPCProp::IMAGE)); + } + + // If we are not in a legacy sprite gani and our sprite is not 0, reset the sprite. + if (!character.gani.starts_with("def[") && character.sprite != 0) + { + character.sprite = 0; + result.resultPropIds.push_back(PROPID(NPCProp::SPRITE)); + } + + // If we are hurting, and didn't get hurt this frame, unset the hurt time. + if (character.lastHurtTime != curTime) + character.lastHurtTime = clock::time_point::min(); + + // Allow spin to hurt things. + if (character.gani == "spin") + { + auto self = m_server->getNPC(id); + float tX = static_cast(character.localPixelX / 16.0f) + 1.5f; + float tY = static_cast(character.localPixelY / 16.0f) + 2.0f; + m_server->hitObjectsAtPoint({ tX, tY + 2.0f }, character.swordPower, m_currentLevel, self); + m_server->hitObjectsAtPoint({ tX, tY - 2.0f }, character.swordPower, m_currentLevel, self); + m_server->hitObjectsAtPoint({ tX + 2.0f, tY }, character.swordPower, m_currentLevel, self); + m_server->hitObjectsAtPoint({ tX - 2.0f, tY }, character.swordPower, m_currentLevel, self); + } + break; + } + + case NPCProp::VISFLAGS: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + visFlags = numProp->value; + break; + } + + case NPCProp::BLOCKFLAGS: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + blockFlags = numProp->value; + break; + } + + case NPCProp::MESSAGE: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + character.chatMessage = strProp->value; + break; + } + + case NPCProp::HURTDXDY: + { + PropertyHurtDxDy* hurtProp = dynamic_cast(base); + if (hurtProp == nullptr) + SETPROP_RETURN_ERROR; + + character.hurtPushDeltaInHalfPixels[0] = hurtProp->hurtDX; + character.hurtPushDeltaInHalfPixels[1] = hurtProp->hurtDY; + break; + } + + case NPCProp::ID: + break; + + case NPCProp::SPRITE: + { + PropertySprite* spriteProp = dynamic_cast(base); + if (spriteProp == nullptr) + SETPROP_RETURN_ERROR; + + character.direction = spriteProp->direction; + character.sprite = spriteProp->sprite; + result.resultFlags.set(SetResults::getLatestOnSend); + + // If we manually set a sprite, change the gani. + if (m_server->Generation != ServerGeneration::ORIGINAL && character.sprite != 0) + { + auto gani = std::format("def[{}]", character.sprite); + //visFlags |= static_cast(NPCVisFlags::UNKNOWNBIT5); + result.resultPropIds.push_back(PROPID(NPCProp::GANI)); + //result.resultPropIds.push_back(PROPID(NPCProp::VISFLAGS)); + } + break; + } + + case NPCProp::COLORS: + { + PropertyColors* colorProp = dynamic_cast(base); + if (colorProp == nullptr) + SETPROP_RETURN_ERROR; + + character.colors = colorProp->values; + break; + } + + case NPCProp::NICKNAME: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + character.nickName = strProp->value; + break; + } + + case NPCProp::HORSEIMAGE: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + character.horseImage = strProp->value; + + if (m_server->Generation == ServerGeneration::ORIGINAL && !character.horseImage.empty() && !character.horseImage.contains('.')) + character.horseImage += ".gif"; + break; + } + + case NPCProp::HEADIMAGE: + { + PropertyHeadGif* headProp = dynamic_cast(base); + if (headProp == nullptr) + SETPROP_RETURN_ERROR; + + std::string img; + if (std::holds_alternative(headProp->image)) + img = std::format("head{}.{}", std::get(headProp->image), (m_server->Generation != ServerGeneration::ORIGINAL ? "png" : "gif")); + else + img = std::get(headProp->image); + + if (m_server->Generation == ServerGeneration::ORIGINAL && !img.empty() && !img.contains('.')) + img += ".gif"; + + character.headImage = props::Limits::apply(img, props::Limits::HeadImageLength); + result.resultFlags.set(SetResults::getLatestOnSend); + break; + } + + case NPCProp::SAVE0: + case NPCProp::SAVE1: + case NPCProp::SAVE2: + case NPCProp::SAVE3: + case NPCProp::SAVE4: + case NPCProp::SAVE5: + case NPCProp::SAVE6: + case NPCProp::SAVE7: + case NPCProp::SAVE8: + case NPCProp::SAVE9: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + auto index = PROPID(prop) - PROPID(NPCProp::SAVE0); + saves[index] = numProp->value; + break; + } + + case NPCProp::ALIGNMENT: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + character.ap = numProp->value; + break; + } + + case NPCProp::IMAGEPART: + { + PropertyImagePart* imgPartProp = dynamic_cast(base); + if (imgPartProp == nullptr) + SETPROP_RETURN_ERROR; + + imagePart = imgPartProp->imagePart; + break; + } + + case NPCProp::BODYIMAGE: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + character.bodyImage = strProp->value; + break; + } + + case NPCProp::GMAPLEVELX: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr || numProp->value == character.mapX) + SETPROP_RETURN_ERROR; + + character.mapX = numProp->value; + break; + } + + case NPCProp::GMAPLEVELY: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr || numProp->value == character.mapY) + SETPROP_RETURN_ERROR; + + character.mapY = numProp->value; + break; + } + + case NPCProp::UNKNOWN48: + break; + + case NPCProp::SCRIPTER: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + break; + + scripter = strProp->value; + } + + case NPCProp::NAME: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + name = strProp->value; + break; + } + + case NPCProp::TYPE: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + scriptType = strProp->value; + break; + } + + case NPCProp::CURLEVEL: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr || !canUpdatePosition) + SETPROP_RETURN_ERROR; + + // No change? Don't do anything. + if (level == strProp->value) + SETPROP_RETURN_ERROR; + + // See if the level exists. + auto newLevel = m_server->getLoadedLevel(strProp->value, levelPtr); + if (newLevel == nullptr) + SETPROP_RETURN_ERROR; + + // Tell everybody we are moving. + // This should technically only be sent to players in the level or those who had been in the level. + auto localPosition = getLocalPosition(); + m_server->sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_NPCMOVED >> (int)id >> (char)(localPosition.x() / 8) >> (char)(localPosition.y() / 8) << strProp->value); + + // Remove ourself from the old level. + if (auto oldLevel = getLevel(); oldLevel != nullptr) + oldLevel->removeNPC(id); + + // Add us to the new level. + newLevel->addNPC(id); + + // Send our props to people in the new level. + m_server->sendPacketToNearby(CString() >> (char)PLO_NPCPROPS >> (int)id << getAllPropsPacket(), character.getGlobalPosition(), newLevel); + + // Tell NCs about our new position. + CString ncPacket = CString() >> (char)PLO_NC_NPCADD >> (int)id >> (char)NPCProp::CURLEVEL << getProp().serialize(); + m_server->sendPacketToType(PLTYPE_ANYNC, ncPacket); + + // Send the NPCWARPED event to the NPC. + scripting.events.addEvent(ScriptEventType::NPCWARPED, source::FromNPC(id)); + break; + } + + case NPCProp::GATTRIB1: + case NPCProp::GATTRIB2: + case NPCProp::GATTRIB3: + case NPCProp::GATTRIB4: + case NPCProp::GATTRIB5: + case NPCProp::GATTRIB6: + case NPCProp::GATTRIB7: + case NPCProp::GATTRIB8: + case NPCProp::GATTRIB9: + case NPCProp::GATTRIB10: + case NPCProp::GATTRIB11: + case NPCProp::GATTRIB12: + case NPCProp::GATTRIB13: + case NPCProp::GATTRIB14: + case NPCProp::GATTRIB15: + case NPCProp::GATTRIB16: + case NPCProp::GATTRIB17: + case NPCProp::GATTRIB18: + case NPCProp::GATTRIB19: + case NPCProp::GATTRIB20: + case NPCProp::GATTRIB21: + case NPCProp::GATTRIB22: + case NPCProp::GATTRIB23: + case NPCProp::GATTRIB24: + case NPCProp::GATTRIB25: + case NPCProp::GATTRIB26: + case NPCProp::GATTRIB27: + case NPCProp::GATTRIB28: + case NPCProp::GATTRIB29: + case NPCProp::GATTRIB30: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + auto index = std::ranges::distance(NPCGaniAttrPackets.begin(), std::ranges::find(NPCGaniAttrPackets, PROPID(prop))); + character.ganiAttributes[index] = strProp->value; + break; + } + + case NPCProp::CLASS: + { + PropertyLongString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + setJoinedClasses(strProp->value); + break; + } + + case NPCProp::X2: + { + PropertyPixelCoordinate* pixelProp = dynamic_cast(base); + if (pixelProp == nullptr || !canUpdatePosition) + SETPROP_RETURN_ERROR; + + character.localPixelX = pixelProp->pixelCoordinate; + result.resultPropIds.push_back(PROPID(NPCProp::X)); + + // Do collision testing. + testForTouch(result); + break; + } + + case NPCProp::Y2: + { + PropertyPixelCoordinate* pixelProp = dynamic_cast(base); + if (pixelProp == nullptr || !canUpdatePosition) + SETPROP_RETURN_ERROR; + + character.localPixelY = pixelProp->pixelCoordinate; + result.resultPropIds.push_back(PROPID(NPCProp::Y)); + + // Do collision testing. + testForTouch(result); + break; + } + + case NPCProp::Z2: + { + PropertyPixelCoordinate* pixelProp = dynamic_cast(base); + if (pixelProp == nullptr || !canUpdatePosition) + SETPROP_RETURN_ERROR; + + character.localPixelZ = pixelProp->pixelCoordinate; + result.resultPropIds.push_back(PROPID(NPCProp::Z)); + + // Do collision testing. + testForTouch(result); + break; + } + } + + // If we are sending other ids, we need to update the mod time for them too. + if (!result.resultPropIds.empty() && !result.resultFlags.test(SetResults::wasInvalid)) + { + for (const auto& id : result.resultPropIds) + modTime[id] = curTime; + } + + return result; +} + +//---------------------------- + +void NPC::sendPropsFromSendResults(PropertySendResults& results, PlayerPtr source) const +{ + CString sendAll, sendLevel, sendSource; + + std::erase_if(results, [](const PropertySendResults::value_type& res) + { + return !canSendProp((NPCProp)res.first.propId); + }); + + collectPacketsFromResults(results, sendAll, sendLevel, sendSource, [this](uint8_t propId, SetResults::ResultFlagType& destinations) + { + return this->getProp((NPCProp)propId); + }); + + // Send the buffers out. + if (sendAll.length() > 0) + m_server->sendPacketToAll(CString() >> (char)PLO_NPCPROPS >> (int)id << sendAll); + + PlayerID exclude = 0; + if (source != nullptr) + exclude = source->getId(); + + if (sendLevel.length() > 0 && !m_currentLevel.expired()) + m_server->sendPacketToNearby(CString() >> (char)PLO_NPCPROPS >> (int)id << sendLevel, character.getGlobalPosition(), getLevel(), { exclude }); + + if (sendSource.length() > 0 && source != nullptr) + source->sendPacket(CString() >> (char)PLO_NPCPROPS >> (int)id << sendSource); +} + +//---------------------------- + +void NPC::setPropsFromPacket(CString& packet, PlayerPtr source) +{ + DO_PACKETLOG(log::printBlock(log::networkdump, "NPC::setPropsFromPacket:\n")); + + PropertySendResults results; + auto setBy = (source != nullptr ? SetBy::CLIENT : SetBy::SERVER); + + while (packet.bytesLeft() > 0) + { + NPCProp propId = (NPCProp)packet.readGUChar(); + + DO_PACKETLOG(size_t oldPos = packet.readPos()); + + auto prop = constructPropFor(propId); + prop->deserialize(packet); + +#ifdef PACKETLOGGING + size_t currentPos = packet.readPos(); + CString rawData = packet.subString(oldPos, currentPos - oldPos); + + log::printBlock(log::networkdump, " {}: {} |", npcPropNames[PROPID(propId)], prop); + for (size_t i = 0; i < rawData.length(); ++i) + { + log::printBlock(log::networkdump, " {:02x}", (unsigned char)rawData[i]); + } + log::printBlock(log::networkdump, "\n"); +#endif + + results.emplace_back(setProp(propId, setBy, prop), prop); + } + DO_PACKETLOG(log::print(log::networkdump, "\n")); + + sendPropsFromSendResults(results, source); +} + +//---------------------------- + +CString NPC::getModifiedPropsPacket() const +{ + DO_PACKETLOG(bool printedHeader = false); + + CString result; + for (auto i = 0; i < NPCPROP_COUNT; ++i) + { + if (!canSendProp((NPCProp)i)) + continue; + + if (modTime[i].has_value() && modTime[i] != m_savedModTime[i]) + { + DO_PACKETLOG(if (!printedHeader) { printedHeader = true; log::printBlock(log::networkdump, "NPC::getModifiedPropsPacket:\n"); log::printBlock(log::networkdump, " NPCProp::ID: value: {}\n", id); }); + + if (i == PROPID(NPCProp::GANI) && !isCharacter()) + { + DO_PACKETLOG(log::printBlock(log::networkdump, " NPCProp::GANI: (empty)\n")); + result >> (char)i >> (char)0; + } + else + { +#ifdef PACKETLOGGING + auto prop = getProp((NPCProp)i); + CString data = prop->serialize(); + + log::printBlock(log::networkdump, " {}: {}", npcPropNames[i], prop); + if ((NPCProp)i != NPCProp::SCRIPT) + { + log::printBlock(log::networkdump, " |"); + for (size_t i = 0; i < data.length(); ++i) + log::printBlock(log::networkdump, " {:02x}", (unsigned char)data[i]); + } + log::printBlock(log::networkdump, "\n"); + + result >> (char)i << data; +#else + result >> (char)i << getProp((NPCProp)i)->serialize(); +#endif + } + } + } + + DO_PACKETLOG(if (printedHeader) log::print(log::networkdump, "\n")); + return result; +} + +CString NPC::getAllPropsPacket(std::optional newTime) const +{ + DO_PACKETLOG(log::printBlock(log::networkdump, "NPC::getAllPropsPacket:\n")); + + CString retVal; + int pmax = NPCPROP_COUNT; + + for (int i = 0; i < pmax; i++) + { + if (!canSendProp((NPCProp)i)) + continue; + + if (modTime[i].has_value() && modTime[i] >= newTime.value_or(clock::time_point::min())) + { + if (i == PROPID(NPCProp::GANI) && !isCharacter()) + { + DO_PACKETLOG(log::printBlock(log::networkdump, " NPCProp::GANI: (empty)\n")); + retVal >> (char)i >> (char)0; + } + else + { +#ifdef PACKETLOGGING + auto prop = getProp((NPCProp)i); + CString data = prop->serialize(); + + log::printBlock(log::networkdump, " {}: {}", npcPropNames[i], prop); + if ((NPCProp)i != NPCProp::SCRIPT) + { + log::printBlock(log::networkdump, " |"); + for (size_t i = 0; i < data.length(); ++i) + log::printBlock(log::networkdump, " {:02x}", (unsigned char)data[i]); + } + log::printBlock(log::networkdump, "\n"); + + retVal >> (char)i << data; +#else + retVal >> (char)i << getProp((NPCProp)i)->serialize(); +#endif + } + } + } + + DO_PACKETLOG(log::print(log::networkdump, "\n")); + return retVal; +} + +//---------------------------- + +void NPC::constructScriptParameters() +{ + scriptParameters.try_emplace("id", set_temporary, "id", gameValueGetter([this]() { return static_cast(id); }), GameValue::func_set{}); + scriptParameters.try_emplace("x", set_temporary, "x", + gameValueGetter([this]() { return character.getGlobalPosition().x() / 16.0; }), + gameValueSetter(this, PROPOPT(NPCProp::X2), + [this](const GameValue& value, std::optional) + { + auto globalPosition = character.getGlobalPosition(); + globalPosition.x() = value.get().value_or(0.0) * 16; + character.localPixelX = toLocalPixelPosition(globalPosition).x(); + moveQueue.clear(); + + // Also update the X prop mod time so older clients can see the movement. + auto now = m_server->getFrameStartTime(); + this->modTime[PROPID(NPCProp::X)] = now; + + // Fix the map position if applicable. + if (auto levelPtr = getLevel(); levelPtr != nullptr && levelPtr->isGmap()) + { + if (auto mapX = toMapPosition(globalPosition).x(); mapX != character.mapX) + { + character.mapX = mapX; + this->modTime[PROPID(NPCProp::GMAPLEVELX)] = now; + } + } + }) + ); + scriptParameters.try_emplace("y", set_temporary, "y", + gameValueGetter([this]() { return character.getGlobalPosition().y() / 16.0; }), + gameValueSetter(this, PROPOPT(NPCProp::Y2), + [this](const GameValue& value, std::optional) + { + auto globalPosition = character.getGlobalPosition(); + globalPosition.y() = value.get().value_or(0.0) * 16; + character.localPixelY = toLocalPixelPosition(globalPosition).y(); + moveQueue.clear(); + + // Also update the Y prop mod time so older clients can see the movement. + auto now = m_server->getFrameStartTime(); + this->modTime[PROPID(NPCProp::Y)] = now; + + // Fix the map position if applicable. + if (auto levelPtr = getLevel(); levelPtr != nullptr && levelPtr->isGmap()) + { + if (auto mapY = toMapPosition(globalPosition).y(); mapY != character.mapY) + { + character.mapY = mapY; + this->modTime[PROPID(NPCProp::GMAPLEVELY)] = now; + } + } + }) + ); + scriptParameters.try_emplace("z", set_temporary, "z", + gameValueGetter([this]() { return getCalculatedTileZ(); }), + gameValueSetter(this, PROPOPT(NPCProp::Z2), [this](const GameValue& value, std::optional) { character.localPixelZ = value.get().value_or(0.0) * 16; })); + scriptParameters.try_emplace("width", set_temporary, "width", gameValueGetter([this]() { return getComputedShape().width() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("height", set_temporary, "height", gameValueGetter([this]() { return getComputedShape().height() / 16.0; }), GameValue::func_set{}); + scriptParameters.try_emplace("hearts", set_temporary, "hearts", + gameValueGetter([this]() { return character.hitpointsInHalves / 2.0; }), + gameValueSetter(this, PROPOPT(NPCProp::POWER), [this](const GameValue& value, std::optional) { character.hitpointsInHalves = value.get().value_or(0.0) * 2; })); + scriptParameters.try_emplace("hp", set_temporary, "hp", + gameValueGetter([this]() { return character.hitpointsInHalves / 2.0; }), + gameValueSetter(this, PROPOPT(NPCProp::POWER), [this](const GameValue& value, std::optional) { character.hitpointsInHalves = value.get().value_or(0.0) * 2; })); + scriptParameters.try_emplace("ap", set_temporary, "ap", gameValueGetter(character.ap), gameValueSetter(this, PROPOPT(NPCProp::ALIGNMENT), character.ap)); + scriptParameters.try_emplace("rupees", set_temporary, "rupees", gameValueGetter(character.gralats), gameValueSetter(this, PROPOPT(NPCProp::RUPEES), character.gralats)); + scriptParameters.try_emplace("gralats", set_temporary, "gralats", gameValueGetter(character.gralats), gameValueSetter(this, PROPOPT(NPCProp::RUPEES), character.gralats)); + scriptParameters.try_emplace("bombs", set_temporary, "bombs", gameValueGetter(character.bombs), gameValueSetter(this, PROPOPT(NPCProp::BOMBS), character.bombs)); + scriptParameters.try_emplace("darts", set_temporary, "darts", gameValueGetter(character.arrows), gameValueSetter(this, PROPOPT(NPCProp::ARROWS), character.arrows)); + scriptParameters.try_emplace("glovepower", set_temporary, "glovepower", gameValueGetter(character.glovePower), gameValueSetter(this, PROPOPT(NPCProp::GLOVEPOWER), character.glovePower)); + scriptParameters.try_emplace("swordpower", set_temporary, "swordpower", gameValueGetter(character.swordPower), gameValueSetter(this, PROPOPT(NPCProp::SWORDIMAGE), character.swordPower)); + scriptParameters.try_emplace("shieldpower", set_temporary, "shieldpower", gameValueGetter(character.shieldPower), gameValueSetter(this, PROPOPT(NPCProp::SHIELDIMAGE), character.shieldPower)); + scriptParameters.try_emplace("headset", set_temporary, "headset", + gameValueGetter( + [this]() + { + int headSet = -1; + if (character.headImage.starts_with("head")) + string::toNumber(character.headImage.substr(4), headSet); + return static_cast(headSet); + }), + gameValueSetter(this, PROPOPT(NPCProp::HEADIMAGE), + [this](const GameValue& value, std::optional) + { + auto headSet = std::clamp(static_cast(value.get().value_or(-1.0)), -1, 99); + if (headSet < 0) return; + character.headImage = std::format("head{}.{}", headSet, (m_server->Generation == ServerGeneration::ORIGINAL ? "gif" : "png")); + }) + ); + scriptParameters.try_emplace("sprite", set_temporary, "sprite", + gameValueGetter(character.sprite), + gameValueSetter(this, PROPOPT(NPCProp::SPRITE), + [this](const GameValue& value, std::optional) + { + character.sprite = static_cast(value.get().value_or(0.0)); + if (character.sprite >= 4 && m_server->Generation != ServerGeneration::ORIGINAL) + { + character.gani = std::format("def[{}]", character.sprite); + this->modTime[PROPID(NPCProp::GANI)] = m_server->getFrameStartTime(); + } + }) + ); + scriptParameters.try_emplace("dir", set_temporary, "dir", + gameValueGetter([this]() { return static_cast(character.direction); }), + gameValueSetter(this, PROPOPT(NPCProp::SPRITE), + [this](const GameValue& value, std::optional) + { + character.direction = std::clamp(static_cast(value.get().value_or(0.0)), 0_ui8, 3_ui8); + }) + ); + scriptParameters.try_emplace("hurtdpower", set_temporary, "hurtdpower", gameValueGetter(character.hurtDeltaInHalves), GameValue::func_set{}); + scriptParameters.try_emplace("hurtdx", set_temporary, "hurtdx", + gameValueGetter([this]() { return character.hurtPushDeltaInHalfPixels[0] / 32.0; }), + gameValueSetter(this, PROPOPT(NPCProp::HURTDXDY), + [this](const GameValue& value, std::optional) + { + auto clampedValue = std::clamp(value.get().value_or(0.0), -1.0, 1.0); + character.hurtPushDeltaInHalfPixels[0] = static_cast(clampedValue * 32); + }) + ); + scriptParameters.try_emplace("hurtdy", set_temporary, "hurtdy", + gameValueGetter([this]() { return character.hurtPushDeltaInHalfPixels[1] / 32.0; }), + gameValueSetter(this, PROPOPT(NPCProp::HURTDXDY), + [this](const GameValue& value, std::optional) + { + auto clampedValue = std::clamp(value.get().value_or(0.0), -1.0, 1.0); + character.hurtPushDeltaInHalfPixels[1] = static_cast(clampedValue * 32); + }) + ); + scriptParameters.try_emplace("save", set_temporary, "save", gameValueGetter(saves), gameValueSetter(this, PROPOPT(NPCProp::SAVE0), saves)); + scriptParameters.try_emplace("timeout", set_temporary, "timeout", + gameValueGetter([this]() { return timeout.count() / 1000.0; }), + gameValueSetter(this, PROPOPT(std::nullopt), + [this](const GameValue& value, std::optional) + { + if (auto doubleValue = value.get(); doubleValue.has_value()) + timeout = std::chrono::milliseconds(static_cast(*doubleValue * 1000)); + }) + ); +} + +//---------------------------- + +void NPC::testForLinks(SetResults& result) +{ + auto levelPtr = getLevel(); + if (levelPtr == nullptr) return; + + // The NPC changed their level and position. + auto informNPCWarped = [&]() + { + // Tell NCs about our new position. + CString ncPacket = CString() >> (char)PLO_NC_NPCADD >> (int)id >> (char)NPCProp::CURLEVEL << getProp().serialize(); + m_server->sendPacketToType(PLTYPE_ANYNC, ncPacket); + + // Tell players that we changed level. + auto localPosition = getLocalPosition(); + m_server->sendPacketToType(PLTYPE_ANYPLAYER, CString() >> (char)PLO_NPCMOVED >> (int)id >> (char)(localPosition.x() / 8) >> (char)(localPosition.y() / 8) << getLevelName()); + m_server->sendPacketToNearby(CString() >> (char)PLO_NPCPROPS >> (int)id << getAllPropsPacket(), character.getGlobalPosition(), levelPtr); + + // Add a scripting event for the warp. + scripting.events.addEvent(ScriptEventType::NPCWARPED, source::FromNPC(id)); + }; + + // The NPC only changed their position, not their level. + auto informNPCOnlyMoved = [&result, this]() + { + result.resultPropIds.push_back(PROPID(NPCProp::X)); + result.resultPropIds.push_back(PROPID(NPCProp::Y)); + result.resultPropIds.push_back(PROPID(NPCProp::X2)); + result.resultPropIds.push_back(PROPID(NPCProp::Y2)); + }; + + // Gmaps are treated as one large map, and so level npcs can freely walk across maps (source: post=1193766) + uint8_t computedMapX = character.localPixelX / 1024; + uint8_t computedMapY = character.localPixelY / 1024; + uint8_t computedLocalX = character.localPixelX % 1024; + uint8_t computedLocalY = character.localPixelY % 1024; + + // Overworld links. + // We test the NPC's x/y position to see if they walked out of the bounds of the current level. + // If they did, we warp them to the new level, if allowed. + const auto& map = levelPtr->getMap(); + if (map != nullptr && (computedMapX != character.mapX || computedMapY != character.mapY)) + { + auto newLevelName = map->getLevelNameAt(computedMapX, computedMapY); + if (warpRestrictions != NPCWarpRestrictions::NOTALLOWED) + { + character.mapX = map->isGmap() ? computedMapX : 0; + character.mapY = map->isGmap() ? computedMapY : 0; + result.resultPropIds.push_back(PROPID(NPCProp::GMAPLEVELX)); + result.resultPropIds.push_back(PROPID(NPCProp::GMAPLEVELY)); + + character.localPixelX = computedLocalX; + character.localPixelY = computedLocalY; + + if (levelPtr->isOnBigMap()) + { + if (auto newLevel = m_server->getLoadedLevel(newLevelName, levelPtr); newLevel != nullptr) + { + setLevel(newLevel); + result.resultPropIds.push_back(PROPID(NPCProp::CURLEVEL)); + informNPCWarped(); + } + } + else informNPCOnlyMoved(); + return; + } + + // They aren't allowed to leave the level, so clamp them to the borders. + character.localPixelX = std::clamp(character.localPixelX, static_cast(0), static_cast(61 * 16)); + character.localPixelY = std::clamp(character.localPixelY, static_cast(0), static_cast(61 * 16)); + informNPCOnlyMoved(); + return; + } + + if (warpRestrictions == NPCWarpRestrictions::ALLOWED) + { + static Position touchTest[] = { { 2, 1 }, { 0, 2 }, { 2, 4 }, { 3, 2 } }; + TilePosition testPos = character.getTilePosition().translate(touchTest[character.direction].x(), touchTest[character.direction].y()); + if (auto linkTouched = levelPtr->getLink(testPos, map != nullptr); linkTouched.has_value()) + { + auto& destLevelName = linkTouched.value()->getDestinationLevel(); + SubLevelPtr destSubLevel = levelPtr->getSubLevelByName(destLevelName); + LevelPtr newLevel = nullptr; + + // Destination level was not found on the map, so check the server for the level. + if (destSubLevel == nullptr) + { + if (auto newLevel = m_server->getLoadedLevel(destLevelName, levelPtr); newLevel != nullptr) + { + destSubLevel = newLevel->getSubLevelByName(destLevelName); + setLevel(newLevel); + } + } + + // If we have a destination level, move us to it. + if (destSubLevel != nullptr) + { + auto mapPosition = destSubLevel->mapPosition.value_or(MapPosition{ 0, 0 }); + character.mapX = mapPosition.x(); + character.mapY = mapPosition.y(); + result.resultPropIds.push_back(PROPID(NPCProp::GMAPLEVELX)); + result.resultPropIds.push_back(PROPID(NPCProp::GMAPLEVELY)); + + auto pos = linkTouched.value()->getDestinationForCharacter(character, source::FromNPC(id)); + character.localPixelX = pos.x(); + character.localPixelY = pos.y(); + + // If we are changing levels, do that now. + if (newLevel != nullptr) + { + setLevel(newLevel); + result.resultPropIds.push_back(PROPID(NPCProp::CURLEVEL)); + informNPCWarped(); + } + else informNPCOnlyMoved(); + } + } + } +} + +void NPC::testForTouch(SetResults& result) +{ + if (m_currentLevel.expired() || !m_server->hasNPCServer()) + return; + + testForLinks(result); +} + +//---------------------------- + +std::string_view toWeaponName(std::string_view code) +{ + constexpr size_t notFound = std::string_view::npos; + + size_t name_start = code.find("toweapons"); + if (name_start == notFound) + return {}; + + name_start += 9; // 9 = strlen("toweapons") + + size_t name_end[2] = { code.find(";", name_start), code.find("}", name_start) }; + if (name_end[0] == notFound && name_end[1] == notFound) + return {}; + + size_t name_pos = name_end[0]; + if (name_end[1] != notFound && name_end[1] < name_end[0]) + name_pos = name_end[1]; + + if (name_pos == notFound) + return {}; + + return string::trim(code.substr(name_start, name_pos - name_start)); +} + +std::vector NPC::getVariableDump() const +{ + constexpr std::array propNames = + { + "image", "script", "x", "y", "power", + "rupees", "arrows", "bombs", "glovepower", "bombpower", + "sword", "shield", "animation", "visibility flags", "blocking flags", + "message", "hurtdxdy", "id", "sprite", "colors", + + "nickname", "horse", "head", "save[0]", "save[1]", + "save[2]", "save[3]", "save[4]", "save[5]", "save[6]", + "save[7]", "save[8]", "save[9]", "alignment", "imagepart", + "body", "ganiattr1", "ganiattr2", "ganiattr3", "ganiattr4", + + "ganiattr5", "mapx", "mapy", "z", "ganiattr6", + "ganiattr7", "ganiattr8", "ganiattr9", "UNKNOWN48", "scripter", + "name", "type", "level", "ganiattr10", "ganiattr11", + "ganiattr12", "ganiattr13", "ganiattr14", "ganiattr15", + + "ganiattr16", "ganiattr17", "ganiattr18", "ganiattr19", "ganiattr20", + "ganiattr21", "ganiattr22", "ganiattr23", "ganiattr24", "ganiattr25", + "ganiattr26", "ganiattr27", "ganiattr28", "ganiattr29", "ganiattr30", + "joinedclasses", "xprecise", "yprecise", "zprecise" + }; + + constexpr std::array propSendOrder = + { + NPCProp::ID, NPCProp::IMAGE, NPCProp::SCRIPT, NPCProp::CLASS, + NPCProp::VISFLAGS, NPCProp::BLOCKFLAGS, + NPCProp::HEADIMAGE, NPCProp::BODYIMAGE, NPCProp::SWORDIMAGE, NPCProp::SHIELDIMAGE, + NPCProp::NICKNAME, NPCProp::SPRITE, NPCProp::GANI, + NPCProp::GATTRIB1, NPCProp::GATTRIB2, NPCProp::GATTRIB3, NPCProp::GATTRIB4, NPCProp::GATTRIB5, + NPCProp::GATTRIB6, NPCProp::GATTRIB7, NPCProp::GATTRIB8, NPCProp::GATTRIB9, NPCProp::GATTRIB10, + NPCProp::GATTRIB11, NPCProp::GATTRIB12, NPCProp::GATTRIB13, NPCProp::GATTRIB14, NPCProp::GATTRIB15, + NPCProp::GATTRIB16, NPCProp::GATTRIB17, NPCProp::GATTRIB18, NPCProp::GATTRIB19, NPCProp::GATTRIB20, + NPCProp::GATTRIB21, NPCProp::GATTRIB22, NPCProp::GATTRIB23, NPCProp::GATTRIB24, NPCProp::GATTRIB25, + NPCProp::GATTRIB26, NPCProp::GATTRIB27, NPCProp::GATTRIB28, NPCProp::GATTRIB29, NPCProp::GATTRIB30, + NPCProp::SAVE0, NPCProp::SAVE1, NPCProp::SAVE2, NPCProp::SAVE3, NPCProp::SAVE4, + NPCProp::SAVE5, NPCProp::SAVE6, NPCProp::SAVE7, NPCProp::SAVE8, NPCProp::SAVE9, + NPCProp::GMAPLEVELX, NPCProp::GMAPLEVELY, NPCProp::X2, NPCProp::Y2, NPCProp::Z2 + }; + + constexpr std::array characterProps = + { + NPCProp::HEADIMAGE, NPCProp::BODYIMAGE, NPCProp::SWORDIMAGE, NPCProp::SHIELDIMAGE, + NPCProp::NICKNAME, NPCProp::SPRITE, NPCProp::GANI, + NPCProp::GATTRIB1, NPCProp::GATTRIB2, NPCProp::GATTRIB3, NPCProp::GATTRIB4, NPCProp::GATTRIB5, + NPCProp::GATTRIB6, NPCProp::GATTRIB7, NPCProp::GATTRIB8, NPCProp::GATTRIB9, NPCProp::GATTRIB10, + NPCProp::GATTRIB11, NPCProp::GATTRIB12, NPCProp::GATTRIB13, NPCProp::GATTRIB14, NPCProp::GATTRIB15, + NPCProp::GATTRIB16, NPCProp::GATTRIB17, NPCProp::GATTRIB18, NPCProp::GATTRIB19, NPCProp::GATTRIB20, + NPCProp::GATTRIB21, NPCProp::GATTRIB22, NPCProp::GATTRIB23, NPCProp::GATTRIB24, NPCProp::GATTRIB25, + NPCProp::GATTRIB26, NPCProp::GATTRIB27, NPCProp::GATTRIB28, NPCProp::GATTRIB29, NPCProp::GATTRIB30 + }; + + std::vector result; + std::string npcname = (!name.empty() ? name : std::format("npcs[{}]", id)); + + result.emplace_back(std::format("Variables dump from npc {}", npcname)); + result.emplace_back(); + if (!scriptType.empty()) + result.emplace_back(std::format("{}.type: {}", npcname, scriptType)); + if (!scripter.empty()) + result.emplace_back(std::format("{}.scripter: {}", npcname, scripter)); + result.emplace_back(std::format("{}.level: {}", npcname, level)); + result.emplace_back(); + result.emplace_back("Attributes:"); + + std::string nameprop; + for (const auto& prop : propSendOrder) + { + auto propId = PROPID(prop); + + // Don't show character props if the NPC is not a character. + if (!isCharacter() && std::ranges::contains(characterProps, prop)) + continue; + + // Don't show props that haven't changed. + if (!modTime[propId].has_value()) + continue; + + nameprop.assign(std::format("{}.{}", npcname, propNames[propId])); + switch (prop) + { + case NPCProp::SCRIPT: + result.emplace_back(std::format("{}: size: {}", nameprop, m_script.getOriginalSource().length())); + break; + + case NPCProp::CLASS: + result.emplace_back(std::format("{}: {}", nameprop, getJoinedClassesList())); + break; + + case NPCProp::SWORDIMAGE: + { + std::string swordImage = character.swordImage; + if (swordImage.empty() && character.swordPower > 0 && character.swordPower <= 4) + swordImage = std::format("sword{}.png", character.swordPower); + + result.emplace_back(std::format("{}: {} ({})", nameprop, swordImage, character.swordPower)); + break; + } + + case NPCProp::SHIELDIMAGE: + { + std::string shieldImage = character.shieldImage; + if (shieldImage.empty() && character.shieldPower > 0 && character.shieldPower <= 3) + shieldImage = std::format("shield{}.png", character.shieldPower); + + result.emplace_back(std::format("{}: {} ({})", nameprop, shieldImage, character.shieldPower)); + break; + } + + case NPCProp::VISFLAGS: + { + std::string activeVisFlags{ (visFlags & PROPID(NPCVisFlags::VISIBLE) ? "visible" : "hidden") }; + if (visFlags & PROPID(NPCVisFlags::DRAWOVERPLAYER)) + activeVisFlags += ", drawoverplayer"; + if (visFlags & PROPID(NPCVisFlags::DRAWUNDERPLAYER)) + activeVisFlags += ", drawunderplayer"; + if (visFlags & PROPID(NPCVisFlags::TIMERSHOW)) + activeVisFlags += ", timershow"; + if (visFlags & PROPID(NPCVisFlags::CREATED)) + activeVisFlags += ", created"; + if (visFlags & PROPID(NPCVisFlags::UNKNOWNBIT6)) + activeVisFlags += ", unknownbit6"; + if (isCharacter()) + activeVisFlags += (visFlags & PROPID(NPCVisFlags::MALE) ? ", male" : ", female"); + + result.emplace_back(std::format("{}: {}", nameprop, activeVisFlags)); + break; + } + + case NPCProp::BLOCKFLAGS: + { + std::string activeBlockFlags{ (blockFlags & PROPID(NPCBlockFlags::NOBLOCK) ? "noblock" : "block") }; + if (blockFlags & PROPID(NPCBlockFlags::CANBECARRIED)) + activeBlockFlags += ", canbecarried"; + if (blockFlags & PROPID(NPCBlockFlags::CANBEPULLED)) + activeBlockFlags += ", canbepulled"; + if (blockFlags & PROPID(NPCBlockFlags::CANBEPUSHED)) + activeBlockFlags += ", canbepushed"; + + result.emplace_back(std::format("{}: {}", nameprop, activeBlockFlags)); + break; + } + + default: + result.emplace_back(std::format("{}: {}", nameprop, getProp(prop))); + break; + } + } + + if (timeout != 0ms) + result.emplace_back(std::format("{}.timeout: {}ms", npcname, timeout.count())); + //npcDump << npcNameStr << ".scripttime (in the last min): " << CString(executionData.second) << "\n"; + //npcDump << npcNameStr << ".scriptcalls: " << CString(executionData.first) << "\n"; + + result.emplace_back(); + result.emplace_back("npc.Flags:"); + + for (const auto& [flag, value] : scripting.variables.store | variables::only_flags) + { + if (value->has() && !value->has() && value->get().value_or(false)) + result.emplace_back(std::format("{}.flags[{}]: true", npcname, flag)); + else result.emplace_back(std::format("{}.flags[{}]: {}", npcname, flag, value->get().value_or(std::string{}))); + } + + result.emplace_back(); + result.emplace_back("npc.Vars:"); + + for (const auto& [flag, value] : scripting.variables.store | variables::no_temporary) + { + if (value->has()) + result.emplace_back(std::format("{}.vars[{}]: {}", npcname, flag, value->get().value_or(0.0))); + if (value->has>()) + { + auto* values = value->get_unsafe>(); + if (values != nullptr && !values->empty()) + { + auto valuesAsStrings = *values | std::views::transform([](const double& val) { return std::format("{}", val); }); + auto valueString = string::join(valuesAsStrings, ", "); + result.emplace_back(std::format("{}.vars[{}]: {{{}}}", npcname, flag, valueString)); + } + } + } + + return result; +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/object/ShowImg.cpp b/server/src/object/ShowImg.cpp new file mode 100644 index 000000000..9feaaa01f --- /dev/null +++ b/server/src/object/ShowImg.cpp @@ -0,0 +1,299 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +//////////////////////////////////////////////////////////////////////////////// + +ShowImg ShowImg::ConstructImage(clock::time_point modTime, const PixelPosition& position, std::string_view image) noexcept +{ + ShowImg showimg{ .image = std::string{ image }, .position = position }; + showimg.modTime.fill(clock::time_point::min()); + showimg.modTime[PROPID(ShowImgProp::IMAGE)] = modTime; + showimg.modTime[PROPID(ShowImgProp::X)] = modTime; + showimg.modTime[PROPID(ShowImgProp::Y)] = modTime; + if (position.z() != 0) + showimg.modTime[PROPID(ShowImgProp::Z)] = modTime; + + return showimg; +} + +ShowImg ShowImg::ConstructText(clock::time_point modTime, const PixelPosition& position, std::string_view text, std::string_view font, std::string_view style) noexcept +{ + // Construct the formatted text string. + std::string formattedTextString; + if (!font.empty()) + { + formattedTextString += "@"; + formattedTextString += font; + if (!style.empty()) + { + formattedTextString += "@"; + formattedTextString += style; + } + } + formattedTextString += "@"; + formattedTextString += text; + + // Create the showimg. + ShowImg showimg{ .image = std::move(formattedTextString), .position = position }; + showimg.modTime.fill(clock::time_point::min()); + showimg.modTime[PROPID(ShowImgProp::IMAGE)] = modTime; + showimg.modTime[PROPID(ShowImgProp::X)] = modTime; + showimg.modTime[PROPID(ShowImgProp::Y)] = modTime; + if (position.z() != 0) + showimg.modTime[PROPID(ShowImgProp::Z)] = modTime; + + return showimg; +} + +ShowImg ShowImg::ConstructGani(clock::time_point modTime, const PixelPosition& position, std::string_view animation, uint8_t direction) noexcept +{ + // Create the showimg. + ShowImg showimg{ .image = std::format("&{},{}", direction, animation), .position = position }; + showimg.modTime.fill(clock::time_point::min()); + showimg.modTime[PROPID(ShowImgProp::IMAGE)] = modTime; + showimg.modTime[PROPID(ShowImgProp::X)] = modTime; + showimg.modTime[PROPID(ShowImgProp::Y)] = modTime; + if (position.z() != 0) + showimg.modTime[PROPID(ShowImgProp::Z)] = modTime; + + return showimg; +} + +ShowImg ShowImg::ConstructPoly(clock::time_point modTime, const std::vector& points) noexcept +{ + std::string polygon{ "#2" }; + for (const auto& point : points) + { + polygon += std::format(",{:.0f}", point); + } + + // Create the showimg. + ShowImg showimg{ .image = std::move(polygon), .position = { 0_i32, 0_i32 } }; + showimg.modTime.fill(clock::time_point::min()); + showimg.modTime[PROPID(ShowImgProp::IMAGE)] = modTime; + showimg.modTime[PROPID(ShowImgProp::X)] = modTime; + showimg.modTime[PROPID(ShowImgProp::Y)] = modTime; + + return showimg; +} + +//---------------------------- + +void ShowImg::processProps(CString& props) +{ + while (props.bytesLeft() > 0) + { + uint8_t propId = props.readGUChar(); + ShowImgProp prop = static_cast(propId); + switch (prop) + { + case ShowImgProp::IMAGE: + { + props::PropertyString prop; + prop.deserialize(props); + + image = prop.value; + break; + } + + case ShowImgProp::X: + { + props::PropertyTileCoordinate prop; + prop.deserialize(props); + + position.x() = prop.pixelCoordinate; + break; + } + + case ShowImgProp::Y: + { + props::PropertyTileCoordinate prop; + prop.deserialize(props); + + position.y() = prop.pixelCoordinate; + break; + } + + case ShowImgProp::LAYER: + { + props::PropertyNumeric prop; + prop.deserialize(props); + + layer = prop.value; + break; + } + + case ShowImgProp::IMAGEPART: + { + props::PropertyImagePart prop; + prop.deserialize(props); + + imagePart = prop.imagePart; + break; + } + + case ShowImgProp::COLORS: + { + props::PropertyArray prop; + prop.deserialize(props); + + std::ranges::transform(prop.values | std::views::take(4), colors.begin(), [](props::GBYTE1 value) + { + return static_cast(value) / 200.0f; + }); + break; + } + + case ShowImgProp::ZOOM: + { + props::PropertyNumeric prop; + prop.deserialize(props); + + zoom = prop.value / 10.0f; + break; + } + + case ShowImgProp::Z: + { + props::PropertyTileCoordinateZ prop; + prop.deserialize(props); + + position.z() = prop.pixelCoordinate; + break; + } + + case ShowImgProp::DRAWMODE: + { + props::PropertyNumeric prop; + prop.deserialize(props); + + drawMode = prop.value; + break; + } + } + } +} + +CString ShowImg::getPropPacket(ShowImgProp prop) const +{ + switch (prop) + { + case ShowImgProp::IMAGE: + { + props::PropertyString prop{ image }; + return prop.serialize(); + } + + case ShowImgProp::X: + { + auto localPosition = toLocalPixelPosition(position); + props::PropertyTileCoordinate prop{ localPosition.x() }; + return prop.serialize(); + } + + case ShowImgProp::Y: + { + auto localPosition = toLocalPixelPosition(position); + props::PropertyTileCoordinate prop{ localPosition.y() }; + return prop.serialize(); + } + + case ShowImgProp::LAYER: + { + props::PropertyNumeric prop{ layer }; + return prop.serialize(); + } + + case ShowImgProp::IMAGEPART: + { + if (imagePart.size.width() == 0 && imagePart.size.height() == 0) + return CString() >> (char)0; + + props::PropertyImagePart prop{ imagePart }; + return CString() >> (char)1 << prop.serialize(); + } + + case ShowImgProp::COLORS: + { + auto toByte = [](float value) + { + return static_cast(std::clamp(value, 0.0f, 1.0f) * 200.0f); + }; + props::PropertyArray prop{ colors | std::views::transform(toByte) }; + return prop.serialize(); + } + + case ShowImgProp::ZOOM: + { + props::PropertyNumeric prop{ static_cast(zoom * 10.0f) }; + return prop.serialize(); + } + + case ShowImgProp::Z: + { + props::PropertyTileCoordinateZ prop{ static_cast(position.z()) }; + return prop.serialize(); + } + + case ShowImgProp::DRAWMODE: + { + props::PropertyNumeric prop{ drawMode }; + return prop.serialize(); + } + } + + return CString(); +} + +CString ShowImg::getAllPropsPacket(std::optional newTime) const +{ + CString result; + + for (uint8_t i = 0; i < SHOWIMGPROP_COUNT; ++i) + { + if (modTime[i].has_value() && modTime[i] >= newTime) + { + auto prop = static_cast(i); + result >> (char)i << getPropPacket(prop); + } + } + + return result; +} + +CString ShowImg::getModifiedPropsPacket() const +{ + CString result; + + for (uint8_t i = 0; i < SHOWIMGPROP_COUNT; ++i) + { + if (modTime[i] != savedModTime[i]) + { + auto prop = static_cast(i); + result >> (char)i << getPropPacket(prop); + } + } + + return result; +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/object/Weapon.cpp b/server/src/object/Weapon.cpp new file mode 100644 index 000000000..e82795bb6 --- /dev/null +++ b/server/src/object/Weapon.cpp @@ -0,0 +1,449 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +Weapon::Weapon(LevelItemType itemType) + : name(LevelItem::getItemName(itemType)), modTime(clock::now()), m_weaponDefault(itemType), m_checksum(0) +{ + m_server = BabyDI::Get(); + assert(m_server != nullptr); +} + +Weapon::Weapon(std::string_view name, std::string_view image, std::string_view script) + : name(name), modTime(clock::now()), m_weaponDefault(LevelItemType::INVALID) +{ + m_server = BabyDI::Get(); + assert(m_server != nullptr); + updateWeapon(image, script); +} + +//---------------------------- + +std::shared_ptr Weapon::loadWeapon(const CString& pWeapon) +{ + // Calculated file name. + // Non-alphanumeric characters are encoded as %000. + + // File Path + auto fileName = std::filesystem::path{ "weapons" } / pWeapon.toString(); + + // Load File + CString fileData; + if (!fileData.load(fileName.string())) + return nullptr; + + fileData.removeAllI("\r"); + + // Grab some information. + bool has_scriptend = fileData.find("SCRIPTEND") != -1; + bool found_scriptend = false; + + // Parse header + CString headerLine = fileData.readString("\n"); + if (headerLine != "GRAWP001") + return nullptr; + + // Definitions + std::string weaponName, weaponImage, weaponScript; + + // Parse File + while (fileData.bytesLeft()) + { + CString curLine = fileData.readString("\n"); + + // Find Command + CString curCommand = curLine.readString(); + + // Parse Line + if (curCommand == "REALNAME") + { + weaponName = curLine.readString("").toString(); + } + else if (curCommand == "IMAGE") + weaponImage = curLine.readString("").toString(); + else if (curCommand == "BYTECODE") + { + /* + CString fileName = curLine.readString(""); + + byteCodeData.load(CString() << "weapon_bytecode/" << fileName); + if (!byteCodeData.isEmpty()) + byteCodeFile = fileName.toString(); + */ + } + else if (curCommand == "SCRIPT") + { + do { + curLine = fileData.readString("\n"); + if (curLine == "SCRIPTEND") + { + found_scriptend = true; + break; + } + + weaponScript.append(curLine.text()).append("\n"); + } + while (fileData.bytesLeft()); + } + } + + // Valid Weapon Name? + if (weaponName.empty()) + return nullptr; + + // Give a warning if our weapon was malformed. + if (has_scriptend && !found_scriptend) + { + log::printLine(log::server, "WARNING: Weapon {} is malformed.", weaponName); + log::printLine(log::server, "SCRIPTEND needs to be on its own line."); + } + + // Create the weapon. + auto weapon = std::make_shared(weaponName, weaponImage, weaponScript); + + // Set the mod time to the file mod time. + weapon->modTime = fs::getFileModTime(fileName); + + // Check if we need to rename the file. + auto expectedFileName = fs::getHTMLEscapedFileName(std::format("weapon{}.txt", weapon->name)).string(); + auto currentFileName = fs::getANSIFileName(pWeapon.toString()); + if (expectedFileName != currentFileName) + { + auto server = BabyDI::Get(); + auto fileData = server->getFileSystemServer().infoi(fs::FileCategory::WEAPON, currentFileName); + if (fileData != nullptr) + { + auto indent = log::server.indent(); + if (server->getFileSystemServer().rename(*fileData, expectedFileName)) + log::printLine(log::server, "Renamed weapon file [{}] to [{}]", currentFileName, expectedFileName); + else + log::printLine(log::server, "** Failed to rename weapon file [{}] to [{}]", currentFileName, expectedFileName); + } + } + + // Give a warning if both a script and a bytecode was found. + /* + if (!weaponScript.empty() && !byteCodeData.isEmpty()) + { + log::printLine(log::server, "WARNING: Weapon {} includes both script and bytecode. Using bytecode.", weapon->name); + weaponScript.clear(); + } + */ + + /* + * TODO(Nalin): Figure out how to reimplement this. + if (!byteCodeData.isEmpty()) + { + auto byteCodeDataPtr = reinterpret_cast(byteCodeData.text()); + std::vector bytecode{ byteCodeDataPtr, byteCodeDataPtr + byteCodeData.length() }; + auto clientByteCode = std::make_shared(std::move(bytecode)); + weapon->m_source.setClientByteCode(clientByteCode); + weapon->m_bytecodeFile = byteCodeFile; + } + */ + + return weapon; +} + +//---------------------------- + +bool Weapon::saveWeapon() +{ + // Don't save default weapons / empty weapons + if (this->isDefault() || name.empty()) + return false; + + // If the bytecode filename is set, the weapon is treated as read-only so it can't be saved + //if (!m_bytecodeFile.empty()) + // return false; + + auto fileName = fs::getHTMLEscapedFileName(std::format("weapon{}.txt", name)); + auto file = m_server->getFileSystemServer().openiForWriting(fs::FileCategory::WEAPON, fileName, true); + if (!file) + return false; + + // Write the file. + file->clear(); + file->writeLine("GRAWP001"); + file->writeConfigLine("REALNAME"sv, name); + file->writeConfigLine("IMAGE"sv, image); + + // Write the script. + const auto& originalSource = m_script.getOriginalSource(); + if (!originalSource.empty()) + { + std::string_view source{ originalSource }; + file->writeLine("SCRIPT"sv); + file->writeLines(string::split(source, "\r\n"sv, false)); + file->writeLine("SCRIPTEND"sv); + } + file->close(); + + return true; +} + +Weapon& Weapon::updateWeapon(std::string_view image, std::string_view script) +{ + setJoinedClasses(""); + m_script = std::move(Script{ name, script }); + this->image = image; + modTime = clock::now(); + + // Set the cryptographic key to be the script's hash. + // This will become the DES encryption key the client will use the encrypt the bytecode. + string::string_hash hash{}; + uint64_t scriptHash = static_cast(hash(script)); + + // Package the key into two GYBTE5's. + uint32_t* hashBytes = reinterpret_cast(&scriptHash); + CString key = CString() >> (long long)(hashBytes[0]) >> (long long)(hashBytes[1]); + m_desKey = key.toString(); + + // CRC32 checksum. + m_checksum = crc32(0L, Z_NULL, 0); + m_checksum = crc32(m_checksum, (const uint8_t*)script.data(), script.length()); + + // Create the header. + // [GBYTE2 length_header_and_bytecode] + // [STRING type,name,[0/1 save_to_disk],[GBYTE[10] desKey]] + std::vector headerParts = + { + "weapon", + name, + "1", + m_desKey + }; + m_header = string::toCSV(headerParts); + + // Header with CRC32. + CString crc32{ (long long)m_checksum }; + headerParts.push_back(crc32.toString()); + m_headerWithCRC = string::toCSV(headerParts); + + // Queue the created event. + scripting.events.addEvent(ScriptEventType::CREATED, source::FromServer()); + + return *this; +} + +//---------------------------- + +void Weapon::registerWeaponWithPlayer(std::shared_ptr player) const +{ + if (isDefault()) + { + player->sendPacket(CString() >> (char)PLO_DEFAULTWEAPON >> (char)m_weaponDefault); + return; + } + + CString weaponPacket; + weaponPacket >> (char)PLO_NPCWEAPONADD >> (char)name.length() << name >> (char)NPCProp::IMAGE >> (char)image.length() << image; + + // Classic weapons. + if (m_server->Generation != ServerGeneration::MODERN && m_script.getClientByteCode().empty()) + { + std::string script = getClientSideScript(); + weaponPacket >> (char)NPCProp::SCRIPT >> (short)script.length() << script; + player->sendPacket(weaponPacket); + } + // If we have bytecode, send the weapon headers. + else + { + auto classes = getJoinedClassesList(); + weaponPacket >> (char)NPCProp::CLASS >> (short)classes.length() << classes; + player->sendPacket(weaponPacket); + + // Tell the client the load the script. + player->sendPacket(CString() >> (char)PLO_LOADSCRIPT << m_headerWithCRC); + } +} + +void Weapon::sendByteCodeToPlayer(std::shared_ptr player) const +{ + // Send the bytecode. + if (const auto& bytecode = m_script.getClientByteCode(); !bytecode.empty()) + { + const char* bytecodePtr = reinterpret_cast(bytecode.data()); + std::string_view bytecodeView(bytecodePtr, bytecode.size()); + player->sendPacket(CString() >> (char)PLO_NPCWEAPONSCRIPT >> (short)m_header.length() << m_header << bytecodeView); + } +} + +//---------------------------- + +std::string Weapon::getJoinedClassesList() const +{ + bool hasExpired = false; + std::string result; + for (const auto& [handle, classPtr] : m_joinedClasses) + { + if (auto scriptClass = classPtr.lock(); scriptClass != nullptr) + { + result += scriptClass->name; + result += ","; + } + else hasExpired = true; + } + result.pop_back(); + + // If we have expired, clear them out. + if (hasExpired) + { + std::erase_if(m_joinedClasses, [this](const decltype(m_joinedClasses)::value_type& pair) { return pair.second.expired(); }); + } + + return result; +} + +void Weapon::setJoinedClasses(std::string_view classes) +{ + if (!m_server->hasNPCServer()) return; + + for (const auto& [handle, classPtr] : m_joinedClasses) + { + if (auto scriptClass = classPtr.lock(); scriptClass != nullptr) + scriptClass->onScriptModified.unsubscribe(handle); + } + + m_joinedClasses.clear(); + + while (!classes.empty()) + { + auto className = string::extractLine(classes, ','); + if (className.empty()) + continue; + + className = string::trim(className); + if (auto scriptClass = m_server->getNPCServer()->getClass(className).lock(); scriptClass != nullptr) + { + auto handle = scriptClass->onScriptModified.subscribe(std::bind(&Weapon::updateScriptClass, this, std::placeholders::_1)); + m_joinedClasses.emplace_back(handle, scriptClass); + m_server->updateWeaponForPlayers(this); + } + } +} + +void Weapon::joinClass(std::string_view className) +{ + auto it = std::ranges::find_if(m_joinedClasses, [&className](const decltype(m_joinedClasses)::value_type& kvp) { return kvp.second.lock()->name == className; }); + if (it != m_joinedClasses.end()) + return; + + if (!m_server->hasNPCServer()) + return; + + if (auto scriptClass = m_server->getNPCServer()->getClass(className).lock(); scriptClass != nullptr) + { + auto handle = scriptClass->onScriptModified.subscribe(std::bind(&Weapon::updateScriptClass, this, std::placeholders::_1)); + m_joinedClasses.emplace_back(handle, scriptClass); + m_server->updateWeaponForPlayers(this); + } + else + { + log::printLine(log::npc, "Error: Weapon '{}' tried to join class '{}', but it does not exist.", name, className); + } +} + +void Weapon::leaveClass(std::string_view className) +{ + auto it = std::ranges::find_if(m_joinedClasses, [&className](const decltype(m_joinedClasses)::value_type& kvp) { return kvp.second.lock()->name == className; }); + if (it == m_joinedClasses.end()) + return; + + if (!m_server->hasNPCServer()) + return; + + if (auto scriptClass = it->second.lock(); scriptClass != nullptr) + scriptClass->onScriptModified.unsubscribe(it->first); + + m_joinedClasses.erase(it); + m_server->updateWeaponForPlayers(this); +} + +std::string Weapon::getClientSideScript() const +{ + std::string result{ m_script.getClientSide() }; + for (const auto& [handle, classPtr] : m_joinedClasses) + { + if (auto scriptClass = classPtr.lock(); scriptClass != nullptr) + { + const auto& clientSide = scriptClass->getScript().getClientSide(); + if (!clientSide.empty()) + { + result += "\xa7"; + result += clientSide; + } + } + } + return result; +} + +void Weapon::updateScriptClass(ScriptClass* scriptClass) +{ + m_server->updateWeaponForPlayers(this); +} + +//---------------------------- + +void Weapon::executeEvents(ScriptEventQueue& events, ScriptObject source) const +{ + if (events.queue().empty()) + return; + + m_script.executeEvents(events, source); + + for (auto& [handle, scriptClassPtr] : m_joinedClasses) + { + if (auto scriptClass = scriptClassPtr.lock(); scriptClass != nullptr) + scriptClass->getScript().executeEvents(events, source); + } + + events.queue().clear(); +} + +//---------------------------- + +ScriptObject source::FromWeapon(WeaponPtr weapon) +{ + size_t hash = string::string_hash{}(weapon->name); + return std::make_pair(hash, ScriptObjectType::WEAPON); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/player/Player.cpp b/server/src/player/Player.cpp index eaefef485..705b759a1 100644 --- a/server/src/player/Player.cpp +++ b/server/src/player/Player.cpp @@ -1,322 +1,303 @@ -#include - -#include -#include -#include -#include - +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include #include #include -#include "IConfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +#ifdef PACKETLOGGING +#define FOR_OUTPUT_PACKETS(DO) \ + DO(PLO_LEVELBOARD) \ + DO(PLO_LEVELLINK) \ + DO(PLO_BADDYPROPS) \ + DO(PLO_NPCPROPS) \ + DO(PLO_LEVELCHEST) \ + DO(PLO_LEVELSIGN) \ + DO(PLO_LEVELNAME) \ + DO(PLO_BOARDMODIFY) \ + DO(PLO_OTHERPLPROPS) \ + DO(PLO_PLAYERPROPS) \ + DO(PLO_ISLEADER) \ + DO(PLO_BOMBADD) \ + DO(PLO_BOMBDEL) \ + DO(PLO_TOALL) \ + DO(PLO_PLAYERWARP) \ + DO(PLO_WARPFAILED) \ + DO(PLO_DISCMESSAGE) \ + DO(PLO_HORSEADD) \ + DO(PLO_HORSEDEL) \ + DO(PLO_ARROWADD) \ + DO(PLO_FIRESPY) \ + DO(PLO_THROWCARRIED) \ + DO(PLO_ITEMADD) \ + DO(PLO_ITEMDEL) \ + DO(PLO_NPCMOVED) \ + DO(PLO_SIGNATURE) \ + DO(PLO_NPCACTION) \ + DO(PLO_BADDYHURT) \ + DO(PLO_FLAGSET) \ + DO(PLO_NPCDEL) \ + DO(PLO_FILESENDFAILED) \ + DO(PLO_FLAGDEL) \ + DO(PLO_SHOWIMGPLAYER) \ + DO(PLO_NPCWEAPONADD) \ + DO(PLO_NPCWEAPONDEL) \ + DO(PLO_RC_ADMINMESSAGE) \ + DO(PLO_EXPLOSION) \ + DO(PLO_PRIVATEMESSAGE) \ + DO(PLO_PUSHAWAY) \ + DO(PLO_LEVELMODTIME) \ + DO(PLO_HURTPLAYER) \ + DO(PLO_STARTMESSAGE) \ + DO(PLO_NEWWORLDTIME) \ + DO(PLO_DEFAULTWEAPON) \ + DO(PLO_HASNPCSERVER) \ + DO(PLO_FILEUPTODATE) \ + DO(PLO_HITOBJECTS) \ + DO(PLO_STAFFGUILDS) \ + DO(PLO_TRIGGERACTION) \ + DO(PLO_PLAYERWARP2) \ + DO(PLO_RC_ACCOUNTADD) \ + DO(PLO_RC_ACCOUNTSTATUS) \ + DO(PLO_RC_ACCOUNTNAME) \ + DO(PLO_RC_ACCOUNTDEL) \ + DO(PLO_RC_ACCOUNTPROPS) \ + DO(PLO_ADDPLAYER) \ + DO(PLO_DELPLAYER) \ + DO(PLO_RC_ACCOUNTPROPSGET) \ + DO(PLO_RC_ACCOUNTCHANGE) \ + DO(PLO_RC_PLAYERPROPSCHANGE) \ + DO(PLO_UNKNOWN60) \ + DO(PLO_RC_SERVERFLAGSGET) \ + DO(PLO_RC_PLAYERRIGHTSGET) \ + DO(PLO_RC_PLAYERCOMMENTSGET) \ + DO(PLO_RC_PLAYERBANGET) \ + DO(PLO_RC_FILEBROWSER_DIRLIST) \ + DO(PLO_RC_FILEBROWSER_DIR) \ + DO(PLO_RC_FILEBROWSER_MESSAGE) \ + DO(PLO_LARGEFILESTART) \ + DO(PLO_LARGEFILEEND) \ + DO(PLO_RC_ACCOUNTLISTGET) \ + DO(PLO_RC_PLAYERPROPS) \ + DO(PLO_RC_PLAYERPROPSGET) \ + DO(PLO_RC_ACCOUNTGET) \ + DO(PLO_RC_CHAT) \ + DO(PLO_PROFILE) \ + DO(PLO_RC_SERVEROPTIONSGET) \ + DO(PLO_RC_FOLDERCONFIGGET) \ + DO(PLO_NC_CONTROL) \ + DO(PLO_NPCSERVERADDR) \ + DO(PLO_NC_LEVELLIST) \ + DO(PLO_UNKNOWN81) \ + DO(PLO_SERVERTEXT) \ + DO(PLO_UNKNOWN83) \ + DO(PLO_LARGEFILESIZE) \ + DO(PLO_RAWDATA) \ + DO(PLO_BOARDPACKET) \ + DO(PLO_FILE) \ + DO(PLO_RC_MAXUPLOADFILESIZE) \ + DO(PLO_UNKNOWN104) \ + DO(PLO_UPDATEPACKAGESIZE) \ + DO(PLO_UPDATEPACKAGEDONE) \ + DO(PLO_BOARDLAYER) \ + DO(PLO_UNKNOWN109) \ + DO(PLO_SETNETCOOKIE) \ + DO(PLO_UNKNOWN124) \ + DO(PLO_NPCBYTECODE) \ + DO(PLO_UNKNOWN132) \ + DO(PLO_UNKNOWN133) \ + DO(PLO_GANISCRIPT) \ + DO(PLO_NPCWEAPONSCRIPT) \ + DO(PLO_NPCDEL2) \ + DO(PLO_HIDENPCS) \ + DO(PLO_SAY2) \ + DO(PLO_FREEZEPLAYER2) \ + DO(PLO_UNFREEZEPLAYER) \ + DO(PLO_SETACTIVELEVEL) \ + DO(PLO_NC_NPCATTRIBUTES) \ + DO(PLO_NC_NPCADD) \ + DO(PLO_NC_NPCDELETE) \ + DO(PLO_NC_NPCSCRIPT) \ + DO(PLO_NC_NPCFLAGS) \ + DO(PLO_NC_CLASSGET) \ + DO(PLO_NC_CLASSADD) \ + DO(PLO_NC_LEVELDUMP) \ + DO(PLO_MOVE) \ + DO(PLO_SHOWIMGNPC) \ + DO(PLO_NC_WEAPONLISTGET) \ + DO(PLO_UNKNOWN168) \ + DO(PLO_UNKNOWN169) \ + DO(PLO_GHOSTMODE) \ + DO(PLO_BIGMAP) \ + DO(PLO_MINIMAP) \ + DO(PLO_GHOSTTEXT) \ + DO(PLO_GHOSTICON) \ + DO(PLO_SHOOT) \ + DO(PLO_DISABLECLASSICMODE) \ + DO(PLO_FULLSTOP2) \ + DO(PLO_SERVERWARP) \ + DO(PLO_RPGWINDOW) \ + DO(PLO_STATUSLIST) \ + DO(PLO_UNKNOWN181) \ + DO(PLO_LISTPROCESSES) \ + DO(PLO_HASPROCESSRUNNING) \ + DO(PLO_TAKESCREENSHOT) \ + DO(PLO_BOARDHEIGHTS) \ + DO(PLO_BOARDMODIFY2) \ + DO(PLO_UPDATEPACKAGEISUPDATED) \ + DO(PLO_NC_CLASSDELETE) \ + DO(PLO_MOVE2) \ + DO(PLO_SERVERLISTCONNECTED) \ + DO(PLO_SHOOT2) \ + DO(PLO_NC_WEAPONGET) \ + DO(PLO_UNKNOWN193) \ + DO(PLO_CLEARWEAPONS) \ + DO(PLO_LOADGANI) \ + DO(PLO_LOADSCRIPT) \ + DO(PLO_SERVEROPTIONS) \ + DO(PLO_SET_ENC_KEY) \ + DO(PLO_BUNDLE) +#define FILL_OUTPUT_ARRAY(name) names[(uint8_t)name] = #name; + +static constexpr std::array FillOutputPacketNamesArray() +{ + std::array names; + names.fill("(unknown packet)"); + FOR_OUTPUT_PACKETS(FILL_OUTPUT_ARRAY) + return names; +} + +std::array OutputPacketNamesArray = FillOutputPacketNamesArray(); +#endif -#include "Account.h" -#include "NPC.h" -#include "Player.h" -#include "Server.h" -#include "Weapon.h" -#include "level/Level.h" -#include "level/Map.h" -#include "utilities/StringUtils.h" +/////////////////////////////////////////////////////////////////////////////// -/* - Logs -*/ -#define serverlog m_server->getServerLog() -#define rclog m_server->getRCLog() +CString ShootPacketWrapper::constructShootV1() const +{ + CString packet; + packet.writeGInt(source); + packet.writeGChar((position.x() % 1024) / 8); + packet.writeGChar((position.y() % 1024) / 8); + packet.writeGChar((position.z() / 16) + 50); + packet.writeGChar(sangle); + packet.writeGChar(sanglez); + packet.writeGChar(power); + packet.writeGChar(gani.length()); + packet.write(gani); + packet.writeGChar(shootParams.length()); + packet.write(shootParams); + return packet; +} -/* - Global Definitions -*/ -const char* __defaultfiles[] = { - "carried.gani", "carry.gani", "carrystill.gani", "carrypeople.gani", "dead.gani", "def.gani", "ghostani.gani", "grab.gani", "gralats.gani", "hatoff.gani", "haton.gani", "hidden.gani", "hiddenstill.gani", "hurt.gani", "idle.gani", "kick.gani", "lava.gani", "lift.gani", "maps1.gani", "maps2.gani", "maps3.gani", "pull.gani", "push.gani", "ride.gani", "rideeat.gani", "ridefire.gani", "ridehurt.gani", "ridejump.gani", "ridestill.gani", "ridesword.gani", "shoot.gani", "sit.gani", "skip.gani", "sleep.gani", "spin.gani", "swim.gani", "sword.gani", "walk.gani", "walkslow.gani", - "sword?.png", "sword?.gif", - "shield?.png", "shield?.gif", - "body.png", "body2.png", "body3.png", - "arrow.wav", "arrowon.wav", "axe.wav", "bomb.wav", "chest.wav", "compudead.wav", "crush.wav", "dead.wav", "extra.wav", "fire.wav", "frog.wav", "frog2.wav", "goal.wav", "horse.wav", "horse2.wav", "item.wav", "item2.wav", "jump.wav", "lift.wav", "lift2.wav", "nextpage.wav", "put.wav", "sign.wav", "steps.wav", "steps2.wav", "stonemove.wav", "sword.wav", "swordon.wav", "thunder.wav", "water.wav", - "pics1.png" -}; -const char* __defaultbodies[] = { - "body.png", "body2.png", "body3.png" -}; -const char* __defaultswords[] = { - "sword1.png", "sword2.png", "sword3.png", "sword4.png" -}; -const char* __defaultshields[] = { - "shield1.png", "shield2.png", "shield3.png" -}; - -// Enum per Attr -int __attrPackets[30] = { 37, 38, 39, 40, 41, 46, 47, 48, 49, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74 }; - -// Sent on Login -bool __sendLogin[propscount] = { - false, true, true, true, true, true, // 0-5 - true, false, true, true, true, true, // 6-11 - false, true, false, false, false, true, // 12-17 - true, false, false, true, true, true, // 18-23 - false, true, true, false, false, false, // 24-29 - false, false, true, false, true, true, // 30-35 - true, true, true, true, true, true, // 36-41 - false, false, false, false, true, true, // 42-47 - true, true, false, false, false, false, // 48-53 - true, true, true, true, true, true, // 54-59 - true, true, true, true, true, true, // 60-65 - true, true, true, true, true, true, // 66-71 - true, true, true, false, false, false, // 72-77 - false, false, false, false, true, // 78-82 -}; - -bool __getLogin[propscount] = { - true, false, false, false, false, false, // 0-5 - false, false, true, true, true, true, // 6-11 - true, true, false, true, true, true, // 12-17 - true, true, true, true, false, false, // 18-23 - true, false, false, false, false, false, // 24-29 - true, true, true, false, true, true, // 30-35 - true, true, true, true, true, true, // 36-41 - false, true, true, true, true, true, // 42-47 - true, true, true, false, false, true, // 48-53 - true, true, true, true, true, true, // 54-59 - true, true, true, true, true, true, // 60-65 - true, true, true, true, true, true, // 66-71 - true, true, true, false, false, false, // 72-77 - true, true, true, false, true, // 78-82 -}; - -// Turn prop 14 off to see the npc-server's profile. -bool __getLoginNC[propscount] = { - true, true, true, true, true, true, // 0-5 - true, true, true, true, true, true, // 6-11 - true, true, true, true, true, true, // 12-17 - true, true, true, true, true, true, // 18-23 - true, true, true, true, true, true, // 24-29 - true, false, true, true, true, true, // 30-35 - true, true, true, true, true, true, // 36-41 - false, true, true, true, true, true, // 42-47 - true, true, true, false, true, true, // 48-53 - true, true, true, true, true, true, // 54-59 - true, true, true, true, true, true, // 60-65 - true, true, true, true, true, true, // 66-71 - true, true, true, true, false, false, // 72-77 - true, true, true, false, false, // 78-82 -}; - -bool __getRCLogin[propscount] = { - true, false, false, false, false, false, // 0-5 - false, false, false, false, false, true, // 6-11 - false, false, false, false, false, false, // 12-17 - true, false, true, false, false, false, // 18-23 - false, false, false, false, false, false, // 24-29 - true, true, false, false, true, false, // 30-35 - false, false, false, false, false, false, // 36-41 - false, false, false, false, false, false, // 42-47 - false, false, false, false, false, true, // 48-53 - false, false, false, false, false, false, // 54-59 - false, false, false, false, false, false, // 60-65 - false, false, false, false, false, false, // 66-71 - false, false, false, false, false, false, // 72-77 - false, false, false, false, true, // 78-82 -}; - -bool __sendLocal[propscount] = { - false, false, true, false, false, false, // 0-5 - false, false, true, true, true, true, // 6-11 - true, true, false, true, true, true, // 12-17 - true, true, true, true, false, false, // 18-23 - true, true, false, false, false, false, // 24-29 - true, true, true, false, true, true, // 30-35 - true, true, true, true, true, true, // 36-41 - false, true, true, true, true, true, // 42-47 - true, true, true, false, false, true, // 48-53 - true, true, true, true, true, true, // 54-59 - true, true, true, true, true, true, // 60-65 - true, true, true, true, true, true, // 66-71 - true, true, true, false, false, false, // 72-77 - true, true, true, false, true, // 78-82 -}; - -bool __playerPropsRC[propscount] = { - true, true, true, true, true, true, // 0-5 - true, false, true, true, true, true, // 6-11 - false, true, false, true, true, false, // 12-17 - true, false, true, false, false, false, // 18-23 - false, false, true, true, true, true, // 24-29 - true, false, true, false, true, true, // 30-35 - true, false, false, false, false, false, // 36-41 - false, false, false, false, false, false, // 42-47 - false, false, false, false, false, false, // 48-53 - false, false, false, false, false, false, // 54-59 - false, false, false, false, false, false, // 60-65 - false, false, false, false, false, false, // 66-71 - false, false, false, false, false, false, // 72-77 - false, false, false, false, false, // 78-82 -}; +CString ShootPacketWrapper::constructShootV2() const +{ + CString packet; + packet.writeGShort(position.x()); + packet.writeGShort(position.y()); + packet.writeGShort(position.z()); + packet.writeChar(offsetx + 32); + packet.writeChar(offsety + 32); + packet.writeGChar(sangle); + packet.writeGChar(sanglez); + packet.writeGChar(power); + packet.writeGChar(gravity); + packet.writeGShort(gani.length()); + packet.write(gani); + packet.writeGChar(shootParams.length()); + packet.write(shootParams); + return packet; +} -/* - Pointer-Functions for Packets -*/ -bool Player::created = false; -typedef bool (Player::*TPLSock)(CString&); -std::vector TPLFunc(256, &Player::msgPLI_NULL); +/////////////////////////////////////////////////////////////////////////////// -void Player::createFunctions() +using PacketHandleFunc = HandlePacketResult (Player::*)(CString&); +using PacketHandleArray = std::array; + +static PacketHandleArray GeneratePacketHandlers() { - if (Player::created) - return; + PacketHandleArray handlers{}; + handlers.fill(&Player::msgPLI_NULL); - // now set non-nulls - TPLFunc[PLI_LEVELWARP] = &Player::msgPLI_LEVELWARP; - TPLFunc[PLI_BOARDMODIFY] = &Player::msgPLI_BOARDMODIFY; - TPLFunc[PLI_REQUESTUPDATEBOARD] = &Player::msgPLI_REQUESTUPDATEBOARD; - TPLFunc[PLI_PLAYERPROPS] = &Player::msgPLI_PLAYERPROPS; - TPLFunc[PLI_NPCPROPS] = &Player::msgPLI_NPCPROPS; - TPLFunc[PLI_BOMBADD] = &Player::msgPLI_BOMBADD; - TPLFunc[PLI_BOMBDEL] = &Player::msgPLI_BOMBDEL; - TPLFunc[PLI_TOALL] = &Player::msgPLI_TOALL; - TPLFunc[PLI_HORSEADD] = &Player::msgPLI_HORSEADD; - TPLFunc[PLI_HORSEDEL] = &Player::msgPLI_HORSEDEL; - TPLFunc[PLI_ARROWADD] = &Player::msgPLI_ARROWADD; - TPLFunc[PLI_FIRESPY] = &Player::msgPLI_FIRESPY; - TPLFunc[PLI_THROWCARRIED] = &Player::msgPLI_THROWCARRIED; - TPLFunc[PLI_ITEMADD] = &Player::msgPLI_ITEMADD; - TPLFunc[PLI_ITEMDEL] = &Player::msgPLI_ITEMDEL; - TPLFunc[PLI_CLAIMPKER] = &Player::msgPLI_CLAIMPKER; - TPLFunc[PLI_BADDYPROPS] = &Player::msgPLI_BADDYPROPS; - TPLFunc[PLI_BADDYHURT] = &Player::msgPLI_BADDYHURT; - TPLFunc[PLI_BADDYADD] = &Player::msgPLI_BADDYADD; - TPLFunc[PLI_FLAGSET] = &Player::msgPLI_FLAGSET; - TPLFunc[PLI_FLAGDEL] = &Player::msgPLI_FLAGDEL; - TPLFunc[PLI_OPENCHEST] = &Player::msgPLI_OPENCHEST; - TPLFunc[PLI_PUTNPC] = &Player::msgPLI_PUTNPC; - TPLFunc[PLI_NPCDEL] = &Player::msgPLI_NPCDEL; - TPLFunc[PLI_WANTFILE] = &Player::msgPLI_WANTFILE; - TPLFunc[PLI_SHOWIMG] = &Player::msgPLI_SHOWIMG; - - TPLFunc[PLI_HURTPLAYER] = &Player::msgPLI_HURTPLAYER; - TPLFunc[PLI_EXPLOSION] = &Player::msgPLI_EXPLOSION; - TPLFunc[PLI_PRIVATEMESSAGE] = &Player::msgPLI_PRIVATEMESSAGE; - TPLFunc[PLI_NPCWEAPONDEL] = &Player::msgPLI_NPCWEAPONDEL; - TPLFunc[PLI_LEVELWARPMOD] = &Player::msgPLI_LEVELWARP; // Shared with PLI_LEVELWARP - TPLFunc[PLI_PACKETCOUNT] = &Player::msgPLI_PACKETCOUNT; - TPLFunc[PLI_ITEMTAKE] = &Player::msgPLI_ITEMDEL; // Shared with PLI_ITEMDEL - TPLFunc[PLI_WEAPONADD] = &Player::msgPLI_WEAPONADD; - TPLFunc[PLI_UPDATEFILE] = &Player::msgPLI_UPDATEFILE; - TPLFunc[PLI_ADJACENTLEVEL] = &Player::msgPLI_ADJACENTLEVEL; - TPLFunc[PLI_HITOBJECTS] = &Player::msgPLI_HITOBJECTS; - TPLFunc[PLI_LANGUAGE] = &Player::msgPLI_LANGUAGE; - TPLFunc[PLI_TRIGGERACTION] = &Player::msgPLI_TRIGGERACTION; - TPLFunc[PLI_MAPINFO] = &Player::msgPLI_MAPINFO; - TPLFunc[PLI_SHOOT] = &Player::msgPLI_SHOOT; - TPLFunc[PLI_SHOOT2] = &Player::msgPLI_SHOOT2; - TPLFunc[PLI_SERVERWARP] = &Player::msgPLI_SERVERWARP; - - TPLFunc[PLI_PROCESSLIST] = &Player::msgPLI_PROCESSLIST; - - TPLFunc[PLI_UNKNOWN46] = &Player::msgPLI_UNKNOWN46; - TPLFunc[PLI_VERIFYWANTSEND] = &Player::msgPLI_VERIFYWANTSEND; - TPLFunc[PLI_UPDATECLASS] = &Player::msgPLI_UPDATECLASS; - TPLFunc[PLI_RAWDATA] = &Player::msgPLI_RAWDATA; - - TPLFunc[PLI_RC_SERVEROPTIONSGET] = &Player::msgPLI_RC_SERVEROPTIONSGET; - TPLFunc[PLI_RC_SERVEROPTIONSSET] = &Player::msgPLI_RC_SERVEROPTIONSSET; - TPLFunc[PLI_RC_FOLDERCONFIGGET] = &Player::msgPLI_RC_FOLDERCONFIGGET; - TPLFunc[PLI_RC_FOLDERCONFIGSET] = &Player::msgPLI_RC_FOLDERCONFIGSET; - TPLFunc[PLI_RC_RESPAWNSET] = &Player::msgPLI_RC_RESPAWNSET; - TPLFunc[PLI_RC_HORSELIFESET] = &Player::msgPLI_RC_HORSELIFESET; - TPLFunc[PLI_RC_APINCREMENTSET] = &Player::msgPLI_RC_APINCREMENTSET; - TPLFunc[PLI_RC_BADDYRESPAWNSET] = &Player::msgPLI_RC_BADDYRESPAWNSET; - TPLFunc[PLI_RC_PLAYERPROPSGET] = &Player::msgPLI_RC_PLAYERPROPSGET; - TPLFunc[PLI_RC_PLAYERPROPSSET] = &Player::msgPLI_RC_PLAYERPROPSSET; - TPLFunc[PLI_RC_DISCONNECTPLAYER] = &Player::msgPLI_RC_DISCONNECTPLAYER; - TPLFunc[PLI_RC_UPDATELEVELS] = &Player::msgPLI_RC_UPDATELEVELS; - TPLFunc[PLI_RC_ADMINMESSAGE] = &Player::msgPLI_RC_ADMINMESSAGE; - TPLFunc[PLI_RC_PRIVADMINMESSAGE] = &Player::msgPLI_RC_PRIVADMINMESSAGE; - TPLFunc[PLI_RC_LISTRCS] = &Player::msgPLI_RC_LISTRCS; - TPLFunc[PLI_RC_DISCONNECTRC] = &Player::msgPLI_RC_DISCONNECTRC; - TPLFunc[PLI_RC_APPLYREASON] = &Player::msgPLI_RC_APPLYREASON; - TPLFunc[PLI_RC_SERVERFLAGSGET] = &Player::msgPLI_RC_SERVERFLAGSGET; - TPLFunc[PLI_RC_SERVERFLAGSSET] = &Player::msgPLI_RC_SERVERFLAGSSET; - TPLFunc[PLI_RC_ACCOUNTADD] = &Player::msgPLI_RC_ACCOUNTADD; - TPLFunc[PLI_RC_ACCOUNTDEL] = &Player::msgPLI_RC_ACCOUNTDEL; - TPLFunc[PLI_RC_ACCOUNTLISTGET] = &Player::msgPLI_RC_ACCOUNTLISTGET; - TPLFunc[PLI_RC_PLAYERPROPSGET2] = &Player::msgPLI_RC_PLAYERPROPSGET2; - TPLFunc[PLI_RC_PLAYERPROPSGET3] = &Player::msgPLI_RC_PLAYERPROPSGET3; - TPLFunc[PLI_RC_PLAYERPROPSRESET] = &Player::msgPLI_RC_PLAYERPROPSRESET; - TPLFunc[PLI_RC_PLAYERPROPSSET2] = &Player::msgPLI_RC_PLAYERPROPSSET2; - TPLFunc[PLI_RC_ACCOUNTGET] = &Player::msgPLI_RC_ACCOUNTGET; - TPLFunc[PLI_RC_ACCOUNTSET] = &Player::msgPLI_RC_ACCOUNTSET; - TPLFunc[PLI_RC_CHAT] = &Player::msgPLI_RC_CHAT; - TPLFunc[PLI_PROFILEGET] = &Player::msgPLI_PROFILEGET; - TPLFunc[PLI_PROFILESET] = &Player::msgPLI_PROFILESET; - TPLFunc[PLI_RC_WARPPLAYER] = &Player::msgPLI_RC_WARPPLAYER; - TPLFunc[PLI_RC_PLAYERRIGHTSGET] = &Player::msgPLI_RC_PLAYERRIGHTSGET; - TPLFunc[PLI_RC_PLAYERRIGHTSSET] = &Player::msgPLI_RC_PLAYERRIGHTSSET; - TPLFunc[PLI_RC_PLAYERCOMMENTSGET] = &Player::msgPLI_RC_PLAYERCOMMENTSGET; - TPLFunc[PLI_RC_PLAYERCOMMENTSSET] = &Player::msgPLI_RC_PLAYERCOMMENTSSET; - TPLFunc[PLI_RC_PLAYERBANGET] = &Player::msgPLI_RC_PLAYERBANGET; - TPLFunc[PLI_RC_PLAYERBANSET] = &Player::msgPLI_RC_PLAYERBANSET; - TPLFunc[PLI_RC_FILEBROWSER_START] = &Player::msgPLI_RC_FILEBROWSER_START; - TPLFunc[PLI_RC_FILEBROWSER_CD] = &Player::msgPLI_RC_FILEBROWSER_CD; - TPLFunc[PLI_RC_FILEBROWSER_END] = &Player::msgPLI_RC_FILEBROWSER_END; - TPLFunc[PLI_RC_FILEBROWSER_DOWN] = &Player::msgPLI_RC_FILEBROWSER_DOWN; - TPLFunc[PLI_RC_FILEBROWSER_UP] = &Player::msgPLI_RC_FILEBROWSER_UP; - TPLFunc[PLI_NPCSERVERQUERY] = &Player::msgPLI_NPCSERVERQUERY; - TPLFunc[PLI_RC_FILEBROWSER_MOVE] = &Player::msgPLI_RC_FILEBROWSER_MOVE; - TPLFunc[PLI_RC_FILEBROWSER_DELETE] = &Player::msgPLI_RC_FILEBROWSER_DELETE; - TPLFunc[PLI_RC_FILEBROWSER_RENAME] = &Player::msgPLI_RC_FILEBROWSER_RENAME; - TPLFunc[PLI_RC_LARGEFILESTART] = &Player::msgPLI_RC_LARGEFILESTART; - TPLFunc[PLI_RC_LARGEFILEEND] = &Player::msgPLI_RC_LARGEFILEEND; - TPLFunc[PLI_RC_FOLDERDELETE] = &Player::msgPLI_RC_FOLDERDELETE; - TPLFunc[PLI_REQUESTTEXT] = &Player::msgPLI_REQUESTTEXT; - TPLFunc[PLI_SENDTEXT] = &Player::msgPLI_SENDTEXT; - TPLFunc[PLI_UPDATEGANI] = &Player::msgPLI_UPDATEGANI; - TPLFunc[PLI_UPDATESCRIPT] = &Player::msgPLI_UPDATESCRIPT; - TPLFunc[PLI_UPDATEPACKAGEREQUESTFILE] = &Player::msgPLI_UPDATEPACKAGEREQUESTFILE; - TPLFunc[PLI_RC_UNKNOWN162] = &Player::msgPLI_RC_UNKNOWN162; - - // NPC-Server Functions -#ifdef V8NPCSERVER - TPLFunc[PLI_NC_NPCGET] = &Player::msgPLI_NC_NPCGET; - TPLFunc[PLI_NC_NPCDELETE] = &Player::msgPLI_NC_NPCDELETE; - TPLFunc[PLI_NC_NPCRESET] = &Player::msgPLI_NC_NPCRESET; - TPLFunc[PLI_NC_NPCSCRIPTGET] = &Player::msgPLI_NC_NPCSCRIPTGET; - TPLFunc[PLI_NC_NPCWARP] = &Player::msgPLI_NC_NPCWARP; - TPLFunc[PLI_NC_NPCFLAGSGET] = &Player::msgPLI_NC_NPCFLAGSGET; - TPLFunc[PLI_NC_NPCSCRIPTSET] = &Player::msgPLI_NC_NPCSCRIPTSET; - TPLFunc[PLI_NC_NPCFLAGSSET] = &Player::msgPLI_NC_NPCFLAGSSET; - TPLFunc[PLI_NC_NPCADD] = &Player::msgPLI_NC_NPCADD; - TPLFunc[PLI_NC_CLASSEDIT] = &Player::msgPLI_NC_CLASSEDIT; - TPLFunc[PLI_NC_CLASSADD] = &Player::msgPLI_NC_CLASSADD; - TPLFunc[PLI_NC_LOCALNPCSGET] = &Player::msgPLI_NC_LOCALNPCSGET; - TPLFunc[PLI_NC_WEAPONLISTGET] = &Player::msgPLI_NC_WEAPONLISTGET; - TPLFunc[PLI_NC_WEAPONGET] = &Player::msgPLI_NC_WEAPONGET; - TPLFunc[PLI_NC_WEAPONADD] = &Player::msgPLI_NC_WEAPONADD; - TPLFunc[PLI_NC_WEAPONDELETE] = &Player::msgPLI_NC_WEAPONDELETE; - TPLFunc[PLI_NC_CLASSDELETE] = &Player::msgPLI_NC_CLASSDELETE; - TPLFunc[PLI_NC_LEVELLISTGET] = &Player::msgPLI_NC_LEVELLISTGET; -#endif + handlers[PLI_PLAYERPROPS] = &Player::msgPLI_PLAYERPROPS; + handlers[PLI_TOALL] = &Player::msgPLI_TOALL; + handlers[PLI_PRIVATEMESSAGE] = &Player::msgPLI_PRIVATEMESSAGE; + handlers[PLI_PACKETCOUNT] = &Player::msgPLI_PACKETCOUNT; + handlers[PLI_LANGUAGE] = &Player::msgPLI_LANGUAGE; + handlers[PLI_PROFILEGET] = &Player::msgPLI_PROFILEGET; + handlers[PLI_PROFILESET] = &Player::msgPLI_PROFILESET; + handlers[PLI_REQUESTTEXT] = &Player::msgPLI_REQUESTTEXT; + handlers[PLI_SENDTEXT] = &Player::msgPLI_SENDTEXT; - // Finished - Player::created = true; + return handlers; } -/* - Constructor - Deconstructor -*/ -Player::Player(CSocket* pSocket, uint16_t pId) - : Account(), - m_playerSock(pSocket), m_id(pId), m_fileQueue(pSocket) +/////////////////////////////////////////////////////////////////////////////// + +HandlePacketResult Player::handlePacket(std::optional id, CString& packet) +{ + static PacketHandleArray PacketHandlers = GeneratePacketHandlers(); + + m_lastData = clock::now(); + + auto handle = id.has_value() ? PacketHandlers[id.value()] : &Player::msgPLI_NULL; + return (this->*handle)(packet); +} + +/////////////////////////////////////////////////////////////////////////////// + +Player::Player(CSocket* pSocket, PlayerID pId) + : m_playerSock(pSocket), m_id(pId), m_fileQueue(pSocket) { - m_lastData = m_lastMovement = m_lastSave = m_last1m = time(0); - m_lastChat = m_lastMessage = m_lastNick = 0; + m_server = BabyDI::Get(); + m_lastData = clock::now(); m_serverName = m_server->getName(); srand((unsigned int)time(0)); - - // Create Functions - if (!Player::created) - Player::createFunctions(); } Player::~Player() @@ -326,65 +307,58 @@ Player::~Player() void Player::cleanup() { - if (m_playerSock == nullptr) - return; - // Send all unsent data (for disconnect messages and whatnot). - m_fileQueue.sendCompress(); + if (m_playerSock != nullptr) + m_fileQueue.sendCompress(); - if (m_id >= 0 && m_server != nullptr && m_loaded) + if (m_id > 0 && m_server != nullptr && m_loaded) { // Save account. - if (isClient() && !m_isLoadOnly) - saveAccount(); - - // Remove from the level. - if (!m_currentLevel.expired()) leaveLevel(); + if (isClient() && !account.loadOnly) + m_server->getAccountLoader().saveAccount(account); // Announce our departure to other clients. if (!isNC()) { - m_server->sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_OTHERPLPROPS >> (short)m_id >> (char)PLPROP_PCONNECTED, this); + m_server->sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_OTHERPLPROPS >> (short)m_id >> (char)PlayerProp::DISCONNECT, this); m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_DELPLAYER >> (short)m_id, this); } - if (!m_accountName.isEmpty()) + if (!account.name.empty()) { if (isRC()) - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "RC Disconnected: " << m_accountName, this); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "RC Disconnected: " << account.name, this); else if (isNC()) - m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_RC_CHAT << "NC Disconnected: " << m_accountName, this); + m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_RC_CHAT << "NC Disconnected: " << account.name, this); } // Log. if (isClient()) - serverlog.out(":: Client disconnected: %s\n", m_accountName.text()); + log::printLine(log::server, "Client disconnected: [{}] {}", m_id, account.name); else if (isRC()) - serverlog.out(":: RC disconnected: %s\n", m_accountName.text()); + log::printLine(log::server, "RC disconnected: [{}] {}", m_id, account.name); else if (isNC()) - serverlog.out(":: NC disconnected: %s\n", m_accountName.text()); - } + log::printLine(log::server, "NC disconnected: [{}] {}", m_id, account.name); - // Clean up. - m_cachedLevels.clear(); - m_singleplayerLevels.clear(); + // Get rid of the player now. + m_server->getPlayerIdGenerator().freeId(m_id); + m_server->getPlayerList().erase(m_id); - if (m_playerSock) - delete m_playerSock; - m_playerSock = nullptr; + m_loaded = false; + } -#ifdef V8NPCSERVER - if (m_scriptObject) + if (m_playerSock) { - m_scriptObject.reset(); + m_server->getSocketManager().unregisterSocket(this); + delete m_playerSock; } -#endif + m_playerSock = nullptr; } bool Player::onRecv() { // If our socket is gone, delete ourself. - if (m_playerSock == 0 || m_playerSock->getState() == SOCKET_STATE_DISCONNECTED) + if (m_playerSock == nullptr || m_playerSock->getState() == SOCKET_STATE_DISCONNECTED) return false; // Grab the data from the socket and put it into our receive buffer. @@ -401,25 +375,38 @@ bool Player::onRecv() else if (m_playerSock->getState() == SOCKET_STATE_DISCONNECTED) return false; + // Hold ourself just in case we are deleted. + auto self = shared_from_this(); + // Do the main function. - return doMain(); + doMain(); + if (m_playerSock != nullptr) + m_server->getSocketManager().updateSingle(this, false, true); + + return true; } bool Player::onSend() { if (m_playerSock == 0 || m_playerSock->getState() == SOCKET_STATE_DISCONNECTED) + { + m_fileQueue.clearBuffers(); return false; + } // Send data. m_fileQueue.sendCompress(); + // Update last send time. + m_lastData = clock::now(); + return true; } void Player::onUnregister() { - // Called when onSend() or onRecv() returns false. - m_server->deletePlayer(shared_from_this()); + if (m_loaded) + m_server->deletePlayer(shared_from_this()); } bool Player::canRecv() @@ -436,313 +423,28 @@ bool Player::canSend() /* Socket-Control Functions */ -bool Player::doMain() +void Player::doMain() { - // definitions - CString unBuffer; - - // parse data - m_recvBuffer.setRead(0); - while (m_recvBuffer.length() > 1) - { -#if defined(WOLFSSL_ENABLED) - if (!this->m_playerSock->webSocket && m_recvBuffer.findi("GET /") > -1 && m_recvBuffer.findi("HTTP/1.1\r\n") > -1) - { - - CString webSocketKeyHeader = "Sec-WebSocket-Key:"; - if (m_recvBuffer.findi(webSocketKeyHeader) < 0) - { - CString simpleHtml = CString() << "" APP_VENDOR " " APP_NAME " v" APP_VERSION "

Welcome to " << m_server->getSettings().getStr("name") << "!

" << m_server->getServerMessage().replaceAll("my server", m_server->getSettings().getStr("name")).text() << "

Powered by " APP_VENDOR " " APP_NAME "
Programmed by " << CString(APP_CREDITS) << "

"; - CString webResponse = CString() << "HTTP/1.1 200 OK\r\nServer: " APP_VENDOR " " APP_NAME " v" APP_VERSION "\r\nContent-Length: " << CString(simpleHtml.length()) << "\r\nContent-Type: text/html\r\n\r\n" - << simpleHtml << "\r\n"; - unsigned int dsize = webResponse.length(); - this->m_playerSock->sendData(webResponse.text(), &dsize); - return false; - } - this->m_playerSock->webSocket = true; - // Get the WebSocket handshake key - m_recvBuffer.setRead(m_recvBuffer.findi(webSocketKeyHeader)); - CString webSocketKey = m_recvBuffer.readString("\r").subString(webSocketKeyHeader.length() + 1).trimI(); - - // Append GUID - webSocketKey << "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - - // Calculate sha1 has of key + GUID and base64 encode it for sending back - webSocketKey.sha1I().base64encodeI(); - webSocketKeyHeader.clear(); - - CString webSockHandshake = CString() << "HTTP/1.1 101 Switching Protocols\r\n" - << "Upgrade: websocket\r\n" - << "Connection: Upgrade\r\n" - << "Sec-WebSocket-Protocol: binary\r\n" - << "Sec-WebSocket-Accept: " - << webSocketKey - << "\r\n\r\n"; - - unsigned int dsize = webSockHandshake.length(); - - this->m_playerSock->sendData(webSockHandshake.text(), &dsize); - - m_recvBuffer.removeI(0, m_recvBuffer.length()); - return true; - } -#endif - // New data. - m_lastData = time(0); - - // packet length - auto len = (unsigned short)m_recvBuffer.readShort(); - if ((unsigned int)len > (unsigned int)m_recvBuffer.length() - 2) - break; - - // get packet - unBuffer = m_recvBuffer.readChars(len); - m_recvBuffer.removeI(0, len + 2); - - // decrypt packet - switch (m_encryptionCodecIn.getGen()) - { - case ENCRYPT_GEN_1: // Gen 1 is not encrypted or compressed. - break; - - // Gen 2 and 3 are zlib compressed. Gen 3 encrypts individual packets - // Uncompress so we can properly decrypt later on. - case ENCRYPT_GEN_2: - case ENCRYPT_GEN_3: - unBuffer.zuncompressI(); - break; - - // Gen 4 and up encrypt the whole combined and compressed packet. - // Decrypt and decompress. - default: - decryptPacket(unBuffer); - break; - } - - // well theres your buffer - if (!parsePacket(unBuffer)) - return false; - } - - // Update the -gr_movement packets. - if (!m_grMovementPackets.isEmpty()) - { - if (!m_grMovementUpdated) - { - std::vector pack = m_grMovementPackets.tokenize("\n"); - for (auto& i: pack) - setProps(i, PLSETPROPS_FORWARD); - } - m_grMovementPackets.clear(42); - } - m_grMovementUpdated = false; - - m_server->getSocketManager().updateSingle(this, false, true); - return true; + // Process the packet data. + processBuffer(m_recvBuffer); } bool Player::doTimedEvents() { - time_t currTime = time(0); - - // If we are disconnected, delete ourself! if (m_playerSock == 0 || m_playerSock->getState() == SOCKET_STATE_DISCONNECTED) { - m_server->deletePlayer(shared_from_this()); - return false; - } - - // Only run for clients. - if (!isClient()) return true; - - // Increase online time. - m_onlineTime++; - - // Disconnect if players are inactive. - CSettings& settings = m_server->getSettings(); - if (settings.getBool("disconnectifnotmoved")) - { - int maxnomovement = settings.getInt("maxnomovement", 1200); - if (((int)difftime(currTime, m_lastMovement) > maxnomovement) && ((int)difftime(currTime, m_lastChat) > maxnomovement)) - { - serverlog.out("Client %s has been disconnected due to inactivity.\n", m_accountName.text()); - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "You have been disconnected due to inactivity."); - return false; - } - } - - // Disconnect if no data has been received in 5 minutes. - if ((int)difftime(currTime, m_lastData) > 300) - { - serverlog.out("Client %s has timed out.\n", m_accountName.text()); + m_fileQueue.clearBuffers(); return false; } - // Increase player AP. - if (settings.getBool("apsystem") && !m_currentLevel.expired()) - { - auto level = getLevel(); - if (level) - { - if (!(m_status & PLSTATUS_PAUSED) && !level->isSparringZone()) - m_apCounter--; - - if (m_apCounter <= 0) - { - if (m_character.ap < 100) - { - m_character.ap++; - setProps(CString() >> (char)PLPROP_ALIGNMENT >> (char)m_character.ap, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - if (m_character.ap < 20) m_apCounter = settings.getInt("aptime0", 30); - else if (m_character.ap < 40) - m_apCounter = settings.getInt("aptime1", 90); - else if (m_character.ap < 60) - m_apCounter = settings.getInt("aptime2", 300); - else if (m_character.ap < 80) - m_apCounter = settings.getInt("aptime3", 600); - else - m_apCounter = settings.getInt("aptime4", 1200); - } - } - } - - // Do singleplayer level events. - { - for (auto& spLevel: m_singleplayerLevels) - { - auto& level = spLevel.second; - if (level) - level->doTimedEvents(); - } - } - - // Save player account every 5 minutes. - if ((int)difftime(currTime, m_lastSave) > 300) - { - m_lastSave = currTime; - if (isClient() && m_loaded && !m_isLoadOnly) saveAccount(); - } - - // Events that happen every minute. - if ((int)difftime(currTime, m_last1m) > 60) - { - m_last1m = currTime; - m_invalidPackets = 0; - } - m_fileQueue.sendCompress(); - return true; } void Player::disconnect() { + m_fileQueue.sendCompress(); m_server->deletePlayer(shared_from_this()); - //m_server->getSocketManager()->unregisterSocket(this); -} - -bool Player::parsePacket(CString& pPacket) -{ - // First packet is always unencrypted zlib. Read it in a special way. - if (m_type == PLTYPE_AWAIT) - { - m_packetCount++; - if (!msgPLI_LOGIN(CString() << pPacket.readString("\n"))) - return false; - } - - while (pPacket.bytesLeft() > 0) - { - // Grab a packet out of the input stream. - CString curPacket; - if (m_nextIsRaw) - { - m_nextIsRaw = false; - curPacket = pPacket.readChars(m_rawPacketSize); - - // The client and RC versions above 1.1 append a \n to the end of the packet. - // Remove it now. - if (isClient() || (isRC() && m_versionId > RCVER_1_1)) - { - if (curPacket[curPacket.length() - 1] == '\n') - curPacket.removeI(curPacket.length() - 1); - } - } - else - curPacket = pPacket.readString("\n"); - - // Generation 3 encrypts individual packets so decrypt it now. - if (m_encryptionCodecIn.getGen() == ENCRYPT_GEN_3) - decryptPacket(curPacket); - - // Get the packet id. - unsigned char id = curPacket.readGUChar(); - - // RC version 1.1 adds a "\n" string to the end of file uploads instead of a newline character. - // This causes issues because it messes with the packet order. - if (isRC() && m_versionId == RCVER_1_1 && id == PLI_RC_FILEBROWSER_UP) - { - curPacket.removeI(curPacket.length() - 1); - curPacket.setRead(1); - pPacket.readChar(); // Read out the n that got left behind. - } - - // Call the function assigned to the packet id. - m_packetCount++; - //printf("Packet: (%i) %s\n", id, curPacket.text() + 1); - - // Forwards packets from server back to client as rc chat (for debugging) - //sendPacket(CString() >> (char)PLO_RC_CHAT << "Server Data [" << CString(id) << "]:" << (curPacket.text() + 1)); - if (!(*this.*TPLFunc[id])(curPacket)) - return false; - } - - return true; -} - -void Player::decryptPacket(CString& pPacket) -{ - // Version 1.41 - 2.18 encryption - // Was already decompressed so just decrypt the packet. - if (m_encryptionCodecIn.getGen() == ENCRYPT_GEN_3) - { - if (!isClient()) - return; - - m_encryptionCodecIn.decrypt(pPacket); - } - - // Version 2.19+ encryption. - // Encryption happens before compression and depends on the compression used so - // first decrypt and then decompress. - if (m_encryptionCodecIn.getGen() == ENCRYPT_GEN_4) - { - // Decrypt the packet. - m_encryptionCodecIn.limitFromType(COMPRESS_BZ2); - m_encryptionCodecIn.decrypt(pPacket); - - // Uncompress packet. - pPacket.bzuncompressI(); - } - else if (m_encryptionCodecIn.getGen() >= ENCRYPT_GEN_5) - { - // Find the compression type and remove it. - int pType = pPacket.readChar(); - pPacket.removeI(0, 1); - - // Decrypt the packet. - m_encryptionCodecIn.limitFromType(pType); // Encryption is partially related to compression. - m_encryptionCodecIn.decrypt(pPacket); - - // Uncompress packet - if (pType == COMPRESS_ZLIB) - pPacket.zuncompressI(); - else if (pType == COMPRESS_BZ2) - pPacket.bzuncompressI(); - else if (pType != COMPRESS_UNCOMPRESSED) - serverlog.out("** [ERROR] Client gave incorrect packet compression type! [%d]\n", pType); - } } void Player::sendPacket(CString pPacket, bool appendNL) @@ -751,6 +453,22 @@ void Player::sendPacket(CString pPacket, bool appendNL) if (pPacket.isEmpty()) return; + // Not connected? + if (m_playerSock == nullptr || m_playerSock->getState() == SOCKET_STATE_DISCONNECTED) + return; + +#ifdef PACKETLOGGING + // This will suck as long as we have gs2lib. + uint32_t pid = static_cast(static_cast(pPacket[0]) - 32); + log::printLine(log::networkdump, "< Out Packet to {}: [{}] {} ({} bytes)", account.name, pid, OutputPacketNamesArray[pid], pPacket.length()); + log::print(log::networkdump, "{}", pPacket.text()); + if (pPacket[pPacket.length() - 1] != '\n') + log::print(log::networkdump, "\n"); + for (int i = 0; i < pPacket.length(); ++i) + log::print(log::networkdump, "{:02x} ", (unsigned char)((pPacket.text())[i])); + log::print(log::networkdump, "\n\n"); +#endif + // append '\n' if (appendNL) { @@ -762,3265 +480,1010 @@ void Player::sendPacket(CString pPacket, bool appendNL) m_fileQueue.addPacket(pPacket); } -bool Player::sendFile(const CString& pFile) +bool Player::sendFile(const std::filesystem::path& file) { // Add the filename to the list of known files so we can resend the file // to the client if it gets changed after it was originally sent - if (isClient()) - m_knownFiles.insert(pFile.toString()); - - FileSystem* fileSystem = m_server->getFileSystem(); - - // Find file. - CString path = fileSystem->find(pFile); - if (path.isEmpty()) + if (auto client = std::dynamic_pointer_cast(shared_from_this()); isClient() && client != nullptr) { - sendPacket(CString() >> (char)PLO_FILESENDFAILED << pFile); - - return false; + client->m_knownFiles.insert(fs::getANSIFileName(file)); } - // Strip filename from the path. - path.removeI(path.findl(FileSystem::getPathSeparator()) + 1); - if (path.find(m_server->getServerPath()) != -1) - path.removeI(0, m_server->getServerPath().length()); + auto& filesystem = m_server->getFileSystem(); + std::string filename = fs::getANSIFileName(file); - // Send the file now. - return this->sendFile(path, pFile); -} + auto sendFailure = [this, &filename](std::string_view message) -> bool + { + if (!message.empty()) + log::printLine(log::server, "[WARNING] {}: {}", message, filename); + sendPacket(CString() >> (char)PLO_FILESENDFAILED << filename); + return false; + }; -bool Player::sendFile(const CString& pPath, const CString& pFile) -{ - CString filepath = m_server->getServerPath() << pPath << pFile; - CString fileData; - fileData.load(filepath); + std::vector fileData; + // Get the file mod time. time_t modTime = 0; - struct stat fileStat; - if (stat(filepath.text(), &fileStat) != -1) - modTime = fileStat.st_mtime; - // See if the file exists. - if (fileData.length() == 0) + // Find the file. + if (std::filesystem::exists(file)) + { + fs::File openedFile{ file }; + fileData = std::move(openedFile.read()); + modTime = clock::to_time_t(toSystemClock(std::filesystem::last_write_time(file))); + } + else { - sendPacket(CString() >> (char)PLO_FILESENDFAILED << pFile); + auto info = filesystem.infoi(fs::FileCategory::ALL, file.filename()); + if (info == nullptr) + return sendFailure("File not found when trying to send to player"); - return false; + // Open the file and read the data. + { + auto openedFile = info->openFile(); + if (openedFile == nullptr) + return sendFailure("File failed to load"); + + fileData = std::move(openedFile->read()); + } + + modTime = clock::to_time_t(info->getModTime()); } // Warn for very large files. These are the cause of many bug reports. - if (fileData.length() > 3145728) // 3MB - serverlog.out("[WARNING] Sending a large file (over 3MB): %s\n", pFile.text()); + if (fileData.size() > 3145728) // 3MB + log::printLine(log::server, "[WARNING] Sending a large file (over 3MB): {}", filename); // See if we have enough room in the packet for the file. // If not, we need to send it as a big file. // 1 (PLO_FILE) + 5 (modTime) + 1 (file.length()) + file.length() + 1 (\n) bool isBigFile = false; - int packetLength = 1 + 5 + 1 + pFile.length() + 1; - if (fileData.length() > 32000) + size_t packetLength = (size_t)1 + 5 + 1 + filename.length() + 1; + if (fileData.size() > 32000) isBigFile = true; // Clients before 2.14 didn't support large files. if (isClient() && m_versionId < CLVER_2_14) { if (m_versionId < CLVER_2_1) packetLength -= 5; // modTime isn't sent. - if (fileData.length() > 64000) - { - sendPacket(CString() >> (char)PLO_FILESENDFAILED << pFile); - return false; - } + if (fileData.size() > 64000) + return sendFailure("File too large for client version"); + isBigFile = false; } // If we are sending a big file, let the client know now. if (isBigFile) { - sendPacket(CString() >> (char)PLO_LARGEFILESTART << pFile); - sendPacket(CString() >> (char)PLO_LARGEFILESIZE >> (long long)fileData.length()); + sendPacket(CString() >> (char)PLO_LARGEFILESTART << filename); + sendPacket(CString() >> (char)PLO_LARGEFILESIZE >> (long long)fileData.size()); } // Send the file now. - while (fileData.length() != 0) + std::span fileDataSpan{ fileData }; + while (!fileDataSpan.empty()) { - int sendSize = clip(32000, 0, fileData.length()); - if (isClient() && m_versionId < CLVER_2_14) sendSize = fileData.length(); + int sendSize = std::clamp((int)fileDataSpan.size(), 0, 32000); + if (isClient() && m_versionId < CLVER_2_14) sendSize = fileData.size(); // Older client versions didn't send the modTime. if (isClient() && m_versionId < CLVER_2_1) { // We don't add a \n to the end of the packet, so subtract 1 from the packet length. sendPacket(CString() >> (char)PLO_RAWDATA >> (int)(packetLength - 1 + sendSize)); - sendPacket(CString() >> (char)PLO_FILE >> (char)pFile.length() << pFile << fileData.subString(0, sendSize), false); + sendPacket(CString() >> (char)PLO_FILE >> (char)filename.length() << filename << std::string_view{ fileDataSpan.subspan(0, sendSize) }, false); } else { sendPacket(CString() >> (char)PLO_RAWDATA >> (int)(packetLength + sendSize)); - sendPacket(CString() >> (char)PLO_FILE >> (long long)modTime >> (char)pFile.length() << pFile << fileData.subString(0, sendSize) << "\n", false); + sendPacket(CString() >> (char)PLO_FILE >> (long long)modTime >> (char)filename.length() << filename << std::string_view{ fileDataSpan.subspan(0, sendSize) } << "\n", false); } - fileData.removeI(0, sendSize); + fileDataSpan = fileDataSpan.subspan(sendSize); } // If we had sent a large file, let the client know we finished sending it. - if (isBigFile) sendPacket(CString() >> (char)PLO_LARGEFILEEND << pFile); + if (isBigFile) sendPacket(CString() >> (char)PLO_LARGEFILEEND << filename); return true; } -bool Player::testSign() -{ - CSettings& settings = m_server->getSettings(); - if (!settings.getBool("serverside", false)) return true; // TODO: NPC server check instead +/////////////////////////////////////////////////////////////////////////////// - // Check for sign collisions. - if ((m_character.sprite % 4) == 0) - { - auto level = getLevel(); - if (level) - { - const auto& signs = level->getSigns(); - for (const auto& sign: signs) - { - float signLoc[] = { (float)sign->getX(), (float)sign->getY() }; - if (getY() == signLoc[1] && inrange(getX(), signLoc[0] - 1.5f, signLoc[0] + 0.5f)) - { - sendPacket(CString() >> (char)PLO_SAY2 << sign->getUText().replaceAll("\n", "#b")); - } - } - } - } - return true; +bool Player::handleLogin(CString& pPacket) +{ + return false; } -void Player::testTouch() +bool Player::sendLogin() { -#ifdef V8NPCSERVER - static const int touchtestd[] = { 24, 16, 0, 32, 24, 56, 48, 32 }; - int dir = m_character.sprite % 4; - - int pixelX = getPixelX(); - int pixelY = getPixelY(); + // Load the account. + m_server->getAccountLoader().loadAccount(account.name, account); - auto level = getLevel(); - auto npcList = level->testTouch(pixelX + touchtestd[dir * 2], pixelY + touchtestd[dir * 2 + 1]); - for (const auto& npc: npcList) + // Check if they are ip-banned or not. + if (m_server->isIpBanned(m_playerSock->getRemoteIp()) && !account.hasRight(PLPERM_MODIFYSTAFFACCOUNT)) { - npc->queueNpcAction("npc.playertouchsme", this); + log::printLine(log::server, "** [Disconnect] '{}': Attempted login from banned IP: {}", account.name, m_playerSock->getRemoteIp()); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "You have been banned from this server."); + return false; } -#endif -} - -void Player::dropItemsOnDeath() -{ - if (!m_server->getSettings().getBool("dropitemsdead", true)) - return; - int mindeathgralats = m_server->getSettings().getInt("mindeathgralats", 1); - int maxdeathgralats = m_server->getSettings().getInt("maxdeathgralats", 50); - - // Determine how many gralats to remove from the account. - int drop_gralats = 0; - if (maxdeathgralats > 0) + // Check to see if the player is banned or not. + if (account.banned && !account.hasRight(PLPERM_MODIFYSTAFFACCOUNT)) { - drop_gralats = rand() % maxdeathgralats; - clip(drop_gralats, mindeathgralats, maxdeathgralats); - if (drop_gralats > m_character.gralats) drop_gralats = m_character.gralats; + log::printLine(log::server, "** [Disconnect] '{}': Attempted login from banned account. (IP: {})", account.name, m_playerSock->getRemoteIp()); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "You have been banned. Reason: " << string::join(string::fromCSV(account.banReason), "\r")); + return false; } - // Determine how many arrows and bombs to remove from the account. - int drop_arrows = rand() % 4; - int drop_bombs = rand() % 4; - if ((drop_arrows * 5) > m_character.arrows) drop_arrows = m_character.arrows / 5; - if ((drop_bombs * 5) > m_character.bombs) drop_bombs = m_character.bombs / 5; - - // Remove gralats/bombs/arrows. - m_character.gralats -= drop_gralats; - m_character.arrows -= (drop_arrows * 5); - m_character.bombs -= (drop_bombs * 5); - sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)PLPROP_RUPEESCOUNT >> (int)m_character.gralats >> (char)PLPROP_ARROWSCOUNT >> (char)m_character.arrows >> (char)PLPROP_BOMBSCOUNT >> (char)m_character.bombs); - - // Add gralats to the level. - while (drop_gralats != 0) + // If we are an RC, check to see if we can log in. + if (isRC() || isNC()) { - char item = 0; - if (drop_gralats % 100 != drop_gralats) - { - drop_gralats -= 100; - item = 19; - } - else if (drop_gralats % 30 != drop_gralats) + // Check and see if we are allowed in. + if (!isStaff() || !isAdminIp()) { - drop_gralats -= 30; - item = 2; + log::printLine(log::rc, "** [Disconnect] '{}': Attempted RC login.", account.name); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "You do not have RC rights."); + return false; } - else if (drop_gralats % 5 != drop_gralats) + } + + // Check to see if we can log in if we are a client. + if (isClient()) + { + // Staff only. + if (m_server->getSettings().get("onlystaff").value_or(false) && !isStaff()) { - drop_gralats -= 5; - item = 1; + log::printLine(log::rc, "** [Disconnect] '{}': Server is staff only.", account.name); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "This server is currently restricted to staff only."); + return false; } - else if (drop_gralats != 0) + + // Check and see if we are allowed in. + if (!isAdminIp()) { - --drop_gralats; - item = 0; + log::printLine(log::rc, "** [Disconnect] '{}': IP does not match the allowed list. (IP: {})", account.name, m_playerSock->getRemoteIp()); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Your IP doesn't match one of the allowed IPs for this account."); + return false; } - - float pX = getX() + 1.5f + (rand() % 8) - 2.0f; - float pY = getY() + 2.0f + (rand() % 8) - 2.0f; - - CString packet = CString() >> (char)PLI_ITEMADD >> (char)(pX * 2) >> (char)(pY * 2) >> (char)item; - packet.readGChar(); // So msgPLI_ITEMADD works. - - msgPLI_ITEMADD(packet); - sendPacket(CString() >> (char)PLO_ITEMADD << packet.subString(1)); } - // Add arrows and bombs to the level. - for (int i = 0; i < drop_arrows; ++i) - { - float pX = getX() + 1.5f + (rand() % 8) - 2.0f; - float pY = getY() + 2.0f + (rand() % 8) - 2.0f; - - CString packet = CString() >> (char)PLI_ITEMADD >> (char)(pX * 2) >> (char)(pY * 2) >> (char)4; // 4 = arrows - packet.readGChar(); // So msgPLI_ITEMADD works. + // Server Signature + // 0x49 (73) is used to tell the client that more than eight + // players will be playing. + sendPacket(CString() >> (char)PLO_SIGNATURE >> (char)73); - msgPLI_ITEMADD(packet); - sendPacket(CString() >> (char)PLO_ITEMADD << packet.subString(1)); - } - for (int i = 0; i < drop_bombs; ++i) + // TODO: Don't hardcode this. + if (m_server->getName().findi("login") > -1) { - float pX = getX() + 1.5f + (rand() % 8) - 2.0f; - float pY = getY() + 2.0f + (rand() % 8) - 2.0f; - - CString packet = CString() >> (char)PLI_ITEMADD >> (char)(pX * 2) >> (char)(pY * 2) >> (char)3; // 3 = bombs - packet.readGChar(); // So msgPLI_ITEMADD works. - - msgPLI_ITEMADD(packet); - sendPacket(CString() >> (char)PLO_ITEMADD << packet.subString(1)); + sendPacket(CString() >> (char)PLO_DISABLECLASSICMODE); + sendPacket(CString() >> (char)PLO_GHOSTICON >> (char)1); } -} - -bool Player::processChat(CString pChat) -{ - std::vector chatParse = pChat.tokenizeConsole(); - if (chatParse.size() == 0) return false; - bool processed = false; - bool setcolorsallowed = m_server->getSettings().getBool("setcolorsallowed", true); - if (chatParse[0] == "setnick") + if (isClient()) { - processed = true; - if ((int)difftime(time(0), m_lastNick) >= 10) - { - m_lastNick = time(0); - CString newName = pChat.subString(8).trim(); - - // Word filter. - int filter = m_server->getWordFilter().apply(this, newName, FILTER_CHECK_NICK); - if (filter & FILTER_ACTION_WARN) - { - setChat(newName); - return true; - } + // Tell the client if we have an npc-server, which disables certain features on the client (like sending NPC prop modifications). + // Later clients don't send this because all client-side functionality was removed. + // There isn't any harm in always sending it, though. + if (m_server->hasNPCServer()) + sendPacket(CString() >> (char)PLO_HASNPCSERVER); - setProps(CString() >> (char)PLPROP_NICKNAME >> (char)newName.length() << newName, PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - else - setChat("Wait 10 seconds before changing your nick again!"); + // This seems to inform the client that they have logged in. + sendPacket(CString() >> (char)PLO_UNKNOWN168); } - else if (chatParse[0] == "sethead" && chatParse.size() == 2) + + // Check if the account is already in use. + bool isGuest = account.loadOnly && account.communityName == "guest"; + if (!isGuest) { - if (!m_server->getSettings().getBool("setheadallowed", true)) return false; - processed = true; + auto& playerList = m_server->getPlayerList(); + for (auto& [pid, player] : playerList) + { + std::string otherAccount = player->account.name; + PlayerID otherID = player->getId(); - // Get the appropriate filesystem. - FileSystem* filesystem = m_server->getFileSystem(); - if (!m_server->getSettings().getBool("nofoldersconfig", false)) - filesystem = m_server->getFileSystem(FS_HEAD); + int meClient = ((m_type & PLTYPE_ANYCLIENT) ? 0 : ((m_type & PLTYPE_ANYRC) ? 1 : 2)); + int themClient = ((player->getType() & PLTYPE_ANYCLIENT) ? 0 : ((player->getType() & PLTYPE_ANYRC) ? 1 : 2)); - // Try to find the file. - CString file = filesystem->findi(chatParse[1]); - if (file.length() == 0) - { - int i = 0; - const char* ext[] = { ".png", ".mng", ".gif" }; - while (i < 3) + if (string::equalsi(otherAccount, account.name) && meClient == themClient && otherID != m_id) { - file = filesystem->findi(CString() << chatParse[1] << ext[i]); - if (file.length() != 0) + if (std::chrono::duration_cast(m_server->getFrameStartTime() - player->getLastData()) > 30s) + { + player->sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Someone else has logged into your account."); + player->disconnect(); + } + else { - chatParse[1] << ext[i]; - break; + log::printLine(log::rc, "** [Disconnect] '{}': Attempted double login.", account.name); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Account is already in use."); + return false; } - ++i; } } - - // Try to load the file. - if (file.length() != 0) - setProps(CString() >> (char)PLPROP_HEADGIF >> (char)(chatParse[1].length() + 100) << chatParse[1], PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - else - m_server->getServerList().sendPacket(CString() >> (char)SVO_GETFILE3 >> (short)m_id >> (char)0 >> (char)chatParse[1].length() << chatParse[1]); } - else if (chatParse[0] == "setbody" && chatParse.size() == 2) - { - if (m_server->getSettings().getBool("setbodyallowed", true) == false) return false; - processed = true; - - // Check to see if it is a default body. - bool isDefault = false; - for (unsigned int i = 0; i < sizeof(__defaultbodies) / sizeof(char*); ++i) - if (chatParse[1].match(CString(__defaultbodies[i])) == true) isDefault = true; - - // Don't search for the file if it is one of the defaults. This protects against - // malicious gservers. - if (isDefault) - { - setProps(CString() >> (char)PLPROP_BODYIMG >> (char)chatParse[1].length() << chatParse[1], PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - return false; - } - // Get the appropriate filesystem. - FileSystem* filesystem = m_server->getFileSystem(); - if (!m_server->getSettings().getBool("nofoldersconfig", false)) - filesystem = m_server->getFileSystem(FS_BODY); + // Tell the serverlist the player is logged in. + if (!isNPCServer()) + m_server->recordPlayerLoggedIn(shared_from_this()); - // Try to find the file. - CString file = filesystem->findi(chatParse[1]); - if (file.length() == 0) - { - int i = 0; - const char* ext[] = { ".png", ".mng", ".gif" }; - while (i < 3) - { - file = filesystem->findi(CString() << chatParse[1] << ext[i]); - if (file.length() != 0) - { - chatParse[1] << ext[i]; - break; - } - ++i; - } - } + // Set loaded to true so our account is saved when we leave. + // This also lets us send data. + m_loaded = true; - // Try to load the file. - if (file.length() != 0) - setProps(CString() >> (char)PLPROP_BODYIMG >> (char)chatParse[1].length() << chatParse[1], PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - else - m_server->getServerList().sendPacket(CString() >> (char)SVO_GETFILE3 >> (short)m_id >> (char)1 >> (char)chatParse[1].length() << chatParse[1]); - } - else if (chatParse[0] == "setsword" && chatParse.size() == 2) - { - if (!m_server->getSettings().getBool("setswordallowed", true)) return false; - processed = true; + // Mark our login time. + loginTime = m_server->getNWTime(); + lastDeadTime = loginTime; - // Check to see if it is a default sword. - bool isDefault = false; - for (unsigned int i = 0; i < sizeof(__defaultswords) / sizeof(char*); ++i) - if (chatParse[1].match(CString(__defaultswords[i])) == true) isDefault = true; + return true; +} - // Don't search for the file if it is one of the defaults. This protects against - // malicious gservers. - if (isDefault) - { - setProps(CString() >> (char)PLPROP_SWORDPOWER >> (char)(m_character.swordPower + 30) >> (char)chatParse[1].length() << chatParse[1], PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - return false; - } +/////////////////////////////////////////////////////////////////////////////// - // Get the appropriate filesystem. - FileSystem* filesystem = m_server->getFileSystem(); - if (!m_server->getSettings().getBool("nofoldersconfig", false)) - filesystem = m_server->getFileSystem(FS_SWORD); +// Exchange props with everybody on the server. +void Player::exchangeMyPropsWithOthers() +{ + // RC props are sent differently. + CString myRCProps; + myRCProps >> (char)PLO_ADDPLAYER >> (short)getId() >> (char)account.name.length() << account.name >> (char)PlayerProp::CURLEVEL << getProp().serialize() >> (char)PlayerProp::PLAYERLISTSTATUS << getProp().serialize() >> (char)PlayerProp::NICKNAME << getProp().serialize() >> (char)PlayerProp::COMMUNITYNAME << getProp().serialize(); - // Try to find the file. - CString file = filesystem->findi(chatParse[1]); - if (file.length() == 0) - { - int i = 0; - const char* ext[] = { ".png", ".mng", ".gif" }; - while (i < 3) - { - file = filesystem->findi(CString() << chatParse[1] << ext[i]); - if (file.length() != 0) - { - chatParse[1] << ext[i]; - break; - } - ++i; - } - } + CString toOthers = CString() >> (char)PLO_OTHERPLPROPS >> (short)m_id; + CString joinLevel = CString() >> (char)PlayerProp::JOINLEAVELVL >> (char)1; + CString myClientProps = (isClient() ? getPropsPacketFromList(loginPropsClientOthers) : getPropsPacketFromList(loginPropsRC)); - // Try to load the file. - if (file.length() != 0) - setProps(CString() >> (char)PLPROP_SWORDPOWER >> (char)(m_character.swordPower + 30) >> (char)chatParse[1].length() << chatParse[1], PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - else - m_server->getServerList().sendPacket(CString() >> (char)SVO_GETFILE3 >> (short)m_id >> (char)2 >> (char)chatParse[1].length() << chatParse[1]); - } - else if (chatParse[0] == "setshield" && chatParse.size() == 2) + CString rcsOnline; + auto& playerList = m_server->getPlayerList(); + for (const auto& [pid, player] : playerList) { - if (!m_server->getSettings().getBool("setshieldallowed", true)) return false; - processed = true; - - // Check to see if it is a default shield. - bool isDefault = false; - for (unsigned int i = 0; i < sizeof(__defaultshields) / sizeof(char*); ++i) - if (chatParse[1].match(CString(__defaultshields[i])) == true) isDefault = true; + if (player.get() == this) continue; - // Don't search for the file if it is one of the defaults. This protects against - // malicious gservers. - if (isDefault) - { - setProps(CString() >> (char)PLPROP_SHIELDPOWER >> (char)(m_character.shieldPower + 10) >> (char)chatParse[1].length() << chatParse[1], PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - return false; - } - - // Get the appropriate filesystem. - FileSystem* filesystem = m_server->getFileSystem(); - if (!m_server->getSettings().getBool("nofoldersconfig", false)) - filesystem = m_server->getFileSystem(FS_SHIELD); - - // Try to find the file. - CString file = filesystem->findi(chatParse[1]); - if (file.length() == 0) - { - int i = 0; - const char* ext[] = { ".png", ".mng", ".gif" }; - while (i < 3) - { - file = filesystem->findi(CString() << chatParse[1] << ext[i]); - if (file.length() != 0) - { - chatParse[1] << ext[i]; - break; - } - ++i; - } - } + // Don't send npc-control players to others + if (player->isNC()) continue; - // Try to load the file. - if (file.length() != 0) - setProps(CString() >> (char)PLPROP_SHIELDPOWER >> (char)(m_character.shieldPower + 10) >> (char)chatParse[1].length() << chatParse[1], PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); + // Send the other player my props. + bool sameLevel = (player->account.level == account.level); + if (player->isClient()) + player->sendPacket(CString() << toOthers << (sameLevel ? joinLevel : "") << myClientProps); else - m_server->getServerList().sendPacket(CString() >> (char)SVO_GETFILE3 >> (short)m_id >> (char)3 >> (char)chatParse[1].length() << chatParse[1]); - } - else if (chatParse[0] == "setskin" && chatParse.size() == 2 && setcolorsallowed) - { - processed = true; - - // id: 0 - if (chatParse[1].toLower() == "grey") chatParse[1] = "gray"; - signed char color = getColor(chatParse[1].toLower()); - if (color != -1) - { - m_character.colors[0] = color; - setProps(CString() >> (char)PLPROP_COLORS >> (char)m_character.colors[0] >> (char)m_character.colors[1] >> (char)m_character.colors[2] >> (char)m_character.colors[3] >> (char)m_character.colors[4], PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - } - else if (chatParse[0] == "setcoat" && chatParse.size() == 2 && setcolorsallowed) - { - processed = true; - - // id: 1 - if (chatParse[1].toLower() == "grey") chatParse[1] = "gray"; - signed char color = getColor(chatParse[1].toLower()); - if (color != -1) - { - m_character.colors[1] = color; - setProps(CString() >> (char)PLPROP_COLORS >> (char)m_character.colors[0] >> (char)m_character.colors[1] >> (char)m_character.colors[2] >> (char)m_character.colors[3] >> (char)m_character.colors[4], PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - } - else if (chatParse[0] == "setsleeves" && chatParse.size() == 2 && setcolorsallowed) - { - processed = true; - - // id: 2 - if (chatParse[1].toLower() == "grey") chatParse[1] = "gray"; - signed char color = getColor(chatParse[1].toLower()); - if (color != -1) - { - m_character.colors[2] = color; - setProps(CString() >> (char)PLPROP_COLORS >> (char)m_character.colors[0] >> (char)m_character.colors[1] >> (char)m_character.colors[2] >> (char)m_character.colors[3] >> (char)m_character.colors[4], PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - } - else if (chatParse[0] == "setshoes" && chatParse.size() == 2 && setcolorsallowed) - { - processed = true; - - // id: 3 - if (chatParse[1].toLower() == "grey") chatParse[1] = "gray"; - signed char color = getColor(chatParse[1].toLower()); - if (color != -1) - { - m_character.colors[3] = color; - setProps(CString() >> (char)PLPROP_COLORS >> (char)m_character.colors[0] >> (char)m_character.colors[1] >> (char)m_character.colors[2] >> (char)m_character.colors[3] >> (char)m_character.colors[4], PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - } - else if (chatParse[0] == "setbelt" && chatParse.size() == 2 && setcolorsallowed) - { - processed = true; - - // id: 4 - if (chatParse[1].toLower() == "grey") chatParse[1] = "gray"; - signed char color = getColor(chatParse[1].toLower()); - if (color != -1) - { - m_character.colors[4] = color; - setProps(CString() >> (char)PLPROP_COLORS >> (char)m_character.colors[0] >> (char)m_character.colors[1] >> (char)m_character.colors[2] >> (char)m_character.colors[3] >> (char)m_character.colors[4], PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - } - else if (chatParse[0] == "warpto") - { - processed = true; - - // To player - if (chatParse.size() == 2) - { - // Permission check. - if (!hasRight(PLPERM_WARPTOPLAYER) && !m_server->getSettings().getBool("warptoforall", false)) - { - setChat("(not authorized to warp)"); - return true; - } - - auto player = m_server->getPlayer(chatParse[1], PLTYPE_ANYCLIENT); - if (player && player->getLevel()) - warp(player->getLevel()->getLevelName(), player->getX(), player->getY()); - } - // To x/y location - else if (chatParse.size() == 3) - { - // Permission check. - if (!hasRight(PLPERM_WARPTO) && !m_server->getSettings().getBool("warptoforall", false)) - { - setChat("(not authorized to warp)"); - return true; - } - - setProps(CString() >> (char)PLPROP_X >> (char)(strtofloat(chatParse[1]) * 2) >> (char)PLPROP_Y >> (char)(strtofloat(chatParse[2]) * 2), PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - // To x/y level - else if (chatParse.size() == 4) - { - // Permission check. - if (!hasRight(PLPERM_WARPTO) && !m_server->getSettings().getBool("warptoforall", false)) - { - setChat("(not authorized to warp)"); - return true; - } - - warp(chatParse[3], (float)strtofloat(chatParse[1]), (float)strtofloat(chatParse[2])); - } - } - else if (chatParse[0] == "summon" && chatParse.size() == 2) - { - processed = true; - - // Permission check. - if (!hasRight(PLPERM_SUMMON)) - { - setChat("(not authorized to summon)"); - return true; - } - - auto p = m_server->getPlayer(chatParse[1], PLTYPE_ANYCLIENT); - if (p) p->warp(m_levelName, getX(), getY()); - } - else if (chatParse[0] == "unstick" || chatParse[0] == "unstuck") - { - if (chatParse.size() == 2 && chatParse[1] == "me") - { - processed = true; + player->sendPacket(myRCProps); - // Check if the player is in a jailed level. - std::vector jailList = m_server->getSettings().getStr("jaillevels").tokenize(","); - for (std::vector::iterator i = jailList.begin(); i != jailList.end(); ++i) - if (i->trim() == m_levelName) return false; - - int unstickTime = m_server->getSettings().getInt("unstickmetime", 30); - if ((int)difftime(time(0), m_lastMovement) >= unstickTime) - { - m_lastMovement = time(0); - CString unstickLevel = m_server->getSettings().getStr("unstickmelevel", "onlinestartlocal.nw"); - float unstickX = m_server->getSettings().getFloat("unstickmex", 30.0f); - float unstickY = m_server->getSettings().getFloat("unstickmey", 30.5f); - warp(unstickLevel, unstickX, unstickY); - setChat("Warped!"); - } - else - setChat(CString() << "Don't move for " << CString(unstickTime) << " seconds before doing '" << pChat << "'!"); - } - } - else if (pChat == "update level" && hasRight(PLPERM_UPDATELEVEL)) - { - processed = true; - if (auto level = getLevel(); level) - level->reload(); - } - else if (pChat == "showadmins") - { - processed = true; - - // Search through the player list for all RC's. - CString msg; - { - auto& playerList = m_server->getPlayerList(); - for (auto& [pid, player]: playerList) - { - // If an RC was found, add it to our string. - if (player->getType() & PLTYPE_ANYRC) - msg << (msg.length() == 0 ? "" : ", ") << player->getAccountName(); - } - } - if (msg.length() == 0) - msg << "(no one)"; - setChat(CString("admins: ") << msg); - } - else if (chatParse[0] == "showguild") - { - processed = true; - CString g = m_guild; - - // If a guild was specified, overwrite our guild with it. - if (chatParse.size() == 2) - g = chatParse[1]; - - if (g.length() != 0) + // Add Player / RC. + if (isClient()) { - CString msg; - { - auto& playerList = m_server->getPlayerList(); - for (auto& [pid, player]: playerList) - { - // If our guild matches, add it to our string. - if (player->getGuild() == g) - msg << (msg.length() == 0 ? "" : ", ") << CString(player->getNickname()).subString(0, player->getNickname().find('(')).trimI(); - } - } - if (msg.length() == 0) - msg << "(no one)"; - setChat(CString("members of '") << g << "': " << msg); + sendPacket(CString() >> (char)PLO_OTHERPLPROPS >> (short)player->getId() + << (sameLevel ? joinLevel : "") + << (player->isClient() ? player->getPropsPacketFromList(loginPropsClientOthers) : player->getPropsPacketFromList(loginPropsRC))); } - } - else if (pChat == "showkills") - { - processed = true; - setChat(CString() << "kills: " << CString((int)m_kills)); - } - else if (pChat == "showdeaths") - { - processed = true; - setChat(CString() << "deaths: " << CString((int)m_deaths)); - } - else if (pChat == "showonlinetime") - { - processed = true; - int seconds = m_onlineTime % 60; - int minutes = (m_onlineTime / 60) % 60; - int hours = m_onlineTime / 3600; - CString msg; - if (hours != 0) msg << CString(hours) << "h "; - if (minutes != 0 || hours != 0) msg << CString(minutes) << "m "; - msg << CString(seconds) << "s"; - setChat(CString() << "onlinetime: " << msg); - } - else if (chatParse[0] == "toguild:") - { - processed = true; - if (m_guild.length() == 0) return false; - - // Get the PM. - CString pm = pChat.text() + 8; - pm.trimI(); - if (pm.length() == 0) return false; - - // Send PM to guild members. - int num = 0; + else { - auto& playerList = m_server->getPlayerList(); - for (auto& [pid, player]: playerList) - { - // If our guild matches, send the PM. - if (player->getGuild() == m_guild) - { - player->sendPacket(CString() >> (char)PLO_PRIVATEMESSAGE >> (short)m_id << "\"\",\"Guild message:\",\"" << pm << "\""); - ++num; - } - } + // Get the other player's RC props. + sendPacket(CString() >> (char)PLO_ADDPLAYER >> (short)player->getId() >> (char)player->account.name.length() << player->account.name + >> (char)PlayerProp::CURLEVEL << player->getProp().serialize() + >> (char)PlayerProp::PLAYERLISTSTATUS << player->getProp().serialize() + >> (char)PlayerProp::NICKNAME << player->getProp().serialize() + >> (char)PlayerProp::COMMUNITYNAME << player->getProp().serialize()); } - - // Tell the player how many guild members received his message. - setChat(CString() << "(" << CString(num) << " guild member" << (num != 0 ? "s" : "") << " received your message)"); } - - return processed; } +/////////////////////////////////////////////////////////////////////////////// + bool Player::isAdminIp() { - std::vector adminIps = m_adminIp.tokenize(","); - for (std::vector::iterator i = adminIps.begin(); i != adminIps.end(); ++i) + for (const auto& ipMask : account.adminIpRange) { - if (m_accountIpStr.match(*i)) + if (ipMask == "0.0.0.0") + return true; + if (CString(account.ipAddress).match(ipMask)) return true; } - return false; } bool Player::isStaff() { - return m_server->isStaff(m_accountName); + return m_server->isStaff(account.name); } -/* - Player: Set Properties -*/ -bool Player::warp(const CString& pLevelName, float pX, float pY, time_t modTime) +bool Player::isJailed() { - CSettings& settings = m_server->getSettings(); - - // Save our current level. - auto currentLevel = m_currentLevel.lock(); - - // Find the level. - auto newLevel = Level::findLevel(pLevelName, m_server); + if (!m_server->cached.jailLevels) + return false; - // If we are warping to the same level, just update the player's location. - if (currentLevel != nullptr && newLevel == currentLevel) + const auto& levels = m_server->cached.jailLevels.getValue(); + auto jailed = std::ranges::find_if(levels, [this](const std::string& level) { - setProps(CString() >> (char)PLPROP_X >> (char)(pX * 2) >> (char)PLPROP_Y >> (char)(pY * 2), PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - return true; - } + return string::equalsi(account.level, string::trim(level)); + }); + + return jailed != levels.end(); +} - // Find the unstickme level. - auto unstickLevel = Level::findLevel(settings.getStr("unstickmelevel", "onlinestartlocal.nw"), m_server); - float unstickX = settings.getFloat("unstickmex", 30.0f); - float unstickY = settings.getFloat("unstickmey", 35.0f); +/////////////////////////////////////////////////////////////////////////////// - // Leave our current level. - leaveLevel(); +double Player::getCalculatedTileZ() const noexcept +{ + return account.character.localPixelZ / 16.0; +} - // See if the new level is on a gmap. - m_pmap.reset(); - if (newLevel) - m_pmap = newLevel->getMap(); +/////////////////////////////////////////////////////////////////////////////// - // Set x/y location. - float oldX = getX(), oldY = getY(); - setX(pX); - setY(pY); +void Player::setNick(CString pNickName, bool force) +{ + auto desiredNickname = string::trim(pNickName.toStringView()); + std::string prefix, guildName; - // Try warping to the new level. - bool warpSuccess = setLevel(pLevelName, modTime); - if (!warpSuccess) + // Determine the guild, if one was supplied. + auto guildStart = desiredNickname.find('('); + if (guildStart != std::string_view::npos) { - // Failed, so try warping back to our old level. - bool warped = true; - if (currentLevel == nullptr) warped = false; + auto guildEnd = desiredNickname.find(')', guildStart); + if (guildEnd == std::string_view::npos) + guildName = desiredNickname.substr(guildStart + 1); else - { - setX(oldX); - setY(oldY); - m_pmap = currentLevel->getMap(); - warped = setLevel(currentLevel->getLevelName()); - } - if (!warped) - { - // Failed, so try warping to the unstick level. If that fails, we disconnect. - if (unstickLevel == 0) return false; - - // Try to warp to the unstick me level. - setX(unstickX); - setY(unstickY); - m_pmap = unstickLevel->getMap(); - if (!setLevel(unstickLevel->getLevelName())) - return false; - } + guildName = desiredNickname.substr(guildStart + 1, guildEnd - guildStart - 1); } - return warpSuccess; -} - -std::shared_ptr Player::getLevel() const -{ - if (isHiddenClient()) return {}; - - auto pLevel = m_currentLevel.lock(); - if (pLevel) return pLevel; - - if (isClient() && m_server->warpPlayerToSafePlace(m_id)) + // If we are forcing a nickname change, do it now and return early. + if (force || m_isExternal || (guildName == "RC" && isRC())) { - return m_currentLevel.lock(); + account.character.nickName = pNickName.toString(); + this->m_guild = guildName; + return; } - return {}; -} - -bool Player::setLevel(const CString& pLevelName, time_t modTime) -{ - // Open Level - auto newLevel = Level::findLevel(pLevelName, m_server); - if (newLevel == nullptr) + // Determine the nickname part. + auto nickNamePart = desiredNickname; { - sendPacket(CString() >> (char)PLO_WARPFAILED << pLevelName); - return false; + // If a guild was supplied, remove it from the nickname. + if (guildStart != std::string_view::npos) + nickNamePart = desiredNickname.substr(0, guildStart); + + // Remove a * if it was supplied (as it denotes that the nickname is equal to the account name, which we will figure out later). + if (nickNamePart.starts_with('*')) + nickNamePart.remove_prefix(1); } - m_currentLevel = newLevel; + nickNamePart = string::trim(nickNamePart); + + // If the nickname is empty, set it to "unknown". + if (nickNamePart.empty()) + nickNamePart = "unknown"; + + // If the nickname is equal to the account name, set the prefix. + if (nickNamePart == account.name) + prefix = "*"; - // Check if the level is a singleplayer level. - // If so, see if we have been there before. If not, duplicate it. - if (newLevel->isSingleplayer()) + // If we had a guild, check our permissions. + if (!guildName.empty()) { - auto nl = (m_singleplayerLevels.find(newLevel->getLevelName()) != m_singleplayerLevels.end() ? m_singleplayerLevels[newLevel->getLevelName()] : nullptr); - if (nl == nullptr) + // Check if the player is in the guild. + auto guildManager = BabyDI::Get(); + if (guildManager->verifyPlayerInGuild(guildName, account.name, nickNamePart)) { - newLevel = newLevel->clone(); - m_currentLevel = newLevel; - m_singleplayerLevels[newLevel->getLevelName()] = newLevel; + account.character.nickName = std::format("{}{} ({})", prefix, nickNamePart, guildName); + m_guild = guildName; + return; } - else - m_currentLevel = nl; - } - // Check if the map is a group map. - if (auto map = m_pmap.lock(); map && map->isGroupMap()) - { - if (!m_levelGroup.isEmpty()) + // Not in a local guild, so see if we can ask the listserver if they are in a global guild. + bool askGlobal = m_server->getSettings().get("globalguilds").value_or(true); + if (!askGlobal) { - // If any players are in this level, they might have been cached on the client. Solve this by manually removing them. - auto& plist = newLevel->getPlayers(); - for (auto id: plist) - { - auto p = m_server->getPlayer(id); - sendPacket(p->getProps(0, 0) >> (char)PLPROP_CURLEVEL >> (char)(newLevel->getLevelName().length() + 1 + 7) << newLevel->getLevelName() << ".unknown" >> (char)PLPROP_X << p->getProp(PLPROP_X) >> (char)PLPROP_Y << p->getProp(PLPROP_Y)); - } - - // Set the correct level now. - const auto& levelName = newLevel->getLevelName(); - auto& groupLevels = m_server->getGroupLevels(); - auto [start, end] = groupLevels.equal_range(levelName.toString()); - while (start != end) - { - if (auto nl = start->second.lock(); nl) - { - if (nl->getLevelName() == levelName) - { - m_currentLevel = nl; - break; - } - } - ++start; - } - if (start == end) - { - newLevel = newLevel->clone(); - m_currentLevel = newLevel; - newLevel->setLevelName(levelName); - groupLevels.insert(std::make_pair(levelName.toString(), newLevel)); - } + // Check for whitelisted global guilds. + askGlobal = std::ranges::contains(string::split(m_server->getSettings().get("allowedglobalguilds").value_or(""), ","sv), guildName); } - } - // Add myself to the level playerlist. - newLevel->addPlayer(m_id); - m_levelName = newLevel->getLevelName(); - - // Tell the client their new level. - if (modTime == 0 || m_versionId < CLVER_2_1) - { - if (auto map = m_pmap.lock(); map && map->getType() == MapType::GMAP && m_versionId >= CLVER_2_1) + // See if it is a global guild. + if (askGlobal) { - sendPacket(CString() >> (char)PLO_PLAYERWARP2 >> (char)(getX() * 2) >> (char)(getY() * 2) >> (char)(getZ() * 2 + 50) >> (char)newLevel->getMapX() >> (char)newLevel->getMapY() << map->getMapName()); + m_server->getServerList().sendPacket( + CString() >> (char)SVO_VERIGUILD >> (short)m_id + >> (char)account.name.length() << account.name + >> (char)nickNamePart.length() << nickNamePart + >> (char)guildName.length() << guildName); } - else - sendPacket(CString() >> (char)PLO_PLAYERWARP >> (char)(getX() * 2) >> (char)(getY() * 2) << m_levelName); } - // Send the level now. - bool succeed = true; - if (m_versionId >= CLVER_2_1) - succeed = sendLevel(newLevel, modTime, false); - else - succeed = sendLevel141(newLevel, modTime, false); - - if (!succeed) - { - leaveLevel(); - sendPacket(CString() >> (char)PLO_WARPFAILED << pLevelName); - return false; - } + account.character.nickName = std::format("{}{}", prefix, nickNamePart); + m_guild.clear(); +} - // If the level is a sparring zone and you have 100 AP, change AP to 99 and - // the apcounter to 1. - if (newLevel->isSparringZone() && m_character.ap == 100) - { - m_character.ap = 99; - m_apCounter = 1; - setProps(CString() >> (char)PLPROP_ALIGNMENT >> (char)m_character.ap, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } +void Player::setChat(const CString& pChat) +{ + sendPropsFromResults(setPropWith(props::SetBy::SERVER, pChat.toString())); +} - // Inform everybody as to the client's new location. This will update the minimap. - CString minimap = this->getProps(0, 0) >> (char)PLPROP_CURLEVEL << this->getProp(PLPROP_CURLEVEL) >> (char)PLPROP_X << this->getProp(PLPROP_X) >> (char)PLPROP_Y << this->getProp(PLPROP_Y); - for (auto& [pid, player]: m_server->getPlayerList()) - { - if (pid == this->getId()) - continue; - if (auto map = m_pmap.lock(); map && map->isGroupMap() && m_levelGroup != player->getGroup()) - continue; +/////////////////////////////////////////////////////////////////////////////// - player->sendPacket(minimap); - } - //m_server->sendPacketToAll(this->getProps(0, 0) >> (char)PLPROP_CURLEVEL << this->getProp(PLPROP_CURLEVEL) >> (char)PLPROP_X << this->getProp(PLPROP_X) >> (char)PLPROP_Y << this->getProp(PLPROP_Y), this); +bool Player::deleteFlag(std::string_view flagName, bool sendToPlayer) +{ + if (sendToPlayer) + sendPacket(CString() >> (char)PLO_FLAGDEL << flagName); - return true; + return account.variables.remove(flagName); } -bool Player::sendLevel(std::shared_ptr pLevel, time_t modTime, bool fromAdjacent) +bool Player::setFlag(std::string_view flagPair, bool sendToPlayers) { - if (pLevel == nullptr) return false; - CSettings& settings = m_server->getSettings(); - - // Send Level - sendPacket(CString() >> (char)PLO_LEVELNAME << pLevel->getLevelName()); - time_t l_time = getCachedLevelModTime(pLevel.get()); - if (modTime == -1) modTime = pLevel->getModTime(); - if (l_time == 0) - { - if (modTime != pLevel->getModTime()) - { - sendPacket(CString() >> (char)PLO_RAWDATA >> (int)((1 + (64 * 64 * 2) + 1))); - sendPacket(CString() << pLevel->getBoardPacket()); + if (!flagPair.contains('=')) + return setFlag(flagPair, std::nullopt, sendToPlayers); - for (const auto& layers: pLevel->getLayers()) - { - if (layers.first == 0) continue; - CString layer = pLevel->getLayerPacket(layers.first); - sendPacket(CString() >> (char)PLO_RAWDATA >> (int)layer.length()); - sendPacket(layer); - } - } + auto separator = flagPair.find('='); + auto flagName = string::trim(flagPair.substr(0, separator)); + auto flagValue = string::trim(flagPair.substr(separator + 1)); + return setFlag(flagName, std::string{ flagValue }, sendToPlayers); +} - // Send links, signs, and mod time. - sendPacket(CString() >> (char)PLO_LEVELMODTIME >> (long long)pLevel->getModTime()); - sendPacket(CString() << pLevel->getLinksPacket()); - sendPacket(CString() << pLevel->getSignsPacket(this)); +bool Player::setFlag(std::string_view flagName, std::optional flagValue, bool sendToPlayer) +{ + if (!flagValue.has_value()) + { + sendPacket(CString() >> (char)PLO_FLAGSET << flagName); + account.variables.add(flagName, true); } - - // Send board changes, chests, horses, and baddies. - if (!fromAdjacent) + else { - sendPacket(CString() << pLevel->getBoardChangesPacket(l_time)); - sendPacket(CString() << pLevel->getChestPacket(this)); - sendPacket(CString() << pLevel->getHorsePacket()); - sendPacket(CString() << pLevel->getBaddyPacket(m_versionId)); + sendPacket(CString() >> (char)PLO_FLAGSET << flagName << "=" << flagValue.value()); + + std::string flag{ flagName }; + if (flagValue.value().empty()) + return deleteFlag(flag, sendToPlayer); + account.variables.add(flag, flagValue.value()); } + return true; +} - // If we are on a gmap, change our level back to the gmap. - if (auto map = m_pmap.lock(); map && map->getType() == MapType::GMAP) - sendPacket(CString() >> (char)PLO_LEVELNAME << map->getMapName()); +/////////////////////////////////////////////////////////////////////////////// - // Tell the client if there are any ghost players in the level. - // We don't support trial accounts so pass 0 (no ghosts) instead of 1 (ghosts present). - sendPacket(CString() >> (char)PLO_GHOSTICON >> (char)0); +bool Player::addWeapon(LevelItemType defaultWeapon) +{ + // Allow Default Weapons..? + if (!m_server->cached.enableDefaultWeapons.getValue()) + return false; - if (!fromAdjacent || !m_pmap.expired()) + auto weapon = m_server->getWeapon(LevelItem::getItemName(defaultWeapon)); + if (!weapon) { - // If we are the leader, send it now. - if (pLevel->isPlayerLeader(getId()) || pLevel->isSingleplayer() == true) - sendPacket(CString() >> (char)PLO_ISLEADER); + weapon = std::make_shared(defaultWeapon); + m_server->NC_AddWeapon(weapon); } - // Send new world time. - sendPacket(CString() >> (char)PLO_NEWWORLDTIME << CString().writeGInt4(m_server->getNWTime())); - if (!fromAdjacent || !m_pmap.expired()) - { - // Send NPCs. - if (auto map = m_pmap.lock(); map && map->getType() == MapType::GMAP) - { - sendPacket(CString() >> (char)PLO_SETACTIVELEVEL << map->getMapName()); + return this->addWeapon(weapon); +} - auto val = pLevel->getNpcsPacket(l_time, m_versionId); - sendPacket(val); +bool Player::addWeapon(std::string_view name) +{ + auto weapon = m_server->getWeapon(name); + return this->addWeapon(weapon); +} - /*sendPacket(CString() >> (char)PLO_SETACTIVELEVEL << m_pmap->getMapName()); - CString pmapLevels = m_pmap->getLevels(); - Level* tmpLvl; - while (pmapLevels.bytesLeft() > 0) - { - CString tmpLvlName = pmapLevels.readString("\n"); - tmpLvl = Level::findLevel(tmpLvlName.guntokenizeI(), server); - if (tmpLvl != NULL) - sendPacket(CString() << tmpLvl->getNpcsPacket(l_time, m_versionId)); - }*/ - } - else - { - sendPacket(CString() >> (char)PLO_SETACTIVELEVEL << pLevel->getLevelName()); - sendPacket(CString() << pLevel->getNpcsPacket(l_time, m_versionId)); - } - } +bool Player::addWeapon(std::shared_ptr weapon) +{ + if (weapon == nullptr) return false; - // Send connecting player props to players in nearby levels. - if (auto level = m_currentLevel.lock(); level && !level->isSingleplayer()) + // See if the player already has the weapon. + if (!account.hasWeapon(weapon->name)) { - // Send my props. - m_server->sendPacketToLevelArea(this->getProps(__getLogin, sizeof(__getLogin) / sizeof(bool)), this->shared_from_this(), { m_id }); - - // Get other player props. - if (auto map = m_pmap.lock(); map) - { - auto sgmap{ this->getMapPosition() }; - auto isGroupMap = map->isGroupMap(); - - for (const auto& [otherid, other]: m_server->getPlayerList()) - { - if (m_id == otherid) continue; - if (!other->isClient()) continue; + account.weapons.push_back(weapon->name); + if (m_id == 0) return true; + } - auto othermap = other->getMap().lock(); - if (!othermap || othermap != map) continue; - if (isGroupMap && this->getGroup() != other->getGroup()) continue; - - // Check if they are nearby before sending the packet. - auto ogmap{ other->getMapPosition() }; - if (abs(ogmap.first - sgmap.first) < 2 && abs(ogmap.second - sgmap.second) < 2) - this->sendPacket(other->getProps(__getLogin, sizeof(__getLogin) / sizeof(bool))); - } - } - else - { - for (auto otherid: level->getPlayers()) - { - if (m_id == otherid) continue; - auto other = m_server->getPlayer(otherid); - this->sendPacket(other->getProps(__getLogin, sizeof(__getLogin) / sizeof(bool))); - } - } - } - - return true; -} - -bool Player::sendLevel141(std::shared_ptr pLevel, time_t modTime, bool fromAdjacent) -{ - if (pLevel == nullptr) return false; - CSettings& settings = m_server->getSettings(); - - time_t l_time = getCachedLevelModTime(pLevel.get()); - if (modTime == -1) modTime = pLevel->getModTime(); - if (l_time != 0) - { - sendPacket(CString() << pLevel->getBoardChangesPacket(l_time)); - } - else - { - if (modTime != pLevel->getModTime()) - { - sendPacket(CString() >> (char)PLO_RAWDATA >> (int)(1 + (64 * 64 * 2) + 1)); - sendPacket(CString() << pLevel->getBoardPacket()); - - if (m_firstLevel) - sendPacket(CString() >> (char)PLO_LEVELNAME << pLevel->getLevelName()); - m_firstLevel = false; - - // Send links, signs, and mod time. - if (!settings.getBool("serverside", false)) // TODO: NPC server check instead. - { - sendPacket(CString() << pLevel->getLinksPacket()); - sendPacket(CString() << pLevel->getSignsPacket(this)); - } - sendPacket(CString() >> (char)PLO_LEVELMODTIME >> (long long)pLevel->getModTime()); - } - else - sendPacket(CString() >> (char)PLO_LEVELBOARD); - - if (!fromAdjacent) - { - sendPacket(CString() << pLevel->getBoardChangesPacket2(l_time)); - sendPacket(CString() << pLevel->getChestPacket(this)); - } - } - - // Send board changes, chests, horses, and baddies. - if (!fromAdjacent) - { - sendPacket(CString() << pLevel->getHorsePacket()); - sendPacket(CString() << pLevel->getBaddyPacket(m_versionId)); - } - - if (fromAdjacent == false) - { - // If we are the leader, send it now. - if (pLevel->isPlayerLeader(getId()) || pLevel->isSingleplayer() == true) - sendPacket(CString() >> (char)PLO_ISLEADER); - } - - // Send new world time. - sendPacket(CString() >> (char)PLO_NEWWORLDTIME << CString().writeGInt4(m_server->getNWTime())); - - // Send NPCs. - if (!fromAdjacent) - sendPacket(CString() << pLevel->getNpcsPacket(l_time, m_versionId)); - - // Send connecting player props to players in nearby levels. - if (!pLevel->isSingleplayer() && !fromAdjacent) - { - m_server->sendPacketToLevelArea(this->getProps(__getLogin, sizeof(__getLogin) / sizeof(bool)), this->shared_from_this(), { m_id }); - - for (auto id: pLevel->getPlayers()) - { - if (id == getId()) continue; - - auto player = m_server->getPlayer(id); - this->sendPacket(player->getProps(__getLogin, sizeof(__getLogin) / sizeof(bool))); - } - } - - return true; -} - -bool Player::leaveLevel(bool resetCache) -{ - // Make sure we are on a level first. - auto levelp = m_currentLevel.lock(); - if (!levelp) return true; - - // Save the time we left the level for the client-side caching. - bool found = false; - for (auto& cl: m_cachedLevels) - { - auto cllevel = cl->level.lock(); - if (cllevel == levelp) - { - cl->modTime = (resetCache ? 0 : time(0)); - found = true; - break; - } - } - if (!found) m_cachedLevels.push_back(std::make_unique(m_currentLevel, time(0))); - - // Remove self from list of players in level. - levelp->removePlayer(m_id); - - // Send PLO_ISLEADER to new level leader. - if (auto& levelPlayerList = levelp->getPlayers(); !levelPlayerList.empty()) - { - auto leader = m_server->getPlayer(levelPlayerList.front()); - leader->sendPacket(CString() >> (char)PLO_ISLEADER); - } - - // Tell everyone I left. - // This prop isn't used at all??? Maybe it is required for 1.41? - // if (m_pmap && m_pmap->getType() != MAPTYPE_GMAP) - { - m_server->sendPacketToLevelArea(this->getProps(0, 0) >> (char)PLPROP_JOINLEAVELVL >> (char)0, this->shared_from_this(), { m_id }); - - for (auto& [pid, player]: m_server->getPlayerList()) - { - if (pid == getId()) continue; - if (player->getLevel() != getLevel()) continue; - this->sendPacket(player->getProps(0, 0) >> (char)PLPROP_JOINLEAVELVL >> (char)0); - } - } - - // Set the level pointer to 0. - m_currentLevel.reset(); - - return true; -} - -time_t Player::getCachedLevelModTime(const Level* level) const -{ - for (auto& cl: m_cachedLevels) - { - auto cllevel = cl->level.lock(); - if (cllevel && cllevel.get() == level) - return cl->modTime; - } - return 0; -} - -void Player::resetLevelCache(const Level* level) -{ - for (auto& cl: m_cachedLevels) - { - auto cllevel = cl->level.lock(); - if (cllevel && cllevel.get() == level) - { - cl->modTime = 0; - return; - } - } -} - -std::pair Player::getMapPosition() const -{ - if (m_currentLevel.expired()) return { 0, 0 }; - if (m_pmap.expired()) return { 0, 0 }; - - auto level = getLevel(); - auto map = m_pmap.lock(); - if (!level || !map) return { 0, 0 }; - - switch (map->getType()) - { - case MapType::BIGMAP: - return { level->getMapX(), level->getMapY() }; - default: - case MapType::GMAP: - return { getProp(PLPROP_GMAPLEVELX).readGUChar(), getProp(PLPROP_GMAPLEVELY).readGUChar() }; - } - - return { 0, 0 }; -} - -void Player::setChat(const CString& pChat) -{ - setProps(CString() >> (char)PLPROP_CURCHAT >> (char)pChat.length() << pChat, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -void Player::setNick(CString pNickName, bool force) -{ - CString newNick, nick, guild; - - // Limit the nickname to 223 characters - if (pNickName.length() > 223) - pNickName = pNickName.subString(0, 223); - - int guild_start = pNickName.find('('); - int guild_end = pNickName.find(')', guild_start); - - // If the player ommitted the ), make sure the guild calculations will work. - if (guild_end == -1 && guild_start != -1) - guild_end = pNickName.length(); - - // If there was no guild, just use the given nickname. - if (guild_start == -1) - nick = pNickName.trim(); - else - { - // We have a guild. Separate the nickname from the guild. - nick = pNickName.subString(0, guild_start); - guild = pNickName.subString(guild_start + 1, guild_end - guild_start - 1); - nick.trimI(); - guild.trimI(); - if (guild[guild.length() - 1] == ')') - guild.removeI(guild.length() - 1); - } - - if (force || (guild == "RC" && isRC())) - { - m_character.nickName = pNickName.toString(); - this->m_guild = guild; - return; - } - - // If a player has put a * before his nick, remove it. - while (!nick.isEmpty() && nick[0] == '*') - nick.removeI(0, 1); - - // If the nickname is now empty, set it to unknown. - if (nick.isEmpty()) nick = "unknown"; - - // If the nickname is equal to the account name, add the *. - if (nick == m_accountName) - newNick = CString("*"); - - // Add the nick name. - newNick << nick; - - // If a guild was specified, add the guild. - if (guild.length() != 0) - { - // Read the guild list. - FileSystem guildFS; - guildFS.addDir("guilds"); - CString guildList = guildFS.load(CString() << "guild" << guild << ".txt"); - if (guildList.isEmpty()) - guildList = guildFS.load(CString() << "guild" << guild.replaceAll(" ", "_") << ".txt"); - - // Find the account in the guild list. - // Will also return -1 if the guild does not exist. - if (guildList.findi(m_accountName) != -1) - { - guildList.setRead(guildList.findi(m_accountName)); - CString line = guildList.readString("\n"); - line.removeAllI("\r"); - if (line.find(":") != -1) - { - std::vector line2 = line.tokenize(":"); - if ((line2[1])[0] == '*') line2[1].removeI(0, 1); - if ((line2[1]) == nick) // Use nick instead of newNick because nick doesn't include the * - { - newNick << " (" << guild << ")"; - m_character.nickName = newNick.toString(); - this->m_guild = guild; - return; - } - } - else - { - newNick << " (" << guild << ")"; - m_character.nickName = newNick.toString(); - this->m_guild = guild; - return; - } - } - else - m_character.nickName = newNick.toString(); - - // See if we can ask if it is a global guild. - bool askGlobal = m_server->getSettings().getBool("globalguilds", true); - if (!askGlobal) - { - // Check for whitelisted global guilds. - std::vector allowed = m_server->getSettings().getStr("allowedglobalguilds").tokenize(","); - if (std::find(allowed.begin(), allowed.end(), guild) != allowed.end()) - askGlobal = true; - } - - // See if it is a global guild. - if (askGlobal) - { - m_server->getServerList().sendPacket( - CString() >> (char)SVO_VERIGUILD >> (short)m_id >> (char)m_accountName.length() << m_accountName >> (char)newNick.length() << newNick >> (char)guild.length() << guild); - } - } - else - { - // Save it. - m_character.nickName = newNick.toString(); - this->m_guild.clear(); - } - - if (m_isExternal) - { - m_character.nickName = pNickName.toString(); - } -} - -bool Player::addWeapon(LevelItemType defaultWeapon) -{ - // Allow Default Weapons..? - CSettings& settings = m_server->getSettings(); - if (!settings.getBool("defaultweapons", true)) - return false; - - auto weapon = m_server->getWeapon(LevelItem::getItemName(defaultWeapon)); - if (!weapon) - { - weapon = std::make_shared(defaultWeapon); - m_server->NC_AddWeapon(weapon); - } - - return this->addWeapon(weapon); -} - -bool Player::addWeapon(const std::string& name) -{ - auto weapon = m_server->getWeapon(name); - return this->addWeapon(weapon); -} - -bool Player::addWeapon(std::shared_ptr weapon) -{ - if (weapon == nullptr) return false; - - // See if the player already has the weapon. - if (vecSearch(m_weaponList, weapon->getName()) == -1) - { - m_weaponList.push_back(weapon->getName()); - if (m_id == -1) return true; - - // Send weapon. - sendPacket(weapon->getWeaponPacket(m_versionId)); - } - - return true; -} - -bool Player::deleteWeapon(LevelItemType defaultWeapon) -{ - auto weapon = m_server->getWeapon(LevelItem::getItemName(defaultWeapon)); - return this->deleteWeapon(weapon); -} - -bool Player::deleteWeapon(const std::string& name) -{ - auto weapon = m_server->getWeapon(name); - return this->deleteWeapon(weapon); -} - -bool Player::deleteWeapon(std::shared_ptr weapon) -{ - if (weapon == nullptr) return false; - - // Remove the weapon. - if (vecRemove(m_weaponList, weapon->getName())) - { - if (m_id == -1) return true; - - // Send delete notice. - sendPacket(CString() >> (char)PLO_NPCWEAPONDEL << weapon->getName()); - } - - return true; -} - -void Player::disableWeapons() -{ - this->m_status &= ~PLSTATUS_ALLOWWEAPONS; - sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)PLPROP_STATUS << getProp(PLPROP_STATUS)); -} - -void Player::enableWeapons() -{ - this->m_status |= PLSTATUS_ALLOWWEAPONS; - sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)PLPROP_STATUS << getProp(PLPROP_STATUS)); -} - -void Player::freezePlayer() -{ - sendPacket(CString() >> (char)PLO_FREEZEPLAYER2); -} - -void Player::unfreezePlayer() -{ - sendPacket(CString() >> (char)PLO_UNFREEZEPLAYER); -} - -void Player::sendRPGMessage(const CString& message) -{ - sendPacket(CString() >> (char)PLO_RPGWINDOW << message.gtokenize()); -} - -void Player::sendSignMessage(const CString& message) -{ - sendPacket(CString() >> (char)PLO_SAY2 << message.replaceAll("\n", "#b")); -} - -void Player::setAni(CString gani) -{ - if (gani.length() > 223) - gani.remove(223); - - CString propPackage; - propPackage >> (char)PLPROP_GANI >> (char)gani.length() << gani; - setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -/* - Player: Flag Functions -*/ - -void Player::deleteFlag(const std::string& pFlagName, bool sendToPlayer) -{ - Account::deleteFlag(pFlagName); - - if (sendToPlayer) - { - sendPacket(CString() >> (char)PLO_FLAGDEL << pFlagName); - } -} - -void Player::setFlag(const std::string& pFlagName, const CString& pFlagValue, bool sendToPlayer) -{ - // Call Default Set Flag - Account::setFlag(pFlagName, pFlagValue); - - // Send to Player - if (sendToPlayer) - { - if (pFlagValue.isEmpty()) - sendPacket(CString() >> (char)PLO_FLAGSET << pFlagName); - else - sendPacket(CString() >> (char)PLO_FLAGSET << pFlagName << "=" << pFlagValue); - } -} - -/* - Player: Packet functions -*/ -bool Player::msgPLI_NULL(CString& pPacket) -{ - pPacket.setRead(0); - printf("Unknown Player Packet: %u (%s)\n", (unsigned int)pPacket.readGUChar(), pPacket.text() + 1); - for (int i = 0; i < pPacket.length(); ++i) printf("%02x ", (unsigned char)((pPacket.text())[i])); - printf("\n"); - - // If we are getting a whole bunch of invalid packets, something went wrong. Disconnect the player. - m_invalidPackets++; - if (m_invalidPackets > 5) - { - serverlog.out("Player %s is sending invalid packets.\n", m_character.nickName.c_str()); - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Disconnected for sending invalid packets."); - return false; - } - - return true; -} - -bool Player::msgPLI_LOGIN(CString& pPacket) -{ - // Read Player-Ip - m_accountIpStr = m_playerSock->getRemoteIp(); -#ifdef HAVE_INET_PTON - inet_pton(AF_INET, m_accountIpStr.text(), &m_accountIp); -#else - m_accountIp = inet_addr(m_accountIpStr.text()); -#endif - - // TODO(joey): Hijack type based on what graal sends, rather than use it directly. - - // Read Client-Type - serverlog.out(":: New login: "); - m_type = (1 << pPacket.readGChar()); - bool getKey = false; - switch (m_type) - { - case PLTYPE_CLIENT: - serverlog.append("Client\n"); - m_encryptionCodecIn.setGen(ENCRYPT_GEN_2); - break; - case PLTYPE_RC: - serverlog.append("RC\n"); - m_encryptionCodecIn.setGen(ENCRYPT_GEN_3); - break; - case PLTYPE_NPCSERVER: - serverlog.append("NPCSERVER\n"); - m_encryptionCodecIn.setGen(ENCRYPT_GEN_3); - break; - case PLTYPE_NC: - serverlog.append("NC\n"); - m_encryptionCodecIn.setGen(ENCRYPT_GEN_3); - getKey = false; - break; - case PLTYPE_CLIENT2: - serverlog.append("New Client (2.19 - 2.21, 3 - 3.01)\n"); - m_encryptionCodecIn.setGen(ENCRYPT_GEN_4); - break; - case PLTYPE_CLIENT3: - serverlog.append("New Client (2.22+)\n"); - m_encryptionCodecIn.setGen(ENCRYPT_GEN_5); - break; - case PLTYPE_RC2: - serverlog.append("New RC (2.22+)\n"); - m_encryptionCodecIn.setGen(ENCRYPT_GEN_5); - getKey = true; - break; - case PLTYPE_WEB: - serverlog.append("Web\n"); - m_encryptionCodecIn.setGen(ENCRYPT_GEN_1); - m_fileQueue.setCodec(ENCRYPT_GEN_1, m_encryptionKey); - getKey = false; - break; - default: - serverlog.append("Unknown (%d)\n", m_type); - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Your client type is unknown. Please inform the " << APP_VENDOR << " Team. Type: " << CString((int)m_type) << "."); - return false; - break; - } - - if (m_type == PLTYPE_CLIENT) - { - // Read Client-Version for v1.3 clients - m_version = pPacket.readChars(8); - m_versionId = getVersionID(m_version); - - if (m_versionId == CLVER_UNKNOWN) - { - m_encryptionCodecIn.setGen(ENCRYPT_GEN_3); - pPacket.setRead(1); - } - } - - if (m_versionId == CLVER_UNKNOWN) - { - // Get Iterator-Key - // 2.19+ RC and any client should get the key. - if ((isClient() && m_type != PLTYPE_WEB) || (isRC() && m_encryptionCodecIn.getGen() > ENCRYPT_GEN_3) || getKey == true) - { - m_encryptionKey = (unsigned char)pPacket.readGChar(); - - m_encryptionCodecIn.reset(m_encryptionKey); - if (m_encryptionCodecIn.getGen() > ENCRYPT_GEN_3) - m_fileQueue.setCodec(m_encryptionCodecIn.getGen(), m_encryptionKey); - } - - // Read Client-Version - m_version = pPacket.readChars(8); - m_versionId = getVersionIDByVersion(m_version); - } - - // Read Account & Password - m_accountName = pPacket.readChars(pPacket.readGUChar()); - CString password = pPacket.readChars(pPacket.readGUChar()); - - // Client Identity: win,"",02e2465a2bf38f8a115f6208e9938ac8,ff144a9abb9eaff4b606f0336d6d8bc5,"6.2 9200 " - // {platform}, {mobile provides 'dc:id2'}, {md5hash:harddisk-id}, {md5hash:network-id}, {uname(release, version)}, {android-id} - CString identity = pPacket.readString(""); - - //serverlog.out(" Key: %d\n", key); - serverlog.out(" Version: %s (%s)\n", m_version.text(), getVersionString(m_version, m_type)); - serverlog.out(" Account: %s\n", m_accountName.text()); - if (!identity.isEmpty()) - { - serverlog.out(" Identity: %s\n", identity.text()); - auto identityTokens = identity.tokenize(",", true); - m_os = identityTokens[0]; - } - - // Check for available slots on the server. - if (m_server->getPlayerList().size() >= (unsigned int)m_server->getSettings().getInt("maxplayers", 128)) - { - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "This server has reached its player limit."); - return false; - } - - // Check if they are ip-banned or not. - if (m_server->isIpBanned(m_playerSock->getRemoteIp()) && !hasRight(PLPERM_MODIFYSTAFFACCOUNT)) - { - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "You have been banned from this server."); - return false; - } - - // Check if the specified client is allowed access. - if (isClient()) - { - auto& allowedVersions = m_server->getAllowedVersions(); - bool allowed = false; - for (CString ver: allowedVersions) - { - if (ver.find(":") != -1) - { - CString ver1 = ver.readString(":").trim(); - CString ver2 = ver.readString("").trim(); - int aVersion[2] = { getVersionID(ver1), getVersionID(ver2) }; - if (m_versionId >= aVersion[0] && m_versionId <= aVersion[1]) - { - allowed = true; - break; - } - } - else - { - int aVersion = getVersionID(ver); - if (m_versionId == aVersion) - { - allowed = true; - break; - } - } - } - if (!allowed) - { - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Your client version is not allowed on this server.\rAllowed: " << m_server->getAllowedVersionString()); - return false; - } - } - - // Verify login details with the serverlist. - // TODO: localhost mode. - if (!m_server->getServerList().getConnected()) - { - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "The login server is offline. Try again later."); - return false; - } - - m_server->getServerList().sendLoginPacketForPlayer(shared_from_this(), password, identity); - return true; -} - -int Player::getVersionIDByVersion(const CString& versionInput) const -{ - if (isClient()) return getVersionID(versionInput); - else if (isNC()) - return getNCVersionID(versionInput); - else if (isRC()) - return getRCVersionID(versionInput); - else - return CLVER_UNKNOWN; -} - -bool Player::msgPLI_LEVELWARP(CString& pPacket) -{ - time_t modTime = 0; - - if (pPacket[0] - 32 == PLI_LEVELWARPMOD) - modTime = (time_t)pPacket.readGUInt5(); - - float loc[2] = { (float)(pPacket.readGChar() / 2.0f), (float)(pPacket.readGChar() / 2.0f) }; - CString newLevel = pPacket.readString(""); - warp(newLevel, loc[0], loc[1], modTime); - - return true; -} - -bool Player::msgPLI_BOARDMODIFY(CString& pPacket) -{ - CSettings& settings = m_server->getSettings(); - signed char loc[2] = { pPacket.readGChar(), pPacket.readGChar() }; - signed char dim[2] = { pPacket.readGChar(), pPacket.readGChar() }; - CString tiles = pPacket.readString(""); - - // Alter level data. - auto level = getLevel(); - if (level->alterBoard(tiles, loc[0], loc[1], dim[0], dim[1], this)) - m_server->sendPacketToOneLevel(CString() >> (char)PLO_BOARDMODIFY << (pPacket.text() + 1), level); - - if (loc[0] < 0 || loc[0] > 63 || loc[1] < 0 || loc[1] > 63) return true; - - // Older clients drop items clientside. - if (m_versionId < CLVER_2_1) - return true; - - // Lay items when you destroy objects. - short oldTile = (getLevel()->getTiles())[loc[0] + (loc[1] * 64)]; - bool bushitems = settings.getBool("bushitems", true); - bool vasesdrop = settings.getBool("vasesdrop", true); - int tiledroprate = settings.getInt("tiledroprate", 50); - LevelItemType dropItem = LevelItemType::INVALID; - - // Bushes, grass, swamp. - if ((oldTile == 2 || oldTile == 0x1a4 || oldTile == 0x1ff || - oldTile == 0x3ff) && - bushitems) - { - if (tiledroprate > 0) - { - if ((rand() % 100) < tiledroprate) - { - dropItem = LevelItem::getItemId(rand() % 6); - } - } - } - // Vase. - else if (oldTile == 0x2ac && vasesdrop) - dropItem = LevelItemType::HEART; - - // Send the item now. - // TODO: Make this a more generic function. - if (dropItem != LevelItemType::INVALID) - { - // TODO: GS2 replacement of item drops. How does it work? - CString packet = CString() >> (char)(loc[0] * 2) >> (char)(loc[1] * 2) >> (char)LevelItem::getItemTypeId(dropItem); - CString packet2 = CString() >> (char)PLI_ITEMADD << packet; - packet2.readGChar(); // So msgPLI_ITEMADD works. - - spawnLevelItem(packet2, false); - - if (getVersion() <= CLVER_5_12) - sendPacket(CString() >> (char)PLO_ITEMADD << packet); - } - - return true; -} - -bool Player::msgPLI_REQUESTUPDATEBOARD(CString& pPacket) -{ - // {130}{CHAR level length}{level}{INT5 modtime}{SHORT x}{SHORT y}{SHORT width}{SHORT height} - CString level = pPacket.readChars(pPacket.readGUChar()); - - time_t modTime = (time_t)pPacket.readGUInt5(); - - short x = pPacket.readGShort(); - short y = pPacket.readGShort(); - short w = pPacket.readGShort(); - short h = pPacket.readGShort(); - - // TODO: What to return? - serverlog.out(":: Received PLI_REQUESTUPDATEBOARD - level: %s - x: %d - y: %d - w: %d - h: %d - modtime: %d\n", level.text(), x, y, w, h, modTime); - - return true; -} - -bool Player::msgPLI_PLAYERPROPS(CString& pPacket) -{ - setProps(pPacket, PLSETPROPS_SETBYPLAYER | PLSETPROPS_FORWARD); - return true; -} - -bool Player::msgPLI_NPCPROPS(CString& pPacket) -{ - // Dont accept npc-properties from clients when an npc-server is present -#ifdef V8NPCSERVER - return true; -#endif - - unsigned int npcId = pPacket.readGUInt(); - CString npcProps = pPacket.readString(""); - - //printf( "npcId: %d\n", npcId ); - //printf( "pPacket: %s\n", npcProps.text()); - //for (int i = 0; i < pPacket.length(); ++i) printf( "%02x ", (unsigned char)pPacket[i] ); - //printf( "\n" ); - - auto level = getLevel(); - auto npc = m_server->getNPC(npcId); - if (!npc) - return true; - - if (npc->getLevel() != level) - return true; - - CString packet = CString() >> (char)PLO_NPCPROPS >> (int)npcId; - packet << npc->setProps(npcProps, m_versionId); - m_server->sendPacketToLevelOnlyGmapArea(packet, shared_from_this(), { m_id }); - - return true; -} - -bool Player::msgPLI_BOMBADD(CString& pPacket) -{ - // TODO(joey): gmap support - unsigned char loc[2] = { pPacket.readGUChar(), pPacket.readGUChar() }; - //float loc[2] = {(float)pPacket.readGChar() / 2.0f, (float)pPacket.readGChar() / 2.0f}; - unsigned char player_power = pPacket.readGUChar(); - unsigned char player = player_power >> 2; - unsigned char power = player_power & 0x03; - unsigned char timeToExplode = pPacket.readGUChar(); // How many 0.05 sec increments until it explodes. Defaults to 55 (2.75 seconds.) - - /* - printf("Place bomb\n"); - printf("Position: (%d, %d)\n", loc[0], loc[1]); - //printf("Position: (%0.2f, %0.2f)\n", loc[0], loc[1]); - printf("Player (?): %d\n", player); - printf("Bomb Power: %d\n", power); - printf("Bomb Explode Timer: %d\n", timeToExplode); - //for (int i = 0; i < pPacket.length(); ++i) printf( "%02x ", (unsigned char)pPacket[i] ); printf( "\n" ); - */ - - m_server->sendPacketToOneLevel(CString() >> (char)PLO_BOMBADD >> (short)m_id << (pPacket.text() + 1), m_currentLevel, { m_id }); - return true; -} - -bool Player::msgPLI_BOMBDEL(CString& pPacket) -{ - m_server->sendPacketToOneLevel(CString() >> (char)PLO_BOMBDEL << (pPacket.text() + 1), m_currentLevel, { m_id }); - return true; -} - -bool Player::msgPLI_TOALL(CString& pPacket) -{ - // Check if the player is in a jailed level. - std::vector jailList = m_server->getSettings().getStr("jaillevels").tokenize(","); - if (std::find_if(jailList.begin(), jailList.end(), [&levelName = this->m_levelName](CString& level) - { - return level.trim() == levelName; - }) != jailList.end()) - return true; - - CString message = pPacket.readString(pPacket.readGUChar()); - - // Word filter. - int filter = m_server->getWordFilter().apply(this, message, FILTER_CHECK_TOALL); - if (filter & FILTER_ACTION_WARN) - { - setChat(message); - return true; - } - - for (auto& [pid, player]: m_server->getPlayerList()) - { - if (pid == m_id) continue; - - // See if the player is allowing toalls. - unsigned char flags = strtoint(player->getProp(PLPROP_ADDITFLAGS)); - if (flags & PLFLAG_NOTOALL) continue; - - player->sendPacket(CString() >> (char)PLO_TOALL >> (short)m_id >> (char)message.length() << message); - } - return true; -} - -bool Player::msgPLI_HORSEADD(CString& pPacket) -{ - m_server->sendPacketToOneLevel(CString() >> (char)PLO_HORSEADD << (pPacket.text() + 1), m_currentLevel, { m_id }); - - float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; - unsigned char dir_bush = pPacket.readGUChar(); - char hdir = dir_bush & 0x03; - char hbushes = dir_bush >> 2; - CString image = pPacket.readString(""); - - auto level = getLevel(); - level->addHorse(image, loc[0], loc[1], hdir, hbushes); - return true; -} - -bool Player::msgPLI_HORSEDEL(CString& pPacket) -{ - m_server->sendPacketToOneLevel(CString() >> (char)PLO_HORSEDEL << (pPacket.text() + 1), m_currentLevel, { m_id }); - - float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; - - auto level = getLevel(); - level->removeHorse(loc[0], loc[1]); - return true; -} - -bool Player::msgPLI_ARROWADD(CString& pPacket) -{ - m_server->sendPacketToOneLevel(CString() >> (char)PLO_ARROWADD >> (short)m_id << (pPacket.text() + 1), m_currentLevel, { m_id }); - return true; -} - -bool Player::msgPLI_FIRESPY(CString& pPacket) -{ - m_server->sendPacketToOneLevel(CString() >> (char)PLO_FIRESPY >> (short)m_id << (pPacket.text() + 1), m_currentLevel, { m_id }); - return true; -} - -bool Player::msgPLI_THROWCARRIED(CString& pPacket) -{ - // TODO: Remove when an npcserver is created. - if (!m_server->getSettings().getBool("duplicatecanbecarried", false) && m_carryNpcId != 0) - { - auto npc = m_server->getNPC(m_carryNpcId); - if (npc) - { - m_carryNpcThrown = true; - - // Add the NPC back to the level if it never left. - auto level = getLevel(); - if (npc->getLevel() == level) - level->addNPC(npc); - } - } - m_server->sendPacketToOneLevel(CString() >> (char)PLO_THROWCARRIED >> (short)m_id << (pPacket.text() + 1), m_currentLevel, { m_id }); - return true; -} - -bool Player::removeItem(LevelItemType itemType) -{ - switch (itemType) - { - case LevelItemType::GREENRUPEE: // greenrupee - case LevelItemType::BLUERUPEE: // bluerupee - case LevelItemType::REDRUPEE: // redrupee - case LevelItemType::GOLDRUPEE: // goldrupee - { - int gralatsRequired; - if (itemType == LevelItemType::GOLDRUPEE) gralatsRequired = 100; - else if (itemType == LevelItemType::REDRUPEE) - gralatsRequired = 30; - else if (itemType == LevelItemType::BLUERUPEE) - gralatsRequired = 5; - else - gralatsRequired = 1; - - if (m_character.gralats >= gralatsRequired) - { - m_character.gralats -= gralatsRequired; - return true; - } - - return false; - } - - case LevelItemType::BOMBS: - { - if (m_character.bombs >= 5) - { - m_character.bombs -= 5; - return true; - } - return false; - } - - case LevelItemType::DARTS: - { - if (m_character.arrows >= 5) - { - m_character.arrows -= 5; - return true; - } - return false; - } - - case LevelItemType::HEART: - { - if (m_character.hitpoints > 1.0f) - { - m_character.hitpoints -= 1.0f; - return true; - } - return false; - } - -#ifndef V8NPCSERVER - // NOTE: not receiving PLI_ITEMTAKE for >2.31, so we will not remove the item - // same is true for sword/shield. assuming its true for the weapon-items, but - // its currently not tested. - case LevelItemType::GLOVE1: - case LevelItemType::GLOVE2: - { - if (m_character.glovePower > 1) - { - m_character.glovePower--; - return true; - } - return false; - } -#endif - - /* - case LevelItemType::BOW: // bow - case LevelItemType::BOMB: // bomb - return false; - - case LevelItemType::SUPERBOMB: // superbomb - case LevelItemType::FIREBALL: // fireball - case LevelItemType::FIREBLAST: // fireblast - case LevelItemType::NUKESHOT: // nukeshot - case LevelItemType::JOLTBOMB: // joltbomb - return false; - - case LevelItemType::SHIELD: // shield - case LevelItemType::MIRRORSHIELD: // mirrorshield - case LevelItemType::LIZARDSHIELD: // lizardshield - return false; - - case LevelItemType::SWORD: // sword - case LevelItemType::BATTLEAXE: // battleaxe - case LevelItemType::LIZARDSWORD: // lizardsword - case LevelItemType::GOLDENSWORD: // goldensword - return false; - - case LevelItemType::FULLHEART: // fullheart - return false; - */ - - case LevelItemType::SPINATTACK: - { - if (m_status & PLSTATUS_HASSPIN) - { - m_status &= ~PLSTATUS_HASSPIN; - return true; - } - return false; - } - } - - return false; -} - -bool Player::msgPLI_ITEMADD(CString& pPacket) -{ - return spawnLevelItem(pPacket, true); -} - -bool Player::spawnLevelItem(CString& pPacket, bool playerDrop) -{ - // TODO(joey): serverside item checking - float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; - unsigned char item = pPacket.readGUChar(); - - LevelItemType itemType = LevelItem::getItemId(item); - if (itemType != LevelItemType::INVALID) - { -#ifdef V8NPCSERVER - if (removeItem(itemType) || !playerDrop) - { -#endif - auto level = getLevel(); - if (level->addItem(loc[0], loc[1], itemType)) - { - m_server->sendPacketToOneLevel(CString() >> (char)PLO_ITEMADD << (pPacket.text() + 1), level, { m_id }); - } - else - { - sendPacket(CString() >> (char)PLO_ITEMDEL << (pPacket.text() + 1)); - } - -#ifdef V8NPCSERVER - } -#endif - } - - return true; -} - -bool Player::msgPLI_ITEMDEL(CString& pPacket) -{ - m_server->sendPacketToOneLevel(CString() >> (char)PLO_ITEMDEL << (pPacket.text() + 1), m_currentLevel, { m_id }); - - float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; - - // Remove the item from the level, getting the type of the item in the process. - auto level = getLevel(); - LevelItemType item = level->removeItem(loc[0], loc[1]); - if (item == LevelItemType::INVALID) return true; - - // If this is a PLI_ITEMTAKE packet, give the item to the player. - if (pPacket[0] - 32 == PLI_ITEMTAKE) - this->setProps(CString() << LevelItem::getItemPlayerProp(item, this), PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - - return true; -} - -bool Player::msgPLI_CLAIMPKER(CString& pPacket) -{ - // Get the player who killed us. - unsigned int pId = pPacket.readGUShort(); - auto killer = m_server->getPlayer(pId, PLTYPE_ANYCLIENT); - if (killer == nullptr || killer.get() == this) - return true; - - // Sparring zone rating code. - // Uses the glicko rating system. - auto level = getLevel(); - if (level == nullptr) return true; - if (level->isSparringZone()) - { - // Get some stats we are going to use. - // Need to parse the other player's PLPROP_RATING. - unsigned int otherRating = killer->getProp(PLPROP_RATING).readGUInt(); - float oldStats[4] = { m_eloRating, m_eloDeviation, (float)((otherRating >> 9) & 0xFFF), (float)(otherRating & 0x1FF) }; - - // If the IPs are the same, don't update the rating to prevent cheating. - if (CString(m_playerSock->getRemoteIp()) == CString(killer->getSocket()->getRemoteIp())) - return true; - - float gSpar[2] = { static_cast(1.0f / pow((1.0f + 3.0f * pow(0.0057565f, 2) * (pow(oldStats[3], 2)) / pow(3.14159265f, 2)), 0.5f)), //Winner - static_cast(1.0f / pow((1.0f + 3.0f * pow(0.0057565f, 2) * (pow(oldStats[1], 2)) / pow(3.14159265f, 2)), 0.5f)) }; //Loser - float ESpar[2] = { 1.0f / (1.0f + pow(10.0f, (-gSpar[1] * (oldStats[2] - oldStats[0]) / 400.0f))), //Winner - 1.0f / (1.0f + pow(10.0f, (-gSpar[0] * (oldStats[0] - oldStats[2]) / 400.0f))) }; //Loser - float dSpar[2] = { static_cast(1.0f / (pow(0.0057565f, 2) * pow(gSpar[0], 2) * ESpar[0] * (1.0f - ESpar[0]))), //Winner - static_cast(1.0f / (pow(0.0057565f, 2) * pow(gSpar[1], 2) * ESpar[1] * (1.0f - ESpar[1]))) }; //Loser - - float tWinRating = oldStats[2] + (0.0057565f / (1.0f / powf(oldStats[3], 2) + 1.0f / dSpar[0])) * (gSpar[0] * (1.0f - ESpar[0])); - float tLoseRating = oldStats[0] + (0.0057565f / (1.0f / powf(oldStats[1], 2) + 1.0f / dSpar[1])) * (gSpar[1] * (0.0f - ESpar[1])); - float tWinDeviation = powf((1.0f / (1.0f / powf(oldStats[3], 2) + 1 / dSpar[0])), 0.5f); - float tLoseDeviation = powf((1.0f / (1.0f / powf(oldStats[1], 2) + 1 / dSpar[1])), 0.5f); - - // Cap the rating. - tWinRating = clip(tWinRating, 0.0f, 4000.0f); - tLoseRating = clip(tLoseRating, 0.0f, 4000.0f); - tWinDeviation = clip(tWinDeviation, 50.0f, 350.0f); - tLoseDeviation = clip(tLoseDeviation, 50.0f, 350.0f); - - // Update the Ratings. - // setProps will cause it to grab the new rating and send it to everybody in the level. - // Therefore, just pass a dummy value. setProps doesn't alter your rating for packet hacking reasons. - if (oldStats[0] != tLoseRating || oldStats[1] != tLoseDeviation) - { - setRating((int)tLoseRating, (int)tLoseDeviation); - this->setProps(CString() >> (char)PLPROP_RATING >> (int)0, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - if (oldStats[2] != tWinRating || oldStats[3] != tWinDeviation) - { - killer->setRating((int)tWinRating, (int)tWinDeviation); - killer->setProps(CString() >> (char)PLPROP_RATING >> (int)0, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - this->setLastSparTime(time(0)); - killer->setLastSparTime(time(0)); - } - else - { - CSettings& settings = m_server->getSettings(); - - // Give a kill to the player who killed me. - if (!settings.getBool("dontchangekills", false)) - killer->setKills(killer->getProp(PLPROP_KILLSCOUNT).readGInt() + 1); - - // Now, adjust their AP if allowed. - if (settings.getBool("apsystem", true)) - { - signed char oAp = killer->getProp(PLPROP_ALIGNMENT).readGChar(); - - // If I have 20 or more AP, they lose AP. - if (oAp > 0 && m_character.ap > 19) - { - int aptime[] = { settings.getInt("aptime0", 30), settings.getInt("aptime1", 90), - settings.getInt("aptime2", 300), settings.getInt("aptime3", 600), - settings.getInt("aptime4", 1200) }; - oAp -= (((oAp / 20) + 1) * (m_character.ap / 20)); - if (oAp < 0) oAp = 0; - killer->setApCounter((oAp < 20 ? aptime[0] : (oAp < 40 ? aptime[1] : (oAp < 60 ? aptime[2] : (oAp < 80 ? aptime[3] : aptime[4]))))); - killer->setProps(CString() >> (char)PLPROP_ALIGNMENT >> (char)oAp, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - } - } - - return true; -} - -bool Player::msgPLI_BADDYPROPS(CString& pPacket) -{ - auto level = getLevel(); - if (level == nullptr) return true; - - unsigned char id = pPacket.readGUChar(); - CString props = pPacket.readString(""); - - // Get the baddy. - LevelBaddy* baddy = level->getBaddy(id); - if (baddy == 0) return true; - - // Get the leader. - auto leaderId = level->getPlayers().front(); - auto leader = m_server->getPlayer(leaderId); - - // Set the props and send to everybody in the level, except the leader. - m_server->sendPacketToOneLevel(CString() >> (char)PLO_BADDYPROPS >> (char)id << props, level, { leaderId }); - baddy->setProps(props); - return true; -} - -bool Player::msgPLI_BADDYHURT(CString& pPacket) -{ - auto level = getLevel(); - auto leaderId = level->getPlayers().front(); - auto leader = m_server->getPlayer(leaderId); - if (leader == nullptr) return true; - leader->sendPacket(CString() >> (char)PLO_BADDYHURT << (pPacket.text() + 1)); - return true; -} - -bool Player::msgPLI_BADDYADD(CString& pPacket) -{ - // Don't add a baddy if we aren't in a level! - if (m_currentLevel.expired()) - return true; - - float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; - unsigned char bType = pPacket.readGUChar(); - unsigned char bPower = pPacket.readGUChar(); - CString bImage = pPacket.readString(""); - bPower = MIN(bPower, 12); // Hard-limit to 6 hearts. - - // Fix the image for 1.41 clients. - if (!bImage.isEmpty() && getExtension(bImage).isEmpty()) - bImage << ".gif"; - - // Add the baddy. - auto level = getLevel(); - LevelBaddy* baddy = level->addBaddy(loc[0], loc[1], bType); - if (baddy == 0) return true; - - // Set the baddy props. - baddy->setRespawn(false); - baddy->setProps(CString() >> (char)BDPROP_POWERIMAGE >> (char)bPower >> (char)bImage.length() << bImage); - - // Send the props to everybody in the level. - m_server->sendPacketToOneLevel(CString() >> (char)PLO_BADDYPROPS >> (char)baddy->getId() << baddy->getProps(), level); - return true; -} - -bool Player::msgPLI_FLAGSET(CString& pPacket) -{ - CSettings& settings = m_server->getSettings(); - CString flagPacket = pPacket.readString(""); - CString flagName, flagValue; - if (flagPacket.find("=") != -1) - { - flagName = flagPacket.readString("="); - flagValue = flagPacket.readString(""); - - // If the value is empty, delete the flag instead. - if (flagValue.isEmpty()) - { - pPacket.setRead(1); // Don't let us read the packet ID. - return msgPLI_FLAGDEL(pPacket); - } - } - else - flagName = flagPacket; - - // Add a little hack for our special gr.strings. - if (flagName.find("gr.") != -1) - { - if (flagName == "gr.fileerror" || flagName == "gr.filedata") - return true; - - if (settings.getBool("flaghack_movement", true)) - { - // gr.x and gr.y are used by the -gr_movement NPC to help facilitate smoother - // movement amongst pre-2.3 clients. - if (flagName == "gr.x") - { - if (m_versionId >= CLVER_2_3) return true; - float pos = (float)atof(flagValue.text()); - if (pos != getX()) - m_grMovementPackets >> (char)PLPROP_X >> (char)(pos * 2.0f) << "\n"; - return true; - } - else if (flagName == "gr.y") - { - if (m_versionId >= CLVER_2_3) return true; - float pos = (float)atof(flagValue.text()); - if (pos != getY()) - m_grMovementPackets >> (char)PLPROP_Y >> (char)(pos * 2.0f) << "\n"; - return true; - } - else if (flagName == "gr.z") - { - if (m_versionId >= CLVER_2_3) return true; - float pos = (float)atof(flagValue.text()); - if (pos != getZ()) - m_grMovementPackets >> (char)PLPROP_Z >> (char)((pos + 0.5f) + 50.0f) << "\n"; - return true; - } - } - } - - // 2.171 clients didn't support this.strings and tried to set them as a - // normal flag. Don't allow that. - if (flagName.find("this.") != -1) return true; - - // Don't allow anybody to set read-only strings. - if (flagName.find("clientr.") != -1) return true; - if (flagName.find("serverr.") != -1) return true; - - // Server flags are handled differently than client flags. - if (flagName.find("server.") != -1) - { - m_server->setFlag(flagName.text(), flagValue); - return true; - } - - // Set Flag - this->setFlag(flagName.text(), flagValue, (m_versionId > CLVER_2_31)); - return true; -} - -bool Player::msgPLI_FLAGDEL(CString& pPacket) -{ - CString flagPacket = pPacket.readString(""); - std::string flagName; - if (flagPacket.find("=") != -1) - flagName = flagPacket.readString("=").trim().text(); - else - flagName = flagPacket.text(); - - // this.flags should never be in any server flag list, so just exit. - if (flagName.find("this.") != std::string::npos) return true; - - // Don't allow anybody to alter read-only strings. - if (flagName.find("clientr.") != std::string::npos) return true; - if (flagName.find("serverr.") != std::string::npos) return true; - - // Server flags are handled differently than client flags. - // TODO: check serveroptions - if (flagName.find("server.") != std::string::npos) - { - m_server->deleteFlag(flagName); - return true; - } - - // Remove Flag - this->deleteFlag(flagName); - return true; -} - -bool Player::msgPLI_OPENCHEST(CString& pPacket) -{ - unsigned char cX = pPacket.readGUChar(); - unsigned char cY = pPacket.readGUChar(); - - if (auto level = getLevel(); level) - { - auto chest = level->getChest(cX, cY); - if (chest.has_value()) - { - auto chestStr = level->getChestStr(chest.value()); - - if (!hasChest(chestStr)) - { - LevelItemType chestItem = chest.value()->getItemIndex(); - setProps(CString() << LevelItem::getItemPlayerProp(chestItem, this), PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - sendPacket(CString() >> (char)PLO_LEVELCHEST >> (char)1 >> (char)cX >> (char)cY); - m_chestList.push_back(chestStr); - } - } - } - - return true; -} - -bool Player::msgPLI_PUTNPC(CString& pPacket) -{ -#ifdef V8NPCSERVER - // Disable if we have an NPC-Server. - return true; -#endif - - CSettings& settings = m_server->getSettings(); - - CString nimage = pPacket.readChars(pPacket.readGUChar()); - CString ncode = pPacket.readChars(pPacket.readGUChar()); - float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; - - // See if putnpc is allowed. - if (!settings.getBool("putnpcenabled")) - return true; - - // Load the code. - CString code = m_server->getFileSystem(0)->load(ncode); - code.removeAllI("\r"); - - // Add NPC to level - m_server->addNPC(nimage, code, loc[0], loc[1], m_currentLevel, false, true); - - return true; -} - -bool Player::msgPLI_NPCDEL(CString& pPacket) -{ -#ifdef V8NPCSERVER - // Disable if we have an NPC-Server. - return true; -#endif - - unsigned int nid = pPacket.readGUInt(); - - // Remove the NPC. - if (auto npc = m_server->getNPC(nid); npc) - m_server->deleteNPC(npc, !m_currentLevel.expired()); + // Send weapon. + weapon->registerWeaponWithPlayer(shared_from_this()); return true; } -bool Player::msgPLI_WANTFILE(CString& pPacket) -{ - // Get file. - CString file = pPacket.readString(""); - - // If we are the 1.41 client, make sure a file extension was sent. - if (m_versionId < CLVER_2_1 && getExtension(file).isEmpty()) - file << ".gif"; - - //printf("WANTFILE: %s\n", file.text()); - - // Send file. - this->sendFile(file); - return true; -} - -bool Player::msgPLI_SHOWIMG(CString& pPacket) -{ - m_server->sendPacketToLevelArea(CString() >> (char)PLO_SHOWIMG >> (short)m_id << (pPacket.text() + 1), this->shared_from_this(), { m_id }); - return true; -} - -bool Player::msgPLI_HURTPLAYER(CString& pPacket) -{ - unsigned short pId = pPacket.readGUShort(); - char hurtdx = pPacket.readGChar(); - char hurtdy = pPacket.readGChar(); - unsigned char power = pPacket.readGUChar(); - unsigned int npc = pPacket.readGUInt(); - - // Get the victim. - auto victim = m_server->getPlayer(pId, PLTYPE_ANYCLIENT); - if (victim == 0) return true; - - // If they are paused, they don't get hurt. - if (victim->getProp(PLPROP_STATUS).readGChar() & PLSTATUS_PAUSED) return true; - - // Send the packet. - victim->sendPacket(CString() >> (char)PLO_HURTPLAYER >> (short)m_id >> (char)hurtdx >> (char)hurtdy >> (char)power >> (int)npc); - - return true; -} - -bool Player::msgPLI_EXPLOSION(CString& pPacket) -{ - CSettings& settings = m_server->getSettings(); - if (settings.getBool("noexplosions", false)) return true; - - unsigned char eradius = pPacket.readGUChar(); - float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; - unsigned char epower = pPacket.readGUChar(); - - // Send the packet out. - CString packet = CString() >> (char)PLO_EXPLOSION >> (short)m_id >> (char)eradius >> (char)(loc[0] * 2) >> (char)(loc[1] * 2) >> (char)epower; - m_server->sendPacketToOneLevel(packet, m_currentLevel, { m_id }); - - return true; -} - -bool Player::msgPLI_PRIVATEMESSAGE(CString& pPacket) -{ - // TODO(joey): Is this needed? - const int sendLimit = 4; - if (isClient() && (int)difftime(time(0), m_lastMessage) <= 4) - { - sendPacket(CString() >> (char)PLO_RC_ADMINMESSAGE << "Server message:\xa7You can only send messages once every " << CString((int)sendLimit) << " seconds."); - return true; - } - m_lastMessage = time(0); - - // Check if the player is in a jailed level. - std::vector jailList = m_server->getSettings().getStr("jaillevels").tokenize(","); - bool jailed = false; - for (std::vector::iterator i = jailList.begin(); i != jailList.end(); ++i) - { - if (i->trim() == m_levelName) - { - jailed = true; - break; - } - } - - // Get the players this message was addressed to. - std::vector pmPlayers; - auto pmPlayerCount = pPacket.readGUShort(); - for (auto i = 0; i < pmPlayerCount; ++i) - pmPlayers.push_back(static_cast(pPacket.readGUShort())); - - // Start constructing the message based on if it is a mass message or a private message. - CString pmMessageType("\"\","); - if (pmPlayerCount > 1) pmMessageType << "\"Mass message:\","; - else - pmMessageType << "\"Private message:\","; - - // Grab the message. - CString pmMessage = pPacket.readString(""); - int messageLimit = 1024; - if (pmMessage.length() > messageLimit) - { - sendPacket(CString() >> (char)PLO_RC_ADMINMESSAGE << "Server message:\xa7There is a message limit of " << CString((int)messageLimit) << " characters."); - return true; - } - - // Word filter. - pmMessage.guntokenizeI(); - if (isClient()) - { - int filter = m_server->getWordFilter().apply(this, pmMessage, FILTER_CHECK_PM); - if (filter & FILTER_ACTION_WARN) - { - sendPacket(CString() >> (char)PLO_RC_ADMINMESSAGE << "Word Filter:\xa7Your PM could not be sent because it was caught by the word filter."); - return true; - } - } - - // Always retokenize string, I don't believe our behavior is inline with official. It was escaping "\", so this unescapes that. - pmMessage.gtokenizeI(); - - // Send the message out. - for (auto pmPlayerId: pmPlayers) - { - if (pmPlayerId >= 16000) - { - auto pmPlayer = getExternalPlayer(pmPlayerId); - if (pmPlayer != nullptr) - { - serverlog.out("Sending PM to global player: %s.\n", pmPlayer->getNickname().c_str()); - pmMessage.guntokenizeI(); - pmExternalPlayer(pmPlayer->getServerName(), pmPlayer->getAccountName(), pmMessage); - pmMessage.gtokenizeI(); - } - } - else - { - auto pmPlayer = m_server->getPlayer(pmPlayerId, PLTYPE_ANYPLAYER | PLTYPE_NPCSERVER); - if (pmPlayer == nullptr || pmPlayer.get() == this) continue; - -#ifdef V8NPCSERVER - if (pmPlayer->isNPCServer()) - { - m_server->handlePM(this, pmMessage.guntokenize()); - continue; - } -#endif - - // Don't send to people who don't want mass messages. - if (pmPlayerCount != 1 && (pmPlayer->getProp(PLPROP_ADDITFLAGS).readGUChar() & PLFLAG_NOMASSMESSAGE)) - continue; - - // Jailed people cannot send PMs to normal players. - if (jailed && !isStaff() && !pmPlayer->isStaff()) - { - sendPacket(CString() >> (char)PLO_PRIVATEMESSAGE >> (short)pmPlayer->getId() << "\"Server Message:\"," - << "\"From jail you can only send PMs to admins (RCs).\""); - continue; - } - - // Send the message. - pmPlayer->sendPacket(CString() >> (char)PLO_PRIVATEMESSAGE >> (short)m_id << pmMessageType << pmMessage); - } - } - - return true; +bool Player::deleteWeapon(LevelItemType defaultWeapon) +{ + auto weapon = m_server->getWeapon(LevelItem::getItemName(defaultWeapon)); + return this->deleteWeapon(weapon); } -bool Player::msgPLI_NPCWEAPONDEL(CString& pPacket) +bool Player::deleteWeapon(std::string_view name) { - CString weapon = pPacket.readString(""); - for (std::vector::iterator i = m_weaponList.begin(); i != m_weaponList.end();) - { - if (*i == weapon) - { - i = m_weaponList.erase(i); - } - else - ++i; - } - return true; + auto weapon = m_server->getWeapon(name); + return this->deleteWeapon(weapon); } -bool Player::msgPLI_PACKETCOUNT(CString& pPacket) +bool Player::deleteWeapon(std::shared_ptr weapon) { - unsigned short count = pPacket.readGUShort(); - if (count != m_packetCount || m_packetCount > 10000) + if (weapon == nullptr) return false; + + // Remove the weapon. + if (std::erase(account.weapons, weapon->name) != 0) { - serverlog.out(":: Warning - Player %s had an invalid packet count.\n", m_accountName.text()); + if (m_id == 0) return true; + + // Send delete notice. + sendPacket(CString() >> (char)PLO_NPCWEAPONDEL << weapon->name); } - m_packetCount = 0; return true; } -bool Player::msgPLI_WEAPONADD(CString& pPacket) -{ -#ifdef V8NPCSERVER - // Disable if we have an NPC-Server. - return true; -#endif - - unsigned char type = pPacket.readGUChar(); +/////////////////////////////////////////////////////////////////////////////// - // Type 0 means it is a default weapon. - if (type == 0) - { - this->addWeapon(LevelItem::getItemId(pPacket.readGChar())); - } - // NPC weapons. - else - { - // Get the NPC id. - unsigned int npcId = pPacket.readGUInt(); - auto npc = m_server->getNPC(npcId); - if (npc == nullptr || npc->getLevel() == nullptr) - return true; +std::string Player::translate(std::string_view key) const +{ + auto translationManager = BabyDI::Get(); + return std::string{ translationManager->getText(getLanguage(), key) }; +} - // Get the name of the weapon. - CString name = npc->getWeaponName(); - if (name.length() == 0) - return true; +/////////////////////////////////////////////////////////////////////////////// - // See if we can find the weapon in the server weapon list. - auto weapon = m_server->getWeapon(name.toString()); +void Player::sendPrivateMessage(PlayerID from, std::string_view message) +{ + if (message.empty()) + return; - // If weapon is nullptr, that means the weapon was not found. Add the weapon to the list. - if (weapon == nullptr) - { - weapon = std::make_shared(name.toString(), npc->getImage(), std::string{ npc->getSource().getClientGS1() }, npc->getLevel()->getModTime(), true); - m_server->NC_AddWeapon(weapon); - } + auto convertedMessage = string::replace(message, "\n", "#b"); + auto lines = string::splitByString(convertedMessage, "#b"sv, false); + auto finalMessage = string::toCSV(lines, true); - // Check and see if the weapon has changed recently. If it has, we should - // send the new NPC to everybody on the server. After updating the script, of course. - if (weapon->getModTime() < npc->getLevel()->getModTime()) - { - // Update Weapon - weapon->updateWeapon(npc->getImage(), std::string{ npc->getSource().getClientGS1() }, npc->getLevel()->getModTime()); + sendPacket(CString() >> (char)PLO_PRIVATEMESSAGE >> (short)from << finalMessage); +} - // Send to Players - m_server->updateWeaponForPlayers(weapon); - } +/////////////////////////////////////////////////////////////////////////////// - // Send the weapon to the player now. - if (!hasWeapon(weapon->getName())) - this->addWeapon(weapon); - } +bool Player::warp(std::string_view levelName, const PixelPosition& position, std::optional clientCachedTime) +{ + if (auto level = m_server->getLoadedLevel(levelName, shared_from_this()); level != nullptr) + return enterLevel(level, position, clientCachedTime); + return false; +} - return true; +bool Player::warp(std::shared_ptr level, const PixelPosition& position, std::optional clientCachedTime) +{ + return enterLevel(level, position, clientCachedTime); } -bool Player::msgPLI_UPDATEFILE(CString& pPacket) +bool Player::enterLevel(std::shared_ptr level, const PixelPosition& position, std::optional clientCachedTime) { - FileSystem* fileSystem = m_server->getFileSystem(); + auto localPosition = toLocalPixelPosition(position); + auto mapPosition = toMapPosition(position); - // Get the packet data and file mod time. - time_t modTime = pPacket.readGUInt5(); - CString file = pPacket.readString(""); - time_t fModTime = fileSystem->getModTime(file); + // Sanity check. + if (!level->isGmap()) + mapPosition = { 0, 0 }; - // If we are the 1.41 client, make sure a file extension was sent. - if (m_versionId < CLVER_2_1 && getExtension(file).isEmpty()) - file << ".gif"; + return enterLevel(level, mapPosition, localPosition, clientCachedTime); +} - //printf("UPDATEFILE: %s\n", file.text()); +bool Player::enterLevel(std::shared_ptr level, const MapPosition& mapPosition, const LocalPixelPosition& position, std::optional clientCachedTime) +{ + auto now = m_server->getFrameStartTime(); - // Make sure it isn't one of the default files. - bool isDefault = false; - for (auto& defaultFile: __defaultfiles) + // If we are already on the level, set the position and abort. + if (account.level == level->levelName) { - if (file.match(defaultFile)) - { - isDefault = true; - break; - } - } + sendPropsFromResults( + setPropWith(props::SetBy::SERVER, position.x()), + setPropWith(props::SetBy::SERVER, position.y()), + setPropWith(props::SetBy::SERVER, mapPosition.x()), + setPropWith(props::SetBy::SERVER, mapPosition.y()) + ); - // If the file on disk is different, send it to the player. - file.setRead(0); - if (!isDefault) - { - if (std::difftime(modTime, fModTime) != 0) - return msgPLI_WANTFILE(file); + return true; } - if (m_versionId < CLVER_2_1) - sendPacket(CString() >> (char)PLO_FILESENDFAILED << file); - else - sendPacket(CString() >> (char)PLO_FILEUPTODATE << file); - return true; -} - -bool Player::msgPLI_ADJACENTLEVEL(CString& pPacket) -{ - time_t modTime = pPacket.readGUInt5(); - CString levelName = pPacket.readString(""); - CString packet; - auto adjacentLevel = Level::findLevel(levelName, m_server); + // Set position. + account.character.localPixelX = position.x(); + account.character.localPixelY = position.y(); + modTime[PROPID(PlayerProp::X)] = now; + modTime[PROPID(PlayerProp::X2)] = now; + modTime[PROPID(PlayerProp::Y)] = now; + modTime[PROPID(PlayerProp::Y2)] = now; - if (!adjacentLevel) - return true; + // Set map position. + account.character.mapX = mapPosition.x(); + account.character.mapY = mapPosition.y(); + modTime[PROPID(PlayerProp::GMAPLEVELX)] = now; + modTime[PROPID(PlayerProp::GMAPLEVELY)] = now; - if (m_currentLevel.expired()) - return false; + // Enter the level. + return enterLevel(level, clientCachedTime); +} - bool alreadyVisited = false; - for (const auto& cl: m_cachedLevels) - { - if (auto clevel = cl->level.lock(); clevel == adjacentLevel) - { - alreadyVisited = true; - break; - } - } +bool Player::enterLevel(std::shared_ptr level, std::optional clientCachedTime) +{ + return true; +} - // Send the level. - if (m_versionId >= CLVER_2_1) - sendLevel(adjacentLevel, modTime, true); - else - sendLevel141(adjacentLevel, modTime, true); +bool Player::leaveLevel() +{ + auto now = m_server->getFrameStartTime(); - // Set our old level back to normal. - //sendPacket(CString() >> (char)PLO_LEVELNAME << level->getLevelName()); - auto map = m_pmap.lock(); - if (map && map->getType() == MapType::GMAP) - sendPacket(CString() >> (char)PLO_LEVELNAME << map->getMapName()); - else - sendPacket(CString() >> (char)PLO_LEVELNAME << getLevel()->getLevelName()); + account.level.clear(); + account.character.mapX = 0; + account.character.mapY = 0; - if (getLevel()->isPlayerLeader(m_id)) - sendPacket(CString() >> (char)PLO_ISLEADER); + modTime[PROPID(PlayerProp::CURLEVEL)] = now; + modTime[PROPID(PlayerProp::GMAPLEVELX)] = now; + modTime[PROPID(PlayerProp::GMAPLEVELY)] = now; return true; } -bool Player::msgPLI_HITOBJECTS(CString& pPacket) +bool Player::leaveSubLevel(std::shared_ptr subLevel) { - float power = (float)pPacket.readGChar() / 2.0f; - float loc[2] = { (float)pPacket.readGChar() / 2.0f, (float)pPacket.readGChar() / 2.0f }; - int nid = (pPacket.bytesLeft() != 0) ? pPacket.readGUInt() : -1; - - // Construct the packet. - // {46}{SHORT player_id / 0 for NPC}{CHAR power}{CHAR x}{CHAR y}[{INT npc_id}] - CString nPacket; - nPacket >> (char)PLO_HITOBJECTS; - nPacket >> (short)((nid == -1) ? m_id : 0); // If it came from an NPC, send 0 for the id. - nPacket >> (char)(power * 2) >> (char)(loc[0] * 2) >> (char)(loc[1] * 2); - if (nid != -1) nPacket >> (int)nid; - - m_server->sendPacketToLevelOnlyGmapArea(nPacket, shared_from_this(), { m_id }); return true; } -bool Player::msgPLI_LANGUAGE(CString& pPacket) +bool Player::sendStaticLevelData(std::shared_ptr staticLevelData, std::shared_ptr subLevel, std::optional clientCachedTime) { - m_language = pPacket.readString(""); - if (m_language.isEmpty()) - m_language = "English"; return true; } -bool Player::msgPLI_TRIGGERACTION(CString& pPacket) +bool Player::sendDynamicLevelData(std::shared_ptr level, std::optional clientCachedTime) { - // Read packet data - unsigned int npcId = pPacket.readGUInt(); - float loc[2] = { - (float)pPacket.readGUChar() / 2.0f, - (float)pPacket.readGUChar() / 2.0f - }; - CString action = pPacket.readString("").trim(); + return true; +} - // Split action data into tokens - std::vector triggerActionData = action.gCommaStrTokens(); - if (triggerActionData.empty()) - { - return true; - } +bool Player::sendNearbyObjects(std::shared_ptr level) +{ + return true; +} - // Grab action name - std::string actualActionName = triggerActionData[0].toLower().toString(); +//////////////////////////////////////////////////////////////////////////////// - // (int)(loc[0]) % 64 == 0.0f, for gmap? - // TODO(joey): move into trigger command dispatcher, some use private player vars. - if (loc[0] == 0.0f && loc[1] == 0.0f) - { - CSettings& settings = m_server->getSettings(); +void Player::constructScriptParameters() +{ + if (!scriptParameters.empty()) + return; - if (settings.getBool("triggerhack_execscript", false)) - { - if (action.find("gr.es_clear") == 0) + scriptParameters.try_emplace("id", set_temporary, "id", gameValueGetter([this]() { return static_cast(getId()); }), GameValue::func_set{}); + scriptParameters.try_emplace("x", set_temporary, "x", + gameValueGetter([this]() { return account.character.getGlobalPosition().x() / 16.0; }), + gameValueSetter(this, PROPOPT(PlayerProp::X2), [this](const GameValue& value, std::optional) { account.character.localPixelX = value.get().value_or(0.0) * 16; })); + scriptParameters.try_emplace("y", set_temporary, "y", + gameValueGetter([this]() { return account.character.getGlobalPosition().y() / 16.0; }), + gameValueSetter(this, PROPOPT(PlayerProp::Y2), [this](const GameValue& value, std::optional) { account.character.localPixelY = value.get().value_or(0.0) * 16; })); + scriptParameters.try_emplace("z", set_temporary, "z", + gameValueGetter([this]() { return getCalculatedTileZ(); }), + gameValueSetter(this, PROPOPT(PlayerProp::Z2), [this](const GameValue& value, std::optional) { account.character.localPixelZ = value.get().value_or(0.0) * 16; })); + scriptParameters.try_emplace("fullhearts", set_temporary, "fullhearts", gameValueGetter(account.maxHitpoints), gameValueSetter(this, PROPOPT(PlayerProp::MAXPOWER), account.maxHitpoints)); + scriptParameters.try_emplace("maxhp", set_temporary, "maxhp", gameValueGetter(account.maxHitpoints), gameValueSetter(this, PROPOPT(PlayerProp::MAXPOWER), account.maxHitpoints)); + scriptParameters.try_emplace("hearts", set_temporary, "hearts", + gameValueGetter([this]() { return account.character.hitpointsInHalves / 2.0; }), + gameValueSetter(this, PROPOPT(PlayerProp::CURPOWER), [this](const GameValue& value, std::optional) { account.character.hitpointsInHalves = value.get().value_or(0.0) * 2; })); + scriptParameters.try_emplace("hp", set_temporary, "hp", + gameValueGetter([this]() { return account.character.hitpointsInHalves / 2.0; }), + gameValueSetter(this, PROPOPT(PlayerProp::CURPOWER), [this](const GameValue& value, std::optional) { account.character.hitpointsInHalves = value.get().value_or(0.0) * 2; })); + scriptParameters.try_emplace("mp", set_temporary, "mp", gameValueGetter(account.character.mp), gameValueSetter(this, PROPOPT(PlayerProp::MAGICPOINTS), account.character.mp)); + scriptParameters.try_emplace("ap", set_temporary, "ap", gameValueGetter(account.character.ap), gameValueSetter(this, PROPOPT(PlayerProp::ALIGNMENT), account.character.ap)); + scriptParameters.try_emplace("rupees", set_temporary, "rupees", gameValueGetter(account.character.gralats), gameValueSetter(this, PROPOPT(PlayerProp::RUPEESCOUNT), account.character.gralats)); + scriptParameters.try_emplace("gralats", set_temporary, "gralats", gameValueGetter(account.character.gralats), gameValueSetter(this, PROPOPT(PlayerProp::RUPEESCOUNT), account.character.gralats)); + scriptParameters.try_emplace("bombs", set_temporary, "bombs", gameValueGetter(account.character.bombs), gameValueSetter(this, PROPOPT(PlayerProp::BOMBSCOUNT), account.character.bombs)); + scriptParameters.try_emplace("darts", set_temporary, "darts", gameValueGetter(account.character.arrows), gameValueSetter(this, PROPOPT(PlayerProp::ARROWSCOUNT), account.character.arrows)); + scriptParameters.try_emplace("glovepower", set_temporary, "glovepower", gameValueGetter(account.character.glovePower), gameValueSetter(this, PROPOPT(PlayerProp::GLOVEPOWER), account.character.glovePower)); + scriptParameters.try_emplace("swordpower", set_temporary, "swordpower", gameValueGetter(account.character.swordPower), gameValueSetter(this, PROPOPT(PlayerProp::SWORDPOWER), account.character.swordPower)); + scriptParameters.try_emplace("shieldpower", set_temporary, "shieldpower", gameValueGetter(account.character.shieldPower), gameValueSetter(this, PROPOPT(PlayerProp::SHIELDPOWER), account.character.shieldPower)); + scriptParameters.try_emplace("shootpower", set_temporary, "shootpower", gameValueGetter(account.character.bowPower), gameValueSetter(this, PROPOPT(PlayerProp::GANI), account.character.bowPower)); + scriptParameters.try_emplace("headset", set_temporary, "headset", + gameValueGetter( + [this]() { - // Clear the parameters. - m_grExecParameterList.clear(); - return true; - } - else if (action.find("gr.es_set") == 0) - { - // Add the parameter to our saved parameter list. - CString parameters = action.subString(9); - if (m_grExecParameterList.isEmpty()) - m_grExecParameterList = parameters; - else - m_grExecParameterList << "," << parameters; - return true; - } - else if (action.find("gr.es_append") == 0) + int headSet = -1; + if (account.character.headImage.starts_with("head")) + string::toNumber(account.character.headImage.substr(4), headSet); + return static_cast(headSet); + }), + gameValueSetter(this, PROPOPT(PlayerProp::HEADGIF), + [this](const GameValue& value, std::optional) { - // Append doesn't add the beginning comma. - CString parameters = action.subString(9); - if (m_grExecParameterList.isEmpty()) - m_grExecParameterList = parameters; - else - m_grExecParameterList << parameters; - return true; - } - else if (action.find("gr.es") == 0) + auto headSet = std::clamp(static_cast(value.get().value_or(-1.0)), -1, 99); + if (headSet < 0) return; + account.character.headImage = std::format("head{}.{}", headSet, (m_server->Generation == ServerGeneration::ORIGINAL ? "gif" : "png")); + }) + ); + scriptParameters.try_emplace("sprite", set_temporary, "sprite", + gameValueGetter(account.character.sprite), + gameValueSetter(this, PROPOPT(PlayerProp::SPRITE), + [this](const GameValue& value, std::optional) { - std::vector actionParts = action.tokenize(","); - if (actionParts.size() != 1) + account.character.sprite = static_cast(value.get().value_or(0.0)); + if (account.character.sprite >= 4 && m_server->Generation != ServerGeneration::ORIGINAL) { - CString account = actionParts[1]; - CString wepname = CString() << "-gr_exec_" << removeExtension(actionParts[2]); - CString wepimage = "wbomb1.png"; - - // Load in all the execscripts. - FileSystem execscripts; - execscripts.addDir("execscripts"); - CString wepscript = execscripts.load(actionParts[2]); - - // Check to see if we were able to load the weapon. - if (wepscript.isEmpty()) - { - serverlog.out("Error: Player %s tried to load execscript %s, but the script was not found.\n", m_accountName.text(), actionParts[2].text()); - return true; - } - - // Format the weapon script properly. - wepscript.removeAllI("\r"); - wepscript.replaceAllI("\n", "\xa7"); - - // Replace parameters. - std::vector parameters = m_grExecParameterList.tokenize(","); - for (int i = 0; i < (int)parameters.size(); i++) - { - CString parmName = "*PARM" + CString(i); - wepscript.replaceAllI(parmName, parameters[i]); - } - - // Set all unreplaced parameters to 0. - for (int i = 0; i < 128; i++) - { - CString parmName = "*PARM" + CString(i); - wepscript.replaceAllI(parmName, "0"); - } - - // Create the weapon packet. - CString weapon_packet = CString() >> (char)PLO_NPCWEAPONADD >> (char)wepname.length() << wepname >> (char)0 >> (char)wepimage.length() << wepimage >> (char)1 >> (short)wepscript.length() << wepscript; - - // Send it to the players now. - if (actionParts[1] == "ALLPLAYERS") - m_server->sendPacketToType(PLTYPE_ANYCLIENT, weapon_packet); - else - { - auto p = m_server->getPlayer(actionParts[1], PLTYPE_ANYCLIENT); - if (p) p->sendPacket(weapon_packet); - } - m_grExecParameterList.clear(); + account.character.gani = std::format("def[{}]", account.character.sprite); + this->modTime[PROPID(PlayerProp::GANI)] = currentTime(); } - return true; - } - } - - if (settings.getBool("triggerhack_files", false)) - { - if (action.find("gr.appendfile") == 0) - { - int start = action.find(",") + 1; - if (start == 0) return true; - int finish = action.find(",", start) + 1; - if (finish == 0) return true; - - // Assemble the file name. - CString filename = action.subString(start, finish - start - 1); - filename.removeAllI("../"); - filename.removeAllI("..\\"); - - // Load the file. - CString file; - file.load(m_server->getServerPath() << "logs/" << filename); - - // Save the file. - file << action.subString(finish) << "\r\n"; - file.save(m_server->getServerPath() << "logs/" << filename); - return true; - } - else if (action.find("gr.writefile") == 0) + }) + ); + scriptParameters.try_emplace("dir", set_temporary, "dir", + gameValueGetter([this]() { return static_cast(account.character.direction); }), + gameValueSetter(this, PROPOPT(PlayerProp::SPRITE), + [this](const GameValue& value, std::optional) { - int start = action.find(",") + 1; - if (start == 0) return true; - int finish = action.find(",", start) + 1; - if (finish == 0) return true; - - // Grab the filename. - CString filename = action.subString(start, finish - start - 1); - filename.removeAllI("../"); - filename.removeAllI("..\\"); - - // Save the file. - CString file = action.subString(finish) << "\r\n"; - file.save(m_server->getServerPath() << "logs/" << filename); - return true; - } - else if (action.find("gr.readfile") == 0) - { - int start = action.find(",") + 1; - if (start == 0) return true; - int finish = action.find(",", start) + 1; - if (finish == 0) return true; - - // Grab the filename. - CString filename = action.subString(start, finish - start - 1); - filename.removeAllI("../"); - filename.removeAllI("..\\"); - - // Load the file. - CString filedata; - filedata.load(m_server->getServerPath() << "logs/" << filename); - filedata.removeAllI("\r"); - - // Tokenize it. - std::vector tokens = filedata.tokenize("\n"); - - // Find the line. - int id = rand() % 0xFFFF; - CString error; - size_t line = strtoint(action.subString(finish)); - if (line >= tokens.size()) - { - // We asked for a line that doesn't exist. Mark it as an error! - line = tokens.size() - 1; - error << CString("1,") + line; - } - - // Check if an error was set. - if (error.isEmpty()) - error = "0"; - - // Apply the ID. - error = CString(id) << "," << error; - - // Send it back to the player. - sendPacket(CString() >> (char)PLO_FLAGSET << "gr.fileerror=" << error); - sendPacket(CString() >> (char)PLO_FLAGSET << "gr.filedata=" << tokens[line]); - } - } + account.character.direction = std::clamp(static_cast(value.get().value_or(0.0)), 0_ui8, 3_ui8); + }) + ); + scriptParameters.try_emplace("hurtpower", set_temporary, "hurtpower", gameValueGetter(account.character.hurtDeltaInHalves), GameValue::func_set{}); + scriptParameters.try_emplace("attachid", set_temporary, "attachid", gameValueGetter(m_attachNPC), GameValue::func_set{}); + scriptParameters.try_emplace("attachtype", set_temporary, "attachtype", 1.0); + scriptParameters.try_emplace("saysnumber", set_temporary, "saysnumber", + gameValueGetter([this]() { return string::toDouble(account.character.chatMessage); }), + GameValue::func_set{} + ); + scriptParameters.try_emplace("lastdead", set_temporary, "lastdead", gameValueGetter(lastDeadTime), GameValue::func_set{}); + scriptParameters.try_emplace("logintime", set_temporary, "logintime", gameValueGetter(loginTime), GameValue::func_set{}); + scriptParameters.try_emplace("kills", set_temporary, "kills", + gameValueGetter(account.kills), + gameValueSetter(this, PROPOPT(PlayerProp::KILLSCOUNT), + [this](const GameValue& value, std::optional) + { + if (!m_server->getSettings().get("dontchangekills").value_or(false)) + account.kills = static_cast(std::max(0.0, value.get().value_or(0.0))); + }) + ); + scriptParameters.try_emplace("deaths", set_temporary, "deaths", + gameValueGetter(account.deaths), + gameValueSetter(this, PROPOPT(PlayerProp::DEATHSCOUNT), + [this](const GameValue& value, std::optional) + { + if (!m_server->getSettings().get("dontchangekills").value_or(false)) + account.deaths = static_cast(std::max(0.0, value.get().value_or(0.0))); + }) + ); + scriptParameters.try_emplace("rating", set_temporary, "rating", + gameValueGetter(account.eloRating), + gameValueSetter(this, PROPOPT(PlayerProp::RATING), + [this](const GameValue& value, std::optional) + { + account.eloRating = static_cast(std::max(0.0, value.get().value_or(0.0))); + }) + ); + scriptParameters.try_emplace("ratingd", set_temporary, "ratingd", + gameValueGetter(account.eloDeviation), + gameValueSetter(this, PROPOPT(PlayerProp::RATING), + [this](const GameValue& value, std::optional) + { + if (!m_server->getSettings().get("dontupdateratingd").value_or(false)) + account.eloDeviation = static_cast(std::clamp(value.get().value_or(0.0), 0.0, 350.0)); + }) + ); + + // trial, classic, vip, gold + scriptParameters.try_emplace("upgradestatus", set_temporary, "upgradestatus", + gameValueGetter([this]() { return isGuest() ? "trial"s : "classic"s; }), + GameValue::func_set{} + ); + + // GR extensions. + scriptParameters.try_emplace("carrysprite", set_temporary, "carrysprite", gameValueGetter(m_carrySprite), gameValueSetter(this, PROPOPT(PlayerProp::CARRYSPRITE), m_carrySprite)); +} + +//////////////////////////////////////////////////////////////////////////////// - if (settings.getBool("triggerhack_props", false)) - { - if (action.find("gr.attr") == 0) - { - int start = action.find(","); - if (start != -1) - { - int attrNum = strtoint(action.subString(7, start - 7)); - if (attrNum > 0 && attrNum <= 30) - { - ++start; - CString val = action.subString(start); - setProps(CString() >> (char)(__attrPackets[attrNum - 1]) >> (char)val.length() << val, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - } - } - if (action.find("gr.fullhearts") == 0) - { - int start = action.find(","); - if (start != -1) - { - ++start; - int hearts = strtoint(action.subString(start).trim()); - setProps(CString() >> (char)PLPROP_MAXPOWER >> (char)hearts, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - } - } - } +/* + Player: Packet functions +*/ +HandlePacketResult Player::msgPLI_NULL(CString& pPacket) +{ + pPacket.setRead(0); + printf("Unknown Player Packet: %u (%s)\n", (unsigned int)pPacket.readGUChar(), pPacket.text() + 1); + for (int i = 0; i < pPacket.length(); ++i) printf("%02x ", (unsigned char)((pPacket.text())[i])); + printf("\n"); - if (settings.getBool("triggerhack_levels", false)) - { - if (action.find("gr.updatelevel") == 0) - { - auto level = getLevel(); - int start = action.find(","); - if (start != -1) - { - ++start; - CString levelName = action.subString(start).trim(); - if (levelName.isEmpty()) - level->reload(); - else - { - LevelPtr targetLevel; - if (getExtension(levelName) == ".singleplayer") - targetLevel = m_singleplayerLevels[removeExtension(levelName)]; - else - targetLevel = m_server->getLevel(levelName.toString()); - if (targetLevel != nullptr) - targetLevel->reload(); - } - } - else - level->reload(); - } - } + // If we are getting a whole bunch of invalid packets, something went wrong. Disconnect the player. + InvalidPackets++; + if (InvalidPackets > 5) + { + log::printLine(log::server, "Player {} is sending invalid packets.", account.character.nickName); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Disconnected for sending invalid packets."); + return HandlePacketResult::Failed; } - bool handled = m_server->getTriggerDispatcher().execute(actualActionName, this, triggerActionData); + return HandlePacketResult::Handled; +} - if (!handled) +HandlePacketResult Player::msgPLI_LOGIN(CString& pPacket) +{ +#if defined(WOLFSSL_ENABLED) + if (!this->m_playerSock->webSocket && pPacket.findi("GET /") > -1 && pPacket.findi("HTTP/1.1\r\n") > -1) { - if (auto level = getLevel(); level) - { -#ifdef V8NPCSERVER - // Send to server scripts - auto npcList = level->findAreaNpcs(int(loc[0] * 16.0), int(loc[1] * 16.0), 8, 8); - for (auto npcTouched: npcList) - npcTouched->queueNpcTrigger(actualActionName, this, utilities::retokenizeArray(triggerActionData, 1)); + return msgWebSocketInit(pPacket); + } #endif - // Send to the level. - m_server->sendPacketToOneLevel(CString() >> (char)PLO_TRIGGERACTION >> (short)m_id << (pPacket.text() + 1), level, { m_id }); - } - } + return HandlePacketResult::Handled; +} - return true; +HandlePacketResult Player::msgWebSocketInit(CString& pPacket) +{ +#if defined(WOLFSSL_ENABLED) + CString webSocketKeyHeader = "Sec-WebSocket-Key:"; + if (pPacket.findi(webSocketKeyHeader) < 0) + { + CString simpleHtml = CString() << "" APP_VENDOR " " APP_NAME " v" APP_VERSION "

Welcome to " << m_server->getSettings().get("name").value_or("") << "!

" << m_server->getServerMessage().replaceAll("my server", m_server->getSettings().get("name").value_or("")).text() << "

Powered by " APP_VENDOR " " APP_NAME "
Programmed by " << CString(APP_CREDITS) << "

"; + CString webResponse = CString() << "HTTP/1.1 200 OK\r\nServer: " APP_VENDOR " " APP_NAME " v" APP_VERSION "\r\nContent-Length: " << CString(simpleHtml.length()) << "\r\nContent-Type: text/html\r\n\r\n" + << simpleHtml << "\r\n"; + unsigned int dsize = webResponse.length(); + this->m_playerSock->sendData(webResponse.text(), &dsize); + return HandlePacketResult::Bubble; + } + this->m_playerSock->webSocket = true; + // Get the WebSocket handshake key + pPacket.setRead(pPacket.findi(webSocketKeyHeader)); + CString webSocketKey = pPacket.readString("\r").subString(webSocketKeyHeader.length() + 1).trimI(); + + // Append GUID + webSocketKey << "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + // Calculate sha1 has of key + GUID and base64 encode it for sending back + webSocketKey.sha1I().base64encodeI(); + webSocketKeyHeader.clear(); + + CString webSockHandshake = CString() << "HTTP/1.1 101 Switching Protocols\r\n" + << "Upgrade: websocket\r\n" + << "Connection: Upgrade\r\n" + << "Sec-WebSocket-Protocol: binary\r\n" + << "Sec-WebSocket-Accept: " + << webSocketKey + << "\r\n\r\n"; + + unsigned int dsize = webSockHandshake.length(); + this->m_playerSock->sendData(webSockHandshake.text(), &dsize); +#endif + return HandlePacketResult::Bubble; } -bool Player::msgPLI_MAPINFO(CString& pPacket) +int Player::getVersionIDByVersion(const CString& versionInput) const { - // Don't know what this does exactly. Might be gmap related. - pPacket.readString(""); - return true; + if (isClient()) return getVersionID(versionInput); + else if (isNC()) + return getNCVersionID(versionInput); + else if (isRC()) + return getRCVersionID(versionInput); + else + return CLVER_UNKNOWN; } -void ShootPacketNew::debug() +HandlePacketResult Player::msgPLI_PLAYERPROPS(CString& pPacket) { - printf("Shoot: %f, %f, %f with gani %s: (len=%d)\n", (float)pixelx / 16.0f, (float)pixely / 16.0f, (float)pixelz / 16.0f, gani.text(), gani.length()); - printf("\t Offset: %d, %d\n", offsetx, offsety); - printf("\t Angle: %d\n", sangle); - printf("\t Z-Angle: %d\n", sanglez); - printf("\t Power: %d\n", speed); - printf("\t Gravity: %d\n", gravity); - printf("\t Gani: %s (len: %d)\n", gani.text(), gani.length()); - printf("\t Shoot Params: %s (len: %d)\n", shootParams.text(), shootParams.length()); + setPropsFromPacket(pPacket, props::SetBy::CLIENT); + return HandlePacketResult::Handled; } -CString ShootPacketNew::constructShootV1() const +HandlePacketResult Player::msgPLI_TOALL(CString& pPacket) { - CString ganiTemp{}; - ganiTemp << gani; - if (!ganiArgs.isEmpty()) + // Check if the player is in a jailed level. + if (isJailed()) + return HandlePacketResult::Handled; + + CString message = pPacket.readString(pPacket.readGUChar()); + + // Word filter. + int filter = m_server->getWordFilter().apply(this, message, FILTER_CHECK_TOALL); + if (filter & FILTER_ACTION_WARN) { - ganiTemp << "," << ganiArgs; + setChat(message); + return HandlePacketResult::Handled; } - CString packet; - packet.writeGInt(0); // shoot-id? - packet.writeGChar(pixelx / 16); - packet.writeGChar(pixely / 16); - packet.writeGChar((pixelz / 16) + 50); - packet.writeGChar(sangle); - packet.writeGChar(sanglez); - packet.writeGChar(speed); - packet.writeGChar(ganiTemp.length()); - packet.write(ganiTemp); - packet.writeGChar(shootParams.length()); - packet.write(shootParams); - return packet; -} -CString ShootPacketNew::constructShootV2() const -{ - CString ganiTemp{}; - ganiTemp << gani; - if (!ganiArgs.isEmpty()) + for (auto& [pid, player] : m_server->getPlayerList()) { - ganiTemp << "," << ganiArgs; + if (pid == m_id) continue; + + // See if the player is allowing toalls. + auto flags = player->getProp().value; + if (flags & PLFLAG_NOTOALL) continue; + + player->sendPacket(CString() >> (char)PLO_TOALL >> (short)m_id >> (char)message.length() << message); } - CString packet; - packet.writeGShort(pixelx); - packet.writeGShort(pixely); - packet.writeGShort(pixelz); - packet.writeChar(offsetx + 32); - packet.writeChar(offsety + 32); - packet.writeGChar(sangle); - packet.writeGChar(sanglez); - packet.writeGChar(speed); - packet.writeGChar(gravity); - packet.writeGShort(ganiTemp.length()); - packet.write(ganiTemp); - packet.writeGChar(shootParams.length()); - packet.write(shootParams); - return packet; + return HandlePacketResult::Handled; } -bool Player::msgPLI_SHOOT(CString& pPacket) +HandlePacketResult Player::msgPLI_PRIVATEMESSAGE(CString& pPacket) { - ShootPacketNew newPacket{}; - int unknown = pPacket.readGInt(); // May be a shoot id for the npc-server. (5/25d/19) joey: all my tests just give 0, my guess would be different types of projectiles but it never came to fruition - - newPacket.pixelx = 16 * pPacket.readGChar(); // 16 * ((float)pPacket.readGUChar() / 2.0f); - newPacket.pixely = 16 * pPacket.readGChar(); // 16 * ((float)pPacket.readGUChar() / 2.0f); - newPacket.pixelz = 16 * (pPacket.readGChar() - 50); // 16 * ((float)pPacket.readGUChar() / 2.0f); - // TODO: calculate offsetx from pixelx/pixely/ - level offset - newPacket.offsetx = 0; - newPacket.offsety = 0; - //if (newPacket.pixelx < 0) { - // newPacket.offsetx = -1; - //} - //if (newPacket.pixely < 0) { - // newPacket.offsety = -1; - //} - newPacket.sangle = pPacket.readGUChar(); // 0-pi = 0-220 - newPacket.sanglez = pPacket.readGUChar(); // 0-pi = 0-220 - newPacket.speed = pPacket.readGUChar(); // speed = pixels per 0.05 seconds. In gscript, each value of 1 translates to 44 pixels. - newPacket.gravity = 8; - newPacket.gani = pPacket.readChars(pPacket.readGUChar()); - unsigned char someParam = pPacket.readGUChar(); // This seems to be the length of shootparams, but the client doesn't limit itself and sends the overflow anyway - newPacket.shootParams = pPacket.readString(""); - - CString oldPacketBuf = CString() >> (char)PLO_SHOOT >> (short)m_id << newPacket.constructShootV1(); - CString newPacketBuf = CString() >> (char)PLO_SHOOT2 >> (short)m_id << newPacket.constructShootV2(); - - m_server->sendPacketToLevelArea(oldPacketBuf, shared_from_this(), { m_id }, [](const auto pl) - { - return pl->getVersion() < CLVER_5_07; - }); - m_server->sendPacketToLevelArea(newPacketBuf, shared_from_this(), { m_id }, [](const auto pl) - { - return pl->getVersion() >= CLVER_5_07; - }); - - // ActionProjectile on server. - // TODO(joey): This is accurate, but have not figured out power/zangle stuff yet. - - //this.speed = (this.power > 0 ? 0 : 20 * 0.05); - //this.horzspeed = cos(this.zangle) * this.speed; - //this.vertspeed = sin(this.zangle) * this.speed; - //this.newx = playerx + 1.5; // offset - //this.newy = playery + 2; // offset - //function CalcPos() { - // this.newx = this.newx + (cos(this.angle) * this.horzspeed); - // this.newy = this.newy - (sin(this.angle) * this.horzspeed); - // setplayerprop #c, Positions #v(this.newx), #v(this.newy); - // if (onwall(this.newx, this.newy)) { - // this.calcpos = 0; - // this.hittime = timevar2; - // } - //} + // Check if the player is in a jailed level. + bool jailed = isJailed(); - return true; -} + // Get the players this message was addressed to. + std::vector pmPlayers; + auto pmPlayerCount = pPacket.readGUShort(); + for (auto i = 0; i < pmPlayerCount; ++i) + pmPlayers.push_back(static_cast(pPacket.readGUShort())); -bool Player::msgPLI_SHOOT2(CString& pPacket) -{ - ShootPacketNew newPacket{}; - newPacket.pixelx = pPacket.readGUShort(); - newPacket.pixely = pPacket.readGUShort(); - newPacket.pixelz = pPacket.readGUShort(); - newPacket.offsetx = pPacket.readGChar(); // level offset x - newPacket.offsety = pPacket.readGChar(); // level offset y - newPacket.sangle = pPacket.readGUChar(); // 0-pi = 0-220 - newPacket.sanglez = pPacket.readGUChar(); // 0-pi = 0-220 - newPacket.speed = pPacket.readGUChar(); // speed = pixels per 0.05 seconds. In gscript, each value of 1 translates to 44 pixels. - newPacket.gravity = pPacket.readGUChar(); - newPacket.gani = pPacket.readChars(pPacket.readGUShort()); - unsigned char someParam = pPacket.readGUChar(); // This seems to be the length of shootparams, but the client doesn't limit itself and sends the overflow anyway - newPacket.shootParams = pPacket.readString(""); - - CString oldPacketBuf = CString() >> (char)PLO_SHOOT >> (short)m_id << newPacket.constructShootV1(); - CString newPacketBuf = CString() >> (char)PLO_SHOOT2 >> (short)m_id << newPacket.constructShootV2(); - - m_server->sendPacketToLevelArea(oldPacketBuf, shared_from_this(), { m_id }, [](const auto pl) - { - return pl->getVersion() < CLVER_5_07; - }); - m_server->sendPacketToLevelArea(newPacketBuf, shared_from_this(), { m_id }, [](const auto pl) - { - return pl->getVersion() >= CLVER_5_07; - }); + // Grab the message. + CString pmMessage = pPacket.readString(""); + int messageLimit = 1024; + if (pmMessage.length() > messageLimit) + { + sendPacket(CString() >> (char)PLO_RC_ADMINMESSAGE << "Server message:\xa7There is a message limit of " << CString((int)messageLimit) << " characters."); + return HandlePacketResult::Handled; + } - return true; -} + // Word filter. + pmMessage.guntokenizeI(); + if (isClient()) + { + int filter = m_server->getWordFilter().apply(this, pmMessage, FILTER_CHECK_PM); + if (filter & FILTER_ACTION_WARN) + { + sendPacket(CString() >> (char)PLO_RC_ADMINMESSAGE << "Word Filter:\xa7Your PM could not be sent because it was caught by the word filter."); + return HandlePacketResult::Handled; + } + } -bool Player::msgPLI_SERVERWARP(CString& pPacket) -{ - CString servername = pPacket.readString(""); - m_server->getServerLog().out("%s is requesting serverwarp to %s", m_accountName.text(), servername.text()); - m_server->getServerList().sendPacket(CString() >> (char)SVO_SERVERINFO >> (short)m_id << servername); - return true; -} + // Construct our message. + std::string constructedMessage = std::format("#b{}:#b{}", pmPlayerCount > 1 ? "Mass message" : "Private message", pmMessage.replaceAll("\n", "#b").toString()); -bool Player::msgPLI_PROCESSLIST(CString& pPacket) -{ - std::vector processes = pPacket.readString("").guntokenize().tokenize("\n"); - return true; + // Send the message out. + for (auto pmPlayerId : pmPlayers) + { + if (pmPlayerId >= PLAYERID_GEN_EXTERNAL) + { + auto pmPlayer = getExternalPlayer(pmPlayerId); + if (pmPlayer != nullptr) + { + log::printLine(log::server, "Sending PM to global player: {}.", pmPlayer->account.character.nickName); + + // Don't send the fully constructed message to external players, just keep it formatted as-is. + pmExternalPlayer(pmPlayer->getServerName(), pmPlayer->account.name, pmMessage); + } + } + else + { + auto pmPlayer = m_server->getPlayer(pmPlayerId, PLTYPE_ANYPLAYER | PLTYPE_NPCSERVER); + if (pmPlayer == nullptr || pmPlayer.get() == this) continue; + + // Don't send to people who don't want mass messages. + if (pmPlayerCount != 1 && (pmPlayer->getProp().value & PLFLAG_NOMASSMESSAGE)) + continue; + + // Jailed people cannot send PMs to normal players. + if (jailed && !isStaff() && !pmPlayer->isStaff()) + { + sendPrivateMessage(pmPlayer->getId(), pmPlayer->translate("Server Message:#bFrom jail you can only send PMs to admins (RCs).")); + continue; + } + + // Send the message. + pmPlayer->sendPrivateMessage(m_id, constructedMessage); + } + } + + return HandlePacketResult::Handled; } -bool Player::msgPLI_UNKNOWN46(CString& pPacket) +HandlePacketResult Player::msgPLI_PACKETCOUNT(CString& pPacket) { -#ifdef DEBUG - printf("TODO: Player::msgPLI_UNKNOWN46: "); - CString packet = pPacket.readString(""); - for (int i = 0; i < packet.length(); ++i) printf("%02x ", (unsigned char)packet[i]); - printf("\n"); -#endif - return true; + unsigned short count = pPacket.readGUShort(); + if (count != PacketCount || PacketCount > 10000) + { + log::printLine(log::server, "Warning - Player {} had an invalid packet count.", account.name); + } + PacketCount = 0; + + return HandlePacketResult::Handled; } -bool Player::msgPLI_RAWDATA(CString& pPacket) +HandlePacketResult Player::msgPLI_LANGUAGE(CString& pPacket) { - m_nextIsRaw = true; - m_rawPacketSize = pPacket.readGUInt(); - return true; + CString language = pPacket.readString(""); + if (language.isEmpty()) + language = "English"; + account.language = language.toString(); + return HandlePacketResult::Handled; } -bool Player::msgPLI_PROFILEGET(CString& pPacket) +HandlePacketResult Player::msgPLI_PROFILEGET(CString& pPacket) { // Send the packet ID for backwards compatibility. m_server->getServerList().sendPacket(CString() >> (char)SVO_GETPROF >> (short)m_id << pPacket); - return true; + return HandlePacketResult::Handled; } -bool Player::msgPLI_PROFILESET(CString& pPacket) +HandlePacketResult Player::msgPLI_PROFILESET(CString& pPacket) { CString acc = pPacket.readChars(pPacket.readGUChar()); - if (acc != m_accountName) return true; + if (acc != account.name) return HandlePacketResult::Handled; // Old gserver would send the packet ID with pPacket so, for // backwards compatibility, do that here. m_server->getServerList().sendPacket(CString() >> (char)SVO_SETPROF << pPacket); - return true; + return HandlePacketResult::Handled; } -bool Player::msgPLI_RC_UNKNOWN162(CString& pPacket) -{ - // Stub. - return true; -} +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/player/PlayerClient.cpp b/server/src/player/PlayerClient.cpp new file mode 100644 index 000000000..573ebe430 --- /dev/null +++ b/server/src/player/PlayerClient.cpp @@ -0,0 +1,2100 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// + +CString _zlibFix( + "//#CLIENTSIDE\xa7" + "if(playerchats) {\xa7" + " this.chr = {ascii(#e(0,1,#c)),0,0,0,0};\xa7" + " for(this.c=0;this.c=11);this.c++) {\xa7" + " this.chr[2] = ascii(#e(this.c,1,#c));\xa7" + " this.chr[3] += 1*(this.chr[2]==this.chr[0]);\xa7" + " if(!(this.chr[2] in {this.chr[0],this.chr[1]})) {\xa7" + " if(this.chr[1]==0) {\xa7" + " if(this.chr[2]!=this.chr[0]) this.chr[1]=this.chr[2];\xa7" + " } else break; //[A][B][C]\xa7" + " }\xa7" + " this.chr[4] += 1*(this.chr[2]==this.chr[1]);\xa7" + " if(this.chr[1]>0 && this.chr[3] in |2,10|) break; //[1=11 && this.chr[4]>1) break; //[A>=11][B>1]\xa7" + " }\xa7" + " if(this.c>0 && this.c == strlen(#c)) setplayerprop #c,\xa0#c\xa0; //Pad\xa7" + "}\xa7" +); + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +using PacketHandleFunc = HandlePacketResult (PlayerClient::*)(CString&); +using PacketHandleArray = std::array; + +static PacketHandleArray GeneratePacketHandlers() +{ + PacketHandleArray handlers{}; + handlers.fill(nullptr); + + handlers[PLI_LEVELWARP] = &PlayerClient::msgPLI_LEVELWARP; + handlers[PLI_BOARDMODIFY] = &PlayerClient::msgPLI_BOARDMODIFY; + handlers[PLI_NPCPROPS] = &PlayerClient::msgPLI_NPCPROPS; + handlers[PLI_BOMBADD] = &PlayerClient::msgPLI_BOMBADD; + handlers[PLI_BOMBDEL] = &PlayerClient::msgPLI_BOMBDEL; + handlers[PLI_HORSEADD] = &PlayerClient::msgPLI_HORSEADD; + handlers[PLI_HORSEDEL] = &PlayerClient::msgPLI_HORSEDEL; + handlers[PLI_ARROWADD] = &PlayerClient::msgPLI_ARROWADD; + handlers[PLI_FIRESPY] = &PlayerClient::msgPLI_FIRESPY; + handlers[PLI_THROWCARRIED] = &PlayerClient::msgPLI_THROWCARRIED; + handlers[PLI_ITEMADD] = &PlayerClient::msgPLI_ITEMADD; + handlers[PLI_ITEMDEL] = &PlayerClient::msgPLI_ITEMDEL; + handlers[PLI_CLAIMPKER] = &PlayerClient::msgPLI_CLAIMPKER; + handlers[PLI_BADDYPROPS] = &PlayerClient::msgPLI_BADDYPROPS; + handlers[PLI_BADDYHURT] = &PlayerClient::msgPLI_BADDYHURT; + handlers[PLI_BADDYADD] = &PlayerClient::msgPLI_BADDYADD; + handlers[PLI_FLAGSET] = &PlayerClient::msgPLI_FLAGSET; + handlers[PLI_FLAGDEL] = &PlayerClient::msgPLI_FLAGDEL; + handlers[PLI_OPENCHEST] = &PlayerClient::msgPLI_OPENCHEST; + handlers[PLI_PUTNPC] = &PlayerClient::msgPLI_PUTNPC; + handlers[PLI_NPCDEL] = &PlayerClient::msgPLI_NPCDEL; + handlers[PLI_WANTFILE] = &PlayerClient::msgPLI_WANTFILE; + handlers[PLI_SHOWIMGPLAYER] = &PlayerClient::msgPLI_SHOWIMGPLAYER; + handlers[PLI_HURTPLAYER] = &PlayerClient::msgPLI_HURTPLAYER; + handlers[PLI_EXPLOSION] = &PlayerClient::msgPLI_EXPLOSION; + handlers[PLI_PRIVATEMESSAGE] = &PlayerClient::msgPLI_PRIVATEMESSAGE; + handlers[PLI_NPCWEAPONDEL] = &PlayerClient::msgPLI_NPCWEAPONDEL; + handlers[PLI_LEVELWARPMOD] = &PlayerClient::msgPLI_LEVELWARP; // Shared with PLI_LEVELWARP + handlers[PLI_ITEMTAKE] = &PlayerClient::msgPLI_ITEMDEL; // Shared with PLI_ITEMDEL + handlers[PLI_WEAPONADD] = &PlayerClient::msgPLI_WEAPONADD; + handlers[PLI_UPDATEFILE] = &PlayerClient::msgPLI_UPDATEFILE; + handlers[PLI_ADJACENTLEVEL] = &PlayerClient::msgPLI_ADJACENTLEVEL; + handlers[PLI_HITOBJECTS] = &PlayerClient::msgPLI_HITOBJECTS; + handlers[PLI_TRIGGERACTION] = &PlayerClient::msgPLI_TRIGGERACTION; + handlers[PLI_TAMPERCHECK] = &PlayerClient::msgPLI_TAMPERCHECK; + handlers[PLI_SHOOT] = &PlayerClient::msgPLI_SHOOT; + handlers[PLI_SERVERWARP] = &PlayerClient::msgPLI_SERVERWARP; + handlers[PLI_PROCESSLIST] = &PlayerClient::msgPLI_PROCESSLIST; + handlers[PLI_ENTERLEVEL] = &PlayerClient::msgPLI_ENTERLEVEL; + handlers[PLI_VERIFYWANTSEND] = &PlayerClient::msgPLI_VERIFYWANTSEND; + handlers[PLI_SHOOT2] = &PlayerClient::msgPLI_SHOOT2; + handlers[PLI_REQUESTUPDATEBOARD] = &PlayerClient::msgPLI_REQUESTUPDATEBOARD; + handlers[PLI_UPDATEGANI] = &PlayerClient::msgPLI_UPDATEGANI; + handlers[PLI_UPDATESCRIPT] = &PlayerClient::msgPLI_UPDATESCRIPT; + handlers[PLI_UPDATEPACKAGEREQUESTFILE] = &PlayerClient::msgPLI_UPDATEPACKAGEREQUESTFILE; + handlers[PLI_UPDATECLASS] = &PlayerClient::msgPLI_UPDATECLASS; + + return handlers; +} + +/////////////////////////////////////////////////////////////////////////////// + +HandlePacketResult PlayerClient::handlePacket(std::optional id, CString& packet) +{ + static PacketHandleArray PacketHandlers = GeneratePacketHandlers(); + + auto handle = id.has_value() ? PacketHandlers[id.value()] : nullptr; + if (handle == nullptr) + return Player::handlePacket(id, packet); + + auto result = (this->*handle)(packet); + if (result == HandlePacketResult::Bubble) + return Player::handlePacket(id, packet); + + return result; +} + +/////////////////////////////////////////////////////////////////////////////// + +PlayerClient::PlayerClient(CSocket* pSocket, PlayerID pId) + : Player(pSocket, pId) +{ + m_lastMovement = m_lastSave = m_last1m = clock::now(); +} + +PlayerClient::~PlayerClient() +{ + cleanup(); +} + +void PlayerClient::cleanup() +{ + if (m_id > 0 && m_server != nullptr && m_loaded) + { + // Adjust carried NPC location. + if (m_carryNPC != 0) + { + if (auto npc = m_server->getNPC(m_carryNPC); npc) + { + npc->sendPropsFromResults( + npc->setPropWith(SetBy::CLIENT, static_cast(account.character.localPixelX + 8)), + npc->setPropWith(SetBy::CLIENT, static_cast(account.character.localPixelY + 16)) + ); + } + m_carryNPC = 0; + } + } + + // Clean up. + m_cachedStaticLevels.clear(); + m_cachedDynamicLevels.clear(); + m_singleplayerLevels.clear(); + + Player::cleanup(); +} + +/////////////////////////////////////////////////////////////////////////////// + +void PlayerClient::doMain() +{ + Player::doMain(); + + // Update the -gr_movement packets. + if (!m_grMovementPackets.isEmpty()) + { + if (!m_grMovementUpdated) + { + std::vector pack = m_grMovementPackets.tokenize("\n"); + for (auto& i : pack) + setPropsFromPacket(i, props::SetBy::CLIENT); + } + m_grMovementPackets.clear(42); + } + m_grMovementUpdated = false; +} + +bool PlayerClient::doTimedEvents() +{ + if (!Player::doTimedEvents()) + return false; + + auto currTime = m_server->getFrameStartTime(); + + // Increase online time. + ++account.onlineSeconds; + + // Disconnect if no data has been sent or received in 5 minutes. + if (timeDifference(currTime, m_lastData) > 300s) + { + log::printLine(log::server, "** [Disconnect] {}: Client has timed out.", account.name); + return false; + } + + // Disconnect if players are inactive. + if (m_server->cached.enableIdleDisconnect.getValue()) + { + int maxnomovement = m_server->cached.idleTimeoutSeconds.getValue(); + if (timeDifference(currTime, m_lastMovement) > std::chrono::seconds{maxnomovement} && timeDifference(currTime, m_lastChat) > std::chrono::seconds{maxnomovement}) + { + log::printLine(log::server, "** [Disconnect] {}: Client has been disconnected due to inactivity.", account.name); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "You have been disconnected due to inactivity."); + return false; + } + } + + // Increase player AP. + if (m_server->cached.enableAPSystem.getValue() && !m_currentLevel.expired()) + { + if (auto subLevel = getSubLevel(); subLevel != nullptr) + { + if (!(account.status & PLSTATUS_PAUSED) && !subLevel->isSparringZone) + { + if (account.apCounter > 0) + --account.apCounter; + else + { + if (account.character.ap < 100) + sendPropsFromResults(setPropWith(props::SetBy::SERVER, static_cast(account.character.ap + 1))); + + if (account.character.ap < 20) + account.apCounter = m_server->cached.apSystemThresholdSeconds[0].getValue(); + else if (account.character.ap < 40) + account.apCounter = m_server->cached.apSystemThresholdSeconds[1].getValue(); + else if (account.character.ap < 60) + account.apCounter = m_server->cached.apSystemThresholdSeconds[2].getValue(); + else if (account.character.ap < 80) + account.apCounter = m_server->cached.apSystemThresholdSeconds[3].getValue(); + else + account.apCounter = m_server->cached.apSystemThresholdSeconds[4].getValue(); + } + } + } + } + + // Do singleplayer level events. + { + for (auto& spLevel : m_singleplayerLevels) + { + auto& level = spLevel.second; + if (level) + level->doTimedEvents(); + } + } + + // Save player account every 5 minutes. + if (timeDifference(currTime, m_lastSave) > 300s) + { + m_lastSave = currTime; + if (isClient() && m_loaded && !account.loadOnly) + m_server->getAccountLoader().saveAccount(account); + } + + // Events that happen every minute. + if (timeDifference(currTime, m_last1m) > 60s) + { + m_last1m = currTime; + InvalidPackets = 0; + } + + return true; +} + +/////////////////////////////////////////////////////////////////////////////// + +bool PlayerClient::handleLogin(CString& pPacket) +{ + // Read Player-Ip + account.ipAddress = m_playerSock->getRemoteIp(); +#ifdef HAVE_INET_PTON + inet_pton(AF_INET, account.ipAddress.c_str(), &m_accountIp); +#else + m_accountIp = inet_addr(account.ipAddress.c_str()); +#endif + + // TODO(joey): Hijack type based on what graal sends, rather than use it directly. + m_type = (1 << pPacket.readGChar()); + + // Set the encryptions. + log::print(log::server, "New login: "); + switch (m_type) + { + case PLTYPE_CLIENT: + log::printLine(log::server, "Client"); + Encryption.setGen(ENCRYPT_GEN_2); + break; + case PLTYPE_CLIENT2: + log::printLine(log::server, "New Client (2.19 - 2.21, 3 - 3.01)"); + Encryption.setGen(ENCRYPT_GEN_4); + break; + case PLTYPE_CLIENT3: + log::printLine(log::server, "New Client (2.22+)"); + Encryption.setGen(ENCRYPT_GEN_5); + break; + case PLTYPE_WEB: + log::printLine(log::server, "Web"); + Encryption.setGen(ENCRYPT_GEN_1); + m_fileQueue.setCodec(ENCRYPT_GEN_1, 0); + break; + default: + log::printLine(log::server, "Unknown ({})", m_type); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Your client type is unknown. Please inform the " << APP_VENDOR << " Team. Type: " << CString((int)m_type) << "."); + return false; + } + + // Handle old clients. + if (m_type == PLTYPE_CLIENT) + { + // Read Client-Version for v1.3 clients + m_version = pPacket.readChars(8); + m_versionId = getVersionID(m_version); + + // 1.41 registers itself as PLTYPE_CLIENT, but does include an encryption key. + if (m_versionId == CLVER_UNKNOWN) + { + Encryption.setGen(ENCRYPT_GEN_3); + pPacket.setRead(1); + } + } + + // Handle newer clients. + if (m_versionId == CLVER_UNKNOWN) + { + m_encryptionKey = (unsigned char)pPacket.readGChar(); + + Encryption.reset(m_encryptionKey); + if (Encryption.getGen() > ENCRYPT_GEN_3) + m_fileQueue.setCodec(Encryption.getGen(), m_encryptionKey); + + // Read Client-Version + m_version = pPacket.readChars(8); + m_versionId = getVersionIDByVersion(m_version); + } + + // Read Account & Password + account.name = pPacket.readChars(pPacket.readGUChar()).toString(); + CString password = pPacket.readChars(pPacket.readGUChar()); + + // Client Identity: win,"",02e2465a2bf38f8a115f6208e9938ac8,ff144a9abb9eaff4b606f0336d6d8bc5,"6.2 9200 " + // {platform}, {mobile provides 'dc:id2'}, {md5hash:harddisk-id}, {md5hash:network-id}, {uname(release, version)}, {android-id} + CString identity = pPacket.readString(""); + + { + auto indent = log::server.indent(); + + //log::printLine(log::server, "Key: {}", key); + log::printLine(log::server, "Version: {} ({})", m_version, getVersionString(m_version, m_type)); + log::printLine(log::server, "Account: {}", account.name); + if (!identity.isEmpty()) + { + log::printLine(log::server, "Identity: {}", identity); + auto identityTokens = identity.tokenize(",", true); + m_os = identityTokens[0]; + } + } + + // Check if the specified client is allowed access. + { + auto& allowedVersions = m_server->getAllowedVersions(); + bool allowed = false; + for (CString ver : allowedVersions) + { + if (ver.find(":") != -1) + { + CString ver1 = ver.readString(":").trim(); + CString ver2 = ver.readString("").trim(); + int aVersion[2] = {getVersionID(ver1), getVersionID(ver2)}; + if (m_versionId >= aVersion[0] && m_versionId <= aVersion[1]) + { + allowed = true; + break; + } + } + else + { + int aVersion = getVersionID(ver); + if (m_versionId == aVersion) + { + allowed = true; + break; + } + } + } + if (!allowed) + { + log::printLine(log::rc, "** [Disconnect] '{}': Client version not allowed. (Version: {} {})", account.name, m_version, getVersionString(m_version, m_type)); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Your client version is not allowed on this server.\rAllowed: " << m_server->getAllowedVersionString()); + return false; + } + } + + // Check for available slots on the server. + if (m_server->getPlayerList().size() >= m_server->cached.maxPlayers.getValue()) + { + log::printLine(log::rc, "** [Disconnect] '{}': Server is full.", account.name); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "This server has reached its player limit."); + return false; + } + + // Verify login details with the serverlist. + // TODO: localhost mode. + if (!m_server->getServerList().getConnected()) + { + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "The login server is offline. Try again later."); + return false; + } + + m_server->getServerList().sendLoginPacketForPlayer(shared_from_this(), password, identity); + return true; +} + +bool PlayerClient::sendLogin() +{ + if (Player::sendLogin() == false) + return false; + + auto& settings = m_server->getSettings(); + bool hasNPCServer = m_server->hasNPCServer(); + + // Recalculate player spar deviation. + { + // c = sqrt( (350*350 - 50*50) / t ) + // where t is the number of rating periods for deviation to go from 50 to 350. + // t = 60 days for us. + const float c = 44.721f; + auto current_time = std::chrono::system_clock::now(); + auto time_difference = current_time - account.lastSparTime; + auto days = std::chrono::duration_cast(time_difference).count(); + if (days != 0) + { + // Find the new deviation. + float deviate = std::min(350.0f, static_cast(sqrt((account.eloDeviation * account.eloDeviation) + (c * c) * days))); + + // Set the new rating. + account.eloDeviation = deviate; + account.lastSparTime = current_time; + } + } + + // Send the player his login props. + sendPacket(CString() >> (char)PLO_PLAYERPROPS << getPropsPacketFromList(loginPropsClientSelf)); + + // Workaround for the 2.31 client. It doesn't request the map file when used with setmap. + // So, just send them all the maps loaded into the server. + if (m_versionId == CLVER_2_31 || m_versionId == CLVER_1_411) + { + for (const auto& map : m_server->getMapList()) + { + if (map->isBigMap()) + msgPLI_WANTFILE(CString() << map->getMapName()); + } + } + + // Sent to rc and client, but rc ignores it so... + sendPacket(CString() >> (char)PLO_CLEARWEAPONS); + + // If the gr.ip hack is enabled, add it to the player's flag list. + if (settings.get("flaghack_ip").value_or(false) == true) + this->setFlag("gr.ip", account.ipAddress, true); + + // Send the player's flags. + for (const auto& [flag, value] : account.variables.store) + { + if (auto serialized = account.variables.serializeModern(flag); serialized.has_value()) + sendPacket(CString() >> (char)PLO_FLAGSET << serialized.value()); + } + + // Send the server's flags to the player. + for (const auto& [flag, value] : m_server->Scripting.variables.store) + { + if (hasNPCServer && !flag.starts_with("serverr.")) continue; + if (auto serialized = m_server->Scripting.variables.serializeModern(flag); serialized.has_value()) + sendPacket(CString() >> (char)PLO_FLAGSET << serialized.value()); + } + + // Delete the bomb and bow. They get automagically added by the client for + // God knows which reason. Bomb and Bow must be capitalized. + sendPacket(CString() >> (char)PLO_NPCWEAPONDEL << "Bomb"); + sendPacket(CString() >> (char)PLO_NPCWEAPONDEL << "Bow"); + + // Send the player's weapons. + for (const auto& weaponName : account.weapons) + { + auto weapon = m_server->getWeapon(weaponName); + if (weapon == nullptr) + { + // Let's check to see if it is a default weapon. If so, we can add it to the server now. + if (auto itemType = LevelItem::getItemId(weaponName); itemType != LevelItemType::INVALID) + { + CString defWeapPacket = CString() >> (char)PLI_WEAPONADD >> (char)0 >> (char)LevelItem::getItemTypeId(itemType); + defWeapPacket.readGChar(); + msgPLI_WEAPONADD(defWeapPacket); + continue; + } + continue; + } + weapon->registerWeaponWithPlayer(shared_from_this()); + } + + // Send any protected weapons we do not have. + for (const auto& weapon : m_server->cached.protectedWeapons.getValue()) + { + if (!account.hasWeapon(weapon)) + this->addWeapon(weapon); + } + + // Send the zlib fixing NPC to client versions 2.21 - 2.31. + if (m_versionId >= CLVER_2_21 && m_versionId <= CLVER_2_31) + { + sendPacket(CString() >> (char)PLO_NPCWEAPONADD >> (char)12 << "-gr_zlib_fix" >> (char)0 >> (char)0 >> (char)1 >> (short)_zlibFix.length() << _zlibFix); + } + + // Tell the client if the server is connected to the listserver. + if (m_server->getServerList().getConnected()) + sendPacket(CString() >> (char)PLO_SERVERLISTCONNECTED); + + // Send the bigmap if it was set. + if (m_versionId >= CLVER_2_1) + { + CString bigmap = settings.get("bigmap").value_or(""); + if (!bigmap.isEmpty()) + { + std::vector vbigmap = bigmap.tokenize(","); + if (vbigmap.size() == 4) + sendPacket(CString() >> (char)PLO_BIGMAP << vbigmap[0].trim() << "," << vbigmap[1].trim() << "," << vbigmap[2].trim() << "," << vbigmap[3].trim()); + } + } + + // Send the minimap if it was set. + if (m_versionId >= CLVER_2_1) + { + CString minimap = settings.get("minimap").value_or(""); + if (!minimap.isEmpty()) + { + std::vector vminimap = minimap.tokenize(","); + if (vminimap.size() == 4) + sendPacket(CString() >> (char)PLO_MINIMAP << vminimap[0].trim() << "," << vminimap[1].trim() << "," << vminimap[2].trim() << "," << vminimap[3].trim()); + } + } + + // Send out RPG Window greeting. + if (m_versionId >= CLVER_2_1) + sendPacket(CString() >> (char)PLO_RPGWINDOW << "\"Welcome to " << settings.get("name").value_or("") << ".\",\"" << CString(APP_VENDOR) << " " << CString(APP_NAME) << " programmed by " << CString(APP_CREDITS) << ".\""); + + // Send the start message to the player. + sendPacket(CString() >> (char)PLO_STARTMESSAGE << m_server->getServerMessage()); + + // This will allow serverwarp and some other things, for some reason. + sendPacket(CString() >> (char)PLO_SERVERTEXT); + + // Send out what guilds should be placed in the Staff section of the playerlist. + CString guildPacket = CString() >> (char)PLO_STAFFGUILDS; + for (const auto& guild : string::split(settings.get("staffguilds").value_or(""), ","sv)) + guildPacket << "\"" << string::trim(guild) << "\","; + sendPacket(guildPacket.remove(guildPacket.length() - 1, 1)); + + // Send out the server's available status list options. + if (m_versionId >= CLVER_2_1) + { + // graal doesn't quote these + CString pliconPacket = CString() >> (char)PLO_STATUSLIST; + for (const auto& status : m_server->cached.playerStatusList.getValue()) + pliconPacket << string::trim(status) << ","; + + sendPacket(pliconPacket.remove(pliconPacket.length() - 1, 1)); + } + + // Ask for processes. This causes windows v6 clients to crash + /* + if (m_versionId < CLVER_6_015) + sendPacket(CString() >> (char)PLO_LISTPROCESSES); + */ + + // Send the level to the player. + // warp will call sendCompress() for us. + if (!warp(account.level, getGlobalPosition()) && m_currentLevel.expired()) + { + log::printLine(log::rc, "** [Disconnect] '{}': No level available for player.", account.name); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "No level available."); + log::printLine(log::server, "** Cannot find level for {}.", account.name); + return false; + } + + // Exchange props with everybody on the server. + exchangeMyPropsWithOthers(); + + // Record prop mod time. + auto curTime = currentTime(); + std::ranges::for_each(modTime, [&curTime](auto& modTime) + { + modTime = curTime; + }); + + m_fileQueue.sendCompress(true); + + // Queue up the login event. + if (m_server->hasNPCServer()) + { + auto npcServer = m_server->getNPCServer(); + npcServer->playerLogin(shared_from_this()); + npcServer->addEventToControlNPC(ScriptEventType::TRIGGERACTION, source::FromPlayer(m_id), "playeronline"); + npcServer->addEventToControlNPC(ScriptEventType::PLAYERLOGIN, source::FromPlayer(m_id)); + } + + return true; +} + +/////////////////////////////////////////////////////////////////////////////// + +bool PlayerClient::processChat(const CString& pChat) +{ + std::vector chatParse = pChat.tokenizeConsole(); + if (chatParse.size() == 0) return false; + bool processed = false; + bool setcolorsallowed = m_server->getSettings().get("setcolorsallowed").value_or(true); + + if (chatParse[0] == "setnick") + { + processed = true; + if (timeDifference(m_server->getFrameStartTime(), m_lastNick) >= 10s) + { + m_lastNick = m_server->getFrameStartTime(); + CString newName = pChat.subString(8).trim(); + + // Word filter. + int filter = m_server->getWordFilter().apply(this, newName, FILTER_CHECK_NICK); + if (filter & FILTER_ACTION_WARN) + { + setChat(newName); + return true; + } + + // SetBy::CLIENT so the server applies nickname restrictions on the player. + auto result = setPropWith(props::SetBy::CLIENT, newName.toString()); + result.resultFlags.set(props::SetResults::sendToSource); + sendPropsFromResults(result); + } + else + setChat("Wait 10 seconds before changing your nick again!"); + } + else if (chatParse[0] == "sethead" && chatParse.size() == 2) + { + if (!m_server->getSettings().get("setheadallowed").value_or(true)) return false; + processed = true; + + // Try to find the file. + auto& filesystem = m_server->getFileSystem(); + auto file = filesystem.findi(fs::FileCategory::HEAD, chatParse[1].toStringView()); + if (file.empty()) + { + int i = 0; + const char* ext[] = {".png", ".mng", ".gif"}; + while (i < 3) + { + file = filesystem.findi(fs::FileCategory::HEAD, std::format("{}{}", chatParse[1].toStringView(), ext[i])); + if (!file.empty()) + { + chatParse[1] << ext[i]; + break; + } + ++i; + } + } + + // Try to load the file. + if (!file.empty()) + sendPropsFromResults(setPropWith(props::SetBy::SERVER, chatParse[1].toString())); + else + m_server->getServerList().sendPacket(CString() >> (char)SVO_GETFILE3 >> (short)m_id >> (char)0 >> (char)chatParse[1].length() << chatParse[1]); + } + else if (chatParse[0] == "setbody" && chatParse.size() == 2) + { + if (m_server->getSettings().get("setbodyallowed").value_or(true) == false) return false; + processed = true; + + // Check to see if it is a default body. + bool isDefault = false; + for (const auto& entry : DefaultBodies) + if (chatParse[1].match(CString(entry.data())) == true) isDefault = true; + + // Don't search for the file if it is one of the defaults. This protects against + // malicious gservers. + if (isDefault) + { + sendPropsFromResults(setPropWith(props::SetBy::SERVER, chatParse[1].toString())); + return false; + } + + // Try to find the file. + auto& filesystem = m_server->getFileSystem(); + auto file = filesystem.findi(fs::FileCategory::BODY, chatParse[1].toStringView()); + if (file.empty()) + { + int i = 0; + const char* ext[] = {".png", ".mng", ".gif"}; + while (i < 3) + { + file = filesystem.findi(fs::FileCategory::BODY, std::format("{}{}", chatParse[1].toStringView(), ext[i])); + if (!file.empty()) + { + chatParse[1] << ext[i]; + break; + } + ++i; + } + } + + // Try to load the file. + if (!file.empty()) + sendPropsFromResults(setPropWith(props::SetBy::SERVER, chatParse[1].toString())); + else + m_server->getServerList().sendPacket(CString() >> (char)SVO_GETFILE3 >> (short)m_id >> (char)1 >> (char)chatParse[1].length() << chatParse[1]); + } + else if (chatParse[0] == "setsword" && chatParse.size() == 2) + { + if (!m_server->getSettings().get("setswordallowed").value_or(true)) return false; + processed = true; + + // Check to see if it is a default sword. + bool isDefault = false; + for (const auto& entry : DefaultSwords) + if (chatParse[1].match(CString(entry.data())) == true) isDefault = true; + + // Don't search for the file if it is one of the defaults. This protects against + // malicious gservers. + if (isDefault) + { + sendPropsFromResults(setPropWith(props::SetBy::SERVER, chatParse[1].toString())); + return false; + } + + // Try to find the file. + auto& filesystem = m_server->getFileSystem(); + auto file = filesystem.findi(fs::FileCategory::SWORD, chatParse[1].toStringView()); + if (file.empty()) + { + int i = 0; + const char* ext[] = {".png", ".mng", ".gif"}; + while (i < 3) + { + file = filesystem.findi(fs::FileCategory::SWORD, std::format("{}{}", chatParse[1].toStringView(), ext[i])); + if (!file.empty()) + { + chatParse[1] << ext[i]; + break; + } + ++i; + } + } + + // Try to load the file. + if (!file.empty()) + sendPropsFromResults(setPropWith(props::SetBy::SERVER, chatParse[1].toString())); + else + m_server->getServerList().sendPacket(CString() >> (char)SVO_GETFILE3 >> (short)m_id >> (char)2 >> (char)chatParse[1].length() << chatParse[1]); + } + else if (chatParse[0] == "setshield" && chatParse.size() == 2) + { + if (!m_server->getSettings().get("setshieldallowed").value_or(true)) return false; + processed = true; + + // Check to see if it is a default shield. + bool isDefault = false; + for (const auto& entry : DefaultShields) + if (chatParse[1].match(CString(entry.data())) == true) isDefault = true; + + // Don't search for the file if it is one of the defaults. This protects against + // malicious gservers. + if (isDefault) + { + sendPropsFromResults(setPropWith(props::SetBy::SERVER, chatParse[1].toString())); + return false; + } + + // Try to find the file. + auto& filesystem = m_server->getFileSystem(); + auto file = filesystem.findi(fs::FileCategory::SHIELD, chatParse[1].toStringView()); + if (file.empty()) + { + int i = 0; + const char* ext[] = {".png", ".mng", ".gif"}; + while (i < 3) + { + file = filesystem.findi(fs::FileCategory::SHIELD, std::format("{}{}", chatParse[1].toStringView(), ext[i])); + if (!file.empty()) + { + chatParse[1] << ext[i]; + break; + } + ++i; + } + } + + // Try to load the file. + if (!file.empty()) + sendPropsFromResults(setPropWith(props::SetBy::SERVER, chatParse[1].toString())); + else + m_server->getServerList().sendPacket(CString() >> (char)SVO_GETFILE3 >> (short)m_id >> (char)3 >> (char)chatParse[1].length() << chatParse[1]); + } + else if (chatParse[0] == "setskin" && chatParse.size() == 2 && setcolorsallowed) + { + processed = true; + + // id: 0 + if (chatParse[1].toLower() == "grey") chatParse[1] = "gray"; + signed char color = getColor(chatParse[1].toLower()); + if (color != -1) + { + account.character.colors[0] = color; + setPropsFromPacket(CString() >> (char)PlayerProp::COLORS >> (char)account.character.colors[0] >> (char)account.character.colors[1] >> (char)account.character.colors[2] >> (char)account.character.colors[3] >> (char)account.character.colors[4], props::SetBy::SERVER); + } + } + else if (chatParse[0] == "setcoat" && chatParse.size() == 2 && setcolorsallowed) + { + processed = true; + + // id: 1 + if (chatParse[1].toLower() == "grey") chatParse[1] = "gray"; + signed char color = getColor(chatParse[1].toLower()); + if (color != -1) + { + account.character.colors[1] = color; + setPropsFromPacket(CString() >> (char)PlayerProp::COLORS >> (char)account.character.colors[0] >> (char)account.character.colors[1] >> (char)account.character.colors[2] >> (char)account.character.colors[3] >> (char)account.character.colors[4], props::SetBy::SERVER); + } + } + else if (chatParse[0] == "setsleeves" && chatParse.size() == 2 && setcolorsallowed) + { + processed = true; + + // id: 2 + if (chatParse[1].toLower() == "grey") chatParse[1] = "gray"; + signed char color = getColor(chatParse[1].toLower()); + if (color != -1) + { + account.character.colors[2] = color; + setPropsFromPacket(CString() >> (char)PlayerProp::COLORS >> (char)account.character.colors[0] >> (char)account.character.colors[1] >> (char)account.character.colors[2] >> (char)account.character.colors[3] >> (char)account.character.colors[4], props::SetBy::SERVER); + } + } + else if (chatParse[0] == "setshoes" && chatParse.size() == 2 && setcolorsallowed) + { + processed = true; + + // id: 3 + if (chatParse[1].toLower() == "grey") chatParse[1] = "gray"; + signed char color = getColor(chatParse[1].toLower()); + if (color != -1) + { + account.character.colors[3] = color; + setPropsFromPacket(CString() >> (char)PlayerProp::COLORS >> (char)account.character.colors[0] >> (char)account.character.colors[1] >> (char)account.character.colors[2] >> (char)account.character.colors[3] >> (char)account.character.colors[4], props::SetBy::SERVER); + } + } + else if (chatParse[0] == "setbelt" && chatParse.size() == 2 && setcolorsallowed) + { + processed = true; + + // id: 4 + if (chatParse[1].toLower() == "grey") chatParse[1] = "gray"; + signed char color = getColor(chatParse[1].toLower()); + if (color != -1) + { + account.character.colors[4] = color; + setPropsFromPacket(CString() >> (char)PlayerProp::COLORS >> (char)account.character.colors[0] >> (char)account.character.colors[1] >> (char)account.character.colors[2] >> (char)account.character.colors[3] >> (char)account.character.colors[4], props::SetBy::SERVER); + } + } + else if (chatParse[0] == "warpto") + { + processed = true; + + // To player + if (chatParse.size() == 2) + { + // Permission check. + if (!account.hasRight(PLPERM_WARPTOPLAYER) && !m_server->getSettings().get("warptoforall").value_or(false)) + { + setChat("(not authorized to warp)"); + return true; + } + + auto player = m_server->getPlayer(chatParse[1], PLTYPE_ANYCLIENT); + if (player && player->getLevel()) + warp(player->getLevel()->levelName, player->getLocalPosition()); + } + // To x/y location + else if (chatParse.size() == 3) + { + // Permission check. + if (!account.hasRight(PLPERM_WARPTO) && !m_server->getSettings().get("warptoforall").value_or(false)) + { + setChat("(not authorized to warp)"); + return true; + } + + setPropsFromPacket(CString() >> (char)PlayerProp::X >> (char)(strtofloat(chatParse[1]) * 2) >> (char)PlayerProp::Y >> (char)(strtofloat(chatParse[2]) * 2), props::SetBy::SERVER); + } + // To x/y level + else if (chatParse.size() == 4) + { + // Permission check. + if (!account.hasRight(PLPERM_WARPTO) && !m_server->getSettings().get("warptoforall").value_or(false)) + { + setChat("(not authorized to warp)"); + return true; + } + + warp(chatParse[3], {static_cast(string::toFloat(chatParse[1].toString()) * 16.0f), static_cast(string::toFloat(chatParse[2].toString()) * 16.0f)}); + } + } + else if (chatParse[0] == "summon" && chatParse.size() == 2) + { + processed = true; + + // Permission check. + if (!account.hasRight(PLPERM_SUMMON)) + { + setChat("(not authorized to summon)"); + return true; + } + + auto p = m_server->getPlayer(chatParse[1], PLTYPE_ANYCLIENT); + if (p) p->warp(account.level, getLocalPosition()); + } + else if (chatParse[0] == "unstick" || chatParse[0] == "unstuck") + { + if (chatParse.size() == 2 && chatParse[1] == "me") + { + processed = true; + + // Check if the player is in a jailed level. + if (isJailed()) + return false; + + int unstickTime = m_server->cached.unstickMeSeconds.getValue(); + if (timeDifference(m_server->getFrameStartTime(), m_lastMovement) < std::chrono::seconds{unstickTime}) + setChat(CString() << "Don't move for " << CString(unstickTime) << " seconds before doing '" << pChat << "'!"); + else + { + m_lastMovement = m_server->getFrameStartTime(); + const auto& unstickLevel = m_server->cached.unstickMeLevel.getValue(); + const auto& unstickX = m_server->cached.unstickMeTile[0].getValue(); + const auto& unstickY = m_server->cached.unstickMeTile[1].getValue(); + warp(unstickLevel, {static_cast(unstickX * 16.0f), static_cast(unstickY * 16.0f)}); + setChat("Warped!"); + } + } + } + else if (pChat == "update level" && account.hasRight(PLPERM_UPDATELEVEL)) + { + processed = true; + if (auto level = getLevel(); level) + level->reload(getMapPosition()); + } + else if (pChat == "showadmins" && m_server->getSettings().get("disableshowadmins").value_or(false) == false) + { + processed = true; + + // Search through the player list for all RC's. + CString msg; + { + auto& playerList = m_server->getPlayerList(); + for (auto& [pid, player] : playerList) + { + // If an RC was found, add it to our string. + if (player->getType() & PLTYPE_ANYRC) + msg << (msg.length() == 0 ? "" : ", ") << player->account.name; + } + } + if (msg.length() == 0) + msg << "(no one)"; + setChat(CString("admins: ") << msg); + } + else if (chatParse[0] == "showguild") + { + processed = true; + CString g = m_guild; + + // If a guild was specified, overwrite our guild with it. + if (chatParse.size() == 2) + g = chatParse[1]; + + if (g.length() != 0) + { + CString msg; + { + auto& playerList = m_server->getPlayerList(); + for (auto& [pid, player] : playerList) + { + // If our guild matches, add it to our string. + if (player->getGuild() == g) + msg << (msg.length() == 0 ? "" : ", ") << CString(player->account.character.nickName).subString(0, player->account.character.nickName.find('(')).trimI(); + } + } + if (msg.length() == 0) + msg << "(no one)"; + setChat(CString("members of '") << g << "': " << msg); + } + } + else if (pChat == "showkills") + { + processed = true; + setChat(CString() << "kills: " << CString((int)account.kills)); + } + else if (pChat == "showdeaths") + { + processed = true; + setChat(CString() << "deaths: " << CString((int)account.deaths)); + } + else if (pChat == "showonlinetime") + { + processed = true; + int seconds = account.onlineSeconds % 60; + int minutes = (account.onlineSeconds / 60) % 60; + int hours = account.onlineSeconds / 3600; + CString msg; + if (hours != 0) msg << CString(hours) << "h "; + if (minutes != 0 || hours != 0) msg << CString(minutes) << "m "; + msg << CString(seconds) << "s"; + setChat(CString() << "onlinetime: " << msg); + } + else if (chatParse[0] == "toguild:") + { + processed = true; + if (m_guild.length() == 0) return false; + + // Get the PM. + CString pm = pChat.text() + 8; + pm.trimI(); + if (pm.length() == 0) return false; + + // Send PM to guild members. + int num = 0; + { + auto& playerList = m_server->getPlayerList(); + for (auto& [pid, player] : playerList) + { + // If our guild matches, send the PM. + if (player->getGuild() == m_guild) + { + player->sendPacket(CString() >> (char)PLO_PRIVATEMESSAGE >> (short)m_id << "\"\",\"Guild message:\",\"" << pm << "\""); + ++num; + } + } + } + + // Tell the player how many guild members received his message. + setChat(CString() << "(" << CString(num) << " guild member" << (num != 0 ? "s" : "") << " received your message)"); + } +#ifdef DEBUG + else if (pChat == "savenpcs" && m_server->hasNPCServer()) + { + processed = true; + m_server->getNPCServer()->saveNPCs(); + setChat(CString() << "(saved npcs)"); + } +#endif + + return processed; +} + +/////////////////////////////////////////////////////////////////////////////// + +void PlayerClient::setGroup(std::string_view group) +{ + // Clear any cached level data from the client that belongs to the old group. + if (!account.groupName.empty()) + resetLevelCache(account.groupName); + + // Finally, set the new group. + if (group.empty()) + account.groupName.clear(); + else + account.groupName = std::format("gr.{}", string::toLower(group)); +} + +/////////////////////////////////////////////////////////////////////////////// + +double PlayerClient::getCalculatedTileZ() const noexcept +{ + auto level = getLevel(); + if (level == nullptr || !level->hasTerrain()) + return account.character.localPixelZ / 16.0; + + PixelPosition testPosition = account.character.getGlobalPosition().translate(24, 48); + auto terrainHeight = level->getHeightAt(testPosition); + auto currentZ = account.character.localPixelZ / 16.0; + return std::max(terrainHeight, currentZ); +} + +/////////////////////////////////////////////////////////////////////////////// + +std::string PlayerClient::getLevelName() const +{ + auto level = getLevel(); + if (level == nullptr) + return {}; + + return level->levelName; +} + +std::shared_ptr PlayerClient::getLevel() const +{ + if (isHiddenClient()) + return nullptr; + + return m_currentLevel.lock(); +} + +std::shared_ptr PlayerClient::getSubLevel() const +{ + if (auto level = getLevel(); level != nullptr) + return level->getSubLevelAtPosition(getMapPosition()); + + return nullptr; +} + +/////////////////////////////////////////////////////////////////////////////// + +bool PlayerClient::warp(std::string_view levelName, const PixelPosition& position, std::optional clientCachedTime) +{ + // Find the level. + auto newLevel = m_server->getLoadedLevel(levelName, shared_from_this()); + + // Check if the new level exists. + if (newLevel == nullptr) + { + sendPacket(CString() >> (char)PLO_WARPFAILED << levelName); + return false; + } + + // If this is a gmap and the level names don't match, fix the position of the warp. + if (newLevel->isGmap() && newLevel->levelName != levelName) + { + auto subLevel = newLevel->getSubLevelByName(levelName); + if (subLevel != nullptr) + { + auto origin = newLevel->getSubLevelOrigin(subLevel).value_or(PixelPosition{}); + return warp(newLevel, position.translate(origin), clientCachedTime); + } + } + + return warp(newLevel, position, clientCachedTime); +} + +bool PlayerClient::warp(std::shared_ptr level, const PixelPosition& position, std::optional clientCachedTime) +{ + // If we are warping to the same level, just update the player's location. + auto localPosition = toLocalPixelPosition(position); + if (!m_currentLevel.expired() && account.level == level->levelName) + { + std::inplace_vector propResults{ + setPropWith(props::SetBy::SERVER, localPosition.x()), + setPropWith(props::SetBy::SERVER, localPosition.y()) + }; + + if (level->isGmap()) + { + auto destMapPosition = toMapPosition(position); + propResults.push_back(setPropWith(props::SetBy::SERVER, destMapPosition.x())); + propResults.push_back(setPropWith(props::SetBy::SERVER, destMapPosition.y())); + } + + sendPropsFromResults(propResults); + return true; + } + + // Set the player's position. + account.character.localPixelX = localPosition.x(); + account.character.localPixelY = localPosition.y(); + + // Tell the client their new level. + if (level->isGmap()) + { + // We have to do this manually since if we set it via setPropWith, it will cause a second level warp. + auto mapPosition = toMapPosition(position); + account.character.mapX = mapPosition.x(); + account.character.mapY = mapPosition.y(); + this->modTime[PROPID(PlayerProp::GMAPLEVELX)] = m_server->getFrameStartTime(); + this->modTime[PROPID(PlayerProp::GMAPLEVELY)] = m_server->getFrameStartTime(); + sendPacket(CString() >> (char)PLO_PLAYERWARP2 << getProp().serialize() << getProp().serialize() << getProp().serialize() >> (char)mapPosition.x() >> (char)mapPosition.y() << level->levelName); + } + else + { + // Reset the map position to 0 if we are warping to a non-gmap level. + if (account.character.mapX != 0) + this->modTime[PROPID(PlayerProp::GMAPLEVELX)] = m_server->getFrameStartTime(); + if (account.character.mapY != 0) + this->modTime[PROPID(PlayerProp::GMAPLEVELY)] = m_server->getFrameStartTime(); + account.character.mapX = account.character.mapY = 0; + + sendPacket(CString() >> (char)PLO_PLAYERWARP << getProp().serialize() << getProp().serialize() << level->levelName); + } + + // Enter the level. + return enterLevel(level, clientCachedTime); +} + +bool PlayerClient::enterLevel(std::shared_ptr level, std::optional clientCachedTime) +{ + auto currentLevel = getLevel(); + bool sameLevel = currentLevel == level; + + // Leave the current level if we are changing levels and add ourself to the new one. + if (!sameLevel) + { + leaveLevel(); + + m_currentLevel = level; + level->addPlayer(m_id); + account.level = level->levelName; + } + + // Send the level now. + auto subLevel = level->getSubLevelAtPosition(getMapPosition()); + bool succeed = sendStaticLevelData(subLevel->staticData.lock(), subLevel, clientCachedTime); + succeed = succeed && sendDynamicLevelData(level, clientCachedTime); + + // If we failed, leave the level and inform the client. + if (!succeed) + { + leaveLevel(); + sendPacket(CString() >> (char)PLO_WARPFAILED << level->levelName); + return false; + } + + // If the level is a sparring zone and you have 100 AP, change AP to 99 and + // the apcounter to 1. + if (level->isSparringZone(getMapPosition()) && account.character.ap == 100) + { + account.apCounter = 1; + sendPropsFromResults(setPropWith(props::SetBy::SERVER, 99_ui8)); + } + + // Inform everybody as to the client's new location. This will update the minimap. + CString minimap = CString() >> (char)PLO_OTHERPLPROPS >> (short)m_id + >> (char)PlayerProp::CURLEVEL << getProp().serialize() + >> (char)PlayerProp::X << getProp().serialize() + >> (char)PlayerProp::Y << getProp().serialize(); + + for (const auto& [pid, player] : players_of_type(m_server->getPlayerList())) + { + if (pid == this->getId()) + continue; + + player->sendPacket(minimap); + } + + // Update RCs. + CString myRCProps = CString() >> (char)PLO_ADDPLAYER >> (short)getId() >> (char)account.name.length() << account.name + >> (char)PlayerProp::CURLEVEL << getProp().serialize() + >> (char)PlayerProp::PLAYERLISTSTATUS << getProp().serialize() + >> (char)PlayerProp::NICKNAME << getProp().serialize() + >> (char)PlayerProp::COMMUNITYNAME << getProp().serialize(); + m_server->sendPacketToType(PLTYPE_ANYCONTROL, myRCProps, this); + + return true; +} + +bool PlayerClient::leaveLevel() +{ + // Make sure we are on a level first. + auto levelp = m_currentLevel.lock(); + if (!levelp) return true; + auto [subLevel, levelData] = levelp->getSubLevelAndStaticDataAtPosition(getMapPosition()); + if (levelData == nullptr) + return false; + + // Leave the sub-level (cache the time). + leaveSubLevel(subLevel); + + // Remove self from list of players in level. + levelp->removePlayer(m_id); + + // Send PLO_ISLEADER to new level leader. + if (auto map = levelp->getMap(); map == nullptr || !map->isGmap()) + { + if (auto& levelPlayerList = levelp->getPlayers(); !levelPlayerList.empty()) + { + if (auto leader = m_server->getPlayer(levelPlayerList.front()); leader != nullptr) + leader->informPlayerIsLevelLeader(); + } + } + + // If I am carrying an NPC, tell others the NPC left the level. + if (m_carryNPC != 0) + { + if (auto npc = m_server->getNPC(m_carryNPC); npc) + { + levelp->removeNPC(m_carryNPC); + CString deletePacket = CString() >> (char)PLO_NPCDEL >> (int)m_carryNPC; + m_server->sendPacketToLevelAndPastVisitorsAfter(levelData.get(), npc->lastUpdateTime, deletePacket); + } + } + + // Tell everyone I left. + { + m_server->sendPacketToNearby(CString() >> (char)PLO_OTHERPLPROPS >> (short)m_id >> (char)PlayerProp::JOINLEAVELVL >> (char)0, getGlobalPosition(), getLevel(), {m_id}); + + for (const auto& [pid, player] : players_of_type(m_server->getPlayerList())) + { + if (pid == getId()) continue; + if (player->getLevel() != getLevel()) continue; + this->sendPacket(CString() >> (char)PLO_OTHERPLPROPS >> (short)player->getId() >> (char)PlayerProp::JOINLEAVELVL >> (char)0); + } + } + + // Clear the level. + m_currentLevel.reset(); + + return true; +} + +bool PlayerClient::leaveSubLevel(std::shared_ptr subLevel) +{ + if (subLevel == nullptr) return false; + auto staticData = subLevel->staticData.lock(); + if (staticData == nullptr) return false; + + auto curTime = m_server->getFrameStartTime(); + auto cacheLevel = [this, &curTime](std::vector>>& cache, std::shared_ptr level) + { + // Save the time we left the level for the client-side caching. + bool found = false; + for (auto& cl : cache) + { + auto cllevel = cl->level.lock(); + if (cllevel == level) + { + cl->lastEnteredTime = curTime; + found = true; + break; + } + } + + if (!found) + cache.push_back(std::make_unique>(CachedLevel{.level = level, .lastEnteredTime = curTime})); + }; + + // Static levels. + cacheLevel(m_cachedStaticLevels, staticData); + + // Dynamic levels. + std::string groupName = account.groupName; + if (!groupName.empty()) + { + if (auto level = subLevel->parentLevel.lock(); level != nullptr && !level->isGroupMap) + groupName.clear(); + } + cacheLevel(m_cachedDynamicLevels[groupName], subLevel); + + return true; +} + +bool PlayerClient::sendStaticLevelData(std::shared_ptr staticLevelData, std::shared_ptr subLevel, std::optional clientCachedTime) +{ + if (staticLevelData == nullptr) + return false; + + PlayerPtr self = shared_from_this(); + auto levelModTime = staticLevelData->modTime; + auto cachedModTime = getLevelLastEnteredTime(staticLevelData.get()); + if (!clientCachedTime.has_value()) clientCachedTime = levelModTime; + + bool sentBoard = false; + + // Tell the client that the following data is for the specified level. + // Gmaps consist of multiple levels so this is the name of the sub-level. + sendPacket(CString() >> (char)PLO_LEVELNAME << staticLevelData->levelName); + + // If we have not entered this level during this session, send board data. + // Also send if the client sends a cache time that doesn't match the level. + // Clients will cache level data so this can be skipped if nothing has changed. + if (!cachedModTime.has_value() || clientCachedTime.value() < levelModTime) + { + // Send board data (tiles, layers, heights). + if (subLevel != nullptr) + { + subLevel->sendBoardToPlayer(self); + subLevel->sendBoardLayersToPlayer(self); + subLevel->sendBoardHeightsToPlayer(self); + } + else + { + staticLevelData->sendBoardToPlayer(self); + staticLevelData->sendBoardLayersToPlayer(self); + // If we are here, we aren't on a gmap so we have no heights to send. + } + + // Mark that we sent board data. + // This is important because the client will get stuck on "Loading" until it gets board data, + // so we need to know if we should send a blank board packet later. + sentBoard = true; + + // Send links (if applicable). + // We need to always send map links for bigmaps due to overflow issues that easily occur while waiting for the warp. + // (The client may start to go beyond the edge of the level and cause an integer overflow in their position). + if (!m_server->hasNPCServer() || m_server->cached.forceClientsideLinks.getValue() || (subLevel && subLevel->isOnBigMap)) + staticLevelData->sendLinksToPlayer(self, false); + + // Send signs (if applicable). + if (!m_server->hasNPCServer() || m_server->cached.forceClientsideSigns.getValue()) + staticLevelData->sendSignsToPlayer(self); + } + + // Send chests. + // Always send chests since RC could have modified them. + staticLevelData->sendChestsToPlayer(self); + + // The client will get stuck on "Loading" until it gets board data. + // So, if we did not send any data, send an empty packet so the client knows it can move on. + if (!sentBoard) + sendPacket(CString() >> (char)PLO_LEVELBOARD); + + // Send the level mod time so the client can cache it. + sendPacket(CString() >> (char)PLO_LEVELMODTIME >> (long long)clock::to_time_t(levelModTime)); + + // Fix the level name. + // If the player is on a gmap, we need to set the level back to the gmap. + // If the player is the leader on their level, also send the isleader packet. + sendPacket(CString() >> (char)PLO_LEVELNAME << getLevelName()); + checkAndInformIfLevelLeader(); + + return true; +} + +bool PlayerClient::sendDynamicLevelData(std::shared_ptr level, std::optional clientCachedTime) +{ + if (level == nullptr) return false; + + // Get the sub-level and static data we are on. + auto [subLevel, staticLevelData] = level->getSubLevelAndStaticDataAtPosition(getMapPosition()); + if (subLevel == nullptr || staticLevelData == nullptr) + return false; + + PlayerPtr self = shared_from_this(); + auto cachedModTime = getLevelLastEnteredTime(subLevel.get()); + + // Send board changes, horses, and baddies. + subLevel->sendBoardChangesToPlayer(self, cachedModTime); + if (!level->isGmap()) + { + level->sendHorsesToPlayer(self); + level->sendBaddiesToPlayer(self); + } + + // Tell the client if there are any ghost players in the level. + // We don't support trial accounts so pass 0 (no ghosts) instead of 1 (ghosts present). + sendPacket(CString() >> (char)PLO_GHOSTICON >> (char)0); + + // If we are the leader, send it now. + checkAndInformIfLevelLeader(); + + // Send NPCs. + sendPacket(CString() >> (char)PLO_SETACTIVELEVEL << level->levelName); + level->sendNPCsToPlayer(self, cachedModTime); + + // Move the carry NPC to the new level. + if (m_carryNPC != 0) + { + if (auto npc = m_server->getNPC(m_carryNPC); npc) + { + if (npc->level != level->levelName) + { + level->addNPC(m_carryNPC); + npc->character.mapX = account.character.mapX; + npc->character.mapY = account.character.mapY; + + // setLevel should refresh all of the modTimes. + npc->setLevel(level); + } + else if (level->isGmap()) + { + npc->sendPropsFromResults( + npc->setPropWith(props::SetBy::SERVER, account.character.mapX), + npc->setPropWith(props::SetBy::SERVER, account.character.mapY) + ); + } + + // Send the carry NPC props to other players. + if (!level->isSinglePlayer) + { + CString carryNPCProps = CString() >> (char)PLO_NPCPROPS >> (int)m_carryNPC << npc->getAllPropsPacket(); + m_server->sendPacketToNearby(carryNPCProps, getGlobalPosition(), level, {m_id}); + } + } + } + + // Send connecting player props to players in nearby levels. + if (!level->isSinglePlayer) + { + CString myProps = CString() >> (char)PLO_OTHERPLPROPS >> (short)m_id >> (char)PlayerProp::JOINLEAVELVL >> (char)1 << getPropsPacketFromList(loginPropsClientOthers); + for (const auto& playerId : level->findInRangePlayersForCommunication(getGlobalPosition())) + { + if (playerId == m_id) continue; + if (auto other = m_server->getPlayer(playerId); other != nullptr) + { + if (!other->isClient()) continue; + + // Exchange props. + other->sendPacket(myProps); + this->sendPacket(CString() >> (char)PLO_OTHERPLPROPS >> (short)other->getId() >> (char)PlayerProp::JOINLEAVELVL >> (char)1 << other->getPropsPacketFromList(loginPropsClientOthers)); + } + } + } + + return true; +} + +void PlayerClient::checkAndInformIfLevelLeader() +{ + if (m_currentLevel.expired()) + return; + + auto level = getLevel(); + if (level == nullptr) + return; + + if (level->isSinglePlayer || level->isPlayerLeader(m_id) || level->isGmap()) + sendPacket(CString() >> (char)PLO_ISLEADER); +} + +void PlayerClient::informPlayerIsLevelLeader() +{ + sendPacket(CString() >> (char)PLO_ISLEADER); +} + +/////////////////////////////////////////////////////////////////////////////// + +std::optional PlayerClient::getLevelLastEnteredTime(const StaticLevelData* level) const +{ + if (level == nullptr) + return std::nullopt; + + for (auto& cl : m_cachedStaticLevels) + { + auto cllevel = cl->level.lock(); + if (cllevel && cllevel.get() == level) + { + if (cl->lastEnteredTime == clock::time_point::min()) + return std::nullopt; + return cl->lastEnteredTime; + } + } + + return std::nullopt; +} + +std::optional PlayerClient::getLevelLastEnteredTime(const SubLevel* level, std::string_view group) const +{ + if (level == nullptr) + return std::nullopt; + + auto groupLevels = m_cachedDynamicLevels.find(group); + if (groupLevels == m_cachedDynamicLevels.end()) + return std::nullopt; + + for (auto& cl : groupLevels->second) + { + auto cllevel = cl->level.lock(); + if (cllevel && cllevel.get() == level) + { + if (cl->lastEnteredTime == clock::time_point::min()) + return std::nullopt; + return cl->lastEnteredTime; + } + } + + return std::nullopt; +} + +void PlayerClient::resetLevelCache(const StaticLevelData* level) +{ + if (level == nullptr) return; + for (auto& cl : m_cachedStaticLevels) + { + auto cllevel = cl->level.lock(); + if (cllevel && cllevel.get() == level) + { + cl->lastEnteredTime = clock::time_point::min(); + return; + } + } +} + +void PlayerClient::resetLevelCache(const SubLevel* level, std::string_view group) +{ + if (level == nullptr) + return; + + auto groupLevels = m_cachedDynamicLevels.find(group); + if (groupLevels == m_cachedDynamicLevels.end()) + return; + + for (auto& cl : groupLevels->second) + { + auto cllevel = cl->level.lock(); + if (cllevel && cllevel.get() == level) + { + cl->lastEnteredTime = clock::time_point::min(); + return; + } + } +} + +void PlayerClient::resetLevelCache(std::string_view group) +{ + auto groupLevels = m_cachedDynamicLevels.find(group); + if (groupLevels == m_cachedDynamicLevels.end()) + return; + + // Collect a list of all the NPCs in the group that need to be deleted. + std::unordered_set npcsToReset; + for (auto& cl : groupLevels->second) + { + if (auto subLevel = cl->level.lock(); subLevel != nullptr) + { + if (auto level = subLevel->parentLevel.lock(); level != nullptr) + { + auto& npcs = level->getNPCs(); + npcsToReset.insert(npcs.begin(), npcs.end()); + } + } + } + + // Send the delete packet so the client forgets about them. + for (auto npcId : npcsToReset) + sendPacket(CString() >> (char)PLO_NPCDEL >> (int)npcId); + + // Finally, clear the cache for the group. + m_cachedDynamicLevels.erase(std::string{group}); +} + +/////////////////////////////////////////////////////////////////////////////// + +void PlayerClient::disableWeapons() +{ + this->account.status &= ~PLSTATUS_ALLOWWEAPONS; + sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)PlayerProp::STATUS << getProp().serialize()); +} + +void PlayerClient::enableWeapons() +{ + this->account.status |= PLSTATUS_ALLOWWEAPONS; + sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)PlayerProp::STATUS << getProp().serialize()); +} + +void PlayerClient::freezePlayer() +{ + sendPacket(CString() >> (char)PLO_FREEZEPLAYER2); +} + +void PlayerClient::unfreezePlayer() +{ + sendPacket(CString() >> (char)PLO_UNFREEZEPLAYER); +} + +void PlayerClient::sendRPGMessage(std::string message) +{ + string::replaceMutate(message, "\n", "#b"); + auto translated = translate(message); + sendPacket(CString() >> (char)PLO_RPGWINDOW << string::toCSV(string::splitByString(translated, "#b"sv, false))); +} + +void PlayerClient::sendSignMessage(std::string message) +{ + string::replaceMutate(message, "\n", "#b"); + sendPacket(CString() >> (char)PLO_SAY2 << translate(message)); +} + +/////////////////////////////////////////////////////////////////////////////// + +void PlayerClient::testForTouch(SetResults& result, uint8_t movementDirection) +{ + if (!m_server->hasNPCServer()) + return; + + // Don't allow improper directions. + movementDirection %= 4; + + // Test for signs. + if (testForSigns(result, movementDirection)) + return; + + // Set for links. + if (testForLinks(result, movementDirection)) + return; + + // Subtract an extra 1 pixel from the top touch test since the 2.31 client was rendering the location of the NPC weirdly. + // When a pixel coordinate of 223 was sent (tile 13.9375), the client would render the NPC at y=14 and break the collision detection. + // Oddly enough, it renders in the correct spot on a reconnect. By allowing a single extra pixel on the touch test, this problem is resolved. + static Position touchTest[] = {{24, 16 - 1}, {0, 32}, {24, 56}, {48, 32}}; + + // Get the bounding box to test with. + PixelRectangleArea testBox{getGlobalPosition().translate(touchTest[movementDirection].x(), touchTest[movementDirection].y()), {0, 0, 48}}; + if (m_server->cached.playerTouchesMeNoZ.getValue()) + { + // If the server is set to ignore Z axis for touch, do so by providing a box of max length in the Z axis. + testBox.position.z() = std::numeric_limits::min(); + testBox.size.length() = std::numeric_limits::max(); + } + + if (auto level = getLevel(); level != nullptr) + { + // Test for NPC touch. + bool touchedNPC = false; + for (const auto& npcId : level->findIntersectingNPCsForCollision(testBox)) + { + if (auto npc = m_server->getNPC(npcId); npc != nullptr) + { + npc->scripting.events.addEvent(ScriptEventType::PLAYERTOUCHSME, source::FromPlayer(m_id)); + touchedNPC = true; + } + } + if (touchedNPC) + { + auto eventDistance = m_server->cached.eventDistance.getValue(); + for (const auto& npcId : level->findInRangeNPCsByDistance(testBox.position, eventDistance)) + { + auto intersectingNPCs = level->findIntersectingNPCsForCollision(testBox); + if (!std::ranges::contains(intersectingNPCs, npcId)) + { + if (auto npc = m_server->getNPC(npcId); npc != nullptr) + npc->scripting.events.addEvent(ScriptEventType::PLAYERTOUCHSOTHER, source::FromPlayer(m_id)); + } + } + } + } +} + +bool PlayerClient::testForSigns(SetResults& result, uint8_t movementDirection) +{ + if (!m_server->hasNPCServer() || m_server->cached.forceClientsideSigns.getValue()) + return false; + + // Test for signs. + if (account.character.direction == 0 && movementDirection == 0) + { + if (auto level = getLevel(); level != nullptr) + { + for (const auto& sign : level->getSigns()) + { + LocalPixelPosition signPos = toLocalPixelPosition(sign.getTileX(), sign.getTileY()); + if (account.character.localPixelY == signPos.y() && account.character.localPixelX >= signPos.x() - 24 && account.character.localPixelX <= signPos.x() + 8) + { + sendSignMessage(sign.text); + return true; + } + } + } + } + return false; +} + +bool PlayerClient::testForLinks(SetResults& result, uint8_t movementDirection) +{ + static Position touchTest[] = {{24, 16}, {0, 32}, {24, 56}, {48, 32}}; + + if (!m_server->hasNPCServer() || m_server->cached.forceClientsideLinks.getValue()) + return false; + + // If we have no level, we can't test anything! + auto level = getLevel(); + if (level == nullptr) + return false; + + // If this is a bigmap, we forced clientside links to fix issues where the client wraps the X/Y values and ends up in weird spots. + // So don't check. + auto map = level->getMap(); + if (map && map->isBigMap()) + return false; + + // Test for links. + PixelPosition testPos = getGlobalPosition().translate(touchTest[movementDirection].x(), touchTest[movementDirection].y()); + TilePosition testPosTiles = toTilePosition(testPos); + if (auto linkTouched = level->getLink(testPosTiles, map != nullptr); linkTouched.has_value()) + { + const auto& destLevelName = linkTouched.value()->getDestinationLevel(); + + // Check if the destination level is on the level's map. + if (auto destSubLevel = level->getSubLevelByName(destLevelName); destSubLevel != nullptr) + { + auto pos = linkTouched.value()->getDestinationForCharacter(account.character, source::FromPlayer(m_id)); + auto levelData = destSubLevel->staticData.lock(); + warp(level->levelName, level->convertToMapPosition(destSubLevel->mapPosition.value_or(MapPosition{0, 0}), pos), getLevelLastEnteredTime(levelData.get())); + return true; + } + // Level is outside of the map, so search normally. + else if (auto newLevel = m_server->getLoadedLevel(destLevelName, shared_from_this()); newLevel != nullptr) + { + PixelPosition origin{}; + if (newLevel->isGmap()) + { + if (auto subLevel = newLevel->getSubLevelByName(destLevelName); subLevel != nullptr) + origin = newLevel->getSubLevelOrigin(subLevel).value_or(PixelPosition{}); + } + + auto pos = toPixelPosition(origin, linkTouched.value()->getDestinationForCharacter(account.character, source::FromPlayer(m_id))); + auto levelData = newLevel->getStaticLevelDataByName(destLevelName); + warp(newLevel->levelName, pos, getLevelLastEnteredTime(levelData.get())); + return true; + } + } + + return false; +} + +void PlayerClient::dropItemsOnDeath() +{ + if (!m_server->getSettings().get("dropitemsdead").value_or(true)) + return; + + auto level = getLevel(); + if (level == nullptr) + return; + + auto mindeathgralats = m_server->getSettings().get("mindeathgralats").value_or(1); + auto maxdeathgralats = m_server->getSettings().get("maxdeathgralats").value_or(50); + const auto& allowedDeathDrops = m_server->getAllowedDeathDrops(); + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution gralatDistribution(mindeathgralats, maxdeathgralats); + std::uniform_int_distribution itemDistribution(0, 3); + + // Determine how many gralats to remove from the account. + uint32_t drop_gralats = 0; + if (maxdeathgralats > 0) + drop_gralats = std::min(gralatDistribution(gen), account.character.gralats); + + // Determine how many arrows and bombs to remove from the account. + int drop_arrows = itemDistribution(gen); + int drop_bombs = itemDistribution(gen); + if ((drop_arrows * 5) > account.character.arrows) drop_arrows = account.character.arrows / 5; + if ((drop_bombs * 5) > account.character.bombs) drop_bombs = account.character.bombs / 5; + + // Check if we can drop arrows and bombs. + if (!std::ranges::contains(allowedDeathDrops, LevelItemType::DARTS)) + drop_arrows = 0; + if (!std::ranges::contains(allowedDeathDrops, LevelItemType::BOMBS)) + drop_bombs = 0; + + // Remove gralats/bombs/arrows. + account.character.gralats -= drop_gralats; + account.character.arrows -= (drop_arrows * 5); + account.character.bombs -= (drop_bombs * 5); + sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)PlayerProp::RUPEESCOUNT >> (int)account.character.gralats >> (char)PlayerProp::ARROWSCOUNT >> (char)account.character.arrows >> (char)PlayerProp::BOMBSCOUNT >> (char)account.character.bombs); + + // Check which gralats we can drop. + bool canDropGold = std::ranges::contains(allowedDeathDrops, LevelItemType::GOLDRUPEE); + bool canDropRed = std::ranges::contains(allowedDeathDrops, LevelItemType::REDRUPEE); + bool canDropBlue = std::ranges::contains(allowedDeathDrops, LevelItemType::BLUERUPEE); + bool canDropGreen = std::ranges::contains(allowedDeathDrops, LevelItemType::GREENRUPEE); + if (!canDropGold && !canDropRed && !canDropBlue && !canDropGreen) + drop_gralats = 0; + + // Add gralats to the level. + TilePosition localTilePos = toTilePosition(getLocalPosition()); + while (drop_gralats != 0) + { + char item = 0; + if (canDropGold && (drop_gralats % 100 != drop_gralats)) + { + drop_gralats -= 100; + item = 19; + } + else if (canDropRed && (drop_gralats % 30 != drop_gralats)) + { + drop_gralats -= 30; + item = 2; + } + else if (canDropBlue && (drop_gralats % 5 != drop_gralats)) + { + drop_gralats -= 5; + item = 1; + } + else if (drop_gralats != 0) + { + if (!canDropGreen) + break; + + --drop_gralats; + item = 0; + } + + float pX = localTilePos.x() + 1.5f + (rand() % 8) - 2.0f; + float pY = localTilePos.y() + 2.0f + (rand() % 8) - 2.0f; + + level->addItem(inform_client, toPixelPosition(getSubLevelOrigin(), pX, pY), static_cast(item)); + } + + // Add arrows and bombs to the level. + for (int i = 0; i < drop_arrows; ++i) + { + float pX = localTilePos.x() + 1.5f + (rand() % 8) - 2.0f; + float pY = localTilePos.y() + 2.0f + (rand() % 8) - 2.0f; + + level->addItem(inform_client, toPixelPosition(getSubLevelOrigin(), pX, pY), LevelItemType::DARTS); + } + for (int i = 0; i < drop_bombs; ++i) + { + float pX = localTilePos.x() + 1.5f + (rand() % 8) - 2.0f; + float pY = localTilePos.y() + 2.0f + (rand() % 8) - 2.0f; + + level->addItem(inform_client, toPixelPosition(getSubLevelOrigin(), pX, pY), LevelItemType::BOMBS); + } +} + +bool PlayerClient::dropItem(const PixelPosition& position, LevelItemType item) +{ + if (removeItem(item)) + { + if (auto level = getLevel(); level && level->addItem(position, item)) + return true; + } + return false; +} + +bool PlayerClient::removeItem(LevelItemType itemType) +{ + switch (itemType) + { + case LevelItemType::GREENRUPEE: // greenrupee + case LevelItemType::BLUERUPEE: // bluerupee + case LevelItemType::REDRUPEE: // redrupee + case LevelItemType::GOLDRUPEE: // goldrupee + { + uint32_t gralatsRequired; + if (itemType == LevelItemType::GOLDRUPEE) gralatsRequired = 100; + else if (itemType == LevelItemType::REDRUPEE) + gralatsRequired = 30; + else if (itemType == LevelItemType::BLUERUPEE) + gralatsRequired = 5; + else + gralatsRequired = 1; + + if (account.character.gralats >= gralatsRequired) + { + account.character.gralats -= gralatsRequired; + return true; + } + + return false; + } + + case LevelItemType::BOMBS: + { + if (account.character.bombs >= 5) + { + account.character.bombs -= 5; + return true; + } + return false; + } + + case LevelItemType::DARTS: + { + if (account.character.arrows >= 5) + { + account.character.arrows -= 5; + return true; + } + return false; + } + + case LevelItemType::HEART: + { + if (account.character.hitpointsInHalves > 2) + { + account.character.hitpointsInHalves -= 2; + return true; + } + return false; + } + + // NOTE: not receiving PLI_ITEMTAKE for >2.31, so we will not remove the item + // same is true for sword/shield. assuming its true for the weapon-items, but + // its currently not tested. + case LevelItemType::GLOVE1: + case LevelItemType::GLOVE2: + { + if (account.character.glovePower > 1) + { + account.character.glovePower--; + return true; + } + return false; + } + + /* + case LevelItemType::BOW: // bow + case LevelItemType::BOMB: // bomb + return false; + + case LevelItemType::SUPERBOMB: // superbomb + case LevelItemType::FIREBALL: // fireball + case LevelItemType::FIREBLAST: // fireblast + case LevelItemType::NUKESHOT: // nukeshot + case LevelItemType::JOLTBOMB: // joltbomb + return false; + + case LevelItemType::SHIELD: // shield + case LevelItemType::MIRRORSHIELD: // mirrorshield + case LevelItemType::LIZARDSHIELD: // lizardshield + return false; + + case LevelItemType::SWORD: // sword + case LevelItemType::BATTLEAXE: // battleaxe + case LevelItemType::LIZARDSWORD: // lizardsword + case LevelItemType::GOLDENSWORD: // goldensword + return false; + + case LevelItemType::FULLHEART: // fullheart + return false; + */ + + case LevelItemType::SPINATTACK: + { + if (account.status & PLSTATUS_HASSPIN) + { + account.status &= ~PLSTATUS_HASSPIN; + return true; + } + return false; + } + } + + return false; +} + +props::SetResults PlayerClient::addItem(LevelItemType itemType, props::SetBy setBy) +{ + switch (itemType) + { + case LevelItemType::GREENRUPEE: + case LevelItemType::BLUERUPEE: + case LevelItemType::REDRUPEE: + case LevelItemType::GOLDRUPEE: + return setPropWith(setBy, account.character.gralats + LevelItem::GetRupeeCount(itemType)); + + case LevelItemType::BOMBS: + return setPropWith(setBy, std::min(99_ui8, static_cast(account.character.bombs + 5))); + + case LevelItemType::DARTS: + return setPropWith(setBy, std::min(99_ui8, static_cast(account.character.arrows + 5))); + + case LevelItemType::HEART: + { + uint8_t maxHearts = static_cast(std::min(account.maxHitpoints, static_cast(m_server->cached.maxHeartLimit.getValue())) * 2); + return setPropWith(setBy, std::min(maxHearts, static_cast(account.character.hitpointsInHalves + 2))); + } + + case LevelItemType::GLOVE1: + case LevelItemType::GLOVE2: + return setPropWith(setBy, std::min(2_ui8, static_cast(account.character.glovePower + 1))); + + case LevelItemType::SPINATTACK: + return setPropWith(setBy, static_cast(account.status | PLSTATUS_HASSPIN)); + } + + return {}; +} + +void PlayerClient::addItem(inform_client_t, LevelItemType itemType, props::SetBy setBy) +{ + sendPropsFromResults(addItem(itemType, setBy)); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/player/PlayerClientOriginal.cpp b/server/src/player/PlayerClientOriginal.cpp new file mode 100644 index 000000000..030d52230 --- /dev/null +++ b/server/src/player/PlayerClientOriginal.cpp @@ -0,0 +1,252 @@ +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +PlayerClientOriginal::PlayerClientOriginal(CSocket* pSocket, PlayerID pId) + : PlayerClient(pSocket, pId) +{ +} + +PlayerClientOriginal::~PlayerClientOriginal() +{ +} + +/////////////////////////////////////////////////////////////////////////////// + +bool PlayerClientOriginal::warp(std::shared_ptr level, const PixelPosition& position, std::optional clientCachedTime) +{ + // If we are warping to the same level, just update the player's location. + auto localPosition = toLocalPixelPosition(position); + if (!m_currentLevel.expired() && account.level == level->levelName) + { + sendPropsFromResults( + setPropWith(props::SetBy::SERVER, localPosition.x()), + setPropWith(props::SetBy::SERVER, localPosition.y()) + ); + return true; + } + + // Set the player's position. + account.character.localPixelX = localPosition.x(); + account.character.localPixelY = localPosition.y(); + + // Tell the client their new level. + sendPacket(CString() >> (char)PLO_PLAYERWARP << getProp().serialize() << getProp().serialize() << level->levelName); + + // Enter the level. + return enterLevel(level, clientCachedTime); +} + +bool PlayerClientOriginal::enterLevel(std::shared_ptr level, std::optional clientCachedTime) +{ + auto currentLevel = getLevel(); + bool sameLevel = currentLevel == level; + + // Leave the current level if we are changing levels. + if (!sameLevel) + { + leaveLevel(); + m_currentLevel = level; + } + + // Add myself to the level playerlist. + if (!sameLevel) + { + level->addPlayer(m_id); + account.level = level->levelName; + } + + // Send the level now. + auto subLevel = level->getSubLevelAtPosition(getMapPosition()); + bool succeed = sendStaticLevelData(subLevel->staticData.lock(), subLevel, clientCachedTime); + succeed = succeed && sendDynamicLevelData(level, clientCachedTime); + + // If we failed, leave the level and inform the client. + if (!succeed) + { + leaveLevel(); + sendPacket(CString() >> (char)PLO_WARPFAILED << level->levelName); + return false; + } + + // If the level is a sparring zone and you have 100 AP, change AP to 99 and + // the apcounter to 1. + if (level->isSparringZone(getMapPosition()) && account.character.ap == 100) + { + account.apCounter = 1; + sendPropsFromResults(setPropWith(props::SetBy::SERVER, 99_ui8)); + } + + // Inform everybody as to the client's new location. This will update the minimap. + CString minimap = CString() >> (char)PLO_OTHERPLPROPS >> (short)m_id + >> (char)PlayerProp::CURLEVEL << getProp().serialize() + >> (char)PlayerProp::X << getProp().serialize() + >> (char)PlayerProp::Y << getProp().serialize(); + + for (const auto& [pid, player] : players_of_type(m_server->getPlayerList())) + { + if (pid == this->getId()) continue; + player->sendPacket(minimap); + } + + // Update RCs. + CString myRCProps = CString() >> (char)PLO_ADDPLAYER >> (short)getId() >> (char)account.name.length() << account.name + >> (char)PlayerProp::CURLEVEL << getProp().serialize() + >> (char)PlayerProp::PLAYERLISTSTATUS << getProp().serialize() + >> (char)PlayerProp::NICKNAME << getProp().serialize() + >> (char)PlayerProp::COMMUNITYNAME << getProp().serialize(); + m_server->sendPacketToType(PLTYPE_ANYCONTROL, myRCProps, this); + + return true; +} + +bool PlayerClientOriginal::sendStaticLevelData(std::shared_ptr staticLevelData, std::shared_ptr subLevel, std::optional clientCachedTime) +{ + if (staticLevelData == nullptr) + return false; + + PlayerPtr self = shared_from_this(); + auto levelModTime = staticLevelData->modTime; + auto cachedModTime = getLevelLastEnteredTime(staticLevelData.get()); + + // If the player has seen this level before, don't sending anything. + if (cachedModTime.has_value()) + { + // Tell the client that there is no board data to load. + // YOU GOTTA SEND THIS NO MATTER WHAT! If you don't, weird things happen on the client. + sendPacket(CString() >> (char)PLO_LEVELBOARD); + } + // Otherwise, send the level board data. + else + { + if (!clientCachedTime.has_value() || clientCachedTime.value() != levelModTime) + { + // Send board tiles. + if (subLevel != nullptr) + subLevel->sendBoardToPlayer(self); + else + staticLevelData->sendBoardToPlayer(self); + + // Tell the client the current level name. + // Only send it the very first time for original clients. + if (!m_firstLevel) + sendPacket(CString() >> (char)PLO_LEVELNAME << staticLevelData->levelName); + m_firstLevel = true; + + // Send links. + staticLevelData->sendLinksToPlayer(self, false); + + // Send signs. + staticLevelData->sendSignsToPlayer(self); + + // Send the level mod time so the client can cache it. + sendPacket(CString() >> (char)PLO_LEVELMODTIME >> (long long)clock::to_time_t(levelModTime)); + } + else + { + // Tell the client that there is no board data to load. + sendPacket(CString() >> (char)PLO_LEVELBOARD); + } + + // Send chests. + staticLevelData->sendChestsToPlayer(self); + } + + return true; +} + +bool PlayerClientOriginal::sendDynamicLevelData(std::shared_ptr level, std::optional clientCachedTime) +{ + if (level == nullptr) return false; + + // Get the sub-level and static data we are on. + auto [subLevel, staticLevelData] = level->getSubLevelAndStaticDataAtPosition(getMapPosition()); + if (subLevel == nullptr || staticLevelData == nullptr) + return false; + + PlayerPtr self = shared_from_this(); + auto cachedModTime = getLevelLastEnteredTime(staticLevelData.get()); + + // Send board changes, horses, and baddies. + subLevel->sendBoardChangesToPlayer(self, cachedModTime); + + // TODO: Maybe bind horses to sub-level and send in sendStaticLevelData. + level->sendHorsesToPlayer(self); + level->sendBaddiesToPlayer(self); + + // If we are the leader, send it now. + if (level->isSinglePlayer || level->isPlayerLeader(getId())) + sendPacket(CString() >> (char)PLO_ISLEADER); + + // Send NPCs. + level->sendNPCsToPlayer(self, cachedModTime); + + // Move the carry NPC to the new level. + if (m_carryNPC != 0) + { + level->addNPC(m_carryNPC); + if (auto npc = m_server->getNPC(m_carryNPC); npc) + { + npc->setLevel(level); + npc->sendPropsFromResults( + npc->setPropWith(props::SetBy::SERVER, account.character.mapX), + npc->setPropWith(props::SetBy::SERVER, account.character.mapY) + ); + + // Send the carry NPC props to other players. + // if (!level->isSingleplayer) + { + CString carryNPCProps = CString() >> (char)PLO_NPCPROPS >> (int)m_carryNPC << npc->getAllPropsPacket(); + m_server->sendPacketToNearby(carryNPCProps, getGlobalPosition(), level, { m_id }); + } + } + } + + // Send connecting player props to players in nearby levels. + // if (!level->isSingleplayer) + { + CString myProps = CString() >> (char)PLO_OTHERPLPROPS >> (short)m_id >> (char)PlayerProp::JOINLEAVELVL >> (char)1 << getPropsPacketFromList(loginPropsClientOthers); + for (const auto& playerId : level->findInRangePlayersForCommunication(getGlobalPosition())) + { + if (playerId == m_id) continue; + if (auto other = m_server->getPlayer(playerId); other != nullptr) + { + if (!other->isClient()) continue; + + // Exchange props. + other->sendPacket(myProps); + this->sendPacket(CString() >> (char)PLO_OTHERPLPROPS >> (short)other->getId() >> (char)PlayerProp::JOINLEAVELVL >> (char)1 << other->getPropsPacketFromList(loginPropsClientOthers)); + } + } + } + + return true; +} + +/////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/player/PlayerExternalPlayers.cpp b/server/src/player/PlayerExternalPlayers.cpp index 94ea5d0d1..3f91a30b1 100644 --- a/server/src/player/PlayerExternalPlayers.cpp +++ b/server/src/player/PlayerExternalPlayers.cpp @@ -1,6 +1,21 @@ -#include "FileSystem.h" -#include "Player.h" -#include "Server.h" +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// std::vector Player::getPMServerList() { @@ -12,7 +27,7 @@ bool Player::addPMServer(CString& option) auto& list = m_server->getServerList(); bool PMSrvExist = false; - for (auto& pmServer: m_privateMessageServerList) + for (auto& pmServer : m_privateMessageServerList) { if (pmServer.text() == option) { @@ -25,11 +40,11 @@ bool Player::addPMServer(CString& option) { m_privateMessageServerList.push_back(option); list.sendPacket(CString() >> (char)SVO_REQUESTLIST >> (short)m_id << CString(CString() << "GraalEngine" - << "\n" - << "pmserverplayers" - << "\n" - << option << "\n") - .gtokenizeI()); + << "\n" + << "pmserverplayers" + << "\n" + << option << "\n") + .gtokenizeI()); return true; } else @@ -45,7 +60,7 @@ bool Player::remPMServer(CString& option) { // Check if a player has disconnected // By value to keep a hold of the shared_ptr until the next iteration. - for (const auto& [externalId, externalPlayer]: m_externalPlayers) + for (const auto& [externalId, externalPlayer] : m_externalPlayers) { if (option == externalPlayer->getServerName()) { @@ -55,7 +70,7 @@ bool Player::remPMServer(CString& option) if (isRC()) sendPacket(CString() >> (char)PLO_DELPLAYER >> externalId); else - sendPacket(CString() >> (char)PLO_OTHERPLPROPS >> externalId >> (char)PLPROP_PCONNECTED); + sendPacket(CString() >> (char)PLO_OTHERPLPROPS >> externalId >> (char)PlayerProp::DISCONNECT); } } } @@ -84,15 +99,15 @@ bool Player::updatePMPlayers(CString& servername, CString& players) { // Check if a player has disconnected // By value to keep a hold of the shared_ptr until the next iteration. - for (const auto& [externalId, externalPlayer]: m_externalPlayers) + for (const auto& [externalId, externalPlayer] : m_externalPlayers) { bool exist2 = false; - for (auto& p2: players2) + for (auto& p2 : players2) { CString tmpPlyr = p2.guntokenize(); CString account = tmpPlyr.readString("\n"); CString nick = tmpPlyr.readString("\n"); - if (servername == externalPlayer->getServerName() && account == externalPlayer->getAccountName()) + if (servername == externalPlayer->getServerName() && account == externalPlayer->account.name) { exist2 = true; externalPlayer->setNick(CString() << nick << " (on " << servername << ")"); @@ -108,9 +123,9 @@ bool Player::updatePMPlayers(CString& servername, CString& players) if (isRC()) sendPacket(CString() >> (char)PLO_DELPLAYER >> externalId); else - sendPacket(CString() >> (char)PLO_OTHERPLPROPS >> externalId >> (char)PLPROP_PCONNECTED); + sendPacket(CString() >> (char)PLO_OTHERPLPROPS >> externalId >> (char)PlayerProp::DISCONNECT); - //m_server->sendPacketTo(PLTYPE_ANYCLIENT, CString() >> (char)PLO_OTHERPLPROPS >> (short)id >> (char)PLPROP_PCONNECTED, this); + //m_server->sendPacketTo(PLTYPE_ANYCLIENT, CString() >> (char)PLO_OTHERPLPROPS >> (short)id >> (char)PlayerProp::DISCONNECT, this); //m_server->sendPacketTo(PLTYPE_ANYRC, CString() >> (char)PLO_DELPLAYER >> (short)id, this); } } @@ -126,9 +141,9 @@ bool Player::updatePMPlayers(CString& servername, CString& players) bool exist = false; if (!m_externalPlayers.empty()) { - for (auto& [externalId, externalPlayer]: m_externalPlayers) + for (auto& [externalId, externalPlayer] : m_externalPlayers) { - if (servername == externalPlayer->getServerName() && account == externalPlayer->getAccountName()) + if (servername == externalPlayer->getServerName() && account == externalPlayer->account.name) { externalPlayer->setNick(CString() << nick << " (on " << servername << ")"); exist = true; @@ -142,8 +157,8 @@ bool Player::updatePMPlayers(CString& servername, CString& players) auto newId = m_externalPlayerIdGenerator.getAvailableId(); auto tmpPlyr2 = std::make_shared(nullptr, newId); m_externalPlayers[newId] = tmpPlyr2; - tmpPlyr2->loadAccount(account); - tmpPlyr2->setAccountName(account); + m_server->getAccountLoader().loadAccount(account.toString(), tmpPlyr2->account); + tmpPlyr2->account.name = account.toString(); tmpPlyr2->setServerName(servername); tmpPlyr2->setExternal(true); tmpPlyr2->setNick(CString() << nick << " (on " << servername << ")"); @@ -153,15 +168,20 @@ bool Player::updatePMPlayers(CString& servername, CString& players) if (!m_externalPlayers.empty()) { - for (auto& [externalId, externalPlayer]: m_externalPlayers) + for (auto& [externalId, externalPlayer] : m_externalPlayers) { if (isRC()) { - sendPacket(CString() >> (char)PLO_ADDPLAYER >> (short)externalId << externalPlayer->getProp(PLPROP_ACCOUNTNAME) >> (char)PLPROP_NICKNAME << externalPlayer->getProp(PLPROP_NICKNAME) >> (char)PLPROP_UNKNOWN81 >> (char)1); + sendPacket(CString() >> (char)PLO_ADDPLAYER >> (short)externalId << externalPlayer->getProp().serialize() + >> (char)PlayerProp::NICKNAME << externalPlayer->getProp().serialize() + >> (char)PlayerProp::PLAYERLISTCATEGORY >> (char)PlayerListCategory::EXTERNAL); } else { - sendPacket(CString() >> (char)PLO_OTHERPLPROPS >> (short)externalId >> (char)PLPROP_ACCOUNTNAME << externalPlayer->getProp(PLPROP_ACCOUNTNAME) >> (char)PLPROP_NICKNAME << externalPlayer->getProp(PLPROP_NICKNAME) >> (char)PLPROP_UNKNOWN81 >> (char)(1)); + sendPacket(CString() >> (char)PLO_OTHERPLPROPS >> (short)externalId + >> (char)PlayerProp::ACCOUNTNAME << externalPlayer->getProp().serialize() + >> (char)PlayerProp::NICKNAME << externalPlayer->getProp().serialize() + >> (char)PlayerProp::PLAYERLISTCATEGORY >> (char)PlayerListCategory::EXTERNAL); } } } @@ -173,19 +193,19 @@ bool Player::pmExternalPlayer(CString servername, CString account, CString& pmMe { auto& list = m_server->getServerList(); list.sendPacket(CString() >> (char)SVO_PMPLAYER >> (short)m_id << CString(CString() << servername << "\n" - << m_accountName << "\n" - << m_character.nickName << "\n" - << "GraalEngine" - << "\n" - << "pmplayer" - << "\n" - << account << "\n" - << pmMessage) - .gtokenizeI()); + << this->account.name << "\n" + << this->account.character.nickName << "\n" + << "GraalEngine" + << "\n" + << "pmplayer" + << "\n" + << account << "\n" + << pmMessage) + .gtokenizeI()); return true; } -PlayerPtr Player::getExternalPlayer(const uint16_t id, bool includeRC) const +PlayerPtr Player::getExternalPlayer(const PlayerID id, bool includeRC) const { auto iter = m_externalPlayers.find(id); if (iter == std::end(m_externalPlayers)) return nullptr; @@ -197,7 +217,7 @@ PlayerPtr Player::getExternalPlayer(const uint16_t id, bool includeRC) const PlayerPtr Player::getExternalPlayer(const CString& account, bool includeRC) const { - for (auto& [externalId, externalPlayer]: m_externalPlayers) + for (auto& [externalId, externalPlayer] : m_externalPlayers) { if (externalPlayer == 0) continue; @@ -206,8 +226,11 @@ PlayerPtr Player::getExternalPlayer(const CString& account, bool includeRC) cons continue; // Compare account names. - if (externalPlayer->getAccountName().toLower() == account.toLower()) + if (string::equalsi(externalPlayer->account.name, account.toString())) return externalPlayer; } return nullptr; } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/player/PlayerLogin.cpp b/server/src/player/PlayerLogin.cpp index ae14de452..0d8fedcb0 100644 --- a/server/src/player/PlayerLogin.cpp +++ b/server/src/player/PlayerLogin.cpp @@ -1,450 +1,102 @@ -#include - -#include -#include - -#include - -#include "IConfig.h" - -#include "NPC.h" -#include "Player.h" -#include "Server.h" -#include "Weapon.h" -#include "level/Level.h" -#include "level/Map.h" - -#define serverlog m_server->getServerLog() -#define rclog m_server->getRCLog() -extern bool __sendLogin[propscount]; -extern bool __getLogin[propscount]; -extern bool __getLoginNC[propscount]; -extern bool __getRCLogin[propscount]; - -CString _zlibFix( - "//#CLIENTSIDE\xa7" - "if(playerchats) {\xa7" - " this.chr = {ascii(#e(0,1,#c)),0,0,0,0};\xa7" - " for(this.c=0;this.c=11);this.c++) {\xa7" - " this.chr[2] = ascii(#e(this.c,1,#c));\xa7" - " this.chr[3] += 1*(this.chr[2]==this.chr[0]);\xa7" - " if(!(this.chr[2] in {this.chr[0],this.chr[1]})) {\xa7" - " if(this.chr[1]==0) {\xa7" - " if(this.chr[2]!=this.chr[0]) this.chr[1]=this.chr[2];\xa7" - " } else break; //[A][B][C]\xa7" - " }\xa7" - " this.chr[4] += 1*(this.chr[2]==this.chr[1]);\xa7" - " if(this.chr[1]>0 && this.chr[3] in |2,10|) break; //[1=11 && this.chr[4]>1) break; //[A>=11][B>1]\xa7" - " }\xa7" - " if(this.c>0 && this.c == strlen(#c)) setplayerprop #c,\xa0#c\xa0; //Pad\xa7" - "}\xa7"); - -/* - Player: Manage Account -*/ -bool Player::sendLogin() +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal { - // We don't need to check if this fails.. because the defaults have already been loaded :) - loadAccount(m_accountName, (isRC() || isNC() ? true : false)); - - // Check to see if the player is banned or not. - if (m_isBanned && !hasRight(PLPERM_MODIFYSTAFFACCOUNT)) - { - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "You have been banned. Reason: " << m_banReason.guntokenize().replaceAll("\n", "\r")); - return false; - } - - // If we are an RC, check to see if we can log in. - if (isRC() || isNC()) - { - // Check and see if we are allowed in. - if (!isStaff() || !isAdminIp()) - { - rclog.out("Attempted RC login by %s.\n", m_accountName.text()); - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "You do not have RC rights."); - return false; - } - } - - // NPC-Control login, then return. - if (isNC()) - return sendLoginNC(); - - // Check to see if we can log in if we are a client. - if (isClient()) - { - // Staff only. - if (m_server->getSettings().getBool("onlystaff", false) && !isStaff()) - { - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "This server is currently restricted to staff only."); - return false; - } - - // Check and see if we are allowed in. - std::vector adminIps = m_adminIp.tokenize(","); - if (!isAdminIp() && vecSearch(adminIps, "0.0.0.0") == -1) - { - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Your IP doesn't match one of the allowed IPs for this account."); - return false; - } - } - - // Server Signature - // 0x49 (73) is used to tell the client that more than eight - // players will be playing. - sendPacket(CString() >> (char)PLO_SIGNATURE >> (char)73); - - if (m_server->getName().findi("login") > -1) - { - sendPacket(CString() >> (char)PLO_FULLSTOP); - sendPacket(CString() >> (char)PLO_GHOSTICON >> (char)1); - } - - if (isClient()) - { -#ifdef V8NPCSERVER - // If we have an NPC Server, send this to prevent clients from sending - // npc props it modifies. - // - // NOTE: This may have been deprecated after v5/v6, don't see it in iLogs - sendPacket(CString() >> (char)PLO_HASNPCSERVER); -#endif - sendPacket(CString() >> (char)PLO_UNKNOWN168); - } - - // Check if the account is already in use. - if (!getGuest()) - { - auto& playerList = m_server->getPlayerList(); - for (auto& [pid, player]: playerList) - { - CString oacc = player->getAccountName(); - unsigned short oid = (unsigned short)player->getId(); - int meClient = ((m_type & PLTYPE_ANYCLIENT) ? 0 : ((m_type & PLTYPE_ANYRC) ? 1 : 2)); - int themClient = ((player->getType() & PLTYPE_ANYCLIENT) ? 0 : ((player->getType() & PLTYPE_ANYRC) ? 1 : 2)); - if (oacc.comparei(m_accountName) && meClient == themClient && oid != m_id) - { - if ((int)difftime(time(0), player->getLastData()) > 30) - { - player->sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Someone else has logged into your account."); - player->disconnect(); - } - else - { - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Account is already in use."); - return false; - } - } - } - } - - // TODO(joey): Placing this here so warp doesn't queue events for this player before - // the login is finished. The server should get first dibs on the player. - m_server->playerLoggedIn(shared_from_this()); - - // Player's load different than RCs. - bool succeeded = false; - if (isClient()) succeeded = sendLoginClient(); - else if (isRC()) - succeeded = sendLoginRC(); - if (succeeded == false) return false; - - // Set loaded to true so our account is saved when we leave. - // This also lets us send data. - m_loaded = true; - - auto& settings = m_server->getSettings(); - - // Send out what guilds should be placed in the Staff section of the playerlist. - std::vector guilds = settings.getStr("staffguilds").tokenize(","); - CString guildPacket = CString() >> (char)PLO_STAFFGUILDS; - for (std::vector::iterator i = guilds.begin(); i != guilds.end(); ++i) - guildPacket << "\"" << ((CString)(*i)).trim() << "\","; - sendPacket(guildPacket.remove(guildPacket.length() - 1, 1)); - - // Send out the server's available status list options. - if ((isClient() && m_versionId >= CLVER_2_1) || isRC()) - { - // graal doesn't quote these - CString pliconPacket = CString() >> (char)PLO_STATUSLIST; - for (const auto& status: m_server->getStatusList()) - pliconPacket << status.trim() << ","; - - sendPacket(pliconPacket.remove(pliconPacket.length() - 1, 1)); - } - - // This comes after status icons for RC - if (isRC()) - sendPacket(CString() >> (char)PLO_RC_MAXUPLOADFILESIZE >> (long long)(1048576 * 20)); - - // Then during iterating the playerlist to send players to the rc client, it sends addplayer followed by rc chat per person. - - // Exchange props with everybody on the server. - { - // RC props are sent in a "special" way. As in retarded. - CString myRCProps; - myRCProps >> (char)PLO_ADDPLAYER >> (short)m_id >> (char)m_accountName.length() << m_accountName >> (char)PLPROP_CURLEVEL << getProp(PLPROP_CURLEVEL) >> (char)PLPROP_PSTATUSMSG << getProp(PLPROP_PSTATUSMSG) >> (char)PLPROP_NICKNAME << getProp(PLPROP_NICKNAME) >> (char)PLPROP_COMMUNITYNAME << getProp(PLPROP_COMMUNITYNAME); - - // Get our client props. - CString myClientProps = (isClient() ? getProps(__getLogin, sizeof(__getLogin) / sizeof(bool)) : getProps(__getRCLogin, sizeof(__getRCLogin) / sizeof(bool))); - - CString rcsOnline; - auto& playerList = m_server->getPlayerList(); - for (auto& [pid, player]: playerList) - { - if (player.get() == this) continue; - - // Don't send npc-control players to others - if (player->isNC()) continue; - - // Send the other player my props. - // Send my flags to the npcserver. - player->sendPacket(player->isClient() ? myClientProps : myRCProps); - - // Add Player / RC. - if (isClient()) - sendPacket(player->isClient() ? player->getProps(__getLogin, sizeof(__getLogin) / sizeof(bool)) : player->getProps(__getRCLogin, sizeof(__getRCLogin) / sizeof(bool))); - else - { - // Level name. If no level, send an empty space. - CString levelName = (player->getLevel() ? player->getLevel()->getLevelName() : " "); - - // Get the other player's RC props. - this->sendPacket(CString() >> (char)PLO_ADDPLAYER >> (short)player->getId() >> (char)player->getAccountName().length() << player->getAccountName() >> (char)PLPROP_CURLEVEL >> (char)levelName.length() << levelName >> (char)PLPROP_PSTATUSMSG << player->getProp(PLPROP_PSTATUSMSG) >> (char)PLPROP_NICKNAME << player->getProp(PLPROP_NICKNAME) >> (char)PLPROP_COMMUNITYNAME << player->getProp(PLPROP_COMMUNITYNAME)); - - // If the other player is an RC, add them to the list of logged in RCs. - if (player->isRC()) - rcsOnline << (rcsOnline.isEmpty() ? "" : ", ") << player->getAccountName(); - } - } - - // If we are an RC, announce the list of currently logged in RCs. - if (isRC() && !rcsOnline.isEmpty()) - sendPacket(CString() >> (char)PLO_RC_CHAT << "Currently online: " << rcsOnline); - } - - // Ask for processes. This causes windows v6 clients to crash - if (isClient() && m_versionId < CLVER_6_015) - sendPacket(CString() >> (char)PLO_LISTPROCESSES); +/////////////////////////////////////////////////////////////////////////////// - return true; +PlayerLogin::PlayerLogin(CSocket* pSocket, PlayerID pId) + : Player(pSocket, pId) +{ } -bool Player::sendLoginClient() +PlayerLogin::~PlayerLogin() { - auto& settings = m_server->getSettings(); - - // Recalculate player spar deviation. - { - // c = sqrt( (350*350 - 50*50) / t ) - // where t is the number of rating periods for deviation to go from 50 to 350. - // t = 60 days for us. - const float c = 44.721f; - time_t current_time = time(0); - time_t periods = (current_time - m_lastSparTime) / 60 / 60 / 24; - if (periods != 0) - { - // Find the new deviation. - float deviate = MIN(sqrt((m_eloDeviation * m_eloDeviation) + (c * c) * periods), 350.0f); - - // Set the new rating. - m_eloDeviation = deviate; - m_lastSparTime = current_time; - } - } - - // Send the player his login props. - sendProps(__sendLogin, sizeof(__sendLogin) / sizeof(bool)); - - // Workaround for the 2.31 client. It doesn't request the map file when used with setmap. - // So, just send them all the maps loaded into the server. - if (m_versionId == CLVER_2_31 || m_versionId == CLVER_1_411) - { - for (const auto& map: m_server->getMapList()) - { - if (map->getType() == MapType::BIGMAP) - msgPLI_WANTFILE(CString() << map->getMapName()); - } - } - - // Sent to rc and client, but rc ignores it so... - sendPacket(CString() >> (char)PLO_CLEARWEAPONS); - - // If the gr.ip hack is enabled, add it to the player's flag list. - if (settings.getBool("flaghack_ip", false) == true) - this->setFlag("gr.ip", this->m_accountIpStr, true); - - // Send the player's flags. - for (auto i = m_flagList.begin(); i != m_flagList.end(); ++i) - { - if (i->second.isEmpty()) sendPacket(CString() >> (char)PLO_FLAGSET << i->first); - else - sendPacket(CString() >> (char)PLO_FLAGSET << i->first << "=" << i->second); - } - - // Send the server's flags to the player. - auto& serverFlags = m_server->getServerFlags(); - for (auto& [flag, value]: serverFlags) - sendPacket(CString() >> (char)PLO_FLAGSET << flag << "=" << value); - - // Delete the bomb and bow. They get automagically added by the client for - // God knows which reason. Bomb and Bow must be capitalized. - sendPacket(CString() >> (char)PLO_NPCWEAPONDEL << "Bomb"); - sendPacket(CString() >> (char)PLO_NPCWEAPONDEL << "Bow"); - - // Send the player's weapons. - for (auto& weaponName: m_weaponList) - { - auto weapon = m_server->getWeapon(weaponName.toString()); - if (weapon == nullptr) - { - // Let's check to see if it is a default weapon. If so, we can add it to the server now. - if (auto itemType = LevelItem::getItemId(weaponName.toString()); itemType != LevelItemType::INVALID) - { - CString defWeapPacket = CString() >> (char)PLI_WEAPONADD >> (char)0 >> (char)LevelItem::getItemTypeId(itemType); - defWeapPacket.readGChar(); - msgPLI_WEAPONADD(defWeapPacket); - continue; - } - continue; - } - sendPacket(weapon->getWeaponPacket(m_versionId)); - } - - // Send any protected weapons we do not have. - auto protectedWeapons = m_server->getSettings().getStr("protectedweapons").gCommaStrTokens(); - std::erase_if(protectedWeapons, [this](CString& val) - { - return std::find(m_weaponList.begin(), m_weaponList.end(), val) != m_weaponList.end(); - }); - for (auto& weaponName: protectedWeapons) - this->addWeapon(weaponName.toString()); - - if (m_versionId >= CLVER_4_0211) - { - // Send the player's weapons. - for (auto& i: m_server->getClassList()) - { - if (i.second != nullptr) - sendPacket(i.second->getClassPacket()); - } - } - - // Send the zlib fixing NPC to client versions 2.21 - 2.31. - if (m_versionId >= CLVER_2_21 && m_versionId <= CLVER_2_31) - { - sendPacket(CString() >> (char)PLO_NPCWEAPONADD >> (char)12 << "-gr_zlib_fix" >> (char)0 >> (char)1 << "-" >> (char)1 >> (short)_zlibFix.length() << _zlibFix); - } - - // Was blank. Sent before weapon list. - sendPacket(CString() >> (char)PLO_UNKNOWN190); - - // Send the level to the player. - // warp will call sendCompress() for us. - if (!warp(m_levelName, getX(), getY()) && m_currentLevel.expired()) - { - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "No level available."); - serverlog.out(CString() << "[" << m_server->getName() << "] " - << "Cannot find level for " << m_accountName << "\n"); - return false; - } - - // Send the bigmap if it was set. - if (isClient() && m_versionId >= CLVER_2_1) - { - CString bigmap = settings.getStr("bigmap"); - if (!bigmap.isEmpty()) - { - std::vector vbigmap = bigmap.tokenize(","); - if (vbigmap.size() == 4) - sendPacket(CString() >> (char)PLO_BIGMAP << vbigmap[0].trim() << "," << vbigmap[1].trim() << "," << vbigmap[2].trim() << "," << vbigmap[3].trim()); - } - } - - // Send the minimap if it was set. - if (isClient() && m_versionId >= CLVER_2_1) - { - CString minimap = settings.getStr("minimap"); - if (!minimap.isEmpty()) - { - std::vector vminimap = minimap.tokenize(","); - if (vminimap.size() == 4) - sendPacket(CString() >> (char)PLO_MINIMAP << vminimap[0].trim() << "," << vminimap[1].trim() << "," << vminimap[2].trim() << "," << vminimap[3].trim()); - } - } +} - // Send out RPG Window greeting. - if (isClient() && m_versionId >= CLVER_2_1) - sendPacket(CString() >> (char)PLO_RPGWINDOW << "\"Welcome to " << settings.getStr("name") << ".\",\"" << CString(APP_VENDOR) << " " << CString(APP_NAME) << " programmed by " << CString(APP_CREDITS) << ".\""); +/////////////////////////////////////////////////////////////////////////////// - // Send the start message to the player. - sendPacket(CString() >> (char)PLO_STARTMESSAGE << m_server->getServerMessage()); +bool PlayerLogin::onRecv() +{ + Player::onRecv(); + return PacketCount == 0; +} - // This will allow serverwarp and some other things, for some reason. - sendPacket(CString() >> (char)PLO_SERVERTEXT); +/////////////////////////////////////////////////////////////////////////////// - m_fileQueue.sendCompress(true); +HandlePacketResult PlayerLogin::handlePacket(std::optional id, CString& packet) +{ + // TODO: Websocket stuff somewhere. + if (msgLoginPacket(packet) == HandlePacketResult::Failed) + disconnect(); - return true; + return HandlePacketResult::Handled; } -bool Player::sendLoginNC() +HandlePacketResult PlayerLogin::msgLoginPacket(CString& pPacket) { - // Send database npcs - auto& npcList = m_server->getNPCNameList(); - for (auto& [npcName, npcPtr]: npcList) - { - auto npc = npcPtr.lock(); - if (npc == nullptr) continue; + // TODO(joey): Hijack type based on what graal sends, rather than use it directly. + m_type = (1 << pPacket.readGChar()); - CString npcPacket = CString() >> (char)PLO_NC_NPCADD >> (int)npc->getId() >> (char)NPCPROP_NAME << npc->getProp(NPCPROP_NAME) >> (char)NPCPROP_TYPE << npc->getProp(NPCPROP_TYPE) >> (char)NPCPROP_CURLEVEL << npc->getProp(NPCPROP_CURLEVEL); - sendPacket(npcPacket); + // Create our appropriate player. + std::shared_ptr player = nullptr; + if (m_type & PLTYPE_ANYCLIENT) + { + if (m_type == PLTYPE_CLIENT) + player = std::make_shared(m_playerSock, m_id); + else player = std::make_shared(m_playerSock, m_id); } - - // Send classes - CString classPacket; - auto& classList = m_server->getClassList(); - for (auto it = classList.begin(); it != classList.end(); ++it) - classPacket >> (char)PLO_NC_CLASSADD << it->first << "\n"; - sendPacket(classPacket); - - // Send list of currently connected NC's - auto& playerList = m_server->getPlayerList(); - for (auto& [playerId, player]: playerList) + else if (m_type & PLTYPE_ANYRC) + player = std::make_shared(m_playerSock, m_id); + else if (m_type & PLTYPE_ANYNC) + player = std::make_shared(m_playerSock, m_id); + else if (m_type & PLTYPE_NPCSERVER) + player = std::make_shared(m_playerSock, m_id); + else { - if (player.get() != this && player->isNC()) - sendPacket(CString() >> (char)PLO_RC_CHAT << "New NC: " << player->getAccountName()); + log::printLine(log::server, "New login, but unknown player type: {}", m_type); + return HandlePacketResult::Failed; } - // Announce to other nc's that we logged in - m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_RC_CHAT << "New NC: " << m_accountName, this); + // Fix the type. + player->setType(m_type); - m_loaded = true; - return true; -} - -bool Player::sendLoginRC() -{ - // This packet clears the players weapons on the client, but official - // also sends it to the RC's so we are maintaining the same behavior - sendPacket(CString() >> (char)PLO_CLEARWEAPONS); + // Update the new player's current packet state to match ours. + player->PacketCount = 1; + player->setReceivedBuffer(m_recvBuffer); - // If no nickname was specified, set the nickname to the account name. - if (m_character.nickName.length() == 0) - m_character.nickName = (CString("*") << m_accountName).toString(); - m_levelName = " "; + // Remove ourselves from the server. + // We need to null our socket to avoid being passed data by the socket manager. + auto self = shared_from_this(); + m_server->swapPlayer(self, player); + m_playerSock = nullptr; - // Set the head to the server's set staff head. - setHeadImage(m_server->getSettings().getStr("staffhead", "head25.png")); + // Pass the login to the new player. + pPacket.setRead(0); + if (player != nullptr && !player->handleLogin(pPacket)) + player->disconnect(); - // Send the RC join message to the RC. - std::vector rcmessage = CString::loadToken(m_server->getServerPath() << "config/rcmessage.txt", "\n", true); - for (const auto& i: rcmessage) - sendPacket(CString() >> (char)PLO_RC_CHAT << i); - - sendPacket(CString() >> (char)PLO_UNKNOWN190); - - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "New RC: " << m_accountName); - return true; + return HandlePacketResult::Handled; } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/player/PlayerNC.cpp b/server/src/player/PlayerNC.cpp index d78d14cfe..d6642b19f 100644 --- a/server/src/player/PlayerNC.cpp +++ b/server/src/player/PlayerNC.cpp @@ -1,618 +1,202 @@ -#include +#include +#include +#include #include -#include +#include +#include +#include #include +#include -#include "NPC.h" -#include "Player.h" -#include "Server.h" -#include "Weapon.h" -#include "level/Level.h" +#include +#include +#include +#include +#include +#include +#include -#define serverlog m_server->getServerLog() -#define npclog m_server->getNPCLog() -#define rclog m_server->getRCLog() - -typedef bool (Player::*TPLSock)(CString&); -extern std::vector TPLFunc; // From Player.cpp - -#ifdef V8NPCSERVER -bool Player::msgPLI_NC_NPCGET(CString& pPacket) -{ - if (!isNC()) - { - npclog.out("[Hack] %s attempted to get a database npc.\n", m_accountName.text()); - return false; - } - - // RC3 keeps sending empty packets of this, yet still uses NPCGET to fetch npcs. Maybe its for pinging the server - // for updated level information on database npcs? Just a thought.. - // 5/26/2019 - confirmed, this is the npc-server pinging the gserver. - if (pPacket.bytesLeft()) - { - unsigned int npcId = pPacket.readGUInt(); - - auto npc = m_server->getNPC(npcId); - if (npc != nullptr) - { - CString npcDump = npc->getVariableDump(); - sendPacket(CString() >> (char)PLO_NC_NPCATTRIBUTES << npcDump.gtokenize()); - } - } - - return true; -} - -bool Player::msgPLI_NC_NPCDELETE(CString& pPacket) -{ - if (!isNC()) - { - npclog.out("[Hack] %s attempted to delete a database npc.\n", m_accountName.text()); - return false; - } - - unsigned int npcId = pPacket.readGUInt(); - auto npc = m_server->getNPC(npcId); - - if (npc != nullptr && npc->getType() == NPCType::DBNPC) - { - CString npcName = npc->getName(); - bool result = m_server->deleteNPC(npc, true); - if (result) - { - m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_NC_NPCDELETE >> (int)npcId); - - CString logMsg; - logMsg << "NPC " << npcName << " deleted by " << m_accountName << "\n"; - npclog.out(logMsg); - m_server->sendToNC(logMsg); - } - } - - return true; -} - -bool Player::msgPLI_NC_NPCRESET(CString& pPacket) +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal { - if (!isNC()) - { - npclog.out("[Hack] %s attempted to reset a database npc.\n", m_accountName.text()); - return false; - } +/////////////////////////////////////////////////////////////////////////////// - unsigned int npcId = pPacket.readGUInt(); - - auto npc = m_server->getNPC(npcId); - if (npc != nullptr && npc->getType() == NPCType::DBNPC) - { - npc->resetNPC(); - - CString logMsg; - logMsg << "NPC script of " << npc->getName() << " reset by " << m_accountName << "\n"; - npclog.out(logMsg); - m_server->sendToNC(logMsg); - } - - return true; -} +using PacketHandleFunc = HandlePacketResult(PlayerNC::*)(CString&); +using PacketHandleArray = std::array; -bool Player::msgPLI_NC_NPCSCRIPTGET(CString& pPacket) +static PacketHandleArray GeneratePacketHandlers() { - if (!isNC()) - { - npclog.out("[Hack] %s attempted to get a database npc script.\n", m_accountName.text()); - return false; - } - - // {160}{INT id}{GSTRING script} - unsigned int npcId = pPacket.readGUInt(); - auto npc = m_server->getNPC(npcId); - if (npc != nullptr) - { - CString code = npc->getSource().getSource(); - sendPacket(CString() >> (char)PLO_NC_NPCSCRIPT >> (int)npcId << code.gtokenize()); - } - // else printf("npc doesn't exist\n"); - - return true; -} - -bool Player::msgPLI_NC_NPCWARP(CString& pPacket) -{ - if (!isNC()) - { - npclog.out("[Hack] %s attempted to warp a database npc.\n", m_accountName.text()); - return false; - } - - unsigned int npcId = pPacket.readGUInt(); - float npcX = (float)pPacket.readGUChar() / 2.0f; - float npcY = (float)pPacket.readGUChar() / 2.0f; - CString npcLevel = pPacket.readString(""); + PacketHandleArray handlers{}; + handlers.fill(nullptr); - auto npc = m_server->getNPC(npcId); - if (npc != nullptr) - { - auto newLevel = m_server->getLevel(npcLevel.toString()); - if (newLevel != nullptr) - npc->warpNPC(newLevel, int(npcX * 16.0), int(npcY * 16.0)); - } + handlers[PLI_RC_CHAT] = &PlayerNC::msgPLI_RC_CHAT; + handlers[PLI_NC_NPCGET] = &PlayerNC::msgPLI_NC_NPCGET; + handlers[PLI_NC_NPCDELETE] = &PlayerNC::msgPLI_NC_NPCDELETE; + handlers[PLI_NC_NPCRESET] = &PlayerNC::msgPLI_NC_NPCRESET; + handlers[PLI_NC_NPCSCRIPTGET] = &PlayerNC::msgPLI_NC_NPCSCRIPTGET; + handlers[PLI_NC_NPCWARP] = &PlayerNC::msgPLI_NC_NPCWARP; + handlers[PLI_NC_NPCFLAGSGET] = &PlayerNC::msgPLI_NC_NPCFLAGSGET; + handlers[PLI_NC_NPCSCRIPTSET] = &PlayerNC::msgPLI_NC_NPCSCRIPTSET; + handlers[PLI_NC_NPCFLAGSSET] = &PlayerNC::msgPLI_NC_NPCFLAGSSET; + handlers[PLI_NC_NPCADD] = &PlayerNC::msgPLI_NC_NPCADD; + handlers[PLI_NC_CLASSEDIT] = &PlayerNC::msgPLI_NC_CLASSEDIT; + handlers[PLI_NC_CLASSADD] = &PlayerNC::msgPLI_NC_CLASSADD; + handlers[PLI_NC_LOCALNPCSGET] = &PlayerNC::msgPLI_NC_LOCALNPCSGET; + handlers[PLI_NC_WEAPONLISTGET] = &PlayerNC::msgPLI_NC_WEAPONLISTGET; + handlers[PLI_NC_WEAPONGET] = &PlayerNC::msgPLI_NC_WEAPONGET; + handlers[PLI_NC_WEAPONADD] = &PlayerNC::msgPLI_NC_WEAPONADD; + handlers[PLI_NC_WEAPONDELETE] = &PlayerNC::msgPLI_NC_WEAPONDELETE; + handlers[PLI_NC_CLASSDELETE] = &PlayerNC::msgPLI_NC_CLASSDELETE; + handlers[PLI_NC_LEVELLISTGET] = &PlayerNC::msgPLI_NC_LEVELLISTGET; - return true; + return handlers; } -bool Player::msgPLI_NC_NPCFLAGSGET(CString& pPacket) -{ - if (!isNC()) - { - npclog.out("[Hack] %s attempted to get a database npc flags.\n", m_accountName.text()); - return false; - } - - unsigned int npcId = pPacket.readGUInt(); - auto npc = m_server->getNPC(npcId); - if (npc != nullptr) - { - CString flagListStr; - - auto& flagList = npc->getFlagList(); - for (const auto& [flag, value]: flagList) - flagListStr << flag << "=" << value << "\n"; - - sendPacket(CString() >> (char)PLO_NC_NPCFLAGS >> (int)npcId << flagListStr.gtokenize()); - } +/////////////////////////////////////////////////////////////////////////////// - return true; -} - -bool Player::msgPLI_NC_NPCSCRIPTSET(CString& pPacket) +HandlePacketResult PlayerNC::handlePacket(std::optional id, CString& packet) { - if (!isNC()) - { - npclog.out("[Hack] %s attempted to set a database npc script.\n", m_accountName.text()); - return false; - } + static PacketHandleArray PacketHandlers = GeneratePacketHandlers(); - unsigned int npcId = pPacket.readGUInt(); - CString npcScript = pPacket.readString("").guntokenize(); + auto handle = id.has_value() ? PacketHandlers[id.value()] : nullptr; + if (handle == nullptr) + return Player::handlePacket(id, packet); - // TODO: Validate permissions + auto result = (this->*handle)(packet); + if (result == HandlePacketResult::Bubble) + return Player::handlePacket(id, packet); - auto npc = m_server->getNPC(npcId); - if (npc != nullptr) - { - npc->setScriptCode(npcScript.toString()); - npc->saveNPC(); - - CString logMsg; - logMsg << "NPC script of " << npc->getName() << " updated by " << m_accountName << "\n"; - npclog.out(logMsg); - m_server->sendToNC(logMsg); - } - - return true; + return result; } -bool Player::msgPLI_NC_NPCFLAGSSET(CString& pPacket) -{ - if (!isNC()) - { - npclog.out("[Hack] %s attempted to set a database npc flags.\n", m_accountName.text()); - return false; - } - - unsigned int npcId = pPacket.readGUInt(); - CString npcFlags = pPacket.readString("").guntokenize(); - - auto npc = m_server->getNPC(npcId); - if (npc != nullptr) - { - auto& flagList = npc->getFlagList(); - auto newFlags = npcFlags.tokenize("\n"); - - CString addedFlagMsg, deletedFlagMsg; - std::unordered_map newFlagList; - - // Iterate the new list of flags from the client - for (auto& flag: newFlags) - { - std::string flagName = flag.readString("=").text(); - CString flagValue = flag.readString(""); - - // Check if the flag is a new flag, or if it has been updated - auto oldFlag = flagList.find(flagName); - if (oldFlag == flagList.end()) - addedFlagMsg << "flag added:\t" << flagName << "=" << flagValue << "\n"; - else if (oldFlag->second != flagValue) - { - addedFlagMsg << "flag added:\t" << flagName << "=" << flagValue << "\n"; - deletedFlagMsg << "flag deleted:\t" << oldFlag->first << "=" << oldFlag->second << "\n"; - flagList.erase(oldFlag); - } - - // Add the flag to the new list - newFlagList[flagName] = flagValue; - } - - // Iterate the old flag list, and find any flags not present in the new flag list. - for (const auto& [flag, value]: flagList) - { - auto newFlag = newFlagList.find(flag); - if (newFlag == newFlagList.end()) - deletedFlagMsg << "flag deleted:\t" << flag << "=" << value << "\n"; - } - - // Update flag list, and save the changes - flagList = std::move(newFlagList); - npc->saveNPC(); - - // Logging - CString updateMsg, logMsg; - updateMsg << "NPC flags of " << npc->getName() << " updated by " << m_accountName; - logMsg << updateMsg << "\n" - << addedFlagMsg << deletedFlagMsg; - npclog.out(logMsg); - m_server->sendToNC(updateMsg); - } +/////////////////////////////////////////////////////////////////////////////// - return true; -} - -bool Player::msgPLI_NC_NPCADD(CString& pPacket) +bool PlayerNC::handleLogin(CString& pPacket) { - if (!isNC()) - { - npclog.out("[Hack] %s attempted to add a database npc.\n", m_accountName.text()); - return false; - } - - CString npcData = pPacket.readString("").guntokenize(); - CString npcName = npcData.readString("\n").trim(); - CString npcId = npcData.readString("\n"); - CString npcType = npcData.readString("\n"); - CString npcScripter = npcData.readString("\n"); - CString npcLevel = npcData.readString("\n"); - CString npcX = npcData.readString("\n"); - CString npcY = npcData.readString("\n"); - - // Require a name - if (npcName.isEmpty()) - return true; - - auto level = m_server->getLevel(npcLevel.toString()); - if (level == nullptr) - { - m_server->sendToNC("Error adding database npc: Level does not exist"); - return true; - } - - auto newNpc = m_server->addServerNpc(strtoint(npcId), (float)strtofloat(npcX), (float)strtofloat(npcY), level, true); - if (newNpc != nullptr) - { - m_server->assignNPCName(newNpc, npcName.toString()); - - CString npcProps = CString() >> (char)NPCPROP_NAME << newNpc->getProp(NPCPROP_NAME) >> (char)NPCPROP_TYPE >> (char)npcType.length() << npcType >> (char)NPCPROP_CURLEVEL << newNpc->getProp(NPCPROP_CURLEVEL); - - // NOTE: This can't be sent to other clients, so rather than assemble the same packet twice just set the scripter separately. - newNpc->setProps(CString() >> (char)NPCPROP_SCRIPTER >> (char)npcScripter.length() << npcScripter << npcProps); - - // Send packet to npc controls about new npc - m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_NC_NPCADD >> (int)newNpc->getId() << npcProps); - - // Persist NPC - newNpc->saveNPC(); - - // Logging - CString logMsg; - logMsg << "NPC " << newNpc->getName() << " added by " << m_accountName << "\n"; - npclog.out(logMsg); - m_server->sendToNC(logMsg); - } - - return true; -} - - #include "scripting/ScriptClass.h" - -bool Player::msgPLI_NC_CLASSEDIT(CString& pPacket) -{ - if (!isNC()) - { - npclog.out("[Hack] %s attempted to edit a class.\n", m_accountName.text()); - return false; - } - - // {112}{class} - CString className = pPacket.readString(""); - auto classObj = m_server->getClass(className.text()); - - if (classObj != nullptr) - { - CString classCode(classObj->getSource().getSource()); - - CString ret; - ret >> (char)PLO_NC_CLASSGET >> (char)className.length() << className << classCode.gtokenize(); - sendPacket(ret); - } - - return true; -} - -bool Player::msgPLI_NC_CLASSADD(CString& pPacket) -{ - if (!isNC()) - { - npclog.out("[Hack] %s attempted to add a class.\n", m_accountName.text()); - return false; - } - - // {113}{CHAR name length}{name}{GSTRING script} - std::string className = pPacket.readChars(pPacket.readGUChar()).text(); - CString classCode = pPacket.readString("").guntokenize(); - - bool hasClass = m_server->hasClass(className); - m_server->updateClass(className, classCode.text()); + // Read Player-Ip + account.ipAddress = m_playerSock->getRemoteIp(); +#ifdef HAVE_INET_PTON + inet_pton(AF_INET, account.ipAddress.c_str(), &m_accountIp); +#else + m_accountIp = inet_addr(account.ipAddress.c_str()); +#endif - // Update Player-Weapons - m_server->updateClassForPlayers(m_server->getClass(className)); + // TODO(joey): Hijack type based on what graal sends, rather than use it directly. + m_type = (1 << pPacket.readGChar()); - if (!hasClass) + // Set the encryptions. + log::print(log::server, "New login: "); + switch (m_type) { - CString ret; - ret >> (char)PLO_NC_CLASSADD << className; - m_server->sendPacketToType(PLTYPE_ANYNC, ret); + case PLTYPE_NC: + log::printLine(log::server, "NC"); + Encryption.setGen(ENCRYPT_GEN_2); + break; + default: + log::printLine(log::server, "Unknown ({})", m_type); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Your client type is unknown. Please inform the " << APP_VENDOR << " Team. Type: " << CString((int)m_type) << "."); + return false; } - // Logging - CString logMsg; - logMsg << "Script " << className << " " << (!hasClass ? "added" : "updated") << " by " << m_accountName << "\n"; - npclog.out(logMsg); - m_server->sendToNC(logMsg); - return true; -} - -bool Player::msgPLI_NC_CLASSDELETE(CString& pPacket) -{ - if (!isNC()) + // Newer RC clients have an encryption key. + if (Encryption.getGen() > ENCRYPT_GEN_3) { - npclog.out("[Hack] %s attempted to delete a class.\n", m_accountName.text()); - return false; - } - - std::string className = pPacket.readString("").text(); + m_encryptionKey = (unsigned char)pPacket.readGChar(); - CString logMsg; - if (m_server->deleteClass(className)) - { - CString ret; - ret >> (char)PLO_NC_CLASSDELETE << className; - m_server->sendPacketToType(PLTYPE_ANYNC, ret); - logMsg << m_accountName << " has deleted class " << className << "\n"; + Encryption.reset(m_encryptionKey); + if (Encryption.getGen() > ENCRYPT_GEN_3) + m_fileQueue.setCodec(Encryption.getGen(), m_encryptionKey); } - else - logMsg << "error: " << className << " does not exist on this server!\n"; - // Logging - npclog.out(logMsg); - m_server->sendToNC(logMsg); - return true; -} + // Read Client-Version + m_version = pPacket.readChars(8); + m_versionId = getVersionIDByVersion(m_version); -bool Player::msgPLI_NC_LOCALNPCSGET(CString& pPacket) -{ - if (!isNC()) - { - npclog.out("[Hack] %s attempted to view level npcs.\n", m_accountName.text()); - return false; - } + // Read Account & Password + account.name = pPacket.readChars(pPacket.readGUChar()).toString(); + CString password = pPacket.readChars(pPacket.readGUChar()); - // {114}{level} - CString level = pPacket.readString(""); - if (level.isEmpty()) - return true; + // Client Identity: win,"",02e2465a2bf38f8a115f6208e9938ac8,ff144a9abb9eaff4b606f0336d6d8bc5,"6.2 9200 " + // {platform}, {mobile provides 'dc:id2'}, {md5hash:harddisk-id}, {md5hash:network-id}, {uname(release, version)}, {android-id} + CString identity = pPacket.readString(""); - auto npcLevel = m_server->getLevel(level.toString()); - if (npcLevel != nullptr) { - CString npcDump; - // Variables dump from level mapname (level.nw) - npcDump << "Variables dump from level " << npcLevel->getLevelName() << "\n"; + auto indent = log::server.indent(); - for (auto npcId: npcLevel->getNPCs()) + //log::printLine(log::server, "Key: {}", key); + log::printLine(log::server, "Version: {} ({})", m_version, getVersionString(m_version, m_type)); + log::printLine(log::server, "Account: {}", account.name); + if (!identity.isEmpty()) { - auto npc = m_server->getNPC(npcId); - npcDump << "\n" - << npc->getVariableDump() << "\n"; + log::printLine(log::server, "Identity: {}", identity); + auto identityTokens = identity.tokenize(",", true); + m_os = identityTokens[0]; } - - sendPacket(CString() >> (char)PLO_NC_LEVELDUMP << npcDump.gtokenize()); } - return true; -} - -bool Player::msgPLI_NC_WEAPONLISTGET(CString& pPacket) -{ - if (!isNC()) + // Check for available slots on the server. + if (m_server->getPlayerList().size() >= m_server->cached.maxPlayers.getValue()) { - npclog.out("[Hack] %s attempted to view the weapon list.\n", m_accountName.text()); + log::printLine(log::rc, "** [Disconnect] '{}': Server is full.", account.name); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "This server has reached its player limit."); return false; } - // Start our packet. - CString ret; - ret >> (char)PLO_NC_WEAPONLISTGET; - - // Iterate weapon list and send names - for (const auto& [weaponName, weapon]: m_server->getWeaponList()) + // Verify login details with the serverlist. + // TODO: localhost mode. + if (!m_server->getServerList().getConnected()) { - if (weapon->isDefault()) - continue; - - ret >> (char)weaponName.length() << weaponName; + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "The login server is offline. Try again later."); + return false; } - sendPacket(ret); + m_server->getServerList().sendLoginPacketForPlayer(shared_from_this(), password, identity); return true; } -bool Player::msgPLI_NC_WEAPONGET(CString& pPacket) +bool PlayerNC::sendLogin() { - if (!isNC()) - { - npclog.out("[Hack] %s attempted to view a weapon.\n", m_accountName.text()); + if (Player::sendLogin() == false) return false; - } - - // {116}{weapon} - CString weaponName = pPacket.readString(""); - auto weapon = m_server->getWeapon(weaponName.toString()); - if (weapon != nullptr && !weapon->isDefault()) + if (auto npcServer = m_server->getNPCServer(); npcServer != nullptr) { - std::string script = weapon->getFullScript(); - std::replace(script.begin(), script.end(), '\n', '\xa7'); - - if (getVersion() < NCVER_2_1) - { - sendPacket(CString() >> (char)PLO_NPCWEAPONADD >> (char)weaponName.length() << weaponName >> (char)0 >> (char)weapon->getImage().length() << weapon->getImage() >> (char)1 >> (short)script.length() << script); - } - else + // Send database npcs + auto& npcList = npcServer->getGlobalNPCList(); + for (auto& [npcName, npcPtr] : npcList) { - sendPacket(CString() >> (char)PLO_NC_WEAPONGET >> - (char)weaponName.length() << weaponName >> - (char)weapon->getImage().length() << weapon->getImage() << script); + auto npc = npcPtr.lock(); + if (npc == nullptr) continue; + + CString npcPacket = CString() >> (char)PLO_NC_NPCADD >> (int)npc->id + >> (char)NPCProp::NAME << npc->getProp().serialize() + >> (char)NPCProp::TYPE << npc->getProp().serialize() + >> (char)NPCProp::CURLEVEL << npc->getProp().serialize(); + sendPacket(npcPacket); } - } - else - m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_RC_CHAT << m_accountName << " prob: weapon " << weaponName << " doesn't exist"); - - return true; -} -bool Player::msgPLI_NC_WEAPONADD(CString& pPacket) -{ - if (!isNC()) - { - npclog.out("[Hack] %s attempted to add a weapon.\n", m_accountName.text()); - return false; + // Send classes + CString classPacket; + auto& classList = npcServer->getClassList(); + for (auto it = classList.begin(); it != classList.end(); ++it) + classPacket >> (char)PLO_NC_CLASSADD << it->first << "\n"; + sendPacket(classPacket); } - // {117}{CHAR weapon length}{weapon}{CHAR image length}{image}{code} - std::string weaponName = pPacket.readChars(pPacket.readGUChar()).toString(); - std::string weaponImage = pPacket.readChars(pPacket.readGUChar()).toString(); - std::string weaponCode = pPacket.readString("").toString(); - - std::replace(weaponCode.begin(), weaponCode.end(), '\xa7', '\n'); - - CString actionTaken; - - // Find Weapon - auto weaponObj = m_server->getWeapon(weaponName); - if (weaponObj != nullptr) + // Send list of currently connected NC's + auto& playerList = m_server->getPlayerList(); + for (auto& [playerId, player] : playerList) { - // default weapon, don't update! - if (weaponObj->isDefault()) - return true; - - // Update Weapon - weaponObj->updateWeapon(std::move(weaponImage), std::move(weaponCode)); - - // Update Player-Weapons - m_server->updateWeaponForPlayers(weaponObj); - - actionTaken = "updated"; - } - else - { - // add weapon - auto weapon = std::make_shared(weaponName, std::move(weaponImage), std::move(weaponCode), 0, true); - bool success = m_server->NC_AddWeapon(weapon); - if (success) - actionTaken = "added"; + if (player.get() != this && player->isNC()) + sendPacket(CString() >> (char)PLO_RC_CHAT << "New NC: " << player->account.name); } - // TODO(joey): Log message should come before the script is executed - if (!actionTaken.isEmpty()) - { - CString logMsg; - logMsg << "Weapon/GUI-script " << weaponName << " " << actionTaken << " by " << m_accountName << "\n"; - npclog.out(logMsg); - m_server->sendToNC(logMsg); - } + // Announce to other nc's that we logged in + m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_RC_CHAT << "New NC: " << account.name, this); return true; } -bool Player::msgPLI_NC_WEAPONDELETE(CString& pPacket) -{ - if (!isNC()) - { - npclog.out("[Hack] %s attempted to delete a weapon.\n", m_accountName.text()); - return false; - } - - // {118}{weapon} - CString weaponName = pPacket.readString(""); - - CString logMsg; - if (m_server->NC_DelWeapon(weaponName.toString())) - logMsg << "Weapon " << weaponName << " deleted by " << m_accountName << "\n"; - else - logMsg << m_accountName << " prob: weapon " << weaponName << " doesn't exist\n"; - - // Logging - npclog.out(logMsg); - m_server->sendToNC(logMsg); - return true; -} - -bool Player::msgPLI_NC_LEVELLISTGET(CString& pPacket) -{ - if (!isNC()) - { - npclog.out("[Hack] %s attempted to view the level list.\n", m_accountName.text()); - return false; - } - - // Start our packet. - CString ret; - - auto& levelList = m_server->getLevelList(); - if (!levelList.empty()) - { - for (const auto& level: levelList) - ret << level->getActualLevelName() << "\n"; - } - - sendPacket(CString() >> (char)PLO_NC_LEVELLIST << ret.gtokenize()); - return true; -} - -// Send's NC Address/Port to Player (RC Only) -void Player::sendNCAddr() -{ - // RC's only! - if (!isRC() || !hasRight(PLPERM_NPCCONTROL)) - return; - - auto npcServer = m_server->getNPCServer(); - if (npcServer != nullptr) - { - // Grab NPCServer & Send - CString npcServerIp = m_server->getAdminSettings().getStr("ns_ip", "auto").toLower(); - if (npcServerIp == "auto") - { - npcServerIp = m_server->getServerList().getServerIP(); - - // Fix for localhost setups - if (m_accountIpStr == m_playerSock->getLocalIp()) - npcServerIp = m_accountIpStr; - } - - sendPacket(CString() >> (char)PLO_NPCSERVERADDR >> (short)npcServer->getId() << npcServerIp << "," << CString(m_server->getNCPort())); - } -} - -#endif +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/player/PlayerProps.cpp b/server/src/player/PlayerProps.cpp index dfe5c1fcf..ad06ba2cb 100644 --- a/server/src/player/PlayerProps.cpp +++ b/server/src/player/PlayerProps.cpp @@ -1,1230 +1,1353 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include +#include #include -#include - -#include "Player.h" -#include "Server.h" -#include "level/Level.h" -#include "level/Map.h" - -#define serverlog m_server->getServerLog() -#define rclog m_server->getRCLog() -extern bool __sendLocal[propscount]; -extern int __attrPackets[30]; - -/* - Player: Prop-Manipulation -*/ -void Player::getProp(CString& buffer, int pPropId) const -{ - auto level = m_currentLevel.lock(); - auto map = m_pmap.lock(); - - switch (pPropId) - { - case PLPROP_NICKNAME: - buffer >> (char)m_character.nickName.length() << m_character.nickName; - return; - - case PLPROP_MAXPOWER: - buffer >> (char)m_maxHitpoints; - return; - case PLPROP_CURPOWER: - buffer >> (char)(m_character.hitpoints * 2); - return; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace preagonal::props; + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// - case PLPROP_RUPEESCOUNT: - buffer >> (int)m_character.gralats; - return; +#ifdef PACKETLOGGING +#define DO_PACKETLOG(LOG) LOG +#else +#define DO_PACKETLOG(LOG) +#endif - case PLPROP_ARROWSCOUNT: - buffer >> (char)m_character.arrows; - return; +#define PRINT_PLAYERPROP(prop, ...) #prop ##sv, +constexpr std::array playerPropNames = +{ + FOR_LIST_OF_PLAYER_PROPS(PRINT_PLAYERPROP) +}; - case PLPROP_BOMBSCOUNT: - buffer >> (char)m_character.bombs; - return; +#ifdef PACKETLOGGING +static void printHeader(const Player* player, std::string_view header) +{ + log::printBlock(log::networkdump, "{}:\n", header); + log::printBlock(log::networkdump, " PlayerProp::ID: value: {}\n", player->getId()); +} - case PLPROP_GLOVEPOWER: - buffer >> (char)m_character.glovePower; - return; +static void printProp(const Player* player, PlayerProp playerProp, PropertyBase* base) +{ + auto prop = player->getProp(playerProp); + CString data = prop->serialize(); + + log::printBlock(log::networkdump, " {}: {}", playerPropNames[PROPID(playerProp)], prop); + log::printBlock(log::networkdump, " |"); + for (size_t i = 0; i < data.length(); ++i) + log::printBlock(log::networkdump, " {:02x}", (unsigned char)data[i]); + log::printBlock(log::networkdump, "\n"); +} +#endif - case PLPROP_BOMBPOWER: - buffer >> (char)m_character.bombPower; - return; +/////////////////////////////////////////////////////////////////////////////// - case PLPROP_SWORDPOWER: - buffer >> (char)(m_character.swordPower + 30) >> (char)m_character.swordImage.length() << m_character.swordImage; - return; +static bool canSendProp(PlayerProp prop) +{ + static Server* server = nullptr; + if (server == nullptr) + server = BabyDI::Get(); - case PLPROP_SHIELDPOWER: - buffer >> (char)(m_character.shieldPower + 10) >> (char)m_character.shieldImage.length() << m_character.shieldImage; - return; + if (server->Generation == ServerGeneration::ORIGINAL && PROPID(prop) > PROPID(PlayerProp::RATING)) + return false; - case PLPROP_GANI: - { - if (isClient() && m_versionId < CLVER_2_1) - { - if (!m_character.bowImage.isEmpty()) - buffer >> (char)(10 + m_character.bowImage.length()) << m_character.bowImage; - else - buffer >> (char)m_character.bowPower; - return; - } + return true; +} - buffer >> (char)m_character.gani.length() << m_character.gani; - return; - } +/////////////////////////////////////////////////////////////////////////////// - case PLPROP_HEADGIF: - buffer >> (char)(m_character.headImage.length() + 100) << m_character.headImage; - return; +std::shared_ptr Player::constructPropFor(PlayerProp prop) const +{ + switch (prop) + { +#define GENERATE_CONSTRUCTPROPFOR_CASE(prop, type, ...) case prop: return std::make_shared(); + FOR_LIST_OF_PLAYER_PROPS(GENERATE_CONSTRUCTPROPFOR_CASE); + } + throw std::invalid_argument("Invalid PlayerProp type in constructPropFor"); +} - case PLPROP_CURCHAT: - buffer >> (char)m_character.chatMessage.length() << m_character.chatMessage; - return; +/////////////////////////////////////////////////////////////////////////////// - case PLPROP_COLORS: - buffer >> (char)m_character.colors[0] >> (char)m_character.colors[1] >> (char)m_character.colors[2] >> (char)m_character.colors[3] >> (char)m_character.colors[4]; - return; +std::shared_ptr Player::getProp(PlayerProp prop) const +{ + switch (prop) + { +#define GENERATE_GETPROP_CASE(prop, type, ...) case prop: return std::make_shared( __VA_ARGS__ ); + FOR_LIST_OF_PLAYER_PROPS(GENERATE_GETPROP_CASE); + } - case PLPROP_ID: - buffer >> (short)m_id; - return; + throw std::invalid_argument("Invalid PlayerProp type in getProp"); +} - case PLPROP_X: - buffer >> (char)(m_x / 8); - return; +/////////////////////////////////////////////////////////////////////////////// - case PLPROP_Y: - buffer >> (char)(m_y / 8); - return; +SetResults Player::setProp(PlayerProp prop, SetBy setBy, std::shared_ptr base) +{ + PropertyBase* basePtr = base.get(); + if (basePtr != nullptr) + return setProp(prop, setBy, basePtr); + throw std::invalid_argument("setProp called with nullptr base pointer."); +} - case PLPROP_Z: - // range: -25 to 85 - buffer >> (char)(std::min(85 * 2, std::max(-25 * 2, (m_z / 8))) + 50); - return; +SetResults Player::setProp(PlayerProp prop, SetBy setBy, PropertyBase* base) +{ + auto player = std::dynamic_pointer_cast(shared_from_this()); + auto level = player ? player->getLevel() : nullptr; + bool restrictedPropAllowed = !m_server->hasNPCServer() || setBy == props::SetBy::SERVER; - case PLPROP_SPRITE: - buffer >> (char)m_character.sprite; - return; + props::SetResults result{ .propId = { PROPID(prop) } }; + result.resultFlags.set(props::SetResults::sendToLevel, clientPropsSharedLocal[PROPID(prop)]); + result.resultFlags.set(props::SetResults::sendToSource, setBy == props::SetBy::SERVER); - case PLPROP_STATUS: - buffer >> (char)m_status; - return; + const auto& curTime = m_server->getFrameStartTime(); + auto oldTime = modTime[PROPID(prop)]; + modTime[PROPID(prop)] = curTime; - case PLPROP_CARRYSPRITE: - buffer >> (char)m_carrySprite; - return; +#define SETPROP_RETURN_ERROR do { result.resultFlags.set(SetResults::wasInvalid); modTime[PROPID(prop)] = oldTime; return result; } while(false) - case PLPROP_CURLEVEL: + switch (prop) + { + case PlayerProp::NICKNAME: { - if (isClient()) // || type == PLTYPE_AWAIT) + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + // Word filter. + CString nick{ strProp->value }; + int filter = m_server->getWordFilter().apply(this, nick, FILTER_CHECK_NICK); + if (filter & FILTER_ACTION_WARN) { - if (map && map->getType() == MapType::GMAP) - buffer >> (char)map->getMapName().length() << map->getMapName(); - else - { - if (level != nullptr && level->isSingleplayer()) - buffer >> (char)(m_levelName.length() + 13) << m_levelName << ".singleplayer"; - else - buffer >> (char)m_levelName.length() << m_levelName; - } - return; + if (account.character.nickName.empty()) + setNick("unknown"); } else - buffer >> (char)1 << " "; - return; - } - - case PLPROP_HORSEGIF: - buffer >> (char)m_character.horseImage.length() << m_character.horseImage; - return; - - case PLPROP_HORSEBUSHES: - buffer >> (char)m_horseBombCount; - return; - - case PLPROP_EFFECTCOLORS: - buffer >> (char)0; - return; - - case PLPROP_CARRYNPC: - buffer >> (int)m_carryNpcId; - return; - - case PLPROP_APCOUNTER: - buffer >> (short)(m_apCounter + 1); - return; + { + setNick(nick, setBy == props::SetBy::SERVER); + } - case PLPROP_MAGICPOINTS: - buffer >> (char)m_mp; - return; + // If the nickname was changed due to restrictions, send the new nick back to the source. + if (account.character.nickName != nick) + { + result.resultFlags.set(props::SetResults::sendToSource); + result.resultFlags.set(props::SetResults::getLatestOnSend); + } - case PLPROP_KILLSCOUNT: - buffer >> (int)m_kills; - return; + result.resultFlags.set(props::SetResults::sendToAll); + break; + } - case PLPROP_DEATHSCOUNT: - buffer >> (int)m_deaths; - return; + case PlayerProp::MAXPOWER: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; - case PLPROP_ONLINESECS: - buffer >> (int)m_onlineTime; - return; + uint8_t newMaxHitpoints = numProp->value; - case PLPROP_IPADDR: - buffer.writeGInt5(m_accountIp); - return; + if (restrictedPropAllowed) + { + account.maxHitpoints = props::Limits::applyMaxHitpoints(newMaxHitpoints); + account.character.hitpointsInHalves = newMaxHitpoints * 2; - case PLPROP_UDPPORT: - buffer >> (int)m_udpport; - return; + result.resultPropIds.push_back(PROPID(PlayerProp::CURPOWER)); + result.resultFlags.set(props::SetResults::sendToSource); + } + break; + } - case PLPROP_ALIGNMENT: - buffer >> (char)m_character.ap; - return; + case PlayerProp::CURPOWER: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; - case PLPROP_ADDITFLAGS: - buffer >> (char)m_additionalFlags; - return; + uint8_t power = numProp->value; + int8_t powerDelta = power - account.character.hitpointsInHalves; - case PLPROP_ACCOUNTNAME: - buffer >> (char)m_accountName.length() << m_accountName; - return; + if (account.character.ap < 40 && powerDelta > 0) + break; - case PLPROP_BODYIMG: - buffer >> (char)m_character.bodyImage.length() << m_character.bodyImage; - return; + account.character.hurtDeltaInHalves = -powerDelta; + account.character.hitpointsInHalves = props::Limits::apply(power, 0, account.maxHitpoints * 2); - case PLPROP_RATING: - { - int temp = (((int)m_eloRating & 0xFFF) << 9) | ((int)m_eloDeviation & 0x1FF); - buffer >> (int)temp; - return; + if (m_server->hasNPCServer() && powerDelta < 0) + { + m_server->queueNPCEvent(level, getGlobalPosition(), ScriptEventType::PLAYERHURT, source::FromPlayer(m_id)); + } + break; } - case PLPROP_ATTACHNPC: + case PlayerProp::RUPEESCOUNT: { - // Only attach type 0 (NPC) supported. - buffer >> (char)0 >> (int)m_attachNPC; - return; - } + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; - // Simplifies login. - // Manually send prop if you are leaving the level. - // 1 = join level, 0 = leave level. - case PLPROP_JOINLEAVELVL: - buffer >> (char)1; - return; - - case PLPROP_PCONNECTED: - //return CString(); - return; - - case PLPROP_PLANGUAGE: - buffer >> (char)m_language.length() << m_language; - return; + uint32_t newGralatCount = std::min(numProp->value, 9999999u); + if (restrictedPropAllowed) + account.character.gralats = newGralatCount; + break; + } - case PLPROP_PSTATUSMSG: + case PlayerProp::ARROWSCOUNT: { - //if (id == -1) - // break; + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; - if (m_statusMsg > m_server->getStatusList().size() - 1) - buffer >> (char)0; - else - buffer >> (char)m_statusMsg; - return; + account.character.arrows = props::Limits::apply(numProp->value, props::Limits::MaxArrows); + break; } - // OS type. - // Windows: wind - case PLPROP_OSTYPE: - buffer >> (char)m_os.length() << m_os; - return; - - // Text codepage. - // Example: 1252 - case PLPROP_TEXTCODEPAGE: - buffer.writeGInt(m_envCodePage); - return; - - case PLPROP_X2: + case PlayerProp::BOMBSCOUNT: { - uint16_t val = (uint16_t)std::abs(m_x) << 1; - if (m_x < 0) - val |= 0x0001; - buffer.writeGShort(val); - return; - } + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; - case PLPROP_Y2: - { - uint16_t val = (uint16_t)std::abs(m_y) << 1; - if (m_y < 0) - val |= 0x0001; - buffer.writeGShort(val); - return; + account.character.bombs = props::Limits::apply(numProp->value, props::Limits::MaxBombs); + break; } - case PLPROP_Z2: + case PlayerProp::GLOVEPOWER: { - // range: -25 to 85 - uint16_t val = std::min(85 * 16, std::max(-25 * 16, m_z)); - val = std::abs(val) << 1; - if (m_z < 0) - val |= 0x0001; - buffer.writeGShort(val); - return; - } - - case PLPROP_GMAPLEVELX: - buffer >> (char)(level ? level->getGmapX() : 0); - return; - - case PLPROP_GMAPLEVELY: - buffer >> (char)(level ? level->getGmapY() : 0); - return; - - // TODO(joey): figure this out. Something to do with guilds? irc-related - // (char)(some bitflag for something, uses the first 3 bits im not sure) - // okay i tested some flags, 1 removes the channel. 3 adds it. not sure what third bit does. - case PLPROP_UNKNOWN81: - //return CString(); - return; - - case PLPROP_COMMUNITYNAME: - buffer >> (char)m_communityName.length() << m_communityName; - return; + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; - default: + uint8_t newGlovePower = numProp->value; + if (restrictedPropAllowed) + account.character.glovePower = props::Limits::apply(newGlovePower, props::Limits::MaxGlovePower); break; - } - - if (inrange(pPropId, 37, 41) || inrange(pPropId, 46, 49) || inrange(pPropId, 54, 74)) - { - for (auto i = 0; i < sizeof(__attrPackets) / sizeof(int); i++) - { - if (__attrPackets[i] == pPropId) - { - char len = std::min(m_character.ganiAttributes[i].length(), 223); - buffer >> (char)len << m_character.ganiAttributes[i].subString(0, len); - return; - } } - } -} - -void Player::setProps(CString& pPacket, uint8_t options, Player* rc) -{ - auto level = getLevel(); - - CString globalBuff, levelBuff, levelBuff2, selfBuff; - bool doTouchTest = false; - bool sentInvalid = false; - int len = 0; - - while (pPacket.bytesLeft() > 0) - { - unsigned char propId = pPacket.readGUChar(); - switch (propId) + case PlayerProp::BOMBPOWER: { - case PLPROP_NICKNAME: - { - CString nick = pPacket.readChars(pPacket.readGUChar()); - - // Word filter. - int filter = m_server->getWordFilter().apply(this, nick, FILTER_CHECK_NICK); - if (filter & FILTER_ACTION_WARN) - { - if (m_character.nickName.empty()) - setNick("unknown"); - } - else - setNick(nick, !(options & PLSETPROPS_SETBYPLAYER)); - - if (options & PLSETPROPS_FORWARD) - globalBuff >> (char)propId << getProp(propId); - - // Send this if the player is located on another server - // globalBuff >> (char)81; + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; - if (!(options & PLSETPROPS_FORWARDSELF)) - selfBuff >> (char)propId << getProp(propId); + account.character.bombPower = props::Limits::apply(numProp->value, props::Limits::MaxBombPower); + break; } - break; - case PLPROP_MAXPOWER: + case PlayerProp::SWORDPOWER: { - uint8_t newMaxPower = pPacket.readGUChar(); - -#ifdef V8NPCSERVER - if (!(options & PLSETPROPS_SETBYPLAYER)) - { -#endif - setMaxPower(newMaxPower); - setPower((float)m_maxHitpoints); + PropertySwordPower* swordProp = dynamic_cast(base); + if (swordProp == nullptr) + SETPROP_RETURN_ERROR; -#ifdef V8NPCSERVER - levelBuff >> (char)PLPROP_MAXPOWER << getProp(PLPROP_MAXPOWER); - selfBuff >> (char)PLPROP_MAXPOWER << getProp(PLPROP_MAXPOWER); -#endif - levelBuff >> (char)PLPROP_CURPOWER << getProp(PLPROP_CURPOWER); - selfBuff >> (char)PLPROP_CURPOWER << getProp(PLPROP_CURPOWER); -#ifdef V8NPCSERVER - } -#endif + if (restrictedPropAllowed && swordProp->power.has_value()) + account.character.swordPower = props::Limits::applySwordPower(swordProp->power.value_or(1)); + account.character.swordImage = props::Limits::apply(swordProp->image, props::Limits::SwordImageLength); break; } - case PLPROP_CURPOWER: + case PlayerProp::SHIELDPOWER: { - float p = (float)pPacket.readGUChar() / 2.0f; - if (m_character.ap < 40 && p > m_character.hitpoints) break; - //if ((status & PLSTATUS_HIDESWORD) != 0) - // break; - setPower(p); + PropertyShieldPower* shieldProp = dynamic_cast(base); + if (shieldProp == nullptr) + SETPROP_RETURN_ERROR; + + if (restrictedPropAllowed && shieldProp->power.has_value()) + account.character.shieldPower = props::Limits::applyShieldPower(shieldProp->power.value_or(1)); + + account.character.shieldImage = props::Limits::apply(shieldProp->image, props::Limits::ShieldImageLength); break; } - case PLPROP_RUPEESCOUNT: + case PlayerProp::GANI: { - unsigned int newGralatCount = std::min(pPacket.readGUInt(), 9999999u); + PropertyGaniOrBowGif* ganiProp = dynamic_cast(base); + if (ganiProp == nullptr) + SETPROP_RETURN_ERROR; -#ifdef V8NPCSERVER - if (!(options & PLSETPROPS_SETBYPLAYER)) + // 1.x servers didn't have ganis. This prop was used for the bow instead. + if (m_server->Generation == ServerGeneration::ORIGINAL) { -#endif - if (rc != nullptr) - { - if (m_server->getSettings().getBool("normaladminscanchangegralats", true) || (rc->isStaff() && rc->hasRight(PLPERM_SETRIGHTS))) - m_character.gralats = newGralatCount; - } - else - { - m_character.gralats = newGralatCount; - } -#ifdef V8NPCSERVER + if (!ganiProp->bowGif.has_value()) + break; + + auto& [image, power] = ganiProp->bowGif.value(); + account.character.bowPower = props::Limits::apply(power, props::Limits::MaxBowPower); + account.character.bowImage = image; + if (!account.character.bowImage.empty() && !account.character.bowImage.contains('.')) + account.character.bowImage += ".gif"; + break; } -#endif - break; - } - case PLPROP_ARROWSCOUNT: - m_character.arrows = pPacket.readGUChar(); - m_character.arrows = clip(m_character.arrows, 0, 99); - break; - - case PLPROP_BOMBSCOUNT: - m_character.bombs = pPacket.readGUChar(); - m_character.bombs = clip(m_character.bombs, 0, 99); - break; + std::string gani = ganiProp->gani.value_or("idle"); + account.character.gani = props::Limits::apply(gani, props::Limits::GaniLength); - case PLPROP_GLOVEPOWER: - { - uint8_t newGlovePower = pPacket.readGUChar(); -#ifdef V8NPCSERVER - if (!(options & PLSETPROPS_SETBYPLAYER)) + // Allow spin to hurt things. + if (account.character.gani == "spin") { -#endif - m_character.glovePower = std::min(newGlovePower, 3); -#ifdef V8NPCSERVER + float tX = static_cast(account.character.localPixelX / 16.0f) + 1.5f; + float tY = static_cast(account.character.localPixelY / 16.0f) + 2.0f; + m_server->hitObjectsAtPoint({ tX, tY + 2.0f }, account.character.swordPower, level, shared_from_this()); + m_server->hitObjectsAtPoint({ tX, tY - 2.0f }, account.character.swordPower, level, shared_from_this()); + m_server->hitObjectsAtPoint({ tX + 2.0f, tY }, account.character.swordPower, level, shared_from_this()); + m_server->hitObjectsAtPoint({ tX - 2.0f, tY }, account.character.swordPower, level, shared_from_this()); } -#endif break; } - case PLPROP_BOMBPOWER: - m_character.bombPower = pPacket.readGUChar(); - m_character.bombPower = clip(m_character.bombPower, 0, 3); - break; - - case PLPROP_SWORDPOWER: + case PlayerProp::HEADGIF: { - int sp = pPacket.readGUChar(); - CString img; + PropertyHeadGif* headProp = dynamic_cast(base); + if (headProp == nullptr) + SETPROP_RETURN_ERROR; - if (sp <= 4) - { - auto& settings = m_server->getSettings(); - sp = clip(sp, 0, settings.getInt("swordlimit", 3)); - img = CString() << "sword" << CString(sp) << (m_versionId < CLVER_2_1 ? ".gif" : ".png"); - } + std::string img; + if (std::holds_alternative(headProp->image)) + img = std::format("head{}.{}", std::get(headProp->image), (m_server->Generation != ServerGeneration::ORIGINAL ? "png" : "gif")); else - { - sp -= 30; - len = pPacket.readGUChar(); - if (len > 0) - { - img = pPacket.readChars(len); - if (!img.isEmpty() && m_versionId < CLVER_2_1 && getExtension(img).isEmpty()) - img << ".gif"; - } - else - img = ""; - } + img = std::get(headProp->image); -#ifdef V8NPCSERVER - if (!(options & PLSETPROPS_SETBYPLAYER)) - { -#endif - setSwordPower(sp); -#ifdef V8NPCSERVER - } -#endif - setSwordImage(img); + if (m_server->Generation == ServerGeneration::ORIGINAL && !img.empty() && !img.contains('.')) + img += ".gif"; + + account.character.headImage = props::Limits::apply(img, props::Limits::HeadImageLength); + result.resultFlags.set(props::SetResults::sendToAll); + break; } - break; - case PLPROP_SHIELDPOWER: + case PlayerProp::CURCHAT: { - int sp = pPacket.readGUChar(); - CString img; + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; - if (sp <= 3) - { - auto& settings = m_server->getSettings(); - sp = clip(sp, 0, settings.getInt("shieldlimit", 3)); - img = CString() << "shield" << CString(sp) << (m_versionId < CLVER_2_1 ? ".gif" : ".png"); - } - else + bool chatChanged = (account.character.chatMessage != strProp->value); + if (!chatChanged) + break; + + account.character.chatMessage = props::Limits::apply(strProp->value, props::Limits::ChatMessageLength); + + if (player != nullptr) { - // This fixes an odd bug with the 1.41 client. - if (pPacket.bytesLeft() == 0) continue; + player->setLastChatTime(m_server->getFrameStartTime()); - sp -= 10; - if (sp < 0) break; - len = pPacket.readGUChar(); - if (len > 0) + // Try to process the chat. If it wasn't processed, apply the word filter to it. + if (!player->processChat(account.character.chatMessage)) { - img = pPacket.readChars(len); - if (!img.isEmpty() && m_versionId < CLVER_2_1 && getExtension(img).isEmpty()) - img << ".gif"; - } - else - img = ""; - } + m_server->queueNPCEvent(level, getGlobalPosition(), ScriptEventType::PLAYERCHATS, source::FromPlayer(m_id)); -#ifdef V8NPCSERVER - if (!(options & PLSETPROPS_SETBYPLAYER)) - { -#endif - setShieldPower(sp); -#ifdef V8NPCSERVER + CString chat = account.character.chatMessage; + int found = m_server->getWordFilter().apply(this, chat, FILTER_CHECK_CHAT); + account.character.chatMessage = chat.toString(); + + if ((found & FILTER_ACTION_REPLACE) || (found & FILTER_ACTION_WARN)) + result.resultFlags.set(props::SetResults::sendToSource); + } } -#endif - setShieldImage(img); + break; } - break; - case PLPROP_GANI: + case PlayerProp::COLORS: { - if (isClient() && m_versionId < CLVER_2_1) - { - int sp = pPacket.readGUChar(); - if (sp < 10) - { - m_character.bowPower = sp; - m_character.bowImage.clear(); - } - else - { - m_character.bowPower = 10; - sp -= 10; - if (sp < 0) break; - m_character.bowImage = CString() << pPacket.readChars(sp); - if (!m_character.bowImage.isEmpty() && m_versionId < CLVER_2_1 && getExtension(m_character.bowImage).isEmpty()) - m_character.bowImage << ".gif"; - } - break; - } + PropertyColors* colorProp = dynamic_cast(base); + if (colorProp == nullptr) + SETPROP_RETURN_ERROR; - setGani(pPacket.readChars(pPacket.readGUChar())); - if (m_character.gani == "spin") - { - CString nPacket; - nPacket >> (char)PLO_HITOBJECTS >> (short)m_id >> (char)m_character.swordPower; - char hx = (char)((getX() + 1.5f) * 2); - char hy = (char)((getY() + 2.0f) * 2); - m_server->sendPacketToOneLevel(CString() << nPacket >> (char)(hx) >> (char)(hy - 4), m_currentLevel, { m_id }); - m_server->sendPacketToOneLevel(CString() << nPacket >> (char)(hx) >> (char)(hy + 4), m_currentLevel, { m_id }); - m_server->sendPacketToOneLevel(CString() << nPacket >> (char)(hx - 4) >> (char)(hy), m_currentLevel, { m_id }); - m_server->sendPacketToOneLevel(CString() << nPacket >> (char)(hx + 4) >> (char)(hy), m_currentLevel, { m_id }); - } + account.character.colors = colorProp->values; + break; } - break; - case PLPROP_HEADGIF: + case PlayerProp::ID: + break; + + case PlayerProp::X: { - len = pPacket.readGUChar(); - CString img; - if (len < 100) - { - img = CString() << "head" << CString(len) << (m_versionId < CLVER_2_1 ? ".gif" : ".png"); - } - else if (len > 100) - { - img = pPacket.readChars(len - 100); + PropertyTileCoordinate* coordProp = dynamic_cast(base); + if (coordProp == nullptr) + SETPROP_RETURN_ERROR; - // TODO(joey): We need to check properties for newline, especially if they are sending to other clients - // as it causes havoc on the client... - int check = img.find("\n", 0); - if (check > 0) - img = img.readChars(check); + if (account.character.localPixelX == coordProp->pixelCoordinate) + break; - if (!img.isEmpty() && m_versionId < CLVER_2_1 && getExtension(img).isEmpty()) - img << ".gif"; - } + auto movementDirection = static_cast(2 + std::clamp(coordProp->pixelCoordinate - account.character.localPixelX, -1, 1)); + account.character.localPixelX = coordProp->pixelCoordinate; + account.status &= (~PLSTATUS_PAUSED); + result.resultPropIds.push_back(PROPID(PlayerProp::X2)); - if (len != 100) + if (player != nullptr) { - setHeadImage(img); - globalBuff >> (char)propId << getProp(propId); + player->setLastMovementTime(m_server->getFrameStartTime()); + player->testForTouch(result, movementDirection); } - break; } - case PLPROP_CURCHAT: + case PlayerProp::Y: { - len = pPacket.readGUChar(); - m_character.chatMessage = pPacket.readChars(std::min(len, 223)); - m_lastChat = time(0); + PropertyTileCoordinate* coordProp = dynamic_cast(base); + if (coordProp == nullptr) + SETPROP_RETURN_ERROR; - // Try to process the chat. If it wasn't processed, apply the word filter to it. - if (!processChat(m_character.chatMessage)) - { - int found = m_server->getWordFilter().apply(this, m_character.chatMessage, FILTER_CHECK_CHAT); - if (!(options & PLSETPROPS_FORWARDSELF)) - { - if ((found & FILTER_ACTION_REPLACE) || (found & FILTER_ACTION_WARN)) - selfBuff >> (char)propId << getProp(propId); - } - } + if (account.character.localPixelY == coordProp->pixelCoordinate) + break; + + auto movementDirection = static_cast(1 + std::clamp(coordProp->pixelCoordinate - account.character.localPixelY, -1, 1)); + account.character.localPixelY = coordProp->pixelCoordinate; + account.status &= (~PLSTATUS_PAUSED); + result.resultPropIds.push_back(PROPID(PlayerProp::Y2)); -#ifdef V8NPCSERVER - // Send chat to npcs if this wasn't changed by the npcserver - if (!rc && !m_character.chatMessage.isEmpty()) + if (player != nullptr) { - if (level != nullptr) - level->sendChatToLevel(this, m_character.chatMessage.text()); + player->setLastMovementTime(m_server->getFrameStartTime()); + player->testForTouch(result, movementDirection); } -#endif - } - break; - - case PLPROP_COLORS: - for (unsigned char& color : m_character.colors) - color = pPacket.readGUChar(); break; + } - case PLPROP_ID: - pPacket.readGUShort(); - break; + case PlayerProp::Z: + { + PropertyTileCoordinateZ* zProp = dynamic_cast(base); + if (zProp == nullptr) + SETPROP_RETURN_ERROR; - case PLPROP_X: - m_x = (pPacket.readGUChar() * 8); - m_status &= (~PLSTATUS_PAUSED); - m_lastMovement = time(0); - m_grMovementUpdated = true; + // If Z is disabled, don't allow changing it. + if (m_server->cached.lockPlayerZ.getValue()) + { + result.resultFlags.reset(); + result.resultFlags.set(SetResults::sendToSource); + result.resultFlags.set(SetResults::getLatestOnSend); + break; + } - // Do collision testing. - doTouchTest = true; + account.character.localPixelZ = zProp->pixelCoordinate; + account.status &= (~PLSTATUS_PAUSED); + result.resultPropIds.push_back(PROPID(PlayerProp::Z2)); - // Let 2.30+ clients see pre-2.30 movement. - levelBuff2 >> (char)PLPROP_X2 << getProp(PLPROP_X2); + if (player != nullptr) + player->setLastMovementTime(m_server->getFrameStartTime()); break; + } - case PLPROP_Y: - m_y = (pPacket.readGUChar() * 8); - m_status &= (~PLSTATUS_PAUSED); - m_lastMovement = time(0); - m_grMovementUpdated = true; - - // Do collision testing. - doTouchTest = true; - - // Let 2.30+ clients see pre-2.30 movement. - levelBuff2 >> (char)PLPROP_Y2 << getProp(PLPROP_Y2); - break; + case PlayerProp::SPRITE: + { + PropertySprite* spriteProp = dynamic_cast(base); + if (spriteProp == nullptr) + SETPROP_RETURN_ERROR; - case PLPROP_Z: - m_z = (pPacket.readGUChar() - 50) * 8; - m_status &= (~PLSTATUS_PAUSED); - m_lastMovement = time(0); - m_grMovementUpdated = true; - doTouchTest = true; + if (account.character.sprite == spriteProp->sprite && account.character.direction == spriteProp->direction) + break; - // Let 2.30+ clients see pre-2.30 movement. - levelBuff2 >> (char)PLPROP_Z2 << getProp(PLPROP_Z2); - break; + bool directionChanged = (account.character.direction != spriteProp->direction); + account.character.direction = spriteProp->direction; + account.character.sprite = spriteProp->sprite; + result.resultFlags.set(SetResults::getLatestOnSend); - case PLPROP_SPRITE: - m_character.sprite = pPacket.readGUChar(); + // If we manually set a sprite, change the gani. + if (m_server->Generation != ServerGeneration::ORIGINAL && account.character.sprite != 0 && (!account.character.gani.starts_with("def[") || modTime[PROPID(PlayerProp::GANI)] < curTime)) + { + auto gani = std::format("def[{}]", account.character.sprite); + result.resultPropIds.push_back(PROPID(PlayerProp::GANI)); + result.resultFlags.set(SetResults::sendToSource); + } -#ifndef V8NPCSERVER // Do collision testing. - doTouchTest = true; -#endif + if (player != nullptr && directionChanged) + player->testForTouch(result, account.character.direction); break; + } - case PLPROP_STATUS: + case PlayerProp::STATUS: { - int oldStatus = m_status; - m_status = pPacket.readGUChar(); + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + int oldStatus = account.status; + account.status = numProp->value; //printf("%s: status: %d, oldStatus: %d\n", m_accountName.text(), status, oldStatus ); - if (m_id == -1) break; + if (m_id == 0) break; // When they come back to life, give them hearts. - if ((oldStatus & PLSTATUS_DEAD) > 0 && (m_status & PLSTATUS_DEAD) == 0) + if ((oldStatus & PLSTATUS_DEAD) > 0 && (account.status & PLSTATUS_DEAD) == 0) { - auto newPower = clip((m_character.ap < 20 ? 3 : (m_character.ap < 40 ? 5 : m_maxHitpoints)), 0.5f, m_maxHitpoints); - setPower(newPower); + // Give them full hearts. If they have less than 20 AP, give them 3 hearts. If they have less than 40 AP, give them 5 hearts. + auto newPower = props::Limits::applyMaxHitpoints(account.character.ap < 20 ? 3 : (account.character.ap < 40 ? 5 : account.maxHitpoints)) * 2; + account.character.hitpointsInHalves = newPower; - selfBuff >> (char)PLPROP_CURPOWER >> (char)(m_character.hitpoints * 2.0f); - levelBuff >> (char)PLPROP_CURPOWER >> (char)(m_character.hitpoints * 2.0f); - - if (level != nullptr && level->isPlayerLeader(m_id)) - sendPacket(CString() >> (char)PLO_ISLEADER); - - /* - // If we are the leader of the level, call warp(). This will fix NPCs not - // working again after we respawn. - if (level != 0 && level->getPlayer(0) == this) - warp(m_levelName, x, y, time(0)); - */ + result.resultPropIds.push_back(PROPID(PlayerProp::CURPOWER)); + result.resultFlags.set(props::SetResults::sendToSource); } // When they die, increase deaths and make somebody else level leader. - if ((oldStatus & PLSTATUS_DEAD) == 0 && (m_status & PLSTATUS_DEAD) > 0) + if ((oldStatus & PLSTATUS_DEAD) == 0 && (account.status & PLSTATUS_DEAD) > 0 && level != nullptr) { - if (level->isSparringZone() == false) + if (level->isSparringZone(getMapPosition()) == false) { - m_deaths++; - dropItemsOnDeath(); + ++account.deaths; + player->dropItemsOnDeath(); } // If we are the leader and there are more players on the level, we want to remove // ourself from the leader position and tell the new leader that they are the leader. - if (level->isPlayerLeader(m_id) && level->getPlayers().size() > 1) + if (level->isPlayerLeader(m_id) && level->getPlayers().size() > 1 && !level->isGmap()) { level->removePlayer(m_id); level->addPlayer(m_id); - auto leader = m_server->getPlayer(level->getPlayers().front()); - if (leader) leader->sendPacket(CString() >> (char)PLO_ISLEADER); + if (auto leader = m_server->getPlayer(level->getPlayers().front()); leader != nullptr) + leader->sendPacket(CString() >> (char)PLO_ISLEADER); } + + // Update our last dead time. + lastDeadTime = m_server->getNWTime(); + + // Queue up the playerdies event. + m_server->queueNPCEvent(level, getGlobalPosition(), ScriptEventType::PLAYERDIES, source::FromPlayer(m_id)); } + break; } - break; - case PLPROP_CARRYSPRITE: - m_carrySprite = pPacket.readGUChar(); + case PlayerProp::CARRYSPRITE: + { + PropertyUnsafeByte* numProp = dynamic_cast(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + m_carrySprite = numProp->value; break; + } - case PLPROP_CURLEVEL: - len = pPacket.readGUChar(); -#ifdef V8NPCSERVER - pPacket.readChars(len); -#else - m_levelName = pPacket.readChars(len); -#endif + case PlayerProp::CURLEVEL: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + if (restrictedPropAllowed) + account.level = strProp->value; break; + } - case PLPROP_HORSEGIF: - len = pPacket.readGUChar(); - m_character.horseImage = pPacket.readChars(std::min(len, 219)); // limit is 219 in case it appends .gif - if (!m_character.horseImage.isEmpty() && m_versionId < CLVER_2_1 && getExtension(m_character.horseImage).isEmpty()) - m_character.horseImage << ".gif"; + case PlayerProp::HORSEGIF: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + account.character.horseImage = strProp->value; + if (m_server->Generation == ServerGeneration::ORIGINAL && !account.character.horseImage.empty() && !account.character.horseImage.contains('.')) + account.character.horseImage += ".gif"; break; + } + + case PlayerProp::HORSEBUSHES: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; - case PLPROP_HORSEBUSHES: - m_horseBombCount = pPacket.readGUChar(); + m_horseBombCount = numProp->value; break; + } + + case PlayerProp::EFFECTCOLORS: + { + PropertyEffectColors* effectColorsProp = dynamic_cast(base); + if (effectColorsProp == nullptr) + SETPROP_RETURN_ERROR; - case PLPROP_EFFECTCOLORS: - len = pPacket.readGUChar(); - if (len > 0) - pPacket.readGInt4(); + m_effectColors = effectColorsProp->values; break; + } - case PLPROP_CARRYNPC: + case PlayerProp::CARRYNPC: { - uint32_t newNpcId = pPacket.readGUInt(); + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; - // Thrown. - if (m_carryNpcId != 0 && newNpcId == 0) + NPCID newNPCID = numProp->value; + + if (player == nullptr) + break; + + // Not supported on gmaps. + if (level && level->isGmap()) { - // TODO: Thrown + // The client seems to freak out when picking up a character on the gmap. + // Normally, the client would "delete" the NPC when picking it up and respawn it when thrown, but on the gmap the + // NPC doesn't get deleted, causing a lot of weird problems. + break; } - else + + // Picked up. + if (player->getCarryNPC() == 0 && newNPCID != 0) { + // Make the NPC invisible while its being carried so other players don't see a ghost NPC sitting on the ground. + // Do not send this to the player who picked up the NPC. It will cause a duplicate NPC to generate in their client, breaking future interactions with the NPC. + // Except, DO send it to the player if the NPC is on a gmap, since, for some reason, the client does the opposite there. + if (auto npc = m_server->getNPC(newNPCID); npc != nullptr) + { + std::inplace_vector results; + results.push_back(npc->setPropWith(props::SetBy::SERVER, static_cast(npc->visFlags & ~ENUM(NPCVisFlags::VISIBLE)))); + npc->sendPropsFromResults(level && level->isGmap() ? nullptr : player, results); + } + // TODO: Remove when an npcserver is created. - if (m_server->getSettings().getBool("duplicatecanbecarried", false) == false) + if (m_server->getSettings().get("duplicatecanbecarried").value_or(false) == false) { - bool isOwner = true; + [[maybe_unused]] bool isOwner = true; { auto& playerList = m_server->getPlayerList(); for (auto& [otherId, other] : playerList) { if (other.get() == this) continue; - if (other->getProp(PLPROP_CARRYNPC).readGUInt() == newNpcId) + if (other->getProp().value == newNPCID) { // Somebody else got this NPC first. Force the player to throw his down // and tell the player to remove the NPC from memory. - sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)PLPROP_CARRYNPC >> (int)0); - sendPacket(CString() >> (char)PLO_NPCDEL2 >> (char)level->getLevelName().length() << level->getLevelName() >> (int)newNpcId); - m_server->sendPacketToOneLevel(CString() >> (char)PLO_OTHERPLPROPS >> (short)m_id >> (char)PLPROP_CARRYNPC >> (int)0, level, { m_id }); + sendPacket(CString() >> (char)PLO_PLAYERPROPS >> (char)PlayerProp::CARRYNPC >> (int)0); + sendPacket(CString() >> (char)PLO_NPCDEL2 >> (char)level->levelName.length() << level->levelName >> (int)newNPCID); + m_server->sendPacketToNearby(CString() >> (char)PLO_OTHERPLPROPS >> (short)m_id >> (char)PlayerProp::CARRYNPC >> (int)0, player->getGlobalPosition(), level, { m_id }); isOwner = false; - newNpcId = 0; + newNPCID = 0; break; } } } - if (isOwner) + } + } + // Thrown. + else if (player->getCarryNPC() != 0 && newNPCID == 0) + { + if (auto npc = m_server->getNPC(player->getCarryNPC()); npc != nullptr) + { + // Player carries NPC above their head. The bottom center of the NPC is carried at x + 1.5, y + 1. + // When thrown, the NPC travels 9 tiles in the direction the player is facing and lands on the ground after 0.5 seconds. + + // Determine the starting position of the NPC. + // Clients handle the rendering, so there is no point to simulate the Z movement, so just place it at the feet of the player. + constexpr int16_t moveDistance = 16 * 9; + auto dir = player->account.character.direction; + auto bbox = npc->getBoundingBox(); + auto pos = player->getLocalPosition(); + pos.translate(24 - static_cast(bbox.size.width()) / 2, 48 - static_cast(bbox.size.height())); + + // Send the position of the NPC to other players in the level so the it will appear in the right spot after the move completes. + // Do not send to the player who threw it. + // The client is still "throwing" the NPC and sending props will break things! + npc->sendPropsFromResults( + player, + SetResults{ .propId = PROPID(NPCProp::VISFLAGS), .resultFlags = (1 << SetResults::sendToLevel) }, + npc->setPropWith(props::SetBy::SERVER, pos.x()), + npc->setPropWith(props::SetBy::SERVER, pos.y()) + ); + + // Queue up the movement of the NPC so clients position it properly. + LocalPixelPosition moveDelta{ dir == 1 ? -moveDistance : dir == 3 ? moveDistance : 0, dir == 0 ? -moveDistance : dir == 2 ? moveDistance : 0 }; + npc->addMoveToQueue(moveDelta, 0.5, ENUM(NPCMoveFlags::NOCACHE)); + if (npc->moveQueue.size() != 0) { - // We own this NPC now so remove it from the level and have everybody else delete it. - auto npc = m_server->getNPC(newNpcId); - level->removeNPC(npc); - m_server->sendPacketToAll(CString() >> (char)PLO_NPCDEL2 >> (char)level->getLevelName().length() << level->getLevelName() >> (int)newNpcId, { m_id }); + // Set up the WASTHROWN event to trigger after 0.5 seconds. + // Also, make the NPC visible again. + auto& move = npc->moveQueue.back(); + move.onComplete = [snpc = npc, pid = player->getId(), server = m_server, dir]() + { + std::inplace_vector results; + + // Make the NPC visible for everybody again. + results.push_back(snpc->setPropWith(props::SetBy::SERVER, static_cast(snpc->visFlags | ENUM(NPCVisFlags::VISIBLE)))); + + // If the NPC is a character, set their gani to idle and fix their direction. + if (snpc->isCharacter()) + { + auto dirprop = snpc->getProp(); + dirprop.direction = dir; + results.push_back(snpc->setProp(props::SetBy::SERVER, dirprop)); + results.push_back(snpc->setPropWith(props::SetBy::SERVER, "idle")); + } + + // Send the results. + snpc->sendPropsFromResults(results); + + // Trigger the WASTHROWN event. + snpc->scripting.events.addEvent(ScriptEventType::WASTHROWN, source::FromPlayer(pid)); + }; } } } - m_carryNpcId = newNpcId; + player->setCarryNPC(newNPCID); + break; } - break; - case PLPROP_APCOUNTER: - m_apCounter = pPacket.readGUShort(); + case PlayerProp::APCOUNTER: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + account.apCounter = numProp->value + 1; break; + } - case PLPROP_MAGICPOINTS: + case PlayerProp::MAGICPOINTS: { - uint8_t newMP = pPacket.readGUChar(); -#ifdef V8NPCSERVER - if (!(options & PLSETPROPS_SETBYPLAYER)) - { -#endif - m_mp = std::min(newMP, 100); -#ifdef V8NPCSERVER - } -#endif + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + if (restrictedPropAllowed) + account.character.mp = props::Limits::apply(numProp->value, props::Limits::MaxMP); break; } - case PLPROP_KILLSCOUNT: - pPacket.readGInt(); + case PlayerProp::KILLSCOUNT: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + if (restrictedPropAllowed) + account.kills = numProp->value; break; + } + + case PlayerProp::DEATHSCOUNT: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; - case PLPROP_DEATHSCOUNT: - pPacket.readGInt(); + if (restrictedPropAllowed) + account.deaths = numProp->value; break; + } - case PLPROP_ONLINESECS: - pPacket.readGInt(); + case PlayerProp::ONLINESECS: break; - case PLPROP_IPADDR: - pPacket.readGInt5(); + case PlayerProp::IPADDR: break; - case PLPROP_UDPPORT: - m_udpport = pPacket.readGInt(); - if (m_id != -1 && m_loaded) - m_server->sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_OTHERPLPROPS >> (short)m_id >> (char)PLPROP_UDPPORT >> (int)m_udpport, this); - // TODO: udp support. + case PlayerProp::UDPPORT: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + m_udpport = numProp->value; break; + } - case PLPROP_ALIGNMENT: + case PlayerProp::ALIGNMENT: { - uint8_t newAlignment = pPacket.readGUChar(); -#ifdef V8NPCSERVER - if (!(options & PLSETPROPS_SETBYPLAYER)) - { -#endif - m_character.ap = std::min(newAlignment, 100); -#ifdef V8NPCSERVER - } -#endif + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + uint8_t newAlignment = numProp->value; + if (restrictedPropAllowed) + account.character.ap = std::min(newAlignment, 100); break; } - case PLPROP_ADDITFLAGS: - m_additionalFlags = pPacket.readGUChar(); + case PlayerProp::ADDITFLAGS: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + m_additionalFlags = numProp->value; break; + } - case PLPROP_ACCOUNTNAME: - len = pPacket.readGUChar(); - pPacket.readChars(len); + case PlayerProp::ACCOUNTNAME: break; - case PLPROP_BODYIMG: - len = pPacket.readGUChar(); - setBodyImage(pPacket.readChars(len)); + case PlayerProp::BODYIMG: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + account.character.bodyImage = props::Limits::apply(strProp->value, props::Limits::BodyImageLength); break; + } + + case PlayerProp::RATING: + { + PropertyEloRating* eloProp = dynamic_cast(base); + if (eloProp == nullptr) + SETPROP_RETURN_ERROR; - case PLPROP_RATING: - len = pPacket.readGInt(); - //m_eloRating = (float)((len >> 9) & 0xFFF); + if (restrictedPropAllowed) + { + account.eloRating = eloProp->rating; + account.eloDeviation = eloProp->deviation; + } break; + } - case PLPROP_ATTACHNPC: + case PlayerProp::ATTACHNPC: { + PropertyAttachNPC* attachProp = dynamic_cast(base); + if (attachProp == nullptr) + SETPROP_RETURN_ERROR; + // Only supports object_type 0 (NPC). - unsigned char object_type = pPacket.readGUChar(); - unsigned int npcID = pPacket.readGUInt(); - m_attachNPC = npcID; - levelBuff >> (char)PLPROP_ATTACHNPC << getProp(PLPROP_ATTACHNPC); + m_attachNPC = attachProp->npcId; break; } - case PLPROP_GMAPLEVELX: + case PlayerProp::GMAPLEVELX: { - int mx = pPacket.readGUChar(); + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr || level == nullptr || !level->isGmap()) + SETPROP_RETURN_ERROR; + + if (account.character.mapX == numProp->value) + break; + + if (auto subLevel = level->getSubLevelAtPosition(getMapPosition()); subLevel != nullptr) + leaveSubLevel(subLevel); - if (auto cmap = level->getMap(); level && cmap && cmap->isGmap()) + account.character.mapX = numProp->value; + + if (auto levelData = level->getStaticLevelDataAtPosition(getMapPosition()); levelData != nullptr) { - auto& newLevelName = cmap->getLevelAt(mx, level->getMapY()); - leaveLevel(); - setLevel(newLevelName, -1); + auto lastEnteredTime = player->getLevelLastEnteredTime(levelData.get()); + sendDynamicLevelData(level, lastEnteredTime); } -#ifdef DEBUG - printf("gmap level x: %d\n", level->getMapX()); -#endif break; } - case PLPROP_GMAPLEVELY: + case PlayerProp::GMAPLEVELY: { - int my = pPacket.readGUChar(); + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr || level == nullptr || !level->isGmap()) + SETPROP_RETURN_ERROR; + + if (account.character.mapY == numProp->value) + break; - if (auto cmap = level->getMap(); level && cmap && cmap->isGmap()) + if (auto subLevel = level->getSubLevelAtPosition(getMapPosition()); subLevel != nullptr) + leaveSubLevel(subLevel); + + account.character.mapY = numProp->value; + + if (auto levelData = level->getStaticLevelDataAtPosition(getMapPosition()); levelData != nullptr) { - auto& newLevelName = cmap->getLevelAt(level->getMapX(), my); - leaveLevel(); - setLevel(newLevelName, -1); + auto lastEnteredTime = player->getLevelLastEnteredTime(levelData.get()); + sendDynamicLevelData(level, lastEnteredTime); } -#ifdef DEBUG - printf("gmap level y: %d\n", level->getMapY()); -#endif break; } - /* - case PLPROP_UNKNOWN50: - break; -*/ - case PLPROP_PCONNECTED: + case PlayerProp::JOINLEAVELVL: break; - case PLPROP_PLANGUAGE: - len = pPacket.readGUChar(); - m_language = pPacket.readChars(len); + case PlayerProp::DISCONNECT: break; - case PLPROP_PSTATUSMSG: - m_statusMsg = pPacket.readGUChar(); - if (m_id == -1 || !m_loaded) - break; + case PlayerProp::LANGUAGE: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; - m_server->sendPacketToAll(CString() >> (char)PLO_OTHERPLPROPS >> (short)m_id >> (char)PLPROP_PSTATUSMSG >> (char)m_statusMsg, { m_id }); + account.language = strProp->value; break; + } - case PLPROP_GATTRIB1: - m_character.ganiAttributes[0] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB2: - m_character.ganiAttributes[1] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB3: - m_character.ganiAttributes[2] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB4: - m_character.ganiAttributes[3] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB5: - m_character.ganiAttributes[4] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB6: - m_character.ganiAttributes[5] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB7: - m_character.ganiAttributes[6] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB8: - m_character.ganiAttributes[7] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB9: - m_character.ganiAttributes[8] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB10: - m_character.ganiAttributes[9] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB11: - m_character.ganiAttributes[10] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB12: - m_character.ganiAttributes[11] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB13: - m_character.ganiAttributes[12] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB14: - m_character.ganiAttributes[13] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB15: - m_character.ganiAttributes[14] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB16: - m_character.ganiAttributes[15] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB17: - m_character.ganiAttributes[16] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB18: - m_character.ganiAttributes[17] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB19: - m_character.ganiAttributes[18] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB20: - m_character.ganiAttributes[19] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB21: - m_character.ganiAttributes[20] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB22: - m_character.ganiAttributes[21] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB23: - m_character.ganiAttributes[22] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB24: - m_character.ganiAttributes[23] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB25: - m_character.ganiAttributes[24] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB26: - m_character.ganiAttributes[25] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB27: - m_character.ganiAttributes[26] = pPacket.readChars(pPacket.readGUChar()); - break; - case PLPROP_GATTRIB28: - m_character.ganiAttributes[27] = pPacket.readChars(pPacket.readGUChar()); + case PlayerProp::PLAYERLISTSTATUS: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + m_statusMsg = numProp->value; + if (m_id == 0 || !m_loaded) + break; + + result.resultFlags.set(props::SetResults::sendToAll); break; - case PLPROP_GATTRIB29: - m_character.ganiAttributes[28] = pPacket.readChars(pPacket.readGUChar()); + } + + case PlayerProp::GATTRIB1: + case PlayerProp::GATTRIB2: + case PlayerProp::GATTRIB3: + case PlayerProp::GATTRIB4: + case PlayerProp::GATTRIB5: + case PlayerProp::GATTRIB6: + case PlayerProp::GATTRIB7: + case PlayerProp::GATTRIB8: + case PlayerProp::GATTRIB9: + case PlayerProp::GATTRIB10: + case PlayerProp::GATTRIB11: + case PlayerProp::GATTRIB12: + case PlayerProp::GATTRIB13: + case PlayerProp::GATTRIB14: + case PlayerProp::GATTRIB15: + case PlayerProp::GATTRIB16: + case PlayerProp::GATTRIB17: + case PlayerProp::GATTRIB18: + case PlayerProp::GATTRIB19: + case PlayerProp::GATTRIB20: + case PlayerProp::GATTRIB21: + case PlayerProp::GATTRIB22: + case PlayerProp::GATTRIB23: + case PlayerProp::GATTRIB24: + case PlayerProp::GATTRIB25: + case PlayerProp::GATTRIB26: + case PlayerProp::GATTRIB27: + case PlayerProp::GATTRIB28: + case PlayerProp::GATTRIB29: + case PlayerProp::GATTRIB30: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + auto index = std::ranges::distance(GaniAttributePropList.begin(), std::ranges::find(GaniAttributePropList, PROPID(prop))); + account.character.ganiAttributes[index] = strProp->value; break; - case PLPROP_GATTRIB30: - m_character.ganiAttributes[29] = pPacket.readChars(pPacket.readGUChar()); + } + + // OS type. + // Windows: wind + case PlayerProp::OSTYPE: + { + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + m_os = strProp->value; break; + } + + // Text codepage. + // Example: 1252 + case PlayerProp::TEXTCODEPAGE: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; - // OS type. - // Windows: wind - case PLPROP_OSTYPE: - m_os = pPacket.readChars(pPacket.readGUChar()); + m_envCodePage = numProp->value; break; + } - // Text codepage. - // Example: 1252 - case PLPROP_TEXTCODEPAGE: - m_envCodePage = pPacket.readGInt(); + // TODO(Nalin): Does this need to be read? + case PlayerProp::ONLINESECS2: break; - // Location, in pixels, of the player on the level in 2.30+ clients. - // Bit 0x0001 controls if it is negative or not. - // Bits 0xFFFE are the actual value. - case PLPROP_X2: - len = pPacket.readGUShort(); - m_x = (len >> 1); + // Location, in pixels, of the player on the level in 2.30+ clients. + // Bit 0x0001 controls if it is negative or not. + // Bits 0xFFFE are the actual value. + case PlayerProp::X2: + { + PropertyPixelCoordinate* pixelProp = dynamic_cast(base); + if (pixelProp == nullptr) + SETPROP_RETURN_ERROR; - // If the first bit is 1, our position is negative. - if ((uint16_t)len & 0x0001) - m_x = -m_x; + if (account.character.localPixelX == pixelProp->pixelCoordinate) + break; - // Let pre-2.30+ clients see 2.30+ movement. - levelBuff2 >> (char)PLPROP_X << getProp(PLPROP_X); + auto movementDirection = static_cast(2 + std::clamp(pixelProp->pixelCoordinate - account.character.localPixelX, -1, 1)); + account.character.localPixelX = pixelProp->pixelCoordinate; + account.status &= (~PLSTATUS_PAUSED); + result.resultPropIds.push_back(PROPID(PlayerProp::X)); - m_status &= (~PLSTATUS_PAUSED); - m_lastMovement = time(0); - m_grMovementUpdated = true; - doTouchTest = true; + if (player != nullptr) + { + player->setLastMovementTime(m_server->getFrameStartTime()); + player->testForTouch(result, movementDirection); + } break; + } - case PLPROP_Y2: - len = pPacket.readGUShort(); - m_y = (len >> 1); - - // If the first bit is 1, our position is negative. - if ((uint16_t)len & 0x0001) - m_y = -m_y; + case PlayerProp::Y2: + { + PropertyPixelCoordinate* pixelProp = dynamic_cast(base); + if (pixelProp == nullptr) + SETPROP_RETURN_ERROR; - // Let pre-2.30+ clients see 2.30+ movement. - levelBuff2 >> (char)PLPROP_Y << getProp(PLPROP_Y); + if (account.character.localPixelY == pixelProp->pixelCoordinate) + break; - m_status &= (~PLSTATUS_PAUSED); - m_lastMovement = time(0); - m_grMovementUpdated = true; + auto movementDirection = static_cast(1 + std::clamp(pixelProp->pixelCoordinate - account.character.localPixelY, -1, 1)); + account.character.localPixelY = pixelProp->pixelCoordinate; + account.status &= (~PLSTATUS_PAUSED); + result.resultPropIds.push_back(PROPID(PlayerProp::Y)); - // Do collision testing. - doTouchTest = true; + if (player != nullptr) + { + player->setLastMovementTime(m_server->getFrameStartTime()); + player->testForTouch(result, movementDirection); + } break; + } - case PLPROP_Z2: - len = pPacket.readGUShort(); - m_z = (len >> 1); + case PlayerProp::Z2: + { + PropertyPixelCoordinate* pixelProp = dynamic_cast(base); + if (pixelProp == nullptr) + SETPROP_RETURN_ERROR; - // If the first bit is 1, our position is negative. - if ((uint16_t)len & 0x0001) - m_z = -m_z; + // If Z is disabled, don't allow changing it. + if (m_server->cached.lockPlayerZ.getValue()) + { + result.resultFlags.reset(); + result.resultFlags.set(SetResults::sendToSource); + result.resultFlags.set(SetResults::getLatestOnSend); + break; + } - // Let pre-2.30+ clients see 2.30+ movement. - levelBuff2 >> (char)PLPROP_Z << getProp(PLPROP_Z); + account.character.localPixelZ = pixelProp->pixelCoordinate; + account.status &= (~PLSTATUS_PAUSED); + result.resultPropIds.push_back(PROPID(PlayerProp::Z)); - m_status &= (~PLSTATUS_PAUSED); - m_lastMovement = time(0); - m_grMovementUpdated = true; + if (player != nullptr) + player->setLastMovementTime(m_server->getFrameStartTime()); + break; + } - // Do collision testing. - doTouchTest = true; + case PlayerProp::PLAYERLISTCATEGORY: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + m_playerListCategory = (PlayerListCategory)numProp->value; break; + } - case PLPROP_UNKNOWN81: + case PlayerProp::COMMUNITYNAME: { - auto val = pPacket.readGUChar(); + PropertyString* strProp = dynamic_cast(base); + if (strProp == nullptr) + SETPROP_RETURN_ERROR; + + account.communityName = strProp->value; break; } - case PLPROP_COMMUNITYNAME: - pPacket.readChars(pPacket.readGUChar()); + case PlayerProp::UNKNOWN83: + { + PropertyNumeric* numProp = dynamic_cast*>(base); + if (numProp == nullptr) + SETPROP_RETURN_ERROR; + + log::printLine(log::server, "Player {} set prop 83 to value {}. This prop is currently unknown.", account.name, numProp->value); break; + } default: { - printf("Unidentified PLPROP: %i, readPos: %d\n", propId, pPacket.readPos()); - for (int i = 0; i < pPacket.length(); ++i) - printf("%02x ", (unsigned char)pPacket[i]); - printf("\n"); - sentInvalid = true; - } - return; + log::printLine(log::server, "Player {} sent an unidentified prop: {}.", account.name, PROPID(prop)); + result.resultFlags.set(props::SetResults::wasInvalid); + break; } + } - if ((options & PLSETPROPS_FORWARD) && __sendLocal[propId]) - levelBuff >> (char)propId << getProp(propId); + // If we are sending other ids, we need to update the mod time for them too. + if (!result.resultPropIds.empty()) + { + for (const auto& id : result.resultPropIds) + modTime[id] = curTime; + } - if ((options & PLSETPROPS_FORWARDSELF)) - selfBuff >> (char)propId << getProp(propId); + return result; +} + +/////////////////////////////////////////////////////////////////////////////// + +void Player::setPropsFromPacket(CString& packet, props::SetBy setBy, Player* originator) +{ + PropertySendResults results; + + while (packet.bytesLeft() > 0) + { + PlayerProp propId = (PlayerProp)packet.readGUChar(); + + auto prop = constructPropFor(propId); + prop->deserialize(packet); + + if (!checkPropSetAccess(propId, setBy, originator)) + continue; + + results.emplace_back(setProp(propId, setBy, prop), prop); } - // Send Buffers Out if (isLoggedIn() && isLoaded()) + sendPropsFromResults(results); +} + +bool Player::checkPropSetAccess(PlayerProp prop, SetBy setBy, Player* originator) const +{ + // Admin check on changing gralats. + if (prop == PlayerProp::RUPEESCOUNT && originator != nullptr) { - if (globalBuff.length() > 0) - m_server->sendPacketToAll(CString() >> (char)PLO_OTHERPLPROPS >> (short)this->m_id << globalBuff, { m_id }); - if (levelBuff.length() > 0) - { - // We need to arrange the props packet in a certain way depending - // on if our client supports precise movement or not. Versions 2.3+ - // support precise movement. - bool MOVE_PRECISE = false; - if (m_versionId >= CLVER_2_3) MOVE_PRECISE = true; + bool canSet = m_server->cached.normalAdminsCanChangeGralats.getValue(); + canSet = canSet || (originator->isStaff() && originator->account.hasRight(PLPERM_SETRIGHTS)); + return canSet; + } + + return true; +} + +void Player::sendPropsFromResults(PropertySendResults& results) +{ + CString sendAll, sendLevel, sendSource; + + std::erase_if(results, [](const PropertySendResults::value_type& res) + { + return !canSendProp((PlayerProp)res.first.propId); + }); + + collectPacketsFromResults(results, sendAll, sendLevel, sendSource, [this](uint8_t propId, SetResults::ResultFlagType& destinations) + { + return this->getProp((PlayerProp)propId); + }); + + // Send the buffers out. + if (sendAll.length() > 0) + m_server->sendPacketToAll(CString() >> (char)PLO_OTHERPLPROPS >> (short)this->m_id << sendAll, { m_id }); + + if (auto player = std::dynamic_pointer_cast(shared_from_this()); player != nullptr && sendLevel.length() > 0) + m_server->sendPacketToNearby(CString() >> (char)PLO_OTHERPLPROPS >> (short)this->m_id << sendLevel, player->getGlobalPosition(), player->getLevel(), { m_id }); + + if (sendSource.length() > 0) + sendPacket(CString() >> (char)PLO_PLAYERPROPS << sendSource); +} + +void Player::setPropsFromRCPacket(CString& pPacket, Player* rc) +{ + [[maybe_unused]] bool hadBomb = false, hadBow = false; + CString outPacket; - m_server->sendPacketToLevelArea(CString() >> (char)PLO_OTHERPLPROPS >> (short)this->m_id << (!MOVE_PRECISE ? levelBuff : levelBuff2) << (!MOVE_PRECISE ? levelBuff2 : levelBuff), shared_from_this(), { m_id }); + // Skip playerworld + pPacket.readChars(pPacket.readGUChar()); + + // Read props from the packet. + CString props = pPacket.readChars(pPacket.readGUChar()); + + // Send props out. + setPropsFromPacket(props, props::SetBy::SERVER, rc); + + // Clear flags + for (const auto& [flag, value] : account.variables.store) + outPacket >> (char)PLO_FLAGDEL << flag << "\n"; + account.variables.store.clear(); + + // Clear Weapons + for (const auto& weapon : account.weapons) + { + outPacket >> (char)PLO_NPCWEAPONDEL << weapon << "\n"; + + // Attempt to fix the funky client bomb capitalization issue. + // Also fix the bomb coming back when you set the player props through RC. + if (weapon == "bomb") + { + outPacket >> (char)PLO_NPCWEAPONDEL << "Bomb\n"; + hadBomb = true; } - if (selfBuff.length() > 0) - this->sendPacket(CString() >> (char)PLO_PLAYERPROPS << selfBuff); + if (weapon == "Bomb") + hadBomb = true; -#ifdef V8NPCSERVER - // Movement check. - //if (options & PLSETPROPS_SETBYPLAYER) - if (!rc) + // Do the same thing with the bow. + if (weapon == "bow") { - if (doTouchTest) - { - if (m_character.sprite % 4 == 0) - testSign(); - testTouch(); - } + outPacket >> (char)PLO_NPCWEAPONDEL << "Bow\n"; + hadBow = true; } -#endif + if (weapon == "Bow") + hadBow = true; } + account.weapons.clear(); - if (sentInvalid) + // Send the packet to clear the flags and weapons from the client. + if (isLoaded()) + sendPacket(outPacket); + + // Re-populate the flag list. + auto flagCount = pPacket.readGUShort(); + while (flagCount-- > 0) { - // If we are getting a whole bunch of invalid packets, something went wrong. Disconnect the player. - m_invalidPackets++; - if (m_invalidPackets > 5) - { - serverlog.out("Player %s is sending invalid packets.\n", m_character.nickName.c_str()); - sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Disconnected for sending invalid packets."); - m_server->deletePlayer(shared_from_this()); - } + CString flag = pPacket.readChars(pPacket.readGUChar()); + std::string name = flag.readString("=").toString(); + std::string val = flag.readString("").toString(); + + if (val.empty()) + setFlag(name, std::nullopt, isLoaded()); + else setFlag(name, val, isLoaded()); + } + + // Clear the chests and re-populate the chest list. + account.savedChests.clear(); + auto chestCount = pPacket.readGUShort(); + while (chestCount > 0) + { + unsigned char len = pPacket.readGUChar(); + uint8_t loc[2] = { pPacket.readGUChar(), pPacket.readGUChar() }; + std::string level = pPacket.readChars(len - 2).toString(); + + account.savedChests.insert(std::make_pair(level, LocalWholeTilePosition{ loc[0], loc[1] })); + --chestCount; + } + + // Re-populate the weapons list. + auto weaponCount = pPacket.readGUChar(); + while (weaponCount > 0) + { + unsigned char len = pPacket.readGUChar(); + if (len == 0) continue; + CString wpn = pPacket.readChars(len); + + // Allow the bomb through if we are actually adding it. + if (wpn == "bomb" || wpn == "Bomb") + hadBomb = true; + + // Allow the bow through if we are actually adding it. + if (wpn == "bow" || wpn == "Bow") + hadBow = true; + + // Send the weapon to the player. + this->addWeapon(wpn.toString()); + --weaponCount; + } + + // KILL THE BOMB DEAD + if (isLoaded()) + { + if (!hadBomb) + sendPacket(CString() >> (char)PLO_NPCWEAPONDEL << "Bomb"); + } + + // Warp the player to his new location now. + if (isLoaded() && isClient()) + { + if (auto player = std::dynamic_pointer_cast(shared_from_this()); player != nullptr) + player->warp(account.level, account.character.getLocalPosition(), std::nullopt); } } -void Player::sendProps(const bool* pProps, int pCount) +CString Player::getPropsPacketFromList(const PropList& props) const { - // Definition + DO_PACKETLOG(bool printedHeader = false); + CString propPacket; // Create Props - if (isClient() && m_versionId < CLVER_2_1) pCount = 37; - for (int i = 0; i < pCount; ++i) + for (int i = 0; i < PLAYERPROP_COUNT; ++i) { - if (pProps[i]) - propPacket >> (char)i << getProp(i); + if (!canSendProp((PlayerProp)i)) + continue; + + if (props[i]) + { + DO_PACKETLOG(if (!printedHeader) { printedHeader = true; printHeader(this, "PlayerProps::getPropsPacketFromList"sv); }); + auto prop = getProp(static_cast(i)); + DO_PACKETLOG(printProp(this, (PlayerProp)i, prop.get())); + propPacket >> (char)i << prop->serialize(); + } } - // Send Packet - sendPacket(CString() >> (char)PLO_PLAYERPROPS << propPacket); + if (m_isExternal) + propPacket >> (char)PlayerProp::PLAYERLISTCATEGORY >> (char)PlayerListCategory::EXTERNAL; + + DO_PACKETLOG(if (printedHeader) log::print(log::networkdump, "\n")); + return propPacket; } -CString Player::getProps(const bool* pProps, int pCount) +CString Player::getPropsForRCPacket() { - CString propPacket; + CString ret; + ret >> (char)account.name.length() << account.name; + ret >> (char)4 << "main"; // worldName - // Start the prop packet. - propPacket >> (char)PLO_OTHERPLPROPS >> (short)this->m_id; + // Add the props. + CString props = getPropsPacketFromList(clientPropsForRCView); + ret >> (char)props.length() << props; - if (pCount > 0) + // Add the player's flags. + ret >> (short)account.variables.store.size(); + for (const auto& [flag, value] : account.variables.store) { - // Check if PLPROP_JOINLEAVELVL is set. - if (isClient() && pProps[PLPROP_JOINLEAVELVL]) - propPacket >> (char)PLPROP_JOINLEAVELVL >> (char)1; + if (auto computedFlag = account.variables.serializeModern(flag); computedFlag.has_value()) + ret >> (char)(std::min((size_t)223, computedFlag.value().length())) << computedFlag.value().substr(0, 223); + } - // Create Props - if (isClient() && m_versionId < CLVER_2_1) pCount = 37; - for (int i = 0; i < pCount; ++i) - { - if (i == PLPROP_JOINLEAVELVL) continue; + // Add the player's chests. + ret >> (short)account.savedChests.size(); + for (const auto& [level, loc] : account.savedChests) + { + ret >> (char)(level.length() + 2) >> (char)loc.x() >> (char)loc.y() << level; + } - if (i == PLPROP_ATTACHNPC && m_attachNPC != 0) - { - propPacket >> (char)i; - getProp(propPacket, i); - } + // Add the player's weapons. + ret >> (char)account.weapons.size(); + for (const auto& weapon : account.weapons) + ret >> (char)weapon.length() << weapon; - if (pProps[i]) - { - propPacket >> (char)i; - getProp(propPacket, i); - } + return ret; +} + +CString Player::getModifiedPropsPacket() const +{ + DO_PACKETLOG(bool printedHeader = false); + + CString result; + for (auto i = 0; i < PLAYERPROP_COUNT; ++i) + { + if (!canSendProp((PlayerProp)i)) + continue; + + if (modTime[i].has_value() && modTime[i] != m_savedModTime[i]) + { + DO_PACKETLOG(if (!printedHeader) { printedHeader = true; printHeader(this, "PlayerProps::getPropsPacketFromList"sv); }); + auto prop = getProp((PlayerProp)i); + DO_PACKETLOG(printProp(this, (PlayerProp)i, prop.get())); + CString data = prop->serialize(); + result >> (char)i << data; } } - if (m_isExternal) - propPacket >> (char)81 << "!"; - - return propPacket; + DO_PACKETLOG(if (printedHeader) log::print(log::networkdump, "\n")); + return result; } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/player/PlayerRC.cpp b/server/src/player/PlayerRC.cpp index a82be5ca2..1602abfca 100644 --- a/server/src/player/PlayerRC.cpp +++ b/server/src/player/PlayerRC.cpp @@ -1,2231 +1,271 @@ +#include +#include #include -#include -#include -#include +#include +#include +#include #include -#if defined(_WIN32) || defined(_WIN64) - #include - #define mkdir _mkdir - #define rmdir _rmdir -#else - #include -#endif - -#include -#include - -#include "IConfig.h" - -#include "Player.h" -#include "Server.h" -#include "level/Level.h" -#include "utilities/TimeUnits.h" - -#define serverlog m_server->getServerLog() -#define rclog m_server->getRCLog() -#define nclog m_server->getNPCLog() -extern bool __playerPropsRC[propscount]; - -// Admin-only server options. They are protected from being changed by people without the -// 'change staff account' right. -const char* __admin[] = { - "name", "description", "url", "serverip", "serverport", "localip", "listip", "listport", - "maxplayers", "onlystaff", "nofoldersconfig", "oldcreated", "serverside", - "triggerhack_weapons", "triggerhack_guilds", "triggerhack_groups", "triggerhack_files", - "triggerhack_rc", "flaghack_movement", "flaghack_ip", - "sharefolder", "language" -}; - -// Files that are protected from being downloaded by people without the -// 'change staff account' right. -const char* __protectedFiles[] = { - "accounts/defaultaccount.txt", - "config/adminconfig.txt", - "config/allowedversions.txt", - "config/rchelp.txt", -}; - -// List of important files. -const char* __importantFiles[] = { - "accounts/defaultaccount.txt", - "config/adminconfig.txt", - "config/allowedversions.txt", - "config/foldersconfig.txt", - "config/ipbans.txt", - "config/rchelp.txt", - "config/rcmessage.txt", - "config/rules.txt", - "config/servermessage.html", - "config/serveroptions.txt", -}; - -const int __importantFileRights[] = { - PLPERM_MODIFYSTAFFACCOUNT, - PLPERM_MODIFYSTAFFACCOUNT, - PLPERM_MODIFYSTAFFACCOUNT, - PLPERM_SETFOLDEROPTIONS, - PLPERM_MODIFYSTAFFACCOUNT, - PLPERM_MODIFYSTAFFACCOUNT, - PLPERM_MODIFYSTAFFACCOUNT, - PLPERM_MODIFYSTAFFACCOUNT, - PLPERM_SETSERVEROPTIONS, - PLPERM_SETSERVEROPTIONS, -}; - -static void updateFile(Player* player, Server* server, CString& dir, CString& file); - -void Player::setPropsRC(CString& pPacket, Player* rc) -{ - bool hadBomb = false, hadBow = false; - CString outPacket; - - // Skip playerworld - pPacket.readChars(pPacket.readGUChar()); - - // Read props from the packet. - CString props = pPacket.readChars(pPacket.readGUChar()); - - // Send props out. - setProps(props, (m_id != -1 ? PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF : 0), rc); - - // Clear flags - for (auto i = m_flagList.begin(); i != m_flagList.end(); ++i) - { - outPacket >> (char)PLO_FLAGDEL << i->first; - if (!i->second.isEmpty()) outPacket << "=" << i->second; - outPacket << "\n"; - } - - // Clear Weapons - for (std::vector::iterator i = m_weaponList.begin(); i != m_weaponList.end(); ++i) - { - outPacket >> (char)PLO_NPCWEAPONDEL << *i << "\n"; - - // Attempt to fix the funky client bomb capitalization issue. - // Also fix the bomb coming back when you set the player props through RC. - if ((*i) == "bomb") - { - outPacket >> (char)PLO_NPCWEAPONDEL << "Bomb\n"; - hadBomb = true; - } - if ((*i) == "Bomb") - hadBomb = true; - - // Do the same thing with the bow. - if ((*i) == "bow") - { - outPacket >> (char)PLO_NPCWEAPONDEL << "Bow\n"; - hadBow = true; - } - if ((*i) == "Bow") - hadBow = true; - } - if (m_id != -1) sendPacket(outPacket); - - // Clear the flags and re-populate the flag list. - m_flagList.clear(); - auto flagCount = pPacket.readGUShort(); - while (flagCount > 0) - { - CString flag = pPacket.readChars(pPacket.readGUChar()); - std::string name = flag.readString("=").text(); - CString val = flag.readString(""); - - setFlag(name, val, (m_id != -1)); - --flagCount; - } - - // Clear the chests and re-populate the chest list. - m_chestList.clear(); - auto chestCount = pPacket.readGUShort(); - while (chestCount > 0) - { - unsigned char len = pPacket.readGUChar(); - char loc[2] = { pPacket.readGChar(), pPacket.readGChar() }; - m_chestList.push_back(CString() << CString((int)loc[0]) << ":" << CString((int)loc[1]) << ":" << pPacket.readChars(len - 2)); - --chestCount; - } - - // Clear the weapons and re-populate the weapons list. - m_weaponList.clear(); - auto weaponCount = pPacket.readGUChar(); - while (weaponCount > 0) - { - unsigned char len = pPacket.readGUChar(); - if (len == 0) continue; - CString wpn = pPacket.readChars(len); - - // Allow the bomb through if we are actually adding it. - if (wpn == "bomb" || wpn == "Bomb") - hadBomb = true; - - // Allow the bow through if we are actually adding it. - if (wpn == "bow" || wpn == "Bow") - hadBow = true; - - // Send the weapon to the player. - this->addWeapon(wpn.toString()); - --weaponCount; - } - - // KILL THE BOMB DEAD - if (m_id != -1) - { - if (!hadBomb) - sendPacket(CString() >> (char)PLO_NPCWEAPONDEL << "Bomb"); - } - - // Warp the player to his new location now. - if (m_id != -1) warp(m_levelName, getX(), getY(), 0); -} - -CString Player::getPropsRC() -{ - CString ret, props; - ret >> (char)m_accountName.length() << m_accountName; - ret >> (char)4 << "main"; // worldName - - // Add the props. - for (int i = 0; i < propscount; ++i) - { - if (__playerPropsRC[i]) - props >> (char)i << getProp(i); - } - ret >> (char)props.length() << props; - - // Add the player's flags. - ret >> (short)m_flagList.size(); - for (auto i = m_flagList.begin(); i != m_flagList.end(); ++i) - { - CString flag = i->first; - if (!i->second.isEmpty()) flag << "=" << i->second; - if (flag.length() > 0xDF) flag.removeI(0xDF); - ret >> (char)flag.length() << flag; - } - - // Add the player's chests. - ret >> (short)m_chestList.size(); - for (std::vector::iterator i = m_chestList.begin(); i != m_chestList.end(); ++i) - { - std::vector chest = (*i).tokenize(":"); - if (chest.size() == 3) - { - CString chestData; - chestData >> (char)atoi(chest[0].text()) >> (char)atoi(chest[1].text()) << chest[2]; - ret >> (char)chestData.length() << chestData; - } - } - - // Add the player's weapons. - ret >> (char)m_weaponList.size(); - for (auto& i: m_weaponList) - ret >> (char)i.length() << i; - - return ret; -} - -bool Player::msgPLI_RC_SERVEROPTIONSGET(CString& pPacket) -{ - if (isClient()) - { - rclog.out("[Hack] %s attempted to view the server options.", m_accountName.text()); - return true; - } - - auto& settings = m_server->getSettings(); - - sendPacket(CString() >> (char)PLO_RC_SERVEROPTIONSGET << settings.getSettings().gtokenize()); - return true; -} - -bool Player::msgPLI_RC_SERVEROPTIONSSET(CString& pPacket) -{ - if (isClient() || !hasRight(PLPERM_SETSERVEROPTIONS)) - { - if (isClient()) rclog.out("[Hack] %s attempted to set the server options.", m_accountName.text()); - else - rclog.out("%s attempted to set the server options.", m_accountName.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " is not authorized to change the server options."); - return true; - } - - auto& settings = m_server->getSettings(); - CString options = pPacket.readString(""); - options.guntokenizeI(); - - // If they don't have the modify staff account right, prevent them from changing admin-only options. - if (!hasRight(PLPERM_MODIFYSTAFFACCOUNT)) - { - std::vector newOptions = options.tokenize("\n"); - options.clear(); - for (auto& newOption: newOptions) - { - CString name = newOption.subString(0, newOption.find("=")); - name.trimI(); - - // See if this command is an admin command. - bool isAdmin = false; - for (auto& j: __admin) - if (name == CString(j)) isAdmin = true; - - // If it is an admin command, replace it with the current value. - if (isAdmin) - newOption = CString() << name << " = " << settings.getStr(name); - - // Add this line back into options. - options << newOption << "\n"; - } - } - - // Save settings. - settings.loadSettings(options, true, true); - - // Reload settings. - m_server->loadSettings(); - m_server->loadMaps(); - rclog.out("%s has updated the server options.\n", m_accountName.text()); - - // Send RC Information - CString outPacket = CString() >> (char)PLO_RC_CHAT << m_accountName << " has updated the server options."; - auto& playerList = m_server->getPlayerList(); - for (auto& [pid, player]: playerList) - { - if (player->getType() & PLTYPE_ANYRC) - { - player->sendPacket(outPacket); -#ifdef V8NPCSERVER - if (hasRight(PLPERM_NPCCONTROL)) - player->sendNCAddr(); -#endif - } - } - - return true; -} - -bool Player::msgPLI_RC_FOLDERCONFIGGET(CString& pPacket) -{ - if (isClient()) - { - rclog.out("[Hack] %s attempted to get the folder config.", m_accountName.text()); - return true; - } - - CString foldersConfig; - foldersConfig.load(m_server->getServerPath() << "config/foldersconfig.txt"); - foldersConfig.removeAllI("\r"); - - sendPacket(CString() >> (char)PLO_RC_FOLDERCONFIGGET << foldersConfig.gtokenize()); - return true; -} - -bool Player::msgPLI_RC_FOLDERCONFIGSET(CString& pPacket) -{ - if (isClient() || !hasRight(PLPERM_SETFOLDEROPTIONS)) - { - if (isClient()) rclog.out("[Hack] %s attempted to set the folder config.", m_accountName.text()); - else - rclog.out("%s attempted to set the folder config.", m_accountName.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " is not authorized to change the folder config."); - return true; - } - - // Save the folder config back to disk - CString folders = pPacket.readString(""); - folders.guntokenizeI(); - folders.replaceAllI("\n", "\r\n"); - folders.save(m_server->getServerPath() << "config/foldersconfig.txt"); - - // Update file system. - m_server->loadFileSystem(); - - rclog.out("%s updated the folder config.\n", m_accountName.text()); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << m_accountName << " updated the folder config."); - return true; -} - -bool Player::msgPLI_RC_RESPAWNSET(CString& pPacket) -{ - // Deprecated - return true; -} - -bool Player::msgPLI_RC_HORSELIFESET(CString& pPacket) -{ - // Deprecated - return true; -} - -bool Player::msgPLI_RC_APINCREMENTSET(CString& pPacket) -{ - // Deprecated - return true; -} - -bool Player::msgPLI_RC_BADDYRESPAWNSET(CString& pPacket) -{ - // Deprecated - return true; -} - -bool Player::msgPLI_RC_PLAYERPROPSGET(CString& pPacket) -{ - // Deprecated - return true; -} - -bool Player::msgPLI_RC_PLAYERPROPSSET(CString& pPacket) -{ - // Deprecated? - - auto p = m_server->getPlayer(pPacket.readGUShort(), PLTYPE_ANYPLAYER); - if (p == nullptr) return true; - - if (isClient() || (p->getAccountName() != m_accountName && !hasRight(PLPERM_SETATTRIBUTES)) || (p->getAccountName() == m_accountName && !hasRight(PLPERM_SETSELFATTRIBUTES))) - { - if (isClient()) rclog.out("[Hack] %s attempted to set a player's properties.", m_accountName.text()); - else - rclog.out("%s attempted to set a player's properties.", m_accountName.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " is not authorized to set the properties of " << p->getAccountName()); - return true; - } - - p->setPropsRC(pPacket, this); - p->saveAccount(); - rclog.out("%s set the attributes of player %s\n", m_accountName.text(), p->getAccountName().text()); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << m_accountName << " set the attributes of player " << p->getAccountName()); - - return true; -} - -bool Player::msgPLI_RC_DISCONNECTPLAYER(CString& pPacket) -{ - auto p = m_server->getPlayer(pPacket.readGUShort(), PLTYPE_ANYPLAYER); - if (p == nullptr) return true; - - if (isClient() || !hasRight(PLPERM_DISCONNECT)) - { - if (isClient()) rclog.out("[Hack] %s attempted to disconnect %s.\n", m_accountName.text(), p->getAccountName().text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " is not authorized to disconnect players."); - return true; - } - - CString reason = pPacket.readString(""); - if (!reason.isEmpty()) - rclog.out("%s disconnected %s: %s\n", m_accountName.text(), p->getAccountName().text(), reason.text()); - else - rclog.out("%s disconnected %s.\n", m_accountName.text(), p->getAccountName().text()); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << m_accountName << " disconnected " << p->getAccountName()); - - CString disconnectMessage = CString() << "One of the server administrators, " << m_accountName << ", has disconnected you"; - if (!reason.isEmpty()) - disconnectMessage << " for the following reason: " << reason; - else - disconnectMessage << "."; - p->sendPacket(CString() >> (char)PLO_DISCMESSAGE << disconnectMessage); - m_server->deletePlayer(p); - return true; -} - -bool Player::msgPLI_RC_UPDATELEVELS(CString& pPacket) -{ - if (isClient() || !hasRight(PLPERM_UPDATELEVEL)) - { - if (isClient()) rclog.out("[Hack] %s attempted to update levels.\n", m_accountName.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " is not authorized to update levels."); - return true; - } - - unsigned short levelCount = pPacket.readGUShort(); - for (int i = 0; i < levelCount; ++i) - { - auto level = m_server->getLevel(pPacket.readChars(pPacket.readGUChar()).toString()); - if (level) level->reload(); - } - return true; -} - -bool Player::msgPLI_RC_ADMINMESSAGE(CString& pPacket) -{ - if (isClient() || !hasRight(PLPERM_ADMINMSG)) - { - if (isClient()) rclog.out("[Hack] %s attempted to send an admin message.\n", m_accountName.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to send an admin message."); - return true; - } - - m_server->sendPacketToAll(CString() >> (char)PLO_RC_ADMINMESSAGE << "Admin " << m_accountName << ":\xa7" << pPacket.readString(""), { m_id }); - return true; -} - -bool Player::msgPLI_RC_PRIVADMINMESSAGE(CString& pPacket) -{ - if (isClient() || !hasRight(PLPERM_ADMINMSG)) - { - if (isClient()) rclog.out("[Hack] %s attempted to send an admin message.\n", m_accountName.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to send an admin message."); - return true; - } - - auto p = m_server->getPlayer(pPacket.readGUShort(), PLTYPE_ANYPLAYER); - if (p == nullptr) return true; - - p->sendPacket(CString() >> (char)PLO_RC_ADMINMESSAGE << "Admin " << m_accountName << ":\xa7" << pPacket.readString("")); - return true; -} - -bool Player::msgPLI_RC_LISTRCS(CString& pPacket) -{ - // Deprecated - return true; -} - -bool Player::msgPLI_RC_DISCONNECTRC(CString& pPacket) -{ - // Deprecated - return true; -} - -bool Player::msgPLI_RC_APPLYREASON(CString& pPacket) -{ - // Deprecated - return true; -} - -bool Player::msgPLI_RC_SERVERFLAGSGET(CString& pPacket) -{ - if (isClient()) - { - rclog.out("[Hack] %s attempted to view the server flags.\n", m_accountName.text()); - return true; - } - CString ret; - ret >> (char)PLO_RC_SERVERFLAGSGET >> (short)m_server->getServerFlags().size(); - for (const auto& [flag, value]: m_server->getServerFlags()) - { - CString flagString = CString() << flag << "=" << value; - ret >> (char)flagString.length() << flagString; - } - sendPacket(ret); - return true; -} - -bool Player::msgPLI_RC_SERVERFLAGSSET(CString& pPacket) -{ - if (isClient() || !hasRight(PLPERM_SETSERVERFLAGS)) - { - if (isClient()) rclog.out("[Hack] %s attempted to set the server flags.\n", m_accountName.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to set the server flags."); - return true; - } - - unsigned short count = pPacket.readGUShort(); - auto& serverFlags = m_server->getServerFlags(); - - // Save server flags. - std::unordered_map oldFlags = serverFlags; - - // Delete server flags. - serverFlags.clear(); - - // Assemble the new server flags. - for (unsigned int i = 0; i < count; ++i) - m_server->setFlag(pPacket.readChars(pPacket.readGUChar()), false); - - // Send flag changes to all players. - for (auto i = serverFlags.begin(); i != serverFlags.end(); ++i) - { - bool found = false; - for (auto j = oldFlags.begin(); j != oldFlags.end();) - { - // Flag name - if (i->first == j->first) - { - // Check to see if the values are the same. - // If they are, set found to true so we don't send it to the player again. - if (i->second == j->second) - found = true; - oldFlags.erase(j++); - if (found) break; - } - else - ++j; - } - - // If we didn't find a match, this is either a new flag, or a changed flag. - if (!found) - { - if (i->second.isEmpty()) - m_server->sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_FLAGSET << i->first); - else - m_server->sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_FLAGSET << i->first << "=" << i->second); - } - } - - // If any flags were deleted, tell that to the players now. - for (auto i = oldFlags.begin(); i != oldFlags.end(); ++i) - m_server->sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_FLAGDEL << i->first); - - rclog.out("%s has updated the server flags.\n", m_accountName.text()); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << m_accountName << " has updated the server flags."); - return true; -} - -bool Player::msgPLI_RC_ACCOUNTADD(CString& pPacket) -{ - if (isClient() || !hasRight(PLPERM_MODIFYSTAFFACCOUNT)) - { - if (isClient()) rclog.out("[Hack] %s attempted to add a new account.\n", m_accountName.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to create new accounts."); - return true; - } - - CString acc = pPacket.readChars(pPacket.readGUChar()); - CString pass = pPacket.readChars(pPacket.readGUChar()); - CString email = pPacket.readChars(pPacket.readGUChar()); - bool banned = (pPacket.readGUChar() != 0); - bool onlyLoad = (pPacket.readGUChar() != 0); - pPacket.readGUChar(); // Admin level, deprecated. - - Account newAccount; - newAccount.loadAccount(acc); - newAccount.setBanned(banned); - newAccount.setLoadOnly(onlyLoad); - newAccount.setEmail(email); - newAccount.saveAccount(); - - rclog.out("%s has created a new account: %s\n", m_accountName.text(), acc.text()); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << m_accountName << " has created a new account: " << acc); - return true; -} - -bool Player::msgPLI_RC_ACCOUNTDEL(CString& pPacket) -{ - if (isClient() || !hasRight(PLPERM_MODIFYSTAFFACCOUNT)) - { - if (isClient()) rclog.out("[Hack] %s attempted to delete an account.\n", m_accountName.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to delete accounts."); - return true; - } - - // Get the account. - // Prevent the defaultaccount from being deleted. - CString acc = pPacket.readString(""); - if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); - if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); - if (m_server->getAccountsFileSystem()->findi(CString(acc) << ".txt").isEmpty()) - return true; - - if (acc == "defaultaccount") - { - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not allowed to delete the default account."); - return true; - } - - // See if the account exists. - CString accfile = CString(acc) << ".txt"; - CString accpath = m_server->getAccountsFileSystem()->find(accfile); - if (accpath.isEmpty()) return true; - - // Remove the account from the file system. - m_server->getAccountsFileSystem()->removeFile(accfile); - - // Delete the file now. - remove(accpath.text()); - rclog.out("%s has deleted the account: %s\n", m_accountName.text(), acc.text()); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << m_accountName << " has deleted the account: " << acc); - return true; -} - -bool Player::msgPLI_RC_ACCOUNTLISTGET(CString& pPacket) -{ - if (isClient()) - { - rclog.out("[Hack] %s attempted to view the account listing.\n", m_accountName.text()); - return true; - } - - CString name = pPacket.readChars(pPacket.readGUChar()); - CString conditions = pPacket.readChars(pPacket.readGUChar()); - - // Fix up name searching. - name.replaceAllI("%", "*"); - if (name.length() == 0) - name = "*"; - - // Start our packet. - CString ret; - ret >> (char)PLO_RC_ACCOUNTLISTGET; - - // Search through all the accounts. - FileSystem* fs = m_server->getAccountsFileSystem(); - for (std::map::iterator i = fs->getFileList().begin(); i != fs->getFileList().end(); ++i) - { - CString acc = removeExtension(i->first); - if (acc.isEmpty()) continue; - if (!acc.match(name)) continue; - if (conditions.length() != 0) - { - if (Account::meetsConditions(i->second, conditions)) - ret >> (char)acc.length() << acc; - } - else - ret >> (char)acc.length() << acc; - } - - sendPacket(ret); - return true; -} - -bool Player::msgPLI_RC_PLAYERPROPSGET2(CString& pPacket) -{ - auto p = m_server->getPlayer(pPacket.readGUShort(), PLTYPE_ANYPLAYER | PLTYPE_NPCSERVER); - if (p == nullptr) return true; - - if (isClient() || !hasRight(PLPERM_VIEWATTRIBUTES)) - { - if (isClient()) rclog.out("[Hack] %s attempted to view the props of player %s.\n", m_accountName.text(), p->getAccountName().text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to view player props."); - return true; - } - - sendPacket(CString() >> (char)PLO_RC_PLAYERPROPSGET >> (short)p->getId() << p->getPropsRC()); - return true; -} - -bool Player::msgPLI_RC_PLAYERPROPSGET3(CString& pPacket) -{ - CString acc = pPacket.readChars(pPacket.readGUChar()); - if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); - if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); - - auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); - if (p == nullptr) - { - if (m_server->getAccountsFileSystem()->findi(CString(acc) << ".txt").isEmpty()) - return true; - - p = std::make_shared(nullptr, 0); - if (!p->loadAccount(acc)) - return true; - } - - if (isClient() || !hasRight(PLPERM_VIEWATTRIBUTES)) - { - if (isClient()) rclog.out("[Hack] %s attempted to view the props of player %s.\n", m_accountName.text(), p->getAccountName().text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to view player props."); - return true; - } - - sendPacket(CString() >> (char)PLO_RC_PLAYERPROPSGET >> (short)p->getId() << p->getPropsRC()); - return true; -} - -bool Player::msgPLI_RC_PLAYERPROPSRESET(CString& pPacket) -{ - CString acc = pPacket.readString(""); - if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); - if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); - - if (isClient() || !hasRight(PLPERM_RESETATTRIBUTES)) - { - if (isClient()) rclog.out("[Hack] %s attempted to reset the account: %s\n", m_accountName.text(), acc.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to reset accounts.\n"); - return true; - } - - // Get the player. Create a new player if they are offline. - auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); - if (p == nullptr) - { - if (m_server->getAccountsFileSystem()->findi(CString(acc) << ".txt").isEmpty()) - return true; - - p = std::make_shared(nullptr, 0); - if (!p->loadAccount(acc)) - return true; - } - - // Save RC stuff. - CString adminip = p->getAdminIp(); - int rights = p->getAdminRights(); - std::vector folders(p->getFolderList()); - - // Reset the player. - p->reset(); - - // Add the RC stuff back in. - p->setAdminIp(adminip); - p->setAdminRights(rights); - p->setFolderRights(folders); - - // Save the account. - p->saveAccount(); - - // If the player is online, boot him from the server. - if (p->getId() != 0) - { - p->sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Your account was reset by " << m_accountName); - p->setLoaded(false); // Don't save the account when the player quits. - m_server->deletePlayer(p); - } - - // Log it. - rclog.out("%s has reset the attributes of account: %s\n", m_accountName.text(), acc.text()); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << m_accountName << " has reset the attributes of account: " << acc); - - return true; -} - -bool Player::msgPLI_RC_PLAYERPROPSSET2(CString& pPacket) -{ - CString acc = pPacket.readChars(pPacket.readGUChar()); - if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); - if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); - - auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); - if (p == nullptr) - { - if (m_server->getAccountsFileSystem()->findi(CString(acc) << ".txt").isEmpty()) - return true; - - p = std::make_shared(nullptr, 0); - if (!p->loadAccount(acc)) - return true; - } - - if (isClient() || (p->getAccountName() != m_accountName && !hasRight(PLPERM_SETATTRIBUTES)) || (p->getAccountName() == m_accountName && !hasRight(PLPERM_SETSELFATTRIBUTES))) - { - if (isClient()) rclog.out("[Hack] %s attempted to set a player's properties.", m_accountName.text()); - else - rclog.out("%s attempted to set a player's properties.", m_accountName.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " is not authorized to set the properties of " << p->getAccountName()); - return true; - } - - // Only people with PLPERM_MODIFYSTAFFACCOUNT can alter the default account. - if (!hasRight(PLPERM_MODIFYSTAFFACCOUNT) && acc == "defaultaccount") - { - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to modify the default account."); - return true; - } - - p->setPropsRC(pPacket, this); - p->saveAccount(); - rclog.out("%s set the attributes of player %s\n", m_accountName.text(), p->getAccountName().text()); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << m_accountName << " set the attributes of player " << p->getAccountName()); - - return true; -} - -bool Player::msgPLI_RC_ACCOUNTGET(CString& pPacket) -{ - CString acc = pPacket.readString(""); - if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); - if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); - - if (isClient()) - { - rclog.out("[Hack] %s attempted to view the account: %s\n", m_accountName.text(), acc.text()); - return true; - } - - // Get the player. - auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); - if (p == nullptr) - { - if (m_server->getAccountsFileSystem()->findi(CString(acc) << ".txt").isEmpty()) - return true; - - p = std::make_shared(nullptr, 0); - if (!p->loadAccount(acc)) - return true; - } - - sendPacket(CString() >> (char)PLO_RC_ACCOUNTGET >> (char)acc.length() << acc >> (char)0 /*>> (char)password_length << password*/ - >> (char)p->getEmail().length() << p->getEmail() >> (char)(p->getBanned() ? 1 : 0) >> (char)(p->getLoadOnly() ? 1 : 0) >> (char)0 /*admin level*/ - >> (char)4 << "main" >> (char)p->getBanLength().length() << p->getBanLength() >> (char)p->getBanReason().length() << p->getBanReason()); - - return true; -} - -bool Player::msgPLI_RC_ACCOUNTSET(CString& pPacket) -{ - CString acc = pPacket.readChars(pPacket.readGUChar()); - if (acc.length() == 0) return true; - if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); - if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); - - if (isClient() || !hasRight(PLPERM_MODIFYSTAFFACCOUNT)) - { - if (isClient()) rclog.out("[Hack] %s attempted to edit the account: %s\n", m_accountName.text(), acc.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to edit accounts.\n"); - return true; - } - - CString pass = pPacket.readChars(pPacket.readGUChar()); - CString email = pPacket.readChars(pPacket.readGUChar()); - bool banned = (pPacket.readGUChar() != 0 ? true : false); - bool loadOnly = (pPacket.readGUChar() != 0 ? true : false); - pPacket.readGUChar(); // admin level - pPacket.readChars(pPacket.readGUChar()); // world - CString banreason = pPacket.readChars(pPacket.readGUChar()); - - // Get player. - auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); - if (p == nullptr) - { - if (m_server->getAccountsFileSystem()->findi(CString(acc) << ".txt").isEmpty()) - return true; - - p = std::make_shared(nullptr, 0); - if (!p->loadAccount(acc)) - return true; - } - - // Set the new account stuff. - p->setEmail(email); - p->setLoadOnly(loadOnly); - if (hasRight(PLPERM_BAN)) - { - p->setBanned(banned); - p->setBanReason(banreason); - } - p->saveAccount(); - - // If the account is currently on RC, reload it. - if (auto pRC = m_server->getPlayer(acc, PLTYPE_ANYRC); pRC) - { - pRC->loadAccount(acc); - } - - // If the player was just now banned, kick him off the server. - if (hasRight(PLPERM_BAN) && banned && p->getId() != 0) - { - p->setLoaded(false); - p->sendPacket(CString() >> (char)PLO_DISCMESSAGE << m_accountName << " has banned you. Reason: " << banreason.guntokenize().replaceAll("\n", "\r")); - m_server->deletePlayer(p); - } - - rclog.out("%s has modified the account: %s\n", m_accountName.text(), acc.text()); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << m_accountName << " has modified the account: " << acc); - - return true; -} - -bool Player::msgPLI_RC_CHAT(CString& pPacket) -{ - if (isClient()) - { - rclog.out("[Hack] %s attempted to send a message to RC.\n", m_accountName.text()); - return true; - } - - CString message = pPacket.readString(""); - if (message.isEmpty()) return true; - std::vector words = message.tokenize(); - -#ifdef V8NPCSERVER - if (isNC()) - { - // TODO(joey): All RC's with NC support are sending two messages at a time. - // Can use this section for npc-server related commands though. - //m_server->sendToNC(CString(m_character.nickName) << ": " << message); - return true; - } -#endif - - if (words[0].text()[0] != '/') - { - m_server->sendToRC(CString(m_character.nickName) << ": " << message); - return true; - } - else - { -#ifndef NDEBUG - if (words[0] == "/sendtext") - { - sendPacket(CString() >> (char)PLO_SERVERTEXT << message.subString(10) << "\n"); - } - else -#endif - if (words[0] == "/help" && words.size() == 1) - { - std::vector commands = CString::loadToken(m_server->getServerPath() << "config/rchelp.txt", "\n", true); - for (auto& command: commands) - sendPacket(CString() >> (char)PLO_RC_CHAT << command); - } - else if (words[0] == "/version" && words.size() == 1) - { - sendPacket(CString() >> (char)PLO_RC_CHAT << APP_NAME << " version: " << APP_VERSION); - } - else if (words[0] == "/credits" && words.size() == 1) - { - sendPacket(CString() >> (char)PLO_RC_CHAT << "Programmed by " << APP_CREDITS); - } - else if (words[0] == "/open" && words.size() != 1) - { - message.setRead(0); - message.readString(" "); - CString acc = message.readString(""); - return msgPLI_RC_PLAYERPROPSGET3(CString() >> (char)acc.length() << acc); - } - else if (words[0] == "/openacc" && words.size() != 1) - { - message.setRead(0); - message.readString(" "); - CString acc = message.readString(""); - return msgPLI_RC_ACCOUNTGET(CString() << acc); - } - else if (words[0] == "/opencomments" && words.size() != 1) - { - message.setRead(0); - message.readString(" "); - CString acc = message.readString(""); - return msgPLI_RC_PLAYERCOMMENTSGET(CString() << acc); - } - else if (words[0] == "/openaccess" && words.size() != 1) - { - message.setRead(0); - message.readString(" "); - - CString acc = message.readString(""); - auto pl = m_server->getPlayer(acc, PLTYPE_ANYPLAYER); - if (pl) - sendPacket(CString() >> (char)PLO_SERVERTEXT << "GraalEngine,lister,ban," << pl->getAccountName() << "," << std::to_string(pl->getDeviceId())); - else - { - // TODO: player not logged in, load from offline? - } - } - else if (words[0] == "/openban" && words.size() != 1) - { - message.setRead(0); - message.readString(" "); - CString acc = message.readString(""); - return msgPLI_RC_PLAYERBANGET(CString() << acc); - } - else if (words[0] == "/openrights" && words.size() != 1) - { - message.setRead(0); - message.readString(" "); - CString acc = message.readString(""); - return msgPLI_RC_PLAYERRIGHTSGET(CString() << acc); - } - else if (words[0] == "/reset" && words.size() != 1) - { - message.setRead(0); - message.readString(" "); - CString acc = message.readString(""); - return msgPLI_RC_PLAYERPROPSRESET(CString() << acc); - } - else if (words[0] == "/refreshservermessage" && words.size() == 1) - { - m_server->loadServerMessage(); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " refreshed the server message."); - rclog.out("%s refreshed the server message.\n", m_accountName.text()); - } - - else if (words[0] == "/refreshfilesystem" && words.size() == 1) - { - m_server->loadFileSystem(); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " refreshed the server file list."); - rclog.out("%s refreshed the server file list.\n", m_accountName.text()); - } - else if (words[0] == "/updatelevel" && words.size() != 1 && hasRight(PLPERM_UPDATELEVEL)) - { - std::vector levels = words[1].tokenize(","); - for (auto& l: levels) - { - auto level = m_server->getLevel(l.toString()); - if (level) - { - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " updated level: " << level->getLevelName()); - rclog.out("%s updated level: %s\n", m_accountName.text(), level->getLevelName().text()); - level->reload(); - } - } - } - else if (words[0] == "/updatelevelall" && words.size() == 1 && hasRight(PLPERM_UPDATELEVEL)) - { - rclog.out("%s updated all the levels", m_accountName.text()); - int count = 0; - auto& levels = m_server->getLevelList(); - for (auto& level: levels) - { - level->reload(); - ++count; - } - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " updated all the levels (" << CString((int)count) << " levels updated)."); - rclog.out(" (%d levels updated).\n", count); - } - else if (words[0] == "/restartserver" && words.size() == 1 && hasRight(PLPERM_MODIFYSTAFFACCOUNT)) - { - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " restarted the server."); - rclog.out("%s restarted the server.\n", m_accountName.text()); - m_server->restart(); - } - else if (words[0] == "/reloadserver" && words.size() == 1 && hasRight(PLPERM_MODIFYSTAFFACCOUNT)) - { - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " reloaded the server configuration files."); - rclog.out("%s reloaded the server configuration files.\n", m_accountName.text()); - m_server->loadConfigFiles(); - } - else if (words[0] == "/updateserverhq" && words.size() == 1 && hasRight(PLPERM_MODIFYSTAFFACCOUNT)) - { - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " sent ServerHQ updates."); - rclog.out("%s sent ServerHQ updates.\n", m_accountName.text()); - m_server->loadAdminSettings(); - m_server->getServerList().sendServerHQ(); - } - else if (words[0] == "/serveruptime" && words.size() == 1) - { - auto time_units = utilities::TimeUnits(std::time(nullptr) - m_server->getServerStartTime()); - - constexpr auto format_time_fn = [](std::string& m, const uint64_t t, const char* fmtStr) - { - if (t > 0) - { - m.append(std::format(" {} {}", t, fmtStr)); - if (t > 1) - m.append("s"); - } - }; - - std::string msg; - format_time_fn(msg, time_units.days, "day"); - format_time_fn(msg, time_units.hours, "hour"); - format_time_fn(msg, time_units.minutes, "minute"); - if (time_units.days == 0) - format_time_fn(msg, time_units.seconds, "second"); - - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server Uptime:" << msg); - } - else if (words[0] == "/reloadwordfilter" && words.size() == 1) - { - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " reloaded the word filter."); - rclog.out("%s reloaded the word filter.\n", m_accountName.text()); - m_server->loadWordFilter(); - } - else if (words[0] == "/reloadipbans" && words.size() == 1) - { - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " reloaded the ip bans."); - rclog.out("%s reloaded the ip bans.\n", m_accountName.text()); - m_server->loadIPBans(); - } - else if (words[0] == "/reloadweapons" && words.size() == 1) - { - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " reloaded the weapons."); - rclog.out("%s reloaded the weapons.\n", m_accountName.text()); - m_server->loadWeapons(true); - } -#ifdef V8NPCSERVER - else if (words[0] == "/savenpcs" && words.size() == 1) - { - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << m_accountName << " saved npc to disk."); - nclog.out("%s saved the npcs to disk.\n", m_accountName.text()); - m_server->saveNpcs(); - } - else if (words[0] == "/stats" && words.size() == 1) - { - auto npcStats = m_server->calculateNpcStats(); - - sendPacket(CString() >> (char)PLO_RC_CHAT << "Top scripts using the most execution time (in the last min)"); - - int idx = 0; - for (auto it = npcStats.begin(); it != npcStats.end(); ++it) - { - idx++; - sendPacket(CString() >> (char)PLO_RC_CHAT << CString(idx) << ". " << CString((*it).first) << " " << (*it).second); - if (idx == 50) - break; - } - } -#endif - else if (words[0] == "/find" && words.size() > 1) - { - std::map found; - - // Assemble the search string. - CString search(words[1]); - for (unsigned int i = 2; i < words.size(); ++i) - search << " " << words[i]; - - // Search for the files. - for (unsigned int i = 0; i < FS_COUNT; ++i) - { - auto& fileList = m_server->getFileSystem(i)->getFileList(); - CString fs("none"); - if (i == 0) fs = "all"; - if (i == 1) fs = "file"; - if (i == 2) fs = "level"; - if (i == 3) fs = "head"; - if (i == 4) fs = "body"; - if (i == 5) fs = "sword"; - if (i == 6) fs = "shield"; - - for (std::map::const_iterator i = fileList.begin(); i != fileList.end(); ++i) - { - if (i->first.match(search)) - found[i->second.removeAll(m_server->getServerPath())] = fs; - } - } - - // Return a list of files found. - for (std::map::const_iterator i = found.begin(); i != found.end(); ++i) - { - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: File found (" << search << "): " << i->first << " [" << i->second << "]"); - } - - // No files found. - if (found.size() == 0) - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: No files found matching: " << search); - } - } - - return true; -} - -bool Player::msgPLI_RC_WARPPLAYER(CString& pPacket) -{ - if (isClient() || !hasRight(PLPERM_WARPTOPLAYER)) - { - if (isClient()) rclog.out("[Hack] %s attempted to warp a player.\n", m_accountName.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to warp players.\n"); - return true; - } - - auto p = m_server->getPlayer(pPacket.readGUShort(), PLTYPE_ANYPLAYER); - if (p == nullptr) return true; - - float loc[2] = { (float)(pPacket.readGChar()) / 2.0f, (float)(pPacket.readGChar()) / 2.0f }; - CString wLevel = pPacket.readString(""); - p->warp(wLevel, loc[0], loc[1]); - - rclog.out("%s has warped %s to %s (%.2f, %.2f)\n", m_accountName.text(), p->getAccountName().text(), wLevel.text(), loc[0], loc[1]); - return true; -} - -bool Player::msgPLI_RC_PLAYERRIGHTSGET(CString& pPacket) -{ - CString acc = pPacket.readString(""); - if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); - if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); - - if (isClient() || (acc != m_accountName && !hasRight(PLPERM_SETRIGHTS))) - { - if (isClient()) rclog.out("[Hack] %s attempted to get the rights of %s\n", m_accountName.text(), acc.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to view that player's rights."); - return true; - } - - // int rights = 0; - CString folders, ip; - - // Get player. - auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); - if (p == nullptr) - { - if (m_server->getAccountsFileSystem()->findi(CString(acc) << ".txt").isEmpty()) - return true; - - p = std::make_shared(nullptr, 0); - if (!p->loadAccount(acc)) - return true; - } - - // Get the folder list. - for (auto& folder: p->getFolderList()) - folders << folder << "\n"; - folders.gtokenizeI(); - - // Send the packet. - sendPacket(CString() >> (char)PLO_RC_PLAYERRIGHTSGET >> (char)acc.length() << acc >> (long long)p->getAdminRights() >> (char)p->getAdminIp().length() << p->getAdminIp() >> (short)folders.length() << folders); - - return true; -} - -bool Player::msgPLI_RC_PLAYERRIGHTSSET(CString& pPacket) -{ - CString acc = pPacket.readChars(pPacket.readGUChar()); - if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); - if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); - - if (isClient() || !hasRight(PLPERM_SETRIGHTS)) - { - if (isClient()) rclog.out("[Hack] %s attempted to set the rights of %s\n", m_accountName.text(), acc.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to set player rights."); - return true; - } - - // Get player. - auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); - if (p == nullptr) - { - if (m_server->getAccountsFileSystem()->findi(CString(acc) << ".txt").isEmpty()) - return true; - - p = std::make_shared(nullptr, 0); - if (!p->loadAccount(acc)) - return true; - } - - // Don't allow RCs to give rights that they don't have. - // Only affect people who don't have PLPERM_MODIFYSTAFFACCOUNT. - int n_adminRights = (int)pPacket.readGUInt5(); - if (!hasRight(PLPERM_MODIFYSTAFFACCOUNT)) - { - for (int i = 0; i < 20; ++i) - { - if ((m_adminRights & (1 << i)) == 0) - n_adminRights &= ~(1 << i); - } - } - - // Don't allow you to remove your own PLPERM_MODIFYSTAFFACCOUNT or PLPERM_SETRIGHTS. - if (p.get() == this) - { - if ((n_adminRights & PLPERM_MODIFYSTAFFACCOUNT) == 0) - n_adminRights |= PLPERM_MODIFYSTAFFACCOUNT; - if ((n_adminRights & PLPERM_SETRIGHTS) == 0) - n_adminRights |= PLPERM_SETRIGHTS; - } - - int changed_rights = m_adminRights ^ n_adminRights; - p->setAdminRights(n_adminRights); - p->setAdminIp(pPacket.readChars(pPacket.readGUChar())); - - // Untokenize and load the directories. - CString folders = pPacket.readChars(pPacket.readGUShort()); - folders.guntokenizeI(); - auto folderList = folders.tokenize("\n"); - - // Remove any invalid directories. - for (std::vector::iterator i = folderList.begin(); i != folderList.end();) - { - if ((*i).find(":") != -1 || (*i).find("..") != -1 || (*i).find(" /*") != -1) - i = folderList.erase(i); - else - ++i; - } - - p->setFolderRights(folderList); - - // Save the account. - p->saveAccount(); - - // If the account is currently on RC, reload it. - if (auto pRC = m_server->getPlayer(acc, PLTYPE_ANYRC | PLTYPE_NPCSERVER); pRC) - { - pRC->loadAccount(acc, true); - -#ifdef V8NPCSERVER - if (changed_rights & PLPERM_NPCCONTROL) - { - if (!(n_adminRights & PLPERM_NPCCONTROL)) - { - if (auto pNC = m_server->getPlayer(acc, PLTYPE_ANYNC); pNC) - pNC->disconnect(); - } - else - pRC->sendNCAddr(); - } -#endif - - // If they are using the File Browser, reload it. - if (pRC->isUsingFileBrowser()) - pRC->msgPLI_RC_FILEBROWSER_START(CString() << ""); - } - - rclog.out("%s has set the rights of %s\n", m_accountName.text(), acc.text()); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << m_accountName << " has set the rights of " << acc); - - return true; -} - -bool Player::msgPLI_RC_PLAYERCOMMENTSGET(CString& pPacket) -{ - CString acc = pPacket.readString(""); - if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); - if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); - - if (isClient()) - { - rclog.out("[Hack] %s attempted to get the comments of %s\n", m_accountName.text(), acc.text()); - return true; - } - - // Get player. - auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); - if (p == nullptr) - { - if (m_server->getAccountsFileSystem()->findi(CString(acc) << ".txt").isEmpty()) - return true; - - p = std::make_shared(nullptr, 0); - if (!p->loadAccount(acc)) - return true; - } - - sendPacket(CString() >> (char)PLO_RC_PLAYERCOMMENTSGET >> (char)acc.length() << acc << p->getComments()); - - return true; -} - -bool Player::msgPLI_RC_PLAYERCOMMENTSSET(CString& pPacket) -{ - CString acc = pPacket.readChars(pPacket.readGUChar()); - if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); - if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); - - if (isClient() || !hasRight(PLPERM_SETCOMMENTS)) - { - if (isClient()) rclog.out("[Hack] %s attempted to set the comments of %s\n", m_accountName.text(), acc.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to set player comments."); - return true; - } - - // Get player. - auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); - if (p == nullptr) - { - if (m_server->getAccountsFileSystem()->findi(CString(acc) << ".txt").isEmpty()) - return true; - - p = std::make_shared(nullptr, 0); - if (!p->loadAccount(acc)) - return true; - } - - CString comment = pPacket.readString(""); - p->setComments(comment); - p->saveAccount(); - - // If the account is currently on RC, reload it. - if (auto pRC = m_server->getPlayer(acc, PLTYPE_ANYRC); pRC) - { - pRC->loadAccount(acc); - } - - rclog.out("%s has set the comments of %s\n", m_accountName.text(), acc.text()); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << m_accountName << " has set the comments of " << acc); - - return true; -} - -bool Player::msgPLI_RC_PLAYERBANGET(CString& pPacket) -{ - CString acc = pPacket.readString(""); - if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); - if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); - - if (isClient()) - { - rclog.out("[Hack] %s attempted to view the ban of %s\n", m_accountName.text(), acc.text()); - return true; - } - - // Get player. - auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); - if (p == nullptr) - { - if (m_server->getAccountsFileSystem()->findi(CString(acc) << ".txt").isEmpty()) - return true; - - p = std::make_shared(nullptr, 0); - if (!p->loadAccount(acc)) - return true; - } - - sendPacket(CString() >> (char)PLO_RC_PLAYERBANGET >> (char)acc.length() << acc >> (char)(p->getBanned() ? 1 : 0) << p->getBanReason()); - - return true; -} - -bool Player::msgPLI_RC_PLAYERBANSET(CString& pPacket) -{ - CString acc = pPacket.readChars(pPacket.readGUChar()); - if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); - if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); - - if (isClient() || !hasRight(PLPERM_BAN)) - { - if (isClient()) rclog.out("[Hack] %s attempted to set the ban of %s\n", m_accountName.text(), acc.text()); - sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to set player bans."); - return true; - } - - // Get player. - auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); - if (p == nullptr) - { - if (m_server->getAccountsFileSystem()->findi(CString(acc) << ".txt").isEmpty()) - return true; - - p = std::make_shared(nullptr, 0); - if (!p->loadAccount(acc)) - return true; - } - - bool banned = (pPacket.readGUChar() == 0 ? false : true); - CString reason = pPacket.readString(""); - - p->setBanned(banned); - p->setBanReason(reason); - p->saveAccount(); - - // If the account is currently on RC, reload it. - if (auto pRC = m_server->getPlayer(acc, PLTYPE_ANYRC); pRC) - { - pRC->loadAccount(acc); - } - - // If the player was just now banned, kick him off the server. - if (banned && p->getId() != 0) - { - p->sendPacket(CString() >> (char)PLO_DISCMESSAGE << m_accountName << " has banned you. Reason: " << reason.guntokenize().replaceAll("\n", "\r")); - m_server->deletePlayer(p); - } - - rclog.out("%s has set the ban of %s\n", m_accountName.text(), acc.text()); - m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << m_accountName << " has set the ban of " << acc); - - return true; -} - -bool Player::msgPLI_RC_FILEBROWSER_START(CString& pPacket) -{ - if (isClient()) - { - rclog.out("[Hack] %s attempted to open the File Browser.\n", m_accountName.text()); - return true; - } - - // If the player has no folder rights, don't open the File Browser. - if (m_folderList.size() == 0) - return true; - - // Get folder list to send to the client. - CString folders; - for (std::vector::iterator i = m_folderList.begin(); i != m_folderList.end(); ++i) - folders << *i << "\n"; - - // Send the folder list and the welcome message. - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_DIRLIST << folders.gtokenize()); - if (!m_isFtp) sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Welcome to the File Browser."); - - // Create a folder map. - std::map folderMap; - for (std::vector::iterator i = m_folderList.begin(); i != m_folderList.end(); ++i) - { - CString rights("r"); - CString wild("*"); - CString folder(*i); - rights = folder.readString(" ").trim().toLower(); - folder.removeI(0, folder.readPos()); - folder.replaceAllI("\\", "/"); - folder.trimI(); - if (folder[folder.length() - 1] != '/') - { - int pos = folder.findl('/'); - if (pos != -1) - { - wild = folder.subString(pos + 1); - folder.removeI(pos + 1); - } - } - folderMap[folder] << rights << ":" << wild << "\n"; - } - - // See if we can use our lastFolder. If we can't, use the first folder. - if (folderMap.find(m_lastFolder) == folderMap.end()) - m_lastFolder = folderMap.begin()->first; - - // Create the file system. - FileSystem fs; - fs.addDir(m_lastFolder); - - // Construct the file list. - CString files; - std::vector wildcards = folderMap[m_lastFolder].tokenize("\n"); - for (std::vector::iterator i = wildcards.begin(); i != wildcards.end(); ++i) - { - CString rights = (*i).readString(":"); - CString wildcard = (*i).readString(""); - (*i).setRead(0); - for (std::map::iterator j = fs.getFileList().begin(); j != fs.getFileList().end(); ++j) - { - // See if the file matches the wildcard. - if (!j->first.match(wildcard)) - continue; - - CString name = j->first; - CString dir; - - // Add the file now. - int size = fs.getFileSize(j->first); - time_t mod = fs.getModTime(j->first); - dir >> (char)j->first.length() << j->first >> (char)rights.length() << rights >> (long long)size >> (long long)mod; - files << " " >> (char)dir.length() << dir; - } - } - - // Send packet. - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_DIR >> (char)m_lastFolder.length() << m_lastFolder << files); - m_isFtp = true; - - return true; -} - -bool Player::msgPLI_RC_FILEBROWSER_CD(CString& pPacket) -{ - if (isClient()) return true; - - CString newFolder = pPacket.readString(""); - CString newRights, wildcard; - newFolder.setRead(0); - - // Create a folder map. - std::map folderMap; - for (std::vector::iterator i = m_folderList.begin(); i != m_folderList.end(); ++i) - { - CString rights("r"); - CString wild("*"); - CString folder(*i); - rights = folder.readString(" ").trim().toLower(); - folder.removeI(0, folder.readPos()); - folder.replaceAllI("\\", "/"); - folder.trimI(); - if (folder[folder.length() - 1] != '/') - { - int pos = folder.findl('/'); - if (pos != -1) - { - wild = folder.subString(pos + 1); - folder.removeI(pos + 1); - } - } - folderMap[folder] << rights << ":" << wild << "\n"; - } - - // See if newFolder is part of the folder map. - // If it isn't, return. - if (folderMap.find(newFolder) == folderMap.end()) - return true; - else - m_lastFolder = newFolder; - - // Create the file system. - FileSystem fs; - fs.addDir(m_lastFolder); - - // Make sure our folder exists. - CString mkdir_path = m_server->getServerPath(); - std::vector f = m_lastFolder.tokenize('/'); - for (std::vector::iterator i = f.begin(); i != f.end(); ++i) - { - if (i->isEmpty()) continue; - mkdir_path << *i << '/'; -#if defined(_WIN32) || defined(_WIN64) - mkdir(mkdir_path.text()); +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +using PacketHandleFunc = HandlePacketResult (PlayerRC::*)(CString&); +using PacketHandleArray = std::array; + +static PacketHandleArray GeneratePacketHandlers() +{ + PacketHandleArray handlers{}; + handlers.fill(nullptr); + + handlers[PLI_RC_SERVEROPTIONSGET] = &PlayerRC::msgPLI_RC_SERVEROPTIONSGET; + handlers[PLI_RC_SERVEROPTIONSSET] = &PlayerRC::msgPLI_RC_SERVEROPTIONSSET; + handlers[PLI_RC_FOLDERCONFIGGET] = &PlayerRC::msgPLI_RC_FOLDERCONFIGGET; + handlers[PLI_RC_FOLDERCONFIGSET] = &PlayerRC::msgPLI_RC_FOLDERCONFIGSET; + handlers[PLI_RC_RESPAWNSET] = &PlayerRC::msgPLI_RC_RESPAWNSET; + handlers[PLI_RC_HORSELIFESET] = &PlayerRC::msgPLI_RC_HORSELIFESET; + handlers[PLI_RC_APINCREMENTSET] = &PlayerRC::msgPLI_RC_APINCREMENTSET; + handlers[PLI_RC_BADDYRESPAWNSET] = &PlayerRC::msgPLI_RC_BADDYRESPAWNSET; + handlers[PLI_RC_PLAYERPROPSGET] = &PlayerRC::msgPLI_RC_PLAYERPROPSGET; + handlers[PLI_RC_PLAYERPROPSSET] = &PlayerRC::msgPLI_RC_PLAYERPROPSSET; + handlers[PLI_RC_DISCONNECTPLAYER] = &PlayerRC::msgPLI_RC_DISCONNECTPLAYER; + handlers[PLI_RC_UPDATELEVELS] = &PlayerRC::msgPLI_RC_UPDATELEVELS; + handlers[PLI_RC_ADMINMESSAGE] = &PlayerRC::msgPLI_RC_ADMINMESSAGE; + handlers[PLI_RC_PRIVADMINMESSAGE] = &PlayerRC::msgPLI_RC_PRIVADMINMESSAGE; + handlers[PLI_RC_LISTRCS] = &PlayerRC::msgPLI_RC_LISTRCS; + handlers[PLI_RC_DISCONNECTRC] = &PlayerRC::msgPLI_RC_DISCONNECTRC; + handlers[PLI_RC_APPLYREASON] = &PlayerRC::msgPLI_RC_APPLYREASON; + handlers[PLI_RC_SERVERFLAGSGET] = &PlayerRC::msgPLI_RC_SERVERFLAGSGET; + handlers[PLI_RC_SERVERFLAGSSET] = &PlayerRC::msgPLI_RC_SERVERFLAGSSET; + handlers[PLI_RC_ACCOUNTADD] = &PlayerRC::msgPLI_RC_ACCOUNTADD; + handlers[PLI_RC_ACCOUNTDEL] = &PlayerRC::msgPLI_RC_ACCOUNTDEL; + handlers[PLI_RC_ACCOUNTLISTGET] = &PlayerRC::msgPLI_RC_ACCOUNTLISTGET; + handlers[PLI_RC_PLAYERPROPSGET2] = &PlayerRC::msgPLI_RC_PLAYERPROPSGET2; + handlers[PLI_RC_PLAYERPROPSGET3] = &PlayerRC::msgPLI_RC_PLAYERPROPSGET3; + handlers[PLI_RC_PLAYERPROPSRESET] = &PlayerRC::msgPLI_RC_PLAYERPROPSRESET; + handlers[PLI_RC_PLAYERPROPSSET2] = &PlayerRC::msgPLI_RC_PLAYERPROPSSET2; + handlers[PLI_RC_ACCOUNTGET] = &PlayerRC::msgPLI_RC_ACCOUNTGET; + handlers[PLI_RC_ACCOUNTSET] = &PlayerRC::msgPLI_RC_ACCOUNTSET; + handlers[PLI_RC_CHAT] = &PlayerRC::msgPLI_RC_CHAT; + handlers[PLI_RC_WARPPLAYER] = &PlayerRC::msgPLI_RC_WARPPLAYER; + handlers[PLI_RC_PLAYERRIGHTSGET] = &PlayerRC::msgPLI_RC_PLAYERRIGHTSGET; + handlers[PLI_RC_PLAYERRIGHTSSET] = &PlayerRC::msgPLI_RC_PLAYERRIGHTSSET; + handlers[PLI_RC_PLAYERCOMMENTSGET] = &PlayerRC::msgPLI_RC_PLAYERCOMMENTSGET; + handlers[PLI_RC_PLAYERCOMMENTSSET] = &PlayerRC::msgPLI_RC_PLAYERCOMMENTSSET; + handlers[PLI_RC_PLAYERBANGET] = &PlayerRC::msgPLI_RC_PLAYERBANGET; + handlers[PLI_RC_PLAYERBANSET] = &PlayerRC::msgPLI_RC_PLAYERBANSET; + handlers[PLI_RC_FILEBROWSER_START] = &PlayerRC::msgPLI_RC_FILEBROWSER_START; + handlers[PLI_RC_FILEBROWSER_CD] = &PlayerRC::msgPLI_RC_FILEBROWSER_CD; + handlers[PLI_RC_FILEBROWSER_END] = &PlayerRC::msgPLI_RC_FILEBROWSER_END; + handlers[PLI_RC_FILEBROWSER_DOWN] = &PlayerRC::msgPLI_RC_FILEBROWSER_DOWN; + handlers[PLI_RC_FILEBROWSER_UP] = &PlayerRC::msgPLI_RC_FILEBROWSER_UP; + handlers[PLI_NPCSERVERQUERY] = &PlayerRC::msgPLI_NPCSERVERQUERY; + handlers[PLI_RC_FILEBROWSER_MOVE] = &PlayerRC::msgPLI_RC_FILEBROWSER_MOVE; + handlers[PLI_RC_FILEBROWSER_DELETE] = &PlayerRC::msgPLI_RC_FILEBROWSER_DELETE; + handlers[PLI_RC_FILEBROWSER_RENAME] = &PlayerRC::msgPLI_RC_FILEBROWSER_RENAME; + handlers[PLI_RC_LARGEFILESTART] = &PlayerRC::msgPLI_RC_LARGEFILESTART; + handlers[PLI_RC_LARGEFILEEND] = &PlayerRC::msgPLI_RC_LARGEFILEEND; + handlers[PLI_RC_FOLDERDELETE] = &PlayerRC::msgPLI_RC_FOLDERDELETE; + handlers[PLI_RC_UNKNOWN162] = &PlayerRC::msgPLI_RC_UNKNOWN162; + + return handlers; +} + +/////////////////////////////////////////////////////////////////////////////// + +HandlePacketResult PlayerRC::handlePacket(std::optional id, CString& packet) +{ + static PacketHandleArray PacketHandlers = GeneratePacketHandlers(); + + auto handle = id.has_value() ? PacketHandlers[id.value()] : nullptr; + if (handle == nullptr) + return Player::handlePacket(id, packet); + + auto result = (this->*handle)(packet); + if (result == HandlePacketResult::Bubble) + return Player::handlePacket(id, packet); + + return result; +} + +/////////////////////////////////////////////////////////////////////////////// + +void PlayerRC::cleanup() +{ + Player::cleanup(); +} + +/////////////////////////////////////////////////////////////////////////////// + +bool PlayerRC::handleLogin(CString& pPacket) +{ + // Read Player-Ip + account.ipAddress = m_playerSock->getRemoteIp(); +#ifdef HAVE_INET_PTON + inet_pton(AF_INET, account.ipAddress.c_str(), &m_accountIp); #else - mkdir(mkdir_path.text(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); + m_accountIp = inet_addr(account.ipAddress.c_str()); #endif - } - - // Construct the file list. - // file packet: {CHAR name_length}{STRING name}{CHAR rights_length}{STRING rights}{INT5 file_size}{INT5 file_mod_time} - // files: {CHAR file_packet_length}{file_packet}[space]{CHAR file_packet_length}{file_packet}[space] - CString files; - std::vector wildcards = folderMap[m_lastFolder].tokenize("\n"); - for (std::vector::iterator i = wildcards.begin(); i != wildcards.end(); ++i) - { - CString rights = (*i).readString(":"); - CString wildcard = (*i).readString(""); - (*i).setRead(0); - for (std::map::iterator j = fs.getFileList().begin(); j != fs.getFileList().end(); ++j) - { - // See if the file matches the wildcard. - if (!j->first.match(wildcard)) - continue; - - CString name = j->first; - CString dir; - - // Add the file now. - int size = fs.getFileSize(j->first); - time_t mod = fs.getModTime(j->first); - dir >> (char)j->first.length() << j->first >> (char)rights.length() << rights >> (long long)size >> (long long)mod; - files << " " >> (char)dir.length() << dir; - } - } - - // Send packet. - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Folder changed to " << m_lastFolder); - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_DIR >> (char)m_lastFolder.length() << m_lastFolder << files); - - return true; -} - -bool Player::msgPLI_RC_FILEBROWSER_END(CString& pPacket) -{ - if (isClient()) return true; - m_isFtp = false; - - return true; -} - -bool Player::msgPLI_RC_FILEBROWSER_DOWN(CString& pPacket) -{ - if (isClient()) - { - rclog.out("[Hack] %s attempted to download a file through the File Browser.\n", m_accountName.text()); - return true; - } - - // Send file. - CString file = pPacket.readString(""); - CString filepath = m_server->getServerPath() << m_lastFolder << file; - CString checkFile = CString() << m_lastFolder << file; - - // Don't let us download/view important files. - if (!hasRight(PLPERM_MODIFYSTAFFACCOUNT)) - { - for (unsigned int j = 0; j < sizeof(__protectedFiles) / sizeof(const char*); ++j) - { - if (checkFile == CString(__protectedFiles[j])) - { - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Insufficient rights to download/view " << checkFile); - return true; - } - } - } - - this->sendFile(m_lastFolder, file); - - rclog.out("%s downloaded file %s\n", m_accountName.text(), file.text()); - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Downloaded file " << file); - - return true; -} - -bool Player::msgPLI_RC_FILEBROWSER_UP(CString& pPacket) -{ - if (isClient()) - { - rclog.out("[Hack] %s attempted to upload a file through the File Browser.\n", m_accountName.text()); - return true; - } - CString file = pPacket.readChars(pPacket.readGUChar()); - CString filepath = m_server->getServerPath() << m_lastFolder; - CString fileData = pPacket.subString(pPacket.readPos()); - CString checkFile = CString() << m_lastFolder << file; + // TODO(joey): Hijack type based on what graal sends, rather than use it directly. + m_type = (1 << pPacket.readGChar()); - // Check if this is a protected file. - bool isProtected = false; - int fileID = -1; - for (int i = 0; i < sizeof(__importantFiles) / sizeof(const char*); ++i) + // Set the encryptions. + log::print(log::server, "New login: "); + switch (m_type) { - if (checkFile == CString(__importantFiles[i])) - { - fileID = i; - isProtected = true; + case PLTYPE_RC: + log::printLine(log::server, "RC"); + Encryption.setGen(ENCRYPT_GEN_2); break; - } - } - - // If this file is protected, see if we have permission to upload this file. - bool hasPermission = true; - if (isProtected) - { - hasPermission = hasRight(PLPERM_MODIFYSTAFFACCOUNT); - if (!hasPermission) - { - if (fileID < (sizeof(__importantFileRights) / sizeof(const int))) - hasPermission = hasRight(__importantFileRights[fileID]); - } - } - - // Don't let us upload/overwrite important files. - if (isProtected && !hasPermission) - { - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Insufficent rights to upload " << checkFile); - return true; - } - - // See if we are uploading a large file or not. - if (m_rcLargeFiles.find(file) == m_rcLargeFiles.end()) - { - // Normal file. Save it and display our message. - fileData.save(filepath << file); - - rclog.out("%s uploaded file %s\n", m_accountName.text(), file.text()); - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Uploaded file " << file); - - // Update file. - updateFile(this, m_server, m_lastFolder, file); - } - else - { - // Large file. Store the data in memory. - m_rcLargeFiles[file] << fileData; - } - - return true; -} - -bool Player::msgPLI_RC_FILEBROWSER_MOVE(CString& pPacket) -{ - if (isClient()) - { - rclog.out("[Hack] %s attempted to move a file through the File Browser.\n", m_accountName.text()); - return true; - } - - CString source; - CString destination; - CString dir(pPacket.readChars(pPacket.readGUChar())); - CString file(pPacket.readString("")); - - // Fix file. - file.removeAllI("\""); - - // Add trailing directory slash if it is missing. - if (dir[dir.length() - 1] != '\\' && dir[dir.length() - 1] != '/') - dir << "/"; - - // Assemble destination and source. - destination << dir << file; - source << m_lastFolder << file; - - // Don't let us move important files. - for (unsigned int j = 0; j < sizeof(__importantFiles) / sizeof(const char*); ++j) - { - if (source == CString(__importantFiles[j])) - { - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Not allowed to move file " << source); - return true; - } - } - - rclog.out("%s moved file %s to %s\n", m_accountName.text(), source.text(), destination.text()); - - // Add working directory. - source = CString(m_server->getServerPath()) << source; - destination = CString(m_server->getServerPath()) << destination; - FileSystem::fixPathSeparators(source); - FileSystem::fixPathSeparators(destination); - - // Save the new file now. - CString temp; - temp.load(source); - if (temp.save(destination) == false) - return true; - - // Remove the old file. -#if defined(WIN32) || defined(WIN64) - wchar_t* wcstr = 0; - - // Determine if the filename is UTF-8 encoded. - int wcsize = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, source.text(), source.length(), 0, 0); - if (wcsize != 0) - { - wcstr = new wchar_t[wcsize + 1]; - memset((void*)wcstr, 0, (wcsize + 1) * sizeof(wchar_t)); - MultiByteToWideChar(CP_UTF8, 0, source.text(), source.length(), wcstr, wcsize); - } - else - { - wcstr = new wchar_t[source.length() + 1]; - for (int i = 0; i < source.length(); ++i) - wcstr[i] = (unsigned char)source[i]; - wcstr[source.length()] = 0; - } - - // Remove the file now. - _wremove(wcstr); - delete[] wcstr; -#else - remove(source.text()); -#endif - - return true; -} - -bool Player::msgPLI_RC_FILEBROWSER_DELETE(CString& pPacket) -{ - if (isClient()) - { - rclog.out("[Hack] %s attempted to delete a file through the File Browser.\n", m_accountName.text()); - return true; + case PLTYPE_NC: + log::printLine(log::server, "NC"); + Encryption.setGen(ENCRYPT_GEN_2); + break; + case PLTYPE_RC2: + log::printLine(log::server, "New RC (2.22+)"); + Encryption.setGen(ENCRYPT_GEN_5); + break; + default: + log::printLine(log::server, "Unknown ({})", m_type); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Your client type is unknown. Please inform the " << APP_VENDOR << " Team. Type: " << CString((int)m_type) << "."); + return false; } - CString file = pPacket.readString(""); - CString filePath = m_server->getServerPath() << m_lastFolder << file; - CString checkFile = CString() << m_lastFolder << file; - FileSystem::fixPathSeparators(filePath); - - // Don't let us delete important files. - for (unsigned int j = 0; j < sizeof(__importantFiles) / sizeof(const char*); ++j) + // Newer RC clients have an encryption key. + if (Encryption.getGen() > ENCRYPT_GEN_3) { - if (checkFile == CString(__importantFiles[j])) - { - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Not allowed to delete file " << checkFile); - return true; - } - } + m_encryptionKey = (unsigned char)pPacket.readGChar(); -#if defined(WIN32) || defined(WIN64) - wchar_t* wcstr = 0; - - // Determine if the filename is UTF-8 encoded. - int wcsize = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, filePath.text(), filePath.length(), 0, 0); - if (wcsize != 0) - { - wcstr = new wchar_t[wcsize + 1]; - memset((void*)wcstr, 0, (wcsize + 1) * sizeof(wchar_t)); - MultiByteToWideChar(CP_UTF8, 0, filePath.text(), filePath.length(), wcstr, wcsize); - } - else - { - wcstr = new wchar_t[filePath.length() + 1]; - for (int i = 0; i < filePath.length(); ++i) - wcstr[i] = (unsigned char)filePath[i]; - wcstr[filePath.length()] = 0; + Encryption.reset(m_encryptionKey); + if (Encryption.getGen() > ENCRYPT_GEN_3) + m_fileQueue.setCodec(Encryption.getGen(), m_encryptionKey); } - // Remove the file now. - _wremove(wcstr); - delete[] wcstr; -#else - remove(filePath.text()); -#endif + // Read Client-Version + m_version = pPacket.readChars(8); + m_versionId = getVersionIDByVersion(m_version); - rclog.out("%s deleted file %s\n", m_accountName.text(), file.text()); - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Deleted file " << file); + // Read Account & Password + account.name = pPacket.readChars(pPacket.readGUChar()).toString(); + CString password = pPacket.readChars(pPacket.readGUChar()); - return true; -} + // Client Identity: win,"",02e2465a2bf38f8a115f6208e9938ac8,ff144a9abb9eaff4b606f0336d6d8bc5,"6.2 9200 " + // {platform}, {mobile provides 'dc:id2'}, {md5hash:harddisk-id}, {md5hash:network-id}, {uname(release, version)}, {android-id} + CString identity = pPacket.readString(""); -bool Player::msgPLI_RC_FILEBROWSER_RENAME(CString& pPacket) -{ - if (isClient()) { - rclog.out("[Hack] %s attempted to rename a file through the File Browser.\n", m_accountName.text()); - return true; - } + auto indent = log::server.indent(); - CString f1 = pPacket.readChars(pPacket.readGUChar()); - CString f2 = pPacket.readChars(pPacket.readGUChar()); - CString f1path = m_server->getServerPath() << m_lastFolder << f1; - CString f2path = m_server->getServerPath() << m_lastFolder << f2; - CString checkFile1 = CString() << m_lastFolder << f1; - CString checkFile2 = CString() << m_lastFolder << f2; - FileSystem::fixPathSeparators(f1path); - FileSystem::fixPathSeparators(f2path); - - // Don't let us rename/overwrite important files. - for (unsigned int j = 0; j < sizeof(__importantFiles) / sizeof(const char*); ++j) - { - if (checkFile1 == CString(__importantFiles[j])) + //log::printLine(log::server, "Key: {}", key); + log::printLine(log::server, "Version: {} ({})", m_version, getVersionString(m_version, m_type)); + log::printLine(log::server, "Account: {}", account.name); + if (!identity.isEmpty()) { - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Not allowed to rename/overwrite file " << checkFile1 << " or " << checkFile2); - return true; + log::printLine(log::server, "Identity: {}", identity); + auto identityTokens = identity.tokenize(",", true); + m_os = identityTokens[0]; } } - // Renaming our logs? First, we need to close them. - if (m_lastFolder == "logs/") - { - if (f1 == "rclog.txt") rclog.close(); - else if (f1 == "serverlog.txt") - serverlog.close(); - } - - // Do the renaming. -#if defined(WIN32) || defined(WIN64) - // Convert to wchar_t because the filename might be UTF-8 encoded. - wchar_t* f1_wcstr = 0; - wchar_t* f2_wcstr = 0; - - // Test f1path. - int f1_wcsize = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, f1path.text(), f1path.length(), 0, 0); - if (f1_wcsize != 0) - { - f1_wcstr = new wchar_t[f1_wcsize + 1]; - memset((void*)f1_wcstr, 0, (f1_wcsize + 1) * sizeof(wchar_t)); - MultiByteToWideChar(CP_UTF8, 0, f1path.text(), f1path.length(), f1_wcstr, f1_wcsize); - } - else - { - f1_wcstr = new wchar_t[f1path.length() + 1]; - for (int i = 0; i < f1path.length(); ++i) - f1_wcstr[i] = (unsigned char)f1path[i]; - f1_wcstr[f1path.length()] = 0; - } - - // Test f2path. - int f2_wcsize = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, f2path.text(), f2path.length(), 0, 0); - if (f2_wcsize != 0) - { - f2_wcstr = new wchar_t[f2_wcsize + 1]; - memset((void*)f2_wcstr, 0, (f2_wcsize + 1) * sizeof(wchar_t)); - MultiByteToWideChar(CP_UTF8, 0, f2path.text(), f2path.length(), f2_wcstr, f2_wcsize); - } - else + // Check for available slots on the server. + if (m_server->getPlayerList().size() >= m_server->cached.maxPlayers.getValue()) { - f2_wcstr = new wchar_t[f2path.length() + 1]; - for (int i = 0; i < f2path.length(); ++i) - f2_wcstr[i] = f2path[i]; - f2_wcstr[f2path.length()] = 0; + log::printLine(log::rc, "** [Disconnect] '{}': Server is full.", account.name); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "This server has reached its player limit."); + return false; } - // Rename. - _wrename(f1_wcstr, f2_wcstr); - delete[] f1_wcstr; - delete[] f2_wcstr; -#else - // Linux uses UTF-8 filenames. - rename(f1path.text(), f2path.text()); -#endif - - // Renaming our logs? We can open them now. - if (m_lastFolder == "logs/") + // Verify login details with the serverlist. + // TODO: localhost mode. + if (!m_server->getServerList().getConnected()) { - if (f1 == "rclog.txt") rclog.open(); - else if (f1 == "serverlog.txt") - serverlog.open(); + sendPacket(CString() >> (char)PLO_DISCMESSAGE << "The login server is offline. Try again later."); + return false; } - rclog.out("%s renamed file %s to %s\n", m_accountName.text(), f1.text(), f2.text()); - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Renamed file " << f1 << " to " << f2); - + m_server->getServerList().sendLoginPacketForPlayer(shared_from_this(), password, identity); return true; } -bool Player::msgPLI_RC_LARGEFILESTART(CString& pPacket) +bool PlayerRC::sendLogin() { - if (isClient()) - { - rclog.out("[Hack] %s is attempting to upload a file through the File Browser.\n", m_accountName.text()); - return true; - } - - CString file = pPacket.readString(""); - m_rcLargeFiles[file] = CString(); + if (Player::sendLogin() == false) + return false; - return true; -} + auto& settings = m_server->getSettings(); -bool Player::msgPLI_RC_LARGEFILEEND(CString& pPacket) -{ - if (isClient()) - { - rclog.out("[Hack] %s attempted to upload a file through the File Browser.\n", m_accountName.text()); - return true; - } + // This packet clears the players weapons on the client, but official + // also sends it to the RC's so we are maintaining the same behavior + sendPacket(CString() >> (char)PLO_CLEARWEAPONS); - CString file = pPacket.readString(""); - CString filepath = m_server->getServerPath() << m_lastFolder << file; + // If no nickname was specified, set the nickname to the account name. + if (account.character.nickName.empty()) + account.character.nickName = std::format("*{}", account.name); + account.level = " "; - // Save the file. - m_rcLargeFiles[file].save(filepath); + // Set the head to the server's set staff head. + account.character.headImage = m_server->getSettings().get("staffhead").value_or("head25.png"); - // Remove the data from memory. - for (std::map::iterator i = m_rcLargeFiles.begin(); i != m_rcLargeFiles.end(); ++i) - { - if (i->first == file) - { - m_rcLargeFiles.erase(i); - break; - } - } + // Send the RC join message to the RC. + std::vector rcmessage = CString::loadToken("config/rcmessage.txt", "\n", true); + for (const auto& i : rcmessage) + sendPacket(CString() >> (char)PLO_RC_CHAT << i); - // Update file. - updateFile(this, m_server, m_lastFolder, file); + // Tell the client if the server is connected to the listserver. + if (m_server->getServerList().getConnected()) + sendPacket(CString() >> (char)PLO_SERVERLISTCONNECTED); - rclog.out("%s uploaded large file %s\n", m_accountName.text(), file.text()); - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Uploaded large file " << file); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "New RC: " << account.name); - return true; -} + // Send out what guilds should be placed in the Staff section of the playerlist. + CString guildPacket = CString() >> (char)PLO_STAFFGUILDS; + for (const auto& guild : string::split(settings.get("staffguilds").value_or(""), ","sv)) + guildPacket << "\"" << string::trim(guild) << "\","; + sendPacket(guildPacket.remove(guildPacket.length() - 1, 1)); -bool Player::msgPLI_RC_FOLDERDELETE(CString& pPacket) -{ - CString folder = pPacket.readString(""); - CString folderpath = m_server->getServerPath() << folder; - FileSystem::fixPathSeparators(folderpath); - folderpath.removeI(folderpath.length() - 1); - if (isClient()) + // Send out the server's available status list options. { - rclog.out("[Hack] %s attempted to delete a folder through the File Browser: %s\n", m_accountName.text(), folder.text()); - return true; - } + // graal doesn't quote these + CString pliconPacket = CString() >> (char)PLO_STATUSLIST; + for (const auto& status : m_server->cached.playerStatusList.getValue()) + pliconPacket << string::trim(status) << ","; - // Try to remove folder. - if (rmdir(folderpath.text())) - { - perror("Error removing folder"); - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Error removing " << folder << ". Folder may not exist or may not be empty."); - return true; + sendPacket(pliconPacket.remove(pliconPacket.length() - 1, 1)); } - rclog.out("%s removed folder %s\n", m_accountName.text(), folder.text()); - sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Folder " << folder << " has been removed.\n"); - msgPLI_RC_FILEBROWSER_START(CString() << ""); + // This comes after status icons for RC + sendPacket(CString() >> (char)PLO_RC_MAXUPLOADFILESIZE >> (long long)(1048576 * 20)); - return true; -} + // Then during iterating the playerlist to send players to the rc client, it sends addplayer followed by rc chat per person. + // TODO: Was this unimplemented? -bool Player::msgPLI_NPCSERVERQUERY(CString& pPacket) -{ -#ifdef V8NPCSERVER - // Read Packet Data - unsigned short pid = pPacket.readGUShort(); - CString message = pPacket.readString(""); + // Exchange props with everybody on the server. + exchangeMyPropsWithOthers(); - // Enact upon the message. - if (message == "location") - sendNCAddr(); -#endif + // If we are an RC, announce the list of currently logged in RCs. + CString rcsOnline; + for (const auto& [pid, player] : players_of_type(m_server->getPlayerList())) + rcsOnline << (rcsOnline.isEmpty() ? "" : ", ") << player->account.name; + if (!rcsOnline.isEmpty()) + sendPacket(CString() >> (char)PLO_RC_CHAT << "Currently online: " << rcsOnline); return true; } -void updateFile(Player* player, Server* server, CString& dir, CString& file) -{ - auto& settings = server->getSettings(); - CString fullPath(dir); - fullPath << file; - - // Find the file extension. - CString ext = getExtension(file); - - // Check and see if it is an account. - if (dir == "accounts/") - { - FileSystem* fs = server->getAccountsFileSystem(); - if (fs->find(file).isEmpty()) - fs->addFile(CString() << dir << file); - return; - } - - bool isNewFile = false; - - // If folder config is off, add it to the file list. - if (settings.getBool("nofoldersconfig", false)) - { - FileSystem* fs = server->getFileSystem(); - if (fs->find(file).isEmpty()) - { - fs->addFile(CString() << dir << file); - isNewFile = true; - } - } - // If folder config is on, try to find which file system to add it to. - else - { - std::vector foldersConfig = CString::loadToken(server->getServerPath() << "config/foldersconfig.txt", "\n", true); - for (auto& folderConfig: foldersConfig) - { - CString type = folderConfig.readString(" ").trim(); - CString folder("world/"); - folder << folderConfig.readString("").trim(); - - if (fullPath.match(folder)) - { - FileSystem* fs = server->getFileSystemByType(type); - FileSystem* fs2 = server->getFileSystem(); - - // See if it exists in that file system. - if (fs->find(file).isEmpty()) - { - if (fs2->find(file).isEmpty()) - fs2->addFile(fullPath); - - fs->addFile(fullPath); - isNewFile = true; - //printf("adding %s to %s\n", file.text(), type.text()); - break; - } - } - } - } - - // If it is a level, see if we can update it. - // TODO: Should combine all server options loading/saving into one function in Server. - if (ext == ".nw" || ext == ".graal" || ext == ".zelda") - { - auto l = Level::findLevel(file, server); - if (l) l->reload(); - } - else if (ext == ".gupd") - server->getPackageManager().findOrAddResource(file.text())->reload(server); - else if (ext == ".dump" || dir.findi(CString("weapons")) > -1) - server->loadWeapons(true); - else if (file == "serveroptions.txt") - { - server->loadSettings(); - server->loadMaps(); - } - else if (file == "adminconfig.txt") - server->loadAdminSettings(); - else if (file == "allowedversions.txt") - server->loadAllowedVersions(); - else if (file == "foldersconfig.txt") - server->loadFileSystem(); - else if (file == "serverflags.txt") - server->loadServerFlags(); - else if (file == "servermessage.html") - server->loadServerMessage(); - else if (file == "ipbans.txt") - server->loadIPBans(); - else if (file == "rules.txt") - server->loadWordFilter(); - else - { - // Check if this is a file that previously existed on the server so we - // can notify existing clients that the file was updated. - auto fileSystem = server->getFileSystem(FS_FILE); - if (!isNewFile && !fileSystem->find(file).isEmpty()) - { - // Game files - const auto& playerList = server->getPlayerList(); - auto fileName = file.toString(); - - CString updatePacket; - updatePacket >> (char)PLO_UPDATEPACKAGEISUPDATED << file << "\n"; - - // Ganis need to be recompiled on update - CString bytecodePacket; - if (ext == ".gani") - { - auto& aniManager = server->getAnimationManager(); - - // delete the resource - aniManager.deleteResource(fileName); - - // reload the resource to compile the bytecode again - auto findAni = aniManager.findOrAddResource(fileName); - if (findAni) - bytecodePacket << findAni->getBytecodePacket(); - } - - // Send the update packet to any v4+ clients that have seen this file - for (auto& [pid, pl]: playerList) - { - if (pl->isClient() && pl->getVersion() >= CLVER_4_0211) - { - if (pl->hasSeenFile(fileName)) - pl->sendPacket(updatePacket); - - // Send GS2 gani scripts - if (!bytecodePacket.isEmpty()) - pl->sendPacket(bytecodePacket); - } - } - } - } -} +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/player/PlayerRequestText.cpp b/server/src/player/PlayerRequestText.cpp index 3fd3becc2..c09c03985 100644 --- a/server/src/player/PlayerRequestText.cpp +++ b/server/src/player/PlayerRequestText.cpp @@ -1,8 +1,21 @@ -#include "FileSystem.h" -#include "Player.h" -#include "Server.h" +#include -bool Player::msgPLI_REQUESTTEXT(CString& pPacket) +#include +#include + +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +HandlePacketResult Player::msgPLI_REQUESTTEXT(CString& pPacket) { // TODO(joey): So I believe these are just requests for information, while sendtext is used to actually do things. @@ -18,79 +31,79 @@ bool Player::msgPLI_REQUESTTEXT(CString& pPacket) { if (option == "simplelist") list.sendPacket(CString() >> (char)SVO_REQUESTLIST >> (short)m_id << CString(weapon << "\n" - << type << "\n" - << "simpleserverlist" - << "\n") - .gtokenizeI()); + << type << "\n" + << "simpleserverlist" + << "\n") + .gtokenizeI()); else if (option == "rebornlist") list.sendPacket(CString() >> (char)SVO_REQUESTLIST >> (short)m_id << packet); else if (option == "subscriptions") { // some versions of the loginserver scripts expected the response of subscriptions2 rather than subscriptions sendPacket(CString() >> (char)PLO_SERVERTEXT << CString(CString() << weapon << "\n" - << type << "\n" - << "subscriptions" - << "\n" - << CString(CString() << "unlimited" - << "\n" - << "Unlimited Subscription" - << "\n" - << "\"\"" - << "\n") - .gtokenizeI()) - .gtokenizeI()); + << type << "\n" + << "subscriptions" + << "\n" + << CString(CString() << "unlimited" + << "\n" + << "Unlimited Subscription" + << "\n" + << "\"\"" + << "\n") + .gtokenizeI()) + .gtokenizeI()); } else if (option == "bantypes") sendPacket(CString() >> (char)PLO_SERVERTEXT << packet << ",\"\"\"Event Interruption\"\",259200\",\"\"\"Message Code Abuse\"\",259200\",\"\"\"General Scamming\"\",604800\",\"Advertising,604800\",\"\"\"General Harassment\"\",604800\",\"\"\"Racism or Severe Vulgarity\"\",1209600\",\"\"\"Sexual Harassment\"\",1209600\",\"Cheating,2592000\",\"\"\"Advertising Money Trade\"\",2592000\",\"\"\"Ban Evasion\"\",2592000\",\"\"\"Speed Hacking\"\",2592000\",\"\"\"Bug Abuse\"\",2592000\",\"\"\"Multiple Jailings\"\",2592000\",\"\"\"Server Destruction\"\",3888000\",\"\"\"Leaking Information\"\",3888000\",\"\"\"Account Scam\"\",7776000\",\"\"\"Account Sharing\"\",315360000\",\"Hacking,315360000\",\"\"\"Multiple Bans\"\",315360000\",\"\"\"Other Unlimited\"\",315360001\""); else if (option == "getglobalitems") sendPacket(CString() >> (char)PLO_SERVERTEXT << CString(weapon << "\n" - << type << "\n" - << "globalitems" - << "\n" - << m_accountName.text() << "\n" - << CString(CString(CString() << "autobill=1" - << "\n" - << "autobillmine=1" - << "\n" - << "bundle=1" - << "\n" - << "creationtime=1212768763" - << "\n" - << "currenttime=1353248504" - << "\n" - << "description=Gives" - << "\n" - << "duration=2629800" - << "\n" - << "flags=subscription" - << "\n" - << "icon=graalicon_big.png" - << "\n" - << "itemid=1" - << "\n" - << "lifetime=1" - << "\n" - << "owner=global" - << "\n" - << "ownertype=server" - << "\n" - << "price=100" - << "\n" - << "quantity=988506" - << "\n" - << "status=available" - << "\n" - << "title=Gold" - << "\n" - << "tradable=1" - << "\n" - << "typeid=62" - << "\n" - << "world=global" - << "\n") - .gtokenizeI()) - .gtokenizeI()) - .gtokenizeI()); + << type << "\n" + << "globalitems" + << "\n" + << account.name << "\n" + << CString(CString(CString() << "autobill=1" + << "\n" + << "autobillmine=1" + << "\n" + << "bundle=1" + << "\n" + << "creationtime=1212768763" + << "\n" + << "currenttime=1353248504" + << "\n" + << "description=Gives" + << "\n" + << "duration=2629800" + << "\n" + << "flags=subscription" + << "\n" + << "icon=graalicon_big.png" + << "\n" + << "itemid=1" + << "\n" + << "lifetime=1" + << "\n" + << "owner=global" + << "\n" + << "ownertype=server" + << "\n" + << "price=100" + << "\n" + << "quantity=988506" + << "\n" + << "status=available" + << "\n" + << "title=Gold" + << "\n" + << "tradable=1" + << "\n" + << "typeid=62" + << "\n" + << "world=global" + << "\n") + .gtokenizeI()) + .gtokenizeI()) + .gtokenizeI()); else if (option == "serverinfo") { list.sendPacket(CString() >> (char)SVO_REQUESTSVRINFO >> (short)m_id << packet); @@ -111,18 +124,18 @@ bool Player::msgPLI_REQUESTTEXT(CString& pPacket) { if (const auto updatePackage = m_server->getPackageManager().findOrAddResource(option.text())) sendPacket(CString() >> (char)PLO_SERVERTEXT << CString(weapon << "\n" - << type << "\n" - << option << "\n" - << /* File count */ CString(updatePackage->getFileList().size()) << "\n" - << /* Total size in bytes */ CString(updatePackage->getPackageSize()) << "\n") - .gtokenizeI()); + << type << "\n" + << option << "\n" + << /* File count */ CString(updatePackage->getFileList().size()) << "\n" + << /* Total size in bytes */ CString(updatePackage->getPackageSize()) << "\n") + .gtokenizeI()); } - m_server->getServerLog().out("[ IN] [RequestText] from %s -> %s\n", m_accountName.gtokenize().text(), packet.text()); - return true; + log::printLine(log::server, "[ IN] [RequestText] from {} -> {}", string::toCSV(account.name), packet); + return HandlePacketResult::Handled; } -bool Player::msgPLI_SENDTEXT(CString& pPacket) +HandlePacketResult Player::msgPLI_SENDTEXT(CString& pPacket) { CString packet = pPacket.readString(""); CString data = packet.guntokenize(); @@ -151,10 +164,10 @@ bool Player::msgPLI_SENDTEXT(CString& pPacket) if (isRC()) { // Irc players start at 16k - sendPacket(CString() >> (char)PLO_ADDPLAYER >> (short)(16000 + 0) >> (char)channelAccount.length() << channelAccount >> (char)PLPROP_NICKNAME >> (char)channelNick.length() << channelNick >> (char)PLPROP_UNKNOWN81 >> (char)3); + sendPacket(CString() >> (char)PLO_ADDPLAYER >> (short)(16000 + 0) >> (char)channelAccount.length() << channelAccount >> (char)PlayerProp::NICKNAME >> (char)channelNick.length() << channelNick >> (char)PlayerProp::PLAYERLISTCATEGORY >> (char)PlayerListCategory::EXTERNAL); } else - sendPacket(CString() >> (char)PLO_OTHERPLPROPS >> (short)(16000 + 0) >> (char)PLPROP_ACCOUNTNAME >> (char)channelAccount.length() << channelAccount >> (char)PLPROP_NICKNAME >> (char)channelNick.length() << channelNick >> (char)PLPROP_UNKNOWN81 >> (char)3); + sendPacket(CString() >> (char)PLO_OTHERPLPROPS >> (short)(16000 + 0) >> (char)PlayerProp::ACCOUNTNAME >> (char)channelAccount.length() << channelAccount >> (char)PlayerProp::NICKNAME >> (char)channelNick.length() << channelNick >> (char)PlayerProp::PLAYERLISTCATEGORY >> (char)PlayerListCategory::EXTERNAL); } else if (params.size() > 3) { @@ -189,7 +202,7 @@ bool Player::msgPLI_SENDTEXT(CString& pPacket) if (params3[0] == "!getserverinfo") { //list->sendPacket(CString() >> (char)SVO_REQUESTSVRINFO >> (short)id << weapon << ",irc,privmsg," << params3[1].gtokenize()); - m_server->getServerLog().out("[ IN] [SVO_SERVERINFO] %s,%s\n", m_accountName.gtokenize().text(), packet.text()); + log::printLine(log::server, "[ IN] [SVO_SERVERINFO] {},{}", string::toCSV(account.name), packet); //list->sendPacket(CString() >> (char)SVO_SERVERINFO >> (short)id << params3[1]); // <-- this solves it for now // I believe the following data is what it's looking for: @@ -199,7 +212,7 @@ bool Player::msgPLI_SENDTEXT(CString& pPacket) else { CString sendMsg = "GraalEngine,irc,privmsg,"; - sendMsg << m_accountName << "," << channel.gtokenize() << "," << msg.gtokenize(); + sendMsg << account.name << "," << channel.gtokenize() << "," << msg.gtokenize(); list.handleText(sendMsg); list.sendTextForPlayer(shared_from_this(), sendMsg); } @@ -211,7 +224,7 @@ bool Player::msgPLI_SENDTEXT(CString& pPacket) if (option == "serverinfo") list.sendPacket(CString() >> (char)SVO_REQUESTSVRINFO >> (short)m_id << packet); - if (!getGuest()) + if (!isGuest()) { if (option == "verifybuddies" || option == "addbuddy" || option == "deletebuddy") list.sendTextForPlayer(shared_from_this(), packet); @@ -228,15 +241,17 @@ bool Player::msgPLI_SENDTEXT(CString& pPacket) if (option == "getban") { // Send param is computer id. Either 0, or the id. It is required though - sendPacket(CString() >> (char)PLO_SERVERTEXT << "GraalEngine,lister,ban," << params[0] << "," - << "0"); + sendPacket(CString() >> (char)PLO_SERVERTEXT << "GraalEngine,lister,ban," << params[0] << "," << "0"); //msgPLI_RC_PLAYERBANGET(params[0]); } } } } - m_server->getServerLog().out("[ IN] [SendText] %s: %s\n", m_accountName.gtokenize().text(), packet.text()); + log::printLine(log::server, "[ IN] [SendText] {}: {}", string::toCSV(account.name), packet); - return true; + return HandlePacketResult::Handled; } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/player/PlayerScripts.cpp b/server/src/player/PlayerScripts.cpp deleted file mode 100644 index 5e7671907..000000000 --- a/server/src/player/PlayerScripts.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "FileSystem.h" -#include "Player.h" -#include "Server.h" -#include "Weapon.h" -#include "utilities/StringUtils.h" - -// packet 157 -bool Player::msgPLI_UPDATEGANI(CString& pPacket) -{ - // Read packet data - uint32_t checksum = pPacket.readGUInt5(); - std::string gani = pPacket.readString("").toString(); - const std::string ganiFile = gani + ".gani"; - - // Try to find the animation in memory or on disk - auto findAni = m_server->getAnimationManager().findOrAddResource(ganiFile); - if (!findAni) - { - //printf("Client requested gani %s, but was not found\n", ganiFile.c_str()); - return true; - } - - // Compare the bytecode checksum from the client with the one for the - // current script, if it doesn't match send the updated bytecode - if (calculateCrc32Checksum(findAni->getByteCode()) != checksum) - sendPacket(findAni->getBytecodePacket()); - - // v4 and up needs this for some reason. - sendPacket(CString() >> (char)PLO_UNKNOWN195 >> (char)gani.length() << gani << "\"SETBACKTO " << findAni->getSetBackTo() << "\""); - return true; -} - -bool Player::msgPLI_UPDATESCRIPT(CString& pPacket) -{ - CString weaponName = pPacket.readString(""); - - m_server->getServerLog().out("PLI_UPDATESCRIPT: \"%s\"\n", weaponName.text()); - - CString out; - - auto weaponObj = m_server->getWeapon(weaponName.toString()); - if (weaponObj != nullptr) - { - CString b = weaponObj->getByteCode(); - out >> (char)PLO_RAWDATA >> (int)b.length() << "\n"; - out >> (char)PLO_NPCWEAPONSCRIPT << b; - - sendPacket(out); - } - - return true; -} - -bool Player::msgPLI_UPDATECLASS(CString& pPacket) -{ - // Get the packet data and file mod time. - time_t modTime = pPacket.readGInt5(); - std::string className = pPacket.readString("").toString(); - - m_server->getServerLog().out("PLI_UPDATECLASS: \"%s\"\n", className.c_str()); - - ScriptClass* classObj = m_server->getClass(className); - - if (classObj != nullptr) - { - CString out; - out >> (char)PLO_RAWDATA >> (int)classObj->getByteCode().length() << "\n"; - out >> (char)PLO_NPCWEAPONSCRIPT << classObj->getByteCode(); - sendPacket(out); - } - else - { - std::vector headerData; - headerData.push_back("class"); - headerData.push_back(className); - headerData.push_back('1'); - headerData.push_back(CString() >> (long long)0 >> (long long)0); - headerData.push_back(CString() >> (long long)0); - - CString gstr = utilities::retokenizeCStringArray(headerData); - - // Should technically be PLO_UNKNOWN197 but for some reason the client breaks player.join() scripts - // if a weapon decides to request an class that doesnt exist on the server. This seems to fix it by - // sending an empty bytecode - sendPacket(CString() >> (char)PLO_NPCWEAPONSCRIPT >> (short)gstr.length() << gstr); - } - - return true; -} diff --git a/server/src/player/PlayerUpdatePackages.cpp b/server/src/player/PlayerUpdatePackages.cpp deleted file mode 100644 index 6e0407563..000000000 --- a/server/src/player/PlayerUpdatePackages.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "FileSystem.h" -#include "Player.h" -#include "Server.h" -#include "UpdatePackage.h" - -bool Player::msgPLI_VERIFYWANTSEND(CString& pPacket) -{ - unsigned long fileChecksum = pPacket.readGUInt5(); - CString fileName = pPacket.readString(""); - - // There is a USECHECKSUM flag in the config, and im pretty - // certain it works similar to this: By always sending the - // update package the client will respond with another request - // including the crc32 hashes of all the files in the package - bool ignoreChecksum = false; - if (getExtension(fileName) == ".gupd") - ignoreChecksum = true; - - if (!ignoreChecksum) - { - CString fileData = m_server->getFileSystem()->load(fileName); - if (!fileData.isEmpty()) - { - if (calculateCrc32Checksum(fileData) == fileChecksum) - { - sendPacket(CString() >> (char)PLO_FILEUPTODATE << fileName); - return true; - } - } - } - - // Send the file to the client - this->sendFile(fileName); - return true; -} - -bool Player::msgPLI_UPDATEPACKAGEREQUESTFILE(CString& pPacket) -{ - CString packageName = pPacket.readChars(pPacket.readGUChar()); - - // 1 -> Install, 2 -> Reinstall - unsigned char installType = pPacket.readGUChar(); - CString fileChecksums = pPacket.readString(""); - - // If this is a reinstall, we need to download everything so clear the checksum data - if (installType == 2) - fileChecksums.clear(); - - auto totalDownloadSize = 0; - std::vector missingFiles; - - { - auto updatePackage = m_server->getPackageManager().findOrAddResource(packageName.toString()); - if (updatePackage) - { - for (const auto& [fileName, entry]: updatePackage->getFileList()) - { - // Compare the checksum for each file entry if the checksum is provided - bool needsFile = true; - if (fileChecksums.bytesLeft() >= 5) - { - uint32_t userFileChecksum = fileChecksums.readGUInt5(); - if (entry.checksum == userFileChecksum) - needsFile = false; - } - - if (needsFile) - { - totalDownloadSize += entry.size; - missingFiles.push_back(fileName); - } - } - } - } - - sendPacket(CString() >> (char)PLO_UPDATEPACKAGESIZE >> (char)packageName.length() << packageName >> (long long)totalDownloadSize); - - for (const auto& wantFile: missingFiles) - this->sendFile(wantFile); - - sendPacket(CString() >> (char)PLO_UPDATEPACKAGEDONE << packageName); - - m_fileQueue.sendCompress(true); - return true; -} diff --git a/server/src/player/packets/PlayerClientPackets.cpp b/server/src/player/packets/PlayerClientPackets.cpp new file mode 100644 index 000000000..a8a67a76b --- /dev/null +++ b/server/src/player/packets/PlayerClientPackets.cpp @@ -0,0 +1,1584 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +HandlePacketResult PlayerClient::msgPLI_LEVELWARP(CString& pPacket) +{ + std::optional modTime; + + if (pPacket[0] - 32 == PLI_LEVELWARPMOD) + modTime = clock::from_time_t((time_t)pPacket.readGUInt5()); + + LocalPixelPosition pos = { static_cast(pPacket.readGChar() * 8), static_cast(pPacket.readGChar() * 8) }; + CString newLevelC = pPacket.readString(""); + std::string_view newLevel = newLevelC.toStringView(); + + bool success = false; + + // If this is a gmap, in order to prevent the client from glitching out, forcefully warp them to the gmap. + if (newLevel.ends_with(".gmap")) + { + success = warp(newLevel, toPixelPosition({ 0, 0 }, pos), modTime); + } + else + { + if (auto level = m_server->getLoadedLevel(newLevel, shared_from_this()); level != nullptr) + { + // If this level is part of a gmap, send the static data first, then warp second (so the appear on the correct level). + if (level->isGmap() && level->levelName != newLevel) + { + // Send the static data first. + auto subLevel = level->getSubLevelByName(newLevel); + success = sendStaticLevelData(subLevel->staticData.lock(), subLevel, modTime); + + // Now warp. + success = success && warp(level, toPixelPosition(level->getSubLevelOrigin(subLevel).value_or(PixelPosition{}), pos), modTime); + } + // Otherwise, just enter the level. + else + { + success = enterLevel(level, toPixelPosition({ 0, 0 }, pos), modTime); + } + } + } + + // If we failed, try to resolve this. + if (!success) + { + if (auto level = getLevel(); level != nullptr) + success = warp(level->levelName, account.character.getLocalPosition()); + } + if (!success) + { + const auto& unstickLevel = m_server->cached.unstickMeLevel.getValue(); + const auto& unstickX = m_server->cached.unstickMeTile[0].getValue(); + const auto& unstickY = m_server->cached.unstickMeTile[1].getValue(); + warp(unstickLevel, { static_cast(unstickX * 16.0f), static_cast(unstickY * 16.0f) }); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_BOARDMODIFY(CString& pPacket) +{ + // Bushes, grasses, swamp, snow grass, desert grass. + constexpr std::array dropTiles = { 0x002, 0x1a4, 0x1ff, 0x7ff, 0x3ff, 0x5d9, 0x34f }; + + uint8_t loc[2] = { pPacket.readGUChar(), pPacket.readGUChar() }; + uint8_t dim[2] = { pPacket.readGUChar(), pPacket.readGUChar() }; + CString tiles = pPacket.readString(""); + + auto level = getLevel(); + if (level == nullptr) + return HandlePacketResult::Handled; + + auto globalPosition = toPixelPosition(getSubLevelOrigin(), LocalWholeTilePosition{ loc[0], loc[1] }); + + // Alter level data. + if (level->alterBoard(tiles, { toWholeTilePosition(globalPosition), { dim[0], dim[1] } }, this)) + { + if (!level->isGmap()) + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_BOARDMODIFY << (pPacket.text() + 1), getGlobalPosition(), level, { m_id }); + else + { + auto mapPosition = getMapPosition(); + m_server->sendPacketToNearby(CString() >> (char)PLO_BOARDMODIFY2 >> (char)mapPosition.x() >> (char)mapPosition.y() << (pPacket.text() + 1), getGlobalPosition(), level, { m_id }); + } + } + + if (loc[0] < 0 || loc[0] > 63 || loc[1] < 0 || loc[1] > 63) + return HandlePacketResult::Handled; + + // Older clients drop items clientside. + if (m_versionId < CLVER_2_1) + return HandlePacketResult::Handled; + + // Lay items when you destroy objects. + auto levelTiles = level->getTiles(getMapPosition()); + if (!levelTiles.has_value()) + return HandlePacketResult::Handled; + + auto oldTile = levelTiles.value()->at(loc[0] + static_cast(loc[1] * 64)); + bool bushitems = m_server->cached.enableBushItemDrops.getValue(); + bool vasesdrop = m_server->cached.enableVaseItemDrops.getValue(); + LevelItemType dropItem = LevelItemType::INVALID; + + // If we support item drops and the tile is in the allowed list, drop the item. + if (std::ranges::contains(dropTiles, oldTile) && bushitems) + { + dropItem = m_server->rollBushItemDrop(); + } + // Vases drop hearts. + else if (oldTile == 0x2ac && vasesdrop) + { + dropItem = LevelItemType::HEART; + } + + // Send the item now. + if (dropItem != LevelItemType::INVALID) + level->addItem(inform_client, toPixelPosition(getSubLevelOrigin(), LocalWholeTilePosition{ loc[0], loc[1] }), dropItem); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_REQUESTUPDATEBOARD(CString& pPacket) +{ + // {130}{CHAR level length}{level}{INT5 modtime}{SHORT x}{SHORT y}{SHORT width}{SHORT height} + CString level = pPacket.readChars(pPacket.readGUChar()); + + time_t modTime = (time_t)pPacket.readGUInt5(); + + short x = pPacket.readGShort(); + short y = pPacket.readGShort(); + short w = pPacket.readGShort(); + short h = pPacket.readGShort(); + + // TODO: What to return? + log::printLine(log::server, "Received PLI_REQUESTUPDATEBOARD - level: {} - x: {} - y: {} - w: {} - h: {} - modtime: {}", level, x, y, w, h, modTime); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_NPCPROPS(CString& pPacket) +{ + // Don't accept if we have an npc-server. + if (m_server->hasNPCServer()) + return HandlePacketResult::Handled; + + unsigned int npcId = pPacket.readGUInt(); + CString npcProps = pPacket.readString(""); + + //printf( "npcId: %d\n", npcId ); + //printf( "pPacket: %s\n", npcProps.text()); + //for (int i = 0; i < pPacket.length(); ++i) printf( "%02x ", (unsigned char)pPacket[i] ); + //printf( "\n" ); + + auto npc = m_server->getNPC(npcId); + if (!npc) + return HandlePacketResult::Handled; + + if (auto level = getLevel(); npc->getLevel() != level) + return HandlePacketResult::Handled; + + npc->setPropsFromPacket(npcProps, shared_from_this()); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_BOMBADD(CString& pPacket) +{ + float loc[2] = { (pPacket.readGUChar() % 128) / 2.0f, (pPacket.readGUChar() % 128) / 2.0f }; + [[maybe_unused]] unsigned char player_power = pPacket.readGUChar(); + [[maybe_unused]] unsigned char player = player_power >> 2; + [[maybe_unused]] unsigned char power = player_power & 0x03; + + // How many 0.05 sec increments until it explodes. + // It takes 3 seconds for a bomb to explode, but by the time the client sends the packet, it has already counted down to 2.75 seconds. + // The 0 is counted as a 0.05 second increment, so we add 50ms to the total. + [[maybe_unused]] std::chrono::milliseconds timeToExplode = (pPacket.readGUChar() * 50ms) + 50ms; + + if (auto level = getLevel(); level != nullptr) + { + if (m_server->hasNPCServer()) + { + auto position = toPixelPosition(getSubLevelOrigin(), loc[0], loc[1]); + if (level->addBombFromClient(position, power, m_id, timeToExplode) == nullptr) + sendPacket(CString() >> (char)PLO_BOMBDEL >> (char)(loc[0] * 2) >> (char)(loc[1] * 2)); + } + else + { + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_BOMBADD >> (short)m_id << (pPacket.text() + 1), getGlobalPosition(), level, { m_id }); + } + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_BOMBDEL(CString& pPacket) +{ + if (auto level = getLevel(); level != nullptr) + { + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_BOMBDEL << (pPacket.text() + 1), getGlobalPosition(), level, { m_id }); + + float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; + level->removeBomb(toPixelPosition(getSubLevelOrigin(), loc[0], loc[1])); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_HORSEADD(CString& pPacket) +{ + if (auto level = getLevel(); level != nullptr) + { + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_HORSEADD << (pPacket.text() + 1), getGlobalPosition(), level, { m_id }); + + float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; + uint8_t dir_bush = pPacket.readGUChar(); + uint8_t hdir = dir_bush & 0x03; + uint8_t hbushes = dir_bush >> 2; + CString image = pPacket.readString(""); + + level->addHorse(image, toPixelPosition(getSubLevelOrigin(), loc[0], loc[1]), hdir, hbushes); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_HORSEDEL(CString& pPacket) +{ + if (auto level = getLevel(); level != nullptr) + { + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_HORSEDEL << (pPacket.text() + 1), getGlobalPosition(), level, { m_id }); + + float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; + level->removeHorse(toPixelPosition(getSubLevelOrigin(), loc[0], loc[1])); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_ARROWADD(CString& pPacket) +{ + [[maybe_unused]] float loc[] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; + [[maybe_unused]] uint8_t flags = pPacket.readGUChar(); + [[maybe_unused]] uint8_t sprite = pPacket.readGUChar(); + [[maybe_unused]] uint8_t power = pPacket.readGUChar(); + + [[maybe_unused]] uint8_t dir = flags & 0b11; + [[maybe_unused]] bool reflect = (flags & 0b100) != 0; + [[maybe_unused]] bool fromPlayer = (flags & 0b1000) != 0; + + if (auto level = getLevel(); level != nullptr) + { + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_ARROWADD >> (short)m_id << (pPacket.text() + 1), getGlobalPosition(), level, { m_id }); + + // Add it to the level. + if (m_server->hasNPCServer()) + { + PixelPosition speed = { (dir == 0 || dir == 2) ? 0 : (dir == 1 ? -16 : 16), (dir == 1 || dir == 3) ? 0 : (dir == 0 ? -16 : 16) }; + level->addArrow(toPixelPosition(getSubLevelOrigin(), loc[0], loc[1]), speed, dir, power, fromPlayer ? source::FromPlayer(m_id) : source::FromServer()); + } + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_FIRESPY(CString& pPacket) +{ + uint8_t length_power = pPacket.readGUChar(); + uint8_t power = length_power & 0b111; // Power is the last three bits. + uint8_t length = length_power >> 3; // Length is the first five bits. + + if (auto level = getLevel(); level != nullptr) + { + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_FIRESPY >> (short)m_id << (pPacket.text() + 1), getGlobalPosition(), level, { m_id }); + + // Add it to the level. + if (m_server->hasNPCServer()) + level->addSpyFire(getGlobalPosition(), source::FromPlayer(m_id), account.character.direction, length, power); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_THROWCARRIED(CString& pPacket) +{ + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_THROWCARRIED >> (short)m_id, getGlobalPosition(), getLevel(), { m_id }); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_ITEMADD(CString& pPacket) +{ + float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; + uint8_t item = pPacket.readGUChar(); + LevelItemType itemType = LevelItem::getItemId(item); + + // If item drops are disabled, tell the client to delete the item and roll back the changes. + if (m_server->cached.disableItemDropping.getValue()) + { + sendPacket(CString() >> (char)PLO_ITEMDEL >> (char)(loc[0] * 2) >> (char)(loc[1] * 2)); + if (m_server->hasNPCServer()) + addItem(inform_client, itemType); + return HandlePacketResult::Handled; + } + + m_server->queueNPCEvent(m_currentLevel.lock(), getGlobalPosition(), ScriptEventType::PLAYERLAYSITEM, source::FromPlayer(m_id)); + + // Check if we should send item drop events to the Control-NPC. + bool itemDropEvents = m_server->cached.enableItemDropEvents.getValue(); + if (itemDropEvents && m_server->cached.itemDropEventsOnlyForGralats.getValue() && !LevelItem::isRupeeType(itemType)) + itemDropEvents = false; + + // If item drop events are enabled, send the item drop event to the Control-NPC. + // This will prevent all client item drops, so beware. + if (itemDropEvents) + { + m_server->getNPCServer()->addEventToControlNPC(ScriptEventType::CUSTOM, source::FromPlayer(m_id), "itemdrop", getLevelName(), std::format("{}", loc[0]), std::format("{}", loc[1]), LevelItem::getItemName(itemType)); + sendPacket(CString() >> (char)PLO_ITEMDEL >> (char)(loc[0] * 2) >> (char)(loc[1] * 2)); + return HandlePacketResult::Handled; + } + + if (auto level = getLevel(); level != nullptr) + { + if (m_server->hasNPCServer()) + { + // Try to drop the item on the level. + // If the item was ultimately not dropped on the level (e.g., a gralats NPC was created), tell the client to delete it. + if (!dropItem(toPixelPosition(getSubLevelOrigin(), loc[0], loc[1]), itemType)) + sendPacket(CString() >> (char)PLO_ITEMDEL >> (char)(loc[0] * 2) >> (char)(loc[1] * 2)); + } + else + { + level->addItem(toPixelPosition(getSubLevelOrigin(), loc[0], loc[1]), itemType); + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_ITEMADD >> (char)(loc[0] * 2) >> (char)(loc[1] * 2) >> (char)item, getGlobalPosition(), level, { m_id }); + } + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_ITEMDEL(CString& pPacket) +{ + if (auto level = getLevel(); level != nullptr) + { + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_ITEMDEL << (pPacket.text() + 1), getGlobalPosition(), level, { m_id }); + + float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; + + // Remove the item from the level, getting the type of the item in the process. + LevelItemType item = level->removeItem(toPixelPosition(getSubLevelOrigin(), loc[0], loc[1])); + if (item == LevelItemType::INVALID) return HandlePacketResult::Handled; + + // If this is a PLI_ITEMTAKE packet, give the item to the player. + if (pPacket[0] - 32 == PLI_ITEMTAKE) + this->setPropsFromPacket(CString() << LevelItem::getItemPlayerProp(item, this), props::SetBy::SERVER); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_CLAIMPKER(CString& pPacket) +{ + // Get the player who killed us. + unsigned int pId = pPacket.readGUShort(); + auto killer = m_server->getPlayer(pId, PLTYPE_ANYCLIENT); + if (killer == nullptr || killer.get() == this) + return HandlePacketResult::Handled; + + // Sparring zone rating code. + // Uses the glicko rating system. + auto level = getLevel(); + if (level == nullptr) return HandlePacketResult::Handled; + if (level->isSparringZone(getMapPosition())) + { + if (m_server->getSettings().get("dontupdateratingd").value_or(false) == false) + { + // Get some stats we are going to use. + // Need to parse the other player's PlayerProp::RATING. + auto otherRating = killer->getProp(); + float oldStats[4] = { account.eloRating, account.eloDeviation, (float)otherRating.rating, (float)otherRating.deviation }; + + // If the IPs are the same, don't update the rating to prevent cheating. + if (CString(m_playerSock->getRemoteIp()) == CString(killer->getSocket()->getRemoteIp())) + return HandlePacketResult::Handled; + + float gSpar[2] = { static_cast(1.0f / pow((1.0f + 3.0f * pow(0.0057565f, 2) * (pow(oldStats[3], 2)) / pow(3.14159265f, 2)), 0.5f)), //Winner + static_cast(1.0f / pow((1.0f + 3.0f * pow(0.0057565f, 2) * (pow(oldStats[1], 2)) / pow(3.14159265f, 2)), 0.5f)) }; //Loser + float ESpar[2] = { static_cast(1.0f / (1.0f + pow(10.0f, (-gSpar[1] * (oldStats[2] - oldStats[0]) / 400.0f)))), //Winner + static_cast(1.0f / (1.0f + pow(10.0f, (-gSpar[0] * (oldStats[0] - oldStats[2]) / 400.0f)))) }; //Loser + float dSpar[2] = { static_cast(1.0f / (pow(0.0057565f, 2) * pow(gSpar[0], 2) * ESpar[0] * (1.0f - ESpar[0]))), //Winner + static_cast(1.0f / (pow(0.0057565f, 2) * pow(gSpar[1], 2) * ESpar[1] * (1.0f - ESpar[1]))) }; //Loser + + float tWinRating = oldStats[2] + (0.0057565f / (1.0f / powf(oldStats[3], 2) + 1.0f / dSpar[0])) * (gSpar[0] * (1.0f - ESpar[0])); + float tLoseRating = oldStats[0] + (0.0057565f / (1.0f / powf(oldStats[1], 2) + 1.0f / dSpar[1])) * (gSpar[1] * (0.0f - ESpar[1])); + float tWinDeviation = powf((1.0f / (1.0f / powf(oldStats[3], 2) + 1 / dSpar[0])), 0.5f); + float tLoseDeviation = powf((1.0f / (1.0f / powf(oldStats[1], 2) + 1 / dSpar[1])), 0.5f); + + // Cap the rating. + tWinRating = std::clamp(tWinRating, 0.0f, 4000.0f); + tLoseRating = std::clamp(tLoseRating, 0.0f, 4000.0f); + tWinDeviation = std::clamp(tWinDeviation, 50.0f, 350.0f); + tLoseDeviation = std::clamp(tLoseDeviation, 50.0f, 350.0f); + + // Update the Ratings. + if (oldStats[0] != tLoseRating || oldStats[1] != tLoseDeviation) + { + sendPropsFromResults(setProp(props::SetBy::SERVER, PropertyEloRating{ tLoseRating, tLoseDeviation })); + } + if (oldStats[2] != tWinRating || oldStats[3] != tWinDeviation) + { + killer->sendPropsFromResults(killer->setProp(props::SetBy::SERVER, PropertyEloRating{ tWinRating, tWinDeviation })); + } + this->account.lastSparTime = std::chrono::system_clock::now(); + killer->account.lastSparTime = std::chrono::system_clock::now(); + } + } + else + { + // Give a kill to the player who killed me. + ++killer->account.kills; + + // Now, adjust their AP if allowed. + if (m_server->cached.enableAPSystem.getValue()) + { + auto oAp = killer->getProp().value; + + // If I have 20 or more AP, they lose AP. + if (oAp > 0 && account.character.ap > 19) + { + int aptime[] = + { + m_server->cached.apSystemThresholdSeconds[0].getValue(), m_server->cached.apSystemThresholdSeconds[1].getValue(), + m_server->cached.apSystemThresholdSeconds[2].getValue(), m_server->cached.apSystemThresholdSeconds[3].getValue(), + m_server->cached.apSystemThresholdSeconds[4].getValue() + }; + + oAp -= (((oAp / 20) + 1) * (account.character.ap / 20)); + if (oAp < 0) oAp = 0; + killer->account.apCounter = (oAp < 20 ? aptime[0] : (oAp < 40 ? aptime[1] : (oAp < 60 ? aptime[2] : (oAp < 80 ? aptime[3] : aptime[4])))); + killer->setPropsFromPacket(CString() >> (char)PlayerProp::ALIGNMENT >> (char)oAp, props::SetBy::SERVER); + } + } + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_BADDYPROPS(CString& pPacket) +{ + auto level = getLevel(); + if (level == nullptr || !level->hasPlayers()) + return HandlePacketResult::Handled; + + bool livingBaddies = level->hasLivingBaddies(); + + unsigned char id = pPacket.readGUChar(); + CString props = pPacket.readString(""); + + // Get the baddy. + auto baddy = level->getBaddyById(id); + if (!baddy.has_value() || baddy.value() == nullptr) + return HandlePacketResult::Handled; + + // Get the leader. + auto leaderId = level->getPlayers().front(); + + // Set the props and send to everybody in the level, except the leader. + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_BADDYPROPS >> (char)id << props, getGlobalPosition(), level, { leaderId }); + baddy.value()->setPropsFromPacket(props); + + if (livingBaddies && !level->hasLivingBaddies()) + m_server->queueNPCEventLocal(m_currentLevel.lock(), ScriptEventType::COMPUSDIED, source::FromPlayer(m_id)); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_BADDYHURT(CString& pPacket) +{ + auto level = getLevel(); + if (level == nullptr || !level->hasPlayers()) + return HandlePacketResult::Handled; + + auto leaderId = level->getPlayers().front(); + auto leader = m_server->getPlayer(leaderId); + if (leader == nullptr) + return HandlePacketResult::Handled; + + leader->sendPacket(CString() >> (char)PLO_BADDYHURT << (pPacket.text() + 1)); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_BADDYADD(CString& pPacket) +{ + // Don't add a baddy if we aren't in a level! + if (m_currentLevel.expired()) + return HandlePacketResult::Handled; + + float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; + uint8_t bType = pPacket.readGUChar(); + uint8_t bPower = pPacket.readGUChar(); + CString bImage = pPacket.readString(""); + bPower = std::min(bPower, 12_ui8); // Hard-limit to 6 hearts. + + auto level = getLevel(); + if (level == nullptr) + return HandlePacketResult::Handled; + + // Fix the image for 1.41 clients. + if (!bImage.isEmpty() && getExtension(bImage).isEmpty()) + bImage << ".gif"; + + // Add the baddy. + LevelBaddy* baddy = level->addBaddy(toLocalPixelPosition(loc[0], loc[1]), static_cast(bType)); + if (baddy == nullptr) return HandlePacketResult::Handled; + + // Set the baddy props. + baddy->setRespawn(false); + baddy->setPropsFromPacket(CString() >> (char)BaddyProp::POWERIMAGE >> (char)bPower >> (char)bImage.length() << bImage); + + // Send the props to everybody in the level. + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_BADDYPROPS >> (char)baddy->id << baddy->getProps(), getGlobalPosition(), level); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_FLAGSET(CString& pPacket) +{ + CString flagPacket = pPacket.readString(""); + CString flagName, flagValue; + + if (flagPacket.find("=") == -1) + flagName = flagPacket; + else + { + flagName = flagPacket.readString("="); + flagValue = flagPacket.readString(""); + + // If the value is empty, delete the flag instead. + if (flagValue.isEmpty()) + { + pPacket.setRead(1); // Don't let us read the packet ID. + return msgPLI_FLAGDEL(pPacket); + } + } + + // Add a little hack for our special gr.strings. + if (flagName.find("gr.") != -1) + { + if (flagName == "gr.fileerror" || flagName == "gr.filedata") + return HandlePacketResult::Handled; + + if (m_server->cached.enableFlaghackMovement.getValue()) + { + // gr.x and gr.y are used by the -gr_movement NPC to help facilitate smoother + // movement amongst pre-2.3 clients. + if (flagName == "gr.x") + { + if (m_versionId >= CLVER_2_3) return HandlePacketResult::Handled; + auto globalPos = getGlobalPosition(); + globalPos.x() = static_cast(atof(flagValue.text()) * 16.0); + if (auto localPos = toLocalPixelPosition(globalPos); localPos.x() != account.character.localPixelX) + { + auto xprop = getProp(); + xprop.pixelCoordinate = localPos.x(); + m_grMovementPackets >> (char)PlayerProp::X; + m_grMovementPackets << xprop.serialize(); + m_grMovementPackets << "\n"; + } + return HandlePacketResult::Handled; + } + else if (flagName == "gr.y") + { + if (m_versionId >= CLVER_2_3) return HandlePacketResult::Handled; + auto globalPos = getGlobalPosition(); + globalPos.y() = static_cast(atof(flagValue.text()) * 16.0); + if (auto localPos = toLocalPixelPosition(globalPos); localPos.y() != account.character.localPixelY) + { + auto yprop = getProp(); + yprop.pixelCoordinate = localPos.y(); + m_grMovementPackets >> (char)PlayerProp::Y; + m_grMovementPackets << yprop.serialize(); + m_grMovementPackets << "\n"; + } + return HandlePacketResult::Handled; + } + else if (flagName == "gr.z") + { + if (m_versionId >= CLVER_2_3) return HandlePacketResult::Handled; + float pos = (float)atof(flagValue.text()); + if (pos != account.character.localPixelZ / 16.0f) + m_grMovementPackets >> (char)PlayerProp::Z >> (char)((pos + 0.5f) + 50.0f) << "\n"; + return HandlePacketResult::Handled; + } + } + } + + // 2.171 clients didn't support this.strings and tried to set them as a + // normal flag. Don't allow that. + if (flagName.find("this.") != -1) return HandlePacketResult::Handled; + + // Don't allow anybody to set read-only strings. + if (flagName.find("clientr.") != -1) return HandlePacketResult::Handled; + if (flagName.find("serverr.") != -1) return HandlePacketResult::Handled; + + // Server flags are handled differently than client flags. + // If we have an npc-server, clients can't set server flags. + if (!m_server->hasNPCServer()) + { + if (flagName.find("server.") != -1) + { + m_server->setFlag(flagName.toStringView(), flagValue.toString()); + return HandlePacketResult::Handled; + } + } + + // Set Flag + if (flagValue.isEmpty()) + setFlag(flagName.toStringView(), std::nullopt, (m_versionId > CLVER_2_31)); + else setFlag(flagName.toStringView(), flagValue.toString(), (m_versionId > CLVER_2_31)); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_FLAGDEL(CString& pPacket) +{ + CString flagPacket = pPacket.readString(""); + + std::string_view flagName; + bool hasValue = false; + + if (flagPacket.find('=') == -1) + flagName = flagPacket.toStringView(); + else + { + flagName = flagPacket.toStringView(); + flagName = flagName.substr(0, flagName.find('=')); + hasValue = true; + } + + // this.flags should never be in any server flag list, so just exit. + if (flagName.find("this.") != std::string_view::npos) return HandlePacketResult::Handled; + + // Don't allow anybody to alter read-only strings. + if (flagName.find("clientr.") != std::string_view::npos) return HandlePacketResult::Handled; + if (flagName.find("serverr.") != std::string_view::npos) return HandlePacketResult::Handled; + + // Server flags are handled differently than client flags. + // TODO: check serveroptions + if (!m_server->hasNPCServer()) + { + if (flagName.find("server.") != std::string_view::npos) + { + m_server->deleteFlag(std::string{ flagName }); + return HandlePacketResult::Handled; + } + } + + // Try to remove the flag. + if (auto flag = account.variables.get(flagName).lock(); flag != nullptr) + { + if (flag->has()) + { + if (hasValue) + account.variables.remove(flagName); + } + else if (flag->has()) + { + if (!hasValue) + account.variables.remove(flagName); + } + } + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_OPENCHEST(CString& pPacket) +{ + uint8_t cX = pPacket.readGChar(); + uint8_t cY = pPacket.readGChar(); + + if (auto level = getLevel(); level) + { + LocalWholeTilePosition chestPos{ cX, cY }; + if (auto chest = level->getChest(getMapPosition(), chestPos); chest.has_value()) + { + auto levelName = level->getLevelNameAtPosition(getGlobalPosition()); + if (!account.hasChest(levelName, chestPos)) + { + LevelItemType chestItem = chest.value()->item; + setPropsFromPacket(CString() << LevelItem::getItemPlayerProp(chestItem, this), props::SetBy::SERVER); + sendPacket(CString() >> (char)PLO_LEVELCHEST >> (char)1 >> (char)cX >> (char)cY); + account.savedChests.insert(std::make_pair(level->levelName, chestPos)); + } + } + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_PUTNPC(CString& pPacket) +{ + // Don't accept if we have an npc-server. + if (m_server->hasNPCServer()) + return HandlePacketResult::Handled; + + CString nimage = pPacket.readChars(pPacket.readGUChar()); + CString ncode = pPacket.readChars(pPacket.readGUChar()); + float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; + + // See if putnpc is allowed. + if (!m_server->getSettings().get("putnpcenabled").value_or(false)) + return HandlePacketResult::Handled; + + // Get the file. + auto file = m_server->getFileSystem().open(fs::FileCategory::FILE, ncode.toStringView()); + if (!file) + return HandlePacketResult::Handled; + + // Load the code. + auto code = file->readAsString(); + string::eraseCharsMutate(code, "\r"sv); + + // Add NPC to level + m_server->addNPC(nimage, code, loc[0], loc[1], m_currentLevel, NPCStorageType::LEVEL, true); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_NPCDEL(CString& pPacket) +{ + // Don't accept if we have an npc-server. + if (m_server->hasNPCServer()) + return HandlePacketResult::Handled; + + unsigned int nid = pPacket.readGUInt(); + + // Remove the NPC. + if (auto npc = m_server->getNPC(nid); npc) + m_server->deleteNPC(npc, !m_currentLevel.expired()); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_WANTFILE(CString& pPacket) +{ + // Get file. + CString file = pPacket.readString(""); + + // If we are the 1.41 client, make sure a file extension was sent. + if (m_versionId < CLVER_2_1 && getExtension(file).isEmpty()) + file << ".gif"; + + //printf("WANTFILE: %s\n", file.text()); + + // Send file. + this->sendFile(std::filesystem::path{ file.toString() }); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_SHOWIMGPLAYER(CString& pPacket) +{ + m_server->sendPacketToNearby(CString() >> (char)PLO_SHOWIMGPLAYER >> (short)m_id << (pPacket.text() + 1), getGlobalPosition(), getLevel(), { m_id }); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_HURTPLAYER(CString& pPacket) +{ + unsigned short pId = pPacket.readGUShort(); + char hurtdx = pPacket.readGChar(); + char hurtdy = pPacket.readGChar(); + unsigned char power = pPacket.readGUChar(); + unsigned int npc = pPacket.readGUInt(); + + // Get the victim. + auto victim = m_server->getPlayer(pId, PLTYPE_ANYCLIENT); + if (victim == 0) return HandlePacketResult::Handled; + + // If they are paused, they don't get hurt. + if (victim->getProp().value & PLSTATUS_PAUSED) return HandlePacketResult::Handled; + + // Send the packet. + victim->sendPacket(CString() >> (char)PLO_HURTPLAYER >> (short)m_id >> (char)hurtdx >> (char)hurtdy >> (char)power >> (int)npc); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_EXPLOSION(CString& pPacket) +{ + if (m_server->cached.disableExplosions.getValue()) + return HandlePacketResult::Handled; + + unsigned char eradius = pPacket.readGUChar(); + float loc[2] = { (float)pPacket.readGUChar() / 2.0f, (float)pPacket.readGUChar() / 2.0f }; + unsigned char epower = pPacket.readGUChar(); + + if (auto level = getLevel(); level != nullptr) + { + // Send the packet out. + CString packet = CString() >> (char)PLO_EXPLOSION >> (short)m_id >> (char)eradius >> (char)(loc[0] * 2) >> (char)(loc[1] * 2) >> (char)epower; + m_server->sendPacketToOneLevelPart(packet, getGlobalPosition(), level, { m_id }); + + // Add it to the level. + if (m_server->hasNPCServer()) + level->addExplosion(toPixelPosition(getSubLevelOrigin(), loc[0], loc[1]), source::FromPlayer(m_id), eradius, epower); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_PRIVATEMESSAGE(CString& pPacket) +{ + const int sendLimit = 4; + if (isClient() && timeDifference(m_server->getFrameStartTime(), m_lastMessage) < 4s) + { + sendPacket(CString() >> (char)PLO_RC_ADMINMESSAGE << "Server message:\xa7You can only send messages once every " << CString((int)sendLimit) << " seconds."); + return HandlePacketResult::Handled; + } + m_lastMessage = m_server->getFrameStartTime(); + return HandlePacketResult::Bubble; +} + +HandlePacketResult PlayerClient::msgPLI_NPCWEAPONDEL(CString& pPacket) +{ + std::string weapon = pPacket.readString("").toString(); + + // If it is a protected weapon, don't delete it. + if (std::ranges::contains(m_server->cached.protectedWeapons.getValue(), weapon)) + return HandlePacketResult::Handled; + + std::erase(account.weapons, weapon); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_WEAPONADD(CString& pPacket) +{ + // Don't accept if we have an npc-server. + if (m_server->hasNPCServer()) + return HandlePacketResult::Handled; + + unsigned char type = pPacket.readGUChar(); + + // Type 0 means it is a default weapon. + if (type == 0) + { + this->addWeapon(LevelItem::getItemId(pPacket.readGChar())); + } + // NPC weapons. + else + { + // Get the NPC id. + unsigned int npcId = pPacket.readGUInt(); + auto npc = m_server->getNPC(npcId); + if (npc == nullptr) + return HandlePacketResult::Handled; + + // Get the level. + auto level = npc->getLevel(); + if (level == nullptr) + return HandlePacketResult::Handled; + + // Get the name of the weapon. + const auto& name = npc->getWeaponName(); + if (name.length() == 0) + return HandlePacketResult::Handled; + + // See if we can find the weapon in the server weapon list. + auto weapon = m_server->getWeapon(name); + + // If weapon is nullptr, that means the weapon was not found. Add the weapon to the list. + if (weapon == nullptr) + { + weapon = std::make_shared(name, npc->image, std::string{ npc->getScript().getOriginalSource() }); + weapon->saveWeapon(); + m_server->NC_AddWeapon(weapon); + } + + // Check and see if the weapon has changed recently. If it has, we should + // send the new NPC to everybody on the server. After updating the script, of course. + if (weapon->modTime < level->modTime) + { + // Update Weapon + weapon->updateWeapon(npc->image, std::string{ npc->getScript().getOriginalSource() }).saveWeapon(); + + // Send to Players + m_server->updateWeaponForPlayers(weapon); + } + + // Send the weapon to the player now. + addWeapon(weapon); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_UPDATEFILE(CString& pPacket) +{ + // Get the packet data and file mod time. + time_t modTime = pPacket.readGUInt5(); + CString file = pPacket.readString(""); + + // If we are the 1.41 client, make sure a file extension was sent. + if (m_versionId < CLVER_2_1 && getExtension(file).isEmpty()) + file << ".gif"; + + auto& fileSystem = m_server->getFileSystem(); + time_t fModTime = 0; + + if (auto info = fileSystem.infoi(fs::FileCategory::ALL, file.toStringView()); info != nullptr) + fModTime = clock::to_time_t(toSystemClock(info->modifiedTime)); + + //printf("UPDATEFILE: %s\n", file.text()); + + // Make sure it isn't one of the default files. + bool isDefault = false; + for (const auto& defaultFile : DefaultFiles) + { + if (file.match(CString(defaultFile.data()))) + { + isDefault = true; + break; + } + } + + // If the file on disk is different, send it to the player. + file.setRead(0); + if (!isDefault) + { + if (std::difftime(modTime, fModTime) != 0) + return msgPLI_WANTFILE(file); + } + + if (m_versionId < CLVER_2_1) + sendPacket(CString() >> (char)PLO_FILESENDFAILED << file); + else + sendPacket(CString() >> (char)PLO_FILEUPTODATE << file); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_ADJACENTLEVEL(CString& pPacket) +{ + std::optional modTime = clock::from_time_t((time_t)pPacket.readGUInt5()); + + CString levelNameC = pPacket.readString(""); + std::string_view levelName = levelNameC.toStringView(); + + // Check if the adjacent level is on the player's current gmap. + // The gmap might have customized data. + if (auto currentLevel = getLevel(); currentLevel != nullptr && currentLevel->isGmap()) + { + if (auto subLevel = currentLevel->getSubLevelByName(levelName); subLevel != nullptr) + { + sendStaticLevelData(subLevel->staticData.lock(), subLevel, modTime); + return HandlePacketResult::Handled; + } + } + + // Otherwise, send the normal static data. + if (auto cachedData = m_server->getCachedLevelData(levelName); cachedData != nullptr) + sendStaticLevelData(cachedData, nullptr, modTime); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_HITOBJECTS(CString& pPacket) +{ + float power = (float)pPacket.readGChar() / 2.0f; + float loc[2] = { (float)pPacket.readGChar() / 2.0f, (float)pPacket.readGChar() / 2.0f }; + int nid = (pPacket.bytesLeft() != 0) ? pPacket.readGUInt() : -1; + + // Construct the packet. + // {46}{SHORT player_id / 0 for NPC}{CHAR power}{CHAR x}{CHAR y}[{INT npc_id}] + CString nPacket; + nPacket >> (char)PLO_HITOBJECTS; + nPacket >> (short)((nid == -1) ? m_id : 0); // If it came from an NPC, send 0 for the id. + nPacket >> (char)(power * 2) >> (char)(loc[0] * 2) >> (char)(loc[1] * 2); + if (nid != -1) nPacket >> (int)nid; + + if (m_server->hasNPCServer()) + { + if (auto level = getLevel(); level != nullptr) + { + auto hitNPCs = level->findIntersectingNPCsForCollision({ static_cast(loc[0] * 16), static_cast(loc[1] * 16) }); + for (const auto& npcId : hitNPCs) + { + if (auto npc = m_server->getNPC(npcId); npc != nullptr && npc->isCharacter() && npc->visFlags != PROPID(NPCVisFlags::HIDDEN)) + { + npc->setPropWith(SetBy::SERVER, static_cast(std::max(0, (int)npc->getProp().value - int(power * 2)))); + npc->hurtAndPush(power, translatePosition(getGlobalPosition(), 24_i32, 32_i32), ScriptEventType::WASHIT, source::FromPlayer(m_id)); + } + } + } + } + + m_server->sendPacketToNearby(nPacket, getGlobalPosition(), getLevel(), {m_id}); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_TRIGGERACTION(CString& pPacket) +{ + // Read packet data + [[maybe_unused]] unsigned int npcId = pPacket.readGUInt(); + float loc[2] = { + (float)pPacket.readGUChar() / 2.0f, + (float)pPacket.readGUChar() / 2.0f + }; + + PixelPosition pixelLoc{ toPixelPosition(getSubLevelOrigin(), loc[0], loc[1]) }; + + // Tokenize the actions. + auto actionData = pPacket.readString("").toString(); + auto actions = string::fromCSV(actionData); + if (actions.empty()) + return HandlePacketResult::Handled; + + // Grab action name. + auto actualActionName{ string::trimMutate(string::toLower(actions[0])) }; + + // TODO(joey): move into trigger command dispatcher, some use private player vars. + { + if (m_server->cached.enableTriggerhackExecscript.getValue()) + { + if (actualActionName == "gr.es_clear") + { + // Clear the parameters. + m_grExecParameterList.clear(); + return HandlePacketResult::Handled; + } + else if (actualActionName == "gr.es_set") + { + // Add the parameter to our saved parameter list. + CString parameters = string::join(actions | std::views::drop(1)); + if (m_grExecParameterList.isEmpty()) + m_grExecParameterList = parameters; + else + m_grExecParameterList << "," << parameters; + return HandlePacketResult::Handled; + } + else if (actualActionName == "gr.es_append") + { + // Append doesn't add the beginning comma. + CString parameters = string::join(actions | std::views::drop(1)); + if (m_grExecParameterList.isEmpty()) + m_grExecParameterList = parameters; + else + m_grExecParameterList << parameters; + return HandlePacketResult::Handled; + } + else if (actualActionName == "gr.es") + { + if (actions.size() > 2) + { + CString account = actions[1]; + CString wepname = CString() << "-gr_exec_" << removeExtension(actions[2]); + CString wepimage = "wbomb1.png"; + + auto filePath = std::filesystem::path{ "execscripts" } / actions[2]; + + // Load in all the execscripts. + CString wepscript; + wepscript.load(filePath.string()); + + // Check to see if we were able to load the weapon. + if (wepscript.isEmpty()) + { + log::printLine(log::server, "Error: Player {} tried to load execscript {}, but the script was not found.", this->account.name, actions[2]); + return HandlePacketResult::Handled; + } + + // Format the weapon script properly. + wepscript.removeAllI("\r"); + wepscript.replaceAllI("\n", "\xa7"); + + // Replace parameters. + std::vector parameters = m_grExecParameterList.tokenize(","); + for (int i = 0; i < (int)parameters.size(); i++) + { + CString parmName = "*PARM" + CString(i); + wepscript.replaceAllI(parmName, parameters[i]); + } + + // Set all unreplaced parameters to 0. + for (int i = 0; i < 128; i++) + { + CString parmName = "*PARM" + CString(i); + wepscript.replaceAllI(parmName, "0"); + } + + // Create the weapon packet. + CString weapon_packet = CString() >> (char)PLO_NPCWEAPONADD >> (char)wepname.length() << wepname >> (char)0 >> (char)wepimage.length() << wepimage >> (char)1 >> (short)wepscript.length() << wepscript; + + // Send it to the players now. + if (account == "ALLPLAYERS") + m_server->sendPacketToType(PLTYPE_ANYCLIENT, weapon_packet); + else + { + auto p = m_server->getPlayer(account, PLTYPE_ANYCLIENT); + if (p) p->sendPacket(weapon_packet); + } + m_grExecParameterList.clear(); + } + return HandlePacketResult::Handled; + } + } + + if (m_server->cached.enableTriggerhackFiles.getValue()) + { + if (actualActionName == "gr.appendfile") + { + int start = actionData.find(",") + 1; + if (start == 0) return HandlePacketResult::Handled; + int finish = actionData.find(",", start) + 1; + if (finish == 0) return HandlePacketResult::Handled; + + // Assemble the file name. + CString filename = actionData.substr(start, finish - start - 1); + filename.removeAllI("../"); + filename.removeAllI("..\\"); + + // Load the file. + CString file; + file.load(CString() << "logs/" << filename); + + // Save the file. + file << actionData.substr(finish) << "\r\n"; + file.save(CString() << "logs/" << filename); + return HandlePacketResult::Handled; + } + else if (actualActionName == "gr.writefile") + { + int start = actionData.find(",") + 1; + if (start == 0) return HandlePacketResult::Handled; + int finish = actionData.find(",", start) + 1; + if (finish == 0) return HandlePacketResult::Handled; + + // Grab the filename. + CString filename = actionData.substr(start, finish - start - 1); + filename.removeAllI("../"); + filename.removeAllI("..\\"); + + // Save the file. + CString file = CString(actionData.substr(finish)) << "\r\n"; + file.save(CString() << "logs/" << filename); + return HandlePacketResult::Handled; + } + else if (actualActionName == "gr.readfile") + { + int start = actionData.find(",") + 1; + if (start == 0) return HandlePacketResult::Handled; + int finish = actionData.find(",", start) + 1; + if (finish == 0) return HandlePacketResult::Handled; + + // Grab the filename. + CString filename = actionData.substr(start, finish - start - 1); + filename.removeAllI("../"); + filename.removeAllI("..\\"); + + // Load the file. + CString filedata; + filedata.load(CString() << "logs/" << filename); + filedata.removeAllI("\r"); + + // Tokenize it. + std::vector tokens = filedata.tokenize("\n"); + + // Find the line. + int id = rand() % 0xFFFF; + CString error; + size_t line = string::toNumber(actionData.substr(finish)); + if (line >= tokens.size()) + { + // We asked for a line that doesn't exist. Mark it as an error! + line = tokens.size() - 1; + error << CString("1,") + line; + } + + // Check if an error was set. + if (error.isEmpty()) + error = "0"; + + // Apply the ID. + error = CString(id) << "," << error; + + // Send it back to the player. + sendPacket(CString() >> (char)PLO_FLAGSET << "gr.fileerror=" << error); + sendPacket(CString() >> (char)PLO_FLAGSET << "gr.filedata=" << tokens[line]); + } + } + + if (m_server->cached.enableTriggerhackProps.getValue()) + { + if (actualActionName == "gr.attr") + { + int start = actionData.find(","); + if (start != -1) + { + int attrNum = string::toNumber(actionData.substr(7, std::max(0, start - 7))); + if (attrNum > 0 && attrNum <= 30) + { + ++start; + CString val = actionData.substr(start); + setPropsFromPacket(CString() >> (char)(GaniAttributePropList[static_cast(attrNum) - 1]) >> (char)val.length() << val, props::SetBy::SERVER); + } + } + } + if (actualActionName == "gr.fullhearts") + { + int start = actionData.find(","); + if (start != -1) + { + ++start; + int hearts = string::toNumber(string::trimMutate(actionData.substr(start))); + sendPropsFromResults(setPropWith(props::SetBy::SERVER, static_cast(hearts))); + } + } + } + + if (m_server->cached.enableTriggerhackLevels.getValue()) + { + if (actualActionName == "gr.updatelevel") + { + auto level = getLevel(); + int start = actionData.find(","); + if (start == -1) + level->reload(getMapPosition()); + else + { + ++start; + std::string levelName = string::trimMutate(actionData.substr(start)); + if (levelName.empty()) + level->reload(getMapPosition()); + else + { + LevelPtr targetLevel; + if (levelName.ends_with(".singleplayer")) + targetLevel = m_singleplayerLevels[removeExtension(levelName)]; + else + targetLevel = m_server->getLoadedLevel(levelName, shared_from_this()); + if (targetLevel != nullptr) + targetLevel->reload(levelName); + } + } + } + } + } + + bool handled = m_server->getTriggerDispatcher().execute(actualActionName, this, actions); + if (!handled) + { + if (actualActionName.starts_with("server") && m_server->hasNPCServer()) + { + // TODO(Nalin): We really should be sending this to the NPC-Server player, not directly calling the NPC-Server. + m_server->getNPCServer()->addEventToControlNPC(ScriptEventType::TRIGGERACTION, source::FromPlayer(m_id), actions); + return HandlePacketResult::Handled; + } + + if (auto level = getLevel(); level) + { + // Send to the level. + if (m_server->cached.sendTriggerActionsToPlayers.getValue()) + m_server->sendPacketToOneLevelPart(CString() >> (char)PLO_TRIGGERACTION >> (short)m_id << (pPacket.text() + 1), getGlobalPosition(), level, { m_id }); + + // Trigger on level NPCs. + if (m_server->hasNPCServer()) + m_server->getNPCServer()->addEventToLevelNPCsAtPosition(ScriptEventType::TRIGGERACTION, source::FromPlayer(m_id), level, pixelLoc, actions); + } + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_TAMPERCHECK(CString& pPacket) +{ + pPacket.readString(""); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_SHOOT(CString& pPacket) +{ + if (auto level = getLevel(); level != nullptr) + { + ShootPacketWrapper newPacket{}; + [[maybe_unused]] int unknown = pPacket.readGInt(); // May be a shoot id for the npc-server. (5/25d/19) joey: all my tests just give 0, my guess would be different types of projectiles but it never came to fruition + + newPacket.position.x() = 8 * pPacket.readGChar(); + newPacket.position.y() = 8 * pPacket.readGChar(); + newPacket.position.z() = 16 * (pPacket.readGChar() - 50); + + // If the player is on a gmap, we need to convert the local pixel position to a map position. + if (level->isGmap()) + newPacket.position.translate(getSubLevelOrigin()); + + // TODO: calculate offsetx from pixelx/pixely/ - level offset + newPacket.offsetx = 0; + newPacket.offsety = 0; + //if (newPacket.pixelx < 0) { + // newPacket.offsetx = -1; + //} + //if (newPacket.pixely < 0) { + // newPacket.offsety = -1; + //} + + newPacket.sangle = pPacket.readGUChar(); // 0 to 2*pi = 0-220 + newPacket.sanglez = pPacket.readGUChar(); // -pi to pi = 0-220 + newPacket.power = pPacket.readGUChar(); // power = 44 pixel increments + newPacket.gravity = static_cast(m_server->Scripting.variables.getValue("gravity").value_or(2.0)); + newPacket.gani = pPacket.readChars(pPacket.readGUChar()); + + // This seems to be the length of shootparams, but the client doesn't limit itself and sends the overflow anyway + [[maybe_unused]] unsigned char someParam = pPacket.readGUChar(); + newPacket.shootParams = pPacket.readString(""); + + CString oldPacketBuf = CString() >> (char)PLO_SHOOT >> (short)m_id << newPacket.constructShootV1(); + CString newPacketBuf = CString() >> (char)PLO_SHOOT2 >> (short)m_id << newPacket.constructShootV2(); + + m_server->sendPacketToNearby(oldPacketBuf, getGlobalPosition(), level, { m_id }, [](const auto pl) { return pl->getVersion() < CLVER_5_07; }); + m_server->sendPacketToNearby(newPacketBuf, getGlobalPosition(), level, { m_id }, [](const auto pl) { return pl->getVersion() >= CLVER_5_07; }); + + if (m_server->hasNPCServer()) + level->addShoot(newPacket.position, newPacket.sangle, newPacket.sanglez, newPacket.power, newPacket.gravity, newPacket.gani, source::FromPlayer(m_id)); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_SHOOT2(CString& pPacket) +{ + if (auto level = getLevel(); level != nullptr) + { + ShootPacketWrapper newPacket{}; + newPacket.position.x() = static_cast(pPacket.readGUShort()); + newPacket.position.y() = static_cast(pPacket.readGUShort()); + newPacket.position.z() = static_cast(pPacket.readGUShort()); + newPacket.offsetx = pPacket.readGChar(); // level offset x + newPacket.offsety = pPacket.readGChar(); // level offset y + newPacket.sangle = pPacket.readGUChar(); // 0 to 2*pi = 0-220 + newPacket.sanglez = pPacket.readGUChar(); // -pi to pi = 0-220 + newPacket.power = pPacket.readGUChar(); // power = 44 pixel increments + newPacket.gravity = pPacket.readGUChar(); + newPacket.gani = pPacket.readChars(pPacket.readGUShort()); + [[maybe_unused]] unsigned char someParam = pPacket.readGUChar(); // This seems to be the length of shootparams, but the client doesn't limit itself and sends the overflow anyway + newPacket.shootParams = pPacket.readString(""); + + CString oldPacketBuf = CString() >> (char)PLO_SHOOT >> (short)m_id << newPacket.constructShootV1(); + CString newPacketBuf = CString() >> (char)PLO_SHOOT2 >> (short)m_id << newPacket.constructShootV2(); + + m_server->sendPacketToNearby(oldPacketBuf, getGlobalPosition(), level, { m_id }, [](const auto pl) { return pl->getVersion() < CLVER_5_07; }); + m_server->sendPacketToNearby(newPacketBuf, getGlobalPosition(), level, { m_id }, [](const auto pl) { return pl->getVersion() >= CLVER_5_07; }); + + if (m_server->hasNPCServer()) + level->addShoot(newPacket.position, newPacket.sangle, newPacket.sanglez, newPacket.power, newPacket.gravity / 16.0f, newPacket.gani, source::FromPlayer(m_id)); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_SERVERWARP(CString& pPacket) +{ + CString servername = pPacket.readString(""); + log::printLine(log::server, "{} is requesting serverwarp to {}", account.name, servername); + m_server->getServerList().sendPacket(CString() >> (char)SVO_SERVERINFO >> (short)m_id << servername); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_PROCESSLIST(CString& pPacket) +{ + std::vector processes = pPacket.readString("").guntokenize().tokenize("\n"); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_ENTERLEVEL(CString& pPacket) +{ + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_VERIFYWANTSEND(CString& pPacket) +{ + unsigned long fileChecksum = pPacket.readGUInt5(); + CString fileName = pPacket.readString(""); + + // There is a USECHECKSUM flag in the config, and im pretty + // certain it works similar to this: By always sending the + // update package the client will respond with another request + // including the crc32 hashes of all the files in the package + bool ignoreChecksum = false; + if (getExtension(fileName) == ".gupd") + ignoreChecksum = true; + + if (!ignoreChecksum) + { + auto info = m_server->getFileSystem().infoi(fs::FileCategory::ALL, fileName.toStringView()); + if (info == nullptr) + return HandlePacketResult::Handled; + + CString fileData; + fileData.load(info->file.string()); + + if (!fileData.isEmpty()) + { + if (calculateCrc32Checksum(fileData) == fileChecksum) + { + sendPacket(CString() >> (char)PLO_FILEUPTODATE << fileName); + return HandlePacketResult::Handled; + } + } + } + + // Send the file to the client + this->sendFile(std::filesystem::path{ fileName.toString() }); + return HandlePacketResult::Handled; +} + +/////////////////////////////////////////////////////////////////////////////// + +HandlePacketResult PlayerClient::msgPLI_UPDATEGANI(CString& pPacket) +{ + // Read packet data + uint32_t checksum = pPacket.readGUInt5(); + std::string gani = pPacket.readString("").toString(); + const std::string ganiFile = gani + ".gani"; + + // Try to find the animation in memory or on disk + auto findAni = m_server->getAnimationManager().findOrAddResource(ganiFile); + if (!findAni) + { + //printf("Client requested gani %s, but was not found\n", ganiFile.c_str()); + return HandlePacketResult::Handled; + } + + // Compare the bytecode checksum from the client with the one for the + // current script, if it doesn't match send the updated bytecode + if (calculateCrc32Checksum(findAni->getByteCode()) != checksum) + sendPacket(findAni->getBytecodePacket()); + + // Tell the client to load the gani. + sendPacket(CString() >> (char)PLO_LOADGANI >> (char)gani.length() << gani << "\"SETBACKTO " << findAni->getSetBackTo() << "\""); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_UPDATESCRIPT(CString& pPacket) +{ + CString weaponName = pPacket.readString(""); + + if (auto weaponObj = m_server->getWeapon(weaponName.toString()); weaponObj != nullptr) + weaponObj->sendByteCodeToPlayer(shared_from_this()); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerClient::msgPLI_UPDATECLASS(CString& pPacket) +{ + // Get the checksum and class name. + uint32_t checkSum = pPacket.readGInt5(); + std::string className = pPacket.readString("").toString(); + + if (!m_server->hasNPCServer()) + return HandlePacketResult::Handled; + + auto npcServer = m_server->getNPCServer(); + if (auto classObj = npcServer->getClass(className).lock(); classObj != nullptr) + { + if (classObj->getCheckSum() == checkSum) + return HandlePacketResult::Handled; + + CString classPacket = classObj->getClassPacket(); + sendPacket(CString() >> (char)PLO_RAWDATA >> (int)classPacket.length()); + sendPacket(classPacket); + } + else + { + std::vector headerData; + headerData.push_back("class"); + headerData.push_back(className); + headerData.push_back('1'); + headerData.push_back(CString() >> (long long)0 >> (long long)0); + headerData.push_back(CString() >> (long long)0); + CString gstr = utilities::retokenizeCStringArray(headerData); + + // Should technically be PLO_LOADSCRIPT but for some reason the client breaks player.join() scripts + // if a weapon decides to request an class that doesnt exist on the server. This seems to fix it by + // sending an empty bytecode + sendPacket(CString() >> (char)PLO_NPCWEAPONSCRIPT >> (short)gstr.length() << gstr); + } + + return HandlePacketResult::Handled; +} + +/////////////////////////////////////////////////////////////////////////////// + +HandlePacketResult PlayerClient::msgPLI_UPDATEPACKAGEREQUESTFILE(CString& pPacket) +{ + CString packageName = pPacket.readChars(pPacket.readGUChar()); + + // 1 -> Install, 2 -> Reinstall + unsigned char installType = pPacket.readGUChar(); + CString fileChecksums = pPacket.readString(""); + + // If this is a reinstall, we need to download everything so clear the checksum data + if (installType == 2) + fileChecksums.clear(); + + auto totalDownloadSize = 0; + std::vector missingFiles; + + { + auto updatePackage = m_server->getPackageManager().findOrAddResource(packageName.toString()); + if (updatePackage) + { + for (const auto& [fileName, entry] : updatePackage->getFileList()) + { + // Compare the checksum for each file entry if the checksum is provided + bool needsFile = true; + if (fileChecksums.bytesLeft() >= 5) + { + uint32_t userFileChecksum = fileChecksums.readGUInt5(); + if (entry.checksum == userFileChecksum) + needsFile = false; + } + + if (needsFile) + { + totalDownloadSize += entry.size; + missingFiles.push_back(fileName); + } + } + } + } + + sendPacket(CString() >> (char)PLO_UPDATEPACKAGESIZE >> (char)packageName.length() << packageName >> (long long)totalDownloadSize); + + for (const auto& wantFile : missingFiles) + this->sendFile(wantFile); + + sendPacket(CString() >> (char)PLO_UPDATEPACKAGEDONE << packageName); + return HandlePacketResult::Handled; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/player/packets/PlayerNCPackets.cpp b/server/src/player/packets/PlayerNCPackets.cpp new file mode 100644 index 000000000..97647b88a --- /dev/null +++ b/server/src/player/packets/PlayerNCPackets.cpp @@ -0,0 +1,681 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +HandlePacketResult PlayerNC::msgPLI_RC_CHAT(CString& pPacket) +{ + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_NPCGET(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to get a database npc.", account.name); + return HandlePacketResult::Handled; + } + + // RC3 keeps sending empty packets of this, yet still uses NPCGET to fetch npcs. Maybe its for pinging the server + // for updated level information on database npcs? Just a thought.. + // 5/26/2019 - confirmed, this is the npc-server pinging the gserver. + if (pPacket.bytesLeft()) + { + NPCID npcId = pPacket.readGUInt(); + + auto npc = m_server->getNPC(npcId); + if (npc != nullptr) + { + auto dump = npc->getVariableDump(); + sendPacket(CString() >> (char)PLO_NC_NPCATTRIBUTES << string::toCSV(dump)); + } + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_NPCDELETE(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to delete a database npc.", account.name); + return HandlePacketResult::Handled; + } + + NPCID npcId = pPacket.readGUInt(); + auto npc = m_server->getNPC(npcId); + + if (npc != nullptr && npc->storageType == NPCStorageType::DATABASE) + { + m_server->getNPCServer()->deleteNPC(npcId); + m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_NC_NPCDELETE >> (int)npcId); + + std::string logMsg = std::format("NPC {} deleted by {}", npc->name, account.name); + log::printLine(log::npc, logMsg); + m_server->sendToNC(logMsg); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_NPCRESET(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to reset a database npc.", account.name); + return HandlePacketResult::Handled; + } + + NPCID npcId = pPacket.readGUInt(); + + auto npc = m_server->getNPC(npcId); + if (npc != nullptr && npc->storageType == NPCStorageType::DATABASE) + { + if (auto level = npc->getLevel(); level != nullptr) + { + auto& levelName = level->levelName; + if (auto levelData = level->getStaticLevelDataAtPosition(npc->character.getMapPosition()); levelData != nullptr) + { + CString packet = CString() >> (char)PLO_NPCDEL2 >> (char)levelName.length() << levelName >> (int)npc->id; + m_server->sendPacketToLevelAndPastVisitorsAfter(levelData.get(), npc->lastUpdateTime, packet); + } + } + npc->resetToInitialState(); + npc->scripting.events.addEvent(ScriptEventType::CREATED, source::FromServer()); + + std::string logMsg = std::format("NPC script of {} reset by {}", npc->name, account.name); + log::printLine(log::npc, logMsg); + m_server->sendToNC(logMsg); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_NPCSCRIPTGET(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to get a database npc script.", account.name); + return HandlePacketResult::Handled; + } + + // {160}{INT id}{GSTRING script} + NPCID npcId = pPacket.readGUInt(); + auto npc = m_server->getNPC(npcId); + if (npc != nullptr) + { + std::string tokenizedScript = string::toCSV(npc->getScript().getOriginalSource(), "\n"); + sendPacket(CString() >> (char)PLO_NC_NPCSCRIPT >> (int)npcId << tokenizedScript); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_NPCWARP(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to warp a database npc.", account.name); + return HandlePacketResult::Handled; + } + + NPCID npcId = pPacket.readGUInt(); + PropertyTileCoordinate tileX{ pPacket.readGUChar() / 2.0f }; + PropertyTileCoordinate tileY{ pPacket.readGUChar() / 2.0f }; + std::string npcLevel = pPacket.readString("").trimI().toString(); + + auto npc = m_server->getNPC(npcId); + if (npc == nullptr) + return HandlePacketResult::Handled; + + // Warping to a different level entirely. + if (npcLevel != npc->getLevelName()) + { + if (auto newLevel = m_server->getLoadedLevel(npcLevel, npc->getLevel()); newLevel != nullptr) + npc->warp(newLevel, { tileX.pixelCoordinate, tileY.pixelCoordinate }); + } + // Changing position in the current level. + else + { + npc->sendPropsFromResults( + npc->setPropWith(SetBy::SERVER, tileX.pixelCoordinate), + npc->setPropWith(SetBy::SERVER, tileY.pixelCoordinate) + ); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_NPCFLAGSGET(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to get a database npc flags.", account.name); + return HandlePacketResult::Handled; + } + + NPCID npcId = pPacket.readGUInt(); + auto npc = m_server->getNPC(npcId); + if (npc != nullptr) + { + std::vector flagList; + for (auto& [flag, value] : npc->scripting.variables.store | variables::only_flags) + { + if (value->has() && !value->has() && value->get().value_or(false)) + flagList.push_back(flag); + else if (value->has()) + flagList.push_back(std::format("{}={}", flag, value->get().value_or(std::string{}))); + } + + sendPacket(CString() >> (char)PLO_NC_NPCFLAGS >> (int)npcId << string::toCSV(flagList)); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_NPCSCRIPTSET(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to set a database npc script.", account.name); + return HandlePacketResult::Handled; + } + + NPCID npcId = pPacket.readGUInt(); + CString npcScript = pPacket.readString("").guntokenize(); + + // TODO: Validate permissions + + auto npc = m_server->getNPC(npcId); + if (npc != nullptr) + { + auto lastUpdateTime = npc->lastUpdateTime; + + npc->setScript(npcScript.toStringView()); + m_server->getNPCLoader().saveNPC(npc); + npc->scripting.events.addEvent(ScriptEventType::CREATED, source::FromServer()); + + std::string logMsg = std::format("NPC script of {} updated by {}", npc->name, account.name); + log::printLine(log::npc, logMsg); + m_server->sendToNC(logMsg); + + npc->sendScriptUpdatesToLevel(lastUpdateTime); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_NPCFLAGSSET(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to set a database npc flags.", account.name); + return HandlePacketResult::Handled; + } + + NPCID npcId = pPacket.readGUInt(); + + if (auto npc = m_server->getNPC(npcId); npc != nullptr) + { + std::vector addedFlags; + std::vector updatedFlags; + std::vector deletedFlags; + auto npcFlags = string::fromCSV(pPacket.readString("").toString()); + + // Remove any flags that do not contain an equal sign, as these are not valid flags for NPCs. + std::erase_if(npcFlags, [](std::string& flag) { return !flag.contains('='); }); + + // Go through the existing flags and delete/update them. + auto it = npc->scripting.variables.store.begin(); + while (it != npc->scripting.variables.store.end()) + { + // Ignore temporary variables and non-flag variables. + if (auto var = it->second; var != nullptr && !var->temporary && var->testAsFlag()) + { + auto flagBeingSet = std::ranges::find_if(npcFlags, [&it](std::string& flag) { return flag.starts_with(it->first); }); + + // Not in range, delete it. + if (flagBeingSet == std::ranges::end(npcFlags)) + { + deletedFlags.emplace_back(std::format("flag deleted:\t{}={}", it->first, it->second->get().value_or(std::string{}))); + it = npc->scripting.variables.store.erase(it); + } + // Is in range, check if updated. + else + { + auto existingValue = it->second->get().value_or(std::string{}); + auto equalPos = flagBeingSet->find('='); + std::string flagValue{ string::trimMutate(flagBeingSet->substr(equalPos + 1)) }; + if (existingValue != flagValue) + { + updatedFlags.emplace_back(std::format("flag updated:\t{}={} -> {}", it->first, existingValue, flagValue)); + it->second->assign(flagValue); + npcFlags.erase(flagBeingSet); + ++it; + } + } + } + else ++it; + } + + // Add new flags. + for (std::string_view flag : npcFlags) + { + auto equalPos = flag.find('='); + if (equalPos == std::string::npos) + continue; + + auto flagName = string::trim(flag.substr(0, equalPos)); + auto flagValue = string::trim(flag.substr(equalPos + 1)); + npc->scripting.variables.add(flagName, GameValue{ std::string{ flagValue } }); + addedFlags.emplace_back(std::format("flag added:\t{}={}", flagName, flagValue)); + } + + // Save the NPC. + m_server->getNPCLoader().saveNPC(npc); + + // Announce changes. + CString updateMsg = std::format("NPC flags of {} updated by {}", npc->name, account.name); + m_server->sendToNC(updateMsg); + log::printLine(log::npc, updateMsg); + if (!addedFlags.empty()) + log::printLine(log::npc, string::join(addedFlags, "\n"sv)); + if (!updatedFlags.empty()) + log::printLine(log::npc, string::join(updatedFlags, "\n"sv)); + if (!deletedFlags.empty()) + log::printLine(log::npc, string::join(deletedFlags, "\n"sv)); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_NPCADD(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to add a database npc.", account.name); + return HandlePacketResult::Handled; + } + + if (!m_server->hasNPCServer()) + return HandlePacketResult::Handled; + + auto packetData = pPacket.readString("").toString(); + auto npcData = string::fromCSV(packetData); + auto& npcName = npcData[0]; + auto npcId = string::toNumber(npcData[1]); + auto& npcType = npcData[2]; + auto& npcScripter = npcData[3]; + auto& npcLevel = npcData[4]; + auto npcX = string::toFloat(npcData[5]); + auto npcY = string::toFloat(npcData[6]); + + if (npcName.empty()) + { + m_server->sendToNC("Error adding database npc: NPC name cannot be empty"); + return HandlePacketResult::Handled; + } + + auto level = m_server->getLoadedLevelNoHint(npcLevel); + if (level == nullptr) + { + m_server->sendToNC("Error adding database npc: Level does not exist"); + return HandlePacketResult::Handled; + } + + if (npcId < NPCID_GEN_MANUAL) + { + m_server->sendToNC(std::format("Error adding database npc: NPC ID must be greater than {}", NPCID_GEN_MANUAL)); + return HandlePacketResult::Handled; + } + + if (m_server->getNPC(npcId) != nullptr) + { + m_server->sendToNC("Error adding database npc: NPC ID already exists"); + return HandlePacketResult::Handled; + } + + auto newNPC = m_server->getNPCServer()->addNPC(npcName, npcId, npcType, npcScripter, level, { npcX, npcY }); + if (newNPC != nullptr) + { + // Persist NPC + m_server->getNPCLoader().saveNPC(newNPC); + + // Logging + std::string logMsg = std::format("NPC {} added by {}", newNPC->name, account.name); + log::printLine(log::npc, logMsg); + m_server->sendToNC(logMsg); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_CLASSEDIT(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to edit a class.", account.name); + return HandlePacketResult::Handled; + } + + if (!m_server->hasNPCServer()) + return HandlePacketResult::Handled; + + // {112}{class} + CString className = pPacket.readString(""); + if (auto classObj = m_server->getNPCServer()->getClass(className.text()).lock(); classObj != nullptr) + sendPacket(CString() >> (char)PLO_NC_CLASSGET >> (char)className.length() << className << string::toCSV(classObj->getScript().getOriginalSource())); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_CLASSADD(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to add a class.", account.name); + return HandlePacketResult::Handled; + } + + if (!m_server->hasNPCServer()) + return HandlePacketResult::Handled; + + // {113}{CHAR name length}{name}{GSTRING script} + std::string className = pPacket.readChars(pPacket.readGUChar()).toString(); + auto classCode = string::join(string::fromCSV(pPacket.readString("").toString()), "\n"sv); + + bool hasClass = false; + if (auto classObj = m_server->getNPCServer()->getClass(className).lock(); classObj != nullptr) + { + hasClass = true; + classObj->setScript(classCode); + m_server->getNPCServer()->updateClass(className, classCode); + m_server->updateClassForPlayers(classObj); + } + + if (!hasClass) + { + m_server->getNPCServer()->addClass(className, classCode); + m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_NC_CLASSADD << className); + } + + // Logging + std::string logMsg = std::format("Script {} {} by {}", className, (!hasClass ? "added" : "updated"), account.name); + log::printLine(log::npc, logMsg); + m_server->sendToNC(logMsg); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_CLASSDELETE(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to delete a class.", account.name); + return HandlePacketResult::Handled; + } + + if (!m_server->hasNPCServer()) + return HandlePacketResult::Handled; + + std::string className = pPacket.readString("").toString(); + + CString logMsg; + if (m_server->getNPCServer()->deleteClass(className)) + { + m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_NC_CLASSDELETE << className); + logMsg << account.name << " has deleted class " << className << "\n"; + } + else + { + logMsg << "error: " << className << " does not exist on this server!\n"; + } + + // Logging + log::print(log::npc, logMsg.toString()); + m_server->sendToNC(logMsg); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_LOCALNPCSGET(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to view level npcs.", account.name); + return HandlePacketResult::Handled; + } + + // {114}{level} + CString level = pPacket.readString(""); + if (level.isEmpty()) + return HandlePacketResult::Handled; + + if (auto npcLevel = m_server->getLoadedLevelNoHint(level.toString()); npcLevel != nullptr) + { + CString npcDump; + // Variables dump from level mapname (level.nw) + npcDump << "Variables dump from level " << npcLevel->levelName << "\n"; + + for (auto npcId : npcLevel->getNPCs()) + { + auto npc = m_server->getNPC(npcId); + npcDump << "\n" + << string::join(npc->getVariableDump(), "\n") << "\n"; + } + + sendPacket(CString() >> (char)PLO_NC_LEVELDUMP << npcDump.gtokenize()); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_WEAPONLISTGET(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to view the weapon list.", account.name); + return HandlePacketResult::Handled; + } + + // Start our packet. + CString ret; + ret >> (char)PLO_NC_WEAPONLISTGET; + + // Iterate weapon list and send names + for (const auto& [weaponName, weapon] : m_server->getWeaponList()) + { + if (weapon->isDefault()) + continue; + + ret >> (char)weaponName.length() << weaponName; + } + + sendPacket(ret); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_WEAPONGET(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to view a weapon.", account.name); + return HandlePacketResult::Handled; + } + + // {116}{weapon} + CString weaponName = pPacket.readString(""); + auto weapon = m_server->getWeapon(weaponName.toString()); + if (weapon == nullptr || weapon->isDefault()) + { + m_server->sendPacketToType(PLTYPE_ANYNC, CString() >> (char)PLO_RC_CHAT << account.name << " prob: weapon " << weaponName << " doesn't exist"); + return HandlePacketResult::Handled; + } + + std::string script = weapon->getScript().getOriginalSource(); + std::replace(script.begin(), script.end(), '\n', '\xa7'); + + if (getVersion() < NCVER_2_1) + { + sendPacket(CString() >> (char)PLO_NPCWEAPONADD + >> (char)weaponName.length() << weaponName + >> (char)0 >> (char)weapon->image.length() << weapon->image + >> (char)1 >> (short)script.length() + << script); + } + else + { + sendPacket(CString() >> (char)PLO_NC_WEAPONGET + >> (char)weaponName.length() << weaponName + >> (char)weapon->image.length() << weapon->image + << script); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_WEAPONADD(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to add a weapon.", account.name); + return HandlePacketResult::Handled; + } + + // {117}{CHAR weapon length}{weapon}{CHAR image length}{image}{code} + std::string weaponName = pPacket.readChars(pPacket.readGUChar()).toString(); + std::string weaponImage = pPacket.readChars(pPacket.readGUChar()).toString(); + std::string weaponCode = pPacket.readString("").toString(); + + std::replace(weaponCode.begin(), weaponCode.end(), '\xa7', '\n'); + + CString actionTaken; + + // Find Weapon + auto weaponObj = m_server->getWeapon(weaponName); + if (weaponObj != nullptr) + { + // default weapon, don't update! + if (weaponObj->isDefault()) + return HandlePacketResult::Handled; + + // Update Weapon + weaponObj->updateWeapon(std::move(weaponImage), std::move(weaponCode)).saveWeapon(); + + // Update Player-Weapons + m_server->updateWeaponForPlayers(weaponObj); + + actionTaken = "updated"; + } + else + { + // add weapon + auto weapon = std::make_shared(weaponName, std::move(weaponImage), std::move(weaponCode)); + weapon->saveWeapon(); + bool success = m_server->NC_AddWeapon(weapon); + if (success) + actionTaken = "added"; + } + + // TODO(joey): Log message should come before the script is executed + if (!actionTaken.isEmpty()) + { + CString logMsg; + logMsg << "Weapon/GUI-script " << weaponName << " " << actionTaken << " by " << account.name << "\n"; + log::print(log::npc, logMsg.toString()); + m_server->sendToNC(logMsg); + } + + // Send the updated weapon list to the player. + msgPLI_NC_WEAPONLISTGET(pPacket); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_WEAPONDELETE(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to delete a weapon.", account.name); + return HandlePacketResult::Handled; + } + + // {118}{weapon} + CString weaponName = pPacket.readString(""); + + bool deleted = false; + CString logMsg; + if (m_server->NC_DelWeapon(weaponName.toString())) + { + logMsg << "Weapon " << weaponName << " deleted by " << account.name << "\n"; + deleted = true; + } + else + { + logMsg << account.name << " prob: weapon " << weaponName << " doesn't exist\n"; + } + + // Logging + log::print(log::npc, logMsg.toString()); + m_server->sendToNC(logMsg); + + // Send the updated weapon list to the player. + if (deleted) + msgPLI_NC_WEAPONLISTGET(pPacket); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerNC::msgPLI_NC_LEVELLISTGET(CString& pPacket) +{ + if (!isNC()) + { + log::printLine(log::npc, "[Hack] {} attempted to view the level list.", account.name); + return HandlePacketResult::Handled; + } + + // Start our packet. + CString ret; + + auto& levelList = m_server->getLevelList(); + if (!levelList.empty()) + { + for (const auto& level : levelList) + ret << level.second->levelName << "\n"; + } + + sendPacket(CString() >> (char)PLO_NC_LEVELLIST << ret.gtokenize()); + return HandlePacketResult::Handled; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/player/packets/PlayerRCPackets.cpp b/server/src/player/packets/PlayerRCPackets.cpp new file mode 100644 index 000000000..2a70110af --- /dev/null +++ b/server/src/player/packets/PlayerRCPackets.cpp @@ -0,0 +1,1916 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +HandlePacketResult PlayerRC::msgPLI_RC_SERVEROPTIONSGET(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to view the server options.", account.name); + return HandlePacketResult::Handled; + } + + auto settings = m_server->getFileSystemServer().openi(fs::FileCategory::CONFIG, "serveroptions.txt"); + auto options = string::toCSV(settings->readAllLines()); + + // RC will automatically add a newline after the last line, so remove the newline if it exists to prevent an extra blank line from showing up in RC. + if (options.back() == ',') + options.pop_back(); + + sendPacket(CString() >> (char)PLO_RC_SERVEROPTIONSGET << options); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_SERVEROPTIONSSET(CString& pPacket) +{ + if (isClient() || !account.hasRight(PLPERM_SETSERVEROPTIONS)) + { + if (isClient()) + log::printLine(log::rc, "[Hack] {} attempted to set the server options.", account.name); + else + log::printLine(log::rc, "{} attempted to set the server options.", account.name); + + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " is not authorized to change the server options."); + return HandlePacketResult::Handled; + } + + auto& settings = m_server->getSettings(); + CString options = pPacket.readString(""); + + // RC will trim the end of the string, so if the last character is not a comma, add it back in. + if (options[options.length() - 1] != ',') + options << ","; + + options.guntokenizeI(); + + // If they don't have the modify staff account right, prevent them from changing admin-only options. + if (!account.hasRight(PLPERM_MODIFYSTAFFACCOUNT)) + { + std::vector newOptions = options.tokenize("\n"); + options.clear(); + for (auto& newOption : newOptions) + { + CString name = newOption.subString(0, newOption.find("=")); + name.trimI(); + + // See if this command is an admin command. + bool isAdmin = false; + for (const auto& j : AdminServerOptions) + if (name == j) isAdmin = true; + + // If it is an admin command, replace it with the current value. + if (isAdmin) + newOption = CString() << name << " = " << settings.get(name).value_or(""); + + // Add this line back into options. + options << newOption << "\n"; + } + } + + // Save settings. + if (auto file = m_server->getFileSystemServer().openiForWriting(fs::FileCategory::CONFIG, "serveroptions.txt"); file != nullptr) + { + file->clear(); + file->write(options.toStringView()); + } + + // Reload settings. + log::printLine(log::rc, "{} has updated the server options.", account.name); + + // Send RC Information + CString outPacket = CString() >> (char)PLO_RC_CHAT << account.name << " has updated the server options."; + auto& playerList = m_server->getPlayerList(); + for (auto& [pid, player] : playerList) + { + if (player->getType() & PLTYPE_ANYRC) + { + player->sendPacket(outPacket); + + // Send the NC address information. + if (m_server->hasNPCServer()) + m_server->getNPCServer()->sendNCLoginToPlayer(player); + } + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_FOLDERCONFIGGET(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to get the folder config.", account.name); + return HandlePacketResult::Handled; + } + + if (auto file = m_server->getFileSystemServer().open(fs::FileCategory::CONFIG, "foldersconfig.txt"); file != nullptr) + { + auto foldersConfig = string::toCSV(file->readAllLines()); + sendPacket(CString() >> (char)PLO_RC_FOLDERCONFIGGET << foldersConfig); + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_FOLDERCONFIGSET(CString& pPacket) +{ + if (isClient() || !account.hasRight(PLPERM_SETFOLDEROPTIONS)) + { + if (isClient()) + log::printLine(log::rc, "[Hack] {} attempted to set the folder config.", account.name); + else + log::printLine(log::rc, "{} attempted to set the folder config.", account.name); + + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " is not authorized to change the folder config."); + return HandlePacketResult::Handled; + } + + if (auto file = m_server->getFileSystemServer().openForWriting(fs::FileCategory::CONFIG, "foldersconfig.txt"); file != nullptr) + { + file->clear(); + CString folders = pPacket.readString(""); + file->writeLines(string::fromCSV(folders.toStringView())); + } + + log::printLine(log::rc, "{} updated the folder config.", account.name); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << account.name << " updated the folder config."); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_RESPAWNSET(CString& pPacket) +{ + // Deprecated + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_HORSELIFESET(CString& pPacket) +{ + // Deprecated + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_APINCREMENTSET(CString& pPacket) +{ + // Deprecated + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_BADDYRESPAWNSET(CString& pPacket) +{ + // Deprecated + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PLAYERPROPSGET(CString& pPacket) +{ + // Deprecated + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PLAYERPROPSSET(CString& pPacket) +{ + // Deprecated? + + auto p = m_server->getPlayer(pPacket.readGUShort(), PLTYPE_ANYPLAYER); + if (p == nullptr) return HandlePacketResult::Handled; + + if (isClient() || (p->account.name != account.name && !account.hasRight(PLPERM_SETATTRIBUTES)) || (p->account.name == account.name && !account.hasRight(PLPERM_SETSELFATTRIBUTES))) + { + if (isClient()) + log::printLine(log::rc, "[Hack] {} attempted to set a player's properties.", account.name); + else + log::printLine(log::rc, "{} attempted to set a player's properties.", account.name); + + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " is not authorized to set the properties of " << p->account.name); + return HandlePacketResult::Handled; + } + + p->setPropsFromRCPacket(pPacket, this); + m_server->getAccountLoader().saveAccount(p->account); + + log::printLine(log::rc, "{} set the attributes of player {}", account.name, p->account.name); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << account.name << " set the attributes of player " << p->account.name); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_DISCONNECTPLAYER(CString& pPacket) +{ + auto p = m_server->getPlayer(pPacket.readGUShort(), PLTYPE_ANYPLAYER); + if (p == nullptr) return HandlePacketResult::Handled; + + if (isClient() || !account.hasRight(PLPERM_DISCONNECT)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to disconnect {}.", account.name, p->account.name); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " is not authorized to disconnect players."); + return HandlePacketResult::Handled; + } + + CString reason = pPacket.readString(""); + if (!reason.isEmpty()) + log::printLine(log::rc, "{} disconnected {}: {}", account.name, p->account.name, reason.text()); + else + log::printLine(log::rc, "{} disconnected {}.", account.name, p->account.name); + + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << account.name << " disconnected " << p->account.name); + + CString disconnectMessage = CString() << "One of the server administrators, " << account.name << ", has disconnected you"; + if (!reason.isEmpty()) + disconnectMessage << " for the following reason: " << reason; + else + disconnectMessage << "."; + p->sendPacket(CString() >> (char)PLO_DISCMESSAGE << disconnectMessage); + m_server->deletePlayer(p); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_UPDATELEVELS(CString& pPacket) +{ + if (isClient() || !account.hasRight(PLPERM_UPDATELEVEL)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to update levels.", account.name); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " is not authorized to update levels."); + return HandlePacketResult::Handled; + } + + unsigned short levelCount = pPacket.readGUShort(); + for (int i = 0; i < levelCount; ++i) + { + auto levelName = pPacket.readChars(pPacket.readGUChar()).toString(); + auto level = m_server->getLoadedLevelNoHint(levelName); + if (level) level->reload(levelName); + } + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_ADMINMESSAGE(CString& pPacket) +{ + if (isClient() || !account.hasRight(PLPERM_ADMINMSG)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to send an admin message.", account.name); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to send an admin message."); + return HandlePacketResult::Handled; + } + + m_server->sendPacketToAll(CString() >> (char)PLO_RC_ADMINMESSAGE << "Admin " << account.name << ":\xa7" << pPacket.readString(""), { m_id }); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PRIVADMINMESSAGE(CString& pPacket) +{ + if (isClient() || !account.hasRight(PLPERM_ADMINMSG)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to send an admin message.", account.name); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to send an admin message."); + return HandlePacketResult::Handled; + } + + auto p = m_server->getPlayer(pPacket.readGUShort(), PLTYPE_ANYPLAYER); + if (p == nullptr) return HandlePacketResult::Handled; + + p->sendPacket(CString() >> (char)PLO_RC_ADMINMESSAGE << "Admin " << account.name << ":\xa7" << pPacket.readString("")); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_LISTRCS(CString& pPacket) +{ + // Deprecated + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_DISCONNECTRC(CString& pPacket) +{ + // Deprecated + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_APPLYREASON(CString& pPacket) +{ + // Deprecated + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_SERVERFLAGSGET(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to view the server flags.", account.name); + return HandlePacketResult::Handled; + } + + CString ret; + ret >> (char)PLO_RC_SERVERFLAGSGET >> (short)m_server->Scripting.variables.store.size(); + for (const auto& [flag, value] : m_server->Scripting.variables.store | variables::no_temporary) + { + if (auto serialized = m_server->Scripting.variables.serializeModern(flag); serialized.has_value()) + ret >> (char)serialized.value().length() << serialized.value(); + } + + sendPacket(ret); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_SERVERFLAGSSET(CString& pPacket) +{ + if (isClient() || !account.hasRight(PLPERM_SETSERVERFLAGS)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to set the server flags.", account.name); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to set the server flags."); + return HandlePacketResult::Handled; + } + + // Collect the new flags. + std::unordered_map> flagMap; + uint16_t count = pPacket.readGUShort(); + for (auto i = 0; i < count; ++i) + { + std::string flagPair = string::trimMutate(pPacket.readChars(pPacket.readGUChar()).toString()); + if (!flagPair.contains('=')) + flagMap.try_emplace(std::move(flagPair), std::string{}); + else + { + std::string flagValue = string::trimLeftMutate(flagPair.substr(flagPair.find('=') + 1)); + string::trimRightMutate(flagPair.erase(flagPair.find('='))); + flagMap.try_emplace(std::move(flagPair), std::move(flagValue)); + } + } + + std::vector removedFlags; + bool hasNPCServer = m_server->hasNPCServer(); + + // Iterate through all the server flags finding deleted flags and sending changes. + for (auto& [flag, value] : m_server->Scripting.variables.store | variables::no_temporary) + { + auto search = flagMap.find(flag); + + // The server variable is not in the map, so it was deleted. + if (search == flagMap.end()) + removedFlags.emplace_back(flag); + else + // The server variable was changed. + { + if (search->second.empty()) + { + value->unassign(); + value->assign(true); + if (!hasNPCServer || flag.starts_with("serverr.")) + m_server->sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_FLAGSET << search->first); + } + else + { + value->unassign(); + value->assign(search->second); + if (!hasNPCServer || flag.starts_with("serverr.")) + m_server->sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_FLAGSET << search->first << "=" << search->second); + } + flagMap.erase(search); + } + } + + // Delete all the removed flags. + for (const auto& flag : removedFlags) + { + auto& store = m_server->Scripting.variables.store; + if (auto search = store.find(flag); search != store.end() && search->second != nullptr) + { + if (!hasNPCServer || flag.starts_with("serverr.")) + m_server->sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_FLAGDEL << flag); + store.erase(search); + } + } + + // Add the new flags. + for (auto& [flag, value] : flagMap) + { + if (value.empty()) + { + m_server->Scripting.variables.add(flag, true); + if (!hasNPCServer || flag.starts_with("serverr.")) + m_server->sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_FLAGSET << flag); + } + else + { + m_server->Scripting.variables.add(flag, value); + if (!hasNPCServer || flag.starts_with("serverr.")) + m_server->sendPacketToType(PLTYPE_ANYCLIENT, CString() >> (char)PLO_FLAGSET << flag << "=" << value); + } + } + + log::printLine(log::rc, "{} has updated the server flags.", account.name); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << account.name << " has updated the server flags."); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_ACCOUNTADD(CString& pPacket) +{ + if (isClient() || !account.hasRight(PLPERM_MODIFYSTAFFACCOUNT)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to add a new account.", account.name); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to create new accounts."); + return HandlePacketResult::Handled; + } + + std::string acc = pPacket.readChars(pPacket.readGUChar()).toString(); + std::string pass = pPacket.readChars(pPacket.readGUChar()).toString(); + std::string email = pPacket.readChars(pPacket.readGUChar()).toString(); + bool banned = (pPacket.readGUChar() != 0); + bool onlyLoad = (pPacket.readGUChar() != 0); + pPacket.readGUChar(); // Admin level, deprecated. + + Account newAccount; + m_server->getAccountLoader().loadAccount(acc, newAccount); + newAccount.banned = banned; + newAccount.email = email; + newAccount.loadOnly = onlyLoad; + m_server->getAccountLoader().saveAccount(newAccount); + + log::printLine(log::rc, "{} has created a new account: {}", account.name, acc.c_str()); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << account.name << " has created a new account: " << acc); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_ACCOUNTDEL(CString& pPacket) +{ + if (isClient() || !account.hasRight(PLPERM_MODIFYSTAFFACCOUNT)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to delete an account.", account.name); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to delete accounts."); + return HandlePacketResult::Handled; + } + + // Prevent the defaultaccount from being deleted. + CString acc = pPacket.readString(""); + if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); + if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); + if (acc == "defaultaccount") + { + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not allowed to delete the default account."); + return HandlePacketResult::Handled; + } + + // Get the account. + auto fileInfo = m_server->getFileSystemServer().infoi(fs::FileCategory::ACCOUNT, std::format("{}.txt", acc)); + if (fileInfo == nullptr) + return HandlePacketResult::Handled; + + // Remove the account from the file system. + fileInfo->deleteFile(); + log::printLine(log::rc, "{} has deleted the account: {}", account.name, acc.text()); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << account.name << " has deleted the account: " << acc); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_ACCOUNTLISTGET(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to view the account listing.", account.name); + return HandlePacketResult::Handled; + } + + CString name = pPacket.readChars(pPacket.readGUChar()); + std::string conditions = pPacket.readChars(pPacket.readGUChar()).toString(); + + // Fix up name searching. + name.replaceAllI("%", "*"); + if (name.length() == 0) + name = "*"; + + // Start our packet. + CString ret; + ret >> (char)PLO_RC_ACCOUNTLISTGET; + + // Search through all the accounts. + for (auto& fileInfoPtr : m_server->getFileSystemServer().info(fs::FileCategory::ACCOUNT)) + { + auto fileInfo = fileInfoPtr.lock(); + if (fileInfo == nullptr) continue; + + auto accountName = fileInfo->file.stem().generic_string(); + if (accountName.empty()) continue; + if (!string::match(accountName, name.toStringView())) continue; + if (conditions.length() == 0 || m_server->getAccountLoader().checkSearchConditions(accountName, string::splitToVector(conditions, std::string_view(",")))) + ret >> (char)accountName.length() << accountName; + } + + sendPacket(ret); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PLAYERPROPSGET2(CString& pPacket) +{ + auto p = m_server->getPlayer(pPacket.readGUShort(), PLTYPE_ANYPLAYER | PLTYPE_NPCSERVER); + if (p == nullptr) return HandlePacketResult::Handled; + + if (isClient() || !account.hasRight(PLPERM_VIEWATTRIBUTES)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to view the props of player {}.", account.name, p->account.name); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to view player props."); + return HandlePacketResult::Handled; + } + + sendPacket(CString() >> (char)PLO_RC_PLAYERPROPSGET >> (short)p->getId() << p->getPropsForRCPacket()); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PLAYERPROPSGET3(CString& pPacket) +{ + CString acc = pPacket.readChars(pPacket.readGUChar()); + if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); + if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); + + auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); + if (p == nullptr) + { + if (!m_server->getFileSystemServer().hasi(fs::FileCategory::ACCOUNT, std::format("{}.txt", acc.toStringView()))) + return HandlePacketResult::Handled; + + p = std::make_shared(nullptr, 0); + if (!m_server->getAccountLoader().loadAccount(acc.toStringView(), p->account)) + return HandlePacketResult::Handled; + } + + if (isClient() || !account.hasRight(PLPERM_VIEWATTRIBUTES)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to view the props of player {}.", account.name, p->account.name); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to view player props."); + return HandlePacketResult::Handled; + } + + sendPacket(CString() >> (char)PLO_RC_PLAYERPROPSGET >> (short)p->getId() << p->getPropsForRCPacket()); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PLAYERPROPSRESET(CString& pPacket) +{ + CString acc = pPacket.readString(""); + if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); + if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); + + if (isClient() || !account.hasRight(PLPERM_RESETATTRIBUTES)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to reset the account: {}", account.name, acc.text()); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to reset accounts."); + return HandlePacketResult::Handled; + } + + // Get the player. Create a new player if they are offline. + auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); + if (p == nullptr) + { + if (!m_server->getFileSystemServer().hasi(fs::FileCategory::ACCOUNT, std::format("{}.txt", acc.toStringView()))) + return HandlePacketResult::Handled; + + p = std::make_shared(nullptr, 0); + if (!m_server->getAccountLoader().loadAccount(acc.toStringView(), p->account)) + return HandlePacketResult::Handled; + } + + // Save RC stuff. + std::vector adminip = p->account.adminIpRange; + uint32_t rights = p->account.adminRights; + std::vector folders; + std::ranges::copy(p->account.folderList, std::back_inserter(folders)); + + // Reset the player. + m_server->getAccountLoader().loadAccount("defaultaccount", p->account); + p->account.name = acc.toStringView(); + m_server->getAccountLoader().saveAccount(p->account); + + // Add the RC stuff back in. + p->account.adminIpRange = adminip; + p->account.adminRights = rights; + p->account.folderList = folders; + + // Save the account. + m_server->getAccountLoader().saveAccount(p->account); + + // If the player is online, boot him from the server. + if (p->getId() != 0) + { + p->sendPacket(CString() >> (char)PLO_DISCMESSAGE << "Your account was reset by " << account.name); + p->setLoaded(false); // Don't save the account when the player quits. + m_server->deletePlayer(p); + } + + // Log it. + log::printLine(log::rc, "{} has reset the attributes of account: {}", account.name, acc.text()); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << account.name << " has reset the attributes of account: " << acc); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PLAYERPROPSSET2(CString& pPacket) +{ + CString acc = pPacket.readChars(pPacket.readGUChar()); + if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); + if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); + + auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); + if (p == nullptr) + { + if (!m_server->getFileSystemServer().hasi(fs::FileCategory::ACCOUNT, std::format("{}.txt", acc.toStringView()))) + return HandlePacketResult::Handled; + + p = std::make_shared(nullptr, 0); + if (!m_server->getAccountLoader().loadAccount(acc.toStringView(), p->account)) + return HandlePacketResult::Handled; + } + + if (isClient() || (p->account.name != account.name && !account.hasRight(PLPERM_SETATTRIBUTES)) || (p->account.name == account.name && !account.hasRight(PLPERM_SETSELFATTRIBUTES))) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to set a player's properties.", account.name); + else + log::printLine(log::rc, "{} attempted to set a player's properties.", account.name); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " is not authorized to set the properties of " << p->account.name); + return HandlePacketResult::Handled; + } + + // Only people with PLPERM_MODIFYSTAFFACCOUNT can alter the default account. + if (!account.hasRight(PLPERM_MODIFYSTAFFACCOUNT) && acc == "defaultaccount") + { + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to modify the default account."); + return HandlePacketResult::Handled; + } + + p->setPropsFromRCPacket(pPacket, this); + m_server->getAccountLoader().saveAccount(p->account); + log::printLine(log::rc, "{} set the attributes of player {}", account.name, p->account.name); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << account.name << " set the attributes of player " << p->account.name); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_ACCOUNTGET(CString& pPacket) +{ + CString acc = pPacket.readString(""); + if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); + if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); + + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to view the account: {}", account.name, acc.text()); + return HandlePacketResult::Handled; + } + + // Get the player. + auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); + if (p == nullptr) + { + if (!m_server->getFileSystemServer().hasi(fs::FileCategory::ACCOUNT, std::format("{}.txt", acc.toStringView()))) + return HandlePacketResult::Handled; + + p = std::make_shared(nullptr, 0); + if (!m_server->getAccountLoader().loadAccount(acc.toStringView(), p->account)) + return HandlePacketResult::Handled; + } + + sendPacket(CString() >> (char)PLO_RC_ACCOUNTGET >> (char)acc.length() << acc + >> (char)0 // >> (char)password_length << password + >> (char)p->account.email.size() << p->account.email + >> (char)(p->account.banned ? 1 : 0) >> (char)(p->account.loadOnly ? 1 : 0) >> (char)0 // admin level + >> (char)4 << "main" + >> (char)p->account.banLength.size() << p->account.banLength + >> (char)p->account.banReason.size() << p->account.banReason); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_ACCOUNTSET(CString& pPacket) +{ + CString acc = pPacket.readChars(pPacket.readGUChar()); + if (acc.length() == 0) return HandlePacketResult::Handled; + if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); + if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); + + if (isClient() || !account.hasRight(PLPERM_MODIFYSTAFFACCOUNT)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to edit the account: {}", account.name, acc.text()); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to edit accounts."); + return HandlePacketResult::Handled; + } + + std::string pass = pPacket.readChars(pPacket.readGUChar()).toString(); + std::string email = pPacket.readChars(pPacket.readGUChar()).toString(); + bool banned = (pPacket.readGUChar() != 0 ? true : false); + bool loadOnly = (pPacket.readGUChar() != 0 ? true : false); + pPacket.readGUChar(); // admin level + pPacket.readChars(pPacket.readGUChar()); // world + std::string banreason = pPacket.readChars(pPacket.readGUChar()).toString(); + + // Get player. + auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); + if (p == nullptr) + { + if (!m_server->getFileSystemServer().hasi(fs::FileCategory::ACCOUNT, std::format("{}.txt", acc.toStringView()))) + return HandlePacketResult::Handled; + + p = std::make_shared(nullptr, 0); + if (!m_server->getAccountLoader().loadAccount(acc.toStringView(), p->account)) + return HandlePacketResult::Handled; + } + + // Set the new account stuff. + p->account.email = email; + p->account.loadOnly = loadOnly; + if (account.hasRight(PLPERM_BAN)) + { + p->account.banned = banned; + p->account.banReason = banreason; + } + m_server->getAccountLoader().saveAccount(p->account); + + // If the account is currently on RC, reload it. + if (auto pRC = m_server->getPlayer(acc, PLTYPE_ANYRC); pRC) + { + m_server->getAccountLoader().loadAccount(acc.toStringView(), pRC->account); + } + + // If the player was just now banned, kick him off the server. + if (account.hasRight(PLPERM_BAN) && banned && p->getId() != 0) + { + auto reason = string::join(string::fromCSV(banreason), std::string_view{ "\r" }); + + p->setLoaded(false); + p->sendPacket(CString() >> (char)PLO_DISCMESSAGE << account.name << " has banned you. Reason: " << reason); + m_server->deletePlayer(p); + } + + log::printLine(log::rc, "{} has modified the account: {}", account.name, acc.text()); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << account.name << " has modified the account: " << acc); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_CHAT(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to send a message to RC.", account.name); + return HandlePacketResult::Handled; + } + + if (isNC()) + { + // TODO(joey): All RC's with NC support are sending two messages at a time. + // Can use this section for npc-server related commands though. + //m_server->sendToNC(CString(account.character.nickName) << ": " << message); + return HandlePacketResult::Handled; + } + + CString message = pPacket.readString(""); + if (message.isEmpty()) return HandlePacketResult::Handled; + auto words = string::splitToVectorView(message.toStringView()); + + if (words[0].at(0) != '/') + { + m_server->sendToRC(CString(account.character.nickName) << ": " << message); + return HandlePacketResult::Handled; + } + else + { +#ifndef NDEBUG + if (words[0] == "/sendtext") + { + sendPacket(CString() >> (char)PLO_SERVERTEXT << message.subString(10) << "\n"); + } + else +#endif + if (words[0] == "/help" && words.size() == 1) + { + if (auto file = m_server->getFileSystemServer().open(fs::FileCategory::CONFIG, "rchelp.txt"); file != nullptr) + { + for (const auto& line : file->readAllLines()) + sendPacket(CString() >> (char)PLO_RC_CHAT << line); + } + } + else if (words[0] == "/version" && words.size() == 1) + { + sendPacket(CString() >> (char)PLO_RC_CHAT << APP_NAME << " version: " << APP_VERSION); + } + else if (words[0] == "/credits" && words.size() == 1) + { + sendPacket(CString() >> (char)PLO_RC_CHAT << "Programmed by " << APP_CREDITS); + } + else if (words[0] == "/open" && words.size() != 1) + { + message.setRead(0); + message.readString(" "); + CString acc = message.readString(""); + return msgPLI_RC_PLAYERPROPSGET3(CString() >> (char)acc.length() << acc); + } + else if (words[0] == "/openacc" && words.size() != 1) + { + message.setRead(0); + message.readString(" "); + CString acc = message.readString(""); + return msgPLI_RC_ACCOUNTGET(CString() << acc); + } + else if (words[0] == "/opencomments" && words.size() != 1) + { + message.setRead(0); + message.readString(" "); + CString acc = message.readString(""); + return msgPLI_RC_PLAYERCOMMENTSGET(CString() << acc); + } + else if (words[0] == "/openaccess" && words.size() != 1) + { + message.setRead(0); + message.readString(" "); + + CString acc = message.readString(""); + auto pl = m_server->getPlayer(acc, PLTYPE_ANYPLAYER); + if (pl) + sendPacket(CString() >> (char)PLO_SERVERTEXT << "GraalEngine,lister,ban," << pl->account.name << "," << std::to_string(pl->getDeviceId())); + else + { + // TODO: player not logged in, load from offline? + } + } + else if (words[0] == "/openban" && words.size() != 1) + { + message.setRead(0); + message.readString(" "); + CString acc = message.readString(""); + return msgPLI_RC_PLAYERBANGET(CString() << acc); + } + else if (words[0] == "/openrights" && words.size() != 1) + { + message.setRead(0); + message.readString(" "); + CString acc = message.readString(""); + return msgPLI_RC_PLAYERRIGHTSGET(CString() << acc); + } + else if (words[0] == "/reset" && words.size() != 1) + { + message.setRead(0); + message.readString(" "); + CString acc = message.readString(""); + return msgPLI_RC_PLAYERPROPSRESET(CString() << acc); + } + else if (words[0] == "/updatelevel" && words.size() != 1 && account.hasRight(PLPERM_UPDATELEVEL)) + { + for (std::string_view l : string::split(std::string_view{ words[1] })) + { + auto level = m_server->getLoadedLevelNoHint(l); + if (level) + { + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " updated level: " << level->levelName); + log::printLine(log::rc, "{} updated level: {}", account.name, level->levelName); + level->reload(l); + } + } + } + else if (words[0] == "/updatelevelall" && words.size() == 1 && account.hasRight(PLPERM_UPDATELEVEL)) + { + log::print(log::rc, "{} updated all the levels", account.name); + int count = 0; + auto& levels = m_server->getLevelList(); + for (auto& [name, level] : levels) + { + level->reload(MapPosition{}); + ++count; + } + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " updated all the levels (" << CString((int)count) << " levels updated)."); + log::printLine(log::rc, " ({} levels updated).", count); + } + else if (words[0] == "/restartserver" && words.size() == 1 && account.hasRight(PLPERM_MODIFYSTAFFACCOUNT)) + { + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " restarted the server."); + log::printLine(log::rc, "{} restarted the server.", account.name); + m_server->restart(); + } + else if (words[0] == "/reloadserver" && words.size() == 1 && account.hasRight(PLPERM_MODIFYSTAFFACCOUNT)) + { + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " reloaded the server configuration files."); + log::printLine(log::rc, "{} reloaded the server configuration files.", account.name); + m_server->loadConfigFiles(); + } + else if (words[0] == "/updateserverhq" && words.size() == 1 && account.hasRight(PLPERM_MODIFYSTAFFACCOUNT)) + { + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " sent ServerHQ updates."); + log::printLine(log::rc, "{} sent ServerHQ updates.", account.name); + m_server->loadAdminSettings(); + m_server->getServerList().sendServerHQ(); + } + else if (words[0] == "/serveruptime" && words.size() == 1) + { + auto time_diff = std::chrono::system_clock::now() - m_server->getServerStartTime(); + + constexpr auto format_time_fn = [](std::string& m, const uint64_t t, const char* fmtStr) + { + if (t > 0) + { + m.append(std::format(" {} {}", t, fmtStr)); + if (t > 1) + m.append("s"); + } + }; + + auto days = std::chrono::duration_cast(time_diff).count(); + auto hours = std::chrono::duration_cast(time_diff).count() % 24; + auto minutes = std::chrono::duration_cast(time_diff).count() % 60; + auto seconds = std::chrono::duration_cast(time_diff).count() % 60; + + std::string msg; + format_time_fn(msg, days, "day"); + format_time_fn(msg, hours, "hour"); + format_time_fn(msg, minutes, "minute"); + if (days == 0) + format_time_fn(msg, seconds, "second"); + + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server Uptime:" << msg); + } + else if (words[0] == "/savenpcs" && words.size() == 1) + { + if (m_server->hasNPCServer()) + { + m_server->getNPCServer()->saveNPCs(); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " saved all npcs to disk."); + log::printLine(log::npc, "{} saved the npcs to disk.", account.name); + } + } + else if (words[0] == "/stats" && words.size() == 1) + { + // TODO(NPCSERVER): Execution stats. + //auto npcStats = m_server->calculateNPCStats(); + + sendPacket(CString() >> (char)PLO_RC_CHAT << "Top scripts using the most execution time (in the last min)"); + + /* + int idx = 0; + for (auto it = npcStats.begin(); it != npcStats.end(); ++it) + { + idx++; + sendPacket(CString() >> (char)PLO_RC_CHAT << CString(idx) << ". " << CString((*it).first) << " " << (*it).second); + if (idx == 50) + break; + } + */ + } + else if (words[0] == "/find" && words.size() > 1) + { + std::map found; + + // Assemble the search string. + CString search(words[1]); + for (unsigned int i = 2; i < words.size(); ++i) + search << " " << words[i]; + + std::vector categories; + for (auto& fileInfoPtr : m_server->getFileSystem().info(fs::FileCategory::ALL)) + { + auto fileInfo = fileInfoPtr.lock(); + if (fileInfo == nullptr) continue; + + CString fileName = fs::getANSIFileName(fileInfo->file); + if (fileName.match(search)) + { + categories.clear(); + if (fileInfo->categories.test(ENUM(fs::FileCategory::FILE))) + categories.push_back("file"); + if (fileInfo->categories.test(ENUM(fs::FileCategory::LEVEL))) + categories.push_back("level"); + if (fileInfo->categories.test(ENUM(fs::FileCategory::HEAD))) + categories.push_back("head"); + if (fileInfo->categories.test(ENUM(fs::FileCategory::BODY))) + categories.push_back("body"); + if (fileInfo->categories.test(ENUM(fs::FileCategory::SWORD))) + categories.push_back("sword"); + if (fileInfo->categories.test(ENUM(fs::FileCategory::SHIELD))) + categories.push_back("shield"); + + found[fileName] = string::join(categories); + } + } + + // Return a list of files found. + for (auto i = found.begin(); i != found.end(); ++i) + { + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: File found (" << search << "): " << i->first << " [" << i->second << "]"); + } + + // No files found. + if (found.size() == 0) + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: No files found matching: " << search); + } + else if (words[0] == "/synctranslation") + { + auto translationManager = BabyDI::Get(); + if (words.size() == 1) + { + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " is synchronizing translations."); + for (const auto& [language, added, removed] : translationManager->syncAllLanguagesWithOriginal()) + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << std::format("Server: '{}' translation synchronized: {} added, {} removed.", language, added, removed)); + } + else + { + auto [actualLanguage, added, removed] = translationManager->syncLanguageWithOriginal(words[1]); + if (actualLanguage.empty()) + sendPacket(CString() >> (char)PLO_RC_CHAT << std::format("Server: Could not find translation language '{}'.", words[1])); + else m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << std::format("Server: {} synchronized the '{}' translation: {} added, {} removed.", account.name, actualLanguage, added, removed)); + } + } + else if (words[0] == "/generatetranslationstubs" && words.size() == 1) + { + auto translationManager = BabyDI::Get(); + auto count = translationManager->generateAllLanguageStubs(); + if (count != 0) + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << std::format(" generated stubs for {} languages.", count)); + else m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << "Server: " << account.name << " tried to generate language stubs, but there was a failure."); + } + // Try to send to the control-NPC. + else if (m_server->hasNPCServer()) + { + words[0].remove_prefix(1); // Remove the slash. + words.insert(words.begin(), "rcchat"sv); + m_server->getNPCServer()->addEventToControlNPC(ScriptEventType::CUSTOM, source::FromPlayer(m_id), words); + } + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_WARPPLAYER(CString& pPacket) +{ + if (isClient() || !account.hasRight(PLPERM_WARPTOPLAYER)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to warp a player.", account.name); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to warp players.\n"); + return HandlePacketResult::Handled; + } + + auto p = m_server->getPlayer(pPacket.readGUShort(), PLTYPE_ANYPLAYER); + if (p == nullptr) return HandlePacketResult::Handled; + + Position pos = { static_cast(pPacket.readGChar() * 8), static_cast(pPacket.readGChar() * 8) }; + CString wLevel = pPacket.readString(""); + p->warp(wLevel, pos); + + log::printLine(log::rc, "{} has warped {} to {} ({:.2f}, {:.2f})", account.name, p->account.name, wLevel.text(), pos.x() / 16.0f, pos.y() / 16.0f); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PLAYERRIGHTSGET(CString& pPacket) +{ + CString acc = pPacket.readString(""); + if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); + if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); + + if (isClient() || (acc != account.name && !account.hasRight(PLPERM_SETRIGHTS))) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to get the rights of {}", account.name, acc.text()); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to view that player's rights."); + return HandlePacketResult::Handled; + } + + // Get player. + auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); + if (p == nullptr) + { + if (!m_server->getFileSystemServer().hasi(fs::FileCategory::ACCOUNT, std::format("{}.txt", acc.toStringView()))) + return HandlePacketResult::Handled; + + p = std::make_shared(nullptr, 0); + if (!m_server->getAccountLoader().loadAccount(acc.toStringView(), p->account)) + return HandlePacketResult::Handled; + } + + // Get the folder list. + auto folders = string::toCSV(p->account.folderList); + + // Send the packet. + auto adminIp = string::join(p->account.adminIpRange); + sendPacket(CString() >> (char)PLO_RC_PLAYERRIGHTSGET >> (char)acc.length() << acc >> (long long)p->account.adminRights >> (char)adminIp.size() << adminIp >> (short)folders.length() << folders); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PLAYERRIGHTSSET(CString& pPacket) +{ + CString acc = pPacket.readChars(pPacket.readGUChar()); + if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); + if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); + + if (isClient() || !account.hasRight(PLPERM_SETRIGHTS)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to set the rights of {}", account.name, acc.text()); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to set player rights."); + return HandlePacketResult::Handled; + } + + // Get player. + auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); + if (p == nullptr) + { + if (!m_server->getFileSystemServer().hasi(fs::FileCategory::ACCOUNT, std::format("{}.txt", acc.toStringView()))) + return HandlePacketResult::Handled; + + p = std::make_shared(nullptr, 0); + if (!m_server->getAccountLoader().loadAccount(acc.toStringView(), p->account)) + return HandlePacketResult::Handled; + } + + // Don't allow RCs to give rights that they don't have. + // Only affect people who don't have PLPERM_MODIFYSTAFFACCOUNT. + int n_adminRights = (int)pPacket.readGUInt5(); + if (!account.hasRight(PLPERM_MODIFYSTAFFACCOUNT)) + { + for (int i = 0; i < 20; ++i) + { + if ((account.adminRights & (1 << i)) == 0) + n_adminRights &= ~(1 << i); + } + } + + // Don't allow you to remove your own PLPERM_MODIFYSTAFFACCOUNT or PLPERM_SETRIGHTS. + if (string::equalsi(p->account.name, this->account.name)) + { + if ((n_adminRights & PLPERM_MODIFYSTAFFACCOUNT) == 0) + n_adminRights |= PLPERM_MODIFYSTAFFACCOUNT; + if ((n_adminRights & PLPERM_SETRIGHTS) == 0) + n_adminRights |= PLPERM_SETRIGHTS; + } + + int changed_rights = account.adminRights ^ n_adminRights; + p->account.adminRights = n_adminRights; + + std::string adminIp = pPacket.readChars(pPacket.readGUChar()).toString(); + p->account.adminIpRange = string::splitToVector(adminIp, ","sv); + + // Untokenize and load the directories. + std::vector folders = string::fromCSV(pPacket.readChars(pPacket.readGUShort()).toString()); + + // Remove any invalid directories. + for (auto i = folders.begin(); i != folders.end();) + { + if ((*i).find(":") != std::string::npos || (*i).find("..") != std::string::npos || (*i).find(" /*") != std::string::npos) + i = folders.erase(i); + else + ++i; + } + + // Assign the directories to the account. + p->account.folderList = folders; + + // Save the account. + m_server->getAccountLoader().saveAccount(p->account); + + // If the account is currently on RC, reload it. + if (auto pRC = m_server->getPlayer(acc, PLTYPE_ANYRC); pRC) + { + std::string nickname = pRC->account.character.nickName; + m_server->getAccountLoader().loadAccount(acc.toStringView(), pRC->account); + pRC->account.character.nickName = nickname; + + if (changed_rights & PLPERM_NPCCONTROL) + { + if (!(n_adminRights & PLPERM_NPCCONTROL)) + { + if (auto pNC = m_server->getPlayer(acc, PLTYPE_ANYNC); pNC) + pNC->disconnect(); + } + else if (m_server->hasNPCServer()) + { + m_server->getNPCServer()->sendNCLoginToPlayer(pRC); + } + } + + // If they are using the File Browser, reload it. + if (pRC->isUsingFileBrowser()) + pRC->msgPLI_RC_FILEBROWSER_START(CString() << ""); + } + + log::printLine(log::rc, "{} has set the rights of {}", account.name, acc.text()); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << account.name << " has set the rights of " << acc); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PLAYERCOMMENTSGET(CString& pPacket) +{ + CString acc = pPacket.readString(""); + if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); + if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); + + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to get the comments of {}", account.name, acc.text()); + return HandlePacketResult::Handled; + } + + // Get player. + auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); + if (p == nullptr) + { + if (!m_server->getFileSystemServer().hasi(fs::FileCategory::ACCOUNT, std::format("{}.txt", acc.toStringView()))) + return HandlePacketResult::Handled; + + p = std::make_shared(nullptr, 0); + if (!m_server->getAccountLoader().loadAccount(acc.toStringView(), p->account)) + return HandlePacketResult::Handled; + } + + sendPacket(CString() >> (char)PLO_RC_PLAYERCOMMENTSGET >> (char)acc.length() << acc << p->account.comments); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PLAYERCOMMENTSSET(CString& pPacket) +{ + CString acc = pPacket.readChars(pPacket.readGUChar()); + if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); + if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); + + if (isClient() || !account.hasRight(PLPERM_SETCOMMENTS)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to set the comments of {}", account.name, acc.text()); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to set player comments."); + return HandlePacketResult::Handled; + } + + // Get player. + auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); + if (p == nullptr) + { + if (!m_server->getFileSystemServer().hasi(fs::FileCategory::ACCOUNT, std::format("{}.txt", acc.toStringView()))) + return HandlePacketResult::Handled; + + p = std::make_shared(nullptr, 0); + if (!m_server->getAccountLoader().loadAccount(acc.toStringView(), p->account)) + return HandlePacketResult::Handled; + } + + CString comment = pPacket.readString(""); + p->account.comments = comment.toStringView(); + m_server->getAccountLoader().saveAccount(p->account); + + // If the account is currently on RC, reload it. + if (auto pRC = m_server->getPlayer(acc, PLTYPE_ANYRC); pRC) + { + m_server->getAccountLoader().loadAccount(acc.toStringView(), pRC->account); + } + + log::printLine(log::rc, "{} has set the comments of {}", account.name, acc.text()); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << account.name << " has set the comments of " << acc); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PLAYERBANGET(CString& pPacket) +{ + CString acc = pPacket.readString(""); + if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); + if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); + + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to view the ban of {}", account.name, acc.text()); + return HandlePacketResult::Handled; + } + + // Get player. + auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); + if (p == nullptr) + { + if (!m_server->getFileSystemServer().hasi(fs::FileCategory::ACCOUNT, std::format("{}.txt", acc.toStringView()))) + return HandlePacketResult::Handled; + + p = std::make_shared(nullptr, 0); + if (!m_server->getAccountLoader().loadAccount(acc.toStringView(), p->account)) + return HandlePacketResult::Handled; + } + + sendPacket(CString() >> (char)PLO_RC_PLAYERBANGET >> (char)acc.length() << acc >> (char)(p->account.banned ? 1 : 0) << p->account.banReason); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_PLAYERBANSET(CString& pPacket) +{ + CString acc = pPacket.readChars(pPacket.readGUChar()); + if (acc.find("/") != -1) acc.removeI(acc.findl('/') + 1); + if (acc.find("\\") != -1) acc.removeI(acc.findl('\\') + 1); + + if (isClient() || !account.hasRight(PLPERM_BAN)) + { + if (isClient()) log::printLine(log::rc, "[Hack] {} attempted to set the ban of {}", account.name, acc.text()); + sendPacket(CString() >> (char)PLO_RC_CHAT << "Server: You are not authorized to set player bans."); + return HandlePacketResult::Handled; + } + + // Get player. + auto p = m_server->getPlayer(acc, PLTYPE_ANYCLIENT); + if (p == nullptr) + { + if (!m_server->getFileSystemServer().hasi(fs::FileCategory::ACCOUNT, std::format("{}.txt", acc.toStringView()))) + return HandlePacketResult::Handled; + + p = std::make_shared(nullptr, 0); + if (!m_server->getAccountLoader().loadAccount(acc.toStringView(), p->account)) + return HandlePacketResult::Handled; + } + + bool banned = (pPacket.readGUChar() == 0 ? false : true); + CString reason = pPacket.readString(""); + p->account.banned = banned; + p->account.banReason = reason.toStringView(); + m_server->getAccountLoader().saveAccount(p->account); + + // If the account is currently on RC, reload it. + if (auto pRC = m_server->getPlayer(acc, PLTYPE_ANYRC); pRC) + { + m_server->getAccountLoader().loadAccount(acc.toStringView(), pRC->account); + } + + // If the player was just now banned, kick him off the server. + if (banned && p->getId() != 0) + { + p->sendPacket(CString() >> (char)PLO_DISCMESSAGE << account.name << " has banned you. Reason: " << reason.guntokenize().replaceAll("", "\r")); + m_server->deletePlayer(p); + } + + log::printLine(log::rc, "{} has set the ban of {}", account.name, acc.text()); + m_server->sendPacketToType(PLTYPE_ANYRC, CString() >> (char)PLO_RC_CHAT << account.name << " has set the ban of " << acc); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_FILEBROWSER_START(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to open the File Browser.", account.name); + return HandlePacketResult::Handled; + } + + // If the player has no folder rights, don't open the File Browser. + if (account.folderList.size() == 0) + return HandlePacketResult::Handled; + + // Get folder list to send to the client. + auto folders = string::toCSV(account.folderList, true); + + // Send the folder list and the welcome message. + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_DIRLIST << folders); + if (!m_isFtp) sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Welcome to the File Browser."); + + // Create a folder map. + std::map folderMap; + for (auto i = account.folderList.begin(); i != account.folderList.end(); ++i) + { + CString rights("r"); + CString wild("*"); + CString folder(*i); + rights = folder.readString(" ").trim().toLower(); + folder.removeI(0, folder.readPos()); + folder.replaceAllI("\\", "/"); + folder.trimI(); + if (folder[folder.length() - 1] != '/') + { + int pos = folder.findl('/'); + if (pos != -1) + { + wild = folder.subString(pos + 1); + folder.removeI(pos + 1); + } + } + folderMap[folder] << rights << ":" << wild << "\n"; + } + + // See if we can use our lastFolder. If we can't, use the first folder. + if (folderMap.find(account.lastFolderAccessed) == folderMap.end()) + account.lastFolderAccessed = folderMap.begin()->first.toStringView(); + + // We want to end with a path separator. + if (!account.lastFolderAccessed.ends_with('/')) + account.lastFolderAccessed += '/'; + + // Create the file system. + std::filesystem::directory_iterator dirs{ account.lastFolderAccessed }; + + // Construct the file list. + CString files; + std::vector wildcards = folderMap[account.lastFolderAccessed].tokenize("\n"); + for (auto i = wildcards.begin(); i != wildcards.end(); ++i) + { + CString rights = (*i).readString(":"); + CString wildcard = (*i).readString(""); + (*i).setRead(0); + for (auto& dirEntry : dirs) + { + if (!dirEntry.is_regular_file()) continue; + CString fileName = fs::getANSIFileName(dirEntry.path()); + + // See if the file matches the wildcard. + if (!fileName.match(wildcard)) + continue; + + // Add the file now. + auto modTime = clock::to_time_t(toSystemClock(dirEntry.last_write_time())); + CString dir = CString() >> (char)fileName.length() << fileName >> (char)rights.length() << rights >> (long long)dirEntry.file_size() >> (long long)modTime; + files << " " >> (char)dir.length() << dir; + } + } + + // Send packet. + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_DIR >> (char)account.lastFolderAccessed.length() << account.lastFolderAccessed << files); + m_isFtp = true; + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_FILEBROWSER_CD(CString& pPacket) +{ + if (isClient()) return HandlePacketResult::Handled; + + CString newFolder = pPacket.readString(""); + CString newRights, wildcard; + newFolder.setRead(0); + + // Create a folder map. + std::map folderMap; + for (auto i = account.folderList.begin(); i != account.folderList.end(); ++i) + { + CString rights("r"); + CString wild("*"); + CString folder(*i); + rights = folder.readString(" ").trim().toLower(); + folder.removeI(0, folder.readPos()); + folder.replaceAllI("\\", "/"); + folder.trimI(); + if (folder[folder.length() - 1] != '/') + { + int pos = folder.findl('/'); + if (pos != -1) + { + wild = folder.subString(pos + 1); + folder.removeI(pos + 1); + } + } + folderMap[folder] << rights << ":" << wild << "\n"; + } + + // See if newFolder is part of the folder map. + // If it isn't, return. + if (folderMap.find(newFolder) == folderMap.end()) + return HandlePacketResult::Handled; + else + account.lastFolderAccessed = newFolder.toStringView(); + + // We want to end with a path separator. + if (!account.lastFolderAccessed.ends_with('/')) + account.lastFolderAccessed += '/'; + + // Make sure our folder exists. + std::filesystem::path fsPath{ account.lastFolderAccessed }; + std::filesystem::create_directories(fsPath); + std::filesystem::directory_iterator dirs{ fsPath }; + + // Construct the file list. + // file packet: {CHAR name_length}{STRING name}{CHAR rights_length}{STRING rights}{INT5 file_size}{INT5 file_mod_time} + // files: {CHAR file_packet_length}{file_packet}[space]{CHAR file_packet_length}{file_packet}[space] + CString files; + std::vector wildcards = folderMap[account.lastFolderAccessed].tokenize("\n"); + for (auto i = wildcards.begin(); i != wildcards.end(); ++i) + { + CString rights = (*i).readString(":"); + CString wildcard = (*i).readString(""); + (*i).setRead(0); + for (auto& dirEntry : dirs) + { + if (!dirEntry.is_regular_file()) continue; + CString fileName = fs::getANSIFileName(dirEntry.path()); + + // See if the file matches the wildcard. + if (!fileName.match(wildcard)) + continue; + + // Add the file now. + auto size = dirEntry.file_size(); + auto modTime = clock::to_time_t(toSystemClock(dirEntry.last_write_time())); + CString dir = CString() >> (char)fileName.length() << fileName >> (char)rights.length() << rights >> (long long)size >> (long long)modTime; + files << " " >> (char)dir.length() << dir; + } + } + + // Send packet. + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Folder changed to " << account.lastFolderAccessed); + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_DIR >> (char)account.lastFolderAccessed.length() << account.lastFolderAccessed << files); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_FILEBROWSER_END(CString& pPacket) +{ + if (isClient()) return HandlePacketResult::Handled; + m_isFtp = false; + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_FILEBROWSER_DOWN(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to download a file through the File Browser.", account.name); + return HandlePacketResult::Handled; + } + + // Send file. + std::filesystem::path file{ pPacket.readString("").toString()}; + std::filesystem::path lastFolderAccessed{ account.lastFolderAccessed }; + CString checkFile = (lastFolderAccessed / file).generic_string(); + + // Don't let us download/view important files. + if (!account.hasRight(PLPERM_MODIFYSTAFFACCOUNT)) + { + for (const auto& file : ProtectedFiles) + { + if (checkFile == file) + { + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Insufficient rights to download/view " << checkFile); + return HandlePacketResult::Handled; + } + } + } + + this->sendFile(lastFolderAccessed / file); + + log::printLine(log::rc, "{} downloaded file {}", account.name, file.generic_string()); + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Downloaded file " << file.generic_string()); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_FILEBROWSER_UP(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to upload a file through the File Browser.", account.name); + return HandlePacketResult::Handled; + } + + std::filesystem::path file{ pPacket.readChars(pPacket.readGUChar()).toString()}; + std::filesystem::path lastFolderAccessed{ account.lastFolderAccessed }; + CString fileData = pPacket.subString(pPacket.readPos()); + CString checkFile = (lastFolderAccessed / file).generic_string(); + + // Check if this is a protected file. + bool isProtected = false; + size_t fileID = std::numeric_limits::max(); + for (size_t i = 0; i < ImportantFiles.size(); ++i) + { + if (checkFile == ImportantFiles[i]) + { + fileID = i; + isProtected = true; + break; + } + } + + // If this file is protected, see if we have permission to upload this file. + bool hasPermission = true; + if (isProtected) + { + hasPermission = account.hasRight(PLPERM_MODIFYSTAFFACCOUNT); + if (!hasPermission) + { + if (fileID < ImportantFileRights.size()) + hasPermission = account.hasRight(ImportantFileRights[fileID]); + } + } + + // Don't let us upload/overwrite important files. + if (isProtected && !hasPermission) + { + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Insufficent rights to upload " << checkFile); + return HandlePacketResult::Handled; + } + + // See if we are uploading a large file or not. + if (m_rcLargeFiles.find(file) == m_rcLargeFiles.end()) + { + // Normal file. Save it and display our message. + fileData.save(checkFile); + + log::printLine(log::rc, "{} uploaded file {}", account.name, file.generic_string()); + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Uploaded file " << file.generic_string()); + } + else + { + // Large file. Store the data in memory. + m_rcLargeFiles[file] << fileData; + } + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_FILEBROWSER_MOVE(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to move a file through the File Browser.", account.name); + return HandlePacketResult::Handled; + } + + std::filesystem::path dir{ pPacket.readChars(pPacket.readGUChar()).toString() }; + std::filesystem::path file{ pPacket.readString("").toString()}; + std::filesystem::path lastFolderAccessed{ account.lastFolderAccessed }; + + // Assemble destination and source. + auto destination = dir / file; + auto source = lastFolderAccessed / file; + + // Don't let us move important files. + for (const auto& importantFile : ImportantFiles) + { + if (source == importantFile) + { + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Not allowed to move file " << source.generic_string()); + return HandlePacketResult::Handled; + } + } + + std::error_code ec; + std::filesystem::rename(source, destination, ec); + + if (ec) + { + log::printLine(log::rc, "Error moving file: {}", ec.message()); + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Error moving file: " << ec.message()); + return HandlePacketResult::Handled; + } + + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Moved file " << source.generic_string() << " to " << destination.generic_string()); + log::printLine(log::rc, "{} moved file {} to {}", account.name, source.generic_string(), destination.generic_string()); + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_FILEBROWSER_DELETE(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to delete a file through the File Browser.", account.name); + return HandlePacketResult::Handled; + } + + CString file = pPacket.readString(""); + std::filesystem::path filePath = std::filesystem::path{ account.lastFolderAccessed } / file.toStringView(); + + // Don't let us delete important files. + CString checkFile = filePath.generic_string(); + for (const auto& file : ImportantFiles) + { + if (checkFile == file) + { + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Not allowed to delete file " << checkFile); + return HandlePacketResult::Handled; + } + } + + // Deleting our logs? First, we need to close them. + if (account.lastFolderAccessed == "logs/") + { + if (file == "rclog.txt") log::rc.close(); + else if (file == "serverlog.txt") + log::server.close(); + else if (file == "npclog.txt") + log::npc.close(); + else if (file == "scriptlog.txt") + log::script.close(); + } + + // Do the deleting. + std::error_code ec; + std::filesystem::remove(filePath, ec); + + // Deleting our logs? We can open them now. + if (account.lastFolderAccessed == "logs/") + { + if (file == "rclog.txt") log::rc.reload(); + else if (file == "serverlog.txt") + log::server.reload(); + else if (file == "npclog.txt") + log::npc.reload(); + else if (file == "scriptlog.txt") + log::script.reload(); + } + + // If we got an error, record it now. + if (ec) + { + log::printLine(log::rc, "Error deleting file: {}", ec.message()); + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Error deleting file: " << ec.message()); + return HandlePacketResult::Handled; + } + + log::printLine(log::rc, "{} deleted file {}", account.name, file.text()); + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Deleted file " << file); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_FILEBROWSER_RENAME(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to rename a file through the File Browser.", account.name); + return HandlePacketResult::Handled; + } + + CString f1 = pPacket.readChars(pPacket.readGUChar()); + CString f2 = pPacket.readChars(pPacket.readGUChar()); + + std::filesystem::path oldPath = std::filesystem::path{ account.lastFolderAccessed } / f1.toStringView(); + std::filesystem::path newPath = std::filesystem::path{ account.lastFolderAccessed } / f2.toStringView(); + + // Don't let us rename/overwrite important files. + CString checkFile1 = oldPath.generic_string(); + CString checkFile2 = newPath.generic_string(); + for (const auto& file : ImportantFiles) + { + if (checkFile1 == file || checkFile2 == file) + { + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Not allowed to rename/overwrite file " << checkFile1 << " or " << checkFile2); + return HandlePacketResult::Handled; + } + } + + // Renaming our logs? First, we need to close them. + if (account.lastFolderAccessed == "logs/") + { + if (f1 == "rclog.txt") log::rc.close(); + else if (f1 == "serverlog.txt") + log::server.close(); + else if (f1 == "npclog.txt") + log::npc.close(); + else if (f1 == "scriptlog.txt") + log::script.close(); + } + + // Do the renaming. + std::error_code ec; + std::filesystem::rename(oldPath, newPath, ec); + + // Renaming our logs? We can open them now. + if (account.lastFolderAccessed == "logs/") + { + if (f1 == "rclog.txt") log::rc.reload(); + else if (f1 == "serverlog.txt") + log::server.reload(); + else if (f1 == "npclog.txt") + log::npc.reload(); + else if (f1 == "scriptlog.txt") + log::script.reload(); + } + + // If we got an error, record it now. + if (ec) + { + log::printLine(log::rc, "Error renaming file: {}", ec.message()); + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Error renaming file: " << ec.message()); + return HandlePacketResult::Handled; + } + + log::printLine(log::rc, "{} renamed file {} to {}", account.name, f1.text(), f2.text()); + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Renamed file " << f1 << " to " << f2); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_LARGEFILESTART(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} is attempting to upload a file through the File Browser.", account.name); + return HandlePacketResult::Handled; + } + + std::filesystem::path file{ pPacket.readString("").toString() }; + m_rcLargeFiles[file] = CString(); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_LARGEFILEEND(CString& pPacket) +{ + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to upload a file through the File Browser.", account.name); + return HandlePacketResult::Handled; + } + + std::filesystem::path file{ pPacket.readString("").toString() }; + std::filesystem::path filePath = std::filesystem::path{ account.lastFolderAccessed } / file; + + // Save the file. + m_rcLargeFiles[file].save(filePath.string()); + + // Remove the data from memory. + for (auto it = m_rcLargeFiles.begin(); it != m_rcLargeFiles.end(); ++it) + { + if (it->first == file) + { + m_rcLargeFiles.erase(it); + break; + } + } + + log::printLine(log::rc, "{} uploaded large file {}", account.name, file.generic_string()); + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Uploaded large file " << file.generic_string()); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_FOLDERDELETE(CString& pPacket) +{ + std::filesystem::path folder{ pPacket.readString("").toString() }; + if (isClient()) + { + log::printLine(log::rc, "[Hack] {} attempted to delete a folder through the File Browser: {}", account.name, folder.generic_string()); + return HandlePacketResult::Handled; + } + + // Try to remove folder. + if (!std::filesystem::remove(folder)) + { + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Error removing " << folder.generic_string() << ". Folder may not exist or may not be empty."); + return HandlePacketResult::Handled; + } + + log::printLine(log::rc, "{} removed folder {}", account.name, folder.generic_string()); + sendPacket(CString() >> (char)PLO_RC_FILEBROWSER_MESSAGE << "Folder " << folder.generic_string() << " has been removed.\n"); + msgPLI_RC_FILEBROWSER_START(CString() << ""); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_NPCSERVERQUERY(CString& pPacket) +{ + if (!m_server->hasNPCServer()) + return HandlePacketResult::Handled; + + // Read Packet Data + [[maybe_unused]] PlayerID pid = static_cast(pPacket.readGUShort()); + CString message = pPacket.readString(""); + + // Enact upon the message. + if (message == "location") + m_server->getNPCServer()->sendNCLoginToPlayer(shared_from_this()); + else + log::printLine(log::server, "[RC] Received unknown PLI_NPCSERVERQUERY message: {}", message); + + return HandlePacketResult::Handled; +} + +HandlePacketResult PlayerRC::msgPLI_RC_UNKNOWN162(CString& pPacket) +{ + // Stub. + return HandlePacketResult::Handled; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/scripting/GS2ScriptManager.cpp b/server/src/scripting/GS2ScriptManager.cpp index 53b3bcad2..2ccb8ed67 100644 --- a/server/src/scripting/GS2ScriptManager.cpp +++ b/server/src/scripting/GS2ScriptManager.cpp @@ -1,4 +1,17 @@ -#include "scripting/GS2ScriptManager.h" +#include +#include +#include +#include +#include + +#include + +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// const uint32_t THREADPOOL_WORKERS = 0; @@ -7,15 +20,9 @@ GS2ScriptManager::GS2ScriptManager() { } -void GS2ScriptManager::compileScript(const std::string& script, user_callback_type finishedCb) +std::future GS2ScriptManager::compileScript(const std::string& script, user_callback_type finishedCb) { - // Check to see if we already compiled this code before - auto cacheSearch = m_bytecodeCache.find(script); - if (cacheSearch != m_bytecodeCache.end()) - { - finishedCb(cacheSearch->second); - return; - } + return queueCompileJob(script, finishedCb); // Disabling any async functionality for now, npcs should be compiled during level-loading // and level should not be sent until all the npcs are finished compiling. Can't really @@ -23,45 +30,35 @@ void GS2ScriptManager::compileScript(const std::string& script, user_callback_ty // we can migrate to using the threadpool for script compilations and delay sending levels // until we finish loading the level. We could also switch to some eager-level-loading method, // preloading any levels that are links from other levels or listed in a loaded map etc.. - - // Queue a job to compile this script - // queueCompileJob(script, finishedCb); - - // Synchronously compile script - syncCompileJob(script, finishedCb); } -void GS2ScriptManager::syncCompileJob(const std::string& script, user_callback_type& finishedCb) +std::future GS2ScriptManager::queueCompileJob(const std::string& script, user_callback_type& finishedCb) { - // Compile code - auto result = _context.compile(script); // , "weapon", "TestCode", true); + std::promise promise; - // Insert into bytecode cache - auto ret = m_bytecodeCache.insert({ script, std::move(result) }); - - // Call the user-defined callback after we insert the bytecode into the cache - finishedCb(ret.first->second); -} - -void GS2ScriptManager::queueCompileJob(const std::string& script, user_callback_type& finishedCb) -{ if constexpr (THREADPOOL_WORKERS == 0) { - syncCompileJob(script, finishedCb); - return; + std::promise promise; + auto response = _context.compile(script); + + if (finishedCb) + finishedCb(response); + + promise.set_value(std::move(response)); + return promise.get_future(); } // Worker job - auto threadFunction = [script, finishedCb, this](CallbackThreadJob::thread_context& context, auto& promise) + auto threadFunction = [&promise, script, finishedCb, this](CompiledWithCallbackThreadJob::thread_context& context, CompiledWithCallbackThreadJob::promise_type& badPromise) { // Compile code - auto result = context.gs2context.compile(script); // , "weapon", "TestCode", true); + auto result = context.gs2context.compile(script); // Call the user-defined callback after we insert the bytecode into the cache - auto completedFunc = [this, script, finishedCb](CompilerResponse& response) + auto completedFunc = [this, &promise, &script, &finishedCb](CompilerResponse& response) { - auto ret = m_bytecodeCache.insert({ script, std::move(response) }); - finishedCb(ret.first->second); + finishedCb(response); + promise.set_value(std::move(response)); }; // Create a tuple with the callback, and arguments @@ -71,8 +68,9 @@ void GS2ScriptManager::queueCompileJob(const std::string& script, user_callback_ m_cbQueue.push(std::move(fnData)); }; - // Queue function into threadpool - m_compilerThreadPool.queue(CallbackThreadJob{ std::move(threadFunction) }); + // Don't use the future returned by the compiler because it is bad and horrible. + m_compilerThreadPool.queue(CompiledWithCallbackThreadJob{ std::move(threadFunction) }); + return promise.get_future(); } void GS2ScriptManager::runQueue() @@ -96,3 +94,6 @@ void GS2ScriptManager::runQueue() tmpQueue.pop(); } } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/scripting/Script.cpp b/server/src/scripting/Script.cpp new file mode 100644 index 000000000..6242e4077 --- /dev/null +++ b/server/src/scripting/Script.cpp @@ -0,0 +1,400 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +constexpr std::string_view clientSideTerminator = "//#CLIENTSIDE"sv; + +//---------------------------- + +static std::string performClientSideJoinHack(std::string_view code) +{ + static const std::array blockStarters = { '\n', '\xa7', ')', '{', ';' }; + + std::string result; + std::vector joins; + + size_t start = 0, end = 0; + while (start < code.length()) + { + // Find the next join. + // If we don't find one, copy the rest of the code and break. + end = code.find("join ", start); + if (end == std::string_view::npos) + { + result += code.substr(start); + break; + } + + // Look for a newline or the start of a code block so we don't capture the word join in a string. + bool join_is_start_of_block = true; + if (end != 0) + { + size_t block_start = end - 1; + while (block_start > 0) + { + // Skip any whitespace before the join. + if (code[block_start] == ' ' || code[block_start] == '\t') + { + --block_start; + continue; + } + // Look for the start of a block or a newline. + else if (!std::ranges::contains(blockStarters, code[block_start])) + { + join_is_start_of_block = false; + break; + } + + // We found a new line or a block start. + break; + } + if (!join_is_start_of_block) + { + result += code.substr(start, end); + start = end + 5; // 5 = strlen("join ") + continue; + } + } + + // Copy the code before the join. + result += code.substr(start, end - start); + + // Get the name of the join. + start = end + 5; // 5 = strlen("join ") + end = code.find(";", start); + if (end == std::string_view::npos) + { + result += "join "; + result += code.substr(start); + break; + } + + // Save the join to the list of joins. + std::string_view join = string::trim(code.substr(start, end - start)); + if (!join.empty()) + { + bool in_sign = false; + + // A simple check to make sure we aren't in a say2 sign. + // This won't identify when the join keyword is used as the first word in the last line of a sign. + // TODO: Improve this check. + if (auto line_end = code.find('\n', start); line_end != std::string_view::npos) + { + if (auto sign_check = code.find("#b", start); sign_check < line_end) + in_sign = true; + } + + // If we are in a sign, add the join back in. + if (in_sign) + { + result += "join "; + result += join; + } + // Otherwise, add the join to our joins list. + else + { + joins.push_back(join); + } + + // Make sure the semi-colon stays. + result += ";"; + } + + start = end + 1; + } + + // Load the files and append them to the result. + auto server = BabyDI::Get(); + if (server && server->hasNPCServer()) + { + for (const auto& className : joins) + { + if (auto classObject = server->getNPCServer()->getClass(className).lock(); classObject != nullptr) + { + if (result.back() != '\n') + result += '\n'; + result += classObject->getScript().getClientSide(); + } + } + } + else + { + std::string classScript; + for (const auto& fileName : joins) + { + auto file = server->getFileSystemServer().open(fs::FileCategory::SCRIPTCLASS, std::format("{}.txt", fileName)); + if (file != nullptr) + { + classScript = Script::minify(file->readAsString()); + string::replaceMutate(classScript, "\r", ""); + if (result.back() != '\n') + result += '\n'; + result += classScript; + } + } + } + + return result; +} + +//---------------------------- + +std::generator Script::getServerJoinedClasses() const noexcept +{ + if (m_server_script == nullptr) + co_return; + + for (const auto& kvp : m_server_script->joinedClasses) + { + co_yield kvp; + + // Get child classes too. + if (auto class_ = kvp.second.lock(); class_ != nullptr) + co_yield std::ranges::elements_of(class_->getScript().getServerJoinedClasses()); + } +} + +//---------------------------- + +const ScriptByteCode& Script::getClientByteCode() const noexcept +{ + static ScriptByteCode empty; + + if (m_client_script == nullptr || !m_client_script->script->has_value()) + return empty; + + if (auto* bytecode = std::any_cast(m_client_script->script); bytecode != nullptr) + return *bytecode; + + return empty; +} + +void Script::executeEvents(ScriptContainer& container, ScriptObject source) const +{ + return executeEvents(container.events, source); +} + +void Script::executeEvents(ScriptEventQueue& events, ScriptObject source) const +{ + if (m_server_script == nullptr || m_server_script->engine == nullptr) + return; + + auto* engine = m_server_script->engine; + for (auto& event : events.queue()) + engine->execute(event, source, m_server_script); +} + +void Script::executeEvents(clear_container_t, ScriptContainer& container, ScriptObject source) const +{ + executeEvents(container, source); + container.events.queue().clear(); +} + +void Script::executeEvents(clear_container_t, ScriptEventQueue& events, ScriptObject source) const +{ + executeEvents(events, source); + events.queue().clear(); +} + +bool Script::runUserDefinedFunction(std::string_view functionName, ScriptEvent& event, ScriptObject source) const +{ + if (m_server_script == nullptr || m_server_script->engine == nullptr) + return false; + + auto* engine = m_server_script->engine; + return engine->executeFunction(functionName, event, source, m_server_script); +} + +//---------------------------- + +std::string Script::minify(const std::string& src) noexcept +{ + if (src.empty()) + return src; + + std::string minified; + std::string_view srcView{ src }; + + // We don't want to trim serverside code so check if we have any. + auto server = BabyDI::Get(); + bool hasServerSide = true; + if (server && !server->hasNPCServer()) + hasServerSide = false; + bool inServerSide = true; + + // Trim the lines. + while (!srcView.empty()) + { + // Find the next newline character. + auto newline = srcView.find('\n'); + if (newline == std::string_view::npos) + newline = srcView.size(); + + // Save the start of the next line since the carriage return check will mess it up. + size_t nextline = std::min(srcView.size(), newline + 1); + + // Search for \r and remove that too. + if (newline > 0 && srcView[newline - 1] == '\r') + --newline; + + // Extract the line. + auto line = srcView.substr(0, newline); + + // Remove single-line comments. + // But don't remove //# comments as those are directives. + if (auto comment = line.find("//"); comment != std::string_view::npos) + { + if (comment + 2 < line.size() && line[comment + 2] != '#') + line = line.substr(0, comment); + else if (line.find(clientSideTerminator) != std::string_view::npos) + { + // If we have a clientside terminator, we are now in clientside code. + inServerSide = false; + } + } + + // Trim the line. + if (!hasServerSide || !inServerSide) + line = string::trim(line); + + // Append the line to minified. + if (!line.empty()) + minified.append(line).append("\n"); + + // Move to the next line. + srcView.remove_prefix(nextline); + } + + // Remove multi-line comments from minified. + std::string::size_type start = 0; + while ((start = minified.find("/*", start)) != std::string::npos) + { + auto end = minified.find("*/", start); + if (end == std::string::npos) + break; + minified.erase(start, end - start + 2); + } + + // Final trim. + string::trimMutate(minified); + + // Return the minified code. + return minified; +} + +void Script::split(std::string& source) noexcept +{ + auto determineClientSideLocation = [](std::string& source) -> std::string::iterator + { + auto clientside = source.begin(); + if (auto clientSep = source.find(clientSideTerminator); clientSep != std::string::npos) + std::advance(clientside, clientSep); + else clientside = source.end(); + return clientside; + }; + + // Check if we have an npc-server or not. + // If we don't, we don't have serverside code, and thus we will ignore the clientside terminator. + auto server = BabyDI::Get(); + bool hasServerSide = true; + if (server && !server->hasNPCServer()) + hasServerSide = false; + + // If we have serverside code, find the start of the clientside terminator. + // We need to mangle the newlines on just the clientside code. + // The serverside code will be fed into a compiler so it should have normal line endings. + auto clientside = source.begin(); + if (hasServerSide) + clientside = determineClientSideLocation(source); + + // Do clientside script joins. + if (!hasServerSide && server->getSettings().get("clientsidejoins").value_or(true) && clientside != source.end()) + { + auto joinedScript = performClientSideJoinHack(std::string_view{ clientside, source.end() }); + source.replace(clientside, source.end(), joinedScript); + if (hasServerSide) + clientside = determineClientSideLocation(source); + else clientside = source.begin(); + } + + // Mangle the line terminators. + std::replace(clientside, source.end(), '\n', '\xa7'); + + // If we don't have an npc-server, we don't support serverside code. + if (!hasServerSide) + { + m_serverside = {}; + m_clientside = string::trim(source); + return; + } + + // Split the code into clientside and serverside. + if (auto clientSep = source.find(clientSideTerminator); clientSep != std::string::npos) + { + auto endOfLine = source.find('\xa7', clientSep); + if (endOfLine == std::string::npos) + endOfLine = clientSep + clientSideTerminator.size(); + + m_serverside = string::trim(std::string_view{ source }.substr(0, clientSep)); + m_clientside = {}; + + if (endOfLine + 1 < source.size()) + m_clientside = string::trim(std::string_view{ source }.substr(endOfLine + 1)); + } + else + { + m_serverside = string::trim(source); + m_clientside = {}; + } +} + +void Script::compileScript() noexcept +{ + m_client_script.reset(); + m_server_script.reset(); + + auto server = BabyDI::Get(); + if (server && server->hasNPCServer()) + { + auto npcServer = server->getNPCServer(); + if (server->Generation == ServerGeneration::CLASSIC) + { + m_server_script = npcServer->scripting.getCompiledServerScript(m_who, m_serverside); + } + else if (server->Generation == ServerGeneration::NEWMAIN || server->Generation == ServerGeneration::MODERN) + { + m_client_script = npcServer->scripting.getCompiledClientScript(m_who, m_clientside); + m_server_script = npcServer->scripting.getCompiledServerScript(m_who, m_serverside); + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/scripting/ScriptClass.cpp b/server/src/scripting/ScriptClass.cpp index b50ca8ca9..a751cdbcf 100644 --- a/server/src/scripting/ScriptClass.cpp +++ b/server/src/scripting/ScriptClass.cpp @@ -1,75 +1,80 @@ -#include "BabyDI.h" +#include +#include +#include +#include +#include -#include +#include -#include "scripting/ScriptClass.h" +#include +#include -#include "Server.h" +#include +#include -ScriptClass::ScriptClass(const std::string& className, const std::string& classSource) - : m_className(className) +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal { - parseScripts(classSource); -} +/////////////////////////////////////////////////////////////////////////////// -ScriptClass::~ScriptClass() +ScriptClass::ScriptClass(std::string_view className, std::string_view classScript) + : name(className) { + setScript(classScript); } -void ScriptClass::parseScripts(const std::string& classSource) +ScriptClass& ScriptClass::setScript(std::string_view classScript) { - auto* server = BabyDI::Get(); - bool gs2default = server->getSettings().getBool("gs2default", false); + m_script = { name, classScript }; + + // Set the cryptographic key to be the script's hash. + string::string_hash hash{}; + uint64_t scriptHash = static_cast(hash(classScript)); + + // Package the key into two GYBTE5's. + uint32_t* hashBytes = reinterpret_cast(&scriptHash); + CString key = CString() >> (long long)(hashBytes[0]) >> (long long)(hashBytes[1]); + m_desKey = key.toString(); - m_source = { classSource, gs2default }; + // CRC32 checksum. + m_checksum = crc32(0L, Z_NULL, 0); + m_checksum = crc32(m_checksum, (const uint8_t*)classScript.data(), classScript.length()); - // Compile GS2 code - auto gs2Script = m_source.getClientGS2(); - if (!gs2Script.empty()) + // Create the header. + // [GBYTE2 length_header_and_bytecode] + // [STRING type,name,[0/1 save_to_disk],[GBYTE[10] checksum]] + std::vector headerParts = { - server->compileGS2Script(this, [this](const CompilerResponse& response) - { - if (response.success) - { - auto bytecodeWithHeader = GS2Context::CreateHeader(response.bytecode, "class", m_className, true); - - // these should be sent for compilation right after - //m_joinedClasses = { response.joinedClasses.begin(), response.joinedClasses.end() }; - - m_bytecode.clear(bytecodeWithHeader.length()); - m_bytecode.write((const char*)bytecodeWithHeader.buffer(), static_cast(bytecodeWithHeader.length())); - - // temp: save bytecode to file - //CString bytecodeFile; - //bytecodeFile << server->getServerPath() << "bytecode/classes/"; - //std::filesystem::create_directories(bytecodeFile.text()); - //bytecodeFile << "class_" << m_className << ".gs2bc"; - - //CString bytecodeDump; - //bytecodeDump.writeInt(1); - //bytecodeDump.write((const char*)bytecodeWithHeader.buffer(), bytecodeWithHeader.length()); - //bytecodeDump.save(bytecodeFile); - } - }); - } + "class", + name, + "1", + m_desKey, + (CString() >> (long long)m_checksum).toString() + }; + m_header = string::toCSV(headerParts); + + // Set the modification time to now. + modTime = std::chrono::system_clock::now(); + + // Send out events. + onScriptModified.post(this); + + return *this; } // -- Function: Get Player Packet -- // CString ScriptClass::getClassPacket() const { - CString out; - - if (!m_bytecode.isEmpty()) + if (const auto& bytecode = m_script.getClientByteCode(); !bytecode.empty()) { - CString b = m_bytecode; + const char* bytecodePtr = reinterpret_cast(bytecode.data()); + std::string_view bytecodeView(bytecodePtr, bytecode.size()); - CString header = b.readChars(b.readGUShort()); - - // Get the mod time and send packet 197. - CString smod = CString() >> (long long)time(0); - smod.gtokenizeI(); - out >> (char)PLO_UNKNOWN197 << header << "," << smod << "\n"; + return CString() >> (char)PLO_LOADSCRIPT >> (char)m_header.length() << m_header << bytecodeView; } - return out; + return {}; } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/scripting/ScriptContainers.cpp b/server/src/scripting/ScriptContainers.cpp new file mode 100644 index 000000000..a17131543 --- /dev/null +++ b/server/src/scripting/ScriptContainers.cpp @@ -0,0 +1,357 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////// +// GameValue +//////////////////////////////////////////////////////////// + +GameValue& GameValue::operator=(const GameValue& other) noexcept +{ + if (this != &other) + { + identifier = other.identifier; + temporary = other.temporary; + m_number = other.m_number; + m_text = other.m_text; + m_array = other.m_array; + m_boolean = other.m_boolean; + m_source = other.m_source; + m_getter = other.m_getter; + m_setter = other.m_setter; + } + return *this; +} + +GameValue& GameValue::operator=(GameValue&& other) noexcept +{ + if (this != &other) + { + identifier = std::move(other.identifier); + temporary = other.temporary; + m_number = std::move(other.m_number); + m_text = std::move(other.m_text); + m_array = std::move(other.m_array); + m_boolean = std::move(other.m_boolean); + m_source = std::move(other.m_source); + m_getter = other.m_getter; + m_setter = other.m_setter; + } + return *this; +} + +bool GameValue::operator==(const GameValue& other) noexcept +{ + return (bool)*this == (bool)other; +} + +GameValue::operator bool() const +{ + if (m_getter) + { + std::optional boolval; + if (m_getter(&boolval, std::nullopt); boolval.has_value()) + return *boolval; + + std::optional doubleval; + if (m_getter(&doubleval, std::nullopt); doubleval.has_value()) + return !DoubleIsZero(*doubleval); + } + if (m_boolean.has_value()) + return m_boolean.value(); + if (m_number.has_value()) + return !DoubleIsZero(m_number.value()); + return false; +} + +GameValue GameValue::flatten(int64_t index) const noexcept +{ + if (m_getter) + { + std::optional number; + m_getter(&number, index); + if (number.has_value()) + return *number; + + std::optional> object; + m_getter(&object, index); + if (object.has_value()) + return *object; + } + if (m_array.has_value() && index < (int64_t)m_array->size()) + return (*m_array)[index]; + if (m_source.has_value() && index < (int64_t)m_source->size()) + return (*m_source)[index]; + + return 0.0; +} + +bool GameValue::testAsFlag() const +{ + if (m_getter) + { + std::optional boolval; + m_getter(&boolval, std::nullopt); + if (boolval.has_value()) + return *boolval; + + std::optional stringval; + m_getter(&stringval, std::nullopt); + return (stringval.has_value() && !stringval->empty()); + } + if (m_boolean.has_value()) + return m_boolean.value(); + if (m_text.has_value()) + return !m_text.value().empty(); + return false; +} + +//---------------------------- + +std::optional GameValue::deserialize(const std::string_view line) +{ + if (line.starts_with("FLAG")) + { + auto data = string::trim(line.substr(5)); + auto separator = data.find('='); + if (separator == std::string_view::npos) + return GameValue{ std::string{ string::trim(data) }, true }; + return GameValue{ std::string{ string::trim(data.substr(0, separator)) }, std::string{ string::trim(data.substr(separator + 1)) } }; + } + else if (line.starts_with("VAR")) + { + auto data = string::trim(line.substr(4)); + auto separator = data.find('='); + if (separator == std::string_view::npos) + return std::nullopt; + + auto identifier = string::trim(data.substr(0, separator)); + auto value = string::trim(data.substr(separator + 1)); + if (value.empty()) + return std::nullopt; + if (value[0] != '{') + return GameValue{ std::string{ identifier }, string::toDouble(std::string{ value }) }; + + std::vector array; + for (std::string_view number : string::split(value.substr(1, value.length() - 2), ","sv)) + array.emplace_back(string::toDouble(std::string{ number })); + return GameValue{ std::string{ identifier }, std::move(array) }; + } + + return std::nullopt; +} + +std::optional GameValue::serializeModern(std::string_view name) const noexcept +{ + if (m_boolean.has_value() && !m_text.has_value() && m_boolean.value_or(false) == true) + return std::string{ name }; + if (m_text.has_value()) + return std::format("{}={}", name, m_text.value_or(""s)); + return std::nullopt; +} + + +//////////////////////////////////////////////////////////// +// GameVariableStore +//////////////////////////////////////////////////////////// + +std::weak_ptr GameVariableStore::add(std::string_view name, GameValue&& value) noexcept +{ + auto var = std::make_shared(std::move(value)); + var->identifier = name; + auto [iter, was_inserted] = store.insert_or_assign(var->identifier, var); + return iter->second; +} + +std::weak_ptr GameVariableStore::add(GameValue&& variable) noexcept +{ + if (variable.identifier.empty()) + return {}; + auto var = std::make_shared(std::move(variable)); + auto [iter, was_inserted] = store.insert_or_assign(var->identifier, var); + return iter->second; +} + +bool GameVariableStore::remove(std::string_view name) noexcept +{ + if (store.empty()) return false; + auto it = store.find(name); + if (it == store.end()) return false; + store.erase(it); + return true; +} + +bool GameVariableStore::contains(std::string_view name) const noexcept +{ + if (store.empty()) return false; + return store.contains(name); +} + +std::weak_ptr GameVariableStore::get(std::string_view name) noexcept +{ + if (store.empty()) return {}; + auto it = store.find(name); + if (it == store.end()) return {}; + return it->second; +} + +const std::weak_ptr GameVariableStore::get(std::string_view name) const noexcept +{ + if (store.empty()) return {}; + auto it = store.find(name); + if (it == store.end()) return {}; + return it->second; +} + +std::weak_ptr GameVariableStore::getOrAdd(std::string_view name) noexcept +{ + if (!store.empty()) + { + auto it = store.find(name); + if (it != store.end()) + return it->second; + } + return add(std::move(name), GameValue{ 0.0 }); +} + +GameValue GameVariableStore::getOrStub(std::string_view name) +{ + if (auto var = getOrAdd(name).lock(); var != nullptr) + { + auto getter = [this, variable = var](GameValueVariant incoming, std::optional index) + { + const auto picker = visit_functions + { + [&](std::optional* ptr) { *ptr = variable->get(index); }, + [&](std::optional* ptr) { *ptr = variable->get(index); }, + [&](std::optional* ptr) { *ptr = variable->get(index); }, + [&](std::optional>* ptr) { *ptr = variable->get>(index); }, + [&](std::optional>* ptr) { *ptr = variable->get>(index); } + }; + std::visit(picker, incoming); + }; + + auto setter = [this, variable = var](GameValueVariant incoming, std::optional index) + { + const auto picker = visit_functions + { + [&](std::optional* ptr) { variable->assign(ptr->value_or(false), index); }, + [&](std::optional* ptr) { variable->assign(ptr->value_or(0.0), index); }, + [&](std::optional* ptr) { variable->assign(ptr->value_or(""s), index); }, + [&](std::optional>* ptr) { variable->assign>(ptr->value_or(std::vector{}), index); }, + [&](std::optional>* ptr) { variable->assign>(ptr->value_or(std::vector{}), index); } + }; + std::visit(picker, incoming); + }; + + return GameValue{ name, getter, setter }; + } + throw std::runtime_error("Failed to create variable stub."); +} + +void GameVariableStore::clearTemporary() noexcept +{ + if (store.empty()) return; + std::erase_if(store, [](const auto& pair) { return pair.second->temporary; }); +} + +void GameVariableStore::clearTemporary(std::string_view prefix) noexcept +{ + if (store.empty()) return; + std::erase_if(store, [prefix](const auto& pair) { return pair.second->temporary && pair.first.starts_with(prefix); }); +} + +std::optional GameVariableStore::serializeModern(std::string_view name) const noexcept +{ + auto var = get(name); + if (auto variable = var.lock(); variable != nullptr) + return variable->serializeModern(name); + + return std::nullopt; +} + +std::vector GameVariableStore::serialize(std::string_view name) const noexcept +{ + std::vector results; + auto var = get(name); + if (auto variable = var.lock(); variable != nullptr) + { + if (variable->has() && !variable->has() && variable->get().value_or(false)) + results.emplace_back(std::format("FLAG {}", name)); + else if (variable->has()) + results.emplace_back(std::format("FLAG {}={}", name, variable->serialize())); + + if (variable->has()) + results.emplace_back(std::format("VAR {}={}", name, variable->serialize())); + if (variable->has>()) + results.emplace_back(std::format("VAR {}={}", name, variable->serialize>())); + } + + return results; +} + + +//////////////////////////////////////////////////////////// +// ScriptEventQueue +//////////////////////////////////////////////////////////// + +bool ScriptEventQueue::hasEvent(ScriptEventType type, ScriptObject initiator) +{ + return std::ranges::find_if(m_eventQueue, + [type, initiator](ScriptEvent& event) + { + return event.type == type && event.initiator == initiator && event.args.size() == 0; + }) != m_eventQueue.end(); +} + +void ScriptEventQueue::addEvent(ScriptEventType type, ScriptObject initiator) +{ + if (hasEvent(type, initiator)) + return; + + if (auto* server = BabyDI::Get(); server != nullptr && server->hasNPCServer()) + m_eventQueue.push_back(std::move(ScriptEvent{ .type = type, .initiator = initiator })); +} + +void ScriptEventQueue::addEvent(const ScriptEvent& event) +{ + if (hasEvent(event.type, event.initiator)) + return; + + if (auto* server = BabyDI::Get(); server != nullptr && server->hasNPCServer()) + m_eventQueue.push_back(event); +} + +void ScriptEventQueue::addEvent(ScriptEvent&& event) +{ + if (hasEvent(event.type, event.initiator)) + return; + + if (auto* server = BabyDI::Get(); server != nullptr && server->hasNPCServer()) + m_eventQueue.push_back(std::move(event)); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/scripting/ScriptEngine.cpp b/server/src/scripting/ScriptEngine.cpp deleted file mode 100644 index 2cf67e104..000000000 --- a/server/src/scripting/ScriptEngine.cpp +++ /dev/null @@ -1,433 +0,0 @@ -#ifdef V8NPCSERVER - - #include "EmbeddedBootstrapScript.h" - - #include "NPC.h" - #include "Player.h" - #include "Server.h" - #include "Weapon.h" - #include "scripting/ScriptEngine.h" - #include "scripting/v8/V8ScriptWrappers.h" - -extern const unsigned char JSBOOTSTRAPSCRIPT[]; -extern const size_t JSBOOTSTRAPSCRIPT_SIZE; - -extern void bindGlobalFunctions(ScriptEngine* scriptEngine); -extern void bindClass_Environment(ScriptEngine* scriptEngine); -extern void bindClass_Level(ScriptEngine* scriptEngine); -extern void bindClass_LevelLink(ScriptEngine* scriptEngine); -extern void bindClass_LevelSign(ScriptEngine* scriptEngine); -extern void bindClass_LevelChest(ScriptEngine* scriptEngine); -extern void bindClass_NPC(ScriptEngine* scriptEngine); -extern void bindClass_Player(ScriptEngine* scriptEngine); -extern void bindClass_Server(ScriptEngine* scriptEngine); -extern void bindClass_Weapon(ScriptEngine* scriptEngine); - -ScriptEngine::~ScriptEngine() -{ - this->cleanup(); -} - -bool ScriptEngine::initialize() -{ - if (m_env) - return true; - - CString bootstrapScript; - bootstrapScript.write((const char*)JSBOOTSTRAPSCRIPT, JSBOOTSTRAPSCRIPT_SIZE); - - // bootstrap file print - SCRIPTENV_D("---START SCRIPT---\n%s\n---END SCRIPT\n\n", bootstrapScript.text()); - - // TODO(joey): Clean this the fuck up - m_env = new V8ScriptEnv(); - m_env->initialize(); - - m_env->callFunctionInScope([&]() -> void - { - ScriptEngine* engine = this; - - // Bind global functions - bindGlobalFunctions(engine); - - // Bind classes to be used for scripts - bindClass_Environment(engine); - bindClass_Server(engine); - bindClass_Level(engine); - bindClass_LevelLink(engine); - bindClass_LevelSign(engine); - bindClass_LevelChest(engine); - bindClass_NPC(engine); - bindClass_Player(engine); - bindClass_Weapon(engine); - }); - - // Create a new context (occurs on initial compile) - m_bootstrapFunction = m_env->compile("bootstrap", bootstrapScript.text()); - assert(m_bootstrapFunction); - - // Bind the server into two separate objects - m_environmentObject = ScriptFactory::wrapObject(m_env, "environment", m_server); - m_serverObject = ScriptFactory::wrapObject(m_env, "server", m_server); - - // Execute the bootstrap function - m_env->callFunctionInScope([&]() -> void - { - IScriptArguments* args = ScriptFactory::createArguments(m_env, m_environmentObject.get()); - args->invoke(m_bootstrapFunction); - delete args; - }); - - m_scriptWatcherRunning.store(true); - m_scriptWatcherThread = std::thread(&ScriptEngine::scriptWatcher, this); - - return true; -} - -void ScriptEngine::scriptWatcher() -{ - const std::chrono::milliseconds sleepTime(50); - - while (m_scriptWatcherRunning.load()) - { - if (m_scriptIsRunning.load()) - { - auto time_now = std::chrono::high_resolution_clock::now(); - std::chrono::milliseconds time_diff; - { - std::lock_guard guard(m_scriptWatcherLock); - time_diff = std::chrono::duration_cast(time_now - m_scriptStartTime); - } - - if (time_diff.count() >= 500) - { - m_env->terminateExecution(); - m_scriptIsRunning.store(false); - //printf("Killed execution for running too long!\n"); - } - else if (time_diff.count() < 450) - std::this_thread::sleep_for(sleepTime); - } - else - std::this_thread::sleep_for(sleepTime); - } -} - -void ScriptEngine::cleanup(bool shutDown) -{ - if (!m_env) - { - return; - } - - // Kill script watcher - m_scriptWatcherRunning.store(false); - if (m_scriptWatcherThread.joinable()) - m_scriptWatcherThread.join(); - - // Clear any registered scripts - m_updateNpcs.clear(); - m_updateNpcsTimer.clear(); - m_updateWeapons.clear(); - - // Remove any registered callbacks - for (auto& _callback: m_callbacks) - { - delete _callback.second; - } - m_callbacks.clear(); - - // Remove cached scripts - for (auto& _cachedScript: m_cachedScripts) - { - delete _cachedScript.second; - } - m_cachedScripts.clear(); - - // Remove bootstrap function - if (m_bootstrapFunction) - { - delete m_bootstrapFunction; - m_bootstrapFunction = nullptr; - } - - // Remove script objects - if (m_environmentObject) - { - m_environmentObject.reset(); - } - - if (m_serverObject) - { - m_serverObject.reset(); - } - - // Cleanup the Script Environment - m_env->cleanup(shutDown); - - // Destroy the environment - delete m_env; - m_env = nullptr; -} - -IScriptFunction* ScriptEngine::compileCache(const std::string& code, bool referenceCount) -{ - // TODO(joey): Temporary naming conventions, maybe pass an optional reference to an object which holds info for the compiler (name, ignore wrap code based off spaces/lines, and execution results?) - static int SCRIPT_ID = 1; - - auto scriptFunctionIter = m_cachedScripts.find(code); - if (scriptFunctionIter != m_cachedScripts.end()) - { - if (referenceCount) - scriptFunctionIter->second->increaseReference(); - return scriptFunctionIter->second; - } - - // Compile script, send errors to server - SCRIPTENV_D("Compiling script:\n---\n%s\n---\n", code.c_str()); - - IScriptFunction* compiledScript = m_env->compile(std::to_string(SCRIPT_ID++), code); - if (compiledScript == nullptr) - { - reportScriptException(m_env->getScriptError()); - SCRIPTENV_D("Error Compiling: %s\n", m_env->getScriptError().getErrorString().c_str()); - return nullptr; - } - - SCRIPTENV_D("Successfully Compiled\n"); - - // Increase reference count to compiled script, and cache it. - if (referenceCount) - compiledScript->increaseReference(); - m_cachedScripts[code] = compiledScript; - return compiledScript; -} - -bool ScriptEngine::clearCache(const std::string& code) -{ - auto scriptFunctionIter = m_cachedScripts.find(code); - if (scriptFunctionIter == m_cachedScripts.end()) - return false; - - IScriptFunction* scriptFunction = scriptFunctionIter->second; - scriptFunction->decreaseReference(); - if (!scriptFunction->isReferenced()) - { - m_cachedScripts.erase(scriptFunctionIter); - delete scriptFunction; - } - - return true; -} - - #include "Level.h" - -bool ScriptEngine::executeNpc(NPC* npc) -{ - SCRIPTENV_D("Begin Global::ExecuteNPC()\n\n"); - - // We always want to create an object for the npc - wrapScriptObject(npc); - - // No script, nothing to execute. - auto npcScript = npc->getSource().getServerSide(); - if (npcScript.empty()) - return false; - - // Wrap user code in a function-object, returning some useful symbols to call for events - std::string codeStr = wrapScript(npcScript); - - // Search the cache, or compile the script - IScriptFunction* compiledScript = compileCache(codeStr); - - // Script failed to compile - if (compiledScript == nullptr) - return false; - - // - // Execute the compiled script - // - m_env->callFunctionInScope([&]() -> void - { - IScriptArguments* args = ScriptFactory::createArguments(m_env, npc->getScriptObject()); - bool result = args->invoke(compiledScript, true); - if (!result) - { - auto level = npc->getLevel(); - if (level) - { - std::string exceptionMsg("NPC Exception at "); - - exceptionMsg.append(level->getLevelName().text()); - exceptionMsg.append(","); - exceptionMsg.append(std::to_string(npc->getX() / 16.0)); - exceptionMsg.append(","); - exceptionMsg.append(std::to_string(npc->getY() / 16.0)); - exceptionMsg.append(": "); - if (!npc->getName().empty()) - { - exceptionMsg.append(npc->getName()); - exceptionMsg.append(" - "); - } - exceptionMsg.append(m_env->getScriptError().getErrorString()); - reportScriptException(exceptionMsg); - } - else - reportScriptException(m_env->getScriptError()); - } - delete args; - }); - - SCRIPTENV_D("End Global::ExecuteNPC()\n\n"); - return true; -} - -bool ScriptEngine::executeWeapon(Weapon* weapon) -{ - SCRIPTENV_D("Begin Global::ExecuteWeapon()\n\n"); - - // We always want to create an object for the weapon - wrapScriptObject(weapon); - - auto weaponScript = weapon->getServerScript(); - if (!weaponScript.empty()) - { - // Wrap user code in a function-object, returning some useful symbols to call for events - std::string codeStr = wrapScript(weaponScript); - - // Search the cache, or compile the script - IScriptFunction* compiledScript = compileCache(codeStr); - - // Script failed to compile - if (compiledScript == nullptr) - return false; - - // - // Execute the compiled script - // - m_env->callFunctionInScope([&]() -> void - { - IScriptArguments* args = ScriptFactory::createArguments(m_env, weapon->getScriptObject()); - bool result = args->invoke(compiledScript, true); - if (!result) - reportScriptException(m_env->getScriptError()); - - delete args; - }); - } - - SCRIPTENV_D("End Global::ExecuteWeapon()\n\n"); - return true; -} - -void ScriptEngine::runTimers(const std::chrono::high_resolution_clock::time_point& time) -{ - auto delta_time = time - m_lastScriptTimer; - m_lastScriptTimer = time; - - // Run scripts every 0.05 seconds - constexpr std::chrono::nanoseconds timestep(std::chrono::milliseconds(50)); - m_accumulator += std::chrono::duration_cast(delta_time); - while (m_accumulator >= timestep) - { - m_accumulator -= timestep; - - for (auto it = m_updateNpcsTimer.begin(); it != m_updateNpcsTimer.end();) - { - NPC* npc = *it; - bool hasUpdates = npc->runScriptTimer(); - - if (!hasUpdates) - it = m_updateNpcsTimer.erase(it); - else - it++; - } - } -} - -void ScriptEngine::runScripts(const std::chrono::high_resolution_clock::time_point& time) -{ - runTimers(time); - - if (!m_updateNpcs.empty() || !m_updateWeapons.empty()) - { - m_env->callFunctionInScope([&]() -> void - { - std::map deleteNpcs; - - // Iterate over npcs - for (auto it = m_updateNpcs.begin(); it != m_updateNpcs.end();) - { - NPC* npc = *it; - auto response = npc->runScriptEvents(); - - if (response == NPCEventResponse::PendingEvents) - { - it++; - continue; - } - - if (response == NPCEventResponse::Delete) - deleteNpcs.emplace(npc->getId(), npc); - - it = m_updateNpcs.erase(it); - } - - // Iterate over weapons - for (auto weapon: m_updateWeapons) - weapon->runScriptEvents(); - m_updateWeapons.clear(); - - // Delete any npcs - for (auto n: deleteNpcs) - m_server->deleteNPC(n.first); - }); - } - - // No actions are queued, so we can assume no functions are cached here. - if (!m_deletedCallbacks.empty()) - { - for (auto it = m_deletedCallbacks.begin(); it != m_deletedCallbacks.end();) - { - IScriptFunction* func = *it; - if (!func->isReferenced()) - { - delete func; - it = m_deletedCallbacks.erase(it); - } - else - ++it; - } - } -} - -void ScriptEngine::removeCallBack(const std::string& callback) -{ - auto it = m_callbacks.find(callback); - if (it != m_callbacks.end()) - { - it->second->decreaseReference(); - m_deletedCallbacks.insert(it->second); - m_callbacks.erase(it); - } -} - -void ScriptEngine::setCallBack(const std::string& callback, IScriptFunction* cbFunc) -{ - removeCallBack(callback); - - m_callbacks[callback] = cbFunc; - cbFunc->increaseReference(); -} - -void ScriptEngine::reportScriptException(const ScriptRunError& error) -{ - m_server->reportScriptException(error); -} - -void ScriptEngine::reportScriptException(const std::string& error_message) -{ - m_server->reportScriptException(error_message); -} - -#endif diff --git a/server/src/scripting/ScriptSystem.cpp b/server/src/scripting/ScriptSystem.cpp new file mode 100644 index 000000000..dd890cb6f --- /dev/null +++ b/server/src/scripting/ScriptSystem.cpp @@ -0,0 +1,101 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +void ScriptSystem::registerScriptEngine(std::string_view name, std::shared_ptr engine) +{ + // Check if the engine is already registered. + if (m_script_engines.find(name) != m_script_engines.end()) + log::printLine(log::server, "Script engine '{}' is already registered. Overwriting.", name); + + // Register the script engine. + m_script_engines.insert_or_assign(std::string{ name }, engine); +} + +CompiledScriptResultPtr ScriptSystem::getCompiledClientScript(std::string_view who, std::string_view source) +{ + // Check for empty source. + auto trimmed = string::trim(source); + if (trimmed.empty()) + return nullptr; + + // We are using GS2. + if (auto it = m_script_engines.find("GS2"); it != m_script_engines.end()) + return getCompiledScript(it->second.get(), who, trimmed); + + // Throw at this point. We should always have a GS2 engine. + assert(false); + return nullptr; +} + +CompiledScriptResultPtr ScriptSystem::getCompiledServerScript(std::string_view who, std::string_view source) +{ + // Check for empty source. + auto trimmed = string::trim(source); + if (trimmed.empty()) + return nullptr; + + // Determine the scripting engine to use. + // TODO: What do we do about GS1 mixed with GS2? + std::string script_engine = defaultScriptEngine; + if (trimmed.starts_with("//#")) + { + // Read the line and get the script engine we are going to use. + auto engine = string::extractLine(trimmed).substr(3); + if (!engine.empty()) + script_engine = engine; + } + + // Find the script engine. + if (auto it = m_script_engines.find(script_engine); it != m_script_engines.end()) + return getCompiledScript(it->second.get(), who, trimmed); + + return nullptr; +} + +std::shared_ptr ScriptSystem::getScriptEngine(std::string_view name) const +{ + auto engine = m_script_engines.find(name); + if (engine == m_script_engines.end()) + return nullptr; + + return engine->second; +} + +/////////////////////////////////////////////////////////////////////////////// + +CompiledScriptResultPtr ScriptSystem::getCompiledScript(IScriptEngine* engine, std::string_view who, std::string_view source) +{ + // Check for a cached script. + size_t script_hash = string::string_hash{}(source); + if (auto it = m_script_cache.find(script_hash); it != m_script_cache.end()) + return it->second; + + // Compile the script. + auto result = engine->compileScript(who, source); + if (std::holds_alternative(result)) + { + auto& context = std::get(result); + auto contextPtr = std::make_shared(std::move(context)); + m_script_cache.insert_or_assign(script_hash, contextPtr); + return contextPtr; + } + + return nullptr; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/scripting/gs1/GS1Commands.cpp b/server/src/scripting/gs1/GS1Commands.cpp new file mode 100644 index 000000000..5e7f31a77 --- /dev/null +++ b/server/src/scripting/gs1/GS1Commands.cpp @@ -0,0 +1,3360 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1::grammar +{ +/////////////////////////////////////////////////////////////////////////////// + +using BuiltInCommandHandleFunc = void (*)(GS1Visitor*, std::string_view, const std::vector&); +using BuiltInCommandHandleMap = std::unordered_map; + +#if DEBUG +static void fn_debugger(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +#endif + +static void fn_addguildmember(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_addstring(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_addweapon(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_attachplayertoobj(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_blockagain(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_callnpc(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_canbecarried(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_canbepulled(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_canbepushed(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_cannotbecarried(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_cannotbepulled(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_cannotbepushed(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_cannotwarp(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_canwarp(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_canwarp2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_carryobject(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_changeimgcolors(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_changeimgmode(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_changeimgpart(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_changeimgvis(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_changeimgzoom(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_copylevel(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_copystrings(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_deletelevel(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_deletestring(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_destroy(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_detachplayer(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_disableweapons(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_dontblock(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_drawoverplayer(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_drawovertrees(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_drawunderplayer(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_enableweapons(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_explodebomb(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_freezeplayer2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_hide(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_hideimg(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_hideimgs(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_hitcompu(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_hitnpc(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_hitobjects(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_hitplayer(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_hurt(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_insertstring(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_join(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_lay(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_lay2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_message(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_move(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_noplayeronwall(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_putbomb(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_putcomp(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_putexplosion(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_putexplosion2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_puthorse(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_putnewcomp(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_putnpc(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_putnpc2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_removearrow(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_removebomb(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_removecompus(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_removeexplo(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_removeguild(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_removeguildmember(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_removehorse(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_removeitem(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_removestring(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_removeweapon(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_replacestring(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_saveinfo(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_savelog(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_savelog2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_say(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_say2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_sendpm(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_sendrpgmessage(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_sendtonc(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_sendtorc(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_serverwarp(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_set(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setani(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setarray(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setbeltcolor(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setbody(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setcharani(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setchargender(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setcharprop(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setcoatcolor(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setgender(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_sethead(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setimg(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setimgpart(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setlevel(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setlevel2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setmap(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setminimap(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setplayerdir(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setplayerprop(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setpm(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setshape(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setshield(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setshoecolor(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setshootparams(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setskincolor(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setsleevecolor(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setstring(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setsword(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_setz(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_shoot(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_shootarrow(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_shootball(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_shootfireball(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_shootfireblast(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_shootnuke(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_show(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_showani(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_showani2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_showcharacter(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_showimg(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_showimg2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_showpoly(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_showpoly2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_showstats(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_showtext(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_showtext2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_sleep(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_spyfire(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_take(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_take2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_takehorse(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_takeplayercarry(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_takeplayerhorse(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_throwcarry(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_timershow(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_tokenize(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_tokenize2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_toweapons(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_triggeraction(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_unfreezeplayer(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_unset(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_updateboard(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_updateboard2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_updateterrain(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_warpto(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +// GR extensions +static void fn_enabledamagereactions(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); +static void fn_disabledamagereactions(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments); + +static BuiltInCommandHandleMap GenerateMap() +{ + string::string_hash hash{}; + BuiltInCommandHandleMap map = + { +#if DEBUG + {hash("gr-debugger"), &fn_debugger}, +#endif + {hash("addguildmember"), &fn_addguildmember}, + {hash("addstring"), &fn_addstring}, + {hash("addweapon"), &fn_addweapon}, + {hash("attachplayertoobj"), &fn_attachplayertoobj}, + {hash("blockagain"), &fn_blockagain}, + {hash("callnpc"), &fn_callnpc}, + {hash("canbecarried"), &fn_canbecarried}, + {hash("canbepulled"), &fn_canbepulled}, + {hash("canbepushed"), &fn_canbepushed}, + {hash("cannotbecarried"), &fn_cannotbecarried}, + {hash("cannotbepulled"), &fn_cannotbepulled}, + {hash("cannotbepushed"), &fn_cannotbepushed}, + {hash("cannotwarp"), &fn_cannotwarp}, + {hash("canwarp"), &fn_canwarp}, + {hash("canwarp2"), &fn_canwarp2}, + {hash("carryobject"), &fn_carryobject}, + {hash("changeimgcolors"), &fn_changeimgcolors}, + {hash("changeimgmode"), &fn_changeimgmode}, + {hash("changeimgpart"), &fn_changeimgpart}, + {hash("changeimgvis"), &fn_changeimgvis}, + {hash("changeimgzoom"), &fn_changeimgzoom}, + {hash("copylevel"), &fn_copylevel}, + {hash("copystrings"), &fn_copystrings}, + {hash("deletelevel"), &fn_deletelevel}, + {hash("deletestring"), &fn_deletestring}, + {hash("destroy"), &fn_destroy}, + {hash("detachplayer"), &fn_detachplayer}, + {hash("disableweapons"), &fn_disableweapons}, + {hash("dontblock"), &fn_dontblock}, + {hash("drawoverplayer"), &fn_drawoverplayer}, + {hash("drawovertrees"), &fn_drawovertrees}, + {hash("drawunderplayer"), &fn_drawunderplayer}, + {hash("enableweapons"), &fn_enableweapons}, + {hash("explodebomb"), &fn_explodebomb}, + {hash("freezeplayer2"), &fn_freezeplayer2}, + {hash("hide"), &fn_hide}, + {hash("hideimg"), &fn_hideimg}, + {hash("hideimgs"), &fn_hideimgs}, + {hash("hitcompu"), &fn_hitcompu}, + {hash("hitnpc"), &fn_hitnpc}, + {hash("hitobjects"), &fn_hitobjects}, + {hash("hitplayer"), &fn_hitplayer}, + {hash("hurt"), &fn_hurt}, + {hash("insertstring"), &fn_insertstring}, + {hash("join"), &fn_join}, + {hash("lay"), &fn_lay}, + {hash("lay2"), &fn_lay2}, + {hash("message"), &fn_message}, + {hash("move"), &fn_move}, + {hash("noplayeronwall"), &fn_noplayeronwall}, + {hash("putbomb"), &fn_putbomb}, + {hash("putcomp"), &fn_putcomp}, + {hash("putexplosion"), &fn_putexplosion}, + {hash("putexplosion2"), &fn_putexplosion2}, + {hash("puthorse"), &fn_puthorse}, + {hash("putnewcomp"), &fn_putnewcomp}, + {hash("putnpc"), &fn_putnpc}, + {hash("putnpc2"), &fn_putnpc2}, + {hash("removearrow"), &fn_removearrow}, + {hash("removebomb"), &fn_removebomb}, + {hash("removecompus"), &fn_removecompus}, + {hash("removeexplo"), &fn_removeexplo}, + {hash("removeguild"), &fn_removeguild}, + {hash("removeguildmember"), &fn_removeguildmember}, + {hash("removehorse"), &fn_removehorse}, + {hash("removeitem"), &fn_removeitem}, + {hash("removestring"), &fn_removestring}, + {hash("removeweapon"), &fn_removeweapon}, + {hash("replacestring"), &fn_replacestring}, + {hash("saveinfo"), &fn_saveinfo}, + {hash("savelog"), &fn_savelog}, + {hash("savelog2"), &fn_savelog2}, + {hash("say"), &fn_say}, + {hash("say2"), &fn_say2}, + {hash("sendpm"), &fn_sendpm}, + {hash("sendrpgmessage"), &fn_sendrpgmessage}, + {hash("sendtonc"), &fn_sendtonc}, + {hash("sendtorc"), &fn_sendtorc}, + {hash("serverwarp"), &fn_serverwarp}, + {hash("set"), &fn_set}, + {hash("setani"), &fn_setani}, + {hash("setarray"), &fn_setarray}, + {hash("setbeltcolor"), &fn_setbeltcolor}, + {hash("setbody"), &fn_setbody}, + {hash("setcharani"), &fn_setcharani}, + {hash("setchargender"), &fn_setchargender}, + {hash("setcharprop"), &fn_setcharprop}, + {hash("setcoatcolor"), &fn_setcoatcolor}, + {hash("setgender"), &fn_setgender}, + {hash("setgif"), &fn_setimg}, + {hash("setgifpart"), &fn_setimgpart}, + {hash("sethead"), &fn_sethead}, + {hash("setimg"), &fn_setimg}, + {hash("setimgpart"), &fn_setimgpart}, + {hash("setlevel"), &fn_setlevel}, + {hash("setlevel2"), &fn_setlevel2}, + {hash("setmap"), &fn_setmap}, + {hash("setminimap"), &fn_setminimap}, + {hash("setplayerdir"), &fn_setplayerdir}, + {hash("setplayerprop"), &fn_setplayerprop}, + {hash("setpm"), &fn_setpm}, + {hash("setshape"), &fn_setshape}, + {hash("setshield"), &fn_setshield}, + {hash("setshoecolor"), &fn_setshoecolor}, + {hash("setshootparams"), &fn_setshootparams}, + {hash("setskincolor"), &fn_setskincolor}, + {hash("setsleevecolor"), &fn_setsleevecolor}, + {hash("setstring"), &fn_setstring}, + {hash("setsword"), &fn_setsword}, + {hash("setz"), &fn_setz}, + {hash("shoot"), &fn_shoot}, + {hash("shootarrow"), &fn_shootarrow}, + {hash("shootball"), &fn_shootball}, + {hash("shootfireball"), &fn_shootfireball}, + {hash("shootfireblast"), &fn_shootfireblast}, + {hash("shootnuke"), &fn_shootnuke}, + {hash("show"), &fn_show}, + {hash("showani"), &fn_showani}, + {hash("showani2"), &fn_showani2}, + {hash("showcharacter"), &fn_showcharacter}, + {hash("showimg"), &fn_showimg}, + {hash("showimg2"), &fn_showimg2}, + {hash("showpoly"), &fn_showpoly}, + {hash("showpoly2"), &fn_showpoly2}, + {hash("showstats"), &fn_showstats}, + {hash("showtext"), &fn_showtext}, + {hash("showtext2"), &fn_showtext2}, + {hash("sleep"), &fn_sleep}, + {hash("spyfire"), &fn_spyfire}, + {hash("take"), &fn_take}, + {hash("take2"), &fn_take2}, + {hash("takehorse"), &fn_takehorse}, + {hash("takeplayercarry"), &fn_takeplayercarry}, + {hash("takeplayerhorse"), &fn_takeplayerhorse}, + {hash("throwcarry"), &fn_throwcarry}, + {hash("timershow"), &fn_timershow}, + {hash("tokenize"), &fn_tokenize}, + {hash("tokenize2"), &fn_tokenize2}, + {hash("toweapons"), &fn_toweapons}, + {hash("triggeraction"), &fn_triggeraction}, + {hash("unfreezeplayer"), &fn_unfreezeplayer}, + {hash("unset"), &fn_unset}, + {hash("updateboard"), &fn_updateboard}, + {hash("updateboard2"), &fn_updateboard2}, + {hash("updateterrain"), &fn_updateterrain}, + {hash("warpto"), &fn_warpto}, + // GR extensions + {hash("enabledamagereactions"), &fn_enabledamagereactions}, + {hash("disabledamagereactions"), &fn_disabledamagereactions}, + }; + return map; +} + +constexpr std::array flagProcessingCommands = +{ + "addstring"sv, + "deletestring"sv, + "insertstring"sv, + "removestring"sv, + "replacestring"sv, + "set"sv, + "setstring"sv, + "unset"sv, +}; + +constexpr std::array translatableCommands = +{ + "say2"sv, + "sendpm"sv, +}; + +/////////////////////////////////////////////////////////////////////////////// + +static std::any translateStringForPlayer(antlr4::tree::ParseTree* node, GS1Visitor* visitor, PlayerPtr player) +{ + if (node == nullptr) + return std::any{}; + if (visitor == nullptr || player == nullptr || node->getTreeType() != antlr4::tree::ParseTreeType::RULE) + return node->accept(visitor); + + return visitor->translateSourceText(node, player->account.language); +} + +/////////////////////////////////////////////////////////////////////////////// + +void processBuiltInCommand(GS1Visitor* visitor, antlr4::tree::ParseTree* node, std::string_view commandName) +{ + static BuiltInCommandHandleMap map = GenerateMap(); + + if (visitor == nullptr) + throw std::runtime_error("processBuiltInCommand received an empty visitor"); + if (commandName.empty()) + throw std::runtime_error("processBuiltInCommand received an empty command name"); + + // Find the command in the map. + size_t hash = string::string_hash{}(commandName); + auto it = map.find(hash); + if (it == map.end()) + { + log::printLine(log::script, "Unknown command in NPC [{}] '{}': {}", visitor->getOriginalSource().first, visitor->who, commandName); + return; + } + + // Find the nearest player. We use this for a couple calculations. + std::optional player = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); + + // Special case for 'setplayerprop' and 'setcharprop', which need to push a unique context onto the stack. + // We need to bring the relevant context to the front so the message code links to the correct player or NPC, since it can touch both. + bool popContext = false; + if (commandName == "setcharprop") + { + auto npc = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); + if (!npc.has_value()) + npc = visitor->getOriginalSource(); + + visitor->pushSource(npc.value()); + popContext = true; + } + else if (commandName == "setplayerprop") + { + if (!player.has_value()) + { + if (visitor->getEvent().initiator.second != ScriptObjectType::PLAYER) + return; + player = visitor->getEvent().initiator; + } + + visitor->pushSource(player.value()); + popContext = true; + } + + // Record if we are expecting a flag for the next argument. + visitor->expectingFlag = (std::ranges::find(flagProcessingCommands, commandName) != std::ranges::end(flagProcessingCommands)); + bool isTranslatable = std::ranges::contains(translatableCommands, commandName); + + std::vector arguments; + std::vector keepAlive; + + // Helper to package a value and keep it alive for the duration of the command execution. + auto makeValue = [&](std::any&& anyValue) + { + if (!anyValue.has_value()) + return; + + keepAlive.emplace_back(std::move(anyValue)); + auto* container = std::any_cast(&keepAlive.back()); + if (container == nullptr) + throw std::runtime_error("BuiltInCommand argument is not a valid GS1ScriptValue"); + + arguments.push_back(std::move(container)); + }; + + // Save the player pointer so we don't keep searching for it. + PlayerPtr playerPtr = nullptr; + auto server = BabyDI::Get(); + if (isTranslatable && player.has_value()) + playerPtr = server->getPlayer(player.value().first); + + // Collect the arguments from the node. + for (size_t i = 0; i < node->children.size(); ++i) + { + // If the command is translatable, run it through the translation process before packaging the value. + if (isTranslatable && visitor->expectingFlag == false && player.has_value()) + { + if (auto stringContext = visitor->walkToContext(node->children[i]); stringContext != nullptr) + { + makeValue(translateStringForPlayer(stringContext, visitor, playerPtr)); + continue; + } + } + + makeValue(node->children[i]->accept(visitor)); + } + + // Unset the expecting flag. + visitor->expectingFlag = false; + + try + { + // Execute the command. + it->second(visitor, commandName, arguments); + } + catch (const std::logic_error& ex) + { + auto server = BabyDI::Get(); + log::printLine(log::npc, "[WARNING] NPC [{}] '{}', error: {}", visitor->getOriginalSource().first, visitor->who, ex.what()); + server->sendToNC(std::format("Script problem: NPC [{}] '{}', issue: {}", visitor->getOriginalSource().first, visitor->who, ex.what())); + } + catch (const std::exception& ex) + { + auto server = BabyDI::Get(); + log::printLine(log::npc, "[ERROR] NPC [{}] '{}', error: {}", visitor->getOriginalSource().first, visitor->who, ex.what()); + server->sendToNC(std::format("Script error: NPC [{}] '{}', error: {}", visitor->getOriginalSource().first, visitor->who, ex.what())); + + if (popContext) + visitor->popSource(); + throw; + } + + // If we pushed a context, we need to pop it after the command execution. + if (popContext) + visitor->popSource(); +} + +/////////////////////////////////////////////////////////////////////////////// + +static std::optional getPositionForArrow(const ScriptObject& source, uint8_t dir) +{ + auto server = BabyDI::Get(); + if (source.second == ScriptObjectType::NPC) + { + if (auto npc = server->getNPC(source.first); npc != nullptr) + { + PixelPosition sourcePosition = npc->character.getGlobalPosition(); + if (npc->isCharacter()) + { + int16_t dX = (dir == 1 ? -24 : (dir == 3 ? 24 : 0)); + int16_t dY = (dir == 0 ? -24 : (dir == 2 ? 24 : 0)); + sourcePosition.translate(16 + dX, 24 + dY); + } + return sourcePosition; + } + } + + return std::nullopt; +} + +/////////////////////////////////////////////////////////////////////////////// + +#if DEBUG +void fn_debugger(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + auto server = BabyDI::Get(); + const auto& sourceNPC = visitor->getOriginalSource(); + auto sourcePlayer = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); + NPCPtr npc = nullptr; + PlayerPtr player = nullptr; + if (sourceNPC.second == ScriptObjectType::NPC) + npc = server->getNPC(sourceNPC.first); + if (sourcePlayer.has_value()) + player = server->getNPCServer()->getPlayer(sourcePlayer.value().first); + + //player->setPropWith(SetBy::SERVER, 0_ui16); +} +#endif + +// addguildmember guild,account,nick; +void fn_addguildmember(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: addguildmember guild,account,nick"); + + auto guild = visitor->getGameValueAs(*arguments[0]); + auto account = visitor->getGameValueAs(*arguments[1]); + auto nick = visitor->getGameValueAs(*arguments[2]); + + if (auto guildManager = BabyDI::Get(); guildManager) + guildManager->addPlayerToGuild(guild, account, nick); +} + +// addstring list,text; +// Adds a string to a string list. +void fn_addstring(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: addstring list,text"); + + if (auto* listVar = visitor->getGameValueFromGS1ScriptValue(*arguments[0]); listVar != nullptr) + { + auto text = visitor->getGameValueAs(*arguments[1]); + auto& list = listVar->get(); + if (list.has_value() && !list.value().empty()) + listVar->assign(list.value() + "," + text); + else + listVar->assign(text); + } +} + +// addweapon weaponname; +// Adds a weapon from a database to your inventory. +void fn_addweapon(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: addweapon weaponname"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto weaponname = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->addWeapon(weaponname); + } +} + +// attachplayertoobj objecttype,id; +// Attaches player to object (objecttype 0 = npcs, nothing else supported). +void fn_attachplayertoobj(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: attachplayertoobj objecttype,id"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto objecttype = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto id = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->setPropWith(SetBy::SERVER, id, objecttype); + } +} + +// blockagain; +// Enables collision. +void fn_blockagain(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + uint8_t blockFlags = npc->blockFlags & ~PROPID(NPCBlockFlags::NOBLOCK); + npc->setPropWith(SetBy::SERVER, blockFlags); + } + } +} + +// callnpc index,eventname,params; +// Sends an event to an NPC. +void fn_callnpc(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() < 2) + throw std::invalid_argument("invalid arguments: callnpc index,eventname,params"); + + NPCID sourceNPC = 0; + if (visitor->getOriginalSource().second == ScriptObjectType::NPC) + sourceNPC = visitor->getOriginalSource().first; + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + std::vector eventAndParams; + eventAndParams.emplace_back(visitor->getGameValueAs(*arguments[1])); + if (arguments.size() > 2) + { + auto params = string::fromCSV(visitor->getGameValueAs(*arguments[2])); + eventAndParams.insert(eventAndParams.end(), std::ranges::begin(params), std::ranges::end(params)); + } + + auto server = BabyDI::Get(); + if (index < level->getNPCs().size()) + { + auto& mapNPCs = level->getNPCs(); + auto iter = mapNPCs.begin(); + std::ranges::advance(iter, index, mapNPCs.end()); + if (iter != mapNPCs.end()) + { + if (auto npc = server->getNPC(*iter); npc != nullptr) + npc->scripting.events.addEvent(ScriptEventType::CUSTOM, source::FromNPC(sourceNPC), eventAndParams); + } + } + } +} + +// canbecarried; +// Flags as carryable. +void fn_canbecarried(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + uint8_t blockFlags = npc->blockFlags | PROPID(NPCBlockFlags::CANBECARRIED); + npc->setPropWith(SetBy::SERVER, blockFlags); + } + } +} + +// canbepulled; +// Flags as pullable. +void fn_canbepulled(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + uint8_t blockFlags = npc->blockFlags | PROPID(NPCBlockFlags::CANBEPULLED); + npc->setPropWith(SetBy::SERVER, blockFlags); + } + } +} + +// canbepushed; +// Flags as pushable. +void fn_canbepushed(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + uint8_t blockFlags = npc->blockFlags | PROPID(NPCBlockFlags::CANBEPUSHED); + npc->setPropWith(SetBy::SERVER, blockFlags); + } + } +} + +// cannotbecarried; +// Flags as not carryable. +void fn_cannotbecarried(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + uint8_t blockFlags = npc->blockFlags & ~PROPID(NPCBlockFlags::CANBECARRIED); + npc->setPropWith(SetBy::SERVER, blockFlags); + } + } +} + +// cannotbepulled; +// Flags as not pullable. +void fn_cannotbepulled(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + uint8_t blockFlags = npc->blockFlags & ~PROPID(NPCBlockFlags::CANBEPULLED); + npc->setPropWith(SetBy::SERVER, blockFlags); + } + } +} + +// cannotbepushed; +// Flags as not pushable. +void fn_cannotbepushed(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + uint8_t blockFlags = npc->blockFlags & ~PROPID(NPCBlockFlags::CANBEPUSHED); + npc->setPropWith(SetBy::SERVER, blockFlags); + } + } +} + +// cannotwarp; +// Flags as not warpable. +void fn_cannotwarp(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->warpRestrictions = NPCWarpRestrictions::NOTALLOWED; + } +} + +// canwarp; +// Flags as being able to change levels by touching any links. +void fn_canwarp(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->warpRestrictions = NPCWarpRestrictions::ALLOWED; + } +} + +// canwarp2; +// Flags as being able to change levels by using level-edge links. +void fn_canwarp2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->warpRestrictions = NPCWarpRestrictions::ONLYOVERWORLD; + } +} + +// carryobject carryobjecttype; +// Sets the carry object type of the NPC. +void fn_carryobject(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + // TODO: There is no NPC prop for the carry image type. We may have to investigate official. + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + [[maybe_unused]] auto carryObjectTypeId = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr && npc->isCharacter()) + npc->setPropWith(SetBy::SERVER, "carrystill"s); + } +} + +// changeimgcolors index,red,green,blue,alpha; +// Sets the RGBA colors of the showimg. +void fn_changeimgcolors(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 5) + throw std::invalid_argument("invalid arguments: changeimgcolors index,red,green,blue,alpha"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto red = visitor->getGameValueAs(*arguments[1]); + auto green = visitor->getGameValueAs(*arguments[2]); + auto blue = visitor->getGameValueAs(*arguments[3]); + auto alpha = visitor->getGameValueAs(*arguments[4]); + + server->getNPCServer()->changeShowImgColors(npc, index, red, green, blue, alpha); + } + } +} + +// changeimgmode index,mode; +// Sets the drawing mode of the showimg. +void fn_changeimgmode(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: changeimgmode index,mode"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto mode = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + + server->getNPCServer()->changeShowImgMode(npc, index, mode); + } + } +} + +// changeimgpart index,x,y,width,height; +// Sets the image part of the showimg. +void fn_changeimgpart(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 5) + throw std::invalid_argument("invalid arguments: changeimgpart index,x,y,width,height"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2])); + auto width = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[3])); + auto height = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[4])); + + server->getNPCServer()->changeShowImgPart(npc, index, ImagePartRectangle{{x, y}, {width, height}}); + } + } +} + +// changeimgvis index,drawingheight; +// Sets the layer of the showimg. +void fn_changeimgvis(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: changeimgvis index,drawingheight"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto drawingheight = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + + server->getNPCServer()->changeShowImgLayer(npc, index, drawingheight); + } + } +} + +// changeimgzoom index,zoomfactor; +// Sets the zoom of the showimg. +void fn_changeimgzoom(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: changeimgzoom index,zoomfactor"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto zoomfactor = static_cast(visitor->getGameValueAs(*arguments[1])); + + server->getNPCServer()->changeShowImgZoom(npc, index, zoomfactor); + } + } +} + +// copylevel oldfile,newfile; +// Makes a copy of a level under a new file name. +void fn_copylevel(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: copylevel oldfile,newfile"); + + auto oldfile = visitor->getGameValueAs(*arguments[0]); + auto newfile = visitor->getGameValueAs(*arguments[1]); + + auto server = BabyDI::Get(); + auto& fs = server->getFileSystem(); + if (auto foundInfo = fs.infoi(fs::FileCategory::LEVEL, oldfile); foundInfo != nullptr) + std::filesystem::copy_file(foundInfo->file, foundInfo->file.parent_path() / newfile, std::filesystem::copy_options::overwrite_existing); +} + +// copystrings fromprefix,toprefix; +// Copies strings that start with fromprefix and replaces the prefix with toprefix. +void fn_copystrings(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: copystrings fromprefix,toprefix"); + + auto fromPrefix = visitor->getGameValueAs(*arguments[0]); + auto toPrefix = visitor->getGameValueAs(*arguments[1]); + + size_t fromStorageType = GS1Visitor::getStorageTypeFromIdentifier(fromPrefix).value_or(ENUM(StorageType::CLIENT)); + size_t toStorageType = GS1Visitor::getStorageTypeFromIdentifier(toPrefix).value_or(ENUM(StorageType::CLIENT)); + GS1Visitor::stripStorageNameFromIdentifier(fromPrefix); + GS1Visitor::stripStorageNameFromIdentifier(toPrefix); + + auto fromStore = visitor->getGameVariableStoreForStorageType(fromStorageType); + auto toStore = visitor->getGameVariableStoreForStorageType(toStorageType); + if (fromStore == nullptr || toStore == nullptr) + return; + + for (auto& [key, value] : fromStore->store) + { + if (key.starts_with(fromPrefix)) + { + auto toKey = std::format("{}{}", toPrefix, key.substr(fromPrefix.size())); + toStore->add(toKey, GameValue{*value}); + } + } +} + +// deletelevel filename; +void fn_deletelevel(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: deletelevel filename"); + + auto filename = visitor->getGameValueAs(*arguments[0]); + + auto server = BabyDI::Get(); + if (auto level = server->getLoadedLevelNoHint(filename); level != nullptr) + { + for (auto playerId : level->getPlayers()) + server->warpPlayerToSafePlace(playerId); + + auto path = server->getFileSystem().find(fs::FileCategory::LEVEL, level->levelName); + std::filesystem::remove(path); + } +} + +// deletestring list,index; +// Deletes a string from a string list at the specified index. +void fn_deletestring(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: deletestring list,index"); + + if (auto* listVar = visitor->getGameValueFromGS1ScriptValue(*arguments[0]); listVar != nullptr) + { + auto list = string::splitToVectorView(listVar->get().value_or(std::string{}), ","sv, false); + auto index = DoubleAsIntegralFloor(std::max(0.0, visitor->getGameValueAs(*arguments[1]))); + + // Check for out of bounds. + if (index >= list.size()) + return; + + // Move the iterator to the index we want to delete. + auto it = list.begin(); + std::advance(it, index); + + // Delete the string. + list.erase(it); + + // Write it back. + listVar->assign(string::join(list, ",")); + } +} + +// destroy; +// Destroys an NPC. +void fn_destroy(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->getOriginalSource(); source.second == ScriptObjectType::NPC) + { + auto server = BabyDI::Get(); + if (server->getSettings().get("protectdbnpcs").value_or(true)) + { + if (auto npc = server->getNPC(source.first); npc != nullptr && npc->storageType == NPCStorageType::DATABASE && npc->scriptType != NPCTYPE_LOCAL && npc->scriptType != NPCTYPE_ITEM) + { + log::printLine(log::npc, "NPC '{}' attempted to destroy itself, but DB NPCs are protected.", npc->name); + return; + } + } + + server->getNPCServer()->deleteNPC(source.first); + + // NPC execution stops after destroy is called. + throw return_exception{}; + } +} + +// detachplayer; +// Detaches the player. +void fn_detachplayer(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->setPropWith(SetBy::SERVER, static_cast(0), 0_ui8); + } +} + +// disableweapons; +// Disables the player's weapons. +void fn_disableweapons(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->disableWeapons(); + } +} + +// dontblock; +// Disables collision. +void fn_dontblock(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + uint8_t blockFlags = npc->blockFlags | PROPID(NPCBlockFlags::NOBLOCK); + npc->setPropWith(SetBy::SERVER, blockFlags); + } + } +} + +// drawoverplayer; +// Configures the NPC to draw over the player. +void fn_drawoverplayer(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + uint8_t newVisFlags = npc->visFlags & ~(PROPID(NPCVisFlags::DRAWUNDERPLAYER) | PROPID(NPCVisFlags::DRAWOVERPLAYER)); + newVisFlags |= (PROPID(NPCVisFlags::DRAWOVERPLAYER) | PROPID(NPCVisFlags::VISIBLE)); + npc->setPropWith(SetBy::SERVER, newVisFlags); + } + } +} + +// drawovertrees; +// Configure the NPC to draw on the same layer as the player. +void fn_drawovertrees(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto visFlags = npc->visFlags; + visFlags &= ~(PROPID(NPCVisFlags::DRAWOVERPLAYER) | PROPID(NPCVisFlags::DRAWUNDERPLAYER)); + npc->setPropWith(SetBy::SERVER, static_cast(visFlags | PROPID(NPCVisFlags::VISIBLE))); + } + } +} + +// drawunderplayer; +// Configure the NPC to draw under the player. +void fn_drawunderplayer(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + uint8_t newVisFlags = npc->visFlags & ~(PROPID(NPCVisFlags::DRAWUNDERPLAYER) | PROPID(NPCVisFlags::DRAWOVERPLAYER)); + newVisFlags |= (PROPID(NPCVisFlags::DRAWUNDERPLAYER) | PROPID(NPCVisFlags::VISIBLE)); + npc->setPropWith(SetBy::SERVER, newVisFlags); + } + } +} + +// enableweapons; +// Enables the player's weapons. +void fn_enableweapons(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->enableWeapons(); + } +} + +// explodebomb index; +// Explodes the bomb at the specified index. +void fn_explodebomb(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: explodebomb index"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + if (auto bomb = level->getBomb(index); bomb.has_value()) + { + auto& power = bomb.value()->power; + auto& position = bomb.value()->position; + level->removeBomb(inform_client, index); + + if (power != 2) + level->addExplosion(inform_client, position, source::FromServer(), 2, power); + else + { + // Superbomb is 5 explosions. + // The center explosion is a size of 4. The others are a size of 2. + level->addExplosion(inform_client, position, source::FromServer(), 4, power); + level->addExplosion(inform_client, translatePosition(position, -32, -32), source::FromServer(), 2, power); + level->addExplosion(inform_client, translatePosition(position, 32, -32), source::FromServer(), 2, power); + level->addExplosion(inform_client, translatePosition(position, -32, 32), source::FromServer(), 2, power); + level->addExplosion(inform_client, translatePosition(position, 32, 32), source::FromServer(), 2, power); + } + } + } +} + +// freezeplayer2; +// Freezes the player, preventing movement and actions. +void fn_freezeplayer2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->freezePlayer(); + } +} + +// hide; +// Hides the NPC. +void fn_hide(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->setPropWith(SetBy::SERVER, static_cast(npc->visFlags & ~PROPID(NPCVisFlags::VISIBLE))); + } +} + +// hideimg index; +// Removes the image at the specified index. +void fn_hideimg(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: hideimg index"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + server->getNPCServer()->hideImages(npc, index); + } + } +} + +// hideimgs indexstart,indexend; +// Removes the images in the specified range. +void fn_hideimgs(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: hideimgs indexstart,indexend"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto indexstart = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto indexend = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + server->getNPCServer()->hideImages(npc, indexstart, indexend); + } + } +} + +// hitcompu index,power,fromx,fromy; +void fn_hitcompu(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + throw unimplemented_error("hitcompu is not implemented yet."); +} + +// hitnpc index,halfhearts,fromx,fromy; +// Hits the specified NPC. +void fn_hitnpc(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("invalid arguments: hitnpc index,halfhearts,fromx,fromy"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto halfhearts = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + auto fromx = static_cast(visitor->getGameValueAs(*arguments[2])); + auto fromy = static_cast(visitor->getGameValueAs(*arguments[3])); + + if (index < level->getNPCs().size()) + { + auto server = BabyDI::Get(); + auto& mapNPCs = level->getNPCs(); + auto iter = mapNPCs.begin(); + std::ranges::advance(iter, index, mapNPCs.end()); + if (iter != mapNPCs.end()) + { + if (auto npc = server->getNPC(*iter); npc != nullptr) + { + // Get the DX/DY. + auto tilePosition = npc->getTilePosition(); + auto dx = tilePosition.x() - fromx; + auto dy = tilePosition.y() - fromy; + float length = std::sqrt(dx * dx + dy * dy); + dx /= length; + dy /= length; + + // Set the NPC's props. + npc->setPropWith(SetBy::SERVER, dx, dy); + npc->setPropWith(SetBy::SERVER, static_cast(std::max(0, npc->character.hitpointsInHalves - halfhearts))); + if (npc->isCharacter()) + npc->setPropWith(SetBy::SERVER, "hurt"sv); + + // Queue up events. + npc->scripting.events.addEvent(ScriptEventType::WASHIT, visitor->getCurrentSource()); + } + } + } + } +} + +// hitobjects power,x,y; +// Hit objects at a location. +void fn_hitobjects(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: hitobjects power,x,y"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + if (auto level = npc->getLevel(); level != nullptr) + { + auto power = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0]) * 2); + auto x = static_cast(visitor->getGameValueAs(*arguments[1])); + auto y = static_cast(visitor->getGameValueAs(*arguments[2])); + server->hitObjectsAtPoint({x, y}, power, level, npc); + } + } + } +} + +// hitplayer index,halfhearts,fromx,fromy; +// Hits a player in the level. +void fn_hitplayer(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("invalid arguments: hitplayer index,halfhearts,fromx,fromy"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto halfhearts = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + auto fromx = static_cast(visitor->getGameValueAs(*arguments[2])); + auto fromy = static_cast(visitor->getGameValueAs(*arguments[3])); + + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + if (auto level = npc->getLevel(); level != nullptr) + { + auto& mapPlayers = level->getPlayers(); + if (index < mapPlayers.size()) + server->hitPlayer(mapPlayers[index], halfhearts, fromx, fromy, npc); + } + } + } +} + +// hurt halfhearts; +// Hurts a player. +void fn_hurt(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: hurt halfhearts"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto halfhearts = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto npcId = visitor->getOriginalSource().first; + if (visitor->getOriginalSource().second != ScriptObjectType::NPC) + npcId = 0; + + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + { + auto tilePos = toTilePosition(player->account.character.getLocalPosition()); + server->hitPlayer(player->getId(), halfhearts, tilePos.x() + 1.5, tilePos.y() + 2, server->getNPC(npcId)); + } + } +} + +// insertstring list,index,text; +// Inserts a string into a string array at the given position. +void fn_insertstring(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: insertstring list,index,text"); + + if (auto* listVar = visitor->getGameValueFromGS1ScriptValue(*arguments[0]); listVar != nullptr) + { + auto list = string::splitToVectorView(listVar->get().value_or(std::string{}), ","sv, false); + auto index = DoubleAsIntegralFloor(std::max(0.0, visitor->getGameValueAs(*arguments[1]))); + auto text = visitor->getGameValueAs(*arguments[2]); + + // Insert blank strings to fill the space. + if (index > list.size()) + { + std::vector emptyStrings(index - list.size(), ""); + for (auto& str : emptyStrings) + list.emplace_back(std::move(str)); + } + + // Insert the text at the specified index. + if (index == list.size()) + { + list.push_back(text); + } + else if (index < list.size()) + { + list.insert(list.begin() + index, text); + } + + // Write it back. + listVar->assign(string::join(list, ",")); + } +} + +// join class; +// Joins a class. +void fn_join(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: join class"); + + auto class_ = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + visitor->scriptContext->joinedClasses.insert({class_, server->getNPCServer()->getClass(class_)}); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->joinClass(class_); + } + else if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::WEAPON); source.has_value()) + { + auto& weaponList = server->getWeaponList(); + if (auto weapon = weaponList.find(source.value().first); weapon != weaponList.end()) + weapon->second->joinClass(class_); + } +} + +// lay itemname; +// Lays the specified item at the feet of the NPC. +void fn_lay(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: lay itemname"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto itemname = std::clamp(DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])), 0_ui8, 24_ui8); + + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + PixelPosition layPosition = npc->character.getGlobalPosition(); + + // Characters lay (+0.5, +3) no matter which direction they are looking. + if (npc->isCharacter()) + layPosition.translate(static_cast(8), static_cast(16 * 3)); + + if (auto level = npc->getLevel(); level != nullptr) + level->addItem(inform_client, layPosition, static_cast(itemname)); + } + } +} + +// lay2 itemname,x,y; +// Lays the specified item at the given x and y location. +void fn_lay2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: lay2 itemname,x,y"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto itemname = std::clamp(DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])), 0_ui8, 24_ui8); + auto x = static_cast(visitor->getGameValueAs(*arguments[1])); + auto y = static_cast(visitor->getGameValueAs(*arguments[2])); + level->addItem(inform_client, toPixelPosition({x, y}), static_cast(itemname)); + } +} + +// message text; +// Sets the NPC message. +void fn_message(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + std::string text{}; + if (arguments.size() != 0) + text = visitor->getGameValueAs(*arguments[0]); + + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->setPropWith(SetBy::SERVER, text); + } +} + +// move dx,dy,time,options; +// Moves an NPC smoothly on the client. +void fn_move(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("invalid arguments: move dx,dy,time,options"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto dx = static_cast(visitor->getGameValueAs(*arguments[0])); + auto dy = static_cast(visitor->getGameValueAs(*arguments[1])); + auto time = static_cast(visitor->getGameValueAs(*arguments[2])); + auto options = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[3])); + + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->addMoveToQueue(toLocalPixelPosition(dx, dy), time, options); + } +} + +// noplayeronwall; +// Disables onwall checks from detecting players. +void fn_noplayeronwall(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->noPlayerOnWall = true; + } +} + +// putbomb power,x,y; +// Creates a bomb at the specified location with the given power. +void fn_putbomb(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: putbomb power,x,y"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto power = std::clamp(DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])), 1_ui8, 3_ui8); + auto x = static_cast(visitor->getGameValueAs(*arguments[1])); + auto y = static_cast(visitor->getGameValueAs(*arguments[2])); + level->addBomb(inform_client, toPixelPosition({x, y}), power); + } +} + +// putcomp baddyname,x,y; +// Adds a new baddy to the level with the specified parameters. +void fn_putcomp(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: putcomp baddyname,x,y"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + uint8_t baddyname = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto x = static_cast(visitor->getGameValueAs(*arguments[1])); + auto y = static_cast(visitor->getGameValueAs(*arguments[2])); + level->putNewBaddy(toLocalPixelPosition(x, y), static_cast(baddyname)); + } +} + +// putexplosion radius,x,y; +// Creates an explosion at the specified location with the given radius. +void fn_putexplosion(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: putexplosion radius,x,y"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto radius = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto x = static_cast(visitor->getGameValueAs(*arguments[1])); + auto y = static_cast(visitor->getGameValueAs(*arguments[2])); + level->addExplosion(inform_client, toPixelPosition({x, y}), visitor->getCurrentSource(), radius, 1); + } +} + +// putexplosion2 power,radius,x,y; +// Creates an explosion at the specified location with the given power and radius. +void fn_putexplosion2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("invalid arguments: putexplosion2 power,radius,x,y"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto power = std::clamp(DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])), 1_ui8, 3_ui8); + auto radius = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + auto x = static_cast(visitor->getGameValueAs(*arguments[2])); + auto y = static_cast(visitor->getGameValueAs(*arguments[3])); + level->addExplosion(inform_client, toPixelPosition({x, y}), visitor->getCurrentSource(), radius, power); + } +} + +// puthorse imagefile,x,y; +// Creates a new horse at the specified location with the given image file. +void fn_puthorse(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: puthorse imagefile,x,y"); + + auto server = BabyDI::Get(); + if (server->getSettings().get("puthorseenabled").value_or(true) == false) + { + log::printLine(log::npc, "puthorse command is disabled on this server."); + return; + } + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto imagefile = visitor->getGameValueAs(*arguments[0]); + auto x = static_cast(visitor->getGameValueAs(*arguments[1])); + auto y = static_cast(visitor->getGameValueAs(*arguments[2])); + level->addHorse(inform_client, imagefile, toPixelPosition({x, y}), 2, 0); + } +} + +// putnewcomp baddyname,x,y,imagefile,power; +// Adds a new baddy to the level with the specified parameters. +void fn_putnewcomp(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 5) + throw std::invalid_argument("invalid arguments: putnewcomp baddyname,x,y,imagefile,power"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + uint8_t baddyname = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto x = static_cast(visitor->getGameValueAs(*arguments[1])); + auto y = static_cast(visitor->getGameValueAs(*arguments[2])); + auto imagefile = visitor->getGameValueAs(*arguments[3]); + auto power = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[4])); + level->putNewBaddy(toLocalPixelPosition(x, y), static_cast(baddyname), power, imagefile); + } +} + +// putnpc imagefile,scriptfile,x,y; +// Creates a new level NPC with the specified parameters. +void fn_putnpc(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("invalid arguments: putnpc imagefile,scriptfile,x,y"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto imagefile = visitor->getGameValueAs(*arguments[0]); + auto scriptfile = visitor->getGameValueAs(*arguments[1]); + auto x = static_cast(visitor->getGameValueAs(*arguments[2])); + auto y = static_cast(visitor->getGameValueAs(*arguments[3])); + + auto server = BabyDI::Get(); + auto& fs = server->getFileSystem(); + if (auto file = fs.openi(fs::FileCategory::FILE, scriptfile); file != nullptr) + { + auto script = file->readAsString(); + server->addNPC(imagefile, script, x, y, level, NPCStorageType::LEVEL, true); + } + } +} + +// putnpc2 x,y,{ script }; +// Creates a new database NPC at the location and with the specified script. +void fn_putnpc2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: putnpc2 x,y,{ script }"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto x = visitor->getGameValueAs(*arguments[0]); + auto y = visitor->getGameValueAs(*arguments[1]); + auto script = visitor->getGameValueAs(*arguments[2]); + string::trimMutate(script); + + auto server = BabyDI::Get(); + server->getNPCServer()->addNPC({}, script, level, {(float)x, (float)y}); + } +} + +// removearrow index; +void fn_removearrow(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + throw std::logic_error("removearrow is clientside only."); +} + +// removebomb index; +// Removes a bomb from the level. +void fn_removebomb(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: removebomb index"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + level->removeBomb(inform_client, index); + } +} + +// removecompus; +// Removes all baddies from the level. +void fn_removecompus(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto level = visitor->findCurrentLevel(); level != nullptr) + level->removeAllBaddies(); +} + +// removeexplo index; +void fn_removeexplo(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + throw std::logic_error("removeexplo is clientside only."); +} + +// removeguild guild; +void fn_removeguild(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: removeguild guild"); + + auto guild = visitor->getGameValueAs(*arguments[0]); + + if (auto guildManager = BabyDI::Get(); guildManager) + guildManager->deleteGuild(guild); +} + +// removeguildmember guild,account,nick; +void fn_removeguildmember(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() < 2) + throw std::invalid_argument("invalid arguments: removeguildmember guild,account,nick"); + + auto guild = visitor->getGameValueAs(*arguments[0]); + auto account = visitor->getGameValueAs(*arguments[1]); + std::string nick = (arguments.size() > 2) ? visitor->getGameValueAs(*arguments[2]) : std::string{}; + + if (auto guildManager = BabyDI::Get(); guildManager) + { + if (nick.empty()) + guildManager->removePlayerEntirelyFromGuild(guild, account); + else guildManager->removePlayerFromGuild(guild, account, nick); + } +} + +// removehorse index; +// Removes the horse from the level at the specified index. +void fn_removehorse(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: removehorse index"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + level->removeHorse(inform_client, index); + } +} + +// removeitem index; +// Removes the item from the level at the specified index. +void fn_removeitem(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: removeitem index"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + level->removeItem(inform_client, index); + } +} + +// removestring list,text; +// Removes all occurrences of the specified text from the string list. +void fn_removestring(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: removestring list,text"); + + if (auto* listVar = visitor->getGameValueFromGS1ScriptValue(*arguments[0]); listVar != nullptr) + { + auto list = listVar->get().value_or(std::string{}); + auto text = visitor->getGameValueAs(*arguments[1]); + + size_t textLength = text.size(); + size_t pos = 0; + while ((pos = list.find(text, pos)) != std::string::npos) + { + if (pos + textLength + 1 < list.size() && list[pos + textLength] == ',') + { + // If the text is followed by a comma, remove it as well. + list.erase(pos, textLength + 1); + } + else + { + list.erase(pos, textLength); + } + } + + listVar->assign(list); + } +} + +// removeweapon weaponname; +// Removes the specified weapon from the player. +void fn_removeweapon(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: removeweapon weaponname"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto weaponname = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->deleteWeapon(weaponname); + } +} + +// replacestring list,index,text; +// Replaces the string at the specified index in the list with the given text. +void fn_replacestring(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: replacestring list,index,text"); + + if (auto* listVar = visitor->getGameValueFromGS1ScriptValue(*arguments[0]); listVar != nullptr) + { + auto list = listVar->get().value_or(std::string{}); + auto index = DoubleAsIntegralFloor(std::max(0.0, visitor->getGameValueAs(*arguments[1]))); + auto text = visitor->getGameValueAs(*arguments[2]); + + auto start = std::ranges::begin(list); + if (index != 0) + { + size_t loc = 0; + start = std::ranges::find_if(list, [&loc, &index](const char& c) + { + return (c == ',' && ++loc == index); + }); + } + if (start == std::ranges::end(list)) + return; + + auto end = std::ranges::find(start, std::ranges::end(list), ','); + list.replace(start, end, text); + listVar->assign(list); + } +} + +// saveinfo text,text; +void fn_saveinfo(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + throw unimplemented_error("saveinfo is not implemented."); +} + +// savelog text; +// Writes text to npclog.txt. +void fn_savelog(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: savelog text"); + + auto text = visitor->getGameValueAs(*arguments[0]); + + auto server = BabyDI::Get(); + server->logToFile("npclog.txt", text); +} + +// savelog2 filename,text; +// Writes text to a specified log file. +void fn_savelog2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: savelog2 filename,text"); + + auto filename = visitor->getGameValueAs(*arguments[0]); + auto text = visitor->getGameValueAs(*arguments[1]); + + auto server = BabyDI::Get(); + server->logToFile(filename, text); +} + +// say signindex; +// Displays the text of a sign at the specified index to the player. +void fn_say(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: say signindex"); + + if (auto levelTuple = visitor->findCurrentLevelData(); std::get<0>(levelTuple) != nullptr) + { + //auto& level = std::get<0>(levelTuple); + //auto& subLevel = std::get<1>(levelTuple); + auto& levelData = std::get<2>(levelTuple); + if (levelData != nullptr) + { + auto signIndex = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); + if (source.has_value() && signIndex < levelData->signs.size()) + { + auto& sign = levelData->signs[signIndex]; + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->sendSignMessage(sign.text); + } + } + } +} + +// say2 message; +// Displays a custom sign message to the player. +void fn_say2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + std::string message; + if (arguments.size() != 0) + { + message = visitor->getGameValueAs(*arguments[0]); + string::eraseCharsMutate(message, "\r\n"sv); + } + + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->sendSignMessage(message); + } +} + +// sendpm message; +// Sends a private message to the player. +void fn_sendpm(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: sendpm message"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto message = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->sendPrivateMessage(NPCServerPlayerID, message); + } +} + +// sendrpgmessage message; +// Sends a message to the F2 message window of the player. +void fn_sendrpgmessage(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + std::string message; + if (arguments.size() != 0) + message = visitor->getGameValueAs(*arguments[0]); + + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->sendRPGMessage(message); + } +} + +// sendtonc message; +// Sends a message to the NC (NPC Control). +void fn_sendtonc(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + std::string message; + if (arguments.size() != 0) + message = visitor->getGameValueAs(*arguments[0]); + + auto server = BabyDI::Get(); + server->sendToNC(message); +} + +// sendtorc message; +// Sends a message to the RC (Remote Control). +void fn_sendtorc(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + std::string message; + if (arguments.size() != 0) + message = visitor->getGameValueAs(*arguments[0]); + + auto server = BabyDI::Get(); + server->sendToRC(message); +} + +// serverwarp servername; +// Warps a player to a different server. +void fn_serverwarp(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: serverwarp servername"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto servername = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + server->getServerList().sendPacket(CString() >> (char)SVO_SERVERINFO >> (short)player->getId() << servername); + } +} + +// set flag; +// Sets a flag on the player. +void fn_set(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: set flag"); + + if (auto* flag = visitor->getGameValueFromGS1ScriptValue(*arguments[0]); flag != nullptr) + { + auto server = BabyDI::Get(); + if (flag->identifier.starts_with("client.") || flag->identifier.starts_with("clientr.")) + { + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->setFlag(flag->identifier, std::nullopt, true); + } + } + else if (flag->identifier.starts_with("server.") || flag->identifier.starts_with("serverr.")) + { + server->setFlag(flag->identifier, std::nullopt); + } + else + { + flag->assign(true); + } + } +} + +// setani gani; +// setani gani,attribs; +// Sets the animation for the player. +void fn_setani(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setani gani,attribs"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto gani = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->setPropWith(SetBy::SERVER, gani); + } +} + +// setarray var,size; +// Creates an array of the given size. +void fn_setarray(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: setarray var,size"); + + if (auto* var = visitor->getGameValueFromGS1ScriptValue(*arguments[0]); var != nullptr) + { + auto size = DoubleAsIntegralFloor(std::max(0.0, visitor->getGameValueAs(*arguments[1]))); + std::vector arrayValues; + arrayValues.assign(size, 0.0); + + var->assign>(GameValue{std::move(arrayValues)}); + } +} + +// setbeltcolor color; +// Sets the player's belt color. +void fn_setbeltcolor(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setbeltcolor color"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto color = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + { + auto colors = player->getProp(); + colors.values[4] = static_cast(color); + player->setPropWith(SetBy::SERVER, colors); + } + } +} + +// setbody filename; +// Sets the body image for the player. +void fn_setbody(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setbody filename"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto filename = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->setPropWith(SetBy::SERVER, filename); + } +} + +// setcharani gani; +// setcharani gani,attribs; +// Sets the NPC character's animation. +void fn_setcharani(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setcharani gani,attribs"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto gani = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->setPropWith(SetBy::SERVER, gani); + } +} + +// setchargender gender; +// Sets the NPC character's gender (controls which voice is used). +void fn_setchargender(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setchargender gender"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto gender = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto visFlags = npc->visFlags; + if (gender == 0) + visFlags |= PROPID(NPCVisFlags::MALE); + else visFlags &= ~PROPID(NPCVisFlags::MALE); + + npc->setPropWith(SetBy::SERVER, visFlags); + } + } +} + +// setcharprop messagecode,text; +// Sets an NPC' character property. +void fn_setcharprop(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() == 0) + throw std::invalid_argument("invalid arguments: setcharprop messagecode,text"); + + if (auto* messagecode = visitor->getGameValueFromGS1ScriptValue(*arguments[0]); messagecode != nullptr) + { + std::string text; + if (arguments.size() == 2) + text = visitor->getGameValueAs(*arguments[1]); + + messagecode->assign(text); + } +} + +// setcoatcolor color; +// Sets the player's coat color. +void fn_setcoatcolor(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setcoatcolor color"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto color = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + { + auto colors = player->getProp(); + colors.values[1] = static_cast(color); + player->setPropWith(SetBy::SERVER, colors); + } + } +} + +// setgender gender; +// Set's the player's gender (controls which voice is used). +void fn_setgender(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setgender gender"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto gender = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + { + auto status = player->account.status; + if (gender == 0) + status |= PLSTATUS_MALE; + else + status &= ~PLSTATUS_MALE; + + player->setPropWith(SetBy::SERVER, status); + } + } +} + +// sethead filename; +// Sets the player's head image. +void fn_sethead(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: sethead filename"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto filename = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + { + // This needs to go to everybody (for the player list), so we have to send it immediately. + auto results = player->setPropWith(SetBy::SERVER, filename); + results.resultFlags = results.sendToAll; + player->sendPropsFromResults(results); + } + } +} + +// setimg filename; +// Sets the image of the NPC to a new one. +void fn_setimg(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setimg filename"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto filename = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->setPropWith(SetBy::SERVER, filename); + } +} + +// setimgpart filename,x,y,width,height; +// Sets a part of the image for the NPC, allowing for more detailed control over the displayed image. +void fn_setimgpart(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 5) + throw std::invalid_argument("invalid arguments: setimgpart filename,x,y,width,height"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto filename = visitor->getGameValueAs(*arguments[0]); + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2])); + auto width = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[3])); + auto height = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[4])); + + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + npc->setPropWith(SetBy::SERVER, filename); + npc->setPropWith(SetBy::SERVER, x, y, width, height); + } + } +} + +// setlevel filename; +// Warps the player to a new level specified by the filename. +void fn_setlevel(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setlevel filename"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto filename = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->warp(filename, player->account.character.getLocalPosition()); + } +} + +// setlevel2 filename,x,y; +// Warps the player to a new level specified by the filename and coordinates (x, y). +void fn_setlevel2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: setlevel2 filename,x,y"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto filename = visitor->getGameValueAs(*arguments[0]); + auto x = visitor->getGameValueAs(*arguments[1]); + auto y = visitor->getGameValueAs(*arguments[2]); + + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->warp(filename, {static_cast(x * 16), static_cast(y * 16)}); + } +} + +// setmap imgfile,levelsfile,x,y; +// Sets the big map for the player with the specified image file, levels file, and coordinates (x, y). +void fn_setmap(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("invalid arguments: setmap imgfile,levelsfile,x,y"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto imgfile = visitor->getGameValueAs(*arguments[0]); + auto levelsfile = visitor->getGameValueAs(*arguments[1]); + auto x = visitor->getGameValueAs(*arguments[2]); + auto y = visitor->getGameValueAs(*arguments[3]); + + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->sendPacket(CString() >> (char)PLO_BIGMAP << imgfile << "," << levelsfile << "," << CString(x) << "," << CString(y)); + } +} + +// setminimap imgfile,levelsfile,x,y; +// Sets the minimap for the player with the specified image file, levels file, and coordinates (x, y). +void fn_setminimap(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("invalid arguments: setminimap imgfile,levelsfile,x,y"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto imgfile = visitor->getGameValueAs(*arguments[0]); + auto levelsfile = visitor->getGameValueAs(*arguments[1]); + auto x = visitor->getGameValueAs(*arguments[2]); + auto y = visitor->getGameValueAs(*arguments[3]); + + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->sendPacket(CString() >> (char)PLO_MINIMAP << imgfile << "," << levelsfile << "," << CString(x) << "," << CString(y)); + } +} + +// setplayerdir dir; +// Sets the direction of the player sprite. +void fn_setplayerdir(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setplayerdir dir"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto dir = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + { + // Set the new player direction relative to their current sprite. + uint8_t sprite = player->account.character.sprite; + uint8_t currentDir = sprite % 4; + uint8_t newDir = currentDir + (dir - currentDir); + + player->setPropWith(SetBy::SERVER, newDir); + } + } +} + +// setplayerprop messagecode,text; +// Sets a property for the player. +void fn_setplayerprop(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: setplayerprop messagecode,text"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + if (auto* messagecode = visitor->getGameValueFromGS1ScriptValue(*arguments[0]); messagecode != nullptr) + { + auto text = visitor->getGameValueAs(*arguments[1]); + messagecode->assign(text); + } + } +} + +// setpm message; +void fn_setpm(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + std::string message; + + if (arguments.size() != 0) + message = visitor->getGameValueAs(*arguments[0]); + + auto server = BabyDI::Get(); + if (auto npcServerPlayer = server->getNPCServer()->getPlayerNPCServer(); npcServerPlayer != nullptr) + { + auto lines = string::split(message, "#b"sv); + auto finalMessage = string::toCSV(lines, true); + npcServerPlayer->privateMessage = finalMessage; + } +} + +// setshape type,width,height; +// type 1 = rectangle +void fn_setshape(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: setshape type,width,height"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto type = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + if (type != 1) + return; + + auto width = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + auto height = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2])); + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->shape = {width, height}; + } +} + +// setshield image,power; +// Sets the player's shield image. +void fn_setshield(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: setshield image,power"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto image = visitor->getGameValueAs(*arguments[0]); + auto power = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->setPropWith(SetBy::SERVER, image, power); + } +} + +// setshoecolor color; +// Sets the player's shoe color. +void fn_setshoecolor(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setshoecolor color"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto color = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + { + auto colors = player->getProp(); + colors.values[3] = static_cast(color); + player->setPropWith(SetBy::SERVER, colors); + } + } +} + +// setshootparams params; +// Sets the shoot parameters that calls to the shoot command will use. +void fn_setshootparams(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setshootparams params"); + + auto params = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + server->setShootParams(string::fromCSV(params)); +} + +// setskincolor color; +// Sets the player's skin color. +void fn_setskincolor(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setskincolor color"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto color = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + { + auto colors = player->getProp(); + colors.values[0] = static_cast(color); + player->setPropWith(SetBy::SERVER, colors); + } + } +} + +// setsleevecolor color; +// Sets the player's sleeve color. +void fn_setsleevecolor(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: setshoecolor color"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto color = visitor->getGameValueAs(*arguments[0]); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + { + auto colors = player->getProp(); + colors.values[2] = static_cast(color); + player->setPropWith(SetBy::SERVER, colors); + } + } +} + +// setstring var,text; +// Sets a string variable with the given text. +void fn_setstring(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() == 0) + throw std::invalid_argument("invalid arguments: setstring var,text"); + + // Assign the string. + if (auto* var = visitor->getGameValueFromGS1ScriptValue(*arguments[0]); var != nullptr) + { + std::string text; + if (arguments.size() == 2) + text = visitor->getGameValueAs(*arguments[1]); + + // Special handling for prefixed variables. + // Maybe think of a way to do this automatically on the assign rather than doing this. + auto server = BabyDI::Get(); + if (var->identifier.starts_with("client.") || var->identifier.starts_with("clientr.")) + { + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + { + if (text.empty()) + player->deleteFlag(var->identifier, true); + else player->setFlag(var->identifier, text, true); + } + } + } + else if (var->identifier.starts_with("server.") || var->identifier.starts_with("serverr.")) + { + if (text.empty()) + server->deleteFlag(var->identifier, true); + else server->setFlag(std::format("{}={}", var->identifier, text), true); + } + else + { + var->assign(text); + } + } +} + +// setsword image,power; +// Sets the players sword image and power. +void fn_setsword(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: setsword image,power"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto image = visitor->getGameValueAs(*arguments[0]); + auto power = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->setPropWith(SetBy::SERVER, image, power); + } +} + +// setz x,y,width,height,a,b,c,d; +void fn_setz(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + throw std::logic_error("setz is clientside only."); +} + +// shoot x,y,z,angle,zangle,power,gani,ganiattribs; +// Creates a shoot style projectile. +void fn_shoot(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() < 7) + throw std::invalid_argument("invalid arguments: shoot x,y,z,angle,zangle,power,gani,ganiattribs"); + + auto level = visitor->findCurrentLevel(); + if (level == nullptr) + return; + + auto pi = std::numbers::pi; + + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + auto z = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2]) * 16); + auto angle = static_cast(std::clamp(visitor->getGameValueAs(*arguments[3]), 0.0, 2 * pi)); + auto zangle = static_cast(std::clamp(visitor->getGameValueAs(*arguments[4]), -(pi / 2), (pi / 2))); + auto power = static_cast(std::clamp(visitor->getGameValueAs(*arguments[5]), 0.0, 5.0) * 44); + auto gani = visitor->getGameValueAs(*arguments[6]); + + auto server = BabyDI::Get(); + auto gravity = static_cast(server->Scripting.variables.getValue("gravity").value_or(2.0)); + level->addShoot(inform_client, {x, y, z}, angle, zangle, power, gravity, gani, visitor->getOriginalSource()); +} + +// shootarrow dir; +// Shoots an arrow in the specified direction. +void fn_shootarrow(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: shootarrow dir"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto dir = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + const auto& source = visitor->getOriginalSource(); + PixelPosition speed = {(dir == 0 || dir == 2) ? 0 : (dir == 1 ? -16 : 16), (dir == 1 || dir == 3) ? 0 : (dir == 0 ? -16 : 16)}; + + auto sourcePosition = getPositionForArrow(source, dir); + if (!sourcePosition.has_value()) + return; + + level->addArrow(inform_client, sourcePosition.value(), speed, dir, arrowTypeNormal, source); + } +} + +// shootball; +// (gr) shootball dir; +void fn_shootball(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + // TODO(GS1): Conformance modes. + + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: shootball dir"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto dir = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + const auto& source = visitor->getOriginalSource(); + PixelPosition speed = {(dir == 0 || dir == 2) ? 0 : (dir == 1 ? -16 : 16), (dir == 1 || dir == 3) ? 0 : (dir == 0 ? -16 : 16)}; + + auto sourcePosition = getPositionForArrow(source, dir); + if (!sourcePosition.has_value()) + return; + + level->addArrow(inform_client, sourcePosition.value(), speed, dir, arrowTypeBall, source); + } +} + +// shootfireball dir; +// Shoots a fireball in the specified direction. +void fn_shootfireball(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: shootfireball dir"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto dir = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + const auto& source = visitor->getOriginalSource(); + PixelPosition speed = {(dir == 0 || dir == 2) ? 0 : (dir == 1 ? -16 : 16), (dir == 1 || dir == 3) ? 0 : (dir == 0 ? -16 : 16)}; + + auto sourcePosition = getPositionForArrow(source, dir); + if (!sourcePosition.has_value()) + return; + + level->addArrow(inform_client, sourcePosition.value(), speed, dir, arrowTypeFireball, source); + } +} + +// shootfireblast dir; +// Shoots a fireblast in the specified direction. +void fn_shootfireblast(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: shootfireblast dir"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto dir = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + const auto& source = visitor->getOriginalSource(); + PixelPosition speed = {(dir == 0 || dir == 2) ? 0 : (dir == 1 ? -16 : 16), (dir == 1 || dir == 3) ? 0 : (dir == 0 ? -16 : 16)}; + + auto sourcePosition = getPositionForArrow(source, dir); + if (!sourcePosition.has_value()) + return; + + level->addArrow(inform_client, sourcePosition.value(), speed, dir, arrowTypeFireblast, source); + } +} + +// shootnuke dir; +// Shoots a nuke in the specified direction. +void fn_shootnuke(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: shootnuke dir"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto dir = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + const auto& source = visitor->getOriginalSource(); + PixelPosition speed = {(dir == 0 || dir == 2) ? 0 : (dir == 1 ? -16 : 16), (dir == 1 || dir == 3) ? 0 : (dir == 0 ? -16 : 16)}; + + auto sourcePosition = getPositionForArrow(source, dir); + if (!sourcePosition.has_value()) + return; + + level->addArrow(inform_client, sourcePosition.value(), speed, dir, arrowTypeNukeshot, source); + } +} + +// show; +// Makes the NPC visible. +void fn_show(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->setPropWith(SetBy::SERVER, static_cast(npc->visFlags | (uint8_t)NPCVisFlags::VISIBLE)); + } +} + +// showani index,x,y,direction,gani,params; +// Shows a gani at the specified position. +void fn_showani(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 5) + throw std::invalid_argument("invalid arguments: showani index,x,y,direction,gani,params"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2]) * 16); + auto direction = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[3])); + auto gani = visitor->getGameValueAs(*arguments[4]); + + server->getNPCServer()->showGani(npc, index, {x, y}, gani, direction); + } + } +} + +// showani2 index,x,y,z,direction,gani,params; +// Shows a gani at the specified position. +void fn_showani2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 6) + throw std::invalid_argument("invalid arguments: showani2 index,x,y,z,direction,gani,params"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2]) * 16); + auto z = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[3]) * 16); + auto direction = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[4])); + auto gani = visitor->getGameValueAs(*arguments[5]); + + server->getNPCServer()->showGani(npc, index, {x, y, z}, gani, direction); + } + } +} + +// showcharacter; +// Turns the NPC into a character. +void fn_showcharacter(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + npc->setPropWith(SetBy::SERVER, "#c#"s); + npc->shape = {0, 0}; + } + } +} + +// showimg index,filename,x,y; +// Displays an image on the level at the specified coordinates. +void fn_showimg(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("invalid arguments: showimg index,filename,x,y"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto filename = visitor->getGameValueAs(*arguments[1]); + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[3]) * 16); + + server->getNPCServer()->showImage(npc, index, {x, y}, filename); + } + } +} + +// showimg2 index,filename,x,y,z; +// Displays an image on the level at the specified coordinates. +void fn_showimg2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 5) + throw std::invalid_argument("invalid arguments: showimg2 index,filename,x,y,z"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto filename = visitor->getGameValueAs(*arguments[1]); + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[3]) * 16); + auto z = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[4]) * 16); + + server->getNPCServer()->showImage(npc, index, {x, y, z}, filename); + } + } +} + +// showpoly index,{ x1,y1,...,xn,yn }; +// Displays a polygon at the specified coordinates. +void fn_showpoly(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: showpoly index,{ x1,y1,...,xn,yn }"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto polygons = visitor->getGameValueAs>(*arguments[1]); + + if (polygons.size() == 0 || polygons.size() % 2 != 0) + throw std::invalid_argument("invalid arguments: showpoly index,{ x1,y1,...,xn,yn }"); + + server->getNPCServer()->showPoly(npc, index, polygons); + } + } +} + +// showpoly2 index,{ x1,y1,z1,...,xn,yn,zn }; +// Displays a polygon at the specified coordinates. +void fn_showpoly2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: showpoly2 index,{ x1,y1,z1,...,xn,yn,zn }"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto polygons = visitor->getGameValueAs>(*arguments[1]); + + if (polygons.size() == 0 || polygons.size() % 3 != 0) + throw std::invalid_argument("invalid arguments: showpoly2 index,{ x1,y1,z1,...,xn,yn,zn }"); + + server->getNPCServer()->showPoly(npc, index, polygons); + } + } +} + +// showstats bitflag; +void fn_showstats(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + throw unimplemented_error("showstats is not implemented yet."); +} + +// showtext index,x,y,font,style,text; +// Displays text at the specified coordinates. +void fn_showtext(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 6) + throw std::invalid_argument("invalid arguments: showtext index,x,y,font,style,text"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2]) * 16); + auto font = visitor->getGameValueAs(*arguments[3]); + auto style = visitor->getGameValueAs(*arguments[4]); + auto text = visitor->getGameValueAs(*arguments[5]); + + server->getNPCServer()->showText(npc, index, {x, y}, text, font, style); + } + } +} + +// showtext2 index,x,y,z,font,style,text; +// Displays text at the specified coordinates. +void fn_showtext2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 7) + throw std::invalid_argument("invalid arguments: showtext2 index,x,y,z,font,style,text"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2]) * 16); + auto z = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[3]) * 16); + auto font = visitor->getGameValueAs(*arguments[4]); + auto style = visitor->getGameValueAs(*arguments[5]); + auto text = visitor->getGameValueAs(*arguments[6]); + + server->getNPCServer()->showText(npc, index, {x, y, z}, text, font, style); + } + } +} + +// sleep duration; +// Pauses script execution for the specified duration in seconds. +void fn_sleep(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: sleep duration"); + + if (auto source = visitor->getOriginalSource(); source.second == ScriptObjectType::NPC) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.first); npc != nullptr) + { + auto duration = visitor->getGameValueAs(*arguments[0]); + npc->timeout = std::chrono::duration_cast(duration_seconds_double(duration)); + throw sleep_exception{}; + } + } +} + +// spyfire length,power; +// Sends a spyfire explosion from the player. +void fn_spyfire(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: spyfire length,power"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto length = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])) & 0b11111; + auto power = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])) & 0b111; + uint8_t length_power = (length << 3) | power; + + auto server = BabyDI::Get(); + if (auto player = server->getPlayer(source.value().first); player != nullptr) + { + if (auto level = player->getLevel(); level != nullptr) + { + server->sendPacketToNearby(CString() >> (char)PLO_FIRESPY >> (short)source.value().first >> (char)(length_power), player->account.character.getGlobalPosition(), level); + level->addSpyFire(player->account.character.getGlobalPosition(), source.value(), player->account.character.direction, length, power); + } + } + } +} + +// take itemname; +// Takes an item on the level in a 10-tile radius from the NPC. +void fn_take(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: take itemname"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + if (auto level = npc->getLevel(); level != nullptr) + { + auto itemname = std::clamp(DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])), 0_ui8, 24_ui8); + + // Get our search position. + PixelPosition searchPosition = npc->character.getGlobalPosition(); + if (npc->isCharacter()) + searchPosition.translate(static_cast(8), static_cast(16 * 3)); + + // Find all the items within 10 tiles of the NPC. + std::vector itemIndices; + auto& levelItems = level->getItems(); + for (size_t i = levelItems.size(); i > 0; --i) + { + auto& item = levelItems[i - 1]; + if (PROPID(item.item) != itemname) + continue; + + auto distance = static_cast(std::hypot(item.position.x() - searchPosition.x(), item.position.y() - searchPosition.y())); + if (distance <= (10 * 16)) + { + itemIndices.push_back(static_cast(i - 1)); + if (LevelItem::isRupeeType(item.item)) + npc->setPropWith(SetBy::SERVER, npc->getProp().value + LevelItem::GetRupeeCount(item.item)); + else if (item.item == LevelItemType::HEART) + npc->setPropWith(SetBy::SERVER, static_cast(npc->getProp().value + 2)); + else if (item.item == LevelItemType::DARTS) + npc->setPropWith(SetBy::SERVER, static_cast(npc->getProp().value + 5)); + else if (item.item == LevelItemType::BOMBS) + npc->setPropWith(SetBy::SERVER, static_cast(npc->getProp().value + 5)); + else if (item.item == LevelItemType::GLOVE1) + npc->setPropWith(SetBy::SERVER, std::max(npc->getProp().value, 1_ui8)); + else if (item.item == LevelItemType::GLOVE2) + npc->setPropWith(SetBy::SERVER, std::max(npc->getProp().value, 2_ui8)); + } + } + + // Remove all taken items. + for (auto& index : itemIndices) + level->removeItem(inform_client, index); + } + } + } +} + +// take2 index; +// Takes an item at the specified index on the level. +void fn_take2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: take2 index"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + if (auto level = npc->getLevel(); level != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + if (auto item = level->getItem(index); item.has_value()) + { + if (LevelItem::isRupeeType(item.value()->item)) + npc->setPropWith(SetBy::SERVER, npc->getProp().value + LevelItem::GetRupeeCount(item.value()->item)); + else if (item.value()->item == LevelItemType::HEART) + npc->setPropWith(SetBy::SERVER, static_cast(npc->getProp().value + 2)); + else if (item.value()->item == LevelItemType::DARTS) + npc->setPropWith(SetBy::SERVER, static_cast(npc->getProp().value + 5)); + else if (item.value()->item == LevelItemType::BOMBS) + npc->setPropWith(SetBy::SERVER, static_cast(npc->getProp().value + 5)); + else if (item.value()->item == LevelItemType::GLOVE1) + npc->setPropWith(SetBy::SERVER, std::max(npc->getProp().value, 1_ui8)); + else if (item.value()->item == LevelItemType::GLOVE2) + npc->setPropWith(SetBy::SERVER, std::max(npc->getProp().value, 2_ui8)); + + level->removeItem(inform_client, index); + } + } + } + } +} + +// takehorse index; +// Mounts the horse at the specified index on the level. +void fn_takehorse(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: takehorse index"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + if (auto level = npc->getLevel(); level != nullptr) + { + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + if (auto horse = level->getHorse(index); horse.has_value()) + { + npc->setPropWith(SetBy::SERVER, horse.value()->image); + level->removeHorse(inform_client, index); + } + } + } + } +} + +// takeplayercarry; +// Takes the carried object from the player. +void fn_takeplayercarry(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + { + player->sendPacket(CString() >> (char)PLO_THROWCARRIED >> (short)player->getId()); + player->setPropWith(SetBy::SERVER, 0xFF_ui8); + } + } +} + +// takeplayerhorse; +// Takes the horse from the player. +void fn_takeplayerhorse(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->setPropWith(SetBy::SERVER, std::string{}); + } +} + +// throwcarry; +// Throws the carried object. +void fn_throwcarry(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr && npc->isCharacter() && npc->character.gani.starts_with("carry")) + npc->setPropWith(SetBy::SERVER, "idle"sv); + } +} + +// timershow; +// Shows the NPC's clientside timeout counter. +void fn_timershow(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->setPropWith(SetBy::SERVER, static_cast(npc->visFlags | PROPID(NPCVisFlags::TIMERSHOW))); + } +} + +// tokenize text; +// Tokenizes a string into tokens using spaces as a delimiter. +void fn_tokenize(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: tokenize text"); + + auto text = visitor->getGameValueAs(*arguments[0]); + visitor->tokenizeTokens = string::splitToVector(text, " "sv); + visitor->builtInStore->add(GameValue{set_temporary, "tokenscount", static_cast(visitor->tokenizeTokens.size())}); +} + +// tokenize2 delims,text; +// Tokenizes a string into tokens using the specified delimiters. +void fn_tokenize2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("invalid arguments: tokenize2 delims,text"); + + auto delims = visitor->getGameValueAs(*arguments[0]); + auto text = visitor->getGameValueAs(*arguments[1]); + visitor->tokenizeTokens = string::splitToVector(text, delims); + visitor->builtInStore->add(GameValue{set_temporary, "tokenscount", static_cast(visitor->tokenizeTokens.size())}); +} + +// toweapons name; +// Adds the NPC as a weapon for the player. +void fn_toweapons(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: toweapons name"); + + // Get our source NPC. + auto server = BabyDI::Get(); + const auto& source = visitor->getOriginalSource(); + if (source.second != ScriptObjectType::NPC || server == nullptr) + return; + + // Get the active player. + PlayerPtr player = nullptr; + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + player = server->getNPCServer()->getPlayer(source.value().first); + if (player == nullptr) + return; + + if (auto npc = server->getNPC(source.first); npc != nullptr) + { + // Get or create the weapon, and make sure the script is current. + auto name = visitor->getGameValueAs(*arguments[0]); + auto weapon = server->getWeapon(name); + if (weapon == nullptr) + { + weapon = std::make_shared(name, npc->image, std::string{npc->getScript().getOriginalSource()}); + weapon->saveWeapon(); + server->NC_AddWeapon(weapon); + } + // Script differs, update the weapon. + else if (weapon->getScript().getOriginalSource() != npc->getScript().getOriginalSource()) + { + weapon->updateWeapon(npc->image, std::string{npc->getScript().getOriginalSource()}).saveWeapon(); + server->updateWeaponForPlayers(weapon); + } + + // Give the weapon to the player. + player->addWeapon(weapon); + } +} + +// triggeraction x,y,action,params; +// Sends out a trigger action. +void fn_triggeraction(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() < 4) + throw std::invalid_argument("invalid arguments: triggeraction x,y,action,params"); + + auto x = visitor->getGameValueAs(*arguments[0]); + auto y = visitor->getGameValueAs(*arguments[1]); + auto action = visitor->getGameValueAs(*arguments[2]); + auto params = string::toCSV(arguments | std::views::drop(3) | std::views::transform([&visitor](GS1ScriptValue* value) + { + return visitor->getGameValueAs(*value); + })); + + auto server = BabyDI::Get(); + if (action == "clientside") + { + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + server->sendTriggerAction(source.value().first, 0, {0, 0}, action, params); + } + else + { + const auto& currentSource = visitor->getCurrentSource(); + LevelPtr targetLevel = visitor->findCurrentLevel(); + uint32_t npcId = 0; + if (currentSource.second == ScriptObjectType::NPC) + npcId = currentSource.first; + if (targetLevel != nullptr) + server->sendTriggerAction(targetLevel, npcId, {static_cast(x * 16), static_cast(y * 16)}, action, params); + } +} + +// unfreezeplayer; +// Unfreezes a player. +void fn_unfreezeplayer(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->unfreezePlayer(); + } +} + +// unset flag; +// Unsets a player's flag. +void fn_unset(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("invalid arguments: unset flag"); + + if (auto* flag = visitor->getGameValueFromGS1ScriptValue(*arguments[0]); flag != nullptr) + { + auto server = BabyDI::Get(); + if (flag->identifier.starts_with("client.") || flag->identifier.starts_with("clientr.")) + { + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + if (auto player = server->getNPCServer()->getPlayer(source.value().first); player != nullptr) + player->deleteFlag(flag->identifier, true); + } + } + else if (flag->identifier.starts_with("server.") || flag->identifier.starts_with("serverr.")) + { + server->deleteFlag(flag->identifier); + } + else + { + flag->assign(false); + } + } +} + +// updateboard x,y,width,height; +// Updates a portion of the map board, making changes visible to other players. +void fn_updateboard(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("invalid arguments: updateboard x,y,width,height"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto x = static_cast(std::max(0.0, visitor->getGameValueAs(*arguments[0]))); + auto y = static_cast(std::max(0.0, visitor->getGameValueAs(*arguments[1]))); + auto width = static_cast(std::max(0.0, visitor->getGameValueAs(*arguments[2]))); + auto height = static_cast(std::max(0.0, visitor->getGameValueAs(*arguments[3]))); + level->updateBoard({{x, y}, {width, height}}); + } +} + +// updateboard2 x,y,width,height; +// Updates a portion of the map board, saves the changes to the map file, and makes the changes visible to other players. +void fn_updateboard2(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("invalid arguments: updateboard2 x,y,width,height"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto x = static_cast(std::max(0.0, visitor->getGameValueAs(*arguments[0]))); + auto y = static_cast(std::max(0.0, visitor->getGameValueAs(*arguments[1]))); + auto width = static_cast(std::max(0.0, visitor->getGameValueAs(*arguments[2]))); + auto height = static_cast(std::max(0.0, visitor->getGameValueAs(*arguments[3]))); + level->updateBoard2({{x, y}, {width, height}}); + } +} + +// updateterrain; +void fn_updateterrain(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + throw std::logic_error("updateterrain is clientside only."); +} + +// warpto levelname,x,y; +// Warps an NPC to a new level. +void fn_warpto(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("invalid arguments: warpto levelname,x,y"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto filename = visitor->getGameValueAs(*arguments[0]); + auto x = visitor->getGameValueAs(*arguments[1]); + auto y = visitor->getGameValueAs(*arguments[2]); + + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + { + if (auto level = server->getLoadedLevel(filename, npc->getLevel()); level != nullptr) + npc->warp(level, {static_cast(x * 16), static_cast(y * 16)}); + } + } +} + +//---------------------------- + +// enabledamagereactions; +// Enables damage reactions for the NPC, allowing it to react to damage with animations and invincibility frames. +void fn_enabledamagereactions(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->allowServerDamageReactions = true; + } +} + +// disabledamagereactions; +// Disables damage reactions for the NPC, going back to the default behavior of not automatically reacting to damage. +void fn_disabledamagereactions(GS1Visitor* visitor, std::string_view commandName, const std::vector& arguments) +{ + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr) + npc->allowServerDamageReactions = false; + } +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1::grammar diff --git a/server/src/scripting/gs1/GS1Flags.cpp b/server/src/scripting/gs1/GS1Flags.cpp new file mode 100644 index 000000000..a58c02ad3 --- /dev/null +++ b/server/src/scripting/gs1/GS1Flags.cpp @@ -0,0 +1,177 @@ +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1 +{ +/////////////////////////////////////////////////////////////////////////////// + +void setEventFlags(ScriptEventType event, GameVariableStore& variableStore) +{ + // Set all our built-in event flags. + for (auto& [eventType, flagName] : eventFlagMap) + variableStore.add(flagName, event == eventType); + + // Valid alternates. + variableStore.add("playerhurted", event == ScriptEventType::PLAYERHURT); + variableStore.add("wasshooted", event == ScriptEventType::WASSHOT); + + // TODO: Put extensions under a server option? + variableStore.add("playertouchesme", event == ScriptEventType::PLAYERTOUCHSME); + variableStore.add("playertouchesother", event == ScriptEventType::PLAYERTOUCHSOTHER); + + /* + washit the npc was slayed with a sword or axe + waspelt the npc was pelt + wasthrown the npc was carried and then thrown + emoticon + */ +} + +void setTriggerActionAndCustomEventFlags(ScriptEvent& event, GameVariableStore& variableStore) +{ + if (event.args.empty() || (event.type != ScriptEventType::TRIGGERACTION && event.type != ScriptEventType::CUSTOM)) + return; + + std::string action; + if (auto* actionStr = std::any_cast(&event.args[0]); actionStr != nullptr) + action = *actionStr; + else if (auto* actionStr = std::any_cast(&event.args[0]); actionStr != nullptr) + action = *actionStr; + else if (auto* actionStr = std::any_cast(&event.args[0]); actionStr != nullptr) + action = std::string(*actionStr); + + if (!action.empty()) + { + if (event.type == ScriptEventType::TRIGGERACTION) + action.insert(0, "action"); + + // Set the action flag. + // Set both the original action and a lowercased version. + variableStore.add(GameValue{ set_temporary, action, true }); + variableStore.add(GameValue{ set_temporary, string::toLower(action), true }); + + // If there are just two arguments, try to unpack the second argument. + if (event.args.size() == 2) + { + if (auto* params = std::any_cast(&event.args[1]); params != nullptr) + { + // Split the parameters by commas. + auto tokens = string::fromCSV(*params); + if (tokens.size() > 1) + { + event.args.erase(event.args.begin() + 1); + event.args.insert(event.args.end(), std::ranges::begin(tokens), std::ranges::end(tokens)); + } + } + } + } +} + +void setPlayerFlags(GameVariableStore& variableStore, NPCPtr npc, PlayerClientPtr player) +{ + if (player == nullptr) + return; + + variableStore.add("canspin", (player->account.status & PLSTATUS_HASSPIN) != 0); + + variableStore.add("carrying", player->getCarrySprite() != PROPID(CarryObjectSprite::NONE)); + variableStore.add("carriesblackstone", player->getCarrySprite() == PROPID(CarryObjectSprite::BLACKSTONE)); + variableStore.add("carriesbush", player->getCarrySprite() == PROPID(CarryObjectSprite::BUSH)); + variableStore.add("carriesnpc", player->getCarryNPC() != 0); + variableStore.add("carriessign", player->getCarrySprite() == PROPID(CarryObjectSprite::SIGN)); + variableStore.add("carriesstone", player->getCarrySprite() == PROPID(CarryObjectSprite::STONE)); + variableStore.add("carriesvase", player->getCarrySprite() == PROPID(CarryObjectSprite::VASE)); + + variableStore.add("weaponsenabled", (player->account.status & PLSTATUS_ALLOWWEAPONS) != 0); + variableStore.add("playerpaused", (player->account.status & PLSTATUS_PAUSED) != 0); + variableStore.add("playerismale", (player->account.status & PLSTATUS_MALE) != 0); + variableStore.add("playerisfemale", (player->account.status & PLSTATUS_MALE) == 0); + variableStore.add("playeronhorse", !player->account.character.horseImage.empty()); + variableStore.add("playeronline", true); + variableStore.add("playerattached", npc != nullptr && player->getAttachedNPC() == npc->id); + + auto level = player->getLevel(); + variableStore.add("isleader", level != nullptr && level->isPlayerLeader(player->getId())); + + // playertrial +} + +void setNPCFlags(ScriptEvent& event, GameVariableStore& variableStore, NPCPtr npc) +{ + if (npc == nullptr) + return; + + variableStore.add("visible", npc->visFlags != PROPID(NPCVisFlags::HIDDEN)); + variableStore.add("shotbyplayer", event.type == ScriptEventType::WASSHOT && event.initiator.second == ScriptObjectType::PLAYER); + variableStore.add("shotbybaddy", event.type == ScriptEventType::WASSHOT && event.initiator.second == ScriptObjectType::SERVER); + + // Extension. + variableStore.add("shotbynpc", event.type == ScriptEventType::WASSHOT && event.initiator.second == ScriptObjectType::NPC); + + variableStore.add("peltwithblackstone", false); + variableStore.add("peltwithbush", false); + variableStore.add("peltwithnpc", false); + variableStore.add("peltwithsign", false); + variableStore.add("peltwithstone", false); + variableStore.add("peltwithvase", false); +} + +void setLevelFlags(GameVariableStore& variableStore, NPCPtr npc, LevelPtr level) +{ + variableStore.add("issparringzone", level != nullptr && npc != nullptr && level->isSparringZone(npc->character.getMapPosition())); + variableStore.add("nopkzone", level != nullptr && npc != nullptr && level->isNoPkZone(npc->character.getMapPosition())); + variableStore.add("isonmap", level != nullptr && level->getMap() != nullptr); + variableStore.add("compsdead", level != nullptr && !level->hasLivingBaddies()); +} + +void setWeaponFlags(ScriptEvent& event, ScriptObject source, GameVariableStore& variableStore) +{ + variableStore.add("isweapon", source.second == ScriptObjectType::WEAPON); +} + +void setOtherFlags(ScriptEvent& event, ScriptObject source, GameVariableStore& variableStore, NPCPtr npc, PlayerClientPtr player, LevelPtr level) +{ + // actionplayer + if (event.type == ScriptEventType::TRIGGERACTION && event.initiator.second == ScriptObjectType::PLAYER && level != nullptr) + { + bool found = false; + size_t index = 0; + for (const auto& playerId : level->getPlayers()) + { + if (playerId == static_cast(source.first)) + { + found = true; + break; + } + ++index; + } + variableStore.add("actionplayer", GameValue{ (double)(found ? index : -1) }); + } + + // playerswimming + variableStore.add("playerswimming", player != nullptr && level != nullptr && inList(level->getTileTypeAt(player->getGlobalPosition().translate(24, 32)), tileset::TileType::WATER, tileset::TileType::LAVA)); + + /* Older flags: + * 'gotbow' and 'gotsword' are older pre-1.3 flags. + */ +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1 diff --git a/server/src/scripting/gs1/GS1Functions.cpp b/server/src/scripting/gs1/GS1Functions.cpp new file mode 100644 index 000000000..30a2ca292 --- /dev/null +++ b/server/src/scripting/gs1/GS1Functions.cpp @@ -0,0 +1,1459 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +//#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1::grammar +{ +/////////////////////////////////////////////////////////////////////////////// + +using BuiltInFunctionHandleFunc = GS1ScriptValue (*)(GS1Visitor*, std::string_view, const std::vector&); +using BuiltInFunctionHandleMap = std::unordered_map; + +static GS1ScriptValue fn_abs(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_aindexof(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_arctan(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_arraylen(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_ascii(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_base64decode(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_base64encode(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_cos(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_exp(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_findnearestplayer(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_findnearestplayers(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_getangle(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_getareanpcs(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_getdir(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_getflagkeys(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_getnearestplayer(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_getnearestplayers(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_getnpc(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_getplayer(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_getz(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_hasweapon(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_imgheight(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_imgwidth(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_indexof(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_int(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_keycode(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_keydown(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_keydown2(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_lindexof(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_log(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_max(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_min(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_onmapx(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_onmapy(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_onwall(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_onwall2(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_onwater(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_onwater2(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_playersays(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_playersays2(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_random(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_sarraylen(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_screenx(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_screeny(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_sin(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_startswith(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_strcontains(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_strequals(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_strlen(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_strtofloat(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_testbomb(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_testcompu(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_testexplo(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_testhorse(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_testitem(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_testnpc(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_testplayer(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_testsign(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_textheight(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_textwidth(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_tiletype(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_vecx(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_vecy(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_worldx(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); +static GS1ScriptValue fn_worldy(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments); + +static BuiltInFunctionHandleMap GenerateMap() +{ + string::string_hash hash{}; + BuiltInFunctionHandleMap map = + { + {hash("abs"), &fn_abs}, + {hash("aindexof"), &fn_aindexof}, + {hash("arctan"), &fn_arctan}, + {hash("arraylen"), &fn_arraylen}, + {hash("ascii"), &fn_ascii}, + {hash("base64decode"), &fn_base64decode}, + {hash("base64encode"), &fn_base64encode}, + {hash("cos"), &fn_cos}, + {hash("exp"), &fn_exp}, + {hash("findnearestplayer"), &fn_findnearestplayer}, + {hash("findnearestplayers"), &fn_findnearestplayers}, + {hash("getangle"), &fn_getangle}, + {hash("getareanpcs"), &fn_getareanpcs}, + {hash("getdir"), &fn_getdir}, + {hash("getflagkeys"), &fn_getflagkeys}, + {hash("getnearestplayer"), &fn_getnearestplayer}, + {hash("getnearestplayers"), &fn_getnearestplayers}, + {hash("getnpc"), &fn_getnpc}, + {hash("getplayer"), &fn_getplayer}, + {hash("getz"), &fn_getz}, + {hash("hasweapon"), &fn_hasweapon}, + {hash("imgheight"), &fn_imgheight}, + {hash("imgwidth"), &fn_imgwidth}, + {hash("indexof"), &fn_indexof}, + {hash("int"), &fn_int}, + {hash("keycode"), &fn_keycode}, + {hash("keydown"), &fn_keydown}, + {hash("keydown2"), &fn_keydown2}, + {hash("lindexof"), &fn_lindexof}, + {hash("log"), &fn_log}, + {hash("max"), &fn_max}, + {hash("min"), &fn_min}, + {hash("onmapx"), &fn_onmapx}, + {hash("onmapy"), &fn_onmapy}, + {hash("onwall"), &fn_onwall}, + {hash("onwall2"), &fn_onwall2}, + {hash("onwater"), &fn_onwater}, + {hash("onwater2"), &fn_onwater2}, + {hash("playersays"), &fn_playersays}, + {hash("playersays2"), &fn_playersays2}, + {hash("random"), &fn_random}, + {hash("sarraylen"), &fn_sarraylen}, + {hash("screenx"), &fn_screenx}, + {hash("screeny"), &fn_screeny}, + {hash("sin"), &fn_sin}, + {hash("startswith"), &fn_startswith}, + {hash("strcontains"), &fn_strcontains}, + {hash("strequals"), &fn_strequals}, + {hash("strlen"), &fn_strlen}, + {hash("strtofloat"), &fn_strtofloat}, + {hash("testbomb"), &fn_testbomb}, + {hash("testcompu"), &fn_testcompu}, + {hash("testexplo"), &fn_testexplo}, + {hash("testhorse"), &fn_testhorse}, + {hash("testitem"), &fn_testitem}, + {hash("testnpc"), &fn_testnpc}, + {hash("testplayer"), &fn_testplayer}, + {hash("testsign"), &fn_testsign}, + {hash("textheight"), &fn_textheight}, + {hash("textwidth"), &fn_textwidth}, + {hash("tiletype"), &fn_tiletype}, + {hash("vecx"), &fn_vecx}, + {hash("vecy"), &fn_vecy}, + {hash("worldx"), &fn_worldx}, + {hash("worldy"), &fn_worldy}, + }; + return map; +} + +constexpr std::array flagProcessingFunctions = +{ + "lindexof"sv, + "sarraylen"sv, +}; + +/////////////////////////////////////////////////////////////////////////////// + +GS1ScriptValue processBuiltInFunction(GS1Visitor* visitor, antlr4::tree::ParseTree* node, std::string_view functionName) +{ + static BuiltInFunctionHandleMap map = GenerateMap(); + + if (visitor == nullptr) + throw std::runtime_error("processBuiltInFunction received an empty visitor"); + if (functionName.empty()) + throw std::runtime_error("processBuiltInFunction received an empty function name"); + + // Find the command in the map. + size_t hash = string::string_hash{}(functionName); + auto it = map.find(hash); + if (it == map.end()) + { + log::printLine(log::script, "Unknown function in NPC '{}': {}", visitor->who, functionName); + return {}; + } + + // Record if we are expecting a flag. + bool oldExpectingFlag = visitor->expectingFlag; + visitor->expectingFlag = (std::ranges::find(flagProcessingFunctions, functionName) != std::ranges::end(flagProcessingFunctions)); + + // Collect the arguments from the node. + std::vector arguments; + std::vector results; + for (size_t i = 0; i < node->children.size(); ++i) + { + auto ret = node->children[i]->accept(visitor); + if (ret.has_value()) + { + results.emplace_back(std::move(ret)); + auto* container = std::any_cast(&results.back()); + if (container == nullptr) + throw std::runtime_error("BuiltInFunction argument is not a valid GS1ScriptValue"); + + arguments.push_back(std::move(container)); + + // Reset the expectingFlag toggle back to normal. + visitor->expectingFlag = oldExpectingFlag; + } + } + + // Reset the expectingFlag toggle back to normal. + visitor->expectingFlag = oldExpectingFlag; + + // Execute the command. + return it->second(visitor, functionName, arguments); +} + +/////////////////////////////////////////////////////////////////////////////// + +// abs(value) +// Absolute value of a number. +GS1ScriptValue fn_abs(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function abs requires exactly one argument"); + + auto value = visitor->getGameValueAs(*arguments[0]); + return std::abs(value); +} + +// aindexof(value, array) +// Returns the index of the first occurrence of value in the array, or -1 if not found. +GS1ScriptValue fn_aindexof(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function aindexof requires exactly two arguments"); + + auto value = visitor->getGameValueAs(*arguments[0]); + auto array = visitor->getGameValueAs>(*arguments[1]); + + auto result = std::ranges::find(array, value); + if (result == std::ranges::end(array)) + return -1.0; + + auto distance = std::ranges::distance(std::ranges::begin(array), result); + return static_cast(distance); +} + +// arctan(value) +// Returns the arctangent of the value in radians. +GS1ScriptValue fn_arctan(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function arctan requires exactly one argument"); + + auto value = visitor->getGameValueAs(*arguments[0]); + return std::atan(value); +} + +// arraylen(array) +// Returns the length of the array. +GS1ScriptValue fn_arraylen(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function arraylen requires exactly one argument"); + + auto array = visitor->getGameValueAs>(*arguments[0]); + + return static_cast(array.size()); +} + +// ascii(string) +// Returns the ASCII value of the first character in the string. +GS1ScriptValue fn_ascii(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function ascii requires exactly one argument"); + + auto str = visitor->getGameValueAs(*arguments[0]); + if (str.empty()) + return 0.0; + + return static_cast(static_cast(str[0])); +} + +// base64decode(string) +// Decodes a Base64 encoded string. +GS1ScriptValue fn_base64decode(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function base64decode requires exactly one argument"); + + auto input = visitor->getGameValueAs(*arguments[0]); + auto output = std::make_unique(input.length()); + unsigned long outputLength = input.length(); + base64_decode(input.c_str(), input.length(), output.get(), &outputLength); + + return std::string{reinterpret_cast(output.get()), outputLength}; +} + +// base64encode(string) +// Encodes a string to Base64 format. +GS1ScriptValue fn_base64encode(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function base64encode requires exactly one argument"); + + auto input = visitor->getGameValueAs(*arguments[0]); + + // Calculate the length of the resulting base64 string. + unsigned long outputLength = 4 * ((input.length() + 2) / 3); + + // Encode. + auto output = std::make_unique(outputLength); + base64_encode(reinterpret_cast(input.c_str()), static_cast(input.length()), output.get(), &outputLength); + + return std::string{output.get(), outputLength}; +} + +// cos(value) +// Returns the cosine of the value in radians. +GS1ScriptValue fn_cos(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function cos requires exactly one argument"); + + auto value = visitor->getGameValueAs(*arguments[0]); + return std::cos(value); +} + +// exp(value) +// Computes e raised to the power of the value. +GS1ScriptValue fn_exp(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function cos requires exactly one argument"); + + auto value = visitor->getGameValueAs(*arguments[0]); + return std::exp(value); +} + +// findnearestplayer(x, y) +// Finds the nearest player to the specified position and returns a player source. +GS1ScriptValue fn_findnearestplayer(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function findnearestplayer requires exactly two arguments"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto x = static_cast(visitor->getGameValueAs(*arguments[0])); + auto y = static_cast(visitor->getGameValueAs(*arguments[1])); + auto position = toPixelPosition({x, y}); + + // Find the nearest player. + std::tuple nearestPlayer{0, std::numeric_limits::max()}; + auto* server = BabyDI::Get(); + for (const auto& id : level->findInRangePlayers(position)) + { + if (auto player = server->getNPCServer()->getPlayer(id); player != nullptr) + { + TilePosition playerPos = toTilePosition(player->account.character.getGlobalPosition()); + auto distance = std::hypot(playerPos.x() - x, playerPos.y() - y); + if (distance < std::get<1>(nearestPlayer)) + nearestPlayer = {id, distance}; + } + } + + // Return the closest player. + if (std::get<0>(nearestPlayer) != 0) + return ScriptObject{std::get<0>(nearestPlayer), ScriptObjectType::PLAYER}; + } + + return 0.0; +} + +// findnearestplayers(x, y) +// Finds all players in the level, orders them by distance from the specified position, and returns a list of player sources. +// Probably not supported in GS1. +GS1ScriptValue fn_findnearestplayers(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw unimplemented_error("Built-in function findnearestplayers not implemented"); +} + +// getangle(dx, dy) +// Returns the angle in radians from the current position to the position specified by dx and dy. +GS1ScriptValue fn_getangle(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + /* + ( 0,-1) up: 1.570796 (pi/2) + (-1, 0) left: 3.141593 (pi) + ( 0, 1) down: 4.712389 (3pi/2) + ( 1, 0) right: 0.000000 (0) + */ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function getangle requires exactly two arguments"); + + auto dx = visitor->getGameValueAs(*arguments[0]); + auto dy = visitor->getGameValueAs(*arguments[1]); + + // No angle if no direction is specified. + if (DoubleIsZero(dx) && DoubleIsZero(dy)) + return 0.0; + + // Flip the Y coordinate to match the game's coordinate system. + dy = -dy; + + // Get the angle. + auto angle = std::atan2(dy, dx); + + // If the angle is negative, we need to adjust it to be in the range [0, 2Ï€). + if (angle < 0.0) + angle += std::numbers::pi * 2; + + return angle; +} + +// getareanpcs(x, y, width, height) +// Returns the indices of all NPCS in the area specified. +GS1ScriptValue fn_getareanpcs(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("Built-in function getareanpcs requires exactly four arguments"); + + std::vector result; + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + auto width = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2]) * 16); + auto height = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[3]) * 16); + + auto npcs = level->findIntersectingNPCs({{x, y}, {width, height}}, true); + for (auto id : npcs) + result.emplace_back(static_cast(id)); + } + return result; +} + +// getdir(dx, dy) +// Returns the direction to look in the relative position specified by dx and dy. +GS1ScriptValue fn_getdir(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (auto* character = getCharacterFromSource(visitor->getOriginalSource()); character != nullptr) + { + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function getdir requires exactly two arguments"); + + auto dx = visitor->getGameValueAs(*arguments[0]); + auto dy = visitor->getGameValueAs(*arguments[1]); + auto ix = static_cast(std::min(-1.0, std::max(1.0, std::round(dx)))); + auto iy = static_cast(std::min(-1.0, std::max(1.0, std::round(dy)))); + + // Up + if (ix == 0 && iy == -1) + return 0.0; + // Left + if (ix == -1 && iy == 0) + return 1.0; + // Down + if (ix == 0 && iy == 1) + return 2.0; + // Right + if (ix == 1 && iy == 0) + return 3.0; + } + + // Default to looking down. + return 2.0; +} + +// getflagkeys(prefix) +// Searches for all flags in the format of prefix### and returns an array of all the ###. +// E.g., bankaccount_0, bankaccount_1, etc. with the prefix "bankaccount_" would return an array of {0, 1, ...}. +GS1ScriptValue fn_getflagkeys(GS1Visitor* visitor, std::string_view functionName, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function getflagkeys requires exactly one argument"); + + auto prefix = visitor->getGameValueAs(*arguments[0]); + + std::vector results; + auto storageType = GS1Visitor::getStorageTypeFromIdentifier(prefix).value_or(ENUM(StorageType::CLIENT)); + GS1Visitor::stripStorageNameFromIdentifier(prefix); + + auto variableStore = visitor->getGameVariableStoreForStorageType(storageType); + if (variableStore == nullptr) + return results; + + for (auto& [key, value] : variableStore->store) + { + if (key.starts_with(prefix)) + { + auto index = string::toNumber(std::string_view{key.c_str() + prefix.length(), key.length() - prefix.length()}); + results.push_back(index); + } + } + + return results; +} + +// getnearestplayer(x, y) +// Finds the nearest player to the specified position and returns the player index. +GS1ScriptValue fn_getnearestplayer(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function getnearestplayer requires exactly two arguments"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto x = static_cast(visitor->getGameValueAs(*arguments[0])); + auto y = static_cast(visitor->getGameValueAs(*arguments[1])); + auto position = toPixelPosition({x, y}); + + // Find the nearest player. + std::tuple nearestPlayer{0, std::numeric_limits::max()}; + auto* server = BabyDI::Get(); + for (const auto& id : level->findInRangePlayers(position)) + { + if (auto player = server->getNPCServer()->getPlayer(id); player != nullptr) + { + TilePosition playerPos = toTilePosition(player->account.character.getGlobalPosition()); + auto distance = std::hypot(playerPos.x() - x, playerPos.y() - y); + if (distance < std::get<1>(nearestPlayer)) + nearestPlayer = {id, distance}; + } + } + + // Return the closest player. + if (std::get<0>(nearestPlayer) != 0) + return static_cast(std::get<0>(nearestPlayer)); + } + + return 0.0; +} + +// getnearestplayers(x, y, flag) +// Returns an array of all the level players sorted by how close they are to the specified position, containing the optional flag. +GS1ScriptValue fn_getnearestplayers(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() < 2) + throw std::invalid_argument("Built-in function getnearestplayers requires two or three arguments"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto x = static_cast(visitor->getGameValueAs(*arguments[0])); + auto y = static_cast(visitor->getGameValueAs(*arguments[1])); + auto position = toPixelPosition({x, y}); + + std::string flag; + if (arguments.size() > 2) + flag = visitor->getGameValueAs(*arguments[2]); + + std::map playersByDistance; + auto* server = BabyDI::Get(); + for (const auto& id : level->findInRangePlayers(position)) + { + if (auto player = server->getNPCServer()->getPlayer(id); player != nullptr) + { + if (!flag.empty() && !player->account.variables.contains(flag)) + continue; + + TilePosition playerPos = toTilePosition(player->account.character.getGlobalPosition()); + auto distance = std::hypot(playerPos.x() - x, playerPos.y() - y); + playersByDistance.emplace(distance, id); + } + } + + // Construct the player ID array. + std::vector playerIds; + for (const auto& [distance, id] : playersByDistance) + playerIds.push_back(static_cast(id)); + + // Return it. + return playerIds; + } + + return std::vector(); +} + +// getnpc(name) +// Returns an NPC object that links to the NPC, or a false value if not found. +GS1ScriptValue fn_getnpc(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function getnpc requires exactly one argument"); + + auto npcName = visitor->getGameValueAs(*arguments[0]); + + auto* server = BabyDI::Get(); + auto& npcList = server->getNPCList(); + for (auto& [id, npc] : npcList) + { + if (npc->name == npcName) + return ScriptObject{id, ScriptObjectType::NPC}; + } + + return 0.0; +} + +// getplayer(account) +// Returns a Player object that links to the player with the specified account name, or a false value if not found. +GS1ScriptValue fn_getplayer(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function getplayer requires exactly one argument"); + + auto playerName = visitor->getGameValueAs(*arguments[0]); + + auto* server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(playerName, PLTYPE_ANYCLIENT); player != nullptr) + { + return ScriptObject{player->getId(), ScriptObjectType::PLAYER}; + } + + return 0.0; +} + +// getz(x, y) +// Returns the Z coordinate at the specified X and Y position in the world. +GS1ScriptValue fn_getz(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function getz requires exactly two arguments"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto x = static_cast(visitor->getGameValueAs(*arguments[0])); + auto y = static_cast(visitor->getGameValueAs(*arguments[1])); + return level->getHeightAt(toPixelPosition({x, y})); + } + + return 0.0; +} + +// hasweapon(name) +// Checks if the player has the specified weapon. +GS1ScriptValue fn_hasweapon(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function hasweapon requires exactly one argument"); + + auto weaponName = visitor->getGameValueAs(*arguments[0]); + auto player = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); + if (player.has_value()) + { + auto* server = BabyDI::Get(); + if (auto playerObject = server->getNPCServer()->getPlayer(player.value().first); playerObject != nullptr) + return GameValue{playerObject->account.hasWeapon(weaponName)}; + } + + return GameValue{false}; +} + +// imgheight(image) +// Returns the height of the specified image. +GS1ScriptValue fn_imgheight(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::runtime_error("Built-in function imgheight is a clientside function"); +} + +// imgwidth(image) +// Returns the width of the specified image. +GS1ScriptValue fn_imgwidth(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::runtime_error("Built-in function imgwidth is a clientside function"); +} + +// indexof(substring, string) +// Returns the index of the first occurrence of substring in the string, or -1 if not found. +GS1ScriptValue fn_indexof(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function indexof requires exactly two arguments"); + + auto substring = visitor->getGameValueAs(*arguments[0]); + auto str = visitor->getGameValueAs(*arguments[1]); + + return str.find(substring) != std::string::npos ? static_cast(str.find(substring)) : -1.0; +} + +// int(value) +// Converts the value to an integer. +GS1ScriptValue fn_int(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function int requires exactly one argument"); + + auto value = visitor->getGameValueAs(*arguments[0]); + return static_cast(static_cast(value)); + /* + if (value < 0.0) + return static_cast(static_cast(value - 0.5)); + else + return static_cast(static_cast(value + 0.5)); + */ +} + +// keycode(key) +// Returns the key code for the specified key. +GS1ScriptValue fn_keycode(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function keycode requires exactly one argument"); + + auto key = visitor->getGameValueAs(*arguments[0]); + if (key.empty()) + return 0.0; + + uint8_t code = static_cast(key.front()); + return static_cast(code); +} + +// keydown(key) +// Checks if the specified key is currently pressed down. (0..10: up, left, down, right, S, A, D, M, tab, Q, P) +GS1ScriptValue fn_keydown(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::runtime_error("Built-in function keydown is a clientside function"); +} + +// keydown2(keycode, ignorecase) +// Checks if the specified key is currently pressed down, with an optional case-insensitive check for key codes. +// (ignorecase must be false to check for shift, ctrl, alt) +GS1ScriptValue fn_keydown2(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::runtime_error("Built-in function keydown2 is a clientside function"); +} + +// lindexof(string, list) +// Returns the index of the first occurrence of string in the string list, or -1 if not found. +GS1ScriptValue fn_lindexof(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function lindexof requires exactly two arguments"); + + auto str = visitor->getGameValueAs(*arguments[0]); + auto list = visitor->getGameValueAs(*arguments[1]); + auto listItems = string::splitToVectorView(list, ","sv); + for (size_t i = 0; i < listItems.size(); ++i) + { + if (string::trim(listItems[i]) == string::trim(str)) + return static_cast(i); + } + + return -1.0; +} + +// log(base, value) +// Returns the logarithm of the value with the given base. +GS1ScriptValue fn_log(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function log requires exactly two arguments"); + + auto base = visitor->getGameValueAs(*arguments[0]); + auto value = visitor->getGameValueAs(*arguments[1]); + if (value <= 0.0) + return 0.0; + + return std::log(value) / std::log(base); +} + +// max(value1, value2) +// Returns the maximum of the two values. +GS1ScriptValue fn_max(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function max requires exactly two arguments"); + + auto value1 = visitor->getGameValueAs(*arguments[0]); + auto value2 = visitor->getGameValueAs(*arguments[1]); + + if (value1 > value2) + return value1; + else + return value2; +} + +// min(value1, value2) +// Returns the minimum of the two values. +GS1ScriptValue fn_min(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function max requires exactly two arguments"); + + auto value1 = visitor->getGameValueAs(*arguments[0]); + auto value2 = visitor->getGameValueAs(*arguments[1]); + + if (value1 < value2) + return value1; + else + return value2; +} + +// onmapx(level) +// The level's X position on the map. +GS1ScriptValue fn_onmapx(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function onmapx requires exactly one argument"); + + if (auto curLevel = visitor->findCurrentLevel(); curLevel != nullptr) + { + auto level = visitor->getGameValueAs(*arguments[0]); + if (auto map = curLevel->getMap(); map != nullptr) + return static_cast(map->getLevelPosition(level).value_or(MapPosition{0, 0}).x()); + } + + return 0.0; +} + +// onmapy(level) +// The level's Y position on the map. +GS1ScriptValue fn_onmapy(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function onmapy requires exactly one argument"); + + if (auto curLevel = visitor->findCurrentLevel(); curLevel != nullptr) + { + auto level = visitor->getGameValueAs(*arguments[0]); + if (auto map = curLevel->getMap(); map != nullptr) + return static_cast(map->getLevelPosition(level).value_or(MapPosition{0, 0}).y()); + } + + return 0.0; +} + +// onwall(x, y) +// Checks if the specified X and Y coordinates are on a wall tile. +GS1ScriptValue fn_onwall(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function onwall requires exactly two arguments"); + + auto x = static_cast(visitor->getGameValueAs(*arguments[0])); + auto y = static_cast(visitor->getGameValueAs(*arguments[1])); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + if (!level->isOnWall(toPixelPosition({x, y}))) + return GameValue{false}; + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr && !npc->noPlayerOnWall) + return GameValue{level->isOnPlayer(toPixelPosition({x, y}))}; + } + return GameValue{true}; + } + + return GameValue{false}; +} + +// onwall2(x, y, width, height) +// Checks if the specified rectangle defined by X, Y, width, and height is on a wall tile. +GS1ScriptValue fn_onwall2(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("Built-in function onwall2 requires exactly four arguments"); + + auto x = static_cast(visitor->getGameValueAs(*arguments[0])); + auto y = static_cast(visitor->getGameValueAs(*arguments[1])); + auto width = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2]) * 16); + auto height = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[3]) * 16); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + if (!level->isOnWall2(PixelRectangleArea{toPixelPosition({x, y}), {width, height}})) + return GameValue{false}; + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto npc = server->getNPC(source.value().first); npc != nullptr && !npc->noPlayerOnWall) + return GameValue{level->isOnPlayer({toPixelPosition({x, y}), {width, height}})}; + } + return GameValue{true}; + } + + return GameValue{false}; +} + +// onwater(x, y) +// Checks if the specified X and Y coordinates are on a water tile. +GS1ScriptValue fn_onwater(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function onwater requires exactly two arguments"); + + auto x = static_cast(visitor->getGameValueAs(*arguments[0])); + auto y = static_cast(visitor->getGameValueAs(*arguments[1])); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + if (level->isOnWater(toPixelPosition({x, y}))) + return GameValue{true}; + } + + return GameValue{false}; +} + +// onwater2(x, y, width, height) +// Checks if the specified X and Y coordinates are on a water tile. +GS1ScriptValue fn_onwater2(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 4) + throw std::invalid_argument("Built-in function onwater2 requires exactly four arguments"); + + auto x = static_cast(visitor->getGameValueAs(*arguments[0])); + auto y = static_cast(visitor->getGameValueAs(*arguments[1])); + auto width = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[2]) * 16); + auto height = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[3]) * 16); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + if (level->isOnWater2(PixelRectangleArea{toPixelPosition({x, y}), {width, height}})) + return GameValue{true}; + } + + return GameValue{false}; +} + +// playersays(text) +// playersays(index,text) +// Checks if the player says the specified text. +// Equivalent to "playerchats && strequals(#c,text)" +GS1ScriptValue fn_playersays(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + std::optional index; + std::string text; + if (arguments.size() == 2) + { + auto specifiedIndex = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + text = visitor->getGameValueAs(*arguments[1]); + if (specifiedIndex >= 0) + index = static_cast(specifiedIndex); + } + else + { + text = visitor->getGameValueAs(*arguments[0]); + } + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + if (auto player = getPlayerFromSource(*source, index); player != nullptr) + { + if (string::equalsi(player->account.character.chatMessage, text)) + return GameValue{true}; + } + } + + return GameValue{false}; +} + +// playersays2(text) +// playersays2(index,text) +// Checks if the player's chat contains the specified text. +// Equivalent to "playerchats && strcontains(#c,text)" +GS1ScriptValue fn_playersays2(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + std::optional index; + std::string text; + if (arguments.size() == 2) + { + auto specifiedIndex = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + text = visitor->getGameValueAs(*arguments[1]); + if (specifiedIndex >= 0) + index = static_cast(specifiedIndex); + } + else + { + text = visitor->getGameValueAs(*arguments[0]); + } + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + if (auto player = getPlayerFromSource(*source, index); player != nullptr) + { + if (string::findi(player->account.character.chatMessage, text) != std::string::npos) + return GameValue{true}; + } + } + + return GameValue{false}; +} + +// random(min, max) +// Returns a random number between min and max. a <= value < b +GS1ScriptValue fn_random(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + using namespace std::chrono; + static std::minstd_rand rng(static_cast(duration_cast(system_clock::now().time_since_epoch()).count())); + + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function max requires exactly two arguments"); + + auto value1 = visitor->getGameValueAs(*arguments[0]); + auto value2 = visitor->getGameValueAs(*arguments[1]); + + std::uniform_real_distribution dist(std::min(value1, value2), std::max(value1, value2)); + return static_cast(dist(rng)); +} + +// sarraylen(list) +// Returns the length of the string list. +GS1ScriptValue fn_sarraylen(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function sarraylen requires exactly one argument"); + + auto list = visitor->getGameValueAs(*arguments[0]); + return static_cast(std::ranges::count(list, ',') + 1); +} + +// screenx(x, y) +// Converts level coordinates (x, y) to the screen's X coordinate. +GS1ScriptValue fn_screenx(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::logic_error("Built-in function screenx is a clientside function"); +} + +// screeny(x, y) +// Converts level coordinates (x, y) to the screen's Y coordinate. +GS1ScriptValue fn_screeny(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::logic_error("Built-in function screeny is a clientside function"); +} + +// sin(value) +// Returns the sine of the value in radians. +GS1ScriptValue fn_sin(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function sin requires exactly one argument"); + + auto value = visitor->getGameValueAs(*arguments[0]); + + if (value < 0 || value > std::numbers::pi) + return 0.0; + + return std::sin(value); +} + +// startswith(prefix, string) +// Checks if the string starts with the given prefix. +GS1ScriptValue fn_startswith(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function startswith requires exactly two arguments"); + + auto prefix = visitor->getGameValueAs(*arguments[0]); + auto str = visitor->getGameValueAs(*arguments[1]); + + return GameValue{string::findi(str, prefix) == 0}; +} + +// strcontains(string, substring) +// Checks if the string contains the given substring. +GS1ScriptValue fn_strcontains(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function strcontains requires exactly two arguments"); + + auto str = visitor->getGameValueAs(*arguments[0]); + auto substring = visitor->getGameValueAs(*arguments[1]); + + return GameValue{string::findi(str, substring) != std::string::npos}; +} + +// strequals(string1, string2) +// Checks if the two strings are equal. +GS1ScriptValue fn_strequals(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function strequals requires exactly two arguments"); + + auto str1 = visitor->getGameValueAs(*arguments[0]); + auto str2 = visitor->getGameValueAs(*arguments[1]); + + return GameValue{string::equalsi(str1, str2)}; +} + +// strlen(string) +// Returns the length of the string. +GS1ScriptValue fn_strlen(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function strlen requires exactly one argument"); + + auto str = visitor->getGameValueAs(*arguments[0]); + + return static_cast(str.length()); +} + +// strtofloat(string) +// Converts a string to a float. +GS1ScriptValue fn_strtofloat(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function strtofloat requires exactly one argument"); + + auto str = visitor->getGameValueAs(*arguments[0]); + if (str.empty()) + return 0.0; + + return string::toDouble(str); +} + +// testbomb(x, y) +// The index of the bomb at level position (x, y), or -1 if there is no bomb at that position. +GS1ScriptValue fn_testbomb(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function testitem requires exactly two arguments"); + + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto& bombs = level->getBombs(); + for (size_t i = 0; i < bombs.size(); ++i) + { + auto& bomb = bombs[i]; + if (bomb.position.x() == x && bomb.position.y() == y) + return static_cast(i); + } + } + + return -1.0; +} + +// testcompu(x, y) +// The index of the baddie at level position (x, y), or -1 if there is no baddie at that position. +GS1ScriptValue fn_testcompu(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function testitem requires exactly two arguments"); + + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + size_t index = 0; + for (const auto& baddy : level->getBaddies()) + { + if (baddy.position.x() == x && baddy.position.y() == y && baddy.mode != BaddyMode::DEAD) + return static_cast(index); + ++index; + } + } + + return -1.0; +} + +// testexplo(x, y) +// The index of the explosion at level position (x, y), or -1 if there is no explosion at that position. +GS1ScriptValue fn_testexplo(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function testitem requires exactly two arguments"); + + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto& explos = level->getExplosions(); + for (size_t i = 0; i < explos.size(); ++i) + { + auto& explo = explos[i]; + if (explo.position.x() == x && explo.position.y() == y) + return static_cast(i); + } + } + + return -1.0; +} + +// testhorse(x, y) +// The index of the horse at level position (x, y), or -1 if there is no horse at that position. +GS1ScriptValue fn_testhorse(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function testhorse requires exactly two arguments"); + + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto& horses = level->getHorses(); + for (size_t i = 0; i < horses.size(); ++i) + { + auto& horse = horses[i]; + if (horse.position.x() == x && horse.position.y() == y) + return static_cast(i); + } + } + + return -1.0; +} + +// testitem(x, y) +// The index of the item at level position (x, y), or -1 if there is no item at that position. +GS1ScriptValue fn_testitem(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function testitem requires exactly two arguments"); + + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto& items = level->getItems(); + for (size_t i = 0; i < items.size(); ++i) + { + auto& item = items[i]; + if (item.position.x() == x && item.position.y() == y) + return static_cast(i); + } + } + + return -1.0; +} + +// testnpc(x, y) +// The index of the NPC at level position (x, y), or -1 if there is no NPC at that position. +GS1ScriptValue fn_testnpc(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function testnpc requires exactly two arguments"); + + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + auto position = PixelPosition{x, y}; + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto* server = BabyDI::Get(); + + bool found = false; + size_t index = 0; + for (const auto& npcId : level->findInRangeNPCs(position)) + { + if (auto npc = server->getNPC(npcId); npc != nullptr) + { + if (positionInRectangle(position, npc->getBoundingBox())) + { + found = true; + break; + } + } + ++index; + } + + if (found) + return static_cast(index); + } + + return -1.0; +} + +// testplayer(x, y) +// The index of the player at level position (x, y), or -2 if there is no player at that position. +// -1 is reserved for the current npc if showcharacter is enabled. +GS1ScriptValue fn_testplayer(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function testplayer requires exactly two arguments"); + + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0]) * 16); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1]) * 16); + auto position = PixelPosition{x, y}; + auto* server = BabyDI::Get(); + + if (auto source = visitor->getOriginalSource(); source.second == ScriptObjectType::NPC) + { + if (auto npc = server->getNPC(source.first); npc != nullptr) + { + // If the current NPC is the one being tested, return -1. + if (positionInRectangle(position, npc->getBoundingBox())) + return -1.0; + } + } + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + bool found = false; + size_t index = 0; + for (const auto& playerId : level->findInRangePlayers(position)) + { + if (auto player = server->getPlayer(playerId); player != nullptr) + { + if (positionInRectangle(position, player->getBoundingBox())) + { + found = true; + break; + } + } + ++index; + } + + if (found) + return static_cast(index); + } + + return -2.0; +} + +// testsign(x, y) +// The index of the sign at level position (x, y), or -1 if there is no sign at that position. +GS1ScriptValue fn_testsign(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function testsign requires exactly two arguments"); + + auto x = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto y = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + size_t index = 0; + for (const auto& [_, position] : level->getSignPositions()) + { + if (position.x() == x && position.y() == y) + return static_cast(index); + ++index; + } + } + return -1.0; +} + +// textheight(zoom, font, style) +// Returns the height of the text in pixels, given the zoom level, font name, and style. +GS1ScriptValue fn_textheight(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw unimplemented_error("Built-in function textheight is a clientside function"); +} + +// textwidth(zoom, font, style, text) +// Returns the width of the text in pixels, given the zoom level, font name, style, and text. +GS1ScriptValue fn_textwidth(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw unimplemented_error("Built-in function textwidth is a clientside function"); +} + +// tiletype(x, y) +// Returns the tile type at level position (x, y). +GS1ScriptValue fn_tiletype(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Built-in function tiletype requires exactly two arguments"); + + if (auto level = visitor->findCurrentLevel(); level != nullptr) + { + auto x = visitor->getGameValueAs(*arguments[0]); + auto y = visitor->getGameValueAs(*arguments[1]); + + auto tilePosition = toTilePosition(Position{x, y}); + auto mapPosition = toMapPosition(tilePosition); + + if (!level->isGmap()) + mapPosition = {0, 0}; + + if (auto tiles = level->getTiles(mapPosition); tiles.has_value()) + { + auto index = static_cast(std::max(x, 0.0) + (std::max(y, 0.0) * 64)); + if (index < 4096) + { + auto server = BabyDI::Get(); + auto tile = tiles.value()->at(index); + return static_cast(ENUM(server->getNPCServer()->getTileType(tile, level))); + } + } + } + + // Not found? Default to blocking. + return 22.0; +} + +// vecx(dir) +// Returns the X component of the vector for the specified direction (0,-1,0,1) for (up, left, down, right). +GS1ScriptValue fn_vecx(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function vecx requires exactly one argument"); + + static double vecValues[] = {0.0, -1.0, 0.0, 1.0}; + auto dir = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])) % 4; + return vecValues[dir]; +} + +// vecy(dir) +// Returns the Y component of the vector for the specified direction (-1,0,1,0) for (up, left, down, right). +GS1ScriptValue fn_vecy(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Built-in function vecy requires exactly one argument"); + + static double vecValues[] = {-1.0, 0.0, 1.0, 0.0}; + auto dir = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])) % 4; + return vecValues[dir]; +} + +// worldx(x, y) +// Converts screen (x, y) to level X. +GS1ScriptValue fn_worldx(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::logic_error("Built-in function worldx is a clientside function"); +} + +// worldy(x, y) +// Converts screen (x, y) to level Y. +GS1ScriptValue fn_worldy(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::logic_error("Built-in function worldy is a clientside function"); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1::grammar diff --git a/server/src/scripting/gs1/GS1MessageCodes.cpp b/server/src/scripting/gs1/GS1MessageCodes.cpp new file mode 100644 index 000000000..3bcb0bfc9 --- /dev/null +++ b/server/src/scripting/gs1/GS1MessageCodes.cpp @@ -0,0 +1,1052 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1::grammar +{ +/////////////////////////////////////////////////////////////////////////////// + +static constexpr PlayerProp GetPlayerPropFromIndex(uint8_t index) +{ + switch (index) + { + case 1: // #1 + return PlayerProp::SWORDPOWER; + case 2: // #2 + return PlayerProp::SHIELDPOWER; + case 3: // #3 + return PlayerProp::HEADGIF; + case 5: // #5 + return PlayerProp::HORSEGIF; + case 7: // #7 + return PlayerProp::GANI; + case 8: // #8 + return PlayerProp::BODYIMG; + case 9: // #c + return PlayerProp::CURCHAT; + case 10: // #m + return PlayerProp::GANI; + case 11: // #n + return PlayerProp::NICKNAME; + } + + if (index >= 20 && index <= 27) + return PlayerProp::COLORS; + + if (index >= 30 && index <= 60) + return static_cast(GaniAttributePropList[std::max(0, index - 30)]); + + return PlayerProp::ID; +} + +static constexpr NPCProp GetNPCPropFromIndex(uint8_t index) +{ + switch (index) + { + case 1: // #1 + return NPCProp::SWORDIMAGE; + case 2: // #2 + return NPCProp::SHIELDIMAGE; + case 3: // #3 + return NPCProp::HEADIMAGE; + case 5: // #5 + return NPCProp::HORSEIMAGE; + case 7: // #7 + return NPCProp::GANI; + case 8: // #8 + return NPCProp::BODYIMAGE; + case 9: // #c + return NPCProp::MESSAGE; + case 10: // #m + return NPCProp::GANI; + case 11: // #n + return NPCProp::NICKNAME; + } + + if (index >= 20 && index <= 27) + return NPCProp::COLORS; + + if (index >= 30 && index <= 60) + return static_cast(NPCGaniAttrPackets[std::max(0, index - 30)]); + + return NPCProp::ID; +} + +/////////////////////////////////////////////////////////////////////////////// + +using MessageCodeHandleFunc = GS1ScriptValue (*)(GS1Visitor*, std::string_view, const std::vector&); +using MessageCodeHandleMap = std::unordered_map; + +static GS1ScriptValue mc_1(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_2(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_3(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_5(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_6(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_7(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_8(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_a(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_b(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_c(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_D(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_E(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_e(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_F(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_f(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_g(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_G(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_I(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_i(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_K(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_k(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_L(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_m(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_n(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_N(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_p(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_Q(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_R(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_S(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_s(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_t(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_T(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_U(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_v(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_W(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_w(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments); + +static GS1ScriptValue mc_C(GS1Visitor* visitor, uint8_t index, std::string_view messageCode, const std::vector& arguments); +static GS1ScriptValue mc_P(GS1Visitor* visitor, uint8_t index, std::string_view messageCode, const std::vector& arguments); + +static MessageCodeHandleMap GenerateMap() +{ + string::string_hash hash{}; + MessageCodeHandleMap map = + { + {hash("1"), &mc_1}, + {hash("2"), &mc_2}, + {hash("3"), &mc_3}, + {hash("5"), &mc_5}, + {hash("6"), &mc_6}, + {hash("7"), &mc_7}, + {hash("8"), &mc_8}, + {hash("a"), &mc_a}, + {hash("b"), &mc_b}, + {hash("c"), &mc_c}, + {hash("D"), &mc_D}, + {hash("E"), &mc_E}, + {hash("e"), &mc_e}, + {hash("F"), &mc_F}, + {hash("f"), &mc_f}, + {hash("g"), &mc_g}, + {hash("G"), &mc_G}, + {hash("I"), &mc_I}, + {hash("i"), &mc_i}, + {hash("K"), &mc_K}, + {hash("k"), &mc_k}, + {hash("L"), &mc_L}, + {hash("m"), &mc_m}, + {hash("n"), &mc_n}, + {hash("N"), &mc_N}, + {hash("p"), &mc_p}, + {hash("Q"), &mc_Q}, + {hash("R"), &mc_R}, + {hash("S"), &mc_S}, + {hash("s"), &mc_s}, + {hash("t"), &mc_t}, + {hash("T"), &mc_T}, + {hash("U"), &mc_U}, + {hash("v"), &mc_v}, + {hash("W"), &mc_W}, + {hash("w"), &mc_w}, + }; + return map; +} + +/// @brief Message codes that switch to flag processing mode, which results in identifiers defaulting to client storage. +/// TODO: This might not be required anymore. +constexpr std::array flagProcessingMessageCodes = +{ + "I"sv, + "s"sv, +}; + +constexpr std::array translatableMessageCodes = +{ + "U"sv, +}; + +/////////////////////////////////////////////////////////////////////////////// + +using pickerReturn = std::pair; +using pickerFunc = std::function const&)>; + +static GS1GameVariable bindPlayerSetter(GS1Visitor* visitor, PlayerID playerId, uint8_t index, GameValue& value) +{ + PlayerProp propId = GetPlayerPropFromIndex(index); + if (propId == PlayerProp::ID) + return {std::move(value), std::nullopt}; + + GameValue result{std::move(value)}; + result.setSetter([visitor, playerId, propIndex = index, propId](GameValueVariant incoming, std::optional index) + { + GameValue value; + const auto picker = visit_functions{ + [&](std::optional* in) + { + if (in->has_value()) value.set(in->value(), index); + }, + [&](std::optional* in) + { + if (in->has_value()) value.set(in->value(), index); + }, + [&](std::optional* in) + { + if (in->has_value()) value.set(in->value(), index); + }, + [&](std::optional>* in) + { + if (in->has_value()) value.set(in->value(), index); + }, + [&](std::optional>* in) + { + if (in->has_value()) value.set(in->value(), index); + } + }; + std::visit(picker, incoming); + + auto* server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(playerId); player != nullptr) + { + if (propId != PlayerProp::COLORS) + { + auto prop = player->getProp(propId); + prop->apply(value); + auto results = player->setProp(propId, SetBy::SERVER, prop); + if (results.resultFlags.test(results.sendToAll)) + player->sendPropsFromResults(results); + } + else + { + auto colors = player->getProp(); + uint8_t colorVal = 0; + + auto strVal = value.get(); + if (strVal.has_value()) + colorVal = visitor->getColorValueFromString(strVal.value()); + else colorVal = DoubleAsIntegralFloor(value.get().value_or(0)); + + colors.values[std::max(0, propIndex - 20)] = colorVal; + player->setProp(SetBy::SERVER, colors); + } + } + }); + + return {std::move(result), std::nullopt}; +} + +static GS1GameVariable bindNPCSetter(GS1Visitor* visitor, NPCID npcId, uint8_t index, GameValue& value) +{ + NPCProp propId = GetNPCPropFromIndex(index); + if (propId == NPCProp::ID) + return {std::move(value), std::nullopt}; + + GameValue result{std::move(value)}; + result.setSetter([visitor, npcId, propIndex = index, propId](GameValueVariant incoming, std::optional index) + { + GameValue value; + const auto picker = visit_functions{ + [&](std::optional* in) + { + if (in->has_value()) value.set(in->value(), index); + }, + [&](std::optional* in) + { + if (in->has_value()) value.set(in->value(), index); + }, + [&](std::optional* in) + { + if (in->has_value()) value.set(in->value(), index); + }, + [&](std::optional>* in) + { + if (in->has_value()) value.set(in->value(), index); + }, + [&](std::optional>* in) + { + if (in->has_value()) value.set(in->value(), index); + } + }; + std::visit(picker, incoming); + + auto* server = BabyDI::Get(); + if (auto npc = server->getNPC(npcId); npc != nullptr) + { + if (propId == NPCProp::COLORS) + { + auto colors = npc->getProp(); + uint8_t colorVal = 0; + + auto strVal = value.get(); + if (strVal.has_value()) + colorVal = visitor->getColorValueFromString(strVal.value()); + else colorVal = DoubleAsIntegralFloor(value.get().value_or(0)); + + colors.values[std::max(0, propIndex - 20)] = colorVal; + npc->setProp(SetBy::SERVER, colors); + } + else + { + auto prop = npc->getProp(propId); + prop->apply(value); + npc->setProp(propId, SetBy::SERVER, prop); + } + } + }); + + return {std::move(result), std::nullopt}; +} + +static GS1ScriptValue handleCharacterBasedMessageCode(GS1Visitor* visitor, const std::vector& arguments, pickerFunc picker) +{ + std::optional index = std::nullopt; + if (arguments.size() == 1) + index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + Character* character = nullptr; + ScriptObject currentSource; + + // An index of -1 means we are looking at the source NPC. + if (index.value_or(0) == -1) + { + auto activeNPC = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::NPC); + if (activeNPC.has_value()) + { + currentSource = activeNPC.value(); + character = getCharacterFromSource(activeNPC.value()); + } + } + // An index of 0 or greater means we are looking at the player. + else if (index.has_value() && index.value() >= 0) + { + auto activePlayer = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); + if (activePlayer.has_value()) + { + currentSource = activePlayer.value(); + if (index.value_or(0) == 0) + character = getCharacterFromSource(activePlayer.value(), {}); + else character = getCharacterFromSource(activePlayer.value(), index); + } + } + // No index means we try to get the character from the current source, biasing to the initiator. + else + { + currentSource = visitor->getCurrentSource(true); + character = getCharacterFromSource(currentSource); + if (character == nullptr) + { + currentSource = visitor->getOriginalSource(); + character = getCharacterFromSource(currentSource); + } + } + + if (character == nullptr) + return std::string{}; + + auto [value, codeIndex] = picker(*character, arguments); + if (currentSource.second == ScriptObjectType::PLAYER) + return bindPlayerSetter(visitor, static_cast(currentSource.first), codeIndex, value); + else if (currentSource.second == ScriptObjectType::NPC) + return bindNPCSetter(visitor, static_cast(currentSource.first), codeIndex, value); + + return value; +} + +/////////////////////////////////////////////////////////////////////////////// + +static std::any translateStringForPlayer(antlr4::tree::ParseTree* node, GS1Visitor* visitor, PlayerPtr player) +{ + if (node == nullptr) + return std::any{}; + if (visitor == nullptr || player == nullptr || node->getTreeType() != antlr4::tree::ParseTreeType::RULE) + return node->accept(visitor); + + return visitor->translateSourceText(node, player->account.language); +} + +/////////////////////////////////////////////////////////////////////////////// + +GS1ScriptValue processMessageCode(GS1Visitor* visitor, antlr4::tree::ParseTree* node, std::string_view messageCode) +{ + static MessageCodeHandleMap map = GenerateMap(); + + if (visitor == nullptr) + throw std::runtime_error("processMessageCode received an empty visitor"); + if (messageCode.empty()) + throw std::runtime_error("processMessageCode received an empty message code"); + + bool isTranslatable = std::ranges::contains(translatableMessageCodes, messageCode); + std::vector arguments; + std::vector keepAlive; + + // Helper to package a value and keep it alive for the duration of the command execution. + auto makeValue = [&](std::any&& anyValue) + { + if (!anyValue.has_value()) + return; + + keepAlive.emplace_back(std::move(anyValue)); + auto* container = std::any_cast(&keepAlive.back()); + if (container == nullptr) + throw std::runtime_error("Message code argument is not a valid GS1ScriptValue"); + + arguments.push_back(std::move(container)); + }; + + { + // Record if we are expecting a flag. + SetAndRestore expectingFlagGuard(visitor->expectingFlag, (std::ranges::find(flagProcessingMessageCodes, messageCode) != std::ranges::end(flagProcessingMessageCodes))); + + auto player = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); + + // Save the player pointer so we don't keep searching for it. + PlayerPtr playerPtr = nullptr; + auto server = BabyDI::Get(); + if (isTranslatable && player.has_value()) + playerPtr = server->getPlayer(player.value().first); + + // Collect the arguments from the node. + for (size_t i = 0; i < node->children.size(); ++i) + { + // If the command is translatable, run it through the translation process before packaging the value. + if (isTranslatable && visitor->expectingFlag == false && player.has_value()) + { + if (auto stringContext = visitor->walkToContext(node->children[i]); stringContext != nullptr) + { + makeValue(translateStringForPlayer(stringContext, visitor, playerPtr)); + continue; + } + } + + makeValue(node->children[i]->accept(visitor)); + } + } + + // #C0 - #C4 + if (messageCode.starts_with("C")) + { + if (messageCode.size() == 2) + { + uint8_t index = static_cast(messageCode[1] - '0'); + if (index >= 0 && index <= 4) + return mc_C(visitor, index, messageCode, arguments); + } + } + // #P1 - #P30 + else if (messageCode.starts_with("P")) + { + std::string_view indexStr = messageCode.substr(1); + auto index = string::toNumber(std::string(indexStr)); + if (index >= 1 && index <= 30) + return mc_P(visitor, index, messageCode, arguments); + } + // Any other message code in the map. + else + { + size_t hash = string::string_hash{}(messageCode); + auto it = map.find(hash); + if (it != map.end()) + return it->second(visitor, messageCode, arguments); + } + + // Not a known message code, so just return the string. + return GameValue{node->getText()}; +} + +/////////////////////////////////////////////////////////////////////////////// + +// #1 | #1(index) [Read / Write] +// Sword image filename of the player. +GS1ScriptValue mc_1(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + return handleCharacterBasedMessageCode(visitor, arguments, [](Character& character, const auto& arguments) -> pickerReturn + { + return std::make_pair(character.swordImage, 1); + }); +} + +// #2 | #2(index) [Read / Write] +// Shield image filename of the player. +GS1ScriptValue mc_2(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + return handleCharacterBasedMessageCode(visitor, arguments, [](Character& character, const auto& arguments) -> pickerReturn + { + return std::make_pair(character.shieldImage, 2); + }); +} + +// #3 | #3(index) [Read / Write] +// Head image filename of the player. +GS1ScriptValue mc_3(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + return handleCharacterBasedMessageCode(visitor, arguments, [](Character& character, const auto& arguments) -> pickerReturn + { + return std::make_pair(character.headImage, 3); + }); +} + +// #4 is unused. + +// #5 | #5(index) [Read / Write] +// Horse image filename of the player. +GS1ScriptValue mc_5(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + return handleCharacterBasedMessageCode(visitor, arguments, [](Character& character, const auto& arguments) -> pickerReturn + { + return std::make_pair(character.horseImage, 5); + }); +} + +// #6 | #6(index) [Read] +// NPC image filename of the carried NPC. +GS1ScriptValue mc_6(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + std::optional index = std::nullopt; + if (arguments.size() == 1) + index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + auto result = getPlayerOrNPCFromSource(visitor->getCurrentSource(), index); + if (!result.has_value()) + return std::string{}; + + const auto picker = visit_functions{ + [](PlayerPtr& player) -> std::string + { + if (auto client = std::dynamic_pointer_cast(player); client != nullptr && client->getCarryNPC() != 0) + { + auto* server = BabyDI::Get(); + if (auto npc = server->getNPC(client->getCarryNPC()); npc != nullptr) + return string::toLower(npc->image); + } + return std::string{}; + }, + [](NPCPtr& npc) -> std::string + { + return std::string{}; + }, + }; + + return std::visit(picker, result.value()); +} + +// #7 | #7(index) [Read / Write] +// Bow image filename of the player. +GS1ScriptValue mc_7(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + return handleCharacterBasedMessageCode(visitor, arguments, [](Character& character, const auto& arguments) -> pickerReturn + { + return std::make_pair(character.bowImage, 7); + }); +} + +// #8 | #8(index) [Read / Write] +// Body image filename of the player. +GS1ScriptValue mc_8(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + return handleCharacterBasedMessageCode(visitor, arguments, [](Character& character, const auto& arguments) -> pickerReturn + { + return std::make_pair(character.bodyImage, 8); + }); +} + +// #a | #a(index) [Read] +// Account name of the player. +GS1ScriptValue mc_a(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + std::optional index = std::nullopt; + if (arguments.size() == 1) + index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source->first); player != nullptr) + { + // Explicitly place it in another string as the return will trigger move semantics. + return std::string{player->account.name}; + } + } + + return std::string{}; +} + +// #b +// Line break for say2. +GS1ScriptValue mc_b(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + // Pass this along with processing it. + // The client deals with it. + return "#b"s; +} + +// #c | #c(index) [Read / Write] +// Current chat text of the player. +GS1ScriptValue mc_c(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + return handleCharacterBasedMessageCode(visitor, arguments, [](Character& character, const auto& arguments) -> pickerReturn + { + return std::make_pair(character.chatMessage, 9); + }); +} + +// #D | #D(filename) +// Current file being downloaded | The download position of the specified file. +GS1ScriptValue mc_D(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::logic_error("Message Code #D is registered as a clientside message code"); +} + +// #E +// The current emoticon character being displayed by the player. +GS1ScriptValue mc_E(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::logic_error("Message Code #E is registered as a clientside message code"); +} + +// #e(start_index, length, string) +// Extracts a substring from the given string. +GS1ScriptValue mc_e(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 3) + throw std::invalid_argument("Message Code #e requires exactly 3 arguments"); + + auto startIndex = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + auto length = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + auto str = visitor->getGameValueAs(*arguments[2]); + return str.substr(startIndex, length); +} + +// #F [Read] +// The level filename of the current player. (#L will return the NPC level filename) +GS1ScriptValue mc_F(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + auto result = getPlayerOrNPCFromSource(visitor->getCurrentSource()); + if (!result.has_value()) + return std::string{}; + + const auto picker = visit_functions{ + [](PlayerPtr& player) -> std::string + { + if (player != nullptr) + { + if (auto client = std::dynamic_pointer_cast(player); client != nullptr) + return client->getLevelName(); + return player->account.level; + } + return std::string{}; + }, + [](NPCPtr& npc) -> std::string + { + if (npc != nullptr) + { + if (auto level = npc->getLevel(); level != nullptr) + return std::string{level->levelName}; + return npc->level; + } + return std::string{}; + }, + }; + + return std::visit(picker, result.value()); +} + +// #f | #f(index) [Read] +// Image filename of the NPC. +GS1ScriptValue mc_f(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + std::optional index = std::nullopt; + if (arguments.size() == 1) + index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + auto npc = getNPCFromSource(visitor->getCurrentSource(), index); + if (npc != nullptr) + { + // Explicitly place it in another string as the return will trigger move semantics. + return string::toLower(npc->image); + } + + return std::string{}; +} + +// #g | #g(index) [Read] +// Guild name of the player. +GS1ScriptValue mc_g(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + std::optional index = std::nullopt; + if (arguments.size() == 1) + index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + if (auto client = getPlayerClientFromSource(visitor->getCurrentSource(), index); client != nullptr) + return client->getGuild().toString(); + + return std::string{}; +} + +// #G | #G(index) [Read] +// Upgrade status of the player (the player's account level). +// player.upgradestatus #G(index) +GS1ScriptValue mc_G(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + std::optional index = std::nullopt; + if (arguments.size() == 1) + index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + if (auto client = getPlayerClientFromSource(visitor->getCurrentSource(), index); client != nullptr) + { + if (client->isGuest()) + return std::string{"guest"}; + + return std::string{"classic"}; + } + + return std::string{}; +} + +// #I(string_list, index) +// Returns the string at the given index from the string list. +GS1ScriptValue mc_I(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Message Code #I requires exactly 2 arguments"); + + auto csvStringList = string::fromCSV(visitor->getGameValueAs(*arguments[0])); + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[1])); + if (index < csvStringList.size()) + return csvStringList[index]; + + return std::string{}; +} + +// #i(image) | #i(image, x, y, width, height) +// Displays an image or part of an image when used in a sign. +GS1ScriptValue mc_i(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::logic_error("Message Code #i is registered as a clientside message code"); +} + +// #K(ascii) +// The character represented by the given ASCII code. +GS1ScriptValue mc_K(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Message Code #K requires exactly 1 argument"); + + uint8_t ascii = std::min(static_cast(255), DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0]))); + return std::string{static_cast(ascii)}; +} + +// #k(key_index) +// The description of the specified key (in client language/key assignments). +GS1ScriptValue mc_k(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::logic_error("Message Code #k is registered as a clientside message code (maybe?)"); +} + +// #L [Read] +// The current level filename of the NPC (use #F for the player). +GS1ScriptValue mc_L(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + auto npc = getNPCFromSource(visitor->getOriginalSource()); + if (npc != nullptr) + { + if (auto level = npc->getLevel(); level != nullptr) + return std::string{level->levelName}; + return std::string{npc->level}; + } + + return std::string{}; +} + +// #m | #m(index) [Read / Write] +// The animation of the player. +GS1ScriptValue mc_m(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + return handleCharacterBasedMessageCode(visitor, arguments, [](Character& character, const auto& arguments) -> pickerReturn + { + return std::make_pair(character.gani, 10); + }); +} + +// #n | #n(index) [Read / Write] +// The nickname of the player. +GS1ScriptValue mc_n(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + return handleCharacterBasedMessageCode(visitor, arguments, [](Character& character, const auto& arguments) -> pickerReturn + { + return std::make_pair(character.nickName, 11); + }); +} + +// #N | #N(index) [Read] +// The database NPC name. +GS1ScriptValue mc_N(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + std::optional index = std::nullopt; + if (arguments.size() == 1) + index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + + if (auto npc = getNPCFromSource(visitor->getCurrentSource(), index); npc != nullptr) + { + // Explicitly place it in another string as the return will trigger move semantics. + return std::string{npc->name}; + } + + return std::string{}; +} + +// #p(index) [Read] +// The action parameter of the specified index. +GS1ScriptValue mc_p(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Message Code #p requires exactly 1 argument"); + + // The first event argument is the name of the triggeraction, so add +1 to get to the params. + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])) + 1; + if (index < visitor->getEvent().args.size()) + { + if (auto* arg = std::any_cast(&visitor->getEvent().args.at(index)); arg != nullptr) + { + // Explicitly place it in another string as the return will trigger move semantics. + return std::string{*arg}; + } + } + return std::string{}; +} + +// #Q(guild_name, account_name) [Read] +// The nickname for a player in a guild. +GS1ScriptValue mc_Q(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 2) + throw std::invalid_argument("Message Code #Q requires exactly 2 arguments"); + + auto guildName = visitor->getGameValueAs(*arguments[0]); + auto accountName = visitor->getGameValueAs(*arguments[1]); + + auto guildManager = BabyDI::Get(); + auto names = guildManager->getPlayerNicknamesForGuild(guildName, accountName); + if (names.has_value()) + { + auto& firstIter = names.value().first; + auto& secondIter = names.value().second; + auto nickNameRange = std::ranges::subrange(firstIter, secondIter) | std::views::values; + return string::join(nickNameRange); + } + + return std::string{}; +} + +// #R(string_list) +// Randomly selects a string from the given string list. +GS1ScriptValue mc_R(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + using namespace std::chrono; + auto seed = static_cast(duration_cast(system_clock::now().time_since_epoch()).count()); + std::minstd_rand rng(seed); + std::uniform_int_distribution dist(0, arguments.size() - 1); + size_t index = dist(rng); + + return visitor->getGameValueAs(*arguments[index]); +} + +// #S +// The player's selected sword (Newworld). +GS1ScriptValue mc_S(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + throw std::logic_error("Message Code #S is registered as a clientside message code"); +} + +// #s(identifier) +// The string value of a variable. +GS1ScriptValue mc_s(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Message Code #s requires exactly 1 argument"); + + return visitor->getGameValueAs(*arguments[0]); +} + +// #t(index) [Read] +// The token at the specified index as created via tokenize. +GS1ScriptValue mc_t(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Message Code #t requires exactly 1 argument"); + + auto index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + if (index >= visitor->tokenizeTokens.size()) + return std::string{}; + + // Explicitly place it in another string as the return will trigger move semantics. + return std::string{visitor->tokenizeTokens[index]}; +} + +// #T(string) +// Trims the string. +GS1ScriptValue mc_T(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Message Code #T requires exactly 1 argument"); + + auto str = visitor->getGameValueAs(*arguments[0]); + string::trim(str); + return str; +} + +// #U(string) +// Replaces the string with a translated version of it. +GS1ScriptValue mc_U(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Message Code #U requires exactly 1 argument"); + + // Translation has already happened. + return visitor->getGameValueAs(*arguments[0]); +} + +// #v(identifier) +// The value of an number variable as a string. +GS1ScriptValue mc_v(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() != 1) + throw std::invalid_argument("Message Code #s requires exactly 1 argument"); + + auto number = visitor->getGameValueAs(*arguments[0]); + return std::format("{}", number); +} + +// #W | #W(index) [Read] +// Image filename of a player's weapon. +GS1ScriptValue mc_W(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() == 0) + throw std::logic_error("Message Code #W is registered as a clientside message code, specify a weapon index: #W(index)"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto* server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source->first); player != nullptr) + { + auto& weaponList = player->account.weapons; + if (weaponList.empty()) + return std::string{}; + + int64_t index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + if (index >= 0 && index < (int64_t)weaponList.size()) + { + if (auto weapon = server->getWeapon(weaponList[(size_t)index]); weapon != nullptr) + { + // Explicitly place it in another string as the return will trigger move semantics. + return std::string{weapon->image}; + } + } + } + } + return std::string{}; +} + +// #w | #w(index) [Read] +// The name of the player's weapon. +GS1ScriptValue mc_w(GS1Visitor* visitor, std::string_view messageCode, const std::vector& arguments) +{ + if (arguments.size() == 0) + throw std::logic_error("Message Code #w is registered as a clientside message code, specify a weapon index: #w(index)"); + + if (auto source = visitor->findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER); source.has_value()) + { + auto* server = BabyDI::Get(); + if (auto player = server->getNPCServer()->getPlayer(source->first); player != nullptr) + { + auto& weaponList = player->account.weapons; + if (weaponList.empty()) + return std::string{}; + + int64_t index = DoubleAsIntegralFloor(visitor->getGameValueAs(*arguments[0])); + if (index >= 0 && index < (int64_t)weaponList.size()) + { + // Explicitly place it in another string as the return will trigger move semantics. + return std::string{weaponList[(size_t)index]}; + } + } + } + return std::string{}; +} + +//---------------------------- + +// #C0 - #C4 | #C0(index) - #C4(index) [Read / Write] +// #C0 - skin color +// #C1 - coat color +// #C2 - sleeves color +// #C3 - shoes color +// #C4 - belt color +// New World additional body colors: +// #C5 - #C7 | #C5(index) - #C7(index) [Read / Write] +GS1ScriptValue mc_C(GS1Visitor* visitor, uint8_t index, std::string_view messageCode, const std::vector& arguments) +{ + return handleCharacterBasedMessageCode(visitor, arguments, [&index](Character& character, const auto& arguments) -> pickerReturn + { + return std::make_pair(GameValue{std::string{getClassicColorName(static_cast(character.colors[index]))}}, static_cast(20 + index)); + }); +} + +// #P1 - #P30 | #P1(index) - #P30(index) [Read / Write] +// Gani attributes. +GS1ScriptValue mc_P(GS1Visitor* visitor, uint8_t index, std::string_view messageCode, const std::vector& arguments) +{ + return handleCharacterBasedMessageCode(visitor, arguments, [&index](Character& character, const auto& arguments) -> pickerReturn + { + return std::make_pair(GameValue{character.ganiAttributes[index]}, static_cast(30 + index - 1)); + }); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1::grammar diff --git a/server/src/scripting/gs1/GS1Variables.cpp b/server/src/scripting/gs1/GS1Variables.cpp new file mode 100644 index 000000000..16aa5ee1f --- /dev/null +++ b/server/src/scripting/gs1/GS1Variables.cpp @@ -0,0 +1,433 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1 +{ +/////////////////////////////////////////////////////////////////////////////// + +void setGlobalVariables(GameVariableStore& variableStore) +{ + auto* server = BabyDI::Get(); + + // timevar + variableStore.add(GameValue{ "timevar", gameValueGetter([server]() { return static_cast(server->getNWTime()); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "timevar2", gameValueGetter([server]() { return static_cast(server->getFrameStartTimeHighPrecision().time_since_epoch().count()); }), GameValue::func_set{} }); + + // allplayers + variableStore.add(GameValue{ "allplayerscount", + gameValueGetter([server]() + { + auto size = std::ranges::distance(server->getNPCServer()->getPlayerList() | std::views::filter([](auto& kvp) { return dynamic_cast(kvp.second.get()) != nullptr && kvp.second->getId() != 0; })); + return static_cast(size); + }), GameValue::func_set{} + }); + variableStore.add(GameValue{ "allplayers", + gameValueGetter([server]() + { + auto playerObjects = server->getNPCServer()->getPlayerList() + | std::views::filter([](auto& kvp) { return dynamic_cast(kvp.second.get()) != nullptr && kvp.second->getId() != 0; }) + | std::views::transform([](auto& kvp) { return ScriptObject{ std::make_pair((size_t)kvp.first, ScriptObjectType::PLAYER)}; }); + std::vector players{ std::ranges::begin(playerObjects), std::ranges::end(playerObjects) }; + return players; + }), GameValue::func_set{} + }); + + // gravity + variableStore.add(GameValue{ "gravity", + gameValueGetter([server]() + { + return server->Scripting.variables.getValue("gravity").value_or(2.0); + }), + gameValueSetter([server](const GameValue& value, std::optional index) + { + if (auto var = server->Scripting.variables.get("gravity").lock(); var != nullptr) + var->set(value.get().value_or(2.0)); + }) + }); + + // waterheight + variableStore.add(GameValue{ "waterheight", + gameValueGetter([server]() + { + return server->Scripting.variables.getValue("waterheight").value_or(0.0); + }), + gameValueSetter([server](const GameValue& value, std::optional index) + { + if (auto var = server->Scripting.variables.get("waterheight").lock(); var != nullptr) + var->set(value.get().value_or(0.0)); + }) + }); + + // nwtime and derivatives. + variableStore.add(GameValue{ "nwtime", + gameValueGetter([server]() { return static_cast(server->getNWTime()); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "nwmin", // 60 min in an hour + gameValueGetter([server]() { return static_cast(server->getNWTime() % 60); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "nwhour", // 24 hours in a day + gameValueGetter([server]() { return static_cast((server->getNWTime() / 60) % 24); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "nwday", // 28 days in a month + gameValueGetter([server]() { return static_cast((server->getNWTime() / 1440) % 28); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "nwweekday", + gameValueGetter([server]() { return static_cast((server->getNWTime() / 1440) % 7) + 1; }), GameValue::func_set{} }); + variableStore.add(GameValue{ "nwweek", // 4 weeks in a month (7 days per week) + gameValueGetter([server]() { return static_cast((server->getNWTime() / 10080) % 4); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "nwmonth", // 10 months in a year + gameValueGetter([server]() { return static_cast((server->getNWTime() / 40320) % 10); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "nwyear", // Years start at 1000 + gameValueGetter([server]() { return static_cast((server->getNWTime() / 403200) + 1000); }), GameValue::func_set{} }); + + // groundheights[] + variableStore.add(GameValue{ "groundheights", + gameValueGetter([server](std::optional index) + { + if (!index.has_value() || index.value() < 0 || index.value() >= (int64_t)server->groundHeights.size()) + return 0.0; + return server->groundHeights.at(index.value()); + }), + gameValueSetter([server](const GameValue& value, std::optional index) + { + if (!index.has_value() || index.value() < 0 || index.value() >= (int64_t)server->groundHeights.size()) + return; + server->groundHeights.at(index.value()) = value.get().value_or(0.0); + }) + }); +} + +void setNPCVariables(GameVariableStore& variableStore, std::weak_ptr npc) +{ + auto npcPtr = npc.lock(); + if (npcPtr == nullptr) + return; + + // board[] + // This variable only checks the sub-level board data, so we need to know the NPC's level and position. + variableStore.add(GameValue{ "board", + gameValueGetter([npc](std::optional index) -> GameValue + { + auto npcPtr = npc.lock(); + if (npcPtr == nullptr) return 0.0; + + auto levelPtr = npcPtr->getLevel(); + if (levelPtr == nullptr || index.value_or(0) < 0 || index.value_or(0) >= 4096) return 0.0; + + const auto& levelTiles = levelPtr->getTiles(npcPtr->character.getMapPosition()); + if (!levelTiles.has_value()) + return 0.0; + + if (!index.has_value()) + { + std::vector tiles; + tiles.insert(tiles.end(), std::ranges::begin(*levelTiles.value()), std::ranges::end(*levelTiles.value())); + return tiles; + } + + return static_cast(levelTiles.value()->at(index.value())); + }), GameValue::func_set{} + }); +} + +void setPlayerVariables(GameVariableStore& variableStore, std::weak_ptr player) +{ + auto playerPtr = player.lock(); + if (playerPtr == nullptr) + return; + + auto* server = BabyDI::Get(); + + // weaponscount + variableStore.add(GameValue{ "weaponscount", + gameValueGetter([player]() { return player.expired() ? 0.0 : static_cast(player.lock()->account.weapons.size()); }), GameValue::func_set{}}); + + // levelorgx / levelorgy + variableStore.add(GameValue{ "levelorgx", + gameValueGetter([server, player]() + { + if (player.expired()) return 0.0; + if (auto npc = server->getNPC(player.lock()->getAttachedNPC()); npc != nullptr) + return -npc->character.getGlobalPosition().x() / 16.0; + return 0.0; + }), GameValue::func_set{} + }); + variableStore.add(GameValue{ "levelorgy", + gameValueGetter([server, player]() + { + if (player.expired()) return 0.0; + if (auto npc = server->getNPC(player.lock()->getAttachedNPC()); npc != nullptr) + return -npc->character.getGlobalPosition().y() / 16.0; + return 0.0; + }), GameValue::func_set{} + }); + + // all the player property shortcuts + playerPtr->constructScriptParameters(); + for (const auto& [name, variable] : playerPtr->scriptParameters) + variableStore.add(GameValue{ set_temporary, std::format("player{}", name), variable.getGetter(), variable.getSetter() }); +} + +void setLevelVariables(GameVariableStore& variableStore, std::weak_ptr level) +{ + // TODO: These variables should be stored on the level so they don't get remade for every single script. Like the object parameters stuff. + + if (level.expired()) + return; + + // players + variableStore.add(GameValue{ "playerscount", gameValueGetter([level]() { return level.expired() ? 0.0 : static_cast(level.lock()->getPlayers().size()); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "players", + gameValueGetter([level](std::optional index) + { + auto levelPtr = level.lock(); + if (levelPtr == nullptr) return std::vector{}; + std::vector players; + + if (index.value_or(0) == -1) + { + // TODO: Current player. + return players; + } + + auto playerObjects = levelPtr->getPlayers() + | std::views::drop(index.value_or(0)) + | std::views::take(index.has_value() ? 1 : std::numeric_limits::max()) + | std::views::transform([](const PlayerID& id) { return ScriptObject{ std::make_pair((size_t)id, ScriptObjectType::PLAYER) }; }); + + std::ranges::copy(playerObjects, std::back_inserter(players)); + return players; + }), GameValue::func_set{} + }); + + // npcs + variableStore.add(GameValue{ "npcscount", gameValueGetter([level]() { return level.expired() ? 0.0 : static_cast(level.lock()->getNPCs().size()); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "npcs", + gameValueGetter([level](std::optional index) + { + auto levelPtr = level.lock(); + if (levelPtr == nullptr || index.value_or(0) < 0) return std::vector{}; + std::vector npcs; + + auto npcObjects = levelPtr->getNPCs() + | std::views::drop(index.value_or(0)) + | std::views::take(index.has_value() ? 1 : std::numeric_limits::max()) + | std::views::transform([](const NPCID& id) { return ScriptObject{ std::make_pair((size_t)id, ScriptObjectType::NPC) }; }); + + std::ranges::copy(npcObjects, std::back_inserter(npcs)); + return npcs; + }), GameValue::func_set{} + }); + + // compus + variableStore.add(GameValue{ "compuscount", gameValueGetter([level]() { return level.expired() ? 0.0 : static_cast(level.lock()->getBaddyCount()); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "compus", + gameValueGetter([level](std::optional index) + { + auto levelPtr = level.lock(); + if (levelPtr == nullptr || index.value_or(0) < 0) return std::vector{}; + std::vector compus; + + auto objects = levelPtr->getBaddies() + | std::views::filter([](const LevelBaddy& baddy) { return baddy.mode != BaddyMode::DEAD; }) + | std::views::drop(index.value_or(0)) + | std::views::take(index.has_value() ? 1 : std::numeric_limits::max()) + | std::views::transform([](const LevelBaddy& baddy) { return ScriptObject{ std::make_pair((size_t)baddy.id, ScriptObjectType::BADDY) }; }); + + std::ranges::copy(objects, std::back_inserter(compus)); + return compus; + }), GameValue::func_set{} + }); + + // bombs + variableStore.add(GameValue{ "bombscount", gameValueGetter([level]() { return level.expired() ? 0.0 : static_cast(level.lock()->getBombs().size()); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "bombs", + gameValueGetter([level](std::optional index) + { + auto levelPtr = level.lock(); + if (levelPtr == nullptr || index.value_or(0) < 0) return std::vector{}; + + if (!index.has_value()) + { + std::vector objectList{}; + for (size_t i = 0; levelPtr && i < levelPtr->getBombs().size(); ++i) + objectList.emplace_back(std::make_pair(i, ScriptObjectType::BOMB)); + return objectList; + } + + return std::vector{ std::make_pair(index.value(), ScriptObjectType::BOMB) }; + }), GameValue::func_set{} + }); + + // arrows + variableStore.add(GameValue{ "arrowscount", gameValueGetter([level]() { return level.expired() ? 0.0 : static_cast(level.lock()->getArrows().size()); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "arrows", + gameValueGetter([level](std::optional index) + { + auto levelPtr = level.lock(); + if (levelPtr == nullptr || index.value_or(0) < 0) return std::vector{}; + + if (!index.has_value()) + { + std::vector objectList{}; + for (size_t i = 0; levelPtr && i < levelPtr->getArrows().size(); ++i) + objectList.emplace_back(std::make_pair(i, ScriptObjectType::ARROW)); + return objectList; + } + + return std::vector{ std::make_pair(index.value(), ScriptObjectType::ARROW) }; + }), GameValue::func_set{} + }); + + // items + variableStore.add(GameValue{ "itemscount", gameValueGetter([level]() { return level.expired() ? 0.0 : static_cast(level.lock()->getItems().size()); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "items", + gameValueGetter([level](std::optional index) + { + auto levelPtr = level.lock(); + if (levelPtr == nullptr || index.value_or(0) < 0) return std::vector{}; + + if (!index.has_value()) + { + std::vector objectList{}; + for (size_t i = 0; levelPtr && i < levelPtr->getItems().size(); ++i) + objectList.emplace_back(std::make_pair(i, ScriptObjectType::ITEM)); + return objectList; + } + + return std::vector{ std::make_pair(index.value(), ScriptObjectType::ITEM) }; + }), GameValue::func_set{} + }); + + // explos + variableStore.add(GameValue{ "exploscount", gameValueGetter([level]() { return level.expired() ? 0.0 : static_cast(level.lock()->getExplosions().size()); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "explos", + gameValueGetter([level](std::optional index) + { + auto levelPtr = level.lock(); + if (levelPtr == nullptr || index.value_or(0) < 0) return std::vector{}; + + if (!index.has_value()) + { + std::vector objectList{}; + for (size_t i = 0; levelPtr && i < levelPtr->getExplosions().size(); ++i) + objectList.emplace_back(std::make_pair(i, ScriptObjectType::EXPLOSION)); + return objectList; + } + + return std::vector{ std::make_pair(index.value(), ScriptObjectType::EXPLOSION) }; + }), GameValue::func_set{} + }); + + // horses + variableStore.add(GameValue{ "horsescount", gameValueGetter([level]() { return level.expired() ? 0.0 : static_cast(level.lock()->getHorses().size()); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "horses", + gameValueGetter([level](std::optional index) + { + auto levelPtr = level.lock(); + if (levelPtr == nullptr || index.value_or(0) < 0) return std::vector{}; + + if (!index.has_value()) + { + std::vector objectList{}; + for (size_t i = 0; levelPtr && i < levelPtr->getHorses().size(); ++i) + objectList.emplace_back(std::make_pair(i, ScriptObjectType::HORSE)); + return objectList; + } + + return std::vector{ std::make_pair(index.value(), ScriptObjectType::HORSE) }; + }), GameValue::func_set{} + }); + + // signs + variableStore.add(GameValue{ "signscount", gameValueGetter([level]() { return level.expired() ? 0.0 : static_cast(level.lock()->getSignCount()); }), GameValue::func_set{} }); + variableStore.add(GameValue{ "signs", + gameValueGetter([level](std::optional index) + { + auto levelPtr = level.lock(); + if (levelPtr == nullptr || index.value_or(0) < 0) return std::vector{}; + + if (!index.has_value()) + { + std::vector objectList{}; + for (size_t i = 0; levelPtr && i < levelPtr->getSignCount(); ++i) + objectList.emplace_back(std::make_pair(i, ScriptObjectType::SIGN)); + return objectList; + } + + return std::vector{ std::make_pair(index.value(), ScriptObjectType::SIGN) }; + }), GameValue::func_set{} + }); + + // board[] + // Needs the map position of the NPC, so moved there. + + // tiles[x,y] -> tiles[] + variableStore.add(GameValue{ "tiles", + gameValueGetter([level](std::optional index) + { + auto levelPtr = level.lock(); + if (levelPtr == nullptr || index.value_or(-1) < 0) return 0.0; + + // Get the tile X/Y out of the index. + uint32_t tileX = static_cast(index.value() >> 32); + uint32_t tileY = static_cast(index.value() & 0xFFFFFFFF); + TilePosition tilePos{ static_cast(tileX), static_cast(tileY) }; + + // Get the tile. + if (auto tile = levelPtr->getMapTileForEditing(tilePos); tile != nullptr) + return static_cast(*tile); + return 0.0; + }), + gameValueSetter([level](const GameValue& value, std::optional index) + { + auto levelPtr = level.lock(); + if (levelPtr == nullptr || index.value_or(-1) < 0) return; + + // Get the tile X/Y out of the index. + uint32_t tileX = static_cast(index.value() >> 32); + uint32_t tileY = static_cast(index.value() & 0xFFFFFFFF); + TilePosition tilePos{ static_cast(tileX), static_cast(tileY) }; + + // Get and update the tile. + if (auto tile = levelPtr->getMapTileForEditing(tilePos); tile != nullptr) + *tile = static_cast(value.get().value_or(0.0)); + }) + }); +} + +void setOtherVariables(GameVariableStore& variableStore, ScriptEvent& event) +{ + // paramscount + variableStore.add(GameValue{ "paramscount", + gameValueGetter([&event]() + { + return static_cast(std::max::size_type>(1, event.args.size()) - 1); + }), GameValue::func_set{} + }); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1 diff --git a/server/src/scripting/gs1/GS1Visitor.cpp b/server/src/scripting/gs1/GS1Visitor.cpp new file mode 100644 index 000000000..c551ee390 --- /dev/null +++ b/server/src/scripting/gs1/GS1Visitor.cpp @@ -0,0 +1,1786 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef DEBUG + #define RECOVERABLE_PARSE_ERROR(MESSAGE, RETVAL) throw std::runtime_error(std::format("GS1 Parse Error: {}", MESSAGE)) +#else + #define RECOVERABLE_PARSE_ERROR(MESSAGE, RETVAL) \ + do { \ + reportError(MESSAGE, context, false); \ + return RETVAL; \ + } \ + while (false) +#endif + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1::grammar +{ +/////////////////////////////////////////////////////////////////////////////// + +constexpr size_t MAX_LOOPS = 10000; + +/////////////////////////////////////////////////////////////////////////////// +// File static functions. + +static std::optional getSymbolType(antlr4::tree::ParseTree* tree) +{ + if (tree == nullptr) return std::nullopt; + + // We might be looking for the direct child. + if (tree->children.size() == 1) + tree = tree->children[0]; + + // Find the symbol type if this is a TerminalNode. + if (auto* node = dynamic_cast(tree); node != nullptr) + return node->getSymbol()->getType(); + + return std::nullopt; +} + +static std::generator getJoinedClassesFromSource(ScriptObject source) +{ + auto* server = BabyDI::Get(); + switch (source.second) + { + case ScriptObjectType::NPC: + if (auto npc = server->getNPC(source.first); npc != nullptr) + { + for (ScriptClassPtr scriptClass : npc->getJoinedClasses()) + co_yield &scriptClass->getScript(); + } + break; + + case ScriptObjectType::WEAPON: + { + auto& weaponList = server->getWeaponList(); + if (auto it = weaponList.find(source.first); it != weaponList.end()) + { + for (ScriptClassPtr scriptClass : it->second->getJoinedClasses()) + co_yield &scriptClass->getScript(); + } + } + } +} + +static GameVariableStore* getGameVariableStoreFromSource(ScriptObject source) +{ + static GameVariableStore invalidStore; + + auto* server = BabyDI::Get(); + switch (source.second) + { + case ScriptObjectType::PLAYER: + if (auto player = server->getNPCServer()->getPlayer(source.first); player != nullptr) + return &player->account.variables; + break; + case ScriptObjectType::NPC: + if (auto npc = server->getNPC(source.first); npc != nullptr) + return &npc->scripting.variables; + break; + case ScriptObjectType::WEAPON: + { + auto& weaponList = server->getWeaponList(); + if (auto it = weaponList.find(source.first); it != weaponList.end()) + return &it->second->scripting.variables; + log::printLine(log::script, "Could not find weapon source."); + return &invalidStore; + } + case ScriptObjectType::LEVEL: + { + auto& levelList = server->getLevelList(); + if (auto it = levelList.find(source.first); it != levelList.end()) + return &it->second->scripting.variables; + log::printLine(log::script, "Could not find level source."); + return &invalidStore; + } + case ScriptObjectType::SERVER: + return &server->Scripting.variables; + } + return nullptr; +} + +static GS1ScriptValue getGS1ScriptValueFromAny(std::any& value) +{ + if (auto* gs1ScriptValue = std::any_cast(&value); gs1ScriptValue != nullptr) + return *gs1ScriptValue; + return {}; +} + +/////////////////////////////////////////////////////////////////////////////// +// Static member functions. + +/////////////////////////////////////////////////////////////////////////////// +// Public member functions. + +GameValue* GS1Visitor::getGameValueFromGS1ScriptValue(GS1ScriptValue& value) +{ + if (auto* gs1GameVariable = std::get_if(&value); gs1GameVariable != nullptr) + return &gs1GameVariable->first; + return nullptr; +} + +std::optional GS1Visitor::getGameValueFromSource(const ScriptObject& source, std::string_view identifier) +{ + auto* server = BabyDI::Get(); + + switch (source.second) + { + case ScriptObjectType::NPC: + if (auto npc = server->getNPC(source.first); npc != nullptr) + return getScriptParameter(*npc, identifier); + break; + case ScriptObjectType::PLAYER: + if (auto player = server->getNPCServer()->getPlayer(source.first); player != nullptr) + return getScriptParameter(*player, identifier); + break; + case ScriptObjectType::BADDY: + if (auto level = findCurrentLevel(); level != nullptr) + { + if (auto baddy = level->getBaddyById(source.first); baddy.has_value()) + return getScriptParameter(*baddy.value(), identifier); + } + break; + case ScriptObjectType::BOMB: + if (auto level = findCurrentLevel(); level != nullptr) + { + if (auto bomb = level->getBomb(source.first); bomb.has_value()) + return getScriptParameter(*bomb.value(), identifier); + } + break; + case ScriptObjectType::ARROW: + if (auto level = findCurrentLevel(); level != nullptr) + { + if (auto arrow = level->getArrow(source.first); arrow.has_value()) + return getScriptParameter(*arrow.value(), identifier); + } + break; + case ScriptObjectType::ITEM: + if (auto level = findCurrentLevel(); level != nullptr) + { + if (auto item = level->getItem(source.first); item.has_value()) + return getScriptParameter(*item.value(), identifier); + } + break; + case ScriptObjectType::EXPLOSION: + if (auto level = findCurrentLevel(); level != nullptr) + { + if (auto explo = level->getExplosion(source.first); explo.has_value()) + return getScriptParameter(*explo.value(), identifier); + } + break; + case ScriptObjectType::HORSE: + if (auto level = findCurrentLevel(); level != nullptr) + { + if (auto horse = level->getHorse(source.first); horse.has_value()) + return getScriptParameter(*horse.value(), identifier); + } + break; + case ScriptObjectType::SIGN: + if (auto level = findCurrentLevel(); level != nullptr) + { + if (auto sign = level->getSign(source.first); sign.has_value()) + return getScriptParameter(*sign.value(), identifier); + } + break; + } + + return std::nullopt; +} + +GameValue GS1Visitor::getGameValueFromStorage(std::string_view identifier, std::optional type) +{ + // If we have a specific storage type, try to get the store for it. + if (type.has_value()) + { + if (auto* store = getGameVariableStoreForStorageType(type.value()); store != nullptr) + return store->getOrStub(identifier); + } + + // First, try to get a built-in variable. + if (builtInStore != nullptr && builtInStore->contains(identifier)) + return builtInStore->getOrStub(identifier); + + auto checkStore = [&](const ScriptObject& source) -> std::optional + { + auto* store = getGameVariableStoreFromSource(source); + bool storeHasIdentifier = store != nullptr && store->contains(identifier); + + // First, if we have a storage type, get directly from the variable store. + if (type.has_value() && storeHasIdentifier) + return store->getOrStub(identifier); + + // Second, if we have no storage type, check for a property. + if (auto property = getGameValueFromSource(source, identifier); property.has_value()) + return property.value(); + + // Lastly, check the variable store. + if (storeHasIdentifier) + return store->getOrStub(identifier); + + return std::nullopt; + }; + + // Second, look in the current source's store. + if (auto result = checkStore(getCurrentSource()); result.has_value()) + return result.value(); + + // Now look in the original source's store. + if (auto result = checkStore(getOriginalSource()); result.has_value()) + return result.value(); + + // Lastly, look at the initiator's store. + if (m_event->initiator != getOriginalSource()) + { + if (auto result = checkStore(m_event->initiator); result.has_value()) + return result.value(); + } + + // If we still don't have a store, use the built-in store. + return builtInStore->getOrStub(identifier); +} + +double GS1Visitor::getColorValueFromString(std::string_view colorString) +{ + auto it = std::ranges::find(colorNames, colorString); + if (it == colorNames.end()) + it = colorNames.begin(); + + return static_cast(std::distance(colorNames.begin(), it)); +} + +GS1ScriptValue GS1Visitor::translateSourceText(antlr4::tree::ParseTree* node, std::string_view language) +{ + // TODO: We should cache this somewhere. + + if (node == nullptr) + return std::string{}; + if (m_parser == nullptr) + return node->getText(); + + // Get the compound string context. + auto compoundStringContext = walkToContext(node); + if (compoundStringContext == nullptr) + return node->getText(); + + // Get the raw text of the compound string. + std::string raw; + auto* tokenStream = m_parser->getTokenStream(); + if (tokenStream != nullptr) + raw = std::move(tokenStream->getText(compoundStringContext->getSourceInterval())); + + // If the text is empty or consists solely of whitespace, just evaluate the original string without translating. + if (string::empty_or_whitespace(raw)) + return node->getText(); + + return translateSourceText(raw, language); +} + +GS1ScriptValue GS1Visitor::translateSourceText(std::string_view sourceText, std::string_view language) +{ + // TODO: We should cache this somewhere. + + // Get the translation manager. + // If we don't have one, just evaluate the original string. + auto translationManager = BabyDI::Get(); + if (translationManager == nullptr) + return std::string{sourceText}; + + // Translate the raw string. + auto translated = translationManager->getText(language, string::trim(sourceText)); + + // Reparse the translated string and get the result. + return processStringExpression(translated); +} + +GS1ScriptValue GS1Visitor::processStringExpression(std::string_view expression) +{ + // If we are already reparsing string content, do not recurse. + if (m_reparsingStringExpression) + return std::string{expression}; + + // Reparse the string expression and get the result. + SetAndRestore sar{m_reparsingStringExpression, true}; + auto result = reparseExpression(expression, "S", [](GS1Parser& parser) { return parser.compound_string(); }); + return getReadOnlyGameValueFromAny(result); +} + +GS1ScriptValue GS1Visitor::processMathExpression(std::string_view expression) +{ + // If we are already reparsing math content, do not recurse. + if (m_reparsingMathExpression) + return 0.0; + + // Reparse the math expression and get the result. + SetAndRestore sar{m_reparsingMathExpression, true}; + auto result = reparseExpression(expression, "E", [](GS1Parser& parser) { return parser.expression(); }); + return getReadOnlyGameValueFromAny(result); +} + +std::any GS1Visitor::reparseExpression(std::string_view expression, std::string_view lexerMode, std::function node) +{ + // Create an input stream for the expression. + antlr4::ANTLRInputStream inputStream{expression}; + GS1Lexer lexer(&inputStream); + + // Put the lexer into the chosen mode. + // E = expression, S = string. + lexer.pushCommand(lexerMode); + + // Fill up our token stream with the lexer. + antlr4::CommonTokenStream tokens(&lexer); + tokens.fill(); + + // Construct a parser to handle our tokens. + GS1Parser parser(&tokens); + + // Get our AST on the fragment. + auto* tree = node(parser); + + // Sanity check the errors. + if (parser.getNumberOfSyntaxErrors() != 0) + throw std::runtime_error(std::format("failed to reparse expression: {}", expression)); + + // Return the result of processing the tree. + return tree->accept(this); +} + +std::vector GS1Visitor::visitChildrenAndCollect(antlr4::tree::ParseTree* node) +{ + if (node == nullptr) return {}; + std::vector results; + for (size_t i = 0; i < node->children.size(); ++i) + { + auto ret = node->children[i]->accept(this); + if (ret.has_value()) + results.emplace_back(std::move(ret)); + } + return results; +} + +/////////////////////////////////////////////////////////////////////////////// +// Member functions. + +std::any GS1Visitor::safeVisit(antlr4::tree::ParseTree* node) +{ + if (node == nullptr) + return {}; + return visit(node); +} + +std::optional GS1Visitor::findNearestScriptObjectSourceFromStack(ScriptObjectType type) const +{ + for (const auto& source : sourceStack()) + { + if (source.second == type) + return source; + } + return std::nullopt; +} + +std::shared_ptr GS1Visitor::findCurrentLevel() const +{ + auto* server = BabyDI::Get(); + auto testSource = [server](const ScriptObject& source) -> std::shared_ptr + { + if (source.second == ScriptObjectType::NPC) + { + if (auto npc = server->getNPC(source.first); npc != nullptr) + return npc->getLevel(); + } + else if (source.second == ScriptObjectType::PLAYER) + { + if (auto player = server->getNPCServer()->getPlayer(source.first); player != nullptr) + return server->getLoadedLevel(player->account.level, player); + } + else if (source.second == ScriptObjectType::LEVEL) + { + auto& levelList = server->getLevelList(); + if (auto level = levelList.find(source.first); level != levelList.end()) + return level->second; + } + return nullptr; + }; + + for (const auto& source : sourceStack()) + { + if (auto level = testSource(source); level != nullptr) + return level; + } + return nullptr; +} + +std::tuple, std::shared_ptr, std::shared_ptr> GS1Visitor::findCurrentLevelData() const +{ + auto* server = BabyDI::Get(); + auto testSource = [server](const ScriptObject& source) -> std::tuple, std::shared_ptr, std::shared_ptr> + { + if (source.second == ScriptObjectType::NPC) + { + if (auto npc = server->getNPC(source.first); npc != nullptr) + { + if (auto level = npc->getLevel(); level != nullptr) + { + auto [subLevel, levelData] = level->getSubLevelAndStaticDataAtPosition(npc->character.getMapPosition()); + return std::make_tuple(level, subLevel, levelData); + } + } + } + else if (source.second == ScriptObjectType::PLAYER) + { + if (auto player = server->getNPCServer()->getPlayer(source.first); player != nullptr) + { + if (auto level = server->getLoadedLevel(player->account.level, player); level != nullptr) + { + auto [subLevel, levelData] = level->getSubLevelAndStaticDataAtPosition(player->getMapPosition()); + return std::make_tuple(level, subLevel, levelData); + } + } + } + else if (source.second == ScriptObjectType::LEVEL) + { + auto& levelList = server->getLevelList(); + if (auto level = levelList.find(source.first); level != levelList.end()) + return std::make_tuple(level->second, nullptr, nullptr); + } + return std::make_tuple(nullptr, nullptr, nullptr); + }; + + for (const auto& source : sourceStack()) + { + if (auto level = testSource(source); std::get<0>(level) != nullptr) + return level; + } + return std::make_tuple(nullptr, nullptr, nullptr); +} + +GameVariableStore* GS1Visitor::findGameVariableStoreFromSourceStack(ScriptObjectType type, int skip) const +{ + std::optional foundSource; + + for (const auto& source : sourceStack()) + { + if (source.second == type) + { + if (skip <= 0) + return getGameVariableStoreFromSource(source); + + foundSource = source; + --skip; + } + } + + if (foundSource.has_value()) + return getGameVariableStoreFromSource(foundSource.value()); + + return nullptr; +} + +GameVariableStore* GS1Visitor::getGameVariableStoreForStorageType(size_t type) +{ + GameVariableStore* store = nullptr; + int skip = 0; + if (inList(type, ENUM(StorageType::THISO), ENUM(StorageType::CLIENTO), ENUM(StorageType::CLIENTRO))) + skip = 1; + + switch (type) + { + case ENUM(StorageType::THIS): + case ENUM(StorageType::LOCAL): + case ENUM(StorageType::TEMP): + case ENUM(StorageType::THISO): + store = findGameVariableStoreFromSourceStack(ScriptObjectType::NPC, skip); + if (store == nullptr) + store = findGameVariableStoreFromSourceStack(ScriptObjectType::WEAPON, skip); + break; + case ENUM(StorageType::CLIENT): + case ENUM(StorageType::CLIENTR): + case ENUM(StorageType::CLIENTO): + case ENUM(StorageType::CLIENTRO): + store = findGameVariableStoreFromSourceStack(ScriptObjectType::PLAYER, skip); + break; + case ENUM(StorageType::SERVER): + case ENUM(StorageType::SERVERR): + store = m_serverStore; + break; + case ENUM(StorageType::LEVEL): + { + auto* server = BabyDI::Get(); + auto pair = getPlayerOrNPCFromSource(m_originalSource); + if (!pair.has_value()) + return nullptr; + + const auto picker = visit_functions{ + [&server](PlayerPtr& player) -> LevelPtr + { + return server->getLoadedLevel(player->account.level, player); + }, + [&server](NPCPtr& npc) -> LevelPtr + { + return npc->getLevel(); + } + }; + + auto level = std::visit(picker, pair.value()); + return &level->scripting.variables; + } + } + + return store; +} + +GS1GameVariable GS1Visitor::getGameVariableFromAny(std::any& value) +{ + if (auto* gs1ScriptValue = std::any_cast(&value); gs1ScriptValue != nullptr) + { + if (auto* gs1GameVariable = std::get_if(gs1ScriptValue); gs1GameVariable != nullptr) + return *gs1GameVariable; + return {}; + } + + if (auto* gs1GameVariable = std::any_cast(&value); gs1GameVariable != nullptr) + return *gs1GameVariable; + + return {}; +} + +GameValue GS1Visitor::getReadOnlyGameValueFromGS1ScriptValue(const GS1ScriptValue& value) +{ + if (auto* gs1GameVariable = std::get_if(&value); gs1GameVariable != nullptr) + { + if (gs1GameVariable->second.has_value()) + return gs1GameVariable->first.flatten(gs1GameVariable->second.value()); + return gs1GameVariable->first; + } + else if (auto* gameValue = std::get_if(&value); gameValue != nullptr) + { + return *gameValue; + } + return {}; +} + +GameValue GS1Visitor::getReadOnlyGameValueFromAny(const std::any& value) +{ + if (auto* gs1ScriptValue = std::any_cast(&value); gs1ScriptValue != nullptr) + return getReadOnlyGameValueFromGS1ScriptValue(*gs1ScriptValue); + return {}; +} + +std::optional GS1Visitor::getSourceFromGS1ScriptValue(GS1ScriptValue& value) +{ + if (auto* scriptObject = std::get_if(&value); scriptObject != nullptr) + return *scriptObject; + else if (auto* gs1GameVariable = std::get_if(&value); gs1GameVariable != nullptr) + { + auto* scriptObject = gs1GameVariable->first.get_unsafe(gs1GameVariable->second); + if (scriptObject != nullptr) + return *scriptObject; + } + return std::nullopt; +} + +void GS1Visitor::setCurrentPlayerVariables(std::optional source) +{ + if (!source.has_value() || source.value().second != ScriptObjectType::PLAYER) + { + builtInStore->clearTemporary("player"); + return; + } + + auto server = BabyDI::Get(); + auto player = server->getNPCServer()->getPlayer(source.value().first); + if (player == nullptr) + return; + + // all the player property shortcuts + player->constructScriptParameters(); + for (const auto& [name, variable] : player->scriptParameters) + builtInStore->add(GameValue{set_temporary, std::format("player{}", name), variable.getGetter(), variable.getSetter()}); +} + +/////////////////////////////////////////////////////////////////////////////// + +void GS1Visitor::execute(const ScriptEvent& event, ScriptObject source, GS1Parser& parser, ScriptExecutionContext& context, antlr4::tree::ParseTree* startNode) +{ + scriptContext = &context; + + m_parser = &parser; + m_event = &event; + m_originalSource = source; + + m_serverStore = getGameVariableStoreFromSource(source::FromServer()); + + // Check for a sleep resume. + // Sleeping scripts use the timeout event to resume themselves. + if (event.type == ScriptEventType::TIMEOUT && !m_sleepCallStack.empty()) + { + m_callStack = std::move(m_sleepCallStack); + m_sleepCallStack.clear(); + startNode = m_callStack.back().first; + + m_currentSource = std::move(m_sleepCurrentSource); + m_sleepCurrentSource.clear(); + } + + // Execute! + try + { + size_t loops = 0; + do + { + visit(startNode); + + if (!m_callStack.empty()) + startNode = m_callStack.back().first; + + assert(loops < 100); + } + while (!m_callStack.empty()); + } + catch (const sleep_exception&) + { + // Save the call stack. + // We do it here because the sleep exception may get caught in multiple blocks in the visitor. + m_sleepCallStack = std::move(m_callStack); + m_callStack.clear(); + + m_sleepCurrentSource = std::move(m_currentSource); + m_currentSource.clear(); + } + + m_callStack.clear(); +} + +/////////////////////////////////////////////////////////////////////////////// + +void GS1Visitor::reportError(std::string_view message, antlr4::tree::ParseTree* node, bool abort) +{ + std::vector> logbatch; + + logbatch.emplace_back(0_ui8, std::format("* GS1 runtime script error for '{}':", who)); + if (abort) logbatch.emplace_back(0_ui8, "* Aborting script execution due to fatal error. *"); + + logbatch.emplace_back(1_ui8, std::format("Error: {}", message)); + if (node != nullptr) logbatch.emplace_back(1_ui8, std::format("Code: '{}'", node->getText())); + + // Log the batch of messages. + log::batch(log::script, logbatch); + + // Send the log messages to the server. + auto server = BabyDI::Get(); + std::ranges::for_each(logbatch, [&server](const auto& kvp) + { + server->sendToNC(kvp.second); + }); + + if (abort) throw std::runtime_error("Terminating GS1 script."); +} + +//////////////////////////////////////////////////////////////////////////////// + +std::any GS1Visitor::visitProgram(GS1Parser::ProgramContext* ctx) +{ + for (auto node : ctx->children) + { + try + { + node->accept(this); + } + // If we get the following exceptions in the block, just continue on to the next statement. + catch (const break_exception&) + {} + catch (const continue_exception&) + {} + catch (const return_exception&) + {} + // Sleeps stop execution. + catch (const sleep_exception&) + { + throw; + } + // Anything else also stops execution. + catch (...) + { + throw; + } + } + + return {}; +} + +std::any GS1Visitor::visitBlock(GS1Parser::BlockContext* ctx) +{ + if (ctx->children.empty()) + return {}; + + antlr4::tree::ParseTree* currentNode = ctx->children[0]; + size_t currentIndex = 0; + + auto moveNext = [&]() + { + // Move to the next node. + if (++currentIndex < ctx->children.size()) + { + currentNode = ctx->children[currentIndex]; + std::get<1>(m_callStack.back()) = currentIndex; + } + else + currentNode = nullptr; + }; + + // If we already have a call stack, resume from where we left off. + if (!m_callStack.empty() && ctx == m_callStack.back().first) + { + std::tie(currentNode, currentIndex) = m_callStack.back(); + currentNode = ctx->children[currentIndex]; + moveNext(); + } + else m_callStack.emplace_back(ctx, currentIndex); + + // Move through the children. + while (currentNode != nullptr) + { + // Visit the current node. + if (antlr4::tree::ErrorNode::is(*currentNode)) + visitErrorNode(dynamic_cast(currentNode)); + else if (antlr4::tree::TerminalNode::is(*currentNode)) + visitTerminal(dynamic_cast(currentNode)); + else + { + try + { + currentNode->accept(this); + } + catch (const sleep_exception&) + { + // Don't pop off the call stack so we can resume from this spot. + throw; + } + catch (...) + { + m_callStack.pop_back(); + throw; + } + } + + // Move to the next node. + moveNext(); + } + + m_callStack.pop_back(); + + return {}; +} + +//////////////////////////////////////////////////////////////////////////////// + +std::any GS1Visitor::visitStatementIf(GS1Parser::StatementIfContext* context) +{ + if ((bool)getReadOnlyGameValueFromAny(visit(context->expression()))) + return visit(context->block(0)); + else + return safeVisit(context->block(1)); +} + +std::any GS1Visitor::visitStatementFor(GS1Parser::StatementForContext* context) +{ + bool enterLoopAfterSleep = false; + + // Sleep resume. + // The block statement should be next in the stack, which means it will resume where it left off. + if (!m_callStack.empty() && m_callStack.back().first == context) + { + m_callStack.pop_back(); + enterLoopAfterSleep = true; + } + else + { + // Assignment. + safeVisit(context->assignmentStatement(0)); + } + + // Condition. + size_t loopCount = 0; + while ((loopCount++ < MAX_LOOPS && (bool)getReadOnlyGameValueFromAny(safeVisit(context->expression(0)))) || enterLoopAfterSleep) + { + enterLoopAfterSleep = false; + + // Block. + try + { + visit(context->block()); + } + catch (const break_exception&) + { + break; + } + catch (const continue_exception&) + { + continue; + } + catch (const sleep_exception&) + { + m_callStack.emplace_back(context, 0); + throw; + } + + // Increment. + safeVisit(context->expression(1)); + safeVisit(context->assignmentStatement(1)); + } + + return {}; +} + +std::any GS1Visitor::visitStatementWhile(GS1Parser::StatementWhileContext* context) +{ + bool enterLoopAfterSleep = false; + + // Sleep resume. + // The block statement should be next in the stack, which means it will resume where it left off. + if (!m_callStack.empty() && m_callStack.back().first == context) + { + m_callStack.pop_back(); + enterLoopAfterSleep = true; + } + + // Condition. + size_t loopCount = 0; + while ((loopCount++ < MAX_LOOPS && (bool)getReadOnlyGameValueFromAny(visit(context->expression()))) || enterLoopAfterSleep) + { + enterLoopAfterSleep = false; + + // Block. + try + { + visit(context->block()); + } + catch (const break_exception&) + { + break; + } + catch (const continue_exception&) + { + continue; + } + catch (const sleep_exception&) + { + m_callStack.emplace_back(context, 0); + throw; + } + } + + return {}; +} + +std::any GS1Visitor::visitStatementWith(GS1Parser::StatementWithContext* context) +{ + auto expression = visit(context->expression()); + auto value = getGS1ScriptValueFromAny(expression); + auto scriptObject = getSourceFromGS1ScriptValue(value); + + // No object? Don't execute the block. + if (!scriptObject.has_value()) + return {}; + + // Push the source object onto the source stack. + m_currentSource.emplace_back(*scriptObject); + setCurrentPlayerVariables(*scriptObject); + + // Execute the block with the new source. + auto result = visit(context->block()); + + // Pop the source off the source stack. + m_currentSource.pop_back(); + setCurrentPlayerVariables(findNearestScriptObjectSourceFromStack(ScriptObjectType::PLAYER)); + + return result; +} + +std::any GS1Visitor::visitStatementFunctionDefinition(GS1Parser::StatementFunctionDefinitionContext* context) +{ + // Don't execute user functions while walking through the tree. + return {}; +} + +std::any GS1Visitor::visitStatementUserFunctionCall(GS1Parser::StatementUserFunctionCallContext* context) +{ + if (m_parser == nullptr) + throw std::runtime_error("GS1Visitor is missing the link to the parser"); + + auto identifier = context->compound_identifier()->getText(); + auto function = m_parser->userFunctions.find(identifier); + if (function != m_parser->userFunctions.end()) + { + try + { + visit(function->second); + } + catch (const return_exception&) + { + } + return {}; + } + + // Try to call the function in our joined classes. + ScriptEvent eventCopy = *m_event; + for (auto script : getJoinedClassesFromSource(m_originalSource)) + { + if (script->runUserDefinedFunction(identifier, eventCopy, m_originalSource)) + return {}; + } + + RECOVERABLE_PARSE_ERROR(std::format("Could not find user function '{}'.", identifier), {}); + return {}; +} + +std::any GS1Visitor::visitStatementBuiltInCommand(GS1Parser::StatementBuiltInCommandContext* context) +{ + // Get the command. + auto command = context->COMMAND()->getText(); + string::trimRightMutate(command); + + try + { + // Process the built-in command. + processBuiltInCommand(this, context, command); + } + catch (const sleep_exception&) + { + throw; + } + catch (const unimplemented_error& e) + { + reportError(e.what(), context, false); + } + catch (const std::exception& e) + { + reportError(e.what(), context); + } + + return {}; +} + +std::any GS1Visitor::visitStatementAssignment(GS1Parser::StatementAssignmentContext* context) +{ + // We need this to fix problems with timeout being both a flag and an NPC property. + SetAndRestore sar{expectingTimeoutAsVariable, true}; + + auto results = visitChildrenAndCollect(context); + if (results.size() != 2 || context->children.size() != 3) + throw std::runtime_error("AssignmentOperation is not a binary expression"); + + auto op = getSymbolType(context->children[1]); + if (!op.has_value()) + throw std::runtime_error("AssignmentOperation has no operation"); + + auto left = getGameVariableFromAny(results[0]); + auto right = getReadOnlyGameValueFromAny(results[1]); + + // Do the assignment operation separately as everything else runs on doubles. + if (op.value() == GS1Parser::OP_ASSIGN) + { + if (left.second.has_value()) + left.first.assign(right, left.second); + else + { + if (auto vec = right.get_unsafe>(); vec != nullptr) + left.first.assign>(right); + else left.first.assign(right); + } + + // Special case for "timeout" to erase any existing sleep call stack. + if (left.first.identifier == "timeout") + { + m_sleepCallStack.clear(); + m_sleepCurrentSource.clear(); + } + + return {}; + } + + double leftD = left.first.get(left.second).value_or(0.0); + double rightD = right.get().value_or(0.0); + + // Perform the operation. + switch (op.value()) + { + case GS1Parser::OP_ASSIGN_ADD: + left.first.assign(leftD + rightD, left.second); + break; + case GS1Parser::OP_ASSIGN_SUB: + left.first.assign(leftD - rightD, left.second); + break; + case GS1Parser::OP_ASSIGN_MUL: + left.first.assign(leftD * rightD, left.second); + break; + case GS1Parser::OP_ASSIGN_DIV: + left.first.assign(leftD / rightD, left.second); + break; + case GS1Parser::OP_ASSIGN_MOD: + left.first.assign(static_cast(static_cast(leftD) % static_cast(rightD)), left.second); + break; + case GS1Parser::OP_ASSIGN_POW: + left.first.assign(std::pow(leftD, rightD), left.second); + break; + } + + // Assignment operations are statements and can't be used inside expressions. + return {}; +} + +//////////////////////////////////////////////////////////////////////////////// + +std::any GS1Visitor::visitExpressionIn(GS1Parser::ExpressionInContext* context) +{ + if (context->children.size() == 1) + return visitChildren(context); + + SetAndRestore sar{expectingTimeoutAsVariable, true}; + + std::vector values; + for (auto& be : context->exponentiationExpression()) + values.emplace_back(getReadOnlyGameValueFromAnyAs(visit(be))); + + std::any right_any; + if (context->primaryExpression() != nullptr) + right_any = visit(context->primaryExpression()); + else right_any = visit(context->range_literal()); + + auto* right_range = std::any_cast>(&right_any); + auto right_value = getReadOnlyGameValueFromAny(right_any); + auto* right_vector = right_value.get_unsafe>(); + + size_t range_op_left = GS1Parser::TOKEN_PIPE; + size_t range_op_right = GS1Parser::TOKEN_PIPE; + if (right_range != nullptr) + { + range_op_left = getSymbolType(context->range_literal()->children[0]).value_or(GS1Parser::TOKEN_PIPE); + range_op_right = getSymbolType(context->range_literal()->children[4]).value_or(GS1Parser::TOKEN_PIPE); + } + // Check for an early exit. + else if (right_vector == nullptr) + return std::make_any(false); + + bool range_met = true; + for (const auto& check : values) + { + if (right_range != nullptr) + { + double first = getReadOnlyGameValueFromAnyAs(right_range->first); + double second = getReadOnlyGameValueFromAnyAs(right_range->second); + bool test_left = false, test_right = false; + if (first < second) + { + test_left = (range_op_left == GS1Parser::TOKEN_PIPE) ? (first <= check) : (first < check); + test_right = (range_op_right == GS1Parser::TOKEN_PIPE) ? (check <= second) : (check < second); + } + else + { + test_left = (range_op_left == GS1Parser::TOKEN_PIPE) ? (first >= check) : (first > check); + test_right = (range_op_right == GS1Parser::TOKEN_PIPE) ? (check >= second) : (check > second); + } + bool in_range = test_left && test_right; + range_met = range_met && in_range; + } + else + { + range_met = range_met && (std::ranges::contains(*right_vector, check)); + } + + // Early out if we already know the result. + if (!range_met) + break; + } + + return std::make_any(range_met); +} + +std::any GS1Visitor::visitExpressionTernary(GS1Parser::ExpressionTernaryContext* context) +{ + if (context->children.size() == 1) + return visitChildren(context); + + std::any result = visit(context->logicalOrExpression()); + for (size_t i = 1; i < context->children.size(); i += 4) + { + if ((bool)getReadOnlyGameValueFromAny(result)) + result = std::move(visit(context->children[i + 1])); + else result = std::move(visit(context->children[i + 3])); + } + return result; +} + +std::any GS1Visitor::visitExpressionLogicOr(GS1Parser::ExpressionLogicOrContext* context) +{ + if (context->children.size() == 1) + return visitChildren(context); + + auto left = (bool)getReadOnlyGameValueFromAny(visit(context->logicalAndExpression(0))); + if (left) return std::make_any(true); + + for (size_t i = 2; i < context->children.size(); i += 2) + { + auto right = (bool)getReadOnlyGameValueFromAny(visit(context->children[i])); + if (right) return std::make_any(true); + } + + return std::make_any(false); +} + +std::any GS1Visitor::visitExpressionLogicAnd(GS1Parser::ExpressionLogicAndContext* context) +{ + if (context->children.size() == 1) + return visitChildren(context); + + auto left = (bool)getReadOnlyGameValueFromAny(visit(context->equalityExpression(0))); + if (!left) return std::make_any(false); + + for (size_t i = 2; i < context->children.size(); i += 2) + { + auto right = (bool)getReadOnlyGameValueFromAny(visit(context->children[i])); + if (!right) return std::make_any(false); + } + + return std::make_any(true); +} + +std::any GS1Visitor::visitExpressionEquality(GS1Parser::ExpressionEqualityContext* context) +{ + if (context->children.size() < 3) + return visitChildren(context); + + auto op = getSymbolType(context->children[1]); + if (!op.has_value()) + throw std::runtime_error("ExpressionEquality does not have an operator"); + + SetAndRestore sar{expectingTimeoutAsVariable, true}; + + auto left = getReadOnlyGameValueFromAny(visit(context->children[0])); + auto right = getReadOnlyGameValueFromAny(visit(context->children[2])); + + auto* left_vector = left.get_unsafe>(); + auto* right_vector = right.get_unsafe>(); + + // Vector equality checks. + if (left_vector != nullptr && right_vector != nullptr) + { + switch (op.value()) + { + case GS1Parser::OP_EQUAL: + case GS1Parser::OP_ASSIGN: + return std::make_any(*left_vector == *right_vector); + case GS1Parser::OP_NOTEQ: + return std::make_any(*left_vector != *right_vector); + } + } + + // Otherwise, we compare the doubles. + auto left_double = left.get().value_or(0.0); + auto right_double = right.get().value_or(0.0); + + // Do the comparison. + switch (op.value()) + { + case GS1Parser::OP_EQUAL: + case GS1Parser::OP_ASSIGN: + return std::make_any(DoublesAreSame(left_double, right_double)); + case GS1Parser::OP_NOTEQ: + return std::make_any(!DoublesAreSame(left_double, right_double)); + } + + throw std::runtime_error("ExpressionEquality has an unknown operator"); +} + +std::any GS1Visitor::visitExpressionRelational(GS1Parser::ExpressionRelationalContext* context) +{ + if (context->children.size() < 3) + return visitChildren(context); + + SetAndRestore sar{expectingTimeoutAsVariable, true}; + + auto op = getSymbolType(context->children[1]); + if (!op.has_value()) + throw std::runtime_error("ExpressionRelational does not have an operator"); + + auto left = getReadOnlyGameValueFromAnyAs(visit(context->children[0])); + auto right = getReadOnlyGameValueFromAnyAs(visit(context->children[2])); + + // Do the comparison. + switch (op.value()) + { + case GS1Parser::OP_LESS: + return std::make_any(left < right); + case GS1Parser::OP_GREAT: + return std::make_any(left > right); + case GS1Parser::OP_LESS_EQ: + return std::make_any(left <= right); + case GS1Parser::OP_GREAT_EQ: + return std::make_any(left >= right); + } + + throw std::runtime_error("ExpressionRelational has an unknown operator"); +} + +std::any GS1Visitor::visitExpressionAdditive(GS1Parser::ExpressionAdditiveContext* context) +{ + if (context->children.size() == 1) + return visitChildren(context); + + SetAndRestore sar{expectingTimeoutAsVariable, true}; + + double result = getReadOnlyGameValueFromAnyAs(visit(context->children[0])); + std::string literal; + for (size_t i = 1; i < context->children.size(); i += 2) + { + auto op = getSymbolType(context->children[i]); + if (!op.has_value()) + continue; + + // Check if the right side is a literal for a small optimization. + double right = 0.0; + auto child = context->children[i + 1]; + if (child->getTreeType() == antlr4::tree::ParseTreeType::TERMINAL) + { + literal = child->getText(); + if (literal == "true") right = 1.0; + else if (literal == "false") right = 0.0; + else right = string::toDouble(literal); + } + else + { + right = getReadOnlyGameValueFromAnyAs(visit(child)); + } + + if (op.value() == GS1Parser::OP_ADD) + result += right; + else if (op.value() == GS1Parser::OP_SUB) + result -= right; + } + + return std::make_any(result); +} + +std::any GS1Visitor::visitExpressionMultiplicative(GS1Parser::ExpressionMultiplicativeContext* context) +{ + if (context->children.size() == 1) + return visitChildren(context); + + SetAndRestore sar{expectingTimeoutAsVariable, true}; + + double result = getReadOnlyGameValueFromAnyAs(visit(context->children[0])); + std::string literal; + for (size_t i = 1; i < context->children.size(); i += 2) + { + auto op = getSymbolType(context->children[i]); + if (!op.has_value()) + continue; + + // Check if the right side is a literal for a small optimization. + double right = 0.0; + auto child = context->children[i + 1]; + if (child->getTreeType() == antlr4::tree::ParseTreeType::TERMINAL) + { + literal = child->getText(); + if (literal == "true") right = 1.0; + else if (literal == "false") right = 0.0; + else right = string::toDouble(literal); + } + else + { + right = getReadOnlyGameValueFromAnyAs(visit(child)); + } + + if (op.value() == GS1Parser::OP_MUL) + result *= right; + else if (op.value() == GS1Parser::OP_DIV) + result /= right; + else if (op.value() == GS1Parser::OP_MOD) + result = static_cast(static_cast(result) % static_cast(right)); + } + + return std::make_any(result); +} + +std::any GS1Visitor::visitExpressionExponentiation(GS1Parser::ExpressionExponentiationContext* context) +{ + if (context->children.size() == 1) + return visitChildren(context); + + SetAndRestore sar{expectingTimeoutAsVariable, true}; + + double result = getReadOnlyGameValueFromAnyAs(visit(context->children[0])); + for (size_t i = 1; i < context->children.size(); i += 2) + { + auto op = getSymbolType(context->children[i]); + if (!op.has_value()) + continue; + + auto right = getReadOnlyGameValueFromAnyAs(visit(context->children[i + 1])); + result = std::pow(result, right); + } + + return std::make_any(result); +} + +std::any GS1Visitor::visitExpressionUnary(GS1Parser::ExpressionUnaryContext* context) +{ + auto op = getSymbolType(context->children[0]); + if (!op.has_value()) + throw std::runtime_error("ExpressionUnary does not have an operator"); + + if (op.value() == GS1Parser::OP_LOGICALNOT) + return std::make_any(DoubleIsZero(getReadOnlyGameValueFromAnyAs(visit(context->unaryExpression())))); + + if (op.value() == GS1Parser::OP_SUB) + { + SetAndRestore sar{expectingTimeoutAsVariable, true}; + return std::make_any(-getReadOnlyGameValueFromAnyAs(visit(context->unaryExpression()))); + } + + return visit(context->unaryExpression()); +} + +std::any GS1Visitor::visitExpressionPostfix(GS1Parser::ExpressionPostfixContext* context) +{ + auto op = getSymbolType(context->children[1]); + if (!op.has_value()) + throw std::runtime_error("ExpressionPostfix has no operation"); + + SetAndRestore sar{expectingTimeoutAsVariable, true}; + + auto anyval = visit(context->children[0]); + auto left = getGameVariableFromAny(anyval); + auto value = left.first.get(left.second).value_or(0.0); + + // Perform the operation. + switch (op.value()) + { + case GS1Parser::OP_INC: + left.first.assign(value + 1.0, left.second); + break; + case GS1Parser::OP_DEC: + left.first.assign(value - 1.0, left.second); + break; + } + + // GS1 assignment operations are statements and can't be used inside expressions. + // So don't return anything. + return {}; +} + +//////////////////////////////////////////////////////////////////////////////// + +std::any GS1Visitor::visitBuiltInFunctionCall(GS1Parser::BuiltInFunctionCallContext* context) +{ + // Get the command. + auto command = context->FUNCTION()->getText(); + string::trimRightMutate(command); + + try + { + // Process the built-in function call. + return processBuiltInFunction(this, context, command); + } + catch (const std::exception& e) + { + reportError(e.what(), context); + } + return {}; +} + +std::any GS1Visitor::visitIdentifierAccess(GS1Parser::IdentifierAccessContext* context) +{ + auto first = visit(context->identifier_value(0)); + if (context->children.size() == 1) + { + // No accessors, just return the first identifier value. + return first; + } + + // The first identifier value should be a ScriptObject. + auto value = getGS1ScriptValueFromAny(first); + auto objectSource = getSourceFromGS1ScriptValue(value); + if (!objectSource.has_value()) + RECOVERABLE_PARSE_ERROR(std::format("Identifier did not contain a script object: {}.", context->children[0]->getText()), 0.0); + + size_t pos = 1; + size_t identifierCount = context->identifier_value().size(); + + // Iterate through the identifier values, adjusting our current source object as we go. + do + { + // Temporarily push the current source onto the stack and get the next identifier value. + // We don't need to keep it on the stack so pop it after we're done. + m_currentSource.push_back(objectSource.value()); + { + first = std::move(visit(context->identifier_value(pos++))); + } + m_currentSource.pop_back(); + + // Check if the result is a ScriptObject. + value = getGS1ScriptValueFromAny(first); + objectSource = getSourceFromGS1ScriptValue(value); + + // If not, we might be done. + if (!objectSource.has_value()) + { + if (pos > identifierCount) + throw std::runtime_error("IdentifierAccess has no valid identifier value."); + return std::make_any(std::move(value)); + } + } + while (pos < identifierCount); + + // If we made it here somehow, just return an empty GS1ScriptValue. + return std::make_any(0.0); +} + +std::any GS1Visitor::visitIdentifierValue(GS1Parser::IdentifierValueContext* context) +{ + auto identifier_any = visit(context->compound_identifier()); + auto* identifier = std::any_cast(&identifier_any); + if (identifier == nullptr) + throw std::runtime_error("IdentifierValue has no valid compound_identifier"); + + auto expressions = context->expression(); + std::optional index = std::nullopt; + + // Identify the storage type based on the identifier name. + auto storage = getStorageTypeFromIdentifier(*identifier); + + // Test for tiles[x,y]. + // Since tiles[x,y] is a unique case, we encode the index with the X/Y. + if (*identifier == "tiles" && expressions.size() == 2) + { + auto param1 = visit(expressions[0]); + auto param2 = visit(expressions[1]); + auto x = static_cast(std::max(0.0, getReadOnlyGameValueFromAnyAs(param1))); + auto y = static_cast(std::max(0.0, getReadOnlyGameValueFromAnyAs(param2))); + index = (static_cast(x) << 32) | y; + } + else if (expressions.size() == 1) + { + // Get the array index. + auto expression_any = visit(expressions[0]); + index = static_cast(getReadOnlyGameValueFromAnyAs(expression_any)); + } + + // If we have an identifier, and the flag store has a matching value, return that. + if (!identifier->empty() && flagStore.contains(*identifier)) + { + // Timeout is annoying, so make sure we are not doing something that needs the NPC timeout. + if (*identifier != "timeout" || !expectingTimeoutAsVariable) + { + if (auto flag = flagStore.get(*identifier).lock(); flag != nullptr) + return std::make_any(GameValue{flag->get().value_or(false)}); + } + } + + // Strip the storage type from the identifier, if needed. + stripStorageNameFromIdentifier(*identifier); + + // If we have no storage value, and we are expecting a flag, force client storage. + if (!storage.has_value() && expectingFlag) + storage = ENUM(StorageType::CLIENT); + + // Get the game variable store for the identifier. + // If there is no storage type, it pulls from the built-in variable store (saved on the script context). + auto variable = getGameValueFromStorage(*identifier, storage); + { + // If it is temp storage, make sure the variable is marked as temporary so it isn't saved. + if (storage.value_or(ENUM(StorageType::THIS)) == ENUM(StorageType::TEMP)) + variable.temporary = true; + + return std::make_any(std::make_pair(variable, index)); + } + + // Return a default value if the identifier is not found. + return std::make_any(GameValue{0.0}); +} + +std::any GS1Visitor::visitCompoundIdentifier(GS1Parser::CompoundIdentifierContext* context) +{ + std::string compoundIdentifier; + + // Temporarily turn off the flag expectation while we build the final identifier. + // This allows things like server.player_#v(playerid) to read from the correct storage area. + bool oldExpectingFlag = expectingFlag; + expectingFlag = false; + + for (auto& tree : context->children) + { + if (tree->getTreeType() == antlr4::tree::ParseTreeType::TERMINAL) + compoundIdentifier.append(tree->getText()); + else + { + auto piece = tree->accept(this); + compoundIdentifier.append(getReadOnlyGameValueFromAnyAs(piece)); + } + } + + expectingFlag = oldExpectingFlag; + + string::trimMutate(compoundIdentifier); + return std::make_any(compoundIdentifier); +} + +std::any GS1Visitor::visitCompoundString(GS1Parser::CompoundStringContext* context) +{ + std::string compoundString; + + for (auto& tree : context->children) + { + if (tree->getTreeType() == antlr4::tree::ParseTreeType::TERMINAL) + compoundString.append(tree->getText()); + else + { + auto piece = tree->accept(this); + if (auto* gs1Val = std::any_cast(&piece); gs1Val != nullptr) + { + // If this is a GS1GameVariable and the results size is 1, just return the piece. + if (auto* gs1GameVariable = std::get_if(gs1Val); gs1GameVariable != nullptr && context->children.size() == 1) + return piece; + } + compoundString.append(getReadOnlyGameValueFromAnyAs(piece)); + } + } + + string::trimMutate(compoundString); + return std::make_any(compoundString); +} + +std::any GS1Visitor::visitMessageCode(GS1Parser::MessageCodeContext* context) +{ + auto results = visitChildrenAndCollect(context); + auto messageCode = context->MESSAGECODE()->getText(); + if (messageCode.empty()) + RECOVERABLE_PARSE_ERROR(std::format("Message code '{}' is not a valid message code.", messageCode), ""s); + + // Trim out the message code. + std::string_view messageCodeView{messageCode}; + if (messageCodeView.ends_with('(')) + messageCodeView.remove_suffix(1); + if (messageCodeView.starts_with('#')) + messageCodeView.remove_prefix(1); + + try + { + // Process the message code. + return processMessageCode(this, context, messageCodeView); + } + catch (const unimplemented_error& e) + { + reportError(e.what(), context, false); + return GS1ScriptValue{""s}; + } + catch (const std::logic_error& e) + { + reportError(e.what(), context, false); + return GS1ScriptValue{""s}; + } + catch (const std::exception& e) + { + reportError(e.what(), context); + } + return {}; +} + +//////////////////////////////////////////////////////////////////////////////// + +std::any GS1Visitor::visitFlowReturn(GS1Parser::FlowReturnContext* context) +{ + throw return_exception{}; +} + +std::any GS1Visitor::visitFlowBreak(GS1Parser::FlowBreakContext* context) +{ + throw break_exception{}; +} + +std::any GS1Visitor::visitFlowContinue(GS1Parser::FlowContinueContext* context) +{ + throw continue_exception(); +} + +//////////////////////////////////////////////////////////////////////////////// + +std::any GS1Visitor::visitLiteral(GS1Parser::LiteralContext* context) +{ + if (context->LITERAL() != nullptr) + { + auto text = context->LITERAL()->getText(); + if (text == "true") return std::make_any(true); + if (text == "false") return std::make_any(false); + return std::make_any(std::stod(text)); + } + else if (context->ALLFEATURES() != nullptr) + return std::make_any(static_cast(0xFFFF)); + else if (context->ALLSTATS() != nullptr) + return std::make_any(static_cast(0xFFFF)); + + return 0.0; +} + +std::any GS1Visitor::visitRangeLiteral(GS1Parser::RangeLiteralContext* context) +{ + auto left = visit(context->expression(0)); + auto right = visit(context->expression(1)); + return std::make_pair(std::move(left), std::move(right)); +} + +std::any GS1Visitor::visitArrayLiteral(GS1Parser::ArrayLiteralContext* context) +{ + std::vector values; + + size_t valueIndex = 0; + for (size_t i = 0; i < context->children.size(); ++i) + { + auto child = context->children[i]; + if (auto symbol = getSymbolType(child); symbol.has_value()) + { + if (*symbol == GS1Parser::TOKEN_COMMA) + { + ++valueIndex; + if (valueIndex > values.size()) + values.push_back(0.0); + } + } + else + { + auto result = child->accept(this); + values.push_back(getReadOnlyGameValueFromAnyAs(result)); + } + } + + // This covers the case of {} and {1,}, which should result in {0} and {1,0}, respectively. + if (valueIndex == values.size()) + values.push_back(0.0); + + return std::make_any(std::move(values)); +} + +std::any GS1Visitor::visitItemLiteral(GS1Parser::ItemLiteralContext* context) +{ + auto text = context->ITEM()->getText(); + auto it = std::ranges::find(ItemNames, text); + if (it == ItemNames.end()) + it = ItemNames.begin(); + + return std::make_any(static_cast(std::distance(ItemNames.begin(), it))); +} + +std::any GS1Visitor::visitCarryLiteral(GS1Parser::CarryLiteralContext* context) +{ + auto text = context->CARRY()->getText(); + auto it = std::ranges::find(carryNames, text); + if (it == carryNames.end()) + it = carryNames.begin(); + + return std::make_any(static_cast(std::distance(carryNames.begin(), it))); +} + +std::any GS1Visitor::visitDirectionLiteral(GS1Parser::DirectionLiteralContext* context) +{ + ptrdiff_t index = 0; + auto text = context->DIRECTION()->getText(); + if (auto it = std::ranges::find(directionNames, text); it != directionNames.end()) + index = std::distance(directionNames.begin(), it); + else + { + index = static_cast(string::toNumber(text)); + index = index % 4; + } + + return std::make_any(static_cast(index)); +} + +std::any GS1Visitor::visitGenderLiteral(GS1Parser::GenderLiteralContext* context) +{ + auto text = context->GENDER()->getText(); + auto it = std::ranges::find(genderNames, text); + if (it == genderNames.end()) + it = genderNames.begin(); + + return std::make_any(static_cast(std::distance(genderNames.begin(), it))); +} + +std::any GS1Visitor::visitColorLiteral(GS1Parser::ColorLiteralContext* context) +{ + return std::make_any(getColorValueFromString(context->COLOR()->getText())); +} + +std::any GS1Visitor::visitBaddyLiteral(GS1Parser::BaddyLiteralContext* context) +{ + auto text = context->BADDY()->getText(); + auto it = std::ranges::find(BaddyNames, text); + if (it == BaddyNames.end()) + it = BaddyNames.begin(); + + return std::make_any(static_cast(std::distance(BaddyNames.begin(), it))); +} + +std::any GS1Visitor::visitPrimaryExpression(GS1Parser::PrimaryExpressionContext* context) +{ + if (context->children.size() == 1) + return visitChildren(context); + + if (auto expression = context->expression(); expression != nullptr) + return visit(expression); + + if (auto messageCode = context->messagecode_string(); messageCode != nullptr) + return visit(messageCode); + + throw std::runtime_error("primaryExpression was unhandled"); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1::grammar diff --git a/server/src/scripting/gs1/ScriptEngineGS1.cpp b/server/src/scripting/gs1/ScriptEngineGS1.cpp new file mode 100644 index 000000000..ba58aaa5a --- /dev/null +++ b/server/src/scripting/gs1/ScriptEngineGS1.cpp @@ -0,0 +1,506 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace preagonal::gs1::grammar; + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs1 +{ +//////////////////////////////////////////////////////////////////////////////// + +static std::string determineEventName(ScriptEvent& event) +{ + auto knownEventIter = eventFlagMap.find(event.type); + if (knownEventIter != eventFlagMap.end()) + return std::string{ knownEventIter->second }; + + if (event.type == ScriptEventType::CUSTOM || event.type == ScriptEventType::TRIGGERACTION) + { + std::string action; + if (auto* actionStr = std::any_cast(&event.args[0]); actionStr != nullptr) + action = *actionStr; + else if (auto* actionStr = std::any_cast(&event.args[0]); actionStr != nullptr) + action = *actionStr; + else if (auto* actionStr = std::any_cast(&event.args[0]); actionStr != nullptr) + action = std::string(*actionStr); + + return std::format("{}{}", (event.type == ScriptEventType::TRIGGERACTION ? "action" : ""), action); + } + + return {}; +} + +//////////////////////////////////////////////////////////////////////////////// + +PlayerPtr getPlayerFromSource(const ScriptObject& source, std::optional index) +{ + if (source.second != ScriptObjectType::PLAYER) + return nullptr; + + // TODO: Current player. + if (index.value_or(0) == -1) + return nullptr; + + auto* server = BabyDI::Get(); + if (auto player = server->getPlayer(source.first); player != nullptr) + { + if (index.has_value() && index.value() >= 0) + { + if (auto level = server->getLoadedLevel(player->account.level, player); level != nullptr && index.value() < (int64_t)level->getPlayers().size()) + { + auto& mapPlayers = level->getPlayers(); + player = server->getPlayer(mapPlayers[index.value()]); + } + } + return player; + } + + return nullptr; +} + +PlayerClientPtr getPlayerClientFromSource(const ScriptObject& source, std::optional index) +{ + auto player = getPlayerFromSource(source, index); + if (auto client = std::dynamic_pointer_cast(player); client != nullptr) + return client; + return nullptr; +} + +NPCPtr getNPCFromSource(const ScriptObject& source, std::optional index) +{ + if (source.second != ScriptObjectType::NPC) + return nullptr; + auto* server = BabyDI::Get(); + if (auto npc = server->getNPC(source.first); npc != nullptr) + { + if (index.has_value() && index.value() >= 0) + { + if (auto level = npc->getLevel(); level != nullptr && index.value() < (int64_t)level->getNPCs().size()) + { + auto& mapNPCs = level->getNPCs(); + auto iter = mapNPCs.begin(); + std::ranges::advance(iter, index.value(), mapNPCs.end()); + if (iter != mapNPCs.end()) + npc = server->getNPC(*iter); + } + } + return npc; + } + return nullptr; +} + +PlayerOrNPC getPlayerOrNPCFromSource(const ScriptObject& source, std::optional index) +{ + if (source.second == ScriptObjectType::SERVER) + return std::nullopt; + + if (source.second == ScriptObjectType::PLAYER) + return getPlayerFromSource(source, index); + else if (source.second == ScriptObjectType::NPC) + return getNPCFromSource(source, index); + + return std::nullopt; +} + +Character* getCharacterFromSource(const ScriptObject& source, std::optional index) +{ + if (source.second == ScriptObjectType::SERVER) + return nullptr; + + if (source.second == ScriptObjectType::PLAYER) + { + if (auto player = getPlayerFromSource(source, index); player != nullptr) + return &player->account.character; + } + else if (source.second == ScriptObjectType::NPC) + { + if (auto npc = getNPCFromSource(source, index); npc != nullptr) + return &npc->character; + } + + return nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// + +GS1ScriptWrapper::GS1ScriptWrapper(std::string_view who, std::string_view script) +{ + errorListenerLexer = std::make_shared("lexing", who); + errorListenerParser = std::make_shared("parsing", who); + + // Load the script (lenient UTF-8 parsing). + input = std::make_shared(); + input->load(script.data(), script.length(), true); + + // Create the lexer. + // We don't need to keep this around. + GS1Lexer lexer{ input.get() }; + lexer.removeErrorListeners(); + lexer.addErrorListener(errorListenerLexer.get()); + + // Fill the tokens from the lexer. + tokens = std::make_shared(&lexer); + tokens->fill(); + + // Create the parser. + parser = std::make_shared(tokens.get()); + parser->removeErrorListeners(); + parser->addErrorListener(errorListenerParser.get()); + + // Run the parser and create our AST. + visitor = std::make_shared(); + program = parser->program(); + +#ifdef DEBUG + //if (who == "MoveTester") + if (false) + { + log::printLine(log::script, program->toStringTree(parser.get(), true)); + } +#endif + + setGlobalVariables(variables); +} + +//////////////////////////////////////////////////////////////////////////////// + +ScriptEngineGS1::ScriptEngineGS1() +{ +} + +CompiledScriptResult ScriptEngineGS1::compileScript(std::string_view who, std::string_view script) +{ + ScriptExecutionContext result{ .engine = this }; + try + { + result.script = std::make_shared(std::in_place_type, who, script); + } + catch (const std::exception& ex) + { + log::printLine(log::script, "GS1 script compilation SUPER failed: {}", ex.what()); + } + + return result; +} + +//---------------------------- + +bool ScriptEngineGS1::prepare(GS1ScriptWrapper& wrapper, ScriptEvent& event, ScriptObject source, CompiledScriptResultPtr context, NPCPtr& npc, LevelPtr& level) +{ + auto& [source_id, source_type] = source; + if (source_type != ScriptObjectType::NPC && source_type != ScriptObjectType::WEAPON) + throw std::invalid_argument("GS1 scripts can only be executed from NPCs and weapons."); + + auto server = BabyDI::Get(); + PlayerClientPtr player = nullptr; + WeaponPtr weapon = nullptr; + + // Get whatever links we can. + if (source_type == ScriptObjectType::PLAYER) + player = server->getPlayer(source_id); + if (source_type == ScriptObjectType::NPC) + npc = server->getNPC(source_id); + if (source_type == ScriptObjectType::WEAPON) + { + if (auto it = server->getWeaponList().find(source_id); it != server->getWeaponList().end()) + weapon = it->second; + } + if (player != nullptr) + level = player->getLevel(); + if (npc != nullptr) + level = npc->getLevel(); + + // Try to get variables from the initiator now. + if (player == nullptr && event.initiator.second == ScriptObjectType::PLAYER) + player = server->getPlayer(event.initiator.first); + if (npc == nullptr && event.initiator.second == ScriptObjectType::NPC) + npc = server->getNPC(event.initiator.first); + if (level == nullptr) + level = (player != nullptr ? player->getLevel() : (npc != nullptr ? npc->getLevel() : nullptr)); + + // Determine the "who" for error messages. + if (npc != nullptr) + wrapper.visitor->who = npc->name; + else if (weapon != nullptr) + wrapper.visitor->who = weapon->name; + else if (player != nullptr) + wrapper.visitor->who = player->account.name; + else + wrapper.visitor->who = "unknown"; + + // Set the built-in store. + wrapper.visitor->builtInStore = &wrapper.variables; + + // Set events. + setTriggerActionAndCustomEventFlags(event, wrapper.visitor->flagStore); + setEventFlags(event.type, wrapper.visitor->flagStore); + + // Set flags. + setPlayerFlags(wrapper.variables, npc, player); + setNPCFlags(event, wrapper.variables, npc); + setLevelFlags(wrapper.variables, npc, level); + setWeaponFlags(event, source, wrapper.variables); + setOtherFlags(event, source, wrapper.variables, npc, player, level); + + // Set variables. + setNPCVariables(wrapper.variables, npc); + setPlayerVariables(wrapper.variables, player); + setLevelVariables(wrapper.variables, level); + setOtherVariables(wrapper.variables, event); + + return true; +} + +//---------------------------- + +bool ScriptEngineGS1::execute(ScriptEvent& event, ScriptObject source, CompiledScriptResultPtr context) +{ + auto* wrapper = std::any_cast(context->script.get()); + if (wrapper == nullptr) + return false; + + auto* server = BabyDI::Get(); + NPCPtr npc = nullptr; + LevelPtr level = nullptr; + +#if !defined(DEBUG) || 1 + // If the event is not in the NPC script, don't bother executing it. + if (event.type != ScriptEventType::CREATED && event.type != ScriptEventType::INITIALIZED) + { + const auto& eventName = determineEventName(event); + if (!wrapper->parser->identifiers.contains(eventName) && !server->cached.runAllScriptEvents.getValue()) + return false; + } +#endif + + if (!prepare(*wrapper, event, source, context, npc, level)) + return false; + +#if defined(DEBUG) && 0 + // Log some testing stuff. + if (event.type != ScriptEventType::CREATED && event.type != ScriptEventType::INITIALIZED) + { + const auto& eventName = determineEventName(event); + if (!wrapper->parser->identifiers.contains(eventName) && !server->cached.runAllScriptEvents.getValue()) + { + log::printLine(log::script, "GS1 script for event '{}' not found in script '{}'.", eventName, wrapper->visitor->who); + return false; + } + } +#endif + + // If this is a control-NPC, temporarily adjust the level it lives in. + bool isControlNPC = npc && npc->scriptType == NPCTYPE_CONTROL; + if (isControlNPC) + { + if (level == nullptr && event.initiator.second == ScriptObjectType::NPC) + { + if (auto initiatingNPC = server->getNPC(event.initiator.first); initiatingNPC != nullptr) + level = initiatingNPC->getLevel(); + } + if (level != nullptr) + npc->level = level->levelName; + } + + try + { + // Execute the script. + wrapper->visitor->execute(event, source, *wrapper->parser.get(), *context, wrapper->program); + } + catch (std::exception& e) + { +#ifdef DEBUG + log::printLine(log::script, "Script execution failure: {}", e.what()); + log::printLine(log::script, wrapper->program->toStringTree(wrapper->parser.get(), true)); + throw; +#endif + // If we had a terminal error, remove the script from the context so it doesn't get executed again. + context->script = nullptr; + } + + // Fix the control-NPC level. + if (isControlNPC) + npc->level.clear(); + + // Special case to handle "created" events for the NPC. + if (npc != nullptr && event.type == ScriptEventType::CREATED) + npc->setPropWith(SetBy::SERVER, static_cast(npc->visFlags | PROPID(NPCVisFlags::CREATED))); + + cleanup(*wrapper); + return true; +} + +bool ScriptEngineGS1::executeFunction(std::string_view function, ScriptEvent& event, ScriptObject source, CompiledScriptResultPtr context) +{ + if (context == nullptr) + return false; + + auto* wrapper = std::any_cast(context->script.get()); + if (wrapper == nullptr) + return false; + + // Check if we have the function. + auto userFunction = wrapper->parser->userFunctions.find(std::string{ function }); + if (userFunction == wrapper->parser->userFunctions.end()) + return false; + + NPCPtr npc = nullptr; + LevelPtr level = nullptr; + + if (!prepare(*wrapper, event, source, context, npc, level)) + return false; + + try + { + // Execute the script. + wrapper->visitor->execute(event, source, *wrapper->parser.get(), *context, userFunction->second); + } + catch (std::exception& e) + { +#ifdef DEBUG + log::printLine(log::script, "Script execution failure: {}", e.what()); + log::printLine(log::script, wrapper->program->toStringTree(wrapper->parser.get(), true)); + throw; +#endif + // If we had a terminal error, remove the script from the context so it doesn't get executed again. + context->script = nullptr; + } + + cleanup(*wrapper); + return true; +} + +//---------------------------- + +double ScriptEngineGS1::processMathExpression(std::string_view expression, ScriptObject source) +{ + static GS1Visitor visitor{}; + static GameVariableStore variableStore{}; + visitor.builtInStore = &variableStore; + + visitor.pushSource(source); + visitor.flagStore.store.clear(); + variableStore.store.clear(); + + ScriptEvent created{.type = ScriptEventType::CREATED, .initiator = source}; + + auto server = BabyDI::Get(); + if (source.second == ScriptObjectType::NPC) + { + if (auto npc = server->getNPC(source.first); npc != nullptr) + { + setNPCFlags(created, visitor.flagStore, npc); + setNPCVariables(variableStore, npc); + } + } + else if (source.second == ScriptObjectType::PLAYER) + { + if (auto player = server->getPlayer(source.first); player != nullptr) + { + setPlayerFlags(visitor.flagStore, nullptr, player); + setPlayerVariables(variableStore, player); + } + } + + try + { + auto result = visitor.processMathExpression(expression); + visitor.popSource(); + return visitor.getGameValueAs(result); + } + catch (std::exception& e) + { + visitor.popSource(); + } + + return 0.0; +} + +std::string ScriptEngineGS1::processStringExpression(std::string_view expression, ScriptObject source) +{ + static GS1Visitor visitor{}; + static GameVariableStore variableStore{}; + visitor.builtInStore = &variableStore; + + visitor.pushSource(source); + visitor.flagStore.store.clear(); + + ScriptEvent created{.type = ScriptEventType::CREATED, .initiator = source}; + + auto server = BabyDI::Get(); + if (source.second == ScriptObjectType::NPC) + { + if (auto npc = server->getNPC(source.first); npc != nullptr) + { + setNPCFlags(created, visitor.flagStore, npc); + setNPCVariables(variableStore, npc); + } + } + else if (source.second == ScriptObjectType::PLAYER) + { + if (auto player = server->getPlayer(source.first); player != nullptr) + { + setPlayerFlags(visitor.flagStore, nullptr, player); + setPlayerVariables(variableStore, player); + } + } + + try + { + auto result = visitor.processStringExpression(expression); + visitor.popSource(); + return visitor.getGameValueAs(result); + } + catch (std::exception& e) + { + visitor.popSource(); + } + + return std::string{}; +} + +//---------------------------- + +void ScriptEngineGS1::cleanup(GS1ScriptWrapper& wrapper) +{ + // Clear the variables (to clear reference counted pointers, just in case). + wrapper.variables.clearTemporary(); + wrapper.visitor->flagStore.clearTemporary(); +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs1 diff --git a/server/src/scripting/gs1/grammar/GS1Lexer.g4 b/server/src/scripting/gs1/grammar/GS1Lexer.g4 new file mode 100644 index 000000000..bb3a677dd --- /dev/null +++ b/server/src/scripting/gs1/grammar/GS1Lexer.g4 @@ -0,0 +1,1291 @@ +lexer grammar GS1Lexer; + +@lexer::header +{ +// -------------------------------------------------------- +#include +#include +#include +#include +#include +// -------------------------------------------------------- +} + +@lexer::context +{ +// -------------------------------------------------------- +constexpr size_t builtInCommandCount = 206 +#if DEBUG + + 1 +#endif + ; +constexpr std::array builtInCommands = { +#if DEBUG + "debugger", +#endif + "addguildmember", + "addstring", + "addtiledef ", + "addtiledef2", + "addweapon", + "attachplayertoobj", + "blockagain", + "blockagainlocal", + "callnpc", + "callweapon", + "canbecarried", + "canbepulled", + "canbepushed", + "cannotbecarried", + "cannotbepulled", + "cannotbepushed", + "cannotwarp", + "canwarp", + "canwarp2", + "carryobject", + "changeimgcolors", + "changeimgmode", + "changeimgpart", + "changeimgvis", + "changeimgzoom", + "copylevel", + "copystrings", + "deletelevel", + "deletestring", + "destroy", + "detachplayer", + "disabledefmovement", + "disablemap", + "disablepause", + "disableselectweapons", + "disableweapons", + "dontblock", + "dontblocklocal", + "drawaslight", + "drawoverplayer", + "drawovertrees", + "drawunderplayer", + "enabledefmovement", + "enablefeatures", + "enablemap", + "enablepause", + "enableselectweapons", + "enableweapons", + "explodebomb", + "followplayer", + "freezeplayer", + "freezeplayer2", + "hide", + "hideimg", + "hideimgs", + "hidelocal", + "hideplayer", + "hidesword", + "hitcompu", + "hitnpc", + "hitobjects", + "hitplayer", + "hurt ", + "insertstring", + "join", + "lay ", + "lay2", + "loadmap", + "message", + "move ", + "noplayerkilling", + "noplayeronwall", + "openurl ", + "openurl2 ", + "play ", + "play2 ", + "playlooped", + "putbomb", + "putcomp", + "putexplosion ", + "putexplosion2", + "puthorse", + "putleaps", + "putnewcomp", + "putnpc", + "putnpc2", + "putobject", + "reflectarrow", + "removearrow", + "removebomb", + "removecompus", + "removeexplo", + "removeguild", + "removeguildmember", + "removehorse", + "removeitem", + "removestring", + "removetiledefs", + "removeweapon", + "replaceani", + "replacestring", + "resetfocus", + "saveinfo", + "savelog", + "savelog2", + "say ", + "say2", + "sendpm", + "sendrpgmessage", + "sendtonc", + "sendtorc", + "serverwarp", + "set ", + "setani", + "setarray", + "setbackpal", + "setbacktile", + "setbacktile2", + "setbeltcolor", + "setbody", + "setbow", + "setcharani", + "setchargender", + "setcharprop", + "setcoatcolor", + "setcoloreffect", + "setcursor ", + "setcursor2", + "seteffect ", + "seteffectmode", + "setfocus", + "setgender", + "setgif ", + "setgifpart", + "sethead", + "setimg ", + "setimgpart", + "setletters", + "setlevel ", + "setlevel2", + "setmap", + "setminimap", + "setmusicvolume", + "setplayerdir", + "setplayerprop", + "setpm", + "setshape ", + "setshape2", + "setshield", + "setshoecolor", + "setshootparams ", + "setskincolor", + "setsleevecolor", + "setspritesimage", + "setstatusimage", + "setstring", + "setsword", + "seturllevel", + "setz ", + "setzoomeffect", + "shoot ", + "shootarrow", + "shootball", + "shootfireball", + "shootfireblast", + "shootnuke", + "show", + "showani", + "showani2", + "showcharacter", + "showfile", + "showimg", + "showimg2", + "showlocal", + "showpoly", + "showpoly2", + "showstats", + "showtext", + "showtext2", + "sleep", + "spyfire", + "stopmidi", + "stopsound", + "take ", + "take2", + "takehorse", + "takeplayercarry", + "takeplayerhorse", + "throwcarry", + "timereverywhere", + "timershow", + "toinventory", + "tokenize ", + "tokenize2", + "toweapons", + "triggeraction", + "unfreezeplayer", + "unset ", + "updateboard ", + "updateboard2 ", + "updateterrain", + "warpto", + "wraptext ", + "wraptext2 ", + + // GR extensions + "enabledamagereactions", + "disabledamagereactions", +}; + +constexpr bool isBuiltInCommand(std::string_view name) +{ + for (const auto& builtIn : builtInCommands) + { + if (name.starts_with(builtIn)) + return true; + } + return false; +} +// -------------------------------------------------------- +} + +@lexer::members +{ +// -------------------------------------------------------- +// Allow injected tokens. +virtual std::unique_ptr nextToken() override +{ + // If we have a before token queued, return it. + if (!m_pendingTokensBefore.empty()) + { + auto token = std::move(m_pendingTokensBefore.front()); + m_pendingTokensBefore.pop_front(); + return token; + } + + // Check for any after tokens now. + if (!m_pendingTokensAfter.empty()) + { + auto token = std::move(m_pendingTokensAfter.front()); + m_pendingTokensAfter.pop_front(); + return token; + } + + // Get the next token. + auto next = antlr4::Lexer::nextToken(); +#if DEBUG + auto text = next->getText(); +#endif + + // If we have before tokens queued, save this one for later and return the queued token. + if (!m_pendingTokensBefore.empty()) + { + auto token = std::move(m_pendingTokensBefore.front()); + m_pendingTokensBefore.pop_front(); + m_pendingTokensBefore.push_front(std::move(next)); + return token; + } + + // Return our token. + return next; +} + +void emitIdentifierBefore(size_t type, std::string_view name) +{ + m_pendingTokensBefore.emplace_back(_factory->create(type, "")); +} + +void emitIdentifierAfter(size_t type, std::string_view name) +{ + m_pendingTokensAfter.emplace_back(_factory->create(type, "")); +} + +int breakpoint() +{ + return _input->index(); +} + +enum POPMODE +{ + POPMODE_COMMAND, + POPMODE_FUNCTION, + POPMODE_ARRAYINDEX +}; + +struct CommandState +{ + std::string_view arguments; + POPMODE popMode; + bool commaPop = true; +}; + +bool canFuncPop() const +{ + return !m_commandStates.empty() && m_commandStates.back().popMode == POPMODE_FUNCTION; +} + +bool canCmdPop() const +{ + return m_commandStates.empty() || m_commandStates.back().popMode == POPMODE_COMMAND; +} + +bool canArrayPop() const +{ + return !m_commandStates.empty() && m_commandStates.back().popMode == POPMODE_ARRAYINDEX; +} + +bool canCommaPop() const +{ + return m_commandStates.empty() || m_commandStates.back().commaPop; +} + +bool isNextArgLeftParen() const +{ + return !m_commandStates.empty() && !m_commandStates.back().arguments.empty() && m_commandStates.back().arguments.front() == '('; +} + +bool isNotDefaultMode() const +{ + return !m_commandStates.empty(); +} + +void pushCommand(std::string_view arguments) +{ + m_commandStates.emplace_back(CommandState{ arguments, POPMODE_COMMAND, true }); + pushMode(IN_PARAM_1); // Just a dummy state that gets immediately cleared. + popNextMode(); +} + +void pushArrayAccess() +{ + m_commandStates.emplace_back(CommandState{ "P", POPMODE_ARRAYINDEX, false }); + pushMode(IN_PARAM_1); // Just a dummy state that gets immediately cleared. + popNextMode(); +} + +void checkIfNextModeOptional() +{ + popNextMode(); + + // Look ahead for comma and right parenthesis. + bool skipNext = false; + try + { + size_t symbol = EOF; + size_t index = 1; + while ((symbol = _input->LA(index++)) != EOF) + { + // If we reached the end, we didn't have a comma, so skip the next mode. + if (symbol == ')') + { + skipNext = true; + break; + } + + // We found a comma, so we can stop looking. + if (symbol == ',') + break; + } + } + catch (...) + { + // Who cares. + } + + // If we are skipping the next mode, do that now. + if (skipNext) + popNextMode(); +} + +void popNextMode(bool terminateEarly = false) +{ + if (m_commandStates.empty()) return; + auto& currentState = m_commandStates.back(); + + if (terminateEarly) currentState.arguments = {}; + if (currentState.arguments.empty()) + { + popMode(); + m_commandStates.pop_back(); + } + else + { + auto mode = currentState.arguments.front(); + currentState.arguments.remove_prefix(1); + + // Last string? Commas are included! + if ((mode == 'S' || mode == 'R') && (currentState.arguments.empty() || currentState.arguments.front() == ')')) + currentState.commaPop = false; + + switch (mode) + { + case 'V': setMode(IN_PARAM_V); emitIdentifierAfter(GS1Lexer::IDENTIFIER, getText()); break; + case 'E': setMode(IN_PARAM_E); break; + case 'P': setMode(IN_PARAM_E); currentState.commaPop = false; break; + case 'S': setMode(IN_PARAM_S); emitIdentifierAfter(GS1Lexer::STRING, getText()); break; + case 'R': setMode(IN_PARAM_R); emitIdentifierAfter(GS1Lexer::STRING, getText()); break; + case 'L': setMode(IN_PARAM_L); emitIdentifierAfter(GS1Lexer::STRING, getText()); break; + case 'M': setMode(IN_PARAM_M); emitIdentifierAfter(GS1Lexer::RAWMESSAGECODE, getText()); break; + case 'B': setMode(IN_PARAM_B); break; + case 'I': setMode(IN_PARAM_I); break; + case 'C': setMode(IN_PARAM_C); break; + case 'G': setMode(IN_PARAM_G); break; + case 'U': setMode(IN_PARAM_U); break; + case 'D': setMode(IN_PARAM_D); break; + case 'X': setMode(IN_PARAM_X); break; + case 'Z': setMode(IN_PARAM_Z); break; + case '(': setMode(IN_PARAM_1); break; + case ')': setMode(IN_PARAM_2); break; + case '<': setMode(IN_PARAM_3); break; + default: setMode(DEFAULT_MODE); break; + } + } +} + +size_t m_braceCount = 0; +std::deque m_commandStates; + +std::deque> m_pendingTokensBefore{}; +std::deque> m_pendingTokensAfter{}; +// -------------------------------------------------------- +} + +tokens { COMMAND, FUNCTION, MESSAGECODE, RAWMESSAGECODE, STRING, BADDY, ITEM, COLOR, GENDER, CARRY, DIRECTION } + +/* + Mode parameter argument guide: + - V variable (number/array/string) + - E expression (variable + math) + - P parameters (multiple expressions) + - S string + - R raw string (string that doesn't process message codes) + - L variable length comma-separated string list + - M message code + - B baddy name + - I item name + - C color name + - G gender name + - U carry item name + - D direction name or number + - X storage special case + - Z code (putnpc2 special case) + - ( left parenthesis + - ) right parenthesis + - < left parenthesis that tests if a comma is found before the ) and, if not, skips the next mode (playersays special case) +*/ + +CMD_DEBUGGER : 'gr-debugger' -> type(COMMAND); +CMD_SETSTRING : 'setstring' { pushCommand("VS"); } -> type(COMMAND); +CMD_ADDSTRING : 'addstring' { pushCommand("VS"); } -> type(COMMAND); +CMD_INSERTSTRING : 'insertstring' { pushCommand("VES"); } -> type(COMMAND); +CMD_REPLACESTRING : 'replacestring' { pushCommand("VES"); } -> type(COMMAND); +CMD_REMOVESTRING : 'removestring' { pushCommand("VS"); } -> type(COMMAND); +CMD_DELETESTRING : 'deletestring' { pushCommand("VE"); } -> type(COMMAND); +CMD_SET : 'set ' { pushCommand("V"); } -> type(COMMAND); +CMD_UNSET : 'unset ' { pushCommand("V"); } -> type(COMMAND); +CMD_SLEEP : 'sleep' { pushCommand("E"); } -> type(COMMAND); +CMD_SETARRAY : 'setarray' { pushCommand("VE"); } -> type(COMMAND); +CMD_TIMEREVERYWHERE : 'timereverywhere' -> type(COMMAND); +CMD_SETGIF : 'setgif ' { pushCommand("S"); } -> type(COMMAND); +CMD_SETIMG : 'setimg' { pushCommand("S"); } -> type(COMMAND); +CMD_SETIMGPART : 'setimgpart' { pushCommand("SEEEE"); } -> type(COMMAND); +CMD_HIDE : 'hide' -> type(COMMAND); +CMD_SHOW : 'show' -> type(COMMAND); +CMD_DONTBLOCK : 'dontblock' -> type(COMMAND); +CMD_BLOCKAGAIN : 'blockagain' -> type(COMMAND); +CMD_DRAWOVERPLAYER : 'drawoverplayer' -> type(COMMAND); +CMD_DRAWOVERTREES : 'drawovertrees' -> type(COMMAND); +CMD_DRAWUNDERPLAYER : 'drawunderplayer' -> type(COMMAND); +CMD_DRAWASLIGHT : 'drawaslight' -> type(COMMAND); +CMD_SETEFFECTMODE : 'seteffectmode ' { pushCommand("EEEE"); } -> type(COMMAND); +CMD_CANBECARRIED : 'canbecarried' -> type(COMMAND); +CMD_CANNOTBECARRIED : 'cannotbecarried' -> type(COMMAND); +CMD_CANBEPUSHED : 'canbepushed' -> type(COMMAND); +CMD_CANNOTBEPUSHED : 'cannotbepushed' -> type(COMMAND); +CMD_CANBEPULLED : 'canbepulled' -> type(COMMAND); +CMD_CANNOTBEPULLED : 'cannotbepulled' -> type(COMMAND); +CMD_MOVE : 'move ' { pushCommand("EEEE"); } -> type(COMMAND); +CMD_SAY : 'say ' { pushCommand("E"); } -> type(COMMAND); +CMD_SAY2 : 'say2' { pushCommand("R"); } -> type(COMMAND); +CMD_LAY : 'lay ' { pushCommand("I"); } -> type(COMMAND); +CMD_LAY2 : 'lay2' { pushCommand("IEE"); } -> type(COMMAND); +CMD_TAKE : 'take ' { pushCommand("I"); } -> type(COMMAND); +CMD_TAKE2 : 'take2' { pushCommand("E"); } -> type(COMMAND); +CMD_MESSAGE : 'message' { pushCommand("S"); } -> type(COMMAND); +CMD_TIMERSHOW : 'timershow' -> type(COMMAND); +CMD_SHOWCHARACTER : 'showcharacter' -> type(COMMAND); +CMD_SETCHARPROP : 'setcharprop' { pushCommand("MS"); } -> type(COMMAND); +CMD_SETCHARANI : 'setcharani' { pushCommand("S"); } -> type(COMMAND); +CMD_SETCHARGENDER : 'setchargender' { pushCommand("G"); } -> type(COMMAND); +CMD_TRIGGERACTION : 'triggeraction' { pushCommand("EESL"); } -> type(COMMAND); +CMD_PUTNPC : 'putnpc' { pushCommand("SSEE"); } -> type(COMMAND); +CMD_PUTNPC2 : 'putnpc2' { pushCommand("EEZ"); } -> type(COMMAND); +CMD_CALLNPC : 'callnpc' { pushCommand("ES"); } -> type(COMMAND); +CMD_CALLWEAPON : 'callweapon' { pushCommand("ESS"); } -> type(COMMAND); +CMD_DESTROY : 'destroy' -> type(COMMAND); +CMD_CARRYOBJECT : 'carryobject' { pushCommand("U"); } -> type(COMMAND); +CMD_THROWCARRY : 'throwcarry' -> type(COMMAND); +CMD_FOLLOWPLAYER : 'followplayer' -> type(COMMAND); +CMD_TOINVENTORY : 'toinventory' { pushCommand("S"); } -> type(COMMAND); +CMD_TOWEAPONS : 'toweapons' { pushCommand("S"); } -> type(COMMAND); +CMD_SETCOLOREFFECT : 'setcoloreffect' { pushCommand("EEEE"); } -> type(COMMAND); +CMD_SETZOOMEFFECT : 'setzoomeffect' { pushCommand("E"); } -> type(COMMAND); +CMD_SHOWIMG : 'showimg' { pushCommand("ESEE"); } -> type(COMMAND); +CMD_SHOWIMG2 : 'showimg2' { pushCommand("ESEEE"); } -> type(COMMAND); +CMD_SHOWANI : 'showani' { pushCommand("EEEDS"); } -> type(COMMAND); +CMD_SHOWANI2 : 'showani2' { pushCommand("EEEEDS"); } -> type(COMMAND); +CMD_SHOWPOLY : 'showpoly' { pushCommand("EE"); } -> type(COMMAND); +CMD_SHOWPOLY2 : 'showpoly2' { pushCommand("EE"); } -> type(COMMAND); +CMD_SHOWTEXT : 'showtext' { pushCommand("EEESSS"); } -> type(COMMAND); +CMD_SHOWTEXT2 : 'showtext2' { pushCommand("EEEESSS"); } -> type(COMMAND); +CMD_HIDEIMG : 'hideimg' { pushCommand("E"); } -> type(COMMAND); +CMD_HIDEIMGS : 'hideimgs' { pushCommand("EE"); } -> type(COMMAND); +CMD_CHANGEIMGPART : 'changeimgpart' { pushCommand("EEEEE"); } -> type(COMMAND); +CMD_CHANGEIMGVIS : 'changeimgvis' { pushCommand("EE"); } -> type(COMMAND); +CMD_CHANGEIMGCOLORS : 'changeimgcolors' { pushCommand("EEEEE"); } -> type(COMMAND); +CMD_CHANGEIMGZOOM : 'changeimgzoom' { pushCommand("EE"); } -> type(COMMAND); +CMD_CHANGEIMGMODE : 'changeimgmode' { pushCommand("EE"); } -> type(COMMAND); +CMD_SHOOTARROW : 'shootarrow' { pushCommand("D"); } -> type(COMMAND); +CMD_SHOOTFIREBALL : 'shootfireball' { pushCommand("D"); } -> type(COMMAND); +CMD_SHOOTFIREBLAST : 'shootfireblast' { pushCommand("D"); } -> type(COMMAND); +CMD_SHOOTNUKE : 'shootnuke' { pushCommand("D"); } -> type(COMMAND); +CMD_SHOOTBALL : 'shootball' -> type(COMMAND); +CMD_SPYFIRE : 'spyfire' { pushCommand("EE"); } -> type(COMMAND); +CMD_HITPLAYER : 'hitplayer' { pushCommand("EEEE"); } -> type(COMMAND); +CMD_HITNPC : 'hitnpc' { pushCommand("EEEE"); } -> type(COMMAND); +CMD_HITOBJECTS : 'hitobjects' { pushCommand("EEE"); } -> type(COMMAND); +CMD_HIDELOCAL : 'hidelocal' -> type(COMMAND); +CMD_SHOWLOCAL : 'showlocal' -> type(COMMAND); +CMD_DONTBLOCKLOCAL : 'dontblocklocal' -> type(COMMAND); +CMD_BLOCKAGAINLOCAL : 'blockagainlocal' -> type(COMMAND); +CMD_TAKEHORSE : 'takehorse' -> type(COMMAND); +CMD_TOKENIZE : 'tokenize ' { pushCommand("S"); } -> type(COMMAND); +CMD_TOKENIZE2 : 'tokenize2' { pushCommand("SS"); } -> type(COMMAND); +CMD_SETSHAPE : 'setshape ' { pushCommand("EEE"); } -> type(COMMAND); +CMD_SETSHAPE2 : 'setshape2' { pushCommand("EEE"); } -> type(COMMAND); +CMD_WRAPTEXT : 'wraptext ' { pushCommand("ESS"); } -> type(COMMAND); +CMD_WRAPTEXT2 : 'wraptext2 ' { pushCommand("EESS"); } -> type(COMMAND); +CMD_SETSHOOTPARAMS : 'setshootparams ' { pushCommand("L"); } -> type(COMMAND); +CMD_SHOOT : 'shoot ' { pushCommand("EEEEEES"); } -> type(COMMAND); +CMD_SETLEVEL : 'setlevel ' { pushCommand("S"); } -> type(COMMAND); +CMD_SETLEVEL2 : 'setlevel2' { pushCommand("SEE"); } -> type(COMMAND); +CMD_SETURLLEVEL : 'seturllevel' { pushCommand("S"); } -> type(COMMAND); +CMD_SETBODY : 'setbody' { pushCommand("S"); } -> type(COMMAND); +CMD_SETHEAD : 'sethead' { pushCommand("S"); } -> type(COMMAND); +CMD_SETSWORD : 'setsword' { pushCommand("SE"); } -> type(COMMAND); +CMD_SETSHIELD : 'setshield' { pushCommand("SE"); } -> type(COMMAND); +CMD_SETBOW : 'setbow' { pushCommand("S"); } -> type(COMMAND); +CMD_SETANI : 'setani' { pushCommand("S"); } -> type(COMMAND); +CMD_SETPLAYERDIR : 'setplayerdir' { pushCommand("D"); } -> type(COMMAND); +CMD_SETGENDER : 'setgender' { pushCommand("G"); } -> type(COMMAND); +CMD_SETSKINCOLOR : 'setskincolor' { pushCommand("C"); } -> type(COMMAND); +CMD_SETCOATCOLOR : 'setcoatcolor' { pushCommand("C"); } -> type(COMMAND); +CMD_SETSLEEVECOLOR : 'setsleevecolor' { pushCommand("C"); } -> type(COMMAND); +CMD_SETSHOECOLOR : 'setshoecolor' { pushCommand("C"); } -> type(COMMAND); +CMD_SETBELTCOLOR : 'setbeltcolor' { pushCommand("C"); } -> type(COMMAND); +CMD_SETPLAYERPROP : 'setplayerprop' { pushCommand("MS"); } -> type(COMMAND); +CMD_TAKEPLAYERCARRY : 'takeplayercarry' -> type(COMMAND); +CMD_TAKEPLAYERHORSE : 'takeplayerhorse' -> type(COMMAND); +CMD_DISABLEWEAPONS : 'disableweapons' -> type(COMMAND); +CMD_ENABLEWEAPONS : 'enableweapons' -> type(COMMAND); +CMD_FREEZEPLAYER : 'freezeplayer ' { pushCommand("E"); } -> type(COMMAND); +CMD_FREEZEPLAYER2 : 'freezeplayer2' -> type(COMMAND); +CMD_UNFREEZEPLAYER : 'unfreezeplayer' -> type(COMMAND); +CMD_HIDEPLAYER : 'hideplayer' { pushCommand("E"); } -> type(COMMAND); +CMD_HIDESWORD : 'hidesword' { pushCommand("E"); } -> type(COMMAND); +CMD_HURT : 'hurt ' { pushCommand("E"); } -> type(COMMAND); +CMD_DISABLEDEFMOVEMENT : 'disabledefmovement' -> type(COMMAND); +CMD_ENABLEDEFMOVEMENT : 'enabledefmovement' -> type(COMMAND); +CMD_DISABLESELECTWEAPONS : 'disableselectweapons' -> type(COMMAND); +CMD_ENABLESELECTWEAPONS : 'enableselectweapons' -> type(COMMAND); +CMD_DISABLEPAUSE : 'disablepause' -> type(COMMAND); +CMD_ENABLEPAUSE : 'enablepause' -> type(COMMAND); +CMD_DISABLEMAP : 'disablemap' -> type(COMMAND); +CMD_ENABLEMAP : 'enablemap' -> type(COMMAND); +CMD_ENABLEFEATURES : 'enablefeatures' { pushCommand("E"); } -> type(COMMAND); +CMD_REPLACEANI : 'replaceani' { pushCommand("SS"); } -> type(COMMAND); +CMD_ATTACHPLAYERTOOBJ : 'attachplayertoobj' { pushCommand("EE"); } -> type(COMMAND); +CMD_DETACHPLAYER : 'detachplayer' -> type(COMMAND); +CMD_UPDATEBOARD : 'updateboard' { pushCommand("EEEE"); } -> type(COMMAND); +CMD_UPDATEBOARD2 : 'updateboard2' { pushCommand("EEEE"); } -> type(COMMAND); +CMD_PUTOBJECT : 'putobject' { pushCommand("SEE"); } -> type(COMMAND); +CMD_PUTBOMB : 'putbomb' { pushCommand("EEE"); } -> type(COMMAND); +CMD_PUTEXPLOSION : 'putexplosion ' { pushCommand("EEE"); } -> type(COMMAND); +CMD_PUTEXPLOSION2 : 'putexplosion2' { pushCommand("EEEE"); } -> type(COMMAND); +CMD_PUTLEAPS : 'putleaps' { pushCommand("EEE"); } -> type(COMMAND); +CMD_PUTHORSE : 'puthorse' { pushCommand("SEE"); } -> type(COMMAND); +CMD_SETBACKPAL : 'setbackpal' { pushCommand("S"); } -> type(COMMAND); +CMD_SETBACKTILE : 'setbacktile' { pushCommand("E"); } -> type(COMMAND); +CMD_SETBACKTILE2 : 'setbacktile2' { pushCommand("EEEEE"); } -> type(COMMAND); +CMD_SETLETTERS : 'setletters' { pushCommand("S"); } -> type(COMMAND); +CMD_SETMAP : 'setmap' { pushCommand("SSEE"); } -> type(COMMAND); +CMD_SETMINIMAP : 'setminimap' { pushCommand("SSEE"); } -> type(COMMAND); +CMD_SETEFFECT : 'seteffect ' { pushCommand("EEEE"); } -> type(COMMAND); +CMD_SETFOCUS : 'setfocus' { pushCommand("EE"); } -> type(COMMAND); +CMD_RESETFOCUS : 'resetfocus' -> type(COMMAND); +CMD_NOPLAYERKILLING : 'noplayerkilling' -> type(COMMAND); +CMD_NOPLAYERONWALL : 'noplayeronwall' -> type(COMMAND); +CMD_REMOVEBOMB : 'removebomb' { pushCommand("E"); } -> type(COMMAND); +CMD_REMOVEARROW : 'removearrow' { pushCommand("E"); } -> type(COMMAND); +CMD_REMOVEITEM : 'removeitem' { pushCommand("E"); } -> type(COMMAND); +CMD_REMOVEEXPLO : 'removeexplo' { pushCommand("E"); } -> type(COMMAND); +CMD_REMOVEHORSE : 'removehorse' { pushCommand("E"); } -> type(COMMAND); +CMD_EXPLODEBOMB : 'explodebomb' { pushCommand("E"); } -> type(COMMAND); +CMD_REFLECTARROW : 'reflectarrow' { pushCommand("E"); } -> type(COMMAND); +CMD_ADDTILEDEF : 'addtiledef ' { pushCommand("SSE"); } -> type(COMMAND); +CMD_ADDTILEDEF2 : 'addtiledef2' { pushCommand("SSEE"); } -> type(COMMAND); +CMD_REMOVETILEDEFS : 'removetiledefs' { pushCommand("S"); } -> type(COMMAND); +CMD_LOADMAP : 'loadmap' { pushCommand("S"); } -> type(COMMAND); +CMD_UPDATETERRAIN : 'updateterrain' -> type(COMMAND); +CMD_SHOWSTATS : 'showstats' { pushCommand("E"); } -> type(COMMAND); +CMD_PUTCOMP : 'putcomp' { pushCommand("BEE"); } -> type(COMMAND); +CMD_PUTNEWCOMP : 'putnewcomp' { pushCommand("BEESE"); } -> type(COMMAND); +CMD_HITCOMPU : 'hitcompu' { pushCommand("EEEE"); } -> type(COMMAND); +CMD_REMOVECOMPUS : 'removecompus' -> type(COMMAND); +CMD_PLAY : 'play ' { pushCommand("S"); } -> type(COMMAND); +CMD_PLAY2 : 'play2 ' { pushCommand("SEEE"); } -> type(COMMAND); +CMD_PLAYLOOPED : 'playlooped' { pushCommand("S"); } -> type(COMMAND); +CMD_STOPSOUND : 'stopsound' { pushCommand("S"); } -> type(COMMAND); +CMD_STOPMIDI : 'stopmidi' -> type(COMMAND); +CMD_SETMUSICVOLUME : 'setmusicvolume' { pushCommand("EE"); } -> type(COMMAND); +CMD_OPENURL : 'openurl ' { pushCommand("S"); } -> type(COMMAND); +CMD_OPENURL2 : 'openurl2 ' { pushCommand("SEE"); } -> type(COMMAND); +CMD_SHOWFILE : 'showfile' { pushCommand("S"); } -> type(COMMAND); +CMD_JOIN : 'join' { pushCommand("S"); } -> type(COMMAND); +CMD_SETCURSOR : 'setcursor ' { pushCommand("E"); } -> type(COMMAND); +CMD_SETCURSOR2 : 'setcursor2' { pushCommand("S"); } -> type(COMMAND); +CMD_CANWARP : 'canwarp' -> type(COMMAND); +CMD_CANWARP2 : 'canwarp2' -> type(COMMAND); +CMD_CANNOTWARP : 'cannotwarp' -> type(COMMAND); +CMD_ADDWEAPON : 'addweapon' { pushCommand("S"); } -> type(COMMAND); +CMD_REMOVEWEAPON : 'removeweapon' { pushCommand("S"); } -> type(COMMAND); +CMD_SETSPRITESIMAGE : 'setspritesimage' { pushCommand("S"); } -> type(COMMAND); +CMD_SETSTATUSIMAGE : 'setstatusimage' { pushCommand("S"); } -> type(COMMAND); +CMD_ADDGUILDMEMBER : 'addguildmember' { pushCommand("SSS"); } -> type(COMMAND); +CMD_REMOVEGUILDMEMBER : 'removeguildmember' { pushCommand("SSS"); } -> type(COMMAND); +CMD_REMOVEGUILD : 'removeguild' { pushCommand("S"); } -> type(COMMAND); +CMD_COPYSTRINGS : 'copystrings' { pushCommand("SS"); } -> type(COMMAND); +CMD_SENDTORC : 'sendtorc' { pushCommand("S"); } -> type(COMMAND); +CMD_SENDTONC : 'sendtonc' { pushCommand("S"); } -> type(COMMAND); +CMD_SENDPM : 'sendpm' { pushCommand("R"); } -> type(COMMAND); +CMD_SETPM : 'setpm' { pushCommand("S"); } -> type(COMMAND); +CMD_SENDRPGMESSAGE : 'sendrpgmessage' { pushCommand("S"); } -> type(COMMAND); +CMD_SERVERWARP : 'serverwarp' { pushCommand("S"); } -> type(COMMAND); +CMD_SETZ : 'setz ' { pushCommand("EEEEEEEE"); } -> type(COMMAND); +CMD_COPYLEVEL : 'copylevel' { pushCommand("SS"); } -> type(COMMAND); +CMD_DELETELEVEL : 'deletelevel' { pushCommand("S"); } -> type(COMMAND); +CMD_SAVEINFO : 'saveinfo' { pushCommand("SS"); } -> type(COMMAND); +CMD_SAVELOG : 'savelog' { pushCommand("S"); } -> type(COMMAND); +CMD_SAVELOG2 : 'savelog2' { pushCommand("SS"); } -> type(COMMAND); +CMD_WARPTO : 'warpto' { pushCommand("SEE"); } -> type(COMMAND); +// GR extensions +CMD_ENABLEDAMAGEREACTIONS : 'enabledamagereactions' -> type(COMMAND); +CMD_DISABLEDAMAGEREACTIONS : 'disabledamagereactions' -> type(COMMAND); + +FUNC_GROUP_1 + : ( + 'abs' + | 'aindexof' + | 'arctan' + | 'arraylen' + | 'ascii' + | 'cos' + | 'exp' + | 'findnearestplayer' + | 'findnearestplayers' + | 'getangle' + | 'getareanpcs' + | 'getdir' + | 'getnearestplayer' + | 'getnearestplayers' + | 'getz' + | 'int' + | 'keydown' + | 'keydown2' + | 'log' + | 'max' + | 'min' + | 'onwall' + | 'onwall2' + | 'onwater' + | 'onwater2' + | 'random' + | 'screenx' + | 'screeny' + | 'sin' + | 'testbomb' + | 'testcompu' + | 'testexplo' + | 'testhorse' + | 'testitem' + | 'testnpc' + | 'testplayer' + | 'testsign' + | 'tiletype' + | 'vecx' + | 'vecy' + | 'worldx' + | 'worldy' + ) { pushCommand("(P)"); } -> type(FUNCTION) + ; + +FUNC_GROUP_2 + : ( + 'base64decode' + | 'base64encode' + | 'getflagkeys' + | 'getnpc' + | 'getplayer' + | 'hasweapon' + | 'imgheight' + | 'imgwidth' + | 'keycode' + | 'onmapy' + | 'strlen' + | 'strtofloat' + | 'onmapx' + ) { pushCommand("(S)"); } -> type(FUNCTION) + ; + +FUNC_GROUP_3 + : ( + 'indexof' + | 'strcontains' + | 'strequals' + | 'startswith' + ) { pushCommand("(SS)"); } -> type(FUNCTION) + ; + +// FUNC_GROUP_4 : 'textwidth' { pushCommand("(ESSS)"); } -> type(FUNCTION); +// FUNC_GROUP_4 : 'textheight' { pushCommand("(ESS)"); } -> type(FUNCTION); +FUNC_GROUP_4 : ('textwidth' | 'textheight') { pushCommand("(ESSS)"); } -> type(FUNCTION); +FUNC_GROUP_5 : 'lindexof' { pushCommand("(SV)"); } -> type(FUNCTION); +FUNC_GROUP_6 : 'sarraylen' { pushCommand("(V)"); } -> type(FUNCTION); +FUNC_GROUP_7 : ('playersays' | 'playersays2') { pushCommand(" type(FUNCTION); + +MC_NOINDEX : '#' ([angcmWw1235678NDLFfpbES] | 'C' [01234567] | 'P1' DIGITS? | 'P2' DIGITS? | 'P3' '0'? | 'P' [456789]) { _input->LA(1) != '(' }? -> type(MESSAGECODE); +MC_SIMPLE : '#' ([angcmWw1235678NDptKkG] | 'C' [01234567] | 'P1' DIGITS? | 'P2' DIGITS? | 'P3' '0'? | 'P' [456789]) { pushCommand("(P)"); } -> type(MESSAGECODE); +MC_COMPUTED_S : '#s' { pushCommand("(V)"); } -> type(MESSAGECODE); +MC_COMPUTED_V : '#v' { pushCommand("(E)"); } -> type(MESSAGECODE); +MC_I : '#I' { pushCommand("(VP)"); } -> type(MESSAGECODE); +MC_T : '#T' { pushCommand("(S)"); } -> type(MESSAGECODE); +MC_U : '#U' { pushCommand("(R)"); } -> type(MESSAGECODE); +MC_e : '#e' { pushCommand("(EES)"); } -> type(MESSAGECODE); +MC_i : '#i' { pushCommand("(SP)"); } -> type(MESSAGECODE); +MC_R : '#R' { pushCommand("(L)"); } -> type(MESSAGECODE); +MC_Q : '#Q' { pushCommand("(SS)"); } -> type(MESSAGECODE); + +// Keep above KW_TRUE/KW_FALSE. +LITERAL + : REAL + | KW_TRUE + | KW_FALSE + ; + +KW_WITH : 'with'; +KW_FUNCTION : 'function' { pushCommand("V()"); }; +KW_IF : 'if'; +KW_ELSE : 'else'; +KW_FOR : 'for'; +KW_WHILE : 'while'; +KW_RETURN : 'return'; +KW_BREAK : 'break'; +KW_CONTINUE : 'continue'; +KW_TRUE : 'true'; +KW_FALSE : 'false'; + +OP_ASSIGN : '='; +OP_ASSIGN2 : ':=' -> type(OP_ASSIGN); +OP_ADD : '+'; +OP_SUB : '-'; +OP_MUL : '*'; +OP_DIV : '/'; +OP_MOD : '%'; +OP_POW : '^'; +OP_ASSIGN_ADD : '+='; +OP_ASSIGN_SUB : '-='; +OP_ASSIGN_MUL : '*='; +OP_ASSIGN_DIV : '/='; +OP_ASSIGN_MOD : '%='; +OP_ASSIGN_POW : '^='; +OP_EQUAL : '=='; +OP_NOTEQ : '!='; +OP_NOTEQ2 : '<>' -> type(OP_NOTEQ); +OP_LESS : '<'; +OP_GREAT : '>'; +OP_LESS_EQ : '<='; +OP_LESS_EQ2 : '=<' -> type(OP_LESS_EQ); +OP_GREAT_EQ : '>='; +OP_GREAT_EQ2 : '=>' -> type(OP_GREAT_EQ); +OP_IN : ' in '; +OP_INC : '++'; +OP_DEC : '--'; +OP_LOGICALAND : '&&'; +OP_LOGICALOR : '||'; +OP_LOGICALNOT : '!'; + +TOKEN_BRACKET_LEFT : '[' { pushArrayAccess(); }; +TOKEN_BRACKET_RIGHT : ']'; +TOKEN_BRACE_LEFT : '{'; +TOKEN_BRACE_RIGHT : '}' { emitIdentifierBefore(GS1Lexer::END, getText()); }; +TOKEN_PAREN_LEFT : '('; +TOKEN_PAREN_RIGHT : ')'; +TOKEN_COMMA : ','; +TOKEN_PIPE : '|'; +TOKEN_QUESTION : '?'; +TOKEN_COLON : ':'; +TOKEN_PERIOD : '.'; + +ALLSTATS + : 'allstats' + ; + +ALLFEATURES + : 'allfeatures' + ; + +LINECOMMENT + : '//' ~ [\r\n]* -> channel(HIDDEN) + ; + +BLOCKCOMMENT + : '/*' .*? '*/' -> channel(HIDDEN) + ; + +REAL + : DIGITS+ ('.' DIGITS+)? + | '.' DIGITS+ + | '0x' HEXDIGITS+ + ; + +IDENTIFIER + : [a-zA-Z0-9_]+ { isNotDefaultMode() || !isBuiltInCommand(getText()) }? + ; + +END + : ';' + ; + +WS + : WHITESPACE+ -> channel(HIDDEN) + ; + +fragment BADDY + : 'graysoldier' + | 'bluesoldier' + | 'redsoldier' + | 'shootingsoldier' + | 'swampsoldier' + | 'frog' + | 'octopus' + | 'goldenwarrior' + | 'lizardon' + | 'dragon' + ; + +fragment COLORS + : 'white' + | 'yellow' + | 'orange' + | 'pink' + | 'red' + | 'darkred' + | 'lightgreen' + | 'green' + | 'darkgreen' + | 'lightblue' + | 'blue' + | 'darkblue' + | 'brown' + | 'cynober' + | 'purple' + | 'darkpurple' + | 'lightgray' + | 'gray' + | 'black' + | 'transparent' + ; + +fragment DIR + : 'up' + | 'left' + | 'down' + | 'right' + ; + +fragment GENDERS + : 'male' + | 'female' + ; + +fragment CARRYNAMES + : 'bush' + | 'sign' + | 'vase' + | 'stone' + | 'blackstone' + | 'bomb' + | 'hotbomb' + | 'superbomb' + | 'joltbomb' + | 'hotjoltbomb' + | 'none' + ; + +fragment ITEMNAMES + : 'greenrupee' + | 'bluerupee' + | 'redrupee' + | 'bombs' + | 'darts' + | 'heart' + | 'glove1' + | 'bow' + | 'bomb' + | 'shield' + | 'sword' + | 'fullheart' + | 'superbomb' + | 'battleaxe' + | 'goldensword' + | 'mirrorshield' + | 'glove2' + | 'lizardshield' + | 'lizardsword' + | 'goldrupee' + | 'fireball' + | 'fireblast' + | 'nukeshot' + | 'joltbomb' + | 'spinattack' + ; + +fragment LETTERS + : [a-zA-Z] + ; + +fragment DIGITS + : [0-9] + ; + +fragment HEXDIGITS + : [0-9a-fA-F] + ; + +fragment WHITESPACE + : [ \r\n\t] + ; + +/////////////////////////////////////////////////////////// +// COMMAND PARSING +/////////////////////////////////////////////////////////// + +mode IN_PARAM_V; + +PARAM_V_WS : WHITESPACE+ -> type(WS), channel(HIDDEN); +PARAM_V_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { canCmdPop() }? { popNextMode(true); emitIdentifierBefore(GS1Lexer::END, getText()); } -> type(TOKEN_BRACE_RIGHT); +PARAM_V_POP_PAREN_LEFT : TOKEN_PAREN_LEFT { isNextArgLeftParen() }? { popNextMode(); popNextMode(); } -> type(TOKEN_PAREN_LEFT); +PARAM_V_POP_PAREN_RIGHT : TOKEN_PAREN_RIGHT { canFuncPop() }? { popNextMode(true); } -> type(TOKEN_PAREN_RIGHT); +PARAM_V_POP_END : END { canCmdPop() }? { popNextMode(true); } -> type(END); +PARAM_V_POP_COMMA : TOKEN_COMMA { popNextMode(); } -> type(TOKEN_COMMA); +PARAM_V_FUNC_GROUP_1 : FUNC_GROUP_1 { pushCommand("(P)"); } -> type(FUNCTION); +PARAM_V_FUNC_GROUP_2 : FUNC_GROUP_2 { pushCommand("(S)"); } -> type(FUNCTION); +PARAM_V_FUNC_GROUP_3 : FUNC_GROUP_3 { pushCommand("(SS)"); } -> type(FUNCTION); +PARAM_V_FUNC_GROUP_4 : FUNC_GROUP_4 { pushCommand("(ESSS)"); } -> type(FUNCTION); +PARAM_V_FUNC_GROUP_5 : FUNC_GROUP_5 { pushCommand("(SV)"); } -> type(FUNCTION); +PARAM_V_FUNC_GROUP_6 : FUNC_GROUP_6 { pushCommand("(V)"); } -> type(FUNCTION); +PARAM_V_FUNC_GROUP_7 : FUNC_GROUP_7 { pushCommand(" type(FUNCTION); +PARAM_V_MC_NOINDEX : MC_NOINDEX -> type(MESSAGECODE); +PARAM_V_MC_SIMPLE : MC_SIMPLE { pushCommand("(P)"); } -> type(MESSAGECODE); +PARAM_V_MC_COMPUTED_S : MC_COMPUTED_S { pushCommand("(V)"); } -> type(MESSAGECODE); +PARAM_V_MC_COMPUTED_V : MC_COMPUTED_V { pushCommand("(E)"); } -> type(MESSAGECODE); +PARAM_V_MC_I : MC_I { pushCommand("(VP)"); } -> type(MESSAGECODE); +PARAM_V_MC_T : MC_T { pushCommand("(S)"); } -> type(MESSAGECODE); +PARAM_V_MC_U : MC_U { pushCommand("(R)"); } -> type(MESSAGECODE); +PARAM_V_MC_e : MC_e { pushCommand("(EES)"); } -> type(MESSAGECODE); +PARAM_V_MC_i : MC_i { pushCommand("(SP)"); } -> type(MESSAGECODE); +PARAM_V_MC_R : MC_R { pushCommand("(L)"); } -> type(MESSAGECODE); +PARAM_V_MC_Q : MC_Q { pushCommand("(SS)"); } -> type(MESSAGECODE); +PARAM_V_LITERAL : LITERAL -> type(LITERAL); +PARAM_V_IDENTIFIER : IDENTIFIER -> type(IDENTIFIER); +PARAM_V_TOKEN_BRACKET_LEFT : TOKEN_BRACKET_LEFT { pushArrayAccess(); } -> type(TOKEN_BRACKET_LEFT); +PARAM_V_TOKEN_PIPE : TOKEN_PIPE -> type(TOKEN_PIPE); +PARAM_V_TOKEN_QUESTION : TOKEN_QUESTION -> type(TOKEN_QUESTION); +PARAM_V_TOKEN_COLON : TOKEN_COLON -> type(TOKEN_COLON); +PARAM_V_TOKEN_PERIOD : TOKEN_PERIOD -> type(TOKEN_PERIOD); + +// -------------------------------------------------------- +mode IN_PARAM_E; + +PARAM_E_WS : WHITESPACE+ -> type(WS), channel(HIDDEN); +PARAM_E_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { canCmdPop() }? { popNextMode(true); emitIdentifierBefore(GS1Lexer::END, getText()); } -> type(TOKEN_BRACE_RIGHT); +PARAM_E_POP_PAREN_RIGHT : TOKEN_PAREN_RIGHT { canFuncPop() && m_braceCount == 0 }? { popNextMode(true); } -> type(TOKEN_PAREN_RIGHT); +PARAM_E_POP_END : END { canCmdPop() }? { popNextMode(true); } -> type(END); +PARAM_E_POP_COMMA : TOKEN_COMMA { canCommaPop() }? { popNextMode(); } -> type(TOKEN_COMMA); +PARAM_E_COMMA : TOKEN_COMMA { !canCommaPop() }? -> type(TOKEN_COMMA); +PARAM_E_FUNC_GROUP_1 : FUNC_GROUP_1 { pushCommand("(P)"); } -> type(FUNCTION); +PARAM_E_FUNC_GROUP_2 : FUNC_GROUP_2 { pushCommand("(S)"); } -> type(FUNCTION); +PARAM_E_FUNC_GROUP_3 : FUNC_GROUP_3 { pushCommand("(SS)"); } -> type(FUNCTION); +PARAM_E_FUNC_GROUP_4 : FUNC_GROUP_4 { pushCommand("(ESSS)"); } -> type(FUNCTION); +PARAM_E_FUNC_GROUP_5 : FUNC_GROUP_5 { pushCommand("(SV)"); } -> type(FUNCTION); +PARAM_E_FUNC_GROUP_6 : FUNC_GROUP_6 { pushCommand("(V)"); } -> type(FUNCTION); +PARAM_E_FUNC_GROUP_7 : FUNC_GROUP_7 { pushCommand(" type(FUNCTION); +PARAM_E_LITERAL : LITERAL -> type(LITERAL); +PARAM_E_IDENTIFIER : IDENTIFIER -> type(IDENTIFIER); +PARAM_E_OP_ASSIGN : OP_ASSIGN -> type(OP_ASSIGN); +PARAM_E_OP_ADD : OP_ADD -> type(OP_ADD); +PARAM_E_OP_SUB : OP_SUB -> type(OP_SUB); +PARAM_E_OP_MUL : OP_MUL -> type(OP_MUL); +PARAM_E_OP_DIV : OP_DIV -> type(OP_DIV); +PARAM_E_OP_MOD : OP_MOD -> type(OP_MOD); +PARAM_E_OP_POW : OP_POW -> type(OP_POW); +PARAM_E_OP_EQUAL : OP_EQUAL -> type(OP_EQUAL); +PARAM_E_OP_NOTEQ : OP_NOTEQ -> type(OP_NOTEQ); +PARAM_E_OP_LESS : OP_LESS -> type(OP_LESS); +PARAM_E_OP_GREAT : OP_GREAT -> type(OP_GREAT); +PARAM_E_OP_LESS_EQ : OP_LESS_EQ -> type(OP_LESS_EQ); +PARAM_E_OP_GREAT_EQ : OP_GREAT_EQ -> type(OP_GREAT_EQ); +PARAM_E_OP_IN : OP_IN -> type(OP_IN); +PARAM_E_OP_INC : OP_INC -> type(OP_INC); +PARAM_E_OP_DEC : OP_DEC -> type(OP_DEC); +PARAM_E_OP_LOGICALAND : OP_LOGICALAND -> type(OP_LOGICALAND); +PARAM_E_OP_LOGICALOR : OP_LOGICALOR -> type(OP_LOGICALOR); +PARAM_E_OP_LOGICALNOT : OP_LOGICALNOT -> type(OP_LOGICALNOT); +PARAM_E_TOKEN_BRACKET_LEFT : TOKEN_BRACKET_LEFT { pushArrayAccess(); } -> type(TOKEN_BRACKET_LEFT); +PARAM_E_POP_TOKEN_BRACKET_RIGHT : TOKEN_BRACKET_RIGHT { canArrayPop() }? { popNextMode(); } -> type(TOKEN_BRACKET_RIGHT); +PARAM_E_TOKEN_BRACKET_RIGHT : TOKEN_BRACKET_RIGHT { !canArrayPop() }? -> type(TOKEN_BRACKET_RIGHT); +PARAM_E_TOKEN_PAREN_LEFT : TOKEN_PAREN_LEFT { ++m_braceCount; } -> type(TOKEN_PAREN_LEFT); +PARAM_E_TOKEN_PAREN_RIGHT : TOKEN_PAREN_RIGHT { --m_braceCount; } -> type(TOKEN_PAREN_RIGHT); +PARAM_E_TOKEN_PIPE : TOKEN_PIPE -> type(TOKEN_PIPE); +PARAM_E_TOKEN_QUESTION : TOKEN_QUESTION -> type(TOKEN_QUESTION); +PARAM_E_TOKEN_COLON : TOKEN_COLON -> type(TOKEN_COLON); +PARAM_E_TOKEN_PERIOD : TOKEN_PERIOD -> type(TOKEN_PERIOD); + +// -------------------------------------------------------- +mode IN_PARAM_S; + +PARAM_S_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { canCmdPop() }? { popNextMode(true); emitIdentifierBefore(GS1Lexer::END, getText()); } -> type(TOKEN_BRACE_RIGHT); +PARAM_S_POP_PAREN_RIGHT : TOKEN_PAREN_RIGHT { canFuncPop() }? { popNextMode(true); } -> type(TOKEN_PAREN_RIGHT); +PARAM_S_POP_END : END { canCmdPop() }? { popNextMode(true); } -> type(END); +PARAM_S_POP_COMMA : TOKEN_COMMA { canCommaPop() }? { popNextMode(); } -> type(TOKEN_COMMA); +PARAM_S_MC_NOINDEX : MC_NOINDEX -> type(MESSAGECODE); +PARAM_S_MC_SIMPLE : MC_SIMPLE { pushCommand("(P)"); } -> type(MESSAGECODE); +PARAM_S_MC_COMPUTED_S : MC_COMPUTED_S { pushCommand("(V)"); } -> type(MESSAGECODE); +PARAM_S_MC_COMPUTED_V : MC_COMPUTED_V { pushCommand("(E)"); } -> type(MESSAGECODE); +PARAM_S_MC_I : MC_I { pushCommand("(VP)"); } -> type(MESSAGECODE); +PARAM_S_MC_T : MC_T { pushCommand("(S)"); } -> type(MESSAGECODE); +PARAM_S_MC_U : MC_U { pushCommand("(R)"); } -> type(MESSAGECODE); +PARAM_S_MC_e : MC_e { pushCommand("(EES)"); } -> type(MESSAGECODE); +PARAM_S_MC_i : MC_i { pushCommand("(SP)"); } -> type(MESSAGECODE); +PARAM_S_MC_R : MC_R { pushCommand("(L)"); } -> type(MESSAGECODE); +PARAM_S_MC_Q : MC_Q { pushCommand("(SS)"); } -> type(MESSAGECODE); +PARAM_S_STRING_ESCAPE : '##' -> type(STRING); +PARAM_S_STRING_LITERAL1 : ~[#),]+ { canFuncPop() && canCommaPop() }? -> type(STRING); +PARAM_S_STRING_LITERAL2 : ~[#};,]+ { canCmdPop() && canCommaPop() }? -> type(STRING); +PARAM_S_STRING_LITERAL_END1 : ~[#)]+ { canFuncPop() && !canCommaPop() }? -> type(STRING); +PARAM_S_STRING_LITERAL_END2 : ~[#};]+ { canCmdPop() && !canCommaPop() }? -> type(STRING); + +// -------------------------------------------------------- +mode IN_PARAM_R; + +PARAM_R_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { canCmdPop() }? { popNextMode(true); emitIdentifierBefore(GS1Lexer::END, getText()); } -> type(TOKEN_BRACE_RIGHT); +PARAM_R_POP_END : END { canCmdPop() }? { popNextMode(true); } -> type(END); +PARAM_R_POP_COMMA : TOKEN_COMMA { canCommaPop() }? { popNextMode(); } -> type(TOKEN_COMMA); +PARAM_R_STRING_LITERAL : ~[};,]+ { canCmdPop() && canCommaPop() }? -> type(STRING); +PARAM_R_STRING_LITERAL_END : ~[};]+ { canCmdPop() && !canCommaPop() }? -> type(STRING); + +// -------------------------------------------------------- +mode IN_PARAM_L; + +PARAM_L_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { canCmdPop() }? { popNextMode(true); emitIdentifierBefore(GS1Lexer::END, getText()); } -> type(TOKEN_BRACE_RIGHT); +PARAM_L_POP_PAREN_RIGHT : TOKEN_PAREN_RIGHT { canFuncPop() }? { popNextMode(true); } -> type(TOKEN_PAREN_RIGHT); +PARAM_L_POP_END : END { canCmdPop() }? { popNextMode(true); } -> type(END); +PARAM_L_COMMA : TOKEN_COMMA { emitIdentifierAfter(GS1Lexer::STRING, getText()); } -> type(TOKEN_COMMA); +PARAM_L_MC_NOINDEX : MC_NOINDEX -> type(MESSAGECODE); +PARAM_L_MC_SIMPLE : MC_SIMPLE { pushCommand("(P)"); } -> type(MESSAGECODE); +PARAM_L_MC_COMPUTED_S : MC_COMPUTED_S { pushCommand("(V)"); } -> type(MESSAGECODE); +PARAM_L_MC_COMPUTED_V : MC_COMPUTED_V { pushCommand("(E)"); } -> type(MESSAGECODE); +PARAM_L_MC_I : MC_I { pushCommand("(VP)"); } -> type(MESSAGECODE); +PARAM_L_MC_T : MC_T { pushCommand("(S)"); } -> type(MESSAGECODE); +PARAM_L_MC_U : MC_U { pushCommand("(R)"); } -> type(MESSAGECODE); +PARAM_L_MC_e : MC_e { pushCommand("(EES)"); } -> type(MESSAGECODE); +PARAM_L_MC_i : MC_i { pushCommand("(SP)"); } -> type(MESSAGECODE); +PARAM_L_MC_R : MC_R { pushCommand("(L)"); } -> type(MESSAGECODE); +PARAM_L_MC_Q : MC_Q { pushCommand("(SS)"); } -> type(MESSAGECODE); +PARAM_L_STRING_ESCAPE : '##' -> type(STRING); +PARAM_L_STRING_LITERAL1 : ~[#),]+ { canFuncPop() }? -> type(STRING); +PARAM_L_STRING_LITERAL2 : ~[#};,]+ { canCmdPop() }? -> type(STRING); + +// -------------------------------------------------------- +mode IN_PARAM_M; + +PARAM_M_WS : WHITESPACE+ -> type(WS), channel(HIDDEN); +PARAM_M_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { canCmdPop() }? { popNextMode(true); emitIdentifierBefore(GS1Lexer::END, getText()); } -> type(TOKEN_BRACE_RIGHT); +PARAM_M_POP_PAREN_RIGHT : TOKEN_PAREN_RIGHT { canFuncPop() }? { popNextMode(true); } -> type(TOKEN_PAREN_RIGHT); +PARAM_M_POP_END : END { canCmdPop() }? { popNextMode(true); } -> type(END); +PARAM_M_POP_COMMA : TOKEN_COMMA { popNextMode(); } -> type(TOKEN_COMMA); +PARAM_M_MC_NOINDEX : MC_NOINDEX -> type(MESSAGECODE); +PARAM_M_MC_SIMPLE : MC_SIMPLE { pushCommand("(P)"); } -> type(MESSAGECODE); +PARAM_M_MC_COMPUTED_S : MC_COMPUTED_S { pushCommand("(V)"); } -> type(MESSAGECODE); +PARAM_M_MC_COMPUTED_V : MC_COMPUTED_V { pushCommand("(E)"); } -> type(MESSAGECODE); +PARAM_M_MC_I : MC_I { pushCommand("(VP)"); } -> type(MESSAGECODE); +PARAM_M_MC_T : MC_T { pushCommand("(S)"); } -> type(MESSAGECODE); +PARAM_M_MC_U : MC_U { pushCommand("(R)"); } -> type(MESSAGECODE); +PARAM_M_MC_e : MC_e { pushCommand("(EES)"); } -> type(MESSAGECODE); +PARAM_M_MC_i : MC_i { pushCommand("(SP)"); } -> type(MESSAGECODE); +PARAM_M_MC_R : MC_R { pushCommand("(L)"); } -> type(MESSAGECODE); +PARAM_M_MC_Q : MC_Q { pushCommand("(SS)"); } -> type(MESSAGECODE); +PARAM_M_FUNC_GROUP_1 : FUNC_GROUP_1 { pushCommand("(P)"); } -> type(FUNCTION); +PARAM_M_FUNC_GROUP_2 : FUNC_GROUP_2 { pushCommand("(S)"); } -> type(FUNCTION); +PARAM_M_FUNC_GROUP_3 : FUNC_GROUP_3 { pushCommand("(SS)"); } -> type(FUNCTION); +PARAM_M_FUNC_GROUP_4 : FUNC_GROUP_4 { pushCommand("(ESSS)"); } -> type(FUNCTION); +PARAM_M_FUNC_GROUP_5 : FUNC_GROUP_5 { pushCommand("(SV)"); } -> type(FUNCTION); +PARAM_M_FUNC_GROUP_6 : FUNC_GROUP_6 { pushCommand("(V)"); } -> type(FUNCTION); +PARAM_M_FUNC_GROUP_7 : FUNC_GROUP_7 { pushCommand(" type(FUNCTION); +PARAM_M_LITERAL : LITERAL -> type(LITERAL); +PARAM_M_IDENTIFIER : IDENTIFIER -> type(IDENTIFIER); +PARAM_M_OP_ADD : OP_ADD -> type(OP_ADD); +PARAM_M_OP_SUB : OP_SUB -> type(OP_SUB); +PARAM_M_OP_MUL : OP_MUL -> type(OP_MUL); +PARAM_M_OP_DIV : OP_DIV -> type(OP_DIV); +PARAM_M_OP_MOD : OP_MOD -> type(OP_MOD); +PARAM_M_OP_POW : OP_POW -> type(OP_POW); +PARAM_M_TOKEN_BRACKET_LEFT : TOKEN_BRACKET_LEFT { pushArrayAccess(); } -> type(TOKEN_BRACKET_LEFT); +PARAM_M_TOKEN_PAREN_LEFT : TOKEN_PAREN_LEFT -> type(TOKEN_PAREN_LEFT); +PARAM_M_TOKEN_PAREN_RIGHT : TOKEN_PAREN_RIGHT -> type(TOKEN_PAREN_RIGHT); + +// -------------------------------------------------------- +mode IN_PARAM_B; + +PARAM_B_WS : WHITESPACE+ -> type(WS), channel(HIDDEN); +PARAM_B_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { canCmdPop() }? { popNextMode(true); emitIdentifierBefore(GS1Lexer::END, getText()); } -> type(TOKEN_BRACE_RIGHT); +PARAM_B_POP_PAREN_RIGHT : TOKEN_PAREN_RIGHT { canFuncPop() }? { popNextMode(true); } -> type(TOKEN_PAREN_RIGHT); +PARAM_B_POP_END : END { canCmdPop() }? { popNextMode(true); } -> type(END); +PARAM_B_POP_COMMA : TOKEN_COMMA { popNextMode(); } -> type(TOKEN_COMMA); +PARAM_B_BADDY : BADDY -> type(BADDY); + +// -------------------------------------------------------- +mode IN_PARAM_I; + +PARAM_I_WS : WHITESPACE+ -> type(WS), channel(HIDDEN); +PARAM_I_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { canCmdPop() }? { popNextMode(true); emitIdentifierBefore(GS1Lexer::END, getText()); } -> type(TOKEN_BRACE_RIGHT); +PARAM_I_POP_PAREN_RIGHT : TOKEN_PAREN_RIGHT { canFuncPop() }? { popNextMode(true); } -> type(TOKEN_PAREN_RIGHT); +PARAM_I_POP_END : END { canCmdPop() }? { popNextMode(true); } -> type(END); +PARAM_I_POP_COMMA : TOKEN_COMMA { popNextMode(); } -> type(TOKEN_COMMA); +PARAM_I_ITEM : ITEMNAMES -> type(ITEM); + +// -------------------------------------------------------- +mode IN_PARAM_C; + +PARAM_C_WS : WHITESPACE+ -> type(WS), channel(HIDDEN); +PARAM_C_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { canCmdPop() }? { popNextMode(true); emitIdentifierBefore(GS1Lexer::END, getText()); } -> type(TOKEN_BRACE_RIGHT); +PARAM_C_POP_PAREN_RIGHT : TOKEN_PAREN_RIGHT { canFuncPop() }? { popNextMode(true); } -> type(TOKEN_PAREN_RIGHT); +PARAM_C_POP_END : END { canCmdPop() }? { popNextMode(true); } -> type(END); +PARAM_C_POP_COMMA : TOKEN_COMMA { popNextMode(); } -> type(TOKEN_COMMA); +PARAM_C_COLOR : COLORS -> type(COLOR); + +// -------------------------------------------------------- +mode IN_PARAM_G; + +PARAM_G_WS : WHITESPACE+ -> type(WS), channel(HIDDEN); +PARAM_G_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { canCmdPop() }? { popNextMode(true); emitIdentifierBefore(GS1Lexer::END, getText()); } -> type(TOKEN_BRACE_RIGHT); +PARAM_G_POP_PAREN_RIGHT : TOKEN_PAREN_RIGHT { canFuncPop() }? { popNextMode(true); } -> type(TOKEN_PAREN_RIGHT); +PARAM_G_POP_END : END { canCmdPop() }? { popNextMode(true); } -> type(END); +PARAM_G_POP_COMMA : TOKEN_COMMA { popNextMode(); } -> type(TOKEN_COMMA); +PARAM_G_GENDER : GENDERS -> type(GENDER); + +// -------------------------------------------------------- +mode IN_PARAM_U; + +PARAM_U_WS : WHITESPACE+ -> type(WS), channel(HIDDEN); +PARAM_U_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { canCmdPop() }? { popNextMode(true); emitIdentifierBefore(GS1Lexer::END, getText()); } -> type(TOKEN_BRACE_RIGHT); +PARAM_U_POP_PAREN_RIGHT : TOKEN_PAREN_RIGHT { canFuncPop() }? { popNextMode(true); } -> type(TOKEN_PAREN_RIGHT); +PARAM_U_POP_END : END { canCmdPop() }? { popNextMode(true); } -> type(END); +PARAM_U_POP_COMMA : TOKEN_COMMA { popNextMode(); } -> type(TOKEN_COMMA); +PARAM_U_CARRY : CARRYNAMES -> type(CARRY); + +// -------------------------------------------------------- +mode IN_PARAM_D; + +PARAM_D_WS : WHITESPACE+ -> type(WS), channel(HIDDEN); +PARAM_D_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { canCmdPop() }? { popNextMode(true); emitIdentifierBefore(GS1Lexer::END, getText()); } -> type(TOKEN_BRACE_RIGHT); +PARAM_D_POP_PAREN_RIGHT : TOKEN_PAREN_RIGHT { canFuncPop() }? { popNextMode(true); } -> type(TOKEN_PAREN_RIGHT); +PARAM_D_POP_END : END { canCmdPop() }? { popNextMode(true); } -> type(END); +PARAM_D_POP_COMMA : TOKEN_COMMA { popNextMode(); } -> type(TOKEN_COMMA); +PARAM_D_DIR : DIR -> type(DIRECTION); +PARAM_D_FUNC_GROUP_1 : FUNC_GROUP_1 { pushCommand("(P)"); } -> type(FUNCTION); +PARAM_D_FUNC_GROUP_2 : FUNC_GROUP_2 { pushCommand("(S)"); } -> type(FUNCTION); +PARAM_D_FUNC_GROUP_3 : FUNC_GROUP_3 { pushCommand("(SS)"); } -> type(FUNCTION); +PARAM_D_FUNC_GROUP_4 : FUNC_GROUP_4 { pushCommand("(ESSS)"); } -> type(FUNCTION); +PARAM_D_FUNC_GROUP_5 : FUNC_GROUP_5 { pushCommand("(SV)"); } -> type(FUNCTION); +PARAM_D_FUNC_GROUP_6 : FUNC_GROUP_6 { pushCommand("(V)"); } -> type(FUNCTION); +PARAM_D_FUNC_GROUP_7 : FUNC_GROUP_7 { pushCommand(" type(FUNCTION); +PARAM_D_LITERAL : LITERAL -> type(LITERAL); +PARAM_D_IDENTIFIER : IDENTIFIER -> type(IDENTIFIER); +PARAM_D_OP_ASSIGN : OP_ASSIGN -> type(OP_ASSIGN); +PARAM_D_OP_ADD : OP_ADD -> type(OP_ADD); +PARAM_D_OP_SUB : OP_SUB -> type(OP_SUB); +PARAM_D_OP_MUL : OP_MUL -> type(OP_MUL); +PARAM_D_OP_DIV : OP_DIV -> type(OP_DIV); +PARAM_D_OP_MOD : OP_MOD -> type(OP_MOD); +PARAM_D_OP_POW : OP_POW -> type(OP_POW); +PARAM_D_OP_EQUAL : OP_EQUAL -> type(OP_EQUAL); +PARAM_D_OP_NOTEQ : OP_NOTEQ -> type(OP_NOTEQ); +PARAM_D_OP_LESS : OP_LESS -> type(OP_LESS); +PARAM_D_OP_GREAT : OP_GREAT -> type(OP_GREAT); +PARAM_D_OP_LESS_EQ : OP_LESS_EQ -> type(OP_LESS_EQ); +PARAM_D_OP_GREAT_EQ : OP_GREAT_EQ -> type(OP_GREAT_EQ); +PARAM_D_OP_IN : OP_IN -> type(OP_IN); +PARAM_D_OP_INC : OP_INC -> type(OP_INC); +PARAM_D_OP_DEC : OP_DEC -> type(OP_DEC); +PARAM_D_OP_LOGICALAND : OP_LOGICALAND -> type(OP_LOGICALAND); +PARAM_D_OP_LOGICALOR : OP_LOGICALOR -> type(OP_LOGICALOR); +PARAM_D_OP_LOGICALNOT : OP_LOGICALNOT -> type(OP_LOGICALNOT); +PARAM_D_TOKEN_BRACKET_LEFT : TOKEN_BRACKET_LEFT { pushArrayAccess(); } -> type(TOKEN_BRACKET_LEFT); +PARAM_D_TOKEN_PAREN_LEFT : TOKEN_PAREN_LEFT -> type(TOKEN_PAREN_LEFT); +PARAM_D_TOKEN_PAREN_RIGHT : TOKEN_PAREN_RIGHT -> type(TOKEN_PAREN_RIGHT); +PARAM_D_TOKEN_PIPE : TOKEN_PIPE -> type(TOKEN_PIPE); +PARAM_D_TOKEN_QUESTION : TOKEN_QUESTION -> type(TOKEN_QUESTION); +PARAM_D_TOKEN_COLON : TOKEN_COLON -> type(TOKEN_COLON); +PARAM_D_TOKEN_PERIOD : TOKEN_PERIOD -> type(TOKEN_PERIOD); + +// -------------------------------------------------------- +mode IN_PARAM_X; + +PARAM_X_IDENTIFIER : IDENTIFIER { popNextMode(); } -> type(IDENTIFIER); +PARAM_X_MC_NOINDEX : MC_NOINDEX -> type(MESSAGECODE); +PARAM_X_MC_SIMPLE : MC_SIMPLE { pushCommand("(P)"); } -> type(MESSAGECODE); +PARAM_X_MC_COMPUTED_S : MC_COMPUTED_S { pushCommand("(V)"); } -> type(MESSAGECODE); +PARAM_X_MC_COMPUTED_V : MC_COMPUTED_V { pushCommand("(E)"); } -> type(MESSAGECODE); +PARAM_X_MC_I : MC_I { pushCommand("(VP)"); } -> type(MESSAGECODE); +PARAM_X_MC_T : MC_T { pushCommand("(S)"); } -> type(MESSAGECODE); +PARAM_X_MC_U : MC_U { pushCommand("(R)"); } -> type(MESSAGECODE); +PARAM_X_MC_e : MC_e { pushCommand("(EES)"); } -> type(MESSAGECODE); +PARAM_X_MC_i : MC_i { pushCommand("(SP)"); } -> type(MESSAGECODE); +PARAM_X_MC_R : MC_R { pushCommand("(L)"); } -> type(MESSAGECODE); +PARAM_X_MC_Q : MC_Q { pushCommand("(SS)"); } -> type(MESSAGECODE); + +// -------------------------------------------------------- +mode IN_PARAM_Z; + +PARAM_Z_START : TOKEN_BRACE_LEFT { m_braceCount == 0 }? { ++m_braceCount; } -> channel(HIDDEN); +PARAM_Z_POP_END : END { m_braceCount == 0 }? { popNextMode(); } -> type(END); +PARAM_Z_POP_BRACE_RIGHT : TOKEN_BRACE_RIGHT { m_braceCount == 1 }? { --m_braceCount; popNextMode(); emitIdentifierBefore(GS1Lexer::END, getText()); } -> channel(HIDDEN); +PARAM_Z_BRACE_LEFT : TOKEN_BRACE_LEFT { ++m_braceCount; } -> type(STRING); +PARAM_Z_BRACE_RIGHT : TOKEN_BRACE_RIGHT { --m_braceCount; } -> type(STRING); +PARAM_Z_END : END { m_braceCount != 0 }? -> type(STRING); +PARAM_Z_STRING : ~[{};]+ -> type(STRING); + +// -------------------------------------------------------- +mode IN_PARAM_1; + +PARAM_1_WS : WHITESPACE+ -> type(WS), channel(HIDDEN); +PARAM_1_TOKEN_PAREN_LEFT : TOKEN_PAREN_LEFT { m_commandStates.back().popMode = POPMODE_FUNCTION; popNextMode(); } -> type(TOKEN_PAREN_LEFT); + +// -------------------------------------------------------- +mode IN_PARAM_2; + +PARAM_2_WS : WHITESPACE+ -> type(WS), channel(HIDDEN); +PARAM_2_TOKEN_PAREN_RIGHT : TOKEN_PAREN_RIGHT { popNextMode(); } -> type(TOKEN_PAREN_RIGHT); + +// -------------------------------------------------------- +mode IN_PARAM_3; + +PARAM_3_WS : WHITESPACE+ -> type(WS), channel(HIDDEN); +PARAM_3_TOKEN_PAREN_LEFT : TOKEN_PAREN_LEFT { m_commandStates.back().popMode = POPMODE_FUNCTION; checkIfNextModeOptional(); } -> type(TOKEN_PAREN_LEFT); diff --git a/server/src/scripting/gs1/grammar/GS1Parser.g4 b/server/src/scripting/gs1/grammar/GS1Parser.g4 new file mode 100644 index 000000000..bbce46e12 --- /dev/null +++ b/server/src/scripting/gs1/grammar/GS1Parser.g4 @@ -0,0 +1,277 @@ +parser grammar GS1Parser; + +options +{ + tokenVocab=GS1Lexer; +} + +@parser::header +{ +// -------------------------------------------------------- +#include +#include +#include +#include +#include +// -------------------------------------------------------- +} + +@parser::context +{ +// -------------------------------------------------------- +struct string_hash +{ + using hash_type = std::hash; + using is_transparent = void; + + [[nodiscard]] size_t operator()(const char* str) const noexcept + { + return hash_type{}(str); + } + [[nodiscard]] size_t operator()(const std::string_view& str) const noexcept + { + return hash_type{}(str); + } + [[nodiscard]] size_t operator()(const std::string& str) const noexcept + { + return hash_type{}(str); + } + [[nodiscard]] size_t operator()(const size_t& hash) const noexcept + { + return hash; + } +}; +// -------------------------------------------------------- +} + +@parser::members +{ +// -------------------------------------------------------- +std::map userFunctions; +std::unordered_set> identifiers; +// -------------------------------------------------------- +void add_user_function(std::string funcName, antlr4::tree::ParseTree* treeNode) +{ + userFunctions.insert_or_assign(funcName, treeNode); +} +void add_identifier(std::string identifier) +{ + identifiers.insert(identifier); +} +// -------------------------------------------------------- +} + +program + : EOF + | block+ + ; + +block + : TOKEN_BRACE_LEFT statement* TOKEN_BRACE_RIGHT + | statement + ; + +statement + : END + | ( ifStatement + | forStatement + | whileStatement + | withStatement + | functionDefinition + ) + | ( flowStatement + | builtinCommandStatement + | userFunctionStatement + | assignmentStatement + | expression + ) (END | EOF) + ; + +//---------------------------------------------------------- + +ifStatement + : KW_IF TOKEN_PAREN_LEFT expression TOKEN_PAREN_RIGHT block (KW_ELSE block)? # StatementIf + ; + +forStatement + : KW_FOR TOKEN_PAREN_LEFT + assignmentStatement? END + expression? END + (assignmentStatement | expression)? TOKEN_PAREN_RIGHT + block # StatementFor + ; + +whileStatement + : KW_WHILE TOKEN_PAREN_LEFT expression TOKEN_PAREN_RIGHT block # StatementWhile + ; + +withStatement + : KW_WITH TOKEN_PAREN_LEFT expression TOKEN_PAREN_RIGHT block # StatementWith + ; + +flowStatement + : KW_RETURN # FlowReturn + | KW_BREAK # FlowBreak + | KW_CONTINUE # FlowContinue + ; + +//---------------------------------------------------------- + +functionDefinition + : KW_FUNCTION compound_identifier TOKEN_PAREN_LEFT TOKEN_PAREN_RIGHT block + { add_user_function($compound_identifier.ctx->getText(), $block.ctx); } # StatementFunctionDefinition + ; + +//---------------------------------------------------------- + +userFunctionStatement + : compound_identifier TOKEN_PAREN_LEFT TOKEN_PAREN_RIGHT # StatementUserFunctionCall + ; + +//---------------------------------------------------------- + +builtinCommandStatement + : COMMAND builtInCommandExpression (TOKEN_COMMA builtInCommandExpression)* # StatementBuiltInCommand + | COMMAND # StatementBuiltInCommand + ; + +builtInCommandExpression + : special_literal + | expression + ; + +//---------------------------------------------------------- + +assignmentStatement + : identifier_access assignment_operator expression # StatementAssignment + ; + +//---------------------------------------------------------- + +expression + : logicalOrExpression (TOKEN_QUESTION expression TOKEN_COLON expression)* # ExpressionTernary + ; + +logicalOrExpression + : logicalAndExpression (OP_LOGICALOR logicalAndExpression)* # ExpressionLogicOr + ; + +logicalAndExpression + : equalityExpression (OP_LOGICALAND equalityExpression)* # ExpressionLogicAnd + ; + +equalityExpression + : relationalExpression ((OP_EQUAL | OP_ASSIGN | OP_NOTEQ) relationalExpression)? # ExpressionEquality + ; + +relationalExpression + : additiveExpression + ((OP_LESS | OP_GREAT | OP_LESS_EQ | OP_GREAT_EQ) additiveExpression)? # ExpressionRelational + ; + +additiveExpression + : multiplicativeExpression ((OP_ADD | OP_SUB) (LITERAL | multiplicativeExpression))* # ExpressionAdditive + ; + +multiplicativeExpression + : inExpression ((OP_MUL | OP_DIV | OP_MOD) (LITERAL | inExpression))* # ExpressionMultiplicative + ; + +inExpression + : exponentiationExpression + ((TOKEN_COMMA exponentiationExpression)* + OP_IN (range_literal | primaryExpression))? # ExpressionIn + ; + +exponentiationExpression + : unaryExpression (OP_POW unaryExpression)* # ExpressionExponentiation + ; + +unaryExpression + : (OP_ADD | OP_SUB | OP_LOGICALNOT) unaryExpression # ExpressionUnary + | postfixExpression # ignoreExpressionUnaryPrimary + ; + +postfixExpression + : primaryExpression (OP_INC | OP_DEC) # ExpressionPostfix + | primaryExpression # ignoreExpressionPostfixPrimary + ; + +primaryExpression + : TOKEN_PAREN_LEFT expression TOKEN_PAREN_RIGHT + | RAWMESSAGECODE messagecode_string + | builtin_function + | array_literal + | literal_literal + | identifier_access + | compound_string + ; + +//---------------------------------------------------------- + +builtin_function + : FUNCTION TOKEN_PAREN_LEFT expression (TOKEN_COMMA expression)* TOKEN_PAREN_RIGHT + { if ($FUNCTION->getText().starts_with("playersays")) add_identifier("playerchats"); + } # BuiltInFunctionCall + ; + +identifier_access + : identifier_value (TOKEN_PERIOD identifier_value)* # IdentifierAccess + ; + +identifier_value + : compound_identifier + (TOKEN_BRACKET_LEFT + expression (TOKEN_COMMA expression)? + TOKEN_BRACKET_RIGHT)? + { add_identifier($compound_identifier.ctx->getText()); } # IdentifierValue + ; + +compound_identifier + : (IDENTIFIER | messagecode_string | REAL | TOKEN_PERIOD)+ # CompoundIdentifier + ; + +compound_string + : (STRING | messagecode_string)+ # CompoundString + ; + +messagecode_string + : MESSAGECODE + (TOKEN_PAREN_LEFT expression (TOKEN_COMMA expression)* TOKEN_PAREN_RIGHT)? # MessageCode + ; + +//---------------------------------------------------------- + +assignment_operator + : ( OP_ASSIGN + | OP_ASSIGN_MUL + | OP_ASSIGN_DIV + | OP_ASSIGN_MOD + | OP_ASSIGN_ADD + | OP_ASSIGN_SUB + | OP_ASSIGN_POW + ) + ; + +array_literal + : TOKEN_BRACE_LEFT (TOKEN_COMMA | expression)* END? TOKEN_BRACE_RIGHT # ArrayLiteral + ; + +literal_literal + : ( LITERAL + | ALLFEATURES + | ALLSTATS ) # Literal + ; + +range_literal + : (TOKEN_PIPE | OP_LESS) expression TOKEN_COMMA expression (TOKEN_PIPE | OP_GREAT) # RangeLiteral + ; + +special_literal + : ITEM # ItemLiteral + | CARRY # CarryLiteral + | DIRECTION # DirectionLiteral + | GENDER # GenderLiteral + | COLOR # ColorLiteral + | BADDY # BaddyLiteral + ; diff --git a/server/src/scripting/gs2/ScriptEngineGS2.cpp b/server/src/scripting/gs2/ScriptEngineGS2.cpp new file mode 100644 index 000000000..5c48a17b2 --- /dev/null +++ b/server/src/scripting/gs2/ScriptEngineGS2.cpp @@ -0,0 +1,86 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::gs2 +{ +/////////////////////////////////////////////////////////////////////////////// + +CompiledScriptResult ScriptEngineGS2::compileScript(std::string_view who, std::string_view script) +{ + // Compile the script. + auto result = m_scriptManager.compileScript(std::string{ script }); + auto response = result.get(); + + // Error. + if (!response.success) + return CompiledScriptResult{ string::join(response.errors | std::views::transform([this](const GS2CompilerError& error) -> std::string { return handleGS2Error(error); }), "\n") }; + + // Construct the compilation result. + ScriptExecutionContext scriptContext{ .engine = this }; + for (const auto& joinedClass : response.joinedClasses) + { + // TODO: Get class. + using p = decltype(scriptContext.joinedClasses)::value_type; + scriptContext.joinedClasses.insert(p(joinedClass, {})); + } + + // Generate the bytecode. + std::vector bytecode; + bytecode.insert(bytecode.end(), response.bytecode.buffer(), response.bytecode.buffer() + response.bytecode.length()); + + // Wrap the bytecode. + auto wrapper = std::make_any>(std::move(bytecode)); + scriptContext.script = std::make_shared(std::move(wrapper)); + + // Return the context. + return CompiledScriptResult{ std::move(scriptContext) }; +} + +std::string ScriptEngineGS2::handleGS2Error(const GS2CompilerError& error) +{ + std::string errorMsg; + switch (error.level()) + { + case ErrorLevel::E_INFO: + errorMsg += std::format("info: {}", error.msg()); + break; + case ErrorLevel::E_WARNING: + errorMsg += std::format("warning: {}", error.msg()); + break; + default: + errorMsg += std::format("error: {}", error.msg()); + break; + } + + if (!errorMsg.empty()) + reportScriptException(std::format("Script compiler output:\n{}", errorMsg)); + + return errorMsg; +} + +void ScriptEngineGS2::reportScriptException(const std::string& error_message) +{ + m_server->sendToNC(error_message); + log::printLine(log::script, error_message); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::gs2 diff --git a/server/src/scripting/v8/V8EnvironmentImpl.cpp b/server/src/scripting/v8/V8EnvironmentImpl.cpp deleted file mode 100644 index e774f0a76..000000000 --- a/server/src/scripting/v8/V8EnvironmentImpl.cpp +++ /dev/null @@ -1,133 +0,0 @@ -#ifdef V8NPCSERVER - - #include - #include - #include - #include - - #include "NPC.h" - #include "Server.h" - #include "scripting/ScriptEngine.h" - #include "scripting/v8/V8ScriptFunction.h" - #include "scripting/v8/V8ScriptObject.h" - -// PROPERTY: env.global -void Environment_GetObject_Global(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Local data = info.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - - info.GetReturnValue().Set(env->global()); -} - -void Environment_ReportException(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1) - - SCRIPTENV_D("Begin Environment::reportException()\n"); - - if (args[0]->IsString()) - { - // Unwrap Object - Server* serverObject = unwrapObject(args.This()); - - // Report exception to server - std::string message = *v8::String::Utf8Value(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - serverObject->reportScriptException(message); - } - - SCRIPTENV_D("End Environment::reportException()\n\n"); -} - -void Environment_SetCallBack(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 2) - - SCRIPTENV_D("Begin Environment::setCallBack()\n"); - - if (args[0]->IsString() && args[1]->IsFunction()) - { - SCRIPTENV_D(" - Set callback for %s with: %s\n", - *v8::String::Utf8Value(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()), - *v8::String::Utf8Value(isolate, args[1]->ToString(isolate->GetCurrentContext()).ToLocalChecked())); - - v8::Local data = args.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - - // Callback name - std::string eventName = *v8::String::Utf8Value(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - - // Persist the callback function so we can retrieve it later on - v8::Local cbFunc = args[1].As(); - V8ScriptFunction* cbFuncWrapper = new V8ScriptFunction(env, cbFunc); - - scriptEngine->setCallBack(eventName, cbFuncWrapper); - } - - SCRIPTENV_D("End Environment::setCallBack()\n\n"); -} - -void Environment_SetNpcEvents(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 2) - - SCRIPTENV_D("Begin Environment::setNpcEvents()\n"); - - if (args[0]->IsObject() && args[1]->IsInt32()) - { - v8::Local context = args.GetIsolate()->GetCurrentContext(); - v8::Local obj = args[0]->ToObject(context).ToLocalChecked(); - - std::string npcConstructor = *v8::String::Utf8Value(isolate, obj->GetConstructorName()); - if (npcConstructor == "npc") - { - NPC* npcObject = unwrapObject(obj); - npcObject->setScriptEvents(args[1]->Int32Value(context).ToChecked()); - } - } - - SCRIPTENV_D("End Environment::setNpcEvents()\n\n"); -} - -void bindClass_Environment(ScriptEngine* scriptEngine) -{ - // Retrieve v8 environment - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - v8::Isolate* isolate = env->isolate(); - - // External pointer - v8::Local engine_ref = v8::External::New(isolate, scriptEngine); - - // Create V8 string for "Environment" - v8::Local envStr = v8::String::NewFromUtf8Literal(isolate, "Environment", v8::NewStringType::kInternalized); - - // Create constructor for class - v8::Local environment_ctor = v8::FunctionTemplate::New(isolate); - v8::Local environment_proto = environment_ctor->PrototypeTemplate(); - environment_ctor->SetClassName(envStr); - environment_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - // Properties - environment_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "global"), Environment_GetObject_Global, nullptr, engine_ref); - - // Method functions - environment_proto->Set(v8::String::NewFromUtf8Literal(isolate, "reportException"), v8::FunctionTemplate::New(isolate, Environment_ReportException, engine_ref)); - environment_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setCallBack"), v8::FunctionTemplate::New(isolate, Environment_SetCallBack, engine_ref)); - environment_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setNpcEvents"), v8::FunctionTemplate::New(isolate, Environment_SetNpcEvents, engine_ref)); - - // Persist the constructor - env->setConstructor("environment", environment_ctor); -} - -#endif diff --git a/server/src/scripting/v8/V8FunctionsImpl.cpp b/server/src/scripting/v8/V8FunctionsImpl.cpp deleted file mode 100644 index 2f4905962..000000000 --- a/server/src/scripting/v8/V8FunctionsImpl.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#ifdef V8NPCSERVER - - #include - #include - - #include "Server.h" - #include "scripting/ScriptEngine.h" - #include "scripting/v8/V8ScriptEnv.h" - #include "scripting/v8/V8ScriptFunction.h" - -// Global Method: print(arg0, arg1, arg2, arg3) [no format atm]; -void Global_Function_Print(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Print call - for (int i = 0; i < args.Length(); i++) - { - v8::HandleScope handle_scope(isolate); - if (i > 0) - printf(" "); - - v8::String::Utf8Value str(isolate, args[i]); - printf("%s", *str); - } - printf("\n"); - fflush(stdout); -} - -// PROPERTY: server object -void Global_GetObject_Server(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Local data = info.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - - V8ScriptObject* v8_serverObject = static_cast*>(scriptEngine->getServerObject()); - info.GetReturnValue().Set(v8_serverObject->handle(info.GetIsolate())); -} - -void bindGlobalFunctions(ScriptEngine* scriptEngine) -{ - // Retrieve v8 environment - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - v8::Isolate* isolate = env->isolate(); - - // External pointer - v8::Local engine_ref = v8::External::New(isolate, scriptEngine); - - // Fetch global template - v8::Local global = env->globalTemplate(); - - // Global functions - global->Set(v8::String::NewFromUtf8Literal(isolate, "print"), v8::FunctionTemplate::New(isolate, Global_Function_Print)); - //global->Set(v8::String::NewFromUtf8(isolate, "testFunc"), v8::FunctionTemplate::New(isolate, Ext_TestFunc, engine_ref)); - - // Global properties - global->Set(v8::String::NewFromUtf8Literal(isolate, "global"), v8::ObjectTemplate::New(isolate), static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete)); - global->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "server"), Global_GetObject_Server, nullptr, engine_ref); -} - -#endif diff --git a/server/src/scripting/v8/V8LevelChestImpl.cpp b/server/src/scripting/v8/V8LevelChestImpl.cpp deleted file mode 100644 index 2c12a5f7d..000000000 --- a/server/src/scripting/v8/V8LevelChestImpl.cpp +++ /dev/null @@ -1,117 +0,0 @@ -#ifdef V8NPCSERVER - - #include - #include - #include - #include - #include - - #include "NPC.h" - #include "Player.h" - #include "level/Level.h" - #include "level/LevelChest.h" - #include "level/Map.h" - #include "scripting/ScriptEngine.h" - #include "scripting/v8/V8ScriptFunction.h" - #include "scripting/v8/V8ScriptObject.h" - -// PROPERTY: chest.x -void Chest_GetNum_X(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelChest, chestObject); - - info.GetReturnValue().Set(chestObject->getX()); -} - -void Chest_SetNum_X(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelChest, chestObject); - - int newValue = (int)value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - - chestObject->setX(newValue); -} - -// PROPERTY: chest.y -void Chest_GetNum_Y(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelChest, chestObject); - - info.GetReturnValue().Set(chestObject->getY()); -} - -void Chest_SetNum_Y(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelChest, chestObject); - - int newValue = (int)value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - - chestObject->setY(newValue); -} - -// PROPERTY: chest.itemtype -void Chest_GetNum_ItemType(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelChest, chestObject); - - info.GetReturnValue().Set((int)chestObject->getItemIndex()); -} - -void Chest_SetNum_ItemType(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelChest, chestObject); - - int newValue = (int)value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - - chestObject->setItemIndex(newValue); -} - -// PROPERTY: chest.signid -void Chest_GetNum_SignId(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelChest, chestObject); - - info.GetReturnValue().Set((int)chestObject->getSignIndex()); -} - -void Chest_SetNum_SignId(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelChest, chestObject); - - int newValue = (int)value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - - chestObject->setSignIndex(newValue); -} - -void bindClass_LevelChest(ScriptEngine* scriptEngine) -{ - // Retrieve v8 environment - auto* env = dynamic_cast(scriptEngine->getScriptEnv()); - v8::Isolate* isolate = env->isolate(); - - // External pointer - v8::Local engine_ref = v8::External::New(isolate, scriptEngine); - - // Create V8 string for "level" - v8::Local chestStr = v8::String::NewFromUtf8Literal(isolate, "chest", v8::NewStringType::kInternalized); - - // Create constructor for class - v8::Local chest_ctor = v8::FunctionTemplate::New(isolate); - v8::Local chest_proto = chest_ctor->PrototypeTemplate(); - chest_ctor->SetClassName(chestStr); - chest_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - // Method functions - //link_proto->Set(v8::String::NewFromUtf8Literal(isolate, "clone"), v8::FunctionTemplate::New(isolate, Level_Function_Clone, engine_ref)); - - // Properties - chest_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "x"), Chest_GetNum_X, Chest_SetNum_X); - chest_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "y"), Chest_GetNum_Y, Chest_SetNum_Y); - chest_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "itemtype"), Chest_GetNum_ItemType, Chest_SetNum_ItemType); - chest_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "signid"), Chest_GetNum_SignId, Chest_SetNum_SignId); - - // Persist the constructor - env->setConstructor(ScriptConstructorId::result, chest_ctor); -} - -#endif diff --git a/server/src/scripting/v8/V8LevelImpl.cpp b/server/src/scripting/v8/V8LevelImpl.cpp deleted file mode 100644 index 0366fd5a4..000000000 --- a/server/src/scripting/v8/V8LevelImpl.cpp +++ /dev/null @@ -1,1552 +0,0 @@ -#ifdef V8NPCSERVER - - #include - #include - #include - #include - - #include "BabyDI.h" - #include "NPC.h" - #include "Player.h" - #include "level/Level.h" - #include "level/Map.h" - #include "scripting/ScriptEngine.h" - #include "scripting/v8/V8ScriptFunction.h" - #include "scripting/v8/V8ScriptObject.h" - -// PROPERTY: level.issparringzone -void Level_GetBool_IsSparringZone(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - info.GetReturnValue().Set(levelObject->isSparringZone()); -} - -// PROPERTY: level.name -void Level_GetStr_Name(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), levelObject->getLevelName().text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -// PROPERTY: level.mapname -void Level_GetStr_MapName(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - auto map = levelObject->getMap(); - if (map) - { - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), map->getMapName().c_str()).ToLocalChecked(); - info.GetReturnValue().Set(strText); - return; - } - - info.GetReturnValue().SetNull(); -} - -// PROPERTY: level.signs -void Level_GetObject_Signs(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalSigns = v8::String::NewFromUtf8(isolate, "_internalSigns", v8::NewStringType::kInternalized).ToLocalChecked(); - if (self->HasRealNamedProperty(context, internalSigns).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalSigns).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Grab external data - v8::Local data = info.Data().As(); - auto* scriptEngine = static_cast(data->Value()); - auto* env = dynamic_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("level.signs"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, levelObject); - - // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed - auto* v8_wrapped = dynamic_cast*>(levelObject->getScriptObject()); - v8_wrapped->addChild("signs", new_instance); - - auto propLinks = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalSigns, new_instance, propLinks).FromJust(); - - info.GetReturnValue().Set(new_instance); -} - -void Level_Sign_Getter(uint32_t index, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - v8::Isolate* isolate = info.GetIsolate(); - - const auto& sign = levelObject->getSigns()[index]; - - auto* v8_wrapped = dynamic_cast*>(sign->getScriptObject()); - - info.GetReturnValue().Set(v8_wrapped->handle(isolate)); -} - -void Level_Sign_Length(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - v8::Isolate* isolate = info.GetIsolate(); - - auto signSize = levelObject->getSigns().size(); - - info.GetReturnValue().Set(v8::BigInt::NewFromUnsigned(isolate, signSize)); -} - -void Level_Sign_Enumerator(const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Get link list - auto& levelSigns = levelObject->getSigns(); - - v8::Local result = v8::Array::New(isolate, (int)levelSigns.size()); - - int idx = 0; - for (auto& sign: levelSigns) - { - result->Set(context, idx, v8::Number::New(isolate, idx)).Check(); - idx++; - } - - info.GetReturnValue().Set(result); -} - -void Level_Sign_Next(const v8::FunctionCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - // Get a reference to the instance of "level.links". - v8::Local obj = info.This(); - - // Get the current index from the iterator object. - v8::Local currentIndex = obj->GetInternalField(0).As(); - v8::Local items = obj->GetInternalField(1).As(); - - // Get the length of the array. - uint32_t len = items->Length(); - - // Check if we have reached the end of the iteration sequence. - if (currentIndex->Value() >= len) - { - auto newObj = v8::Object::New(isolate); - newObj->Set(context, v8::String::NewFromUtf8Literal(isolate, "done"), v8::True(isolate)).Check(); - info.GetReturnValue().Set(newObj); - - return; - } - - // Get the value at the current index. - v8::Local value = items->Get(context, (uint32_t)currentIndex->Value()).ToLocalChecked(); - - // Update the iterator's index. - obj->SetInternalField(0, v8::Integer::New(isolate, (int32_t)currentIndex->Value() + 1)); - - // Create the next() result object. - v8::Local result = v8::Object::New(isolate); - result->Set(context, v8::String::NewFromUtf8Literal(isolate, "value"), value).Check(); - result->Set(context, v8::String::NewFromUtf8Literal(isolate, "done"), v8::False(isolate)).Check(); - - info.GetReturnValue().Set(result); -} - -void Level_Sign_Iterator(const v8::FunctionCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Get link list - auto& levelSigns = levelObject->getSigns(); - - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - v8::Local obj = info.This(); - - int current_index = 0; - - v8::Local test_ctor = v8::FunctionTemplate::New(isolate); - test_ctor->InstanceTemplate()->SetInternalFieldCount(2); - - v8::Local test_proto = test_ctor->PrototypeTemplate(); - - test_proto->Set(v8::String::NewFromUtf8Literal(isolate, "next"), v8::FunctionTemplate::New(isolate, Level_Sign_Next, obj)); - - v8::Local new_instance = test_ctor->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetInternalField(0, v8::Number::New(isolate, current_index)); - - // Adds child property to the wrapped object, so it can clear the pointer when - v8::Local result = v8::Array::New(isolate, (int)levelSigns.size()); - - int idx = 0; - for (auto& sign: levelSigns) - { - auto* v8_wrapped = dynamic_cast*>(sign->getScriptObject()); - result->Set(context, idx++, v8_wrapped->handle(isolate)).Check(); - } - - new_instance->SetInternalField(1, result); - - info.GetReturnValue().Set(new_instance); -} - -// Level Method: level.signs.add(x, y, signText) -void Level_Function_AddLevelSign(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 3); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber() && args[1]->IsNumber() && args[2]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - // Argument parsing - int levelX = (int)args[0]->NumberValue(context).ToChecked(); - int levelY = (int)args[1]->NumberValue(context).ToChecked(); - CString signText = *v8::String::Utf8Value(isolate, args[2]->ToString(context).ToLocalChecked()); - - auto newSign = levelObject->addSign(levelX, levelY, signText); - - auto* v8_wrapped = dynamic_cast*>(newSign->getScriptObject()); - args.GetReturnValue().Set(v8_wrapped->handle(isolate)); - } -} - -// Level Method: level.signs.remove(index) -void Level_Function_RemoveLevelSign(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - int index = (int)args[0]->NumberValue(context).ToChecked(); - - args.GetReturnValue().Set(levelObject->removeSign(index)); - } - - args.GetReturnValue().Set(false); -} - -// PROPERTY: level.chests -void Level_GetObject_Chests(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalChests = v8::String::NewFromUtf8(isolate, "_internalChests", v8::NewStringType::kInternalized).ToLocalChecked(); - if (self->HasRealNamedProperty(context, internalChests).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalChests).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Grab external data - v8::Local data = info.Data().As(); - auto* scriptEngine = static_cast(data->Value()); - auto* env = dynamic_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("level.chests"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, levelObject); - - // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed - auto* v8_wrapped = dynamic_cast*>(levelObject->getScriptObject()); - v8_wrapped->addChild("chests", new_instance); - - auto propLinks = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalChests, new_instance, propLinks).FromJust(); - - info.GetReturnValue().Set(new_instance); -} - -void Level_Chest_Getter(uint32_t index, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - v8::Isolate* isolate = info.GetIsolate(); - - const auto& chest = levelObject->getChests()[index]; - - auto* v8_wrapped = dynamic_cast*>(chest->getScriptObject()); - - info.GetReturnValue().Set(v8_wrapped->handle(isolate)); -} - -void Level_Chest_Length(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - v8::Isolate* isolate = info.GetIsolate(); - - auto chestSize = levelObject->getChests().size(); - - info.GetReturnValue().Set(v8::BigInt::NewFromUnsigned(isolate, chestSize)); -} - -void Level_Chest_Enumerator(const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Get link list - auto& levelChests = levelObject->getChests(); - - v8::Local result = v8::Array::New(isolate, (int)levelChests.size()); - - int idx = 0; - for (auto& sign: levelChests) - { - result->Set(context, idx, v8::Number::New(isolate, idx)).Check(); - idx++; - } - - info.GetReturnValue().Set(result); -} - -void Level_Chest_Next(const v8::FunctionCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - // Get a reference to the instance of "level.links". - v8::Local obj = info.This(); - - // Get the current index from the iterator object. - v8::Local currentIndex = obj->GetInternalField(0).As(); - v8::Local items = obj->GetInternalField(1).As(); - - // Get the length of the array. - uint32_t len = items->Length(); - - // Check if we have reached the end of the iteration sequence. - if (currentIndex->Value() >= len) - { - auto newObj = v8::Object::New(isolate); - newObj->Set(context, v8::String::NewFromUtf8Literal(isolate, "done"), v8::True(isolate)).Check(); - info.GetReturnValue().Set(newObj); - - return; - } - - // Get the value at the current index. - v8::Local value = items->Get(context, (uint32_t)currentIndex->Value()).ToLocalChecked(); - - // Update the iterator's index. - obj->SetInternalField(0, v8::Integer::New(isolate, (int32_t)currentIndex->Value() + 1)); - - // Create the next() result object. - v8::Local result = v8::Object::New(isolate); - result->Set(context, v8::String::NewFromUtf8Literal(isolate, "value"), value).Check(); - result->Set(context, v8::String::NewFromUtf8Literal(isolate, "done"), v8::False(isolate)).Check(); - - info.GetReturnValue().Set(result); -} - -void Level_Chest_Iterator(const v8::FunctionCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Get link list - auto& levelChests = levelObject->getChests(); - - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - v8::Local obj = info.This(); - - int current_index = 0; - - v8::Local test_ctor = v8::FunctionTemplate::New(isolate); - test_ctor->InstanceTemplate()->SetInternalFieldCount(2); - - v8::Local test_proto = test_ctor->PrototypeTemplate(); - - test_proto->Set(v8::String::NewFromUtf8Literal(isolate, "next"), v8::FunctionTemplate::New(isolate, Level_Chest_Next, obj)); - - v8::Local new_instance = test_ctor->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetInternalField(0, v8::Number::New(isolate, current_index)); - - // Adds child property to the wrapped object, so it can clear the pointer when - v8::Local result = v8::Array::New(isolate, (int)levelChests.size()); - - int idx = 0; - for (auto& chest: levelChests) - { - auto* v8_wrapped = dynamic_cast*>(chest->getScriptObject()); - result->Set(context, idx++, v8_wrapped->handle(isolate)).Check(); - } - - new_instance->SetInternalField(1, result); - - info.GetReturnValue().Set(new_instance); -} - -// Level Method: level.chests.add(x, y, itemType, signIndex) -void Level_Function_AddLevelChest(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 4); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber() && args[1]->IsNumber() && args[2]->IsNumber() && args[3]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - // Argument parsing - int levelX = (int)args[0]->NumberValue(context).ToChecked(); - int levelY = (int)args[1]->NumberValue(context).ToChecked(); - LevelItemType levelItemType = (LevelItemType)args[2]->NumberValue(context).ToChecked(); - int signId = (int)args[3]->NumberValue(context).ToChecked(); - - auto newChest = levelObject->addChest(levelX, levelY, levelItemType, signId); - - auto* v8_wrapped = dynamic_cast*>(newChest->getScriptObject()); - args.GetReturnValue().Set(v8_wrapped->handle(isolate)); - } -} - -// Level Method: level.chests.remove(index) -void Level_Function_RemoveLevelChest(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - int index = (int)args[0]->NumberValue(context).ToChecked(); - - args.GetReturnValue().Set(levelObject->removeChest(index)); - } - - args.GetReturnValue().Set(false); -} - -// PROPERTY: level.npcs -void Level_GetObject_Npcs(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalNpcs = v8::String::NewFromUtf8(isolate, "_internalNpcs", v8::NewStringType::kInternalized).ToLocalChecked(); - if (self->HasRealNamedProperty(context, internalNpcs).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalNpcs).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Grab external data - v8::Local data = info.Data().As(); - auto* scriptEngine = static_cast(data->Value()); - auto* env = dynamic_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("level.npcs"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, levelObject); - - // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed - auto* v8_wrapped = dynamic_cast*>(levelObject->getScriptObject()); - v8_wrapped->addChild("npcs", new_instance); - - auto propNpcs = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalNpcs, new_instance, propNpcs).FromJust(); - - info.GetReturnValue().Set(new_instance); -} - -void Level_Npc_Getter(uint32_t index, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - v8::Isolate* isolate = info.GetIsolate(); - auto& npcList = levelObject->getNPCs(); - auto* server = BabyDI::Get(); - - if (server && npcList.size() > index) - { - auto npcId = *std::next(npcList.begin(), index); - auto npc = server->getNPC(npcId); - auto* v8_wrapped = dynamic_cast*>(npc->getScriptObject()); - - info.GetReturnValue().Set(v8_wrapped->handle(isolate)); - } -} - -void Level_Npc_Length(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - v8::Isolate* isolate = info.GetIsolate(); - - auto npcSize = levelObject->getNPCs().size(); - - info.GetReturnValue().Set(v8::BigInt::NewFromUnsigned(isolate, npcSize)); -} - -void Level_Npc_Enumerator(const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Get link list - auto& levelNpcs = levelObject->getNPCs(); - - v8::Local result = v8::Array::New(isolate, (int)levelNpcs.size()); - - int idx = 0; - for (auto& npc: levelNpcs) - { - result->Set(context, idx, v8::Number::New(isolate, idx)).Check(); - idx++; - } - - info.GetReturnValue().Set(result); -} - -void Level_Npc_Next(const v8::FunctionCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - // Get a reference to the instance of "level.links". - v8::Local obj = info.This(); - - // Get the current index from the iterator object. - v8::Local currentIndex = obj->GetInternalField(0).As(); - v8::Local items = obj->GetInternalField(1).As(); - - // Get the length of the array. - uint32_t len = items->Length(); - - // Check if we have reached the end of the iteration sequence. - if (currentIndex->Value() >= len) - { - auto newObj = v8::Object::New(isolate); - newObj->Set(context, v8::String::NewFromUtf8Literal(isolate, "done"), v8::True(isolate)).Check(); - info.GetReturnValue().Set(newObj); - - return; - } - - // Get the value at the current index. - v8::Local value = items->Get(context, (uint32_t)currentIndex->Value()).ToLocalChecked(); - - // Update the iterator's index. - obj->SetInternalField(0, v8::Integer::New(isolate, (int32_t)currentIndex->Value() + 1)); - - // Create the next() result object. - v8::Local result = v8::Object::New(isolate); - result->Set(context, v8::String::NewFromUtf8Literal(isolate, "value"), value).Check(); - result->Set(context, v8::String::NewFromUtf8Literal(isolate, "done"), v8::False(isolate)).Check(); - - info.GetReturnValue().Set(result); -} - -void Level_Npc_Iterator(const v8::FunctionCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Get link list - auto& levelNpcs = levelObject->getNPCs(); - auto* server = BabyDI::Get(); - - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - v8::Local obj = info.This(); - - int current_index = 0; - - v8::Local test_ctor = v8::FunctionTemplate::New(isolate); - test_ctor->InstanceTemplate()->SetInternalFieldCount(2); - - v8::Local test_proto = test_ctor->PrototypeTemplate(); - - test_proto->Set(v8::String::NewFromUtf8Literal(isolate, "next"), v8::FunctionTemplate::New(isolate, Level_Npc_Next, obj)); - - v8::Local new_instance = test_ctor->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetInternalField(0, v8::Number::New(isolate, current_index)); - - // Adds child property to the wrapped object, so it can clear the pointer when - v8::Local result = v8::Array::New(isolate, (int)levelNpcs.size()); - - int idx = 0; - for (auto& npcId: levelNpcs) - { - auto npc = server->getNPC(npcId); - auto* v8_wrapped = dynamic_cast*>(npc->getScriptObject()); - result->Set(context, idx++, v8_wrapped->handle(isolate)).Check(); - } - - new_instance->SetInternalField(1, result); - - info.GetReturnValue().Set(new_instance); -} - -// Level Method: level.npcs.add(x, y, script, options); -void Level_Function_AddLevelNpc(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 3); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber() && args[1]->IsNumber() && args[2]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - // Argument parsing - float npcX = (float)args[0]->NumberValue(context).ToChecked(); - float npcY = (float)args[1]->NumberValue(context).ToChecked(); - CString script = *v8::String::Utf8Value(isolate, args[2]->ToString(context).ToLocalChecked()); - - // TODO(joey): additional options parsing - if (args.Length() == 4) - { - } - - auto* server = BabyDI::Get(); - auto level = server->getLevel(levelObject->getLevelName().toString()); - - auto npc = server->addNPC("", script, npcX, npcY, level, true, true); - if (npc != nullptr) - { - npc->setScriptType("LOCALN"); - levelObject->addNPC(npc); - - auto* v8_wrapped = dynamic_cast*>(npc->getScriptObject()); - args.GetReturnValue().Set(v8_wrapped->handle(isolate)); - } - } -} - -// Level Method: level.npcs.remove(index) -void Level_Function_RemoveLevelNpc(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - int index = (int)args[0]->NumberValue(context).ToChecked(); - auto& npcList = levelObject->getNPCs(); - auto* server = BabyDI::Get(); - - if (server && npcList.size() > index) - { - auto npcId = *std::next(npcList.begin(), index); - args.GetReturnValue().Set(server->deleteNPC(npcId, true)); - return; - } - } - - args.GetReturnValue().Set(false); -} - -// PROPERTY: level.players -void Level_GetArray_Players(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Get npcs list - auto& playerList = levelObject->getPlayers(); - auto* server = BabyDI::Get(); - - v8::Local result = v8::Array::New(isolate, server ? (int)playerList.size() : 0); - - if (server) - { - int idx = 0; - for (auto it = playerList.begin(); it != playerList.end(); ++it) - { - auto player = server->getPlayer(*it); - V8ScriptObject* v8_wrapped = static_cast*>(player->getScriptObject()); - result->Set(context, idx++, v8_wrapped->handle(isolate)).Check(); - } - } - - info.GetReturnValue().Set(result); -} - -// PROPERTY: level.tiles -void Level_GetObject_Tiles(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalTiles = v8::String::NewFromUtf8(isolate, "_internalTiles", v8::NewStringType::kInternalized).ToLocalChecked(); - if (self->HasRealNamedProperty(context, internalTiles).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalTiles).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Grab external data - v8::Local data = info.Data().As(); - auto* scriptEngine = static_cast(data->Value()); - auto* env = dynamic_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("level.tiles"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, levelObject); - - // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed - auto* v8_wrapped = dynamic_cast*>(levelObject->getScriptObject()); - v8_wrapped->addChild("tiles", new_instance); - - auto propTiles = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalTiles, new_instance, propTiles).FromJust(); - info.GetReturnValue().Set(new_instance); -} - -void Level_Tile_Getter(uint32_t index, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - if (index > 4096) - return; - - v8::Isolate* isolate = info.GetIsolate(); - - auto tile = levelObject->getTiles()[index]; - - v8::Local tileValue = v8::Integer::New(isolate, tile); - info.GetReturnValue().Set(tileValue); -} - -void Level_Tile_Setter(uint32_t index, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - if (index > 4096) - return; - - v8::Isolate* isolate = info.GetIsolate(); - - // Get new value - if (value->IsUint32()) - { - // Get new value - unsigned int newValue = value->Uint32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - - levelObject->modifyBoardDirect(index, (short)newValue); - } - - // Needed to indicate we handled the request - info.GetReturnValue().Set(value); -} - -// PROPERTY: level.links -void Level_GetObject_Links(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalLinks = v8::String::NewFromUtf8(isolate, "_internalLinks", v8::NewStringType::kInternalized).ToLocalChecked(); - if (self->HasRealNamedProperty(context, internalLinks).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalLinks).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Grab external data - v8::Local data = info.Data().As(); - auto* scriptEngine = static_cast(data->Value()); - auto* env = dynamic_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("level.links"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, levelObject); - - // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed - auto* v8_wrapped = dynamic_cast*>(levelObject->getScriptObject()); - v8_wrapped->addChild("links", new_instance); - - auto propLinks = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalLinks, new_instance, propLinks).FromJust(); - - info.GetReturnValue().Set(new_instance); -} - -void Level_Link_Getter(uint32_t index, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - v8::Isolate* isolate = info.GetIsolate(); - - if (levelObject->getLinks().empty()) - { - return; - } - - const auto& link = levelObject->getLinks()[index]; - - auto* v8_wrapped = dynamic_cast*>(link->getScriptObject()); - - info.GetReturnValue().Set(v8_wrapped->handle(isolate)); -} - -void Level_Link_Length(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - v8::Isolate* isolate = info.GetIsolate(); - - auto linkSize = levelObject->getLinks().size(); - - info.GetReturnValue().Set(v8::BigInt::NewFromUnsigned(isolate, linkSize)); -} - -void Level_Link_Enumerator(const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Get link list - auto& levelLinks = levelObject->getLinks(); - - v8::Local result = v8::Array::New(isolate, (int)levelLinks.size()); - - int idx = 0; - for (auto& link: levelLinks) - { - result->Set(context, idx, v8::Number::New(isolate, idx)).Check(); - idx++; - } - - info.GetReturnValue().Set(result); -} - -void Level_Link_Next(const v8::FunctionCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - // Get a reference to the instance of "level.links". - v8::Local obj = info.This(); - - // Get the current index from the iterator object. - v8::Local currentIndex = obj->GetInternalField(0).As(); - v8::Local items = obj->GetInternalField(1).As(); - - // Get the length of the array. - uint32_t len = items->Length(); - - // Check if we have reached the end of the iteration sequence. - if (currentIndex->Value() >= len) - { - auto newObj = v8::Object::New(isolate); - newObj->Set(context, v8::String::NewFromUtf8Literal(isolate, "done"), v8::True(isolate)).Check(); - info.GetReturnValue().Set(newObj); - - return; - } - - // Get the value at the current index. - v8::Local value = items->Get(context, (uint32_t)currentIndex->Value()).ToLocalChecked(); - - // Update the iterator's index. - obj->SetInternalField(0, v8::Integer::New(isolate, (int32_t)currentIndex->Value() + 1)); - - // Create the next() result object. - v8::Local result = v8::Object::New(isolate); - result->Set(context, v8::String::NewFromUtf8Literal(isolate, "value"), value).Check(); - result->Set(context, v8::String::NewFromUtf8Literal(isolate, "done"), v8::False(isolate)).Check(); - - info.GetReturnValue().Set(result); -} - -void Level_Link_Iterator(const v8::FunctionCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Level, levelObject); - - // Get link list - auto& levelLinks = levelObject->getLinks(); - - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - v8::Local obj = info.This(); - - int current_index = 0; - - v8::Local test_ctor = v8::FunctionTemplate::New(isolate); - test_ctor->InstanceTemplate()->SetInternalFieldCount(2); - - v8::Local test_proto = test_ctor->PrototypeTemplate(); - - test_proto->Set(v8::String::NewFromUtf8Literal(isolate, "next"), v8::FunctionTemplate::New(isolate, Level_Link_Next, obj)); - - v8::Local new_instance = test_ctor->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetInternalField(0, v8::Number::New(isolate, current_index)); - - // Adds child property to the wrapped object, so it can clear the pointer when - v8::Local result = v8::Array::New(isolate, (int)levelLinks.size()); - - int idx = 0; - for (auto& link: levelLinks) - { - auto* v8_wrapped = dynamic_cast*>(link->getScriptObject()); - result->Set(context, idx++, v8_wrapped->handle(isolate)).Check(); - } - - new_instance->SetInternalField(1, result); - - info.GetReturnValue().Set(new_instance); -} - -// Level Method: level.links.add("dest.nw", x, y, width, height, newX, newY) -void Level_Function_AddLevelLink(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 7); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsString() && args[1]->IsNumber() && args[2]->IsNumber() && args[3]->IsNumber() && args[4]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - // Argument parsing - CString destination = *v8::String::Utf8Value(isolate, args[0]->ToString(context).ToLocalChecked()); - int levelX = (int)args[1]->NumberValue(context).ToChecked(); - int levelY = (int)args[2]->NumberValue(context).ToChecked(); - int width = (int)args[3]->NumberValue(context).ToChecked(); - int height = (int)args[4]->NumberValue(context).ToChecked(); - CString newX = *v8::String::Utf8Value(isolate, args[5]->ToString(context).ToLocalChecked()); - CString newY = *v8::String::Utf8Value(isolate, args[6]->ToString(context).ToLocalChecked()); - - auto newLevelLink = levelObject->addLink(); - newLevelLink->setNewLevel(destination); - newLevelLink->setX(levelX); - newLevelLink->setY(levelY); - newLevelLink->setWidth(width); - newLevelLink->setHeight(height); - newLevelLink->setNewX(newX); - newLevelLink->setNewY(newY); - - auto* v8_wrapped = dynamic_cast*>(newLevelLink->getScriptObject()); - args.GetReturnValue().Set(v8_wrapped->handle(isolate)); - } -} - -// Level Method: level.links.remove(index) -void Level_Function_RemoveLevelLink(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - int index = (int)args[0]->NumberValue(context).ToChecked(); - - args.GetReturnValue().Set(levelObject->removeLink(index)); - } - - args.GetReturnValue().Set(false); -} - -// Level Method: level.savelevel(levelname); -void Level_Function_SaveLevel(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - v8::Local context = args.GetIsolate()->GetCurrentContext(); - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - // Create level from user input - if (args[0]->IsString()) - { - std::string filename = *v8::String::Utf8Value(isolate, args[0]->ToString(context).ToLocalChecked()); - - if (levelObject != nullptr) - { - levelObject->saveLevel(filename); - } - } -} - -// Level Method: level.findareanpcs(x, y, width, height); -void Level_Function_FindAreaNpcs(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 4); - - // Unwrap Object - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - v8::Local context = isolate->GetCurrentContext(); - - // Argument parsing - int startX = (int)(16 * args[0]->NumberValue(context).ToChecked()); - int startY = (int)(16 * args[1]->NumberValue(context).ToChecked()); - int endX = 16 * args[2]->Int32Value(context).ToChecked(); - int endY = 16 * args[3]->Int32Value(context).ToChecked(); - - std::vector npcList = levelObject->findAreaNpcs(startX, startY, endX, endY); - - // Create array of objects - v8::Local result = v8::Array::New(isolate, (int)npcList.size()); - - int idx = 0; - for (auto npc: npcList) - { - V8ScriptObject* v8_wrapped = static_cast*>(npc->getScriptObject()); - result->Set(context, idx++, v8_wrapped->handle(isolate)).Check(); - } - - args.GetReturnValue().Set(result); -} - -// Level Method: level.findnearestplayers(x, y); -void Level_Function_FindNearestPlayers(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 2); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber() && args[1]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - // Argument parsing - float targetX = (float)args[0]->NumberValue(context).ToChecked(); - float targetY = (float)args[1]->NumberValue(context).ToChecked(); - - auto& playerList = levelObject->getPlayers(); - auto* server = BabyDI::Get(); - if (server == nullptr) [[unlikely]] - { - v8::Local result = v8::Array::New(isolate, (int)0); - args.GetReturnValue().Set(result); - return; - } - - // Get distance for each player in the level, and sort it - std::vector>> playerListSorted; - - for (auto plId: playerList) - { - auto pl = server->getPlayer(plId); - double distance = sqrt(pow(pl->getY() - targetY, 2) + pow(pl->getX() - targetX, 2)); - playerListSorted.emplace_back(distance, pl); - } - - std::sort(playerListSorted.begin(), playerListSorted.end()); - - // Create array of objects - v8::Local key_distance = v8::String::NewFromUtf8(isolate, "distance", v8::NewStringType::kInternalized).ToLocalChecked(); - v8::Local key_player = v8::String::NewFromUtf8(isolate, "player", v8::NewStringType::kInternalized).ToLocalChecked(); - v8::Local result = v8::Array::New(isolate, (int)playerListSorted.size()); - - int idx = 0; - for (auto& it: playerListSorted) - { - auto* v8_wrapped = static_cast*>(it.second->getScriptObject()); - - v8::Local object = v8::Object::New(isolate); - object->Set(context, key_distance, v8::Number::New(isolate, it.first)).Check(); - object->Set(context, key_player, v8_wrapped->handle(isolate)).Check(); - result->Set(context, idx++, object).Check(); - } - - args.GetReturnValue().Set(result); - } -} - -// Level Method: level.shoot(float x, float y, float z, float angle, float zangle, float strength, str ani, str aniparams); -void Level_Function_Shoot(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the minimum 8 arguments - V8ENV_THROW_MINARGCOUNT(args, isolate, 8); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber() && args[1]->IsNumber() && args[2]->IsNumber() && args[3]->IsNumber() && args[4]->IsNumber() && args[5]->IsNumber() && args[6]->IsString() && args[7]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - auto* server = BabyDI::Get(); - if (server == nullptr) return; - auto level = server->getLevel(levelObject->getLevelName().toString()); - - auto x = (float)args[0]->NumberValue(context).ToChecked(); - auto y = (float)args[1]->NumberValue(context).ToChecked(); - auto z = (float)args[2]->NumberValue(context).ToChecked(); - auto angle = (float)args[3]->NumberValue(context).ToChecked(); - auto zangle = (float)args[4]->NumberValue(context).ToChecked(); - auto strength = (float)args[5]->NumberValue(context).ToChecked(); - std::string ani = *v8::String::Utf8Value(isolate, args[6]->ToString(context).ToLocalChecked()); - - CString aniArgs; - for (int i = 7; i < args.Length(); i++) - { - aniArgs << (std::string)*v8::String::Utf8Value(isolate, args[i]->ToString(context).ToLocalChecked()) << "\n"; - } - aniArgs.gtokenizeI(); - - // Send the packet out. - server->sendShootToOneLevel(level, x, y, z, angle, zangle, strength, ani, aniArgs.text()); - } -} - -// Level Method: level.putexplosion(radius, x, y); -void Level_Function_PutExplosion(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 3); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber() && args[1]->IsNumber() && args[2]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - auto* server = BabyDI::Get(); - if (server == nullptr) return; - auto level = server->getLevel(levelObject->getLevelName().toString()); - - unsigned char eradius = args[0]->Int32Value(context).ToChecked(); - float loc[2] = { - (float)(args[1]->NumberValue(context).ToChecked()), - (float)(args[2]->NumberValue(context).ToChecked()) - }; - - unsigned char epower = 1; - - // Send the packet out. - CString packet = CString() >> (char)PLO_EXPLOSION >> (short)0 >> (char)eradius >> (char)(loc[0] * 2) >> (char)(loc[1] * 2) >> (char)epower; - server->sendPacketToOneLevel(packet, level); - } -} - -// Level Method: level.putnpc(x, y, script, options); -void Level_Function_PutNPC(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 3); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber() && args[1]->IsNumber() && args[2]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - // Argument parsing - float npcX = (float)args[0]->NumberValue(context).ToChecked(); - float npcY = (float)args[1]->NumberValue(context).ToChecked(); - CString script = *v8::String::Utf8Value(isolate, args[2]->ToString(context).ToLocalChecked()); - - // TODO(joey): additional options parsing - if (args.Length() == 4) - { - } - - auto* server = BabyDI::Get(); - auto level = server->getLevel(levelObject->getLevelName().toString()); - - auto npc = server->addNPC("", script, npcX, npcY, level, false, true); - if (npc != nullptr) - { - npc->setScriptType("LOCALN"); - levelObject->addNPC(npc); - - V8ScriptObject* v8_wrapped = static_cast*>(npc->getScriptObject()); - args.GetReturnValue().Set(v8_wrapped->handle(isolate)); - } - } -} - -// Level Method: level.onwall(x, y); -void Level_Function_OnWall(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 2); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber() && args[1]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - // Argument parsing - int npcX = int(16.0 * args[0]->NumberValue(context).ToChecked()); - int npcY = int(16.0 * args[1]->NumberValue(context).ToChecked()); - - args.GetReturnValue().Set(levelObject->isOnWall(npcX, npcY)); - } -} - -// Level Method: level.onwall2(x, y, w, h); -void Level_Function_OnWall2(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 4); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsNumber() && args[1]->IsNumber() && args[2]->IsNumber() && args[3]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, Level, levelObject); - - // Argument parsing - auto npcX = args[0]->NumberValue(context).ToChecked(); - auto npcY = args[1]->NumberValue(context).ToChecked(); - auto width = args[2]->Int32Value(context).ToChecked(); - auto height = args[3]->Int32Value(context).ToChecked(); - - if (std::lround(npcX) != int(npcX)) - { - width++; - } - - if (std::lround(npcY) != int(npcY)) - { - height++; - } - - args.GetReturnValue().Set(levelObject->isOnWall2(int(npcX), int(npcY), width, height)); - } -} - -void Setup_LevelTiles(V8ScriptEnv* env, v8::Isolate* isolate) -{ // Create the level tiles template - v8::Local level_tiles_ctor = v8::FunctionTemplate::New(isolate); - level_tiles_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "tiles")); - level_tiles_ctor->InstanceTemplate()->SetInternalFieldCount(1); - level_tiles_ctor->InstanceTemplate()->SetHandler( - v8::IndexedPropertyHandlerConfiguration( - Level_Tile_Getter, - Level_Tile_Setter, - nullptr, - nullptr, - nullptr, - v8::Local(), - v8::PropertyHandlerFlags::kNone)); - env->setConstructor("level.tiles", level_tiles_ctor); -} - -void Setup_LevelLinks(V8ScriptEnv* env, v8::Isolate* isolate, v8::Local& engine_ref) -{ // Create the level link template - v8::Local level_links_ctor = v8::FunctionTemplate::New(isolate); - level_links_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "links")); - level_links_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - level_links_ctor->InstanceTemplate()->SetHandler( - v8::IndexedPropertyHandlerConfiguration( - Level_Link_Getter, - nullptr, - nullptr, - nullptr, - Level_Link_Enumerator, - v8::Local(), - v8::PropertyHandlerFlags::kNone)); - v8::Local level_links_proto = level_links_ctor->PrototypeTemplate(); - - level_links_proto->Set(v8::String::NewFromUtf8Literal(isolate, "add"), v8::FunctionTemplate::New(isolate, Level_Function_AddLevelLink, engine_ref)); - level_links_proto->Set(v8::String::NewFromUtf8Literal(isolate, "remove"), v8::FunctionTemplate::New(isolate, Level_Function_RemoveLevelLink, engine_ref)); - level_links_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "length"), Level_Link_Length); - - // Define the Symbol.iterator method on the prototype to make "level.links" iterable - v8::Local level_links_iterator = v8::FunctionTemplate::New(isolate); - level_links_iterator->SetCallHandler(Level_Link_Iterator); - level_links_proto->Set(v8::Symbol::GetIterator(isolate), level_links_iterator); - - env->setConstructor("level.links", level_links_ctor); -} - -void Setup_LevelSigns(V8ScriptEnv* env, v8::Isolate* isolate, v8::Local& engine_ref) -{ // Create the level signs template - v8::Local level_signs_ctor = v8::FunctionTemplate::New(isolate); - level_signs_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "signs")); - level_signs_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - level_signs_ctor->InstanceTemplate()->SetHandler( - v8::IndexedPropertyHandlerConfiguration( - Level_Sign_Getter, - nullptr, - nullptr, - nullptr, - Level_Sign_Enumerator, - v8::Local(), - v8::PropertyHandlerFlags::kNone)); - v8::Local level_signs_proto = level_signs_ctor->PrototypeTemplate(); - - level_signs_proto->Set(v8::String::NewFromUtf8Literal(isolate, "add"), v8::FunctionTemplate::New(isolate, Level_Function_AddLevelSign, engine_ref)); - level_signs_proto->Set(v8::String::NewFromUtf8Literal(isolate, "remove"), v8::FunctionTemplate::New(isolate, Level_Function_RemoveLevelSign, engine_ref)); - level_signs_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "length"), Level_Sign_Length); - - // Define the Symbol.iterator method on the prototype to make "level.signs" iterable - v8::Local level_signs_iterator = v8::FunctionTemplate::New(isolate); - level_signs_iterator->SetCallHandler(Level_Sign_Iterator); - level_signs_proto->Set(v8::Symbol::GetIterator(isolate), level_signs_iterator); - - env->setConstructor("level.signs", level_signs_ctor); -} - -void Setup_LevelChests(V8ScriptEnv* env, v8::Isolate* isolate, v8::Local& engine_ref) -{ // Create the level chests template - v8::Local level_chests_ctor = v8::FunctionTemplate::New(isolate); - level_chests_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "chests")); - level_chests_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - level_chests_ctor->InstanceTemplate()->SetHandler( - v8::IndexedPropertyHandlerConfiguration( - Level_Chest_Getter, - nullptr, - nullptr, - nullptr, - Level_Chest_Enumerator, - v8::Local(), - v8::PropertyHandlerFlags::kNone)); - v8::Local level_chests_proto = level_chests_ctor->PrototypeTemplate(); - - level_chests_proto->Set(v8::String::NewFromUtf8Literal(isolate, "add"), v8::FunctionTemplate::New(isolate, Level_Function_AddLevelChest, engine_ref)); - level_chests_proto->Set(v8::String::NewFromUtf8Literal(isolate, "remove"), v8::FunctionTemplate::New(isolate, Level_Function_RemoveLevelChest, engine_ref)); - level_chests_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "length"), Level_Chest_Length); - - // Define the Symbol.iterator method on the prototype to make "level.chests" iterable - v8::Local level_chests_iterator = v8::FunctionTemplate::New(isolate); - level_chests_iterator->SetCallHandler(Level_Chest_Iterator); - level_chests_proto->Set(v8::Symbol::GetIterator(isolate), level_chests_iterator); - - env->setConstructor("level.chests", level_chests_ctor); -} - -void Setup_LevelNpcs(V8ScriptEnv* env, v8::Isolate* isolate, v8::Local& engine_ref) -{ // Create the level chests template - v8::Local level_npcs_ctor = v8::FunctionTemplate::New(isolate); - level_npcs_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "npcs")); - level_npcs_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - level_npcs_ctor->InstanceTemplate()->SetHandler( - v8::IndexedPropertyHandlerConfiguration( - Level_Npc_Getter, - nullptr, - nullptr, - nullptr, - Level_Npc_Enumerator, - v8::Local(), - v8::PropertyHandlerFlags::kNone)); - v8::Local level_npcs_proto = level_npcs_ctor->PrototypeTemplate(); - - level_npcs_proto->Set(v8::String::NewFromUtf8Literal(isolate, "add"), v8::FunctionTemplate::New(isolate, Level_Function_AddLevelNpc, engine_ref)); - level_npcs_proto->Set(v8::String::NewFromUtf8Literal(isolate, "remove"), v8::FunctionTemplate::New(isolate, Level_Function_RemoveLevelNpc, engine_ref)); - level_npcs_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "length"), Level_Npc_Length); - - // Define the Symbol.iterator method on the prototype to make "level.chests" iterable - v8::Local level_npcs_iterator = v8::FunctionTemplate::New(isolate); - level_npcs_iterator->SetCallHandler(Level_Npc_Iterator); - level_npcs_proto->Set(v8::Symbol::GetIterator(isolate), level_npcs_iterator); - - env->setConstructor("level.npcs", level_npcs_ctor); -} - -void bindClass_Level(ScriptEngine* scriptEngine) -{ - // Retrieve v8 environment - auto* env = dynamic_cast(scriptEngine->getScriptEnv()); - v8::Isolate* isolate = env->isolate(); - - // External pointer - v8::Local engine_ref = v8::External::New(isolate, scriptEngine); - - // Create V8 string for "level" - v8::Local levelStr = v8::String::NewFromUtf8Literal(isolate, "level", v8::NewStringType::kInternalized); - - // Create constructor for class - v8::Local level_ctor = v8::FunctionTemplate::New(isolate); - v8::Local level_proto = level_ctor->PrototypeTemplate(); - level_ctor->SetClassName(levelStr); - level_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - // Method functions - // level_proto->Set(v8::String::NewFromUtf8Literal(isolate, "clone"), v8::FunctionTemplate::New(isolate, Level_Function_Clone, engine_ref)); - level_proto->Set(v8::String::NewFromUtf8Literal(isolate, "savelevel"), v8::FunctionTemplate::New(isolate, Level_Function_SaveLevel, engine_ref)); - level_proto->Set(v8::String::NewFromUtf8Literal(isolate, "findareanpcs"), v8::FunctionTemplate::New(isolate, Level_Function_FindAreaNpcs, engine_ref)); - level_proto->Set(v8::String::NewFromUtf8Literal(isolate, "findnearestplayers"), v8::FunctionTemplate::New(isolate, Level_Function_FindNearestPlayers, engine_ref)); - // level_proto->Set(v8::String::NewFromUtf8Literal(isolate, "reload"), v8::FunctionTemplate::New(isolate, Level_Function_Reload, engine_ref)); - level_proto->Set(v8::String::NewFromUtf8Literal(isolate, "shoot"), v8::FunctionTemplate::New(isolate, Level_Function_Shoot, engine_ref)); - level_proto->Set(v8::String::NewFromUtf8Literal(isolate, "putexplosion"), v8::FunctionTemplate::New(isolate, Level_Function_PutExplosion, engine_ref)); - level_proto->Set(v8::String::NewFromUtf8Literal(isolate, "putnpc"), v8::FunctionTemplate::New(isolate, Level_Function_PutNPC, engine_ref)); - level_proto->Set(v8::String::NewFromUtf8Literal(isolate, "onwall"), v8::FunctionTemplate::New(isolate, Level_Function_OnWall, engine_ref)); - level_proto->Set(v8::String::NewFromUtf8Literal(isolate, "onwall2"), v8::FunctionTemplate::New(isolate, Level_Function_OnWall2, engine_ref)); - - // Properties - // level_proto->SetAccessor(v8::String::NewFromUtf8(isolate, "isnopkzone"), Level_GetBool_IsNoPkZone); // TODO(joey): must be missing a status flag or something - level_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "issparringzone"), Level_GetBool_IsSparringZone); - level_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "name"), Level_GetStr_Name); - level_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "mapname"), Level_GetStr_MapName); - level_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "npcs"), Level_GetObject_Npcs, nullptr, engine_ref); - level_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "links"), Level_GetObject_Links, nullptr, engine_ref); - level_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "signs"), Level_GetObject_Signs, nullptr, engine_ref); - level_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "chests"), Level_GetObject_Chests, nullptr, engine_ref); - level_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "players"), Level_GetArray_Players); - level_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "tiles"), Level_GetObject_Tiles, nullptr, engine_ref); - - Setup_LevelTiles(env, isolate); - Setup_LevelLinks(env, isolate, engine_ref); - Setup_LevelSigns(env, isolate, engine_ref); - Setup_LevelChests(env, isolate, engine_ref); - Setup_LevelNpcs(env, isolate, engine_ref); - - // Persist the constructor - env->setConstructor(ScriptConstructorId::result, level_ctor); -} - -#endif diff --git a/server/src/scripting/v8/V8LevelLinkImpl.cpp b/server/src/scripting/v8/V8LevelLinkImpl.cpp deleted file mode 100644 index 1de830e91..000000000 --- a/server/src/scripting/v8/V8LevelLinkImpl.cpp +++ /dev/null @@ -1,174 +0,0 @@ -#ifdef V8NPCSERVER - - #include - #include - #include - #include - #include - - #include "NPC.h" - #include "Player.h" - #include "level/Level.h" - #include "level/LevelLink.h" - #include "level/Map.h" - #include "scripting/ScriptEngine.h" - #include "scripting/v8/V8ScriptFunction.h" - #include "scripting/v8/V8ScriptObject.h" - -// PROPERTY: link.newlevel -void Link_GetStr_NewLevel(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), linkObject->getNewLevel().text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Link_SetStr_NewLevel(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - linkObject->setNewLevel(*newValue); -} - -// PROPERTY: link.x -void Link_GetNum_X(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - info.GetReturnValue().Set(linkObject->getX()); -} - -void Link_SetNum_X(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - int newValue = (int)value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - - linkObject->setX(newValue); -} - -// PROPERTY: link.y -void Link_GetNum_Y(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - info.GetReturnValue().Set(linkObject->getY()); -} - -void Link_SetNum_Y(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - int newValue = (int)value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - - linkObject->setY(newValue); -} - -// PROPERTY: link.width -void Link_GetNum_Width(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - info.GetReturnValue().Set(linkObject->getWidth()); -} - -void Link_SetNum_Width(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - int newValue = (int)value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - - linkObject->setWidth(newValue); -} - -// PROPERTY: link.height -void Link_GetNum_Height(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - info.GetReturnValue().Set(linkObject->getHeight()); -} - -void Link_SetNum_Height(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - int newValue = (int)value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - - linkObject->setHeight(newValue); -} - -// PROPERTY: link.newx -void Link_GetStr_NewX(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), linkObject->getNewX().text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Link_SetStr_NewX(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - linkObject->setNewX(*newValue); -} - -// PROPERTY: link.newx -void Link_GetStr_NewY(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), linkObject->getNewY().text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Link_SetStr_NewY(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelLink, linkObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - linkObject->setNewY(*newValue); -} - -void bindClass_LevelLink(ScriptEngine* scriptEngine) -{ - // Retrieve v8 environment - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - v8::Isolate* isolate = env->isolate(); - - // External pointer - v8::Local engine_ref = v8::External::New(isolate, scriptEngine); - - // Create V8 string for "level" - v8::Local linkStr = v8::String::NewFromUtf8Literal(isolate, "link", v8::NewStringType::kInternalized); - - // Create constructor for class - v8::Local link_ctor = v8::FunctionTemplate::New(isolate); - v8::Local link_proto = link_ctor->PrototypeTemplate(); - link_ctor->SetClassName(linkStr); - link_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - // Method functions - //link_proto->Set(v8::String::NewFromUtf8Literal(isolate, "clone"), v8::FunctionTemplate::New(isolate, Level_Function_Clone, engine_ref)); - - // Properties - link_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "newlevel"), Link_GetStr_NewLevel, Link_SetStr_NewLevel); - link_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "x"), Link_GetNum_X, Link_SetNum_X); - link_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "y"), Link_GetNum_Y, Link_SetNum_Y); - link_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "width"), Link_GetNum_Width, Link_SetNum_Width); - link_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "height"), Link_GetNum_Height, Link_SetNum_Height); - link_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "newx"), Link_GetStr_NewX, Link_SetStr_NewX); - link_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "newy"), Link_GetStr_NewY, Link_SetStr_NewY); - - // Persist the constructor - env->setConstructor(ScriptConstructorId::result, link_ctor); -} - -#endif diff --git a/server/src/scripting/v8/V8LevelSignImpl.cpp b/server/src/scripting/v8/V8LevelSignImpl.cpp deleted file mode 100644 index 3ee482b87..000000000 --- a/server/src/scripting/v8/V8LevelSignImpl.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#ifdef V8NPCSERVER - - #include - #include - #include - #include - #include - - #include "NPC.h" - #include "Player.h" - #include "level/Level.h" - #include "level/LevelLink.h" - #include "level/Map.h" - #include "scripting/ScriptEngine.h" - #include "scripting/v8/V8ScriptFunction.h" - #include "scripting/v8/V8ScriptObject.h" - -// PROPERTY: sign.text -void Sign_GetStr_Text(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelSign, signObject); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), signObject->getUText().text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Sign_SetStr_Text(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelSign, signObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - signObject->setUText(*newValue); -} - -// PROPERTY: sign.x -void Sign_GetNum_X(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelSign, signObject); - - info.GetReturnValue().Set(signObject->getX()); -} - -void Sign_SetNum_X(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelSign, signObject); - - int newValue = (int)value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - - signObject->setX(newValue); -} - -// PROPERTY: sign.y -void Sign_GetNum_Y(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelSign, signObject); - - info.GetReturnValue().Set(signObject->getY()); -} - -void Sign_SetNum_Y(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, LevelSign, signObject); - - int newValue = (int)value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - - signObject->setY(newValue); -} - -void bindClass_LevelSign(ScriptEngine* scriptEngine) -{ - // Retrieve v8 environment - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - v8::Isolate* isolate = env->isolate(); - - // External pointer - v8::Local engine_ref = v8::External::New(isolate, scriptEngine); - - // Create V8 string for "level" - v8::Local signStr = v8::String::NewFromUtf8Literal(isolate, "sign", v8::NewStringType::kInternalized); - - // Create constructor for class - v8::Local sign_ctor = v8::FunctionTemplate::New(isolate); - v8::Local sign_proto = sign_ctor->PrototypeTemplate(); - sign_ctor->SetClassName(signStr); - sign_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - // Method functions - //link_proto->Set(v8::String::NewFromUtf8Literal(isolate, "clone"), v8::FunctionTemplate::New(isolate, Level_Function_Clone, engine_ref)); - - // Properties - sign_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "text"), Sign_GetStr_Text, Sign_SetStr_Text); - sign_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "x"), Sign_GetNum_X, Sign_SetNum_X); - sign_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "y"), Sign_GetNum_Y, Sign_SetNum_Y); - - // Persist the constructor - env->setConstructor(ScriptConstructorId::result, sign_ctor); -} - -#endif diff --git a/server/src/scripting/v8/V8NPCImpl.cpp b/server/src/scripting/v8/V8NPCImpl.cpp deleted file mode 100644 index 24a4a73c3..000000000 --- a/server/src/scripting/v8/V8NPCImpl.cpp +++ /dev/null @@ -1,1619 +0,0 @@ -#ifdef V8NPCSERVER - - #include - #include - #include - #include - - #include - - #include "NPC.h" - #include "level/Level.h" - #include "scripting/ScriptEngine.h" - #include "scripting/v8/V8ScriptFunction.h" - #include "scripting/v8/V8ScriptObject.h" - -// Property: npc.id -void NPC_GetInt_Id(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - info.GetReturnValue().Set(npcObject->getId()); -} - -// Property: npc.name -void NPC_GetStr_Name(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::Local npcName = v8::String::NewFromUtf8(info.GetIsolate(), npcObject->getName().c_str()).ToLocalChecked(); - info.GetReturnValue().Set(npcName); -} - -// Property: npc.x -void NPC_GetNum_X(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - info.GetReturnValue().Set(npcObject->getX()); -} - -void NPC_SetNum_X(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - auto newValue = (float)value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - npcObject->setX(newValue); - - //npcObject->updatePropModTime(NPCPROP_X); - npcObject->updatePropModTime(NPCPROP_X2); -} - -// Property: npc.y -void NPC_GetNum_Y(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - info.GetReturnValue().Set(npcObject->getY()); -} - -void NPC_SetNum_Y(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - auto newValue = (float)value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - npcObject->setY(newValue); - - //npcObject->updatePropModTime(NPCPROP_Y); - npcObject->updatePropModTime(NPCPROP_Y2); -} - -// PROPERTY: Level -void NPC_GetObject_Level(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - if (!npcObject->getIsNpcDeleteRequested()) - { - auto npcLevel = npcObject->getLevel(); - if (npcLevel != nullptr) - { - auto* v8_wrapped = static_cast*>(npcLevel->getScriptObject()); - info.GetReturnValue().Set(v8_wrapped->handle(info.GetIsolate())); - return; - } - } - - info.GetReturnValue().SetNull(); -} - -// PROPERTY: LevelName -void NPC_GetStr_LevelName(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - auto npcLevel = npcObject->getLevel(); - CString levelName(""); - if (npcLevel != nullptr) - levelName = npcLevel->getLevelName(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), levelName.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -// PROPERTY: Timeout -void NPC_GetNum_Timeout(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - double timeout = npcObject->getTimeout() / 20; - info.GetReturnValue().Set(timeout); -} - -void NPC_SetNum_Timeout(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - double timeout = value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - npcObject->setTimeout((int)(timeout * 20)); -} - -// PROPERTY: Rupees -void NPC_GetInt_Rupees(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - info.GetReturnValue().Set(npcObject->getRupees()); -} - -void NPC_SetInt_Rupees(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - int newValue = value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - npcObject->setRupees(newValue); - npcObject->updatePropModTime(NPCPROP_RUPEES); -} - -// PROPERTY: Bombs -void NPC_GetInt_Bombs(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - CString npcProp = npcObject->getProp(NPCPROP_BOMBS); - info.GetReturnValue().Set(npcProp.readGUChar()); -} - -void NPC_SetInt_Bombs(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - int newValue = value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - npcObject->setProps(CString() >> (char)NPCPROP_BOMBS >> (char)clip(newValue, 0, 99), CLVER_2_17, true); -} - -// PROPERTY: Darts -void NPC_GetInt_Darts(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - CString npcProp = npcObject->getProp(NPCPROP_ARROWS); - info.GetReturnValue().Set(npcProp.readGUChar()); -} - -void NPC_SetInt_Darts(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - int newValue = value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - npcObject->setProps(CString() >> (char)NPCPROP_ARROWS >> (char)clip(newValue, 0, 99), CLVER_2_17, true); -} - -// PROPERTY: Hearts -void NPC_GetInt_Hearts(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - CString npcProp = npcObject->getProp(NPCPROP_POWER); - info.GetReturnValue().Set((float)npcProp.readGUChar() / 2.0f); -} - -void NPC_SetInt_Hearts(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - int newValue = (int)(value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked() * 2); - npcObject->setProps(CString() >> (char)NPCPROP_POWER >> (char)clip(newValue, 0, 40), CLVER_2_17, true); -} - -// PROPERTY: npc.height -void NPC_GetInt_Height(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - info.GetReturnValue().Set(npcObject->getHeight()); -} - -// PROPERTY: npc.width -void NPC_GetInt_Width(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - info.GetReturnValue().Set(npcObject->getWidth()); -} - -// PROPERTY: npc.glovepower -void NPC_GetInt_GlovePower(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - CString npcProp = npcObject->getProp(NPCPROP_GLOVEPOWER); - info.GetReturnValue().Set(npcProp.readGUChar()); -} - -void NPC_SetInt_GlovePower(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - int newValue = value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - npcObject->setProps(CString() >> (char)NPCPROP_GLOVEPOWER >> (char)clip(newValue, 0, 3), CLVER_2_17, true); -} - -// PROPERTY: npc.dir -void NPC_GetInt_Dir(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - info.GetReturnValue().Set(npcObject->getSprite()); -} - -void NPC_SetInt_Dir(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - int newValue = value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - npcObject->setSprite(newValue % 4); - npcObject->updatePropModTime(NPCPROP_SPRITE); -} - -// PROPERTY: npc.ap -void NPC_GetInt_Alignment(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - CString npcProp = npcObject->getProp(NPCPROP_ALIGNMENT); - info.GetReturnValue().Set(npcProp.readGUChar()); -} - -void NPC_SetInt_Alignment(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - int newValue = clip(value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(), 0, 100); - npcObject->setProps(CString() >> (char)NPCPROP_ALIGNMENT >> (char)newValue, CLVER_2_17, true); -} - -// PROPERTY: npc.bodyimg -void NPC_GetStr_BodyImage(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - auto& message = npcObject->getBodyImage(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), message.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void NPC_SetStr_BodyImage(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - npcObject->setBodyImage(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_BODYIMAGE); -} - -// PROPERTY: npc.headimg -void NPC_GetStr_HeadImage(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), npcObject->getHeadImage().text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void NPC_SetStr_HeadImage(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - npcObject->setHeadImage(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_HEADIMAGE); -} - -// PROPERTY: npc.horseimg -void NPC_GetStr_HorseImage(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), npcObject->getHorseImage().text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void NPC_SetStr_HorseImage(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - npcObject->setHorseImage(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_HORSEIMAGE); -} - -// PROPERTY: npc.image -void NPC_GetStr_Image(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), npcObject->getImage().c_str()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void NPC_SetStr_Image(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - npcObject->setImage(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_IMAGE); -} - -// PROPERTY: npc.nick -void NPC_GetStr_Nickname(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), npcObject->getNickname().c_str()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void NPC_SetStr_Nickname(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - npcObject->setNickname(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_NICKNAME); -} - -// PROPERTY: npc.shieldimg -void NPC_GetStr_ShieldImage(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), npcObject->getShieldImage().text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void NPC_SetStr_ShieldImage(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - npcObject->setShieldImage(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_SHIELDIMAGE); -} - -// PROPERTY: npc.swordimg -void NPC_GetStr_SwordImage(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), npcObject->getSwordImage().text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void NPC_SetStr_SwordImage(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - npcObject->setSwordImage(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_SWORDIMAGE); -} - -// PROPERTY: Message -void NPC_GetStr_Message(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - auto& message = npcObject->getChat(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), message.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void NPC_SetStr_Message(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - npcObject->setChat(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_MESSAGE); -} - -// PROPERTY: Animation -void NPC_GetStr_Ani(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - auto& propValue = npcObject->getGani(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), propValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void NPC_SetStr_Ani(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - - npcObject->setGani(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_GANI); -} - -// NPC Method: npc.canwarp(); -void NPC_Function_CanWarp(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - npcObject->allowNpcWarping(NPCWarpType::AllLinks); -} - -// NPC Method: npc.canwarp2(); -void NPC_Function_CanWarp2(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - npcObject->allowNpcWarping(NPCWarpType::OverworldLinks); -} - -// NPC Method: npc.cannotwarp(); -void NPC_Function_CannotWarp(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - npcObject->allowNpcWarping(NPCWarpType::None); -} - -// NPC Method: npc.destroy(); -void NPC_Function_Destroy(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - bool status = npcObject->deleteNPC(); - args.GetReturnValue().Set(status); -} - -// NPC Method: npc.blockagain(); -void NPC_Function_BlockAgain(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - npcObject->setBlockingFlags(NPCBLOCKFLAG_BLOCK); - npcObject->updatePropModTime(NPCPROP_BLOCKFLAGS); -} - -// NPC Method: npc.dontblock(); -void NPC_Function_DontBlock(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - npcObject->setBlockingFlags(NPCBLOCKFLAG_NOBLOCK); - npcObject->updatePropModTime(NPCPROP_BLOCKFLAGS); -} - -// NPC Method: npc.drawoverplayer(); -void NPC_Function_DrawOverPlayer(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - // Toggle flags - int flags = npcObject->getVisibleFlags(); - flags = (flags | NPCVISFLAG_DRAWOVERPLAYER) & ~(NPCVISFLAG_DRAWUNDERPLAYER); - - npcObject->setVisibleFlags(flags); - npcObject->updatePropModTime(NPCPROP_VISFLAGS); -} - -// NPC Method: npc.drawunderplayer(); -void NPC_Function_DrawUnderPlayer(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - // Toggle flags - int flags = npcObject->getVisibleFlags(); - flags = (flags | NPCVISFLAG_DRAWUNDERPLAYER) & ~(NPCVISFLAG_DRAWOVERPLAYER); - - npcObject->setVisibleFlags(flags); - npcObject->updatePropModTime(NPCPROP_VISFLAGS); -} - -// NPC Method: npc.hide(); -void NPC_Function_Hide(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - // Toggle flags - int flags = npcObject->getVisibleFlags(); - npcObject->setVisibleFlags(flags & ~(NPCVISFLAG_VISIBLE)); - npcObject->updatePropModTime(NPCPROP_VISFLAGS); -} - -// NPC Method: npc.show(); -void NPC_Function_Show(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - // Toggle flags - int flags = npcObject->getVisibleFlags(); - npcObject->setVisibleFlags(flags | NPCVISFLAG_VISIBLE); - npcObject->updatePropModTime(NPCPROP_VISFLAGS); -} - -// NPC Method: npc.message(); -void NPC_Function_Message(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Validate arguments - if (args.Length() == 0) - { - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - npcObject->setChat(""); - npcObject->updatePropModTime(NPCPROP_MESSAGE); - } - else if (args[0]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - v8::String::Utf8Value newValue(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - - npcObject->setChat(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_MESSAGE); - } -} - -// NPC Method: npc.move(x, y, time, options); -void NPC_Function_Move(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 4); - - // Unwrap object - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - v8::Local context = isolate->GetCurrentContext(); - - // Argument parsing - int dx = int(16.0 * args[0]->NumberValue(context).ToChecked()); - int dy = int(16.0 * args[1]->NumberValue(context).ToChecked()); - double time_fps = args[2]->NumberValue(context).ToChecked(); - int options = args[3]->Int32Value(context).ToChecked(); - - npcObject->moveNPC(dx, dy, time_fps, options); -} - -// NPC Method: npc.setimg(image); -void NPC_Function_SetImg(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - // Unwrap object - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - if (args[0]->IsString()) - { - v8::Local context = args.GetIsolate()->GetCurrentContext(); - v8::String::Utf8Value image(isolate, args[0]->ToString(context).ToLocalChecked()); - - npcObject->setImage(*image); - npcObject->updatePropModTime(NPCPROP_IMAGE); - npcObject->updatePropModTime(NPCPROP_IMAGEPART); - } -} - -// NPC Method: npc.setimgpart(filename,offsetx,offsety,width,height) -void NPC_Function_SetImgPart(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 5); - - // Unwrap object - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - if (args[0]->IsString()) - { - v8::Local context = isolate->GetCurrentContext(); - v8::String::Utf8Value image(isolate, args[0]->ToString(context).ToLocalChecked()); - - // TODO(joey): may need to check the types individually - int offsetx = args[1]->Int32Value(context).ToChecked(); - int offsety = args[2]->Int32Value(context).ToChecked(); - int width = args[3]->Int32Value(context).ToChecked(); - int height = args[4]->Int32Value(context).ToChecked(); - - npcObject->setImage(std::string(*image, image.length()), offsetx, offsety, width, height); - npcObject->updatePropModTime(NPCPROP_IMAGE); - npcObject->updatePropModTime(NPCPROP_IMAGEPART); - } -} - -// NPC Method: npc.showcharacter(); -void NPC_Function_ShowCharacter(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Unwrap object - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - npcObject->setImage("#c#"); - npcObject->setHeadImage("head0.png"); - npcObject->setBodyImage("body.png"); - npcObject->setShieldImage("shield1.png"); - npcObject->setSwordImage("sword1.png"); - npcObject->setColorId(0, 2); // orange - npcObject->setColorId(1, 5); // dark red - npcObject->setColorId(2, 21); // black - npcObject->setColorId(3, 5); // dark red - npcObject->setColorId(4, 21); // black - npcObject->setWidth(32); - npcObject->setHeight(48); - - npcObject->updatePropModTime(NPCPROP_IMAGE); - npcObject->updatePropModTime(NPCPROP_IMAGEPART); - npcObject->updatePropModTime(NPCPROP_HEADIMAGE); - npcObject->updatePropModTime(NPCPROP_BODYIMAGE); - npcObject->updatePropModTime(NPCPROP_SHIELDIMAGE); - npcObject->updatePropModTime(NPCPROP_SWORDIMAGE); - npcObject->updatePropModTime(NPCPROP_COLORS); -} - -// NPC Method: npc.setani("walk", "ani", "params"); -void NPC_Function_SetAni(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_MINARGCOUNT(args, isolate, 1); - - if (args[0]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - v8::String::Utf8Value newValue(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - - CString animation(*newValue); - for (int i = 1; i < args.Length(); i++) - { - if (args[i]->IsString() || args[i]->IsNumber()) - { - v8::String::Utf8Value aniParam(isolate, args[i]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - animation << "," << *aniParam; - } - } - - npcObject->setGani(std::string(animation.text(), animation.length())); - npcObject->updatePropModTime(NPCPROP_GANI); - } -} - -// NPC Method: npc.setcharprop(code, value); -void NPC_Function_SetCharProp(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 2); - - CString code = *v8::String::Utf8Value(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - v8::String::Utf8Value newValue(isolate, args[1]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - - auto len = newValue.length(); - if (len > 223) - len = 223; - - if (code[0] == '#') - { - // Unwrap object - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - switch (code[1]) - { - case '1': // sword image - { - npcObject->setSwordImage(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_SWORDIMAGE); - break; - } - - case '2': // shield image - { - npcObject->setShieldImage(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_SHIELDIMAGE); - break; - } - - case '3': // head image - { - npcObject->setHeadImage(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_HEADIMAGE); - break; - } - - case '5': // horse image (needs to be tested) - npcObject->setHorseImage(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_HORSEIMAGE); - break; - - case '8': // body image - npcObject->setBodyImage(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_BODYIMAGE); - break; - - case 'c': // chat - npcObject->setChat(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_MESSAGE); - break; - - case 'n': // nickname - npcObject->setNickname(std::string(*newValue, newValue.length())); - npcObject->updatePropModTime(NPCPROP_NICKNAME); - break; - - case 'C': // colors - { - if (code[2] >= '0' && code[2] < '5') - { - npcObject->setColorId(code[2] - '0', getColor(*newValue)); - npcObject->updatePropModTime(NPCPROP_COLORS); - } - break; - } - } - } -} - -// NPC Method: npc.settimer(time); -void NPC_Function_SetTimer(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - if (args[0]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - double timeout = args[0]->NumberValue(isolate->GetCurrentContext()).ToChecked(); - npcObject->setTimeout((int)(timeout * 20)); - } -} - -// NPC Method: npc.setshape(type, pixelWidth, pixelHeight); -void NPC_Function_SetShape(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the specified arguments - V8ENV_THROW_ARGCOUNT(args, isolate, 3); - - if (args[0]->IsInt32() && args[1]->IsInt32() && args[2]->IsInt32()) - { - v8::Local context = isolate->GetCurrentContext(); - - // Unwrap Object - NPC* npcObject = unwrapObject(args.This()); - - int width = args[1]->Int32Value(context).ToChecked(); - int height = args[2]->Int32Value(context).ToChecked(); - npcObject->setWidth(width); - npcObject->setHeight(height); - } -} - -// NPC Method: npc.registerAction(string, function); -void NPC_Function_RegisterTrigger(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 2); - - SCRIPTENV_D("Begin NPC::registerAction()\n"); - - if (args[0]->IsString() && args[1]->IsFunction()) - { - SCRIPTENV_D(" - Register npc action %s with: %s\n", - *v8::String::Utf8Value(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()), - *v8::String::Utf8Value(isolate, args[1]->ToString(isolate->GetCurrentContext()).ToLocalChecked())); - - v8::Local data = args.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - - // Callback name - std::string eventName = *v8::String::Utf8Value(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - std::transform(eventName.begin(), eventName.end(), eventName.begin(), ::tolower); - - // Persist the callback function so we can retrieve it later on - v8::Local cbFunc = args[1].As(); - V8ScriptFunction* cbFuncWrapper = new V8ScriptFunction(env, cbFunc); - - // Unwrap Object - NPC* npcObject = unwrapObject(args.This()); - npcObject->registerTriggerAction(eventName, cbFuncWrapper); - } - - SCRIPTENV_D("End NPC::registerAction()\n"); -} - -// NPC Method: npc.scheduleevent(time, function); -void NPC_Function_ScheduleEvent(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_MINARGCOUNT(args, isolate, 2); - - SCRIPTENV_D("Begin NPC::registerAction()\n"); - - if (args[0]->IsNumber() && args[1]->IsFunction()) - { - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - SCRIPTENV_D(" - Register npc schedule event %s with: %s\n", - *v8::String::Utf8Value(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()), - *v8::String::Utf8Value(isolate, args[1]->ToString(isolate->GetCurrentContext()).ToLocalChecked())); - - v8::Local context = isolate->GetCurrentContext(); - - v8::Local data = args.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - - // Callback name - double time_til = args[0]->NumberValue(context).ToChecked(); - int timer_frames = (int)(time_til * 20); - - // Persist the callback function so we can retrieve it later on - v8::Local cbFunc = args[1].As(); - V8ScriptFunction* cbFuncWrapper = new V8ScriptFunction(env, cbFunc); - - IScriptArguments* v8args; - if (args.Length() > 2) - { - v8::Local paramData = args[2]->ToObject(context).ToLocalChecked(); - - auto v8ScriptData = std::make_shared(env, paramData); - v8args = ScriptFactory::createArguments(env, npcObject->getScriptObject(), std::move(v8ScriptData)); - } - else - v8args = ScriptFactory::createArguments(env, npcObject->getScriptObject()); - - ScriptAction action(cbFuncWrapper, v8args, "_scheduleevent"); - npcObject->scheduleEvent(timer_frames, action); - scriptEngine->registerNpcTimer(npcObject); - } - - SCRIPTENV_D("End NPC::registerAction()\n"); -} - - #include "scripting/ScriptClass.h" - #include "scripting/v8/V8ScriptWrappers.h" - -// NPC Function: NPC.join("class"); -void NPC_Function_Join(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - if (args[0]->IsString()) - { - v8::Local context = isolate->GetCurrentContext(); - v8::Local data = args.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - - std::string className = *v8::String::Utf8Value(isolate, args[0]->ToString(context).ToLocalChecked()); - - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - ScriptClass* classObject = npcObject->joinClass(className); - if (classObject != nullptr) - { - auto classCodeWrap = wrapScript(classObject->getSource().getServerSide()); - auto scriptFunction = scriptEngine->compileCache(classCodeWrap, false); - - if (scriptFunction != nullptr) - { - V8ScriptFunction* v8_function = static_cast(scriptFunction); - v8::Local newArgs[] = { args.This() }; - - // Execute - v8::TryCatch try_catch(isolate); - v8::Local localFunction = v8_function->function(); - v8::MaybeLocal scriptTableRet = localFunction->Call(context, args.This(), 1, newArgs); - if (!scriptTableRet.IsEmpty()) - { - args.GetReturnValue().Set(scriptTableRet.ToLocalChecked()); - return; - } - } - } - - //Server *server = scriptEngine->getServer(); - //auto classObj = server->getClass(className); - - //if (classObj && !classObj->source().empty()) - //{ - // V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - // // Split the code - // std::string serverCode = classObj->serverCode(); - // std::string clientCode = classObj->clientCode(); - - // //auto currentClass = server->getClassObject(className); - // //if (currentClass == nullptr) - // // currentClass = server->addClass(className, clientCode); - // //npcObject->addClassCode(className, clientCode); - // // Add class to npc - // //npcObject->addClassCode(className, clientCode); - // - // // Wrap code - // std::string classCodeWrap = ScriptEngine::wrapScript(serverCode); - - // // TODO(joey): maybe we shouldn't cache this using this method, since classes can be used with - // // multiple wrappers. - // IScriptFunction *function = scriptEngine->compileCache(classCodeWrap, false); - // if (function == nullptr) - // return; - - // V8ScriptFunction *v8_function = static_cast(function); - // v8::Local newArgs[] = { args.This() }; - - // // Execute - // v8::TryCatch try_catch(isolate); - // v8::Local scriptFunction = v8_function->Function(); - // v8::MaybeLocal scriptTableRet = scriptFunction->Call(context, args.This(), 1, newArgs); - // if (!scriptTableRet.IsEmpty()) - // { - // args.GetReturnValue().Set(scriptTableRet.ToLocalChecked()); - // return; - // } - - // // TODO(joey): error handling - //} - } -} - -void NPC_Function_SetPM(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - // Exclusive for Control-NPC - if (npcObject->getName() != "Control-NPC") - return; - - v8::Local data = args.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - - if (args[0]->IsFunction()) - { - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - - // Persist the callback function so we can retrieve it later on - v8::Local cbFunc = args[0].As(); - V8ScriptFunction* cbFuncWrapper = new V8ScriptFunction(env, cbFunc); - - // Set pm function - scriptEngine->getServer()->setPMFunction(npcObject->getId(), cbFuncWrapper); - } - else - { - scriptEngine->getServer()->setPMFunction(0); - } -} - -void NPC_Function_Warpto(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 3); - - // warpto levelname,x,y; - if (args[0]->IsString() && args[1]->IsNumber() && args[2]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, NPC, npcObject); - - if (npcObject->isWarpable()) - { - v8::Local context = isolate->GetCurrentContext(); - - v8::String::Utf8Value levelName(isolate, args[0]->ToString(context).ToLocalChecked()); - int newX = int(16.0 * args[1]->NumberValue(context).ToChecked()); - int newY = int(16.0 * args[2]->NumberValue(context).ToChecked()); - - v8::Local data = args.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - Server* server = scriptEngine->getServer(); - - auto level = server->getLevel(*levelName); - if (level != nullptr) - { - npcObject->warpNPC(level, newX, newY); - args.GetReturnValue().Set(true); - return; - } - } - } - - args.GetReturnValue().Set(false); -} - -// Called when javascript creates a new object -// js example: let jsNpc = new NPC(); -//void Npc_Constructor(const v8::FunctionCallbackInfo& args) -//{ -// // TODO(joey): more proof of concept, likely to get axed. -// -// // Called by V8. and should return an NPC object -// SCRIPTENV_D("Npc_Constructor called\n"); -// -// v8::Isolate *isolate = args.GetIsolate(); -// v8::Local context = isolate->GetCurrentContext(); -// -// // Throw an exception on method functions for constructor calls -// V8ENV_THROW_METHOD(args, isolate); -// -// // Retrieve external data for this call -// v8::Local data = args.Data().As(); -// ScriptEngine *scriptEngine = static_cast(data->Value()); -// -// NPC *newNpc = new NPC(scriptEngine->getServer()); -// -// assert(args.This()->InternalFieldCount() > 0); -// -// args.This()->SetAlignedPointerInInternalField(0, newNpc); -// args.GetReturnValue().Set(args.This()); -//} - -// PROPERTY: NPC Attributes -// TODO(joey): use lazy property instead? TBD -//void NPC_GetObject_Attrs2(v8::Local prop, const v8::PropertyCallbackInfo& info) -//{ -// //printf("Called getattr\n"); -// -// v8::Isolate *isolate = info.GetIsolate(); -// v8::Local context = isolate->GetCurrentContext(); -// v8::Local self = info.This(); -// -// V8ENV_SAFE_UNWRAP(info, NPC, npcObject); -// -// v8::Local internalAttr = v8::String::NewFromUtf8(isolate, "_internalAttr", v8::NewStringType::kInternalized).ToLocalChecked(); -// -// v8::Local ctor_tpl = persistentToLocal(isolate, _persist_npc_attrs_ctor); -// v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); -// new_instance->SetAlignedPointerInInternalField(0, npcObject); -// -// // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed -// V8ScriptObject *v8_wrapped = static_cast *>(npcObject->getScriptObject()); -// v8_wrapped->addChild("attr", new_instance); -// -// v8::PropertyAttribute propAttr = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); -// self->DefineOwnProperty(context, internalAttr, new_instance, propAttr).FromJust(); -// info.GetReturnValue().Set(new_instance); -//} - -void NPC_GetObject_Attrs(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalAttr = v8::String::NewFromUtf8(isolate, "_internalAttr", v8::NewStringType::kInternalized).ToLocalChecked(); - if (self->HasRealNamedProperty(context, internalAttr).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalAttr).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - // Grab external data - v8::Local data = info.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("npc.attr"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, npcObject); - - // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed - V8ScriptObject* v8_wrapped = static_cast*>(npcObject->getScriptObject()); - v8_wrapped->addChild("attr", new_instance); - - v8::PropertyAttribute propAttr = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalAttr, new_instance, propAttr).FromJust(); - info.GetReturnValue().Set(new_instance); -} - -const char __nAttrPackets[30] = { 36, 37, 38, 39, 40, 44, 45, 46, 47, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73 }; - -void NPC_Attrs_Getter(uint32_t index, const v8::PropertyCallbackInfo& info) -{ - if (index < 1 || index > 30) - return; - index--; - - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::Isolate* isolate = info.GetIsolate(); - - // TODO(joey): Object is not getting unset here. - - CString npcAttr = npcObject->getProp(__nAttrPackets[index]); - CString npcAttrValue = npcAttr.readChars(npcAttr.readGUChar()); - - // Get server flag with the property - v8::Local strText = v8::String::NewFromUtf8(isolate, npcAttrValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void NPC_Attrs_Setter(uint32_t index, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - if (index < 1 || index > 30) - return; - index--; - - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::Isolate* isolate = info.GetIsolate(); - - // Get new value - v8::String::Utf8Value newValue(isolate, value); - int strLength = newValue.length(); - if (strLength > 223) - strLength = 223; - - npcObject->setProps(CString() >> (char)__nAttrPackets[index] >> (char)strLength << *newValue, CLVER_2_17, true); - - // Needed to indicate we handled the request - info.GetReturnValue().Set(value); -} - -// PROPERTY: npc.colors -void NPC_GetObject_Colors(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalName = v8::String::NewFromUtf8(isolate, "_internalColors", v8::NewStringType::kInternalized).ToLocalChecked(); - if (self->HasRealNamedProperty(context, internalName).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalName).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - // Grab external data - v8::Local data = info.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("npc.colors"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, npcObject); - - // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed - V8ScriptObject* v8_wrapped = static_cast*>(npcObject->getScriptObject()); - v8_wrapped->addChild("colors", new_instance); - - v8::PropertyAttribute propAttributes = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalName, new_instance, propAttributes).FromJust(); - info.GetReturnValue().Set(new_instance); -} - -void NPC_Colors_Getter(uint32_t index, const v8::PropertyCallbackInfo& info) -{ - if (index > 4) - return; - - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - int colorValue = npcObject->getColorId(index); - info.GetReturnValue().Set(colorValue); -} - -void NPC_Colors_Setter(uint32_t index, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - if (index > 4) - return; - - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - char colorIndex; - - if (value->IsUint32()) - { - // Get new value - unsigned int newValue = value->Uint32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - if (newValue > 32) // Unsure how many colors exist, capping at 32 for now - newValue = 32; - - colorIndex = static_cast(newValue); - } - else // if (value->IsString()) - { - v8::String::Utf8Value newValue(info.GetIsolate(), value); - colorIndex = getColor(*newValue); - if (colorIndex < 0) - return; - } - - npcObject->setColorId(index, colorIndex); - npcObject->updatePropModTime(NPCPROP_COLORS); - - // Needed to indicate we handled the request - info.GetReturnValue().Set(value); -} - -// PROPERTY: npc.flags -void NPC_GetObject_Flags(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalFlags = v8::String::NewFromUtf8(isolate, "_internalFlags", v8::NewStringType::kInternalized).ToLocalChecked(); - if (self->HasRealNamedProperty(context, internalFlags).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalFlags).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - // Grab external data - v8::Local data = info.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("npc.flags"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, npcObject); - - // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed - V8ScriptObject* v8_wrapped = static_cast*>(npcObject->getScriptObject()); - v8_wrapped->addChild("flags", new_instance); - - v8::PropertyAttribute propAttr = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalFlags, new_instance, propAttr).FromJust(); - info.GetReturnValue().Set(new_instance); -} - -void NPC_Flags_Getter(v8::Local property, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::Isolate* isolate = info.GetIsolate(); - - // Get property name - v8::Local name = v8::Local::Cast(property); - v8::String::Utf8Value utf8(isolate, name); - - // Get server flag with the property - CString flagValue = npcObject->getFlag(*utf8); - v8::Local strText = v8::String::NewFromUtf8(isolate, flagValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void NPC_Flags_Setter(v8::Local property, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::Isolate* isolate = info.GetIsolate(); - - // Get property name - v8::Local name = v8::Local::Cast(property); - v8::String::Utf8Value utf8(isolate, name); - - // Get new value - v8::String::Utf8Value newValue(isolate, value); - npcObject->setFlag(*utf8, *newValue); - - // Needed to indicate we handled the request - info.GetReturnValue().Set(value); -} - -void NPC_Flags_Enumerator(const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - // Get flags list - auto& flagList = npcObject->getFlagList(); - - v8::Local result = v8::Array::New(isolate, (int)flagList.size()); - - int idx = 0; - for (auto& flag: flagList) - result->Set(context, idx++, v8::String::NewFromUtf8(isolate, flag.first.c_str()).ToLocalChecked()).Check(); - - info.GetReturnValue().Set(result); -} - -// PROPERTY: npc.save -void NPC_GetObject_Save(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalSave = v8::String::NewFromUtf8(isolate, "_internalSave", v8::NewStringType::kInternalized).ToLocalChecked(); - if (self->HasRealNamedProperty(context, internalSave).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalSave).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - // Grab external data - v8::Local data = info.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("npc.save"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, npcObject); - - // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed - V8ScriptObject* v8_wrapped = static_cast*>(npcObject->getScriptObject()); - v8_wrapped->addChild("save", new_instance); - - v8::PropertyAttribute propSave = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalSave, new_instance, propSave).FromJust(); - info.GetReturnValue().Set(new_instance); -} - -void NPC_Save_Getter(uint32_t index, const v8::PropertyCallbackInfo& info) -{ - if (index > 9) - return; - - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - int npcSaveValue = npcObject->getSave(index); - info.GetReturnValue().Set(npcSaveValue); -} - -void NPC_Save_Setter(uint32_t index, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - if (index > 9) - return; - - V8ENV_SAFE_UNWRAP(info, NPC, npcObject); - - // Get new value - unsigned int newValue = value->Uint32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - if (newValue > 223) - newValue = 223; - - npcObject->setSave(index, static_cast(newValue)); - npcObject->updatePropModTime(NPCPROP_SAVE0 + index); - - // Needed to indicate we handled the request - info.GetReturnValue().Set(value); -} - -void bindClass_NPC(ScriptEngine* scriptEngine) -{ - // Retrieve v8 environment - auto* env = dynamic_cast(scriptEngine->getScriptEnv()); - v8::Isolate* isolate = env->isolate(); - - // External pointer - v8::Local engine_ref = v8::External::New(isolate, scriptEngine); - - // Create V8 string for "npc" - v8::Local npcStr = v8::String::NewFromUtf8Literal(isolate, "npc", v8::NewStringType::kInternalized); - - // Create constructor for class - v8::Local npc_ctor = v8::FunctionTemplate::New(isolate, nullptr, engine_ref); - v8::Local npc_proto = npc_ctor->PrototypeTemplate(); - - npc_ctor->SetClassName(npcStr); - npc_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - // Static functions on the npc object - //npc_ctor->Set(v8::String::NewFromUtf8(isolate, "create"), v8::FunctionTemplate::New(isolate, Npc_createFunction, engine_ref)); - - // Method functions - // TODO(joey): Implement these functions - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "blockagain"), v8::FunctionTemplate::New(isolate, NPC_Function_BlockAgain, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "canwarp"), v8::FunctionTemplate::New(isolate, NPC_Function_CanWarp, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "canwarp2"), v8::FunctionTemplate::New(isolate, NPC_Function_CanWarp2, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "cannotwarp"), v8::FunctionTemplate::New(isolate, NPC_Function_CannotWarp, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "destroy"), v8::FunctionTemplate::New(isolate, NPC_Function_Destroy, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "dontblock"), v8::FunctionTemplate::New(isolate, NPC_Function_DontBlock, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "drawoverplayer"), v8::FunctionTemplate::New(isolate, NPC_Function_DrawOverPlayer, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "drawunderplayer"), v8::FunctionTemplate::New(isolate, NPC_Function_DrawUnderPlayer, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "hide"), v8::FunctionTemplate::New(isolate, NPC_Function_Hide, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "message"), v8::FunctionTemplate::New(isolate, NPC_Function_Message, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "move"), v8::FunctionTemplate::New(isolate, NPC_Function_Move, engine_ref)); - // npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "noplayeronwall"), v8::FunctionTemplate::New(isolate, NPC_Function_NoPlayerOnWall, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setimg"), v8::FunctionTemplate::New(isolate, NPC_Function_SetImg, engine_ref)); // setimg(filename); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setimgpart"), v8::FunctionTemplate::New(isolate, NPC_Function_SetImgPart, engine_ref)); // setimgpart(filename,offsetx,offsety,width,height); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "showcharacter"), v8::FunctionTemplate::New(isolate, NPC_Function_ShowCharacter, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setani"), v8::FunctionTemplate::New(isolate, NPC_Function_SetAni, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setcharani"), v8::FunctionTemplate::New(isolate, NPC_Function_SetAni, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setcharprop"), v8::FunctionTemplate::New(isolate, NPC_Function_SetCharProp, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "settimer"), v8::FunctionTemplate::New(isolate, NPC_Function_SetTimer, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setshape"), v8::FunctionTemplate::New(isolate, NPC_Function_SetShape, engine_ref)); // setshape(1, pixelWidth, pixelHeight) - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "show"), v8::FunctionTemplate::New(isolate, NPC_Function_Show, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "warpto"), v8::FunctionTemplate::New(isolate, NPC_Function_Warpto, engine_ref)); // warpto levelname,x,y; - - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "join"), v8::FunctionTemplate::New(isolate, NPC_Function_Join, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "registerTrigger"), v8::FunctionTemplate::New(isolate, NPC_Function_RegisterTrigger, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setpm"), v8::FunctionTemplate::New(isolate, NPC_Function_SetPM, engine_ref)); - npc_proto->Set(v8::String::NewFromUtf8Literal(isolate, "scheduleevent"), v8::FunctionTemplate::New(isolate, NPC_Function_ScheduleEvent, engine_ref)); - - // Properties - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "ani"), NPC_GetStr_Ani, NPC_SetStr_Ani); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "ap"), NPC_GetInt_Alignment, NPC_SetInt_Alignment); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "bodyimg"), NPC_GetStr_BodyImage, NPC_SetStr_BodyImage); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "bombs"), NPC_GetInt_Bombs, NPC_SetInt_Bombs); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "chat"), NPC_GetStr_Message, NPC_SetStr_Message); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "darts"), NPC_GetInt_Darts, NPC_SetInt_Darts); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "dir"), NPC_GetInt_Dir, NPC_SetInt_Dir); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "glovepower"), NPC_GetInt_GlovePower, NPC_SetInt_GlovePower); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "headimg"), NPC_GetStr_HeadImage, NPC_SetStr_HeadImage); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "hearts"), NPC_GetInt_Hearts, NPC_SetInt_Hearts); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "height"), NPC_GetInt_Height); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "horseimg"), NPC_GetStr_HorseImage, NPC_SetStr_HorseImage); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "id"), NPC_GetInt_Id); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "image"), NPC_GetStr_Image, NPC_SetStr_Image); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "level"), NPC_GetObject_Level); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "levelname"), NPC_GetStr_LevelName); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "name"), NPC_GetStr_Name); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "nick"), NPC_GetStr_Nickname, NPC_SetStr_Nickname); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "rupees"), NPC_GetInt_Rupees, NPC_SetInt_Rupees); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "shieldimg"), NPC_GetStr_ShieldImage, NPC_SetStr_ShieldImage); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "swordimg"), NPC_GetStr_SwordImage, NPC_SetStr_SwordImage); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "timeout"), NPC_GetNum_Timeout, NPC_SetNum_Timeout); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "width"), NPC_GetInt_Width); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "x"), NPC_GetNum_X, NPC_SetNum_X); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "y"), NPC_GetNum_Y, NPC_SetNum_Y); - - //npc_ctor->InstanceTemplate()->SetLazyDataProperty(v8::String::NewFromUtf8(isolate, "attr"), NPC_GetObject_Attrs2, v8::Local(), static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete)); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "attr"), NPC_GetObject_Attrs, nullptr, engine_ref); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "colors"), NPC_GetObject_Colors, nullptr, engine_ref); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "flags"), NPC_GetObject_Flags, nullptr, engine_ref); - npc_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "save"), NPC_GetObject_Save, nullptr, engine_ref); - - // Create the npc-attributes flags template - v8::Local npc_attrs_ctor = v8::FunctionTemplate::New(isolate); - npc_attrs_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "attr")); - npc_attrs_ctor->InstanceTemplate()->SetInternalFieldCount(1); - npc_attrs_ctor->InstanceTemplate()->SetHandler(v8::IndexedPropertyHandlerConfiguration( - NPC_Attrs_Getter, NPC_Attrs_Setter, nullptr, nullptr, nullptr, v8::Local(), - v8::PropertyHandlerFlags::kNone)); - env->setConstructor("npc.attr", npc_attrs_ctor); - - // Create the npc colors template - v8::Local npc_colors_ctor = v8::FunctionTemplate::New(isolate); - npc_colors_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "colors")); - npc_colors_ctor->InstanceTemplate()->SetInternalFieldCount(1); - npc_colors_ctor->InstanceTemplate()->SetHandler(v8::IndexedPropertyHandlerConfiguration( - NPC_Colors_Getter, NPC_Colors_Setter, nullptr, nullptr, nullptr, v8::Local(), - v8::PropertyHandlerFlags::kNone)); - env->setConstructor("npc.colors", npc_colors_ctor); - - // Create the npc flags template - v8::Local npc_flags_ctor = v8::FunctionTemplate::New(isolate); - npc_flags_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "flags")); - npc_flags_ctor->InstanceTemplate()->SetInternalFieldCount(1); - npc_flags_ctor->InstanceTemplate()->SetHandler(v8::NamedPropertyHandlerConfiguration( - NPC_Flags_Getter, NPC_Flags_Setter, nullptr, nullptr, NPC_Flags_Enumerator, v8::Local(), - v8::PropertyHandlerFlags::kOnlyInterceptStrings)); - env->setConstructor("npc.flags", npc_flags_ctor); - - // Create the npc saves template - v8::Local npc_save_ctor = v8::FunctionTemplate::New(isolate); - npc_save_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "save")); - npc_save_ctor->InstanceTemplate()->SetInternalFieldCount(1); - npc_save_ctor->InstanceTemplate()->SetHandler(v8::IndexedPropertyHandlerConfiguration( - NPC_Save_Getter, NPC_Save_Setter, nullptr, nullptr, nullptr, v8::Local(), - v8::PropertyHandlerFlags::kNone)); - env->setConstructor("npc.save", npc_save_ctor); - - // Persist the npc constructor - env->setConstructor(ScriptConstructorId::result, npc_ctor); - - // DISABLED: it would just allow scripts to construct npcs, better off disabled? - // Set the npc constructor on the global object - //v8::Local global = env->globalTemplate(); - //global->Set(npcStr, npc_ctor); -} - -#endif diff --git a/server/src/scripting/v8/V8PlayerImpl.cpp b/server/src/scripting/v8/V8PlayerImpl.cpp deleted file mode 100644 index 3b5f4581d..000000000 --- a/server/src/scripting/v8/V8PlayerImpl.cpp +++ /dev/null @@ -1,1414 +0,0 @@ -#ifdef V8NPCSERVER - - #include - #include - #include - - #include - - #include "NPC.h" - #include "Player.h" - #include "Server.h" - #include "level/Level.h" - #include "scripting/ScriptEngine.h" - #include "scripting/v8/V8ScriptFunction.h" - #include "scripting/v8/V8ScriptObject.h" - -// PROPERTY: player.id -void Player_GetInt_Id(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getId()); -} - -// PROPERTY: player.account -void Player_GetStr_Account(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - CString accountName = playerObject->getAccountName(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), accountName.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -// PROPERTY: player.ani -void Player_GetStr_Ani(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - const CString& animation = playerObject->getAnimation(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), animation.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Player_SetStr_Ani(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - int len = newValue.length(); - if (len > 223) - len = 223; - - CString propPackage; - propPackage >> (char)PLPROP_GANI >> (char)len; - propPackage.write(*newValue, len); - playerObject->setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.ap -void Player_GetInt_Alignment(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getAlignment()); -} - -void Player_SetInt_Alignment(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - char newValue = (char)value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - playerObject->setProps(CString() >> (char)PLPROP_ALIGNMENT >> (char)newValue, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.bodyimg -void Player_GetStr_BodyImage(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - const CString& propValue = playerObject->getBodyImage(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), propValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Player_SetStr_BodyImage(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - int len = newValue.length(); - if (len > 223) - len = 223; - - CString propPackage; - propPackage >> (char)PLPROP_BODYIMG >> (char)len; - propPackage.write(*newValue, len); - playerObject->setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.bombs -void Player_GetInt_Bombs(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getBombCount()); -} - -void Player_SetInt_Bombs(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - char newValue = (char)value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - playerObject->setProps(CString() >> (char)PLPROP_BOMBSCOUNT >> (char)newValue, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.chat -void Player_GetStr_Chat(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - const CString& propValue = playerObject->getChatMsg(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), propValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Player_SetStr_Chat(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - int len = newValue.length(); - if (len > 223) - len = 223; - - CString propPackage; - propPackage >> (char)PLPROP_CURCHAT >> (char)len; - propPackage.write(*newValue, len); - playerObject->setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.darts -void Player_GetInt_Darts(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getArrowCount()); -} - -void Player_SetInt_Darts(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - char newValue = (char)value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - playerObject->setProps(CString() >> (char)PLPROP_ARROWSCOUNT >> (char)newValue, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.dir -void Player_GetInt_Dir(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getSprite() % 4); -} - -void Player_SetInt_Dir(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - char newValue = (char)(value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked() % 4); - playerObject->setProps(CString() >> (char)PLPROP_SPRITE >> (char)newValue, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.fullhearts -void Player_GetInt_Fullhearts(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getMaxPower()); -} - -void Player_SetInt_Fullhearts(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - int newValue = value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - playerObject->setProps(CString() >> (char)PLPROP_MAXPOWER >> (char)clip(newValue, 0, 20), PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.glovepower -void Player_GetInt_GlovePower(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getGlovePower()); -} - -void Player_SetInt_GlovePower(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - char newValue = (char)value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - playerObject->setProps(CString() >> (char)PLPROP_GLOVEPOWER >> (char)newValue, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.guild -void Player_GetStr_Guild(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - const CString& propValue = playerObject->getGuild(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), propValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Player_SetStr_Guild(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::String::Utf8Value newValue = v8::String::Utf8Value(info.GetIsolate(), value); - - CString playerNick = playerObject->getNickname(); - int pos = playerNick.find("(", 0); - if (pos != -1) - playerNick = playerNick.readChars(pos).trimRight(); - if (newValue.length() > 0) - playerNick << " (" << *newValue << ")"; - - int len = playerNick.length(); - if (len > 223) - len = 223; - - CString propPackage; - propPackage >> (char)PLPROP_NICKNAME >> (char)len; - propPackage.write(playerNick.text(), len); - playerObject->setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.hearts -void Player_GetNum_Hearts(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getPower()); -} - -void Player_SetNum_Hearts(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - int newValue = (int)(value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked() * 2); - playerObject->setProps(CString() >> (char)PLPROP_CURPOWER >> (char)clip(newValue, 0, 40), PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.headimg -void Player_GetStr_HeadImage(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - const CString& propValue = playerObject->getHeadImage(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), propValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Player_SetStr_HeadImage(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::String::Utf8Value newValue(info.GetIsolate(), value); - int len = newValue.length(); - if (len > 123) - len = 123; - - CString propPackage; - propPackage >> (char)PLPROP_HEADGIF >> (char)(len + 100); - propPackage.write(*newValue, len); - playerObject->setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.isadmin -void Player_GetBool_IsAdmin(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set((playerObject->getType() & PLTYPE_ANYRC) != 0); -} - -// PROPERTY: player.isclient -void Player_GetBool_IsClient(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set((playerObject->getType() & PLTYPE_ANYCLIENT) != 0); -} - -// PROPERTY: player.isstaff -void Player_GetBool_IsStaff(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->isStaff()); -} - -// PROPERTY: player.level -void Player_GetObject_Level(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - auto levelObject = playerObject->getLevel(); - if (levelObject != nullptr) - { - V8ScriptObject* v8_wrapped = static_cast*>(levelObject->getScriptObject()); - info.GetReturnValue().Set(v8_wrapped->handle(info.GetIsolate())); - return; - } - - info.GetReturnValue().SetNull(); -} - -// PROPERTY: player.levelname -void Player_GetStr_LevelName(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - auto levelObject = playerObject->getLevel(); - - CString levelName; - if (levelObject != nullptr) - levelName = levelObject->getLevelName(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), levelName.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -// PROPERTY: player.mp -void Player_GetInt_MagicPower(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getMagicPower()); -} - -void Player_SetInt_MagicPower(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - unsigned char newValue = (unsigned char)value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - playerObject->setProps(CString() >> (char)PLPROP_MAGICPOINTS >> (char)newValue, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.nickname -void Player_GetStr_Nickname(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - const CString& propValue = playerObject->getNickname(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), propValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Player_SetStr_Nickname(v8::Local props, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::String::Utf8Value newValue = v8::String::Utf8Value(info.GetIsolate(), value); - int len = newValue.length(); - if (len > 223) - len = 223; - - CString propPackage; - propPackage >> (char)PLPROP_NICKNAME >> (char)len; - propPackage.write(*newValue, len); - playerObject->setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.platform -void Player_GetString_Platform(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - const CString& propValue = playerObject->getPlatform(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), propValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -// PROPERTY: player.rupees -void Player_GetInt_Rupees(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getRupees()); -} - -void Player_SetInt_Rupees(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - int newValue = value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - playerObject->setProps(CString() >> (char)PLPROP_RUPEESCOUNT >> (int)newValue, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.shieldimg -void Player_GetStr_ShieldImage(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - const CString& propValue = playerObject->getShieldImage(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), propValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Player_SetStr_ShieldImage(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::String::Utf8Value newValue = v8::String::Utf8Value(info.GetIsolate(), value); - int len = newValue.length(); - if (len > 223) - len = 223; - - CString propPackage; - propPackage >> (char)PLPROP_SHIELDPOWER >> (char)(playerObject->getShieldPower() + 10) >> (char)len; - propPackage.write(*newValue, len); - playerObject->setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.shieldpower -void Player_GetInt_ShieldPower(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getShieldPower()); -} - -void Player_SetInt_ShieldPower(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - char newValue = (char)value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - const CString& shieldImg = playerObject->getShieldImage(); - - CString propPackage; - propPackage >> (char)PLPROP_SHIELDPOWER >> (char)(newValue + 10); - propPackage >> (char)shieldImg.length() << shieldImg; - playerObject->setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.swordimg -void Player_GetStr_SwordImage(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - const CString& propValue = playerObject->getSwordImage(); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), propValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Player_SetStr_SwordImage(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::String::Utf8Value newValue = v8::String::Utf8Value(info.GetIsolate(), value); - int len = newValue.length(); - if (len > 223) - len = 223; - - CString propPackage; - propPackage >> (char)PLPROP_SWORDPOWER >> (char)(playerObject->getSwordPower() + 30) >> (char)len; - propPackage.write(*newValue, len); - playerObject->setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.swordpower -void Player_GetInt_SwordPower(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getSwordPower()); -} - -void Player_SetInt_SwordPower(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - char newValue = (char)value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - const CString& swordImg = playerObject->getSwordImage(); - - CString propPackage; - propPackage >> (char)PLPROP_SWORDPOWER >> (char)(newValue + 30); - propPackage >> (char)swordImg.length() << swordImg; - playerObject->setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.x -void Player_GetNum_X(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getX()); -} - -void Player_SetNum_X(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - double newValue = value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - int newValueInt = (int)(16 * newValue); - if (newValueInt < 0) - { - newValueInt = (-newValueInt << 1) | 0x0001; - } - else - newValueInt <<= 1; - - playerObject->setProps(CString() >> (char)PLPROP_X2 >> (short)newValueInt, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.y -void Player_GetNum_Y(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - info.GetReturnValue().Set(playerObject->getY()); -} - -void Player_SetNum_Y(v8::Local prop, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - double newValue = value->NumberValue(info.GetIsolate()->GetCurrentContext()).ToChecked(); - int newValueInt = (int)(16 * newValue); - if (newValueInt < 0) - { - newValueInt = (-newValueInt << 1) | 0x0001; - } - else - newValueInt <<= 1; - - playerObject->setProps(CString() >> (char)PLPROP_Y2 >> (short)newValueInt, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - -// PROPERTY: player.attr -void Player_GetObject_Attrs(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalAttr = v8::String::NewFromUtf8(isolate, "_internalAttr", v8::NewStringType::kInternalized).ToLocalChecked(); - if (self->HasRealNamedProperty(context, internalAttr).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalAttr).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - // Grab external data - v8::Local data = info.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("player.attr"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, playerObject); - - // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed - V8ScriptObject* v8_wrapped = static_cast*>(playerObject->getScriptObject()); - v8_wrapped->addChild("attr", new_instance); - - v8::PropertyAttribute propAttr = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalAttr, new_instance, propAttr).FromJust(); - info.GetReturnValue().Set(new_instance); -} - -const char __pAttrPackets[30] = { 37, 38, 39, 40, 41, 46, 47, 48, 49, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74 }; - -void Player_Attr_Getter(uint32_t index, const v8::PropertyCallbackInfo& info) -{ - if (index < 1 || index > 30) - return; - index--; - - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::Isolate* isolate = info.GetIsolate(); - - CString playerAttr = playerObject->getProp(__pAttrPackets[index]); - CString playerAttrValue = playerAttr.readChars(playerAttr.readGUChar()); - - v8::Local strText = v8::String::NewFromUtf8(isolate, playerAttrValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Player_Attr_Setter(uint32_t index, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - if (index < 1 || index > 30) - return; - index--; - - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::Isolate* isolate = info.GetIsolate(); - - // Get new value - v8::String::Utf8Value newValue(isolate, value); - int strLength = newValue.length(); - if (strLength > 223) - strLength = 223; - - CString propPackage; - propPackage.writeGChar(__pAttrPackets[index]); - propPackage.writeGChar(strLength); - propPackage.write(*newValue, strLength); - playerObject->setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - - // Needed to indicate we handled the request - info.GetReturnValue().Set(value); -} - -// PROPERTY: player.colors -void Player_GetObject_Colors(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalName = v8::String::NewFromUtf8(isolate, "_internalColors", v8::NewStringType::kInternalized).ToLocalChecked(); - if (self->HasRealNamedProperty(context, internalName).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalName).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - // Grab external data - v8::Local data = info.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("player.colors"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, playerObject); - - // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed - V8ScriptObject* v8_wrapped = static_cast*>(playerObject->getScriptObject()); - v8_wrapped->addChild("colors", new_instance); - - v8::PropertyAttribute propAttributes = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalName, new_instance, propAttributes).FromJust(); - info.GetReturnValue().Set(new_instance); -} - -void Player_Colors_Getter(uint32_t index, const v8::PropertyCallbackInfo& info) -{ - if (index > 4) - return; - - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - int colorValue = playerObject->getColorId(index); - info.GetReturnValue().Set(colorValue); -} - -void Player_Colors_Setter(uint32_t index, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - if (index > 4) - return; - - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - CString playerProp = playerObject->getProp(PLPROP_COLORS); - char colors[5]; - for (unsigned int i = 0; i < 5; i++) - colors[i] = playerProp.readGUChar(); - - if (value->IsUint32()) - { - // Get new value - unsigned int newValue = value->Uint32Value(info.GetIsolate()->GetCurrentContext()).ToChecked(); - if (newValue > 32) // Unsure how many colors exist, capping at 32 for now - newValue = 32; - - colors[index] = newValue; - } - else // if (value->IsString()) - { - v8::String::Utf8Value newValue(info.GetIsolate(), value); - colors[index] = getColor(*newValue); - if (colors[index] < 0) - return; - } - - CString propPackage; - propPackage >> (char)PLPROP_COLORS >> (char)colors[0] >> (char)colors[1] >> (char)colors[2] >> (char)colors[3] >> (char)colors[4]; - playerObject->setProps(propPackage, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - - // Needed to indicate we handled the request - info.GetReturnValue().Set(value); -} - -// PROPERTY: Player Flags -void Player_GetObject_Flags(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalFlags = v8::String::NewFromUtf8(isolate, "_internalFlags", v8::NewStringType::kInternalized).ToLocalChecked(); - if (self->HasRealNamedProperty(context, internalFlags).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalFlags).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - // Grab external data - v8::Local data = info.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("player.flags"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, playerObject); - - // Adds child property to the wrapped object, so it can clear the pointer when the parent is destroyed - V8ScriptObject* v8_wrapped = static_cast*>(playerObject->getScriptObject()); - v8_wrapped->addChild("flags", new_instance); - - v8::PropertyAttribute propAttr = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalFlags, new_instance, propAttr).FromJust(); - info.GetReturnValue().Set(new_instance); -} - -void Player_Flags_Getter(v8::Local property, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::Isolate* isolate = info.GetIsolate(); - - // TODO(joey): playerObject is not getting unset here. - - // Get property name - v8::Local name = v8::Local::Cast(property); - v8::String::Utf8Value utf8(isolate, name); - - // Get server flag with the property - CString flagValue = playerObject->getFlag(*utf8); - v8::Local strText = v8::String::NewFromUtf8(isolate, flagValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Player_Flags_Setter(v8::Local property, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::Isolate* isolate = info.GetIsolate(); - - // Get property name - v8::Local name = v8::Local::Cast(property); - v8::String::Utf8Value utf8(isolate, name); - - // Get new value - v8::String::Utf8Value newValue(isolate, value); - if (newValue.length() == 0) - { - playerObject->deleteFlag(*utf8, true); - } - else - { - playerObject->setFlag(*utf8, *newValue, true); - } - - // Needed to indicate we handled the request - info.GetReturnValue().Set(value); -} - -void Player_Flags_Enumerator(const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - // Get flags list - auto& flagList = playerObject->getFlagList(); - - v8::Local result = v8::Array::New(isolate, (int)flagList.size()); - - int idx = 0; - for (auto it = flagList.begin(); it != flagList.end(); ++it) - result->Set(context, idx++, v8::String::NewFromUtf8(isolate, it->first.c_str()).ToLocalChecked()).Check(); - - info.GetReturnValue().Set(result); -} - -void Player_GetArray_Weapons(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Player, playerObject); - - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - - // Get npcs list - auto& weaponList = playerObject->getWeaponList(); - - v8::Local result = v8::Array::New(isolate, (int)weaponList.size()); - - // TODO(joey): We don't store the weapon objects on the player, maybe we should so we can use the object directly - // in scripts. - int idx = 0; - for (auto& weapon: weaponList) - { - //V8ScriptObject *v8_wrapped = static_cast *>((*it)->getScriptObject()); - v8::Local weaponName = v8::String::NewFromUtf8(info.GetIsolate(), weapon.text()).ToLocalChecked(); - result->Set(context, idx++, weaponName).Check(); - } - - info.GetReturnValue().Set(result); -} - -// Player Function: player.addweapon("weaponName"); -void Player_Function_AddWeapon(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - // Validate arguments - if (args[0]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - v8::String::Utf8Value newValue(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - - bool result = playerObject->addWeapon(*newValue); - args.GetReturnValue().Set(result); - return; - } - - args.GetReturnValue().Set(false); -} - -// Player Function: player.hasweapon("weaponName"); -void Player_Function_HasWeapon(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - // Validate arguments - if (args[0]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - v8::String::Utf8Value newValue(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - - bool result = playerObject->hasWeapon(*newValue); - args.GetReturnValue().Set(result); - return; - } - - args.GetReturnValue().Set(false); -} - -// Player Function: player.removeweapon("weaponName"); -void Player_Function_RemoveWeapon(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - // Validate arguments - if (args[0]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - v8::String::Utf8Value newValue(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - - bool result = playerObject->deleteWeapon(*newValue); - args.GetReturnValue().Set(result); - return; - } - - args.GetReturnValue().Set(false); -} - -// Player Function: player.disableweapons(); -void Player_Function_DisableWeapons(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - playerObject->disableWeapons(); -} - -// Player Function: player.enableweapons(); -void Player_Function_EnableWeapons(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - playerObject->enableWeapons(); -} - -// Player Function: player.freezeplayer(); -void Player_Function_FreezePlayer(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - playerObject->freezePlayer(); -} - -// Player Function: player.unfreezeplayer(); -void Player_Function_UnfreezePlayer(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - playerObject->unfreezePlayer(); -} - -// Player Function: player.say("message"); or player.say(index) for signs in a level -void Player_Function_Say(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - // Validate arguments - if (args[0]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - v8::String::Utf8Value newValue(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - playerObject->sendSignMessage(*newValue); - } - else if (args[0]->IsInt32()) - { - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - int signIndex = args[0]->Int32Value(isolate->GetCurrentContext()).ToChecked(); - - auto level = playerObject->getLevel(); - if (level != nullptr) - { - auto& signs = level->getSigns(); - if (signIndex < signs.size()) - playerObject->sendSignMessage(signs[signIndex]->getUText().replaceAll("\n", "#b")); - } - } -} - -// Player Function: player.sendpm("message"); -void Player_Function_SendPM(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - // Validate arguments - if (args[0]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - // TODO(joey): Function this like Server::sendPM(fromPlayer, toPlayer, message); - - // Get server - v8::Local data = args.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - Server* server = scriptEngine->getServer(); - - // Get npc-server - auto npcServer = server->getNPCServer(); - assert(npcServer); - - // Parse argument - v8::String::Utf8Value newValue(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - - // PM message - CString pmMessage(*newValue); - playerObject->sendPacket(CString() >> (char)PLO_PRIVATEMESSAGE >> (short)npcServer->getId() << "\"\"," << pmMessage.gtokenize()); - } -} - -// Player Function: player.sendrpgmessage("message"); -void Player_Function_SendRPGMessage(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - // Validate arguments - if (args[0]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - v8::String::Utf8Value newValue(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - playerObject->sendRPGMessage(*newValue); - } -} - -// Player Function: player.setani("walk", "ani", "params"); -void Player_Function_SetAni(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_MINARGCOUNT(args, isolate, 1); - - // Validate arguments - if (args[0]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - v8::String::Utf8Value newValue(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - - CString animation(*newValue); - for (int i = 1; i < args.Length(); i++) - { - if (args[i]->IsString() || args[i]->IsNumber()) - { - v8::String::Utf8Value aniParam(isolate, args[i]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - animation << "," << *aniParam; - } - } - - playerObject->setAni(animation); - } -} - -// Player Function: player.setlevel2("levelname", x, y); -void Player_Function_SetLevel2(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 3); - - // Validate arguments - if (args[0]->IsString() && args[1]->IsNumber() && args[2]->IsNumber()) - { - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - v8::Local context = isolate->GetCurrentContext(); - - v8::String::Utf8Value levelName(isolate, args[0]->ToString(isolate->GetCurrentContext()).ToLocalChecked()); - double newX = args[1]->NumberValue(context).ToChecked(); - double newY = args[2]->NumberValue(context).ToChecked(); - - bool result = playerObject->warp(*levelName, (float)newX, (float)newY); - args.GetReturnValue().Set(result); - return; - } - - args.GetReturnValue().Set(false); -} - -// Player function: player.attached(npcId|npcObject) -void Player_Function_Attached(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - unsigned int npcId = 0; - - // Validate arguments - if (args[0]->IsInt32()) - { - v8::Local context = isolate->GetCurrentContext(); - npcId = args[0]->Int32Value(context).ToChecked(); - } - else if (args[0]->IsObject()) - { - v8::Local context = isolate->GetCurrentContext(); - v8::Local obj = args[0]->ToObject(context).ToLocalChecked(); - - std::string npcConstructor = *v8::String::Utf8Value(isolate, obj->GetConstructorName()); - if (npcConstructor == "npc") - { - NPC* npcObject = unwrapObject(obj); - if (npcObject) - npcId = npcObject->getId(); - } - } - - // - if (npcId > 0) - { - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - args.GetReturnValue().Set(playerObject->getAttachedNPC() == npcId); - return; - } - - args.GetReturnValue().Set(false); -} - -// Player Function: player.attachnpc(npcid); -void Player_Function_AttachNpc(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - unsigned int npcId = 0; - - // Validate arguments - if (args[0]->IsInt32()) - { - v8::Local context = isolate->GetCurrentContext(); - npcId = args[0]->Int32Value(context).ToChecked(); - } - else if (args[0]->IsObject()) - { - v8::Local context = isolate->GetCurrentContext(); - v8::Local obj = args[0]->ToObject(context).ToLocalChecked(); - - std::string npcConstructor = *v8::String::Utf8Value(isolate, obj->GetConstructorName()); - if (npcConstructor == "npc") - { - NPC* npcObject = unwrapObject(obj); - if (npcObject) - npcId = npcObject->getId(); - } - } - - // - if (npcId > 0) - { - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - CString propPacket; - propPacket >> (char)PLPROP_ATTACHNPC >> (char)0 >> (int)npcId; - playerObject->setProps(propPacket, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); - args.GetReturnValue().Set(true); - return; - } - - args.GetReturnValue().Set(false); -} - -void Player_Function_DetachNpc(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - CString propPacket; - propPacket >> (char)PLPROP_ATTACHNPC >> (char)0 >> (int)0; - playerObject->setProps(propPacket, PLSETPROPS_FORWARD | PLSETPROPS_FORWARDSELF); -} - - #include "scripting/ScriptClass.h" - #include "scripting/v8/V8ScriptWrappers.h" - -// Player Function: player.join("class"); -void Player_Function_Join(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - if (args[0]->IsString()) - { - v8::Local context = isolate->GetCurrentContext(); - v8::Local data = args.Data().As(); - ScriptEngine* scriptEngine = static_cast(data->Value()); - - std::string className = *v8::String::Utf8Value(isolate, args[0]->ToString(context).ToLocalChecked()); - - Server* server = scriptEngine->getServer(); - auto classObj = server->getClass(className); - - if (classObj && !classObj->getSource().empty()) - { - auto& classCode = classObj->getSource(); - - // Wrap code - std::string classCodeWrap = wrapScript(classCode.getServerSide()); - - // TODO(joey): maybe we shouldn't cache this using this method, since classes can be used with - // multiple wrappers. - IScriptFunction* function = scriptEngine->compileCache(classCodeWrap, false); - if (function != nullptr) - { - V8ScriptFunction* v8_function = static_cast(function); - - v8::Local newArgs[] = { args.This() }; - - // Execute - v8::TryCatch try_catch(isolate); - v8::Local scriptFunction = v8_function->function(); - v8::MaybeLocal scriptTableRet = scriptFunction->Call(context, args.This(), 1, newArgs); - if (!scriptTableRet.IsEmpty()) - { - args.GetReturnValue().Set(scriptTableRet.ToLocalChecked()); - return; - } - } - - // TODO(joey): error handling - } - } -} - -void Player_Function_TriggerAction(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_MINARGCOUNT(args, isolate, 4); - - if (args[0]->IsNumber() && args[1]->IsNumber() && args[2]->IsString() && args[3]->IsString()) - { - v8::Local context = isolate->GetCurrentContext(); - char trigx = (char)(args[0]->NumberValue(context).ToChecked() * 2); - char trigy = (char)(args[1]->NumberValue(context).ToChecked() * 2); - - CString trigaction = *v8::String::Utf8Value(isolate, args[2]->ToString(context).ToLocalChecked()); - for (int i = 3; i < args.Length(); i++) - trigaction << "," << *v8::String::Utf8Value(isolate, args[i]->ToString(context).ToLocalChecked()); - - // Unwrap Object - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - playerObject->sendPacket(CString() >> (char)PLO_TRIGGERACTION >> (short)0 >> (int)0 >> (char)trigx >> (char)trigy << trigaction); - } -} - -// Player Function : player.triggerclient("gui"/"weapon", wep, args) -> onActionClientSide -void Player_Function_TriggerClient(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_MINARGCOUNT(args, isolate, 2); - - if (args[0]->IsString() && args[1]->IsString()) - { - v8::Local context = isolate->GetCurrentContext(); - - CString actionType = *v8::String::Utf8Value(isolate, args[0]->ToString(context).ToLocalChecked()); - if (actionType == "gui" || actionType == "weapon") - { - CString trigaction = CString("clientside"); - for (int i = 1; i < args.Length(); i++) - trigaction << "," << *v8::String::Utf8Value(isolate, args[i]->ToString(context).ToLocalChecked()); - - // Unwrap Object - V8ENV_SAFE_UNWRAP(args, Player, playerObject); - - playerObject->sendPacket(CString() >> (char)PLO_TRIGGERACTION >> (short)0 >> (int)0 >> (char)0 >> (char)0 << trigaction); - } - } -} - -void bindClass_Player(ScriptEngine* scriptEngine) -{ - // Retrieve v8 environment - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - v8::Isolate* isolate = env->isolate(); - - // External pointer - v8::Local engine_ref = v8::External::New(isolate, scriptEngine); - - // Create V8 string for "player" - v8::Local className = v8::String::NewFromUtf8Literal(isolate, "player", v8::NewStringType::kInternalized); - - // Create constructor for class - v8::Local player_ctor = v8::FunctionTemplate::New(isolate, nullptr, engine_ref); // , Player_Constructor); - v8::Local player_proto = player_ctor->PrototypeTemplate(); - - player_ctor->SetClassName(className); - player_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - // Method functions - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "addweapon"), v8::FunctionTemplate::New(isolate, Player_Function_AddWeapon)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "disableweapons"), v8::FunctionTemplate::New(isolate, Player_Function_DisableWeapons)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "enableweapons"), v8::FunctionTemplate::New(isolate, Player_Function_EnableWeapons)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "freezeplayer"), v8::FunctionTemplate::New(isolate, Player_Function_FreezePlayer, engine_ref)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "unfreezeplayer"), v8::FunctionTemplate::New(isolate, Player_Function_UnfreezePlayer, engine_ref)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "hasweapon"), v8::FunctionTemplate::New(isolate, Player_Function_HasWeapon)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "removeweapon"), v8::FunctionTemplate::New(isolate, Player_Function_RemoveWeapon)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "say"), v8::FunctionTemplate::New(isolate, Player_Function_Say, engine_ref)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "sendpm"), v8::FunctionTemplate::New(isolate, Player_Function_SendPM, engine_ref)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "sendrpgmessage"), v8::FunctionTemplate::New(isolate, Player_Function_SendRPGMessage, engine_ref)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setani"), v8::FunctionTemplate::New(isolate, Player_Function_SetAni, engine_ref)); - //player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setgender"), v8::FunctionTemplate::New(isolate, Player_Function_SetGender, engine_ref)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setlevel2"), v8::FunctionTemplate::New(isolate, Player_Function_SetLevel2, engine_ref)); - //player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setplayerprop"), v8::FunctionTemplate::New(isolate, Player_Function_SetPlayerProp, engine_ref)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "attached"), v8::FunctionTemplate::New(isolate, Player_Function_Attached, engine_ref)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "attachnpc"), v8::FunctionTemplate::New(isolate, Player_Function_AttachNpc, engine_ref)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "detachnpc"), v8::FunctionTemplate::New(isolate, Player_Function_DetachNpc, engine_ref)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "join"), v8::FunctionTemplate::New(isolate, Player_Function_Join, engine_ref)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "triggeraction"), v8::FunctionTemplate::New(isolate, Player_Function_TriggerAction, engine_ref)); - player_proto->Set(v8::String::NewFromUtf8Literal(isolate, "triggerclient"), v8::FunctionTemplate::New(isolate, Player_Function_TriggerClient, engine_ref)); - - // Properties - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "id"), Player_GetInt_Id); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "account"), Player_GetStr_Account); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "ani"), Player_GetStr_Ani, Player_SetStr_Ani); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "ap"), Player_GetInt_Alignment, Player_SetInt_Alignment); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "bodyimg"), Player_GetStr_BodyImage, Player_SetStr_BodyImage); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "bombs"), Player_GetInt_Bombs, Player_SetInt_Bombs); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "chat"), Player_GetStr_Chat, Player_SetStr_Chat); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "darts"), Player_GetInt_Darts, Player_SetInt_Darts); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "dir"), Player_GetInt_Dir, Player_SetInt_Dir); - //player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "hatimg"), Player_GetStr_HatImage, Player_SetStr_HatImage); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "hearts"), Player_GetNum_Hearts, Player_SetNum_Hearts); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "headimg"), Player_GetStr_HeadImage, Player_SetStr_HeadImage); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "fullhearts"), Player_GetInt_Fullhearts, Player_SetInt_Fullhearts); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "glovepower"), Player_GetInt_GlovePower, Player_SetInt_GlovePower); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "guild"), Player_GetStr_Guild, Player_SetStr_Guild); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "isadmin"), Player_GetBool_IsAdmin); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "isclient"), Player_GetBool_IsClient); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "isstaff"), Player_GetBool_IsStaff); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "level"), Player_GetObject_Level); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "levelname"), Player_GetStr_LevelName); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "mp"), Player_GetInt_MagicPower, Player_SetInt_MagicPower); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "nick"), Player_GetStr_Nickname, Player_SetStr_Nickname); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "platform"), Player_GetString_Platform); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "rupees"), Player_GetInt_Rupees, Player_SetInt_Rupees); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "shieldimg"), Player_GetStr_ShieldImage, Player_SetStr_ShieldImage); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "shieldpower"), Player_GetInt_ShieldPower, Player_SetInt_ShieldPower); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "swordimg"), Player_GetStr_SwordImage, Player_SetStr_SwordImage); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "swordpower"), Player_GetInt_SwordPower, Player_SetInt_SwordPower); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "x"), Player_GetNum_X, Player_SetNum_X); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "y"), Player_GetNum_Y, Player_SetNum_Y); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "attr"), Player_GetObject_Attrs, nullptr, engine_ref); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "colors"), Player_GetObject_Colors, nullptr, engine_ref); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "flags"), Player_GetObject_Flags, nullptr, engine_ref); - player_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "weapons"), Player_GetArray_Weapons); - - // Create the player attr template - v8::Local player_attr_ctor = v8::FunctionTemplate::New(isolate); - player_attr_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "attr")); - player_attr_ctor->InstanceTemplate()->SetInternalFieldCount(1); - player_attr_ctor->InstanceTemplate()->SetHandler(v8::IndexedPropertyHandlerConfiguration( - Player_Attr_Getter, Player_Attr_Setter, nullptr, nullptr, nullptr, v8::Local(), - v8::PropertyHandlerFlags::kNone)); - env->setConstructor("player.attr", player_attr_ctor); - - // Create the player colors template - v8::Local player_colors_ctor = v8::FunctionTemplate::New(isolate); - player_colors_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "colors")); - player_colors_ctor->InstanceTemplate()->SetInternalFieldCount(1); - player_colors_ctor->InstanceTemplate()->SetHandler(v8::IndexedPropertyHandlerConfiguration( - Player_Colors_Getter, Player_Colors_Setter, nullptr, nullptr, nullptr, v8::Local(), - v8::PropertyHandlerFlags::kNone)); - env->setConstructor("player.colors", player_colors_ctor); - - // Create the player flags template - v8::Local player_flags_ctor = v8::FunctionTemplate::New(isolate); - player_flags_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "flags")); - player_flags_ctor->InstanceTemplate()->SetInternalFieldCount(1); - player_flags_ctor->InstanceTemplate()->SetHandler(v8::NamedPropertyHandlerConfiguration( - Player_Flags_Getter, Player_Flags_Setter, nullptr, nullptr, Player_Flags_Enumerator, v8::Local(), - v8::PropertyHandlerFlags::kHasNoSideEffect)); - env->setConstructor("player.flags", player_flags_ctor); - - // Persist the player constructor - env->setConstructor(ScriptConstructorId::result, player_ctor); - - // Set the player constructor on the global object - //v8::Local global = env->globalTemplate(); - //global->Set(className, player_ctor); -} - -#endif diff --git a/server/src/scripting/v8/V8ScriptEnv.cpp b/server/src/scripting/v8/V8ScriptEnv.cpp deleted file mode 100644 index 0fd83e0a3..000000000 --- a/server/src/scripting/v8/V8ScriptEnv.cpp +++ /dev/null @@ -1,210 +0,0 @@ -#include -#include - -#include "scripting/interface/ScriptBindings.h" -#include "scripting/v8/V8ScriptEnv.h" -#include "scripting/v8/V8ScriptArguments.h" -#include "scripting/v8/V8ScriptFunction.h" - -bool _v8_initialized = false; -int V8ScriptEnv::m_count = 0; -std::unique_ptr V8ScriptEnv::m_platform; - -V8ScriptEnv::V8ScriptEnv() - : m_initialized(false), m_isolate(nullptr) -{ -} - -V8ScriptEnv::~V8ScriptEnv() -{ - this->cleanup(); -} - -void V8ScriptEnv::initialize() -{ - if (m_initialized) - return; - - // Force v8 to use strict mode - const char* flags = "--use_strict"; - v8::V8::SetFlagsFromString(flags, strlen(flags)); - - // Initialize V8. - v8::V8::InitializeICUDefaultLocation("."); - v8::V8::InitializeExternalStartupData("."); - - // Initialize v8 if this is the first vm - if (!_v8_initialized) - { - m_platform = v8::platform::NewDefaultPlatform(); - v8::V8::InitializePlatform(m_platform.get()); - v8::V8::Initialize(); - _v8_initialized = true; - } - - // Sets the lower limit of the stack, as far as I know std::thread does not let me control the size - // of the stack, or lets me know how large it is. This fix seems to work for now, if it causes issues we can - // most-likely figure out what the default stack size is per thread and set the constraints through that. - // Fix from https://fw.hardijzer.nl/?p=97 - v8::ResourceConstraints rc; - rc.set_stack_limit((uint32_t*)(((uint64_t)&rc) / 2)); - m_createParams.constraints = rc; - - // Create v8 isolate - m_createParams.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); - m_isolate = v8::Isolate::New(m_createParams); - - // Create global object and persist it - v8::HandleScope handle_scope(m_isolate); - v8::Local global_tpl = v8::ObjectTemplate::New(m_isolate); - m_globalTpl.Reset(m_isolate, global_tpl); - - // Increment v8 environment counter - V8ScriptEnv::m_count++; - - // Initialized - m_initialized = true; -} - -void V8ScriptEnv::cleanup(bool shutDown) -{ - if (!m_initialized) - { - return; - } - - // Clear persistent handles to function-constructors - for (auto& it: m_constructorMap) - it.second.Reset(); - m_constructorMap.clear(); - - // Clear persistent handles to the global object, and context - m_global.Reset(); - m_globalTpl.Reset(); - m_context.Reset(); - - // Dispose of v8 isolate - m_isolate->Dispose(); - m_isolate = nullptr; - delete m_createParams.array_buffer_allocator; - - // Decrease v8 environment counter - V8ScriptEnv::m_count--; - m_initialized = false; - - // Shutdown v8 - if (shutDown && V8ScriptEnv::m_count == 0) - { - // After this is run, you can not reinitialize v8! - v8::V8::Dispose(); - v8::V8::ShutdownPlatform(); - _v8_initialized = false; - } -} - -bool V8ScriptEnv::parseErrors(v8::TryCatch* tryCatch) -{ - if (tryCatch->HasCaught()) - { - // Fetch the v8 isolate and context - v8::Isolate* isolate = this->isolate(); - v8::Local context = this->context(); - - v8::Handle message = tryCatch->Message(); - if (!message.IsEmpty()) - { - m_lastScriptError.error = *v8::String::Utf8Value(isolate, tryCatch->Exception()); - m_lastScriptError.filename = *v8::String::Utf8Value(isolate, message->GetScriptResourceName()); - m_lastScriptError.error_line = *v8::String::Utf8Value(isolate, message->GetSourceLine(context).ToLocalChecked()); - m_lastScriptError.lineno = message->GetLineNumber(context).ToChecked(); - m_lastScriptError.startcol = message->GetStartColumn(context).ToChecked(); - m_lastScriptError.endcol = message->GetEndColumn(context).ToChecked(); - } - - return true; - } - - return false; -} - -IScriptFunction* V8ScriptEnv::compile(const std::string& name, const std::string& source) -{ - // Fetch the v8 isolate and context - v8::Isolate* isolate = this->isolate(); - v8::Local context = this->context(); - - // Create a stack-allocated scope for v8 calls - v8::Locker lock(isolate); - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope handle_scope(isolate); - - // Create context with global template - if (context.IsEmpty()) - { - v8::Local global_tpl = persistentToLocal(isolate, m_globalTpl); - context = v8::Context::New(isolate, 0, global_tpl); - m_context.Reset(isolate, context); - m_global.Reset(isolate, context->Global()); - } - - // Enter the context for compiling and running the script. - v8::Context::Scope context_scope(context); - - // Create a string containing the JavaScript source code. - v8::Local sourceStr = v8::String::NewFromUtf8(isolate, source.c_str(), v8::NewStringType::kNormal).ToLocalChecked(); - - // Compile the source code. - v8::TryCatch try_catch(isolate); - v8::ScriptOrigin origin(v8::String::NewFromUtf8(isolate, name.c_str(), v8::NewStringType::kNormal).ToLocalChecked()); - v8::Local script; - if (!v8::Script::Compile(context, sourceStr, &origin).ToLocal(&script)) - { - parseErrors(&try_catch); - return nullptr; - } - - // Run the script to get the result. - v8::Local result; - - if (!script->Run(context).ToLocal(&result)) - { - parseErrors(&try_catch); - return nullptr; - } - - assert(!try_catch.HasCaught()); - return new V8ScriptFunction(this, result.As()); -} - -void V8ScriptEnv::callFunctionInScope(std::function function) -{ - // Fetch the v8 isolate, and create a stack-allocated scope for v8 calls - v8::Locker lock(isolate()); - v8::Isolate::Scope isolate_scope(isolate()); - v8::HandleScope handle_scope(isolate()); - - // Call function in context if we have one - if (m_context.IsEmpty()) - function(); - else - { - v8::Context::Scope context_scope(context()); - function(); - } -} - -void V8ScriptEnv::terminateExecution() -{ - assert(m_isolate); - m_isolate->TerminateExecution(); -} - -bool V8ScriptEnv::setConstructor(const std::string& key, v8::Local func_tpl) -{ - auto it = m_constructorMap.find(key); - if (it != m_constructorMap.end()) - return false; - - m_constructorMap[key] = v8::Global(isolate(), func_tpl); - return true; -} diff --git a/server/src/scripting/v8/V8ServerImpl.cpp b/server/src/scripting/v8/V8ServerImpl.cpp deleted file mode 100644 index f95dbf0e4..000000000 --- a/server/src/scripting/v8/V8ServerImpl.cpp +++ /dev/null @@ -1,633 +0,0 @@ -#ifdef V8NPCSERVER - - #include - #include - #include - - #include "NPC.h" - #include "Player.h" - #include "level/Level.h" - #include "scripting/ScriptEngine.h" - #include "scripting/v8/V8ScriptFunction.h" - #include "scripting/v8/V8ScriptObject.h" - -void Server_Function_HttpGet(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Check the number of arguments passed. - if (args.Length() < 1) - { - isolate->ThrowException(v8::Exception::TypeError( - v8::String::NewFromUtf8(isolate, "Wrong number of arguments").ToLocalChecked())); - return; - } - - // Check the argument types. - if (!args[0]->IsString()) - { - isolate->ThrowException(v8::Exception::TypeError( - v8::String::NewFromUtf8(isolate, "Wrong arguments").ToLocalChecked())); - return; - } - - v8::String::Utf8Value url(isolate, args[0]); - - std::string urlAndQuery = *url; - std::regex urlRegex("(https?://[^/]+)(/?.*)"); - std::smatch match; - std::string onlyPath; - std::string onlyUrl; - - if (std::regex_search(urlAndQuery, match, urlRegex) && match.size() == 3) - { - onlyUrl = match[1].str(); - onlyPath = match[2].str(); - } - else - { - isolate->ThrowException(v8::Exception::Error( - v8::String::NewFromUtf8(isolate, "Invalid url").ToLocalChecked())); - return; - } - - auto cli = httplib::Client(onlyUrl); - cli.enable_server_certificate_verification(false); - - auto r = cli.Get(onlyPath); - - if (r->status < 200 || r->status >= 300) - { - isolate->ThrowException(v8::Exception::Error( - v8::String::NewFromUtf8(isolate, to_string(r.error()).c_str()).ToLocalChecked())); - return; - } - - args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, r->body.c_str()).ToLocalChecked()); -} - -void Server_Function_HttpPost(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Check the number of arguments passed. - if (args.Length() < 2) - { - isolate->ThrowException(v8::Exception::TypeError( - v8::String::NewFromUtf8(isolate, "Wrong number of arguments").ToLocalChecked())); - return; - } - - // Check the argument types. - if (!args[0]->IsString() || !args[1]->IsString()) - { - isolate->ThrowException(v8::Exception::TypeError( - v8::String::NewFromUtf8(isolate, "Wrong arguments").ToLocalChecked())); - return; - } - - v8::String::Utf8Value url(isolate, args[0]); - v8::String::Utf8Value postData(isolate, args[1]); - std::string urlAndQuery = *url; - std::regex urlRegex("(https?://[^/]+)(/?.*)"); - std::smatch match; - std::string onlyPath; - std::string onlyUrl; - - if (std::regex_search(urlAndQuery, match, urlRegex) && match.size() == 3) - { - onlyUrl = match[1].str(); - onlyPath = match[2].str(); - } - else - { - isolate->ThrowException(v8::Exception::Error( - v8::String::NewFromUtf8(isolate, "Invalid url").ToLocalChecked())); - return; - } - - std::string contentTypeStr; - if (args.Length() >= 3) - { - v8::String::Utf8Value contentType(isolate, args[2]); - contentTypeStr = *contentType; - } - else - { - contentTypeStr = "application/json"; - } - - auto cli = httplib::Client(onlyUrl); - cli.enable_server_certificate_verification(false); - - std::string postDataStr = *postData; - - auto r = cli.Post(onlyPath, postDataStr, contentTypeStr); - - if (r->status < 200 || r->status >= 300) - { - isolate->ThrowException(v8::Exception::Error( - v8::String::NewFromUtf8(isolate, to_string(r.error()).c_str()).ToLocalChecked())); - return; - } - - args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, r->body.c_str()).ToLocalChecked()); -} - -void Server_Function_FindLevel(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - v8::Local context = args.GetIsolate()->GetCurrentContext(); - Server* serverObject = unwrapObject(args.This()); - - // Find level from user input - if (args[0]->IsString()) - { - v8::String::Utf8Value levelName(isolate, args[0]->ToString(context).ToLocalChecked()); - auto levelObject = serverObject->getLevel(*levelName); - if (levelObject != nullptr) - { - V8ScriptObject* v8_wrapped = static_cast*>(levelObject->getScriptObject()); - args.GetReturnValue().Set(v8_wrapped->handle(isolate)); - } - } -} - -void Server_Function_CreateLevel(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 2); - - v8::Local context = args.GetIsolate()->GetCurrentContext(); - Server* serverObject = unwrapObject(args.This()); - - // Create level from user input - if (args[0]->IsInt32() && args[1]->IsString()) - { - short fillTile = args[0]->Uint32Value(context).ToChecked(); - std::string levelName = *v8::String::Utf8Value(isolate, args[1]->ToString(context).ToLocalChecked()); - - auto levelObject = Level::createLevel(fillTile, levelName); - if (levelObject != nullptr) - { - V8ScriptObject* v8_wrapped = static_cast*>(levelObject->getScriptObject()); - args.GetReturnValue().Set(v8_wrapped->handle(isolate)); - } - } -} - -void Server_Function_FindNPC(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - v8::Local context = args.GetIsolate()->GetCurrentContext(); - Server* serverObject = unwrapObject(args.This()); - - // Find npc object from user input - NPCPtr npcObject; - if (args[0]->IsString()) - { - v8::String::Utf8Value npcName(isolate, args[0]->ToString(context).ToLocalChecked()); - npcObject = serverObject->getNPCByName(*npcName); - } - else if (args[0]->IsInt32()) - { - unsigned int npcId = args[0]->Uint32Value(context).ToChecked(); - npcObject = serverObject->getNPC(npcId); - } - - // Set the return value as the handle from the wrapped object - if (npcObject != nullptr) - { - V8ScriptObject* v8_wrapped = static_cast*>(npcObject->getScriptObject()); - args.GetReturnValue().Set(v8_wrapped->handle(isolate)); - } -} - -void Server_Function_FindPlayer(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - // TODO(joey): second parameter could indicticate if it should skip rcs? - - v8::Local context = args.GetIsolate()->GetCurrentContext(); - Server* serverObject = unwrapObject(args.This()); - - // Find player object from user input - PlayerPtr playerObject; - if (args[0]->IsString()) - { - v8::String::Utf8Value accountName(isolate, args[0]->ToString(context).ToLocalChecked()); - playerObject = serverObject->getPlayer(*accountName, PLTYPE_ANYCLIENT); - } - else if (args[0]->IsInt32()) - { - unsigned int playerId = args[0]->Uint32Value(context).ToChecked(); - playerObject = serverObject->getPlayer(playerId, PLTYPE_ANYPLAYER); - } - - // Set the return value as the handle from the wrapped object - if (playerObject != nullptr) - { - V8ScriptObject* v8_wrapped = static_cast*>(playerObject->getScriptObject()); - args.GetReturnValue().Set(v8_wrapped->handle(isolate)); - } -} - - -// Server Method: server.loadstring(str filename); -void Server_Function_LoadString(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 1); - - v8::Local context = args.GetIsolate()->GetCurrentContext(); - Server* serverObject = unwrapObject(args.This()); - - PlayerPtr npcServer = serverObject->getNPCServer(); - if (npcServer && args[0]->IsString()) - { - v8::String::Utf8Value filePath(isolate, args[0]->ToString(context).ToLocalChecked()); - const auto& folderRights = npcServer->getFolderRights(); - - if (folderRights.hasPermission(*filePath, FilePermissions::Read)) - { - CString fileData; - if (fileData.load(serverObject->getServerPath(*filePath))) - { - auto result = v8::String::NewFromUtf8(isolate, fileData.text(), v8::NewStringType::kNormal, fileData.length()); - args.GetReturnValue().Set(result.ToLocalChecked()); - } - } - } -} - -// Server Method: server.savestring(str filename, str filedata, bool append = false); -void Server_Function_SaveString(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_MINARGCOUNT(args, isolate, 2); - - v8::Local context = args.GetIsolate()->GetCurrentContext(); - Server* serverObject = unwrapObject(args.This()); - - PlayerPtr npcServer = serverObject->getNPCServer(); - if (npcServer && args[0]->IsString() && args[1]->IsString()) - { - v8::String::Utf8Value filePath(isolate, args[0]->ToString(context).ToLocalChecked()); - v8::String::Utf8Value fileData(isolate, args[1]->ToString(context).ToLocalChecked()); - const auto& folderRights = npcServer->getFolderRights(); - - if (folderRights.hasPermission(*filePath, FilePermissions::Read)) - { - auto path = serverObject->getServerPath(*filePath); - - CString data; - if (args.Length() > 2 && args[2]->BooleanValue(isolate)) - data.load(path); - - data.write(*fileData, fileData.length(), false); - args.GetReturnValue().Set(data.save(path)); - } - } -} - -// Server Method: server.setshootparams(str shootparams); -void Server_Function_SetShootParams(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - // Throw an exception on constructor calls for method functions - V8ENV_THROW_CONSTRUCTOR(args, isolate); - - // Throw an exception if we don't receive the minimum 8 arguments - V8ENV_THROW_MINARGCOUNT(args, isolate, 1); - - v8::Local context = isolate->GetCurrentContext(); - - if (args[0]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Server, server); - - if (server == nullptr) return; - - CString shootParams; - for (int i = 0; i < args.Length(); i++) - { - shootParams << (std::string)*v8::String::Utf8Value(isolate, args[i]->ToString(context).ToLocalChecked()) << "\n"; - } - shootParams.gtokenizeI(); - - server->setShootParams(shootParams.text()); - } -} - -void Server_Function_SaveLog(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_ARGCOUNT(args, isolate, 2); - - if (args[0]->IsString() && args[1]->IsString()) - { - V8ENV_SAFE_UNWRAP(args, Server, serverObject); - - v8::Local context = args.GetIsolate()->GetCurrentContext(); - v8::String::Utf8Value filename(isolate, args[0]->ToString(context).ToLocalChecked()); - v8::String::Utf8Value message(isolate, args[1]->ToString(context).ToLocalChecked()); - - serverObject->logToFile(*filename, *message); - } -} - -void Server_Function_SendToNC(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_MINARGCOUNT(args, isolate, 1); - - v8::Local context = args.GetIsolate()->GetCurrentContext(); - - std::string msg; - for (int i = 0; i < args.Length(); i++) - { - v8::String::Utf8Value str(isolate, args[i]->ToString(context).ToLocalChecked()); - msg.append(*str).append(" "); - } - - if (!msg.empty()) - { - V8ENV_SAFE_UNWRAP(args, Server, serverObject); - serverObject->sendToNC(msg); - } -} - -void Server_Function_SendToRC(const v8::FunctionCallbackInfo& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - - V8ENV_THROW_CONSTRUCTOR(args, isolate); - V8ENV_THROW_MINARGCOUNT(args, isolate, 1); - - v8::Local context = args.GetIsolate()->GetCurrentContext(); - - std::string msg; - for (int i = 0; i < args.Length(); i++) - { - v8::String::Utf8Value str(isolate, args[i]->ToString(context).ToLocalChecked()); - msg.append(" ").append(*str); - } - - if (!msg.empty()) - { - V8ENV_SAFE_UNWRAP(args, Server, serverObject); - serverObject->sendToRC(CString("[Server]:") << msg); - } -} - -// PROPERTY: server.timevar -void Server_Get_TimeVar(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - V8ENV_SAFE_UNWRAP(info, Server, serverObject); - - unsigned int timevar = serverObject->getNWTime(); - info.GetReturnValue().Set(timevar); -} - -// PROPERTY: server.timevar2 -void Server_Get_TimeVar2(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - auto timevar = (unsigned int)time(0); - info.GetReturnValue().Set(timevar); -} - -// PROPERTY: Server Flags -void Server_GetObject_Flags(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - - v8::Local internalProperty = v8::String::NewFromUtf8Literal(isolate, "_internalFlags", v8::NewStringType::kInternalized); - if (self->HasRealNamedProperty(context, internalProperty).ToChecked()) - { - info.GetReturnValue().Set(self->Get(context, internalProperty).ToLocalChecked()); - return; - } - - V8ENV_SAFE_UNWRAP(info, Server, serverObject); - - // Grab external data - v8::Local data = info.Data().As(); - auto* scriptEngine = static_cast(data->Value()); - auto* env = dynamic_cast(scriptEngine->getScriptEnv()); - - // Find constructor - v8::Local ctor_tpl = env->getConstructor("server.flags"); - assert(!ctor_tpl.IsEmpty()); - - // Create new instance - v8::Local new_instance = ctor_tpl->InstanceTemplate()->NewInstance(context).ToLocalChecked(); - new_instance->SetAlignedPointerInInternalField(0, serverObject); - - v8::PropertyAttribute propAttr = static_cast(v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::DontEnum); - self->DefineOwnProperty(context, internalProperty, new_instance, propAttr).FromJust(); - info.GetReturnValue().Set(new_instance); -} - -void Server_Flags_Getter(v8::Local property, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local self = info.This(); - Server* serverObject = unwrapObject(self); - - // Get property name - v8::Local name = v8::Local::Cast(property); - v8::String::Utf8Value utf8(isolate, name); - - // Get server flag with the property - CString flagValue = serverObject->getFlag(*utf8); - v8::Local strText = v8::String::NewFromUtf8(isolate, flagValue.text()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void Server_Flags_Setter(v8::Local property, v8::Local value, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local self = info.This(); - Server* serverObject = unwrapObject(self); - - // Get property name - v8::Local name = v8::Local::Cast(property); - v8::String::Utf8Value utf8(isolate, name); - - // Get new value - v8::String::Utf8Value newValue(isolate, value); - serverObject->setFlag(*utf8, *newValue, true); -} - -void Server_Flags_Enumerator(const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - Server* serverObject = unwrapObject(self); - - // Get flags list - auto& flagList = serverObject->getServerFlags(); - - v8::Local result = v8::Array::New(isolate, (int)flagList.size()); - - int idx = 0; - for (const auto& [flag, value]: flagList) - result->Set(context, idx++, v8::String::NewFromUtf8(isolate, flag.c_str()).ToLocalChecked()).Check(); - - info.GetReturnValue().Set(result); -} - -// PROPERTY: server.npcs -void Server_GetArray_Npcs(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - Server* serverObject = unwrapObject(self); - - // Get npcs list - auto& npcList = serverObject->getNPCList(); - - v8::Local result = v8::Array::New(isolate, (int)npcList.size()); - - int idx = 0; - for (auto it = npcList.begin(); it != npcList.end(); ++it) - { - V8ScriptObject* v8_wrapped = static_cast*>(it->second->getScriptObject()); - result->Set(context, idx++, v8_wrapped->handle(isolate)).Check(); - } - - info.GetReturnValue().Set(result); -} - -// PROPERTY: server.players -void Server_GetArray_Players(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - Server* serverObject = unwrapObject(self); - - // Get npcs list - auto& playerList = serverObject->getPlayerList(); - - v8::Local result = v8::Array::New(isolate); - - int idx = 0; - for (auto it = playerList.begin(); it != playerList.end(); ++it) - { - Player* pl = it->second.get(); - if (pl->isHiddenClient()) - continue; - - V8ScriptObject* v8_wrapped = static_cast*>(pl->getScriptObject()); - result->Set(context, idx++, v8_wrapped->handle(isolate)).Check(); - } - - info.GetReturnValue().Set(result); -} - -// PROPERTY: server.serverlist -void Server_GetArray_Serverlist(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - Server* serverObject = unwrapObject(self); - - // Get npcs list - auto& listserver = serverObject->getServerList(); - auto& serverList = listserver.getServerList(); - - v8::Local result = v8::Object::New(isolate); - - for (auto it = serverList.begin(); it != serverList.end(); ++it) - { - v8::Local key_servername = v8::String::NewFromUtf8(isolate, it->first.c_str(), v8::NewStringType::kNormal).ToLocalChecked(); - result->Set(context, key_servername, v8::Number::New(isolate, it->second)).Check(); - } - - info.GetReturnValue().Set(result); -} - -void bindClass_Server(ScriptEngine* scriptEngine) -{ - // Retrieve v8 environment - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - v8::Isolate* isolate = env->isolate(); - - // External pointer - v8::Local engine_ref = v8::External::New(isolate, scriptEngine); - - // Create V8 string for "server" - v8::Local serverStr = v8::String::NewFromUtf8Literal(isolate, "server", v8::NewStringType::kInternalized); - - // Create constructor for class - v8::Local server_ctor = v8::FunctionTemplate::New(isolate); - v8::Local server_proto = server_ctor->PrototypeTemplate(); - - server_ctor->SetClassName(serverStr); - server_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - // Method functions - server_proto->Set(v8::String::NewFromUtf8Literal(isolate, "httpget"), v8::FunctionTemplate::New(isolate, Server_Function_HttpGet, engine_ref)); - server_proto->Set(v8::String::NewFromUtf8Literal(isolate, "httppost"), v8::FunctionTemplate::New(isolate, Server_Function_HttpPost, engine_ref)); - server_proto->Set(v8::String::NewFromUtf8Literal(isolate, "findlevel"), v8::FunctionTemplate::New(isolate, Server_Function_FindLevel, engine_ref)); - server_proto->Set(v8::String::NewFromUtf8Literal(isolate, "createlevel"), v8::FunctionTemplate::New(isolate, Server_Function_CreateLevel, engine_ref)); - server_proto->Set(v8::String::NewFromUtf8Literal(isolate, "findnpc"), v8::FunctionTemplate::New(isolate, Server_Function_FindNPC, engine_ref)); - server_proto->Set(v8::String::NewFromUtf8Literal(isolate, "findplayer"), v8::FunctionTemplate::New(isolate, Server_Function_FindPlayer, engine_ref)); - server_proto->Set(v8::String::NewFromUtf8Literal(isolate, "loadstring"), v8::FunctionTemplate::New(isolate, Server_Function_LoadString, engine_ref)); - server_proto->Set(v8::String::NewFromUtf8Literal(isolate, "savestring"), v8::FunctionTemplate::New(isolate, Server_Function_SaveString, engine_ref)); - server_proto->Set(v8::String::NewFromUtf8Literal(isolate, "setshootparams"), v8::FunctionTemplate::New(isolate, Server_Function_SetShootParams, engine_ref)); - server_proto->Set(v8::String::NewFromUtf8Literal(isolate, "savelog"), v8::FunctionTemplate::New(isolate, Server_Function_SaveLog, engine_ref)); - server_proto->Set(v8::String::NewFromUtf8Literal(isolate, "sendtonc"), v8::FunctionTemplate::New(isolate, Server_Function_SendToNC, engine_ref)); - server_proto->Set(v8::String::NewFromUtf8Literal(isolate, "sendtorc"), v8::FunctionTemplate::New(isolate, Server_Function_SendToRC, engine_ref)); - - // Properties - server_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "flags"), Server_GetObject_Flags, nullptr, engine_ref); - server_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "npcs"), Server_GetArray_Npcs); - server_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "players"), Server_GetArray_Players); - server_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "serverlist"), Server_GetArray_Serverlist); - server_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "timevar"), Server_Get_TimeVar); - server_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "timevar2"), Server_Get_TimeVar2); - - // Create the server flags template - v8::Local server_flags_ctor = v8::FunctionTemplate::New(isolate); - server_flags_ctor->SetClassName(v8::String::NewFromUtf8Literal(isolate, "flags")); - server_flags_ctor->InstanceTemplate()->SetInternalFieldCount(1); - server_flags_ctor->InstanceTemplate()->SetHandler(v8::NamedPropertyHandlerConfiguration( - Server_Flags_Getter, Server_Flags_Setter, nullptr, nullptr, Server_Flags_Enumerator, v8::Local(), - v8::PropertyHandlerFlags::kOnlyInterceptStrings)); - env->setConstructor("server.flags", server_flags_ctor); - - // Persist the constructor - env->setConstructor("server", server_ctor); -} - -#endif diff --git a/server/src/scripting/v8/V8WeaponImpl.cpp b/server/src/scripting/v8/V8WeaponImpl.cpp deleted file mode 100644 index 17c7cd46e..000000000 --- a/server/src/scripting/v8/V8WeaponImpl.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#ifdef V8NPCSERVER - - #include - #include - #include - #include - - #include "Server.h" - #include "Weapon.h" - #include "scripting/ScriptEngine.h" - #include "scripting/v8/V8ScriptFunction.h" - #include "scripting/v8/V8ScriptObject.h" - -// PROPERTY: Name -void Weapon_GetStr_Name(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Local self = info.This(); - Weapon* weaponObject = unwrapObject(self); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), weaponObject->getName().c_str()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -// PROPERTY: Image -void Weapon_GetStr_Image(v8::Local prop, const v8::PropertyCallbackInfo& info) -{ - v8::Local self = info.This(); - Weapon* weaponObject = unwrapObject(self); - - v8::Local strText = v8::String::NewFromUtf8(info.GetIsolate(), weaponObject->getImage().c_str()).ToLocalChecked(); - info.GetReturnValue().Set(strText); -} - -void bindClass_Weapon(ScriptEngine* scriptEngine) -{ - // Retrieve v8 environment - V8ScriptEnv* env = static_cast(scriptEngine->getScriptEnv()); - v8::Isolate* isolate = env->isolate(); - - // External pointer - // v8::Local engine_ref = v8::External::New(isolate, scriptEngine); - - // Create V8 string for "weapon" - v8::Local weaponStr = v8::String::NewFromUtf8Literal(isolate, "weapon", v8::NewStringType::kInternalized); - - // Create constructor for class - v8::Local weapon_ctor = v8::FunctionTemplate::New(isolate); - v8::Local weapon_proto = weapon_ctor->PrototypeTemplate(); - weapon_ctor->SetClassName(weaponStr); - weapon_ctor->InstanceTemplate()->SetInternalFieldCount(1); - - // Method functions - //weapon_proto->Set(v8::String::NewFromUtf8(isolate, "setCallBack"), v8::FunctionTemplate::New(isolate, Weapon_SetCallBack, engine_ref)); - - // Properties - weapon_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "name"), Weapon_GetStr_Name); - weapon_proto->SetAccessor(v8::String::NewFromUtf8Literal(isolate, "image"), Weapon_GetStr_Image); - - // Persist the constructor - env->setConstructor(ScriptConstructorId::result, weapon_ctor); -} - -#endif diff --git a/server/src/utilities/Events.cpp b/server/src/utilities/Events.cpp new file mode 100644 index 000000000..05ce03329 --- /dev/null +++ b/server/src/utilities/Events.cpp @@ -0,0 +1,71 @@ +#include + +#include + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +//////////////////////////////////////////////////////////////////////////////// + +EventHandleBase::~EventHandleBase() +{ + if (m_dispatcher) + m_dispatcher->unsubscribe(this); +} + +EventHandleBase::EventHandleBase(EventDispatcherBase* dispatcher, size_t id) + : m_dispatcher(dispatcher), m_eventId(id) +{ +}; + +EventDispatcherBase::EventDispatcherBase() +{ +} + +EventDispatcherBase::~EventDispatcherBase() +{ + unsubscribeAll(); +} + +bool EventDispatcherBase::unsubscribe(EventHandleBase* handle) +{ + if (!handle) + return false; + + // Check if the event handle event exists + auto itr = m_eventHandlers.find(handle->m_eventId); + if (itr == m_eventHandlers.end()) + return false; + + // Unregister the event handler. + // Remove the parent dispatcher just in case + // it was manually unsubscribed instead of deleted. + // Otherwise you might get a double delete. + handle->m_dispatcher = nullptr; + m_eventHandlers.erase(itr); + + return true; +}; + +bool EventDispatcherBase::unsubscribe(std::shared_ptr handle) +{ + if (handle) + return unsubscribe(handle.get()); + else + return false; +}; + +void EventDispatcherBase::unsubscribeAll() +{ + for (auto& handler : m_eventHandlers) + { + auto ptr = handler.second.lock(); + if (ptr) + ptr->m_dispatcher = nullptr; + } + + m_eventHandlers.clear(); +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/utilities/FilePermissions.cpp b/server/src/utilities/FilePermissions.cpp index eb3055c52..4a0a40c46 100644 --- a/server/src/utilities/FilePermissions.cpp +++ b/server/src/utilities/FilePermissions.cpp @@ -1,11 +1,19 @@ -#include "utilities/FilePermissions.h" -#include +#include #include +#include +#include + +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// const char FOLDER_SEPARATOR = '/'; const std::regex WILDCARD_REGEX(R"(\*)"); -std::vector splitInput(const std::string& input, const char delimiter = FOLDER_SEPARATOR) +static std::vector splitInput(const std::string& input, const char delimiter = FOLDER_SEPARATOR) { std::istringstream stream(input); std::string line; @@ -92,3 +100,6 @@ bool FilePermissions::match(const std::string& path, const FilePermissions::Perm return true; } + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/utilities/Log.cpp b/server/src/utilities/Log.cpp new file mode 100644 index 000000000..9e4f48bed --- /dev/null +++ b/server/src/utilities/Log.cpp @@ -0,0 +1,92 @@ +#include +#include +#include +#include +#include + +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal::log +{ +/////////////////////////////////////////////////////////////////////////////// + +Indent::Indent(Log* log, uint8_t level) + : m_log(log) +{ + assert(m_log != nullptr); + m_old_level = m_log->indentLevel; + m_log->indentLevel += level; +} + +Indent::Indent(IndentAbsolute_t is_absolute, Log* log, uint8_t level) + : m_log(log) +{ + assert(m_log != nullptr); + m_old_level = m_log->indentLevel; + m_log->indentLevel = level; +} + +Indent::Indent(Indent&& other) noexcept + : m_log(std::move(other.m_log)), m_old_level(std::move(other.m_old_level)) +{ + other.m_log = nullptr; + other.m_old_level = 0; +} + +Indent::~Indent() noexcept +{ + if (m_log != nullptr) + m_log->indentLevel = m_old_level; +} + +/////////////////////////////////////////////////////////////////////////////// + +Log& Log::reload() +{ + std::lock_guard lock(mutex); + if (file && file->is_open()) + { + file->flush(); + file->close(); + } + + file = std::make_unique(); + file->open(filename, std::ios::binary | std::ios::out | std::ios::app); + return *this; +} + +Log& Log::close() +{ + std::lock_guard lock(mutex); + if (file && file->is_open()) + { + file->flush(); + file->close(); + file.reset(); + } + return *this; +} + +Log& Log::clear() +{ + std::lock_guard lock(mutex); + if (file && file->is_open()) + { + file->close(); + } + + file = std::make_unique(); + file->open(filename, std::ios::binary | std::ios::out | std::ios::trunc); + return *this; +} + +std::ofstream* Log::getFile() +{ + if (!file || !file->is_open()) + reload(); + return file.get(); +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::log diff --git a/server/src/utilities/PropertySerializers.cpp b/server/src/utilities/PropertySerializers.cpp new file mode 100644 index 000000000..2c809cd71 --- /dev/null +++ b/server/src/utilities/PropertySerializers.cpp @@ -0,0 +1,818 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal::props +{ +//////////////////////////////////////////////////////////////////////////////// + +int getServerGeneration() +{ + auto server = BabyDI::Get(); + return static_cast(server->Generation); +} + +//////////////////////////////////////////////////////////////////////////////// + +// ----------------------------------------------- +// PropertyString + +CString PropertyString::serialize() const +{ + return CString() >> (char)value.length() << value; +} + +void PropertyString::deserialize(CString& data) +{ + value = data.readChars(data.readGUChar()); +} + +void PropertyString::apply(const GameValue& gameValue) +{ + value = gameValue.get().value_or(""); +} + +std::format_context::iterator PropertyString::format(std::format_context& ctx) const +{ + return std::format_to(ctx.out(), "value: {}", value.empty() ? "(empty)" : value); +} + +// ----------------------------------------------- +// PropertyLongString + +CString PropertyLongString::serialize() const +{ + return CString() >> (short)value.length() << value; +} + +void PropertyLongString::deserialize(CString& data) +{ + value = data.readChars(data.readGUShort()); +} + +// ----------------------------------------------- +// PropertySwordPower + +CString PropertySwordPower::serialize() const +{ + auto powerVal = power.value_or(0); + if (powerVal == 0) + return CString() >> (char)0; + if (powerVal > 0 && powerVal <= 4 && image.empty()) + return CString() >> (char)powerVal; + return CString() >> (char)(powerVal + 30) >> (char)image.length() << image; +} + +void PropertySwordPower::deserialize(CString& data) +{ + uint8_t powerVal = 0; + data.readGInto(powerVal); + if (powerVal < 30) + { + powerVal = Limits::applySwordPower(powerVal); + + // For older clients, we use a default image name. + if (powerVal > 0 && powerVal <= 4) + { + auto server = BabyDI::Get(); + image = std::format("sword{}.{}", powerVal, (server->Generation != ServerGeneration::ORIGINAL ? "png" : "gif")); + } + else image.clear(); + + power = powerVal; + return; + } + + // If the power is 30 or more, its sword power + custom image. + powerVal = Limits::applySwordPower(powerVal - 30); + power = powerVal; + + // Read the image name. + // If there is no extension, assume its a .gif. + image = data.readChars(data.readGUChar()); + if (!image.contains(".")) + image += ".gif"; +} + +void PropertySwordPower::apply(const GameValue& gameValue) +{ + image = gameValue.get().value_or(""); +} + +std::format_context::iterator PropertySwordPower::format(std::format_context& ctx) const +{ + if (power.value_or(0) > 0 && power.value_or(0) <= 4 && image.empty()) + return std::format_to(ctx.out(), "image: (preset {0}) sword{0}.{1}, power: {2}", power.value_or(0), (getServerGeneration() == 0 ? "gif" : "png"), power.value_or(0)); + if (power.has_value() && power.value() == 0) + return std::format_to(ctx.out(), "image: (empty), power: 0"); + return std::format_to(ctx.out(), "image: {}, power: {}", (image.empty() ? "(empty)" : image), power.value_or(0)); +} + +// ----------------------------------------------- +// PropertyShieldPower + +CString PropertyShieldPower::serialize() const +{ + auto powerVal = power.value_or(0); + if (powerVal == 0) + return CString() >> (char)0; + if (powerVal > 0 && powerVal <= 3 && image.empty()) + return CString() >> (char)powerVal; + return CString() >> (char)(powerVal + 10) >> (char)image.length() << image; +} + +void PropertyShieldPower::deserialize(CString& data) +{ + auto server = BabyDI::Get(); + + uint8_t powerVal = 0; + data.readGInto(powerVal); + if (powerVal < 10) + { + powerVal = Limits::applyShieldPower(powerVal); + + // For older clients, we use a default image name. + if (powerVal > 0 && powerVal <= 4) + image = std::format("shield{}.{}", powerVal, (server->Generation != ServerGeneration::ORIGINAL ? "png" : "gif")); + else image.clear(); + + power = powerVal; + return; + } + + // If the power is 10 or more, its shield power + custom image. + powerVal = Limits::applyShieldPower(powerVal - 10); + power = powerVal; + + // This fixes an odd bug with the 1.41 client. + if (data.bytesLeft() == 0) return; + + // Read the image name. + image = data.readChars(data.readGUChar()); + + // If there is no extension, assume its a .gif, for 1.x servers. + if (server->Generation == ServerGeneration::ORIGINAL) + { + if (!image.contains(".")) + image += ".gif"; + } +} + +void PropertyShieldPower::apply(const GameValue& gameValue) +{ + image = gameValue.get().value_or(""); +} + +std::format_context::iterator PropertyShieldPower::format(std::format_context& ctx) const +{ + if (power.value_or(0) > 0 && power.value_or(0) <= 3 && image.empty()) + return std::format_to(ctx.out(), "image: (preset {0}) shield{0}.{1}, power: {2}", power.value_or(0), (getServerGeneration() == 0 ? "gif" : "png"), power.value_or(0)); + if (power.has_value() && power.value() == 0) + return std::format_to(ctx.out(), "image: (empty), power: 0"); + return std::format_to(ctx.out(), "image: {}, power: {}", (image.empty() ? "(empty)" : image), power.value_or(0)); +} + +// ----------------------------------------------- +// PropertyGaniOrBowGif + +CString PropertyGaniOrBowGif::serialize() const +{ + if (gani.has_value()) + return CString() >> (char)gani->length() << *gani; + else if (bowGif.has_value()) + { + auto& [image, preset] = *bowGif; + if (image.empty() && preset < 10) + return CString() >> (char)preset; + + return CString() >> (char)(10 + image.length()) << image; + } + return CString(); +} + +void PropertyGaniOrBowGif::deserialize(CString& data) +{ + // Gani for later clients. + if (getServerGeneration() != 0) + { + gani = data.readChars(data.readGUChar()); + } + // Graal 1.411 and earlier clients used BOWGIF instead of GANI. + else + { + uint8_t preset = data.readGUChar(); + if (preset < 10) + { + // If the preset is less than 10, its a bow preset. + bowGif = std::make_pair(std::string(), preset); + } + else + { + // Otherwise, its a custom bow image. + auto image = data.readChars(preset - 10); + if (!image.isEmpty() && !image.contains(".")) + image += ".gif"; + bowGif = std::make_pair(std::move(image), 0); + } + } +} + +void PropertyGaniOrBowGif::apply(const GameValue& gameValue) +{ + gani = gameValue.get().value_or(""); +} + +std::format_context::iterator PropertyGaniOrBowGif::format(std::format_context& ctx) const +{ + if (gani.has_value()) + return std::format_to(ctx.out(), "gani: {}", (gani.value().empty() ? "(empty)" : gani.value())); + else if (bowGif.has_value()) + return std::format_to(ctx.out(), "bowGif: {}, bowPower: {}", (bowGif.value().first.empty() ? "(empty)" : bowGif.value().first), bowGif.value().second); + else + return std::format_to(ctx.out(), "(empty)"); +} + +// ----------------------------------------------- +// PropertyHeadGif + +CString PropertyHeadGif::serialize() const +{ + if (std::holds_alternative(image)) + { + auto preset = std::min(static_cast(99), std::get(image)); + return CString() >> (char)preset; + } + + auto& headImage = std::get(image); + return CString() >> (char)(100 + headImage.length()) << headImage; +} + +void PropertyHeadGif::deserialize(CString& data) +{ + auto length = data.readGUChar(); + if (length < 100) + { + image = length; + return; + } + + auto headImage = data.readChars(length - 100); + + if (getServerGeneration() == 0) + { + if (!headImage.contains(".")) + headImage += ".gif"; + } + + image = std::move(headImage.toString()); +} + +void PropertyHeadGif::apply(const GameValue& gameValue) +{ + image = gameValue.get().value_or(""); +} + +std::format_context::iterator PropertyHeadGif::format(std::format_context& ctx) const +{ + if (std::holds_alternative(image)) + { + uint8_t preset = std::get(image); + return std::format_to(ctx.out(), "head: (preset {0}) head{0}.{1}", preset, (getServerGeneration() == 0 ? "gif" : "png")); + } + else + { + auto& head = std::get(image); + return std::format_to(ctx.out(), "head: {}", (head.empty() ? "(empty)" : head)); + } +} + +// ----------------------------------------------- +// PropertyEloRating + +CString PropertyEloRating::serialize() const +{ + auto packed = ((static_cast(rating) & 0xFFF) << 9) | (static_cast(deviation) & 0x1FF); + return CString().writeGInt(packed); +} + +void PropertyEloRating::deserialize(CString& data) +{ + uint32_t packed = data.readGInt(); + rating = ((packed >> 9) & 0xFFF); + deviation = (packed & 0x1FF); +} + +void PropertyEloRating::apply(const GameValue& gameValue) +{ + auto array = gameValue.get>(); + if (!array.has_value() || array.value().size() != 2) + { + rating = 0; + deviation = 0; + return; + } + + auto& values = array.value(); + rating = static_cast(values[0]); + deviation = static_cast(values[1]); +} + +std::format_context::iterator PropertyEloRating::format(std::format_context& ctx) const +{ + return std::format_to(ctx.out(), "rating: {:.2f}, deviation: {:.2f}", rating, deviation); +} + +// ----------------------------------------------- +// PropertyAttachNPC + +CString PropertyAttachNPC::serialize() const +{ + return CString() >> (char)type >> (int)npcId; +} + +void PropertyAttachNPC::deserialize(CString& data) +{ + type = data.readGUChar(); + npcId = data.readGInt(); +} + +void PropertyAttachNPC::apply(const GameValue& gameValue) +{ + npcId = static_cast(gameValue.get().value_or(0)); +} + +std::format_context::iterator PropertyAttachNPC::format(std::format_context& ctx) const +{ + return std::format_to(ctx.out(), "type: {}, NPCID: {}", type, static_cast(npcId)); +} + +// ----------------------------------------------- +// PropertyPixelCoordinate + +CString PropertyPixelCoordinate::serialize() const +{ + uint16_t val = (uint16_t)std::abs(pixelCoordinate) << 1; + if (pixelCoordinate < 0) + val |= 0x0001; + return CString() >> (short)val; +} + +void PropertyPixelCoordinate::deserialize(CString& data) +{ + auto len = data.readGUShort(); + pixelCoordinate = (len >> 1); + + // If the first bit is 1, our pixelCoordinate is negative. + if ((uint16_t)len & 0x0001) + pixelCoordinate = -pixelCoordinate; +} + +void PropertyPixelCoordinate::apply(const GameValue& gameValue) +{ + pixelCoordinate = static_cast(gameValue.get().value_or(0) * 16); +} + +std::format_context::iterator PropertyPixelCoordinate::format(std::format_context& ctx) const +{ + return std::format_to(ctx.out(), "pixel: {} (tile: {:.2f})", pixelCoordinate, (pixelCoordinate / 16.0f)); +} + +// ----------------------------------------------- +// PropertyTileCoordinate + +CString PropertyTileCoordinate::serialize() const +{ + CString result; + + // Writing 223 will break the packet flow (as it will overflow to the newline char), so avoid doing that. + // 223 will be -11 and 224 will be -10.5. + uint8_t halftile = static_cast(pixelCoordinate / 8); + if (halftile == 223) + halftile = 224; + + result.writeGCharUnsafe(halftile); + return result; +} + +void PropertyTileCoordinate::deserialize(CString& data) +{ + int16_t halftile = 0; + uint8_t read = data.readGChar(); + if (read >= 216) + halftile = static_cast(read); + else halftile = read; + + pixelCoordinate = static_cast(halftile * 8); +} + +void PropertyTileCoordinate::apply(const GameValue& gameValue) +{ + pixelCoordinate = static_cast(gameValue.get().value_or(0) * 16); +} + +std::format_context::iterator PropertyTileCoordinate::format(std::format_context& ctx) const +{ + return std::format_to(ctx.out(), "tile: {:.2f} (pixel: {})", (pixelCoordinate / 16.0f), pixelCoordinate); +} + +// ----------------------------------------------- +// PropertyTileCoordinateZ + +CString PropertyTileCoordinateZ::serialize() const +{ + return CString() >> (char)(std::min(170, std::max(-50, (pixelCoordinate / 16))) + 50); +} + +void PropertyTileCoordinateZ::deserialize(CString& data) +{ + pixelCoordinate = (data.readGUChar() - 50) * 16; +} + +void PropertyTileCoordinateZ::apply(const GameValue& gameValue) +{ + pixelCoordinate = static_cast(gameValue.get().value_or(0) * 16); +} + +std::format_context::iterator PropertyTileCoordinateZ::format(std::format_context& ctx) const +{ + return std::format_to(ctx.out(), "tile: {:.2f} (pixel: {})", (pixelCoordinate / 16.0f), pixelCoordinate); +} + +// ----------------------------------------------- +// PropertyGS1Script + +CString PropertyGS1Script::serialize() const +{ + auto server = BabyDI::Get(); + + // Modern sends scripts in a different way. + if (server->Generation == ServerGeneration::MODERN) + return CString() >> (short)0; + + return CString() >> (short)(script.length() > 0x705F ? 0x705F : script.length()) << script.substr(0, 0x705F); +} + +void PropertyGS1Script::deserialize(CString& data) +{ + auto length = data.readGUShort(); + script = data.readChars(length); +} + +void PropertyGS1Script::apply(const GameValue& gameValue) +{ + script = gameValue.get().value_or(""); +} + +std::format_context::iterator PropertyGS1Script::format(std::format_context& ctx) const +{ + return std::format_to(ctx.out(), "script size: {}", script.size()); +} + +// ----------------------------------------------- +// PropertyHurtDxDy + +PropertyHurtDxDy::PropertyHurtDxDy(float dx, float dy) +{ + hurtDX = static_cast(std::clamp(dx, -1.0f, 1.0f) * 32); + hurtDY = static_cast(std::clamp(dy, -1.0f, 1.0f) * 32); +} + +PropertyHurtDxDy::PropertyHurtDxDy(int8_t dx, int8_t dy) +{ + hurtDX = std::clamp(dx, static_cast(-32), 32_i8); + hurtDY = std::clamp(dy, static_cast(-32), 32_i8); +} + +PropertyHurtDxDy::PropertyHurtDxDy(const Position& displacement) +{ + hurtDX = static_cast((std::clamp(displacement.x(), -9.0f, 9.0f) / 9.0f) * 32); + hurtDY = static_cast((std::clamp(displacement.y(), -9.0f, 9.0f) / 9.0f) * 32); +} + +PropertyHurtDxDy::PropertyHurtDxDy(const Position& displacement) +{ + hurtDX = (std::clamp(displacement.x(), static_cast(-144), 144_i16) * 32) / 144_i16; + hurtDY = (std::clamp(displacement.y(), static_cast(-144), 144_i16) * 32) / 144_i16; +} + +CString PropertyHurtDxDy::serialize() const +{ + auto clampedDX = std::clamp(hurtDX, static_cast(-32), 32_i8); + auto clampedDY = std::clamp(hurtDY, static_cast(-32), 32_i8); + + // The range is from 0 - 64 with 32 being the center. + // So a value of 32 is 0, a value of 0 is -32, and a value of 64 is +32. + // 32 represents 9 tiles of displacement. + + return CString() >> (char)(clampedDX + 32) >> (char)(clampedDY + 32); +} + +void PropertyHurtDxDy::deserialize(CString& data) +{ + int8_t dx = data.readGChar(); + int8_t dy = data.readGChar(); + + // Recenter the values around 0. + hurtDX = static_cast(dx - 32); + hurtDY = static_cast(dy - 32); +} + +void PropertyHurtDxDy::apply(const GameValue& gameValue) +{ + auto array = gameValue.get>(); + if (!array.has_value() || array.value().size() != 2) + { + hurtDX = 0; + hurtDY = 0; + return; + } + + auto& values = array.value(); + float dx = std::clamp(static_cast(values[0]), -1.0f, 1.0f); + float dy = std::clamp(static_cast(values[1]), -1.0f, 1.0f); + hurtDX = static_cast(dx * 32); + hurtDY = static_cast(dy * 32); +} + +std::format_context::iterator PropertyHurtDxDy::format(std::format_context& ctx) const +{ + auto [dx, dy] = getAsTiles(); + return std::format_to(ctx.out(), "dx: {:.2f}, dy: {:.2f}", dx, dy); +} + +std::pair PropertyHurtDxDy::getAsTiles() const +{ + std::pair result; + result.first = std::clamp(hurtDX / 32.0f, -1.0f, 1.0f); + result.second = std::clamp(hurtDY / 32.0f, -1.0f, 1.0f); + return result; +} + +// ----------------------------------------------- +// PropertyImagePart + +CString PropertyImagePart::serialize() const +{ + return CString() >> (short)imagePart.position.x() >> (short)imagePart.position.y() >> (char)imagePart.size.width() >> (char)imagePart.size.height(); +} + +void PropertyImagePart::deserialize(CString& data) +{ + uint16_t x = 0, y = 0; + uint8_t width = 0, height = 0; + + x = data.readGUShort(); + y = data.readGUShort(); + width = data.readGUChar(); + height = data.readGUChar(); + + imagePart.position = {std::clamp(x, 0_ui16, 16000_ui16), std::clamp(y, 0_ui16, 16000_ui16)}; + imagePart.size = {std::clamp(width, 0_ui8, 220_ui8), std::clamp(height, 0_ui8, 220_ui8)}; +} + +void PropertyImagePart::apply(const GameValue& gameValue) +{ + auto array = gameValue.get>(); + if (!array.has_value() || array.value().size() < 4) + return; + + auto& values = array.value(); + imagePart.position = {static_cast(values[0]), static_cast(values[1])}; + imagePart.size = {static_cast(values[2]), static_cast(values[3])}; +} + +std::format_context::iterator PropertyImagePart::format(std::format_context& ctx) const +{ + return std::format_to(ctx.out(), "pos: ({}, {}), size: ({}, {})", imagePart.position.x(), imagePart.position.y(), imagePart.size.width(), imagePart.size.height()); +} + +// ----------------------------------------------- +// PropertySprite + +PropertySprite::PropertySprite(uint8_t sprite) +{ + this->sprite = sprite >> 2; + this->direction = sprite & 0b0000'0011; +} + +CString PropertySprite::serialize() const +{ + return CString() >> (char)((sprite << 2) | direction); +} + +void PropertySprite::deserialize(CString& data) +{ + uint8_t spriteDir = 0; + data.readGInto(spriteDir); + + // The first 6 bits are the sprite, the last 2 bits are the direction. + sprite = spriteDir >> 2; + direction = spriteDir & 0b0000'0011; +} + +void PropertySprite::apply(const GameValue& gameValue) +{ + auto value = static_cast(gameValue.get().value_or(0.0)); + sprite = value >> 2; + direction = value & 0b0000'0011; +} + +std::format_context::iterator PropertySprite::format(std::format_context& ctx) const +{ + return std::format_to(ctx.out(), "sprite: {}, direction: {}", sprite, direction); +} + +// ----------------------------------------------- +// PropertyColors + +CString PropertyColors::serialize() const +{ + size_t count = getColorCount(); + size_t maxValue = getMaxColorValue(); + CString result; + for (size_t i = 0; i < count; ++i) + { + result >> std::clamp((ValueType)values[i], static_cast(0), static_cast(maxValue)); + } + return result; +} + +void PropertyColors::deserialize(CString& data) +{ + size_t count = getColorCount(); + size_t maxValue = getMaxColorValue(); + for (size_t i = 0; i < count; ++i) + { + if (static_cast(data.bytesLeft()) < sizeof(ValueType)) + throw std::runtime_error("Not enough data to deserialize PropertyArray."); + data.readGInto(values[i]); + values[i] = std::clamp(values[i], static_cast(0), static_cast(maxValue)); + } +} + +void PropertyColors::apply(const GameValue& gameValue) +{ + if (gameValue.get>().has_value()) + { + auto* vec = gameValue.get_unsafe>(); + if (vec == nullptr) + return; + + // Convert all values to type T and insert into the values array. + size_t count = getColorCount(); + size_t maxValue = getMaxColorValue(); + for (size_t i = 0; i < count && i < vec->size(); ++i) + { + values[i] = std::clamp(static_cast((*vec)[i]), static_cast(0), static_cast(maxValue)); + } + } +} + +std::format_context::iterator PropertyColors::format(std::format_context& ctx) const +{ + std::ostringstream out; + size_t count = getColorCount(); + + for (size_t i = 0; i < count; ++i) + { + out << std::format("{}", values[i]); + if (i < count - 1) + out << ", "; + } + + return std::format_to(ctx.out(), "values: [{}]", out.str()); +} + +int PropertyColors::getColorCount() const noexcept +{ + auto server = BabyDI::Get(); + return server->isNewWorldMode() ? 8 : 5; +} + +size_t PropertyColors::getMaxColorValue() const noexcept +{ + auto server = BabyDI::Get(); + size_t colorCount = CLASSICCOLORS_COUNT; + if (server->Generation == ServerGeneration::MODERN && server->cached.enableExBodyColors.getValue()) + colorCount += HTMLCOLORS_COUNT; + return colorCount; +} + +//////////////////////////////////////////////////////////////////////////////// + +uint8_t Limits::applyMaxHitpoints(uint8_t maxHitpoints) +{ + auto server = BabyDI::Get(); + auto heartLimit = std::min(server->cached.maxHeartLimit.getValue(), 20_ui8); + return std::clamp(maxHitpoints, 0_ui8, heartLimit); +} + +int8_t Limits::applySwordPower(int8_t swordPower) +{ + auto server = BabyDI::Get(); + int8_t minimum = (server->cached.enableHealingSwords.getValue() ? -(server->cached.swordPowerLimit.getValue()) : 0); + int8_t maximum = server->cached.swordPowerLimit.getValue(); + return std::clamp(swordPower, minimum, maximum); +} + +uint8_t Limits::applyShieldPower(uint8_t shieldPower) +{ + auto server = BabyDI::Get(); + return std::clamp(shieldPower, 0_ui8, server->cached.shieldPowerLimit.getValue()); +} + +//////////////////////////////////////////////////////////////////////////////// + +void collectPacketsFromResults(const PropertySendResults& results, CString& outAll, CString& outLevel, CString& outSource, PropertyContainerGetter getProp) +{ + // The map allows us to to sort the results by increasing ID order. If the client receives a prop it doesn't understand, it stops processing them. + // This ensures that all the props the client CAN read come before the ones it can't. + // Using a map also allows us to avoid duplicates, as the key is the prop ID. + static std::map>, std::less<>> sendOrder; + + // Add all the results to the send order. + sendOrder.clear(); + for (const auto& [result, prop] : results) + { + if (result.resultFlags.test(SetResults::wasInvalid)) + continue; + + sendOrder[result.propId] = std::make_tuple(result, prop); + for (const auto& additionalPropId : result.resultPropIds) + sendOrder[additionalPropId] = std::make_tuple(result, nullptr); + } + + // Loop through all the sorted results and add them to the buffers. + for (auto& [propId, resultTuple] : sendOrder) + { + auto& setResults = std::get<0>(resultTuple); + std::shared_ptr base = std::get<1>(resultTuple); + if (base == nullptr && !getProp) + continue; + + // We want to support sending different props for different destinations. + // As long as we have destinations to send to, we will keep sending the prop. By default, it will send to every destination. + // If we have SetResults::getLatestOnSend set, then the callback will return which destinations the prop is for. + // We can then mark those destinations off and keep looping. + + auto& destinationFlags = setResults.resultFlags; + SetResults::ResultFlagType sendFlags{setResults.resultFlags}; + int loopCount = 0; + while (destinationFlags.test(SetResults::sendToAll) || destinationFlags.test(SetResults::sendToLevel) || destinationFlags.test(SetResults::sendToSource)) + { + // If the base prop is null, or if we need to get the latest value on send, execute the callback to get the latest prop value. + if (base == nullptr || setResults.resultFlags.test(SetResults::getLatestOnSend)) + { + sendFlags = destinationFlags; + base = getProp(propId, sendFlags); + } + + // Send to each destination and mark that it got sent. + if (sendFlags.test(SetResults::sendToAll)) + { + outAll >> (char)propId << base->serialize(); + destinationFlags.reset(SetResults::sendToAll); + } + if (sendFlags.test(SetResults::sendToLevel)) + { + outLevel >> (char)propId << base->serialize(); + destinationFlags.reset(SetResults::sendToLevel); + } + if (sendFlags.test(SetResults::sendToSource)) + { + outSource >> (char)propId << base->serialize(); + destinationFlags.reset(SetResults::sendToSource); + } + + // Sanity check to prevent infinite loops. If we loop more times than there are possible destinations, something went wrong. + if (++loopCount > setResults.resultFlags.size()) + break; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal::props diff --git a/server/src/utilities/Settings.cpp b/server/src/utilities/Settings.cpp new file mode 100644 index 000000000..83b7268c1 --- /dev/null +++ b/server/src/utilities/Settings.cpp @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +//////////////////////////////////////////////////////////////////////////////// + +void Settings::load(const std::filesystem::path& file) +{ + auto server = BabyDI::Get(); + if (server == nullptr) + throw std::runtime_error("Failed to get server instance in Settings::load."); + + auto serveroptions = server->getFileSystemServer().openi(fs::FileCategory::CONFIG, file); + if (serveroptions == nullptr) + { + log::printLine(log::server, "[ERROR] Failed to load {}, the file may be missing or malformed.", file.string()); + return; + } + + string_ordered_multimap oldSettings = std::move(m_settings); + m_settings.clear(); + + // Read all of the new settings. + for (const auto& curLine : serveroptions->readAllLines()) + { + std::string_view line = string::trim(curLine); + if (line.empty() || line.starts_with('#')) + continue; + + auto sep = line.find('='); + if (sep == std::string_view::npos) + continue; + + auto comment = line.find('#'); + auto key = string::trim(line.substr(0, sep)); + auto value = string::trim(line.substr(sep + 1, comment - sep)); + + m_settings.emplace(key, value); + } + + string_set settingWasChanged; + + // Walk through the settings and assemble a list of all changed settings. + for (const auto& [key, value] : m_settings) + { + // Check if this value is contained in oldSettings. + auto [begin, end] = oldSettings.equal_range(key); + auto oldValues = std::ranges::subrange(begin, end) | std::views::values; + if (!std::ranges::contains(oldValues, value)) + settingWasChanged.insert(key); + } + + // Find any deleted settings by walking through the old settings and checking if they are in the new settings. + for (const auto& [key, value] : oldSettings) + { + if (m_settings.find(key) == m_settings.end()) + settingWasChanged.insert(key); + } + + // Post update events for any changed settings. + for (const auto& key : settingWasChanged) + { + if (auto eventIt = m_settingUpdateEvents.find(key); eventIt != m_settingUpdateEvents.end()) + { + auto& event = eventIt->second; + event.post(); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/utilities/StringUtils.cpp b/server/src/utilities/StringUtils.cpp index ba0f27b45..a2ac76679 100644 --- a/server/src/utilities/StringUtils.cpp +++ b/server/src/utilities/StringUtils.cpp @@ -1,34 +1,43 @@ -#include +#include +#include +#include -#include "utilities/StringUtils.h" +#include +#include + +//////////////////////////////////////////////////////////////////////////////// namespace utilities { - std::string retokenizeArray(const std::vector& triggerData, int start_idx) - { - std::string ret; - for (auto i = start_idx; i < triggerData.size(); i++) - { - if (!ret.empty()) - ret.append(","); +//////////////////////////////////////////////////////////////////////////////// - ret.append(triggerData[i].gtokenize().toString()); - } +std::string retokenizeArray(const std::vector& triggerData, int start_idx) +{ + std::string ret; + for (size_t i = start_idx; i < triggerData.size(); i++) + { + if (!ret.empty()) + ret.append(","); - return ret; + ret.append(triggerData[i].gtokenize().toString()); } - CString retokenizeCStringArray(const std::vector& triggerData, int start_idx) - { - CString ret; - for (auto i = start_idx; i < triggerData.size(); i++) - { - if (!ret.isEmpty()) - ret << ","; + return ret; +} - ret << triggerData[i].gtokenize(); - } +CString retokenizeCStringArray(const std::vector& triggerData, int start_idx) +{ + CString ret; + for (size_t i = start_idx; i < triggerData.size(); i++) + { + if (!ret.isEmpty()) + ret << ","; - return ret; + ret << triggerData[i].gtokenize(); } -} // namespace utilities + + return ret; +} + +//////////////////////////////////////////////////////////////////////////////// +} // end namespace utilities diff --git a/server/src/utilities/manager/GuildManager.cpp b/server/src/utilities/manager/GuildManager.cpp new file mode 100644 index 000000000..f196ff237 --- /dev/null +++ b/server/src/utilities/manager/GuildManager.cpp @@ -0,0 +1,301 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +GuildManager::~GuildManager() +{ + saveGuilds(); +} + +//---------------------------- + +void GuildManager::loadGuilds(const std::filesystem::path& directory) +{ + auto indent = log::server.indent(); + m_filesystem.categoryEventCallback[ENUM(fs::FileCategory::FILE)] = [this](fs::FileEventCollection events, fs::FileData& fileData) + { + if (events.test(fs::FileEvent::Deleted)) + { + auto guildName = string::replace(fs::getANSIFileName(fileData.file.stem()).substr(5), "_"sv, " "sv); + m_guilds.erase(guildName); + log::printLine(log::server, "Guild '{}' removed from filesystem.", guildName); + + auto server = BabyDI::Get(); + for (const auto& [playerId, player] : players_of_type(server->getPlayerList())) + { + // If the player was in the guild, set their nickname prop so it gets stripped. + if (player->getGuild() == guildName) + { + player->setNick(player->account.character.nickName); + player->sendPropsFromResults(player->setPropWith(props::SetBy::SERVER, player->account.character.nickName)); + } + } + } + if (events.test(fs::FileEvent::Added)) + { + if (auto guild = loadGuild(fileData.file); guild != nullptr) + log::printLine(log::server, "Guild '{}' loaded from filesystem.", guild->name); + } + if (events.test(fs::FileEvent::Modified)) + { + if (auto guild = loadGuild(fileData.file); guild != nullptr) + log::printLine(log::server, "Guild '{}' modified in filesystem.", guild->name); + } + }; + + m_filesystem.addFoldersConfigEntry(fs::FileCategory::FILE, directory / "guild*.txt"); + m_filesystem.bind(directory); + m_filesystem.waitUntilFilesSearched(); + + for (auto info : m_filesystem.info(fs::FileCategory::FILE) | toSharedPtr) + { + if (info == nullptr) continue; + if (auto guild = loadGuild(info->file); guild != nullptr) + log::printLine(log::server, guild->name); + } +} + +void GuildManager::saveGuilds() +{ + for (auto& [guildName, guild] : m_guilds) + saveGuild(guildName); +} + +//---------------------------- + +Guild* GuildManager::loadGuild(const std::filesystem::path& filePath) +{ + std::ifstream file{ filePath, std::ios::in }; + if (!file.is_open()) + { + log::printLine(log::server, "** [Error] Could not open guild file: {}", filePath.generic_string()); + return nullptr; + } + + auto guildName = string::replace(fs::getANSIFileName(filePath.stem()).substr(5), "_"sv, " "sv); + Guild guild{ .name = guildName, .filePath = filePath }; + + std::string line; + while (std::getline(file, line)) + { + std::string_view lineView{ line }; + lineView = string::trim(lineView); + if (lineView.empty()) + continue; + + if (auto pos = lineView.find(':'); pos == std::string_view::npos) + { + guild.members.emplace(lineView, std::string{}); + } + else + { + auto account = string::trimRight(lineView.substr(0, pos)); + auto nickName = string::trimLeft(lineView.substr(pos + 1)); + if (nickName.starts_with('*')) + nickName.remove_prefix(1); + + guild.members.emplace(account, nickName); + } + } + m_guilds[guildName] = std::move(guild); + file.close(); + + return &m_guilds.at(guildName); +} + +//---------------------------- + +bool GuildManager::guildExists(std::string_view guildName) const +{ + return m_guilds.find(guildName) != m_guilds.end(); +} + +bool GuildManager::verifyPlayerInGuild(std::string_view guildName, std::string_view account, std::string_view nickName) const +{ + auto it = m_guilds.find(guildName); + if (it != m_guilds.end()) + { + const auto& members = it->second.members; + auto memberIt = members.find(account); + while (memberIt != members.end()) + { + if (memberIt->second.empty() || memberIt->second == nickName) + return true; + ++memberIt; + } + } + return false; +} + +std::optional GuildManager::getPlayerNicknamesForGuild(std::string_view guildName, std::string_view account) const +{ + auto it = m_guilds.find(guildName); + if (it != m_guilds.end()) + { + const auto& members = it->second.members; + return members.equal_range(account); + } + return std::nullopt; +} + +//---------------------------- + +bool GuildManager::createGuild(std::string_view guildName) +{ + if (guildExists(guildName)) + { + log::printLine(log::server, "** [Error] Cannot create guild that already exists: {}", guildName); + return false; + } + + auto directories = m_filesystem.getManagedDirectories(); + auto directory = directories.begin(); + if (directory == directories.end()) + { + log::printLine(log::server, "** [Error] No guilds directory found."); + return false; + } + + Guild guild; + guild.name = std::string{ guildName }; + guild.filePath = *directory / std::format("guild{}.txt", string::replace(guildName, " "sv, "_"sv)); + + std::ofstream file{ guild.filePath, std::ios::out | std::ios::trunc }; + if (!file.is_open()) + { + log::printLine(log::server, "** [Error] Could not create guild file: {}", guild.filePath.generic_string()); + return false; + } + + m_guilds[guild.name] = std::move(guild); + file.close(); + + return true; +} + +bool GuildManager::deleteGuild(std::string_view guildName) +{ + if (auto it = m_guilds.find(guildName); it != m_guilds.end()) + { + std::filesystem::remove(it->second.filePath); + //m_guilds.erase(it); + return true; + } + return false; +} + +bool GuildManager::saveGuild(std::string_view guildName) +{ + if (auto it = m_guilds.find(guildName); it != m_guilds.end()) + { + Guild& guild = it->second; + if (!guild.modifiedSinceLastSave) + return true; + + std::ofstream file{ guild.filePath, std::ios::out | std::ios::trunc }; + if (!file.is_open()) + { + log::printLine(log::server, "** [Error] Could not save guild: {}", guildName); + return false; + } + + for (const auto& [account, nickName] : it->second.members) + { + file << account; + if (!nickName.empty()) + file << ':' << nickName; + file << std::endl; + } + + file.close(); + guild.modifiedSinceLastSave = false; + + // Update the file mod time so we don't get a file modified event. + if (auto fileInfo = m_filesystem.info(fs::FileCategory::FILE, guild.filePath.filename()); fileInfo != nullptr) + fileInfo->refreshModTime(); + + return true; + } + return false; +} + +bool GuildManager::addPlayerToGuild(std::string_view guildName, std::string_view account, std::string_view nickName) +{ + auto it = m_guilds.find(guildName); + if (it != m_guilds.end()) + { + it->second.members.emplace(account, nickName); + it->second.modifiedSinceLastSave = true; + return true; + } + + // Create the guild. + if (createGuild(guildName)) + { + m_guilds.at(std::string{ guildName }).members.emplace(account, nickName); + saveGuild(guildName); + return true; + } + + return false; +} + +bool GuildManager::removePlayerFromGuild(std::string_view guildName, std::string_view account, std::string_view nickName) +{ + auto it = m_guilds.find(guildName); + if (it != m_guilds.end()) + { + auto& members = it->second.members; + auto membersIt = members.equal_range(account); + while (membersIt.first != membersIt.second) + { + if (membersIt.first->second == nickName) + { + members.erase(membersIt.first); + it->second.modifiedSinceLastSave = true; + return true; + } + ++membersIt.first; + } + } + return false; +} + +bool GuildManager::removePlayerEntirelyFromGuild(std::string_view guildName, std::string_view account) +{ + auto it = m_guilds.find(guildName); + if (it != m_guilds.end()) + { + auto& members = it->second.members; + auto result = members.erase(std::string{ account }); + it->second.modifiedSinceLastSave = true; + return result > 0; + } + return false; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/utilities/manager/TranslationManagerClassic.cpp b/server/src/utilities/manager/TranslationManagerClassic.cpp new file mode 100644 index 000000000..4e46ce965 --- /dev/null +++ b/server/src/utilities/manager/TranslationManagerClassic.cpp @@ -0,0 +1,277 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +// https://r12a.github.io/app-conversion/ +constexpr std::array supportedLanguages = +{ + "Deutsch"sv, "English"sv, "Espa\u00F1ol"sv, "Fran\u00E7ais"sv, "Italiano"sv, + "Nederlands"sv, "Norsk"sv, "Portugu\u00EAs"sv, "Svenska"sv, +}; + +constexpr std::string_view filePrefix = "slanguage"sv; +constexpr std::string_view originalLanguage = "Original"sv; + +//---------------------------- + +void TranslationManagerClassic::loadTranslations(const std::filesystem::path& directory) +{ + auto indent = log::server.indent(); + + std::filesystem::directory_iterator dirSearch{ directory, std::filesystem::directory_options::follow_directory_symlink | std::filesystem::directory_options::skip_permission_denied }; + for (const auto& entry : dirSearch) + { + if (!entry.is_regular_file()) + continue; + + // slanguageDomain.txt + auto fileName = fs::getANSIFileName(entry.path()); + if (!fileName.starts_with(filePrefix) || !fileName.ends_with(".txt")) + continue; + + loadDomain(entry.path()); + } + + // Always include the "Original" domain. + if (!m_domains.contains(originalLanguage)) + m_domains.emplace(originalLanguage, TranslationMap{ .filename = directory / "slanguageOriginal.txt" }); +} + +void TranslationManagerClassic::reloadTranslation(const std::filesystem::path& filePath) +{ + // slanguageDomain.txt + auto file = fs::getANSIFileName(filePath); + if (!file.starts_with(filePrefix) || !file.ends_with(".txt")) + return; + + loadDomain(filePath); +} + +void TranslationManagerClassic::loadDomain(const std::filesystem::path& filePath) +{ + auto domain = fs::getANSIFileName(filePath.stem()).substr(filePrefix.length()); + if (domain.empty()) + return; + + TranslationMap translations{ .filename = filePath }; + + // Translation file format: + // md5: "text" + + auto lines = CString::loadToken(filePath.string(), "\n", true); + for (auto& line : lines) + { + auto str = string::trim(line.toStringView()); + if (str.empty()) continue; + + auto md5 = str.substr(0, 32); + if (md5.length() != 32) continue; + + auto separator = str.find(':'); + if (separator == std::string_view::npos) continue; + + auto start = str.find('"', separator); + if (start == std::string_view::npos) continue; + ++start; + + auto end = str.find('"', start); + if (end == std::string_view::npos) continue; + + auto value = string::unescapeQuotes(str.substr(start, end - start)); + translations.lines.emplace(md5, std::move(value)); + } + + m_domains.emplace(std::move(domain), std::move(translations)); +} + +void TranslationManagerClassic::saveTranslations() +{ + for (const auto& [domain, map] : m_domains) + { + if (map.filename.empty()) + continue; + + std::ofstream file{ map.filename }; + if (!file.is_open()) + continue; + + for (const auto& [key, value] : map.lines) + file << key << ": \"" << string::escapeQuotes(value) << "\"\n"; + } +} + +std::tuple TranslationManagerClassic::syncLanguageWithOriginal(std::string_view language) +{ + std::tuple result{ ""sv, 0, 0}; + constexpr size_t addIndex = 1; + constexpr size_t removeIndex = 2; + + // Don't sync original with original. + if (string::equalsi(language, originalLanguage)) + return result; + + // Find the original domain. + auto original = m_domains.find(originalLanguage); + if (original == m_domains.end()) + return result; + + // Get our calculated language domain. + auto calculatedLanguage = language::mapToClassic(language); + auto domain = m_domains.find(calculatedLanguage); + if (domain == m_domains.end()) + { + if (std::ranges::find(supportedLanguages, calculatedLanguage) == std::ranges::end(supportedLanguages)) + return result; + + std::string languageFile{ filePrefix }; + languageFile.append(calculatedLanguage).append(".txt"); + domain = m_domains.emplace(calculatedLanguage, TranslationMap{ .filename = original->second.filename.parent_path() / languageFile }).first; + } + + // Can't find a language, return failure. + if (domain == m_domains.end()) + return result; + + // Record the calculated language. + std::get<0>(result) = calculatedLanguage; + + // File for unused translations. + std::filesystem::path unusedFileName = domain->second.filename; + unusedFileName.replace_extension(".unused"); + { + std::ofstream unusedFile; + + // Check for any translations that aren't in the original file anymore. + for (auto iter = domain->second.lines.begin(); iter != domain->second.lines.end();) + { + const auto& [key, value] = *iter; + if (!original->second.lines.contains(key)) + { + if (!unusedFile.is_open()) + unusedFile.open(unusedFileName, std::ios::out | std::ios::app); + if (unusedFile.is_open()) + { + unusedFile << key << ": \"" << string::escapeQuotes(value) << "\"\n"; + iter = domain->second.lines.erase(iter); + ++std::get(result); + continue; + } + } + ++iter; + } + } + + // Add any translations from the original that aren't in this language file. + for (const auto& [key, value] : original->second.lines) + { + if (!domain->second.lines.contains(key)) + { + domain->second.lines.emplace(key, value); + ++std::get(result); + } + } + + // Save the translations. + std::ofstream file{ domain->second.filename }; + if (file.is_open()) + { + for (const auto& [key, value] : domain->second.lines) + file << key << ": \"" << string::escapeQuotes(value) << "\"\n"; + } + + return result; +} + +std::generator> TranslationManagerClassic::syncAllLanguagesWithOriginal() +{ + for (const auto& [domain, map] : m_domains) + { + if (domain == originalLanguage) + continue; + + co_yield syncLanguageWithOriginal(domain); + } +} + +size_t TranslationManagerClassic::generateAllLanguageStubs() +{ + size_t count = 0; + for (const auto& language : supportedLanguages) + { + auto result = syncLanguageWithOriginal(language); + if (std::get<1>(result) != 0) + ++count; + } + return count; +} + +std::string_view TranslationManagerClassic::getText(std::string_view language, std::string_view key) +{ + auto hash = generateHash(key); + + auto findTranslation = [this](std::string_view language, std::string_view key) -> std::string* + { + auto domain = m_domains.find(language); + if (domain == m_domains.end()) + return nullptr; + if (auto line = domain->second.lines.find(key); line != domain->second.lines.end()) + return &line->second; + return nullptr; + }; + + // Search the target language, then "Original". + if (auto line = findTranslation(language, hash); line != nullptr) + return *line; + if (auto line = findTranslation(originalLanguage, hash); line != nullptr) + return *line; + + // Not found, add to "Original" and return the key. + if (auto domain = m_domains.find(originalLanguage); domain != m_domains.end()) + domain->second.lines.emplace(hash, key); + + return key; +} + +std::string TranslationManagerClassic::generateHash(std::string_view key) const +{ + hash_state md5; + uint8_t output[16]{}; + + md5_init(&md5); + md5_process(&md5, reinterpret_cast(key.data()), key.length()); + md5_done(&md5, output); + + // Convert output to a hex string. + std::string hexString; + for (const auto& byte : output) + hexString += std::format("{:02x}", byte); + + return hexString; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/server/src/utilities/manager/TranslationManagerModern.cpp b/server/src/utilities/manager/TranslationManagerModern.cpp new file mode 100644 index 000000000..b9e0abc43 --- /dev/null +++ b/server/src/utilities/manager/TranslationManagerModern.cpp @@ -0,0 +1,46 @@ +#include +#include +#include + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +namespace preagonal +{ +/////////////////////////////////////////////////////////////////////////////// + +void TranslationManagerModern::loadTranslations(const std::filesystem::path& directory) +{ +} + +void TranslationManagerModern::reloadTranslation(const std::filesystem::path& filePath) +{ +} + +void TranslationManagerModern::saveTranslations() +{ +} + +std::tuple TranslationManagerModern::syncLanguageWithOriginal(std::string_view language) +{ + return { "not implemented"sv, 0, 0}; +} + +std::generator> TranslationManagerModern::syncAllLanguagesWithOriginal() +{ + co_return; +} + +size_t TranslationManagerModern::generateAllLanguageStubs() +{ + return 0; +} + +std::string_view TranslationManagerModern::getText(std::string_view language, std::string_view key) +{ + return key; +} + +/////////////////////////////////////////////////////////////////////////////// +} // end namespace preagonal diff --git a/vcpkg b/vcpkg deleted file mode 160000 index 6f1ddd6b6..000000000 --- a/vcpkg +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6f1ddd6b6878e7e66fcc35c65ba1d8feec2e01f8 diff --git a/vcpkg.json b/vcpkg.json index 30e69c59f..777d02740 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,8 +1,23 @@ { - "features": { - "npcserver": { - "description": "Dependencies for the npc-server", - "dependencies": [ "openssl" ] - } - } + "name": "gserver", + "version": "4.0.0", + "builtin-baseline": "5a874b05fb981e2643fd0c1c9ad63e19dfe76b27", + "dependencies": [ + { + "name": "antlr4", + "version>=": "4.13.2#1" + }, + "bzip2", + { + "name": "catch2", + "version>=": "3.4.0" + }, + "miniupnpc", + "pkgconf", + { + "name": "wolfssl", + "version>=": "5.8.0" + }, + "zlib" + ] }