diff --git a/.github/workflows/build-and-push-docker.yml b/.github/workflows/build-and-push-docker.yml index e4bffabe..b7ae1d06 100644 --- a/.github/workflows/build-and-push-docker.yml +++ b/.github/workflows/build-and-push-docker.yml @@ -1,14 +1,14 @@ -name: CI +name: Docker on: push: branches: - - "main" + - main tags: - "v*.*.*" pull_request: branches: - - "main" + - main env: REGISTRY: ghcr.io @@ -20,15 +20,21 @@ jobs: permissions: contents: read packages: write + attestations: write + id-token: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: recursive + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - name: Log in to the Container registry - uses: docker/login-action@v3 + if: github.event_name != 'pull_request' + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -36,21 +42,32 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - # generate Docker tags based on the following events/attributes tags: | type=ref,event=pr type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=canary,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - name: Build and push Docker image - uses: docker/build-push-action@v5 + id: push + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Sign Docker image + if: github.event_name != 'pull_request' + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c3eae601..3963fdbb 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -3,6 +3,8 @@ name: CI on: push: branches: [ main ] + tags: + - "v*.*.*" pull_request: branches: [ main ] @@ -10,38 +12,51 @@ jobs: build-and-test: name: Build & Test (${{ matrix.os }}) runs-on: ${{ matrix.os }} - continue-on-error: true + continue-on-error: ${{ github.event_name == 'pull_request' }} strategy: matrix: - os: [ windows-2022, ubuntu-22.04, macos-13 ] + include: + - os: windows-2025 + artifact: windows + debug_preset: windows-msvc-relwithdebinfo + - os: ubuntu-24.04 + artifact: linux + debug_preset: linux-gnu-relwithdebinfo + - os: macos-15-intel + artifact: macos + debug_preset: macos-relwithdebinfo steps: - - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true - name: Add msbuild to PATH (Windows only) - if: ${{ matrix.os == 'windows-2022' }} - uses: microsoft/setup-msbuild@767f00a3f09872d96a0cb9fcd5e6a4ff33311330 + if: ${{ matrix.os == 'windows-2025' }} + uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3 with: - vs-version: '[17,18)' + vs-version: '[18,19)' msbuild-architecture: x64 - - name: Install libssl and switch to XCode 15.2 (Mac Only) - if: ${{ matrix.os == 'macos-13' }} - run: | - brew install openssl@3 - sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer - - name: Get CMake 3.x - uses: lukka/get-cmake@28983e0d3955dba2bb0a6810caae0c6cf268ec0c + - name: Get CMake + uses: lukka/get-cmake@591817e96fcad43505fb4eae36172462abb3a42e # v4.3.3 with: - cmakeVersion: "~3.25.0" # <--= optional, use most recent 3.25.x version + cmakeVersion: "latest" - name: cmake - uses: lukka/run-cmake@67c73a83a46f86c4e0b96b741ac37ff495478c38 + uses: lukka/run-cmake@5d55ea7949e25f69f0ecb516d8d572297e03a956 # v10.9 with: - workflowPreset: "ci-${{matrix.os}}" + workflowPreset: "${{ matrix.debug_preset }}" + + - name: Extract Linux debug symbols + if: matrix.os == 'ubuntu-24.04' + run: | + find build -type f -name '*Server' | while read bin; do + objcopy --only-keep-debug "$bin" "${bin}.debug" + objcopy --strip-debug --add-gnu-debuglink="${bin}.debug" "$bin" + done + - name: artifacts - uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: build-${{matrix.os}} + name: build-${{matrix.artifact}} path: | build/*/*Server* build/*/*.ini @@ -52,5 +67,30 @@ jobs: build/*/navmeshes/ build/*/migrations/ build/*/*.dcf - !build/*/*.pdb !build/*/d*/ + !build/*/*.dSYM/ + !build/**/*.debug + + - name: debug symbols (Windows) + if: matrix.os == 'windows-2025' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: debug-${{matrix.artifact}} + path: | + build/*/*.pdb + build/*/d*/ + retention-days: 30 + - name: debug symbols (Linux) + if: matrix.os == 'ubuntu-24.04' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: debug-${{matrix.artifact}} + path: build/**/*.debug + retention-days: 30 + - name: debug symbols (macOS) + if: matrix.os == 'macos-15-intel' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: debug-${{matrix.artifact}} + path: build/**/*.dSYM/ + retention-days: 30 diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml new file mode 100644 index 00000000..2a3a470b --- /dev/null +++ b/.github/workflows/canary.yml @@ -0,0 +1,93 @@ +name: Canary + +on: + workflow_run: + workflows: ["CI"] + branches: [main] + types: [completed] + +permissions: + contents: write + actions: read + +jobs: + canary-release: + name: Publish Canary Release + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Get last release tag + id: last_tag + run: | + tag=$(git describe --tags --abbrev=0 --match "v*.*.*" 2>/dev/null || echo "none") + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Generate changelog since last release tag + uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0 + id: cliff + with: + config: cliff.toml + args: --unreleased --strip header + env: + OUTPUT: CHANGES.md + GITHUB_REPO: ${{ github.repository }} + + - name: Prepend header to changelog + run: | + last="${{ steps.last_tag.outputs.tag }}" + sha="${{ github.event.workflow_run.head_sha }}" + short="${sha:0:7}" + if [ "$last" != "none" ]; then + header="Changes since **$last** ([full diff](https://github.com/${{ github.repository }}/compare/${last}...${sha}))\n\n" + else + header="Changes up to \`${short}\`\n\n" + fi + printf "%b" "$header" | cat - CHANGES.md > CHANGES.tmp && mv CHANGES.tmp CHANGES.md + + - name: Download artifacts from CI run + uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21 + with: + run_id: ${{ github.event.workflow_run.id }} + path: artifacts/ + + - name: Package artifacts + run: | + declare -A platform_map=( + ["build-windows"]="darkflame-universe-windows" + ["build-linux"]="darkflame-universe-linux" + ["build-macos"]="darkflame-universe-macos" + ) + cd artifacts + for dir in build-*/; do + name="${dir%/}" + out="${platform_map[$name]:-$name}" + zip -r "../${out}.zip" "$dir" + done + cd .. + ls -lh *.zip + + - name: Delete existing canary release + run: gh release delete canary --yes --cleanup-tag 2>/dev/null || true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create canary pre-release + uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 + with: + tag: canary + name: "Canary ${{ steps.last_tag.outputs.tag }}+${{ github.event.workflow_run.head_sha }}" + bodyFile: CHANGES.md + artifacts: "*.zip" + artifactContentType: application/zip + token: ${{ secrets.GITHUB_TOKEN }} + prerelease: true + draft: false + allowUpdates: true + removeArtifacts: true + commit: ${{ github.event.workflow_run.head_sha }} diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 00000000..a91b0ee5 --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,38 @@ +name: PR Title Check + +on: + pull_request_target: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: write + statuses: write + +jobs: + check-title: + name: Conventional Commit Title + runs-on: ubuntu-latest + + steps: + - name: Check PR title follows Conventional Commits + uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + perf + refactor + docs + chore + test + ci + revert + requireScope: false + subjectPattern: ^.+$ + subjectPatternError: | + The PR title "{title}" must have a description after the type/scope prefix. + Example: "feat: add new login flow" or "fix(auth): handle null token" + wip: true + validateSingleCommit: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..b634b90b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,70 @@ +name: Release + +on: + workflow_run: + workflows: ["CI"] + types: [completed] + +permissions: + contents: write + actions: read + +jobs: + release: + name: Create Release + # Only run when CI completed successfully on a tag push (not a PR branch named like a version) + if: | + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + startsWith(github.event.workflow_run.head_branch, 'v') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Generate changelog + uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0 + id: cliff + with: + config: cliff.toml + args: --latest --strip header + env: + OUTPUT: CHANGES.md + GITHUB_REPO: ${{ github.repository }} + + - name: Download artifacts from CI run + uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21 + with: + run_id: ${{ github.event.workflow_run.id }} + path: artifacts/ + + - name: Package artifacts + run: | + declare -A platform_map=( + ["build-windows"]="darkflame-universe-windows" + ["build-linux"]="darkflame-universe-linux" + ["build-macos"]="darkflame-universe-macos" + ) + cd artifacts + for dir in build-*/; do + name="${dir%/}" + out="${platform_map[$name]:-$name}" + zip -r "../${out}.zip" "$dir" + done + cd .. + ls -lh *.zip + + - name: Create GitHub Release + uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 + with: + tag: ${{ github.event.workflow_run.head_branch }} + name: ${{ github.event.workflow_run.head_branch }} + bodyFile: CHANGES.md + artifacts: "*.zip" + artifactContentType: application/zip + token: ${{ secrets.GITHUB_TOKEN }} + prerelease: false + draft: false diff --git a/.gitignore b/.gitignore index d7af5d1f..b1a336af 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,5 @@ docker-compose.override.yml # CMake scripts !cmake/* !cmake/toolchains/* +.mcp.json +.claude/ diff --git a/CMakeLists.txt b/CMakeLists.txt index f9802f1d..63786950 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ set(CMAKE_C_STANDARD 99) set(CMAKE_CXX_STANDARD 20) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Export the compile commands for debugging set(CMAKE_POLICY_DEFAULT_CMP0063 NEW) # Set CMAKE visibility policy to NEW on project and subprojects set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) # Set C and C++ symbol visibility to hide inlined functions @@ -67,7 +68,11 @@ set(RECASTNAVIGATION_EXAMPLES OFF CACHE BOOL "" FORCE) # Disabled no-register # Disabled unknown pragmas because Linux doesn't understand Windows pragmas. if(UNIX) - add_link_options("-Wl,-rpath,$ORIGIN/") + if(APPLE) + add_link_options("-Wl,-rpath,@loader_path/") + else() + add_link_options("-Wl,-rpath,$ORIGIN/") + endif() add_compile_options("-fPIC") add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0 _GLIBCXX_USE_CXX17_ABI=0) diff --git a/CMakePresets.json b/CMakePresets.json index 3ed904e7..bc6c8dad 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -49,7 +49,7 @@ "inherits": "default", "displayName": "[Multi] Windows (MSVC)", "description": "Set architecture to 64-bit (b/c RakNet)", - "generator": "Visual Studio 17 2022", + "generator": "Visual Studio 18 2026", "binaryDir": "${sourceDir}/build/msvc", "architecture": { "value": "x64" @@ -162,6 +162,13 @@ "rhs": "Darwin" }, "binaryDir": "${sourceDir}/build/macos" + }, + { + "name": "macos-relwithdebinfo", + "inherits": ["macos", "relwithdebinfo-config"], + "displayName": "[RelWithDebInfo] MacOS", + "description": "Create a RelWithDebInfo build for MacOS", + "binaryDir": "${sourceDir}/build/macos-relwithdebinfo" } ], "buildPresets": [ @@ -255,7 +262,7 @@ { "name": "macos-relwithdebinfo", "inherits": "default", - "configurePreset": "macos", + "configurePreset": "macos-relwithdebinfo", "displayName": "[RelWithDebInfo] MacOS", "description": "This preset is used to build in release mode with debug info on MacOS", "configuration": "RelWithDebInfo" @@ -374,7 +381,7 @@ { "name": "macos-relwithdebinfo", "inherits": "default", - "configurePreset": "macos", + "configurePreset": "macos-relwithdebinfo", "displayName": "[RelWithDebInfo] MacOS", "description": "Runs all tests on a MacOS configuration", "configuration": "RelWithDebInfo" @@ -603,7 +610,7 @@ "steps": [ { "type": "configure", - "name": "macos" + "name": "macos-relwithdebinfo" }, { "type": "build", @@ -616,7 +623,7 @@ ] }, { - "name": "ci-macos-13", + "name": "ci-macos-15-intel", "displayName": "[Release] MacOS", "description": "CI workflow preset for MacOS", "steps": [ diff --git a/Dockerfile b/Dockerfile index 8c722f7e..da99f066 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,9 @@ RUN --mount=type=cache,target=/app/build,id=build-cache \ cd /app/build && \ cmake .. && \ make -j$(nproc --ignore 1) && \ - cp -r /app/build/* /tmp/persisted-build/ + cp -r /app/build/* /tmp/persisted-build/ && \ + mkdir -p /tmp/persisted-build/mariadbcpp && \ + cp /app/build/thirdparty/mariadb-connector-cpp/src/mariadb_connector_cpp-build/libmariadbcpp.so /tmp/persisted-build/mariadbcpp/ FROM debian:12 as runtime diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 00000000..18b2597e --- /dev/null +++ b/cliff.toml @@ -0,0 +1,35 @@ +[changelog] +header = "" +body = """ +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | upper_first }} +{% for commit in commits %} +- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/commit/{{ commit.id }}))\ +{% if commit.github.username %} by [@{{ commit.github.username }}](https://github.com/{{ commit.github.username }}){% endif %} +{% endfor %} +{% endfor %} +""" +footer = "" +trim = true + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^docs", group = "Documentation" }, + { message = "^chore", group = "Chores" }, + { message = "^test", group = "Testing" }, + { message = "^ci", group = "CI/CD" }, + { message = "^revert", group = "Reverts" }, +] +filter_commits = false +tag_pattern = "v[0-9].*" +skip_tags = "canary" +ignore_tags = "" +topo_order = false +sort_commits = "oldest" diff --git a/cmake/FindMariaDB.cmake b/cmake/FindMariaDB.cmake index d3f89931..edae8b31 100644 --- a/cmake/FindMariaDB.cmake +++ b/cmake/FindMariaDB.cmake @@ -10,14 +10,15 @@ if(WIN32 AND NOT MARIADB_BUILD_SOURCE) file(MAKE_DIRECTORY "${MARIADB_MSI_DIR}") file(MAKE_DIRECTORY "${MARIADB_CONNECTOR_DIR}") - # These values need to be updated whenever a new minor release replaces an old one - # Go to https://mariadb.com/downloads/connectors/ to find the up-to-date URL parts - set(MARIADB_CONNECTOR_C_VERSION "3.2.7") - set(MARIADB_CONNECTOR_C_BUCKET "2319651") - set(MARIADB_CONNECTOR_C_MD5 "f8636d733f1d093af9d4f22f3239f885") - set(MARIADB_CONNECTOR_CPP_VERSION "1.0.2") - set(MARIADB_CONNECTOR_CPP_BUCKET "2531525") - set(MARIADB_CONNECTOR_CPP_MD5 "3034bbd6ca00a0125345f9fd1a178401") + # These values track the published Windows MSI packages used by the prebuilt path. + # Keep the Connector/C++ package version aligned with the checked out submodule tag when possible. + # Go to https://mariadb.com/downloads/connectors/ to find the up-to-date URL parts. + set(MARIADB_CONNECTOR_C_VERSION "3.4.8") + set(MARIADB_CONNECTOR_C_BUCKET "4516894") + set(MARIADB_CONNECTOR_C_MD5 "50f6fc0c77b8d3bacbeac0126e179861") + set(MARIADB_CONNECTOR_CPP_VERSION "1.1.7") + set(MARIADB_CONNECTOR_CPP_BUCKET "4464908") + set(MARIADB_CONNECTOR_CPP_MD5 "08644a7ff084b5933325cadb904796e5") set(MARIADB_CONNECTOR_C_MSI "mariadb-connector-c-${MARIADB_CONNECTOR_C_VERSION}-win64.msi") set(MARIADB_CONNECTOR_CPP_MSI "mariadb-connector-cpp-${MARIADB_CONNECTOR_CPP_VERSION}-win64.msi") @@ -79,23 +80,39 @@ else() # Build from source -DWITH_EXTERNAL_ZLIB=ON -DOPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR} -DCMAKE_C_FLAGS=-w # disable zlib warnings - -DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0) + -DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0\ -Wno-inconsistent-missing-override\ -include\ cstdint) else() set(MARIADB_EXTRA_CMAKE_ARGS -DCMAKE_C_FLAGS=-w # disable zlib warnings - -DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0) + -DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0\ -include\ cstdint) endif() + set(MARIADBCPP_BUILD_DIR "${PROJECT_BINARY_DIR}/thirdparty/mariadb-connector-cpp/src/mariadb_connector_cpp-build") set(MARIADBCPP_INSTALL_DIR ${PROJECT_BINARY_DIR}/prefix) - set(MARIADBCPP_LIBRARY_DIR ${PROJECT_BINARY_DIR}/mariadbcpp) - set(MARIADBCPP_PLUGIN_DIR ${MARIADBCPP_LIBRARY_DIR}/plugin) set(MARIADBCPP_SOURCE_DIR ${PROJECT_SOURCE_DIR}/thirdparty/mariadb-connector-cpp) set(MARIADB_INCLUDE_DIR "${MARIADBCPP_SOURCE_DIR}/include") + + if(WIN32) + set(MARIADBCPP_LIBRARY_DIR ${PROJECT_BINARY_DIR}/mariadbcpp) + set(MARIADBCPP_PLUGIN_DIR ${MARIADBCPP_LIBRARY_DIR}/plugin) + set(MARIADB_INSTALL_COMMAND) + else() + set(MARIADBCPP_LIBRARY_DIR ${MARIADBCPP_BUILD_DIR}) + set(MARIADBCPP_PLUGIN_DIR ${MARIADBCPP_BUILD_DIR}/libmariadb) + set(MARIADB_INSTALL_COMMAND INSTALL_COMMAND ${CMAKE_COMMAND} -E true) + endif() + + if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0") + set(MARIADB_POLICY_VERSION_ARG -DCMAKE_POLICY_VERSION_MINIMUM=3.5) + endif() + ExternalProject_Add(mariadb_connector_cpp PREFIX "${PROJECT_BINARY_DIR}/thirdparty/mariadb-connector-cpp" SOURCE_DIR ${MARIADBCPP_SOURCE_DIR} + BINARY_DIR ${MARIADBCPP_BUILD_DIR} INSTALL_DIR ${MARIADBCPP_INSTALL_DIR} CMAKE_ARGS -Wno-dev + ${MARIADB_POLICY_VERSION_ARG} -DWITH_UNIT_TESTS=OFF -DMARIADB_LINK_DYNAMIC=OFF -DCMAKE_BUILD_RPATH_USE_ORIGIN=${CMAKE_BUILD_RPATH_USE_ORIGIN} @@ -103,6 +120,7 @@ else() # Build from source -DINSTALL_LIBDIR=${MARIADBCPP_LIBRARY_DIR} -DINSTALL_PLUGINDIR=${MARIADBCPP_PLUGIN_DIR} ${MARIADB_EXTRA_CMAKE_ARGS} + ${MARIADB_INSTALL_COMMAND} BUILD_ALWAYS true ) diff --git a/dChatServer/ChatIgnoreList.cpp b/dChatServer/ChatIgnoreList.cpp index 6433da70..661e74f9 100644 --- a/dChatServer/ChatIgnoreList.cpp +++ b/dChatServer/ChatIgnoreList.cpp @@ -3,6 +3,7 @@ #include "MessageType/Chat.h" #include "BitStreamUtils.h" #include "Game.h" +#include "dConfig.h" #include "Logger.h" #include "eObjectBits.h" @@ -72,8 +73,8 @@ void ChatIgnoreList::AddIgnore(Packet* packet) { return; } - constexpr int32_t MAX_IGNORES = 32; - if (receiver.ignoredPlayers.size() > MAX_IGNORES) { + const int32_t MAX_IGNORES = Game::config->GetValue("max_ignores", 32); + if (receiver.ignoredPlayers.size() >= MAX_IGNORES) { LOG_DEBUG("Player %llu has too many ignores", playerId); return; } diff --git a/dChatServer/ChatPacketHandler.cpp b/dChatServer/ChatPacketHandler.cpp index 844631b4..10904f4b 100644 --- a/dChatServer/ChatPacketHandler.cpp +++ b/dChatServer/ChatPacketHandler.cpp @@ -435,6 +435,11 @@ void ChatPacketHandler::HandleChatMessage(Packet* packet) { inStream.IgnoreBytes(4); inStream.Read(channel); inStream.Read(size); + if (size > MAX_MESSAGE_LENGTH) { + LOG("Received a probably spoofed chat message, ignoring msg"); + return; + } + inStream.IgnoreBytes(77); LUWString message(size); @@ -479,6 +484,11 @@ void ChatPacketHandler::HandlePrivateChatMessage(Packet* packet) { if (channel != eChatChannel::PRIVATE_CHAT) LOG("WARNING: Received Private chat with the wrong channel!"); inStream.Read(size); + if (size > MAX_MESSAGE_LENGTH) { + LOG("Received a probably spoofed chat message, ignoring msg"); + return; + } + inStream.IgnoreBytes(77); inStream.Read(LUReceiverName); diff --git a/dChatServer/ChatServer.cpp b/dChatServer/ChatServer.cpp index 34b4d6e3..13f2bde0 100644 --- a/dChatServer/ChatServer.cpp +++ b/dChatServer/ChatServer.cpp @@ -202,8 +202,11 @@ int main(int argc, char** argv) { //Delete our objects here: Database::Destroy("ChatServer"); delete Game::server; + Game::server = nullptr; delete Game::logger; + Game::logger = nullptr; delete Game::config; + Game::config = nullptr; return EXIT_SUCCESS; } diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp index 6c75fd1d..56fc8974 100644 --- a/dChatServer/PlayerContainer.cpp +++ b/dChatServer/PlayerContainer.cpp @@ -105,15 +105,7 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) { auto* team = TeamContainer::GetTeam(playerID); if (team != nullptr) { - const auto memberName = GeneralUtils::UTF8ToUTF16(player.playerName); - - for (const auto memberId : team->memberIDs) { - const auto& otherMember = GetPlayerData(memberId); - - if (!otherMember) continue; - - TeamContainer::SendTeamSetOffWorldFlag(otherMember, playerID, { 0, 0, 0 }); - } + TeamContainer::RemoveMember(team, playerID, false, false, true); } ChatWeb::SendWSPlayerUpdate(player, eActivityType::PlayerLoggedOut); @@ -176,6 +168,7 @@ LWOOBJID PlayerContainer::GetId(const std::u16string& playerName) { return toReturn; } +// TODO Make this a pointer again or do something to make it so you cant edit the LWOOBJID_EMPTY entry? it should be ignored in all cases anyways though... PlayerData& PlayerContainer::GetPlayerDataMutable(const LWOOBJID& playerID) { return m_Players.contains(playerID) ? m_Players[playerID] : m_Players[LWOOBJID_EMPTY]; } diff --git a/dChatServer/TeamContainer.cpp b/dChatServer/TeamContainer.cpp index bbdab0ac..536cc57f 100644 --- a/dChatServer/TeamContainer.cpp +++ b/dChatServer/TeamContainer.cpp @@ -477,7 +477,7 @@ TeamData* TeamContainer::CreateLocalTeam(std::vector members) { } } - newTeam->lootFlag = 1; + newTeam->lootFlag = 0; TeamStatusUpdate(newTeam); diff --git a/dCommon/AMFDeserialize.cpp b/dCommon/AMFDeserialize.cpp index 884a0784..9d46b175 100644 --- a/dCommon/AMFDeserialize.cpp +++ b/dCommon/AMFDeserialize.cpp @@ -3,6 +3,7 @@ #include #include "Amf3.h" +#include "StringifiedEnum.h" /** * AMF3 Reference document https://rtmp.veriskope.com/pdf/amf3-file-format-spec.pdf @@ -53,7 +54,7 @@ std::unique_ptr AMFDeserialize::Read(RakNet::BitStream& inStream) case eAmf::VectorObject: [[fallthrough]]; case eAmf::Dictionary: - throw marker; + throw std::invalid_argument(StringifiedEnum::ToString(marker).data()); default: throw std::invalid_argument("Invalid AMF3 marker" + std::to_string(static_cast(marker))); } @@ -88,6 +89,11 @@ const std::string AMFDeserialize::ReadString(RakNet::BitStream& inStream) { // Right shift by 1 bit to get index if reference or size of next string if value length = length >> 1; if (isReference) { + constexpr int32_t maxStringSize = 1024 * 1024; + if (length > maxStringSize) { + LOG("1MB string attempted to be allocated in AMF deserialize, possible spoof, aborting deserialize."); + throw std::invalid_argument("1MB string attempted to be allocated in AMF deserialize, possible spoof, aborting deserialize."); + } std::string value(length, 0); inStream.Read(&value[0], length); // Empty strings are never sent by reference @@ -117,6 +123,12 @@ std::unique_ptr AMFDeserialize::ReadAmfArray(RakNet::BitStream& i if (key.size() == 0) break; arrayValue->Insert(key, Read(inStream)); } + + constexpr int32_t maxArraySize = 10'000; + if (sizeOfDenseArray > maxArraySize) { + LOG("Someone sent 10,000 dense array entries, probably a bad packet."); + throw std::invalid_argument("Someone sent 10,000 dense array entries, probably a bad packet."); + } // Finally read dense portion for (uint32_t i = 0; i < sizeOfDenseArray; i++) { arrayValue->Insert(i, Read(inStream)); diff --git a/dCommon/Amf3.h b/dCommon/Amf3.h index 9a34ad59..689e7607 100644 --- a/dCommon/Amf3.h +++ b/dCommon/Amf3.h @@ -369,11 +369,39 @@ public: template AmfType& PushDebug(const std::string_view name) { + size_t i = 0; + for (; i < m_Dense.size(); i++) { + const auto& cast = dynamic_cast(m_Dense[i].get()); + if (!cast) continue; + + const auto& nameValue = cast->Get("name"); + if (!nameValue || nameValue->GetValue() != name) continue; + + // found a duplicate, return this instead + auto valueCast = dynamic_cast(cast->Get("value")); + if (valueCast) return *valueCast; + } + auto* value = PushArray(); - value->Insert("name", name.data()); + value->Insert("name", name.data()); return value->Insert("value", std::make_unique()); } + AMFArrayValue& PushDebug(const NiPoint3& point) { + PushDebug("X") = point.x; + PushDebug("Y") = point.y; + PushDebug("Z") = point.z; + return *this; + } + + AMFArrayValue& PushDebug(const NiQuaternion& rot) { + PushDebug("W") = rot.w; + PushDebug("X") = rot.x; + PushDebug("Y") = rot.y; + PushDebug("Z") = rot.z; + return *this; + } + private: /** * The associative portion. These values are key'd with strings to an AMFValue. diff --git a/dCommon/BrickByBrickFix.cpp b/dCommon/BrickByBrickFix.cpp index b5a2df9b..d299341f 100644 --- a/dCommon/BrickByBrickFix.cpp +++ b/dCommon/BrickByBrickFix.cpp @@ -52,8 +52,7 @@ uint32_t BrickByBrickFix::TruncateBrokenBrickByBrickXml() { if (actualUncompressedSize != -1) { uint32_t previousSize = completeUncompressedModel.size(); - completeUncompressedModel.append(reinterpret_cast(uncompressedChunk.get())); - completeUncompressedModel.resize(previousSize + actualUncompressedSize); + completeUncompressedModel.append(reinterpret_cast(uncompressedChunk.get()), actualUncompressedSize); } else { LOG("Failed to inflate chunk %i for model %llu. Error: %i", chunkCount, model.id, err); break; diff --git a/dCommon/CMakeLists.txt b/dCommon/CMakeLists.txt index ba67974e..0e7c7d52 100644 --- a/dCommon/CMakeLists.txt +++ b/dCommon/CMakeLists.txt @@ -49,11 +49,10 @@ if (UNIX) elseif (WIN32) include(FetchContent) - # TODO Keep an eye on the zlib repository for an update to disable testing. Don't forget to update CMakePresets FetchContent_Declare( zlib - URL https://github.com/madler/zlib/archive/refs/tags/v1.2.11.zip - URL_HASH MD5=9d6a627693163bbbf3f26403a3a0b0b1 + URL https://github.com/madler/zlib/archive/refs/tags/v1.3.2.zip + URL_HASH MD5=adbba6eef8960c3412818b2e241f46dc GIT_PROGRESS TRUE GIT_SHALLOW 1 ) @@ -62,12 +61,12 @@ elseif (WIN32) set(CMAKE_POLICY_DEFAULT_CMP0048 NEW) # Disable warning about the minimum version of cmake used for bcrypt being deprecated in the future set(CMAKE_WARN_DEPRECATED OFF CACHE BOOL "" FORCE) + # Disable zlib tests + set(ZLIB_BUILD_TESTING OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(zlib) set(ZLIB_INCLUDE_DIRS ${zlib_SOURCE_DIR} ${zlib_BINARY_DIR}) - set_target_properties(zlib PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIRS}") - add_library(ZLIB::ZLIB ALIAS zlib) else () message( FATAL_ERROR diff --git a/dCommon/GeneralUtils.cpp b/dCommon/GeneralUtils.cpp index 7cc9278b..96bf9e01 100644 --- a/dCommon/GeneralUtils.cpp +++ b/dCommon/GeneralUtils.cpp @@ -308,8 +308,9 @@ std::vector GeneralUtils::GetSqlFileNamesFromFolder(const std::stri for (const auto& t : std::filesystem::directory_iterator(folder)) { if (t.is_directory() || t.is_symlink()) continue; auto filename = t.path().filename().string(); - const auto index = std::stoi(GeneralUtils::SplitString(filename, '_').at(0)); - filenames.emplace(index, std::move(filename)); + // Ensure the file has a name in the format of xxxxxxxx_anything_goes_here.sql + const auto migrationNumber = TryParse(GeneralUtils::SplitString(filename, '_').at(0)); + if (migrationNumber.has_value()) filenames.emplace(migrationNumber.value(), std::move(filename)); } // Now sort the map by the oldest migration. diff --git a/dCommon/GeneralUtils.h b/dCommon/GeneralUtils.h index 828fcee3..30be70be 100644 --- a/dCommon/GeneralUtils.h +++ b/dCommon/GeneralUtils.h @@ -205,6 +205,12 @@ namespace GeneralUtils { return isParsed ? static_cast(result) : std::optional{}; } + // A version of TryParse that will return `errorVal` if `str` failed to parse. + template + [[nodiscard]] T TryParse(std::string_view str, const T errorVal) { + return TryParse(str).value_or(errorVal); + } + template requires(!Numeric) [[nodiscard]] std::optional TryParse(std::string_view str); @@ -237,6 +243,12 @@ namespace GeneralUtils { return std::nullopt; } + // A version of TryParse that will return `errorVal` if `str` failed to parse. + template + [[nodiscard]] T TryParse(std::string_view str, const T errorVal) { + return TryParse(str).value_or(errorVal); + } + #endif /** @@ -258,6 +270,11 @@ namespace GeneralUtils { return z ? std::make_optional(x.value(), y.value(), z.value()) : std::nullopt; } + // Alternative overload of TryParse with a default value + [[nodiscard]] inline NiPoint3 TryParse(const std::string_view strX, const std::string_view strY, const std::string_view strZ, const NiPoint3 errorVal) { + return TryParse(strX, strY, strZ).value_or(errorVal); + } + /** * The TryParse overload for handling NiPoint3 by passing a span of three strings * @param str The string vector representing the X, Y, and Z coordinates @@ -268,6 +285,11 @@ namespace GeneralUtils { return (str.size() == 3) ? TryParse(str[0], str[1], str[2]) : std::nullopt; } + // Alternative overload of TryParse with a default value + [[nodiscard]] inline NiPoint3 TryParse(const std::span str, const NiPoint3 errorVal) { + return TryParse(str).value_or(errorVal); + } + template std::u16string to_u16string(const T value) { return GeneralUtils::ASCIIToUTF16(std::to_string(value)); diff --git a/dCommon/LDFFormat.cpp b/dCommon/LDFFormat.cpp index da28ae6e..04ba643a 100644 --- a/dCommon/LDFFormat.cpp +++ b/dCommon/LDFFormat.cpp @@ -10,163 +10,151 @@ #include #include -using LDFKey = std::string_view; -using LDFTypeAndValue = std::string_view; - -using LDFType = std::string_view; -using LDFValue = std::string_view; - //! Returns a pointer to a LDFData value based on string format -LDFBaseData* LDFBaseData::DataFromString(const std::string_view& format) { +std::unique_ptr LDFBaseData::DataFromString(const std::string_view& format) { + std::unique_ptr toReturn; // A valid LDF must be at least 3 characters long (=0:) is the shortest valid LDF (empty UTF-16 key with no initial value) - if (format.empty() || format.length() <= 2) return nullptr; - auto equalsPosition = format.find('='); - // You can have an empty key, just make sure the type and value might exist - if (equalsPosition == std::string::npos || equalsPosition == (format.size() - 1)) return nullptr; + if (!format.empty() && format.length() > 2) { + auto equalsPosition = format.find('='); + // You can have an empty key, just make sure the type and value might exist + if (equalsPosition != std::string::npos && equalsPosition != (format.size() - 1)) { - std::pair keyValue; - keyValue.first = format.substr(0, equalsPosition); - keyValue.second = format.substr(equalsPosition + 1, format.size()); + const std::string_view keyValue = format.substr(0, equalsPosition); + const std::string_view typeAndValue = format.substr(equalsPosition + 1, format.size()); - std::u16string key = GeneralUtils::ASCIIToUTF16(keyValue.first); + const auto key = GeneralUtils::ASCIIToUTF16(keyValue); - auto colonPosition = keyValue.second.find(':'); + const auto colonPosition = typeAndValue.find(':'); - // If : is the first thing after an =, then this is an invalid LDF since - // we dont have a type to use. - if (colonPosition == std::string::npos || colonPosition == 0) return nullptr; + // If : is the first thing after an =, then this is an invalid LDF since + // we dont have a type to use. + if (colonPosition != std::string::npos && colonPosition != 0) { + const std::string_view ldfType = typeAndValue.substr(0, colonPosition); + const std::string_view ldfValue = typeAndValue.substr(colonPosition + 1, typeAndValue.size()); - std::pair ldfTypeAndValue; - ldfTypeAndValue.first = keyValue.second.substr(0, colonPosition); - ldfTypeAndValue.second = keyValue.second.substr(colonPosition + 1, keyValue.second.size()); + // Only allow empty values for string values. + if (!ldfValue.empty() || (ldfType == "0" /* UTF-16 */ || ldfType == "13" /* UTF-8 */)) { + const eLDFType type = GeneralUtils::TryParse(ldfType, LDF_TYPE_UNKNOWN); + switch (type) { + case LDF_TYPE_UTF_16: { + std::u16string data = GeneralUtils::UTF8ToUTF16(ldfValue); + toReturn.reset(new LDFData(key, data)); + break; + } - // Only allow empty values for string values. - if (ldfTypeAndValue.second.size() == 0 && !(ldfTypeAndValue.first == "0" || ldfTypeAndValue.first == "13")) return nullptr; + case LDF_TYPE_S32: { + const auto data = GeneralUtils::TryParse(ldfValue); + if (data) { + toReturn.reset(new LDFData(key, data.value())); + } else { + LOG("Warning: Attempted to process invalid int32 value (%s) from string (%s)", ldfValue.data(), format.data()); + } - eLDFType type; - char* storage; - try { - type = static_cast(strtol(ldfTypeAndValue.first.data(), &storage, 10)); - } catch (std::exception) { - LOG("Attempted to process invalid ldf type (%s) from string (%s)", ldfTypeAndValue.first.data(), format.data()); - return nullptr; - } + break; + } - LDFBaseData* returnValue = nullptr; - switch (type) { - case LDF_TYPE_UTF_16: { - std::u16string data = GeneralUtils::UTF8ToUTF16(ldfTypeAndValue.second); - returnValue = new LDFData(key, data); - break; - } + case LDF_TYPE_FLOAT: { + const auto data = GeneralUtils::TryParse(ldfValue); + if (data) { + toReturn.reset(new LDFData(key, data.value())); + } else { + LOG("Warning: Attempted to process invalid float value (%s) from string (%s)", ldfValue.data(), format.data()); + } + break; + } - case LDF_TYPE_S32: { - const auto data = GeneralUtils::TryParse(ldfTypeAndValue.second); - if (!data) { - LOG("Warning: Attempted to process invalid int32 value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; - } - returnValue = new LDFData(key, data.value()); + case LDF_TYPE_DOUBLE: { + const auto data = GeneralUtils::TryParse(ldfValue); + if (data) { + toReturn.reset(new LDFData(key, data.value())); + } else { + LOG("Warning: Attempted to process invalid double value (%s) from string (%s)", ldfValue.data(), format.data()); + } + break; + } - break; - } + case LDF_TYPE_U32: + { + uint32_t data; + bool parsed = true; + // Have to do this really weird parsing to allow for copy ellision + if (ldfValue == "true") { + data = 1; + } else if (ldfValue == "false") { + data = 0; + } else { + const auto dataOptional = GeneralUtils::TryParse(ldfValue); + if (!dataOptional) { + LOG("Warning: Attempted to process invalid uint32 value (%s) from string (%s)", ldfValue.data(), format.data()); + parsed = false; + } else { + data = dataOptional.value(); + } + } - case LDF_TYPE_FLOAT: { - const auto data = GeneralUtils::TryParse(ldfTypeAndValue.second); - if (!data) { - LOG("Warning: Attempted to process invalid float value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; - } - returnValue = new LDFData(key, data.value()); - break; - } + if (parsed) toReturn.reset(new LDFData(key, data)); + break; + } - case LDF_TYPE_DOUBLE: { - const auto data = GeneralUtils::TryParse(ldfTypeAndValue.second); - if (!data) { - LOG("Warning: Attempted to process invalid double value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; - } - returnValue = new LDFData(key, data.value()); - break; - } + case LDF_TYPE_BOOLEAN: { + bool data; + bool parsed = true; + // Have to do this really weird parsing to allow for copy ellision + if (ldfValue == "true") { + data = true; + } else if (ldfValue == "false") { + data = false; + } else { + const auto dataOptional = GeneralUtils::TryParse(ldfValue); + if (!dataOptional) { + LOG("Warning: Attempted to process invalid bool value (%s) from string (%s)", ldfValue.data(), format.data()); + parsed = false; + } else { + data = dataOptional.value(); + } + } - case LDF_TYPE_U32: - { - uint32_t data; + if (parsed) toReturn.reset(new LDFData(key, data)); + break; + } - if (ldfTypeAndValue.second == "true") { - data = 1; - } else if (ldfTypeAndValue.second == "false") { - data = 0; - } else { - const auto dataOptional = GeneralUtils::TryParse(ldfTypeAndValue.second); - if (!dataOptional) { - LOG("Warning: Attempted to process invalid uint32 value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; + case LDF_TYPE_U64: { + const auto data = GeneralUtils::TryParse(ldfValue); + if (data) { + toReturn.reset(new LDFData(key, data.value())); + } else { + LOG("Warning: Attempted to process invalid uint64 value (%s) from string (%s)", ldfValue.data(), format.data()); + } + break; + } + + case LDF_TYPE_OBJID: { + const auto data = GeneralUtils::TryParse(ldfValue); + if (data) { + toReturn.reset(new LDFData(key, data.value())); + } else { + LOG("Warning: Attempted to process invalid LWOOBJID value (%s) from string (%s)", ldfValue.data(), format.data()); + } + break; + } + + case LDF_TYPE_UTF_8: { + toReturn.reset(new LDFData(key, ldfValue.data())); + break; + } + + case LDF_TYPE_UNKNOWN: + [[fallthrough]]; + default: { + LOG("Warning: Attempted to process invalid unknown value (%s) from string (%s)", ldfValue.data(), format.data()); + break; + } + + } + } } - data = dataOptional.value(); } - - returnValue = new LDFData(key, data); - break; } - case LDF_TYPE_BOOLEAN: { - bool data; - - if (ldfTypeAndValue.second == "true") { - data = true; - } else if (ldfTypeAndValue.second == "false") { - data = false; - } else { - const auto dataOptional = GeneralUtils::TryParse(ldfTypeAndValue.second); - if (!dataOptional) { - LOG("Warning: Attempted to process invalid bool value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; - } - data = dataOptional.value(); - } - - returnValue = new LDFData(key, data); - break; - } - - case LDF_TYPE_U64: { - const auto data = GeneralUtils::TryParse(ldfTypeAndValue.second); - if (!data) { - LOG("Warning: Attempted to process invalid uint64 value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; - } - returnValue = new LDFData(key, data.value()); - break; - } - - case LDF_TYPE_OBJID: { - const auto data = GeneralUtils::TryParse(ldfTypeAndValue.second); - if (!data) { - LOG("Warning: Attempted to process invalid LWOOBJID value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - return nullptr; - } - returnValue = new LDFData(key, data.value()); - break; - } - - case LDF_TYPE_UTF_8: { - std::string data = ldfTypeAndValue.second.data(); - returnValue = new LDFData(key, data); - break; - } - - case LDF_TYPE_UNKNOWN: { - LOG("Warning: Attempted to process invalid unknown value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data()); - break; - } - - default: { - LOG("Warning: Attempted to process invalid LDF type (%d) from string (%s)", type, format.data()); - break; - } - } - return returnValue; + return toReturn; } diff --git a/dCommon/LDFFormat.h b/dCommon/LDFFormat.h index 7c2e939b..0f515d30 100644 --- a/dCommon/LDFFormat.h +++ b/dCommon/LDFFormat.h @@ -1,11 +1,12 @@ -#ifndef __LDFFORMAT__H__ -#define __LDFFORMAT__H__ +#ifndef LDFFORMAT_H +#define LDFFORMAT_H // Custom Classes #include "dCommonVars.h" #include "GeneralUtils.h" // C++ +#include #include #include #include @@ -46,17 +47,17 @@ public: virtual std::string GetValueAsString() const = 0; - virtual LDFBaseData* Copy() const = 0; + virtual std::unique_ptr Copy() const = 0; /** * Given an input string, return the data as a LDF key. */ - static LDFBaseData* DataFromString(const std::string_view& format); + static std::unique_ptr DataFromString(const std::string_view& format); }; template -class LDFData: public LDFBaseData { +class LDFData : public LDFBaseData { private: std::u16string key; T value; @@ -164,8 +165,8 @@ public: return this->GetValueString(); } - LDFBaseData* Copy() const override { - return new LDFData(key, value); + std::unique_ptr Copy() const override { + return std::make_unique>(key, value); } inline static const T Default = {}; @@ -226,4 +227,89 @@ template<> inline std::string LDFData::GetValueString() const { return template<> inline std::string LDFData::GetValueString() const { return this->value; } -#endif //!__LDFFORMAT__H__ +struct LwoNameValue { + using LDFPtr = std::unique_ptr; + using ValueType = std::map; + + LwoNameValue& operator=(const LwoNameValue& other) { + this->values = other.Copy(); + return *this; + } + + template + void Insert(const std::u16string& key, const T& value) { + this->values.insert_or_assign(key, std::unique_ptr(std::make_unique>(key, value))); + } + + void Insert(const std::u16string& key, const char* value) { + this->Insert(key, value); + } + + void Insert(const std::u16string& key, const char16_t* value) { + this->Insert(key, value); + } + + template + void Insert(const std::string& key, const T& value) { + this->Insert(GeneralUtils::UTF8ToUTF16(key), value); + } + + void Insert(const std::string& key, const char* value) { + this->Insert(GeneralUtils::UTF8ToUTF16(key), value); + } + + void Insert(const std::string& key, const char16_t* value) { + this->Insert(GeneralUtils::UTF8ToUTF16(key), value); + } + + const LDFPtr& ParseInsert(const std::string& data) { + LDFPtr toInsert(LDFBaseData::DataFromString(data)); + return toInsert ? + this->values.insert_or_assign(toInsert->GetKey(), std::move(toInsert)).first->second : + this->values.insert_or_assign(u"FAILED_TO_PARSE_" + GeneralUtils::UTF8ToUTF16(data), std::make_unique>("", "")).first->second; + } + + const LDFPtr& ParseInsert(const std::u16string& data) { + return this->ParseInsert(GeneralUtils::UTF16ToWTF8(data)); + } + + ValueType::const_iterator begin() const { + return this->values.cbegin(); + } + + ValueType::const_iterator end() const { + return this->values.cend(); + } + + void Erase(const std::u16string& key) { + this->values.erase(key); + } + + void Erase(const std::string& key) { + this->Erase(GeneralUtils::ASCIIToUTF16(key)); + } + + ValueType::iterator find(const ValueType::key_type& key) { + return this->values.find(key); + } + + ValueType::const_iterator find(const ValueType::key_type& key) const { + return this->values.find(key); + } + + LwoNameValue() = default; + + LwoNameValue(const LwoNameValue& other) { + this->values = other.Copy(); + } + + ValueType values; +private: + ValueType Copy() const { + ValueType copy; + for (const auto& [key, value] : this->values) copy.insert_or_assign(key, value->Copy()); + return copy; + } +}; + +#endif //!LDFFORMAT_H diff --git a/dCommon/Logger.cpp b/dCommon/Logger.cpp index 1888f1eb..0f31f541 100644 --- a/dCommon/Logger.cpp +++ b/dCommon/Logger.cpp @@ -96,3 +96,17 @@ bool Logger::GetLogToConsole() const { } return toReturn; } + +FuncEntry::FuncEntry(const char* funcName, const char* fileName, const uint32_t line) { + m_FuncName = funcName; + if (!m_FuncName) m_FuncName = "Unknown"; + m_Line = line; + m_FileName = fileName; + LOG("--> %s::%s:%i", m_FileName, m_FuncName, m_Line); +} + +FuncEntry::~FuncEntry() { + if (!m_FuncName || !m_FileName) return; + + LOG("<-- %s::%s:%i", m_FileName, m_FuncName, m_Line); +} diff --git a/dCommon/Logger.h b/dCommon/Logger.h index 3a1771e6..e7785a13 100644 --- a/dCommon/Logger.h +++ b/dCommon/Logger.h @@ -32,6 +32,19 @@ constexpr const char* GetFileNameFromAbsolutePath(const char* path) { #define LOG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->Log(str_, message, ##__VA_ARGS__); } while(0) #define LOG_DEBUG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->LogDebug(str_, message, ##__VA_ARGS__); } while(0) +// Place this right at the start of a function. Will log a message when called and then once you leave the function. +#define LOG_ENTRY auto str_ = GetFileNameFromAbsolutePath(__FILE__); FuncEntry funcEntry_(__FUNCTION__, str_, __LINE__) + +class FuncEntry { +public: + FuncEntry(const char* funcName, const char* fileName, const uint32_t line); + ~FuncEntry(); +private: + const char* m_FuncName = nullptr; + const char* m_FileName = nullptr; + uint32_t m_Line = 0; +}; + // Writer class for writing data to files. class Writer { public: diff --git a/dCommon/LxfmlBugged.cpp b/dCommon/LxfmlBugged.cpp index f5eeebc8..fcd56ee0 100644 --- a/dCommon/LxfmlBugged.cpp +++ b/dCommon/LxfmlBugged.cpp @@ -53,16 +53,21 @@ Lxfml::Result Lxfml::NormalizePositionOnlyFirstPart(const std::string_view data) continue; } - auto x = GeneralUtils::TryParse(split[9]).value(); - auto y = GeneralUtils::TryParse(split[10]).value(); - auto z = GeneralUtils::TryParse(split[11]).value(); - if (x < lowest.x) lowest.x = x; - if (y < lowest.y) lowest.y = y; - if (z < lowest.z) lowest.z = z; + try { + auto x = GeneralUtils::TryParse(split[9]).value(); + auto y = GeneralUtils::TryParse(split[10]).value(); + auto z = GeneralUtils::TryParse(split[11]).value(); + if (x < lowest.x) lowest.x = x; + if (y < lowest.y) lowest.y = y; + if (z < lowest.z) lowest.z = z; - if (highest.x < x) highest.x = x; - if (highest.y < y) highest.y = y; - if (highest.z < z) highest.z = z; + if (highest.x < x) highest.x = x; + if (highest.y < y) highest.y = y; + if (highest.z < z) highest.z = z; + } catch (std::exception& e) { + LOG("Failed to parse a split value of either (%s), (%s), or (%s).", split[9].c_str(), split[10].c_str(), split[11].c_str()); + return toReturn; // Early return since we failed to parse this lxfml. + } } auto delta = (highest - lowest) / 2.0f; diff --git a/dCommon/Metrics.cpp b/dCommon/Metrics.cpp index 5232cf78..f196bde6 100644 --- a/dCommon/Metrics.cpp +++ b/dCommon/Metrics.cpp @@ -1,105 +1,77 @@ -#include "Metrics.hpp" +#include "Metrics.h" + +#include "StringifiedEnum.h" #include -std::unordered_map Metrics::m_Metrics = {}; -std::vector Metrics::m_Variables = { - MetricVariable::GameLoop, - MetricVariable::PacketHandling, - MetricVariable::UpdateEntities, - MetricVariable::UpdateSpawners, - MetricVariable::Physics, - MetricVariable::UpdateReplica, - MetricVariable::Ghosting, - MetricVariable::CPUTime, - MetricVariable::Sleep, - MetricVariable::Frame, -}; +namespace { + std::unordered_map g_Metrics = {}; + std::vector g_Variables = { + MetricVariable::GameLoop, + MetricVariable::PacketHandling, + MetricVariable::UpdateEntities, + MetricVariable::UpdateSpawners, + MetricVariable::Physics, + MetricVariable::UpdateReplica, + MetricVariable::Ghosting, + MetricVariable::CPUTime, + MetricVariable::Sleep, + MetricVariable::Frame, + }; +} void Metrics::AddMeasurement(MetricVariable variable, int64_t value) { - const auto& iter = m_Metrics.find(variable); - - Metric* metric; - - if (iter == m_Metrics.end()) { - metric = new Metric(); - - m_Metrics[variable] = metric; - } else { - metric = iter->second; - } + auto& metric = g_Metrics[variable]; AddMeasurement(metric, value); } -void Metrics::AddMeasurement(Metric* metric, int64_t value) { - const auto index = metric->measurementIndex; +void Metrics::AddMeasurement(Metric& metric, int64_t value) { + const auto index = metric.measurementIndex; - metric->measurements[index] = value; + metric.measurements[index] = value; - if (metric->max == -1 || value > metric->max) { - metric->max = value; - } else if (metric->min == -1 || metric->min > value) { - metric->min = value; + if (metric.max == -1 || value > metric.max) { + metric.max = value; + } else if (metric.min == -1 || metric.min > value) { + metric.min = value; } - if (metric->measurementSize < MAX_MEASURMENT_POINTS) { - metric->measurementSize++; + if (metric.measurementSize < MAX_MEASURMENT_POINTS) { + metric.measurementSize++; } - metric->measurementIndex = (index + 1) % MAX_MEASURMENT_POINTS; + metric.measurementIndex = (index + 1) % MAX_MEASURMENT_POINTS; } -const Metric* Metrics::GetMetric(MetricVariable variable) { - const auto& iter = m_Metrics.find(variable); - - if (iter == m_Metrics.end()) { - return nullptr; - } - - Metric* metric = iter->second; +const Metric& Metrics::GetMetric(MetricVariable variable) { + auto& metric = g_Metrics[variable]; int64_t average = 0; - for (size_t i = 0; i < metric->measurementSize; i++) { - average += metric->measurements[i]; + for (size_t i = 0; i < metric.measurementSize; i++) { + average += metric.measurements[i]; } - average /= metric->measurementSize; + average /= metric.measurementSize; - metric->average = average; + metric.average = average; return metric; } void Metrics::StartMeasurement(MetricVariable variable) { - const auto& iter = m_Metrics.find(variable); + auto& metric = g_Metrics[variable]; - Metric* metric; - - if (iter == m_Metrics.end()) { - metric = new Metric(); - - m_Metrics[variable] = metric; - } else { - metric = iter->second; - } - - metric->activeMeasurement = std::chrono::high_resolution_clock::now(); + metric.activeMeasurement = std::chrono::high_resolution_clock::now(); } void Metrics::EndMeasurement(MetricVariable variable) { const auto end = std::chrono::high_resolution_clock::now(); - const auto& iter = m_Metrics.find(variable); + auto& metric = g_Metrics[variable]; - if (iter == m_Metrics.end()) { - return; - } - - Metric* metric = iter->second; - - const auto elapsed = end - metric->activeMeasurement; + const auto elapsed = end - metric.activeMeasurement; const auto nanoseconds = std::chrono::duration_cast(elapsed).count(); @@ -110,44 +82,12 @@ float Metrics::ToMiliseconds(int64_t nanoseconds) { return static_cast(nanoseconds) / 1e6; } -std::string Metrics::MetricVariableToString(MetricVariable variable) { - switch (variable) { - case MetricVariable::GameLoop: - return "GameLoop"; - case MetricVariable::PacketHandling: - return "PacketHandling"; - case MetricVariable::UpdateEntities: - return "UpdateEntities"; - case MetricVariable::UpdateSpawners: - return "UpdateSpawners"; - case MetricVariable::Physics: - return "Physics"; - case MetricVariable::UpdateReplica: - return "UpdateReplica"; - case MetricVariable::Sleep: - return "Sleep"; - case MetricVariable::CPUTime: - return "CPUTime"; - case MetricVariable::Frame: - return "Frame"; - case MetricVariable::Ghosting: - return "Ghosting"; - - default: - return "Invalid"; - } +const std::string_view Metrics::MetricVariableToString(MetricVariable variable) { + return StringifiedEnum::ToString(variable); } const std::vector& Metrics::GetAllMetrics() { - return m_Variables; -} - -void Metrics::Clear() { - for (const auto& pair : m_Metrics) { - delete pair.second; - } - - m_Metrics.clear(); + return g_Variables; } /* RSS Memory utilities diff --git a/dCommon/Metrics.h b/dCommon/Metrics.h new file mode 100644 index 00000000..ddf9bf09 --- /dev/null +++ b/dCommon/Metrics.h @@ -0,0 +1,48 @@ +#pragma once + +#include "dCommonVars.h" +#include +#include +#include +#include +#include + +#define MAX_MEASURMENT_POINTS 1024 + +enum class MetricVariable : int32_t { + GameLoop, + PacketHandling, + UpdateEntities, + UpdateSpawners, + Physics, + UpdateReplica, + Ghosting, + CPUTime, + Sleep, + Frame, +}; + +struct Metric { + int64_t measurements[MAX_MEASURMENT_POINTS] = {}; + size_t measurementIndex = 0; + size_t measurementSize = 0; + int64_t max = -1; + int64_t min = -1; + int64_t average = 0; + std::chrono::time_point activeMeasurement; +}; + +namespace Metrics { + void AddMeasurement(MetricVariable variable, int64_t value); + void AddMeasurement(Metric& metric, int64_t value); + const Metric& GetMetric(MetricVariable variable); + void StartMeasurement(MetricVariable variable); + void EndMeasurement(MetricVariable variable); + float ToMiliseconds(int64_t nanoseconds); + const std::string_view MetricVariableToString(MetricVariable variable); + const std::vector& GetAllMetrics(); + + size_t GetPeakRSS(); + size_t GetCurrentRSS(); + size_t GetProcessID(); +}; diff --git a/dCommon/Metrics.hpp b/dCommon/Metrics.hpp deleted file mode 100644 index c03c914f..00000000 --- a/dCommon/Metrics.hpp +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include "dCommonVars.h" -#include -#include -#include -#include - -#define MAX_MEASURMENT_POINTS 1024 - -enum class MetricVariable : int32_t -{ - GameLoop, - PacketHandling, - UpdateEntities, - UpdateSpawners, - Physics, - UpdateReplica, - Ghosting, - CPUTime, - Sleep, - Frame, -}; - -struct Metric -{ - int64_t measurements[MAX_MEASURMENT_POINTS] = {}; - size_t measurementIndex = 0; - size_t measurementSize = 0; - int64_t max = -1; - int64_t min = -1; - int64_t average = 0; - std::chrono::time_point activeMeasurement; -}; - -class Metrics -{ -public: - ~Metrics(); - - static void AddMeasurement(MetricVariable variable, int64_t value); - static void AddMeasurement(Metric* metric, int64_t value); - static const Metric* GetMetric(MetricVariable variable); - static void StartMeasurement(MetricVariable variable); - static void EndMeasurement(MetricVariable variable); - static float ToMiliseconds(int64_t nanoseconds); - static std::string MetricVariableToString(MetricVariable variable); - static const std::vector& GetAllMetrics(); - - static size_t GetPeakRSS(); - static size_t GetCurrentRSS(); - static size_t GetProcessID(); - - static void Clear(); - -private: - Metrics(); - - static std::unordered_map m_Metrics; - static std::vector m_Variables; -}; diff --git a/dCommon/NiPoint3.h b/dCommon/NiPoint3.h index b968a4de..d1b65c93 100644 --- a/dCommon/NiPoint3.h +++ b/dCommon/NiPoint3.h @@ -1,6 +1,8 @@ #ifndef __NIPOINT3_H__ #define __NIPOINT3_H__ - +#ifndef GLM_ENABLE_EXPERIMENTAL +# define GLM_ENABLE_EXPERIMENTAL +#endif /*! \file NiPoint3.hpp \brief Defines a point in space in XYZ coordinates diff --git a/dCommon/NiQuaternion.h b/dCommon/NiQuaternion.h index a4546e2c..c4ef0661 100644 --- a/dCommon/NiQuaternion.h +++ b/dCommon/NiQuaternion.h @@ -1,6 +1,8 @@ #ifndef NIQUATERNION_H #define NIQUATERNION_H - +#ifndef GLM_ENABLE_EXPERIMENTAL +# define GLM_ENABLE_EXPERIMENTAL +#endif // Custom Classes #include "NiPoint3.h" diff --git a/dCommon/Sd0.cpp b/dCommon/Sd0.cpp index 5b97cb75..e06f242e 100644 --- a/dCommon/Sd0.cpp +++ b/dCommon/Sd0.cpp @@ -45,6 +45,12 @@ Sd0::Sd0(std::istream& buffer) { uint32_t bufferSize = buffer.tellg(); buffer.seekg(0, std::ios::beg); WriteSize(firstChunk, bufferSize); + // its expected that if we got here, we got an old sd0 buffer where we ignored the sd0 part + // that means this can be at most the compressed chunk limit. + if (bufferSize > MAX_UNCOMPRESSED_CHUNK_SIZE) { + LOG("Possible bad chunk size of %i specified, rejecting.", bufferSize); + return; + } firstChunk.resize(firstChunk.size() + bufferSize); auto* dataStart = reinterpret_cast(firstChunk.data() + GetDataOffset(true)); if (!buffer.read(dataStart, bufferSize)) { @@ -71,6 +77,12 @@ Sd0::Sd0(std::istream& buffer) { WriteSize(chunk, chunkSize); + // Assuming a good buffer that is large enough to take up 2 zlib buffers + // any buffer should be compressed enough to take up less size than its uncompressed counterpart + if (chunkSize > MAX_UNCOMPRESSED_CHUNK_SIZE) { + LOG("Possible bad chunk size of %i specified, rejecting.", chunkSize); + break; + } chunk.resize(chunkSize + dataOffset); auto* dataStart = reinterpret_cast(chunk.data() + dataOffset); if (!buffer.read(dataStart, chunkSize)) { @@ -95,6 +107,11 @@ void Sd0::FromData(const uint8_t* data, size_t bufferSize) { startOffset, numToCopy, compressedChunk.data(), compressedChunk.size()); + if (compressedSize == -1) { + LOG("Failed to compress chunk, aborting"); + break; + } + auto& chunk = m_Chunks.emplace_back(); bool firstBuffer = m_Chunks.size() == 1; auto dataOffset = GetDataOffset(firstBuffer); @@ -119,6 +136,12 @@ std::string Sd0::GetAsStringUncompressed() const { auto dataOffset = GetDataOffset(first); first = false; const auto chunkSize = chunk.size(); + if (chunkSize <= static_cast(dataOffset)) { + LOG("Bad chunkSize for data, aborting"); + toReturn = ""; + totalSize = 0; + break; + } auto oldSize = toReturn.size(); toReturn.resize(oldSize + MAX_UNCOMPRESSED_CHUNK_SIZE); @@ -128,6 +151,13 @@ std::string Sd0::GetAsStringUncompressed() const { reinterpret_cast(toReturn.data()) + oldSize, MAX_UNCOMPRESSED_CHUNK_SIZE, error); + if (uncompressedSize == -1) { + LOG("Failed to decompress chunk, aborting"); + toReturn = ""; + totalSize = 0; + break; + } + totalSize += uncompressedSize; } diff --git a/dCommon/ZCompression.cpp b/dCommon/ZCompression.cpp index e5b3c8fb..11ec4e83 100644 --- a/dCommon/ZCompression.cpp +++ b/dCommon/ZCompression.cpp @@ -3,12 +3,12 @@ #include "zlib.h" namespace ZCompression { - int32_t GetMaxCompressedLength(int32_t nLenSrc) { - int32_t n16kBlocks = (nLenSrc + 16383) / 16384; // round up any fraction of a block + uint32_t GetMaxCompressedLength(uint32_t nLenSrc) { + uint32_t n16kBlocks = (nLenSrc + 16383) / 16384; // round up any fraction of a block return (nLenSrc + 6 + (n16kBlocks * 5)); } - int32_t Compress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst) { + int32_t Compress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst) { z_stream zInfo = { 0 }; zInfo.total_in = zInfo.avail_in = nLenSrc; zInfo.total_out = zInfo.avail_out = nLenDst; @@ -27,7 +27,7 @@ namespace ZCompression { return(nRet); } - int32_t Decompress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst, int32_t& nErr) { + int32_t Decompress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst, int32_t& nErr) { // Get the size of the decompressed data z_stream zInfo = { 0 }; zInfo.total_in = zInfo.avail_in = nLenSrc; diff --git a/dCommon/ZCompression.h b/dCommon/ZCompression.h index 22a5ff86..d58a8778 100644 --- a/dCommon/ZCompression.h +++ b/dCommon/ZCompression.h @@ -3,10 +3,10 @@ #include namespace ZCompression { - int32_t GetMaxCompressedLength(int32_t nLenSrc); + uint32_t GetMaxCompressedLength(uint32_t nLenSrc); - int32_t Compress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst); + int32_t Compress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst); - int32_t Decompress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst, int32_t& nErr); + int32_t Decompress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst, int32_t& nErr); } diff --git a/dCommon/dClient/AssetManager.cpp b/dCommon/dClient/AssetManager.cpp index bc4fbf35..92a458d3 100644 --- a/dCommon/dClient/AssetManager.cpp +++ b/dCommon/dClient/AssetManager.cpp @@ -7,7 +7,7 @@ #include "zlib.h" constexpr uint32_t CRC32_INIT = 0xFFFFFFFF; -constexpr auto NULL_TERMINATOR = std::string_view{"\0\0\0", 4}; +constexpr auto NULL_TERMINATOR = std::string_view{ "\0\0\0", 4 }; AssetManager::AssetManager(const std::filesystem::path& path) { if (!std::filesystem::is_directory(path)) { @@ -25,7 +25,7 @@ AssetManager::AssetManager(const std::filesystem::path& path) { if (!std::filesystem::exists(m_Path / ".." / "versions")) { throw std::runtime_error("No \"versions\" directory found in the parent directories of \"res\" - packed asset bundle cannot be loaded."); } - + m_AssetBundleType = eAssetBundleType::Packed; m_RootPath = (m_Path / ".."); @@ -34,7 +34,7 @@ AssetManager::AssetManager(const std::filesystem::path& path) { if (!std::filesystem::exists(m_Path / ".." / ".." / "versions")) { throw std::runtime_error("No \"versions\" directory found in the parent directories of \"res\" - packed asset bundle cannot be loaded."); } - + m_AssetBundleType = eAssetBundleType::Packed; m_RootPath = (m_Path / ".." / ".."); @@ -54,15 +54,15 @@ AssetManager::AssetManager(const std::filesystem::path& path) { } switch (m_AssetBundleType) { - case eAssetBundleType::Packed: { - this->LoadPackIndex(); - break; - } - case eAssetBundleType::None: - [[fallthrough]]; - case eAssetBundleType::Unpacked: { - break; - } + case eAssetBundleType::Packed: { + this->LoadPackIndex(); + break; + } + case eAssetBundleType::None: + [[fallthrough]]; + case eAssetBundleType::Unpacked: { + break; + } } } @@ -79,7 +79,7 @@ bool AssetManager::HasFile(std::string fixedName) const { std::replace(fixedName.begin(), fixedName.end(), '\\', '/'); if (std::filesystem::exists(m_ResPath / fixedName)) return true; - if (this->m_AssetBundleType == eAssetBundleType::Unpacked) return false; + if (this->m_AssetBundleType == eAssetBundleType::Unpacked || !m_PackIndex) return false; std::replace(fixedName.begin(), fixedName.end(), '/', '\\'); if (fixedName.rfind("client\\res\\", 0) != 0) fixedName = "client\\res\\" + fixedName; @@ -145,8 +145,12 @@ bool AssetManager::GetFile(std::string fixedName, char** data, uint32_t* len) co } const auto& pack = this->m_PackIndex->GetPacks().at(packIndex); - const bool success = pack.ReadFileFromPack(crc, data, len); - + bool success = false; + try { + success = pack.ReadFileFromPack(crc, data, len); + } catch (std::exception& e) { + LOG("Failed to read file %s from pack file", fixedName.c_str()); + } return success; } diff --git a/dCommon/dClient/Pack.cpp b/dCommon/dClient/Pack.cpp index 800cfa19..0f558a54 100644 --- a/dCommon/dClient/Pack.cpp +++ b/dCommon/dClient/Pack.cpp @@ -46,6 +46,7 @@ bool Pack::HasFile(const uint32_t crc) const { } bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) const { + const auto pathStr = m_FilePath.string(); // Time for some wacky C file reading for speed reasons PackRecord pkRecord{}; @@ -65,16 +66,21 @@ bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) cons bool isCompressed = (pkRecord.m_IsCompressed & 0xff) > 0; auto inPackSize = isCompressed ? pkRecord.m_CompressedSize : pkRecord.m_UncompressedSize; - FILE* file; + FILE* file = nullptr; #ifdef _WIN32 - fopen_s(&file, m_FilePath.string().c_str(), "rb"); + fopen_s(&file, pathStr.c_str(), "rb"); #elif __APPLE__ // macOS has 64bit file IO by default - file = fopen(m_FilePath.string().c_str(), "rb"); + file = fopen(pathStr.c_str(), "rb"); #else - file = fopen64(m_FilePath.string().c_str(), "rb"); + file = fopen64(pathStr.c_str(), "rb"); #endif + if (!file) { + LOG("No file found for path %s", pathStr.c_str()); + throw std::runtime_error("Could not find file " + pathStr); + } + fseek(file, pos, SEEK_SET); if (!isCompressed) { @@ -102,14 +108,18 @@ bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) cons int32_t readInData = fread(&size, sizeof(uint32_t), 1, file); pos += 4; // Move pointer position 4 to the right - char* chunk = static_cast(malloc(size)); - int32_t readInData2 = fread(chunk, sizeof(int8_t), size, file); + std::unique_ptr chunk(new char[size]); + int32_t readInData2 = fread(chunk.get(), sizeof(int8_t), size, file); pos += size; // Move pointer position the amount of bytes read to the right int32_t err; - currentReadPos += ZCompression::Decompress(reinterpret_cast(chunk), size, reinterpret_cast(decompressedData + currentReadPos), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err); + const auto countToRead = ZCompression::Decompress(reinterpret_cast(chunk.get()), size, reinterpret_cast(decompressedData + currentReadPos), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err); + if (countToRead == -1) { + LOG("Error decompressing zlib data from file %s", pathStr.c_str()); + throw std::runtime_error("Error decompressing zlib data from file " + pathStr); + } + currentReadPos += countToRead; - free(chunk); } *data = decompressedData; diff --git a/dCommon/dConfig.cpp b/dCommon/dConfig.cpp index 7ebfad25..21a0746d 100644 --- a/dCommon/dConfig.cpp +++ b/dCommon/dConfig.cpp @@ -84,3 +84,7 @@ void dConfig::ProcessLine(const std::string& line) { this->m_ConfigValues.insert(std::make_pair(key, value)); } + +std::string dConfig::GetValue(const std::string& key, const char* emptyValue) { + return GetValue(key, std::string(emptyValue)); +}; diff --git a/dCommon/dConfig.h b/dCommon/dConfig.h index 36c94148..8e645caa 100644 --- a/dCommon/dConfig.h +++ b/dCommon/dConfig.h @@ -5,6 +5,8 @@ #include #include +#include "GeneralUtils.h" + class dConfig { public: dConfig(const std::string& filepath); @@ -22,6 +24,14 @@ public: */ const std::string& GetValue(std::string key); + // Gets a value from the config and returns the parsed value, or the default value should parsing have failed. + template + T GetValue(const std::string& key, const T emptyValue = T()) { + return GeneralUtils::TryParse(GetValue(key)).value_or(emptyValue); + } + + std::string GetValue(const std::string& key, const char* emptyValue); + /** * Loads the config from a file */ @@ -43,3 +53,9 @@ private: std::vector> m_ConfigHandlers; std::string m_ConfigFilePath; }; + +template<> +inline std::string dConfig::GetValue(const std::string& key, const std::string emptyValue) { + const auto& value = GetValue(key); + return value.empty() ? emptyValue : value; +}; diff --git a/dCommon/dEnums/dCommonVars.h b/dCommon/dEnums/dCommonVars.h index f9efa2c6..c3ed1b14 100644 --- a/dCommon/dEnums/dCommonVars.h +++ b/dCommon/dEnums/dCommonVars.h @@ -16,8 +16,8 @@ // These are the same define, but they mean two different things in different contexts // so a different define to distinguish what calculation is happening will help clarity. -#define FRAMES_TO_MS(x) 1000 / x -#define MS_TO_FRAMES(x) 1000 / x +#define FRAMES_TO_MS(x) (1000 / (x)) +#define MS_TO_FRAMES(x) (1000 / (x)) //=========== FRAME TIMINGS =========== constexpr uint32_t highFramerate = 60; @@ -58,6 +58,7 @@ constexpr LWOCLONEID LWOCLONEID_INVALID = -1; //!< Invalid LWOCLONEID constexpr LWOINSTANCEID LWOINSTANCEID_INVALID = -1; //!< Invalid LWOINSTANCEID constexpr LWOMAPID LWOMAPID_INVALID = -1; //!< Invalid LWOMAPID constexpr uint64_t LWOZONEID_INVALID = 0; //!< Invalid LWOZONEID +constexpr uint32_t MAX_MESSAGE_LENGTH = 0x500000; //!< Prevent exceptionally large msgs from being processed. Should always be used to check user provided inputs. constexpr float PI = 3.14159f; @@ -110,18 +111,6 @@ private: constexpr LWOSCENEID LWOSCENEID_INVALID = -1; -struct LWONameValue { - uint32_t length = 0; //!< The length of the name - std::u16string name; //!< The name - - LWONameValue() = default; - - LWONameValue(const std::u16string& name) { - this->name = name; - this->length = static_cast(name.length()); - } -}; - struct FriendData { public: bool isOnline = false; diff --git a/dCommon/dEnums/eCharacterVersion.h b/dCommon/dEnums/eCharacterVersion.h index 15610c3a..7ba3be8f 100644 --- a/dCommon/dEnums/eCharacterVersion.h +++ b/dCommon/dEnums/eCharacterVersion.h @@ -20,7 +20,8 @@ enum class eCharacterVersion : uint32_t { NJ_JAYMISSIONS, NEXUS_FORCE_EXPLORER, // Fixes pet ids in player inventories PET_IDS, // Fixes pet ids in player inventories - UP_TO_DATE, // will become INVENTORY_PERSISTENT_IDS + INVENTORY_PERSISTENT_IDS, // Fixes racing meta missions + UP_TO_DATE, // will become RACING_META_MISSIONS }; #endif //!__ECHARACTERVERSION__H__ diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.cpp index 19111490..f84e692b 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.cpp @@ -1,6 +1,5 @@ #include "CDActivitiesTable.h" - void CDActivitiesTable::LoadValuesFromDatabase() { // First, get the size of the table uint32_t size = 0; @@ -56,3 +55,13 @@ std::vector CDActivitiesTable::Query(std::function CDActivitiesTable::GetActivity(const uint32_t activityID) { + auto& entries = GetEntries(); + for (const auto& entry : entries) { + if (entry.ActivityID == activityID) { + return entry; + } + } + return std::nullopt; +} diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.h index 3e1d4c37..86731bba 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.h +++ b/dDatabase/CDClientDatabase/CDClientTables/CDActivitiesTable.h @@ -2,6 +2,7 @@ // Custom Classes #include "CDTable.h" +#include struct CDActivities { uint32_t ActivityID; @@ -31,4 +32,5 @@ public: // Queries the table with a custom "where" clause std::vector Query(std::function predicate); + std::optional GetActivity(const uint32_t activityID); }; diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDItemComponentTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDItemComponentTable.cpp index 0d8b1ad9..8b69e262 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDItemComponentTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDItemComponentTable.cpp @@ -154,8 +154,8 @@ std::map CDItemComponentTable::ParseCraftingCurrencies(const CDIt // Checking for 2 here, not sure what to do when there's more stuff than expected if (amountSplit.size() == 2) { currencies.insert({ - std::stoull(amountSplit[0]), - std::stoi(amountSplit[1]) + GeneralUtils::TryParse(amountSplit[0], LOT_NULL), + GeneralUtils::TryParse(amountSplit[1], 0) }); } } diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.cpp index c98254ea..6466d64c 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.cpp @@ -93,13 +93,14 @@ std::vector CDMissionsTable::Query(std::function p } const CDMissions* CDMissionsTable::GetPtrByMissionID(uint32_t missionID) const { + const CDMissions* toReturn = &Default; for (const auto& entry : GetEntries()) { if (entry.id == missionID) { - return const_cast(&entry); + toReturn = &entry; } } - return &Default; + return toReturn; } const CDMissions& CDMissionsTable::GetByMissionID(uint32_t missionID, bool& found) const { diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.h index c5ae0e88..fea37bd6 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.h +++ b/dDatabase/CDClientDatabase/CDClientTables/CDMissionsTable.h @@ -66,6 +66,7 @@ public: // Queries the table with a custom "where" clause std::vector Query(std::function predicate); + // Cannot be null. const CDMissions* GetPtrByMissionID(uint32_t missionID) const; const CDMissions& GetByMissionID(uint32_t missionID, bool& found) const; diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.cpp index a07446b5..522c3e88 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.cpp @@ -38,3 +38,11 @@ std::vector CDObjectSkillsTable::Query(std::function CDObjectSkillsTable::Get(const LOT lot) const { + std::vector toReturn; + for (const auto& entry : GetEntries()) { + if (entry.objectTemplate == lot) toReturn.push_back(entry); + } + return toReturn; +} diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.h index 731f6657..ed314212 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.h +++ b/dDatabase/CDClientDatabase/CDClientTables/CDObjectSkillsTable.h @@ -4,12 +4,13 @@ #include "CDTable.h" #include +#include struct CDObjectSkills { uint32_t objectTemplate; //!< The LOT of the item uint32_t skillID; //!< The Skill ID of the object uint32_t castOnType; //!< ??? - uint32_t AICombatWeight; //!< ??? + int32_t AICombatWeight; //!< ??? }; class CDObjectSkillsTable : public CDTable> { @@ -17,5 +18,6 @@ public: void LoadValuesFromDatabase(); // Queries the table with a custom "where" clause std::vector Query(std::function predicate); + std::vector Get(const LOT lot) const; }; diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDObjectsTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDObjectsTable.cpp index 738a13ac..ede9dad7 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDObjectsTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDObjectsTable.cpp @@ -69,7 +69,7 @@ const CDObjects& CDObjectsTable::GetByID(const uint32_t lot) { entry.name = tableData.getStringField("name", ""); UNUSED(entry.placeable = tableData.getIntField("placeable", -1)); entry.type = tableData.getStringField("type", ""); - UNUSED(ntry.description = tableData.getStringField(4, "")); + UNUSED(entry.description = tableData.getStringField(4, "")); UNUSED(entry.localize = tableData.getIntField("localize", -1)); UNUSED(entry.npcTemplateID = tableData.getIntField("npcTemplateID", -1)); UNUSED(entry.displayName = tableData.getStringField("displayName", "")); diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDRailActivatorComponent.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDRailActivatorComponent.cpp index 72a26beb..277a5cee 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDRailActivatorComponent.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDRailActivatorComponent.cpp @@ -56,7 +56,7 @@ CDRailActivatorComponent CDRailActivatorComponentTable::GetEntryByID(int32_t id) std::pair CDRailActivatorComponentTable::EffectPairFromString(std::string& str) { const auto split = GeneralUtils::SplitString(str, ':'); if (split.size() == 2) { - return { std::stoi(split.at(0)), GeneralUtils::ASCIIToUTF16(split.at(1)) }; + return { GeneralUtils::TryParse(split.at(0), 0), GeneralUtils::ASCIIToUTF16(split.at(1)) }; } return {}; diff --git a/dDatabase/GameDatabase/ITables/IProperty.h b/dDatabase/GameDatabase/ITables/IProperty.h index c4957e5b..24171576 100644 --- a/dDatabase/GameDatabase/ITables/IProperty.h +++ b/dDatabase/GameDatabase/ITables/IProperty.h @@ -34,6 +34,7 @@ public: }; struct PropertyEntranceResult { + // This is the number of entries that are in the query IF it were ran without a limit. int32_t totalEntriesMatchingQuery{}; // The entries that match the query. This should only contain up to 12 entries. std::vector entries; @@ -48,7 +49,7 @@ public: // Get the properties for the given property lookup params. // This is expected to return a result set of up to 12 properties // so as not to transfer too much data at once. - virtual std::optional GetProperties(const PropertyLookup& params) = 0; + virtual IProperty::PropertyEntranceResult GetProperties(const PropertyLookup& params) = 0; // Update the property moderation info for the given property id. virtual void UpdatePropertyModerationInfo(const IProperty::Info& info) = 0; diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index 456ab5fa..accfb27e 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -9,6 +9,20 @@ typedef std::unique_ptr& UniquePreppedStmtRef; typedef std::unique_ptr UniqueResultSet; +// This struct is used to keep the PreparedStatement alive alongside the ResultSet, since the ResultSet will be invalidated if the PreparedStatement is destroyed. +// Declaring the members in reverse order of usage to ensure the PreparedStatement is destroyed after the ResultSet. This is guaranteed by the C++ standard. +struct PreparedStmtResultSet { + std::unique_ptr m_stmt; + std::unique_ptr m_resultSet; + + PreparedStmtResultSet(sql::PreparedStatement* stmt = nullptr, sql::ResultSet* resultSet = nullptr) + : m_stmt(stmt), m_resultSet(resultSet) {} + + sql::ResultSet* operator->() const { + return m_resultSet.get(); + } +}; + // Purposefully no definition for this to provide linker errors in the case someone tries to // bind a parameter to a type that isn't defined. template @@ -113,7 +127,7 @@ public: std::string GetBehavior(const LWOOBJID behaviorId) override; void RemoveBehavior(const LWOOBJID characterId) override; void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override; - std::optional GetProperties(const IProperty::PropertyLookup& params) override; + IProperty::PropertyEntranceResult GetProperties(const IProperty::PropertyLookup& params) override; std::vector GetDescendingLeaderboard(const uint32_t activityId) override; std::vector GetAscendingLeaderboard(const uint32_t activityId) override; std::vector GetNsLeaderboard(const uint32_t activityId) override; @@ -136,12 +150,15 @@ private: // Generic query functions that can be used for any query. // Return type may be different depending on the query, so it is up to the caller to check the return type. // The first argument is the query string, and the rest are the parameters to bind to the query. - // The return type is a unique_ptr to the result set, which is deleted automatically when it goes out of scope + // The return type is a PreparedStmtResultSet which keeps the PreparedStatement alive alongside the ResultSet. template - inline std::unique_ptr ExecuteSelect(const std::string& query, Args&&... args) { - std::unique_ptr preppedStmt(CreatePreppedStmt(query)); - SetParams(preppedStmt, std::forward(args)...); - DLU_SQL_TRY_CATCH_RETHROW(return std::unique_ptr(preppedStmt->executeQuery())); + inline PreparedStmtResultSet ExecuteSelect(const std::string& query, Args&&... args) { + PreparedStmtResultSet toReturn; + toReturn.m_stmt.reset(CreatePreppedStmt(query)); + SetParams(toReturn.m_stmt, std::forward(args)...); + DLU_SQL_TRY_CATCH_RETHROW(toReturn.m_resultSet.reset(toReturn.m_stmt->executeQuery())); + // Return the PreparedStmtResultSet, which now owns both the PreparedStatement and ResultSet via unique_ptr and will ensure they are properly cleaned up. + return toReturn; } template diff --git a/dDatabase/GameDatabase/MySQL/Tables/CharInfo.cpp b/dDatabase/GameDatabase/MySQL/Tables/CharInfo.cpp index 5d744b3d..719a8372 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/CharInfo.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/CharInfo.cpp @@ -12,7 +12,7 @@ std::vector MySQLDatabase::GetApprovedCharacterNames() { return toReturn; } -std::optional CharInfoFromQueryResult(std::unique_ptr stmt) { +std::optional CharInfoFromQueryResult(PreparedStmtResultSet& stmt) { if (!stmt->next()) { return std::nullopt; } @@ -31,15 +31,13 @@ std::optional CharInfoFromQueryResult(std::unique_ptr MySQLDatabase::GetCharacterInfo(const LWOOBJID charId) { - return CharInfoFromQueryResult( - ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE id = ? LIMIT 1;", charId) - ); + auto result = ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE id = ? LIMIT 1;", charId); + return CharInfoFromQueryResult(result); } std::optional MySQLDatabase::GetCharacterInfo(const std::string_view name) { - return CharInfoFromQueryResult( - ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE name = ? LIMIT 1;", name) - ); + auto result = ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE name = ? LIMIT 1;", name); + return CharInfoFromQueryResult(result); } std::vector MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accountId) { diff --git a/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp b/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp index eb60bf08..81439c56 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp @@ -14,7 +14,7 @@ std::optional MySQLDatabase::GetDonationTotal(const uint32_t activityI return donation_total->getUInt("donation_total"); } -std::vector ProcessQuery(UniqueResultSet& rows) { +std::vector ProcessQuery(PreparedStmtResultSet& rows) { std::vector entries; entries.reserve(rows->rowsCount()); diff --git a/dDatabase/GameDatabase/MySQL/Tables/Mail.cpp b/dDatabase/GameDatabase/MySQL/Tables/Mail.cpp index 27d76899..c6900492 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Mail.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Mail.cpp @@ -48,7 +48,7 @@ std::vector MySQLDatabase::GetMailForPlayer(const LWOOBJID characterId } std::optional MySQLDatabase::GetMail(const uint64_t mailId) { - auto res = ExecuteSelect("SELECT attachment_lot, attachment_count FROM mail WHERE id=? LIMIT 1;", mailId); + auto res = ExecuteSelect("SELECT attachment_lot, attachment_count, receiver_id FROM mail WHERE id=? LIMIT 1;", mailId); if (!res->next()) { return std::nullopt; @@ -57,6 +57,7 @@ std::optional MySQLDatabase::GetMail(const uint64_t mailId) { MailInfo toReturn; toReturn.itemLOT = res->getInt("attachment_lot"); toReturn.itemCount = res->getInt("attachment_count"); + toReturn.receiverId = res->getUInt64("receiver_id"); return toReturn; } diff --git a/dDatabase/GameDatabase/MySQL/Tables/Property.cpp b/dDatabase/GameDatabase/MySQL/Tables/Property.cpp index 18916e24..ee958c80 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Property.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Property.cpp @@ -1,7 +1,7 @@ #include "MySQLDatabase.h" #include "ePropertySortType.h" -IProperty::Info ReadPropertyInfo(UniqueResultSet& result) { +IProperty::Info ReadPropertyInfo(PreparedStmtResultSet& result) { IProperty::Info info; info.id = result->getUInt64("id"); info.ownerId = result->getInt64("owner_id"); @@ -18,10 +18,10 @@ IProperty::Info ReadPropertyInfo(UniqueResultSet& result) { return info; } -std::optional MySQLDatabase::GetProperties(const IProperty::PropertyLookup& params) { - std::optional result; +IProperty::PropertyEntranceResult MySQLDatabase::GetProperties(const IProperty::PropertyLookup& params) { + IProperty::PropertyEntranceResult result; std::string query; - std::unique_ptr properties; + PreparedStmtResultSet properties; if (params.sortChoice == SORT_TYPE_FEATURED || params.sortChoice == SORT_TYPE_FRIENDS) { query = R"QUERY( @@ -73,8 +73,7 @@ std::optional MySQLDatabase::GetProperties(co params.playerId ); if (count->next()) { - if (!result) result = IProperty::PropertyEntranceResult(); - result->totalEntriesMatchingQuery = count->getUInt("count"); + result.totalEntriesMatchingQuery = count->getUInt("count"); } } else { if (params.sortChoice == SORT_TYPE_REPUTATION) { @@ -127,14 +126,12 @@ std::optional MySQLDatabase::GetProperties(co params.playerSort ); if (count->next()) { - if (!result) result = IProperty::PropertyEntranceResult(); - result->totalEntriesMatchingQuery = count->getUInt("count"); + result.totalEntriesMatchingQuery = count->getUInt("count"); } } while (properties->next()) { - if (!result) result = IProperty::PropertyEntranceResult(); - result->entries.push_back(ReadPropertyInfo(properties)); + result.entries.push_back(ReadPropertyInfo(properties)); } return result; diff --git a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp index 06865c5e..6367560a 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp @@ -1,6 +1,6 @@ #include "MySQLDatabase.h" -IUgc::Model ReadModel(UniqueResultSet& result) { +IUgc::Model ReadModel(PreparedStmtResultSet& result) { IUgc::Model model; // blob is owned by the query, so we need to do a deep copy :/ diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index 3b6dc643..afcd0d26 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -111,7 +111,7 @@ public: std::string GetBehavior(const LWOOBJID behaviorId) override; void RemoveBehavior(const LWOOBJID characterId) override; void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override; - std::optional GetProperties(const IProperty::PropertyLookup& params) override; + IProperty::PropertyEntranceResult GetProperties(const IProperty::PropertyLookup& params) override; std::vector GetDescendingLeaderboard(const uint32_t activityId) override; std::vector GetAscendingLeaderboard(const uint32_t activityId) override; std::vector GetNsLeaderboard(const uint32_t activityId) override; @@ -170,91 +170,91 @@ private: template<> inline void SetParam(PreppedStmtRef stmt, const int index, const std::string_view param) { - LOG("%s", param.data()); + LOG_DEBUG("%s", param.data()); stmt.bind(index, param.data()); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const char* param) { - LOG("%s", param); + LOG_DEBUG("%s", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const std::string param) { - LOG("%s", param.c_str()); + LOG_DEBUG("%s", param.c_str()); stmt.bind(index, param.c_str()); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const int8_t param) { - LOG("%u", param); + LOG_DEBUG("%u", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const uint8_t param) { - LOG("%d", param); + LOG_DEBUG("%d", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const int16_t param) { - LOG("%u", param); + LOG_DEBUG("%u", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const uint16_t param) { - LOG("%d", param); + LOG_DEBUG("%d", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const uint32_t param) { - LOG("%u", param); + LOG_DEBUG("%u", param); stmt.bind(index, static_cast(param)); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const int32_t param) { - LOG("%d", param); + LOG_DEBUG("%d", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const int64_t param) { - LOG("%llu", param); + LOG_DEBUG("%llu", param); stmt.bind(index, static_cast(param)); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const uint64_t param) { - LOG("%llu", param); + LOG_DEBUG("%llu", param); stmt.bind(index, static_cast(param)); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const float param) { - LOG("%f", param); + LOG_DEBUG("%f", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const double param) { - LOG("%f", param); + LOG_DEBUG("%f", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const bool param) { - LOG("%d", param); + LOG_DEBUG("%d", param); stmt.bind(index, param); } template<> inline void SetParam(PreppedStmtRef stmt, const int index, const std::istream* param) { - LOG("Blob"); + LOG_DEBUG("Blob"); // This is the one time you will ever see me use const_cast. std::stringstream stream; stream << param->rdbuf(); @@ -264,10 +264,10 @@ inline void SetParam(PreppedStmtRef stmt, const int index, const std::istream* p template<> inline void SetParam(PreppedStmtRef stmt, const int index, const std::optional param) { if (param) { - LOG("%d", param.value()); + LOG_DEBUG("%d", param.value()); stmt.bind(index, static_cast(param.value())); } else { - LOG("Null"); + LOG_DEBUG("Null"); stmt.bindNull(index); } } @@ -275,10 +275,10 @@ inline void SetParam(PreppedStmtRef stmt, const int index, const std::optional inline void SetParam(PreppedStmtRef stmt, const int index, const std::optional param) { if (param) { - LOG("%d", param.value()); + LOG_DEBUG("%d", param.value()); stmt.bind(index, static_cast(param.value())); } else { - LOG("Null"); + LOG_DEBUG("Null"); stmt.bindNull(index); } } diff --git a/dDatabase/GameDatabase/SQLite/Tables/Mail.cpp b/dDatabase/GameDatabase/SQLite/Tables/Mail.cpp index 7b0a4860..a101beaa 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Mail.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Mail.cpp @@ -47,7 +47,7 @@ std::vector SQLiteDatabase::GetMailForPlayer(const LWOOBJID characterI } std::optional SQLiteDatabase::GetMail(const uint64_t mailId) { - auto [_, res] = ExecuteSelect("SELECT attachment_lot, attachment_count FROM mail WHERE id=? LIMIT 1;", mailId); + auto [_, res] = ExecuteSelect("SELECT attachment_lot, attachment_count, receiver_id FROM mail WHERE id=? LIMIT 1;", mailId); if (res.eof()) { return std::nullopt; @@ -56,6 +56,7 @@ std::optional SQLiteDatabase::GetMail(const uint64_t mailId) { MailInfo toReturn; toReturn.itemLOT = res.getIntField("attachment_lot"); toReturn.itemCount = res.getIntField("attachment_count"); + toReturn.receiverId = res.getInt64Field("receiver_id"); return toReturn; } diff --git a/dDatabase/GameDatabase/SQLite/Tables/Property.cpp b/dDatabase/GameDatabase/SQLite/Tables/Property.cpp index 67fc57b3..caff8441 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Property.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Property.cpp @@ -18,8 +18,8 @@ IProperty::Info ReadPropertyInfo(CppSQLite3Query& propertyEntry) { return toReturn; } -std::optional SQLiteDatabase::GetProperties(const IProperty::PropertyLookup& params) { - std::optional result; +IProperty::PropertyEntranceResult SQLiteDatabase::GetProperties(const IProperty::PropertyLookup& params) { + IProperty::PropertyEntranceResult result; std::string query; std::pair propertiesRes; @@ -73,8 +73,7 @@ std::optional SQLiteDatabase::GetProperties(c params.playerId ); if (!count.eof()) { - result = IProperty::PropertyEntranceResult(); - result->totalEntriesMatchingQuery = count.getIntField("count"); + result.totalEntriesMatchingQuery = count.getIntField("count"); } } else { if (params.sortChoice == SORT_TYPE_REPUTATION) { @@ -127,15 +126,13 @@ std::optional SQLiteDatabase::GetProperties(c params.playerSort ); if (!count.eof()) { - result = IProperty::PropertyEntranceResult(); - result->totalEntriesMatchingQuery = count.getIntField("count"); + result.totalEntriesMatchingQuery = count.getIntField("count"); } } auto& [_, properties] = propertiesRes; - if (!properties.eof() && !result.has_value()) result = IProperty::PropertyEntranceResult(); while (!properties.eof()) { - result->entries.push_back(ReadPropertyInfo(properties)); + result.entries.push_back(ReadPropertyInfo(properties)); properties.nextRow(); } diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index 2c7890dd..b8706c49 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -90,7 +90,7 @@ class TestSQLDatabase : public GameDatabase { std::string GetBehavior(const LWOOBJID behaviorId) override; void RemoveBehavior(const LWOOBJID behaviorId) override; void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override; - std::optional GetProperties(const IProperty::PropertyLookup& params) override { return {}; }; + IProperty::PropertyEntranceResult GetProperties(const IProperty::PropertyLookup& params) override { return {}; }; std::vector GetDescendingLeaderboard(const uint32_t activityId) override { return {}; }; std::vector GetAscendingLeaderboard(const uint32_t activityId) override { return {}; }; std::vector GetNsLeaderboard(const uint32_t activityId) override { return {}; }; diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 7d8046e0..069d984a 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -189,15 +189,21 @@ Entity::~Entity() { } if (m_ParentEntity) { + GameMessages::ChildRemoved removedMsg{}; + removedMsg.childID = m_ObjectID; + removedMsg.target = m_ParentEntity->GetObjectID(); + removedMsg.Send(); m_ParentEntity->RemoveChild(this); } } void Entity::Initialize() { - RegisterMsg(this, &Entity::MsgRequestServerObjectInfo); - RegisterMsg(this, &Entity::MsgDropClientLoot); - RegisterMsg(this, &Entity::MsgGetFactionTokenType); - RegisterMsg(this, &Entity::MsgPickupItem); + RegisterMsg(&Entity::MsgRequestServerObjectInfo); + RegisterMsg(&Entity::MsgDropClientLoot); + RegisterMsg(&Entity::MsgGetFactionTokenType); + RegisterMsg(&Entity::MsgPickupItem); + RegisterMsg(&Entity::MsgChildRemoved); + RegisterMsg(&Entity::MsgGetFlag); /** * Setup trigger */ @@ -310,15 +316,16 @@ void Entity::Initialize() { controllablePhysics->LoadFromXml(m_Character->GetXMLDoc()); const auto mapID = Game::server->GetZoneID(); + const auto& targetSceneName = m_Character->GetTargetScene(); //If we came from another zone, put us in the starting loc - if (m_Character->GetZoneID() != Game::server->GetZoneID() || mapID == 1603) { // Exception for Moon Base as you tend to spawn on the roof. + // Exception for Moon Base as you tend to spawn on the roof. + // second exception if we have a specified targetScene since that would only be possible in a test map + if (m_Character->GetZoneID() != Game::server->GetZoneID() || mapID == 1603 || !targetSceneName.empty()) { NiPoint3 pos; NiQuaternion rot = QuatUtils::IDENTITY; - const auto& targetSceneName = m_Character->GetTargetScene(); auto* targetScene = Game::entityManager->GetSpawnPointEntity(targetSceneName); - if (m_Character->HasBeenToWorld(mapID) && targetSceneName.empty()) { pos = m_Character->GetRespawnPoint(mapID); rot = Game::zoneManager->GetZone()->GetSpawnRot(); @@ -421,7 +428,7 @@ void Entity::Initialize() { comp->SetMaxArmor(destCompData[0].armor); comp->SetDeathBehavior(destCompData[0].death_behavior); - comp->SetIsSmashable(destCompData[0].isSmashable); + comp->SetIsSmashable(comp->GetIsSmashable() || destCompData[0].isSmashable); comp->SetLootMatrixID(destCompData[0].LootMatrixIndex); comp->SetCurrencyIndex(destCompData[0].CurrencyIndex); @@ -499,7 +506,7 @@ void Entity::Initialize() { auto& systemAddress = m_Character->GetParentUser() ? m_Character->GetParentUser()->GetSystemAddress() : UNASSIGNED_SYSTEM_ADDRESS; AddComponent(characterID, m_Character, systemAddress)->LoadFromXml(m_Character->GetXMLDoc()); - AddComponent(characterID); + AddComponent(characterID)->LoadFromXml(m_Character->GetXMLDoc()); } const auto inventoryID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::INVENTORY); @@ -606,14 +613,15 @@ void Entity::Initialize() { if (rebuildResetTime != 0.0f) { quickBuildComponent->SetResetTime(rebuildResetTime); - - // Known bug with moving platform in FV that casues it to build at the end instead of the start. - // This extends the smash time so players can ride up the lift. - if (m_TemplateID == 9483) { - quickBuildComponent->SetResetTime(quickBuildComponent->GetResetTime() + 25); - } } + const auto objectID = GetObjectID(); + // FV tree handler for when built so it sets the state to moving at the correct time + if (GetLOT() == 9483) quickBuildComponent->AddQuickBuildCompleteCallback([objectID](Entity* user) { + auto* const entity = Game::entityManager->GetEntity(objectID); + if (entity) GameMessages::SendPlatformResync(entity, UNASSIGNED_SYSTEM_ADDRESS, false, 0, 1, 1, eMovementPlatformState::Moving, true); + }); + const auto activityID = GetVar(u"activityID"); if (activityID > 0) { @@ -943,13 +951,13 @@ void Entity::SetGMLevel(eGameMasterLevel value) { } } -void Entity::WriteLDFData(const std::vector& ldf, RakNet::BitStream& outBitStream) const { +void Entity::WriteLDFData(const LwoNameValue& ldf, RakNet::BitStream& outBitStream) const { RakNet::BitStream settingStream; - int32_t numberOfValidKeys = ldf.size(); + int32_t numberOfValidKeys = ldf.values.size(); // Writing keys value pairs the client does not expect to receive or interpret will result in undefined behavior, // so we need to filter out any keys that are not valid and fix the number of valid keys to be correct. - for (LDFBaseData* data : ldf) { + for (const auto& data : ldf.values | std::views::values) { if (data && data->GetValueType() != eLDFType::LDF_TYPE_UNKNOWN) { data->WriteToPacket(settingStream); } else { @@ -981,16 +989,16 @@ void Entity::WriteBaseReplicaData(RakNet::BitStream& outBitStream, eReplicaPacke const auto& syncLDF = GetVar>(u"syncLDF"); // Only sync for models. - if (!m_Settings.empty() && (GetComponent() && !GetComponent())) { + if (!m_Settings.values.empty() && (GetComponent() && !GetComponent())) { outBitStream.Write1(); // Has ldf data WriteLDFData(m_Settings, outBitStream); } else if (!syncLDF.empty()) { // Find all the ldf data we need to write - std::vector ldfData; - ldfData.reserve(m_Settings.size()); + LwoNameValue ldfData; for (const auto& data : syncLDF) { - ldfData.push_back(GetVarData(data)); + const auto* toInsert = GetVarData(data); + if (toInsert) ldfData.values.insert_or_assign(data, toInsert->Copy()); } outBitStream.Write1(); // Has ldf data @@ -1607,26 +1615,10 @@ void Entity::Kill(Entity* murderer, const eKillType killType) { else Game::entityManager->DestroyEntity(this); } - const auto& grpNameQBShowBricks = GetVar(u"grpNameQBShowBricks"); - + const auto& grpNameQBShowBricks = GetVarAsString(u"grpNameQBShowBricks"); if (!grpNameQBShowBricks.empty()) { - auto spawners = Game::zoneManager->GetSpawnersByName(grpNameQBShowBricks); - - Spawner* spawner = nullptr; - - if (!spawners.empty()) { - spawner = spawners[0]; - } else { - spawners = Game::zoneManager->GetSpawnersInGroup(grpNameQBShowBricks); - - if (!spawners.empty()) { - spawner = spawners[0]; - } - } - - if (spawner != nullptr) { - spawner->Spawn(); - } + for (auto* const spawner : Game::zoneManager->GetSpawnersByName(grpNameQBShowBricks)) if (spawner) spawner->Spawn(); + for (auto* const spawner : Game::zoneManager->GetSpawnersInGroup(grpNameQBShowBricks)) if (spawner) spawner->Spawn(); } // Track a player being smashed @@ -2040,13 +2032,7 @@ void Entity::SetI64(const std::u16string& name, const int64_t value) { } bool Entity::HasVar(const std::u16string& name) const { - for (auto* data : m_Settings) { - if (data->GetKey() == name) { - return true; - } - } - - return false; + return m_Settings.values.contains(name); } uint16_t Entity::GetNetworkId() const { @@ -2078,24 +2064,13 @@ void Entity::SendNetworkVar(const std::string& data, const SystemAddress& sysAdd GameMessages::SendSetNetworkScriptVar(this, sysAddr, data); } -LDFBaseData* Entity::GetVarData(const std::u16string& name) const { - for (auto* data : m_Settings) { - if (data == nullptr) { - continue; - } - - if (data->GetKey() != name) { - continue; - } - - return data; - } - - return nullptr; +const LDFBaseData* const Entity::GetVarData(const std::u16string& name) const { + const auto itr = m_Settings.values.find(name); + return itr != m_Settings.values.cend() ? itr->second.get() : nullptr; } std::string Entity::GetVarAsString(const std::u16string& name) const { - auto* data = GetVarData(name); + const auto* const data = GetVarData(name); return data ? data->GetValueAsString() : ""; } @@ -2246,8 +2221,7 @@ void Entity::RegisterMsg(const MessageType::Game msgId, std::function(msg); +bool Entity::MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfo& requestInfo) { AMFArrayValue response; response.Insert("visible", true); response.Insert("objectID", std::to_string(m_ObjectID)); @@ -2265,6 +2239,7 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) { objectInfo.PushDebug("Template ID(LOT)") = GetLOT(); objectInfo.PushDebug("Object ID") = std::to_string(GetObjectID()); objectInfo.PushDebug("Spawner's Object ID") = std::to_string(GetSpawnerID()); + objectInfo.PushDebug("Owner override") = std::to_string(m_OwnerOverride); auto& componentDetails = objectInfo.PushDebug("Component Information"); for (const auto [id, component] : m_Components) { @@ -2272,7 +2247,7 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) { } auto& configData = objectInfo.PushDebug("Config Data"); - for (const auto config : m_Settings) { + for (const auto& config : m_Settings.values | std::views::values) { configData.PushDebug(GeneralUtils::UTF16ToWTF8(config->GetKey())) = config->GetValueAsString(); } @@ -2283,9 +2258,7 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) { return true; } -bool Entity::MsgDropClientLoot(GameMessages::GameMsg& msg) { - auto& dropLootMsg = static_cast(msg); - +bool Entity::MsgDropClientLoot(GameMessages::DropClientLoot& dropLootMsg) { if (dropLootMsg.item != LOT_NULL && dropLootMsg.item != 0) { Loot::Info info{ .id = dropLootMsg.lootID, @@ -2302,13 +2275,11 @@ bool Entity::MsgDropClientLoot(GameMessages::GameMsg& msg) { return true; } -bool Entity::MsgGetFlag(GameMessages::GameMsg& msg) { - auto& flagMsg = static_cast(msg); +bool Entity::MsgGetFlag(GameMessages::GetFlag& flagMsg) { if (m_Character) flagMsg.flag = m_Character->GetPlayerFlag(flagMsg.flagID); return true; } -bool Entity::MsgGetFactionTokenType(GameMessages::GameMsg& msg) { - auto& tokenMsg = static_cast(msg); +bool Entity::MsgGetFactionTokenType(GameMessages::GetFactionTokenType& tokenMsg) { GameMessages::GetFlag getFlagMsg{}; getFlagMsg.flagID = ePlayerFlag::ASSEMBLY_FACTION; @@ -2331,8 +2302,7 @@ bool Entity::MsgGetFactionTokenType(GameMessages::GameMsg& msg) { return tokenMsg.tokenType != LOT_NULL; } -bool Entity::MsgPickupItem(GameMessages::GameMsg& msg) { - auto& pickupItemMsg = static_cast(msg); +bool Entity::MsgPickupItem(GameMessages::PickupItem& pickupItemMsg) { if (GetObjectID() == pickupItemMsg.lootOwnerID) { PickupItem(pickupItemMsg.lootID); } else { @@ -2352,3 +2322,8 @@ bool Entity::MsgPickupItem(GameMessages::GameMsg& msg) { return true; } + +bool Entity::MsgChildRemoved(GameMessages::ChildRemoved& msg) { + GetScript()->OnChildRemoved(*this, msg); + return true; +} diff --git a/dGame/Entity.h b/dGame/Entity.h index 6d50efa7..a8781d42 100644 --- a/dGame/Entity.h +++ b/dGame/Entity.h @@ -20,6 +20,12 @@ namespace GameMessages { struct ShootingGalleryFire; struct ChildLoaded; struct PlayerResurrectionFinished; + struct RequestServerObjectInfo; + struct DropClientLoot; + struct GetFlag; + struct GetFactionTokenType; + struct PickupItem; + struct ChildRemoved; }; namespace MessageType { @@ -91,9 +97,9 @@ public: LWOOBJID GetSpawnerID() const { return m_SpawnerID; } - const std::vector& GetSettings() const { return m_Settings; } + const LwoNameValue& GetSettings() const { return m_Settings; } - const std::vector& GetNetworkSettings() const { return m_NetworkSettings; } + const LwoNameValue& GetNetworkSettings() const { return m_NetworkSettings; } bool GetIsDead() const; @@ -106,6 +112,7 @@ public: uint16_t GetNetworkId() const; + // Cannot return nullptr. Entity* GetOwner() const; const NiPoint3& GetDefaultPosition() const; @@ -175,11 +182,12 @@ public: void AddComponent(eReplicaComponentType componentId, Component* component); - bool MsgRequestServerObjectInfo(GameMessages::GameMsg& msg); - bool MsgDropClientLoot(GameMessages::GameMsg& msg); - bool MsgGetFlag(GameMessages::GameMsg& msg); - bool MsgGetFactionTokenType(GameMessages::GameMsg& msg); - bool MsgPickupItem(GameMessages::GameMsg& msg); + bool MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfo& msg); + bool MsgDropClientLoot(GameMessages::DropClientLoot& msg); + bool MsgGetFlag(GameMessages::GetFlag& msg); + bool MsgGetFactionTokenType(GameMessages::GetFactionTokenType& msg); + bool MsgPickupItem(GameMessages::PickupItem& msg); + bool MsgChildRemoved(GameMessages::ChildRemoved& msg); // This is expceted to never return nullptr, an assert checks this. CppScripts::Script* const GetScript() const; @@ -304,6 +312,12 @@ public: template void SetNetworkVar(const std::u16string& name, std::vector value, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS); + template + void SetNetworkVar(const std::string& name, T value, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS); + + template + LwoNameValue::ValueType::iterator InsertNetworkVar(const std::u16string& name, T value); + template T GetNetworkVar(const std::u16string& name); @@ -316,11 +330,6 @@ public: template ComponentType* AddComponent(VaArgs... args); - /** - * Get the LDF data. - */ - LDFBaseData* GetVarData(const std::u16string& name) const; - /** * Get the LDF value and convert it to a string. */ @@ -342,14 +351,19 @@ public: bool HandleMsg(GameMessages::GameMsg& msg) const; - void RegisterMsg(const MessageType::Game msgId, auto* self, const auto handler) { - RegisterMsg(msgId, std::bind(handler, self, std::placeholders::_1)); - } - - template - inline void RegisterMsg(auto* self, const auto handler) { - T msg; - RegisterMsg(msg.msgId, self, handler); + // Provided a function that has a derived GameMessage as its only argument and returns a boolean, + // this will register it as a handler for that message type. Casting is done automatically to the type + // of the message in the first argument. This object is expected to exist as long as the handler can be called. + template + inline void RegisterMsg(bool (Entity::* handler)(DerivedGameMsg&)) { + static_assert(std::is_base_of_v, "DerivedGameMsg must inherit from GameMsg"); + const auto boundFunction = std::bind(handler, this, std::placeholders::_1); + // This is the actual function that will be registered, which casts the base GameMsg to the derived type + const auto castWrapper = [boundFunction](GameMessages::GameMsg& msg) { + return boundFunction(static_cast(msg)); + }; + DerivedGameMsg msg; + RegisterMsg(msg.msgId, castWrapper); } /** @@ -358,13 +372,20 @@ public: static Observable OnPlayerPositionUpdate; private: - void WriteLDFData(const std::vector& ldf, RakNet::BitStream& outBitStream) const; + + /** + * Get the LDF data. + */ + const LDFBaseData* const GetVarData(const std::u16string& name) const; + template + LwoNameValue::ValueType::iterator InsertLnvData(LwoNameValue& lnv, const std::u16string& key, T value); + void WriteLDFData(const LwoNameValue& ldf, RakNet::BitStream& outBitStream) const; LWOOBJID m_ObjectID; LOT m_TemplateID; - std::vector m_Settings; - std::vector m_NetworkSettings; + LwoNameValue m_Settings; + LwoNameValue m_NetworkSettings; NiPoint3 m_DefaultPosition; NiQuaternion m_DefaultRotation = QuatUtils::IDENTITY; @@ -446,13 +467,13 @@ T* Entity::GetComponent() const { template const T& Entity::GetVar(const std::u16string& name) const { - auto* data = GetVarData(name); + const auto* const data = GetVarData(name); if (data == nullptr) { return LDFData::Default; } - auto* typed = dynamic_cast*>(data); + auto* typed = dynamic_cast* const>(data); if (typed == nullptr) { return LDFData::Default; @@ -470,52 +491,44 @@ T Entity::GetVarAs(const std::u16string& name) const { template void Entity::SetVar(const std::u16string& name, T value) { - auto* data = GetVarData(name); + InsertLnvData(m_Settings, name, value); +} - if (data == nullptr) { - auto* data = new LDFData(name, value); - - m_Settings.push_back(data); - - return; +template +LwoNameValue::ValueType::iterator Entity::InsertLnvData(LwoNameValue& lnv, const std::u16string& key, T value) { + auto itr = lnv.values.find(key); + if (itr != lnv.values.end()) { + auto* lnvCast = dynamic_cast*>(itr->second.get()); + if (!lnvCast) { + // Is of different type + itr->second = std::make_unique>(key, value); + } else { + // Is the same type and exists + lnvCast->SetValue(value); + } + } else { + // Doesn't exist + itr = lnv.values.insert_or_assign(key, std::make_unique>(key, value)).first; } - auto* typed = dynamic_cast*>(data); + return itr; +} - if (typed == nullptr) { - return; - } - - typed->SetValue(value); +template +LwoNameValue::ValueType::iterator Entity::InsertNetworkVar(const std::u16string& name, T value) { + return InsertLnvData(m_NetworkSettings, name, value); } template void Entity::SetNetworkVar(const std::u16string& name, T value, const SystemAddress& sysAddr) { - LDFData* newData = nullptr; + const auto itr = InsertNetworkVar(name, value); - for (auto* data : m_NetworkSettings) { - if (data->GetKey() != name) - continue; + SendNetworkVar(itr->second->GetString(), sysAddr); +} - newData = dynamic_cast*>(data); - if (newData != nullptr) { - newData->SetValue(value); - } else { // If we're changing types - m_NetworkSettings.erase( - std::remove(m_NetworkSettings.begin(), m_NetworkSettings.end(), data), m_NetworkSettings.end() - ); - delete data; - } - - break; - } - - if (newData == nullptr) { - newData = new LDFData(name, value); - } - - m_NetworkSettings.push_back(newData); - SendNetworkVar(newData->GetString(true), sysAddr); +template +void Entity::SetNetworkVar(const std::string& name, T value, const SystemAddress& sysAddr) { + SetNetworkVar(GeneralUtils::UTF8ToUTF16(name), value, sysAddr); } template @@ -524,28 +537,11 @@ void Entity::SetNetworkVar(const std::u16string& name, std::vector values, co auto index = 1; for (const auto& value : values) { - LDFData* newData = nullptr; const auto& indexedName = name + u"." + GeneralUtils::to_u16string(index); - - for (auto* data : m_NetworkSettings) { - if (data->GetKey() != indexedName) - continue; - - newData = dynamic_cast*>(data); - newData->SetValue(value); - break; - } - - if (newData == nullptr) { - newData = new LDFData(indexedName, value); - } - - m_NetworkSettings.push_back(newData); - - if (index == values.size()) { - updates << newData->GetString(true); - } else { - updates << newData->GetString(true) << "\n"; + const auto itr = InsertNetworkVar(indexedName, value); + updates << itr->second->GetString(); + if (index != values.size()) { + updates << "\n"; } index++; @@ -556,18 +552,15 @@ void Entity::SetNetworkVar(const std::u16string& name, std::vector values, co template T Entity::GetNetworkVar(const std::u16string& name) { - for (auto* data : m_NetworkSettings) { - if (data == nullptr || data->GetKey() != name) - continue; + T toReturn = LDFData::Default; - auto* typed = dynamic_cast*>(data); - if (typed == nullptr) - continue; - - return typed->GetValue(); + const auto itr = m_NetworkSettings.values.find(name); + if (itr != m_NetworkSettings.values.cend()) { + auto* cast = dynamic_cast*>(itr->second.get()); + if (cast) toReturn = cast->GetValue(); } - return LDFData::Default; + return toReturn; } /** diff --git a/dGame/EntityManager.cpp b/dGame/EntityManager.cpp index 12be2e06..fee2b66a 100644 --- a/dGame/EntityManager.cpp +++ b/dGame/EntityManager.cpp @@ -10,7 +10,6 @@ #include "SkillComponent.h" #include "SwitchComponent.h" #include "UserManager.h" -#include "Metrics.hpp" #include "dZoneManager.h" #include "MissionComponent.h" #include "Game.h" @@ -361,16 +360,24 @@ void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr LOG("Attempted to construct null entity"); return; } + // Don't construct GM invisible entities unless it's for the GM themselves + // GMs can see other GMs if they are the same or lower level + GameMessages::GetGMInvis getGMInvisMsg; + getGMInvisMsg.Send(entity->GetObjectID()); + if (getGMInvisMsg.bGMInvis && sysAddr != entity->GetSystemAddress()) { + auto* toUser = UserManager::Instance()->GetUser(sysAddr); + if (!toUser) return; + auto* constructedUser = UserManager::Instance()->GetUser(entity->GetSystemAddress()); + if (!constructedUser) return; + if (toUser->GetMaxGMLevel() < constructedUser->GetMaxGMLevel()) return; + } if (entity->GetNetworkId() == 0) { uint16_t networkId; - if (!m_LostNetworkIds.empty()) { networkId = m_LostNetworkIds.top(); m_LostNetworkIds.pop(); - } else { - networkId = ++m_NetworkIdCounter; - } + } else networkId = ++m_NetworkIdCounter; entity->SetNetworkId(networkId); } @@ -379,10 +386,8 @@ void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr if (std::find(m_EntitiesToGhost.begin(), m_EntitiesToGhost.end(), entity) == m_EntitiesToGhost.end()) { m_EntitiesToGhost.push_back(entity); } - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) { CheckGhosting(entity); - return; } } @@ -413,14 +418,9 @@ void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr Game::server->Send(stream, sysAddr, false); } - if (entity->IsPlayer()) { - if (entity->GetGMLevel() > eGameMasterLevel::CIVILIAN) { - GameMessages::SendToggleGMInvis(entity->GetObjectID(), true, sysAddr); - } - } } -void EntityManager::ConstructAllEntities(const SystemAddress& sysAddr) { +void EntityManager::ConstructAllEntities(const SystemAddress& sysAddr) { //ZoneControl is special: ConstructEntity(m_ZoneControlEntity, sysAddr); @@ -488,11 +488,7 @@ void EntityManager::QueueGhostUpdate(LWOOBJID playerID) { void EntityManager::UpdateGhosting() { for (const auto playerID : m_PlayersToUpdateGhosting) { auto* player = PlayerManager::GetPlayer(playerID); - - if (player == nullptr) { - continue; - } - + if (!player) continue; UpdateGhosting(player); } @@ -519,6 +515,7 @@ void EntityManager::UpdateGhosting(Entity* player) { const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint); + auto ghostingDistanceMax = m_GhostDistanceMaxSquared; auto ghostingDistanceMin = m_GhostDistanceMinSqaured; @@ -555,35 +552,25 @@ void EntityManager::UpdateGhosting(Entity* player) { } void EntityManager::CheckGhosting(Entity* entity) { - if (entity == nullptr) { - return; - } + if (!entity) return; const auto& referencePoint = entity->GetPosition(); - for (auto* player : PlayerManager::GetAllPlayers()) { auto* ghostComponent = player->GetComponent(); if (!ghostComponent) continue; const auto& entityPoint = ghostComponent->GetGhostReferencePoint(); - const auto id = entity->GetObjectID(); - const auto observed = ghostComponent->IsObserved(id); - const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint); if (observed && distance > m_GhostDistanceMaxSquared) { ghostComponent->GhostEntity(id); - DestructEntity(entity, player->GetSystemAddress()); - entity->SetObservers(entity->GetObservers() - 1); } else if (!observed && m_GhostDistanceMinSqaured > distance) { ghostComponent->ObserveEntity(id); - ConstructEntity(entity, player->GetSystemAddress()); - entity->SetObservers(entity->GetObservers() + 1); } } diff --git a/dGame/LeaderboardManager.cpp b/dGame/LeaderboardManager.cpp index 488bc2b0..a7ca59c1 100644 --- a/dGame/LeaderboardManager.cpp +++ b/dGame/LeaderboardManager.cpp @@ -18,7 +18,8 @@ #include "DluAssert.h" #include "CDActivitiesTable.h" -#include "Metrics.hpp" + +#include namespace LeaderboardManager { std::map leaderboardCache; @@ -38,10 +39,10 @@ Leaderboard::~Leaderboard() { } void Leaderboard::Clear() { - for (auto& entry : entries) for (auto ldfData : entry) delete ldfData; + entries.clear(); } -inline void WriteLeaderboardRow(std::ostringstream& leaderboard, const uint32_t& index, LDFBaseData* data) { +inline void WriteLeaderboardRow(std::ostringstream& leaderboard, const uint32_t& index, const std::unique_ptr& data) { leaderboard << "\nResult[0].Row[" << index << "]." << data->GetString(); } @@ -58,8 +59,8 @@ void Leaderboard::Serialize(RakNet::BitStream& bitStream) const { int32_t rowNumber = 0; for (auto& entry : entries) { - for (auto* data : entry) { - WriteLeaderboardRow(leaderboard, rowNumber, data); + for (const auto& data : entry.values | std::views::values) { + if (data) WriteLeaderboardRow(leaderboard, rowNumber, data); } rowNumber++; } @@ -84,57 +85,56 @@ void QueryToLdf(Leaderboard& leaderboard, const std::vector for (const auto& leaderboardEntry : leaderboardEntries) { constexpr int32_t MAX_NUM_DATA_PER_ROW = 9; auto& entry = leaderboard.PushBackEntry(); - entry.reserve(MAX_NUM_DATA_PER_ROW); - entry.push_back(new LDFData(u"CharacterID", leaderboardEntry.charId)); - entry.push_back(new LDFData(u"LastPlayed", leaderboardEntry.lastPlayedTimestamp)); - entry.push_back(new LDFData(u"NumPlayed", leaderboardEntry.numTimesPlayed)); - entry.push_back(new LDFData(u"name", GeneralUtils::ASCIIToUTF16(leaderboardEntry.name))); - entry.push_back(new LDFData(u"RowNumber", leaderboardEntry.ranking)); + entry.Insert(u"CharacterID", leaderboardEntry.charId); + entry.Insert(u"LastPlayed", leaderboardEntry.lastPlayedTimestamp); + entry.Insert(u"NumPlayed", leaderboardEntry.numTimesPlayed); + entry.Insert(u"name", GeneralUtils::ASCIIToUTF16(leaderboardEntry.name)); + entry.Insert(u"RowNumber", leaderboardEntry.ranking); switch (leaderboard.GetLeaderboardType()) { case ShootingGallery: - entry.push_back(new LDFData(u"Score", leaderboardEntry.primaryScore)); + entry.Insert(u"Score", leaderboardEntry.primaryScore); // Score:1 - entry.push_back(new LDFData(u"Streak", leaderboardEntry.secondaryScore)); + entry.Insert(u"Streak", leaderboardEntry.secondaryScore); // Streak:1 - entry.push_back(new LDFData(u"HitPercentage", leaderboardEntry.tertiaryScore)); + entry.Insert(u"HitPercentage", leaderboardEntry.tertiaryScore); // HitPercentage:3 between 0 and 1 break; case Racing: - entry.push_back(new LDFData(u"BestTime", leaderboardEntry.primaryScore)); + entry.Insert(u"BestTime", leaderboardEntry.primaryScore); // BestLapTime:3 - entry.push_back(new LDFData(u"BestLapTime", leaderboardEntry.secondaryScore)); + entry.Insert(u"BestLapTime", leaderboardEntry.secondaryScore); // BestTime:3 - entry.push_back(new LDFData(u"License", 1)); + entry.Insert(u"License", 1); // License:1 - 1 if player has completed mission 637 and 0 otherwise - entry.push_back(new LDFData(u"NumWins", leaderboardEntry.numWins)); + entry.Insert(u"NumWins", leaderboardEntry.numWins); // NumWins:1 break; case UnusedLeaderboard4: - entry.push_back(new LDFData(u"Points", leaderboardEntry.primaryScore)); + entry.Insert(u"Points", leaderboardEntry.primaryScore); // Points:1 break; case MonumentRace: - entry.push_back(new LDFData(u"Time", leaderboardEntry.primaryScore)); + entry.Insert(u"Time", leaderboardEntry.primaryScore); // Time:1(?) break; case FootRace: - entry.push_back(new LDFData(u"Time", leaderboardEntry.primaryScore)); + entry.Insert(u"Time", leaderboardEntry.primaryScore); // Time:1 break; case Survival: - entry.push_back(new LDFData(u"Points", leaderboardEntry.primaryScore)); + entry.Insert(u"Points", leaderboardEntry.primaryScore); // Points:1 - entry.push_back(new LDFData(u"Time", leaderboardEntry.secondaryScore)); + entry.Insert(u"Time", leaderboardEntry.secondaryScore); // Time:1 break; case SurvivalNS: - entry.push_back(new LDFData(u"Wave", leaderboardEntry.primaryScore)); + entry.Insert(u"Wave", leaderboardEntry.primaryScore); // Wave:1 - entry.push_back(new LDFData(u"Time", leaderboardEntry.secondaryScore)); + entry.Insert(u"Time", leaderboardEntry.secondaryScore); // Time:1 break; case Donations: - entry.push_back(new LDFData(u"Score", leaderboardEntry.primaryScore)); + entry.Insert(u"Score", leaderboardEntry.primaryScore); // Score:1 break; case None: @@ -289,6 +289,10 @@ void LeaderboardManager::SaveScore(const LWOOBJID& playerID, const GameID activi ILeaderboard::Score oldScoreFlipped{oldScore->secondaryScore, oldScore->primaryScore, oldScore->tertiaryScore}; ILeaderboard::Score newScoreFlipped{newScore.secondaryScore, newScore.primaryScore, newScore.tertiaryScore}; newHighScore = newScoreFlipped > oldScoreFlipped; + } else if (leaderboardType == Leaderboard::Type::Donations) { + // Donations just need to go up if updated + newHighScore = true; + newScore.primaryScore += oldScore->primaryScore; } if (newHighScore) { diff --git a/dGame/LeaderboardManager.h b/dGame/LeaderboardManager.h index 760d282e..c99634c1 100644 --- a/dGame/LeaderboardManager.h +++ b/dGame/LeaderboardManager.h @@ -70,8 +70,7 @@ public: private: - using LeaderboardEntry = std::vector; - using LeaderboardEntries = std::vector; + using LeaderboardEntries = std::vector; LeaderboardEntries entries; LWOOBJID relatedPlayer; @@ -81,7 +80,7 @@ private: bool weekly; uint32_t numResults; public: - LeaderboardEntry& PushBackEntry() { + LwoNameValue& PushBackEntry() { return entries.emplace_back(); } diff --git a/dGame/TradingManager.cpp b/dGame/TradingManager.cpp index c7143354..43d1c98a 100644 --- a/dGame/TradingManager.cpp +++ b/dGame/TradingManager.cpp @@ -10,6 +10,11 @@ #include "CharacterComponent.h" #include "MissionComponent.h" #include "eMissionTaskType.h" +#include + +namespace { + std::unique_ptr g_EmptyTrade; +} TradingManager* TradingManager::m_Address = nullptr; @@ -233,55 +238,38 @@ void Trade::SendUpdateToOther(LWOOBJID participant) { GameMessages::SendServerTradeUpdate(other->GetObjectID(), coins, items, other->GetSystemAddress()); } -TradingManager::TradingManager() { -} - -TradingManager::~TradingManager() { - for (const auto& pair : trades) { - delete pair.second; - } - - trades.clear(); -} - -Trade* TradingManager::GetTrade(LWOOBJID tradeId) const { +const std::unique_ptr& TradingManager::GetTrade(LWOOBJID tradeId) const { const auto& pair = trades.find(tradeId); - if (pair == trades.end()) return nullptr; + if (pair == trades.end()) return g_EmptyTrade; return pair->second; } -Trade* TradingManager::GetPlayerTrade(LWOOBJID playerId) const { - for (const auto& pair : trades) { - if (pair.second->IsParticipant(playerId)) { - return pair.second; +const std::unique_ptr& TradingManager::GetPlayerTrade(LWOOBJID playerId) const { + for (const auto& trade : trades | std::views::values) { + if (trade->IsParticipant(playerId)) { + return trade; } } - return nullptr; + return g_EmptyTrade; } void TradingManager::CancelTrade(const LWOOBJID canceller, LWOOBJID tradeId, const bool sendCancelMessage) { - auto* trade = GetTrade(tradeId); + const auto& trade = GetTrade(tradeId); if (trade == nullptr) return; if (sendCancelMessage) trade->Cancel(canceller); - delete trade; - trades.erase(tradeId); } -Trade* TradingManager::NewTrade(LWOOBJID participantA, LWOOBJID participantB) { +void TradingManager::NewTrade(LWOOBJID participantA, LWOOBJID participantB) { const LWOOBJID tradeId = ObjectIDManager::GenerateObjectID(); - auto* trade = new Trade(tradeId, participantA, participantB); - - trades[tradeId] = trade; + trades.insert_or_assign(tradeId, std::make_unique(tradeId, participantA, participantB)); LOG("Created new trade between (%llu) <-> (%llu)", participantA, participantB); - - return trade; } diff --git a/dGame/TradingManager.h b/dGame/TradingManager.h index fa55aa9d..d9984a5b 100644 --- a/dGame/TradingManager.h +++ b/dGame/TradingManager.h @@ -2,15 +2,16 @@ #include "Entity.h" -struct TradeItem -{ +#include +#include + +struct TradeItem { LWOOBJID itemId; LOT itemLot; uint32_t itemCount; }; -class Trade -{ +class Trade { public: explicit Trade(LWOOBJID tradeId, LWOOBJID participantA, LWOOBJID participantB); ~Trade(); @@ -50,8 +51,7 @@ private: }; -class TradingManager -{ +class TradingManager { public: static TradingManager* Instance() { if (!m_Address) { @@ -61,16 +61,13 @@ public: return m_Address; } - explicit TradingManager(); - ~TradingManager(); - - Trade* GetTrade(LWOOBJID tradeId) const; - Trade* GetPlayerTrade(LWOOBJID playerId) const; + const std::unique_ptr& GetTrade(LWOOBJID tradeId) const; + const std::unique_ptr& GetPlayerTrade(LWOOBJID playerId) const; void CancelTrade(const LWOOBJID canceller, LWOOBJID tradeId, const bool sendCancelMessage = true); - Trade* NewTrade(LWOOBJID participantA, LWOOBJID participantB); + void NewTrade(LWOOBJID participantA, LWOOBJID participantB); private: static TradingManager* m_Address; //For singleton method - std::unordered_map trades; + std::unordered_map> trades; }; diff --git a/dGame/User.h b/dGame/User.h index 7fe8d335..2c9f374c 100644 --- a/dGame/User.h +++ b/dGame/User.h @@ -31,7 +31,7 @@ public: std::string& GetSessionKey() { return m_SessionKey; } SystemAddress& GetSystemAddress() { return m_SystemAddress; } - eGameMasterLevel GetMaxGMLevel() { return m_MaxGMLevel; } + eGameMasterLevel GetMaxGMLevel() const { return m_MaxGMLevel; } uint32_t GetLastCharID() { return m_LastCharID; } void SetLastCharID(uint32_t newCharID) { m_LastCharID = newCharID; } diff --git a/dGame/UserManager.cpp b/dGame/UserManager.cpp index 15fbc2d6..87bbfc90 100644 --- a/dGame/UserManager.cpp +++ b/dGame/UserManager.cpp @@ -514,7 +514,7 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet) return; } - if (!Database::Get()->GetCharacterInfo(newName)) { + if (!Database::Get()->IsNameInUse(newName)) { if (autoRejectNames) { Database::Get()->SetCharacterName(objectID, newName); LOG("Character %s auto-renamed to preapproved name %s due to mute", character->GetName().c_str(), newName.c_str()); diff --git a/dGame/dBehaviors/AirMovementBehavior.cpp b/dGame/dBehaviors/AirMovementBehavior.cpp index 46d18680..ad8fe541 100644 --- a/dGame/dBehaviors/AirMovementBehavior.cpp +++ b/dGame/dBehaviors/AirMovementBehavior.cpp @@ -30,6 +30,21 @@ void AirMovementBehavior::Sync(BehaviorContext* context, RakNet::BitStream& bitS return; } + // So a player can't send an arbitrary behaviorID in a modified client and cast any behavior on any air behavior + Behavior* toSync = nullptr; + if (m_GroundAction->GetBehaviorID() == behaviorId) { + toSync = m_GroundAction; + } else if (m_HitAction->GetBehaviorID() == behaviorId) { + toSync = m_HitAction; + } else if (m_HitActionEnemy->GetBehaviorID() == behaviorId) { + toSync = m_HitActionEnemy; + } else if (m_TimeoutAction->GetBehaviorID() == behaviorId) { + toSync = m_TimeoutAction; + } else { + LOG("Invalid Air Movement Behavior sync for behaviorID %i on behavior %i", behaviorId, m_behaviorId); + return; + } + LWOOBJID target{}; if (!bitStream.Read(target)) { @@ -37,15 +52,17 @@ void AirMovementBehavior::Sync(BehaviorContext* context, RakNet::BitStream& bitS return; } - auto* behavior = CreateBehavior(behaviorId); - if (Game::entityManager->GetEntity(target) != nullptr) { branch.target = target; } - behavior->Handle(context, bitStream, branch); + toSync->Handle(context, bitStream, branch); } void AirMovementBehavior::Load() { - this->m_Timeout = (GetFloat("timeout_ms") / 1000.0f); + m_Timeout = (GetFloat("timeout_ms") / 1000.0f); + m_GroundAction = GetAction("ground_action"); + m_HitAction = GetAction("hit_action"); + m_HitActionEnemy = GetAction("hit_action_enemy"); + m_TimeoutAction = GetAction("timeout_action"); } diff --git a/dGame/dBehaviors/AirMovementBehavior.h b/dGame/dBehaviors/AirMovementBehavior.h index 0edb1a76..cfa2dbf2 100644 --- a/dGame/dBehaviors/AirMovementBehavior.h +++ b/dGame/dBehaviors/AirMovementBehavior.h @@ -15,4 +15,9 @@ public: void Load() override; private: float m_Timeout; + + Behavior* m_GroundAction{}; + Behavior* m_HitAction{}; + Behavior* m_HitActionEnemy{}; + Behavior* m_TimeoutAction{}; }; diff --git a/dGame/dBehaviors/AndBehavior.cpp b/dGame/dBehaviors/AndBehavior.cpp index ad986e37..c9d6e06e 100644 --- a/dGame/dBehaviors/AndBehavior.cpp +++ b/dGame/dBehaviors/AndBehavior.cpp @@ -10,7 +10,9 @@ void AndBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bitStream, } void AndBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitStream, const BehaviorBranchContext branch) { + LOG_ENTRY; for (auto* behavior : this->m_behaviors) { + LOG("%i calculating %i", m_behaviorId, behavior->GetBehaviorID()); behavior->Calculate(context, bitStream, branch); } } diff --git a/dGame/dBehaviors/AreaOfEffectBehavior.cpp b/dGame/dBehaviors/AreaOfEffectBehavior.cpp index ce41785f..6f1b76fd 100644 --- a/dGame/dBehaviors/AreaOfEffectBehavior.cpp +++ b/dGame/dBehaviors/AreaOfEffectBehavior.cpp @@ -42,6 +42,7 @@ void AreaOfEffectBehavior::Handle(BehaviorContext* context, RakNet::BitStream& b LWOOBJID target{}; if (!bitStream.Read(target)) { LOG("failed to read in target %i from bitStream, aborting target Handle!", i); + continue; }; targets.push_back(target); } diff --git a/dGame/dBehaviors/BasicAttackBehavior.cpp b/dGame/dBehaviors/BasicAttackBehavior.cpp index 41b02e7c..e265a034 100644 --- a/dGame/dBehaviors/BasicAttackBehavior.cpp +++ b/dGame/dBehaviors/BasicAttackBehavior.cpp @@ -68,7 +68,7 @@ void BasicAttackBehavior::DoHandleBehavior(BehaviorContext* context, RakNet::Bit } if (isBlocked) { - destroyableComponent->SetAttacksToBlock(std::min(destroyableComponent->GetAttacksToBlock() - 1, 0U)); + destroyableComponent->SetAttacksToBlock(std::max(static_cast(destroyableComponent->GetAttacksToBlock() - 1), 0)); Game::entityManager->SerializeEntity(targetEntity); this->m_OnFailBlocked->Handle(context, bitStream, branch); return; @@ -103,9 +103,10 @@ void BasicAttackBehavior::DoHandleBehavior(BehaviorContext* context, RakNet::Bit return; } - uint32_t totalDamageDealt = armorDamageDealt + healthDamageDealt; + uint64_t totalDamageDealt = armorDamageDealt + healthDamageDealt; // A value that's too large may be a cheating attempt, so we set it to MIN + // Can't overflow here either because should we somehow get to a 64 bit number it'll be clamped to a sane value. if (totalDamageDealt > this->m_MaxDamage) { totalDamageDealt = this->m_MinDamage; } diff --git a/dGame/dBehaviors/Behavior.h b/dGame/dBehaviors/Behavior.h index f9867ffe..49d664c7 100644 --- a/dGame/dBehaviors/Behavior.h +++ b/dGame/dBehaviors/Behavior.h @@ -95,4 +95,6 @@ public: Behavior& operator=(const Behavior& other) = default; Behavior& operator=(Behavior&& other) = default; + + uint32_t GetBehaviorID() const { return m_behaviorId; } }; diff --git a/dGame/dBehaviors/BlockBehavior.cpp b/dGame/dBehaviors/BlockBehavior.cpp index 0d3e164b..8a7c8a64 100644 --- a/dGame/dBehaviors/BlockBehavior.cpp +++ b/dGame/dBehaviors/BlockBehavior.cpp @@ -48,15 +48,13 @@ void BlockBehavior::UnCast(BehaviorContext* context, BehaviorBranchContext branc return; } - auto* destroyableComponent = entity->GetComponent(); + auto* const destroyableComponent = entity->GetComponent(); - destroyableComponent->SetAttacksToBlock(this->m_numAttacksCanBlock); - - if (destroyableComponent == nullptr) { - return; + if (destroyableComponent) { + // ??? what is going on here? + destroyableComponent->SetAttacksToBlock(this->m_numAttacksCanBlock); + destroyableComponent->SetAttacksToBlock(0); } - - destroyableComponent->SetAttacksToBlock(0); } void BlockBehavior::Timer(BehaviorContext* context, BehaviorBranchContext branch, LWOOBJID second) { diff --git a/dGame/dBehaviors/ChainBehavior.cpp b/dGame/dBehaviors/ChainBehavior.cpp index feb27988..237c5280 100644 --- a/dGame/dBehaviors/ChainBehavior.cpp +++ b/dGame/dBehaviors/ChainBehavior.cpp @@ -11,6 +11,11 @@ void ChainBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bitStrea return; } + if (chainIndex == 0) { + LOG("Received invalid chain index of 0 for behavior %i.", m_behaviorId); + return; + } + chainIndex--; if (chainIndex < this->m_behaviors.size()) { diff --git a/dGame/dBehaviors/JetPackBehavior.cpp b/dGame/dBehaviors/JetPackBehavior.cpp index 8cfb87da..78f25fcd 100644 --- a/dGame/dBehaviors/JetPackBehavior.cpp +++ b/dGame/dBehaviors/JetPackBehavior.cpp @@ -7,6 +7,7 @@ void JetPackBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bit_stream, const BehaviorBranchContext branch) { auto* entity = Game::entityManager->GetEntity(branch.target); + if (!entity) return; GameMessages::SendSetJetPackMode(entity, true, this->m_BypassChecks, this->m_EnableHover, this->m_effectId, this->m_Airspeed, this->m_MaxAirspeed, this->m_VerticalVelocity, this->m_WarningEffectID); @@ -21,6 +22,7 @@ void JetPackBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bit_st void JetPackBehavior::UnCast(BehaviorContext* context, BehaviorBranchContext branch) { auto* entity = Game::entityManager->GetEntity(branch.target); + if (!entity) return; GameMessages::SendSetJetPackMode(entity, false); diff --git a/dGame/dBehaviors/NpcCombatSkillBehavior.cpp b/dGame/dBehaviors/NpcCombatSkillBehavior.cpp index 69a6ea9d..433594a2 100644 --- a/dGame/dBehaviors/NpcCombatSkillBehavior.cpp +++ b/dGame/dBehaviors/NpcCombatSkillBehavior.cpp @@ -5,20 +5,40 @@ void NpcCombatSkillBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bit_stream, BehaviorBranchContext branch) { context->skillTime = this->m_npcSkillTime; + const auto* const targetEntity = Game::entityManager->GetEntity(branch.target); + const auto* const sourceEntity = Game::entityManager->GetEntity(context->caster); - for (auto* behavior : this->m_behaviors) { - behavior->Calculate(context, bit_stream, branch); + bool cast = true; + // Check that the target is within the cast range + if (targetEntity && sourceEntity && this->m_maxRange != 0.0f) { + const auto targetPos = targetEntity->GetPosition(); + const auto sourcePos = sourceEntity->GetPosition(); + const auto distance = NiPoint3::DistanceSquared(targetPos, sourcePos); + cast = distance >= this->m_minRange && distance <= this->m_maxRange; + } + + if (cast) { + for (auto* behavior : this->m_behaviors) { + behavior->Calculate(context, bit_stream, branch); + } + } else { + // We failed to find a valid target, do not continue the behavior + context->foundTarget = false; } } void NpcCombatSkillBehavior::Load() { this->m_npcSkillTime = GetFloat("npc skill time"); + this->m_minRange = GetFloat("min range") * 0.9f; // Make the min and max 10% smaller to account for server/client position disagreements + this->m_minRange *= this->m_minRange; + this->m_maxRange = GetFloat("max range") * 0.9f; // Make the min and max 10% smaller to account for server/client position disagreements + this->m_maxRange *= this->m_maxRange; const auto parameters = GetParameterNames(); - for (const auto& parameter : parameters) { - if (parameter.first.rfind("behavior", 0) == 0) { - auto* action = GetAction(parameter.second); + for (const auto& [parameter, value] : parameters) { + if (parameter.rfind("behavior", 0) == 0) { + auto* action = GetAction(value); this->m_behaviors.push_back(action); } diff --git a/dGame/dBehaviors/NpcCombatSkillBehavior.h b/dGame/dBehaviors/NpcCombatSkillBehavior.h index 07826f48..a1c7c0e9 100644 --- a/dGame/dBehaviors/NpcCombatSkillBehavior.h +++ b/dGame/dBehaviors/NpcCombatSkillBehavior.h @@ -8,6 +8,9 @@ public: float m_npcSkillTime; + float m_maxRange{}; + float m_minRange{}; + /* * Inherited */ diff --git a/dGame/dBehaviors/SwitchMultipleBehavior.cpp b/dGame/dBehaviors/SwitchMultipleBehavior.cpp index 00d639d5..ef7309b5 100644 --- a/dGame/dBehaviors/SwitchMultipleBehavior.cpp +++ b/dGame/dBehaviors/SwitchMultipleBehavior.cpp @@ -17,6 +17,11 @@ void SwitchMultipleBehavior::Handle(BehaviorContext* context, RakNet::BitStream& return; }; + if (m_behaviors.empty()) { + LOG("No behaviors were loaded for %i, aborting call.", m_behaviorId); + return; + } + uint32_t trigger = 0; for (unsigned int i = 0; i < this->m_behaviors.size(); i++) { diff --git a/dGame/dBehaviors/TacArcBehavior.cpp b/dGame/dBehaviors/TacArcBehavior.cpp index 3e477896..076fc515 100644 --- a/dGame/dBehaviors/TacArcBehavior.cpp +++ b/dGame/dBehaviors/TacArcBehavior.cpp @@ -114,15 +114,13 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS context->FilterTargets(validTargets, this->m_ignoreFactionList, this->m_includeFactionList, this->m_targetSelf, this->m_targetEnemy, this->m_targetFriend, this->m_targetTeam); for (auto validTarget : validTargets) { - if (targets.size() >= this->m_maxTargets) break; if (std::find(targets.begin(), targets.end(), validTarget) != targets.end()) continue; if (validTarget->GetIsDead()) continue; const auto targetPos = validTarget->GetPosition(); - // make sure we aren't too high or low in comparison to the targer - const auto heightDifference = std::abs(reference.y - targetPos.y); - if (targetPos.y > reference.y && heightDifference > this->m_upperBound || targetPos.y < reference.y && heightDifference > this->m_lowerBound) + // make sure we aren't too high or low in comparison to the target + if (targetPos.y > (reference.y + m_upperBound) || targetPos.y < (reference.y + m_lowerBound)) continue; const auto forward = QuatUtils::Forward(self->GetRotation()); @@ -147,13 +145,28 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS } } - std::sort(targets.begin(), targets.end(), [reference](Entity* a, Entity* b) { + std::sort(targets.begin(), targets.end(), [this, reference, combatAi](Entity* a, Entity* b) { const auto aDistance = Vector3::DistanceSquared(reference, a->GetPosition()); const auto bDistance = Vector3::DistanceSquared(reference, b->GetPosition()); - return aDistance > bDistance; + return aDistance < bDistance; }); + + if (m_useAttackPriority) { + // this should be using the attack priority column on the destroyable component + // We want targets with no threat level to remain the same order as above + // std::stable_sort(targets.begin(), targets.end(), [combatAi](Entity* a, Entity* b) { + // const auto aThreat = combatAi->GetThreat(a->GetObjectID()); + // const auto bThreat = combatAi->GetThreat(b->GetObjectID()); + + // If enabled for this behavior, prioritize threat over distance + // return aThreat > bThreat; + // }); + } + + // After we've sorted and found our closest targets, size the vector down in case there are too many + if (m_maxTargets > 0 && targets.size() > m_maxTargets) targets.resize(m_maxTargets); const auto hit = !targets.empty(); bitStream.Write(hit); @@ -194,9 +207,13 @@ void TacArcBehavior::Load() { GetFloat("offset_y", 0.0f), GetFloat("offset_z", 0.0f) ); + // https://explorer.lu/skills/behaviors/6212/6203 HACK: i cant figure out why the dragon fire wall doesnt work with the offset, probably has to be fixed with the near/far height parameters + if (m_behaviorId == 6203) { + this->m_offset = NiPoint3Constant::ZERO; + } this->m_method = GetInt("method", 1); - this->m_upperBound = std::abs(GetFloat("upper_bound", 4.4f)); - this->m_lowerBound = std::abs(GetFloat("lower_bound", 0.4f)); + this->m_upperBound = GetFloat("upper_bound", 4.4f); + this->m_lowerBound = GetFloat("lower_bound", 0.4f) - 5.0f; // Makes it so players and objects can still be targetted when slightly below the caster. FIXME: use bounding spheres at some point this->m_usePickedTarget = GetBoolean("use_picked_target", false); this->m_useTargetPostion = GetBoolean("use_target_position", false); this->m_checkEnv = GetBoolean("check_env", false); diff --git a/dGame/dBehaviors/VerifyBehavior.cpp b/dGame/dBehaviors/VerifyBehavior.cpp index c7ede52f..ec4b27a8 100644 --- a/dGame/dBehaviors/VerifyBehavior.cpp +++ b/dGame/dBehaviors/VerifyBehavior.cpp @@ -25,7 +25,7 @@ void VerifyBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS const auto distance = Vector3::DistanceSquared(self->GetPosition(), entity->GetPosition()); - if (distance > this->m_range * this->m_range) { + if (distance > this->m_range) { success = false; } } else if (this->m_blockCheck) { @@ -57,4 +57,5 @@ void VerifyBehavior::Load() { this->m_action = GetAction("action"); this->m_range = GetFloat("range"); + this->m_range = this->m_range * this->m_range * 0.9f; // Range checks are slightly smaller than the actual range to account for client/server discrepancies } diff --git a/dGame/dComponents/ActivityComponent.cpp b/dGame/dComponents/ActivityComponent.cpp index 7567e20b..7fa73f01 100644 --- a/dGame/dComponents/ActivityComponent.cpp +++ b/dGame/dComponents/ActivityComponent.cpp @@ -22,6 +22,7 @@ #include "eMatchUpdate.h" #include "ServiceType.h" #include "MessageType/Chat.h" +#include "ObjectIDManager.h" #include "CDCurrencyTableTable.h" #include "CDActivityRewardsTable.h" @@ -29,10 +30,14 @@ #include "LeaderboardManager.h" #include "CharacterComponent.h" #include "Amf3.h" +#include + +namespace { + const ActivityInstance g_EmptyInstance{ nullptr, CDActivities{} }; +} ActivityComponent::ActivityComponent(Entity* parent, int32_t componentID) : Component(parent, componentID) { - using namespace GameMessages; - RegisterMsg(this, &ActivityComponent::OnGetObjectReportInfo); + RegisterMsg(&ActivityComponent::OnGetObjectReportInfo); /* * This is precisely what the client does functionally * Use the component id as the default activity id and load its data from the database @@ -72,9 +77,9 @@ void ActivityComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsIniti if (m_DirtyActivityInfo) { outBitStream.Write(m_ActivityPlayers.size()); if (!m_ActivityPlayers.empty()) { - for (const auto& activityPlayer : m_ActivityPlayers) { - outBitStream.Write(activityPlayer->playerID); - for (const auto& activityValue : activityPlayer->values) { + for (const auto& [playerID, values] : m_ActivityPlayers) { + outBitStream.Write(playerID); + for (const auto& activityValue : values) { outBitStream.Write(activityValue); } } @@ -112,78 +117,81 @@ void ActivityComponent::PlayerJoin(Entity* player) { if (HasLobby()) { PlayerJoinLobby(player); } else if (!IsPlayedBy(player)) { - auto* instance = NewInstance(); - instance->AddParticipant(player); + NewInstance().AddParticipant(player); } } void ActivityComponent::PlayerJoinLobby(Entity* player) { if (!m_Parent->HasComponent(eReplicaComponentType::QUICK_BUILD)) GameMessages::SendMatchResponse(player, player->GetSystemAddress(), 0); // tell the client they joined a lobby - LobbyPlayer* newLobbyPlayer = new LobbyPlayer(); - newLobbyPlayer->entityID = player->GetObjectID(); - Lobby* playerLobby = nullptr; + LobbyPlayer newLobbyPlayer{}; + newLobbyPlayer.entityID = player->GetObjectID(); + LWOOBJID playerLobbyID = LWOOBJID_EMPTY; auto* character = player->GetCharacter(); if (character != nullptr) character->SetLastNonInstanceZoneID(Game::zoneManager->GetZone()->GetWorldID()); - for (Lobby* lobby : m_Queue) { - if (lobby->players.size() < m_ActivityInfo.maxTeamSize || m_ActivityInfo.maxTeamSize == 1 && lobby->players.size() < m_ActivityInfo.maxTeams) { + for (auto& [lobbyID, lobby] : m_Queue) { + if (lobby.players.size() < m_ActivityInfo.maxTeamSize || m_ActivityInfo.maxTeamSize == 1 && lobby.players.size() < m_ActivityInfo.maxTeams) { // If an empty slot in an existing lobby is found - lobby->players.push_back(newLobbyPlayer); - playerLobby = lobby; + lobby.players.push_back(newLobbyPlayer); + playerLobbyID = lobbyID; // Update the joining player on players already in the lobby, and update players already in the lobby on the joining player - std::string matchUpdateJoined = "player=9:" + std::to_string(player->GetObjectID()) + "\nplayerName=0:" + player->GetCharacter()->GetName(); - for (LobbyPlayer* joinedPlayer : lobby->players) { - auto* entity = joinedPlayer->GetEntity(); + LDFData playerLDF("player", player->GetObjectID()); + LDFData playerName("playerName", player->GetCharacter()->GetName()); + std::string matchUpdateJoined = playerLDF.GetString() + "\n" + playerName.GetString(); + for (const auto& joinedPlayer : lobby.players) { + auto* const entity = joinedPlayer.GetEntity(); if (entity == nullptr) { continue; } - std::string matchUpdate = "player=9:" + std::to_string(entity->GetObjectID()) + "\nplayerName=0:" + entity->GetCharacter()->GetName(); + LDFData entityLDF("player", entity->GetObjectID()); + LDFData entityName("playerName", entity->GetCharacter()->GetName()); + std::string matchUpdate = entityLDF.GetString() + "\n" + entityName.GetString(); GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchUpdate, eMatchUpdate::PLAYER_ADDED); - PlayerReady(entity, joinedPlayer->ready); + PlayerReady(entity, joinedPlayer.ready); GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateJoined, eMatchUpdate::PLAYER_ADDED); } + break; } } - if (!playerLobby) { + if (playerLobbyID == LWOOBJID_EMPTY) { // If all lobbies are full - playerLobby = new Lobby(); - playerLobby->players.push_back(newLobbyPlayer); - playerLobby->timer = m_ActivityInfo.waitTime / 1000; - m_Queue.push_back(playerLobby); + playerLobbyID = ObjectIDManager::GenerateObjectID(); + auto& newLobby = m_Queue[playerLobbyID]; + newLobby.players.push_back(newLobbyPlayer); + newLobby.timer = m_ActivityInfo.waitTime / 1000; } + const auto& lobby = m_Queue[playerLobbyID]; - if (m_ActivityInfo.maxTeamSize != 1 && playerLobby->players.size() >= m_ActivityInfo.minTeamSize || m_ActivityInfo.maxTeamSize == 1 && playerLobby->players.size() >= m_ActivityInfo.minTeams) { + if (m_ActivityInfo.maxTeamSize != 1 && lobby.players.size() >= m_ActivityInfo.minTeamSize || m_ActivityInfo.maxTeamSize == 1 && lobby.players.size() >= m_ActivityInfo.minTeams) { // Update the joining player on the match timer - std::string matchTimerUpdate = "time=3:" + std::to_string(playerLobby->timer); - GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_READY); + LDFData matchTimer("time", lobby.timer); + GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchTimer.GetString(), eMatchUpdate::PHASE_WAIT_READY); } } void ActivityComponent::PlayerLeave(LWOOBJID playerID) { - // Removes the player from a lobby and notifies the others, not applicable for non-lobby instances - for (Lobby* lobby : m_Queue) { - for (int i = 0; i < lobby->players.size(); ++i) { - if (lobby->players[i]->entityID == playerID) { - std::string matchUpdateLeft = "player=9:" + std::to_string(playerID); - for (LobbyPlayer* lobbyPlayer : lobby->players) { - auto* entity = lobbyPlayer->GetEntity(); + for (auto& lobby : m_Queue | std::views::values) { + for (int i = 0; i < lobby.players.size(); i++) { + const auto& player = lobby.players[i]; + if (player.entityID == playerID) { + LDFData matchUpdateLeft("player", playerID); + for (const auto& lobbyPlayer : lobby.players) { + auto* const entity = lobbyPlayer.GetEntity(); if (entity == nullptr) continue; - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateLeft, eMatchUpdate::PLAYER_REMOVED); + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateLeft.GetString(), eMatchUpdate::PLAYER_REMOVED); } - delete lobby->players[i]; - lobby->players[i] = nullptr; - lobby->players.erase(lobby->players.begin() + i); + lobby.players.erase(lobby.players.begin() + i); return; } @@ -192,85 +200,79 @@ void ActivityComponent::PlayerLeave(LWOOBJID playerID) { } void ActivityComponent::Update(float deltaTime) { - std::vector lobbiesToRemove{}; + std::vector lobbiesToRemove{}; // Ticks all the lobbies, not applicable for non-instance activities - for (Lobby* lobby : m_Queue) { - for (LobbyPlayer* player : lobby->players) { - auto* entity = player->GetEntity(); + for (auto& [lobbyID, lobby] : m_Queue) { + for (const auto& player : lobby.players) { + const auto* const entity = player.GetEntity(); if (entity == nullptr) { - PlayerLeave(player->entityID); + PlayerLeave(player.entityID); return; } } - if (lobby->players.empty()) { - lobbiesToRemove.push_back(lobby); + if (lobby.players.empty()) { + lobbiesToRemove.push_back(lobbyID); continue; } // Update the match time for all players - if (m_ActivityInfo.maxTeamSize != 1 && lobby->players.size() >= m_ActivityInfo.minTeamSize - || m_ActivityInfo.maxTeamSize == 1 && lobby->players.size() >= m_ActivityInfo.minTeams) { - if (lobby->timer == m_ActivityInfo.waitTime / 1000) { - for (LobbyPlayer* joinedPlayer : lobby->players) { - auto* entity = joinedPlayer->GetEntity(); + if (m_ActivityInfo.maxTeamSize != 1 && lobby.players.size() >= m_ActivityInfo.minTeamSize + || m_ActivityInfo.maxTeamSize == 1 && lobby.players.size() >= m_ActivityInfo.minTeams) { + if (lobby.timer == m_ActivityInfo.waitTime / 1000) { + for (const auto& joinedPlayer : lobby.players) { + auto* const entity = joinedPlayer.GetEntity(); if (entity == nullptr) continue; - std::string matchTimerUpdate = "time=3:" + std::to_string(lobby->timer); - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_READY); + LDFData matchTimerUpdate("time", lobby.timer); + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate.GetString(), eMatchUpdate::PHASE_WAIT_READY); } } - lobby->timer -= deltaTime; + lobby.timer -= deltaTime; } bool lobbyReady = true; - for (LobbyPlayer* player : lobby->players) { - if (player->ready) continue; + for (const auto& player : lobby.players) { + if (player.ready) continue; lobbyReady = false; } // If everyone's ready, jump the timer - if (lobbyReady && lobby->timer > m_ActivityInfo.startDelay / 1000) { - lobby->timer = m_ActivityInfo.startDelay / 1000; + if (lobbyReady && lobby.timer > m_ActivityInfo.startDelay / 1000) { + lobby.timer = m_ActivityInfo.startDelay / 1000; // Update players in lobby on switch to start delay - std::string matchTimerUpdate = "time=3:" + std::to_string(lobby->timer); - for (LobbyPlayer* player : lobby->players) { - auto* entity = player->GetEntity(); + LDFData matchTimerUpdate("time", lobby.timer); + for (const auto& player : lobby.players) { + auto* const entity = player.GetEntity(); if (entity == nullptr) continue; - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_START); + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate.GetString(), eMatchUpdate::PHASE_WAIT_START); } } // The timer has elapsed, start the instance - if (lobby->timer <= 0.0f) { + if (lobby.timer <= 0.0f) { LOG("Setting up instance."); - ActivityInstance* instance = NewInstance(); - LoadPlayersIntoInstance(instance, lobby->players); - instance->StartZone(); - lobbiesToRemove.push_back(lobby); + auto& instance = NewInstance(); + LoadPlayersIntoInstance(instance, lobby.players); + instance.StartZone(); + lobbiesToRemove.push_back(lobbyID); } } - while (!lobbiesToRemove.empty()) { - RemoveLobby(lobbiesToRemove.front()); - lobbiesToRemove.erase(lobbiesToRemove.begin()); + for (const auto id : lobbiesToRemove) { + RemoveLobby(id); } } -void ActivityComponent::RemoveLobby(Lobby* lobby) { - for (int i = 0; i < m_Queue.size(); ++i) { - if (m_Queue[i] == lobby) { - m_Queue.erase(m_Queue.begin() + i); - return; - } - } +void ActivityComponent::RemoveLobby(const LWOOBJID lobbyID) { + if (m_Queue.contains(lobbyID)) m_Queue.erase(lobbyID); } bool ActivityComponent::HasLobby() const { @@ -279,9 +281,9 @@ bool ActivityComponent::HasLobby() const { } bool ActivityComponent::PlayerIsInQueue(Entity* player) { - for (Lobby* lobby : m_Queue) { - for (LobbyPlayer* lobbyPlayer : lobby->players) { - if (player->GetObjectID() == lobbyPlayer->entityID) return true; + for (const auto& lobby : m_Queue | std::views::values) { + for (const auto& lobbyPlayer : lobby.players) { + if (player->GetObjectID() == lobbyPlayer.entityID) return true; } } @@ -289,8 +291,8 @@ bool ActivityComponent::PlayerIsInQueue(Entity* player) { } bool ActivityComponent::IsPlayedBy(Entity* player) const { - for (const auto* instance : this->m_Instances) { - for (const auto* instancePlayer : instance->GetParticipants()) { + for (const auto& instance : m_Instances) { + for (const auto* instancePlayer : instance.GetParticipants()) { if (instancePlayer != nullptr && instancePlayer->GetObjectID() == player->GetObjectID()) return true; } @@ -300,8 +302,8 @@ bool ActivityComponent::IsPlayedBy(Entity* player) const { } bool ActivityComponent::IsPlayedBy(LWOOBJID playerID) const { - for (const auto* instance : this->m_Instances) { - for (const auto* instancePlayer : instance->GetParticipants()) { + for (const auto& instance : m_Instances) { + for (const auto* instancePlayer : instance.GetParticipants()) { if (instancePlayer != nullptr && instancePlayer->GetObjectID() == playerID) return true; } @@ -325,142 +327,100 @@ bool ActivityComponent::CheckCost(Entity* player) const { } bool ActivityComponent::TakeCost(Entity* player) const { - auto* inventoryComponent = player->GetComponent(); - return CheckCost(player) && inventoryComponent->RemoveItem(m_ActivityInfo.optionalCostLOT, m_ActivityInfo.optionalCostCount, eInventoryType::ALL); + return CheckCost(player) && inventoryComponent && inventoryComponent->RemoveItem(m_ActivityInfo.optionalCostLOT, m_ActivityInfo.optionalCostCount, eInventoryType::ALL); } void ActivityComponent::PlayerReady(Entity* player, bool bReady) { - for (Lobby* lobby : m_Queue) { - for (LobbyPlayer* lobbyPlayer : lobby->players) { - if (lobbyPlayer->entityID == player->GetObjectID()) { + for (auto& lobby : m_Queue | std::views::values) { + for (auto& lobbyPlayer : lobby.players) { + if (lobbyPlayer.entityID == player->GetObjectID()) { - lobbyPlayer->ready = bReady; + lobbyPlayer.ready = bReady; // Update players in lobby on player being ready - std::string matchReadyUpdate = "player=9:" + std::to_string(player->GetObjectID()); + LDFData matchReadyUpdate("player", player->GetObjectID()); eMatchUpdate readyStatus = eMatchUpdate::PLAYER_READY; if (!bReady) readyStatus = eMatchUpdate::PLAYER_NOT_READY; - for (LobbyPlayer* otherPlayer : lobby->players) { - auto* entity = otherPlayer->GetEntity(); + for (const auto& otherPlayer : lobby.players) { + auto* const entity = otherPlayer.GetEntity(); if (entity == nullptr) continue; - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchReadyUpdate, readyStatus); + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchReadyUpdate.GetString(), readyStatus); } } } } } -ActivityInstance* ActivityComponent::NewInstance() { - auto* instance = new ActivityInstance(m_Parent, m_ActivityInfo); - m_Instances.push_back(instance); - return instance; +ActivityInstance& ActivityComponent::NewInstance() { + m_Instances.push_back(ActivityInstance(m_Parent, m_ActivityInfo)); + return m_Instances.back(); } -void ActivityComponent::LoadPlayersIntoInstance(ActivityInstance* instance, const std::vector& lobby) const { - for (LobbyPlayer* player : lobby) { - auto* entity = player->GetEntity(); +void ActivityComponent::LoadPlayersIntoInstance(ActivityInstance& instance, const std::vector& lobby) const { + for (const auto& player : lobby) { + auto* const entity = player.GetEntity(); if (entity == nullptr || !CheckCost(entity)) { continue; } - instance->AddParticipant(entity); + instance.AddParticipant(entity); } } -const std::vector& ActivityComponent::GetInstances() const { - return m_Instances; -} - -ActivityInstance* ActivityComponent::GetInstance(const LWOOBJID playerID) { - for (const auto* instance : GetInstances()) { - for (const auto* participant : instance->GetParticipants()) { +const ActivityInstance& ActivityComponent::GetInstance(const LWOOBJID playerID) const { + for (const auto& instance : m_Instances) { + for (const auto* participant : instance.GetParticipants()) { if (participant->GetObjectID() == playerID) - return const_cast(instance); + return instance; } } - return nullptr; + return g_EmptyInstance; } -void ActivityComponent::ClearInstances() { - for (ActivityInstance* instance : m_Instances) { - delete instance; - } - m_Instances.clear(); -} - -ActivityPlayer* ActivityComponent::GetActivityPlayerData(LWOOBJID playerID) { - for (auto* activityData : m_ActivityPlayers) { - if (activityData->playerID == playerID) { - return activityData; - } - } - - return nullptr; +bool ActivityComponent::PlayerHasActivityData(LWOOBJID playerID) const { + return m_ActivityPlayers.contains(playerID); } void ActivityComponent::RemoveActivityPlayerData(LWOOBJID playerID) { - for (size_t i = 0; i < m_ActivityPlayers.size(); i++) { - if (m_ActivityPlayers[i]->playerID == playerID) { - delete m_ActivityPlayers[i]; - m_ActivityPlayers[i] = nullptr; - - m_ActivityPlayers.erase(m_ActivityPlayers.begin() + i); - m_DirtyActivityInfo = true; - Game::entityManager->SerializeEntity(m_Parent); - - return; - } - } -} - -ActivityPlayer* ActivityComponent::AddActivityPlayerData(LWOOBJID playerID) { - auto* data = GetActivityPlayerData(playerID); - if (data != nullptr) - return data; - - m_ActivityPlayers.push_back(new ActivityPlayer{ playerID, {} }); + m_ActivityPlayers.erase(playerID); m_DirtyActivityInfo = true; - Game::entityManager->SerializeEntity(m_Parent); - - return GetActivityPlayerData(playerID); } -float_t ActivityComponent::GetActivityValue(LWOOBJID playerID, uint32_t index) { - auto value = -1.0f; +float_t ActivityComponent::GetActivityValue(LWOOBJID playerID, uint32_t index) const { + float value = -1.0f; - auto* data = GetActivityPlayerData(playerID); - if (data != nullptr) { - value = data->values[std::min(index, static_cast(9))]; + const auto& data = m_ActivityPlayers.find(playerID); + if (data != m_ActivityPlayers.cend()) { + value = data->second[std::min(index, static_cast(9))]; } - + LOG_DEBUG("Player %llu has score %f at index %i", playerID, value, index); return value; } void ActivityComponent::SetActivityValue(LWOOBJID playerID, uint32_t index, float_t value) { - auto* data = AddActivityPlayerData(playerID); - if (data != nullptr) { - data->values[std::min(index, static_cast(9))] = value; - } + auto& data = m_ActivityPlayers[playerID]; + data[std::min(index, static_cast(9))] = value; + LOG_DEBUG("%llu index %i has score of %f", playerID, index, value); m_DirtyActivityInfo = true; Game::entityManager->SerializeEntity(m_Parent); } void ActivityComponent::PlayerRemove(LWOOBJID playerID) { - for (auto* instance : GetInstances()) { - auto participants = instance->GetParticipants(); + for (int i = 0; i < m_Instances.size(); i++) { + auto& instance = m_Instances[i]; + auto participants = instance.GetParticipants(); for (const auto* participant : participants) { if (participant != nullptr && participant->GetObjectID() == playerID) { - instance->RemoveParticipant(participant); + instance.RemoveParticipant(participant); RemoveActivityPlayerData(playerID); // If the instance is empty after the delete of the participant, delete the instance too - if (instance->GetParticipants().empty()) { - m_Instances.erase(std::find(m_Instances.begin(), m_Instances.end(), instance)); - delete instance; + if (instance.GetParticipants().empty()) { + m_Instances.erase(m_Instances.begin() + i); } return; } @@ -591,22 +551,19 @@ Entity* LobbyPlayer::GetEntity() const { return Game::entityManager->GetEntity(entityID); } -bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportInfo = static_cast(msg); - +bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { auto& activityInfo = reportInfo.info->PushDebug("Activity"); auto& instances = activityInfo.PushDebug("Instances: " + std::to_string(m_Instances.size())); size_t i = 0; for (const auto& activityInstance : m_Instances) { - if (!activityInstance) continue; auto& instance = instances.PushDebug("Instance " + std::to_string(i++)); - instance.PushDebug("Score") = activityInstance->GetScore(); - instance.PushDebug("Next Zone Clone ID") = activityInstance->GetNextZoneCloneID(); + instance.PushDebug("Score") = activityInstance.GetScore(); + instance.PushDebug("Next Zone Clone ID") = activityInstance.GetNextZoneCloneID(); { auto& activityInfo = instance.PushDebug("Activity Info"); - const auto& instanceActInfo = activityInstance->GetActivityInfo(); + const auto& instanceActInfo = activityInstance.GetActivityInfo(); activityInfo.PushDebug("ActivityID") = instanceActInfo.ActivityID; activityInfo.PushDebug("locStatus") = instanceActInfo.locStatus; activityInfo.PushDebug("instanceMapID") = instanceActInfo.instanceMapID; @@ -629,7 +586,7 @@ bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { } auto& participants = instance.PushDebug("Participants"); - for (const auto* participant : activityInstance->GetParticipants()) { + for (const auto* participant : activityInstance.GetParticipants()) { if (!participant) continue; auto* character = participant->GetCharacter(); if (!character) continue; @@ -639,38 +596,36 @@ bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { auto& queue = activityInfo.PushDebug("Queue"); i = 0; - for (const auto& lobbyQueue : m_Queue) { + for (const auto& lobbyQueue : m_Queue | std::views::values) { auto& lobby = queue.PushDebug("Lobby " + std::to_string(i++)); - lobby.PushDebug("Timer") = lobbyQueue->timer; + lobby.PushDebug("Timer") = lobbyQueue.timer; auto& players = lobby.PushDebug("Players"); - for (const auto* player : lobbyQueue->players) { - if (!player) continue; - auto* playerEntity = player->GetEntity(); + for (const auto& player : lobbyQueue.players) { + const auto* const playerEntity = player.GetEntity(); if (!playerEntity) continue; auto* character = playerEntity->GetCharacter(); if (!character) continue; - players.PushDebug(std::to_string(playerEntity->GetObjectID()) + ": " + character->GetName()) = player->ready ? "Ready" : "Not Ready"; + players.PushDebug(std::to_string(playerEntity->GetObjectID()) + ": " + character->GetName()) = player.ready ? "Ready" : "Not Ready"; } } auto& activityPlayers = activityInfo.PushDebug("Activity Players"); - for (const auto* activityPlayer : m_ActivityPlayers) { - if (!activityPlayer) continue; - auto* const activityPlayerEntity = Game::entityManager->GetEntity(activityPlayer->playerID); + for (const auto& [playerID, playerScores] : m_ActivityPlayers) { + auto* const activityPlayerEntity = Game::entityManager->GetEntity(playerID); if (!activityPlayerEntity) continue; auto* character = activityPlayerEntity->GetCharacter(); if (!character) continue; - auto& playerData = activityPlayers.PushDebug(std::to_string(activityPlayer->playerID) + " " + character->GetName()); + auto& playerData = activityPlayers.PushDebug(std::to_string(playerID) + " " + character->GetName()); auto& scores = playerData.PushDebug("Scores"); for (size_t i = 0; i < 10; ++i) { - scores.PushDebug(std::to_string(i)) = activityPlayer->values[i]; + scores.PushDebug(std::to_string(i)) = playerScores[i]; } } - + activityInfo.PushDebug("ActivityID") = m_ActivityID; return true; } diff --git a/dGame/dComponents/ActivityComponent.h b/dGame/dComponents/ActivityComponent.h index dc249068..856ca70f 100644 --- a/dGame/dComponents/ActivityComponent.h +++ b/dGame/dComponents/ActivityComponent.h @@ -8,14 +8,15 @@ #include "eReplicaComponentType.h" #include "CDActivitiesTable.h" +#include namespace GameMessages { class GameMsg; }; - /** - * Represents an instance of an activity, having participants and score - */ +/** + * Represents an instance of an activity, having participants and score + */ class ActivityInstance { public: ActivityInstance(Entity* parent, CDActivities activityInfo) { m_Parent = parent; m_ActivityInfo = activityInfo; }; @@ -104,7 +105,7 @@ struct LobbyPlayer { /** * The ID of the entity that is in the lobby */ - LWOOBJID entityID; + LWOOBJID entityID = LWOOBJID_EMPTY; /** * Whether or not the entity is ready @@ -126,12 +127,12 @@ struct Lobby { /** * The lobby of players */ - std::vector players; + std::vector players; /** * The timer that determines when the activity should start */ - float timer; + float timer{}; }; /** @@ -142,12 +143,12 @@ struct ActivityPlayer { /** * The entity that the score is tracked for */ - LWOOBJID playerID; + LWOOBJID playerID{}; /** * The list of score for this entity */ - float values[10]; + float values[10]{}; }; /** @@ -194,13 +195,13 @@ public: * @param instance the instance to load the players into * @param lobby the players to load into the instance */ - void LoadPlayersIntoInstance(ActivityInstance* instance, const std::vector& lobby) const; + void LoadPlayersIntoInstance(ActivityInstance& instance, const std::vector& lobby) const; /** * Removes a lobby from the activity manager * @param lobby the lobby to remove */ - void RemoveLobby(Lobby* lobby); + void RemoveLobby(const LWOOBJID lobbyID); /** * Marks a player as (un)ready in a lobby @@ -215,6 +216,10 @@ public: */ int GetActivityID() { return m_ActivityInfo.ActivityID; } + // Whether or not team loot should be dropped on death for this activity + // if true, and a player is supposed to get loot, they are skipped + bool GetNoTeamLootOnDeath() const { return m_ActivityInfo.noTeamLootOnDeath; } + /** * Returns if this activity has a lobby, e.g. if it needs to instance players to some other map * @return true if this activity has a lobby, false otherwise @@ -242,7 +247,7 @@ public: */ bool IsPlayedBy(LWOOBJID playerID) const; - /** + /** * Checks if the entity has enough cost to play this activity * @param player the entity to check * @return true if the entity has enough cost to play this activity, false otherwise @@ -267,20 +272,14 @@ public: * Creates a new instance for this activity * @return a new instance for this activity */ - ActivityInstance* NewInstance(); - - /** - * Returns all the currently active instances of this activity - * @return all the currently active instances of this activity - */ - const std::vector& GetInstances() const; + ActivityInstance& NewInstance(); /** * Returns the instance that some entity is currently playing in * @param playerID the entity to check for * @return if any, the instance that the entity is currently in */ - ActivityInstance* GetInstance(const LWOOBJID playerID); + const ActivityInstance& GetInstance(const LWOOBJID playerID) const; /** * @brief Reloads the config settings for this component @@ -288,23 +287,12 @@ public: */ void ReloadConfig(); - /** - * Removes all the instances - */ - void ClearInstances(); - - /** - * Returns all the score for the players that are currently playing this activity - * @return - */ - std::vector GetActivityPlayers() { return m_ActivityPlayers; }; - /** * Returns activity data for a specific entity (e.g. score and such). * @param playerID the entity to get data for * @return the activity data (score) for the passed player in this activity, if it exists */ - ActivityPlayer* GetActivityPlayerData(LWOOBJID playerID); + bool PlayerHasActivityData(LWOOBJID playerID) const; /** * Sets some score value for an entity @@ -320,7 +308,7 @@ public: * @param index the index to get score for * @return activity score for the passed parameters */ - float_t GetActivityValue(LWOOBJID playerID, uint32_t index); + float_t GetActivityValue(LWOOBJID playerID, uint32_t index) const; /** * Removes activity score tracking for some entity @@ -328,13 +316,6 @@ public: */ void RemoveActivityPlayerData(LWOOBJID playerID); - /** - * Adds activity score tracking for some entity - * @param playerID the entity to add the activity score for - * @return the created entry - */ - ActivityPlayer* AddActivityPlayerData(LWOOBJID playerID); - /** * Sets the mapID that this activity points to * @param mapID the map ID to set @@ -342,8 +323,7 @@ public: void SetInstanceMapID(uint32_t mapID) { m_ActivityInfo.instanceMapID = mapID; }; private: - - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg); /** * The database information for this activity */ @@ -352,17 +332,17 @@ private: /** * All the active instances of this activity */ - std::vector m_Instances; + std::vector m_Instances; /** * The current lobbies for this activity */ - std::vector m_Queue; + std::map m_Queue; /** * All the activity score for the players in this activity */ - std::vector m_ActivityPlayers; + std::map> m_ActivityPlayers; /** * The activity id diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index d264801f..89c39c33 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -13,6 +13,8 @@ #include "CDClientDatabase.h" #include "CDClientManager.h" +#include "CDObjectSkillsTable.h" +#include "CDSkillBehaviorTable.h" #include "DestroyableComponent.h" #include @@ -23,12 +25,13 @@ #include "SkillComponent.h" #include "QuickBuildComponent.h" #include "DestroyableComponent.h" -#include "Metrics.hpp" #include "CDComponentsRegistryTable.h" #include "CDPhysicsComponentTable.h" #include "dNavMesh.h" +#include "Amf3.h" BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { + RegisterMsg(&BaseCombatAIComponent::MsgGetObjectReportInfo); m_Target = LWOOBJID_EMPTY; m_DirtyStateOrTarget = true; m_State = AiState::spawn; @@ -42,7 +45,7 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo //Grab the aggro information from BaseCombatAI: auto componentQuery = CDClientDatabase::CreatePreppedStmt( - "SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius FROM BaseCombatAIComponent WHERE id = ?;"); + "SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius, minRoundLength, maxRoundLength, combatRoundLength FROM BaseCombatAIComponent WHERE id = ?;"); componentQuery.bind(1, static_cast(componentID)); auto componentResult = componentQuery.execQuery(); @@ -62,44 +65,37 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo if (!componentResult.fieldIsNull("hardTetherRadius")) m_HardTetherRadius = componentResult.getFloatField("hardTetherRadius"); + + m_MinRoundLength = componentResult.getFloatField("minRoundLength"); + m_MaxRoundLength = componentResult.getFloatField("maxRoundLength"); + m_CombatRoundLength = componentResult.getFloatField("combatRoundLength"); } - componentResult.finalize(); - // Get aggro and tether radius from settings and use this if it is present. Only overwrite the // radii if it is greater than the one in the database. - if (m_Parent) { - auto aggroRadius = m_Parent->GetVar(u"aggroRadius"); - m_AggroRadius = aggroRadius != 0 ? aggroRadius : m_AggroRadius; - auto tetherRadius = m_Parent->GetVar(u"tetherRadius"); - m_HardTetherRadius = tetherRadius != 0 ? tetherRadius : m_HardTetherRadius; - } + m_AggroRadius = m_Parent->HasVar(u"aggroRadius") ? m_Parent->GetVar(u"aggroRadius") : m_AggroRadius; + m_HardTetherRadius = m_Parent->HasVar(u"tetherRadius") ? m_Parent->GetVar(u"tetherRadius") : m_HardTetherRadius; /* * Find skills */ - auto skillQuery = CDClientDatabase::CreatePreppedStmt( - "SELECT skillID, cooldown, behaviorID FROM SkillBehavior WHERE skillID IN (SELECT skillID FROM ObjectSkills WHERE objectTemplate = ?);"); - skillQuery.bind(1, static_cast(parent->GetLOT())); + for (const auto objectSkill : CDClientManager::GetTable()->Get(parent->GetLOT())) { + const auto skillBehavior = CDClientManager::GetTable()->GetSkillByID(objectSkill.skillID); + if (skillBehavior.skillID == objectSkill.skillID) { + const auto skillId = skillBehavior.skillID; - auto result = skillQuery.execQuery(); + const auto abilityCooldown = skillBehavior.cooldown; - while (!result.eof()) { - const auto skillId = static_cast(result.getIntField("skillID")); + const auto behaviorId = skillBehavior.behaviorID; - const auto abilityCooldown = static_cast(result.getFloatField("cooldown")); + const auto combatWeight = objectSkill.AICombatWeight; - const auto behaviorId = static_cast(result.getIntField("behaviorID")); + auto* behavior = Behavior::CreateBehavior(behaviorId); - auto* behavior = Behavior::CreateBehavior(behaviorId); + AiSkillEntry entry = { .skillId = skillId, .cooldown = 0.0f, .abilityCooldown = abilityCooldown, .behavior = behavior, .combatWeight = combatWeight }; - std::stringstream behaviorQuery; - - AiSkillEntry entry = { skillId, 0, abilityCooldown, behavior }; - - m_SkillEntries.push_back(entry); - - result.nextRow(); + m_SkillEntries.push_back(entry); + } } Stun(1.0f); @@ -209,8 +205,10 @@ void BaseCombatAIComponent::Update(const float deltaTime) { } if (stunnedThisFrame) { - m_MovementAI->Stop(); + if (!m_MovementAI->IsPaused()) m_MovementAI->Pause(); + // in this case we just become unstunned so check if we paused and resume if we did + if (!m_Stunned && m_MovementAI->IsPaused()) m_MovementAI->Resume(); return; } @@ -245,10 +243,12 @@ void BaseCombatAIComponent::Update(const float deltaTime) { void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { bool hasSkillToCast = false; + int32_t maxSkillWeights = 0; for (auto& entry : m_SkillEntries) { if (entry.cooldown > 0.0f) { entry.cooldown -= deltaTime; } else { + maxSkillWeights += entry.combatWeight; hasSkillToCast = true; } } @@ -316,12 +316,14 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { SetAiState(AiState::aggro); } else { SetAiState(AiState::idle); + if (m_MovementAI) m_MovementAI->SetMaxSpeed(1.0f); } if (!hasSkillToCast) return; if (m_Target == LWOOBJID_EMPTY) { SetAiState(AiState::idle); + if (m_MovementAI) m_MovementAI->SetMaxSpeed(1.0f); return; } @@ -332,14 +334,23 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { LookAt(target->GetPosition()); } - for (auto i = 0; i < m_SkillEntries.size(); ++i) { - auto entry = m_SkillEntries.at(i); + // Roll to find which skill we'll try to cast + auto randomizedWeight = GeneralUtils::GenerateRandomNumber(0, maxSkillWeights); - if (entry.cooldown > 0) { + for (auto& entry : m_SkillEntries) { + // Skill isn't cooled off yet + if (entry.cooldown > 0.0f) { continue; } - const auto result = skillComponent->CalculateBehavior(entry.skillId, entry.behavior->m_behaviorId, LWOOBJID_EMPTY); + randomizedWeight -= entry.combatWeight; + + // if the weight is still greater than 0 continue to the next rolled skill + if (randomizedWeight > 0) { + continue; + } + + const auto result = skillComponent->CalculateBehavior(entry.skillId, entry.behavior->m_behaviorId, GetTarget()); if (result.success) { if (m_MovementAI != nullptr) { @@ -354,8 +365,6 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { entry.cooldown = entry.abilityCooldown + m_SkillTime; - m_SkillEntries[i] = entry; - break; } } @@ -476,6 +485,7 @@ std::vector BaseCombatAIComponent::GetTargetWithinAggroRange() const { for (auto id : m_Parent->GetTargetsInPhantom()) { auto* other = Game::entityManager->GetEntity(id); + if (!other) continue; const auto distance = Vector3::DistanceSquared(m_Parent->GetPosition(), other->GetPosition()); @@ -616,6 +626,11 @@ void BaseCombatAIComponent::Wander() { return; } + // If we have a path to follow we should almost certainly do that instead of wandering. + if (m_MovementAI->HasPath()) { + return; + } + m_MovementAI->SetHaltDistance(0); const auto& info = m_MovementAI->GetInfo(); @@ -744,8 +759,8 @@ void BaseCombatAIComponent::SetTetherSpeed(float value) { m_TetherSpeed = value; } -void BaseCombatAIComponent::Stun(const float time) { - if (m_StunImmune || m_StunTime > time) { +void BaseCombatAIComponent::Stun(const float time, const bool force) { + if (!force && (m_StunImmune || m_StunTime > time)) { return; } @@ -839,3 +854,80 @@ void BaseCombatAIComponent::IgnoreThreat(const LWOOBJID threat, const float valu SetThreat(threat, 0.0f); m_Target = LWOOBJID_EMPTY; } + +bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + using enum AiState; + auto& cmptType = reportInfo.info->PushDebug("Base Combat AI"); + cmptType.PushDebug("Component ID") = GetComponentID(); + auto& targetInfo = cmptType.PushDebug("Current Target Info"); + targetInfo.PushDebug("Current Target ID") = std::to_string(m_Target); + // if (m_Target != LWOOBJID_EMPTY) { + // LWOGameMessages::ObjGetName nameMsg(m_CurrentTarget); + // SEND_GAMEOBJ_MSG(nameMsg); + // if (!nameMsg.msg.name.empty()) targetInfo.PushDebug("Name") = nameMsg.msg.name; + // } + + auto& roundInfo = cmptType.PushDebug("Round Info"); + // roundInfo.PushDebug("Combat Round Time") = m_CombatRoundLength; + // roundInfo.PushDebug("Minimum Time") = m_MinRoundLength; + // roundInfo.PushDebug("Maximum Time") = m_MaxRoundLength; + // roundInfo.PushDebug("Selected Time") = m_SelectedTime; + // roundInfo.PushDebug("Combat Start Delay") = m_CombatStartDelay; + std::string curState; + switch (m_State) { + case idle: curState = "Idling"; break; + case aggro: curState = "Aggroed"; break; + case tether: curState = "Returning to Tether"; break; + case spawn: curState = "Spawn"; break; + case dead: curState = "Dead"; break; + default: curState = "Unknown or Undefined"; break; + } + cmptType.PushDebug("Current Combat State") = curState; + + //switch (m_CombatBehaviorType) { + // case 0: curState = "Passive"; break; + // case 1: curState = "Aggressive"; break; + // case 2: curState = "Passive (Turret)"; break; + // case 3: curState = "Aggressive (Turret)"; break; + // default: curState = "Unknown or Undefined"; break; + //} + //cmptType.PushDebug("Current Combat Behavior State") = curState; + + //switch (m_CombatRole) { + // case 0: curState = "Melee"; break; + // case 1: curState = "Ranged"; break; + // case 2: curState = "Support"; break; + // default: curState = "Unknown or Undefined"; break; + //} + //cmptType.PushDebug("Current Combat Role") = curState; + + auto& tetherPoint = cmptType.PushDebug("Tether Point"); + tetherPoint.PushDebug("X") = m_StartPosition.x; + tetherPoint.PushDebug("Y") = m_StartPosition.y; + tetherPoint.PushDebug("Z") = m_StartPosition.z; + cmptType.PushDebug("Hard Tether Radius") = m_HardTetherRadius; + cmptType.PushDebug("Soft Tether Radius") = m_SoftTetherRadius; + cmptType.PushDebug("Aggro Radius") = m_AggroRadius; + cmptType.PushDebug("Tether Speed") = m_TetherSpeed; + cmptType.PushDebug("Aggro Speed") = m_TetherSpeed; + // cmptType.PushDebug("Specified Min Range") = m_SpecificMinRange; + // cmptType.PushDebug("Specified Max Range") = m_SpecificMaxRange; + auto& threats = cmptType.PushDebug("Target Threats"); + for (const auto& [id, threat] : m_ThreatEntries) { + threats.PushDebug(std::to_string(id)) = threat; + } + + auto& ignoredThreats = cmptType.PushDebug("Temp Ignored Threats"); + for (const auto& [id, threat] : m_RemovedThreatList) { + ignoredThreats.PushDebug(std::to_string(id) + " - Time") = threat; + } + auto& skillInfo = cmptType.PushDebug("Skill Info"); + for (const auto& skill : m_SkillEntries) { + auto& skillDebug = skillInfo.PushDebug("Skill ID " + std::to_string(skill.skillId)); + skillDebug.PushDebug("Cooldown") = skill.cooldown; + skillDebug.PushDebug("Ability Cooldown") = skill.abilityCooldown; + skillDebug.PushDebug("AI Combat Weight") = skill.combatWeight; + } + + return true; +} diff --git a/dGame/dComponents/BaseCombatAIComponent.h b/dGame/dComponents/BaseCombatAIComponent.h index 164b2ef5..728e0d4a 100644 --- a/dGame/dComponents/BaseCombatAIComponent.h +++ b/dGame/dComponents/BaseCombatAIComponent.h @@ -33,13 +33,15 @@ enum class AiState : uint32_t { */ struct AiSkillEntry { - uint32_t skillId; + uint32_t skillId{}; - float cooldown; + float cooldown{}; - float abilityCooldown; + float abilityCooldown{}; - Behavior* behavior; + Behavior* behavior{}; + + int32_t combatWeight{}; }; /** @@ -181,8 +183,9 @@ public: /** * Stuns the entity for a certain amount of time, will not work if the entity is stun immune * @param time the time to stun the entity, if stunnable + * @param force whether or not to force the stun and ignore checks */ - void Stun(float time); + void Stun(float time, const bool force = false); /** * Gets the radius that will cause this entity to get aggro'd, causing a target chase @@ -234,6 +237,10 @@ public: // Ignore a threat for a certain amount of time void IgnoreThreat(const LWOOBJID target, const float time); + bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + + void SetStartingPosition(const NiPoint3& pos) { m_StartPosition = pos; } + private: /** * Returns the current target or the target that currently is the largest threat to this entity @@ -392,9 +399,17 @@ private: */ bool m_DirtyStateOrTarget = false; + // Min amount of time to remain as in combat after casting a skill + float m_MinRoundLength = 0.0f; + + // max amount of time to remain as in combat after casting a skill + float m_MaxRoundLength = 0.0f; + // The amount of time the entity will be forced to tether for float m_ForcedTetherTime = 0.0f; + float m_CombatRoundLength = 0.0f; + // The amount of time a removed threat will be ignored for. std::map m_RemovedThreatList; diff --git a/dGame/dComponents/BouncerComponent.cpp b/dGame/dComponents/BouncerComponent.cpp index a7c7f1a8..3f0ffc9a 100644 --- a/dGame/dComponents/BouncerComponent.cpp +++ b/dGame/dComponents/BouncerComponent.cpp @@ -8,15 +8,30 @@ #include "GameMessages.h" #include "BitStream.h" #include "eTriggerEventType.h" +#include "Amf3.h" BouncerComponent::BouncerComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { m_PetEnabled = false; m_PetBouncerEnabled = false; m_PetSwitchLoaded = false; + m_Destination = GeneralUtils::TryParse( + GeneralUtils::SplitString(m_Parent->GetVarAsString(u"bouncer_destination"), '\x1f')) + .value_or(NiPoint3Constant::ZERO); + m_Speed = GeneralUtils::TryParse(m_Parent->GetVarAsString(u"bouncer_speed")).value_or(-1.0f); + m_UsesHighArc = GeneralUtils::TryParse(m_Parent->GetVarAsString(u"bouncer_uses_high_arc")).value_or(false); + m_LockControls = GeneralUtils::TryParse(m_Parent->GetVarAsString(u"lock_controls")).value_or(false); + m_IgnoreCollision = !GeneralUtils::TryParse(m_Parent->GetVarAsString(u"ignore_collision")).value_or(true); + m_StickLanding = GeneralUtils::TryParse(m_Parent->GetVarAsString(u"stickLanding")).value_or(false); + m_UsesGroupName = GeneralUtils::TryParse(m_Parent->GetVarAsString(u"uses_group_name")).value_or(false); + m_GroupName = m_Parent->GetVarAsString(u"grp_name"); + m_MinNumTargets = GeneralUtils::TryParse(m_Parent->GetVarAsString(u"num_targets_to_activate")).value_or(1); + m_CinematicPath = m_Parent->GetVarAsString(u"attached_cinematic_path"); if (parent->GetLOT() == 7625) { LookupPetSwitch(); } + + RegisterMsg(&BouncerComponent::MsgGetObjectReportInfo); } BouncerComponent::~BouncerComponent() { @@ -94,3 +109,53 @@ void BouncerComponent::LookupPetSwitch() { }); } } + +bool BouncerComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + auto& cmptType = reportInfo.info->PushDebug("Bouncer"); + cmptType.PushDebug("Component ID") = GetComponentID(); + auto& destPos = cmptType.PushDebug("Destination Position"); + if (m_Destination != NiPoint3Constant::ZERO) { + destPos.PushDebug(m_Destination); + } else { + destPos.PushDebug("WARNING: Bouncer has no target position, is likely missing config data"); + } + + + if (m_Speed == -1.0f) { + cmptType.PushDebug("WARNING: Bouncer has no speed value, is likely missing config data"); + } else { + cmptType.PushDebug("Bounce Speed") = m_Speed; + } + cmptType.PushDebug("Bounce trajectory arc") = m_UsesHighArc ? "High Arc" : "Low Arc"; + cmptType.PushDebug("Collision Enabled") = m_IgnoreCollision; + cmptType.PushDebug("Stick Landing") = m_StickLanding; + cmptType.PushDebug("Locks character's controls") = m_LockControls; + if (!m_CinematicPath.empty()) cmptType.PushDebug("Cinematic Camera Path (plays during bounce)") = m_CinematicPath; + + auto* switchComponent = m_Parent->GetComponent(); + auto& respondsToFactions = cmptType.PushDebug("Responds to Factions"); + if (!switchComponent || switchComponent->GetFactionsToRespondTo().empty()) respondsToFactions.PushDebug("Faction 1"); + else { + for (const auto faction : switchComponent->GetFactionsToRespondTo()) { + respondsToFactions.PushDebug(("Faction " + std::to_string(faction))); + } + } + + cmptType.PushDebug("Uses a group name for interactions") = m_UsesGroupName; + if (!m_UsesGroupName) { + if (m_MinNumTargets > 1) { + cmptType.PushDebug("WARNING: Bouncer has a required number of objects to activate, but no group for interactions."); + } + + if (!m_GroupName.empty()) { + cmptType.PushDebug("WARNING: Has a group name for interactions , but is marked to not use that name."); + } + } else { + if (m_GroupName.empty()) { + cmptType.PushDebug("WARNING: Set to use a group name for inter actions, but no group name is assigned"); + } + cmptType.PushDebug("Number of interactions to activate bouncer") = m_MinNumTargets; + } + + return true; +} diff --git a/dGame/dComponents/BouncerComponent.h b/dGame/dComponents/BouncerComponent.h index 53ba26fa..f0493ee9 100644 --- a/dGame/dComponents/BouncerComponent.h +++ b/dGame/dComponents/BouncerComponent.h @@ -51,6 +51,8 @@ public: */ void LookupPetSwitch(); + bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + private: /** * Whether this bouncer needs to be activated by a pet @@ -66,6 +68,36 @@ private: * Whether the pet switch for this bouncer has been located */ bool m_PetSwitchLoaded; + + // The bouncer destination + NiPoint3 m_Destination; + + // The speed at which the player is bounced + float m_Speed{}; + + // Whether to use a high arc for the bounce trajectory + bool m_UsesHighArc{}; + + // Lock controls when bouncing + bool m_LockControls{}; + + // Ignore collision when bouncing + bool m_IgnoreCollision{}; + + // Stick the landing afterwards or let the player slide + bool m_StickLanding{}; + + // Whether or not there is a group name + bool m_UsesGroupName{}; + + // The group name for targets + std::string m_GroupName{}; + + // The number of targets to activate the bouncer + int32_t m_MinNumTargets{}; + + // The cinematic path to play during the bounce + std::string m_CinematicPath{}; }; #endif // BOUNCERCOMPONENT_H diff --git a/dGame/dComponents/BuffComponent.cpp b/dGame/dComponents/BuffComponent.cpp index bf09d2ea..8d699b54 100644 --- a/dGame/dComponents/BuffComponent.cpp +++ b/dGame/dComponents/BuffComponent.cpp @@ -450,19 +450,10 @@ const std::vector& BuffComponent::GetBuffParameters(int32_t buffI param.value = result.getFloatField("NumberValue"); param.effectId = result.getIntField("EffectID"); - if (!result.fieldIsNull("StringValue")) { - std::istringstream stream(result.getStringField("StringValue")); - std::string token; - - while (std::getline(stream, token, ',')) { - try { - const auto value = std::stof(token); - - param.values.push_back(value); - } catch (std::invalid_argument& exception) { - LOG("Failed to parse value (%s): (%s)!", token.c_str(), exception.what()); - } - } + for (const auto& str : GeneralUtils::SplitString(result.getStringField("StringValue"), ',')) { + if (str.empty()) continue; + const auto value = GeneralUtils::TryParse(str); + if (value) param.values.push_back(value.value()); } parameters.push_back(param); diff --git a/dGame/dComponents/CharacterComponent.cpp b/dGame/dComponents/CharacterComponent.cpp index a0c44961..0c5230f8 100644 --- a/dGame/dComponents/CharacterComponent.cpp +++ b/dGame/dComponents/CharacterComponent.cpp @@ -24,6 +24,7 @@ #include "WorldPackets.h" #include "MessageType/Game.h" #include +#include CharacterComponent::CharacterComponent(Entity* parent, const int32_t componentID, Character* character, const SystemAddress& systemAddress) : Component(parent, componentID) { m_Character = character; @@ -49,11 +50,10 @@ CharacterComponent::CharacterComponent(Entity* parent, const int32_t componentID m_LastUpdateTimestamp = std::time(nullptr); m_SystemAddress = systemAddress; - RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &CharacterComponent::OnGetObjectReportInfo); + RegisterMsg(&CharacterComponent::OnGetObjectReportInfo); } -bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportInfo = static_cast(msg); +bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { auto& cmptType = reportInfo.info->PushDebug("Character"); @@ -492,7 +492,7 @@ Item* CharacterComponent::RocketEquip(Entity* player) { if (!rocket) return rocket; // build and define the rocket config - for (LDFBaseData* data : rocket->GetConfig()) { + for (const auto& data : rocket->GetConfig().values | std::views::values) { if (data->GetKey() == u"assemblyPartLOTs") { std::string newRocketStr = data->GetValueAsString() + ";"; GeneralUtils::ReplaceInString(newRocketStr, "+", ";"); @@ -515,12 +515,12 @@ void CharacterComponent::RocketUnEquip(Entity* player) { } void CharacterComponent::TrackMissionCompletion(bool isAchievement) { - UpdatePlayerStatistic(MissionsCompleted); - // Achievements are tracked separately for the zone if (isAchievement) { const auto mapID = Game::zoneManager->GetZoneID().GetMapID(); GetZoneStatisticsForMap(mapID).m_AchievementsCollected++; + } else { + UpdatePlayerStatistic(MissionsCompleted); } } @@ -798,8 +798,14 @@ std::string CharacterComponent::StatisticsToString() const { return result.str(); } -uint64_t CharacterComponent::GetStatisticFromSplit(std::vector split, uint32_t index) { - return split.size() > index ? std::stoull(split.at(index)) : 0; +uint64_t CharacterComponent::GetStatisticFromSplit(const std::vector& split, const uint32_t index) { + uint64_t toReturn = 0; + if (index < split.size()) { + const auto parsed = GeneralUtils::TryParse(split[index]); + if (parsed) toReturn = *parsed; + } + + return toReturn; } ZoneStatistics& CharacterComponent::GetZoneStatisticsForMap(LWOMAPID mapID) { diff --git a/dGame/dComponents/CharacterComponent.h b/dGame/dComponents/CharacterComponent.h index c1f107b5..a37dda43 100644 --- a/dGame/dComponents/CharacterComponent.h +++ b/dGame/dComponents/CharacterComponent.h @@ -17,6 +17,10 @@ enum class eGameActivity : uint32_t; class Item; +namespace GameMessages { + struct GetObjectReportInfo; +} + /** * The statistics that can be achieved per zone */ @@ -331,7 +335,7 @@ public: void LoadVisitedLevelsXml(const tinyxml2::XMLElement& doc); private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); /** * The map of active venture vision effects @@ -446,7 +450,7 @@ private: * @param index the statistics ID in the string * @return the integer value of this statistic, parsed from the string */ - static uint64_t GetStatisticFromSplit(std::vector split, uint32_t index); + static uint64_t GetStatisticFromSplit(const std::vector& split, const uint32_t index); /** * Gets all the statistics for a certain map, if it doesn't exist, it creates empty stats @@ -522,6 +526,7 @@ private: /** * Total amount of meters traveled by this character + * Should be a double and then truncated so decimals can be tracked */ uint64_t m_MetersTraveled; diff --git a/dGame/dComponents/CollectibleComponent.cpp b/dGame/dComponents/CollectibleComponent.cpp index f6ba25b2..10b50c2d 100644 --- a/dGame/dComponents/CollectibleComponent.cpp +++ b/dGame/dComponents/CollectibleComponent.cpp @@ -1,5 +1,37 @@ #include "CollectibleComponent.h" +#include "MissionComponent.h" +#include "dServer.h" +#include "Amf3.h" + +CollectibleComponent::CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId) : + Component(parentEntity, componentID), m_CollectibleId(collectibleId) { + RegisterMsg(&CollectibleComponent::MsgGetObjectReportInfo); +} + void CollectibleComponent::Serialize(RakNet::BitStream& outBitStream, bool isConstruction) { outBitStream.Write(GetCollectibleId()); } + +bool CollectibleComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportMsg) { + auto& cmptType = reportMsg.info->PushDebug("Collectible"); + auto collectibleID = static_cast(m_CollectibleId) + static_cast(Game::server->GetZoneID() << 8); + + cmptType.PushDebug("Component ID") = GetComponentID(); + + cmptType.PushDebug("Collectible ID") = GetCollectibleId(); + cmptType.PushDebug("Mission Tracking ID (for save data)") = collectibleID; + + auto* localCharEntity = Game::entityManager->GetEntity(reportMsg.clientID); + bool collected = false; + if (localCharEntity) { + auto* missionComponent = localCharEntity->GetComponent(); + + if (m_CollectibleId != 0) { + collected = missionComponent->HasCollectible(collectibleID); + } + } + + cmptType.PushDebug("Has been collected") = collected; + return true; +} diff --git a/dGame/dComponents/CollectibleComponent.h b/dGame/dComponents/CollectibleComponent.h index d9356112..5c9b5ea0 100644 --- a/dGame/dComponents/CollectibleComponent.h +++ b/dGame/dComponents/CollectibleComponent.h @@ -7,10 +7,12 @@ class CollectibleComponent final : public Component { public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::COLLECTIBLE; - CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId) : Component(parentEntity, componentID), m_CollectibleId(collectibleId) {} + CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId); int16_t GetCollectibleId() const { return m_CollectibleId; } void Serialize(RakNet::BitStream& outBitStream, bool isConstruction) override; + + bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); private: int16_t m_CollectibleId = 0; }; diff --git a/dGame/dComponents/Component.h b/dGame/dComponents/Component.h index b54a8fab..094ed9f3 100644 --- a/dGame/dComponents/Component.h +++ b/dGame/dComponents/Component.h @@ -55,17 +55,18 @@ public: virtual void LoadFromXml(const tinyxml2::XMLDocument& doc) {} virtual void Serialize(RakNet::BitStream& outBitStream, bool isConstruction) {} - protected: + template + inline void RegisterMsg(bool (GameObjClass::*handler)(DerivedMsg&)) { + static_assert(std::is_base_of_v, "DerivedMsg must inherit from GameMsg"); + static_assert(std::is_base_of_v, "GameObjClass must inherit from Component"); + const auto handlerBound = std::bind(handler, static_cast(this), std::placeholders::_1); + const auto castWrapper = [handlerBound](GameMessages::GameMsg& msg) { + return handlerBound(static_cast(msg)); + }; - inline void RegisterMsg(const MessageType::Game msgId, auto* self, const auto handler) { - m_Parent->RegisterMsg(msgId, std::bind(handler, self, std::placeholders::_1)); - } - - template - inline void RegisterMsg(auto* self, const auto handler) { - T msg; - RegisterMsg(msg.msgId, self, handler); + DerivedMsg msg; + m_Parent->RegisterMsg(msg.msgId, castWrapper); } /** diff --git a/dGame/dComponents/ControllablePhysicsComponent.cpp b/dGame/dComponents/ControllablePhysicsComponent.cpp index b2a41358..2b26368f 100644 --- a/dGame/dComponents/ControllablePhysicsComponent.cpp +++ b/dGame/dComponents/ControllablePhysicsComponent.cpp @@ -18,7 +18,7 @@ #include "Amf3.h" ControllablePhysicsComponent::ControllablePhysicsComponent(Entity* entity, const int32_t componentID) : PhysicsComponent(entity, componentID) { - RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &ControllablePhysicsComponent::OnGetObjectReportInfo); + RegisterMsg(&ControllablePhysicsComponent::OnGetObjectReportInfo); m_Velocity = {}; m_AngularVelocity = {}; @@ -359,9 +359,8 @@ void ControllablePhysicsComponent::SetStunImmunity( ); } -bool ControllablePhysicsComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - PhysicsComponent::OnGetObjectReportInfo(msg); - auto& reportInfo = static_cast(msg); +bool ControllablePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + PhysicsComponent::OnGetObjectReportInfo(reportInfo); auto& info = reportInfo.subCategory->PushDebug("Controllable Info"); auto& vel = info.PushDebug("Velocity"); diff --git a/dGame/dComponents/ControllablePhysicsComponent.h b/dGame/dComponents/ControllablePhysicsComponent.h index 419e9250..76de5b51 100644 --- a/dGame/dComponents/ControllablePhysicsComponent.h +++ b/dGame/dComponents/ControllablePhysicsComponent.h @@ -284,7 +284,7 @@ public: private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); /** * The entity that owns this component */ diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 0658757c..90591367 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -48,7 +48,6 @@ Implementation DestroyableComponent::IsEnemyImplentation; Implementation DestroyableComponent::IsFriendImplentation; DestroyableComponent::DestroyableComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { - using namespace GameMessages; m_iArmor = 0; m_fMaxArmor = 0.0f; m_iImagination = 0; @@ -86,8 +85,9 @@ DestroyableComponent::DestroyableComponent(Entity* parent, const int32_t compone m_DamageCooldownTimer = 0.0f; - RegisterMsg(this, &DestroyableComponent::OnGetObjectReportInfo); - RegisterMsg(this, &DestroyableComponent::OnSetFaction); + RegisterMsg(&DestroyableComponent::OnGetObjectReportInfo); + RegisterMsg(&DestroyableComponent::OnSetFaction); + RegisterMsg(&DestroyableComponent::OnIsDead); } DestroyableComponent::~DestroyableComponent() { @@ -754,7 +754,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType const auto isPlayer = m_Parent->IsPlayer(); - GameMessages::SendDie(m_Parent, source, source, true, killType, deathType, 0, 0, 0, isPlayer, false, 1); + GameMessages::SendDie(m_Parent, source, source, true, killType, deathType, 0, 0, 0, isPlayer, true, 1); //NANI?! if (!isPlayer) { @@ -784,8 +784,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType lootMsg.sourceID = source; lootMsg.item = LOT_NULL; lootMsg.Send(); - lootMsg.Send(m_Parent->GetSystemAddress()); - character->SetCoins(coinsTotal, eLootSourceType::PICKUP); + character->SetCoins(coinsTotal, eLootSourceType::DELETION); } } @@ -794,7 +793,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType std::vector scriptedActs = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::SCRIPTED_ACTIVITY); for (Entity* scriptEntity : scriptedActs) { - if (scriptEntity->GetObjectID() != zoneControl->GetObjectID()) { // Don't want to trigger twice on instance worlds + if (!zoneControl || scriptEntity->GetObjectID() != zoneControl->GetObjectID()) { // Don't want to trigger twice on instance worlds scriptEntity->GetScript()->OnPlayerDied(scriptEntity, m_Parent); } } @@ -882,9 +881,9 @@ void DestroyableComponent::FixStats() { int32_t currentImagination = destroyableComponent->GetImagination(); // Unequip all items - auto equipped = inventoryComponent->GetEquippedItems(); + const auto equipped = inventoryComponent->GetEquippedItems(); - for (auto& equippedItem : equipped) { + for (const auto& equippedItem : equipped) { // Get the item with the item ID auto* item = inventoryComponent->FindItemById(equippedItem.second.id); @@ -925,7 +924,7 @@ void DestroyableComponent::FixStats() { buffComponent->ReApplyBuffs(); // Requip all items - for (auto& equippedItem : equipped) { + for (const auto& equippedItem : equipped) { // Get the item with the item ID auto* item = inventoryComponent->FindItemById(equippedItem.second.id); @@ -965,6 +964,8 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) { if (m_Parent->IsPlayer()) { //remove hardcore_lose_uscore_on_death_percent from the player's uscore: auto* character = m_Parent->GetComponent(); + if (!character) return; + auto uscore = character->GetUScore(); auto uscoreToLose = static_cast(uscore * (Game::entityManager->GetHardcoreLoseUscoreOnDeathPercent() / 100.0f)); @@ -1061,8 +1062,7 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) { } } -bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportInfo = static_cast(msg); +bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { auto& destroyableInfo = reportInfo.info->PushDebug("Destroyable"); destroyableInfo.PushDebug("DestructibleComponent DB Table Template ID") = m_ComponentID; @@ -1122,8 +1122,8 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { stats.PushDebug("Imagination") = m_iImagination; stats.PushDebug("Maximum Imagination") = m_fMaxImagination; stats.PushDebug("Damage Absorption Points") = m_DamageToAbsorb; - destroyableInfo.PushDebug("Is GM Immune") = m_IsGMImmune; - destroyableInfo.PushDebug("Is Shielded") = m_IsShielded; + stats.PushDebug("Is GM Immune") = m_IsGMImmune; + stats.PushDebug("Is Shielded") = m_IsShielded; destroyableInfo.PushDebug("Attacks To Block") = m_AttacksToBlock; destroyableInfo.PushDebug("Damage Reduction") = m_DamageReduction; std::stringstream factionsStream; @@ -1140,7 +1140,7 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { destroyableInfo.PushDebug("Enemy Factions") = factionsStream.str(); - destroyableInfo.PushDebug("Is Smashable") = m_IsSmashable; + destroyableInfo.PushDebug("Is A Smashable") = m_IsSmashable; destroyableInfo.PushDebug("Is Smashed") = m_IsSmashed; destroyableInfo.PushDebug("Is Module Assembly") = m_IsModuleAssembly; destroyableInfo.PushDebug("Explode Factor") = m_ExplodeFactor; @@ -1184,10 +1184,14 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { return true; } -bool DestroyableComponent::OnSetFaction(GameMessages::GameMsg& msg) { - auto& modifyFaction = static_cast(msg); +bool DestroyableComponent::OnSetFaction(GameMessages::SetFaction& setFaction) { m_DirtyHealth = true; Game::entityManager->SerializeEntity(m_Parent); - SetFaction(modifyFaction.factionID, modifyFaction.bIgnoreChecks); + SetFaction(setFaction.factionID, setFaction.bIgnoreChecks); + return true; +} + +bool DestroyableComponent::OnIsDead(GameMessages::IsDead& isDead) { + isDead.bDead = m_IsDead || (GetHealth() == 0 && GetArmor() == 0); return true; } diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index 9b3e46af..7ef55a21 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -11,6 +11,8 @@ namespace GameMessages { struct GetObjectReportInfo; + struct SetFaction; + struct IsDead; }; namespace CppScripts { @@ -470,8 +472,9 @@ public: // handle hardcode mode drops void DoHardcoreModeDrops(const LWOOBJID source); - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); - bool OnSetFaction(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + bool OnSetFaction(GameMessages::SetFaction& setFaction); + bool OnIsDead(GameMessages::IsDead& isDead); void SetIsDead(const bool value) { m_IsDead = value; } diff --git a/dGame/dComponents/GhostComponent.cpp b/dGame/dComponents/GhostComponent.cpp index 4755a1f9..c27cd31a 100644 --- a/dGame/dComponents/GhostComponent.cpp +++ b/dGame/dComponents/GhostComponent.cpp @@ -1,9 +1,21 @@ #include "GhostComponent.h" +#include "PlayerManager.h" +#include "Character.h" +#include "ControllablePhysicsComponent.h" +#include "UserManager.h" +#include "User.h" + +#include "Amf3.h" +#include "GameMessages.h" GhostComponent::GhostComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { m_GhostReferencePoint = NiPoint3Constant::ZERO; m_GhostOverridePoint = NiPoint3Constant::ZERO; m_GhostOverride = false; + + RegisterMsg(&GhostComponent::OnToggleGMInvis); + RegisterMsg(&GhostComponent::OnGetGMInvis); + RegisterMsg(&GhostComponent::MsgGetObjectReportInfo); } GhostComponent::~GhostComponent() { @@ -55,3 +67,48 @@ bool GhostComponent::IsObserved(LWOOBJID id) { void GhostComponent::GhostEntity(LWOOBJID id) { m_ObservedEntities.erase(id); } + +bool GhostComponent::OnToggleGMInvis(GameMessages::ToggleGMInvis& gmInvisMsg) { + gmInvisMsg.bStateOut = !m_IsGMInvisible; + m_IsGMInvisible = !m_IsGMInvisible; + LOG_DEBUG("GM Invisibility toggled to: %s", m_IsGMInvisible ? "true" : "false"); + gmInvisMsg.Send(UNASSIGNED_SYSTEM_ADDRESS); + auto* thisUser = UserManager::Instance()->GetUser(m_Parent->GetSystemAddress()); + if (!thisUser) { + LOG("Unable to find user for entity %llu when toggling GM invisibility!", m_Parent->GetObjectID()); + return false; + } + + for (const auto& player : PlayerManager::GetAllPlayers()) { + if (!player || player->GetObjectID() == m_Parent->GetObjectID()) continue; + auto* toUser = UserManager::Instance()->GetUser(player->GetSystemAddress()); + if (m_IsGMInvisible) { + if (toUser->GetMaxGMLevel() < thisUser->GetMaxGMLevel()) { + Game::entityManager->DestructEntity(m_Parent, player->GetSystemAddress()); + } + } else { + if (toUser->GetMaxGMLevel() < thisUser->GetMaxGMLevel()) { + Game::entityManager->ConstructEntity(m_Parent, player->GetSystemAddress()); + auto* controllableComp = m_Parent->GetComponent(); + controllableComp->SetDirtyPosition(true); + } + } + } + Game::entityManager->SerializeEntity(m_Parent); + + return true; +} + +bool GhostComponent::OnGetGMInvis(GameMessages::GetGMInvis& gmInvisMsg) { + LOG_DEBUG("GM Invisibility requested: %s", m_IsGMInvisible ? "true" : "false"); + gmInvisMsg.bGMInvis = m_IsGMInvisible; + return gmInvisMsg.bGMInvis; +} + +bool GhostComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportMsg) { + auto& cmptType = reportMsg.info->PushDebug("Ghost"); + cmptType.PushDebug("Component ID") = GetComponentID(); + cmptType.PushDebug("Is GM Invis") = false; + + return true; +} diff --git a/dGame/dComponents/GhostComponent.h b/dGame/dComponents/GhostComponent.h index edf05c13..13a89640 100644 --- a/dGame/dComponents/GhostComponent.h +++ b/dGame/dComponents/GhostComponent.h @@ -7,6 +7,10 @@ class NiPoint3; +namespace tinyxml2 { + class XMLDocument; +} + class GhostComponent final : public Component { public: static inline const eReplicaComponentType ComponentType = eReplicaComponentType::GHOST; @@ -39,7 +43,14 @@ public: void GhostEntity(const LWOOBJID id); + bool OnToggleGMInvis(GameMessages::ToggleGMInvis& msg); + + bool OnGetGMInvis(GameMessages::GetGMInvis& msg); + + bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg); + private: + NiPoint3 m_GhostReferencePoint; NiPoint3 m_GhostOverridePoint; @@ -49,6 +60,9 @@ private: std::unordered_set m_LimboConstructions; bool m_GhostOverride; + + bool m_IsGMInvisible{ false }; + }; #endif //!__GHOSTCOMPONENT__H__ diff --git a/dGame/dComponents/HavokVehiclePhysicsComponent.cpp b/dGame/dComponents/HavokVehiclePhysicsComponent.cpp index afec4427..52819657 100644 --- a/dGame/dComponents/HavokVehiclePhysicsComponent.cpp +++ b/dGame/dComponents/HavokVehiclePhysicsComponent.cpp @@ -3,7 +3,7 @@ #include "Amf3.h" HavokVehiclePhysicsComponent::HavokVehiclePhysicsComponent(Entity* parent, const int32_t componentID) : PhysicsComponent(parent, componentID) { - RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &HavokVehiclePhysicsComponent::OnGetObjectReportInfo); + RegisterMsg(&HavokVehiclePhysicsComponent::OnGetObjectReportInfo); m_Velocity = NiPoint3Constant::ZERO; m_AngularVelocity = NiPoint3Constant::ZERO; @@ -102,9 +102,8 @@ void HavokVehiclePhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bo outBitStream.Write0(); } -bool HavokVehiclePhysicsComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - PhysicsComponent::OnGetObjectReportInfo(msg); - auto& reportInfo = static_cast(msg); +bool HavokVehiclePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + PhysicsComponent::OnGetObjectReportInfo(reportInfo); if (!reportInfo.subCategory) { return false; } diff --git a/dGame/dComponents/HavokVehiclePhysicsComponent.h b/dGame/dComponents/HavokVehiclePhysicsComponent.h index 3a84adca..3b2aba78 100644 --- a/dGame/dComponents/HavokVehiclePhysicsComponent.h +++ b/dGame/dComponents/HavokVehiclePhysicsComponent.h @@ -68,7 +68,7 @@ public: void SetRemoteInputInfo(const RemoteInputInfo&); private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); NiPoint3 m_Velocity; NiPoint3 m_AngularVelocity; diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index 212572ae..2f3436fe 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -44,8 +44,7 @@ #include InventoryComponent::InventoryComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { - using namespace GameMessages; - RegisterMsg(this, &InventoryComponent::OnGetObjectReportInfo); + RegisterMsg(&InventoryComponent::OnGetObjectReportInfo); this->m_Dirty = true; this->m_Equipped = {}; this->m_Pushed = {}; @@ -174,7 +173,7 @@ void InventoryComponent::AddItem( const uint32_t count, eLootSourceType lootSourceType, eInventoryType inventoryType, - const std::vector& config, + const LwoNameValue& config, const LWOOBJID parent, const bool showFlyingLoot, bool isModMoveAndEquip, @@ -205,7 +204,7 @@ void InventoryComponent::AddItem( auto* inventory = GetInventory(inventoryType); - if (!config.empty() || bound) { + if (!config.values.empty() || bound) { const auto slot = preferredSlot != -1 && inventory->IsSlotEmpty(preferredSlot) ? preferredSlot : inventory->FindEmptySlot(); if (slot == -1) { @@ -357,7 +356,7 @@ void InventoryComponent::MoveItemToInventory(Item* item, const eInventoryType in const auto subkey = item->GetSubKey(); - if (subkey == LWOOBJID_EMPTY && item->GetConfig().empty() && (!item->GetBound() || (item->GetBound() && item->GetInfo().isBOP))) { + if (subkey == LWOOBJID_EMPTY && item->GetConfig().values.empty() && (!item->GetBound() || (item->GetBound() && item->GetInfo().isBOP))) { auto left = std::min(count, origin->GetLotCount(lot)); while (left > 0) { @@ -380,11 +379,7 @@ void InventoryComponent::MoveItemToInventory(Item* item, const eInventoryType in isModMoveAndEquip = false; } } else { - std::vector config; - - for (auto* const data : item->GetConfig()) { - config.push_back(data->Copy()); - } + const auto config = item->GetConfig(); const auto delta = std::min(item->GetCount(), count); @@ -745,18 +740,17 @@ void InventoryComponent::Serialize(RakNet::BitStream& outBitStream, const bool b outBitStream.Write0(); - bool flag = !item.config.empty(); + bool flag = !item.config.values.empty(); outBitStream.Write(flag); if (flag) { RakNet::BitStream ldfStream; - ldfStream.Write(item.config.size()); // Key count - for (LDFBaseData* data : item.config) { + ldfStream.Write(item.config.values.size()); // Key count + for (const auto& data : item.config.values | std::views::values) { if (data->GetKey() == u"assemblyPartLOTs") { std::string newRocketStr = data->GetValueAsString() + ";"; GeneralUtils::ReplaceInString(newRocketStr, "+", ";"); - LDFData* ldf_data = new LDFData(u"assemblyPartLOTs", GeneralUtils::ASCIIToUTF16(newRocketStr)); - ldf_data->WriteToPacket(ldfStream); - delete ldf_data; + LDFData ldf_data(u"assemblyPartLOTs", GeneralUtils::ASCIIToUTF16(newRocketStr)); + ldf_data.WriteToPacket(ldfStream); } else { data->WriteToPacket(ldfStream); } @@ -783,7 +777,7 @@ void InventoryComponent::Update(float deltaTime) { } } -void InventoryComponent::UpdateSlot(const std::string& location, EquippedItem item, bool keepCurrent) { +void InventoryComponent::UpdateSlot(const std::string& location, const EquippedItem& item, bool keepCurrent) { const auto index = m_Equipped.find(location); if (index != m_Equipped.end()) { @@ -967,8 +961,9 @@ void InventoryComponent::EquipScripts(Item* equippedItem) { auto* itemScript = CppScripts::GetScript(m_Parent, scriptCompData.script_name); if (!itemScript) { LOG("null script?"); + } else { + itemScript->OnFactionTriggerItemEquipped(m_Parent, equippedItem->GetId()); } - itemScript->OnFactionTriggerItemEquipped(m_Parent, equippedItem->GetId()); } } @@ -982,8 +977,9 @@ void InventoryComponent::UnequipScripts(Item* unequippedItem) { auto* itemScript = CppScripts::GetScript(m_Parent, scriptCompData.script_name); if (!itemScript) { LOG("null script?"); + } else { + itemScript->OnFactionTriggerItemUnequipped(m_Parent, unequippedItem->GetId()); } - itemScript->OnFactionTriggerItemUnequipped(m_Parent, unequippedItem->GetId()); } } @@ -1079,7 +1075,7 @@ void InventoryComponent::PushEquippedItems() { } void InventoryComponent::PopEquippedItems() { - auto current = m_Equipped; + const auto current = m_Equipped; for (const auto& pair : current) { auto* const item = FindItemById(pair.second.id); @@ -1634,7 +1630,7 @@ void InventoryComponent::LoadPetXml(const tinyxml2::XMLDocument& document) { DatabasePet databasePet; databasePet.lot = lot; databasePet.moderationState = moderationStatus; - databasePet.name = std::string(name); + databasePet.name = name ? name : ""; SetDatabasePet(id, databasePet); @@ -1847,9 +1843,8 @@ std::string DebugInvToString(const eInventoryType inv, bool verbose) { return ""; } -bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& report = static_cast(msg); - auto& cmpt = report.info->PushDebug("Inventory"); +bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + auto& cmpt = reportInfo.info->PushDebug("Inventory"); cmpt.PushDebug("Component ID") = GetComponentID(); uint32_t numItems = 0; for (auto* inventory : m_Inventories | std::views::values) numItems += inventory->GetItems().size(); @@ -1859,7 +1854,7 @@ bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { for (const auto& [id, inventoryMut] : m_Inventories) { if (!inventoryMut) continue; const auto* const inventory = inventoryMut; - auto& curInv = itemsInBags.PushDebug(DebugInvToString(id, report.bVerbose) + " - " + std::to_string(id)); + auto& curInv = itemsInBags.PushDebug(DebugInvToString(id, reportInfo.bVerbose) + " - " + std::to_string(id)); for (uint32_t i = 0; i < inventory->GetSize(); i++) { const auto* const item = inventory->FindItemBySlot(i); if (!item) continue; @@ -1876,7 +1871,7 @@ bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { slot.PushDebug("Bind on equip") = item->GetInfo().isBOE; slot.PushDebug("Is currently bound") = item->GetBound(); auto& extra = slot.PushDebug("Extra Info"); - for (const auto* const setting : item->GetConfig()) { + for (const auto& setting : item->GetConfig().values | std::views::values) { if (setting) extra.PushDebug(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString(); } } @@ -1892,7 +1887,7 @@ bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { equipSlot.PushDebug("Slot") = info.slot; equipSlot.PushDebug("Count") = info.count; auto& extra = equipSlot.PushDebug("Extra Info"); - for (const auto* const setting : info.config) { + for (const auto& setting : info.config.values | std::views::values) { if (setting) extra.PushDebug(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString(); } } diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index 3728c428..74ad326d 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -31,6 +31,10 @@ typedef std::map EquipmentMap; enum class eItemType : int32_t; +namespace GameMessages { + struct GetObjectReportInfo; +} + /** * Handles the inventory of entity, including the items they possess and have equipped. An entity can have inventories * of different types, each type representing a different group of items, see `eInventoryType` for a list of @@ -130,7 +134,7 @@ public: uint32_t count, eLootSourceType lootSourceType = eLootSourceType::NONE, eInventoryType inventoryType = INVALID, - const std::vector& config = {}, + const LwoNameValue& config = {}, LWOOBJID parent = LWOOBJID_EMPTY, bool showFlyingLoot = true, bool isModMoveAndEquip = false, @@ -209,7 +213,7 @@ public: * @param item the item to place * @param keepCurrent stores the item in an additional temp slot if there's already an item equipped */ - void UpdateSlot(const std::string& location, EquippedItem item, bool keepCurrent = false); + void UpdateSlot(const std::string& location, const EquippedItem& item, bool keepCurrent = false); /** * Removes a slot from the inventory @@ -411,7 +415,7 @@ public: // Used to migrate a character version, no need to call outside of that context void RegenerateItemIDs(); - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); ~InventoryComponent() override; diff --git a/dGame/dComponents/LUPExhibitComponent.h b/dGame/dComponents/LUPExhibitComponent.h index 4e93dbf9..f3450304 100644 --- a/dGame/dComponents/LUPExhibitComponent.h +++ b/dGame/dComponents/LUPExhibitComponent.h @@ -22,7 +22,7 @@ public: void NextLUPExhibit(); private: float m_UpdateTimer = 0.0f; - std::array m_LUPExhibits = { 11121, 11295, 11423, 11979 }; + const std::array m_LUPExhibits = { 11121, 11295, 11423, 11979 }; uint8_t m_LUPExhibitIndex = 0; bool m_DirtyLUPExhibit = true; }; diff --git a/dGame/dComponents/MissionComponent.cpp b/dGame/dComponents/MissionComponent.cpp index b3a503d5..08c4ce8d 100644 --- a/dGame/dComponents/MissionComponent.cpp +++ b/dGame/dComponents/MissionComponent.cpp @@ -3,6 +3,7 @@ * Copyright 2019 */ +#include #include #include @@ -27,12 +28,11 @@ std::unordered_map> MissionComponent: //! Initializer MissionComponent::MissionComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { - using namespace GameMessages; m_LastUsedMissionOrderUID = Game::zoneManager->GetUniqueMissionIdStartingValue(); - RegisterMsg(this, &MissionComponent::OnGetObjectReportInfo); - RegisterMsg(this, &MissionComponent::OnGetMissionState); - RegisterMsg(this, &MissionComponent::OnMissionNeedsLot); + RegisterMsg(&MissionComponent::OnGetObjectReportInfo); + RegisterMsg(&MissionComponent::OnGetMissionState); + RegisterMsg(&MissionComponent::OnMissionNeedsLot); } //! Destructor @@ -143,13 +143,22 @@ void MissionComponent::RemoveMission(uint32_t missionId) { void MissionComponent::Progress(eMissionTaskType type, int32_t value, LWOOBJID associate, const std::string& targets, int32_t count, bool ignoreAchievements) { LOG("Progressing missions %s %i %llu %s %s", StringifiedEnum::ToString(type).data(), value, associate, targets.c_str(), ignoreAchievements ? "(ignoring achievements)" : ""); - std::vector acceptedAchievements; - if (count > 0 && !ignoreAchievements) { - acceptedAchievements = LookForAchievements(type, value, true, associate, targets, count); + + // If we are already iterating m_Missions, defer this call to avoid iterator invalidation + // from re-entrant insertions (e.g. a completing achievement triggering LookForAchievements). + if (m_IsProgressing) { + m_PendingProgress.push_back({ type, value, associate, targets, count, ignoreAchievements }); + return; } + std::vector acceptedAchievements; + if (count > 0 && !ignoreAchievements) { + acceptedAchievements = LookForAchievements(type, value, associate, targets, count); + } + + m_IsProgressing = true; for (const auto& [id, mission] : m_Missions) { - if (!mission || std::find(acceptedAchievements.begin(), acceptedAchievements.end(), mission->GetMissionId()) != acceptedAchievements.end()) continue; + if (!mission) continue; if (mission->IsAchievement() && ignoreAchievements) continue; @@ -157,6 +166,18 @@ void MissionComponent::Progress(eMissionTaskType type, int32_t value, LWOOBJID a mission->Progress(type, value, associate, targets, count); } + m_IsProgressing = false; + + // Drain any Progress() calls that were deferred during the loop above. + // Each call here may itself defer further calls, which are drained recursively + // before returning, so the while loop only needs one pass in practice. + while (!m_PendingProgress.empty()) { + auto pending = std::move(m_PendingProgress); + m_PendingProgress.clear(); + for (const auto& p : pending) { + Progress(p.type, p.value, p.associate, p.targets, p.count, p.ignoreAchievements); + } + } } void MissionComponent::ForceProgress(const uint32_t missionId, const uint32_t taskId, const int32_t value, const bool acceptMission) { @@ -279,10 +300,7 @@ bool MissionComponent::GetMissionInfo(uint32_t missionId, CDMissions& result) { return true; } -#define MISSION_NEW_METHOD - -const std::vector MissionComponent::LookForAchievements(eMissionTaskType type, int32_t value, bool progress, LWOOBJID associate, const std::string& targets, int32_t count) { -#ifdef MISSION_NEW_METHOD +const std::vector MissionComponent::LookForAchievements(eMissionTaskType type, int32_t value, LWOOBJID associate, const std::string& targets, int32_t count) { // Query for achievments, using the cache const auto& result = QueryAchievements(type, value, targets); @@ -309,85 +327,9 @@ const std::vector MissionComponent::LookForAchievements(eMissionTaskTy instance->Accept(); acceptedAchievements.push_back(missionID); - - if (progress) { - // Progress mission to bring it up to speed - instance->Progress(type, value, associate, targets, count); - } } return acceptedAchievements; -#else - auto* missionTasksTable = CDClientManager::GetTable(); - auto* missionsTable = CDClientManager::GetTable(); - - auto tasks = missionTasksTable->Query([=](const CDMissionTasks& entry) { - return entry.taskType == static_cast(type); - }); - - std::vector acceptedAchievements; - - for (const auto& task : tasks) { - if (GetMission(task.id) != nullptr) { - continue; - } - - const auto missionEntries = missionsTable->Query([=](const CDMissions& entry) { - return entry.id == static_cast(task.id) && !entry.isMission; - }); - - if (missionEntries.empty()) { - continue; - } - - const auto mission = missionEntries[0]; - - if (mission.isMission || !MissionPrerequisites::CanAccept(mission.id, m_Missions)) { - continue; - } - - if (task.target != value && task.targetGroup != targets) { - auto stream = std::istringstream(task.targetGroup); - std::string token; - - auto found = false; - - while (std::getline(stream, token, ',')) { - try { - const auto target = std::stoul(token); - - found = target == value; - - if (found) { - break; - } - } catch (std::invalid_argument& exception) { - LOG("Failed to parse target (%s): (%s)!", token.c_str(), exception.what()); - } - } - - if (!found) { - continue; - } - } - - auto* instance = new Mission(this, mission.id); - - m_Missions.insert_or_assign(mission.id, instance); - - if (instance->IsMission()) instance->SetUniqueMissionOrderID(++m_LastUsedMissionOrderUID); - - instance->Accept(); - - acceptedAchievements.push_back(mission.id); - - if (progress) { - instance->Progress(type, value, associate, targets, count); - } - } - - return acceptedAchievements; -#endif } const std::vector& MissionComponent::QueryAchievements(eMissionTaskType type, int32_t value, const std::string targets) { @@ -496,7 +438,7 @@ bool MissionComponent::RequiresItem(const LOT lot) { } } - const auto required = LookForAchievements(eMissionTaskType::GATHER, lot, false); + const auto required = LookForAchievements(eMissionTaskType::GATHER, lot); return !required.empty(); } @@ -507,45 +449,48 @@ void MissionComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) { if (mis == nullptr) return; - auto* cur = mis->FirstChildElement("cur"); auto* done = mis->FirstChildElement("done"); - - auto* doneM = done->FirstChildElement(); - - while (doneM) { - int missionId; - - doneM->QueryAttribute("id", &missionId); - - auto* mission = new Mission(this, missionId); - - mission->LoadFromXmlDone(*doneM); - - doneM = doneM->NextSiblingElement(); - - m_Missions.insert_or_assign(missionId, mission); - } - - auto* currentM = cur->FirstChildElement(); - - uint32_t missionOrder{}; - while (currentM) { - int missionId; - - currentM->QueryAttribute("id", &missionId); - - auto* mission = m_Missions.contains(missionId) ? m_Missions[missionId] : new Mission(this, missionId); - - mission->LoadFromXmlCur(*currentM); - - if (currentM->QueryAttribute("o", &missionOrder) == tinyxml2::XML_SUCCESS && mission->IsMission()) { - mission->SetUniqueMissionOrderID(missionOrder); - if (missionOrder > m_LastUsedMissionOrderUID) m_LastUsedMissionOrderUID = missionOrder; + if (done) { + auto* doneM = done->FirstChildElement(); + + while (doneM) { + int missionId; + + doneM->QueryAttribute("id", &missionId); + + auto* mission = new Mission(this, missionId); + + mission->LoadFromXmlDone(*doneM); + + doneM = doneM->NextSiblingElement(); + + m_Missions.insert_or_assign(missionId, mission); } + } + + auto* cur = mis->FirstChildElement("cur"); + if (cur) { + auto* currentM = cur->FirstChildElement(); - currentM = currentM->NextSiblingElement(); + uint32_t missionOrder{}; + while (currentM) { + int missionId; - m_Missions.insert_or_assign(missionId, mission); + currentM->QueryAttribute("id", &missionId); + + auto* mission = m_Missions.contains(missionId) ? m_Missions[missionId] : new Mission(this, missionId); + + mission->LoadFromXmlCur(*currentM); + + if (currentM->QueryAttribute("o", &missionOrder) == tinyxml2::XML_SUCCESS && mission->IsMission()) { + mission->SetUniqueMissionOrderID(missionOrder); + if (missionOrder > m_LastUsedMissionOrderUID) m_LastUsedMissionOrderUID = missionOrder; + } + + currentM = currentM->NextSiblingElement(); + + m_Missions.insert_or_assign(missionId, mission); + } } } @@ -706,9 +651,8 @@ void PushMissions(const std::map& missions, AMFArrayValue& V } } -bool MissionComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportMsg = static_cast(msg); - auto& missionInfo = reportMsg.info->PushDebug("Mission (Laggy)"); +bool MissionComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + auto& missionInfo = reportInfo.info->PushDebug("Mission (Laggy)"); missionInfo.PushDebug("Component ID") = GetComponentID(); // Sort the missions so they are easier to parse and present to the end user std::map achievements; @@ -724,26 +668,51 @@ bool MissionComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { // None of these should be empty, but if they are dont print the field if (!achievements.empty() || !missions.empty()) { auto& incompleteMissions = missionInfo.PushDebug("Incomplete Missions"); - PushMissions(achievements, incompleteMissions, reportMsg.bVerbose); - PushMissions(missions, incompleteMissions, reportMsg.bVerbose); + PushMissions(achievements, incompleteMissions, reportInfo.bVerbose); + PushMissions(missions, incompleteMissions, reportInfo.bVerbose); } if (!doneMissions.empty()) { auto& completeMissions = missionInfo.PushDebug("Completed Missions"); - PushMissions(doneMissions, completeMissions, reportMsg.bVerbose); + PushMissions(doneMissions, completeMissions, reportInfo.bVerbose); } return true; } -bool MissionComponent::OnGetMissionState(GameMessages::GameMsg& msg) { - auto misState = static_cast(msg); - misState.missionState = GetMissionState(misState.missionID); +bool MissionComponent::OnGetMissionState(GameMessages::GetMissionState& getMissionState) { + getMissionState.missionState = GetMissionState(getMissionState.missionID); return true; } -bool MissionComponent::OnMissionNeedsLot(GameMessages::GameMsg& msg) { - const auto& needMsg = static_cast(msg); - return RequiresItem(needMsg.item); +bool MissionComponent::OnMissionNeedsLot(GameMessages::MissionNeedsLot& missionNeedsLot) { + return RequiresItem(missionNeedsLot.item); +} + +void MissionComponent::FixRacingMetaMissions() { + for (auto* const mission : m_Missions | std::views::values) { + if (!mission || mission->IsComplete()) continue; + + for (auto* const task : mission->GetTasks()) { + if (!task) continue; + + // has to be a racing meta mission and have a taskparam1 of 4 + if (task->GetType() != eMissionTaskType::RACING || !task->GetClientInfo().taskParam1.starts_with("4")) continue; + + // Each target is racing mission that needs to be completed. + // If its completed, progress the meta task by 1. + uint32_t progress = 0; + for (const auto& target : task->GetAllTargets()) { + if (target == 0) continue; + auto* racingMission = GetMission(target); + if (racingMission && racingMission->IsComplete()) { + progress++; + } + } + task->SetProgress(progress); + } + // in case the mission is actually complete, give them the rewards + mission->CheckCompletion(); + } } diff --git a/dGame/dComponents/MissionComponent.h b/dGame/dComponents/MissionComponent.h index a5cd058a..8545b171 100644 --- a/dGame/dComponents/MissionComponent.h +++ b/dGame/dComponents/MissionComponent.h @@ -134,13 +134,12 @@ public: * Checks if there's any achievements we might be able to accept for the given parameters * @param type the task type for tasks in the achievement that we wish to progress * @param value the value to progress by - * @param progress if we can accept the mission, this will apply the progression * @param associate optional associate related to mission progression * @param targets optional multiple targets related to mission progression * @param count the number of values to progress by (differs by task type) * @return true if a achievement was accepted, false otherwise */ - const std::vector LookForAchievements(eMissionTaskType type, int32_t value, bool progress = true, LWOOBJID associate = LWOOBJID_EMPTY, const std::string& targets = "", int32_t count = 1); + const std::vector LookForAchievements(eMissionTaskType type, int32_t value, LWOOBJID associate = LWOOBJID_EMPTY, const std::string& targets = "", int32_t count = 1); /** * Checks if there's a mission active that requires the collection of the specified LOT @@ -170,10 +169,12 @@ public: bool HasMission(uint32_t missionId); void ResetMission(const int32_t missionId); + + void FixRacingMetaMissions(); private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); - bool OnGetMissionState(GameMessages::GameMsg& msg); - bool OnMissionNeedsLot(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + bool OnGetMissionState(GameMessages::GetMissionState& getMissionState); + bool OnMissionNeedsLot(GameMessages::MissionNeedsLot& missionNeedsLot); /** * All the missions owned by this entity, mapped by mission ID */ @@ -206,6 +207,25 @@ private: * In live this value started at 745. */ uint32_t m_LastUsedMissionOrderUID = 746U; + + /** + * Holds arguments for a Progress() call that arrived re-entrantly while m_Missions was + * being iterated, so it can be replayed after the active loop finishes. + */ + struct PendingProgress { + eMissionTaskType type{}; + int32_t value{}; + LWOOBJID associate{}; + std::string targets; + int32_t count{}; + bool ignoreAchievements{}; + }; + + /// True while the m_Missions range-for loop in Progress() is executing. + bool m_IsProgressing = false; + + /// Re-entrant Progress() calls deferred here for replay after the active loop finishes. + std::vector m_PendingProgress; }; #endif // MISSIONCOMPONENT_H diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index 85d206e2..48585237 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -8,40 +8,43 @@ #include "ControlBehaviorMsgs.h" #include "tinyxml2.h" #include "InventoryComponent.h" +#include "MissionComponent.h" #include "SimplePhysicsComponent.h" +#include "eMissionTaskType.h" #include "eObjectBits.h" #include "Database.h" #include "DluAssert.h" ModelComponent::ModelComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { - using namespace GameMessages; m_OriginalPosition = m_Parent->GetDefaultPosition(); m_OriginalRotation = m_Parent->GetDefaultRotation(); m_IsPaused = false; m_NumListeningInteract = 0; m_userModelID = m_Parent->GetVarAs(u"userModelID"); - RegisterMsg(this, &ModelComponent::OnRequestUse); - RegisterMsg(this, &ModelComponent::OnResetModelToDefaults); - RegisterMsg(this, &ModelComponent::OnGetObjectReportInfo); + RegisterMsg(&ModelComponent::OnRequestUse); + RegisterMsg(&ModelComponent::OnResetModelToDefaults); + RegisterMsg(&ModelComponent::OnGetObjectReportInfo); } -bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) { - auto& reset = static_cast(msg); - for (auto& behavior : m_Behaviors) behavior.HandleMsg(reset); - GameMessages::UnSmash unsmash; - unsmash.target = GetParent()->GetObjectID(); - unsmash.duration = 0.0f; - unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS); +bool ModelComponent::OnResetModelToDefaults(GameMessages::ResetModelToDefaults& reset) { + if (reset.bResetBehaviors) for (auto& behavior : m_Behaviors) behavior.HandleMsg(reset); - m_Parent->SetPosition(m_OriginalPosition); - m_Parent->SetRotation(m_OriginalRotation); + if (reset.bUnSmash) { + GameMessages::UnSmash unsmash; + unsmash.target = GetParent()->GetObjectID(); + unsmash.duration = 0.0f; + unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS); + m_NumActiveUnSmash = 0; + } + + if (reset.bResetPos) m_Parent->SetPosition(m_OriginalPosition); + if (reset.bResetRot) m_Parent->SetRotation(m_OriginalRotation); m_Parent->SetVelocity(NiPoint3Constant::ZERO); m_Speed = 3.0f; m_NumListeningInteract = 0; - m_NumActiveUnSmash = 0; m_NumActiveAttack = 0; GameMessages::SetFaction set{}; @@ -56,10 +59,9 @@ bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) { return true; } -bool ModelComponent::OnRequestUse(GameMessages::GameMsg& msg) { +bool ModelComponent::OnRequestUse(GameMessages::RequestUse& requestUse) { bool toReturn = false; if (!m_IsPaused) { - auto& requestUse = static_cast(msg); for (auto& behavior : m_Behaviors) behavior.HandleMsg(requestUse); toReturn = true; } @@ -184,6 +186,7 @@ void ModelComponent::AddBehavior(AddMessage& msg) { // Check if this behavior is able to be found via lot (if so, its a loot behavior). insertedBehavior.SetIsLoot(inventoryComponent->FindItemByLot(msg.GetBehaviorId(), eInventoryType::BEHAVIORS)); } + ProgressAddBehaviorMission(*playerEntity); } auto* const simplePhysComponent = m_Parent->GetComponent(); @@ -193,6 +196,11 @@ void ModelComponent::AddBehavior(AddMessage& msg) { } } +void ModelComponent::ProgressAddBehaviorMission(Entity& playerEntity) { + auto* const missionComponent = playerEntity.GetComponent(); + if (missionComponent) missionComponent->Progress(eMissionTaskType::ADD_BEHAVIOR, 0); +} + std::string ModelComponent::SaveBehavior(const PropertyBehavior& behavior) const { tinyxml2::XMLDocument doc; auto* root = doc.NewElement("Behavior"); @@ -213,8 +221,8 @@ void ModelComponent::RemoveBehavior(MoveToInventoryMessage& msg, const bool keep auto* const inventoryComponent = playerEntity->GetComponent(); if (inventoryComponent && !behavior.GetIsLoot()) { // config is owned by the item - std::vector config; - config.push_back(new LDFData(u"userModelName", behavior.GetName())); + LwoNameValue config; + config.Insert(u"userModelName", behavior.GetName()); inventoryComponent->AddItem(7965, 1, eLootSourceType::PROPERTY, eInventoryType::BEHAVIORS, config, LWOOBJID_EMPTY, true, false, msg.GetBehaviorId()); } } @@ -340,10 +348,9 @@ void ModelComponent::RemoveAttack() { } } -bool ModelComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportMsg = static_cast(msg); - if (!reportMsg.info) return false; - auto& cmptInfo = reportMsg.info->PushDebug("Model Behaviors (Mutable)"); +bool ModelComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + if (!reportInfo.info) return false; + auto& cmptInfo = reportInfo.info->PushDebug("Model Behaviors (Mutable)"); cmptInfo.PushDebug("Component ID") = GetComponentID(); cmptInfo.PushDebug("Name") = "Objects_" + std::to_string(m_Parent->GetLOT()) + "_name"; diff --git a/dGame/dComponents/ModelComponent.h b/dGame/dComponents/ModelComponent.h index 97b165cb..e821406b 100644 --- a/dGame/dComponents/ModelComponent.h +++ b/dGame/dComponents/ModelComponent.h @@ -32,9 +32,9 @@ public: void LoadBehaviors(); void Update(float deltaTime) override; - bool OnRequestUse(GameMessages::GameMsg& msg); - bool OnResetModelToDefaults(GameMessages::GameMsg& msg); - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnRequestUse(GameMessages::RequestUse& requestUse); + bool OnResetModelToDefaults(GameMessages::ResetModelToDefaults& resetModelToDefaults); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override; @@ -98,6 +98,8 @@ public: return msg.GetNeedsNewBehaviorID(); }; + void ProgressAddBehaviorMission(Entity& playerEntity); + void AddBehavior(AddMessage& msg); void RemoveBehavior(MoveToInventoryMessage& msg, const bool keepItem); diff --git a/dGame/dComponents/MovementAIComponent.cpp b/dGame/dComponents/MovementAIComponent.cpp index faedc848..8a10df25 100644 --- a/dGame/dComponents/MovementAIComponent.cpp +++ b/dGame/dComponents/MovementAIComponent.cpp @@ -16,8 +16,15 @@ #include "CDComponentsRegistryTable.h" #include "QuickBuildComponent.h" #include "CDPhysicsComponentTable.h" +#include "Amf3.h" #include "dNavMesh.h" +#include "eWaypointCommandType.h" +#include "StringifiedEnum.h" +#include "SkillComponent.h" +#include "GeneralUtils.h" +#include "RenderComponent.h" +#include "InventoryComponent.h" namespace { /** @@ -30,8 +37,6 @@ MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t component m_Info = info; m_AtFinalWaypoint = true; - m_BaseCombatAI = nullptr; - m_BaseCombatAI = m_Parent->GetComponent(); //Try and fix the insane values: @@ -57,7 +62,9 @@ MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t component m_SavedVelocity = NiPoint3Constant::ZERO; m_IsBounced = false; - if (!m_Parent->GetComponent()) SetPath(m_Parent->GetVarAsString(u"attached_path")); + RegisterMsg(&MovementAIComponent::OnGetObjectReportInfo); + + SetPath(m_Parent->GetVarAsString(u"attached_path")); } void MovementAIComponent::SetPath(const std::string pathName) { @@ -122,7 +129,11 @@ void MovementAIComponent::Update(const float deltaTime) { m_TimeTravelled += deltaTime; - SetPosition(ApproximateLocation()); + const auto approxPos = ApproximateLocation(); + SetPosition(approxPos); + // Set the AIs new home based on where our current waypoint is IF we're idle, that way we can return to this + // when resuming the pathing after losing aggro while moving the aggro hitbox with us + if (m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle) m_BaseCombatAI->SetStartingPosition(approxPos); if (m_TimeTravelled < m_TimeToTravel) return; m_TimeTravelled = 0.0f; @@ -157,32 +168,43 @@ void MovementAIComponent::Update(const float deltaTime) { SetRotation(QuatUtils::LookAt(source, m_NextWaypoint)); } } else { - // Check if there are more waypoints in the queue, if so set our next destination to the next waypoint - const auto waypointNum = m_IsBounced ? m_CurrentPath.size() : m_CurrentPathWaypointCount - m_CurrentPath.size() - 1; - if (m_CurrentPath.empty()) { - if (m_Path) { - if (m_Path->pathBehavior == PathBehavior::Loop) { - SetPath(m_Path->pathWaypoints); - } else if (m_Path->pathBehavior == PathBehavior::Bounce) { - m_IsBounced = !m_IsBounced; - std::vector waypoints = m_Path->pathWaypoints; - if (m_IsBounced) std::ranges::reverse(waypoints); - SetPath(waypoints); - } else if (m_Path->pathBehavior == PathBehavior::Once) { - m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); + // Only try to renew or continue the path if we're in the idle or spawn state and we actually have a combatAI component + if (!m_BaseCombatAI || (m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle)) { + // Check if there are more waypoints in the queue, if so set our next destination to the next waypoint + const auto waypointNum = m_IsBounced ? m_CurrentPath.size() : m_CurrentPathWaypointCount - m_CurrentPath.size() - 1; + RunWaypointCommands(waypointNum); + if (m_CurrentPath.empty()) { + if (m_Path) { + if (m_Path->pathBehavior == PathBehavior::Loop) { + SetPath(m_Path->pathWaypoints); + } else if (m_Path->pathBehavior == PathBehavior::Bounce) { + m_IsBounced = !m_IsBounced; + std::vector waypoints = m_Path->pathWaypoints; + if (m_IsBounced) std::ranges::reverse(waypoints); + SetPath(waypoints); + } else if (m_Path->pathBehavior == PathBehavior::Once) { + // In this case we intended to follow a path and once we've followed it we camp there, otherwise we'd just wander home again. + Stop(); + return; + } + } else { Stop(); + if (m_FollowedTarget != LWOOBJID_EMPTY) { + GameMessages::GetPosition getPos; + if (!getPos.Send(m_FollowedTarget)) { + LOG("Target %llu does not exist anymore to follow", m_FollowedTarget); + m_FollowedTarget = LWOOBJID_EMPTY; + } else { + SetDestination(getPos.pos); + } + } return; } } else { - m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); - Stop(); - return; - } - } else { - m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); - SetDestination(m_CurrentPath.top().position); + SetDestination(m_CurrentPath.top().position); - m_CurrentPath.pop(); + m_CurrentPath.pop(); + } } } @@ -209,8 +231,7 @@ NiPoint3 MovementAIComponent::GetCurrentWaypoint() const { NiPoint3 MovementAIComponent::ApproximateLocation() const { auto source = m_SourcePosition; - - if (AtFinalWaypoint()) return source; + if (AtFinalWaypoint()) return m_Parent->GetPosition(); auto destination = m_NextWaypoint; @@ -420,5 +441,148 @@ NiPoint3 MovementAIComponent::GetDestination() const { void MovementAIComponent::SetMaxSpeed(const float value) { if (value == m_MaxSpeed) return; m_MaxSpeed = value; - m_Acceleration = value / 5; + m_Acceleration = value / 5.0f; +} + +void MovementAIComponent::RunWaypointCommands(uint32_t waypointNum) { + m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); + + if (!m_Path || waypointNum >= m_Path->pathWaypoints.size()) return; + const auto& commands = m_Path->pathWaypoints[waypointNum].commands; + for (const auto& [command, data] : commands) { + LOG_DEBUG("%s %s %s", StringifiedEnum::ToString(command).data(), m_Path->pathName.c_str(), data.c_str()); + const auto dataSplit = GeneralUtils::SplitString(data, ','); + switch (command) { + case eWaypointCommandType::INVALID: break; + case eWaypointCommandType::BOUNCE: break; + case eWaypointCommandType::STOP: Pause(); break; + case eWaypointCommandType::GROUP_EMOTE: break; + case eWaypointCommandType::SET_VARIABLE: break; // Empty in the client + case eWaypointCommandType::CAST_SKILL: { + const auto skill = GeneralUtils::TryParse(data); + if (skill) { + auto* const skillComponent = m_Parent->GetComponent(); + if (skillComponent) skillComponent->CastSkill(skill.value()); + } + break; + } + case eWaypointCommandType::EQUIP_INVENTORY: { + auto* const inventoryComponent = m_Parent->GetComponent(); + if (inventoryComponent) { + // items should always exist + auto* const item = inventoryComponent->GetInventory(eInventoryType::ITEMS)->FindItemBySlot(0); + if (item) inventoryComponent->EquipItem(item); + } + break; + } + case eWaypointCommandType::UNEQUIP_INVENTORY: { + auto* const inventoryComponent = m_Parent->GetComponent(); + if (inventoryComponent) { + // items should always exist + auto* const item = inventoryComponent->GetInventory(eInventoryType::ITEMS)->FindItemBySlot(0); + if (item) inventoryComponent->UnEquipItem(item); + } + break; + } + case eWaypointCommandType::DELAY: { + // Pause(GeneralUtils::TryParse(data).value_or(0.0f)); + break; + } + case eWaypointCommandType::EMOTE: { + // m_Delay = RenderComponent::GetAnimationTime(m_Parent, data); + // const auto emoteID = GeneralUtils::TryParse(data); + // if (emoteID) GameMessages::SendPlayEmote(m_Parent->GetObjectID(), emoteID.value(), LWOOBJID_EMPTY, UNASSIGNED_SYSTEM_ADDRESS); + break; + } + case eWaypointCommandType::TELEPORT: break; + case eWaypointCommandType::PATH_SPEED: m_BaseSpeed = GetBaseSpeed(m_Parent->GetLOT()) * GeneralUtils::TryParse(data).value_or(1.0f); break; + case eWaypointCommandType::REMOVE_NPC: break; + case eWaypointCommandType::CHANGE_WAYPOINT: SetPath(dataSplit[0]); break; + case eWaypointCommandType::DELETE_SELF: break; + case eWaypointCommandType::KILL_SELF: m_Parent->Smash(); break; + case eWaypointCommandType::SPAWN_OBJECT: break; + case eWaypointCommandType::PLAY_SOUND: break; + } + } +} + +bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + + auto& movementInfo = reportInfo.info->PushDebug("MovementAI"); + if (m_Path) { + movementInfo.PushDebug("Path") = m_Path->pathName; + } + + auto& movementAiInfo = movementInfo.PushDebug("Movement AI Info"); + movementAiInfo.PushDebug("Movement Type") = m_Info.movementType; + movementAiInfo.PushDebug("Wander Radius") = m_Info.wanderRadius; + movementAiInfo.PushDebug("Wander Speed") = m_Info.wanderSpeed; + movementAiInfo.PushDebug("Wander Chance") = m_Info.wanderChance; + movementAiInfo.PushDebug("Wander Delay Min") = m_Info.wanderDelayMin; + movementAiInfo.PushDebug("Wander Delay Max") = m_Info.wanderDelayMax; + + auto& speedInfo = movementInfo.PushDebug("Speed Info"); + speedInfo.PushDebug("Base Speed") = m_BaseSpeed; + speedInfo.PushDebug("Max Speed") = m_MaxSpeed; + speedInfo.PushDebug("Current Speed") = m_CurrentSpeed; + speedInfo.PushDebug("Acceleration") = m_Acceleration; + + movementInfo.PushDebug("Halt Distance") = m_HaltDistance; + movementInfo.PushDebug("Time To Travel") = m_TimeToTravel; + movementInfo.PushDebug("Time Travelled") = m_TimeTravelled; + movementInfo.PushDebug("Lock Rotation") = m_LockRotation; + movementInfo.PushDebug("Paused") = m_Paused; + movementInfo.PushDebug("Pulling To Point") = m_PullingToPoint; + movementInfo.PushDebug("At Final Waypoint") = m_AtFinalWaypoint; + + auto& pullPointInfo = movementInfo.PushDebug("Pull Point"); + pullPointInfo.PushDebug("X") = m_PullPoint.x; + pullPointInfo.PushDebug("Y") = m_PullPoint.y; + pullPointInfo.PushDebug("Z") = m_PullPoint.z; + + // movementInfo.PushDebug("Delay") = m_Delay; + + auto& waypoints = movementInfo.PushDebug("Interpolated Waypoints"); + int i = 0; + for (const auto& point : m_InterpolatedWaypoints) { + auto& waypoint = waypoints.PushDebug("Waypoint " + std::to_string(++i)); + waypoint.PushDebug("X") = point.x; + waypoint.PushDebug("Y") = point.y; + waypoint.PushDebug("Z") = point.z; + } + + i = 0; + auto& currentPath = movementInfo.PushDebug("Current Path"); + auto pathCopy = m_CurrentPath; // Copy to avoid modifying the original stack + while (!pathCopy.empty()) { + const auto& waypoint = pathCopy.top(); + auto& pathWaypoint = currentPath.PushDebug("Waypoint " + std::to_string(++i)); + pathWaypoint.PushDebug("X") = waypoint.position.x; + pathWaypoint.PushDebug("Y") = waypoint.position.y; + pathWaypoint.PushDebug("Z") = waypoint.position.z; + + pathCopy.pop(); + } + + movementInfo.PushDebug("Followed Target") = std::to_string(m_FollowedTarget); + + return true; +} + +void MovementAIComponent::FollowTarget(const LWOOBJID target) { + if (target == LWOOBJID_EMPTY) { + m_FollowedTarget = target; + return; + } + GameMessages::GetPosition getPos; + if (!getPos.Send(target)) { + LOG("Tried to follow target %llu but they don't exist", target); + m_FollowedTarget = LWOOBJID_EMPTY; + return; + } + + m_FollowedTarget = target; + SetMaxSpeed(1.0f); + m_CurrentSpeed = 1.0f; + SetDestination(getPos.pos); } diff --git a/dGame/dComponents/MovementAIComponent.h b/dGame/dComponents/MovementAIComponent.h index 72ff45e8..eeb871cf 100644 --- a/dGame/dComponents/MovementAIComponent.h +++ b/dGame/dComponents/MovementAIComponent.h @@ -31,27 +31,27 @@ struct MovementAIInfo { /** * The radius that the entity can wander in */ - float wanderRadius; + float wanderRadius{}; /** * The speed at which the entity wanders */ - float wanderSpeed; + float wanderSpeed{}; /** * This is only used for the emotes */ - float wanderChance; + float wanderChance{}; /** * The min amount of delay before wandering */ - float wanderDelayMin; + float wanderDelayMin{}; /** * The max amount of delay before wandering */ - float wanderDelayMax; + float wanderDelayMax{}; }; /** @@ -211,8 +211,19 @@ public: bool IsPaused() const { return m_Paused; } + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + + bool HasPath() const { return m_Path != nullptr; } + + void FollowTarget(const LWOOBJID target); private: + /** + * @brief + * Runs the commands on a waypoint if a path exists + */ + void RunWaypointCommands(uint32_t waypointNum); + /** * Sets the current position of the entity * @param value the position to set @@ -328,6 +339,8 @@ private: // The number of waypoints that were on the path in the call to SetPath uint32_t m_CurrentPathWaypointCount{ 0 }; + + LWOOBJID m_FollowedTarget{ LWOOBJID_EMPTY }; }; #endif // MOVEMENTAICOMPONENT_H diff --git a/dGame/dComponents/MultiZoneEntranceComponent.cpp b/dGame/dComponents/MultiZoneEntranceComponent.cpp index f8dabf18..674af099 100644 --- a/dGame/dComponents/MultiZoneEntranceComponent.cpp +++ b/dGame/dComponents/MultiZoneEntranceComponent.cpp @@ -17,7 +17,10 @@ MultiZoneEntranceComponent::MultiZoneEntranceComponent(Entity* parent, const int MultiZoneEntranceComponent::~MultiZoneEntranceComponent() {} void MultiZoneEntranceComponent::OnUse(Entity* originator) { - auto* rocket = originator->GetComponent()->RocketEquip(originator); + auto* const characterComponent = originator->GetComponent(); + if (!characterComponent) return; + + auto* rocket = characterComponent->RocketEquip(originator); if (!rocket) return; // the LUP world menu is just the property menu, the client knows how to handle it @@ -26,7 +29,7 @@ void MultiZoneEntranceComponent::OnUse(Entity* originator) { void MultiZoneEntranceComponent::OnSelectWorld(Entity* originator, uint32_t index) { auto* rocketLaunchpadControlComponent = m_Parent->GetComponent(); - if (!rocketLaunchpadControlComponent) return; + if (!rocketLaunchpadControlComponent || index >= m_LUPWorlds.size()) return; rocketLaunchpadControlComponent->Launch(originator, m_LUPWorlds[index], 0); } diff --git a/dGame/dComponents/PetComponent.cpp b/dGame/dComponents/PetComponent.cpp index 6fb5eedf..cd2b50b9 100644 --- a/dGame/dComponents/PetComponent.cpp +++ b/dGame/dComponents/PetComponent.cpp @@ -922,7 +922,9 @@ void PetComponent::Deactivate() { } void PetComponent::Release() { - auto* inventoryComponent = GetOwner()->GetComponent(); + auto* const owner = GetOwner(); + if (!owner) return; + auto* const inventoryComponent = owner->GetComponent(); if (inventoryComponent == nullptr) { return; @@ -932,9 +934,9 @@ void PetComponent::Release() { inventoryComponent->RemoveDatabasePet(m_DatabaseId); - auto* item = inventoryComponent->FindItemBySubKey(m_DatabaseId); + auto* const item = inventoryComponent->FindItemBySubKey(m_DatabaseId); - item->SetCount(0, false, false); + if (item) item->SetCount(0, false, false); } void PetComponent::Command(const NiPoint3& position, const LWOOBJID source, const int32_t commandType, const int32_t typeId, const bool overrideObey) { diff --git a/dGame/dComponents/PhantomPhysicsComponent.cpp b/dGame/dComponents/PhantomPhysicsComponent.cpp index 4ba95f5f..a65df484 100644 --- a/dGame/dComponents/PhantomPhysicsComponent.cpp +++ b/dGame/dComponents/PhantomPhysicsComponent.cpp @@ -29,7 +29,7 @@ #include "dpShapeSphere.h" PhantomPhysicsComponent::PhantomPhysicsComponent(Entity* parent, const int32_t componentID) : PhysicsComponent(parent, componentID) { - RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &PhantomPhysicsComponent::OnGetObjectReportInfo); + RegisterMsg(&PhantomPhysicsComponent::OnGetObjectReportInfo); m_Position = m_Parent->GetDefaultPosition(); m_Rotation = m_Parent->GetDefaultRotation(); @@ -58,31 +58,16 @@ PhantomPhysicsComponent::PhantomPhysicsComponent(Entity* parent, const int32_t c } if (m_IsRespawnVolume) { - { - auto respawnString = std::stringstream(m_Parent->GetVarAsString(u"rspPos")); + const auto respawnPos = GeneralUtils::SplitString(m_Parent->GetVarAsString(u"rspPos"), '\x1f'); + m_RespawnPos = GeneralUtils::TryParse(respawnPos, NiPoint3Constant::ZERO); - std::string segment; - std::vector seglist; - - while (std::getline(respawnString, segment, '\x1f')) { - seglist.push_back(segment); - } - - m_RespawnPos = NiPoint3(std::stof(seglist[0]), std::stof(seglist[1]), std::stof(seglist[2])); - } - - { - auto respawnString = std::stringstream(m_Parent->GetVarAsString(u"rspRot")); - - std::string segment; - std::vector seglist; - - while (std::getline(respawnString, segment, '\x1f')) { - seglist.push_back(segment); - } - - m_RespawnRot = NiQuaternion(std::stof(seglist[0]), std::stof(seglist[1]), std::stof(seglist[2]), std::stof(seglist[3])); - } + const auto respawnRot = GeneralUtils::SplitString(m_Parent->GetVarAsString(u"rspRot"), '\x1f'); + m_RespawnRot = respawnRot.size() >= 4 ? NiQuaternion( + GeneralUtils::TryParse(respawnRot[0], 1.0f), + GeneralUtils::TryParse(respawnRot[1], 0.0f), + GeneralUtils::TryParse(respawnRot[2], 0.0f), + GeneralUtils::TryParse(respawnRot[3], 0.0f)) + : QuatUtils::IDENTITY; } // HF - RespawnPoints. Legacy respawn entity. @@ -242,9 +227,8 @@ void PhantomPhysicsComponent::SetRotation(const NiQuaternion& rot) { if (m_dpEntity) m_dpEntity->SetRotation(rot); } -bool PhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - PhysicsComponent::OnGetObjectReportInfo(msg); - auto& reportInfo = static_cast(msg); +bool PhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + PhysicsComponent::OnGetObjectReportInfo(reportInfo); if (!reportInfo.subCategory) { return false; } diff --git a/dGame/dComponents/PhantomPhysicsComponent.h b/dGame/dComponents/PhantomPhysicsComponent.h index ac42dca3..9a9e1880 100644 --- a/dGame/dComponents/PhantomPhysicsComponent.h +++ b/dGame/dComponents/PhantomPhysicsComponent.h @@ -12,7 +12,6 @@ #include "eReplicaComponentType.h" #include "PhysicsComponent.h" -class LDFBaseData; class Entity; class dpEntity; enum class ePhysicsEffectType : uint32_t ; @@ -116,7 +115,7 @@ public: void SetMax(uint32_t max); private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); /** * A scale to apply to the size of the physics object diff --git a/dGame/dComponents/PhysicsComponent.cpp b/dGame/dComponents/PhysicsComponent.cpp index 7d301476..16722c54 100644 --- a/dGame/dComponents/PhysicsComponent.cpp +++ b/dGame/dComponents/PhysicsComponent.cpp @@ -31,11 +31,11 @@ PhysicsComponent::PhysicsComponent(Entity* parent, const int32_t componentID) : if (m_Parent->HasVar(u"CollisionGroupID")) m_CollisionGroup = m_Parent->GetVar(u"CollisionGroupID"); - RegisterMsg(MessageType::Game::GET_POSITION, this, &PhysicsComponent::OnGetPosition); + RegisterMsg(&PhysicsComponent::OnGetPosition); } -bool PhysicsComponent::OnGetPosition(GameMessages::GameMsg& msg) { - static_cast(msg).pos = GetPosition(); +bool PhysicsComponent::OnGetPosition(GameMessages::GetPosition& msg) { + msg.pos = GetPosition(); return true; } @@ -245,8 +245,7 @@ void PhysicsComponent::SpawnVertices(dpEntity* entity) const { } } -bool PhysicsComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& reportInfo = static_cast(msg); +bool PhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { auto& info = reportInfo.info->PushDebug("Physics"); reportInfo.subCategory = &info; diff --git a/dGame/dComponents/PhysicsComponent.h b/dGame/dComponents/PhysicsComponent.h index 67a4a0a5..066451da 100644 --- a/dGame/dComponents/PhysicsComponent.h +++ b/dGame/dComponents/PhysicsComponent.h @@ -7,6 +7,7 @@ namespace GameMessages { struct GetObjectReportInfo; + struct GetPosition; }; namespace Raknet { @@ -33,7 +34,7 @@ public: int32_t GetCollisionGroup() const noexcept { return m_CollisionGroup; } void SetCollisionGroup(int32_t group) noexcept { m_CollisionGroup = group; } protected: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg); dpEntity* CreatePhysicsEntity(eReplicaComponentType type); @@ -41,7 +42,7 @@ protected: void SpawnVertices(dpEntity* entity) const; - bool OnGetPosition(GameMessages::GameMsg& msg); + bool OnGetPosition(GameMessages::GetPosition& msg); NiPoint3 m_Position; diff --git a/dGame/dComponents/PropertyEntranceComponent.cpp b/dGame/dComponents/PropertyEntranceComponent.cpp index 1f3b28a2..f282a73d 100644 --- a/dGame/dComponents/PropertyEntranceComponent.cpp +++ b/dGame/dComponents/PropertyEntranceComponent.cpp @@ -131,7 +131,7 @@ void PropertyEntranceComponent::OnPropertyEntranceSync(Entity* entity, bool incl const auto lookupResult = Database::Get()->GetProperties(propertyLookup); - for (const auto& propertyEntry : lookupResult->entries) { + for (const auto& propertyEntry : lookupResult.entries) { const auto owner = propertyEntry.ownerId; const auto otherCharacter = Database::Get()->GetCharacterInfo(owner); if (!otherCharacter.has_value()) { @@ -174,5 +174,5 @@ void PropertyEntranceComponent::OnPropertyEntranceSync(Entity* entity, bool incl } // Query here is to figure out whether or not to display the button to go to the next page or not. - GameMessages::SendPropertySelectQuery(m_Parent->GetObjectID(), startIndex, lookupResult->totalEntriesMatchingQuery - (startIndex + numResults) > 0, character->GetPropertyCloneID(), false, true, entries, sysAddr); + GameMessages::SendPropertySelectQuery(m_Parent->GetObjectID(), startIndex, lookupResult.totalEntriesMatchingQuery - (startIndex + numResults) > 0, character->GetPropertyCloneID(), false, true, entries, sysAddr); } diff --git a/dGame/dComponents/PropertyManagementComponent.cpp b/dGame/dComponents/PropertyManagementComponent.cpp index 59b918e6..54371fde 100644 --- a/dGame/dComponents/PropertyManagementComponent.cpp +++ b/dGame/dComponents/PropertyManagementComponent.cpp @@ -107,20 +107,12 @@ std::vector PropertyManagementComponent::GetPaths() const { std::vector points; - std::istringstream stream(result.getStringField("path")); - std::string token; - - while (std::getline(stream, token, ' ')) { - try { - auto value = std::stof(token); - - points.push_back(value); - } catch (std::invalid_argument& exception) { - LOG("Failed to parse value (%s): (%s)!", token.c_str(), exception.what()); - } + for (const auto& str : GeneralUtils::SplitString(result.getStringField("path"), ' ')) { + const auto value = GeneralUtils::TryParse(str); + if (value) points.push_back(value.value()); } - for (auto i = 0u; i < points.size(); i += 3) { + for (auto i = 0u; i + 2 < points.size(); i += 3) { paths.emplace_back(points[i], points[i + 1], points[i + 2]); } @@ -354,10 +346,7 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N info.spawner = nullptr; info.spawnerID = spawnerID; info.spawnerNodeID = 0; - - for (auto* setting : item->GetConfig()) { - info.settings.push_back(setting->Copy()); - } + info.settings = item->GetConfig(); Entity* newEntity = Game::entityManager->CreateEntity(info); if (newEntity != nullptr) { @@ -401,11 +390,11 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N auto* spawner = Game::zoneManager->GetSpawner(spawnerId); - info.nodes[0]->config.push_back(new LDFData(u"modelBehaviors", 0)); - info.nodes[0]->config.push_back(new LDFData(u"userModelID", info.spawnerID)); - info.nodes[0]->config.push_back(new LDFData(u"modelType", 2)); - info.nodes[0]->config.push_back(new LDFData(u"propertyObjectID", true)); - info.nodes[0]->config.push_back(new LDFData(u"componentWhitelist", 1)); + info.nodes[0]->config.Insert(u"modelBehaviors", 0); + info.nodes[0]->config.Insert(u"userModelID", info.spawnerID); + info.nodes[0]->config.Insert(u"modelType", 2); + info.nodes[0]->config.Insert(u"propertyObjectID", true); + info.nodes[0]->config.Insert(u"componentWhitelist", 1); auto* model = spawner->Spawn(); auto* modelComponent = model->GetComponent(); @@ -484,28 +473,19 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet if (model->GetLOT() == 14) { //add it to the inv - std::vector settings; - + LwoNameValue actualConfig; + //fill our settings with BBB gurbage - LDFBaseData* ldfBlueprintID = new LDFData(u"blueprintid", model->GetVar(u"blueprintid")); - LDFBaseData* userModelDesc = new LDFData(u"userModelDesc", u"A cool model you made!"); - LDFBaseData* userModelHasBhvr = new LDFData(u"userModelHasBhvr", false); - LDFBaseData* userModelID = new LDFData(u"userModelID", model->GetVar(u"userModelID")); - LDFBaseData* userModelMod = new LDFData(u"userModelMod", false); - LDFBaseData* userModelName = new LDFData(u"userModelName", u"My Cool Model"); - LDFBaseData* propertyObjectID = new LDFData(u"userModelOpt", true); - LDFBaseData* modelType = new LDFData(u"userModelPhysicsType", 2); + actualConfig.Insert(u"blueprintid", model->GetVar(u"blueprintid")); + actualConfig.Insert(u"userModelDesc", u"A cool model you made!"); + actualConfig.Insert(u"userModelHasBhvr", false); + actualConfig.Insert(u"userModelID", model->GetVar(u"userModelID")); + actualConfig.Insert(u"userModelMod", false); + actualConfig.Insert(u"userModelName", u"My Cool Model"); + actualConfig.Insert(u"userModelOpt", true); + actualConfig.Insert(u"userModelPhysicsType", 2); - settings.push_back(ldfBlueprintID); - settings.push_back(userModelDesc); - settings.push_back(userModelHasBhvr); - settings.push_back(userModelID); - settings.push_back(userModelMod); - settings.push_back(userModelName); - settings.push_back(propertyObjectID); - settings.push_back(modelType); - - inventoryComponent->AddItem(6662, 1, eLootSourceType::DELETION, eInventoryType::MODELS_IN_BBB, settings, LWOOBJID_EMPTY, false, false, spawnerId); + inventoryComponent->AddItem(6662, 1, eLootSourceType::DELETION, eInventoryType::MODELS_IN_BBB, actualConfig, LWOOBJID_EMPTY, false, false, spawnerId); auto* item = inventoryComponent->FindItemBySubKey(spawnerId); if (item == nullptr) { @@ -623,23 +603,23 @@ void PropertyManagementComponent::Load() { info.spawnerID = databaseModel.id; - std::vector settings; + LwoNameValue& settings = node->config; //BBB property models need to have extra stuff set for them: if (databaseModel.lot == 14) { LWOOBJID blueprintID = databaseModel.ugcId; - settings.push_back(new LDFData(u"blueprintid", blueprintID)); - settings.push_back(new LDFData(u"componentWhitelist", 1)); - settings.push_back(new LDFData(u"modelType", 2)); - settings.push_back(new LDFData(u"propertyObjectID", true)); - settings.push_back(new LDFData(u"userModelID", databaseModel.id)); + settings.Insert(u"blueprintid", blueprintID); + settings.Insert(u"componentWhitelist", 1); + settings.Insert(u"modelType", 2); + settings.Insert(u"propertyObjectID", true); + settings.Insert(u"userModelID", databaseModel.id); } else { - settings.push_back(new LDFData(u"modelType", 2)); - settings.push_back(new LDFData(u"userModelID", databaseModel.id)); - settings.push_back(new LDFData(u"modelBehaviors", 0)); - settings.push_back(new LDFData(u"propertyObjectID", true)); - settings.push_back(new LDFData(u"componentWhitelist", 1)); + settings.Insert(u"modelType", 2); + settings.Insert(u"userModelID", databaseModel.id); + settings.Insert(u"modelBehaviors", 0); + settings.Insert(u"propertyObjectID", true); + settings.Insert(u"componentWhitelist", 1); } std::ostringstream userModelBehavior; @@ -654,9 +634,7 @@ void PropertyManagementComponent::Load() { firstAdded = true; } - settings.push_back(new LDFData(u"userModelBehaviors", userModelBehavior.str())); - - node->config = settings; + settings.Insert(u"userModelBehaviors", userModelBehavior.str()); const auto spawnerId = Game::zoneManager->MakeSpawner(info); @@ -780,15 +758,17 @@ void PropertyManagementComponent::OnQueryPropertyData(Entity* originator, const privacy = static_cast(this->privacyOption); if (moderatorRequested) { auto moderationInfo = Database::Get()->GetPropertyInfo(zoneId, cloneId); - if (moderationInfo->rejectionReason != "") { - moderatorRequested = false; - rejectionReason = moderationInfo->rejectionReason; - } else if (moderationInfo->rejectionReason == "" && moderationInfo->modApproved == 1) { - moderatorRequested = false; - rejectionReason = ""; - } else { - moderatorRequested = true; - rejectionReason = ""; + if (moderationInfo) { + if (moderationInfo->rejectionReason != "") { + moderatorRequested = false; + rejectionReason = moderationInfo->rejectionReason; + } else if (moderationInfo->rejectionReason == "" && moderationInfo->modApproved == 1) { + moderatorRequested = false; + rejectionReason = ""; + } else { + moderatorRequested = true; + rejectionReason = ""; + } } } } diff --git a/dGame/dComponents/ProximityMonitorComponent.cpp b/dGame/dComponents/ProximityMonitorComponent.cpp index fd3f1b5d..14e440d5 100644 --- a/dGame/dComponents/ProximityMonitorComponent.cpp +++ b/dGame/dComponents/ProximityMonitorComponent.cpp @@ -4,6 +4,8 @@ #include "ControllablePhysicsComponent.h" #include "EntityManager.h" #include "SimplePhysicsComponent.h" +#include "Amf3.h" +#include "dpShapeSphere.h" const std::unordered_set ProximityMonitorComponent::m_EmptyObjectSet = {}; @@ -12,6 +14,7 @@ ProximityMonitorComponent::ProximityMonitorComponent(Entity* parent, const int32 SetProximityRadius(radiusSmall, "rocketSmall"); SetProximityRadius(radiusLarge, "rocketLarge"); } + RegisterMsg(&ProximityMonitorComponent::OnGetObjectReportInfo); } ProximityMonitorComponent::~ProximityMonitorComponent() { @@ -60,6 +63,31 @@ bool ProximityMonitorComponent::IsInProximity(const std::string& name, LWOOBJID return collisions.contains(objectID); } +bool ProximityMonitorComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + auto& proxInfo = reportInfo.info->PushDebug("Proximity Monitor"); + for (const auto& [name, entity] : m_ProximitiesData) { + if (!entity) continue; + auto& proxAmf = proxInfo.PushDebug(name); + const auto* const shape = entity->GetShape(); + if (shape && shape->GetShapeType() == dpShapeType::Sphere) { + const auto* const sphere = static_cast(shape); + proxAmf.PushDebug("Radius") = sphere->GetRadius(); + } + proxAmf.PushDebug("Sleeping") = entity->GetSleeping(); + proxAmf.PushDebug("Scale") = entity->GetScale(); + proxAmf.PushDebug("Gargantuan") = entity->GetIsGargantuan(); + proxAmf.PushDebug("Static") = entity->GetIsStatic(); + proxAmf.PushDebug("Position").PushDebug(entity->GetPosition()); + proxAmf.PushDebug("Rotation").PushDebug(entity->GetRotation()); + auto& collidingAmf = proxAmf.PushDebug("Colliding Objects"); + for (const auto& colliding : entity->GetCurrentlyCollidingObjects()) { + collidingAmf.PushDebug(std::to_string(colliding)); + } + } + + return true; +} + void ProximityMonitorComponent::Update(float deltaTime) { for (const auto& prox : m_ProximitiesData) { if (!prox.second) continue; diff --git a/dGame/dComponents/ProximityMonitorComponent.h b/dGame/dComponents/ProximityMonitorComponent.h index b83c0df0..b4aa1f1a 100644 --- a/dGame/dComponents/ProximityMonitorComponent.h +++ b/dGame/dComponents/ProximityMonitorComponent.h @@ -64,6 +64,7 @@ public: private: + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); /** * All the proximity sensors for this component, indexed by name */ diff --git a/dGame/dComponents/QuickBuildComponent.cpp b/dGame/dComponents/QuickBuildComponent.cpp index 4f167c17..52b2ddee 100644 --- a/dGame/dComponents/QuickBuildComponent.cpp +++ b/dGame/dComponents/QuickBuildComponent.cpp @@ -22,6 +22,8 @@ #include "RenderComponent.h" #include "CppScripts.h" +#include "StringifiedEnum.h" +#include "Amf3.h" QuickBuildComponent::QuickBuildComponent(Entity* const entity, const int32_t componentID) : Component{ entity, componentID } { std::u16string checkPreconditions = entity->GetVar(u"CheckPrecondition"); @@ -42,6 +44,7 @@ QuickBuildComponent::QuickBuildComponent(Entity* const entity, const int32_t com } SpawnActivator(); + RegisterMsg(&QuickBuildComponent::OnGetObjectReportInfo); } QuickBuildComponent::~QuickBuildComponent() { @@ -380,7 +383,7 @@ void QuickBuildComponent::StartQuickBuild(Entity* const user) { m_Builder = user->GetObjectID(); auto* character = user->GetComponent(); - character->SetCurrentActivity(eGameActivity::QUICKBUILDING); + if (character) character->SetCurrentActivity(eGameActivity::QUICKBUILDING); Game::entityManager->SerializeEntity(user); @@ -568,3 +571,30 @@ void QuickBuildComponent::AddQuickBuildCompleteCallback(const std::function& callback) { m_QuickBuildStateCallbacks.push_back(callback); } + +bool QuickBuildComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + auto& quickbuild = reportInfo.info->PushDebug("Quick Build"); + quickbuild.PushDebug("State") = StringifiedEnum::ToString(m_State).data(); + quickbuild.PushDebug("Timer") = m_Timer; + quickbuild.PushDebug("TimerIncomplete") = m_TimerIncomplete; + quickbuild.PushDebug("ActivatorPosition").PushDebug(m_ActivatorPosition); + quickbuild.PushDebug("ActivatorId") = std::to_string(m_ActivatorId); + quickbuild.PushDebug("ShowResetEffect") = m_ShowResetEffect; + quickbuild.PushDebug("Taken") = m_Taken; + quickbuild.PushDebug("ResetTime") = m_ResetTime; + quickbuild.PushDebug("CompleteTime") = m_CompleteTime; + quickbuild.PushDebug("TakeImagination") = m_TakeImagination; + quickbuild.PushDebug("Interruptible") = m_Interruptible; + quickbuild.PushDebug("SelfActivator") = m_SelfActivator; + auto& modules = quickbuild.PushDebug("CustomModules"); + for (const auto cmodule : m_CustomModules) modules.PushDebug("Module") = cmodule; + quickbuild.PushDebug("ActivityId") = m_ActivityId; + quickbuild.PushDebug("PostImaginationCost") = m_PostImaginationCost; + quickbuild.PushDebug("TimeBeforeSmash") = m_TimeBeforeSmash; + quickbuild.PushDebug("TimeBeforeDrain") = m_TimeBeforeDrain; + quickbuild.PushDebug("DrainedImagination") = m_DrainedImagination; + quickbuild.PushDebug("RepositionPlayer") = m_RepositionPlayer; + quickbuild.PushDebug("SoftTimer") = m_SoftTimer; + quickbuild.PushDebug("Builder") = std::to_string(m_Builder); + return true; +} diff --git a/dGame/dComponents/QuickBuildComponent.h b/dGame/dComponents/QuickBuildComponent.h index 8f5f1773..4a3b9ef8 100644 --- a/dGame/dComponents/QuickBuildComponent.h +++ b/dGame/dComponents/QuickBuildComponent.h @@ -261,6 +261,8 @@ public: m_StateDirty = true; } private: + + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); /** * Whether or not the quickbuild state has been changed since we last serialized it. */ diff --git a/dGame/dComponents/RacingControlComponent.cpp b/dGame/dComponents/RacingControlComponent.cpp index 8749d64e..9eb43b9e 100644 --- a/dGame/dComponents/RacingControlComponent.cpp +++ b/dGame/dComponents/RacingControlComponent.cpp @@ -26,6 +26,7 @@ #include "dZoneManager.h" #include "CDActivitiesTable.h" #include "eStateChangeType.h" +#include #include #ifndef M_PI @@ -33,7 +34,7 @@ #endif RacingControlComponent::RacingControlComponent(Entity* parent, const int32_t componentID) - : Component(parent, componentID) { + : ActivityComponent(parent, componentID) { m_PathName = u"MainPath"; m_NumberOfLaps = 3; m_RemainingLaps = m_NumberOfLaps; @@ -57,6 +58,7 @@ RacingControlComponent::RacingControlComponent(Entity* parent, const int32_t com CDActivitiesTable* activitiesTable = CDClientManager::GetTable(); std::vector activities = activitiesTable->Query([=](CDActivities entry) {return (entry.instanceMapID == worldID); }); for (CDActivities activity : activities) m_ActivityID = activity.ActivityID; + RegisterMsg(&RacingControlComponent::MsgConfigureRacingControl); } RacingControlComponent::~RacingControlComponent() {} @@ -70,7 +72,7 @@ void RacingControlComponent::OnPlayerLoaded(Entity* player) { auto* vehicle = inventoryComponent->FindItemByLot(8092); // If the race has already started, send the player back to the main world. - if (m_Loaded || !vehicle) { + if (m_Loaded || !vehicle || !TakeCost(player)) { auto* characterComponent = player->GetComponent(); if (characterComponent) characterComponent->SendToZone(m_MainWorld); return; @@ -178,11 +180,10 @@ void RacingControlComponent::LoadPlayerVehicle(Entity* player, moduleAssemblyComponent->SetSubKey(item->GetSubKey()); moduleAssemblyComponent->SetUseOptionalParts(false); - for (auto* config : item->GetConfig()) { - if (config->GetKey() == u"assemblyPartLOTs") { - moduleAssemblyComponent->SetAssemblyPartsLOTs( - GeneralUtils::ASCIIToUTF16(config->GetValueAsString())); - } + const auto& lnv = item->GetConfig().values; + const auto itr = lnv.find(u"assemblyPartLOTs"); + if (itr != lnv.end()) { + moduleAssemblyComponent->SetAssemblyPartsLOTs(GeneralUtils::ASCIIToUTF16(itr->second->GetValueAsString())); } } @@ -306,7 +307,7 @@ void RacingControlComponent::OnRequestDie(Entity* player, const std::u16string& eKillType::VIOLENT, deathType, 0, 0, 90.0f, false, true, 0); auto* destroyableComponent = vehicle->GetComponent(); - uint32_t respawnImagination = 0; + int32_t respawnImagination = 0; // Reset imagination to half its current value, rounded up to the nearest value divisible by 10, as it was done in live. // Do not actually change the value yet. Do that on respawn. if (destroyableComponent) { @@ -871,10 +872,10 @@ void RacingControlComponent::Update(float deltaTime) { } } -void RacingControlComponent::MsgConfigureRacingControl(const GameMessages::ConfigureRacingControl& msg) { - for (const auto& dataUnique : msg.racingSettings) { +bool RacingControlComponent::MsgConfigureRacingControl(const GameMessages::ConfigureRacingControl& msg) { + for (const auto& dataUnique : msg.racingSettings | std::views::values) { if (!dataUnique) continue; - const auto* const data = dataUnique.get(); + const auto* const data = dataUnique.get(); if (data->GetKey() == u"Race_PathName" && data->GetValueType() == LDF_TYPE_UTF_16) { m_PathName = static_cast*>(data)->GetValue(); } else if (data->GetKey() == u"activityID" && data->GetValueType() == LDF_TYPE_S32) { @@ -886,4 +887,5 @@ void RacingControlComponent::MsgConfigureRacingControl(const GameMessages::Confi m_MinimumPlayersForGroupAchievements = static_cast*>(data)->GetValue(); } } + return true; } diff --git a/dGame/dComponents/RacingControlComponent.h b/dGame/dComponents/RacingControlComponent.h index a1e99e29..bedb636b 100644 --- a/dGame/dComponents/RacingControlComponent.h +++ b/dGame/dComponents/RacingControlComponent.h @@ -6,7 +6,7 @@ #include "BitStream.h" #include "Entity.h" -#include "Component.h" +#include "ActivityComponent.h" #include "eReplicaComponentType.h" #include @@ -104,7 +104,7 @@ struct RacingPlayerInfo { /** * Component that's attached to a manager entity in each race zone that loads player vehicles, keep scores, etc. */ -class RacingControlComponent final : public Component { +class RacingControlComponent final : public ActivityComponent { public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::RACING_CONTROL; @@ -152,7 +152,7 @@ public: */ RacingPlayerInfo* GetPlayerData(LWOOBJID playerID); - void MsgConfigureRacingControl(const GameMessages::ConfigureRacingControl& msg); + bool MsgConfigureRacingControl(const GameMessages::ConfigureRacingControl& msg); private: diff --git a/dGame/dComponents/RigidbodyPhantomPhysicsComponent.cpp b/dGame/dComponents/RigidbodyPhantomPhysicsComponent.cpp index 8f8df0ad..fde91d00 100644 --- a/dGame/dComponents/RigidbodyPhantomPhysicsComponent.cpp +++ b/dGame/dComponents/RigidbodyPhantomPhysicsComponent.cpp @@ -14,7 +14,7 @@ #include "Amf3.h" RigidbodyPhantomPhysicsComponent::RigidbodyPhantomPhysicsComponent(Entity* parent, const int32_t componentID) : PhysicsComponent(parent, componentID) { - RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &RigidbodyPhantomPhysicsComponent::OnGetObjectReportInfo); + RegisterMsg(&RigidbodyPhantomPhysicsComponent::OnGetObjectReportInfo); m_Position = m_Parent->GetDefaultPosition(); m_Rotation = m_Parent->GetDefaultRotation(); @@ -59,9 +59,8 @@ void RigidbodyPhantomPhysicsComponent::SpawnVertices() const { PhysicsComponent::SpawnVertices(m_dpEntity); } -bool RigidbodyPhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - PhysicsComponent::OnGetObjectReportInfo(msg); - auto& reportInfo = static_cast(msg); +bool RigidbodyPhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + PhysicsComponent::OnGetObjectReportInfo(reportInfo); auto& info = reportInfo.subCategory->PushDebug("Rigidbody Phantom Info"); info.PushDebug("Scale") = m_Scale; return true; diff --git a/dGame/dComponents/RigidbodyPhantomPhysicsComponent.h b/dGame/dComponents/RigidbodyPhantomPhysicsComponent.h index dc18da49..75cb11cd 100644 --- a/dGame/dComponents/RigidbodyPhantomPhysicsComponent.h +++ b/dGame/dComponents/RigidbodyPhantomPhysicsComponent.h @@ -29,7 +29,7 @@ public: void SpawnVertices() const; private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); float m_Scale{}; diff --git a/dGame/dComponents/ScriptComponent.cpp b/dGame/dComponents/ScriptComponent.cpp index c9123377..2c9be34b 100644 --- a/dGame/dComponents/ScriptComponent.cpp +++ b/dGame/dComponents/ScriptComponent.cpp @@ -8,20 +8,21 @@ #include "GameMessages.h" #include "Amf3.h" +#include + ScriptComponent::ScriptComponent(Entity* parent, const int32_t componentID, const std::string& scriptName, bool serialized, bool client) : Component(parent, componentID) { - using namespace GameMessages; m_Serialized = serialized; m_Client = client; m_ScriptName = scriptName; SetScript(scriptName); - RegisterMsg(this, &ScriptComponent::OnGetObjectReportInfo); + RegisterMsg(&ScriptComponent::OnGetObjectReportInfo); } void ScriptComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { if (bIsInitialUpdate) { const auto& networkSettings = m_Parent->GetNetworkSettings(); - auto hasNetworkSettings = !networkSettings.empty(); + auto hasNetworkSettings = !networkSettings.values.empty(); outBitStream.Write(hasNetworkSettings); if (hasNetworkSettings) { @@ -29,9 +30,9 @@ void ScriptComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitial // First write the most inner LDF data RakNet::BitStream ldfData; ldfData.Write(0); - ldfData.Write(networkSettings.size()); + ldfData.Write(networkSettings.values.size()); - for (auto* networkSetting : networkSettings) { + for (const auto& networkSetting : networkSettings.values | std::views::values) { networkSetting->WriteToPacket(ldfData); } @@ -52,13 +53,12 @@ void ScriptComponent::SetScript(const std::string& scriptName) { m_Script = CppScripts::GetScript(m_Parent, scriptName); } -bool ScriptComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - auto& infoMsg = static_cast(msg); +bool ScriptComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { - auto& scriptInfo = infoMsg.info->PushDebug("Script"); + auto& scriptInfo = reportInfo.info->PushDebug("Script"); scriptInfo.PushDebug("Script Name") = m_ScriptName.empty() ? "None" : m_ScriptName; auto& networkSettings = scriptInfo.PushDebug("Network Settings"); - for (const auto* const setting : m_Parent->GetNetworkSettings()) { + for (const auto& setting : m_Parent->GetNetworkSettings().values | std::views::values) { networkSettings.PushDebug(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString(); } diff --git a/dGame/dComponents/ScriptComponent.h b/dGame/dComponents/ScriptComponent.h index cf4b2537..e79b8e91 100644 --- a/dGame/dComponents/ScriptComponent.h +++ b/dGame/dComponents/ScriptComponent.h @@ -43,7 +43,7 @@ public: */ void SetScript(const std::string& scriptName); - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); private: diff --git a/dGame/dComponents/SimplePhysicsComponent.cpp b/dGame/dComponents/SimplePhysicsComponent.cpp index c0efa544..dcb87021 100644 --- a/dGame/dComponents/SimplePhysicsComponent.cpp +++ b/dGame/dComponents/SimplePhysicsComponent.cpp @@ -16,7 +16,7 @@ #include "Amf3.h" SimplePhysicsComponent::SimplePhysicsComponent(Entity* parent, const int32_t componentID) : PhysicsComponent(parent, componentID) { - RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &SimplePhysicsComponent::OnGetObjectReportInfo); + RegisterMsg(&SimplePhysicsComponent::OnGetObjectReportInfo); m_Position = m_Parent->GetDefaultPosition(); m_Rotation = m_Parent->GetDefaultRotation(); @@ -76,9 +76,8 @@ void SimplePhysicsComponent::SetPhysicsMotionState(uint32_t value) { m_PhysicsMotionState = value; } -bool SimplePhysicsComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { - PhysicsComponent::OnGetObjectReportInfo(msg); - auto& reportInfo = static_cast(msg); +bool SimplePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { + PhysicsComponent::OnGetObjectReportInfo(reportInfo); auto& info = reportInfo.subCategory->PushDebug("Simple Physics Info"); auto& velocity = info.PushDebug("Velocity"); velocity.PushDebug("x") = m_Velocity.x; diff --git a/dGame/dComponents/SimplePhysicsComponent.h b/dGame/dComponents/SimplePhysicsComponent.h index d928e489..40268922 100644 --- a/dGame/dComponents/SimplePhysicsComponent.h +++ b/dGame/dComponents/SimplePhysicsComponent.h @@ -86,7 +86,7 @@ public: void SetClimbableType(const eClimbableType& value) { m_ClimbableType = value; } private: - bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); /** * The current velocity of the entity diff --git a/dGame/dComponents/SkillComponent.cpp b/dGame/dComponents/SkillComponent.cpp index 5ac27bb6..1d4eb29d 100644 --- a/dGame/dComponents/SkillComponent.cpp +++ b/dGame/dComponents/SkillComponent.cpp @@ -125,8 +125,7 @@ void SkillComponent::SyncPlayerProjectile(const LWOOBJID projectileId, RakNet::B this->m_managedProjectiles.erase(this->m_managedProjectiles.begin() + index); GameMessages::ActivityNotify notify; - notify.notification.push_back( std::make_unique>(u"shot_done", sync_entry.skillId)); - + notify.notification.Insert(u"shot_done", sync_entry.skillId); m_Parent->OnActivityNotify(notify); } diff --git a/dGame/dComponents/SwitchComponent.h b/dGame/dComponents/SwitchComponent.h index ecbdeb73..755d134a 100644 --- a/dGame/dComponents/SwitchComponent.h +++ b/dGame/dComponents/SwitchComponent.h @@ -67,6 +67,10 @@ public: */ static SwitchComponent* GetClosestSwitch(NiPoint3 position); + const std::vector& GetFactionsToRespondTo() const { + return m_FactionsToRespondTo; + } + private: /** * A list of all pet switches. diff --git a/dGame/dComponents/TriggerComponent.cpp b/dGame/dComponents/TriggerComponent.cpp index e0bd1c4c..19384938 100644 --- a/dGame/dComponents/TriggerComponent.cpp +++ b/dGame/dComponents/TriggerComponent.cpp @@ -385,26 +385,30 @@ void TriggerComponent::HandleSetPhysicsVolumeEffect(Entity* targetEntity, std::v } phantomPhysicsComponent->SetPhysicsEffectActive(true); ePhysicsEffectType effectType = ePhysicsEffectType::PUSH; - std::transform(argArray.at(0).begin(), argArray.at(0).end(), argArray.at(0).begin(), ::tolower); //Transform to lowercase - if (argArray.at(0) == "push") effectType = ePhysicsEffectType::PUSH; - else if (argArray.at(0) == "attract") effectType = ePhysicsEffectType::ATTRACT; - else if (argArray.at(0) == "repulse") effectType = ePhysicsEffectType::REPULSE; - else if (argArray.at(0) == "gravity") effectType = ePhysicsEffectType::GRAVITY_SCALE; - else if (argArray.at(0) == "friction") effectType = ePhysicsEffectType::FRICTION; + if (!argArray.empty()) { + std::transform(argArray.at(0).begin(), argArray.at(0).end(), argArray.at(0).begin(), ::tolower); //Transform to lowercase + if (argArray.at(0) == "push") effectType = ePhysicsEffectType::PUSH; + else if (argArray.at(0) == "attract") effectType = ePhysicsEffectType::ATTRACT; + else if (argArray.at(0) == "repulse") effectType = ePhysicsEffectType::REPULSE; + else if (argArray.at(0) == "gravity") effectType = ePhysicsEffectType::GRAVITY_SCALE; + else if (argArray.at(0) == "friction") effectType = ePhysicsEffectType::FRICTION; + } phantomPhysicsComponent->SetEffectType(effectType); - phantomPhysicsComponent->SetDirectionalMultiplier(std::stof(argArray.at(1))); + if (argArray.size() > 1) { + phantomPhysicsComponent->SetDirectionalMultiplier(GeneralUtils::TryParse(argArray.at(1), 0.0f)); + } if (argArray.size() > 4) { const NiPoint3 direction = - GeneralUtils::TryParse(argArray.at(2), argArray.at(3), argArray.at(4)).value_or(NiPoint3Constant::ZERO); + GeneralUtils::TryParse(argArray.at(2), argArray.at(3), argArray.at(4), NiPoint3Constant::ZERO); phantomPhysicsComponent->SetDirection(direction); } - if (argArray.size() > 5) { - const uint32_t min = GeneralUtils::TryParse(argArray.at(6)).value_or(0); + if (argArray.size() > 6) { + const uint32_t min = GeneralUtils::TryParse(argArray.at(6), 0); phantomPhysicsComponent->SetMin(min); - const uint32_t max = GeneralUtils::TryParse(argArray.at(7)).value_or(0); + const uint32_t max = GeneralUtils::TryParse(argArray.at(7), 0); phantomPhysicsComponent->SetMax(max); } diff --git a/dGame/dEntity/EntityInfo.h b/dGame/dEntity/EntityInfo.h index e16c315d..68eaa95d 100644 --- a/dGame/dEntity/EntityInfo.h +++ b/dGame/dEntity/EntityInfo.h @@ -34,7 +34,7 @@ struct EntityInfo { LOT lot; NiPoint3 pos; NiQuaternion rot = QuatUtils::IDENTITY; - std::vector settings; - std::vector networkSettings; + LwoNameValue settings; + LwoNameValue networkSettings; float scale; }; diff --git a/dGame/dGameMessages/DoClientProjectileImpact.h b/dGame/dGameMessages/DoClientProjectileImpact.h index c0354e47..16d326fa 100644 --- a/dGame/dGameMessages/DoClientProjectileImpact.h +++ b/dGame/dGameMessages/DoClientProjectileImpact.h @@ -61,6 +61,7 @@ public: uint32_t sBitStreamLength{}; stream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false; for (uint32_t k = 0; k < sBitStreamLength; k++) { unsigned char character; stream.Read(character); diff --git a/dGame/dGameMessages/EchoStartSkill.h b/dGame/dGameMessages/EchoStartSkill.h index 0afef3f4..f2229f88 100644 --- a/dGame/dGameMessages/EchoStartSkill.h +++ b/dGame/dGameMessages/EchoStartSkill.h @@ -100,6 +100,7 @@ public: uint32_t sBitStreamLength{}; stream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false; for (uint32_t k = 0; k < sBitStreamLength; k++) { unsigned char character; stream.Read(character); diff --git a/dGame/dGameMessages/EchoSyncSkill.h b/dGame/dGameMessages/EchoSyncSkill.h index 5ea866f1..63c23b3e 100644 --- a/dGame/dGameMessages/EchoSyncSkill.h +++ b/dGame/dGameMessages/EchoSyncSkill.h @@ -47,6 +47,7 @@ public: uint32_t sBitStreamLength{}; stream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false; for (unsigned int k = 0; k < sBitStreamLength; k++) { unsigned char character; stream.Read(character); diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index 1e89e2a9..e7aa97f9 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -61,6 +61,11 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System User* usr = UserManager::Instance()->GetUser(sysAddr); + if (!usr) { + LOG("Failed to find a logged in user for (%llu), aborting GM: %4i, %s!", objectID, messageID, StringifiedEnum::ToString(messageID).data()); + return; + } + if (!entity) { LOG("Failed to find associated entity (%llu), aborting GM: %4i, %s!", objectID, messageID, StringifiedEnum::ToString(messageID).data()); return; @@ -76,7 +81,8 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System if (msg->requiredGmLevel > eGameMasterLevel::CIVILIAN) { auto* usingEntity = Game::entityManager->GetEntity(usr->GetLoggedInChar()); if (!usingEntity || usingEntity->GetGMLevel() < msg->requiredGmLevel) { - LOG("User %s (%llu) does not have the required GM level to execute this command.", usingEntity->GetCharacter()->GetName().c_str(), usingEntity->GetObjectID()); + if (usingEntity) LOG("User %s (%llu) does not have the required GM level to execute this command.", usingEntity->GetCharacter()->GetName().c_str(), usingEntity->GetObjectID()); + else LOG("ObjectID %llu tried to use a gm required message.", usr->GetLoggedInChar()); return; } } @@ -167,8 +173,8 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::SendRestoreToPostLoadStats(entity, sysAddr); - auto* destroyable = entity->GetComponent(); - destroyable->SetImagination(destroyable->GetImagination()); + auto* const destroyable = entity->GetComponent(); + if (destroyable) destroyable->SetImagination(destroyable->GetImagination()); Game::entityManager->SerializeEntity(entity); std::vector racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL); @@ -186,7 +192,7 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System std::vector scriptedActs = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::SCRIPT); for (Entity* scriptEntity : scriptedActs) { - if (scriptEntity->GetObjectID() != zoneControl->GetObjectID()) { // Don't want to trigger twice on instance worlds + if (!zoneControl || scriptEntity->GetObjectID() != zoneControl->GetObjectID()) { // Don't want to trigger twice on instance worlds scriptEntity->GetScript()->OnPlayerLoaded(scriptEntity, entity); } } @@ -332,9 +338,9 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System if (behaviorId > 0) { auto bs = RakNet::BitStream(reinterpret_cast(&startSkill.sBitStream[0]), startSkill.sBitStream.size(), false); - auto* skillComponent = entity->GetComponent(); + auto* const skillComponent = entity->GetComponent(); - success = skillComponent->CastPlayerSkill(behaviorId, startSkill.uiSkillHandle, bs, startSkill.optionalTargetID, startSkill.skillID); + if (skillComponent) success = skillComponent->CastPlayerSkill(behaviorId, startSkill.uiSkillHandle, bs, startSkill.optionalTargetID, startSkill.skillID); if (success && entity->GetCharacter()) { DestroyableComponent* destComp = entity->GetComponent(); @@ -387,9 +393,9 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System if (usr != nullptr) { auto bs = RakNet::BitStream(reinterpret_cast(&sync.sBitStream[0]), sync.sBitStream.size(), false); - auto* skillComponent = entity->GetComponent(); + auto* const skillComponent = entity->GetComponent(); - skillComponent->SyncPlayerSkill(sync.uiSkillHandle, sync.uiBehaviorHandle, bs); + if (skillComponent) skillComponent->SyncPlayerSkill(sync.uiSkillHandle, sync.uiBehaviorHandle, bs); } EchoSyncSkill echo = EchoSyncSkill(); diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 522f4741..0acb4452 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include "RakString.h" //CDB includes: @@ -365,18 +366,19 @@ void GameMessages::SendResetMissions(Entity* entity, const SystemAddress& sysAdd void GameMessages::SendPlatformResync(Entity* entity, const SystemAddress& sysAddr, bool bStopAtDesiredWaypoint, int iIndex, int iDesiredWaypointIndex, int nextIndex, - eMovementPlatformState movementState) { + eMovementPlatformState movementState, bool special) { CBITSTREAM; CMSGHEADER; + const auto objID = entity->GetObjectID(); const auto lot = entity->GetLOT(); - if (lot == 12341 || lot == 5027 || lot == 5028 || lot == 14335 || lot == 14447 || lot == 14449 || lot == 11306 || lot == 11308) { + if (lot == 12341 || lot == 5027 || lot == 5028 || lot == 14335 || lot == 14447 || lot == 14449 || lot == 11306 || lot == 11308 || lot == 9483) { iDesiredWaypointIndex = (lot == 11306 || lot == 11308) ? 1 : 0; - iIndex = 0; - nextIndex = 0; + iIndex = lot == 9483 ? 1 : 0; + nextIndex = lot == 9483 && !special ? 1 : 0; bStopAtDesiredWaypoint = true; - movementState = eMovementPlatformState::Stationary; + movementState = lot == 9483 && !special ? eMovementPlatformState::Stopped : eMovementPlatformState::Stationary; } bitStream.Write(entity->GetObjectID()); @@ -465,20 +467,20 @@ void GameMessages::SendAddItemToInventoryClientSync(Entity* entity, const System bitStream.Write(lootSourceType != eLootSourceType::NONE); // Loot source if (lootSourceType != eLootSourceType::NONE) bitStream.Write(lootSourceType); - LWONameValue extraInfo; + std::u16string extraInfo; - auto config = item->GetConfig(); + const auto& config = item->GetConfig(); - for (auto* data : config) { - extraInfo.name += GeneralUtils::ASCIIToUTF16(data->GetString()) + u","; + for (const auto& data : config.values | std::views::values) { + extraInfo += GeneralUtils::ASCIIToUTF16(data->GetString()) + u","; } - if (extraInfo.name.length() > 0) extraInfo.name.pop_back(); // remove the last comma + if (extraInfo.length() > 0) extraInfo.pop_back(); // remove the last comma - bitStream.Write(extraInfo.name.size()); - if (extraInfo.name.size() > 0) { - for (uint32_t i = 0; i < extraInfo.name.size(); ++i) { - bitStream.Write(extraInfo.name[i]); + bitStream.Write(extraInfo.size()); + if (extraInfo.size() > 0) { + for (uint32_t i = 0; i < extraInfo.size(); ++i) { + bitStream.Write(extraInfo[i]); } bitStream.Write(0x00); } @@ -743,13 +745,9 @@ void GameMessages::SendBroadcastTextToChatbox(Entity* entity, const SystemAddres bitStream.Write(entity->GetObjectID()); bitStream.Write(MessageType::Game::BROADCAST_TEXT_TO_CHATBOX); - LWONameValue attribs; - attribs.name = attrs; - attribs.length = attrs.size(); - - bitStream.Write(attribs.length); - for (uint32_t i = 0; i < attribs.length; ++i) { - bitStream.Write(attribs.name[i]); + bitStream.Write(attrs.size()); + for (uint32_t i = 0; i < attrs.size(); ++i) { + bitStream.Write(attrs[i]); } bitStream.Write(0x00); // Null Terminator @@ -1847,18 +1845,6 @@ void GameMessages::SendNotifyClientFailedPrecondition(LWOOBJID objectId, const S SEND_PACKET; } -void GameMessages::SendToggleGMInvis(LWOOBJID objectId, bool enabled, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::TOGGLE_GM_INVIS); - bitStream.Write(enabled); // does not matter? - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - void GameMessages::SendSetName(LWOOBJID objectID, std::u16string name, const SystemAddress& sysAddr) { CBITSTREAM; CMSGHEADER; @@ -2149,6 +2135,7 @@ void GameMessages::HandleUpdatePropertyOrModelForFilterCheck(RakNet::BitStream& inStream.Read(worldId); inStream.Read(descriptionLength); + if (descriptionLength > MAX_MESSAGE_LENGTH) return; for (uint32_t i = 0; i < descriptionLength; ++i) { uint16_t character; inStream.Read(character); @@ -2156,6 +2143,7 @@ void GameMessages::HandleUpdatePropertyOrModelForFilterCheck(RakNet::BitStream& } inStream.Read(nameLength); + if (nameLength > MAX_MESSAGE_LENGTH) return; for (uint32_t i = 0; i < nameLength; ++i) { uint16_t character; inStream.Read(character); @@ -2417,12 +2405,26 @@ void GameMessages::SendUnSmash(Entity* entity, LWOOBJID builderID, float duratio void GameMessages::HandleControlBehaviors(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { AMFDeserialize reader; - std::unique_ptr amfArguments{ static_cast(reader.Read(inStream).release()) }; + std::unique_ptr amfArguments; + try { + auto deserializedData = reader.Read(inStream); + if (!deserializedData || deserializedData->GetValueType() != eAmf::Array) { + LOG("Failed to deserialize AMF data for control behaviors command: not an array"); + return; + } + + amfArguments.reset(static_cast(deserializedData.release())); + } catch (...) { + LOG("Failed to deserialize AMF data for control behaviors command"); + return; + } if (amfArguments->GetValueType() != eAmf::Array) return; uint32_t commandLength{}; inStream.Read(commandLength); + if (commandLength > MAX_MESSAGE_LENGTH) return; // Prevent DoS via unbounded command buffer + std::string command; command.reserve(commandLength); for (uint32_t i = 0; i < commandLength; ++i) { @@ -2472,9 +2474,15 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent uint32_t sd0Size; inStream.Read(sd0Size); - std::unique_ptr sd0Data(new char[sd0Size]); - if (sd0Data == nullptr) return; + // For the sake of letting players make models as big as they want, only reject if we cant allocate the required memory. + std::unique_ptr sd0Data; + try { + sd0Data.reset(new char[sd0Size]); + } catch (std::exception& e) { + LOG("Failed to allocate sd0 of size %u", sd0Size); + return; + } inStream.ReadAlignedBytes(reinterpret_cast(sd0Data.get()), sd0Size); @@ -2611,11 +2619,11 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent info.spawnerID = entity->GetObjectID(); info.spawnerNodeID = 0; - info.settings.push_back(new LDFData(u"blueprintid", blueprintIDs[i])); - info.settings.push_back(new LDFData(u"componentWhitelist", 1)); - info.settings.push_back(new LDFData(u"modelType", 2)); - info.settings.push_back(new LDFData(u"propertyObjectID", true)); - info.settings.push_back(new LDFData(u"userModelID", modelIDs[i])); + info.settings.Insert(u"blueprintid", blueprintIDs[i]); + info.settings.Insert(u"componentWhitelist", 1); + info.settings.Insert(u"modelType", 2); + info.settings.Insert(u"propertyObjectID", true); + info.settings.Insert(u"userModelID", modelIDs[i]); Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr); if (newEntity) { Game::entityManager->ConstructEntity(newEntity); @@ -2650,6 +2658,7 @@ void GameMessages::HandlePropertyEntranceSync(RakNet::BitStream& inStream, Entit inStream.Read(startIndex); inStream.Read(filterTextLength); + if (filterTextLength > MAX_MESSAGE_LENGTH) return; for (auto i = 0u; i < filterTextLength; i++) { char c; inStream.Read(c); @@ -3038,6 +3047,7 @@ void GameMessages::HandleVerifyAck(RakNet::BitStream& inStream, Entity* entity, uint32_t sBitStreamLength = 0; inStream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return; for (uint64_t k = 0; k < sBitStreamLength; k++) { uint8_t character; inStream.Read(character); @@ -3209,7 +3219,7 @@ void GameMessages::HandleClientTradeRequest(RakNet::BitStream& inStream, Entity* LOG("Trade request to (%llu)", i64Invitee); - auto* trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); + const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); if (trade != nullptr) { if (!trade->IsParticipant(i64Invitee)) { @@ -3232,7 +3242,7 @@ void GameMessages::HandleClientTradeRequest(RakNet::BitStream& inStream, Entity* } void GameMessages::HandleClientTradeCancel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - auto* trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); + const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); if (trade == nullptr) return; @@ -3246,7 +3256,7 @@ void GameMessages::HandleClientTradeAccept(RakNet::BitStream& inStream, Entity* LOG("Trade accepted from (%llu) -> (%d)", entity->GetObjectID(), bFirst); - auto* trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); + const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); if (trade == nullptr) return; @@ -3259,6 +3269,7 @@ void GameMessages::HandleClientTradeUpdate(RakNet::BitStream& inStream, Entity* inStream.Read(currency); inStream.Read(itemCount); + if (itemCount > MAX_MESSAGE_LENGTH) return; LOG("Trade update from (%llu) -> (%llu), (%i)", entity->GetObjectID(), currency, itemCount); @@ -3311,7 +3322,7 @@ void GameMessages::HandleClientTradeUpdate(RakNet::BitStream& inStream, Entity* LOG("Trade item from (%llu) -> (%llu)/(%llu), (%i), (%llu), (%i), (%i)", entity->GetObjectID(), itemId, itemId2, lot, unknown1, unknown2, unknown3); } - auto* trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); + const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); if (trade == nullptr) return; @@ -3628,6 +3639,8 @@ void GameMessages::HandlePetTamingTryBuild(RakNet::BitStream& inStream, Entity* inStream.Read(brickCount); + if (brickCount > MAX_MESSAGE_LENGTH) return; // Prevent DoS via unbounded brick count + bricks.reserve(brickCount); for (uint32_t i = 0; i < brickCount; i++) { @@ -3669,6 +3682,7 @@ void GameMessages::HandleRequestSetPetName(RakNet::BitStream& inStream, Entity* inStream.Read(nameLength); + if (nameLength > MAX_MESSAGE_LENGTH) return; for (size_t i = 0; i < nameLength; i++) { char16_t character; inStream.Read(character); @@ -3738,6 +3752,7 @@ void GameMessages::HandleMessageBoxResponse(RakNet::BitStream& inStream, Entity* inStream.Read(iButton); inStream.Read(identifierLength); + if (identifierLength > MAX_MESSAGE_LENGTH) return; for (size_t i = 0; i < identifierLength; i++) { char16_t character; inStream.Read(character); @@ -3745,6 +3760,7 @@ void GameMessages::HandleMessageBoxResponse(RakNet::BitStream& inStream, Entity* } inStream.Read(userDataLength); + if (userDataLength > MAX_MESSAGE_LENGTH) return; for (size_t i = 0; i < userDataLength; i++) { char16_t character; inStream.Read(character); @@ -3792,6 +3808,7 @@ void GameMessages::HandleChoiceBoxRespond(RakNet::BitStream& inStream, Entity* e std::u16string identifier; inStream.Read(buttonIdentifierLength); + if (buttonIdentifierLength > MAX_MESSAGE_LENGTH) return; for (size_t i = 0; i < buttonIdentifierLength; i++) { char16_t character; inStream.Read(character); @@ -3801,6 +3818,7 @@ void GameMessages::HandleChoiceBoxRespond(RakNet::BitStream& inStream, Entity* e inStream.Read(iButton); inStream.Read(identifierLength); + if (identifierLength > MAX_MESSAGE_LENGTH) return; for (size_t i = 0; i < identifierLength; i++) { char16_t character; inStream.Read(character); @@ -4154,7 +4172,13 @@ void GameMessages::HandleUpdatePropertyPerformanceCost(RakNet::BitStream& inStre return; } - Database::Get()->UpdatePerformanceCost(zone->GetZoneID(), performanceCost); + const auto* const propertyManagementComponent = entity->GetComponent(); + const auto* const ownerEntity = propertyManagementComponent ? propertyManagementComponent->GetOwner() : nullptr; + const auto* const character = ownerEntity ? ownerEntity->GetCharacter() : nullptr; + const auto& zoneID = zone->GetZoneID(); + if (character && character->GetPropertyCloneID() == zoneID.GetCloneID()) { + Database::Get()->UpdatePerformanceCost(zoneID, performanceCost); + } } void GameMessages::HandleVehicleNotifyHitImaginationServer(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { @@ -4779,13 +4803,19 @@ void GameMessages::HandleParseChatMessage(RakNet::BitStream& inStream, Entity* e uint32_t wsStringLength; inStream.Read(wsStringLength); + + if (wsStringLength > MAX_MESSAGE_LENGTH) { + LOG("Max message length reached, capping message."); + wsStringLength = MAX_MESSAGE_LENGTH; + } + for (uint32_t i = 0; i < wsStringLength; ++i) { uint16_t character; inStream.Read(character); wsString.push_back(character); } - if (wsString[0] == L'/') { + if (!wsString.empty() && wsString[0] == L'/') { SlashCommandHandler::HandleChatCommand(wsString, entity, sysAddr); } } @@ -4802,6 +4832,7 @@ void GameMessages::HandleFireEventServerSide(RakNet::BitStream& inStream, Entity LWOOBJID senderID{}; inStream.Read(argsLength); + if (argsLength > MAX_MESSAGE_LENGTH) return; for (uint32_t i = 0; i < argsLength; ++i) { uint16_t character; inStream.Read(character); @@ -5233,7 +5264,8 @@ void GameMessages::HandleRemoveItemFromInventory(RakNet::BitStream& inStream, En int eInvType = INVENTORY_MAX; bool eLootTypeSourceIsDefault = false; int eLootTypeSource = LOOTTYPE_NONE; - LWONameValue extraInfo; + int32_t extraInfoLength = 0; + std::u16string extraInfo; bool forceDeletion = true; bool iLootTypeSourceIsDefault = false; LWOOBJID iLootTypeSource = LWOOBJID_EMPTY; @@ -5259,12 +5291,12 @@ void GameMessages::HandleRemoveItemFromInventory(RakNet::BitStream& inStream, En if (eInvTypeIsDefault) inStream.Read(eInvType); inStream.Read(eLootTypeSourceIsDefault); if (eLootTypeSourceIsDefault) inStream.Read(eLootTypeSource); - inStream.Read(extraInfo.length); - if (extraInfo.length > 0) { - for (uint32_t i = 0; i < extraInfo.length; ++i) { + inStream.Read(extraInfoLength); + if (extraInfoLength > 0) { + for (uint32_t i = 0; i < extraInfoLength; ++i) { uint16_t character; inStream.Read(character); - extraInfo.name.push_back(character); + extraInfo.push_back(character); } uint16_t nullTerm; inStream.Read(nullTerm); @@ -5432,7 +5464,7 @@ void GameMessages::HandleModularBuildFinish(RakNet::BitStream& inStream, Entity* std::vector modList; auto& oldPartList = character->GetVar(u"currentModifiedBuild"); bool everyPieceSwapped = !oldPartList.empty(); // If the player didn't put a build in initially, then they should not get this achievement. - if (count >= 3) { + if (count >= 3 && count < 8) { std::u16string modules; for (uint32_t k = 0; k < count; k++) { @@ -5472,10 +5504,8 @@ void GameMessages::HandleModularBuildFinish(RakNet::BitStream& inStream, Entity* //inv->UnequipItem(inv->GetItemStackByLOT(6086, eInventoryType::ITEMS)); // take off the thinking cap //Game::entityManager->SerializeEntity(entity); - const auto moduleAssembly = new LDFData(u"assemblyPartLOTs", modules); - - std::vector config; - config.push_back(moduleAssembly); + LwoNameValue config; + config.Insert(u"assemblyPartLOTs", modules); LWOOBJID newID = ObjectIDManager::GetPersistentID(); @@ -5721,7 +5751,6 @@ void GameMessages::HandleUseNonEquipmentItem(RakNet::BitStream& inStream, Entity void GameMessages::HandleMatchRequest(RakNet::BitStream& inStream, Entity* entity) { LWOOBJID activator; - //std::map additionalPlayers; uint32_t playerChoicesLen; std::string playerChoices; int type; @@ -5729,6 +5758,7 @@ void GameMessages::HandleMatchRequest(RakNet::BitStream& inStream, Entity* entit inStream.Read(activator); inStream.Read(playerChoicesLen); + if (playerChoicesLen > MAX_MESSAGE_LENGTH) return; for (uint32_t i = 0; i < playerChoicesLen; ++i) { uint16_t character; inStream.Read(character); @@ -5818,6 +5848,8 @@ void GameMessages::HandleReportBug(RakNet::BitStream& inStream, Entity* entity) uint32_t messageLength; inStream.Read(messageLength); + if (messageLength > MAX_MESSAGE_LENGTH) return; + for (uint32_t i = 0; i < (messageLength); ++i) { uint16_t character; inStream.Read(character); @@ -5829,6 +5861,7 @@ void GameMessages::HandleReportBug(RakNet::BitStream& inStream, Entity* entity) uint32_t clientVersionLength; inStream.Read(clientVersionLength); + if (clientVersionLength > MAX_MESSAGE_LENGTH) return; for (unsigned int k = 0; k < clientVersionLength; k++) { unsigned char character; inStream.Read(character); @@ -5837,6 +5870,7 @@ void GameMessages::HandleReportBug(RakNet::BitStream& inStream, Entity* entity) uint32_t nOtherPlayerIDLength; inStream.Read(nOtherPlayerIDLength); + if (nOtherPlayerIDLength > MAX_MESSAGE_LENGTH) return; for (unsigned int k = 0; k < nOtherPlayerIDLength; k++) { unsigned char character; inStream.Read(character); @@ -5845,6 +5879,7 @@ void GameMessages::HandleReportBug(RakNet::BitStream& inStream, Entity* entity) uint32_t selectionLength; inStream.Read(selectionLength); + if (selectionLength > MAX_MESSAGE_LENGTH) return; for (unsigned int k = 0; k < selectionLength; k++) { unsigned char character; inStream.Read(character); @@ -5881,7 +5916,7 @@ void GameMessages::HandlePlayerRailArrivedNotification(RakNet::BitStream& inStre const SystemAddress& sysAddr) { uint32_t pathNameLength; inStream.Read(pathNameLength); - + if (pathNameLength > MAX_MESSAGE_LENGTH) return; std::u16string pathName; for (auto k = 0; k < pathNameLength; k++) { uint16_t c; @@ -5943,6 +5978,7 @@ void GameMessages::HandleUpdatePlayerStatistic(RakNet::BitStream& inStream, Enti void GameMessages::HandleDeactivateBubbleBuff(RakNet::BitStream& inStream, Entity* entity) { auto controllablePhysicsComponent = entity->GetComponent(); if (controllablePhysicsComponent) controllablePhysicsComponent->DeactivateBubbleBuff(); + GameMessages::SendDeactivateBubbleBuffFromServer(entity->GetObjectID(), entity->GetSystemAddress()); } void GameMessages::HandleActivateBubbleBuff(RakNet::BitStream& inStream, Entity* entity) { @@ -6146,14 +6182,17 @@ void GameMessages::HandleUpdateInventoryGroup(RakNet::BitStream& inStream, Entit uint32_t size{}; if (!inStream.Read(size)) return; + if (size > MAX_MESSAGE_LENGTH) return; // Bounds check before resize action.resize(size); if (!inStream.Read(action.data(), size)) return; if (!inStream.Read(size)) return; + if (size > MAX_MESSAGE_LENGTH) return; // Bounds check before resize groupUpdate.groupId.resize(size); if (!inStream.Read(groupUpdate.groupId.data(), size)) return; if (!inStream.Read(size)) return; + if (size > MAX_MESSAGE_LENGTH / 2) return; // Bounds check: size * 2 would overflow or exceed limit groupName.resize(size); if (!inStream.Read(reinterpret_cast(groupName.data()), size * 2)) return; @@ -6266,7 +6305,7 @@ namespace GameMessages { bitStream.Write(id); std::string toWrite; - for (const auto* item : localizeParams) { + for (const auto& item : localizeParams | std::views::values) { toWrite += item->GetString() + "\n"; } if (!toWrite.empty()) toWrite.pop_back(); @@ -6446,7 +6485,11 @@ namespace GameMessages { } void TeamPickupItem::Serialize(RakNet::BitStream& stream) const { - stream.Write(lootID); - stream.Write(lootOwnerID); + stream.Write(lootID); + stream.Write(lootOwnerID); + } + + void ToggleGMInvis::Serialize(RakNet::BitStream& stream) const { + stream.Write(bStateOut); } } diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 1b52d67a..7a34de6c 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -13,6 +13,7 @@ #include "Brick.h" #include "MessageType/Game.h" #include "eGameMasterLevel.h" +#include "LDFFormat.h" class AMFBaseValue; class AMFArrayValue; @@ -102,9 +103,11 @@ namespace GameMessages { void SendPlayNDAudioEmitter(Entity* entity, const SystemAddress& sysAddr, std::string audioGUID); void SendStartPathing(Entity* entity); + + // special is for the FV tree platform, feature is complete if we just do that so meh void SendPlatformResync(Entity* entity, const SystemAddress& sysAddr, bool bStopAtDesiredWaypoint = false, int iIndex = 0, int iDesiredWaypointIndex = 1, int nextIndex = 1, - eMovementPlatformState movementState = eMovementPlatformState::Moving); + eMovementPlatformState movementState = eMovementPlatformState::Moving, bool special = false); void SendResetMissions(Entity* entity, const SystemAddress& sysAddr, const int32_t missionid = -1); void SendRestoreToPostLoadStats(Entity* entity, const SystemAddress& sysAddr); @@ -243,8 +246,6 @@ namespace GameMessages { bool cancelOnLogout = false, bool cancelOnRemoveBuff = true, bool cancelOnUi = false, bool cancelOnUnequip = false, bool cancelOnZone = false, bool addedByTeammate = false, bool applyOnTeammates = false, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS); - void SendToggleGMInvis(LWOOBJID objectId, bool enabled, const SystemAddress& sysAddr); - void SendSetName(LWOOBJID objectID, std::u16string name, const SystemAddress& sysAddr); // Property messages @@ -713,7 +714,7 @@ namespace GameMessages { bool translate{}; int32_t time{}; std::u16string id{}; - std::vector localizeParams{}; + LwoNameValue localizeParams{}; std::u16string imageName{}; std::u16string text{}; void Serialize(RakNet::BitStream& bitStream) const override; @@ -736,7 +737,7 @@ namespace GameMessages { struct ConfigureRacingControl : public GameMsg { ConfigureRacingControl() : GameMsg(MessageType::Game::CONFIGURE_RACING_CONTROL) {} - std::vector> racingSettings{}; + LwoNameValue racingSettings{}; }; struct SetModelToBuild : public GameMsg { @@ -756,7 +757,7 @@ namespace GameMessages { struct ActivityNotify : public GameMsg { ActivityNotify() : GameMsg(MessageType::Game::ACTIVITY_NOTIFY) {} - std::vector> notification{}; + LwoNameValue notification{}; }; struct ShootingGalleryFire : public GameMsg { @@ -848,6 +849,11 @@ namespace GameMessages { struct ResetModelToDefaults : public GameMsg { ResetModelToDefaults() : GameMsg(MessageType::Game::RESET_MODEL_TO_DEFAULTS) {} + + bool bResetPos{ true }; + bool bResetRot{ true }; + bool bUnSmash{ true }; + bool bResetBehaviors{ true }; }; struct EmotePlayed : public GameMsg { @@ -932,5 +938,38 @@ namespace GameMessages { LWOOBJID lootID{}; LWOOBJID lootOwnerID{}; }; + + struct IsDead : public GameMsg { + IsDead() : GameMsg(MessageType::Game::IS_DEAD) {} + + bool bDead{}; + }; + + struct ToggleGMInvis : public GameMsg { + ToggleGMInvis() : GameMsg(MessageType::Game::TOGGLE_GM_INVIS) {} + + void Serialize(RakNet::BitStream& stream) const override; + bool bStateOut{ false }; + + }; + + struct GetGMInvis : public GameMsg { + GetGMInvis() : GameMsg(MessageType::Game::GET_GM_INVIS) {} + + bool bGMInvis{ false }; + }; + + struct ChildRemoved : public GameMsg { + ChildRemoved() : GameMsg(MessageType::Game::CHILD_REMOVED) {} + + LWOOBJID childID{}; + }; + + struct ObjectLoaded : public GameMsg { + ObjectLoaded() : GameMsg(MessageType::Game::OBJECT_LOADED) {} + + LWOOBJID objectID{}; + LOT lot{}; + }; }; #endif // GAMEMESSAGES_H diff --git a/dGame/dGameMessages/RequestServerProjectileImpact.h b/dGame/dGameMessages/RequestServerProjectileImpact.h index 394bd9c7..18158399 100644 --- a/dGame/dGameMessages/RequestServerProjectileImpact.h +++ b/dGame/dGameMessages/RequestServerProjectileImpact.h @@ -54,6 +54,7 @@ public: uint32_t sBitStreamLength{}; stream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false; for (uint32_t k = 0; k < sBitStreamLength; k++) { unsigned char character; stream.Read(character); diff --git a/dGame/dGameMessages/StartSkill.h b/dGame/dGameMessages/StartSkill.h index 91e35572..6ca51008 100644 --- a/dGame/dGameMessages/StartSkill.h +++ b/dGame/dGameMessages/StartSkill.h @@ -111,6 +111,7 @@ public: uint32_t sBitStreamLength{}; stream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false; for (uint32_t k = 0; k < sBitStreamLength; k++) { unsigned char character; stream.Read(character); diff --git a/dGame/dGameMessages/SyncSkill.h b/dGame/dGameMessages/SyncSkill.h index fb5525bc..3128ce91 100644 --- a/dGame/dGameMessages/SyncSkill.h +++ b/dGame/dGameMessages/SyncSkill.h @@ -46,6 +46,7 @@ public: stream.Read(bDone); uint32_t sBitStreamLength{}; stream.Read(sBitStreamLength); + if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false; for (uint32_t k = 0; k < sBitStreamLength; k++) { unsigned char character; stream.Read(character); diff --git a/dGame/dInventory/EquippedItem.h b/dGame/dInventory/EquippedItem.h index 78da9e8d..ec10f95b 100644 --- a/dGame/dInventory/EquippedItem.h +++ b/dGame/dInventory/EquippedItem.h @@ -31,5 +31,5 @@ struct EquippedItem /** * The configuration of the item with any extra data */ - std::vector config = {}; + LwoNameValue config = {}; }; diff --git a/dGame/dInventory/Inventory.cpp b/dGame/dInventory/Inventory.cpp index db83ac58..f5742165 100644 --- a/dGame/dInventory/Inventory.cpp +++ b/dGame/dInventory/Inventory.cpp @@ -75,7 +75,8 @@ uint32_t Inventory::GetLotCount(const LOT lot) const { } void Inventory::SetSize(const uint32_t value) { - free += static_cast(value) - static_cast(size); + const auto delta = static_cast(value) - static_cast(size); + free = static_cast(std::max(0, static_cast(free) + delta)); size = value; diff --git a/dGame/dInventory/Item.cpp b/dGame/dInventory/Item.cpp index 7ee27f01..f18cd0b3 100644 --- a/dGame/dInventory/Item.cpp +++ b/dGame/dInventory/Item.cpp @@ -28,6 +28,7 @@ #include "CDObjectSkillsTable.h" #include "CDComponentsRegistryTable.h" #include "CDPackageComponentTable.h" +#include namespace { const std::map ExtraSettingAbbreviations = { @@ -46,7 +47,7 @@ namespace { }; } -Item::Item(const LWOOBJID id, const LOT lot, Inventory* inventory, const uint32_t slot, const uint32_t count, const bool bound, const std::vector& config, const LWOOBJID parent, LWOOBJID subKey, eLootSourceType lootSourceType) { +Item::Item(const LWOOBJID id, const LOT lot, Inventory* inventory, const uint32_t slot, const uint32_t count, const bool bound, const LwoNameValue& config, const LWOOBJID parent, LWOOBJID subKey, eLootSourceType lootSourceType) { if (!Inventory::IsValidItem(lot)) { return; } @@ -71,7 +72,7 @@ Item::Item( Inventory* inventory, const uint32_t slot, const uint32_t count, - const std::vector& config, + const LwoNameValue& config, const LWOOBJID parent, bool showFlyingLoot, bool isModMoveAndEquip, @@ -131,11 +132,11 @@ uint32_t Item::GetSlot() const { return slot; } -std::vector Item::GetConfig() const { +const LwoNameValue& Item::GetConfig() const { return config; } -std::vector& Item::GetConfig() { +LwoNameValue& Item::GetConfig() { return config; } @@ -379,7 +380,7 @@ void Item::UseNonEquip(Item* item) { } void Item::Disassemble(const eInventoryType inventoryType) { - for (auto* data : config) { + for (const auto& data : config.values | std::views::values) { if (data->GetKey() == u"assemblyPartLOTs") { auto modStr = data->GetValueAsString(); @@ -401,7 +402,8 @@ void Item::Disassemble(const eInventoryType inventoryType) { const auto deliminator = '+'; while (std::getline(ssData, token, deliminator)) { - const auto modLot = std::stoi(token.substr(2, token.size() - 1)); + if (token.size() <= 2) continue; // invalid token, must have size of at least 3. + const auto modLot = GeneralUtils::TryParse(token.substr(2, token.size() - 1), LOT_NULL); modArray.push_back(modLot); } @@ -440,7 +442,10 @@ void Item::DisassembleModel(uint32_t numToDismantle) { std::vector renderAssetSplit = GeneralUtils::SplitString(renderAsset, '/'); if (renderAssetSplit.empty()) return; - std::string lxfmlPath = "BrickModels" + lxfmlFolderName + "/" + GeneralUtils::SplitString(renderAssetSplit.back(), '.').at(0) + ".lxfml"; + const auto renderAssetSplitSplit = GeneralUtils::SplitString(renderAssetSplit.back(), '.'); + if (renderAssetSplitSplit.empty()) return; + + std::string lxfmlPath = "BrickModels" + lxfmlFolderName + "/" + renderAssetSplitSplit[0] + ".lxfml"; auto file = Game::assetManager->GetFile(lxfmlPath.c_str()); if (!file) { @@ -526,18 +531,12 @@ void Item::RemoveFromInventory() { Item::~Item() { delete preconditions; - - for (auto* value : config) { - delete value; - } - - config.clear(); } void Item::SaveConfigXml(tinyxml2::XMLElement& i) const { tinyxml2::XMLElement* x = nullptr; - for (const auto* config : this->config) { + for (const auto& config : config.values | std::views::values) { const auto& key = GeneralUtils::UTF16ToWTF8(config->GetKey()); const auto saveKey = ExtraSettingAbbreviations.find(key); if (saveKey == ExtraSettingAbbreviations.end()) { @@ -557,12 +556,11 @@ void Item::LoadConfigXml(const tinyxml2::XMLElement& i) { const auto* x = i.FirstChildElement("x"); if (!x) return; - for (const auto& pair : ExtraSettingAbbreviations) { - const auto* data = x->Attribute(pair.second.c_str()); + for (const auto& [fullName, abbreviation] : ExtraSettingAbbreviations) { + const auto* data = x->Attribute(abbreviation.c_str()); if (!data) continue; - const auto value = pair.first + "=" + data; - config.push_back(LDFBaseData::DataFromString(value)); + config.ParseInsert(fullName + "=" + data); } } diff --git a/dGame/dInventory/Item.h b/dGame/dInventory/Item.h index 846a7aa7..baccf28f 100644 --- a/dGame/dInventory/Item.h +++ b/dGame/dInventory/Item.h @@ -40,7 +40,7 @@ public: uint32_t slot, uint32_t count, bool bound, - const std::vector& config, + const LwoNameValue& config, LWOOBJID parent, LWOOBJID subKey, eLootSourceType lootSourceType = eLootSourceType::NONE @@ -64,7 +64,7 @@ public: Inventory* inventory, uint32_t slot = 0, uint32_t count = 1, - const std::vector& config = {}, + const LwoNameValue& config = {}, LWOOBJID parent = LWOOBJID_EMPTY, bool showFlyingLoot = true, bool isModMoveAndEquip = false, @@ -118,13 +118,13 @@ public: * Returns current config info for this item, e.g. for rockets * @return current config info for this item */ - std::vector& GetConfig(); + LwoNameValue& GetConfig(); /** * Returns current config info for this item, e.g. for rockets * @return current config info for this item */ - std::vector GetConfig() const; + const LwoNameValue& GetConfig() const; /** * Returns the database info for this item @@ -269,7 +269,7 @@ private: /** * Config data for this item, e.g. for rocket parts and car parts */ - std::vector config; + LwoNameValue config; /** * The inventory this item belongs to diff --git a/dGame/dInventory/ItemSet.cpp b/dGame/dInventory/ItemSet.cpp index d8d320ed..1544e611 100644 --- a/dGame/dInventory/ItemSet.cpp +++ b/dGame/dInventory/ItemSet.cpp @@ -128,8 +128,8 @@ void ItemSet::OnEquip(const LOT lot) { return; } - auto* skillComponent = m_InventoryComponent->GetParent()->GetComponent(); - auto* missionComponent = m_InventoryComponent->GetParent()->GetComponent(); + auto [skillComponent, missionComponent] = m_InventoryComponent->GetParent()->GetComponentsMut(); + if (!skillComponent || !missionComponent) return; // Nothing to do here if these are null for (const auto skill : skillSet) { auto* skillTable = CDClientManager::GetTable(); diff --git a/dGame/dMission/Mission.cpp b/dGame/dMission/Mission.cpp index 3d46cde8..4afb2207 100644 --- a/dGame/dMission/Mission.cpp +++ b/dGame/dMission/Mission.cpp @@ -74,23 +74,19 @@ Mission::Mission(MissionComponent* missionComponent, const uint32_t missionId) { void Mission::LoadFromXmlDone(const tinyxml2::XMLElement& element) { // Start custom XML - if (element.Attribute("state") != nullptr) { - m_State = static_cast(std::stoul(element.Attribute("state"))); - } + m_State = static_cast(element.UnsignedAttribute("state")); // End custom XML - if (element.Attribute("cct") != nullptr) { - m_Completions = std::stoul(element.Attribute("cct")); + m_Completions = element.UnsignedAttribute("cct"); - m_Timestamp = std::stoul(element.Attribute("cts")); - } + m_Timestamp = element.UnsignedAttribute("cts"); } void Mission::LoadFromXmlCur(const tinyxml2::XMLElement& element) { const auto* const character = GetCharacter(); // Start custom XML if (element.Attribute("state") != nullptr) { - m_State = static_cast(std::stoul(element.Attribute("state"))); + m_State = static_cast(element.IntAttribute("state", -1)); } // End custom XML @@ -106,7 +102,7 @@ void Mission::LoadFromXmlCur(const tinyxml2::XMLElement& element) { const auto type = curTask->GetType(); - auto value = std::stoul(task->Attribute("v")); + auto value = task->UnsignedAttribute("v"); curTask->SetProgress(value, false); task = task->NextSiblingElement(); @@ -114,7 +110,7 @@ void Mission::LoadFromXmlCur(const tinyxml2::XMLElement& element) { if (type == eMissionTaskType::COLLECTION || type == eMissionTaskType::VISIT_PROPERTY) { std::vector uniques; while (task != nullptr && value > 0) { - const auto unique = std::stoul(task->Attribute("v")); + const auto unique = task->UnsignedAttribute("v"); uniques.push_back(unique); @@ -407,9 +403,7 @@ void Mission::Catchup() { task->Progress(target); } } - } - - if (type == eMissionTaskType::PLAYER_FLAG) { + } else if (type == eMissionTaskType::PLAYER_FLAG) { for (int32_t target : task->GetAllTargets()) { const auto flag = GetCharacter()->GetPlayerFlag(target); @@ -423,6 +417,24 @@ void Mission::Catchup() { break; } } + } else if (type == eMissionTaskType::RACING) { + // check if its a racing meta task ("4") and then set its progress to the current completed missions in all tasks + const auto& clientInfo = task->GetClientInfo(); + if (clientInfo.taskParam1.starts_with("4")) { + // Each target is racing mission that needs to be completed. + // If its completed, progress the meta task by 1. + // at the end set the task progress to avoid sending excess msgs across the wire + uint32_t progress = 0; + for (const auto& target : task->GetAllTargets()) { + if (target == 0) continue; + + auto* racingMission = m_MissionComponent->GetMission(target); + if (racingMission != nullptr && racingMission->IsComplete()) { + progress++; + } + } + task->SetProgress(progress); + } } } } diff --git a/dGame/dMission/MissionTask.cpp b/dGame/dMission/MissionTask.cpp index 801eaeda..f23deb6e 100644 --- a/dGame/dMission/MissionTask.cpp +++ b/dGame/dMission/MissionTask.cpp @@ -218,7 +218,6 @@ void MissionTask::Progress(int32_t value, LWOOBJID associate, const std::string& uint32_t activityId; uint32_t lot; uint32_t collectionId; - std::vector settings; switch (type) { case eMissionTaskType::UNKNOWN: @@ -418,7 +417,24 @@ void MissionTask::Progress(int32_t value, LWOOBJID associate, const std::string& AddProgress(count); } else if (associate == 4 || associate == 5 || associate == 14) { if (!InAllTargets(value)) break; - AddProgress(count); + if (associate == 4) { + // Recount all completed target missions so this stays idempotent with + // Catchup, which sets the same value on Accept(). AddProgress would + // double-count when LookForAchievements runs Catchup then Progress. + auto* entity = mission->GetAssociate(); + if (entity == nullptr) break; + auto* missionComponent = entity->GetComponent(); + if (missionComponent == nullptr) break; + uint32_t completedCount = 0; + for (const auto& target : GetAllTargets()) { + if (target == 0) continue; + auto* targetMission = missionComponent->GetMission(target); + if (targetMission != nullptr && targetMission->IsComplete()) completedCount++; + } + SetProgress(completedCount); + } else { + AddProgress(count); + } } else if (associate == 17) { if (!InAllTargets(value)) break; AddProgress(count); @@ -446,6 +462,8 @@ void MissionTask::Progress(int32_t value, LWOOBJID associate, const std::string& break; } case eMissionTaskType::PLACE_MODEL: + [[fallthrough]]; + case eMissionTaskType::ADD_BEHAVIOR: { AddProgress(count); break; diff --git a/dGame/dPropertyBehaviors/ControlBehaviors.cpp b/dGame/dPropertyBehaviors/ControlBehaviors.cpp index 70a32981..0b180979 100644 --- a/dGame/dPropertyBehaviors/ControlBehaviors.cpp +++ b/dGame/dPropertyBehaviors/ControlBehaviors.cpp @@ -164,7 +164,12 @@ void ControlBehaviors::ProcessCommand(Entity* const modelEntity, const AMFArrayV LOG("Unknown behavior command (%s)", command.data()); } - if (needsNewBehaviorID) RequestUpdatedID(context); + // If we need a new behaviorID, request it and progress the mission for adding a behavior. + // "add" takes care of this in the ModelComponent directly so we do not need to do it here for that command. + if (needsNewBehaviorID) { + RequestUpdatedID(context); + context.modelComponent->ProgressAddBehaviorMission(*context.modelOwner); + } } ControlBehaviors::ControlBehaviors() { diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.cpp b/dGame/dPropertyBehaviors/PropertyBehavior.cpp index 0eb3f9df..c19c4d2f 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.cpp +++ b/dGame/dPropertyBehaviors/PropertyBehavior.cpp @@ -5,6 +5,7 @@ #include "ControlBehaviorMsgs.h" #include "tinyxml2.h" #include "ModelComponent.h" +#include "StringifiedEnum.h" #include @@ -16,50 +17,63 @@ PropertyBehavior::PropertyBehavior(bool _isTemplated) { isTemplated = _isTemplated; } +bool CheckStateRange(const BehaviorState state) { + return state >= BehaviorState::HOME_STATE && state <= BehaviorState::STAR_STATE; +} + template<> void PropertyBehavior::HandleMsg(AddStripMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(AddActionMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(RearrangeStripMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(UpdateActionMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(UpdateStripUiMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(RemoveStripMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(RemoveActionsMessage& msg) { + if (!CheckStateRange(msg.GetActionContext().GetStateId())) return; m_States[msg.GetActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetActionContext().GetStateId(); }; template<> void PropertyBehavior::HandleMsg(SplitStripMessage& msg) { + if (!CheckStateRange(msg.GetSourceActionContext().GetStateId())) return; + if (!CheckStateRange(msg.GetDestinationActionContext().GetStateId())) return; m_States[msg.GetSourceActionContext().GetStateId()].HandleMsg(msg); m_States[msg.GetDestinationActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetDestinationActionContext().GetStateId(); @@ -67,6 +81,8 @@ void PropertyBehavior::HandleMsg(SplitStripMessage& msg) { template<> void PropertyBehavior::HandleMsg(MigrateActionsMessage& msg) { + if (!CheckStateRange(msg.GetSourceActionContext().GetStateId())) return; + if (!CheckStateRange(msg.GetDestinationActionContext().GetStateId())) return; m_States[msg.GetSourceActionContext().GetStateId()].HandleMsg(msg); m_States[msg.GetDestinationActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetDestinationActionContext().GetStateId(); @@ -74,6 +90,8 @@ void PropertyBehavior::HandleMsg(MigrateActionsMessage& msg) { template<> void PropertyBehavior::HandleMsg(MergeStripsMessage& msg) { + if (!CheckStateRange(msg.GetSourceActionContext().GetStateId())) return; + if (!CheckStateRange(msg.GetDestinationActionContext().GetStateId())) return; m_States[msg.GetSourceActionContext().GetStateId()].HandleMsg(msg); m_States[msg.GetDestinationActionContext().GetStateId()].HandleMsg(msg); m_LastEditedState = msg.GetDestinationActionContext().GetStateId(); @@ -178,13 +196,32 @@ void PropertyBehavior::Deserialize(const tinyxml2::XMLElement& behavior) { } void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) { - for (auto& state : m_States | std::views::values) state.Update(deltaTime, modelComponent); + auto& activeState = GetActiveState(); + UpdateResult updateResult{}; + activeState.Update(deltaTime, modelComponent, updateResult); + if (updateResult.newState.has_value() && updateResult.newState.value() != m_ActiveState) { + LOG("Behavior %llu is changing from state %s to %s", m_BehaviorId, StringifiedEnum::ToString(m_ActiveState).data(), StringifiedEnum::ToString(updateResult.newState.value()).data()); + GameMessages::ResetModelToDefaults resetMsg{}; + resetMsg.bResetPos = false; + resetMsg.bResetRot = false; + resetMsg.bUnSmash = false; + modelComponent.OnResetModelToDefaults(resetMsg); + HandleMsg(resetMsg); + m_ActiveState = updateResult.newState.value(); + } } void PropertyBehavior::OnChatMessageReceived(const std::string& sMessage) { - for (auto& state : m_States | std::views::values) state.OnChatMessageReceived(sMessage); + auto& activeState = GetActiveState(); + activeState.OnChatMessageReceived(sMessage); } void PropertyBehavior::OnHit() { - for (auto& state : m_States | std::views::values) state.OnHit(); + auto& activeState = GetActiveState(); + activeState.OnHit(); +} + +State& PropertyBehavior::GetActiveState() { + DluAssert(m_States.contains(m_ActiveState)); + return m_States[m_ActiveState]; } diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.h b/dGame/dPropertyBehaviors/PropertyBehavior.h index f6a6be10..4fca613b 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.h +++ b/dGame/dPropertyBehaviors/PropertyBehavior.h @@ -1,18 +1,23 @@ #ifndef __PROPERTYBEHAVIOR__H__ #define __PROPERTYBEHAVIOR__H__ +#include "BehaviorStates.h" #include "State.h" +#include + namespace tinyxml2 { class XMLElement; } -enum class BehaviorState : uint32_t; - class AMFArrayValue; class BehaviorMessageBase; class ModelComponent; +struct UpdateResult { + std::optional newState; +}; + /** * Represents the Entity of a Property Behavior and holds data associated with the behavior */ @@ -45,6 +50,7 @@ public: void OnHit(); private: + State& GetActiveState(); // The current active behavior state. Behaviors can only be in ONE state at a time. BehaviorState m_ActiveState; diff --git a/dGame/dPropertyBehaviors/State.cpp b/dGame/dPropertyBehaviors/State.cpp index 5a2828e4..82d7ae0b 100644 --- a/dGame/dPropertyBehaviors/State.cpp +++ b/dGame/dPropertyBehaviors/State.cpp @@ -163,8 +163,8 @@ void State::Deserialize(const tinyxml2::XMLElement& state) { } } -void State::Update(float deltaTime, ModelComponent& modelComponent) { - for (auto& strip : m_Strips) strip.Update(deltaTime, modelComponent); +void State::Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult) { + for (auto& strip : m_Strips) strip.Update(deltaTime, modelComponent, updateResult); } void State::OnChatMessageReceived(const std::string& sMessage) { diff --git a/dGame/dPropertyBehaviors/State.h b/dGame/dPropertyBehaviors/State.h index a8d03ba7..436bb210 100644 --- a/dGame/dPropertyBehaviors/State.h +++ b/dGame/dPropertyBehaviors/State.h @@ -9,6 +9,7 @@ namespace tinyxml2 { class AMFArrayValue; class ModelComponent; +struct UpdateResult; class State { public: @@ -21,7 +22,7 @@ public: void Serialize(tinyxml2::XMLElement& state) const; void Deserialize(const tinyxml2::XMLElement& state); - void Update(float deltaTime, ModelComponent& modelComponent); + void Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult); void OnChatMessageReceived(const std::string& sMessage); void OnHit(); diff --git a/dGame/dPropertyBehaviors/Strip.cpp b/dGame/dPropertyBehaviors/Strip.cpp index 330284a9..4ddc96cb 100644 --- a/dGame/dPropertyBehaviors/Strip.cpp +++ b/dGame/dPropertyBehaviors/Strip.cpp @@ -160,13 +160,14 @@ void Strip::SpawnDrop(LOT dropLOT, Entity& entity) { } } -void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) { +void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult) { auto& entity = *modelComponent.GetParent(); auto& nextAction = GetNextAction(); auto number = nextAction.GetValueParameterDouble(); auto valueStr = nextAction.GetValueParameterString(); auto numberAsInt = static_cast(number); auto nextActionType = GetNextAction().GetType(); + LOG_DEBUG("Processing Strip Action: %s with number %.2f and string %s", nextActionType.data(), number, valueStr.data()); // TODO replace with switch case and nextActionType with enum /* BEGIN Move */ @@ -223,7 +224,8 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) { unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS); modelComponent.AddUnSmash(); - m_PausedTime = number; + // since it may take time for the message to relay to clients + m_PausedTime = number + 0.5f; } else if (nextActionType == "Wait") { m_PausedTime = number; } else if (nextActionType == "Chat") { @@ -258,6 +260,21 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) { for (; numberAsInt > 0; numberAsInt--) SpawnDrop(6431, entity); // 1 Armor powerup } /* END Gameplay */ + /* BEGIN StateMachine */ + else if (nextActionType == "ChangeStateHome") { + updateResult.newState = BehaviorState::HOME_STATE; + } else if (nextActionType == "ChangeStateCircle") { + updateResult.newState = BehaviorState::CIRCLE_STATE; + } else if (nextActionType == "ChangeStateSquare") { + updateResult.newState = BehaviorState::SQUARE_STATE; + } else if (nextActionType == "ChangeStateDiamond") { + updateResult.newState = BehaviorState::DIAMOND_STATE; + } else if (nextActionType == "ChangeStateTriangle") { + updateResult.newState = BehaviorState::TRIANGLE_STATE; + } else if (nextActionType == "ChangeStateStar") { + updateResult.newState = BehaviorState::STAR_STATE; + } + /* END StateMachine*/ else { static std::set g_WarnedActions; if (!g_WarnedActions.contains(nextActionType.data())) { @@ -330,7 +347,7 @@ bool Strip::CheckMovement(float deltaTime, ModelComponent& modelComponent) { return moveFinished; } -void Strip::Update(float deltaTime, ModelComponent& modelComponent) { +void Strip::Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult) { // No point in running a strip with only one action. // Strips are also designed to have 2 actions or more to run. if (!HasMinimumActions()) return; @@ -354,9 +371,9 @@ void Strip::Update(float deltaTime, ModelComponent& modelComponent) { // Check for trigger blocks and if not a trigger block proc this blocks action if (m_NextActionIndex == 0) { + LOG("Behavior strip started %s", nextAction.GetType().data()); if (nextAction.GetType() == "OnInteract") { modelComponent.AddInteract(); - } else if (nextAction.GetType() == "OnChat") { // logic here if needed } else if (nextAction.GetType() == "OnAttack") { @@ -365,7 +382,7 @@ void Strip::Update(float deltaTime, ModelComponent& modelComponent) { Game::entityManager->SerializeEntity(entity); m_WaitingForAction = true; } else { // should be a normal block - ProcNormalAction(deltaTime, modelComponent); + ProcNormalAction(deltaTime, modelComponent, updateResult); } } diff --git a/dGame/dPropertyBehaviors/Strip.h b/dGame/dPropertyBehaviors/Strip.h index 1a61afd5..330aafbc 100644 --- a/dGame/dPropertyBehaviors/Strip.h +++ b/dGame/dPropertyBehaviors/Strip.h @@ -12,6 +12,7 @@ namespace tinyxml2 { class AMFArrayValue; class ModelComponent; +struct UpdateResult; class Strip { public: @@ -33,9 +34,9 @@ public: // Checks the movement logic for whether or not to proceed // Returns true if the movement can continue, false if it needs to wait more. bool CheckMovement(float deltaTime, ModelComponent& modelComponent); - void Update(float deltaTime, ModelComponent& modelComponent); + void Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult); void SpawnDrop(LOT dropLOT, Entity& entity); - void ProcNormalAction(float deltaTime, ModelComponent& modelComponent); + void ProcNormalAction(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult); void RemoveStates(ModelComponent& modelComponent) const; // 2 actions are required for strips to work diff --git a/dGame/dUtilities/BrickDatabase.cpp b/dGame/dUtilities/BrickDatabase.cpp index 61a7341d..2cc034e9 100644 --- a/dGame/dUtilities/BrickDatabase.cpp +++ b/dGame/dUtilities/BrickDatabase.cpp @@ -66,13 +66,9 @@ const BrickList& BrickDatabase::GetBricks(const LxfmlPath& lxfmlPath) { std::string materialString(materialList); const auto materials = GeneralUtils::SplitString(materialString, ','); - if (!materials.empty()) { - brick.materialID = std::stoi(materials[0]); - } else { - brick.materialID = 0; - } + brick.materialID = GeneralUtils::TryParse(materials[0], 0); } else if (materialID != nullptr) { - brick.materialID = std::stoi(materialID); + brick.materialID = GeneralUtils::TryParse(materialID, 0); } else { brick.materialID = 0; // This is bad, makes it so the minigame can't be played } diff --git a/dGame/dUtilities/CheatDetection.cpp b/dGame/dUtilities/CheatDetection.cpp index a0f8c53a..acc415a6 100644 --- a/dGame/dUtilities/CheatDetection.cpp +++ b/dGame/dUtilities/CheatDetection.cpp @@ -54,17 +54,23 @@ void LogAndSaveFailedAntiCheatCheck(const LWOOBJID& id, const SystemAddress& sys // If player exists and entity exists in world, use both for logging info. if (entity && player) { + const auto* const playerChar = player->GetCharacter(); + const auto& playerName = playerChar ? playerChar->GetName() : "(null player character)"; + const auto* const entityChar = entity->GetCharacter(); + const auto& entityName = entityChar ? entityChar->GetName() : "(null entity character)"; LOG("Player (%s) (%llu) at system address (%s) with sending player (%s) (%llu) does not match their own.", - player->GetCharacter()->GetName().c_str(), player->GetObjectID(), + playerName.c_str(), player->GetObjectID(), sysAddr.ToString(), - entity->GetCharacter()->GetName().c_str(), entity->GetObjectID()); - if (player->GetCharacter()) toReport = player->GetCharacter()->GetParentUser(); + entityName.c_str(), entity->GetObjectID()); + if (playerChar) toReport = playerChar->GetParentUser(); // In the case that the target entity id did not exist, just log the player info. } else if (player) { + const auto* const playerChar = player->GetCharacter(); + const auto& playerName = playerChar ? playerChar->GetName() : "(null player character)"; LOG("Player (%s) (%llu) at system address (%s) with sending player (%llu) does not match their own.", - player->GetCharacter()->GetName().c_str(), player->GetObjectID(), + playerName.c_str(), player->GetObjectID(), sysAddr.ToString(), id); - if (player->GetCharacter()) toReport = player->GetCharacter()->GetParentUser(); + if (playerChar) toReport = playerChar->GetParentUser(); // In the rare case that the player does not exist, just log the system address and who the target id was. } else { LOG("Player at system address (%s) with sending player (%llu) does not match their own.", @@ -76,8 +82,11 @@ void LogAndSaveFailedAntiCheatCheck(const LWOOBJID& id, const SystemAddress& sys auto* user = UserManager::Instance()->GetUser(sysAddr); if (user) { + const auto* const lastChar = user->GetLastUsedChar(); + const auto& lastName = lastChar ? lastChar->GetName() : "(null last char)"; + const auto lastObjID = lastChar ? lastChar->GetObjectID() : LWOOBJID_EMPTY; LOG("User at system address (%s) (%s) (%llu) sent a packet as (%llu) which is not an id they own.", - sysAddr.ToString(), user->GetLastUsedChar()->GetName().c_str(), user->GetLastUsedChar()->GetObjectID(), id); + sysAddr.ToString(), lastName.c_str(), lastObjID, id); // Can't know sending player. Just log system address for IP banning. } else { LOG("No user found for system address (%s).", sysAddr.ToString()); diff --git a/dGame/dUtilities/Loot.cpp b/dGame/dUtilities/Loot.cpp index 0b88f5af..2edec1c4 100644 --- a/dGame/dUtilities/Loot.cpp +++ b/dGame/dUtilities/Loot.cpp @@ -21,6 +21,8 @@ #include "TeamManager.h" #include "CDObjectsTable.h" #include "ObjectIDManager.h" +#include "CDActivitiesTable.h" +#include "ScriptedActivityComponent.h" namespace { std::unordered_set CachedMatrices; @@ -142,13 +144,17 @@ void DropFactionLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) { // Drops 1 token for each player on a team // token drops are always given to every player on the team. -void DropFactionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) { +void DropFactionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) { for (const auto member : team.members) { GameMessages::GetPosition memberPosMsg{}; memberPosMsg.target = member; memberPosMsg.Send(); if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue; + GameMessages::IsDead isDeadMsg{}; + // Skip dead players + if (noTeamLootOnDeath && isDeadMsg.Send(member) && isDeadMsg.bDead) continue; + GameMessages::GetFactionTokenType factionTokenType{}; factionTokenType.target = member; // If we're not in a faction, this message will return false @@ -186,7 +192,7 @@ void DropPowerupLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) { // Drop the power up with no owner // Power ups can be picked up by anyone on a team, however unlike actual loot items, // if multiple clients say they picked one up, we let them pick it up. -void DropPowerupLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) { +void DropPowerupLoot(const Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) { lootMsg.lootID = ObjectIDManager::GenerateObjectID(); lootMsg.ownerID = LWOOBJID_EMPTY; // By setting ownerID to empty, any client that gets this DropClientLoot message can pick up the item. CalcFinalDropPos(lootMsg); @@ -198,6 +204,10 @@ void DropPowerupLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) { memberPosMsg.Send(); if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue; + GameMessages::IsDead isDeadMsg{}; + // Skip dead players + if (noTeamLootOnDeath && isDeadMsg.Send(member) && isDeadMsg.bDead) continue; + lootMsg.target = member; // By sending this message with the same ID to all players on the team, all players on the team are allowed to pick it up. lootMsg.Send(); @@ -230,7 +240,7 @@ void DropMissionLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) { // Check if the item needs to be dropped for anyone on the team // Only players who need the item will have it dropped -void DropMissionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) { +void DropMissionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) { GameMessages::MissionNeedsLot needMsg{}; needMsg.item = lootMsg.item; for (const auto member : team.members) { @@ -239,6 +249,10 @@ void DropMissionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) { memberPosMsg.Send(); if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue; + GameMessages::IsDead isDeadMsg{}; + // Skip dead players + if (noTeamLootOnDeath && isDeadMsg.Send(member) && isDeadMsg.bDead) continue; + needMsg.target = member; // Will return false if the item is not required if (needMsg.Send()) { @@ -274,19 +288,24 @@ void DropRegularLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) { // Drop a regular piece of loot. // Most items will go through this. // Finds the next loot owner on the team the is in range of the kill and gives them this reward. -void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg) { +void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) { auto earningPlayer = LWOOBJID_EMPTY; lootMsg.lootID = ObjectIDManager::GenerateObjectID(); CalcFinalDropPos(lootMsg); GameMessages::GetPosition memberPosMsg{}; + GameMessages::IsDead isDeadMsg{}; // Find the next loot owner. Eventually this will run into the `player` passed into this function, since those will // have the same ID, this loop will only ever run at most 4 times. do { earningPlayer = team.GetNextLootOwner(); memberPosMsg.target = earningPlayer; memberPosMsg.Send(); - } while (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS); - + if (noTeamLootOnDeath) { + isDeadMsg.target = earningPlayer; + // Skip dead players + isDeadMsg.Send(); + } + } while (isDeadMsg.bDead || (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS)); if (team.lootOption == 0 /* Shared loot */) { lootMsg.target = earningPlayer; lootMsg.ownerID = earningPlayer; @@ -304,10 +323,10 @@ void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg) { DistrbuteMsgToTeam(lootMsg, team); } -void DropLoot(Entity* player, const LWOOBJID source, const std::map& rolledItems, uint32_t minCoins, uint32_t maxCoins) { +void DropLoot(Entity* player, const LWOOBJID source, const std::map& rolledItems, uint32_t minCoins, uint32_t maxCoins, const bool noTeamLootOnDeath) { player = player->GetOwner(); // if the owner is overwritten, we collect that here const auto playerID = player->GetObjectID(); - if (!player || !player->IsPlayer()) { + if (!player->IsPlayer()) { LOG("Trying to drop loot for non-player %llu:%i", playerID, player->GetLOT()); return; } @@ -342,43 +361,53 @@ void DropLoot(Entity* player, const LWOOBJID source, const std::mapGetByID(lootLot); if (lootLot == TOKEN_PROXY) { - team ? DropFactionLoot(*team, lootMsg) : DropFactionLoot(*player, lootMsg); + team ? DropFactionLoot(*team, lootMsg, noTeamLootOnDeath) : DropFactionLoot(*player, lootMsg); } else if (info.table.MissionDrop) { - team ? DropMissionLoot(*team, lootMsg) : DropMissionLoot(*player, lootMsg); + team ? DropMissionLoot(*team, lootMsg, noTeamLootOnDeath) : DropMissionLoot(*player, lootMsg); } else if (object.type == "Powerup") { - team ? DropPowerupLoot(*team, lootMsg) : DropPowerupLoot(*player, lootMsg); + team ? DropPowerupLoot(*team, lootMsg, noTeamLootOnDeath) : DropPowerupLoot(*player, lootMsg); } else { - team ? DropRegularLoot(*team, lootMsg) : DropRegularLoot(*player, lootMsg); + team ? DropRegularLoot(*team, lootMsg, noTeamLootOnDeath) : DropRegularLoot(*player, lootMsg); } } } - // Coin roll is divided up between the members, rounded up, then dropped for each player - const uint32_t coinRoll = static_cast(minCoins + GeneralUtils::GenerateRandomNumber(0, 1) * (maxCoins - minCoins)); - const auto droppedCoins = team ? std::ceil(coinRoll / team->members.size()) : coinRoll; + // Filter out dead player if we need to + std::vector lootEarners; if (team) { - for (auto member : team->members) { - GameMessages::DropClientLoot lootMsg{}; - lootMsg.target = member; - lootMsg.ownerID = member; - lootMsg.currency = droppedCoins; - lootMsg.spawnPos = spawnPosition; - lootMsg.sourceID = source; - lootMsg.item = LOT_NULL; - lootMsg.Send(); - const auto* const memberEntity = Game::entityManager->GetEntity(member); - if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress()); + if (noTeamLootOnDeath) { + for (const auto member : team->members) { + GameMessages::IsDead isDeadMsg{}; + isDeadMsg.target = member; + if (isDeadMsg.Send() && !isDeadMsg.bDead) { + lootEarners.push_back(member); + } + } + } else { + lootEarners = team->members; } } else { + lootEarners.push_back(playerID); + } + + // Coin roll is divided up between the members, rounded up, then dropped for each player + const uint32_t coinRoll = static_cast(minCoins + GeneralUtils::GenerateRandomNumber(0, 1) * (maxCoins - minCoins)); + // Just in case its empty don't allow divide by 0 + const auto droppedCoins = lootEarners.empty() ? coinRoll : static_cast(std::ceil(static_cast(coinRoll) / lootEarners.size())); + + // Drops coins for each alive member of a team (or just a player) + for (auto member : lootEarners) { GameMessages::DropClientLoot lootMsg{}; - lootMsg.target = playerID; - lootMsg.ownerID = playerID; + lootMsg.target = member; + lootMsg.ownerID = member; lootMsg.currency = droppedCoins; lootMsg.spawnPos = spawnPosition; lootMsg.sourceID = source; lootMsg.item = LOT_NULL; + CalcFinalDropPos(lootMsg); lootMsg.Send(); - lootMsg.Send(player->GetSystemAddress()); + const auto* const memberEntity = Game::entityManager->GetEntity(member); + if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress()); } } @@ -531,6 +560,9 @@ void Loot::GiveActivityLoot(Entity* player, const LWOOBJID source, uint32_t acti void Loot::DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex, uint32_t minCoins, uint32_t maxCoins) { player = player->GetOwner(); // if the owner is overwritten, we collect that here + auto* scriptedActivityComponent = Game::entityManager->GetZoneControlEntity()->GetComponent(); + const bool noTeamLootOnDeath = scriptedActivityComponent ? scriptedActivityComponent->GetNoTeamLootOnDeath() : false; + auto* inventoryComponent = player->GetComponent(); if (!inventoryComponent) @@ -538,7 +570,7 @@ void Loot::DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex, const auto result = ::RollLootMatrix(matrixIndex); - ::DropLoot(player, source, result, minCoins, maxCoins); + ::DropLoot(player, source, result, minCoins, maxCoins, noTeamLootOnDeath); } void Loot::DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating) { diff --git a/dGame/dUtilities/Mail.cpp b/dGame/dUtilities/Mail.cpp index 5f1377e7..b0054513 100644 --- a/dGame/dUtilities/Mail.cpp +++ b/dGame/dUtilities/Mail.cpp @@ -74,69 +74,74 @@ namespace Mail { void SendRequest::Handle() { SendResponse response; auto* character = player->GetCharacter(); - const bool restrictMailOnMute = UserManager::Instance()->GetMuteRestrictMail() && character->GetParentUser()->GetIsMuted(); - const bool restrictedMailAccess = character->HasPermission(ePermissionMap::RestrictedMailAccess); - - if (character && !(restrictedMailAccess || restrictMailOnMute)) { - mailInfo.recipient = std::regex_replace(mailInfo.recipient, std::regex("[^0-9a-zA-Z]+"), ""); - auto receiverID = Database::Get()->GetCharacterInfo(mailInfo.recipient); - - if (!receiverID) { - response.status = eSendResponse::RecipientNotFound; - } else if (GeneralUtils::CaseInsensitiveStringCompare(mailInfo.recipient, character->GetName()) || receiverID->id == character->GetID()) { - response.status = eSendResponse::CannotMailSelf; - } else { - uint32_t mailCost = Game::zoneManager->GetWorldConfig().mailBaseFee; - uint32_t stackSize = 0; - - auto inventoryComponent = player->GetComponent(); - Item* item = nullptr; - - bool hasAttachment = mailInfo.itemID != 0 && mailInfo.itemCount > 0; - - if (hasAttachment) { - item = inventoryComponent->FindItemById(mailInfo.itemID); - if (item) { - mailCost += (item->GetInfo().baseValue * Game::zoneManager->GetWorldConfig().mailPercentAttachmentFee); - mailInfo.itemLOT = item->GetLot(); - } - } - - if (hasAttachment && !item) { - response.status = eSendResponse::AttachmentNotFound; - } else if (player->GetCharacter()->GetCoins() - mailCost < 0) { - response.status = eSendResponse::NotEnoughCoins; - } else { - bool removeSuccess = true; - // Remove coins and items from the sender - player->GetCharacter()->SetCoins(player->GetCharacter()->GetCoins() - mailCost, eLootSourceType::MAIL); - if (inventoryComponent && hasAttachment && item) { - removeSuccess = inventoryComponent->RemoveItem(mailInfo.itemLOT, mailInfo.itemCount, ALL, true); - auto* missionComponent = player->GetComponent(); - if (missionComponent && removeSuccess) missionComponent->Progress(eMissionTaskType::GATHER, mailInfo.itemLOT, LWOOBJID_EMPTY, "", -mailInfo.itemCount); - } - - // we passed all the checks, now we can actully send the mail - if (removeSuccess) { - mailInfo.senderId = character->GetID(); - mailInfo.senderUsername = character->GetName(); - mailInfo.receiverId = receiverID->id; - mailInfo.itemSubkey = LWOOBJID_EMPTY; - - //clear out the attachementID - mailInfo.itemID = 0; - - Database::Get()->InsertNewMail(mailInfo); - response.status = eSendResponse::Success; - character->SaveXMLToDatabase(); - } else { - response.status = eSendResponse::AttachmentNotFound; - } - } - } + if (!character) { + response.status = eSendResponse::UnknownError; } else { - response.status = eSendResponse::SenderAccountIsMuted; + const bool restrictMailOnMute = UserManager::Instance()->GetMuteRestrictMail() && character->GetParentUser()->GetIsMuted(); + const bool restrictedMailAccess = character->HasPermission(ePermissionMap::RestrictedMailAccess); + + if (character && !(restrictedMailAccess || restrictMailOnMute)) { + mailInfo.recipient = std::regex_replace(mailInfo.recipient, std::regex("[^0-9a-zA-Z]+"), ""); + auto receiverID = Database::Get()->GetCharacterInfo(mailInfo.recipient); + + if (!receiverID) { + response.status = eSendResponse::RecipientNotFound; + } else if (GeneralUtils::CaseInsensitiveStringCompare(mailInfo.recipient, character->GetName()) || receiverID->id == character->GetID()) { + response.status = eSendResponse::CannotMailSelf; + } else { + uint32_t mailCost = Game::zoneManager->GetWorldConfig().mailBaseFee; + uint32_t stackSize = 0; + + auto inventoryComponent = player->GetComponent(); + Item* item = nullptr; + + bool hasAttachment = mailInfo.itemID != 0 && mailInfo.itemCount > 0; + + if (hasAttachment) { + item = inventoryComponent->FindItemById(mailInfo.itemID); + if (item) { + mailCost += (item->GetInfo().baseValue * Game::zoneManager->GetWorldConfig().mailPercentAttachmentFee); + mailInfo.itemLOT = item->GetLot(); + } + } + + if (hasAttachment && !item) { + response.status = eSendResponse::AttachmentNotFound; + } else if (player->GetCharacter()->GetCoins() - mailCost < 0) { + response.status = eSendResponse::NotEnoughCoins; + } else { + bool removeSuccess = true; + // Remove coins and items from the sender + player->GetCharacter()->SetCoins(player->GetCharacter()->GetCoins() - mailCost, eLootSourceType::MAIL); + if (inventoryComponent && hasAttachment && item) { + removeSuccess = inventoryComponent->RemoveItem(mailInfo.itemLOT, mailInfo.itemCount, ALL, true); + auto* missionComponent = player->GetComponent(); + if (missionComponent && removeSuccess) missionComponent->Progress(eMissionTaskType::GATHER, mailInfo.itemLOT, LWOOBJID_EMPTY, "", -mailInfo.itemCount); + } + + // we passed all the checks, now we can actully send the mail + if (removeSuccess) { + mailInfo.senderId = character->GetID(); + mailInfo.senderUsername = character->GetName(); + mailInfo.receiverId = receiverID->id; + mailInfo.itemSubkey = LWOOBJID_EMPTY; + + //clear out the attachementID + mailInfo.itemID = 0; + + Database::Get()->InsertNewMail(mailInfo); + response.status = eSendResponse::Success; + character->SaveXMLToDatabase(); + } else { + response.status = eSendResponse::AttachmentNotFound; + } + } + } + } else { + response.status = eSendResponse::SenderAccountIsMuted; + } } + LOG("Finished send with status %s", StringifiedEnum::ToString(response.status).data()); response.Send(sysAddr); } @@ -193,7 +198,7 @@ namespace Mail { if (mailID > 0 && playerID == player->GetObjectID() && inv) { auto playerMail = Database::Get()->GetMail(mailID); - if (!playerMail) { + if (!playerMail || playerMail->receiverId != player->GetObjectID()) { response.status = eAttachmentCollectResponse::MailNotFound; } else if (!inv->HasSpaceForLoot({ {playerMail->itemLOT, playerMail->itemCount} })) { response.status = eAttachmentCollectResponse::NoSpaceInInventory; @@ -225,15 +230,21 @@ namespace Mail { DeleteResponse response; response.mailID = mailID; - auto mailData = Database::Get()->GetMail(mailID); - if (mailData && !(mailData->itemLOT > 0 && mailData->itemCount > 0)) { - Database::Get()->DeleteMail(mailID); - response.status = eDeleteResponse::Success; - } else if (mailData && mailData->itemLOT > 0 && mailData->itemCount > 0) { - response.status = eDeleteResponse::HasAttachments; - } else { - response.status = eDeleteResponse::NotFound; + const auto mailData = Database::Get()->GetMail(mailID); + response.status = eDeleteResponse::NotFound; + if (mailData) { + if (mailData->receiverId != playerID) { + LOG("Player %llu attempted to delete mail owned by %llu. Possible spoof?", playerID, mailData->receiverId); + } else { + if (!(mailData->itemLOT > 0 && mailData->itemCount > 0)) { + Database::Get()->DeleteMail(mailID); + response.status = eDeleteResponse::Success; + } else if (mailData->itemLOT > 0 && mailData->itemCount > 0) { + response.status = eDeleteResponse::HasAttachments; + } + } } + LOG("DeleteRequest status %s", StringifiedEnum::ToString(response.status).data()); response.Send(sysAddr); } @@ -253,11 +264,19 @@ namespace Mail { void ReadRequest::Handle() { ReadResponse response; + response.status = eReadResponse::UnknownError; response.mailID = mailID; - if (Database::Get()->GetMail(mailID)) { - response.status = eReadResponse::Success; - Database::Get()->MarkMailRead(mailID); + const auto mail = Database::Get()->GetMail(mailID); + if (mail) { + if (mail->receiverId == player->GetObjectID()) { + response.status = eReadResponse::Success; + Database::Get()->MarkMailRead(mailID); + } else { + LOG("Player %llu tried to mark mail read for player %llu", mail->receiverId, player->GetObjectID()); + } + } else { + LOG("No mail by ID %llu found to mark as read.", mailID); } LOG("ReadRequest %s", StringifiedEnum::ToString(response.status).data()); diff --git a/dGame/dUtilities/Preconditions.cpp b/dGame/dUtilities/Preconditions.cpp index 0c322987..26f1f7bd 100644 --- a/dGame/dUtilities/Preconditions.cpp +++ b/dGame/dUtilities/Preconditions.cpp @@ -13,6 +13,7 @@ #include "DestroyableComponent.h" #include "GameMessages.h" #include "eMissionState.h" +#include "PetComponent.h" std::map Preconditions::cache = {}; @@ -79,6 +80,9 @@ bool Precondition::Check(Entity* player, bool evaluateCosts) const { case PreconditionType::DoesNotHaveRacingLicence: case PreconditionType::LegoClubMember: case PreconditionType::NoInteraction: + case PreconditionType::NotFreeTrial: + case PreconditionType::MissionActive: + case PreconditionType::DoesNotHaveFlag: any = true; break; case PreconditionType::DoesNotHaveItem: @@ -114,11 +118,13 @@ bool Precondition::Check(Entity* player, bool evaluateCosts) const { bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluateCosts) const { - auto* missionComponent = player->GetComponent(); - auto* inventoryComponent = player->GetComponent(); - auto* destroyableComponent = player->GetComponent(); - auto* levelComponent = player->GetComponent(); auto* character = player->GetCharacter(); + auto [missionComponent, inventoryComponent, destroyableComponent, levelComponent] = + player->GetComponentsMut(); + + if (!missionComponent || !inventoryComponent || !destroyableComponent || !levelComponent || !character) { + return false; + } Mission* mission; @@ -152,7 +158,7 @@ bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluat if (missionComponent == nullptr) return false; return missionComponent->GetMissionState(value) >= eMissionState::COMPLETE; case PreconditionType::PetDeployed: - return false; // TODO + return PetComponent::GetActivePet(player->GetObjectID()) != nullptr; case PreconditionType::HasFlag: return character->GetPlayerFlag(value); case PreconditionType::WithinShape: @@ -160,9 +166,9 @@ bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluat case PreconditionType::InBuild: return character->GetBuildMode(); case PreconditionType::TeamCheck: - return false; // TODO + return false; // TODO: requires knowing the player's minigame team assignment (red/blue etc.); DLU does not track this per-player case PreconditionType::IsPetTaming: - return false; // TODO + return PetComponent::GetTamingPet(player->GetObjectID()) != nullptr; case PreconditionType::HasFaction: for (const auto faction : destroyableComponent->GetFactionIDs()) { if (faction == static_cast(value)) { @@ -180,15 +186,24 @@ bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluat return true; case PreconditionType::HasRacingLicence: - return false; // TODO + return false; // TODO: requires a racing licence level on the player; DLU does not track this case PreconditionType::DoesNotHaveRacingLicence: - return false; // TODO + return false; // TODO: requires a racing licence level on the player; DLU does not track this case PreconditionType::LegoClubMember: - return false; // TODO + return true; // Live LU opened LEGO CLUB to All players at some point, so always return true case PreconditionType::NoInteraction: - return false; // TODO + return false; // TODO: requires tracking the player's currently active interaction object; DLU does not track this case PreconditionType::HasLevel: return levelComponent->GetLevel() >= value; + case PreconditionType::NotFreeTrial: + return true; // DLU does not support free trial accounts; all players pass this check + case PreconditionType::MissionActive: { + if (missionComponent == nullptr) return false; + const auto state = missionComponent->GetMissionState(value); + return state == eMissionState::ACTIVE || state == eMissionState::COMPLETE_ACTIVE; + } + case PreconditionType::DoesNotHaveFlag: + return !character->GetPlayerFlag(value); default: return true; // There are a couple more unknown preconditions. Always return true in this case. } @@ -228,6 +243,7 @@ PreconditionExpression::PreconditionExpression(const std::string& conditions) { case '&': case ';': case '(': + case ':': b << conditions.substr(i + 1); done = true; break; diff --git a/dGame/dUtilities/Preconditions.h b/dGame/dUtilities/Preconditions.h index 2b6e1216..0a1ac70b 100644 --- a/dGame/dUtilities/Preconditions.h +++ b/dGame/dUtilities/Preconditions.h @@ -26,7 +26,10 @@ enum class PreconditionType DoesNotHaveRacingLicence, LegoClubMember, NoInteraction, - HasLevel = 22 + NotFreeTrial, + MissionActive, + HasLevel, + DoesNotHaveFlag = 23 }; diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index 08ea3403..fdc40ee5 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -261,7 +261,7 @@ void SlashCommandHandler::Startup() { Command TestMapCommand{ .help = "Transfers you to the given zone", - .info = "Transfers you to the given zone by id and clone id. Add \"force\" to skip checking if the zone is accessible (this can softlock your character, though, if you e.g. try to teleport to Frostburgh).", + .info = "Transfers you to the given zone by id and clone id and then spawns you at the specified spawn point if one was specified. Ignores instance-id for now.", .aliases = { "testmap", "tm" }, .handle = DEVGMCommands::TestMap, .requiredLevel = eGameMasterLevel::FORUM_MODERATOR @@ -468,7 +468,7 @@ void SlashCommandHandler::Startup() { Command InspectCommand{ .help = "Inspect an object", - .info = "Finds the closest entity with the given component or LNV variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the the IDs of all components the entity has. See detailed usage in the DLU docs", + .info = "Finds the closest entity with the given component or LNV variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the IDs of all components the entity has. Use `localCharacter` or `zoneControl` to inspect your current character or the zone control object.", .aliases = { "inspect" }, .handle = DEVGMCommands::Inspect, .requiredLevel = eGameMasterLevel::DEVELOPER @@ -929,10 +929,10 @@ void SlashCommandHandler::Startup() { Command GmInvisCommand{ .help = "Toggles invisibility for the character", - .info = "Toggles invisibility for the character, though it's currently a bit buggy. Requires nonzero GM Level for the character, but the account must have a GM level of 8", + .info = "Toggles invisibility for the character, making them invisible to other players and lower GM levels", .aliases = { "gminvis" }, .handle = GMGreaterThanZeroCommands::GmInvis, - .requiredLevel = eGameMasterLevel::DEVELOPER + .requiredLevel = eGameMasterLevel::FORUM_MODERATOR }; RegisterCommand(GmInvisCommand); @@ -1088,7 +1088,7 @@ void SlashCommandHandler::Startup() { .info = "Resurrects the player", .aliases = { "resurrect" }, .handle = GMZeroCommands::Resurrect, - .requiredLevel = eGameMasterLevel::CIVILIAN + .requiredLevel = eGameMasterLevel::DEVELOPER }; RegisterCommand(ResurrectCommand); diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index 1b3bc8dd..89142ade 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -13,7 +13,7 @@ #include "dpShapeSphere.h" #include "dZoneManager.h" #include "EntityInfo.h" -#include "Metrics.hpp" +#include "Metrics.h" #include "PlayerManager.h" #include "SlashCommandHandler.h" #include "UserManager.h" @@ -26,6 +26,8 @@ #include "Database.h" #include "CDObjectsTable.h" #include "CDRewardCodesTable.h" +#include "CDLootMatrixTable.h" +#include "CDLootTableTable.h" // Components #include "BuffComponent.h" @@ -89,7 +91,8 @@ namespace DEVGMCommands { GameMessages::SendChatModeUpdate(entity->GetObjectID(), eGameMasterLevel::CIVILIAN); entity->SetGMLevel(eGameMasterLevel::CIVILIAN); - GameMessages::SendToggleGMInvis(entity->GetObjectID(), false, UNASSIGNED_SYSTEM_ADDRESS); + GameMessages::ToggleGMInvis msg; + msg.Send(entity->GetObjectID()); GameMessages::SendSlashCommandFeedbackText(entity, u"Your game master level has been changed, you may not be able to use all commands."); } @@ -176,14 +179,15 @@ namespace DEVGMCommands { charComp->m_Character->SetRightHand(minifigItemId); } else { Game::entityManager->ConstructEntity(entity); + Game::entityManager->ConstructEntity(entity, entity->GetSystemAddress()); ChatPackets::SendSystemMessage(sysAddr, u"Invalid Minifig item to change, try one of the following: Eyebrows, Eyes, HairColor, HairStyle, Pants, LeftHand, Mouth, RightHand, Shirt, Hands"); return; } Game::entityManager->ConstructEntity(entity); + Game::entityManager->ConstructEntity(entity, entity->GetSystemAddress()); ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(lowerName) + u" set to " + (GeneralUtils::to_u16string(minifigItemId))); - GameMessages::SendToggleGMInvis(entity->GetObjectID(), false, UNASSIGNED_SYSTEM_ADDRESS); // need to retoggle because it gets reenabled on creation of new character } void PlayAnimation(Entity* entity, const SystemAddress& sysAddr, const std::string args) { @@ -377,8 +381,6 @@ namespace DEVGMCommands { line.erase(std::remove(line.begin(), line.end(), '\r'), line.end()); SlashCommandHandler::HandleChatCommand(GeneralUtils::ASCIIToUTF16(line), &entity, sysAddr); } - } else { - ChatPackets::SendSystemMessage(sysAddr, u"Unknown macro! Is the filename right?"); } } @@ -743,11 +745,40 @@ namespace DEVGMCommands { auto tables = query.execQuery(); + std::map lotToName{}; + std::map nameToLot{}; while (!tables.eof()) { - std::string message = std::to_string(tables.getIntField("id")) + " - " + tables.getStringField("name"); - ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::UTF8ToUTF16(message, message.size())); + const auto lot = tables.getIntField("id"); + const auto name = tables.getStringField("name"); + lotToName[lot] = name; + nameToLot[name] = lot; tables.nextRow(); } + + // if there arent a ton of results, print them to chat instead + if (lotToName.size() < 5) { + std::stringstream ss; + ss << "Lookup results for \"" << args << "\":"; + for (const auto& [lot, name] : lotToName) { + ss << "\nLOT: " << lot << " - Name: " << name; + } + ChatPackets::SendSystemMessage(sysAddr, ss.str()); + } else { + AMFArrayValue response; + response.Insert("visible", true); + response.Insert("objectID", "Search Results for: " + args); + response.Insert("serverInfo", true); + auto* const info = response.InsertArray("data"); + auto& lotSort = info->PushDebug("Sorted by LOT"); + for (const auto& [lot, name] : lotToName) { + auto& entry = lotSort.PushDebug(std::to_string(lot)) = name; + } + auto& nameSort = info->PushDebug("Sorted by Name"); + for (const auto& [name, lot] : nameToLot) { + auto& entry = nameSort.PushDebug(name) = std::to_string(lot); + } + GameMessages::SendUIMessageServerToSingleClient("ToggleObjectDebugger", response, sysAddr); + } } void Spawn(Entity* entity, const SystemAddress& sysAddr, const std::string args) { @@ -771,7 +802,7 @@ namespace DEVGMCommands { info.spawner = nullptr; info.spawnerID = entity->GetObjectID(); info.spawnerNodeID = 0; - info.settings = { new LDFData(u"SpawnedFromSlashCommand", true) }; + info.settings.Insert(u"SpawnedFromSlashCommand", true); Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr); @@ -794,7 +825,7 @@ namespace DEVGMCommands { } const auto numberToSpawnOptional = GeneralUtils::TryParse(splitArgs[1]); - if (!numberToSpawnOptional && numberToSpawnOptional.value() > 0) { + if (!numberToSpawnOptional) { ChatPackets::SendSystemMessage(sysAddr, u"Invalid number of enemies to spawn."); return; } @@ -802,7 +833,7 @@ namespace DEVGMCommands { // Must spawn within a radius of at least 0.0f const auto radiusToSpawnWithinOptional = GeneralUtils::TryParse(splitArgs[2]); - if (!radiusToSpawnWithinOptional && radiusToSpawnWithinOptional.value() < 0.0f) { + if (!radiusToSpawnWithinOptional || radiusToSpawnWithinOptional.value() < 0.0f) { ChatPackets::SendSystemMessage(sysAddr, u"Invalid radius to spawn within."); return; } @@ -813,7 +844,7 @@ namespace DEVGMCommands { info.spawner = nullptr; info.spawnerID = entity->GetObjectID(); info.spawnerNodeID = 0; - info.settings = { new LDFData(u"SpawnedFromSlashCommand", true) }; + info.settings.Insert(u"SpawnedFromSlashCommand", true); auto playerPosition = entity->GetPosition(); while (numberToSpawn > 0) { @@ -1020,7 +1051,8 @@ namespace DEVGMCommands { ChatPackets::SendSystemMessage(sysAddr, u"Requesting map change..."); LWOCLONEID cloneId = 0; - bool force = false; + LWOINSTANCEID instanceID{}; + std::string targetScene; const auto reqZoneOptional = GeneralUtils::TryParse(splitArgs[0]); if (!reqZoneOptional) { @@ -1030,29 +1062,34 @@ namespace DEVGMCommands { const LWOMAPID reqZone = reqZoneOptional.value(); if (splitArgs.size() > 1) { - auto index = 1; - - if (splitArgs[index] == "force") { - index++; - - force = true; + const auto cloneIdOptional = GeneralUtils::TryParse(splitArgs[1]); + if (!cloneIdOptional) { + ChatPackets::SendSystemMessage(sysAddr, u"Invalid clone id."); + return; } - if (splitArgs.size() > index) { - const auto cloneIdOptional = GeneralUtils::TryParse(splitArgs[index]); - if (!cloneIdOptional) { - ChatPackets::SendSystemMessage(sysAddr, u"Invalid clone id."); + cloneId = cloneIdOptional.value(); + + if (splitArgs.size() > 2) { + const auto instanceIDVal = GeneralUtils::TryParse(splitArgs[2]); + if (!instanceIDVal) { + ChatPackets::SendSystemMessage(sysAddr, u"Invalid instance id."); return; } - cloneId = cloneIdOptional.value(); + + instanceID = instanceIDVal.value(); + } + + if (splitArgs.size() > 3) { + targetScene = splitArgs[3]; } } const auto objid = entity->GetObjectID(); - if (force || Game::zoneManager->CheckIfAccessibleZone(reqZone)) { // to prevent tomfoolery + if (Game::zoneManager->CheckIfAccessibleZone(reqZone)) { // to prevent tomfoolery - ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, reqZone, cloneId, false, [objid](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { + ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, reqZone, cloneId, false, [objid, targetScene](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { auto* entity = Game::entityManager->GetEntity(objid); if (!entity) return; @@ -1070,6 +1107,7 @@ namespace DEVGMCommands { entity->GetCharacter()->SetZoneID(zoneID); entity->GetCharacter()->SetZoneInstance(zoneInstance); entity->GetCharacter()->SetZoneClone(zoneClone); + entity->GetCharacter()->SetTargetScene(targetScene); entity->GetComponent()->SetLastRocketConfig(u""); } @@ -1102,6 +1140,10 @@ namespace DEVGMCommands { } const auto& password = splitArgs[2]; + if (password.length() >= 50) { + ChatPackets::SendSystemMessage(sysAddr, u"Password is too long."); + return; + } ZoneInstanceManager::Instance()->CreatePrivateZone(Game::server, zone.value(), clone.value(), password); @@ -1233,10 +1275,10 @@ namespace DEVGMCommands { auto* inventoryComponent = entity->GetComponent(); if (!inventoryComponent) return; - std::vector data{}; - data.push_back(new LDFData(u"reforgedLOT", reforgedItem.value())); + LwoNameValue config; + config.Insert(u"reforgedLOT", reforgedItem.value()); - inventoryComponent->AddItem(baseItem.value(), 1, eLootSourceType::MODERATION, eInventoryType::INVALID, data); + inventoryComponent->AddItem(baseItem.value(), 1, eLootSourceType::MODERATION, eInventoryType::INVALID, config); } void Crash(Entity* entity, const SystemAddress& sysAddr, const std::string args) { @@ -1247,38 +1289,26 @@ namespace DEVGMCommands { } void Metrics(Entity* entity, const SystemAddress& sysAddr, const std::string args) { + AMFArrayValue response; + response.Insert("visible", true); + response.Insert("objectID", "Metrics"); + response.Insert("serverInfo", true); + auto* info = response.InsertArray("data"); for (const auto variable : Metrics::GetAllMetrics()) { - auto* metric = Metrics::GetMetric(variable); + auto& metricData = info->PushDebug(Metrics::MetricVariableToString(variable)); - if (metric == nullptr) { - continue; - } + const auto& metric = Metrics::GetMetric(variable); - ChatPackets::SendSystemMessage( - sysAddr, - GeneralUtils::ASCIIToUTF16(Metrics::MetricVariableToString(variable)) + - u": " + - GeneralUtils::to_u16string(Metrics::ToMiliseconds(metric->average)) + - u"ms" - ); + metricData.PushDebug("Maximum") = std::to_string(Metrics::ToMiliseconds(metric.max)) + "ms"; + metricData.PushDebug("Minimum") = std::to_string(Metrics::ToMiliseconds(metric.min)) + "ms"; + metricData.PushDebug("Average") = std::to_string(Metrics::ToMiliseconds(metric.average)) + "ms"; + metricData.PushDebug("Measurements Count") = std::to_string(metric.measurementSize); } - - ChatPackets::SendSystemMessage( - sysAddr, - u"Peak RSS: " + GeneralUtils::to_u16string(static_cast(static_cast(Metrics::GetPeakRSS()) / 1.024e6)) + - u"MB" - ); - - ChatPackets::SendSystemMessage( - sysAddr, - u"Current RSS: " + GeneralUtils::to_u16string(static_cast(static_cast(Metrics::GetCurrentRSS()) / 1.024e6)) + - u"MB" - ); - - ChatPackets::SendSystemMessage( - sysAddr, - u"Process ID: " + GeneralUtils::to_u16string(Metrics::GetProcessID()) - ); + auto& processInfo = info->PushDebug("Process Info"); + processInfo.PushDebug("Peak RSS") = std::to_string(static_cast(Metrics::GetPeakRSS()) / 1.024e6) + "MB"; + processInfo.PushDebug("Current RSS") = std::to_string(static_cast(Metrics::GetCurrentRSS()) / 1.024e6) + "MB"; + processInfo.PushDebug("Process ID") = Metrics::GetProcessID(); + GameMessages::SendUIMessageServerToSingleClient("ToggleObjectDebugger", response, sysAddr); } void ReloadConfig(Entity* entity, const SystemAddress& sysAddr, const std::string args) { @@ -1310,19 +1340,30 @@ namespace DEVGMCommands { const auto loops = GeneralUtils::TryParse(splitArgs[2]); if (!loops) return; + auto* const lootMatrixTable = CDClientManager::GetTable(); + auto* const lootTableTable = CDClientManager::GetTable(); + bool found = false; + for (const auto& entry : lootMatrixTable->GetMatrix(lootMatrixIndex.value())) { + for (const auto& loot : lootTableTable->GetTable(entry.LootTableIndex)) { + found = targetLot.value() == loot.itemid; + if (found) break; + } + } + + if (!found) { + std::stringstream ss; + ss << "Target LOT " << targetLot.value() << " not found in loot matrix " << lootMatrixIndex.value() << "."; + ChatPackets::SendSystemMessage(sysAddr, ss.str()); + return; + } + uint64_t totalRuns = 0; for (uint32_t i = 0; i < loops; i++) { while (true) { const auto lootRoll = Loot::RollLootMatrix(nullptr, lootMatrixIndex.value()); totalRuns += 1; - bool doBreak = false; - for (const auto& kv : lootRoll) { - if (static_cast(kv.first) == targetLot) { - doBreak = true; - } - } - if (doBreak) break; + if (lootRoll.contains(targetLot.value())) break; } } @@ -1479,7 +1520,15 @@ namespace DEVGMCommands { void Inspect(Entity* entity, const SystemAddress& sysAddr, const std::string args) { const auto splitArgs = GeneralUtils::SplitString(args, ' '); if (splitArgs.empty()) return; - const auto idParsed = GeneralUtils::TryParse(splitArgs[0]); + std::optional idIntermed; + if (splitArgs[0] == "zoneControl") { + idIntermed = 0x3FFF'FFFFFFFE; + } else if (splitArgs[0] == "localCharacter") { + idIntermed = entity->GetObjectID(); + } else { + idIntermed = GeneralUtils::TryParse(splitArgs[0]); + } + const auto idParsed = idIntermed; // First try to get the object by its ID if provided. // Second try to get the object by player name. diff --git a/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp b/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp index 7d017cf0..7575b9cc 100644 --- a/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp +++ b/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp @@ -275,7 +275,8 @@ namespace GMGreaterThanZeroCommands { } void GmInvis(Entity* entity, const SystemAddress& sysAddr, const std::string args) { - GameMessages::SendToggleGMInvis(entity->GetObjectID(), true, UNASSIGNED_SYSTEM_ADDRESS); + GameMessages::ToggleGMInvis msg; + msg.Send(entity->GetObjectID()); } void SetName(Entity* entity, const SystemAddress& sysAddr, const std::string args) { diff --git a/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp b/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp index 9e0bb475..4ef1eb4e 100644 --- a/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp +++ b/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp @@ -187,8 +187,13 @@ namespace GMZeroCommands { auto splitArgs = GeneralUtils::SplitString(args, ' '); if (splitArgs.empty()) return; - ChatPackets::SendSystemMessage(sysAddr, u"Requesting private map..."); const auto& password = splitArgs[0]; + if (password.length() >= 50) { + ChatPackets::SendSystemMessage(sysAddr, u"Password is too long."); + return; + } + + ChatPackets::SendSystemMessage(sysAddr, u"Requesting private map..."); ZoneInstanceManager::Instance()->RequestPrivateZone(Game::server, false, password, [=](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", sysAddr.ToString(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort); diff --git a/dGame/dUtilities/VanityUtilities.cpp b/dGame/dUtilities/VanityUtilities.cpp index 48d3c0da..950f26e2 100644 --- a/dGame/dUtilities/VanityUtilities.cpp +++ b/dGame/dUtilities/VanityUtilities.cpp @@ -45,10 +45,8 @@ void VanityUtilities::SpawnVanity() { info.pos = { 259.5f, 246.4f, -705.2f }; info.rot = { 0.0f, 0.0f, 1.0f, 0.0f }; info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID(); - info.settings = { - new LDFData(u"hasCustomText", true), - new LDFData(u"customText", ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/TESTAMENT.md").string())) - }; + info.settings.Insert(u"hasCustomText", true); + info.settings.Insert(u"customText", ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/TESTAMENT.md").string())); auto* entity = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(entity); @@ -231,7 +229,7 @@ void ParseXml(const std::string& file) { auto* configElement = object->FirstChildElement("config"); std::vector keys = {}; - std::vector config = {}; + LwoNameValue config; if (configElement) { for (auto* key = configElement->FirstChildElement("key"); key != nullptr; key = key->NextSiblingElement("key")) { @@ -239,16 +237,16 @@ void ParseXml(const std::string& file) { auto* data = key->GetText(); if (!data) continue; - LDFBaseData* configData = LDFBaseData::DataFromString(data); + const auto& configData = config.ParseInsert(data); if (configData->GetKey() == u"useLocationsAsRandomSpawnPoint" && configData->GetValueType() == eLDFType::LDF_TYPE_BOOLEAN) { - useLocationsAsRandomSpawnPoint = static_cast(configData); + useLocationsAsRandomSpawnPoint = static_cast*>(configData.get())->GetValue(); + config.Erase(u"useLocationsAsRandomSpawnPoint"); continue; } keys.push_back(configData->GetKey()); - config.push_back(configData); } } - if (!keys.empty()) config.push_back(new LDFData>(u"syncLDF", keys)); + if (!keys.empty()) config.Insert>(u"syncLDF", keys); VanityObject objectData{ .m_Name = name, diff --git a/dGame/dUtilities/VanityUtilities.h b/dGame/dUtilities/VanityUtilities.h index 2afd4a6b..4da74479 100644 --- a/dGame/dUtilities/VanityUtilities.h +++ b/dGame/dUtilities/VanityUtilities.h @@ -19,7 +19,7 @@ struct VanityObject { std::vector m_Equipment; std::vector m_Phrases; std::map> m_Locations; - std::vector m_Config; + LwoNameValue m_Config; }; diff --git a/dMasterServer/InstanceManager.cpp b/dMasterServer/InstanceManager.cpp index 2a402f87..6ddd32d5 100644 --- a/dMasterServer/InstanceManager.cpp +++ b/dMasterServer/InstanceManager.cpp @@ -308,7 +308,7 @@ const InstancePtr& InstanceManager::FindPrivateInstance(const std::string& passw continue; } - LOG("Password: %s == %s => %d", password.c_str(), instance->GetPassword().c_str(), password == instance->GetPassword()); + LOG("Checking private zone password match (result: %d)", password == instance->GetPassword()); if (instance->GetPassword() == password) { return instance; @@ -332,6 +332,12 @@ int InstanceManager::GetHardCap(LWOMAPID mapID) { return zone ? zone->population_hard_cap : 12; } +void InstanceManager::PruneUnreadyInstances() { + for (int i = static_cast(m_Instances.size()) - 1; i >= 0; i--) { + if (!m_Instances[i]->GetIsReady()) m_Instances.erase(m_Instances.cbegin() + i); + } +} + void Instance::SetShutdownComplete(const bool value) { m_Shutdown = value; } @@ -359,4 +365,3 @@ bool Instance::IsFull(bool isFriendTransfer) const { return true; } - diff --git a/dMasterServer/InstanceManager.h b/dMasterServer/InstanceManager.h index a6ba6d9a..5481efbc 100644 --- a/dMasterServer/InstanceManager.h +++ b/dMasterServer/InstanceManager.h @@ -133,6 +133,7 @@ public: const InstancePtr& CreatePrivateInstance(LWOMAPID mapID, LWOCLONEID cloneID, const std::string& password); const InstancePtr& FindPrivateInstance(const std::string& password); void SetIsShuttingDown(bool value) { this->m_IsShuttingDown = value; }; + void PruneUnreadyInstances(); private: std::string mExternalIP; diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index 32a2cb56..4fd89fbe 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -328,20 +328,14 @@ int main(int argc, char** argv) { } Game::randomEngine = std::mt19937(time(0)); - uint32_t maxClients = 999; - uint32_t ourPort = 2000; - std::string ourIP = "localhost"; - const auto maxClientsString = Game::config->GetValue("max_clients"); - if (!maxClientsString.empty()) maxClients = std::stoi(maxClientsString); - const auto masterServerPortString = Game::config->GetValue("master_server_port"); - if (!masterServerPortString.empty()) ourPort = std::atoi(masterServerPortString.c_str()); - const auto externalIPString = Game::config->GetValue("external_ip"); - if (!externalIPString.empty()) ourIP = externalIPString; + uint32_t maxClients = Game::config->GetValue("max_clients", 999); + uint32_t ourPort = Game::config->GetValue("master_server_port", 2000); + std::string ourIP = Game::config->GetValue("external_ip", "localhost"); char salt[BCRYPT_HASHSIZE]; char hash[BCRYPT_HASHSIZE]; - const auto& cfgPassword = Game::config->GetValue("master_password"); - int res = GenerateBCryptPassword(!cfgPassword.empty() ? cfgPassword : "3.25DARKFLAME1", 13, salt, hash); + const auto& cfgPassword = Game::config->GetValue("master_password", "3.25DARKFLAME1"); + int res = GenerateBCryptPassword(cfgPassword, 13, salt, hash); assert(res == 0); Game::server = new dServer(ourIP, ourPort, 0, maxClients, true, false, Game::logger, "", 0, ServiceType::MASTER, Game::config, &Game::lastSignal, hash); @@ -535,8 +529,7 @@ void HandlePacket(Packet* packet) { case MessageType::Master::REQUEST_ZONE_TRANSFER: { LOG("Received zone transfer req"); - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; uint64_t requestID = 0; uint8_t mythranShift = false; uint32_t zoneID = 0; @@ -575,8 +568,7 @@ void HandlePacket(Packet* packet) { //This is here because otherwise we'd have to include IM in //non-master servers. This packet allows us to add World //servers back if master crashed - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; uint32_t theirPort = 0; uint32_t theirZoneID = 0; @@ -602,13 +594,13 @@ void HandlePacket(Packet* packet) { instance->SetSysAddr(packet->systemAddress); } } - break; + break; case ServiceType::CHAT: chatServerMasterPeerSysAddr = packet->systemAddress; break; - case ServiceType::AUTH: - authServerMasterPeerSysAddr = packet->systemAddress; - break; + case ServiceType::AUTH: + authServerMasterPeerSysAddr = packet->systemAddress; + break; default: // We just ignore any other server type break; @@ -664,8 +656,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::PLAYER_ADDED: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; LWOMAPID theirZoneID = 0; LWOINSTANCEID theirInstanceID = 0; @@ -674,7 +665,7 @@ void HandlePacket(Packet* packet) { inStream.Read(theirInstanceID); const auto& instance = - Game::im->FindInstance(theirZoneID, theirInstanceID); + Game::im->FindInstanceWithPrivate(theirZoneID, theirInstanceID); if (instance) { instance->AddPlayer(Player()); } else { @@ -684,8 +675,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::PLAYER_REMOVED: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; LWOMAPID theirZoneID = 0; LWOINSTANCEID theirInstanceID = 0; @@ -702,8 +692,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::CREATE_PRIVATE_ZONE: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; uint32_t mapId; LWOCLONEID cloneId; @@ -714,20 +703,21 @@ void HandlePacket(Packet* packet) { uint32_t len; inStream.Read(len); + len = std::min(len, 50); // cap the master password at 50 characters + for (uint32_t i = 0; len > i; i++) { char character; inStream.Read(character); password += character; } const auto& newInst = Game::im->CreatePrivateInstance(mapId, cloneId, password.c_str()); - LOG("Creating private zone %i/%i/%i with password %s", newInst->GetMapID(), newInst->GetCloneID(), newInst->GetInstanceID(), password.c_str()); + LOG("Creating private zone %i/%i/%i", newInst->GetMapID(), newInst->GetCloneID(), newInst->GetInstanceID()); break; } case MessageType::Master::REQUEST_PRIVATE_ZONE: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; uint64_t requestID = 0; uint8_t mythranShift = false; @@ -739,6 +729,7 @@ void HandlePacket(Packet* packet) { uint32_t len; inStream.Read(len); + len = std::min(len, 50); for (uint32_t i = 0; i < len; i++) { char character; inStream.Read(character); @@ -747,7 +738,7 @@ void HandlePacket(Packet* packet) { const auto& instance = Game::im->FindPrivateInstance(password.c_str()); - LOG("Join private zone: %llu %d %s %p", requestID, mythranShift, password.c_str(), instance.get()); + LOG("Join private zone: %llu %d %p", requestID, mythranShift, instance.get()); if (instance == nullptr) { return; @@ -761,8 +752,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::WORLD_READY: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; LWOMAPID zoneID; LWOINSTANCEID instanceID; @@ -772,7 +762,7 @@ void HandlePacket(Packet* packet) { LOG("Got world ready %i %i", zoneID, instanceID); - const auto& instance = Game::im->FindInstance(zoneID, instanceID); + const auto& instance = Game::im->FindInstanceWithPrivate(zoneID, instanceID); if (instance == nullptr) { LOG("Failed to find zone to ready"); @@ -785,8 +775,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::PREP_ZONE: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; int32_t zoneID; inStream.Read(zoneID); @@ -801,8 +790,7 @@ void HandlePacket(Packet* packet) { } case MessageType::Master::AFFIRM_TRANSFER_RESPONSE: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; uint64_t requestID; @@ -870,13 +858,11 @@ int ShutdownSequence(int32_t signal) { } // A server might not be finished spinning up yet, remove all of those here. + // prune the unready ones before looping over all of them + Game::im->PruneUnreadyInstances(); for (const auto& instance : Game::im->GetInstances()) { if (!instance) continue; - if (!instance->GetIsReady()) { - Game::im->RemoveInstance(instance); - } - instance->SetIsShuttingDown(true); } diff --git a/dNavigation/dNavMesh.cpp b/dNavigation/dNavMesh.cpp index d0432c89..430bc0a7 100644 --- a/dNavigation/dNavMesh.cpp +++ b/dNavigation/dNavMesh.cpp @@ -36,6 +36,10 @@ dNavMesh::~dNavMesh() { void dNavMesh::LoadNavmesh() { + struct FPClose { + FILE* fp{}; + ~FPClose() { if (fp) fclose(fp); } + } fp; std::string path = (BinaryPathFinder::GetBinaryDir() / "navmeshes/" / (std::to_string(m_ZoneId) + ".bin")).string(); @@ -43,55 +47,48 @@ void dNavMesh::LoadNavmesh() { return; } - FILE* fp; - #ifdef _WIN32 - fopen_s(&fp, path.c_str(), "rb"); + fopen_s(&fp.fp, path.c_str(), "rb"); #elif __APPLE__ // macOS has 64bit file IO by default - fp = fopen(path.c_str(), "rb"); + fp.fp = fopen(path.c_str(), "rb"); #else - fp = fopen64(path.c_str(), "rb"); + fp.fp = fopen64(path.c_str(), "rb"); #endif - if (!fp) { + if (!fp.fp) { return; } // Read header. NavMeshSetHeader header; - size_t readLen = fread(&header, sizeof(NavMeshSetHeader), 1, fp); + size_t readLen = fread(&header, sizeof(NavMeshSetHeader), 1, fp.fp); if (readLen != 1) { - fclose(fp); return; } if (header.magic != NAVMESHSET_MAGIC) { - fclose(fp); return; } if (header.version != NAVMESHSET_VERSION) { - fclose(fp); return; } dtNavMesh* mesh = dtAllocNavMesh(); if (!mesh) { - fclose(fp); return; } dtStatus status = mesh->init(&header.params); if (dtStatusFailed(status)) { - fclose(fp); return; } // Read tiles. for (int i = 0; i < header.numTiles; ++i) { NavMeshTileHeader tileHeader; - readLen = fread(&tileHeader, sizeof(tileHeader), 1, fp); + readLen = fread(&tileHeader, sizeof(tileHeader), 1, fp.fp); if (readLen != 1) return; if (!tileHeader.tileRef || !tileHeader.dataSize) @@ -100,14 +97,15 @@ void dNavMesh::LoadNavmesh() { unsigned char* data = static_cast(dtAlloc(tileHeader.dataSize, DT_ALLOC_PERM)); if (!data) break; memset(data, 0, tileHeader.dataSize); - readLen = fread(data, tileHeader.dataSize, 1, fp); - if (readLen != 1) return; + readLen = fread(data, tileHeader.dataSize, 1, fp.fp); + if (readLen != 1) { + dtFree(data); + return; + } mesh->addTile(data, tileHeader.dataSize, DT_TILE_FREE_DATA, tileHeader.tileRef, 0); } - fclose(fp); - m_NavMesh = mesh; } @@ -186,13 +184,18 @@ std::vector dNavMesh::GetPath(const NiPoint3& startPos, const NiPoint3 path.push_back(startPos); //insert the start pos // Linearly interpolate between these two points: - for (int i = 0; i < numPoints; i++) { - NiPoint3 newPoint{ startPos }; + const auto yDist = endPos.y - startPos.y; + // Ensure we cannot divide by zero + const auto xDist = endPos.x - startPos.x; + if (xDist != 0.0f) { + for (int i = 0; i < numPoints; i++) { + NiPoint3 newPoint{ startPos }; - newPoint.x += speed; - newPoint.y = newPoint.y + (((endPos.y - startPos.y) / (endPos.x - startPos.x)) * (newPoint.x - startPos.x)); + newPoint.x += speed; + newPoint.y = newPoint.y + ((yDist / xDist) * (newPoint.x - startPos.x)); - path.push_back(newPoint); + path.push_back(newPoint); + } } path.push_back(endPos); //finally insert our end pos @@ -211,16 +214,16 @@ std::vector dNavMesh::GetPath(const NiPoint3& startPos, const NiPoint3 ePos[2] = endPos.z; dtStatus pathFindStatus; - dtPolyRef startRef; - dtPolyRef endRef; + dtPolyRef startRef{}; + dtPolyRef endRef{}; float polyPickExt[3] = { 32.0f, 32.0f, 32.0f }; dtQueryFilter filter{}; //Find our start poly - m_NavQuery->findNearestPoly(sPos, polyPickExt, &filter, &startRef, 0); + const auto startResult = m_NavQuery->findNearestPoly(sPos, polyPickExt, &filter, &startRef, 0); //Find our end poly - m_NavQuery->findNearestPoly(ePos, polyPickExt, &filter, &endRef, 0); + const auto endResult = m_NavQuery->findNearestPoly(ePos, polyPickExt, &filter, &endRef, 0); pathFindStatus = DT_FAILURE; int m_nstraightPath = 0; @@ -231,7 +234,7 @@ std::vector dNavMesh::GetPath(const NiPoint3& startPos, const NiPoint3 dtPolyRef m_straightPathPolys[MAX_POLYS]; int m_straightPathOptions = 0; - if (startRef && endRef) { + if (dtStatusSucceed(startResult) && dtStatusSucceed(endResult)) { m_NavQuery->findPath(startRef, endRef, sPos, ePos, &filter, m_polys, &m_npolys, MAX_POLYS); if (m_npolys) { diff --git a/dNet/AuthPackets.cpp b/dNet/AuthPackets.cpp index a46ed9f1..e1f500b3 100644 --- a/dNet/AuthPackets.cpp +++ b/dNet/AuthPackets.cpp @@ -157,8 +157,7 @@ void AuthPackets::HandleLoginRequest(dServer* server, Packet* packet) { } //If we aren't running in live mode, then only GMs are allowed to enter: - const auto& closedToNonDevs = Game::config->GetValue("closed_to_non_devs"); - if (closedToNonDevs.size() > 0 && bool(std::stoi(closedToNonDevs)) && accountInfo->maxGmLevel == eGameMasterLevel::CIVILIAN) { + if (Game::config->GetValue("closed_to_non_devs", false) && accountInfo->maxGmLevel == eGameMasterLevel::CIVILIAN) { stamps.emplace_back(eStamps::GM_REQUIRED, 1); AuthPackets::SendLoginResponse(server, packet->systemAddress, eLoginResponse::PERMISSIONS_NOT_HIGH_ENOUGH, "The server is currently only open to developers.", "", 2001, username, stamps); return; @@ -307,6 +306,6 @@ void AuthPackets::SendLoginResponse(dServer* server, const SystemAddress& sysAdd bitStream.Write(LUString(username)); server->SendToMaster(bitStream); - LOG("Set sessionKey: %i for user %s", sessionKey, username.c_str()); + LOG("Set session key for user %s", username.c_str()); } } diff --git a/dNet/ChatPackets.cpp b/dNet/ChatPackets.cpp index 86622ad1..0c54e214 100644 --- a/dNet/ChatPackets.cpp +++ b/dNet/ChatPackets.cpp @@ -58,6 +58,10 @@ void ChatPackets::SendChatMessage(const SystemAddress& sysAddr, char chatChannel SEND_PACKET_BROADCAST; } +void ChatPackets::SendSystemMessage(const SystemAddress& sysAddr, const std::string& message, const bool broadcast) { + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(message), broadcast); +} + void ChatPackets::SendSystemMessage(const SystemAddress& sysAddr, const std::u16string& message, const bool broadcast) { CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GENERAL_CHAT_MESSAGE); diff --git a/dNet/ChatPackets.h b/dNet/ChatPackets.h index 53d0eced..8139fb03 100644 --- a/dNet/ChatPackets.h +++ b/dNet/ChatPackets.h @@ -58,6 +58,7 @@ namespace ChatPackets { }; void SendChatMessage(const SystemAddress& sysAddr, char chatChannel, const std::string& senderName, LWOOBJID playerObjectID, bool senderMythran, const std::u16string& message); + void SendSystemMessage(const SystemAddress& sysAddr, const std::string& message, bool broadcast = false); void SendSystemMessage(const SystemAddress& sysAddr, const std::u16string& message, bool broadcast = false); void SendMessageFail(const SystemAddress& sysAddr); void SendRoutedMsg(const LUBitStream& msg, const LWOOBJID targetID, const SystemAddress& sysAddr); diff --git a/dNet/ClientPackets.cpp b/dNet/ClientPackets.cpp index a6b9f8c6..43d36e0f 100644 --- a/dNet/ClientPackets.cpp +++ b/dNet/ClientPackets.cpp @@ -11,14 +11,16 @@ ChatMessage ClientPackets::HandleChatMessage(Packet* packet) { CINSTREAM_SKIP_HEADER; ChatMessage message; - uint32_t messageLength; + int32_t messageLength{}; inStream.Read(message.chatChannel); inStream.Read(message.unknown); inStream.Read(messageLength); - for (uint32_t i = 0; i < (messageLength - 1); ++i) { - uint16_t character; + if (messageLength > MAX_MESSAGE_LENGTH || messageLength < 0) return message; + + for (int32_t i = 0; i < (messageLength - 1); ++i) { + char16_t character; inStream.Read(character); message.message.push_back(character); } @@ -106,6 +108,7 @@ ChatModerationRequest ClientPackets::HandleChatModerationRequest(Packet* packet) uint16_t messageLength; inStream.Read(messageLength); + if (messageLength > MAX_MESSAGE_LENGTH) return request; for (uint32_t i = 0; i < messageLength; ++i) { uint16_t character; inStream.Read(character); diff --git a/dNet/MasterPackets.cpp b/dNet/MasterPackets.cpp index aac49929..eb473422 100644 --- a/dNet/MasterPackets.cpp +++ b/dNet/MasterPackets.cpp @@ -77,7 +77,8 @@ void MasterPackets::SendZoneTransferResponse(dServer* server, const SystemAddres void MasterPackets::HandleServerInfo(Packet* packet) { RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + uint64_t header{}; + inStream.Read(header); uint32_t theirPort = 0; uint32_t theirZoneID = 0; diff --git a/dNet/WorldPackets.cpp b/dNet/WorldPackets.cpp index 1c9898a9..e963b10c 100644 --- a/dNet/WorldPackets.cpp +++ b/dNet/WorldPackets.cpp @@ -11,6 +11,7 @@ #include "BitStreamUtils.h" #include +#include void HTTPMonitorInfo::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write(port); @@ -88,27 +89,24 @@ void WorldPackets::SendCreateCharacter(const SystemAddress& sysAddr, int64_t rep RakNet::BitStream data; - std::vector> ldfData; - ldfData.push_back(std::move(make_unique>(u"objid", player))); - ldfData.push_back(std::move(make_unique>(u"template", 1))); - ldfData.push_back(std::move(make_unique>(u"xmlData", xmlData))); - ldfData.push_back(std::move(make_unique>(u"name", username))); - ldfData.push_back(std::move(make_unique>(u"gmlevel", static_cast(gm)))); - ldfData.push_back(std::move(make_unique>(u"chatmode", static_cast(gm)))); - ldfData.push_back(std::move(make_unique>(u"reputation", reputation))); - ldfData.push_back(std::move(make_unique>(u"propertycloneid", cloneID))); + LwoNameValue ldfData; + ldfData.Insert(u"objid", player); + ldfData.Insert(u"template", 1); + ldfData.Insert(u"xmlData", xmlData); + ldfData.Insert(u"name", username); + ldfData.Insert(u"gmlevel", static_cast(gm)); + ldfData.Insert(u"chatmode", static_cast(gm)); + ldfData.Insert(u"reputation", reputation); + ldfData.Insert(u"propertycloneid", cloneID); - data.Write(ldfData.size()); - for (const auto& toSerialize : ldfData) toSerialize->WriteToPacket(data); + data.Write(ldfData.values.size()); + for (const auto& toSerialize : ldfData | std::views::values) toSerialize->WriteToPacket(data); //Compress the data before sending: const uint32_t reservedSize = ZCompression::GetMaxCompressedLength(data.GetNumberOfBytesUsed()); - uint8_t* compressedData = new uint8_t[reservedSize]; + auto compressedData = std::make_unique(reservedSize); - // TODO There should be better handling here for not enough memory... - if (!compressedData) return; - - size_t size = ZCompression::Compress(data.GetData(), data.GetNumberOfBytesUsed(), compressedData, reservedSize); + size_t size = ZCompression::Compress(data.GetData(), data.GetNumberOfBytesUsed(), compressedData.get(), reservedSize); assert(size <= reservedSize); @@ -123,11 +121,10 @@ void WorldPackets::SendCreateCharacter(const SystemAddress& sysAddr, int64_t rep * an assertion is done to prevent bad data from being saved or sent. */ #pragma warning(disable:6385) // C6385 Reading invalid data from 'compressedData'. - bitStream.WriteAlignedBytes(compressedData, size); + bitStream.WriteAlignedBytes(compressedData.get(), size); #pragma warning(default:6385) SEND_PACKET; - delete[] compressedData; LOG("Sent CreateCharacter for ID: %llu", player); } diff --git a/dNet/ZoneInstanceManager.cpp b/dNet/ZoneInstanceManager.cpp index 354d3634..3be4b9a8 100644 --- a/dNet/ZoneInstanceManager.cpp +++ b/dNet/ZoneInstanceManager.cpp @@ -1,26 +1,17 @@ -#define _VARIADIC_MAX 10 #include "ZoneInstanceManager.h" // Custom Classes #include "MasterPackets.h" -#include "dServer.h" - -// C++ -#include // Static Variables ZoneInstanceManager* ZoneInstanceManager::m_Address = nullptr; //! Requests a zone transfer -void ZoneInstanceManager::RequestZoneTransfer(dServer* server, uint32_t zoneID, uint32_t zoneClone, bool mythranShift, std::function callback) { +void ZoneInstanceManager::RequestZoneTransfer(dServer* server, uint32_t zoneID, uint32_t zoneClone, bool mythranShift, TransferCallback callback) { + const auto nextID = ++currentRequestID; + requests[nextID] = callback; - ZoneTransferRequest* request = new ZoneTransferRequest(); - request->requestID = ++currentRequestID; - request->callback = callback; - - this->requests.push_back(request); - - MasterPackets::SendZoneTransferRequest(server, request->requestID, mythranShift, zoneID, zoneClone); + MasterPackets::SendZoneTransferRequest(server, nextID, mythranShift, zoneID, zoneClone); } //! Handles a zone transfer response @@ -43,18 +34,11 @@ void ZoneInstanceManager::HandleRequestZoneTransferResponse(Packet* packet) { LUString serverIP(255); inStream.Read(serverIP); - for (uint32_t i = 0; i < this->requests.size(); ++i) { - if (this->requests[i]->requestID == requestID) { - - // Call the request callback - this->requests[i]->callback(mythranShift, zoneID, zoneInstance, zoneClone, serverIP.string, serverPort); - - delete this->requests[i]; - this->requests.erase(this->requests.begin() + i); - return; - } + const auto entry = requests.find(requestID); + if (entry != requests.end()) { + entry->second(mythranShift, zoneID, zoneInstance, zoneClone, serverIP.string, serverPort); + requests.erase(entry); } - } void ZoneInstanceManager::CreatePrivateZone(dServer* server, uint32_t zoneID, uint32_t zoneClone, const std::string& password) { @@ -65,12 +49,9 @@ void ZoneInstanceManager::RequestPrivateZone( dServer* server, bool mythranShift, const std::string& password, - std::function callback) { - ZoneTransferRequest* request = new ZoneTransferRequest(); - request->requestID = ++currentRequestID; - request->callback = callback; + TransferCallback callback) { + const auto nextID = ++currentRequestID; + requests[nextID] = callback; - this->requests.push_back(request); - - MasterPackets::SendZoneRequestPrivate(server, request->requestID, mythranShift, password); + MasterPackets::SendZoneRequestPrivate(server, nextID, mythranShift, password); } diff --git a/dNet/ZoneInstanceManager.h b/dNet/ZoneInstanceManager.h index 47080cde..b47f92e2 100644 --- a/dNet/ZoneInstanceManager.h +++ b/dNet/ZoneInstanceManager.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -14,25 +15,20 @@ class dServer; \brief A class for handling zone transfers and zone-related functions */ - //! The zone request -struct ZoneTransferRequest { - uint64_t requestID; - std::function callback; -}; - //! The zone manager class ZoneInstanceManager { private: static ZoneInstanceManager* m_Address; //!< The singleton instance - std::vector requests; //!< The zone transfer requests + using TransferCallback = std::function; + std::map requests; //!< The zone transfer requests uint64_t currentRequestID; //!< The current request ID public: //! The singleton method static ZoneInstanceManager* Instance() { - if (m_Address == 0) { + if (m_Address == nullptr) { m_Address = new ZoneInstanceManager; m_Address->currentRequestID = 0; } @@ -47,7 +43,7 @@ public: \param mythranShift Whether or not this is a mythran shift \param callback The callback function */ - void RequestZoneTransfer(dServer* server, uint32_t zoneID, uint32_t zoneClone, bool mythranShift, std::function callback); + void RequestZoneTransfer(dServer* server, uint32_t zoneID, uint32_t zoneClone, bool mythranShift, TransferCallback callback); //! Handles a zone transfer response /*! @@ -58,6 +54,5 @@ public: void CreatePrivateZone(dServer* server, uint32_t zoneID, uint32_t zoneClone, const std::string& password); - void RequestPrivateZone(dServer* server, bool mythranShift, const std::string& password, std::function callback); - + void RequestPrivateZone(dServer* server, bool mythranShift, const std::string& password, TransferCallback callback); }; diff --git a/dNet/dServer.cpp b/dNet/dServer.cpp index 0a8e0ab9..92cfab5b 100644 --- a/dNet/dServer.cpp +++ b/dNet/dServer.cpp @@ -215,6 +215,8 @@ bool dServer::Startup() { mPeer = RakNetworkFactory::GetRakPeerInterface(); if (!mPeer) return false; + + if (mUseEncryption) mPeer->InitializeSecurity(nullptr, nullptr, nullptr, nullptr); if (!mPeer->Startup(mMaxConnections, 10, &mSocketDescriptor, 1)) return false; if (mIsInternal) { @@ -226,19 +228,16 @@ bool dServer::Startup() { } mPeer->SetMaximumIncomingConnections(mMaxConnections); - if (mUseEncryption) mPeer->InitializeSecurity(NULL, NULL, NULL, NULL); return true; } void dServer::UpdateMaximumMtuSize() { - auto maxMtuSize = mConfig->GetValue("maximum_mtu_size"); - mPeer->SetMTUSize(maxMtuSize.empty() ? 1228 : std::stoi(maxMtuSize)); + mPeer->SetMTUSize(mConfig->GetValue("maximum_mtu_size", 1228)); } void dServer::UpdateBandwidthLimit() { - auto newBandwidth = mConfig->GetValue("maximum_outgoing_bandwidth"); - mPeer->SetPerConnectionOutgoingBandwidthLimit(!newBandwidth.empty() ? std::stoi(newBandwidth) : 0); + mPeer->SetPerConnectionOutgoingBandwidthLimit(mConfig->GetValue("maximum_outgoing_bandwidth", 0)); } void dServer::Shutdown() { diff --git a/dPhysics/dpEntity.cpp b/dPhysics/dpEntity.cpp index 6fc40452..6368764f 100644 --- a/dPhysics/dpEntity.cpp +++ b/dPhysics/dpEntity.cpp @@ -109,6 +109,7 @@ void dpEntity::SetPosition(const NiPoint3& newPos) { } void dpEntity::SetRotation(const NiQuaternion& newRot) { + if (!m_CollisionShape) return; m_Rotation = newRot; if (m_CollisionShape->GetShapeType() == dpShapeType::Box) { @@ -118,6 +119,7 @@ void dpEntity::SetRotation(const NiQuaternion& newRot) { } void dpEntity::SetScale(float newScale) { + if (!m_CollisionShape) return; m_Scale = newScale; if (m_CollisionShape->GetShapeType() == dpShapeType::Box) { diff --git a/dPhysics/dpGrid.cpp b/dPhysics/dpGrid.cpp index 7a0db1f8..c338efe3 100644 --- a/dPhysics/dpGrid.cpp +++ b/dPhysics/dpGrid.cpp @@ -2,6 +2,7 @@ #include "dpEntity.h" #include +#include dpGrid::dpGrid(int numCells, int cellSize) { NUM_CELLS = numCells; @@ -122,38 +123,35 @@ void dpGrid::HandleEntity(dpEntity* entity, dpEntity* other) { void dpGrid::HandleCell(int x, int z, float deltaTime) { auto& entities = m_Cells[x][z]; //vector of entities contained within this cell. - for (auto en : entities) { + for (auto* en : entities) { if (!en) continue; if (en->GetIsStatic() || en->GetSleeping()) continue; //Check against all entities that are in the same cell as us - for (auto other : entities) - HandleEntity(en, other); + for (auto other : entities) HandleEntity(en, other); - //To try neighbouring cells as well: (can be disabled if needed) - //we only check 4 of the 8 neighbouring cells, otherwise we'd get duplicates and cpu cycles wasted... - - if (x > 0 && z > 0) { - for (auto other : m_Cells[x - 1][z - 1]) - HandleEntity(en, other); + // All 8 neighbours in one pass. + // staticOnly=false — canonical 4: covers each dynamic-vs-dynamic pair exactly once, + // since the higher-index cell checks back to the lower-index cell. + // staticOnly=true — skipped 4: dynamic entities there are handled when those cells + // process their own en loop; static ones never drive a loop, so + // we handle them here explicitly to avoid missing exits. + struct NeighbourCheck { int dx, dz; bool staticOnly; }; + constexpr NeighbourCheck kNeighbours[8] = { + { -1, -1, false }, { -1, 0, false }, { 0, -1, false }, { -1, 1, false }, + { 1, -1, true }, { 1, 0, true }, { 0, 1, true }, { 1, 1, true }, + }; + for (auto [dx, dz, staticOnly] : kNeighbours) { + const int nx = x + dx; + const int nz = z + dz; + // Ensure the cell we're checking is within the valid range + if (nx < 0 || nx >= NUM_CELLS || nz < 0 || nz >= NUM_CELLS) continue; + for (auto* other : m_Cells[nx][nz]) { + if (!staticOnly || (other && other->GetIsStatic())) + HandleEntity(en, other); + } } - if (x > 0) { - for (auto other : m_Cells[x - 1][z]) - HandleEntity(en, other); - } - - if (z > 0) { - for (auto other : m_Cells[x][z - 1]) - HandleEntity(en, other); - } - - if (x > 0 && z < NUM_CELLS - 1) { - for (auto other : m_Cells[x - 1][z + 1]) - HandleEntity(en, other); - } - - for (auto& [id, entity] : m_GargantuanObjects) - HandleEntity(en, entity); + for (auto* entity : m_GargantuanObjects | std::views::values) HandleEntity(en, entity); } } diff --git a/dPhysics/dpWorld.cpp b/dPhysics/dpWorld.cpp index c9bc742a..a3df390c 100644 --- a/dPhysics/dpWorld.cpp +++ b/dPhysics/dpWorld.cpp @@ -87,7 +87,7 @@ void dpWorld::Shutdown() { } bool dpWorld::IsLoaded() { - return m_NavMesh->IsNavmeshLoaded(); + return m_NavMesh && m_NavMesh->IsNavmeshLoaded(); } void dpWorld::StepWorld(float deltaTime) { diff --git a/dScripts/02_server/CMakeLists.txt b/dScripts/02_server/CMakeLists.txt index 8114b226..048139a0 100644 --- a/dScripts/02_server/CMakeLists.txt +++ b/dScripts/02_server/CMakeLists.txt @@ -37,6 +37,7 @@ target_include_directories(dScriptsServerBase PUBLIC "." "Minigame" "Minigame/General" "Objects" + "Objects/Hatchlings" ) target_precompile_headers(dScriptsServerBase REUSE_FROM dScriptsBase) diff --git a/dScripts/02_server/DLU/CMakeLists.txt b/dScripts/02_server/DLU/CMakeLists.txt index fb257d3e..3185df56 100644 --- a/dScripts/02_server/DLU/CMakeLists.txt +++ b/dScripts/02_server/DLU/CMakeLists.txt @@ -1,3 +1,4 @@ set(DSCRIPTS_SOURCES_02_SERVER_DLU "DLUVanityTeleportingObject.cpp" + "RegisterWithZoneControl.cpp" PARENT_SCOPE) diff --git a/dScripts/02_server/DLU/RegisterWithZoneControl.cpp b/dScripts/02_server/DLU/RegisterWithZoneControl.cpp new file mode 100644 index 00000000..e588f240 --- /dev/null +++ b/dScripts/02_server/DLU/RegisterWithZoneControl.cpp @@ -0,0 +1,12 @@ +#include "RegisterWithZoneControl.h" + +#include "Entity.h" +#include "EntityManager.h" +#include "GameMessages.h" + +void RegisterWithZoneControl::OnStartup(Entity* self) { + GameMessages::ObjectLoaded objLoaded; + objLoaded.objectID = self->GetObjectID(); + objLoaded.lot = self->GetLOT(); + objLoaded.Send(Game::entityManager->GetZoneControlEntity()->GetObjectID()); +} diff --git a/dScripts/02_server/DLU/RegisterWithZoneControl.h b/dScripts/02_server/DLU/RegisterWithZoneControl.h new file mode 100644 index 00000000..dda89882 --- /dev/null +++ b/dScripts/02_server/DLU/RegisterWithZoneControl.h @@ -0,0 +1,14 @@ +// Darkflame Universe +// Copyright 2026 + +#ifndef REGISTERWITHZONECONTROL_H +#define REGISTERWITHZONECONTROL_H + +#include "CppScripts.h" + +class RegisterWithZoneControl : public CppScripts::Script { +public: + void OnStartup(Entity* self) override; +}; + +#endif //!REGISTERWITHZONECONTROL_H diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp index b3938fd4..869e09ba 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp @@ -16,6 +16,7 @@ #include "eReplicaComponentType.h" #include "RenderComponent.h" #include "PlayerManager.h" +#include "eStateChangeType.h" #include @@ -48,10 +49,30 @@ void BossSpiderQueenEnemyServer::OnStartup(Entity* self) { combat->SetStunImmune(true); m_CurrentBossStage = 1; - + ToggleAttacking(*self, false); + self->SetProximityRadius(65.0f, "AggroRadius"); // Obtain faction and collision group to save for subsequent resets } +void BossSpiderQueenEnemyServer::OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status) { + if (name != "AggroRadius" || !entering || !entering->IsPlayer()) return; + + auto playerCount = self->GetVar(u"player_count"); + + if (status == "ENTER") { + if (playerCount == 0) { + ToggleAttacking(*self, true); + } + playerCount++; + } else if (status == "LEAVE") { + playerCount--; + if (playerCount == 0) { + ToggleAttacking(*self, false); + } + } + self->SetVar(u"player_count", playerCount); +} + void BossSpiderQueenEnemyServer::OnDie(Entity* self, Entity* killer) { if (Game::zoneManager->GetZoneID().GetMapID() == instanceZoneID && killer) { for (const auto& player : PlayerManager::GetAllPlayers()) { @@ -71,6 +92,7 @@ void BossSpiderQueenEnemyServer::OnDie(Entity* self, Entity* killer) { self->SetPosition({ 10000, 0, 10000 }); Game::entityManager->SerializeEntity(self); + ToggleAttacking(*self, false); controller->OnFireEventServerSide(self, "ClearProperty"); } @@ -634,3 +656,19 @@ float BossSpiderQueenEnemyServer::PlayAnimAndReturnTime(Entity* self, const std: return animTimer; } + +void BossSpiderQueenEnemyServer::ToggleAttacking(Entity& self, bool on) { + const auto stoppedFlag = self.GetVarAs(u"stoppedFlag"); + + if (!on) { + if (stoppedFlag) return; + + self.SetVar(u"stoppedFlag", true); + combat->Stun(100000.0f, true); // forcibly stun so we stop attacking people trying to put on armor + } else { + if (!stoppedFlag) return; + + self.SetVar(u"stoppedFlag", false); + combat->Stun(0.0f, true); // forcibly turn off the stun we put on above + } +} diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h index b5909000..0f975abe 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h @@ -46,7 +46,10 @@ public: void OnTimerDone(Entity* self, std::string timerName) override; + void OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status); + private: + void ToggleAttacking(Entity& self, bool on); //Regular variables: DestroyableComponent* destroyable = nullptr; ControllablePhysicsComponent* controllable = nullptr; diff --git a/dScripts/02_server/Enemy/AM/AmDarklingDragon.cpp b/dScripts/02_server/Enemy/AM/AmDarklingDragon.cpp index 61d25436..5fbbda20 100644 --- a/dScripts/02_server/Enemy/AM/AmDarklingDragon.cpp +++ b/dScripts/02_server/Enemy/AM/AmDarklingDragon.cpp @@ -97,17 +97,14 @@ void AmDarklingDragon::OnHitOrHealResult(Entity* self, Entity* attacker, int32_t info.pos = objectPosition; info.rot = rotation; info.spawnerID = self->GetObjectID(); - info.settings = { - new LDFData(u"rebuild_activators", + info.settings.Insert(u"rebuild_activators", std::to_string(objectPosition.x + forward.x) + "\x1f" + std::to_string(objectPosition.y) + "\x1f" + - std::to_string(objectPosition.z + forward.z) - ), - new LDFData(u"respawn", 100000), - new LDFData(u"rebuild_reset_time", 15), - new LDFData(u"no_timed_spawn", true), - new LDFData(u"Dragon", self->GetObjectID()) - }; + std::to_string(objectPosition.z + forward.z)); + info.settings.Insert(u"respawn", 100000); + info.settings.Insert(u"rebuild_reset_time", 15); + info.settings.Insert(u"no_timed_spawn", true); + info.settings.Insert(u"Dragon", self->GetObjectID()); auto* golemObject = Game::entityManager->CreateEntity(info); diff --git a/dScripts/02_server/Enemy/FV/FvMaelstromDragon.cpp b/dScripts/02_server/Enemy/FV/FvMaelstromDragon.cpp index b4b038b8..2ddc6309 100644 --- a/dScripts/02_server/Enemy/FV/FvMaelstromDragon.cpp +++ b/dScripts/02_server/Enemy/FV/FvMaelstromDragon.cpp @@ -113,17 +113,14 @@ void FvMaelstromDragon::OnHitOrHealResult(Entity* self, Entity* attacker, int32_ info.pos = objectPosition; info.rot = rotation; info.spawnerID = self->GetObjectID(); - info.settings = { - new LDFData(u"rebuild_activators", + info.settings.Insert(u"rebuild_activators", std::to_string(objectPosition.x + forward.x) + "\x1f" + std::to_string(objectPosition.y) + "\x1f" + - std::to_string(objectPosition.z + forward.z) - ), - new LDFData(u"respawn", 100000), - new LDFData(u"rebuild_reset_time", 15), - new LDFData(u"no_timed_spawn", true), - new LDFData(u"Dragon", self->GetObjectID()) - }; + std::to_string(objectPosition.z + forward.z)); + info.settings.Insert(u"respawn", 100000); + info.settings.Insert(u"rebuild_reset_time", 15); + info.settings.Insert(u"no_timed_spawn", true); + info.settings.Insert(u"Dragon", self->GetObjectID()); auto* golemObject = Game::entityManager->CreateEntity(info); diff --git a/dScripts/02_server/Enemy/General/BaseEnemyApe.cpp b/dScripts/02_server/Enemy/General/BaseEnemyApe.cpp index db6754f3..26948c57 100644 --- a/dScripts/02_server/Enemy/General/BaseEnemyApe.cpp +++ b/dScripts/02_server/Enemy/General/BaseEnemyApe.cpp @@ -82,17 +82,14 @@ void BaseEnemyApe::OnTimerDone(Entity* self, std::string timerName) { entityInfo.spawnerID = self->GetObjectID(); entityInfo.lot = self->GetVar(u"QuickbuildAnchorLOT") != 0 ? self->GetVar(u"QuickbuildAnchorLOT") : 7549; - entityInfo.settings = { - new LDFData(u"rebuild_activators", + entityInfo.settings.Insert(u"rebuild_activators", std::to_string(objectPosition.GetX()) + "\x1f" + std::to_string(objectPosition.GetY()) + "\x1f" + - std::to_string(objectPosition.GetZ()) - ), - new LDFData(u"no_timed_spawn", true), - new LDFData(u"ape", self->GetObjectID()) - }; + std::to_string(objectPosition.GetZ())); + entityInfo.settings.Insert(u"no_timed_spawn", true); + entityInfo.settings.Insert(u"ape", self->GetObjectID()); - auto* anchor = Game::entityManager->CreateEntity(entityInfo); + auto* anchor = Game::entityManager->CreateEntity(entityInfo, nullptr, self); Game::entityManager->ConstructEntity(anchor); self->SetVar(u"QB", anchor->GetObjectID()); @@ -140,3 +137,9 @@ void BaseEnemyApe::StunApe(Entity* self, bool stunState) { self->SetBoolean(u"knockedOut", stunState); } } + +void BaseEnemyApe::OnChildRemoved(Entity& self, GameMessages::ChildRemoved& childRemoved) { + if (self.GetVar(u"QB") == childRemoved.childID) { + self.SetVar(u"QB", LWOOBJID_EMPTY); + } +} diff --git a/dScripts/02_server/Enemy/General/BaseEnemyApe.h b/dScripts/02_server/Enemy/General/BaseEnemyApe.h index 54a734bb..a31f1612 100644 --- a/dScripts/02_server/Enemy/General/BaseEnemyApe.h +++ b/dScripts/02_server/Enemy/General/BaseEnemyApe.h @@ -10,6 +10,7 @@ public: void OnTimerDone(Entity* self, std::string timerName) override; void OnFireEventServerSide(Entity* self, Entity* sender, std::string args, int32_t param1, int32_t param2, int32_t param3) override; + void OnChildRemoved(Entity& self, GameMessages::ChildRemoved& childRemoved) override; private: static void StunApe(Entity* self, bool stunState); }; diff --git a/dScripts/02_server/Enemy/General/BaseEnemyMech.cpp b/dScripts/02_server/Enemy/General/BaseEnemyMech.cpp index 8e70d4c3..45f1e9ad 100644 --- a/dScripts/02_server/Enemy/General/BaseEnemyMech.cpp +++ b/dScripts/02_server/Enemy/General/BaseEnemyMech.cpp @@ -23,7 +23,7 @@ void BaseEnemyMech::OnDie(Entity* self, Entity* killer) { NiPoint3 newLoc = { controlPhys->GetPosition().x, dpWorld::GetNavMesh()->GetHeightAtPoint(controlPhys->GetPosition()), controlPhys->GetPosition().z }; EntityInfo info = EntityInfo(); - std::vector cfg; + LwoNameValue cfg; std::u16string activatorPosStr; activatorPosStr += (GeneralUtils::to_u16string(controlPhys->GetPosition().x)); activatorPosStr.push_back(0x1f); @@ -31,8 +31,7 @@ void BaseEnemyMech::OnDie(Entity* self, Entity* killer) { activatorPosStr.push_back(0x1f); activatorPosStr += (GeneralUtils::to_u16string(controlPhys->GetPosition().z)); - LDFBaseData* activatorPos = new LDFData(u"rebuild_activators", activatorPosStr); - cfg.push_back(activatorPos); + cfg.Insert(u"rebuild_activators", activatorPosStr); info.lot = qbTurretLOT; info.pos = newLoc; info.rot = controlPhys->GetRotation(); diff --git a/dScripts/02_server/Enemy/General/TreasureChestDragonServer.cpp b/dScripts/02_server/Enemy/General/TreasureChestDragonServer.cpp index ec6327e0..b0a98afc 100644 --- a/dScripts/02_server/Enemy/General/TreasureChestDragonServer.cpp +++ b/dScripts/02_server/Enemy/General/TreasureChestDragonServer.cpp @@ -20,24 +20,10 @@ void TreasureChestDragonServer::OnUse(Entity* self, Entity* user) { if (scriptedActivityComponent == nullptr) { return; } - - auto rating = 1; - + auto* team = TeamManager::Instance()->GetTeam(user->GetObjectID()); - - if (team != nullptr) { - rating = team->members.size(); - - for (const auto member : team->members) { - auto* memberObject = Game::entityManager->GetEntity(member); - - if (memberObject == nullptr) continue; - - Loot::DropActivityLoot(memberObject, self->GetObjectID(), scriptedActivityComponent->GetActivityID(), rating); - } - } else { - Loot::DropActivityLoot(user, self->GetObjectID(), scriptedActivityComponent->GetActivityID(), rating); - } + + Loot::DropActivityLoot(user, self->GetObjectID(), scriptedActivityComponent->GetActivityID(), team ? team->members.size() : 1); self->Smash(self->GetObjectID()); } diff --git a/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp b/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp index fc724fb9..7e8ca7e8 100644 --- a/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp +++ b/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp @@ -19,7 +19,7 @@ void NpcAgCourseStarter::OnUse(Entity* self, Entity* user) { const auto userId = user->GetObjectID(); const auto& userSysAddr = user->GetSystemAddress(); - if (scriptedActivityComponent->GetActivityPlayerData(userId) != nullptr) { + if (scriptedActivityComponent->PlayerHasActivityData(userId)) { GameMessages::SendNotifyClientObject(selfId, u"exit", 0, 0, LWOOBJID_EMPTY, "", userSysAddr); } else { GameMessages::SendNotifyClientObject(selfId, u"start", 0, 0, LWOOBJID_EMPTY, "", userSysAddr); @@ -45,18 +45,18 @@ void NpcAgCourseStarter::OnMessageBoxResponse(Entity* self, Entity* sender, int3 GameMessages::SendNotifyClientObject(selfId, u"start_timer", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr); GameMessages::SendActivityStart(selfId, senderSysAddr); - auto* const data = scriptedActivityComponent->AddActivityPlayerData(senderId); - if (data->values[1] != 0) return; + const auto score = scriptedActivityComponent->GetActivityValue(senderId, 1); + if (score != 0 && score != -1.0f) return; const auto raceStartTime = Game::server->GetUptime() + std::chrono::seconds(4); // Offset for starting timer const auto fRaceStartTime = std::chrono::duration>(raceStartTime).count(); - data->values[1] = fRaceStartTime; + scriptedActivityComponent->SetActivityValue(senderId, 1, fRaceStartTime); Game::entityManager->SerializeEntity(self); } else if (identifier == u"FootRaceCancel") { GameMessages::SendNotifyClientObject(selfId, u"stop_timer", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr); - if (scriptedActivityComponent->GetActivityPlayerData(senderId) != nullptr) { + if (scriptedActivityComponent->PlayerHasActivityData(senderId)) { GameMessages::SendNotifyClientObject(selfId, u"exit", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr); } else { GameMessages::SendNotifyClientObject(selfId, u"start", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr); @@ -74,8 +74,7 @@ void NpcAgCourseStarter::OnFireEventServerSide(Entity* self, Entity* sender, std const auto senderId = sender->GetObjectID(); const auto& senderSysAddr = sender->GetSystemAddress(); - auto* const data = scriptedActivityComponent->GetActivityPlayerData(senderId); - if (!data) return; + if (!scriptedActivityComponent->PlayerHasActivityData(senderId)) return; if (args == "course_cancel") { GameMessages::SendNotifyClientObject(selfId, u"cancel_timer", 0, 0, @@ -84,8 +83,11 @@ void NpcAgCourseStarter::OnFireEventServerSide(Entity* self, Entity* sender, std } else if (args == "course_finish") { const auto raceEndTime = Game::server->GetUptime(); const auto fRaceEndTime = std::chrono::duration>(raceEndTime).count(); - const auto raceTimeElapsed = fRaceEndTime - data->values[1]; - data->values[2] = raceTimeElapsed; + const float startTime = scriptedActivityComponent->GetActivityValue(senderId, 1); + if (startTime == 0 || startTime == -1.0f) return; + + const auto raceTimeElapsed = fRaceEndTime - startTime; + scriptedActivityComponent->SetActivityValue(senderId, 2, raceTimeElapsed); auto* const missionComponent = sender->GetComponent(); if (missionComponent != nullptr) { diff --git a/dScripts/02_server/Map/AG_Spider_Queen/ZoneAgSpiderQueen.cpp b/dScripts/02_server/Map/AG_Spider_Queen/ZoneAgSpiderQueen.cpp index 2091041b..2b367ed2 100644 --- a/dScripts/02_server/Map/AG_Spider_Queen/ZoneAgSpiderQueen.cpp +++ b/dScripts/02_server/Map/AG_Spider_Queen/ZoneAgSpiderQueen.cpp @@ -71,9 +71,7 @@ void ZoneAgSpiderQueen::OnTimerDone(Entity* self, std::string timerName) { info.pos = spawnTarget->GetPosition(); info.rot = spawnTarget->GetRotation(); info.lot = chestObject; - info.settings = { - new LDFData(u"parent_tag", self->GetObjectID()) - }; + info.settings.Insert(u"parent_tag", self->GetObjectID()); auto* chest = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(chest); diff --git a/dScripts/02_server/Map/AM/AmDrawBridge.cpp b/dScripts/02_server/Map/AM/AmDrawBridge.cpp index 590024b5..c356145b 100644 --- a/dScripts/02_server/Map/AM/AmDrawBridge.cpp +++ b/dScripts/02_server/Map/AM/AmDrawBridge.cpp @@ -48,7 +48,7 @@ void AmDrawBridge::OnTimerDone(Entity* self, std::string timerName) { } self->SetNetworkVar(u"BridgeLeaving", true); - self->SetVar(u"BridgeDown", false); + self->SetNetworkVar(u"InUse", false); } else if (timerName == "SmashEffectBridge") { self->SetNetworkVar(u"SmashBridge", 5); } else if (timerName == "rotateBridgeDown") { diff --git a/dScripts/02_server/Map/AM/AmSkullkinDrill.cpp b/dScripts/02_server/Map/AM/AmSkullkinDrill.cpp index 576688e7..df5a300f 100644 --- a/dScripts/02_server/Map/AM/AmSkullkinDrill.cpp +++ b/dScripts/02_server/Map/AM/AmSkullkinDrill.cpp @@ -36,13 +36,14 @@ void AmSkullkinDrill::OnStartup(Entity* self) { Entity* AmSkullkinDrill::GetStandObj(Entity* self) { const auto& myGroup = self->GetGroups(); - if (myGroup.empty()) { + if (myGroup.empty() || myGroup[0].empty()) { return nullptr; } + const auto& group = myGroup[0]; std::string groupName = "Drill_Stand_"; - groupName.push_back(myGroup[0][myGroup[0].size() - 1]); + groupName.push_back(group.back()); const auto standObjs = Game::entityManager->GetEntitiesInGroup(groupName); diff --git a/dScripts/02_server/Map/AM/AmSkullkinTower.cpp b/dScripts/02_server/Map/AM/AmSkullkinTower.cpp index 17ae2ea9..19c39f8b 100644 --- a/dScripts/02_server/Map/AM/AmSkullkinTower.cpp +++ b/dScripts/02_server/Map/AM/AmSkullkinTower.cpp @@ -37,12 +37,10 @@ void AmSkullkinTower::SpawnLegs(Entity* self, const std::string& loc) { return; } - std::vector config = { new LDFData(u"Leg", loc) }; - EntityInfo info{}; info.lot = legLOT; info.spawnerID = self->GetObjectID(); - info.settings = config; + info.settings.Insert("Leg", loc); info.rot = newRot; if (loc == "Right") { diff --git a/dScripts/02_server/Map/General/Binoculars.cpp b/dScripts/02_server/Map/General/Binoculars.cpp index 854ccf71..786c0faf 100644 --- a/dScripts/02_server/Map/General/Binoculars.cpp +++ b/dScripts/02_server/Map/General/Binoculars.cpp @@ -7,9 +7,14 @@ void Binoculars::OnUse(Entity* self, Entity* user) { const auto number = self->GetVarAsString(u"number"); - int32_t flag = std::stoi(std::to_string(Game::server->GetZoneID()).substr(0, 2) + number); - if (user->GetCharacter()->GetPlayerFlag(flag) == false) { - user->GetCharacter()->SetPlayerFlag(flag, true); + const int32_t flag = GeneralUtils::TryParse(std::to_string(Game::server->GetZoneID()).substr(0, 2) + number, 0); + GameMessages::GetFlag flagMsg; + flagMsg.target = user->GetObjectID(); + flagMsg.flagID = flag; + flagMsg.Send(); + if (!flagMsg.flag) { + auto* const character = user->GetCharacter(); + if (character) character->SetPlayerFlag(flag, true); GameMessages::SendFireEventClientSide(self->GetObjectID(), user->GetSystemAddress(), u"achieve", LWOOBJID_EMPTY, 0, -1, LWOOBJID_EMPTY); } } diff --git a/dScripts/02_server/Map/General/GrowingFlower.cpp b/dScripts/02_server/Map/General/GrowingFlower.cpp index 04cc11ab..2c3929e2 100644 --- a/dScripts/02_server/Map/General/GrowingFlower.cpp +++ b/dScripts/02_server/Map/General/GrowingFlower.cpp @@ -2,6 +2,7 @@ #include "MissionComponent.h" #include "eMissionTaskType.h" #include "eMissionState.h" +#include "ScriptedActivityComponent.h" #include "Loot.h" void GrowingFlower::OnSkillEventFired(Entity* self, Entity* target, const std::string& message) { @@ -13,7 +14,10 @@ void GrowingFlower::OnSkillEventFired(Entity* self, Entity* target, const std::s const auto mission1 = self->GetVar(u"missionID"); const auto mission2 = self->GetVar(u"missionID2"); - Loot::DropActivityLoot(target, self->GetObjectID(), self->GetLOT(), 0); + auto* scriptedActivityComponent = self->GetComponent(); + if (scriptedActivityComponent != nullptr) { + Loot::DropActivityLoot(target, self->GetObjectID(), scriptedActivityComponent->GetActivityID(), 0); + } auto* missionComponent = target->GetComponent(); if (missionComponent != nullptr) { diff --git a/dScripts/02_server/Map/General/PetDigServer.cpp b/dScripts/02_server/Map/General/PetDigServer.cpp index 77a50e5a..e20194a4 100644 --- a/dScripts/02_server/Map/General/PetDigServer.cpp +++ b/dScripts/02_server/Map/General/PetDigServer.cpp @@ -204,12 +204,10 @@ void PetDigServer::SpawnPet(Entity* self, const Entity* owner, const DigInfo dig info.pos = self->GetPosition(); info.rot = self->GetRotation(); info.spawnerID = self->GetSpawnerID(); - info.settings = { - new LDFData(u"tamer", owner->GetObjectID()), - new LDFData(u"group", "pet" + std::to_string(owner->GetObjectID())), - new LDFData(u"spawnAnim", "spawn-pet"), - new LDFData(u"spawnTimer", 1.0) - }; + info.settings.Insert(u"tamer", owner->GetObjectID()); + info.settings.Insert(u"group", "pet" + std::to_string(owner->GetObjectID())); + info.settings.Insert(u"spawnAnim", "spawn-pet"); + info.settings.Insert(u"spawnTimer", 1.0); auto* spawnedPet = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(spawnedPet); diff --git a/dScripts/02_server/Map/General/QbSpawner.cpp b/dScripts/02_server/Map/General/QbSpawner.cpp index 0562fdd8..9081ec57 100644 --- a/dScripts/02_server/Map/General/QbSpawner.cpp +++ b/dScripts/02_server/Map/General/QbSpawner.cpp @@ -66,14 +66,12 @@ void QbSpawner::OnTimerDone(Entity* self, std::string timerName) { info.rot = newRot; info.spawnerID = self->GetObjectID(); info.spawnerNodeID = 0; - info.settings = { - new LDFData(u"no_timed_spawn", true), - new LDFData(u"aggroRadius", 70), - new LDFData(u"softtetherRadius", 80), - new LDFData(u"tetherRadius", 90), - new LDFData(u"wanderRadius", 5), - new LDFData(u"mobTableLoc", i) - }; + info.settings.Insert(u"no_timed_spawn", true); + info.settings.Insert(u"aggroRadius", 70); + info.settings.Insert(u"softtetherRadius", 80); + info.settings.Insert(u"tetherRadius", 90); + info.settings.Insert(u"wanderRadius", 5); + info.settings.Insert(u"mobTableLoc", i); auto* child = Game::entityManager->CreateEntity(info, nullptr, self); Game::entityManager->ConstructEntity(child); diff --git a/dScripts/02_server/Map/General/StoryBoxInteractServer.cpp b/dScripts/02_server/Map/General/StoryBoxInteractServer.cpp index 4ad78d6a..a5ce92cd 100644 --- a/dScripts/02_server/Map/General/StoryBoxInteractServer.cpp +++ b/dScripts/02_server/Map/General/StoryBoxInteractServer.cpp @@ -33,14 +33,18 @@ void StoryBoxInteractServer::OnUse(Entity* self, Entity* user) { const auto storyText = self->GetVarAsString(u"storyText"); if (storyText.length() > 2) { auto storyValue = GeneralUtils::TryParse(storyText.substr(storyText.length() - 2)); - if(!storyValue) return; + if (!storyValue) return; int32_t boxFlag = self->GetVar(u"altFlagID"); if (boxFlag <= 0) { boxFlag = (10000 + Game::server->GetZoneID() + storyValue.value()); } - - if (user->GetCharacter()->GetPlayerFlag(boxFlag) == false) { - user->GetCharacter()->SetPlayerFlag(boxFlag, true); + GameMessages::GetFlag flagMsg; + flagMsg.target = user->GetObjectID(); + flagMsg.flagID = boxFlag; + flagMsg.Send(); + if (!flagMsg.flag) { + auto* const character = user->GetCharacter(); + if (character) character->SetPlayerFlag(boxFlag, true); GameMessages::SendFireEventClientSide(self->GetObjectID(), user->GetSystemAddress(), u"achieve", LWOOBJID_EMPTY, 0, -1, LWOOBJID_EMPTY); } } diff --git a/dScripts/02_server/Map/General/VisToggleNotifierServer.cpp b/dScripts/02_server/Map/General/VisToggleNotifierServer.cpp index a02856ef..16d805eb 100644 --- a/dScripts/02_server/Map/General/VisToggleNotifierServer.cpp +++ b/dScripts/02_server/Map/General/VisToggleNotifierServer.cpp @@ -14,7 +14,7 @@ void VisToggleNotifierServer::OnMissionDialogueOK(Entity* self, Entity* target, auto spawners = Game::zoneManager->GetSpawnersByName(itr->second); if (spawners.empty()) return; for (const auto spawner : spawners) { - auto spawnedObjIds = spawner->GetSpawnedObjectIDs(); + const auto& spawnedObjIds = spawner->GetSpawnedObjectIDs(); for (const auto& objId : spawnedObjIds) { GameMessages::SendNotifyClientObject(objId, u"SetVisibility", visible); } diff --git a/dScripts/02_server/Map/NS/NsConcertChoiceBuildManager.cpp b/dScripts/02_server/Map/NS/NsConcertChoiceBuildManager.cpp index 554eabac..aab66449 100644 --- a/dScripts/02_server/Map/NS/NsConcertChoiceBuildManager.cpp +++ b/dScripts/02_server/Map/NS/NsConcertChoiceBuildManager.cpp @@ -26,19 +26,18 @@ void NsConcertChoiceBuildManager::SpawnCrate(Entity* self) { const auto splitGroup = GeneralUtils::SplitString(group, '_'); if (splitGroup.size() < 2) return; - const auto groupNumber = std::stoi(splitGroup.at(1)); + const auto groupNumber = GeneralUtils::TryParse(splitGroup.at(1), -1); + if (groupNumber == -1) return; EntityInfo info{}; info.lot = crate.lot; info.pos = self->GetPosition(); info.rot = self->GetRotation(); info.spawnerID = self->GetObjectID(); - info.settings = { - new LDFData(u"startsQBActivator", true), - new LDFData(u"grpNameQBShowBricks", crate.group + std::to_string(groupNumber)), - new LDFData(u"groupID", GeneralUtils::ASCIIToUTF16("Crate_" + group)), - new LDFData(u"crateTime", crate.time), - }; + info.settings.Insert(u"startsQBActivator", true); + info.settings.Insert(u"grpNameQBShowBricks", crate.group + std::to_string(groupNumber)); + info.settings.Insert(u"groupID", GeneralUtils::ASCIIToUTF16("Crate_" + group)); + info.settings.Insert(u"crateTime", crate.time); auto* spawnedCrate = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(spawnedCrate); diff --git a/dScripts/02_server/Map/NT/NtCombatChallengeServer.cpp b/dScripts/02_server/Map/NT/NtCombatChallengeServer.cpp index 2b2f16f9..c036b023 100644 --- a/dScripts/02_server/Map/NT/NtCombatChallengeServer.cpp +++ b/dScripts/02_server/Map/NT/NtCombatChallengeServer.cpp @@ -89,7 +89,7 @@ void NtCombatChallengeServer::SpawnTargetDummy(Entity* self) { info.spawnerID = self->GetObjectID(); info.pos = self->GetPosition(); info.rot = self->GetRotation(); - info.settings = { new LDFData(u"custom_script_server", "scripts\\02_server\\Map\\NT\\L_NT_COMBAT_CHALLENGE_DUMMY.lua") }; + info.settings.Insert(u"custom_script_server", "scripts\\02_server\\Map\\NT\\L_NT_COMBAT_CHALLENGE_DUMMY.lua"); auto* dummy = Game::entityManager->CreateEntity(info, nullptr, self); diff --git a/dScripts/02_server/Map/NT/NtParadoxPanelServer.cpp b/dScripts/02_server/Map/NT/NtParadoxPanelServer.cpp index e64ad2dd..f182c8a5 100644 --- a/dScripts/02_server/Map/NT/NtParadoxPanelServer.cpp +++ b/dScripts/02_server/Map/NT/NtParadoxPanelServer.cpp @@ -33,7 +33,8 @@ void NtParadoxPanelServer::OnUse(Entity* self, Entity* user) { const auto flag = self->GetVar(u"flag"); - player->GetCharacter()->SetPlayerFlag(flag, true); + auto* const character = player->GetCharacter(); + if (character) character->SetPlayerFlag(flag, true); RenderComponent::PlayAnimation(player, u"rebuild-celebrate"); diff --git a/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp b/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp index 6f2f6d36..18054365 100644 --- a/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp +++ b/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp @@ -95,10 +95,8 @@ void ZoneAgProperty::LoadInstance(Entity* self) { for (auto* spawner : Game::zoneManager->GetSpawnersByName(self->GetVar(InstancerSpawner))) { for (auto* spawnerNode : spawner->m_Info.nodes) { - spawnerNode->config.push_back( - new LDFData(u"custom_script_server", - R"(scripts\ai\GENERAL\L_INSTANCE_EXIT_TRANSFER_PLAYER_TO_LAST_NON_INSTANCE.lua)")); - spawnerNode->config.push_back(new LDFData(u"transferText", u"SPIDER_QUEEN_EXIT_QUESTION")); + spawnerNode->config.Insert(u"custom_script_server", R"(scripts\ai\GENERAL\L_INSTANCE_EXIT_TRANSFER_PLAYER_TO_LAST_NON_INSTANCE.lua)"); + spawnerNode->config.Insert(u"transferText", u"SPIDER_QUEEN_EXIT_QUESTION"); } } @@ -413,7 +411,8 @@ void ZoneAgProperty::BaseOnFireEventServerSide(Entity* self, Entity* sender, std if (player == nullptr) return; - player->GetCharacter()->SetPlayerFlag(self->GetVar(defeatedProperyFlag), true); + auto* const character = player->GetCharacter(); + if (character) character->SetPlayerFlag(self->GetVar(defeatedProperyFlag), true); GameMessages::SendNotifyClientObject(self->GetObjectID(), u"PlayCinematic", 0, 0, LWOOBJID_EMPTY, destroyedCinematic, UNASSIGNED_SYSTEM_ADDRESS); diff --git a/dScripts/02_server/Map/VE/VeMissionConsole.cpp b/dScripts/02_server/Map/VE/VeMissionConsole.cpp index 011d680d..aa7035eb 100644 --- a/dScripts/02_server/Map/VE/VeMissionConsole.cpp +++ b/dScripts/02_server/Map/VE/VeMissionConsole.cpp @@ -15,7 +15,7 @@ void VeMissionConsole::OnUse(Entity* self, Entity* user) { // The flag to set is 101 const auto flagNumber = self->GetVar(m_NumberVariable); - const int32_t flag = std::stoi("101" + GeneralUtils::UTF16ToWTF8(flagNumber)); + const int32_t flag = GeneralUtils::TryParse("101" + GeneralUtils::UTF16ToWTF8(flagNumber), 0); auto* character = user->GetCharacter(); if (character != nullptr) { diff --git a/dScripts/02_server/Map/njhub/CMakeLists.txt b/dScripts/02_server/Map/njhub/CMakeLists.txt index 94d99867..b2d0ba7f 100644 --- a/dScripts/02_server/Map/njhub/CMakeLists.txt +++ b/dScripts/02_server/Map/njhub/CMakeLists.txt @@ -6,6 +6,7 @@ set(DSCRIPTS_SOURCES_02_SERVER_MAP_NJHUB "EnemySkeletonSpawner.cpp" "FallingTile.cpp" "FlameJetServer.cpp" + "LightningOrbServer.cpp" "ImaginationShrineServer.cpp" "Lieutenant.cpp" "MonCoreNookDoors.cpp" @@ -18,6 +19,7 @@ set(DSCRIPTS_SOURCES_02_SERVER_MAP_NJHUB "NjJayMissionItems.cpp" "NjNPCMissionSpinjitzuServer.cpp" "NjNyaMissionitems.cpp" + "OldManNPC.cpp" "NjScrollChestServer.cpp" "NjWuNPC.cpp" "RainOfArrows.cpp") diff --git a/dScripts/02_server/Map/njhub/CavePrisonCage.cpp b/dScripts/02_server/Map/njhub/CavePrisonCage.cpp index 8496ecfb..591c29d5 100644 --- a/dScripts/02_server/Map/njhub/CavePrisonCage.cpp +++ b/dScripts/02_server/Map/njhub/CavePrisonCage.cpp @@ -13,7 +13,10 @@ void CavePrisonCage::OnStartup(Entity* self) { return; } - auto* spawner = Game::zoneManager->GetSpawnersByName("PrisonCounterweight_0" + GeneralUtils::UTF16ToWTF8(myNum))[0]; + const auto spawners = Game::zoneManager->GetSpawnersByName("PrisonCounterweight_0" + GeneralUtils::UTF16ToWTF8(myNum)); + if (spawners.empty()) return; + + auto* spawner = spawners[0]; self->SetVar(u"CWSpawner", spawner); @@ -21,6 +24,7 @@ void CavePrisonCage::OnStartup(Entity* self) { } void CavePrisonCage::Setup(Entity* self, Spawner* spawner) { + if (!spawner) return; SpawnCounterweight(self, spawner); NiPoint3 mypos = self->GetPosition(); @@ -62,6 +66,8 @@ void CavePrisonCage::OnQuickBuildNotifyState(Entity* self, eQuickBuildState stat } void CavePrisonCage::SpawnCounterweight(Entity* self, Spawner* spawner) { + if (!spawner) return; + spawner->Reset(); auto* counterweight = spawner->Spawn(); @@ -164,7 +170,8 @@ void CavePrisonCage::OnTimerDone(Entity* self, std::string timerName) { const auto flagNum = 2020 + self->GetVarAs(u"myNumber"); // Set the flag on the builder character - builder->GetCharacter()->SetPlayerFlag(flagNum, true); + auto* const character = builder->GetCharacter(); + if (character) character->SetPlayerFlag(flagNum, true); // Setup a timer named 'VillagerEscape' to be triggered in 5 seconds self->AddTimer("VillagerEscape", 5.0f); diff --git a/dScripts/02_server/Map/njhub/LightningOrbServer.cpp b/dScripts/02_server/Map/njhub/LightningOrbServer.cpp new file mode 100644 index 00000000..e686d540 --- /dev/null +++ b/dScripts/02_server/Map/njhub/LightningOrbServer.cpp @@ -0,0 +1,12 @@ +#include "LightningOrbServer.h" + +void LightningOrbServer::OnCollisionPhantom(Entity* self, Entity* target) { + GameMessages::GetPosition playerPos; + playerPos.Send(target->GetObjectID()); + GameMessages::GetPosition selfPos; + selfPos.Send(self->GetObjectID()); + const NiPoint3 newVec((playerPos.pos.x - selfPos.pos.x) * 2.5, 15, (playerPos.pos.z - selfPos.pos.z) * 2.5); + // ahhhh aron said to put a TODO here moving platforms don't work lol. disable this so people can actually do the puzzle + // GameMessages::SendKnockback(target->GetObjectID(), self->GetObjectID(), self->GetObjectID(), 0, newVec); + // GameMessages::SendPlayFXEffect(target->GetObjectID(), -1, u"knockback", "knockback"); +} diff --git a/dScripts/02_server/Map/njhub/LightningOrbServer.h b/dScripts/02_server/Map/njhub/LightningOrbServer.h new file mode 100644 index 00000000..962d7b6a --- /dev/null +++ b/dScripts/02_server/Map/njhub/LightningOrbServer.h @@ -0,0 +1,8 @@ +#pragma once +#include "CppScripts.h" + +class LightningOrbServer : public CppScripts::Script +{ +public: + void OnCollisionPhantom(Entity* self, Entity* target) override; +}; diff --git a/dScripts/02_server/Map/njhub/OldManNPC.cpp b/dScripts/02_server/Map/njhub/OldManNPC.cpp new file mode 100644 index 00000000..8a649389 --- /dev/null +++ b/dScripts/02_server/Map/njhub/OldManNPC.cpp @@ -0,0 +1,29 @@ +#include "OldManNPC.h" + +#include "eMissionState.h" +#include "Character.h" +#include "MissionComponent.h" + +void ResetMissions(Entity& user) { + for (int32_t i = 1; i < 7; i++) { + int32_t flag = 2020 + i; + auto* const character = user.GetCharacter(); + if (character) character->SetPlayerFlag(flag, false); + } +} + +void OldManNPC::OnUse(Entity* self, Entity* user) { + const auto* const missionComponent = user->GetComponent(); + if (!missionComponent) return; + + const auto* const mission = missionComponent->GetMission(2039); + if (!mission) { + ResetMissions(*user); // shouldnt be needed for dlu but it is because the mission is null + return; + } + + const auto missionState = mission->GetMissionState(); + if (missionState == eMissionState::AVAILABLE || missionState == eMissionState::COMPLETE_AVAILABLE) { + ResetMissions(*user); + } +} diff --git a/dScripts/02_server/Map/njhub/OldManNPC.h b/dScripts/02_server/Map/njhub/OldManNPC.h new file mode 100644 index 00000000..a7924f98 --- /dev/null +++ b/dScripts/02_server/Map/njhub/OldManNPC.h @@ -0,0 +1,10 @@ +#ifndef OLDMANNPC_H +#define OLDMANNPC_H + +#include "CppScripts.h" + +class OldManNPC : public CppScripts::Script { + void OnUse(Entity* self, Entity* user) override; +}; + +#endif //!OLDMANNPC_H diff --git a/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp b/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp index 919a2b4f..be47b53b 100644 --- a/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp +++ b/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp @@ -408,7 +408,7 @@ void NjMonastryBossInstance::SummonWave(Entity* self, Entity* frakjaw) { // Stop the music for the first, fourth and fifth wave const auto wave = self->GetVar(WaveNumberVariable); - if (wave >= 1 || wave < (m_Waves.size() - 1)) { + if (wave >= 1 && wave < (m_Waves.size() - 1)) { GameMessages::SendNotifyClientObject(self->GetObjectID(), StopMusicNotification, 0, 0, LWOOBJID_EMPTY, AudioWaveAudio + std::to_string(wave - 1), UNASSIGNED_SYSTEM_ADDRESS); @@ -515,9 +515,7 @@ void NjMonastryBossInstance::FightOver(Entity* self) { info.pos = treasureChest->GetPosition(); info.rot = treasureChest->GetRotation(); info.spawnerID = self->GetObjectID(); - info.settings = { - new LDFData(u"parent_tag", self->GetObjectID()) - }; + info.settings.Insert(u"parent_tag", self->GetObjectID()); // Finally spawn a treasure chest at the correct spawn point auto* chestObject = Game::entityManager->CreateEntity(info); diff --git a/dScripts/02_server/Minigame/General/MinigameTreasureChestServer.cpp b/dScripts/02_server/Minigame/General/MinigameTreasureChestServer.cpp index 7b0abaa1..00cc68d6 100644 --- a/dScripts/02_server/Minigame/General/MinigameTreasureChestServer.cpp +++ b/dScripts/02_server/Minigame/General/MinigameTreasureChestServer.cpp @@ -4,6 +4,7 @@ #include "EntityManager.h" #include "dZoneManager.h" #include "Loot.h" +#include "dServer.h" void MinigameTreasureChestServer::OnUse(Entity* self, Entity* user) { auto* sac = self->GetComponent(); @@ -18,25 +19,9 @@ void MinigameTreasureChestServer::OnUse(Entity* self, Entity* user) { UpdatePlayer(self, user->GetObjectID()); auto* team = TeamManager::Instance()->GetTeam(user->GetObjectID()); - uint32_t activityRating = 0; - if (team != nullptr) { - for (const auto& teamMemberID : team->members) { - auto* teamMember = Game::entityManager->GetEntity(teamMemberID); - if (teamMember != nullptr) { - activityRating = CalculateActivityRating(self, teamMemberID); - - if (self->GetLOT() == frakjawChestId) activityRating = team->members.size(); - - Loot::DropActivityLoot(teamMember, self->GetObjectID(), sac->GetActivityID(), activityRating); - } - } - } else { - activityRating = CalculateActivityRating(self, user->GetObjectID()); - - if (self->GetLOT() == frakjawChestId) activityRating = 1; - - Loot::DropActivityLoot(user, self->GetObjectID(), sac->GetActivityID(), activityRating); - } + uint32_t activityRating = CalculateActivityRating(self, user->GetObjectID()); + if (self->GetLOT() == frakjawChestId || Game::server->GetZoneID() == 1204) activityRating = team != nullptr ? team->members.size() : 1; + Loot::DropActivityLoot(user, self->GetObjectID(), sac->GetActivityID(), activityRating); sac->PlayerRemove(user->GetObjectID()); diff --git a/dScripts/02_server/Objects/CMakeLists.txt b/dScripts/02_server/Objects/CMakeLists.txt index 1b96d79f..da7e9b60 100644 --- a/dScripts/02_server/Objects/CMakeLists.txt +++ b/dScripts/02_server/Objects/CMakeLists.txt @@ -1,4 +1,11 @@ +add_subdirectory(Hatchlings) + set(DSCRIPTS_SOURCES_02_SERVER_OBJECTS "AgSurvivalBuffStation.cpp" - "StinkyFishTarget.cpp" - PARENT_SCOPE) + "StinkyFishTarget.cpp") + +foreach(file ${DSCRIPTS_SOURCES_02_SERVER_OBJECTS_HATCHLINGS}) + set(DSCRIPTS_SOURCES_02_SERVER_OBJECTS ${DSCRIPTS_SOURCES_02_SERVER_OBJECTS} "Hatchlings/${file}") +endforeach() + +set(DSCRIPTS_SOURCES_02_SERVER_OBJECTS ${DSCRIPTS_SOURCES_02_SERVER_OBJECTS} PARENT_SCOPE) diff --git a/dScripts/02_server/Objects/Hatchlings/CMakeLists.txt b/dScripts/02_server/Objects/Hatchlings/CMakeLists.txt new file mode 100644 index 00000000..0655ec64 --- /dev/null +++ b/dScripts/02_server/Objects/Hatchlings/CMakeLists.txt @@ -0,0 +1,3 @@ +set(DSCRIPTS_SOURCES_02_SERVER_OBJECTS_HATCHLINGS + "HatchlingPets.cpp" + PARENT_SCOPE) diff --git a/dScripts/02_server/Objects/Hatchlings/HatchlingPets.cpp b/dScripts/02_server/Objects/Hatchlings/HatchlingPets.cpp new file mode 100644 index 00000000..bc29ae8d --- /dev/null +++ b/dScripts/02_server/Objects/Hatchlings/HatchlingPets.cpp @@ -0,0 +1,83 @@ +#include "HatchlingPets.h" + +#include "Entity.h" +#include "MovementAIComponent.h" + +void HatchlingPets::OnStartup(Entity* self) { + self->SetVar(u"follow", false); + + self->SetProximityRadius(5, "StopFollow"); + self->SetProximityRadius(15, "Wander"); + self->SetProximityRadius(50, "Teleport"); + + Wander(*self, *self->GetOwner()); + self->AddComponent(-1, MovementAIInfo{ .wanderRadius = 2.5f }); +} + +void HatchlingPets::OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status) { + auto* const parent = self->GetOwner(); + if (!entering || !entering->IsPlayer() || parent->GetObjectID() != entering->GetObjectID()) return; + + if (name == "StopFollow") { + if (status == "ENTER") { + if (self->GetVar(u"follow")) { + const auto randomWanderTime = GeneralUtils::GenerateRandomNumber(4, 9); + self->AddTimer("StartWander", randomWanderTime); + // stop following the player + auto* const movementAI = self->GetComponent(); + if (movementAI) { + movementAI->Stop(); + movementAI->FollowTarget(LWOOBJID_EMPTY); + } + self->SetVar(u"follow", false); + } + } + } else if (name == "Wander") { + if (status == "LEAVE") { + self->CancelAllTimers(); + // follow the player + auto* const movementAI = self->GetComponent(); + if (movementAI) { + movementAI->Stop(); + movementAI->FollowTarget(entering->GetObjectID()); + } + self->SetVar(u"follow", true); + } + } else if (name == "Teleport") { + if (status == "LEAVE") { + // stop following the player + auto* const movementAI = self->GetComponent(); + if (movementAI) { + movementAI->Stop(); + movementAI->FollowTarget(LWOOBJID_EMPTY); + } + GameMessages::GetPosition getPos; + getPos.Send(entering->GetObjectID()); + getPos.pos.z += 5.0f; + self->SetPosition(getPos.pos); + Game::entityManager->SerializeEntity(*self); + } + } +} + +void HatchlingPets::OnTimerDone(Entity* self, std::string timerName) { + if (timerName == "StartWander") { + Wander(*self, *self->GetOwner()); + } +} + +void HatchlingPets::Wander(Entity& self, Entity& player) { + GameMessages::GetPosition getPos; + if (!getPos.Send(player.GetObjectID())) { + LOG("Failed to get position for %llu", player.GetObjectID()); + return; + } + + const auto xWander = GeneralUtils::GenerateRandomNumber(0, 20) - 10.0f; + const auto zWander = GeneralUtils::GenerateRandomNumber(0, 20) - 10.0f; + getPos.pos.x += xWander; + getPos.pos.z += zWander; + auto* const movementAI = self.GetComponent(); + if (movementAI) movementAI->SetDestination(getPos.pos); + self.AddTimer("StartWander", GeneralUtils::GenerateRandomNumber(4, 9)); +} diff --git a/dScripts/02_server/Objects/Hatchlings/HatchlingPets.h b/dScripts/02_server/Objects/Hatchlings/HatchlingPets.h new file mode 100644 index 00000000..0dd2d5b6 --- /dev/null +++ b/dScripts/02_server/Objects/Hatchlings/HatchlingPets.h @@ -0,0 +1,14 @@ +#ifndef HATCHLINGPETS_H +#define HATCHLINGPETS_H + +#include "CppScripts.h" +#include "NiPoint3.h" + +class HatchlingPets : public CppScripts::Script { + void OnStartup(Entity* self) override; + void OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status) override; + void OnTimerDone(Entity* self, std::string timerName) override; + void Wander(Entity& self, Entity& player); +}; + +#endif //!HATCHLINGPETS_H diff --git a/dScripts/02_server/Objects/StinkyFishTarget.cpp b/dScripts/02_server/Objects/StinkyFishTarget.cpp index 9840235d..ae595b89 100644 --- a/dScripts/02_server/Objects/StinkyFishTarget.cpp +++ b/dScripts/02_server/Objects/StinkyFishTarget.cpp @@ -21,9 +21,7 @@ void StinkyFishTarget::OnSkillEventFired(Entity* self, Entity* caster, const std entityInfo.pos = self->GetPosition(); entityInfo.rot = self->GetRotation(); entityInfo.spawnerID = self->GetObjectID(); - entityInfo.settings = { - new LDFData(u"no_timed_spawn", true) - }; + entityInfo.settings.Insert(u"no_timed_spawn", true); auto* fish = Game::entityManager->CreateEntity(entityInfo); Game::entityManager->ConstructEntity(fish); diff --git a/dScripts/ActivityManager.cpp b/dScripts/ActivityManager.cpp index 060fda08..35459538 100644 --- a/dScripts/ActivityManager.cpp +++ b/dScripts/ActivityManager.cpp @@ -7,6 +7,7 @@ #include "Logger.h" #include "Loot.h" #include "ShootingGalleryComponent.h" +#include "RacingControlComponent.h" bool ActivityManager::IsPlayerInActivity(Entity* self, LWOOBJID playerID) { const auto* sac = self->GetComponent(); @@ -99,6 +100,8 @@ bool ActivityManager::TakeActivityCost(const Entity* self, const LWOOBJID player activityComponent = self->GetComponent(); } + if (!activityComponent) return false; + auto* player = Game::entityManager->GetEntity(playerID); if (player == nullptr) return false; @@ -111,7 +114,7 @@ uint32_t ActivityManager::CalculateActivityRating(Entity* self, const LWOOBJID p if (sac == nullptr) return 0; - return sac->GetInstance(playerID)->GetParticipants().size(); + return sac->GetInstance(playerID).GetParticipants().size(); } uint32_t ActivityManager::GetActivityID(const Entity* self) { diff --git a/dScripts/BaseConsoleTeleportServer.cpp b/dScripts/BaseConsoleTeleportServer.cpp index d4a49299..7392f759 100644 --- a/dScripts/BaseConsoleTeleportServer.cpp +++ b/dScripts/BaseConsoleTeleportServer.cpp @@ -94,9 +94,12 @@ void BaseConsoleTeleportServer::TransferPlayer(Entity* self, Entity* player, int const auto& teleportZone = self->GetVar(u"transferZoneID"); + auto* const character = player->GetCharacter(); + if (character && self->HasVar(u"spawnPoint")) character->SetTargetScene(self->GetVarAsString(u"spawnPoint")); + auto* characterComponent = player->GetComponent(); - if (characterComponent) characterComponent->SendToZone(std::stoi(GeneralUtils::UTF16ToWTF8(teleportZone))); + if (characterComponent) characterComponent->SendToZone(GeneralUtils::TryParse(GeneralUtils::UTF16ToWTF8(teleportZone), 0)); UpdatePlayerTable(self, player, false); } diff --git a/dScripts/BasePropertyServer.cpp b/dScripts/BasePropertyServer.cpp index 9048adeb..69b839f3 100644 --- a/dScripts/BasePropertyServer.cpp +++ b/dScripts/BasePropertyServer.cpp @@ -127,12 +127,15 @@ void BasePropertyServer::BasePlayerLoaded(Entity* self, Entity* player) { if (player->GetObjectID() != propertyOwner) return; } else { - const auto defeatedFlag = player->GetCharacter()->GetPlayerFlag(self->GetVar(defeatedProperyFlag)); + GameMessages::GetFlag flagMsg; + flagMsg.target = player->GetObjectID(); + flagMsg.flagID = self->GetVar(defeatedProperyFlag); + flagMsg.Send(); self->SetNetworkVar(UnclaimedVariable, true); self->SetVar(PlayerIDVariable, player->GetObjectID()); - if (!defeatedFlag) { + if (!flagMsg.flag) { StartMaelstrom(self, player); SpawnSpots(self); GameMessages::SendPlay2DAmbientSound(player, GUIDMaelstrom); diff --git a/dScripts/BaseRandomServer.cpp b/dScripts/BaseRandomServer.cpp index 17817d7a..4b67a5e3 100644 --- a/dScripts/BaseRandomServer.cpp +++ b/dScripts/BaseRandomServer.cpp @@ -124,21 +124,23 @@ void BaseRandomServer::NotifySpawnerOfDeath(Entity* self, Spawner* spawner) { return; } - const auto& sectionName = spawnerName.substr(0, spawnerName.size() - 7); + if (spawnerName.size() >= 7) { + const auto& sectionName = spawnerName.substr(0, spawnerName.size() - 7); - const auto variableName = u"mobsDead" + GeneralUtils::ASCIIToUTF16(sectionName); + const auto variableName = u"mobsDead" + GeneralUtils::ASCIIToUTF16(sectionName); - auto mobDeathCount = self->GetVar(variableName); + auto mobDeathCount = self->GetVar(variableName); - mobDeathCount++; + mobDeathCount++; - if (mobDeathCount >= mobDeathResetNumber) { - const auto& zoneInfo = GeneralUtils::SplitString(sectionName, '_'); + if (mobDeathCount >= mobDeathResetNumber) { + const auto& zoneInfo = GeneralUtils::SplitString(sectionName, '_'); - SpawnSection(self, sectionName, sectionMultipliers[zoneInfo[sectionIDConst - 1]]); + SpawnSection(self, sectionName, sectionMultipliers[zoneInfo[sectionIDConst - 1]]); + } + + self->SetVar(variableName, mobDeathCount); } - - self->SetVar(variableName, mobDeathCount); } void BaseRandomServer::NamedEnemyDeath(Entity* self, Spawner* spawner) { diff --git a/dScripts/BaseRandomServer.h b/dScripts/BaseRandomServer.h index bc5d6b21..daa8bd4c 100644 --- a/dScripts/BaseRandomServer.h +++ b/dScripts/BaseRandomServer.h @@ -37,7 +37,7 @@ public: void NamedTimerDone(Entity* self, const std::string& timerName); protected: - std::vector namedMobs = { + const std::vector namedMobs = { 11988, // Ronin 11984, // Spiderling 12654, // Horsemen diff --git a/dScripts/BaseSurvivalServer.cpp b/dScripts/BaseSurvivalServer.cpp index 07890e71..f71991ba 100644 --- a/dScripts/BaseSurvivalServer.cpp +++ b/dScripts/BaseSurvivalServer.cpp @@ -24,7 +24,8 @@ void BaseSurvivalServer::BasePlayerLoaded(Entity* self, Entity* player) { if (waitingIter != state.waitingPlayers.end() || playersIter != state.players.end()) { auto* characterComponent = player->GetComponent(); - if (characterComponent) characterComponent->SendToZone(player->GetCharacter()->GetLastNonInstanceZoneID()); + const auto* const character = player->GetCharacter(); + if (characterComponent && character) characterComponent->SendToZone(character->GetLastNonInstanceZoneID()); return; } diff --git a/dScripts/CppScripts.cpp b/dScripts/CppScripts.cpp index 4c0ba749..a6c3c00d 100644 --- a/dScripts/CppScripts.cpp +++ b/dScripts/CppScripts.cpp @@ -166,6 +166,7 @@ #include "AgSalutingNpcs.h" #include "BossSpiderQueenEnemyServer.h" #include "RockHydrantSmashable.h" +#include "HatchlingPets.h" // Misc Scripts #include "ExplodingAsset.h" @@ -273,11 +274,13 @@ #include "MonCoreNookDoors.h" #include "MonCoreSmashableDoors.h" #include "FlameJetServer.h" +#include "LightningOrbServer.h" #include "BurningTile.h" #include "NjEarthDragonPetServer.h" #include "NjEarthPetServer.h" #include "NjDragonEmblemChestServer.h" #include "NjNyaMissionitems.h" +#include "OldManNPC.h" // Scripted equipment #include "AnvilOfArmor.h" @@ -338,6 +341,8 @@ #include "ImaginationBackPack.h" #include "NsWinterRaceServer.h" +#include "RegisterWithZoneControl.h" + #include #include #include @@ -420,6 +425,7 @@ namespace { {"scripts\\ai\\NS\\L_NS_CONCERT_INSTRUMENT_QB.lua", []() {return new NsConcertInstrument();}}, {"scripts\\ai\\NS\\L_NS_JONNY_FLAG_MISSION_SERVER.lua", []() {return new NsJohnnyMissionServer();}}, {"scripts\\02_server\\Objects\\L_STINKY_FISH_TARGET.lua", []() {return new StinkyFishTarget();}}, + {"scripts\\02_server\\Objects\\Hatchlings\\L_HATCHLING_PETS.lua", []() {return new HatchlingPets();}}, {"scripts\\zone\\PROPERTY\\NS\\L_ZONE_NS_PROPERTY.lua", []() {return new ZoneNsProperty();}}, {"scripts\\02_server\\Map\\Property\\NS_Med\\L_ZONE_NS_MED_PROPERTY.lua", []() {return new ZoneNsMedProperty();}}, {"scripts\\02_server\\Map\\NS\\L_NS_TOKEN_CONSOLE_SERVER.lua", []() {return new NsTokenConsoleServer();}}, @@ -623,11 +629,13 @@ namespace { {"scripts\\02_server\\Map\\njhub\\L_MON_CORE_SMASHABLE_DOORS.lua", []() {return new MonCoreSmashableDoors();}}, {"scripts\\02_server\\Map\\njhub\\L_MON_CORE_SMASHABLE_DOORS.lua", []() {return new MonCoreSmashableDoors();}}, {"scripts\\02_server\\Map\\njhub\\L_FLAME_JET_SERVER.lua", []() {return new FlameJetServer();}}, + {"scripts\\02_server\\Map\\njhub\\L_LIGHTNING_ORB_SERVER.lua", []() {return new LightningOrbServer();}}, {"scripts\\02_server\\Map\\njhub\\L_BURNING_TILE.lua", []() {return new BurningTile();}}, {"scripts\\02_server\\Map\\njhub\\L_SPAWN_EARTH_PET_SERVER.lua", []() {return new NjEarthDragonPetServer();}}, {"scripts\\02_server\\Map\\njhub\\L_EARTH_PET_SERVER.lua", []() {return new NjEarthPetServer();}}, {"scripts\\02_server\\Map\\njhub\\L_DRAGON_EMBLEM_CHEST_SERVER.lua", []() {return new NjDragonEmblemChestServer();}}, {"scripts\\02_server\\Map\\njhub\\L_NYA_MISSION_ITEMS.lua", []() {return new NjNyaMissionitems();}}, + {"scripts\\02_server\\Map\\njhub\\L_OLD_MAN_NPC.lua", []() {return new OldManNPC();}}, //DLU {"scripts\\02_server\\DLU\\DLUVanityTeleportingObject.lua", []() {return new DLUVanityTeleportingObject();}}, @@ -661,6 +669,7 @@ namespace { //WBL {"scripts\\zone\\LUPs\\WBL_generic_zone.lua", []() {return new WblGenericZone();}}, + {"scripts\\zone\\LUPs\\Moonbase Intro\\MOONBASE-INTRO_INTRO_CINEMATIC.lua", []() {return new WblGenericZone();}}, //Alpha {"scripts\\ai\\FV\\L_TRIGGER_GAS.lua", []() {return new TriggerGas();}}, @@ -706,7 +715,8 @@ namespace { {"scripts\\ai\\RACING\\OBJECTS\\VEHICLE_DEATH_TRIGGER_WATER_SERVER.lua", []() {return new VehicleDeathTriggerWaterServer();}}, {"scripts\\equipmenttriggers\\L_TRIAL_FACTION_ARMOR_SERVER.lua", []() {return new TrialFactionArmorServer();}}, {"scripts\\equipmenttriggers\\ImaginationBackPack.lua", []() {return new ImaginationBackPack();}}, - + {"scripts\\ai\\MINIGAME\\SG_GF\\SERVER\\SG_CANNON_INSTANCE_ACTOR.lua", [](){return new RegisterWithZoneControl();}}, + {"scripts\\ai\\MINIGAME\\SG_GF\\SERVER\\SG_CANNON_INSTANCE_EFFECT.lua", [](){return new RegisterWithZoneControl();}}, }; std::set g_ExcludedScripts = { @@ -730,6 +740,11 @@ namespace { "scripts\\zone\\LUPs\\RobotCity Intro\\WBL_RCIntro_InfectedCitizen.lua", "scripts\\ai\\MINIGAME\\SIEGE\\OBJECTS\\ATTACKER_BOUNCER_SERVER.lua", "scripts\\ai\\AG\\L_AG_ZONE_PLAYER.lua", + "scripts\\ai\\GENERAL\\L_NPC_GENERIC_MOVEMENT.lua", // Really old alpha script + "scripts\\zone\\LUPs\\DeepFreeze Intro\\WBL_Enemy_Beaver.lua", // Really old alpha script + "scripts\\ai\\GENERAL\\L_NPC_GENERIC_WANDER_SMALL.lua", // Really old alpha script + "scripts\\ai\\NP\\L_NPC_NP_OLD_MAN_SHERLAND.lua", // This NPC doesn't even exist in modern crux, the only place this is used... + "scripts\\02_server\\Map\\General\\L_SIMPLE_MOVER_SWITCH.lua", // This platform does not exist even when moved manually on a client }; }; @@ -743,7 +758,8 @@ CppScripts::Script* const CppScripts::GetScript(Entity* parent, const std::strin Script* script = itrTernary != scriptLoader.cend() ? itrTernary->second() : &InvalidToReturn; if (script == &InvalidToReturn && !scriptName.empty() && !g_ExcludedScripts.contains(scriptName)) { - LOG_DEBUG("LOT %i attempted to load CppScript for '%s', but returned InvalidScript.", parent->GetLOT(), scriptName.c_str()); + const auto [x, y, z] = parent->GetPosition(); + LOG_DEBUG("LOT %i at %f %f %f attempted to load CppScript for '%s', but returned InvalidScript.", parent->GetLOT(), x, y, z, scriptName.c_str()); } g_Scripts[scriptName] = script; diff --git a/dScripts/CppScripts.h b/dScripts/CppScripts.h index eb483a11..5ba4cee2 100644 --- a/dScripts/CppScripts.h +++ b/dScripts/CppScripts.h @@ -383,6 +383,8 @@ namespace CppScripts { * @param fire The child info */ virtual void OnChildLoaded(Entity& self, GameMessages::ChildLoaded& childLoaded) {}; + + virtual void OnChildRemoved(Entity& self, GameMessages::ChildRemoved& childRemoved) {}; }; Script* const GetScript(Entity* parent, const std::string& scriptName); diff --git a/dScripts/NtFactionSpyServer.cpp b/dScripts/NtFactionSpyServer.cpp index 532c128a..7414bb34 100644 --- a/dScripts/NtFactionSpyServer.cpp +++ b/dScripts/NtFactionSpyServer.cpp @@ -73,7 +73,7 @@ void NtFactionSpyServer::OnCinematicUpdate(Entity* self, Entity* sender, eCinema // Make sure we have a path of type _ if (pathSplit.size() >= 2) { auto pathRoot = pathSplit.at(0); - auto pathIndex = std::stoi(GeneralUtils::UTF16ToWTF8(pathSplit.at(1))) - 1; + auto pathIndex = GeneralUtils::TryParse(GeneralUtils::UTF16ToWTF8(pathSplit.at(1)), -1) - 1; const auto& dialogueTable = self->GetVar>(m_SpyDialogueTableVariable); // Make sure we're listening to the root we're interested in diff --git a/dScripts/SpawnPetBaseServer.cpp b/dScripts/SpawnPetBaseServer.cpp index 395a20c2..04a24ae6 100644 --- a/dScripts/SpawnPetBaseServer.cpp +++ b/dScripts/SpawnPetBaseServer.cpp @@ -26,12 +26,10 @@ void SpawnPetBaseServer::OnUse(Entity* self, Entity* user) { info.rot = spawner->GetRotation(); info.lot = self->GetVar(u"petLOT"); info.spawnerID = self->GetObjectID(); - info.settings = { - new LDFData(u"tamer", user->GetObjectID()), - new LDFData(u"groupID", petType + (GeneralUtils::to_u16string(user->GetObjectID())) + u";" + petType + u"s"), - new LDFData(u"spawnAnim", self->GetVar(u"spawnAnim")), - new LDFData(u"spawnTimer", 1.0f) - }; + info.settings.Insert(u"tamer", user->GetObjectID()); + info.settings.Insert(u"groupID", petType + (GeneralUtils::to_u16string(user->GetObjectID())) + u";" + petType + u"s"); + info.settings.Insert(u"spawnAnim", self->GetVar(u"spawnAnim")); + info.settings.Insert(u"spawnTimer", 1.0f); auto* pet = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(pet); @@ -57,7 +55,7 @@ bool SpawnPetBaseServer::CheckNumberOfPets(Entity* self, Entity* user) { if (petID.empty()) continue; - const auto* spawnedPet = Game::entityManager->GetEntity(std::stoull(petID)); + const auto* spawnedPet = Game::entityManager->GetEntity(GeneralUtils::TryParse(petID, LWOOBJID_EMPTY)); if (spawnedPet == nullptr) continue; diff --git a/dScripts/ai/AG/AgFans.cpp b/dScripts/ai/AG/AgFans.cpp index 15f2ed0f..060d29de 100644 --- a/dScripts/ai/AG/AgFans.cpp +++ b/dScripts/ai/AG/AgFans.cpp @@ -24,8 +24,11 @@ void AgFans::OnStartup(Entity* self) { } void AgFans::ToggleFX(Entity* self, bool hit) { - std::string fanGroup = self->GetGroups()[0]; - std::vector fanVolumes = Game::entityManager->GetEntitiesInGroup(fanGroup); + const auto groups = self->GetGroups(); + std::string fanGroup = groups.empty() ? "" : groups[0]; + const auto fanVolumes = Game::entityManager->GetEntitiesInGroup(fanGroup); + const auto fxObjs = Game::entityManager->GetEntitiesInGroup(fanGroup + "fx"); + auto* const fxObj = fxObjs.empty() ? nullptr : fxObjs[0]; auto* renderComponent = static_cast(self->GetComponent(eReplicaComponentType::RENDER)); @@ -47,8 +50,7 @@ void AgFans::ToggleFX(Entity* self, bool hit) { volumePhys->SetPhysicsEffectActive(false); Game::entityManager->SerializeEntity(volume); if (!hit) { - Entity* fxObj = Game::entityManager->GetEntitiesInGroup(fanGroup + "fx")[0]; - RenderComponent::PlayAnimation(fxObj, u"trigger"); + if (fxObj) RenderComponent::PlayAnimation(fxObj, u"trigger"); } } } else if (!self->GetVar(u"on") && self->GetVar(u"alive")) { @@ -63,8 +65,7 @@ void AgFans::ToggleFX(Entity* self, bool hit) { volumePhys->SetPhysicsEffectActive(true); Game::entityManager->SerializeEntity(volume); if (!hit) { - Entity* fxObj = Game::entityManager->GetEntitiesInGroup(fanGroup + "fx")[0]; - RenderComponent::PlayAnimation(fxObj, u"idle"); + if (fxObj) RenderComponent::PlayAnimation(fxObj, u"idle"); } } } diff --git a/dScripts/ai/FV/FvBrickPuzzleServer.cpp b/dScripts/ai/FV/FvBrickPuzzleServer.cpp index f8601e3f..77fa0d32 100644 --- a/dScripts/ai/FV/FvBrickPuzzleServer.cpp +++ b/dScripts/ai/FV/FvBrickPuzzleServer.cpp @@ -6,6 +6,7 @@ void FvBrickPuzzleServer::OnStartup(Entity* self) { const auto myGroup = GeneralUtils::UTF16ToWTF8(self->GetVar(u"spawner_name")); + if (myGroup.size() <= 10) return; const auto pipeNum = GeneralUtils::TryParse(myGroup.substr(10, 1)); if (!pipeNum) return; @@ -17,6 +18,7 @@ void FvBrickPuzzleServer::OnStartup(Entity* self) { void FvBrickPuzzleServer::OnDie(Entity* self, Entity* killer) { const auto myGroup = GeneralUtils::UTF16ToWTF8(self->GetVar(u"spawner_name")); + if (myGroup.size() <= 10) return; const auto pipeNum = GeneralUtils::TryParse(myGroup.substr(10, 1)); if (!pipeNum) return; diff --git a/dScripts/ai/FV/FvFacilityBrick.cpp b/dScripts/ai/FV/FvFacilityBrick.cpp index 26c07647..1402991c 100644 --- a/dScripts/ai/FV/FvFacilityBrick.cpp +++ b/dScripts/ai/FV/FvFacilityBrick.cpp @@ -9,9 +9,12 @@ void FvFacilityBrick::OnStartup(Entity* self) { } void FvFacilityBrick::OnNotifyObject(Entity* self, Entity* sender, const std::string& name, int32_t param1, int32_t param2) { - auto* brickSpawner = Game::zoneManager->GetSpawnersByName("ImaginationBrick")[0]; - auto* bugSpawner = Game::zoneManager->GetSpawnersByName("MaelstromBug")[0]; - auto* canisterSpawner = Game::zoneManager->GetSpawnersByName("BrickCanister")[0]; + const auto brickObjs = Game::zoneManager->GetSpawnersByName("ImaginationBrick"); + auto* const brickSpawner = brickObjs.empty() ? nullptr : brickObjs[0]; + const auto bugObjs = Game::zoneManager->GetSpawnersByName("MaelstromBug"); + auto* const bugSpawner = bugObjs.empty() ? nullptr : bugObjs[0]; + const auto canisterObjs = Game::zoneManager->GetSpawnersByName("BrickCanister"); + auto* const canisterSpawner = canisterObjs.empty() ? nullptr : canisterObjs[0]; if (name == "ConsoleLeftUp") { GameMessages::SendStopFXEffect(self, true, "LeftPipeOff"); @@ -62,7 +65,7 @@ void FvFacilityBrick::OnNotifyObject(Entity* self, Entity* sender, const std::st canisterSpawner->Reset(); canisterSpawner->Deactivate(); } else if (self->GetVar(u"ConsoleLEFTActive") || self->GetVar(u"ConsoleRIGHTActive")) { - brickSpawner->Activate(); + if (brickSpawner) brickSpawner->Activate(); auto* object = Game::entityManager->GetEntitiesInGroup("Brick")[0]; @@ -70,17 +73,25 @@ void FvFacilityBrick::OnNotifyObject(Entity* self, Entity* sender, const std::st GameMessages::SendStopFXEffect(object, true, "bluebrick"); } - bugSpawner->Reset(); - bugSpawner->Deactivate(); + if (bugSpawner) { + bugSpawner->Reset(); + bugSpawner->Deactivate(); + } - canisterSpawner->Reset(); - canisterSpawner->Activate(); + if (canisterSpawner) { + canisterSpawner->Reset(); + canisterSpawner->Activate(); + } } else { - brickSpawner->Reset(); - brickSpawner->Deactivate(); + if (brickSpawner) { + brickSpawner->Reset(); + brickSpawner->Deactivate(); + } - bugSpawner->Reset(); - bugSpawner->Activate(); + if (bugSpawner) { + bugSpawner->Reset(); + bugSpawner->Activate(); + } } } diff --git a/dScripts/ai/FV/FvPandaSpawnerServer.cpp b/dScripts/ai/FV/FvPandaSpawnerServer.cpp index bc9f1c8a..e02bcb55 100644 --- a/dScripts/ai/FV/FvPandaSpawnerServer.cpp +++ b/dScripts/ai/FV/FvPandaSpawnerServer.cpp @@ -38,10 +38,8 @@ void FvPandaSpawnerServer::OnCollisionPhantom(Entity* self, Entity* target) { info.spawnerID = target->GetObjectID(); info.pos = self->GetPosition(); info.lot = 5643; - info.settings = { - new LDFData(u"tamer", target->GetObjectID()), - new LDFData(u"groupID", u"panda" + (GeneralUtils::to_u16string(target->GetObjectID())) + u";pandas") - }; + info.settings.Insert(u"tamer", target->GetObjectID()); + info.settings.Insert(u"groupID", u"panda" + (GeneralUtils::to_u16string(target->GetObjectID())) + u";pandas"); auto* panda = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(panda); diff --git a/dScripts/ai/GF/GfBanana.cpp b/dScripts/ai/GF/GfBanana.cpp index 93741d24..51c9f230 100644 --- a/dScripts/ai/GF/GfBanana.cpp +++ b/dScripts/ai/GF/GfBanana.cpp @@ -55,36 +55,20 @@ void GfBanana::OnHit(Entity* self, Entity* attacker) { return; } + bananaEntity->Smash(LWOOBJID_EMPTY, eKillType::SILENT); - bananaEntity->SetPosition(bananaEntity->GetPosition() - NiPoint3Constant::UNIT_Y * 8); - - auto* bananaDestroyable = bananaEntity->GetComponent(); - - bananaDestroyable->SetHealth(0); - - bananaDestroyable->Smash(attacker->GetObjectID()); - - /* - auto position = self->GetPosition(); const auto rotation = self->GetRotation(); - - position.y += 12; - position.x -= rotation.GetRightVector().x * 5; - position.z -= rotation.GetRightVector().z * 5; - - EntityInfo info {}; - - info.pos = position; - info.rot = rotation; + EntityInfo info{}; info.lot = 6718; + info.pos = self->GetPosition(); + info.pos.y += 12; + info.pos.x -= QuatUtils::Right(rotation).x * 5; + info.pos.z -= QuatUtils::Right(rotation).z * 5; + info.rot = rotation; info.spawnerID = self->GetObjectID(); - - auto* entity = Game::entityManager->CreateEntity(info); - - Game::entityManager->ConstructEntity(entity, UNASSIGNED_SYSTEM_ADDRESS); - */ - - Game::entityManager->SerializeEntity(self); + info.settings.Insert(u"motionType", 5); + auto* const newEn = Game::entityManager->CreateEntity(info, nullptr, self); + Game::entityManager->ConstructEntity(newEn); } void GfBanana::OnTimerDone(Entity* self, std::string timerName) { diff --git a/dScripts/ai/GF/GfBananaCluster.cpp b/dScripts/ai/GF/GfBananaCluster.cpp index 6e5e91db..f461c761 100644 --- a/dScripts/ai/GF/GfBananaCluster.cpp +++ b/dScripts/ai/GF/GfBananaCluster.cpp @@ -1,5 +1,9 @@ #include "GfBananaCluster.h" #include "Entity.h" +#include "dpWorld.h" +#include "dNavMesh.h" +#include "Loot.h" +#include "DestroyableComponent.h" void GfBananaCluster::OnStartup(Entity* self) { self->AddTimer("startup", 100); @@ -10,3 +14,21 @@ void GfBananaCluster::OnTimerDone(Entity* self, std::string timerName) { self->ScheduleKillAfterUpdate(nullptr); } } + +// Hack in banana loot dropping from tree area since it seemed to do that in live for some reason +void GfBananaCluster::OnHit(Entity* self, Entity* attacker) { + auto* parentEntity = self->GetParentEntity(); + GameMessages::GetPosition posMsg{}; + if (parentEntity) { + posMsg.target = parentEntity->GetObjectID(); + } + posMsg.Send(); + + const auto rotation = parentEntity ? parentEntity->GetRotation() : self->GetRotation(); + + if (dpWorld::GetNavMesh()) posMsg.pos.y = dpWorld::GetNavMesh()->GetHeightAtPoint(posMsg.pos) + 3.0f; + else posMsg.pos = posMsg.pos - (NiPoint3Constant::UNIT_Y * 8); + posMsg.pos.x -= QuatUtils::Right(rotation).x * 5; + posMsg.pos.z -= QuatUtils::Right(rotation).z * 5; + self->SetPosition(posMsg.pos); +} diff --git a/dScripts/ai/GF/GfBananaCluster.h b/dScripts/ai/GF/GfBananaCluster.h index 81bb8b0b..ceff708c 100644 --- a/dScripts/ai/GF/GfBananaCluster.h +++ b/dScripts/ai/GF/GfBananaCluster.h @@ -7,4 +7,5 @@ public: void OnStartup(Entity* self) override; void OnTimerDone(Entity* self, std::string timerName) override; + void OnHit(Entity* self, Entity* attacker) override; }; diff --git a/dScripts/ai/GF/PetDigBuild.cpp b/dScripts/ai/GF/PetDigBuild.cpp index 9caf2f8c..b720907e 100644 --- a/dScripts/ai/GF/PetDigBuild.cpp +++ b/dScripts/ai/GF/PetDigBuild.cpp @@ -13,14 +13,12 @@ void PetDigBuild::OnQuickBuildComplete(Entity* self, Entity* target) { info.pos = pos; info.rot = self->GetRotation(); info.spawnerID = self->GetSpawnerID(); - info.settings = { - new LDFData(u"builder", target->GetObjectID()), - new LDFData(u"X", self->GetObjectID()) - }; + info.settings.Insert(u"builder", target->GetObjectID()); + info.settings.Insert(u"X", self->GetObjectID()); if (!flagNumber.empty()) { info.lot = 7410; // Normal GF treasure - info.settings.push_back(new LDFData(u"groupID", u"Flag" + flagNumber)); + info.settings.Insert(u"groupID", u"Flag" + flagNumber); } else { auto* missionComponent = target->GetComponent(); if (missionComponent != nullptr && missionComponent->GetMissionState(746) == eMissionState::ACTIVE) { diff --git a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp index e97316e5..473cd7c5 100644 --- a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp +++ b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp @@ -241,7 +241,7 @@ void SGCannon::GameOverTimerFunc(Entity* self) { void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) { if (self->GetVar(GameStartedVariable)) { - const auto spawnNumber = static_cast(std::stoi(name.substr(7))); + const auto spawnNumber = GeneralUtils::TryParse(name.substr(7), 0); const auto& activeSpawns = self->GetVar>(ActiveSpawnsVariable); if (activeSpawns.size() <= spawnNumber) { LOG_DEBUG("Trying to spawn %i when spawns size is only %i", spawnNumber, activeSpawns.size()); @@ -261,15 +261,13 @@ void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) { info.spawnerID = self->GetObjectID(); info.pos = path->pathWaypoints[0].position; - info.settings = { - new LDFData(u"SpawnData", toSpawn), - new LDFData(u"custom_script_server", "scripts/ai/ACT/SG_TARGET.lua"), // this script is never loaded - new LDFData(u"custom_script_client", "scripts/client/ai/SG_TARGET_CLIENT.lua"), - new LDFData(u"attached_path", path->pathName), - new LDFData(u"attached_path_start", 0), - new LDFData(u"groupID", u"SGEnemy"), - new LDFData(u"wave", self->GetVar(ThisWaveVariable)), - }; + info.settings.Insert(u"SpawnData", toSpawn); + info.settings.Insert(u"custom_script_server", "scripts/ai/ACT/SG_TARGET.lua"); // this script is never loaded; + info.settings.Insert(u"custom_script_client", "scripts/client/ai/SG_TARGET_CLIENT.lua"); + info.settings.Insert(u"attached_path", path->pathName); + info.settings.Insert(u"attached_path_start", 0); + info.settings.Insert(u"groupID", u"SGEnemy"); + info.settings.Insert(u"wave", self->GetVar(ThisWaveVariable)); auto* enemy = Game::entityManager->CreateEntity(info, nullptr, self); @@ -621,10 +619,11 @@ void SGCannon::OnActivityNotify(Entity* self, GameMessages::ActivityNotify& noti if (!self->GetVar(GameStartedVariable)) return; const auto& params = notify.notification; - if (params.empty()) return; + const auto itr = params.values.find(u"shot_done"); + if (itr == params.values.end()) return; - const auto& param = params[0]; - if (param->GetValueType() != LDF_TYPE_S32 || param->GetKey() != u"shot_done") return; + const auto& param = itr->second; + if (param->GetValueType() != LDF_TYPE_S32) return; const auto superChargeShotDone = static_cast*>(param.get())->GetValue() == GetConstants().cannonSuperChargeSkill; diff --git a/dScripts/ai/NS/NsConcertQuickBuild.cpp b/dScripts/ai/NS/NsConcertQuickBuild.cpp index 7fd3f125..2278eb5c 100644 --- a/dScripts/ai/NS/NsConcertQuickBuild.cpp +++ b/dScripts/ai/NS/NsConcertQuickBuild.cpp @@ -41,7 +41,9 @@ void NsConcertQuickBuild::OnStartup(Entity* self) { return; // Get the manager of the crate of this quick build - const auto groupNumber = std::stoi(splitGroup.at(3)); + const auto groupNumber = GeneralUtils::TryParse(splitGroup.at(3), -1); + if (groupNumber == -1) return; + const auto managerObjects = Game::entityManager->GetEntitiesInGroup("CB_" + std::to_string(groupNumber)); if (managerObjects.empty()) return; diff --git a/dScripts/ai/NS/NsGetFactionMissionServer.cpp b/dScripts/ai/NS/NsGetFactionMissionServer.cpp index 185bd344..d395cfd8 100644 --- a/dScripts/ai/NS/NsGetFactionMissionServer.cpp +++ b/dScripts/ai/NS/NsGetFactionMissionServer.cpp @@ -42,8 +42,11 @@ void NsGetFactionMissionServer::OnRespondToMission(Entity* self, int missionID, } if (flagID != -1) { - player->GetCharacter()->SetPlayerFlag(ePlayerFlag::JOINED_A_FACTION, true); - player->GetCharacter()->SetPlayerFlag(flagID, true); + auto* const character = player->GetCharacter(); + if (character) { + character->SetPlayerFlag(ePlayerFlag::JOINED_A_FACTION, true); + character->SetPlayerFlag(flagID, true); + } } MissionComponent* mis = static_cast(player->GetComponent(eReplicaComponentType::MISSION)); diff --git a/dScripts/ai/NS/WH/RockHydrantSmashable.cpp b/dScripts/ai/NS/WH/RockHydrantSmashable.cpp index d388baac..6549a85b 100644 --- a/dScripts/ai/NS/WH/RockHydrantSmashable.cpp +++ b/dScripts/ai/NS/WH/RockHydrantSmashable.cpp @@ -6,13 +6,11 @@ void RockHydrantSmashable::OnDie(Entity* self, Entity* killer) { const auto hydrantName = self->GetVar(u"hydrant"); - LDFBaseData* data = new LDFData(u"hydrant", GeneralUtils::UTF16ToWTF8(hydrantName)); - EntityInfo info{}; info.lot = ROCK_HYDRANT_BROKEN; info.pos = self->GetPosition(); info.rot = self->GetRotation(); - info.settings = { data }; + info.settings.Insert(u"hydrant", GeneralUtils::UTF16ToWTF8(hydrantName)); info.spawnerID = self->GetSpawnerID(); auto* hydrant = Game::entityManager->CreateEntity(info); diff --git a/dScripts/ai/PETS/HydrantSmashable.cpp b/dScripts/ai/PETS/HydrantSmashable.cpp index fc83a5d3..9e770615 100644 --- a/dScripts/ai/PETS/HydrantSmashable.cpp +++ b/dScripts/ai/PETS/HydrantSmashable.cpp @@ -6,13 +6,11 @@ void HydrantSmashable::OnDie(Entity* self, Entity* killer) { const auto hydrantName = self->GetVar(u"hydrant"); - LDFBaseData* data = new LDFData(u"hydrant", GeneralUtils::UTF16ToWTF8(hydrantName)); - EntityInfo info{}; info.lot = HYDRANT_BROKEN; info.pos = self->GetPosition(); info.rot = self->GetRotation(); - info.settings = { data }; + info.settings.Insert(u"hydrant", GeneralUtils::UTF16ToWTF8(hydrantName)); info.spawnerID = self->GetSpawnerID(); auto* hydrant = Game::entityManager->CreateEntity(info); diff --git a/dScripts/ai/PROPERTY/AG/AgPropGuard.cpp b/dScripts/ai/PROPERTY/AG/AgPropGuard.cpp index 4e77cb55..0670408d 100644 --- a/dScripts/ai/PROPERTY/AG/AgPropGuard.cpp +++ b/dScripts/ai/PROPERTY/AG/AgPropGuard.cpp @@ -33,7 +33,8 @@ void AgPropGuard::OnMissionDialogueOK(Entity* self, Entity* target, int missionI ) { //GameMessages::SendNotifyClientObject(Game::entityManager->GetZoneControlEntity()->GetObjectID(), u"GuardChat", target->GetObjectID(), 0, target->GetObjectID(), "", target->GetSystemAddress()); - target->GetCharacter()->SetPlayerFlag(113, true); + auto* const character = target->GetCharacter(); + if (character) character->SetPlayerFlag(113, true); Game::entityManager->GetZoneControlEntity()->AddTimer("GuardFlyAway", 1.0f); } diff --git a/dScripts/ai/RACING/TRACK_FV/FvRaceServer.cpp b/dScripts/ai/RACING/TRACK_FV/FvRaceServer.cpp index 5dd43f48..9c53a990 100644 --- a/dScripts/ai/RACING/TRACK_FV/FvRaceServer.cpp +++ b/dScripts/ai/RACING/TRACK_FV/FvRaceServer.cpp @@ -10,46 +10,42 @@ void FvRaceServer::OnStartup(Entity* self) { GameMessages::ConfigureRacingControl config; auto& raceSet = config.racingSettings; - raceSet.push_back(make_unique>(u"GameType", u"Racing")); - raceSet.push_back(make_unique>(u"GameState", u"Starting")); - raceSet.push_back(make_unique>(u"Number_Of_PlayersPerTeam", 6)); - raceSet.push_back(make_unique>(u"Minimum_Players_to_Start", 2)); - raceSet.push_back(make_unique>(u"Minimum_Players_for_Group_Achievements", 2)); + raceSet.Insert(u"GameType", u"Racing"); + raceSet.Insert(u"GameState", u"Starting"); + raceSet.Insert(u"Number_Of_PlayersPerTeam", 6); + raceSet.Insert(u"Minimum_Players_to_Start", 2); + raceSet.Insert(u"Minimum_Players_for_Group_Achievements", 2); - raceSet.push_back(make_unique>(u"Car_Object", 7703)); - raceSet.push_back(make_unique>(u"Race_PathName", u"MainPath")); - raceSet.push_back(make_unique>(u"Current_Lap", 1)); - raceSet.push_back(make_unique>(u"Number_of_Laps", 3)); - raceSet.push_back(make_unique>(u"activityID", 54)); + raceSet.Insert(u"Car_Object", 7703); + raceSet.Insert(u"Race_PathName", u"MainPath"); + raceSet.Insert(u"Current_Lap", 1); + raceSet.Insert(u"Number_of_Laps", 3); + raceSet.Insert(u"activityID", 54); - raceSet.push_back(make_unique>(u"Place_1", 100)); - raceSet.push_back(make_unique>(u"Place_2", 90)); - raceSet.push_back(make_unique>(u"Place_3", 80)); - raceSet.push_back(make_unique>(u"Place_4", 70)); - raceSet.push_back(make_unique>(u"Place_5", 60)); - raceSet.push_back(make_unique>(u"Place_6", 50)); + raceSet.Insert(u"Place_1", 100); + raceSet.Insert(u"Place_2", 90); + raceSet.Insert(u"Place_3", 80); + raceSet.Insert(u"Place_4", 70); + raceSet.Insert(u"Place_5", 60); + raceSet.Insert(u"Place_6", 50); - raceSet.push_back(make_unique>(u"Num_of_Players_1", 15)); - raceSet.push_back(make_unique>(u"Num_of_Players_2", 25)); - raceSet.push_back(make_unique>(u"Num_of_Players_3", 50)); - raceSet.push_back(make_unique>(u"Num_of_Players_4", 85)); - raceSet.push_back(make_unique>(u"Num_of_Players_5", 90)); - raceSet.push_back(make_unique>(u"Num_of_Players_6", 100)); + raceSet.Insert(u"Num_of_Players_1", 15); + raceSet.Insert(u"Num_of_Players_2", 25); + raceSet.Insert(u"Num_of_Players_3", 50); + raceSet.Insert(u"Num_of_Players_4", 85); + raceSet.Insert(u"Num_of_Players_5", 90); + raceSet.Insert(u"Num_of_Players_6", 100); - raceSet.push_back(make_unique>(u"Number_of_Spawn_Groups", 1)); - raceSet.push_back(make_unique>(u"Red_Spawners", 4847)); - raceSet.push_back(make_unique>(u"Blue_Spawners", 4848)); - raceSet.push_back(make_unique>(u"Blue_Flag", 4850)); - raceSet.push_back(make_unique>(u"Red_Flag", 4851)); - raceSet.push_back(make_unique>(u"Red_Point", 4846)); - raceSet.push_back(make_unique>(u"Blue_Point", 4845)); - raceSet.push_back(make_unique>(u"Red_Mark", 4844)); - raceSet.push_back(make_unique>(u"Blue_Mark", 4843)); + raceSet.Insert(u"Number_of_Spawn_Groups", 1); + raceSet.Insert(u"Red_Spawners", 4847); + raceSet.Insert(u"Blue_Spawners", 4848); + raceSet.Insert(u"Blue_Flag", 4850); + raceSet.Insert(u"Red_Flag", 4851); + raceSet.Insert(u"Red_Point", 4846); + raceSet.Insert(u"Blue_Point", 4845); + raceSet.Insert(u"Red_Mark", 4844); + raceSet.Insert(u"Blue_Mark", 4843); - const std::vector racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL); - for (auto* const racingController : racingControllers) { - auto* racingComponent = racingController->GetComponent(); - if (racingComponent) racingComponent->MsgConfigureRacingControl(config); - } + config.Send(self->GetObjectID()); } diff --git a/dScripts/ai/RACING/TRACK_GF/GfRaceServer.cpp b/dScripts/ai/RACING/TRACK_GF/GfRaceServer.cpp index d465d57f..c7a4cf94 100644 --- a/dScripts/ai/RACING/TRACK_GF/GfRaceServer.cpp +++ b/dScripts/ai/RACING/TRACK_GF/GfRaceServer.cpp @@ -10,46 +10,42 @@ void GfRaceServer::OnStartup(Entity* self) { GameMessages::ConfigureRacingControl config; auto& raceSet = config.racingSettings; - raceSet.push_back(make_unique>(u"GameType", u"Racing")); - raceSet.push_back(make_unique>(u"GameState", u"Starting")); - raceSet.push_back(make_unique>(u"Number_Of_PlayersPerTeam", 6)); - raceSet.push_back(make_unique>(u"Minimum_Players_to_Start", 2)); - raceSet.push_back(make_unique>(u"Minimum_Players_for_Group_Achievements", 2)); + raceSet.Insert(u"GameType", u"Racing"); + raceSet.Insert(u"GameState", u"Starting"); + raceSet.Insert(u"Number_Of_PlayersPerTeam", 6); + raceSet.Insert(u"Minimum_Players_to_Start", 2); + raceSet.Insert(u"Minimum_Players_for_Group_Achievements", 2); - raceSet.push_back(make_unique>(u"Car_Object", 7703)); - raceSet.push_back(make_unique>(u"Race_PathName", u"MainPath")); - raceSet.push_back(make_unique>(u"Current_Lap", 1)); - raceSet.push_back(make_unique>(u"Number_of_Laps", 3)); - raceSet.push_back(make_unique>(u"activityID", 39)); + raceSet.Insert(u"Car_Object", 7703); + raceSet.Insert(u"Race_PathName", u"MainPath"); + raceSet.Insert(u"Current_Lap", 1); + raceSet.Insert(u"Number_of_Laps", 3); + raceSet.Insert(u"activityID", 39); - raceSet.push_back(make_unique>(u"Place_1", 100)); - raceSet.push_back(make_unique>(u"Place_2", 90)); - raceSet.push_back(make_unique>(u"Place_3", 80)); - raceSet.push_back(make_unique>(u"Place_4", 70)); - raceSet.push_back(make_unique>(u"Place_5", 60)); - raceSet.push_back(make_unique>(u"Place_6", 50)); + raceSet.Insert(u"Place_1", 100); + raceSet.Insert(u"Place_2", 90); + raceSet.Insert(u"Place_3", 80); + raceSet.Insert(u"Place_4", 70); + raceSet.Insert(u"Place_5", 60); + raceSet.Insert(u"Place_6", 50); - raceSet.push_back(make_unique>(u"Num_of_Players_1", 15)); - raceSet.push_back(make_unique>(u"Num_of_Players_2", 25)); - raceSet.push_back(make_unique>(u"Num_of_Players_3", 50)); - raceSet.push_back(make_unique>(u"Num_of_Players_4", 85)); - raceSet.push_back(make_unique>(u"Num_of_Players_5", 90)); - raceSet.push_back(make_unique>(u"Num_of_Players_6", 100)); + raceSet.Insert(u"Num_of_Players_1", 15); + raceSet.Insert(u"Num_of_Players_2", 25); + raceSet.Insert(u"Num_of_Players_3", 50); + raceSet.Insert(u"Num_of_Players_4", 85); + raceSet.Insert(u"Num_of_Players_5", 90); + raceSet.Insert(u"Num_of_Players_6", 100); - raceSet.push_back(make_unique>(u"Number_of_Spawn_Groups", 1)); - raceSet.push_back(make_unique>(u"Red_Spawners", 4847)); - raceSet.push_back(make_unique>(u"Blue_Spawners", 4848)); - raceSet.push_back(make_unique>(u"Blue_Flag", 4850)); - raceSet.push_back(make_unique>(u"Red_Flag", 4851)); - raceSet.push_back(make_unique>(u"Red_Point", 4846)); - raceSet.push_back(make_unique>(u"Blue_Point", 4845)); - raceSet.push_back(make_unique>(u"Red_Mark", 4844)); - raceSet.push_back(make_unique>(u"Blue_Mark", 4843)); + raceSet.Insert(u"Number_of_Spawn_Groups", 1); + raceSet.Insert(u"Red_Spawners", 4847); + raceSet.Insert(u"Blue_Spawners", 4848); + raceSet.Insert(u"Blue_Flag", 4850); + raceSet.Insert(u"Red_Flag", 4851); + raceSet.Insert(u"Red_Point", 4846); + raceSet.Insert(u"Blue_Point", 4845); + raceSet.Insert(u"Red_Mark", 4844); + raceSet.Insert(u"Blue_Mark", 4843); - const std::vector racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL); - for (auto* const racingController : racingControllers) { - auto* racingComponent = racingController->GetComponent(); - if (racingComponent) racingComponent->MsgConfigureRacingControl(config); - } + config.Send(self->GetObjectID()); } diff --git a/dScripts/ai/RACING/TRACK_NS/NsRaceServer.cpp b/dScripts/ai/RACING/TRACK_NS/NsRaceServer.cpp index 5ef6c6d1..3c9efa3a 100644 --- a/dScripts/ai/RACING/TRACK_NS/NsRaceServer.cpp +++ b/dScripts/ai/RACING/TRACK_NS/NsRaceServer.cpp @@ -10,45 +10,41 @@ void NsRaceServer::OnStartup(Entity* self) { GameMessages::ConfigureRacingControl config; auto& raceSet = config.racingSettings; - raceSet.push_back(make_unique>(u"GameType", u"Racing")); - raceSet.push_back(make_unique>(u"GameState", u"Starting")); - raceSet.push_back(make_unique>(u"Number_Of_PlayersPerTeam", 6)); - raceSet.push_back(make_unique>(u"Minimum_Players_to_Start", 2)); - raceSet.push_back(make_unique>(u"Minimum_Players_for_Group_Achievements", 2)); + raceSet.Insert(u"GameType", u"Racing"); + raceSet.Insert(u"GameState", u"Starting"); + raceSet.Insert(u"Number_Of_PlayersPerTeam", 6); + raceSet.Insert(u"Minimum_Players_to_Start", 2); + raceSet.Insert(u"Minimum_Players_for_Group_Achievements", 2); - raceSet.push_back(make_unique>(u"Car_Object", 7703)); - raceSet.push_back(make_unique>(u"Race_PathName", u"MainPath")); - raceSet.push_back(make_unique>(u"Current_Lap", 1)); - raceSet.push_back(make_unique>(u"Number_of_Laps", 3)); - raceSet.push_back(make_unique>(u"activityID", 42)); + raceSet.Insert(u"Car_Object", 7703); + raceSet.Insert(u"Race_PathName", u"MainPath"); + raceSet.Insert(u"Current_Lap", 1); + raceSet.Insert(u"Number_of_Laps", 3); + raceSet.Insert(u"activityID", 42); - raceSet.push_back(make_unique>(u"Place_1", 100)); - raceSet.push_back(make_unique>(u"Place_2", 90)); - raceSet.push_back(make_unique>(u"Place_3", 80)); - raceSet.push_back(make_unique>(u"Place_4", 70)); - raceSet.push_back(make_unique>(u"Place_5", 60)); - raceSet.push_back(make_unique>(u"Place_6", 50)); + raceSet.Insert(u"Place_1", 100); + raceSet.Insert(u"Place_2", 90); + raceSet.Insert(u"Place_3", 80); + raceSet.Insert(u"Place_4", 70); + raceSet.Insert(u"Place_5", 60); + raceSet.Insert(u"Place_6", 50); - raceSet.push_back(make_unique>(u"Num_of_Players_1", 15)); - raceSet.push_back(make_unique>(u"Num_of_Players_2", 25)); - raceSet.push_back(make_unique>(u"Num_of_Players_3", 50)); - raceSet.push_back(make_unique>(u"Num_of_Players_4", 85)); - raceSet.push_back(make_unique>(u"Num_of_Players_5", 90)); - raceSet.push_back(make_unique>(u"Num_of_Players_6", 100)); + raceSet.Insert(u"Num_of_Players_1", 15); + raceSet.Insert(u"Num_of_Players_2", 25); + raceSet.Insert(u"Num_of_Players_3", 50); + raceSet.Insert(u"Num_of_Players_4", 85); + raceSet.Insert(u"Num_of_Players_5", 90); + raceSet.Insert(u"Num_of_Players_6", 100); - raceSet.push_back(make_unique>(u"Number_of_Spawn_Groups", 1)); - raceSet.push_back(make_unique>(u"Red_Spawners", 4847)); - raceSet.push_back(make_unique>(u"Blue_Spawners", 4848)); - raceSet.push_back(make_unique>(u"Blue_Flag", 4850)); - raceSet.push_back(make_unique>(u"Red_Flag", 4851)); - raceSet.push_back(make_unique>(u"Red_Point", 4846)); - raceSet.push_back(make_unique>(u"Blue_Point", 4845)); - raceSet.push_back(make_unique>(u"Red_Mark", 4844)); - raceSet.push_back(make_unique>(u"Blue_Mark", 4843)); + raceSet.Insert(u"Number_of_Spawn_Groups", 1); + raceSet.Insert(u"Red_Spawners", 4847); + raceSet.Insert(u"Blue_Spawners", 4848); + raceSet.Insert(u"Blue_Flag", 4850); + raceSet.Insert(u"Red_Flag", 4851); + raceSet.Insert(u"Red_Point", 4846); + raceSet.Insert(u"Blue_Point", 4845); + raceSet.Insert(u"Red_Mark", 4844); + raceSet.Insert(u"Blue_Mark", 4843); - std::vector racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL); - for (auto* const racingController : racingControllers) { - auto* racingComponent = racingController->GetComponent(); - if (racingComponent) racingComponent->MsgConfigureRacingControl(config); - } + config.Send(self->GetObjectID()); } diff --git a/dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp b/dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp index 9c3cc009..8baaa4c6 100644 --- a/dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp +++ b/dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp @@ -9,45 +9,41 @@ void NsWinterRaceServer::OnStartup(Entity* self) { GameMessages::ConfigureRacingControl config; auto& raceSet = config.racingSettings; - raceSet.push_back(make_unique>("GameType", u"Racing")); - raceSet.push_back(make_unique>("GameState", u"Starting")); - raceSet.push_back(make_unique>("Number_Of_PlayersPerTeam", 6)); - raceSet.push_back(make_unique>("Minimum_Players_to_Start", 2)); - raceSet.push_back(make_unique>("Minimum_Players_for_Group_Achievments", 2)); + raceSet.Insert("GameType", u"Racing"); + raceSet.Insert("GameState", u"Starting"); + raceSet.Insert("Number_Of_PlayersPerTeam", 6); + raceSet.Insert("Minimum_Players_to_Start", 2); + raceSet.Insert("Minimum_Players_for_Group_Achievments", 2); - raceSet.push_back(make_unique>("Car_Object", 7703)); - raceSet.push_back(make_unique>("Race_PathName", u"MainPath")); - raceSet.push_back(make_unique>("Current_Lap", 1)); - raceSet.push_back(make_unique>("Number_of_Laps", 3)); - raceSet.push_back(make_unique>("activityID", 42)); + raceSet.Insert("Car_Object", 7703); + raceSet.Insert("Race_PathName", u"MainPath"); + raceSet.Insert("Current_Lap", 1); + raceSet.Insert("Number_of_Laps", 3); + raceSet.Insert("activityID", 60); - raceSet.push_back(make_unique>("Place_1", 100)); - raceSet.push_back(make_unique>("Place_2", 90)); - raceSet.push_back(make_unique>("Place_3", 80)); - raceSet.push_back(make_unique>("Place_4", 70)); - raceSet.push_back(make_unique>("Place_5", 60)); - raceSet.push_back(make_unique>("Place_6", 50)); + raceSet.Insert("Place_1", 100); + raceSet.Insert("Place_2", 90); + raceSet.Insert("Place_3", 80); + raceSet.Insert("Place_4", 70); + raceSet.Insert("Place_5", 60); + raceSet.Insert("Place_6", 50); - raceSet.push_back(make_unique>("Num_of_Players_1", 15)); - raceSet.push_back(make_unique>("Num_of_Players_2", 25)); - raceSet.push_back(make_unique>("Num_of_Players_3", 50)); - raceSet.push_back(make_unique>("Num_of_Players_4", 85)); - raceSet.push_back(make_unique>("Num_of_Players_5", 90)); - raceSet.push_back(make_unique>("Num_of_Players_6", 100)); + raceSet.Insert("Num_of_Players_1", 15); + raceSet.Insert("Num_of_Players_2", 25); + raceSet.Insert("Num_of_Players_3", 50); + raceSet.Insert("Num_of_Players_4", 85); + raceSet.Insert("Num_of_Players_5", 90); + raceSet.Insert("Num_of_Players_6", 100); - raceSet.push_back(make_unique>("Number_of_Spawn_Groups", 1)); - raceSet.push_back(make_unique>("Red_Spawners", 4847)); - raceSet.push_back(make_unique>("Blue_Spawners", 4848)); - raceSet.push_back(make_unique>("Blue_Flag", 4850)); - raceSet.push_back(make_unique>("Red_Flag", 4851)); - raceSet.push_back(make_unique>("Red_Point", 4846)); - raceSet.push_back(make_unique>("Blue_Point", 4845)); - raceSet.push_back(make_unique>("Red_Mark", 4844)); - raceSet.push_back(make_unique>("Blue_Mark", 4843)); + raceSet.Insert("Number_of_Spawn_Groups", 1); + raceSet.Insert("Red_Spawners", 4847); + raceSet.Insert("Blue_Spawners", 4848); + raceSet.Insert("Blue_Flag", 4850); + raceSet.Insert("Red_Flag", 4851); + raceSet.Insert("Red_Point", 4846); + raceSet.Insert("Blue_Point", 4845); + raceSet.Insert("Red_Mark", 4844); + raceSet.Insert("Blue_Mark", 4843); - const auto racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL); - for (auto* const racingController : racingControllers) { - auto* const racingComponent = racingController->GetComponent(); - if (racingComponent) racingComponent->MsgConfigureRacingControl(config); - } + config.Send(self->GetObjectID()); } diff --git a/dServer/CMakeLists.txt b/dServer/CMakeLists.txt index ca4e6198..988465df 100644 --- a/dServer/CMakeLists.txt +++ b/dServer/CMakeLists.txt @@ -8,3 +8,5 @@ target_include_directories(dServer PUBLIC ".") target_include_directories(dServer PRIVATE "${PROJECT_SOURCE_DIR}/dCommon/" # BinaryPathFinder.h ) + +target_link_libraries(dServer PRIVATE dCommon) diff --git a/dWeb/Web.cpp b/dWeb/Web.cpp index abf9bd36..f8cf2edf 100644 --- a/dWeb/Web.cpp +++ b/dWeb/Web.cpp @@ -184,7 +184,7 @@ static void DLOG(char ch, void *param) { static size_t len{}; if (ch != '\n') buf[len++] = ch; // we provide the newline in our logger if (ch == '\n' || len >= sizeof(buf)) { - LOG_DEBUG("%.*s", static_cast(len), buf); + if (Game::logger) LOG_DEBUG("%.*s", static_cast(len), buf); len = 0; } } diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index ad570534..e4d324f1 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -14,7 +14,7 @@ #include "dConfig.h" #include "dpWorld.h" #include "dZoneManager.h" -#include "Metrics.hpp" +#include "Metrics.h" #include "PerformanceManager.h" #include "Diagnostics.h" #include "BinaryPathFinder.h" @@ -763,7 +763,8 @@ void HandleMasterPacket(Packet* packet) { case MessageType::Master::NEW_SESSION_ALERT: { CINSTREAM_SKIP_HEADER; - uint32_t sessionKey = inStream.Read(sessionKey); + uint32_t sessionKey{}; + inStream.Read(sessionKey); LUString username; inStream.Read(username); @@ -970,8 +971,7 @@ void HandlePacket(Packet* packet) { } case MessageType::World::LOGIN_REQUEST: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header = inStream.Read(header); + CINSTREAM_SKIP_HEADER; LWOOBJID playerID = 0; inStream.Read(playerID); @@ -1019,6 +1019,7 @@ void HandlePacket(Packet* packet) { if (user) { Character* c = user->GetLastUsedChar(); if (c != nullptr) { + if (Game::entityManager->GetEntity(c->GetObjectID())) return; std::u16string username = GeneralUtils::ASCIIToUTF16(c->GetName()); Game::server->GetReplicaManager()->AddParticipant(packet->systemAddress); @@ -1115,6 +1116,12 @@ void HandlePacket(Packet* packet) { case eCharacterVersion::PET_IDS: { LOG("Regenerating item ids"); inventoryComponent->RegenerateItemIDs(); + levelComponent->SetCharacterVersion(eCharacterVersion::INVENTORY_PERSISTENT_IDS); + [[fallthrough]]; + } + case eCharacterVersion::INVENTORY_PERSISTENT_IDS: { + LOG("Fixing racing meta missions"); + missionComponent->FixRacingMetaMissions(); levelComponent->SetCharacterVersion(eCharacterVersion::UP_TO_DATE); [[fallthrough]]; } @@ -1250,13 +1257,22 @@ void HandlePacket(Packet* packet) { return; } - Entity* entity = Game::entityManager->GetEntity(user->GetLastUsedChar()->GetObjectID()); - if (entity) entity->ProcessPositionUpdate(positionUpdate); + if (const auto* const lastChar = user->GetLastUsedChar()) { + if (auto* const entity = Game::entityManager->GetEntity(lastChar->GetObjectID())) { + entity->ProcessPositionUpdate(positionUpdate); + } + } break; } case MessageType::World::MAIL: { - Mail::HandleMail(inStream, packet->systemAddress, UserManager::Instance()->GetUser(packet->systemAddress)->GetLastUsedChar()->GetEntity()); + if (auto* const user = UserManager::Instance()->GetUser(packet->systemAddress)) { + if (auto* const lastChar = user->GetLastUsedChar()) { + if (auto* const entity = lastChar->GetEntity()) { + Mail::HandleMail(inStream, packet->systemAddress, entity); + } + } + } break; } @@ -1279,7 +1295,8 @@ void HandlePacket(Packet* packet) { LWOOBJID objectID = 0; auto user = UserManager::Instance()->GetUser(packet->systemAddress); if (user) { - objectID = user->GetLastUsedChar()->GetObjectID(); + const auto* const lastChar = user->GetLastUsedChar(); + if (lastChar) objectID = lastChar->GetObjectID(); } bitStream.Write(objectID); @@ -1379,13 +1396,19 @@ void HandlePacket(Packet* packet) { return; } - if (user->GetIsMuted()) { - user->GetLastUsedChar()->SendMuteNotice(); + const auto* const lastChar = user->GetLastUsedChar(); + if (!lastChar) { + LOG("No last used character for chat message %i", user->GetAccountID()); return; } - std::string playerName = user->GetLastUsedChar()->GetName(); - bool isMythran = user->GetLastUsedChar()->GetGMLevel() > eGameMasterLevel::CIVILIAN; - bool isOk = Game::chatFilter->IsSentenceOkay(GeneralUtils::UTF16ToWTF8(chatMessage.message), user->GetLastUsedChar()->GetGMLevel()).empty(); + + if (user->GetIsMuted()) { + lastChar->SendMuteNotice(); + return; + } + std::string playerName = lastChar->GetName(); + bool isMythran = lastChar->GetGMLevel() > eGameMasterLevel::CIVILIAN; + bool isOk = Game::chatFilter->IsSentenceOkay(GeneralUtils::UTF16ToWTF8(chatMessage.message), lastChar->GetGMLevel()).empty(); LOG_DEBUG("Msg: %s was approved previously? %i", GeneralUtils::UTF16ToWTF8(chatMessage.message).c_str(), user->GetLastChatMessageApproved()); if (!isOk) return; if (!isOk && !isMythran) return; @@ -1516,7 +1539,6 @@ void FinalizeShutdown() { LOG("Shutdown complete, zone (%i), instance (%i)", Game::server->GetZoneID(), g_InstanceID); //Delete our objects here: - Metrics::Clear(); dpWorld::Shutdown(); Database::Destroy("WorldServer"); if (Game::chatFilter) delete Game::chatFilter; diff --git a/dZoneManager/Level.cpp b/dZoneManager/Level.cpp index 13c85c1a..72fbdcdd 100644 --- a/dZoneManager/Level.cpp +++ b/dZoneManager/Level.cpp @@ -16,6 +16,7 @@ #include "AssetManager.h" #include "ClientVersion.h" #include "dConfig.h" +#include Level::Level(Zone* parentZone, const std::string& filepath) { m_ParentZone = parentZone; @@ -30,7 +31,7 @@ Level::Level(Zone* parentZone, const std::string& filepath) { ReadChunks(stream); } -void Level::MakeSpawner(SceneObject obj) { +void Level::MakeSpawner(const SceneObject& obj) { SpawnerInfo spawnInfo = SpawnerInfo(); SpawnerNode* node = new SpawnerNode(); spawnInfo.templateID = obj.lot; @@ -40,14 +41,14 @@ void Level::MakeSpawner(SceneObject obj) { node->rotation = obj.rotation; node->config = obj.settings; spawnInfo.nodes.push_back(node); - for (LDFBaseData* data : obj.settings) { + for (const auto& data : obj.settings.values | std::views::values) { if (!data) continue; if (data->GetKey() == u"spawntemplate") { - spawnInfo.templateID = std::stoi(data->GetValueAsString()); + spawnInfo.templateID = GeneralUtils::TryParse(data->GetValueAsString(), 0); } if (data->GetKey() == u"spawner_node_id") { - node->nodeID = std::stoi(data->GetValueAsString()); + node->nodeID = GeneralUtils::TryParse(data->GetValueAsString(), 0u); } if (data->GetKey() == u"spawner_name") { @@ -55,45 +56,44 @@ void Level::MakeSpawner(SceneObject obj) { } if (data->GetKey() == u"max_to_spawn") { - spawnInfo.maxToSpawn = std::stoi(data->GetValueAsString()); + spawnInfo.maxToSpawn = GeneralUtils::TryParse(data->GetValueAsString(), 0); } if (data->GetKey() == u"spawner_active_on_load") { - spawnInfo.activeOnLoad = std::stoi(data->GetValueAsString()); + spawnInfo.activeOnLoad = GeneralUtils::TryParse(data->GetValueAsString(), false); } if (data->GetKey() == u"active_on_load") { - spawnInfo.activeOnLoad = std::stoi(data->GetValueAsString()); + spawnInfo.activeOnLoad = GeneralUtils::TryParse(data->GetValueAsString(), false); } if (data->GetKey() == u"respawn") { if (data->GetValueType() == eLDFType::LDF_TYPE_FLOAT) // Floats are in seconds { - spawnInfo.respawnTime = std::stof(data->GetValueAsString()); - } else if (data->GetValueType() == eLDFType::LDF_TYPE_U32) // Ints are in ms? + spawnInfo.respawnTime = GeneralUtils::TryParse(data->GetValueAsString(), 0.0f); + } else if (data->GetValueType() == eLDFType::LDF_TYPE_U32) // Ints are in ms { - spawnInfo.respawnTime = std::stoul(data->GetValueAsString()) / 1000; + spawnInfo.respawnTime = GeneralUtils::TryParse(data->GetValueAsString(), 0) / 1000; } } if (data->GetKey() == u"spawnsGroupOnSmash") { - spawnInfo.spawnsOnSmash = std::stoi(data->GetValueAsString()); + spawnInfo.spawnsOnSmash = GeneralUtils::TryParse(data->GetValueAsString(), false); } if (data->GetKey() == u"spawnNetNameForSpawnGroupOnSmash") { spawnInfo.spawnOnSmashGroupName = data->GetValueAsString(); } if (data->GetKey() == u"groupID") { // Load object groups - std::string groupStr = data->GetValueAsString(); - spawnInfo.groups = GeneralUtils::SplitString(groupStr, ';'); + spawnInfo.groups = GeneralUtils::SplitString(data->GetValueAsString(), ';'); if (spawnInfo.groups.back().empty()) spawnInfo.groups.erase(spawnInfo.groups.end() - 1); } if (data->GetKey() == u"no_auto_spawn") { - spawnInfo.noAutoSpawn = static_cast*>(data)->GetValue(); + spawnInfo.noAutoSpawn = GeneralUtils::TryParse(data->GetValueAsString(), false); } if (data->GetKey() == u"no_timed_spawn") { - spawnInfo.noTimedSpawn = static_cast*>(data)->GetValue(); + spawnInfo.noTimedSpawn = GeneralUtils::TryParse(data->GetValueAsString(), false); } if (data->GetKey() == u"spawnActivator") { - spawnInfo.spawnActivator = static_cast*>(data)->GetValue(); + spawnInfo.spawnActivator = GeneralUtils::TryParse(data->GetValueAsString(), false); } } @@ -236,10 +236,11 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) { BinaryIO::BinaryRead(file, obj.lot); if (header.fileInfo.version >= 38) { - uint32_t tmp = 1; + int32_t tmp = 1; BinaryIO::BinaryRead(file, tmp); if (tmp > -1 && tmp < 11) obj.nodeType = tmp; } + if (header.fileInfo.version >= 32) { BinaryIO::BinaryRead(file, obj.glomId); } @@ -257,20 +258,14 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) { Game::zoneManager->GetZoneMut()->SetSpawnRot(obj.rotation); } - std::string sData = GeneralUtils::UTF16ToWTF8(ldfString); - std::stringstream ssData(sData); - std::string token; - char deliminator = '\n'; - - while (std::getline(ssData, token, deliminator)) { - LDFBaseData* ldfData = LDFBaseData::DataFromString(token); - obj.settings.push_back(ldfData); + for (const auto& token : GeneralUtils::SplitString(GeneralUtils::UTF16ToWTF8(ldfString), '\n')) { + obj.settings.ParseInsert(token); } // We should never have more than 1 zone control object bool skipLoadingObject = obj.lot == zoneControlObject->GetLOT(); - for (LDFBaseData* data : obj.settings) { + for (const auto& data : obj.settings | std::views::values) { if (!data) continue; if (data->GetKey() == u"gatingOnFeature") { gating.featureName = data->GetValueAsString(); @@ -290,17 +285,12 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) { } // If this is a client only object, we can skip loading it if (data->GetKey() == u"loadOnClientOnly") { - skipLoadingObject |= static_cast(std::stoi(data->GetValueAsString())); + skipLoadingObject |= GeneralUtils::TryParse(data->GetValueAsString(), false); break; } } if (skipLoadingObject) { - for (auto* setting : obj.settings) { - delete setting; - setting = nullptr; - } - continue; } diff --git a/dZoneManager/Level.h b/dZoneManager/Level.h index 1f0b4f2e..22860fe7 100644 --- a/dZoneManager/Level.h +++ b/dZoneManager/Level.h @@ -40,7 +40,7 @@ public: public: Level(Zone* parentZone, const std::string& filepath); - static void MakeSpawner(SceneObject obj); + static void MakeSpawner(const SceneObject& obj); std::map m_ChunkHeaders; private: diff --git a/dZoneManager/Spawner.cpp b/dZoneManager/Spawner.cpp index 3baf193f..e7045842 100644 --- a/dZoneManager/Spawner.cpp +++ b/dZoneManager/Spawner.cpp @@ -6,8 +6,10 @@ #include #include "GeneralUtils.h" #include "dZoneManager.h" +#include +#include -Spawner::Spawner(const SpawnerInfo info) { +Spawner::Spawner(const SpawnerInfo& info) { m_Info = info; m_Active = m_Info.activeOnLoad && info.spawnActivator; m_EntityInfo = EntityInfo(); @@ -53,18 +55,15 @@ Spawner::Spawner(const SpawnerInfo info) { } for (Spawner* ssSpawner : spawnSmashSpawnersN) { m_SpawnSmashFoundGroup = true; - m_SpawnOnSmash = ssSpawner; + m_SpawnOnSmashID = ssSpawner ? ssSpawner->m_Info.spawnerID : LWOOBJID_EMPTY; ssSpawner->AddSpawnedEntityDieCallback([=, this]() { - Spawn(); + // Intentionally left as a non debug log since i have no idea how much stuff this would affect + LOG("WOULD HAVE SPAWNED %i", m_EntityInfo.lot); }); } } } -Spawner::~Spawner() { - -} - Entity* Spawner::Spawn() { std::vector freeNodes; for (SpawnerNode* node : m_Info.nodes) { @@ -76,9 +75,25 @@ Entity* Spawner::Spawn() { return Spawn(freeNodes); } -Entity* Spawner::Spawn(std::vector freeNodes, const bool force) { - if (force || ((m_Entities.size() < m_Info.amountMaintained) && (freeNodes.size() > 0) && (m_AmountSpawned < m_Info.maxToSpawn || m_Info.maxToSpawn == -1))) { - SpawnerNode* spawnNode = freeNodes[GeneralUtils::GenerateRandomNumber(0, freeNodes.size() - 1)]; +Entity* Spawner::Spawn(const std::vector& freeNodes, const bool force) { + Entity* spawnedEntity = nullptr; + if (force || ((m_Entities.size() < m_Info.amountMaintained) && !freeNodes.empty() && (m_AmountSpawned < m_Info.maxToSpawn || m_Info.maxToSpawn == -1))) { + // first sum the weights we were provided + int32_t spawnWeight = 0; + for (const auto* const node : freeNodes) spawnWeight += node->weight; + auto chosenWeight = GeneralUtils::GenerateRandomNumber(1, spawnWeight); + + // Default to 0 incase something goes wrong in this calc + // Roll the spawner nodes based on their weights, higher weights = more likely to spawn + SpawnerNode* spawnNode = freeNodes[0]; + for (auto* const node : freeNodes) { + chosenWeight -= node->weight; + if (chosenWeight <= 0) { + spawnNode = node; + break; // we rolled a spawner + } + } + ++m_AmountSpawned; m_EntityInfo.pos = spawnNode->position; m_EntityInfo.rot = spawnNode->rotation; @@ -92,26 +107,24 @@ Entity* Spawner::Spawn(std::vector freeNodes, const bool force) { m_EntityInfo.spawnerID = m_Info.spawnerID; } - Entity* rezdE = Game::entityManager->CreateEntity(m_EntityInfo, nullptr); + spawnedEntity = Game::entityManager->CreateEntity(m_EntityInfo, nullptr); - rezdE->GetGroups() = m_Info.groups; + spawnedEntity->GetGroups() = m_Info.groups; - Game::entityManager->ConstructEntity(rezdE); + Game::entityManager->ConstructEntity(spawnedEntity); - m_Entities.insert({ rezdE->GetObjectID(), spawnNode }); - spawnNode->entities.push_back(rezdE->GetObjectID()); + m_Entities[spawnedEntity->GetObjectID()] = spawnNode; + spawnNode->entities.push_back(spawnedEntity->GetObjectID()); if (m_Entities.size() == m_Info.amountMaintained) { m_NeedsUpdate = false; } for (const auto& cb : m_EntitySpawnedCallbacks) { - cb(rezdE); + cb(spawnedEntity); } - - return rezdE; } - return nullptr; + return spawnedEntity; } void Spawner::AddSpawnedEntityDieCallback(std::function callback) { @@ -147,18 +160,18 @@ void Spawner::SoftReset() { m_NeedsUpdate = true; } -void Spawner::SetRespawnTime(float time) { +void Spawner::SetRespawnTime(const float time) { m_Info.respawnTime = time; for (size_t i = 0; i < m_WaitTimes.size(); ++i) { m_WaitTimes[i] = 0; - }; + } m_Start = true; m_NeedsUpdate = true; } -void Spawner::SetNumToMaintain(int32_t value) { +void Spawner::SetNumToMaintain(const int32_t value) { m_Info.amountMaintained = value; } @@ -176,41 +189,35 @@ void Spawner::Update(const float deltaTime) { return; } - if (!m_NeedsUpdate) return; - if (!m_Active) return; - //if (m_Info.noTimedSpawn) return; - if (m_Info.spawnsOnSmash) { - if (!m_SpawnSmashFoundGroup) { + if (!m_NeedsUpdate || !m_Active || m_Info.spawnsOnSmash) return; - } - return; - } - for (size_t i = 0; i < m_WaitTimes.size(); ++i) { + for (size_t i = 0; i < m_WaitTimes.size(); ) { m_WaitTimes[i] += deltaTime; if (m_WaitTimes[i] >= m_Info.respawnTime) { m_WaitTimes.erase(m_WaitTimes.begin() + i); Spawn(); + } else { + i++; } } } -std::vector Spawner::GetSpawnedObjectIDs() const { +const std::vector Spawner::GetSpawnedObjectIDs() const { std::vector ids; ids.reserve(m_Entities.size()); - for (const auto& [objId, spawnerNode] : m_Entities) { + for (const auto objId : m_Entities | std::views::keys) { ids.push_back(objId); } return ids; } void Spawner::NotifyOfEntityDeath(const LWOOBJID& objectID) { - for (std::function cb : m_SpawnedEntityDieCallbacks) { + for (const auto& cb : m_SpawnedEntityDieCallbacks) { cb(); } m_NeedsUpdate = true; - //m_RespawnTime = 10.0f; m_WaitTimes.push_back(0.0f); SpawnerNode* node; @@ -218,19 +225,20 @@ void Spawner::NotifyOfEntityDeath(const LWOOBJID& objectID) { if (it != m_Entities.end()) node = it->second; else return; - if (!node) { - return; - } + if (!node) return; - for (size_t i = 0; i < node->entities.size(); ++i) { + for (size_t i = 0; i < node->entities.size();) { if (node->entities[i] && node->entities[i] == objectID) node->entities.erase(node->entities.begin() + i); + else + i++; } m_Entities.erase(objectID); - if (m_SpawnOnSmash != nullptr) { - m_SpawnOnSmash->Reset(); + auto* const spawnOnSmash = Game::zoneManager->GetSpawner(m_SpawnOnSmashID); + if (spawnOnSmash) { + spawnOnSmash->Reset(); } } @@ -243,6 +251,6 @@ void Spawner::Activate() { } } -void Spawner::SetSpawnLot(LOT lot) { +void Spawner::SetSpawnLot(const LOT lot) { m_EntityInfo.lot = lot; } diff --git a/dZoneManager/Spawner.h b/dZoneManager/Spawner.h index f96e1892..a5c9f355 100644 --- a/dZoneManager/Spawner.h +++ b/dZoneManager/Spawner.h @@ -11,13 +11,42 @@ #include "LDFFormat.h" #include "EntityInfo.h" +/** + * Any given spawner owns a certain number of spawner nodes + * these nodes are where entities are actually spawned + * The first spawner nodes waypoint in any given network contains the base config for all the spawner nodes + * Then each spawner node after the first may contain duplicate settings which override the base ones + * If spawner node 1 has an attached_path of "1", then all spawner nodes in this spawner network will have + * an attached_path of "1". + * Each spawner node can also specify attached_path of any other value and it will override the one provided by node 1. + * If a spawner node does NOT provide an override, the first one will be used + * I have no clue why the nodes are pointers, beats me + * sn = SpawnerNode + * Spawner + * ---------------- + * | sn | + * | sn | + * | sn | + * | | + * | sn | + * | sn | + * ----------------- + */ struct SpawnerNode { + // This spawner nodes position in the world NiPoint3 position = NiPoint3Constant::ZERO; + // The rotation of this spawner in the world NiQuaternion rotation = QuatUtils::IDENTITY; + // This spawners nodes ID in this spawner network uint32_t nodeID = 0; + // The max number of entities that can be spawned by this node uint32_t nodeMax = 1; + // The weight (chance) this spawner node has. Higher is more common + int32_t weight = 1; + // The IDs of entities spawned by this spawner node std::vector entities; - std::vector config; + // The config of all entities spawned by this node + LwoNameValue config; }; struct SpawnerInfo { @@ -45,11 +74,10 @@ struct SpawnerInfo { class Spawner { public: - Spawner(SpawnerInfo info); - ~Spawner(); + Spawner(const SpawnerInfo& info); Entity* Spawn(); - Entity* Spawn(std::vector freeNodes, bool force = false); + Entity* Spawn(const std::vector& freeNodes, bool force = false); void Update(float deltaTime); void NotifyOfEntityDeath(const LWOOBJID& objectID); void Activate(); @@ -57,16 +85,16 @@ public: int32_t GetAmountSpawned() { return m_AmountSpawned; }; std::string GetName() { return m_Info.name; }; std::vector GetGroups() { return m_Info.groups; }; - void AddSpawnedEntityDieCallback(std::function callback); - void AddEntitySpawnedCallback(std::function callback); - void SetSpawnLot(LOT lot); + void AddSpawnedEntityDieCallback(const std::function callback); + void AddEntitySpawnedCallback(const std::function callback); + void SetSpawnLot(const LOT lot); void Reset(); void DestroyAllEntities(); void SoftReset(); - void SetRespawnTime(float time); - void SetNumToMaintain(int32_t value); + void SetRespawnTime(const float time); + void SetNumToMaintain(const int32_t value); bool GetIsSpawnSmashGroup() const { return m_SpawnSmashFoundGroup; }; - std::vector GetSpawnedObjectIDs() const; + const std::vector GetSpawnedObjectIDs() const; SpawnerInfo m_Info; bool m_Active = true; @@ -82,7 +110,7 @@ private: EntityInfo m_EntityInfo; int32_t m_AmountSpawned = 0; bool m_Start = false; - Spawner* m_SpawnOnSmash = nullptr; + LWOOBJID m_SpawnOnSmashID = LWOOBJID_EMPTY; }; #endif // SPAWNER_H diff --git a/dZoneManager/Zone.cpp b/dZoneManager/Zone.cpp index d2eb7c10..07f7f9d0 100644 --- a/dZoneManager/Zone.cpp +++ b/dZoneManager/Zone.cpp @@ -136,36 +136,47 @@ void Zone::LoadZoneIntoMemory() { m_Paths.reserve(pathCount); for (uint32_t i = 0; i < pathCount; ++i) LoadPath(file); - for (Path path : m_Paths) { + for (const Path& path : m_Paths) { if (path.pathType != PathType::Spawner) continue; - SpawnerInfo info = SpawnerInfo(); - for (PathWaypoint waypoint : path.pathWaypoints) { + SpawnerInfo info{}; + for (size_t i = 0; i < path.pathWaypoints.size(); i++) { + const auto& waypoint = path.pathWaypoints[i]; SpawnerNode* node = new SpawnerNode(); node->position = waypoint.position; node->rotation = waypoint.rotation; node->nodeID = 0; - node->config = waypoint.config; + node->config = path.pathWaypoints[0].config; + // All spawner waypoints get the config data of the first waypoint, but then we + // overwrite settings on this waypoint if we have another one defined of the same name + if (i != 0) { + for (const auto& [key, value] : waypoint.config) { + node->config.ParseInsert(value->GetString()); + } + } - for (LDFBaseData* data : waypoint.config) { + for (const auto& data : waypoint.config | std::views::values) { if (!data) continue; if (data->GetKey() == u"spawner_node_id") { - node->nodeID = std::stoi(data->GetValueAsString()); + node->nodeID = GeneralUtils::TryParse(data->GetValueAsString(), 0); } else if (data->GetKey() == u"spawner_max_per_node") { - node->nodeMax = std::stoi(data->GetValueAsString()); + node->nodeMax = GeneralUtils::TryParse(data->GetValueAsString(), 0); } else if (data->GetKey() == u"groupID") { // Load object group - std::string groupStr = data->GetValueAsString(); - info.groups = GeneralUtils::SplitString(groupStr, ';'); + info.groups = GeneralUtils::SplitString(data->GetValueAsString(), ';'); if (info.groups.back().empty()) info.groups.erase(info.groups.end() - 1); } else if (data->GetKey() == u"grpNameQBShowBricks") { - if (data->GetValueAsString().empty()) continue; - /*std::string groupStr = data->GetValueAsString(); - info.groups.push_back(groupStr);*/ info.grpNameQBShowBricks = data->GetValueAsString(); } else if (data->GetKey() == u"spawner_name") { info.name = data->GetValueAsString(); + } else if (data->GetKey() == u"weight") { + node->weight = GeneralUtils::TryParse(data->GetValueAsString(), 1); + if (node->weight <= 0) { + LOG("Found a spawner with a weight of <= 0, is this intentional? %s:%i", info.name.c_str(), node->nodeID); + node->weight = 1; + } } } + info.nodes.push_back(node); } info.templateID = path.spawner.spawnedLOT; @@ -500,7 +511,7 @@ void Zone::LoadPath(std::istream& file) { command.data = value; } else LOG("Tried to load invalid waypoint command '%s'", parameter.c_str()); } else { - waypoint.config.emplace_back(LDFBaseData::DataFromString(parameter + "=" + value)); + waypoint.config.ParseInsert(parameter + "=" + value); } } diff --git a/dZoneManager/Zone.h b/dZoneManager/Zone.h index fcd0c3ff..5f1a8d64 100644 --- a/dZoneManager/Zone.h +++ b/dZoneManager/Zone.h @@ -77,7 +77,7 @@ struct PathWaypoint { CameraPathWaypoint camera; RacingPathWaypoint racing; float speed{}; - std::vector config; + LwoNameValue config; std::vector commands; }; diff --git a/dZoneManager/dZMCommon.h b/dZoneManager/dZMCommon.h index 5acdc6b7..53f1d3c4 100644 --- a/dZoneManager/dZMCommon.h +++ b/dZoneManager/dZMCommon.h @@ -13,9 +13,8 @@ struct SceneObject { NiPoint3 position; NiQuaternion rotation = QuatUtils::IDENTITY; float scale = 1.0f; - //std::string settings; uint32_t value3; - std::vector settings; + LwoNameValue settings; }; #define LOT_MARKER_PLAYER_START 1931 diff --git a/docker-compose.yml b/docker-compose.yml index dbd16603..8e4cb760 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,6 +43,7 @@ services: - MYSQL_PASSWORD=${MARIADB_PASSWORD:?error} - EXTERNAL_IP=${EXTERNAL_IP:-localhost} - CLIENT_NET_VERSION=${CLIENT_NET_VERSION:-171022} + - MAXIMUM_OUTGOING_BANDWIDTH=${MAXIMUM_OUTGOING_BANDWIDTH:0} depends_on: - darkflamedb ports: diff --git a/docs/Commands.md b/docs/Commands.md index f9a00452..2d93adeb 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -1,51 +1,115 @@ - # In-game commands -* All commands are prefixed by `/` and typed in the in-game chat window. Some commands require elevated gmlevel privileges. Operands within `<>` are required, operands within `()` are not. + +* All commands are prefixed by `/` and typed in the in-game chat window. Some commands require elevated gmlevel privileges. Operands within `<>` are required, operands within `()` are optional. ## General Commands |Command|Usage|Description|Admin Level Requirement| |--- |--- |--- |--- | +|help|`/help (command/page)`|If a command is given, display detailed info on that command. Otherwise display a list of commands with short descriptions. Aliases: `/h`.|0| |credits|`/credits`|Displays the names of the people behind Darkflame Universe.|0| -|die|`/die`|Smashes the player.|0| -|info|`/info`|Displays server info to the user, including where to find the server's source code.|0| -|instanceinfo|`/instanceinfo`|Displays in the chat the current zone, clone, and instance id.|0| -|ping|`/ping (-l)`|Displays in chat your average ping. If the `-l` flag is used, the latest ping is displayed.|| -|pvp|`/pvp`|Toggle your PVP flag.|0| -|resurrect|`/resurrect`|Resurrects the player.|0| -|requestmailcount|`/requestmailcount`|Sends notification with number of unread messages in the player's mailbox.|0| -|who|`/who`|Displays in chat all players on the instance.|0| -|togglenameplate|`/togglenameplate`|Turns the nameplate above your head that is visible to other players off and on.|8 unless `allow_nameplate_off` is set to exactly `1` in the settings then admin level requirement is 0| -|toggleskipcinematics|`/toggleskipcinematics`|Skips mission and world load related cinematics.|8 unless `allow_players_to_skip_cinematics` is set to exactly `1` in the settings then admin level requirement is 0| +|die|`/die`|Smashes the player as if they were killed by something|0| +|info|`/info`|Displays server info to the user, including where to find the server's source code|0| +|instanceinfo|`/instanceinfo`|Display LWOZoneID info for the current zone|0| +|ping|`/ping (-l)`|Displays your average ping. If the `-l` flag is used, the latest ping is displayed.|0| +|pvp|`/pvp`|Toggle your PVP flag|0| +|requestmailcount|`/requestmailcount`|Sends notification with number of unread messages in the player's mailbox. Aliases: `/checkmail`.|0| +|who|`/who`|Displays all players on the instance|0| +|togglenameplate|`/togglenameplate`|Turns the nameplate above your head that is visible to other players off and on. This must be enabled by a server admin. Aliases: `/tnp`.|8 or 0 if `allow_nameplate_off` is 1| +|toggleskipcinematics|`/toggleskipcinematics`|Skips mission and world load related cinematics. This must be enabled by a server admin. Aliases: `/tsc`.|8 or 0 if `allow_players_to_skip_cinematics` is 1| + +## Client-handled Commands + +These commands are registered by the server so they appear in help, but their behavior is handled by the LEGO Universe client. + +### User-Client related +|Command|Usage|Description|Admin Level Requirement| +|--- |--- |--- |--- | +|faq|`/faq`|Show the LU FAQ Page. Aliases: `/faqs`.|0| +|camp|`/camp`|Returns you to the character select screen. Aliases: `/logoutcharacter`.|0| +|cancelqueue|`/cancelqueue`|Cancel Your position in the queue if you are in one.|0| +|exit|`/exit`|Exit to desktop. Aliases: `/quit`.|0| +|forums|`/forums`|Show the LU Forums!|0| +|loc|`/loc`|Output your current location on the map to the chat box. Aliases: `/locate`, `/location`.|0| +|logout|`/logout`|Returns you to the login screen. Aliases: `/logoutaccount`.|0| +|minigames|`/minigames`|Show the LEGO minigames page!|0| +|shop|`/shop`|Show the LEGO shop page. Aliases: `/store`.|0| +|perfoptionslow|`/perfoptionslow`|Sets the default low-spec performance options in the cfg file|0| +|perfoptionsmid|`/perfoptionsmid`|Sets the default medium-spec performance options in the cfg file|0| +|perfoptionshigh|`/perfoptionshigh`|Sets the default high-spec performance options in the cfg file|0| +|recommendedperfoptions|`/recommendedperfoptions`|Sets the recommended performance options in the cfg file|0| + + +### Friend-Team related +|Command|Usage|Description|Admin Level Requirement| +|--- |--- |--- |--- | +|addfriend|`/addfriend `|[name] Add a player to your friends list.|0| +|addignore|`/addignore `|[name] Add a player to your ignore list.|0| +|invite|`/invite `|[name] Invite a player to your team. Aliases: `/inviteteam`, `/teaminvite`, `/tinvite`.|0| +|kickplayer|`/kickplayer `|Kicks a player from your current team. The `/kick` alias is reserved by the moderation command. Aliases: `/teamkickplayer`, `/tkick`, `/tkickplayer`.|0| +|leader|`/leader `|[name] Set the leader for your current team. Aliases: `/setleader`, `/teamsetleader`, `/tleader`, `/tsetleader`.|0| +|leave|`/leave`|Leave your current team. Aliases: `/leaveteam`, `/teamleave`, `/tleave`.|0| +|removefriend|`/removefriend `|[name] Removes a player from your friends list.|0| +|removeIgnore|`/removeIgnore `|[name] Removes a player from your ignore list.|0| +|setloot|`/setloot `|[rr\|ffa] Set the loot for your current team (round-robin/free for all). Aliases: `/teamsetloot`, `/tloot`, `/tsetloot`.|0| +|s|`/s `|Say something outloud so that everyone can hear you. Aliases: `/say`.|0| +|team|`/team `|Send a message to your teammates. Aliases: `/t`.|0| +|tell|`/tell `|Send a private message to another player. Aliases: `/w`, `/whisper`.|0| + + +### Actions +|Command|Usage|Description|Admin Level Requirement| +|--- |--- |--- |--- | +|yes|`/yes`|Aye aye, captain!|0| +|sigh|`/sigh`|Another day, another brick.|0| +|shrug|`/shrug`|I dunno...|0| +|talk|`/talk`|Jibber Jabber|0| +|thumb|`/thumb`|Oh, yeah!. Aliases: `/thumbs`, `/thumbsup`.|0| +|cringe|`/cringe`|I don't even want to talk about it...|0| +|victory!|`/victory!`|Victory!|0| +|backflip|`/backflip`|Do a flip!|0| +|clap|`/clap`|A round of applause!|0| +|thanks|`/thanks`|Express your gratitude for another.|0| +|wave|`/wave`|Wave to other players.|0| +|why|`/why`|Why\|!?!!|0| +|gasp|`/gasp`|Oh my goodness!|0| +|cry|`/cry`|Show everyone your 'Aw' face.|0| +|dance|`/dance`|Dance 'til you can't dance no more.|0| +|giggle|`/giggle`|A good little chuckle|0| +|salute|`/salute`|For those about to build...|0| ## Moderation Commands |Command|Usage|Description|Admin Level Requirement| |--- |--- |--- |--- | -|gmlevel|`/gmlevel `|Within the authorized range of levels for the current account, changes the character's game master level to the specified value. This is required to use certain commands. Aliases: `/setgmlevel`, `/makegm`.|Account GM level greater than 0| -|kick|`/kick `|Kicks the player off the server.|2| -|mailitem|`/mailitem `|Mails an item to the given player. The mailed item has predetermined content. The sender name is set to "Darkflame Universe." The title of the message is "Lost item." The body of the message is "This is a replacement item for one you lost."|3| -|ban|`/ban `|Bans a user from the server.|4| -|approveproperty|`/approveproperty`|Approves the property the player is currently visiting.|5| +|gmlevel|`/gmlevel `|Within the authorized range of levels for the current account, changes the character's game master level to the specified value. This is required to use certain commands. Aliases: `/makegm`, `/setgmlevel`.|0; account GM level must be greater than 0| +|kick|`/kick `|Kicks the player off the server|2| +|mailitem|`/mailitem `|Mails an item to the given player. The mailed item has predetermined content. The sender name is set to "Darkflame Universe". The title of the message is "Lost item". The body of the message is "This is a replacement item for one you lost".|3| +|ban|`/ban `|Bans a user from the server|4| +|approveproperty|`/approveproperty`|Approves the property the player is currently visiting|5| |mute|`/mute (days) (hours)`|Mute player for the given amount of time. If no time is given, the mute is indefinite.|6| -|fly|`/fly `|This toggles your flying state with an optional parameter for the speed scale.|6| -|attackimmune|`/attackimmune `|Sets the character's immunity to basic attacks state, where value can be one of "1", to make yourself immune to basic attack damage, or "0" to undo.|8| -|gmimmune|`/gmimmunve `|Sets the character's GMImmune state, where value can be one of "1", to make yourself immune to damage, or "0" to undo.|8| -|gminvis|`/gminvis`|Toggles invisibility for the character, though it's currently a bit buggy. Requires nonzero GM Level for the character, but the account must have a GM level of 8.|8| -|setname|`/setname `|Sets a temporary name for your player. The name resets when you log out.|8| -|title|`/title `|Temporarily appends your player's name with " - <title>". This resets when you log out.|8| +|fly|`/fly <speed>`|Toggles your flying state with an optional parameter for the speed scale.|8| +|attackimmune|`/attackimmune <value>`|Sets the character's immunity to basic attacks state, where value can be one of "1", to make yourself immune to basic attack damage, or "0" to undo|8| +|gmimmune|`/gmimmune <value>`|Sets the character's GMImmune state, where value can be one of "1", to make yourself immune to damage, or "0" to undo|8| +|gminvis|`/gminvis`|Toggles invisibility for the character. Does not save across worlds.|1| +|setname|`/setname <name>`|Sets a temporary name for your player. The name resets when you log out|8| +|title|`/title <title>`|Temporarily appends your player's name with " - <title>". This resets when you log out|8| +|showall|`/showall (displayZoneData) (displayIndividualPlayers)`|Usage: /showall (displayZoneData: Default 1) (displayIndividualPlayers: Default 1)|2| +|findplayer|`/findplayer <player name>`|Find the World Server a player is in if they are online|2| +|spectate|`/spectate (player name)`|Specify a player name to spectate. They must be in the same world as you. Leave blank to stop spectating. Aliases: `/follow`.|2| ## Server Operation Commands |Command|Usage|Description|Admin Level Requirement| |--- |--- |--- |--- | -|announce|`/announce`|Sends a announcement. `/setanntitle` and `/setannmsg` must be called first to configure the announcement.|8| -|kill|`/kill <username>`|Smashes the character whom the given user is playing.|8| -|metrics|`/metrics`|Prints some information about the server's performance.|8| -|setannmsg|`/setannmsg <title>`|Sets the message of an announcement.|8| -|setanntitle|`/setanntitle <title>`|Sets the title of an announcement.|8| +|announce|`/announce`|Sends an announcement. `/setanntitle` and `/setannmsg` must be called first to configure the announcement.|8| +|kill|`/kill <username>`|Smashes the character whom the given user is playing|8| +|metrics|`/metrics`|Prints some information about the server's performance|8| +|setannmsg|`/setannmsg <message>`|Sets the message of an announcement. Use with `/setanntitle` and `/announce`|8| +|setanntitle|`/setanntitle <title>`|Sets the title of an announcement. Use with `/setannmsg` and `/announce`|8| +|shutdown|`/shutdown`|Shuts this world down|8| |shutdownuniverse|`/shutdownuniverse`|Sends a shutdown message to the master server. This will send an announcement to all players that the universe will shut down in 10 minutes.|9| -|uptime|`/uptime`|Displays the time the current world server has been active.|8| +|uptime|`/uptime`|Display the time the current world server has been active|8| ## Development Commands @@ -53,73 +117,75 @@ These commands are primarily for development and testing. The usage of many of t |Command|Usage|Description|Admin Level Requirement| |--- |--- |--- |--- | -|fix-stats|`/fix-stats`|Resets skills, buffs, and destroyables.|0| -|join|`/join <password>`|Joins a private zone with given password.|0| -|leave-zone|`/leave-zone` or <br> `/leavezone`|If you are in an instanced zone, transfers you to the closest main world. For example, if you are in an instance of Avant Gardens Survival or the Spider Queen Battle, you are sent to Avant Gardens. If you are in the Battle of Nimbus Station, you are sent to Nimbus Station.|0| +|fix-stats|`/fix-stats`|Resets skills, buffs, and destroyables|0| +|join|`/join <password>`|Join a private zone with given password|0| +|leave-zone|`/leave-zone`|If you are in an instanced zone, transfers you to the closest main world. For example, if you are in an instance of Avant Gardens Survival or the Spider Queen Battle, you are sent to Avant Gardens. If you are in the Battle of Nimbus Station, you are sent to Nimbus Station. Aliases: `/leavezone`.|0| +|resurrect|`/resurrect`|Resurrects the player.|8| |setminifig|`/setminifig <body part> <minifig item id>`|Alters your player's minifig. Body part can be one of "Eyebrows", "Eyes", "HairColor", "HairStyle", "Pants", "LeftHand", "Mouth", "RightHand", "Shirt", or "Hands". Changing minifig parts could break the character so this command is limited to GMs.|1| -|testmap|`/testmap <zone> (force) (clone-id)`|Transfers you to the given zone by id and clone id. Add "force" to skip checking if the zone is accessible (this can softlock your character, though, if you e.g. try to teleport to Frostburgh).|1| -|reportproxphys|`/reportproxphys`|Prints to console the position and radius of proximity sensors.|6| -|spawnphysicsverts|`/spawnphysicsverts`|Spawns a 1x1 brick at all vertices of phantom physics objects.|6| -|teleport|`/teleport <x/source player> (y) <z/target player>` or <br> `/tele <x/source player> (y) <z/target player>`|Teleports you. If no Y is given, you are teleported to the height of the terrain or physics object at (x, z). Any of the coordinates can use the syntax of an exact position (10.0), or a relative position (~+10.0). A ~ means use the current value of that axis as the base value. Addition or subtraction is supported (~+10) (~-10). If source player and target player are players that exist in the world, then the source player will be teleported to target player. Alias: `/tele`.|6| -|activatespawner|`/activatespawner <spawner name>`|Activates spawner by name.|8| +|testmap|`/testmap <zone> (clone-id) (instance-id) (spawn-point)`|Transfers you to the given zone by id and clone id and then spawns you at the specified spawn point if one was specified. Ignores instance-id for now. Aliases: `/tm`.|1| +|reportproxphys|`/reportproxphys`|Prints to console the position and radius of proximity sensors.|9| +|spawnphysicsverts|`/spawnphysicsverts`|Spawns a 1x1 brick at all vertices of phantom physics objects|8| +|teleport|`/teleport <x/source player> (y) <z/target player>`|Teleports you. If no Y is given, you are teleported to the height of the terrain or physics object at (x, z). Any of the coordinates can use the syntax of an exact position (10.0), or a relative position (~+10.0). A ~ means use the current value of that axis as the base value. Addition or subtraction is supported (~+10) (~-10). If source player and target player are players that exist in the world, then the source player will be teleported to target player. Aliases: `/tele`, `/tp`.|6| +|activatespawner|`/activatespawner <spawner name>`|Activates spawner by name|8| |addmission|`/addmission <mission id>`|Accepts the mission, adding it to your journal.|8| |boost|`/boost (time)`|Adds a passive boost action if you are in a vehicle. If time is given it will end after that amount of time|8| |unboost|`/unboost`|Removes a passive vehicle boost|8| -|buff|`/buff <id> <duration>`|Applies the buff with the given id for the given number of seconds.|8| -|buffme|`/buffme`|Sets health, armor, and imagination to 999.|8| -|buffmed|`/buffmed`|Sets health, armor, and imagination to 9.|8| -|clearflag|`/clearflag <flag id>`|Removes the given health or inventory flag from your player. Equivalent of calling `/setflag off <flag id>`.|8| -|completemission|`/completemission <mission id>`|Completes the mission, removing it from your journal.|8| -|createprivate|`/createprivate <zone id> <clone id> <password>`|Creates a private zone with password.|8| -|debugui|`/debugui`|Toggle Debug UI.|8| -|dismount|`/dismount`|Dismounts you from the vehicle or mount.|8| -|reloadconfig|`/reloadconfig`|Reloads the server with the new config values.|8| -|force-save|`/force-save`|While saving to database usually happens on regular intervals and when you disconnect from the server, this command saves your player's data to the database.|8| -|freecam|`/freecam`|Toggles freecam mode.|8| -|freemoney|`/freemoney <coins>`|Gives coins.|8| -|getnavmeshheight|`/getnavmeshheight`|Displays the navmesh height at your current position.|8| -|giveuscore|`/giveuscore <uscore>`|Gives uscore.|8| -|gmadditem|`/gmadditem <id> (count)`|Adds the given item to your inventory by id.|8| -|inspect|`/inspect <component or ldf variable or player name> (-m <waypoint> \| -a <animation> \| -s \| -p \| -f (faction) \| -t)`|Finds the closest entity with the given component or LDF variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the the IDs of all components the entity has. See [Detailed `/inspect` Usage](#detailed-inspect-usage) below.|8| -|list-spawns|`/list-spawns`|Lists all the character spawn points in the zone. Additionally, this command will display the current scene that plays when the character lands in the next zone, if there is one.|8| -|locrow|`/locrow`|Prints the your current position and rotation information to the console.|8| -|lookup|`/lookup <query>`|Searches through the Objects table in the client SQLite database for items whose display name, name, or description contains the query. Query can be multiple words delimited by spaces.|8| -|playanimation|`/playanimation <id>`|Plays animation with given ID. Alias: `/playanim`.|8| -|playeffect|`/playeffect <effect id> <effect type> <effect name>`|Plays an effect.|8| -|playlvlfx|`/playlvlfx`|Plays the level up animation on your character.|8| -|playrebuildfx|`/playrebuildfx`|Plays the quickbuild animation on your character.|8| -|pos|`/pos`|Displays your current position in chat and in the console.|8| -|refillstats|`/refillstats`|Refills health, armor, and imagination to their maximum level.|8| -|reforge|`/reforge <base item id> <reforged item id>`|Reforges an item.|8| -|resetmission|`/resetmission <mission id>`|Sets the state of the mission to accepted but not yet started.|8| -|rot|`/rot`|Displays your current rotation in chat and in the console.|8| +|buff|`/buff <id> <duration>`|Applies a buff with the given id for the given number of seconds|8| +|buffme|`/buffme`|Sets health, armor, and imagination to 999|8| +|buffmed|`/buffmed`|Sets health, armor, and imagination to 9|8| +|clearflag|`/clearflag <flag id>`|Removes the given health or inventory flag from your player. Equivalent of calling `/setflag off <flag id>`|8| +|completemission|`/completemission <mission id>`|Completes the mission, removing it from your journal|8| +|createprivate|`/createprivate <zone id> <clone id> <password>`|Creates a private zone with password|8| +|debugui|`/debugui`|Toggle Debug UI|8| +|dismount|`/dismount`|Dismounts you from the vehicle or mount|8| +|reloadconfig|`/reloadconfig`|Reloads the server with the new config values. Aliases: `/reload-config`.|8| +|forcesave|`/forcesave`|While saving to database usually happens on regular intervals and when you disconnect from the server, this command saves your player's data to the database. Aliases: `/force-save`.|8| +|freecam|`/freecam`|Toggles freecam mode|8| +|freemoney|`/freemoney <coins>`|Give yourself coins. Aliases: `/givemoney`, `/money`, `/givecoins`, `/coins`.|8| +|getnavmeshheight|`/getnavmeshheight`|Display the navmesh height at your current position|8| +|giveuscore|`/giveuscore <uscore>`|Gives uscore|8| +|gmadditem|`/gmadditem <id> (count)`|Adds the given item to your inventory by id. Aliases: `/give`.|8| +|inspect|`/inspect <component or ldf variable or player name> (-m <waypoint> | -a <animation> | -s | -p | -f (faction) | -t)`|Finds the closest entity with the given component or LNV variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the IDs of all components the entity has. Use `localCharacter` or `zoneControl` to inspect your current character or the zone control object.|8| +|list-spawns|`/list-spawns`|Lists all the character spawn points in the zone. Additionally, this command will display the current scene that plays when the character lands in the next zone, if there is one. Aliases: `/listspawns`.|8| +|locrow|`/locrow`|Prints your current position and rotation information to the console|8| +|lookup|`/lookup <query>`|Searches through the Objects table in the client SQLite database for items whose display name, name, or description contains the query. Query can be multiple words delimited by spaces.|8| +|playanimation|`/playanimation <id>`|Play an animation with given ID. Aliases: `/playanim`.|8| +|playeffect|`/playeffect <effect id> <effect type> <effect name>`|Plays an effect|8| +|playlvlfx|`/playlvlfx`|Plays the level up animation on your character|8| +|playrebuildfx|`/playrebuildfx`|Plays the quickbuild animation on your character|8| +|pos|`/pos`|Displays your current position in chat and in the console|8| +|refillstats|`/refillstats`|Refills health, armor, and imagination to their maximum level|8| +|reforge|`/reforge <base item id> <reforged item id>`|Reforges an item|8| +|resetmission|`/resetmission <mission id>`|Sets the state of the mission to accepted but not yet started|8| +|rot|`/rot`|Displays your current rotation in chat and in the console|8| |runmacro|`/runmacro <macro>`|Runs any command macro found in `./res/macros/`|8| -|setcontrolscheme|`/setcontrolscheme <scheme number>`|Sets the character control scheme to the specified number.|8| -|setcurrency|`/setcurrency <coins>`|Sets your coins.|8| -|setflag|`/setflag (value) <flag id>`|Sets the given inventory or health flag to the given value, where value can be one of "on" or "off". If no value is given, by default this adds the flag to your character (equivalent of calling `/setflag on <flag id>`).|8| -|setinventorysize|`/setinventorysize <size> (inventory)` or <br> `/setinvsize <size> (inventory)`|Sets your inventory size to the given size. If `inventory` is provided, the number or string will be used to set that inventory to the requested size. Alias: `/setinvsize`|8| -|setuistate|`/setuistate <ui state>`|Changes UI state.|8| -|spawn|`/spawn <id>`|Spawns an object at your location by id.|8| +|setcontrolscheme|`/setcontrolscheme <scheme number>`|Sets the character control scheme to the specified number|8| +|setcurrency|`/setcurrency <coins>`|Sets your coins. Aliases: `/setcoins`.|8| +|setflag|`/setflag (value) <flag id>`|Sets the given inventory or health flag to the given value, where value can be one of "on" or "off". If no value is given, by default this adds the flag to your character (equivalent of calling `/setflag on <flag id>`)|8| +|setinventorysize|`/setinventorysize <size> (inventory)`|Sets your inventory size to the given size. If `inventory` is provided, the number or string will be used to set that inventory to the requested size. Aliases: `/setinvsize`, `/setinvensize`.|8| +|setuistate|`/setuistate <ui state>`|Changes UI state|8| +|spawn|`/spawn <id>`|Spawns an object at your location by id|8| |spawngroup|`/spawngroup <id> <amount> <radius>`|Spawns `<amount>` of object `<id>` within the given `<radius>` from your location|8| -|speedboost|`/speedboost <amount>`|Sets the speed multiplier to the given amount. `/speedboost 1.5` will set the speed multiplier to 1.5x the normal speed.|8| -|startcelebration|`/startcelebration <id>`|Starts a celebration effect on your character.|8| -|stopeffect|`/stopeffect <effect id>`|Stops the given effect.|8| -|toggle|`/toggle <ui state>`|Toggles UI state.|8| -|tpall|`/tpall`|Teleports all characters to your current position.|8| -|triggerspawner|`/triggerspawner <spawner name>`|Triggers spawner by name.|8| -|unlock-emote|`/unlock-emote <emote id>`|Unlocks for your character the emote of the given id.|8| -|Set Level|`/setlevel <requested_level> (username)`|Sets the using entities level to the requested level. Takes an optional parameter of an in-game players username to set the level of.|8| -|setskillslot|`/setskillslot <slot> <skill id>`||8| +|speedboost|`/speedboost <amount>`|Sets the speed multiplier to the given amount. `/speedboost 1.5` will set the speed multiplier to 1.5x the normal speed|8| +|startcelebration|`/startcelebration <id>`|Starts a celebration effect on your character|8| +|stopeffect|`/stopeffect <effect id>`|Stops the given effect|8| +|toggle|`/toggle <ui state>`|Toggles UI state|8| +|tpall|`/tpall`|Teleports all characters to your current position|8| +|triggerspawner|`/triggerspawner <spawner name>`|Triggers spawner by name|8| +|unlock-emote|`/unlock-emote <emote id>`|Unlocks for your character the emote of the given id. Aliases: `/unlockemote`.|8| +|setlevel|`/setlevel <requested_level> (username)`|Sets the using entities level to the requested level. Takes an optional parameter of an in-game players username to set the level of|8| +|setskillslot|`/setskillslot <slot> <skill id>`|Set an action slot to a specific skill|8| |setfaction|`/setfaction <faction id>`|Clears the users current factions and sets it|8| |addfaction|`/addfaction <faction id>`|Add the faction to the users list of factions|8| |getfactions|`/getfactions`|Shows the player's factions|8| |setrewardcode|`/setrewardcode <code>`|Sets the rewardcode for the account you are logged into if it's a valid rewardcode, See cdclient table `RewardCodes`|8| -|barfight|`/barfight start`|Starts a barfight (turns everyones pvp on)|8| -|despawn|`/despawn <objectID>`|Despawns the entity objectID IF it was spawned in through a slash command.|8| -|execute|`/execute <subcommand> ... run <command>`|Execute commands with modified context (Minecraft-style). Subcommands: `as <playername>` (execute as different player), `at <playername>` (execute from player's position), `positioned <x> <y> <z>` (execute from coordinates - supports absolute coordinates like `100 200 300` or relative coordinates like `~5 ~10 ~` where `~` means current position). Example: `/execute as Player1 run pos`, `/execute positioned ~5 ~ ~-3 run spawn 1234`|8| -|crash|`/crash`|Crashes the server.|9| -|rollloot|`/rollloot <loot matrix index> <item id> <amount>`|Given a `loot matrix index`, look for `item id` in that matrix `amount` times and print to the chat box statistics of rolling that loot matrix.|9| -|castskill|`/castskill <skill id>`|Casts the skill as the player|9| +|barfight|`/barfight`|Starts a barfight (turns everyones pvp on)|8| +|despawn|`/despawn <object id>`|Despawns an object by id|8| +|execute|`/execute <subcommand> ... run <command>`|Execute commands as different entities or from different positions. Usage: /execute <subcommand> ... run <command>. Subcommands: as <entity>, at <entity>, positioned <x> <y> <z>. Aliases: `/exec`.|8| +|crash|`/crash`|Crashes the server. Aliases: `/pumpkin`.|9| +|rollloot|`/rollloot <loot matrix index> <item id> <amount>`|Given a `loot matrix index`, look for `item id` in that matrix `amount` times and print to the chat box statistics of rolling that loot matrix. Aliases: `/roll-loot`.|8| +|castskill|`/castskill <skill id>`|Casts the skill as the player|8| +|deleteinven|`/deleteinven <inventory>`|Delete all items from a specified inventory|8| ## Detailed `/inspect` Usage diff --git a/resources/chatconfig.ini b/resources/chatconfig.ini index 1391df44..7c6bc28c 100644 --- a/resources/chatconfig.ini +++ b/resources/chatconfig.ini @@ -12,3 +12,5 @@ web_server_enabled=0 # Unused for now # web_server_listen_ip=127.0.0.1 web_server_listen_port=2005 + +max_ignores=32 diff --git a/tests/dCommonTests/AMFDeserializeTests.cpp b/tests/dCommonTests/AMFDeserializeTests.cpp index 0a8c0ac7..759e6fc4 100644 --- a/tests/dCommonTests/AMFDeserializeTests.cpp +++ b/tests/dCommonTests/AMFDeserializeTests.cpp @@ -213,7 +213,7 @@ TEST(dCommonTests, AMFDeserializeUnimplementedValuesTest) { bool caughtException = false; try { ReadFromBitStream(testBitStream); - } catch (eAmf unimplementedValueType) { + } catch (std::exception& e) { caughtException = true; } diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index fe72f2da..61c13de2 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -70,7 +70,7 @@ endif() FetchContent_Declare( glm GIT_REPOSITORY https://github.com/g-truc/glm.git - GIT_TAG bf71a834948186f4097caa076cd2663c69a10e1e #refs/tags/1.0.1 + GIT_TAG 8d1fd52e5ab5590e2c81768ace50c72bae28f2ed #refs/tags/1.0.3 GIT_PROGRESS TRUE GIT_SHALLOW 1 ) diff --git a/thirdparty/mariadb-connector-cpp b/thirdparty/mariadb-connector-cpp index ef087399..4d0ea2ea 160000 --- a/thirdparty/mariadb-connector-cpp +++ b/thirdparty/mariadb-connector-cpp @@ -1 +1 @@ -Subproject commit ef0873998b3f94a4f76a485fb90b14866fbb99d4 +Subproject commit 4d0ea2ea0cdbba4cface18f1ab38dfcfec9aff8d diff --git a/thirdparty/recastnavigation b/thirdparty/recastnavigation index c5cbd530..9f4ce644 160000 --- a/thirdparty/recastnavigation +++ b/thirdparty/recastnavigation @@ -1 +1 @@ -Subproject commit c5cbd53024c8a9d8d097a4371215e3342d2fdc87 +Subproject commit 9f4ce64458dfae86e1239c525ddc219c4e9e06f1