mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-06-20 13:44:21 +00:00
Compare commits
60 Commits
object-deb
...
issue-1339
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd24e20165 | ||
|
|
54dc3a0b80 | ||
|
|
a156a8fcba | ||
|
|
f6c9a27a2b | ||
|
|
8e09ffd6e8 | ||
|
|
0c1808686c | ||
|
|
4d6a624da2 | ||
|
|
4ab09cf1aa | ||
|
|
4ef9f43266 | ||
|
|
f3a5add038 | ||
|
|
f5d33a773a | ||
|
|
67bbe4c1f0 | ||
|
|
482ff82656 | ||
|
|
8061f512aa | ||
|
|
247576e101 | ||
|
|
8dfdca7fbd | ||
|
|
8283d1fa95 | ||
|
|
434c9b6315 | ||
|
|
3c64b26c39 | ||
|
|
347b1d17d4 | ||
|
|
c723ce2588 | ||
|
|
66b7d3606e | ||
|
|
40fef36530 | ||
|
|
bf020baa17 | ||
|
|
a713216540 | ||
|
|
ea86a708e4 | ||
|
|
ca7424cbeb | ||
|
|
991e55f305 | ||
|
|
5410acffaa | ||
|
|
86f8601bbd | ||
|
|
4658318a3a | ||
|
|
11d44ffb98 | ||
|
|
2fb16420f3 | ||
|
|
96089a8d9a | ||
|
|
eac50acfcc | ||
|
|
ca60787055 | ||
|
|
396dcb0465 | ||
|
|
6e545eb1b9 | ||
|
|
46aac016fd | ||
|
|
83823fa64f | ||
|
|
0dd504c803 | ||
|
|
a70c365c23 | ||
|
|
281d9762ef | ||
|
|
002aa896d8 | ||
|
|
f3a5f60d81 | ||
|
|
4c9c773ec5 | ||
|
|
ec6253c80c | ||
|
|
c2dba31f70 | ||
|
|
74630b56c8 | ||
|
|
fd6029ae10 | ||
|
|
ff645a6662 | ||
|
|
e051229fb6 | ||
|
|
ce28834dce | ||
|
|
cbdd5d9bc6 | ||
|
|
62ac65c520 | ||
|
|
5d5bce53d0 | ||
|
|
5791c55a9e | ||
|
|
17d0c45382 | ||
|
|
7dbbef81ac | ||
|
|
06958cb9cd |
29
.github/copilot-instructions.md
vendored
Normal file
29
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# GitHub Copilot Instructions
|
||||
|
||||
* c++20 standard, please use the latest features except NO modules.
|
||||
* use `.contains` for searching in associative containers
|
||||
* use const as much as possible. If it can be const, it should be made const
|
||||
* DO NOT USE const_cast EVER.
|
||||
* use `cstdint` bitwidth types ALWAYS for integral types.
|
||||
* NEVER use std::wstring. If wide strings are necessary, use std::u16string with conversion utilties in GeneralUtils.h.
|
||||
* Functions are ALWAYS PascalCase.
|
||||
* local variables are camelCase
|
||||
* NEVER use snake case
|
||||
* indentation is TABS, not SPACES.
|
||||
* TABS are 4 spaces by default
|
||||
* Use trailing braces ALWAYS
|
||||
* global variables are prefixed with `g_`
|
||||
* if global variables or functions are needed, they should be located in an anonymous namespace
|
||||
* Use `GeneralUtils::TryParse` for ANY parsing of strings to integrals.
|
||||
* Use brace initialization when possible.
|
||||
* ALWAYS default initialize variables.
|
||||
* Pointers should be avoided unless necessary. Use references when the pointer has been checked and should not be null
|
||||
* headers should be as compact as possible. Do NOT include extra data that isnt needed.
|
||||
* Remember to include logs (LOG macro uses printf style logging) while putting verbose logs under LOG_DEBUG.
|
||||
* NEVER USE `RakNet::BitStream::ReadBit`
|
||||
* NEVER assume pointers are good, always check if they are null. Once a pointer is checked and is known to be non-null, further accesses no longer need checking
|
||||
* Be wary of TOCTOU. Prevent all possible issues relating to TOCTOU.
|
||||
* new memory allocations should never be used unless absolutely necessary.
|
||||
* new for reconstruction of objects is allowed
|
||||
* Prefer following the format of the file over correct formatting. Consistency over correctness.
|
||||
* When using auto, ALWAYS put a * for pointers.
|
||||
33
.github/workflows/build-and-push-docker.yml
vendored
33
.github/workflows/build-and-push-docker.yml
vendored
@@ -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
|
||||
|
||||
74
.github/workflows/build-and-test.yml
vendored
74
.github/workflows/build-and-test.yml
vendored
@@ -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)'
|
||||
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
|
||||
uses: lukka/get-cmake@591817e96fcad43505fb4eae36172462abb3a42e # v4.3.3
|
||||
with:
|
||||
cmakeVersion: "~3.25.0" # <--= optional, use most recent 3.25.x version
|
||||
cmakeVersion: "~3.25.0"
|
||||
- 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
|
||||
|
||||
93
.github/workflows/canary.yml
vendored
Normal file
93
.github/workflows/canary.yml
vendored
Normal file
@@ -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 }}
|
||||
38
.github/workflows/pr-title-check.yml
vendored
Normal file
38
.github/workflows/pr-title-check.yml
vendored
Normal file
@@ -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
|
||||
70
.github/workflows/release.yml
vendored
Normal file
70
.github/workflows/release.yml
vendored
Normal file
@@ -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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -126,3 +126,5 @@ docker-compose.override.yml
|
||||
# CMake scripts
|
||||
!cmake/*
|
||||
!cmake/toolchains/*
|
||||
.mcp.json
|
||||
.claude/
|
||||
|
||||
@@ -15,6 +15,11 @@ set(CMAKE_C_STANDARD 99)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
|
||||
endif()
|
||||
|
||||
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 +72,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)
|
||||
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
35
cliff.toml
Normal file
35
cliff.toml
Normal file
@@ -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"
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -176,6 +176,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];
|
||||
}
|
||||
|
||||
@@ -477,7 +477,7 @@ TeamData* TeamContainer::CreateLocalTeam(std::vector<LWOOBJID> members) {
|
||||
}
|
||||
}
|
||||
|
||||
newTeam->lootFlag = 1;
|
||||
newTeam->lootFlag = 0;
|
||||
|
||||
TeamStatusUpdate(newTeam);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <stdexcept>
|
||||
|
||||
#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<AMFBaseValue> 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<int32_t>(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<AMFArrayValue> 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));
|
||||
|
||||
@@ -374,6 +374,21 @@ public:
|
||||
return value->Insert<AmfType>("value", std::make_unique<AmfType>());
|
||||
}
|
||||
|
||||
AMFArrayValue& PushDebug(const NiPoint3& point) {
|
||||
PushDebug<AMFDoubleValue>("X") = point.x;
|
||||
PushDebug<AMFDoubleValue>("Y") = point.y;
|
||||
PushDebug<AMFDoubleValue>("Z") = point.z;
|
||||
return *this;
|
||||
}
|
||||
|
||||
AMFArrayValue& PushDebug(const NiQuaternion& rot) {
|
||||
PushDebug<AMFDoubleValue>("W") = rot.w;
|
||||
PushDebug<AMFDoubleValue>("X") = rot.x;
|
||||
PushDebug<AMFDoubleValue>("Y") = rot.y;
|
||||
PushDebug<AMFDoubleValue>("Z") = rot.z;
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* The associative portion. These values are key'd with strings to an AMFValue.
|
||||
|
||||
@@ -52,8 +52,7 @@ uint32_t BrickByBrickFix::TruncateBrokenBrickByBrickXml() {
|
||||
|
||||
if (actualUncompressedSize != -1) {
|
||||
uint32_t previousSize = completeUncompressedModel.size();
|
||||
completeUncompressedModel.append(reinterpret_cast<char*>(uncompressedChunk.get()));
|
||||
completeUncompressedModel.resize(previousSize + actualUncompressedSize);
|
||||
completeUncompressedModel.append(reinterpret_cast<char*>(uncompressedChunk.get()), actualUncompressedSize);
|
||||
} else {
|
||||
LOG("Failed to inflate chunk %i for model %llu. Error: %i", chunkCount, model.id, err);
|
||||
break;
|
||||
|
||||
@@ -308,8 +308,9 @@ std::vector<std::string> 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<uint32_t>(GeneralUtils::SplitString(filename, '_').at(0));
|
||||
if (migrationNumber.has_value()) filenames.emplace(migrationNumber.value(), std::move(filename));
|
||||
}
|
||||
|
||||
// Now sort the map by the oldest migration.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// C++
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "dPlatforms.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
#include "DluAssert.h"
|
||||
|
||||
#include <glm/ext/vector_float3.hpp>
|
||||
|
||||
@@ -204,6 +205,12 @@ namespace GeneralUtils {
|
||||
return isParsed ? static_cast<T>(result) : std::optional<T>{};
|
||||
}
|
||||
|
||||
// A version of TryParse that will return `errorVal` if `str` failed to parse.
|
||||
template <Numeric T>
|
||||
[[nodiscard]] T TryParse(std::string_view str, const T errorVal) {
|
||||
return TryParse<T>(str).value_or(errorVal);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
requires(!Numeric<T>)
|
||||
[[nodiscard]] std::optional<T> TryParse(std::string_view str);
|
||||
@@ -236,6 +243,12 @@ namespace GeneralUtils {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// A version of TryParse that will return `errorVal` if `str` failed to parse.
|
||||
template <std::floating_point T>
|
||||
[[nodiscard]] T TryParse(std::string_view str, const T errorVal) {
|
||||
return TryParse<T>(str).value_or(errorVal);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
@@ -257,6 +270,11 @@ namespace GeneralUtils {
|
||||
return z ? std::make_optional<T>(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<NiPoint3>(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
|
||||
@@ -267,6 +285,11 @@ namespace GeneralUtils {
|
||||
return (str.size() == 3) ? TryParse<T>(str[0], str[1], str[2]) : std::nullopt;
|
||||
}
|
||||
|
||||
// Alternative overload of TryParse with a default value
|
||||
[[nodiscard]] inline NiPoint3 TryParse(const std::span<const std::string> str, const NiPoint3 errorVal) {
|
||||
return TryParse<NiPoint3>(str).value_or(errorVal);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::u16string to_u16string(const T value) {
|
||||
return GeneralUtils::ASCIIToUTF16(std::to_string(value));
|
||||
@@ -305,7 +328,7 @@ namespace GeneralUtils {
|
||||
template<typename Container>
|
||||
inline Container::value_type GetRandomElement(const Container& container) {
|
||||
DluAssert(!container.empty());
|
||||
return container[GenerateRandomNumber<typename Container::value_type>(0, container.size() - 1)];
|
||||
return container[GenerateRandomNumber<typename Container::size_type>(0, container.size() - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -5,13 +5,43 @@
|
||||
#include "TinyXmlUtils.h"
|
||||
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
// The base LXFML xml file to use when creating new models.
|
||||
std::string g_base = R"(<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<LXFML versionMajor="5" versionMinor="0">
|
||||
<Meta>
|
||||
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
|
||||
<Brand name="LEGOUniverse"/>
|
||||
<BrickSet version="457"/>
|
||||
</Meta>
|
||||
<Bricks>
|
||||
</Bricks>
|
||||
<RigidSystems>
|
||||
</RigidSystems>
|
||||
<GroupSystems>
|
||||
<GroupSystem>
|
||||
</GroupSystem>
|
||||
</GroupSystems>
|
||||
</LXFML>)";
|
||||
}
|
||||
|
||||
Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) {
|
||||
Result toReturn;
|
||||
|
||||
// Handle empty or invalid input
|
||||
if (data.empty()) {
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
tinyxml2::XMLDocument doc;
|
||||
const auto err = doc.Parse(data.data());
|
||||
// Use length-based parsing to avoid expensive string copy
|
||||
const auto err = doc.Parse(data.data(), data.size());
|
||||
if (err != tinyxml2::XML_SUCCESS) {
|
||||
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
@@ -20,7 +50,6 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
|
||||
|
||||
auto lxfml = reader["LXFML"];
|
||||
if (!lxfml) {
|
||||
LOG("Failed to find LXFML element.");
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
@@ -49,16 +78,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
|
||||
// Calculate the lowest and highest points on the entire model
|
||||
for (const auto& transformation : transformations | std::views::values) {
|
||||
auto split = GeneralUtils::SplitString(transformation, ',');
|
||||
if (split.size() < 12) {
|
||||
LOG("Not enough in the split?");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value();
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value();
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value();
|
||||
if (x < lowest.x) lowest.x = x;
|
||||
if (y < lowest.y) lowest.y = y;
|
||||
if (split.size() < 12) continue;
|
||||
|
||||
auto xOpt = GeneralUtils::TryParse<float>(split[9]);
|
||||
auto yOpt = GeneralUtils::TryParse<float>(split[10]);
|
||||
auto zOpt = GeneralUtils::TryParse<float>(split[11]);
|
||||
|
||||
if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) continue;
|
||||
|
||||
auto x = xOpt.value();
|
||||
auto y = yOpt.value();
|
||||
auto z = zOpt.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;
|
||||
@@ -87,13 +119,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
|
||||
for (auto& transformation : transformations | std::views::values) {
|
||||
auto split = GeneralUtils::SplitString(transformation, ',');
|
||||
if (split.size() < 12) {
|
||||
LOG("Not enough in the split?");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value() - newRootPos.x + curPosition.x;
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y + curPosition.y;
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value() - newRootPos.z + curPosition.z;
|
||||
auto xOpt = GeneralUtils::TryParse<float>(split[9]);
|
||||
auto yOpt = GeneralUtils::TryParse<float>(split[10]);
|
||||
auto zOpt = GeneralUtils::TryParse<float>(split[11]);
|
||||
|
||||
if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) {
|
||||
continue;
|
||||
}
|
||||
auto x = xOpt.value() - newRootPos.x + curPosition.x;
|
||||
auto y = yOpt.value() - newRootPos.y + curPosition.y;
|
||||
auto z = zOpt.value() - newRootPos.z + curPosition.z;
|
||||
std::stringstream stream;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
stream << split[i];
|
||||
@@ -128,3 +166,345 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
|
||||
toReturn.center = newRootPos;
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
// Deep-clone an XMLElement (attributes, text, and child elements) into a target document
|
||||
// with maximum depth protection to prevent infinite loops
|
||||
static tinyxml2::XMLElement* CloneElementDeep(const tinyxml2::XMLElement* src, tinyxml2::XMLDocument& dstDoc, int maxDepth = 100) {
|
||||
if (!src || maxDepth <= 0) return nullptr;
|
||||
auto* dst = dstDoc.NewElement(src->Name());
|
||||
|
||||
// copy attributes
|
||||
for (const tinyxml2::XMLAttribute* attr = src->FirstAttribute(); attr; attr = attr->Next()) {
|
||||
dst->SetAttribute(attr->Name(), attr->Value());
|
||||
}
|
||||
|
||||
// copy children (elements and text)
|
||||
for (const tinyxml2::XMLNode* child = src->FirstChild(); child; child = child->NextSibling()) {
|
||||
if (const tinyxml2::XMLElement* childElem = child->ToElement()) {
|
||||
// Recursively clone child elements with decremented depth
|
||||
auto* clonedChild = CloneElementDeep(childElem, dstDoc, maxDepth - 1);
|
||||
if (clonedChild) dst->InsertEndChild(clonedChild);
|
||||
} else if (const tinyxml2::XMLText* txt = child->ToText()) {
|
||||
auto* n = dstDoc.NewText(txt->Value());
|
||||
dst->InsertEndChild(n);
|
||||
} else if (const tinyxml2::XMLComment* c = child->ToComment()) {
|
||||
auto* n = dstDoc.NewComment(c->Value());
|
||||
dst->InsertEndChild(n);
|
||||
}
|
||||
}
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
std::vector<Lxfml::Result> Lxfml::Split(const std::string_view data, const NiPoint3& curPosition) {
|
||||
std::vector<Result> results;
|
||||
|
||||
// Handle empty or invalid input
|
||||
if (data.empty()) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// Prevent processing extremely large inputs that could cause hangs
|
||||
if (data.size() > 10000000) { // 10MB limit
|
||||
return results;
|
||||
}
|
||||
|
||||
tinyxml2::XMLDocument doc;
|
||||
// Use length-based parsing to avoid expensive string copy
|
||||
const auto err = doc.Parse(data.data(), data.size());
|
||||
if (err != tinyxml2::XML_SUCCESS) {
|
||||
return results;
|
||||
}
|
||||
|
||||
auto* lxfml = doc.FirstChildElement("LXFML");
|
||||
if (!lxfml) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// Build maps: partRef -> Part element, partRef -> Brick element, boneRef -> partRef, brickRef -> Brick element
|
||||
std::unordered_map<std::string, tinyxml2::XMLElement*> partRefToPart;
|
||||
std::unordered_map<std::string, tinyxml2::XMLElement*> partRefToBrick;
|
||||
std::unordered_map<std::string, std::string> boneRefToPartRef;
|
||||
std::unordered_map<std::string, tinyxml2::XMLElement*> brickByRef;
|
||||
|
||||
auto* bricksParent = lxfml->FirstChildElement("Bricks");
|
||||
if (bricksParent) {
|
||||
for (auto* brick = bricksParent->FirstChildElement("Brick"); brick; brick = brick->NextSiblingElement("Brick")) {
|
||||
const char* brickRef = brick->Attribute("refID");
|
||||
if (brickRef) brickByRef.emplace(std::string(brickRef), brick);
|
||||
for (auto* part = brick->FirstChildElement("Part"); part; part = part->NextSiblingElement("Part")) {
|
||||
const char* partRef = part->Attribute("refID");
|
||||
if (partRef) {
|
||||
partRefToPart.emplace(std::string(partRef), part);
|
||||
partRefToBrick.emplace(std::string(partRef), brick);
|
||||
}
|
||||
auto* bone = part->FirstChildElement("Bone");
|
||||
if (bone) {
|
||||
const char* boneRef = bone->Attribute("refID");
|
||||
if (boneRef) boneRefToPartRef.emplace(std::string(boneRef), partRef ? std::string(partRef) : std::string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect RigidSystem elements
|
||||
std::vector<tinyxml2::XMLElement*> rigidSystems;
|
||||
auto* rigidSystemsParent = lxfml->FirstChildElement("RigidSystems");
|
||||
if (rigidSystemsParent) {
|
||||
for (auto* rs = rigidSystemsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
|
||||
rigidSystems.push_back(rs);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect top-level groups (immediate children of GroupSystem)
|
||||
std::vector<tinyxml2::XMLElement*> groupRoots;
|
||||
auto* groupSystemsParent = lxfml->FirstChildElement("GroupSystems");
|
||||
if (groupSystemsParent) {
|
||||
for (auto* gs = groupSystemsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) {
|
||||
for (auto* group = gs->FirstChildElement("Group"); group; group = group->NextSiblingElement("Group")) {
|
||||
groupRoots.push_back(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track used bricks and rigidsystems
|
||||
std::unordered_set<std::string> usedBrickRefs;
|
||||
std::unordered_set<tinyxml2::XMLElement*> usedRigidSystems;
|
||||
|
||||
// Track used groups to avoid processing them twice
|
||||
std::unordered_set<tinyxml2::XMLElement*> usedGroups;
|
||||
|
||||
// Helper to create output document from sets of brick refs and rigidsystem pointers
|
||||
auto makeOutput = [&](const std::unordered_set<std::string>& bricksToInclude, const std::vector<tinyxml2::XMLElement*>& rigidSystemsToInclude, const std::vector<tinyxml2::XMLElement*>& groupsToInclude = {}) {
|
||||
tinyxml2::XMLDocument outDoc;
|
||||
outDoc.Parse(g_base.c_str());
|
||||
auto* outRoot = outDoc.FirstChildElement("LXFML");
|
||||
auto* outBricks = outRoot->FirstChildElement("Bricks");
|
||||
auto* outRigidSystems = outRoot->FirstChildElement("RigidSystems");
|
||||
auto* outGroupSystems = outRoot->FirstChildElement("GroupSystems");
|
||||
|
||||
// clone and insert bricks
|
||||
for (const auto& bref : bricksToInclude) {
|
||||
auto it = brickByRef.find(bref);
|
||||
if (it == brickByRef.end()) continue;
|
||||
tinyxml2::XMLElement* cloned = CloneElementDeep(it->second, outDoc);
|
||||
if (cloned) outBricks->InsertEndChild(cloned);
|
||||
}
|
||||
|
||||
// clone and insert rigidsystems
|
||||
for (auto* rsPtr : rigidSystemsToInclude) {
|
||||
tinyxml2::XMLElement* cloned = CloneElementDeep(rsPtr, outDoc);
|
||||
if (cloned) outRigidSystems->InsertEndChild(cloned);
|
||||
}
|
||||
|
||||
// clone and insert group(s) if requested
|
||||
if (outGroupSystems && !groupsToInclude.empty()) {
|
||||
// clear default children
|
||||
while (outGroupSystems->FirstChild()) outGroupSystems->DeleteChild(outGroupSystems->FirstChild());
|
||||
// create a GroupSystem element and append requested groups
|
||||
auto* newGS = outDoc.NewElement("GroupSystem");
|
||||
for (auto* gptr : groupsToInclude) {
|
||||
tinyxml2::XMLElement* clonedG = CloneElementDeep(gptr, outDoc);
|
||||
if (clonedG) newGS->InsertEndChild(clonedG);
|
||||
}
|
||||
outGroupSystems->InsertEndChild(newGS);
|
||||
}
|
||||
|
||||
// Print to string
|
||||
tinyxml2::XMLPrinter printer;
|
||||
outDoc.Print(&printer);
|
||||
// Normalize position and compute center using existing helper
|
||||
std::string xmlString = printer.CStr();
|
||||
if (xmlString.size() > 5000000) { // 5MB limit for normalization
|
||||
Result emptyResult;
|
||||
emptyResult.lxfml = xmlString;
|
||||
return emptyResult;
|
||||
}
|
||||
auto normalized = NormalizePosition(xmlString, curPosition);
|
||||
return normalized;
|
||||
};
|
||||
|
||||
// 1) Process groups (each top-level Group becomes one output; nested groups are included)
|
||||
for (auto* groupRoot : groupRoots) {
|
||||
// Skip if this group was already processed as part of another group
|
||||
if (usedGroups.find(groupRoot) != usedGroups.end()) continue;
|
||||
|
||||
// Helper to collect all partRefs in a group's subtree
|
||||
std::function<void(const tinyxml2::XMLElement*, std::unordered_set<std::string>&)> collectParts = [&](const tinyxml2::XMLElement* g, std::unordered_set<std::string>& partRefs) {
|
||||
if (!g) return;
|
||||
const char* partAttr = g->Attribute("partRefs");
|
||||
if (partAttr) {
|
||||
for (auto& tok : GeneralUtils::SplitString(partAttr, ',')) partRefs.insert(tok);
|
||||
}
|
||||
for (auto* child = g->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectParts(child, partRefs);
|
||||
};
|
||||
|
||||
// Collect all groups that need to be merged into this output
|
||||
std::vector<tinyxml2::XMLElement*> groupsToInclude{ groupRoot };
|
||||
usedGroups.insert(groupRoot);
|
||||
|
||||
// Build initial sets of bricks and boneRefs from the starting group
|
||||
std::unordered_set<std::string> partRefs;
|
||||
collectParts(groupRoot, partRefs);
|
||||
|
||||
std::unordered_set<std::string> bricksIncluded;
|
||||
std::unordered_set<std::string> boneRefsIncluded;
|
||||
for (const auto& pref : partRefs) {
|
||||
auto pit = partRefToBrick.find(pref);
|
||||
if (pit != partRefToBrick.end()) {
|
||||
const char* bref = pit->second->Attribute("refID");
|
||||
if (bref) bricksIncluded.insert(std::string(bref));
|
||||
}
|
||||
auto partIt = partRefToPart.find(pref);
|
||||
if (partIt != partRefToPart.end()) {
|
||||
auto* bone = partIt->second->FirstChildElement("Bone");
|
||||
if (bone) {
|
||||
const char* bref = bone->Attribute("refID");
|
||||
if (bref) boneRefsIncluded.insert(std::string(bref));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iteratively include any RigidSystems that reference any boneRefsIncluded
|
||||
// and check if those rigid systems' bricks span other groups
|
||||
bool changed = true;
|
||||
std::vector<tinyxml2::XMLElement*> rigidSystemsToInclude;
|
||||
int maxIterations = 1000; // Safety limit to prevent infinite loops
|
||||
int iteration = 0;
|
||||
while (changed && iteration < maxIterations) {
|
||||
changed = false;
|
||||
iteration++;
|
||||
|
||||
// First, expand rigid systems based on current boneRefsIncluded
|
||||
for (auto* rs : rigidSystems) {
|
||||
if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue;
|
||||
// parse boneRefs of this rigid system (from its <Rigid> children)
|
||||
bool intersects = false;
|
||||
std::vector<std::string> rsBoneRefs;
|
||||
for (auto* rigid = rs->FirstChildElement("Rigid"); rigid; rigid = rigid->NextSiblingElement("Rigid")) {
|
||||
const char* battr = rigid->Attribute("boneRefs");
|
||||
if (!battr) continue;
|
||||
for (auto& tok : GeneralUtils::SplitString(battr, ',')) {
|
||||
rsBoneRefs.push_back(tok);
|
||||
if (boneRefsIncluded.find(tok) != boneRefsIncluded.end()) intersects = true;
|
||||
}
|
||||
}
|
||||
if (!intersects) continue;
|
||||
// include this rigid system and all boneRefs it references
|
||||
usedRigidSystems.insert(rs);
|
||||
rigidSystemsToInclude.push_back(rs);
|
||||
for (const auto& br : rsBoneRefs) {
|
||||
boneRefsIncluded.insert(br);
|
||||
auto bpIt = boneRefToPartRef.find(br);
|
||||
if (bpIt != boneRefToPartRef.end()) {
|
||||
auto partRef = bpIt->second;
|
||||
auto pbIt = partRefToBrick.find(partRef);
|
||||
if (pbIt != partRefToBrick.end()) {
|
||||
const char* bref = pbIt->second->Attribute("refID");
|
||||
if (bref && bricksIncluded.insert(std::string(bref)).second) changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second, check if the newly included bricks span any other groups
|
||||
// If so, merge those groups into the current output
|
||||
for (auto* otherGroup : groupRoots) {
|
||||
if (usedGroups.find(otherGroup) != usedGroups.end()) continue;
|
||||
|
||||
// Collect partRefs from this other group
|
||||
std::unordered_set<std::string> otherPartRefs;
|
||||
collectParts(otherGroup, otherPartRefs);
|
||||
|
||||
// Check if any of these partRefs correspond to bricks we've already included
|
||||
bool spansOtherGroup = false;
|
||||
for (const auto& pref : otherPartRefs) {
|
||||
auto pit = partRefToBrick.find(pref);
|
||||
if (pit != partRefToBrick.end()) {
|
||||
const char* bref = pit->second->Attribute("refID");
|
||||
if (bref && bricksIncluded.find(std::string(bref)) != bricksIncluded.end()) {
|
||||
spansOtherGroup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (spansOtherGroup) {
|
||||
// Merge this group into the current output
|
||||
usedGroups.insert(otherGroup);
|
||||
groupsToInclude.push_back(otherGroup);
|
||||
changed = true;
|
||||
|
||||
// Add all partRefs, boneRefs, and bricks from this group
|
||||
for (const auto& pref : otherPartRefs) {
|
||||
auto pit = partRefToBrick.find(pref);
|
||||
if (pit != partRefToBrick.end()) {
|
||||
const char* bref = pit->second->Attribute("refID");
|
||||
if (bref) bricksIncluded.insert(std::string(bref));
|
||||
}
|
||||
auto partIt = partRefToPart.find(pref);
|
||||
if (partIt != partRefToPart.end()) {
|
||||
auto* bone = partIt->second->FirstChildElement("Bone");
|
||||
if (bone) {
|
||||
const char* bref = bone->Attribute("refID");
|
||||
if (bref) boneRefsIncluded.insert(std::string(bref));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (iteration >= maxIterations) {
|
||||
// Iteration limit reached, stop processing to prevent infinite loops
|
||||
// The file is likely malformed, so just skip further processing
|
||||
return results;
|
||||
}
|
||||
// include bricks from bricksIncluded into used set
|
||||
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
|
||||
|
||||
// make output doc and push result (include all merged groups' XML)
|
||||
auto normalized = makeOutput(bricksIncluded, rigidSystemsToInclude, groupsToInclude);
|
||||
results.push_back(normalized);
|
||||
}
|
||||
|
||||
// 2) Process remaining RigidSystems (each becomes its own file)
|
||||
for (auto* rs : rigidSystems) {
|
||||
if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue;
|
||||
std::unordered_set<std::string> bricksIncluded;
|
||||
// collect boneRefs referenced by this rigid system
|
||||
for (auto* rigid = rs->FirstChildElement("Rigid"); rigid; rigid = rigid->NextSiblingElement("Rigid")) {
|
||||
const char* battr = rigid->Attribute("boneRefs");
|
||||
if (!battr) continue;
|
||||
for (auto& tok : GeneralUtils::SplitString(battr, ',')) {
|
||||
auto bpIt = boneRefToPartRef.find(tok);
|
||||
if (bpIt != boneRefToPartRef.end()) {
|
||||
auto partRef = bpIt->second;
|
||||
auto pbIt = partRefToBrick.find(partRef);
|
||||
if (pbIt != partRefToBrick.end()) {
|
||||
const char* bref = pbIt->second->Attribute("refID");
|
||||
if (bref) bricksIncluded.insert(std::string(bref));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// mark used
|
||||
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
|
||||
usedRigidSystems.insert(rs);
|
||||
|
||||
std::vector<tinyxml2::XMLElement*> rsVec{ rs };
|
||||
auto normalized = makeOutput(bricksIncluded, rsVec);
|
||||
results.push_back(normalized);
|
||||
}
|
||||
|
||||
// 3) Any remaining bricks not included become their own files
|
||||
for (const auto& [bref, brickPtr] : brickByRef) {
|
||||
if (usedBrickRefs.find(bref) != usedBrickRefs.end()) continue;
|
||||
std::unordered_set<std::string> bricksIncluded{ bref };
|
||||
auto normalized = makeOutput(bricksIncluded, {});
|
||||
results.push_back(normalized);
|
||||
usedBrickRefs.insert(bref);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "NiPoint3.h"
|
||||
|
||||
@@ -18,6 +19,7 @@ namespace Lxfml {
|
||||
// Normalizes a LXFML model to be positioned relative to its local 0, 0, 0 rather than a game worlds 0, 0, 0.
|
||||
// Returns a struct of its new center and the updated LXFML containing these edits.
|
||||
[[nodiscard]] Result NormalizePosition(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
|
||||
[[nodiscard]] std::vector<Result> Split(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
|
||||
|
||||
// these are only for the migrations due to a bug in one of the implementations.
|
||||
[[nodiscard]] Result NormalizePositionOnlyFirstPart(const std::string_view data);
|
||||
|
||||
@@ -53,16 +53,21 @@ Lxfml::Result Lxfml::NormalizePositionOnlyFirstPart(const std::string_view data)
|
||||
continue;
|
||||
}
|
||||
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value();
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value();
|
||||
auto z = GeneralUtils::TryParse<float>(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<float>(split[9]).value();
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value();
|
||||
auto z = GeneralUtils::TryParse<float>(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;
|
||||
|
||||
@@ -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<char*>(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<char*>(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<size_t>(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<uint8_t*>(toReturn.data()) + oldSize, MAX_UNCOMPRESSED_CHUNK_SIZE,
|
||||
error);
|
||||
|
||||
if (uncompressedSize == -1) {
|
||||
LOG("Failed to decompress chunk, aborting");
|
||||
toReturn = "";
|
||||
totalSize = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
totalSize += uncompressedSize;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
#include <cstdint>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,9 @@ public:
|
||||
[[nodiscard]]
|
||||
AssetStream GetFile(const char* name) const;
|
||||
|
||||
[[nodiscard]]
|
||||
AssetStream GetFile(const std::string& name) const { return GetFile(name.c_str()); };
|
||||
|
||||
private:
|
||||
void LoadPackIndex();
|
||||
|
||||
|
||||
@@ -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<char*>(malloc(size));
|
||||
int32_t readInData2 = fread(chunk, sizeof(int8_t), size, file);
|
||||
std::unique_ptr<char[]> 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<uint8_t*>(chunk), size, reinterpret_cast<uint8_t*>(decompressedData + currentReadPos), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err);
|
||||
const auto countToRead = ZCompression::Decompress(reinterpret_cast<uint8_t*>(chunk.get()), size, reinterpret_cast<uint8_t*>(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;
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#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<typename T>
|
||||
T GetValue(const std::string& key, const T emptyValue = T()) {
|
||||
return GeneralUtils::TryParse<T>(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<std::function<void()>> 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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -50,7 +50,10 @@ enum class eMissionState : int {
|
||||
/**
|
||||
* The mission has been completed before and has now been completed again. Used for daily missions.
|
||||
*/
|
||||
COMPLETE_READY_TO_COMPLETE = 12
|
||||
COMPLETE_READY_TO_COMPLETE = 12,
|
||||
|
||||
// The mission is failed (don't know where this is used)
|
||||
FAILED = 16,
|
||||
};
|
||||
|
||||
#endif //!__MISSIONSTATE__H__
|
||||
|
||||
@@ -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<CDActivities> CDActivitiesTable::Query(std::function<bool(CDActiviti
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
std::optional<const CDActivities> CDActivitiesTable::GetActivity(const uint32_t activityID) {
|
||||
auto& entries = GetEntries();
|
||||
for (const auto& entry : entries) {
|
||||
if (entry.ActivityID == activityID) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
// Custom Classes
|
||||
#include "CDTable.h"
|
||||
#include <optional>
|
||||
|
||||
struct CDActivities {
|
||||
uint32_t ActivityID;
|
||||
@@ -31,4 +32,5 @@ public:
|
||||
|
||||
// Queries the table with a custom "where" clause
|
||||
std::vector<CDActivities> Query(std::function<bool(CDActivities)> predicate);
|
||||
std::optional<const CDActivities> GetActivity(const uint32_t activityID);
|
||||
};
|
||||
|
||||
@@ -154,8 +154,8 @@ std::map<LOT, uint32_t> 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<LOT>(amountSplit[0], LOT_NULL),
|
||||
GeneralUtils::TryParse<uint32_t>(amountSplit[1], 0)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,13 +93,14 @@ std::vector<CDMissions> CDMissionsTable::Query(std::function<bool(CDMissions)> 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<CDMissions*>(&entry);
|
||||
toReturn = &entry;
|
||||
}
|
||||
}
|
||||
|
||||
return &Default;
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
const CDMissions& CDMissionsTable::GetByMissionID(uint32_t missionID, bool& found) const {
|
||||
|
||||
@@ -66,6 +66,7 @@ public:
|
||||
// Queries the table with a custom "where" clause
|
||||
std::vector<CDMissions> Query(std::function<bool(CDMissions)> predicate);
|
||||
|
||||
// Cannot be null.
|
||||
const CDMissions* GetPtrByMissionID(uint32_t missionID) const;
|
||||
|
||||
const CDMissions& GetByMissionID(uint32_t missionID, bool& found) const;
|
||||
|
||||
@@ -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", ""));
|
||||
|
||||
@@ -56,7 +56,7 @@ CDRailActivatorComponent CDRailActivatorComponentTable::GetEntryByID(int32_t id)
|
||||
std::pair<uint32_t, std::u16string> 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 {};
|
||||
|
||||
@@ -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<IProperty::Info> 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<IProperty::PropertyEntranceResult> 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;
|
||||
|
||||
@@ -9,6 +9,20 @@
|
||||
typedef std::unique_ptr<sql::PreparedStatement>& UniquePreppedStmtRef;
|
||||
typedef std::unique_ptr<sql::ResultSet> 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<sql::PreparedStatement> m_stmt;
|
||||
std::unique_ptr<sql::ResultSet> 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<typename ParamType>
|
||||
@@ -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<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override;
|
||||
IProperty::PropertyEntranceResult GetProperties(const IProperty::PropertyLookup& params) override;
|
||||
std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) override;
|
||||
std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override;
|
||||
std::vector<ILeaderboard::Entry> 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<typename... Args>
|
||||
inline std::unique_ptr<sql::ResultSet> ExecuteSelect(const std::string& query, Args&&... args) {
|
||||
std::unique_ptr<sql::PreparedStatement> preppedStmt(CreatePreppedStmt(query));
|
||||
SetParams(preppedStmt, std::forward<Args>(args)...);
|
||||
DLU_SQL_TRY_CATCH_RETHROW(return std::unique_ptr<sql::ResultSet>(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>(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<typename... Args>
|
||||
|
||||
@@ -12,7 +12,7 @@ std::vector<std::string> MySQLDatabase::GetApprovedCharacterNames() {
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
std::optional<ICharInfo::Info> CharInfoFromQueryResult(std::unique_ptr<sql::ResultSet> stmt) {
|
||||
std::optional<ICharInfo::Info> CharInfoFromQueryResult(PreparedStmtResultSet& stmt) {
|
||||
if (!stmt->next()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -31,15 +31,13 @@ std::optional<ICharInfo::Info> CharInfoFromQueryResult(std::unique_ptr<sql::Resu
|
||||
}
|
||||
|
||||
std::optional<ICharInfo::Info> 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<ICharInfo::Info> 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<LWOOBJID> MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accountId) {
|
||||
|
||||
@@ -14,7 +14,7 @@ std::optional<uint32_t> MySQLDatabase::GetDonationTotal(const uint32_t activityI
|
||||
return donation_total->getUInt("donation_total");
|
||||
}
|
||||
|
||||
std::vector<ILeaderboard::Entry> ProcessQuery(UniqueResultSet& rows) {
|
||||
std::vector<ILeaderboard::Entry> ProcessQuery(PreparedStmtResultSet& rows) {
|
||||
std::vector<ILeaderboard::Entry> entries;
|
||||
entries.reserve(rows->rowsCount());
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ std::vector<MailInfo> MySQLDatabase::GetMailForPlayer(const LWOOBJID characterId
|
||||
}
|
||||
|
||||
std::optional<MailInfo> 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<MailInfo> 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;
|
||||
}
|
||||
|
||||
@@ -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<IProperty::PropertyEntranceResult> MySQLDatabase::GetProperties(const IProperty::PropertyLookup& params) {
|
||||
std::optional<IProperty::PropertyEntranceResult> result;
|
||||
IProperty::PropertyEntranceResult MySQLDatabase::GetProperties(const IProperty::PropertyLookup& params) {
|
||||
IProperty::PropertyEntranceResult result;
|
||||
std::string query;
|
||||
std::unique_ptr<sql::ResultSet> properties;
|
||||
PreparedStmtResultSet properties;
|
||||
|
||||
if (params.sortChoice == SORT_TYPE_FEATURED || params.sortChoice == SORT_TYPE_FRIENDS) {
|
||||
query = R"QUERY(
|
||||
@@ -73,8 +73,7 @@ std::optional<IProperty::PropertyEntranceResult> 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<IProperty::PropertyEntranceResult> 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;
|
||||
|
||||
@@ -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 :/
|
||||
|
||||
@@ -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<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override;
|
||||
IProperty::PropertyEntranceResult GetProperties(const IProperty::PropertyLookup& params) override;
|
||||
std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) override;
|
||||
std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override;
|
||||
std::vector<ILeaderboard::Entry> 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<int32_t>(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<sqlite_int64>(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<sqlite_int64>(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<uint32_t> param) {
|
||||
if (param) {
|
||||
LOG("%d", param.value());
|
||||
LOG_DEBUG("%d", param.value());
|
||||
stmt.bind(index, static_cast<int>(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<u
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const std::optional<LWOOBJID> param) {
|
||||
if (param) {
|
||||
LOG("%d", param.value());
|
||||
LOG_DEBUG("%d", param.value());
|
||||
stmt.bind(index, static_cast<sqlite_int64>(param.value()));
|
||||
} else {
|
||||
LOG("Null");
|
||||
LOG_DEBUG("Null");
|
||||
stmt.bindNull(index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ std::vector<MailInfo> SQLiteDatabase::GetMailForPlayer(const LWOOBJID characterI
|
||||
}
|
||||
|
||||
std::optional<MailInfo> 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<MailInfo> 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;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ IProperty::Info ReadPropertyInfo(CppSQLite3Query& propertyEntry) {
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
std::optional<IProperty::PropertyEntranceResult> SQLiteDatabase::GetProperties(const IProperty::PropertyLookup& params) {
|
||||
std::optional<IProperty::PropertyEntranceResult> result;
|
||||
IProperty::PropertyEntranceResult SQLiteDatabase::GetProperties(const IProperty::PropertyLookup& params) {
|
||||
IProperty::PropertyEntranceResult result;
|
||||
std::string query;
|
||||
std::pair<CppSQLite3Statement, CppSQLite3Query> propertiesRes;
|
||||
|
||||
@@ -73,8 +73,7 @@ std::optional<IProperty::PropertyEntranceResult> 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<IProperty::PropertyEntranceResult> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override { return {}; };
|
||||
IProperty::PropertyEntranceResult GetProperties(const IProperty::PropertyLookup& params) override { return {}; };
|
||||
std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) override { return {}; };
|
||||
std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override { return {}; };
|
||||
std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) override { return {}; };
|
||||
|
||||
@@ -84,6 +84,8 @@
|
||||
#include "GhostComponent.h"
|
||||
#include "AchievementVendorComponent.h"
|
||||
#include "VanityUtilities.h"
|
||||
#include "ObjectIDManager.h"
|
||||
#include "ePlayerFlag.h"
|
||||
|
||||
// Table includes
|
||||
#include "CDComponentsRegistryTable.h"
|
||||
@@ -187,12 +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(MessageType::Game::REQUEST_SERVER_OBJECT_INFO, this, &Entity::MsgRequestServerObjectInfo);
|
||||
RegisterMsg(&Entity::MsgRequestServerObjectInfo);
|
||||
RegisterMsg(&Entity::MsgDropClientLoot);
|
||||
RegisterMsg(&Entity::MsgGetFactionTokenType);
|
||||
RegisterMsg(&Entity::MsgPickupItem);
|
||||
RegisterMsg(&Entity::MsgChildRemoved);
|
||||
RegisterMsg(&Entity::MsgGetFlag);
|
||||
/**
|
||||
* Setup trigger
|
||||
*/
|
||||
@@ -287,7 +298,7 @@ void Entity::Initialize() {
|
||||
AddComponent<LUPExhibitComponent>(lupExhibitID);
|
||||
}
|
||||
|
||||
const auto racingControlID =compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RACING_CONTROL);
|
||||
const auto racingControlID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RACING_CONTROL);
|
||||
if (racingControlID > 0) {
|
||||
AddComponent<RacingControlComponent>(racingControlID);
|
||||
}
|
||||
@@ -419,6 +430,7 @@ void Entity::Initialize() {
|
||||
comp->SetIsSmashable(destCompData[0].isSmashable);
|
||||
|
||||
comp->SetLootMatrixID(destCompData[0].LootMatrixIndex);
|
||||
comp->SetCurrencyIndex(destCompData[0].CurrencyIndex);
|
||||
Loot::CacheMatrix(destCompData[0].LootMatrixIndex);
|
||||
|
||||
// Now get currency information
|
||||
@@ -493,7 +505,7 @@ void Entity::Initialize() {
|
||||
auto& systemAddress = m_Character->GetParentUser() ? m_Character->GetParentUser()->GetSystemAddress() : UNASSIGNED_SYSTEM_ADDRESS;
|
||||
AddComponent<CharacterComponent>(characterID, m_Character, systemAddress)->LoadFromXml(m_Character->GetXMLDoc());
|
||||
|
||||
AddComponent<GhostComponent>(characterID);
|
||||
AddComponent<GhostComponent>(characterID)->LoadFromXml(m_Character->GetXMLDoc());
|
||||
}
|
||||
|
||||
const auto inventoryID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::INVENTORY);
|
||||
@@ -1663,7 +1675,7 @@ void Entity::AddLootItem(const Loot::Info& info) const {
|
||||
|
||||
auto* const characterComponent = GetComponent<CharacterComponent>();
|
||||
if (!characterComponent) return;
|
||||
|
||||
LOG("Player %llu has been allowed to pickup %i with id %llu", m_ObjectID, info.lot, info.id);
|
||||
auto& droppedLoot = characterComponent->GetDroppedLoot();
|
||||
droppedLoot[info.id] = info;
|
||||
}
|
||||
@@ -2240,13 +2252,14 @@ void Entity::RegisterMsg(const MessageType::Game msgId, std::function<bool(GameM
|
||||
m_MsgHandlers.emplace(msgId, handler);
|
||||
}
|
||||
|
||||
bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
|
||||
auto& requestInfo = static_cast<GameMessages::RequestServerObjectInfo&>(msg);
|
||||
bool Entity::MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfo& requestInfo) {
|
||||
AMFArrayValue response;
|
||||
response.Insert("visible", true);
|
||||
response.Insert("objectID", std::to_string(m_ObjectID));
|
||||
response.Insert("serverInfo", true);
|
||||
GameMessages::GetObjectReportInfo info{};
|
||||
info.clientID = requestInfo.clientId;
|
||||
info.bVerbose = requestInfo.bVerbose;
|
||||
info.info = response.InsertArray("data");
|
||||
auto& objectInfo = info.info->PushDebug("Object Details");
|
||||
auto* table = CDClientManager::GetTable<CDObjectsTable>();
|
||||
@@ -2260,17 +2273,87 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
|
||||
|
||||
auto& componentDetails = objectInfo.PushDebug("Component Information");
|
||||
for (const auto [id, component] : m_Components) {
|
||||
componentDetails.PushDebug<AMFStringValue>(StringifiedEnum::ToString(id)) = "";
|
||||
componentDetails.PushDebug(StringifiedEnum::ToString(id));
|
||||
}
|
||||
|
||||
auto& configData = objectInfo.PushDebug("Config Data");
|
||||
for (const auto config : m_Settings) {
|
||||
configData.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(config->GetKey())) = config->GetValueAsString();
|
||||
|
||||
}
|
||||
|
||||
HandleMsg(info);
|
||||
|
||||
auto* client = Game::entityManager->GetEntity(requestInfo.clientId);
|
||||
if (client) GameMessages::SendUIMessageServerToSingleClient("ToggleObjectDebugger", response, client->GetSystemAddress());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Entity::MsgDropClientLoot(GameMessages::DropClientLoot& dropLootMsg) {
|
||||
if (dropLootMsg.item != LOT_NULL && dropLootMsg.item != 0) {
|
||||
Loot::Info info{
|
||||
.id = dropLootMsg.lootID,
|
||||
.lot = dropLootMsg.item,
|
||||
.count = dropLootMsg.count,
|
||||
};
|
||||
AddLootItem(info);
|
||||
}
|
||||
|
||||
if (dropLootMsg.item == LOT_NULL && dropLootMsg.currency != 0) {
|
||||
RegisterCoinDrop(dropLootMsg.currency);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Entity::MsgGetFlag(GameMessages::GetFlag& flagMsg) {
|
||||
if (m_Character) flagMsg.flag = m_Character->GetPlayerFlag(flagMsg.flagID);
|
||||
return true;
|
||||
}
|
||||
bool Entity::MsgGetFactionTokenType(GameMessages::GetFactionTokenType& tokenMsg) {
|
||||
GameMessages::GetFlag getFlagMsg{};
|
||||
|
||||
getFlagMsg.flagID = ePlayerFlag::ASSEMBLY_FACTION;
|
||||
MsgGetFlag(getFlagMsg);
|
||||
if (getFlagMsg.flag) tokenMsg.tokenType = 8318;
|
||||
|
||||
getFlagMsg.flagID = ePlayerFlag::SENTINEL_FACTION;
|
||||
MsgGetFlag(getFlagMsg);
|
||||
if (getFlagMsg.flag) tokenMsg.tokenType = 8319;
|
||||
|
||||
getFlagMsg.flagID = ePlayerFlag::PARADOX_FACTION;
|
||||
MsgGetFlag(getFlagMsg);
|
||||
if (getFlagMsg.flag) tokenMsg.tokenType = 8320;
|
||||
|
||||
getFlagMsg.flagID = ePlayerFlag::VENTURE_FACTION;
|
||||
MsgGetFlag(getFlagMsg);
|
||||
if (getFlagMsg.flag) tokenMsg.tokenType = 8321;
|
||||
|
||||
LOG("Returning token type %i", tokenMsg.tokenType);
|
||||
return tokenMsg.tokenType != LOT_NULL;
|
||||
}
|
||||
|
||||
bool Entity::MsgPickupItem(GameMessages::PickupItem& pickupItemMsg) {
|
||||
if (GetObjectID() == pickupItemMsg.lootOwnerID) {
|
||||
PickupItem(pickupItemMsg.lootID);
|
||||
} else {
|
||||
auto* const characterComponent = GetComponent<CharacterComponent>();
|
||||
if (!characterComponent) return false;
|
||||
auto& droppedLoot = characterComponent->GetDroppedLoot();
|
||||
const auto it = droppedLoot.find(pickupItemMsg.lootID);
|
||||
if (it != droppedLoot.end()) {
|
||||
CDObjectsTable* objectsTable = CDClientManager::GetTable<CDObjectsTable>();
|
||||
const CDObjects& object = objectsTable->GetByID(it->second.lot);
|
||||
if (object.id != 0 && object.type == "Powerup") {
|
||||
return false; // Let powerups be duplicated
|
||||
}
|
||||
}
|
||||
droppedLoot.erase(pickupItemMsg.lootID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Entity::MsgChildRemoved(GameMessages::ChildRemoved& msg) {
|
||||
GetScript()->OnChildRemoved(*this, msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -106,6 +112,7 @@ public:
|
||||
|
||||
uint16_t GetNetworkId() const;
|
||||
|
||||
// Cannot return nullptr.
|
||||
Entity* GetOwner() const;
|
||||
|
||||
const NiPoint3& GetDefaultPosition() const;
|
||||
@@ -175,7 +182,12 @@ public:
|
||||
|
||||
void AddComponent(eReplicaComponentType componentId, Component* component);
|
||||
|
||||
bool MsgRequestServerObjectInfo(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;
|
||||
@@ -338,8 +350,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));
|
||||
// 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<typename DerivedGameMsg>
|
||||
inline void RegisterMsg(bool (Entity::* handler)(DerivedGameMsg&)) {
|
||||
static_assert(std::is_base_of_v<GameMessages::GameMsg, DerivedGameMsg>, "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<DerivedGameMsg&>(msg));
|
||||
};
|
||||
DerivedGameMsg msg;
|
||||
RegisterMsg(msg.msgId, castWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -600,5 +623,5 @@ auto Entity::GetComponents() const {
|
||||
|
||||
template<typename... T>
|
||||
auto Entity::GetComponentsMut() const {
|
||||
return std::tuple{GetComponent<T>()...};
|
||||
return std::tuple{ GetComponent<T>()... };
|
||||
}
|
||||
|
||||
@@ -87,6 +87,8 @@ void EntityManager::ReloadConfig() {
|
||||
auto hcXpReduction = Game::config->GetValue("hardcore_uscore_reduction");
|
||||
m_HardcoreUscoreReduction = hcXpReduction.empty() ? 1.0f : GeneralUtils::TryParse<float>(hcXpReduction).value_or(1.0f);
|
||||
m_HardcoreMode = GetHardcoreDisabledWorlds().contains(Game::zoneManager->GetZoneID().GetMapID()) ? false : m_HardcoreMode;
|
||||
auto hcCoinKeep = Game::config->GetValue("hardcore_coin_keep");
|
||||
m_HardcoreCoinKeep = hcCoinKeep.empty() ? false : GeneralUtils::TryParse<float>(hcCoinKeep).value_or(0.0f);
|
||||
}
|
||||
|
||||
void EntityManager::Initialize() {
|
||||
@@ -359,16 +361,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);
|
||||
}
|
||||
@@ -377,10 +387,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;
|
||||
}
|
||||
}
|
||||
@@ -411,14 +419,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);
|
||||
|
||||
@@ -486,11 +489,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);
|
||||
}
|
||||
|
||||
@@ -517,6 +516,7 @@ void EntityManager::UpdateGhosting(Entity* player) {
|
||||
|
||||
const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint);
|
||||
|
||||
|
||||
auto ghostingDistanceMax = m_GhostDistanceMaxSquared;
|
||||
auto ghostingDistanceMin = m_GhostDistanceMinSqaured;
|
||||
|
||||
@@ -553,35 +553,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<GhostComponent>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ public:
|
||||
const std::set<LOT>& GetHardcoreUscoreReducedLots() const { return m_HardcoreUscoreReducedLots; };
|
||||
const std::set<LOT>& GetHardcoreUscoreExcludedEnemies() const { return m_HardcoreUscoreExcludedEnemies; };
|
||||
const std::set<LWOMAPID>& GetHardcoreDisabledWorlds() const { return m_HardcoreDisabledWorlds; };
|
||||
float GetHardcoreCoinKeep() const { return m_HardcoreCoinKeep; }
|
||||
|
||||
// Messaging
|
||||
bool SendMessage(GameMessages::GameMsg& msg) const;
|
||||
@@ -125,6 +126,7 @@ private:
|
||||
std::set<LOT> m_HardcoreUscoreReducedLots{};
|
||||
std::set<LOT> m_HardcoreUscoreExcludedEnemies{};
|
||||
std::set<LWOMAPID> m_HardcoreDisabledWorlds{};
|
||||
float m_HardcoreCoinKeep{};
|
||||
};
|
||||
|
||||
#endif // ENTITYMANAGER_H
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,6 +9,16 @@ Team::Team() {
|
||||
lootOption = Game::config->GetValue("default_team_loot") == "0" ? 0 : 1;
|
||||
}
|
||||
|
||||
LWOOBJID Team::GetNextLootOwner() {
|
||||
lootRound++;
|
||||
|
||||
if (lootRound >= members.size()) {
|
||||
lootRound = 0;
|
||||
}
|
||||
|
||||
return members[lootRound];
|
||||
}
|
||||
|
||||
TeamManager::TeamManager() {
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
struct Team {
|
||||
Team();
|
||||
|
||||
LWOOBJID GetNextLootOwner();
|
||||
LWOOBJID teamID = LWOOBJID_EMPTY;
|
||||
char lootOption = 0;
|
||||
std::vector<LWOOBJID> members{};
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "BitStreamUtils.h"
|
||||
#include "CheatDetection.h"
|
||||
#include "CharacterComponent.h"
|
||||
#include "dConfig.h"
|
||||
#include "eCharacterVersion.h"
|
||||
|
||||
UserManager* UserManager::m_Address = nullptr;
|
||||
@@ -92,6 +93,23 @@ void UserManager::Initialize() {
|
||||
StripCR(line);
|
||||
m_PreapprovedNames.push_back(line);
|
||||
}
|
||||
|
||||
// Initialize cached config values and register a handler to update them on config reload
|
||||
// This avoids repeated lookups into dConfig at runtime.
|
||||
if (Game::config) {
|
||||
m_MuteAutoRejectNames = (Game::config->GetValue("mute_auto_reject_names") == "1");
|
||||
m_MuteRestrictTrade = (Game::config->GetValue("mute_restrict_trade") == "1");
|
||||
m_MuteRestrictMail = (Game::config->GetValue("mute_restrict_mail") == "1");
|
||||
|
||||
Game::config->AddConfigHandler([this]() {
|
||||
this->m_MuteAutoRejectNames = (Game::config->GetValue("mute_auto_reject_names") == "1");
|
||||
this->m_MuteRestrictTrade = (Game::config->GetValue("mute_restrict_trade") == "1");
|
||||
this->m_MuteRestrictMail = (Game::config->GetValue("mute_restrict_mail") == "1");
|
||||
});
|
||||
}
|
||||
else {
|
||||
LOG("Warning: dConfig not initialized before UserManager. Cached config values will not be available.");
|
||||
}
|
||||
}
|
||||
|
||||
UserManager::~UserManager() {
|
||||
@@ -301,7 +319,9 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
|
||||
inStream.Read(eyes);
|
||||
inStream.Read(mouth);
|
||||
|
||||
const auto name = LUWStringName.GetAsString();
|
||||
const bool autoRejectNames = this->GetMuteAutoRejectNames() && u->GetIsMuted();
|
||||
|
||||
const auto name = autoRejectNames ? "" : LUWStringName.GetAsString();
|
||||
std::string predefinedName = GetPredefinedName(firstNameIndex, middleNameIndex, lastNameIndex);
|
||||
|
||||
LOT shirtLOT = FindCharShirtID(shirtColor, shirtStyle);
|
||||
@@ -319,6 +339,10 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoRejectNames) {
|
||||
LOG("AccountID: %i is muted, forcing use of predefined name", u->GetAccountID());
|
||||
}
|
||||
|
||||
if (name.empty()) {
|
||||
LOG("AccountID: %i is creating a character with predefined name: %s", u->GetAccountID(), predefinedName.c_str());
|
||||
} else {
|
||||
@@ -369,6 +393,7 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
|
||||
|
||||
//Check to see if our name was pre-approved:
|
||||
bool nameOk = IsNamePreapproved(name);
|
||||
|
||||
if (!nameOk && u->GetMaxGMLevel() > eGameMasterLevel::FORUM_MODERATOR) nameOk = true;
|
||||
|
||||
// If predefined name is invalid, change it to be their object id
|
||||
@@ -448,9 +473,10 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet)
|
||||
|
||||
LUWString LUWStringName;
|
||||
inStream.Read(LUWStringName);
|
||||
const auto newName = LUWStringName.GetAsString();
|
||||
auto newName = LUWStringName.GetAsString();
|
||||
|
||||
Character* character = nullptr;
|
||||
const bool autoRejectNames = this->GetMuteAutoRejectNames() && u->GetIsMuted();
|
||||
|
||||
//Check if this user has this character:
|
||||
bool ownsCharacter = CheatDetection::VerifyLwoobjidIsSender(
|
||||
@@ -471,13 +497,30 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet)
|
||||
if (!ownsCharacter || !character) {
|
||||
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::UNKNOWN_ERROR);
|
||||
} else if (ownsCharacter && character) {
|
||||
if (autoRejectNames) {
|
||||
// Create a random preapproved name (fallback to default if none available)
|
||||
if (!m_FirstNames.empty() && !m_MiddleNames.empty() && !m_LastNames.empty()) {
|
||||
std::string firstName = GeneralUtils::GetRandomElement(m_FirstNames);
|
||||
std::string middleName = GeneralUtils::GetRandomElement(m_MiddleNames);
|
||||
std::string lastName = GeneralUtils::GetRandomElement(m_LastNames);
|
||||
newName = firstName + middleName + lastName;
|
||||
} else {
|
||||
newName = "character" + std::to_string(objectID);
|
||||
}
|
||||
}
|
||||
|
||||
if (newName == character->GetName()) {
|
||||
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::NAME_UNAVAILABLE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Database::Get()->GetCharacterInfo(newName)) {
|
||||
if (IsNamePreapproved(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());
|
||||
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);
|
||||
UserManager::RequestCharacterList(sysAddr);
|
||||
} else if (IsNamePreapproved(newName)) {
|
||||
Database::Get()->SetCharacterName(objectID, newName);
|
||||
LOG("Character %s now known as %s", character->GetName().c_str(), newName.c_str());
|
||||
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);
|
||||
|
||||
@@ -41,6 +41,11 @@ public:
|
||||
|
||||
size_t GetUserCount() const { return m_Users.size(); }
|
||||
|
||||
// Access cached config values
|
||||
bool GetMuteAutoRejectNames() const { return m_MuteAutoRejectNames; }
|
||||
bool GetMuteRestrictTrade() const { return m_MuteRestrictTrade; }
|
||||
bool GetMuteRestrictMail() const { return m_MuteRestrictMail; }
|
||||
|
||||
private:
|
||||
static UserManager* m_Address; //Singleton
|
||||
std::map<SystemAddress, User*> m_Users;
|
||||
@@ -50,6 +55,11 @@ private:
|
||||
std::vector<std::string> m_MiddleNames;
|
||||
std::vector<std::string> m_LastNames;
|
||||
std::vector<std::string> m_PreapprovedNames;
|
||||
|
||||
// Cached config values that can change on config reload
|
||||
bool m_MuteAutoRejectNames = false;
|
||||
bool m_MuteRestrictTrade = false;
|
||||
bool m_MuteRestrictMail = false;
|
||||
};
|
||||
|
||||
#endif // USERMANAGER_H
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -15,4 +15,9 @@ public:
|
||||
void Load() override;
|
||||
private:
|
||||
float m_Timeout;
|
||||
|
||||
Behavior* m_GroundAction{};
|
||||
Behavior* m_HitAction{};
|
||||
Behavior* m_HitActionEnemy{};
|
||||
Behavior* m_TimeoutAction{};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<int32_t>(static_cast<int32_t>(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;
|
||||
}
|
||||
|
||||
@@ -95,4 +95,6 @@ public:
|
||||
|
||||
Behavior& operator=(const Behavior& other) = default;
|
||||
Behavior& operator=(Behavior&& other) = default;
|
||||
|
||||
uint32_t GetBehaviorID() const { return m_behaviorId; }
|
||||
};
|
||||
|
||||
@@ -48,15 +48,13 @@ void BlockBehavior::UnCast(BehaviorContext* context, BehaviorBranchContext branc
|
||||
return;
|
||||
}
|
||||
|
||||
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
|
||||
auto* const destroyableComponent = entity->GetComponent<DestroyableComponent>();
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -195,8 +208,8 @@ void TacArcBehavior::Load() {
|
||||
GetFloat("offset_z", 0.0f)
|
||||
);
|
||||
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);
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
#include "Amf3.h"
|
||||
|
||||
ActivityComponent::ActivityComponent(Entity* parent, int32_t componentID) : Component(parent, componentID) {
|
||||
using namespace GameMessages;
|
||||
RegisterMsg<GetObjectReportInfo>(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
|
||||
@@ -45,33 +44,6 @@ ActivityComponent::ActivityComponent(Entity* parent, int32_t componentID) : Comp
|
||||
m_ActivityID = parent->GetVar<int32_t>(u"activityID");
|
||||
LoadActivityData(m_ActivityID);
|
||||
}
|
||||
|
||||
auto* destroyableComponent = m_Parent->GetComponent<DestroyableComponent>();
|
||||
|
||||
if (destroyableComponent) {
|
||||
// First lookup the loot matrix id for this component id.
|
||||
CDActivityRewardsTable* activityRewardsTable = CDClientManager::GetTable<CDActivityRewardsTable>();
|
||||
std::vector<CDActivityRewards> activityRewards = activityRewardsTable->Query([=](CDActivityRewards entry) {return (entry.LootMatrixIndex == destroyableComponent->GetLootMatrixID()); });
|
||||
|
||||
uint32_t startingLMI = 0;
|
||||
|
||||
// If we have one, set the starting loot matrix id to that.
|
||||
if (activityRewards.size() > 0) {
|
||||
startingLMI = activityRewards[0].LootMatrixIndex;
|
||||
}
|
||||
|
||||
if (startingLMI > 0) {
|
||||
// We may have more than 1 loot matrix index to use depending ont the size of the team that is looting the activity.
|
||||
// So this logic will get the rest of the loot matrix indices for this activity.
|
||||
|
||||
std::vector<CDActivityRewards> objectTemplateActivities = activityRewardsTable->Query([=](CDActivityRewards entry) {return (activityRewards[0].objectTemplate == entry.objectTemplate); });
|
||||
for (const auto& item : objectTemplateActivities) {
|
||||
if (item.activityRating > 0 && item.activityRating < 5) {
|
||||
m_ActivityLootMatrices.insert({ item.activityRating, item.LootMatrixIndex });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
void ActivityComponent::LoadActivityData(const int32_t activityId) {
|
||||
CDActivitiesTable* activitiesTable = CDClientManager::GetTable<CDActivitiesTable>();
|
||||
@@ -352,9 +324,8 @@ bool ActivityComponent::CheckCost(Entity* player) const {
|
||||
}
|
||||
|
||||
bool ActivityComponent::TakeCost(Entity* player) const {
|
||||
|
||||
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
|
||||
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) {
|
||||
@@ -618,9 +589,7 @@ Entity* LobbyPlayer::GetEntity() const {
|
||||
return Game::entityManager->GetEntity(entityID);
|
||||
}
|
||||
|
||||
bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
|
||||
auto& reportInfo = static_cast<GameMessages::GetObjectReportInfo&>(msg);
|
||||
|
||||
bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
||||
auto& activityInfo = reportInfo.info->PushDebug("Activity");
|
||||
|
||||
auto& instances = activityInfo.PushDebug("Instances: " + std::to_string(m_Instances.size()));
|
||||
@@ -698,10 +667,6 @@ bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
|
||||
}
|
||||
}
|
||||
|
||||
auto& lootMatrices = activityInfo.PushDebug("Loot Matrices");
|
||||
for (const auto& [activityRating, lootMatrixID] : m_ActivityLootMatrices) {
|
||||
lootMatrices.PushDebug<AMFIntValue>("Loot Matrix " + std::to_string(activityRating)) = lootMatrixID;
|
||||
}
|
||||
activityInfo.PushDebug<AMFIntValue>("ActivityID") = m_ActivityID;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -215,6 +215,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
|
||||
@@ -341,15 +345,9 @@ public:
|
||||
*/
|
||||
void SetInstanceMapID(uint32_t mapID) { m_ActivityInfo.instanceMapID = mapID; };
|
||||
|
||||
/**
|
||||
* Returns the LMI that this activity points to for a team size
|
||||
* @param teamSize the team size to get the LMI for
|
||||
* @return the LMI that this activity points to for a team size
|
||||
*/
|
||||
uint32_t GetLootMatrixForTeamSize(uint32_t teamSize) { return m_ActivityLootMatrices[teamSize]; }
|
||||
private:
|
||||
|
||||
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
|
||||
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg);
|
||||
/**
|
||||
* The database information for this activity
|
||||
*/
|
||||
@@ -370,11 +368,6 @@ private:
|
||||
*/
|
||||
std::vector<ActivityPlayer*> m_ActivityPlayers;
|
||||
|
||||
/**
|
||||
* LMIs for team sizes
|
||||
*/
|
||||
std::unordered_map<uint32_t, uint32_t> m_ActivityLootMatrices;
|
||||
|
||||
/**
|
||||
* The activity id
|
||||
*/
|
||||
|
||||
@@ -27,8 +27,10 @@
|
||||
#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;
|
||||
@@ -476,6 +478,7 @@ std::vector<LWOOBJID> 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());
|
||||
|
||||
@@ -839,3 +842,72 @@ 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<AMFIntValue>("Component ID") = GetComponentID();
|
||||
auto& targetInfo = cmptType.PushDebug("Current Target Info");
|
||||
targetInfo.PushDebug<AMFStringValue>("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<AMFDoubleValue>("Combat Round Time") = m_CombatRoundLength;
|
||||
// roundInfo.PushDebug<AMFDoubleValue>("Minimum Time") = m_MinRoundLength;
|
||||
// roundInfo.PushDebug<AMFDoubleValue>("Maximum Time") = m_MaxRoundLength;
|
||||
// roundInfo.PushDebug<AMFDoubleValue>("Selected Time") = m_SelectedTime;
|
||||
// roundInfo.PushDebug<AMFDoubleValue>("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<AMFStringValue>("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<AMFDoubleValue>("X") = m_StartPosition.x;
|
||||
tetherPoint.PushDebug<AMFDoubleValue>("Y") = m_StartPosition.y;
|
||||
tetherPoint.PushDebug<AMFDoubleValue>("Z") = m_StartPosition.z;
|
||||
cmptType.PushDebug<AMFDoubleValue>("Hard Tether Radius") = m_HardTetherRadius;
|
||||
cmptType.PushDebug<AMFDoubleValue>("Soft Tether Radius") = m_SoftTetherRadius;
|
||||
cmptType.PushDebug<AMFDoubleValue>("Aggro Radius") = m_AggroRadius;
|
||||
cmptType.PushDebug<AMFDoubleValue>("Tether Speed") = m_TetherSpeed;
|
||||
cmptType.PushDebug<AMFDoubleValue>("Aggro Speed") = m_TetherSpeed;
|
||||
// cmptType.PushDebug<AMFDoubleValue>("Specified Min Range") = m_SpecificMinRange;
|
||||
// cmptType.PushDebug<AMFDoubleValue>("Specified Max Range") = m_SpecificMaxRange;
|
||||
auto& threats = cmptType.PushDebug("Target Threats");
|
||||
for (const auto& [id, threat] : m_ThreatEntries) {
|
||||
threats.PushDebug<AMFDoubleValue>(std::to_string(id)) = threat;
|
||||
}
|
||||
|
||||
auto& ignoredThreats = cmptType.PushDebug("Temp Ignored Threats");
|
||||
for (const auto& [id, threat] : m_ThreatEntries) {
|
||||
ignoredThreats.PushDebug<AMFDoubleValue>(std::to_string(id) + " - Time") = threat;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -234,6 +234,8 @@ public:
|
||||
// Ignore a threat for a certain amount of time
|
||||
void IgnoreThreat(const LWOOBJID target, const float time);
|
||||
|
||||
bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Returns the current target or the target that currently is the largest threat to this entity
|
||||
|
||||
@@ -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<NiPoint3>(
|
||||
GeneralUtils::SplitString(m_Parent->GetVarAsString(u"bouncer_destination"), '\x1f'))
|
||||
.value_or(NiPoint3Constant::ZERO);
|
||||
m_Speed = GeneralUtils::TryParse<float>(m_Parent->GetVarAsString(u"bouncer_speed")).value_or(-1.0f);
|
||||
m_UsesHighArc = GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"bouncer_uses_high_arc")).value_or(false);
|
||||
m_LockControls = GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"lock_controls")).value_or(false);
|
||||
m_IgnoreCollision = !GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"ignore_collision")).value_or(true);
|
||||
m_StickLanding = GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"stickLanding")).value_or(false);
|
||||
m_UsesGroupName = GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"uses_group_name")).value_or(false);
|
||||
m_GroupName = m_Parent->GetVarAsString(u"grp_name");
|
||||
m_MinNumTargets = GeneralUtils::TryParse<int32_t>(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<AMFIntValue>("Component ID") = GetComponentID();
|
||||
auto& destPos = cmptType.PushDebug("Destination Position");
|
||||
if (m_Destination != NiPoint3Constant::ZERO) {
|
||||
destPos.PushDebug(m_Destination);
|
||||
} else {
|
||||
destPos.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Bouncer has no target position, is likely missing config data");
|
||||
}
|
||||
|
||||
|
||||
if (m_Speed == -1.0f) {
|
||||
cmptType.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Bouncer has no speed value, is likely missing config data");
|
||||
} else {
|
||||
cmptType.PushDebug<AMFDoubleValue>("Bounce Speed") = m_Speed;
|
||||
}
|
||||
cmptType.PushDebug<AMFStringValue>("Bounce trajectory arc") = m_UsesHighArc ? "High Arc" : "Low Arc";
|
||||
cmptType.PushDebug<AMFBoolValue>("Collision Enabled") = m_IgnoreCollision;
|
||||
cmptType.PushDebug<AMFBoolValue>("Stick Landing") = m_StickLanding;
|
||||
cmptType.PushDebug<AMFBoolValue>("Locks character's controls") = m_LockControls;
|
||||
if (!m_CinematicPath.empty()) cmptType.PushDebug<AMFStringValue>("Cinematic Camera Path (plays during bounce)") = m_CinematicPath;
|
||||
|
||||
auto* switchComponent = m_Parent->GetComponent<SwitchComponent>();
|
||||
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<AMFBoolValue>("Uses a group name for interactions") = m_UsesGroupName;
|
||||
if (!m_UsesGroupName) {
|
||||
if (m_MinNumTargets > 1) {
|
||||
cmptType.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Bouncer has a required number of objects to activate, but no group for interactions.");
|
||||
}
|
||||
|
||||
if (!m_GroupName.empty()) {
|
||||
cmptType.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Has a group name for interactions , but is marked to not use that name.");
|
||||
}
|
||||
} else {
|
||||
if (m_GroupName.empty()) {
|
||||
cmptType.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Set to use a group name for inter actions, but no group name is assigned");
|
||||
}
|
||||
cmptType.PushDebug<AMFIntValue>("Number of interactions to activate bouncer") = m_MinNumTargets;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -450,19 +450,10 @@ const std::vector<BuffParameter>& 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<float>(str);
|
||||
if (value) param.values.push_back(value.value());
|
||||
}
|
||||
|
||||
parameters.push_back(param);
|
||||
|
||||
@@ -49,11 +49,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<GameMessages::GetObjectReportInfo&>(msg);
|
||||
bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
||||
|
||||
auto& cmptType = reportInfo.info->PushDebug("Character");
|
||||
|
||||
@@ -70,7 +69,7 @@ bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
|
||||
for (const auto zoneID : m_VisitedLevels) {
|
||||
std::stringstream sstream;
|
||||
sstream << "MapID: " << zoneID.GetMapID() << " CloneID: " << zoneID.GetCloneID();
|
||||
vl.PushDebug<AMFStringValue>(sstream.str()) = "";
|
||||
vl.PushDebug(sstream.str());
|
||||
}
|
||||
|
||||
// visited locations
|
||||
@@ -95,7 +94,7 @@ bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
|
||||
const int32_t flagId = base + i;
|
||||
std::stringstream stream;
|
||||
stream << "Flag: " << flagId;
|
||||
allFlags.PushDebug<AMFStringValue>(stream.str()) = "";
|
||||
allFlags.PushDebug(stream.str());
|
||||
}
|
||||
flagChunkCopy >>= 1;
|
||||
}
|
||||
@@ -515,12 +514,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 +797,14 @@ std::string CharacterComponent::StatisticsToString() const {
|
||||
return result.str();
|
||||
}
|
||||
|
||||
uint64_t CharacterComponent::GetStatisticFromSplit(std::vector<std::string> split, uint32_t index) {
|
||||
return split.size() > index ? std::stoull(split.at(index)) : 0;
|
||||
uint64_t CharacterComponent::GetStatisticFromSplit(const std::vector<std::string>& split, const uint32_t index) {
|
||||
uint64_t toReturn = 0;
|
||||
if (index < split.size()) {
|
||||
const auto parsed = GeneralUtils::TryParse<uint64_t>(split[index]);
|
||||
if (parsed) toReturn = *parsed;
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
ZoneStatistics& CharacterComponent::GetZoneStatisticsForMap(LWOMAPID mapID) {
|
||||
|
||||
@@ -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<std::string> split, uint32_t index);
|
||||
static uint64_t GetStatisticFromSplit(const std::vector<std::string>& 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;
|
||||
|
||||
|
||||
@@ -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<uint32_t>(m_CollectibleId) + static_cast<uint32_t>(Game::server->GetZoneID() << 8);
|
||||
|
||||
cmptType.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
|
||||
|
||||
cmptType.PushDebug<AMFIntValue>("Collectible ID") = GetCollectibleId();
|
||||
cmptType.PushDebug<AMFIntValue>("Mission Tracking ID (for save data)") = collectibleID;
|
||||
|
||||
auto* localCharEntity = Game::entityManager->GetEntity(reportMsg.clientID);
|
||||
bool collected = false;
|
||||
if (localCharEntity) {
|
||||
auto* missionComponent = localCharEntity->GetComponent<MissionComponent>();
|
||||
|
||||
if (m_CollectibleId != 0) {
|
||||
collected = missionComponent->HasCollectible(collectibleID);
|
||||
}
|
||||
}
|
||||
|
||||
cmptType.PushDebug<AMFBoolValue>("Has been collected") = collected;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -55,17 +55,18 @@ public:
|
||||
virtual void LoadFromXml(const tinyxml2::XMLDocument& doc) {}
|
||||
|
||||
virtual void Serialize(RakNet::BitStream& outBitStream, bool isConstruction) {}
|
||||
|
||||
protected:
|
||||
template<typename GameObjClass, typename DerivedMsg>
|
||||
inline void RegisterMsg(bool (GameObjClass::*handler)(DerivedMsg&)) {
|
||||
static_assert(std::is_base_of_v<GameMessages::GameMsg, DerivedMsg>, "DerivedMsg must inherit from GameMsg");
|
||||
static_assert(std::is_base_of_v<Component, GameObjClass>, "GameObjClass must inherit from Component");
|
||||
const auto handlerBound = std::bind(handler, static_cast<GameObjClass*>(this), std::placeholders::_1);
|
||||
const auto castWrapper = [handlerBound](GameMessages::GameMsg& msg) {
|
||||
return handlerBound(static_cast<DerivedMsg&>(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<typename T>
|
||||
inline void RegisterMsg(auto* self, const auto handler) {
|
||||
T msg;
|
||||
RegisterMsg(msg.msgId, self, handler);
|
||||
DerivedMsg msg;
|
||||
m_Parent->RegisterMsg(msg.msgId, castWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<GameMessages::GetObjectReportInfo&>(msg);
|
||||
bool ControllablePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
||||
PhysicsComponent::OnGetObjectReportInfo(reportInfo);
|
||||
auto& info = reportInfo.subCategory->PushDebug("Controllable Info");
|
||||
|
||||
auto& vel = info.PushDebug("Velocity");
|
||||
|
||||
@@ -284,7 +284,7 @@ public:
|
||||
|
||||
private:
|
||||
|
||||
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
|
||||
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
|
||||
/**
|
||||
* The entity that owns this component
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include "Logger.h"
|
||||
#include "Game.h"
|
||||
#include "dConfig.h"
|
||||
#include "CDLootMatrixTable.h"
|
||||
#include "CDLootTableTable.h"
|
||||
#include "CDRarityTableTable.h"
|
||||
|
||||
#include "Amf3.h"
|
||||
#include "AmfSerialize.h"
|
||||
@@ -45,7 +48,6 @@ Implementation<bool, const Entity*> DestroyableComponent::IsEnemyImplentation;
|
||||
Implementation<bool, const Entity*> 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;
|
||||
@@ -83,8 +85,9 @@ DestroyableComponent::DestroyableComponent(Entity* parent, const int32_t compone
|
||||
|
||||
m_DamageCooldownTimer = 0.0f;
|
||||
|
||||
RegisterMsg<GetObjectReportInfo>(this, &DestroyableComponent::OnGetObjectReportInfo);
|
||||
RegisterMsg<GameMessages::SetFaction>(this, &DestroyableComponent::OnSetFaction);
|
||||
RegisterMsg(&DestroyableComponent::OnGetObjectReportInfo);
|
||||
RegisterMsg(&DestroyableComponent::OnSetFaction);
|
||||
RegisterMsg(&DestroyableComponent::OnIsDead);
|
||||
}
|
||||
|
||||
DestroyableComponent::~DestroyableComponent() {
|
||||
@@ -694,6 +697,8 @@ void DestroyableComponent::NotifySubscribers(Entity* attacker, uint32_t damage)
|
||||
}
|
||||
|
||||
void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType, const std::u16string& deathType, uint32_t skillID) {
|
||||
if (m_IsDead) return;
|
||||
|
||||
//check if hardcore mode is enabled
|
||||
if (Game::entityManager->GetHardcoreMode()) {
|
||||
DoHardcoreModeDrops(source);
|
||||
@@ -706,6 +711,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
|
||||
Game::entityManager->SerializeEntity(m_Parent);
|
||||
}
|
||||
|
||||
m_IsDead = true;
|
||||
m_KillerID = source;
|
||||
|
||||
auto* owner = Game::entityManager->GetEntity(source);
|
||||
@@ -748,45 +754,16 @@ 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) {
|
||||
if (owner != nullptr) {
|
||||
auto* team = TeamManager::Instance()->GetTeam(owner->GetObjectID());
|
||||
|
||||
if (team != nullptr && m_Parent->GetComponent<BaseCombatAIComponent>() != nullptr) {
|
||||
LWOOBJID specificOwner = LWOOBJID_EMPTY;
|
||||
auto* scriptedActivityComponent = m_Parent->GetComponent<ScriptedActivityComponent>();
|
||||
uint32_t teamSize = team->members.size();
|
||||
uint32_t lootMatrixId = GetLootMatrixID();
|
||||
|
||||
if (scriptedActivityComponent) {
|
||||
lootMatrixId = scriptedActivityComponent->GetLootMatrixForTeamSize(teamSize);
|
||||
}
|
||||
|
||||
if (team->lootOption == 0) { // Round robin
|
||||
specificOwner = TeamManager::Instance()->GetNextLootOwner(team);
|
||||
|
||||
auto* member = Game::entityManager->GetEntity(specificOwner);
|
||||
|
||||
if (member) Loot::DropLoot(member, m_Parent->GetObjectID(), lootMatrixId, GetMinCoins(), GetMaxCoins());
|
||||
} else {
|
||||
for (const auto memberId : team->members) { // Free for all
|
||||
auto* member = Game::entityManager->GetEntity(memberId);
|
||||
|
||||
if (member == nullptr) continue;
|
||||
|
||||
Loot::DropLoot(member, m_Parent->GetObjectID(), lootMatrixId, GetMinCoins(), GetMaxCoins());
|
||||
}
|
||||
}
|
||||
} else { // drop loot for non team user
|
||||
Loot::DropLoot(owner, m_Parent->GetObjectID(), GetLootMatrixID(), GetMinCoins(), GetMaxCoins());
|
||||
}
|
||||
Loot::DropLoot(owner, m_Parent->GetObjectID(), GetLootMatrixID(), GetMinCoins(), GetMaxCoins());
|
||||
}
|
||||
} else {
|
||||
//Check if this zone allows coin drops
|
||||
if (Game::zoneManager->GetPlayerLoseCoinOnDeath()) {
|
||||
if (Game::zoneManager->GetPlayerLoseCoinOnDeath() && !Game::entityManager->GetHardcoreMode()) {
|
||||
auto* character = m_Parent->GetCharacter();
|
||||
uint64_t coinsTotal = character->GetCoins();
|
||||
const uint64_t minCoinsToLose = Game::zoneManager->GetWorldConfig().coinsLostOnDeathMin;
|
||||
@@ -799,8 +776,15 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
|
||||
|
||||
coinsTotal -= coinsToLose;
|
||||
|
||||
Loot::DropLoot(m_Parent, m_Parent->GetObjectID(), -1, coinsToLose, coinsToLose);
|
||||
character->SetCoins(coinsTotal, eLootSourceType::PICKUP);
|
||||
GameMessages::DropClientLoot lootMsg{};
|
||||
lootMsg.target = m_Parent->GetObjectID();
|
||||
lootMsg.ownerID = m_Parent->GetObjectID();
|
||||
lootMsg.currency = coinsToLose;
|
||||
lootMsg.spawnPos = m_Parent->GetPosition();
|
||||
lootMsg.sourceID = source;
|
||||
lootMsg.item = LOT_NULL;
|
||||
lootMsg.Send();
|
||||
character->SetCoins(coinsTotal, eLootSourceType::DELETION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -809,7 +793,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
|
||||
|
||||
std::vector<Entity*> 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);
|
||||
}
|
||||
}
|
||||
@@ -980,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<CharacterComponent>();
|
||||
if (!character) return;
|
||||
|
||||
auto uscore = character->GetUScore();
|
||||
|
||||
auto uscoreToLose = static_cast<uint64_t>(uscore * (Game::entityManager->GetHardcoreLoseUscoreOnDeathPercent() / 100.0f));
|
||||
@@ -1000,7 +986,14 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
|
||||
for (const auto item : itemMap | std::views::values) {
|
||||
// Don't drop excluded items or null ones
|
||||
if (!item || Game::entityManager->GetHardcoreExcludedItemDrops().contains(item->GetLot())) continue;
|
||||
GameMessages::SendDropClientLoot(m_Parent, source, item->GetLot(), 0, m_Parent->GetPosition(), item->GetCount());
|
||||
GameMessages::DropClientLoot lootMsg{};
|
||||
lootMsg.target = m_Parent->GetObjectID();
|
||||
lootMsg.ownerID = m_Parent->GetObjectID();
|
||||
lootMsg.sourceID = m_Parent->GetObjectID();
|
||||
lootMsg.item = item->GetLot();
|
||||
lootMsg.count = 1;
|
||||
lootMsg.spawnPos = m_Parent->GetPosition();
|
||||
for (int i = 0; i < item->GetCount(); i++) Loot::DropItem(*m_Parent, lootMsg);
|
||||
item->SetCount(0, false, false);
|
||||
}
|
||||
Game::entityManager->SerializeEntity(m_Parent);
|
||||
@@ -1012,13 +1005,35 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
|
||||
//get character:
|
||||
auto* chars = m_Parent->GetCharacter();
|
||||
if (chars) {
|
||||
auto coins = chars->GetCoins();
|
||||
auto oldCoins = chars->GetCoins();
|
||||
// Floor this so there arent coins generated from rounding
|
||||
auto coins = static_cast<uint64_t>(oldCoins * Game::entityManager->GetHardcoreCoinKeep());
|
||||
auto coinsToDrop = oldCoins - coins;
|
||||
LOG("Player had %llu coins, will lose %i coins to have %i", oldCoins, coinsToDrop, coins);
|
||||
|
||||
//lose all coins:
|
||||
chars->SetCoins(0, eLootSourceType::NONE);
|
||||
chars->SetCoins(coins, eLootSourceType::NONE);
|
||||
|
||||
//drop all coins:
|
||||
GameMessages::SendDropClientLoot(m_Parent, source, LOT_NULL, coins, m_Parent->GetPosition());
|
||||
constexpr auto MAX_TO_DROP_PER_GM = 100'000;
|
||||
GameMessages::DropClientLoot lootMsg{};
|
||||
lootMsg.target = m_Parent->GetObjectID();
|
||||
lootMsg.ownerID = m_Parent->GetObjectID();
|
||||
lootMsg.spawnPos = m_Parent->GetPosition();
|
||||
lootMsg.sourceID = source;
|
||||
lootMsg.item = LOT_NULL;
|
||||
lootMsg.Send();
|
||||
lootMsg.Send(m_Parent->GetSystemAddress());
|
||||
while (coinsToDrop > MAX_TO_DROP_PER_GM) {
|
||||
LOG("Dropping 100,000, %llu left", coinsToDrop);
|
||||
lootMsg.currency = 100'000;
|
||||
lootMsg.Send();
|
||||
lootMsg.Send(m_Parent->GetSystemAddress());
|
||||
coinsToDrop -= 100'000;
|
||||
}
|
||||
lootMsg.currency = coinsToDrop;
|
||||
lootMsg.Send();
|
||||
lootMsg.Send(m_Parent->GetSystemAddress());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1033,8 +1048,8 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
|
||||
auto maxHealth = GetMaxHealth();
|
||||
const auto uscoreMultiplier = Game::entityManager->GetHardcoreUscoreEnemiesMultiplier();
|
||||
const bool isUscoreReducedLot =
|
||||
Game::entityManager->GetHardcoreUscoreReducedLots().contains(lot) ||
|
||||
Game::entityManager->GetHardcoreUscoreReduced();
|
||||
Game::entityManager->GetHardcoreUscoreReducedLots().contains(lot) ||
|
||||
Game::entityManager->GetHardcoreUscoreReduced();
|
||||
const auto uscoreReduction = isUscoreReducedLot ? Game::entityManager->GetHardcoreUscoreReduction() : 1.0f;
|
||||
|
||||
int uscore = maxHealth * Game::entityManager->GetHardcoreUscoreEnemiesMultiplier() * uscoreReduction;
|
||||
@@ -1047,40 +1062,90 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
|
||||
}
|
||||
}
|
||||
|
||||
bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
|
||||
auto& reportInfo = static_cast<GameMessages::GetObjectReportInfo&>(msg);
|
||||
|
||||
bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
||||
auto& destroyableInfo = reportInfo.info->PushDebug("Destroyable");
|
||||
destroyableInfo.PushDebug<AMFIntValue>("Health") = m_iHealth;
|
||||
destroyableInfo.PushDebug<AMFDoubleValue>("Max Health") = m_fMaxHealth;
|
||||
destroyableInfo.PushDebug<AMFIntValue>("Armor") = m_iArmor;
|
||||
destroyableInfo.PushDebug<AMFDoubleValue>("Max Armor") = m_fMaxArmor;
|
||||
destroyableInfo.PushDebug<AMFIntValue>("Imagination") = m_iImagination;
|
||||
destroyableInfo.PushDebug<AMFDoubleValue>("Max Imagination") = m_fMaxImagination;
|
||||
destroyableInfo.PushDebug<AMFIntValue>("Damage To Absorb") = m_DamageToAbsorb;
|
||||
destroyableInfo.PushDebug<AMFBoolValue>("Is GM Immune") = m_IsGMImmune;
|
||||
destroyableInfo.PushDebug<AMFBoolValue>("Is Shielded") = m_IsShielded;
|
||||
destroyableInfo.PushDebug<AMFIntValue>("DestructibleComponent DB Table Template ID") = m_ComponentID;
|
||||
|
||||
if (m_CurrencyIndex == -1) {
|
||||
destroyableInfo.PushDebug<AMFBoolValue>("Has Loot Currency") = false;
|
||||
} else {
|
||||
destroyableInfo.PushDebug<AMFIntValue>("Loot Currency ID") = m_CurrencyIndex;
|
||||
auto& detailedCoinInfo = destroyableInfo.PushDebug("Coin Info");
|
||||
detailedCoinInfo.PushDebug<AMFIntValue>("Min Coins") = m_MinCoins;
|
||||
detailedCoinInfo.PushDebug<AMFIntValue>("Max Coins") = m_MaxCoins;
|
||||
}
|
||||
|
||||
if (m_LootMatrixID == -1 || m_LootMatrixID == 0) {
|
||||
destroyableInfo.PushDebug<AMFBoolValue>("Has Loot Matrix") = false;
|
||||
} else {
|
||||
auto& lootInfo = destroyableInfo.PushDebug("Loot Info");
|
||||
lootInfo.PushDebug<AMFIntValue>("Loot Matrix ID") = m_LootMatrixID;
|
||||
auto* const componentsRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
||||
auto* const itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
|
||||
auto* const lootMatrixTable = CDClientManager::GetTable<CDLootMatrixTable>();
|
||||
auto* const lootTableTable = CDClientManager::GetTable<CDLootTableTable>();
|
||||
auto* const rarityTableTable = CDClientManager::GetTable<CDRarityTableTable>();
|
||||
|
||||
const auto& matrix = lootMatrixTable->GetMatrix(m_LootMatrixID);
|
||||
|
||||
for (const auto& entry : matrix) {
|
||||
auto& thisEntry = lootInfo.PushDebug("Loot table Index - " + std::to_string(entry.LootTableIndex));
|
||||
thisEntry.PushDebug<AMFDoubleValue>("Percent chance to drop") = entry.percent * 100.0f;
|
||||
thisEntry.PushDebug<AMFDoubleValue>("Minimum amount to drop") = entry.minToDrop;
|
||||
thisEntry.PushDebug<AMFDoubleValue>("Maximum amount to drop") = entry.maxToDrop;
|
||||
const auto& lootTable = lootTableTable->GetTable(entry.LootTableIndex);
|
||||
const auto& rarityTable = rarityTableTable->GetRarityTable(entry.RarityTableIndex);
|
||||
|
||||
auto& thisRarity = thisEntry.PushDebug("Rarity");
|
||||
for (const auto& rarity : rarityTable) {
|
||||
thisRarity.PushDebug<AMFDoubleValue>("Rarity " + std::to_string(rarity.rarity)) = rarity.randmax;
|
||||
}
|
||||
|
||||
auto& thisItems = thisEntry.PushDebug("Drop(s) Info");
|
||||
for (const auto& loot : lootTable) {
|
||||
uint32_t itemComponentId = componentsRegistryTable->GetByIDAndType(loot.itemid, eReplicaComponentType::ITEM);
|
||||
uint32_t rarity = itemComponentTable->GetItemComponentByID(itemComponentId).rarity;
|
||||
auto title = "%[Objects_" + std::to_string(loot.itemid) + "_name] " + std::to_string(loot.itemid);
|
||||
if (loot.MissionDrop) title += " - Mission Drop";
|
||||
thisItems.PushDebug(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto* const entity = Game::entityManager->GetEntity(reportInfo.clientID);
|
||||
destroyableInfo.PushDebug<AMFBoolValue>("Is on your team") = entity ? IsFriend(entity) : false;
|
||||
auto& stats = destroyableInfo.PushDebug("Statistics");
|
||||
stats.PushDebug<AMFIntValue>("Health") = m_iHealth;
|
||||
stats.PushDebug<AMFDoubleValue>("Maximum Health") = m_fMaxHealth;
|
||||
stats.PushDebug<AMFIntValue>("Armor") = m_iArmor;
|
||||
stats.PushDebug<AMFDoubleValue>("Maximum Armor") = m_fMaxArmor;
|
||||
stats.PushDebug<AMFIntValue>("Imagination") = m_iImagination;
|
||||
stats.PushDebug<AMFDoubleValue>("Maximum Imagination") = m_fMaxImagination;
|
||||
stats.PushDebug<AMFIntValue>("Damage Absorption Points") = m_DamageToAbsorb;
|
||||
stats.PushDebug<AMFBoolValue>("Is GM Immune") = m_IsGMImmune;
|
||||
stats.PushDebug<AMFBoolValue>("Is Shielded") = m_IsShielded;
|
||||
destroyableInfo.PushDebug<AMFIntValue>("Attacks To Block") = m_AttacksToBlock;
|
||||
destroyableInfo.PushDebug<AMFIntValue>("Damage Reduction") = m_DamageReduction;
|
||||
auto& factions = destroyableInfo.PushDebug("Factions");
|
||||
size_t i = 0;
|
||||
std::stringstream factionsStream;
|
||||
for (const auto factionID : m_FactionIDs) {
|
||||
factions.PushDebug<AMFStringValue>(std::to_string(i++) + " " + std::to_string(factionID)) = "";
|
||||
factionsStream << factionID << " ";
|
||||
}
|
||||
auto& enemyFactions = destroyableInfo.PushDebug("Enemy Factions");
|
||||
i = 0;
|
||||
|
||||
destroyableInfo.PushDebug<AMFStringValue>("Factions") = factionsStream.str();
|
||||
|
||||
factionsStream.str("");
|
||||
for (const auto enemyFactionID : m_EnemyFactionIDs) {
|
||||
enemyFactions.PushDebug<AMFStringValue>(std::to_string(i++) + " " + std::to_string(enemyFactionID)) = "";
|
||||
factionsStream << enemyFactionID << " ";
|
||||
}
|
||||
destroyableInfo.PushDebug<AMFBoolValue>("Is Smashable") = m_IsSmashable;
|
||||
destroyableInfo.PushDebug<AMFBoolValue>("Is Dead") = m_IsDead;
|
||||
|
||||
destroyableInfo.PushDebug<AMFStringValue>("Enemy Factions") = factionsStream.str();
|
||||
|
||||
destroyableInfo.PushDebug<AMFBoolValue>("Is A Smashable") = m_IsSmashable;
|
||||
destroyableInfo.PushDebug<AMFBoolValue>("Is Smashed") = m_IsSmashed;
|
||||
destroyableInfo.PushDebug<AMFBoolValue>("Is Module Assembly") = m_IsModuleAssembly;
|
||||
destroyableInfo.PushDebug<AMFDoubleValue>("Explode Factor") = m_ExplodeFactor;
|
||||
destroyableInfo.PushDebug<AMFBoolValue>("Has Threats") = m_HasThreats;
|
||||
destroyableInfo.PushDebug<AMFIntValue>("Loot Matrix ID") = m_LootMatrixID;
|
||||
destroyableInfo.PushDebug<AMFIntValue>("Min Coins") = m_MinCoins;
|
||||
destroyableInfo.PushDebug<AMFIntValue>("Max Coins") = m_MaxCoins;
|
||||
|
||||
destroyableInfo.PushDebug<AMFStringValue>("Killer ID") = std::to_string(m_KillerID);
|
||||
|
||||
// "Scripts"; idk what to do about scripts yet
|
||||
@@ -1095,16 +1160,38 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
|
||||
immuneCounts.PushDebug<AMFIntValue>("Quickbuild Interrupt") = m_ImmuneToQuickbuildInterruptCount;
|
||||
immuneCounts.PushDebug<AMFIntValue>("Pull To Point") = m_ImmuneToPullToPointCount;
|
||||
|
||||
destroyableInfo.PushDebug<AMFIntValue>("Death Behavior") = m_DeathBehavior;
|
||||
auto& deathInfo = destroyableInfo.PushDebug("Death Info");
|
||||
deathInfo.PushDebug<AMFBoolValue>("Is Dead") = m_IsDead;
|
||||
switch (m_DeathBehavior) {
|
||||
case 0:
|
||||
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Fade";
|
||||
break;
|
||||
case 1:
|
||||
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Stay";
|
||||
break;
|
||||
case 2:
|
||||
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Immediate";
|
||||
break;
|
||||
case -1:
|
||||
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Invulnerable";
|
||||
break;
|
||||
default:
|
||||
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Other";
|
||||
break;
|
||||
}
|
||||
destroyableInfo.PushDebug<AMFDoubleValue>("Damage Cooldown Timer") = m_DamageCooldownTimer;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DestroyableComponent::OnSetFaction(GameMessages::GameMsg& msg) {
|
||||
auto& modifyFaction = static_cast<GameMessages::SetFaction&>(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;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
namespace GameMessages {
|
||||
struct GetObjectReportInfo;
|
||||
struct SetFaction;
|
||||
struct IsDead;
|
||||
};
|
||||
|
||||
namespace CppScripts {
|
||||
@@ -370,6 +372,8 @@ public:
|
||||
*/
|
||||
uint32_t GetLootMatrixID() const { return m_LootMatrixID; }
|
||||
|
||||
void SetCurrencyIndex(int32_t currencyIndex) { m_CurrencyIndex = currencyIndex; }
|
||||
|
||||
/**
|
||||
* Returns the ID of the entity that killed this entity, if any
|
||||
* @return the ID of the entity that killed this entity, if any
|
||||
@@ -468,8 +472,11 @@ 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; }
|
||||
|
||||
static Implementation<bool, const Entity*> IsEnemyImplentation;
|
||||
static Implementation<bool, const Entity*> IsFriendImplentation;
|
||||
@@ -585,6 +592,9 @@ private:
|
||||
*/
|
||||
uint32_t m_LootMatrixID;
|
||||
|
||||
// The currency index to determine how much loot to drop
|
||||
int32_t m_CurrencyIndex{ -1 };
|
||||
|
||||
/**
|
||||
* The min amount of coins that will drop when this entity is smashed
|
||||
*/
|
||||
|
||||
@@ -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<ControllablePhysicsComponent>();
|
||||
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<AMFIntValue>("Component ID") = GetComponentID();
|
||||
cmptType.PushDebug<AMFBoolValue>("Is GM Invis") = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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<LWOOBJID> m_LimboConstructions;
|
||||
|
||||
bool m_GhostOverride;
|
||||
|
||||
bool m_IsGMInvisible{ false };
|
||||
|
||||
};
|
||||
|
||||
#endif //!__GHOSTCOMPONENT__H__
|
||||
|
||||
@@ -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<GameMessages::GetObjectReportInfo&>(msg);
|
||||
bool HavokVehiclePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
||||
PhysicsComponent::OnGetObjectReportInfo(reportInfo);
|
||||
if (!reportInfo.subCategory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -39,10 +39,12 @@
|
||||
#include "CDObjectSkillsTable.h"
|
||||
#include "CDSkillBehaviorTable.h"
|
||||
#include "StringifiedEnum.h"
|
||||
#include "Amf3.h"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
InventoryComponent::InventoryComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
|
||||
RegisterMsg(&InventoryComponent::OnGetObjectReportInfo);
|
||||
this->m_Dirty = true;
|
||||
this->m_Equipped = {};
|
||||
this->m_Pushed = {};
|
||||
@@ -279,7 +281,14 @@ void InventoryComponent::AddItem(
|
||||
|
||||
case 1:
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
GameMessages::SendDropClientLoot(this->m_Parent, this->m_Parent->GetObjectID(), lot, 0, this->m_Parent->GetPosition(), 1);
|
||||
GameMessages::DropClientLoot lootMsg{};
|
||||
lootMsg.target = m_Parent->GetObjectID();
|
||||
lootMsg.ownerID = m_Parent->GetObjectID();
|
||||
lootMsg.sourceID = m_Parent->GetObjectID();
|
||||
lootMsg.item = lot;
|
||||
lootMsg.count = 1;
|
||||
lootMsg.spawnPos = m_Parent->GetPosition();
|
||||
Loot::DropItem(*m_Parent, lootMsg);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -440,7 +449,7 @@ Item* InventoryComponent::FindItemBySubKey(LWOOBJID id, eInventoryType inventory
|
||||
}
|
||||
}
|
||||
|
||||
bool InventoryComponent::HasSpaceForLoot(const std::unordered_map<LOT, int32_t>& loot) {
|
||||
bool InventoryComponent::HasSpaceForLoot(const Loot::Return& loot) {
|
||||
std::unordered_map<eInventoryType, int32_t> spaceOffset{};
|
||||
|
||||
uint32_t slotsNeeded = 0;
|
||||
@@ -626,7 +635,7 @@ void InventoryComponent::UpdateXml(tinyxml2::XMLDocument& document) {
|
||||
for (const auto& pair : this->m_Inventories) {
|
||||
auto* inventory = pair.second;
|
||||
|
||||
static const auto EXCLUDED_INVENTORIES = {VENDOR_BUYBACK, MODELS_IN_BBB, ITEM_SETS};
|
||||
static const auto EXCLUDED_INVENTORIES = { VENDOR_BUYBACK, MODELS_IN_BBB, ITEM_SETS };
|
||||
if (std::ranges::find(EXCLUDED_INVENTORIES, inventory->GetType()) != EXCLUDED_INVENTORIES.end()) {
|
||||
continue;
|
||||
}
|
||||
@@ -718,10 +727,6 @@ void InventoryComponent::Serialize(RakNet::BitStream& outBitStream, const bool b
|
||||
for (const auto& pair : m_Equipped) {
|
||||
const auto item = pair.second;
|
||||
|
||||
if (bIsInitialUpdate) {
|
||||
AddItemSkills(item.lot);
|
||||
}
|
||||
|
||||
outBitStream.Write(item.id);
|
||||
outBitStream.Write(item.lot);
|
||||
|
||||
@@ -957,8 +962,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -972,8 +978,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1169,14 +1176,12 @@ LOT InventoryComponent::GetConsumable() const {
|
||||
void InventoryComponent::AddItemSkills(const LOT lot) {
|
||||
const auto info = Inventory::FindItemComponent(lot);
|
||||
|
||||
const auto slot = FindBehaviorSlot(static_cast<eItemType>(info.itemType));
|
||||
const auto slot = FindBehaviorSlot(info.equipLocation);
|
||||
|
||||
if (slot == BehaviorSlot::Invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto index = m_Skills.find(slot);
|
||||
|
||||
const auto skill = FindSkill(lot);
|
||||
|
||||
SetSkill(slot, skill);
|
||||
@@ -1204,7 +1209,7 @@ void InventoryComponent::FixInvisibleItems() {
|
||||
void InventoryComponent::RemoveItemSkills(const LOT lot) {
|
||||
const auto info = Inventory::FindItemComponent(lot);
|
||||
|
||||
const auto slot = FindBehaviorSlot(static_cast<eItemType>(info.itemType));
|
||||
const auto slot = FindBehaviorSlot(info.equipLocation);
|
||||
|
||||
if (slot == BehaviorSlot::Invalid) {
|
||||
return;
|
||||
@@ -1216,15 +1221,31 @@ void InventoryComponent::RemoveItemSkills(const LOT lot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto old = index->second;
|
||||
const auto skillId = FindSkill(lot);
|
||||
|
||||
GameMessages::SendRemoveSkill(m_Parent, old);
|
||||
// Only act on this slot if it still holds the skill from this item.
|
||||
// Another item may have overwritten the slot since this one was equipped.
|
||||
if (index->second != skillId) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_Skills.erase(slot);
|
||||
|
||||
// Find another slot that still holds this skillID (if any).
|
||||
const auto surviving = std::ranges::find_if(m_Skills, [skillId](const auto& pair) {
|
||||
return pair.second == skillId;
|
||||
});
|
||||
|
||||
// The client stores one acquiredSkillsInfo entry per skillID, tagged with the slotID
|
||||
// it was originally added with. Always send RemoveSkill to clear that entry, then
|
||||
// re-add with the surviving slot so the client shows it in the correct place.
|
||||
GameMessages::SendRemoveSkill(m_Parent, skillId);
|
||||
if (surviving != m_Skills.end()) {
|
||||
GameMessages::SendAddSkill(m_Parent, skillId, surviving->first);
|
||||
}
|
||||
|
||||
if (slot == BehaviorSlot::Primary) {
|
||||
m_Skills.insert_or_assign(BehaviorSlot::Primary, 1);
|
||||
|
||||
GameMessages::SendAddSkill(m_Parent, 1, BehaviorSlot::Primary);
|
||||
}
|
||||
}
|
||||
@@ -1316,23 +1337,17 @@ void InventoryComponent::RemoveDatabasePet(LWOOBJID id) {
|
||||
m_Pets.erase(id);
|
||||
}
|
||||
|
||||
BehaviorSlot InventoryComponent::FindBehaviorSlot(const eItemType type) {
|
||||
switch (type) {
|
||||
case eItemType::HAT:
|
||||
return BehaviorSlot::Head;
|
||||
case eItemType::NECK:
|
||||
return BehaviorSlot::Neck;
|
||||
case eItemType::LEFT_HAND:
|
||||
return BehaviorSlot::Offhand;
|
||||
case eItemType::RIGHT_HAND:
|
||||
return BehaviorSlot::Primary;
|
||||
case eItemType::CONSUMABLE:
|
||||
return BehaviorSlot::Consumable;
|
||||
default:
|
||||
return BehaviorSlot::Invalid;
|
||||
}
|
||||
BehaviorSlot InventoryComponent::FindBehaviorSlot(const std::string& equipLocation) {
|
||||
// Skill slot is determined by equipLocation, not itemType.
|
||||
// Mapping confirmed against live captures and client data (issue #1339).
|
||||
if (equipLocation == "special_r") return BehaviorSlot::Primary;
|
||||
if (equipLocation == "hair") return BehaviorSlot::Head;
|
||||
if (equipLocation == "special_l") return BehaviorSlot::Offhand;
|
||||
if (equipLocation == "clavicle") return BehaviorSlot::Neck;
|
||||
return BehaviorSlot::Invalid;
|
||||
}
|
||||
|
||||
|
||||
bool InventoryComponent::IsTransferInventory(eInventoryType type, bool includeVault) {
|
||||
return type == VENDOR_BUYBACK || (includeVault && (type == VAULT_ITEMS || type == VAULT_MODELS)) || type == TEMP_ITEMS || type == TEMP_MODELS || type == MODELS_IN_BBB;
|
||||
}
|
||||
@@ -1624,7 +1639,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);
|
||||
|
||||
@@ -1673,10 +1688,28 @@ bool InventoryComponent::SetSkill(BehaviorSlot slot, uint32_t skillId) {
|
||||
const auto index = m_Skills.find(slot);
|
||||
if (index != m_Skills.end()) {
|
||||
const auto old = index->second;
|
||||
GameMessages::SendRemoveSkill(m_Parent, old);
|
||||
// Only remove the old skill from the client if no other slot still holds it.
|
||||
// The client's acquiredSkillsInfo is keyed by skillID (one entry per skill),
|
||||
// so RemoveSkill clears it globally — sending it while another slot still uses
|
||||
// the same skillID would break that slot on the client.
|
||||
const auto usedElsewhere = std::ranges::any_of(m_Skills, [&](const auto& pair) {
|
||||
return pair.first != slot && pair.second == old;
|
||||
});
|
||||
if (!usedElsewhere) {
|
||||
GameMessages::SendRemoveSkill(m_Parent, old);
|
||||
}
|
||||
}
|
||||
|
||||
// Only send AddSkill if the client doesn't already know about this skillID.
|
||||
// The client early-exits on duplicate AddSkill (same skillID already in
|
||||
// acquiredSkillsInfo) without updating the slot — so only send when it's new.
|
||||
const auto alreadyKnown = std::ranges::any_of(m_Skills, [&](const auto& pair) {
|
||||
return pair.first != slot && pair.second == skillId;
|
||||
});
|
||||
if (!alreadyKnown) {
|
||||
GameMessages::SendAddSkill(m_Parent, skillId, slot);
|
||||
}
|
||||
|
||||
GameMessages::SendAddSkill(m_Parent, skillId, slot);
|
||||
m_Skills.insert_or_assign(slot, skillId);
|
||||
return true;
|
||||
}
|
||||
@@ -1793,3 +1826,98 @@ void InventoryComponent::RegenerateItemIDs() {
|
||||
inventory->RegenerateItemIDs();
|
||||
}
|
||||
}
|
||||
|
||||
std::string DebugInvToString(const eInventoryType inv, bool verbose) {
|
||||
switch (inv) {
|
||||
case ITEMS:
|
||||
return "Backpack";
|
||||
case VAULT_ITEMS:
|
||||
return "Bank";
|
||||
case BRICKS:
|
||||
return verbose ? "Bricks" : "Bricks (contents only shown in high-detail report)";
|
||||
case MODELS_IN_BBB:
|
||||
return "Models in BBB";
|
||||
case TEMP_ITEMS:
|
||||
return "Temp Equip";
|
||||
case MODELS:
|
||||
return verbose ? "Model" : "Model (contents only shown in high-detail report)";
|
||||
case TEMP_MODELS:
|
||||
return "Module";
|
||||
case BEHAVIORS:
|
||||
return "B3 Behavior";
|
||||
case PROPERTY_DEEDS:
|
||||
return "Property";
|
||||
case BRICKS_IN_BBB:
|
||||
return "Brick In BBB";
|
||||
case VENDOR:
|
||||
return "Vendor";
|
||||
case VENDOR_BUYBACK:
|
||||
return "BuyBack";
|
||||
case QUEST:
|
||||
return "Quest";
|
||||
case DONATION:
|
||||
return "Donation";
|
||||
case VAULT_MODELS:
|
||||
return "Bank Model";
|
||||
case ITEM_SETS:
|
||||
return "Bank Behavior";
|
||||
case INVALID:
|
||||
return "Invalid";
|
||||
case ALL:
|
||||
return "All";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
||||
auto& cmpt = reportInfo.info->PushDebug("Inventory");
|
||||
cmpt.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
|
||||
uint32_t numItems = 0;
|
||||
for (auto* inventory : m_Inventories | std::views::values) numItems += inventory->GetItems().size();
|
||||
cmpt.PushDebug<AMFIntValue>("Inventory Item Count") = numItems;
|
||||
|
||||
auto& itemsInBags = cmpt.PushDebug("Items in bags");
|
||||
for (const auto& [id, inventoryMut] : m_Inventories) {
|
||||
if (!inventoryMut) continue;
|
||||
const auto* const inventory = inventoryMut;
|
||||
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;
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "%[Objects_" << item->GetLot() << "_name] Slot " << item->GetSlot();
|
||||
auto& slot = curInv.PushDebug(ss.str());
|
||||
slot.PushDebug<AMFStringValue>("Object ID") = std::to_string(item->GetId());
|
||||
slot.PushDebug<AMFIntValue>("LOT") = item->GetLot();
|
||||
if (item->GetSubKey() != LWOOBJID_EMPTY) slot.PushDebug<AMFStringValue>("Subkey") = std::to_string(item->GetSubKey());
|
||||
slot.PushDebug<AMFIntValue>("Count") = item->GetCount();
|
||||
slot.PushDebug<AMFIntValue>("Slot") = item->GetSlot();
|
||||
slot.PushDebug<AMFBoolValue>("Bind on pickup") = item->GetInfo().isBOP;
|
||||
slot.PushDebug<AMFBoolValue>("Bind on equip") = item->GetInfo().isBOE;
|
||||
slot.PushDebug<AMFBoolValue>("Is currently bound") = item->GetBound();
|
||||
auto& extra = slot.PushDebug("Extra Info");
|
||||
for (const auto* const setting : item->GetConfig()) {
|
||||
if (setting) extra.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto& equipped = cmpt.PushDebug("Equipped Items");
|
||||
for (const auto& [location, info] : GetEquippedItems()) {
|
||||
std::stringstream ss;
|
||||
ss << "%[Objects_" << info.lot << "_name]";
|
||||
auto& equipSlot = equipped.PushDebug(ss.str());
|
||||
equipSlot.PushDebug<AMFStringValue>("Location") = location;
|
||||
equipSlot.PushDebug<AMFStringValue>("Object ID") = std::to_string(info.id);
|
||||
equipSlot.PushDebug<AMFIntValue>("Slot") = info.slot;
|
||||
equipSlot.PushDebug<AMFIntValue>("Count") = info.count;
|
||||
auto& extra = equipSlot.PushDebug("Extra Info");
|
||||
for (const auto* const setting : info.config) {
|
||||
if (setting) extra.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(setting->GetKey())) = setting->GetValueAsString();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "eInventoryType.h"
|
||||
#include "eReplicaComponentType.h"
|
||||
#include "eLootSourceType.h"
|
||||
#include "Loot.h"
|
||||
|
||||
class Entity;
|
||||
class ItemSet;
|
||||
@@ -30,6 +31,10 @@ typedef std::map<std::string, EquippedItem> 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
|
||||
@@ -200,7 +205,7 @@ public:
|
||||
* @param loot a map of items to add and how many to add
|
||||
* @return whether the entity has enough space for all the items
|
||||
*/
|
||||
bool HasSpaceForLoot(const std::unordered_map<LOT, int32_t>& loot);
|
||||
bool HasSpaceForLoot(const Loot::Return& loot);
|
||||
|
||||
/**
|
||||
* Equips an item in the specified slot
|
||||
@@ -362,11 +367,10 @@ public:
|
||||
void RemoveDatabasePet(LWOOBJID id);
|
||||
|
||||
/**
|
||||
* Returns the current behavior slot active for the passed item type
|
||||
* @param type the item type to find the behavior slot for
|
||||
* @return the current behavior slot active for the passed item type
|
||||
* Returns the behavior slot for the given equipLocation string.
|
||||
* This is the authoritative mapping used for skill slot assignment.
|
||||
*/
|
||||
static BehaviorSlot FindBehaviorSlot(eItemType type);
|
||||
static BehaviorSlot FindBehaviorSlot(const std::string& equipLocation);
|
||||
|
||||
/**
|
||||
* Checks if the inventory type is a temp inventory
|
||||
@@ -398,6 +402,8 @@ public:
|
||||
|
||||
std::map<BehaviorSlot, uint32_t> GetSkills() { return m_Skills; };
|
||||
|
||||
void ClearSkills() { m_Skills.clear(); };
|
||||
|
||||
bool SetSkill(int slot, uint32_t skillId);
|
||||
bool SetSkill(BehaviorSlot slot, uint32_t skillId);
|
||||
|
||||
@@ -410,6 +416,8 @@ public:
|
||||
// Used to migrate a character version, no need to call outside of that context
|
||||
void RegenerateItemIDs();
|
||||
|
||||
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
|
||||
|
||||
~InventoryComponent() override;
|
||||
|
||||
private:
|
||||
|
||||
@@ -22,7 +22,7 @@ public:
|
||||
void NextLUPExhibit();
|
||||
private:
|
||||
float m_UpdateTimer = 0.0f;
|
||||
std::array<LOT, 4> m_LUPExhibits = { 11121, 11295, 11423, 11979 };
|
||||
const std::array<LOT, 4> m_LUPExhibits = { 11121, 11295, 11423, 11979 };
|
||||
uint8_t m_LUPExhibitIndex = 0;
|
||||
bool m_DirtyLUPExhibit = true;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user