Compare commits

..

41 Commits

Author SHA1 Message Date
Aaron Kimbrell
73bcb949d7 fix: restore SetPossessor in Mount() and scope IsRacing to vehicles
SetPossessor was missing from Mount(), breaking direct possessions via
PossessableComponent::OnUse which bypasses HandlePossession. IsRacing
now only set/cleared when the mount has HavokVehiclePhysicsComponent,
preventing non-vehicle possessions from incorrectly affecting the
distance-driven statistic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 22:36:45 -05:00
Aaron Kimbrell
d079f3621b feat: enhance possession mechanics with skill set integration and improved message handling 2026-06-08 21:30:22 -05:00
David Markowitz
a156a8fcba fix: security vulnerabilities (#1980)
* fix: security vulnerabilities

Tested that all functions related to the touched files work

will test sqlite on a CI build

* fix failing test

* ai feedback

* add buffer size checking

* use c_str

* dont log session key

* Try this for a mac definition

* be quiet apple
2026-06-07 20:59:11 -07:00
Aaron Kimbrell
f6c9a27a2b fix: update permissions for PR title check workflow (#1981) 2026-06-07 02:07:17 -07:00
Aaron Kimbrell
8e09ffd6e8 feat: enhance CI/CD workflows with Docker support and artifact management (#1977)
* feat: enhance CI/CD workflows with Docker support and artifact management

* chore: update GitHub Actions workflows with version pinning and permission adjustments

* feat: update CI workflows to support new OS versions and improve artifact handling

* chore: remove macOS-specific installation of libssl and XCode switching

* fix: update artifact zipping logic to target build directories in canary and release workflows

* chore: update actions/checkout to version 6.0.2 in CI workflow

* fix: update continue-on-error condition in CI workflow and add macOS RelWithDebInfo build preset

* fix: rename step to accurately reflect Docker image signing process

* don't ignore pdb's
2026-06-06 01:30:12 -05:00
David Markowitz
0c1808686c fix: tac arc absolute value bug (#1979)
Tested that tac arc correctly checks the full range
2026-06-06 01:17:00 -05:00
Johntor
4d6a624da2 docs: Refine command documentation (#1978)
* Refine command documentation

Updated command descriptions From dGame/dUtilities/SlashCommandHandler.cpp. Added aliases and improved formatting for better readability.

* fix: grammatical feedback

---------

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>
2026-06-02 00:00:13 -07:00
David Markowitz
4ab09cf1aa Remove non-functioning saving of gm invis, re-add gm invis as a feature (#1976)
Does not save, only works for this world.  Fixed an issue where the incorrect comparison was used to make players invisible again (the same check that makes then INvisible needs to make them visible.)
2026-05-27 04:35:00 -05:00
David Markowitz
4ef9f43266 feat: make gm registration simpler and safer (#1932)
* gm registration re-work

* fix errors

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* remove duplicate message

* Remove duplicate function

* add null check

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-05-19 13:42:56 -05:00
David Markowitz
f3a5add038 fix(tac arc): incorrect check causing any enemy lower than you to not attack you (#1975)
tested that close range enemies above and below you now correctly attack you
2026-05-18 03:15:03 -07:00
David Markowitz
f5d33a773a fix: security fixes (#1974)
* fix: security fixes

dont print passwords for worlds
bound strings from clients
actually enable encryption between rakpeers
dont allow underflow when reading a string

Tested that packets are encrypted
tested that models can still be built
tested that combat still works

* add check

* use c++ nullptr instead of NULL

* initialize to 0

* globalize constant (should be in a namespace at least in the future)

* Update GameMessages.cpp

* check bounds
2026-05-17 14:21:22 -05:00
David Markowitz
67bbe4c1f0 fix(Missions): mission progression undefined behavior (#1973)
* fix: mission progression undefined behavior

defer the sub calls until after the loop has finished, that way no ub happens.  tested that mission progression all the way up until joining a faction still works and meta missions still function.

* default initialize

* Update MissionComponent.h
2026-05-17 14:21:08 -05:00
David Markowitz
482ff82656 fix: adding custom behaviors (#1969) 2026-04-14 01:04:26 -07:00
David Markowitz
8061f512aa feat: fix go outside and play mission (#1967)
* feat: fix go outside and play mission

* Update dWorldServer/WorldServer.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dGame/dComponents/MissionComponent.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* const

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-09 00:55:57 -05:00
David Markowitz
247576e101 fix: use copy ellision (#1963)
* use copy ellision

tested that the server still starts

* Update dDatabase/GameDatabase/MySQL/MySQLDatabase.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 15:35:28 -07:00
David Markowitz
8dfdca7fbd feat: add mission progression for behaviors (#1962)
* feat: add mission progression for behaviors

* Add const to ptr

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-31 13:02:23 -07:00
Aaron Kimbrell
8283d1fa95 fix: mariadb on newer gcc and newer cmake version (#1961)
* Fix newer gcc issues in mariadb

* fix error with newer cmake versions

* update mariadb to latest

* fix macos and docker

* fix: update Windows MSI package comments and align Connector/C++ version

* Update cmake/FindMariaDB.cmake

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: only pass CMAKE_POLICY_VERSION_MINIMUM to ExternalProject when CMake >= 4.0

Agent-Logs-Url: https://github.com/DarkflameUniverse/DarkflameServer/sessions/a247f729-a0b1-4fb6-825e-d23045b1ee55

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-03-29 13:59:09 -05:00
David Markowitz
434c9b6315 fix: imaginite on racing minigames and add null checks (#1958) 2026-02-23 01:16:36 -08:00
David Markowitz
3c64b26c39 fix: macos ci (#1955) 2026-02-11 19:49:51 -08:00
David Markowitz
347b1d17d4 fix: not checking pending names on rename (#1954) 2026-02-11 19:49:39 -08:00
David Markowitz
c723ce2588 fix: donations requiring new high score vs adding to previous one (#1951)
tested that jawbox works as intended now for donation counting on the leaderboards
2026-01-13 22:48:29 -08:00
Terrev
66b7d3606e fix: flower activity (#1950) 2025-12-26 13:58:10 -08:00
David Markowitz
40fef36530 fix: coins dropping on killer (#1948)
tested that when a player dies the coins spawn on their body instead
2025-12-13 22:00:58 -08:00
David Markowitz
bf020baa17 fix: temp fixes for ghosting so I can continue being on break (#1947)
* fix: temp fixes for ghosting so I can continue being on break

disables the ghost feature for now so i can continue my break

* Update GhostComponent.cpp
2025-12-08 20:39:33 -08:00
David Markowitz
a713216540 fix: saving gm invis for non gms (#1940) 2025-11-18 22:04:07 -06:00
David Markowitz
ea86a708e4 fix: uninitialized memory (#1937) 2025-11-18 19:06:03 -08:00
David Markowitz
ca7424cbeb fix: chest loot not working (#1933)
* fix bons and dragon loot

* fix chest server loot
2025-11-16 16:17:49 -06:00
David Markowitz
991e55f305 feat: dont drop loot for dead players if configured in the zone activity settings (#1935)
* feat: dont drop loot for dead players if configured in the zone activity settings

* fix errors

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dGame/dComponents/ActivityComponent.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dGame/dUtilities/Loot.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-16 16:17:26 -06:00
David Markowitz
5410acffaa fix: chat server crash (#1931)
* fix: chat server crash

* Update dChatServer/ChatServer.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-16 13:48:57 -08:00
David Markowitz
86f8601bbd feat: various debug command improvements (#1934)
* feat: various debug command improvements

* add missing utility function

* Update dGame/dUtilities/SlashCommands/DEVGMCommands.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-16 13:48:16 -08:00
David Markowitz
4658318a3a fix: deactivate bubble buff from server too (#1936) 2025-11-16 13:46:59 -08:00
Aaron Kimbrell
11d44ffb98 feat: proper gminvs with ghosting (#1920)
* feat: proper gminvis ghosting

* address feedback

---------

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>
2025-11-15 16:43:33 -08:00
David Markowitz
2fb16420f3 fix: ape anchor not respawning (#1927)
* fix: ape anchor not respawning

* add return

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-15 13:30:02 -08:00
David Markowitz
96089a8d9a fix: fb race activityid (#1929) 2025-11-15 13:29:11 -08:00
David Markowitz
eac50acfcc fix: correct mission tracking (#1930)
checked that live captures did not track achievements in this count
2025-11-15 13:29:03 -08:00
David Markowitz
ca60787055 fix: ffa -> shared loot for activities (#1925) 2025-10-26 01:01:21 -07:00
David Markowitz
396dcb0465 feat: add logger feature to log on function entry and exit (#1924)
* feat: add logger feature to log on function entry and exit

* i didnt save the file
2025-10-25 14:53:49 -05:00
David Markowitz
6e545eb1b9 Update Loot.cpp (#1923) 2025-10-24 21:53:00 -07:00
David Markowitz
46aac016fd fix: unintended stopping (#1922) 2025-10-23 23:41:16 -05:00
David Markowitz
83823fa64f fix: resurrect not available for non-gms (#1919) 2025-10-20 23:05:22 -07:00
David Markowitz
0dd504c803 feat: behavior states (#1918) 2025-10-20 01:16:36 -05:00
203 changed files with 2287 additions and 1494 deletions

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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
View File

@@ -126,3 +126,5 @@ docker-compose.override.yml
# CMake scripts
!cmake/*
!cmake/toolchains/*
.mcp.json
.claude/

View File

@@ -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)

View File

@@ -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": [

View File

@@ -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
View 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"

View File

@@ -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
)

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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];
}

View File

@@ -477,7 +477,7 @@ TeamData* TeamContainer::CreateLocalTeam(std::vector<LWOOBJID> members) {
}
}
newTeam->lootFlag = 1;
newTeam->lootFlag = 0;
TeamStatusUpdate(newTeam);

View File

@@ -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));

View File

@@ -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;

View File

@@ -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.

View File

@@ -205,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);
@@ -237,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
/**
@@ -258,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
@@ -268,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));

View File

@@ -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);
}

View File

@@ -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:

View File

@@ -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;

View File

@@ -11,65 +11,6 @@ Vector3 QuatUtils::Euler(const NiQuaternion& quat) {
return glm::eulerAngles(quat);
}
NiQuaternion NiQuaternion::operator*(const float scalar) const noexcept {
return NiQuaternion(this->w * scalar, this->x * scalar, this->y * scalar, this->z * scalar);
}
NiQuaternion& NiQuaternion::operator*=(const NiQuaternion& q) {
auto& [ow, ox, oy, oz] = q;
auto [cw, cx, cy, cz] = *this; // Current rotation copied because otherwise it screws up the math
this->w = cw * ow - cx * ox - cy * oy - cz * oz;
this->x = cw * ox + cx * ow + cy * oz - cz * oy;
this->y = cw * oy + cy * ow + cz * ox - cx * oz;
this->z = cw * oz + cz * ow + cx * oy - cy * ox;
return *this;
}
NiQuaternion NiQuaternion::operator* (const NiQuaternion& q) const {
auto& [ow, ox, oy, oz] = q;
return NiQuaternion
(
/* w */w * ow - x * ox - y * oy - z * oz,
/* x */w * ox + x * ow + y * oz - z * oy,
/* y */w * oy + y * ow + z * ox - x * oz,
/* z */w * oz + z * ow + x * oy - y * ox
);
}
NiQuaternion NiQuaternion::operator/(const float& q) const noexcept {
return NiQuaternion(this->w / q, this->x / q, this->y / q, this->z / q);
}
void NiQuaternion::Normalize() {
float length = Dot(*this);
float invLength = 1.0f / std::sqrt(length);
*this = *this * invLength;
}
float NiQuaternion::Dot(const NiQuaternion& q) const noexcept {
return (this->w * q.w) + (this->x * q.x) + (this->y * q.y) + (this->z * q.z);
}
void NiQuaternion::Inverse() noexcept {
NiQuaternion copy = *this;
copy.Conjugate();
const float inv = 1.0f / Dot(*this);
*this = copy / inv;
}
void NiQuaternion::Conjugate() noexcept {
x = -x;
y = -y;
z = -z;
}
NiQuaternion NiQuaternion::Diff(const NiQuaternion& q) const noexcept {
NiQuaternion inv = *this;
inv.Inverse();
return inv * q;
}
// MARK: Helper Functions
//! Look from a specific point in space to another point in space (Y-locked)

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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));
};

View File

@@ -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;
};

View File

@@ -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,8 +58,9 @@ 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.14159265358979323846264338327950288f;
constexpr float PI = 3.14159f;
//============ STRUCTS ==============

View File

@@ -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__

View File

@@ -1,23 +0,0 @@
// Darkflame Universe
// Copyright 2025
#ifndef DMATH_H
#define DMATH_H
#include <cmath>
namespace Math {
constexpr float PI = 3.14159265358979323846264338327950288f;
constexpr float RATIO_DEG_TO_RAD = PI / 180.0f;
constexpr float RATIO_RAD_TO_DEG = 180.0f / PI;
inline float DegToRad(float degrees) {
return degrees * RATIO_DEG_TO_RAD;
}
inline float RadToDeg(float radians) {
return radians * RATIO_RAD_TO_DEG;
}
};
#endif //!DMATH_H

View File

@@ -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;
}

View File

@@ -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);
};

View File

@@ -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)
});
}
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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", ""));

View File

@@ -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 {};

View File

@@ -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;

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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());

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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 :/

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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 {}; };

View File

@@ -189,15 +189,21 @@ Entity::~Entity() {
}
if (m_ParentEntity) {
GameMessages::ChildRemoved removedMsg{};
removedMsg.childID = m_ObjectID;
removedMsg.target = m_ParentEntity->GetObjectID();
removedMsg.Send();
m_ParentEntity->RemoveChild(this);
}
}
void Entity::Initialize() {
RegisterMsg<GameMessages::RequestServerObjectInfo>(this, &Entity::MsgRequestServerObjectInfo);
RegisterMsg<GameMessages::DropClientLoot>(this, &Entity::MsgDropClientLoot);
RegisterMsg<GameMessages::GetFactionTokenType>(this, &Entity::MsgGetFactionTokenType);
RegisterMsg<GameMessages::PickupItem>(this, &Entity::MsgPickupItem);
RegisterMsg(&Entity::MsgRequestServerObjectInfo);
RegisterMsg(&Entity::MsgDropClientLoot);
RegisterMsg(&Entity::MsgGetFactionTokenType);
RegisterMsg(&Entity::MsgPickupItem);
RegisterMsg(&Entity::MsgChildRemoved);
RegisterMsg(&Entity::MsgGetFlag);
/**
* Setup trigger
*/
@@ -499,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);
@@ -2246,8 +2252,7 @@ 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));
@@ -2283,9 +2288,7 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
return true;
}
bool Entity::MsgDropClientLoot(GameMessages::GameMsg& msg) {
auto& dropLootMsg = static_cast<GameMessages::DropClientLoot&>(msg);
bool Entity::MsgDropClientLoot(GameMessages::DropClientLoot& dropLootMsg) {
if (dropLootMsg.item != LOT_NULL && dropLootMsg.item != 0) {
Loot::Info info{
.id = dropLootMsg.lootID,
@@ -2302,13 +2305,11 @@ bool Entity::MsgDropClientLoot(GameMessages::GameMsg& msg) {
return true;
}
bool Entity::MsgGetFlag(GameMessages::GameMsg& msg) {
auto& flagMsg = static_cast<GameMessages::GetFlag&>(msg);
bool Entity::MsgGetFlag(GameMessages::GetFlag& flagMsg) {
if (m_Character) flagMsg.flag = m_Character->GetPlayerFlag(flagMsg.flagID);
return true;
}
bool Entity::MsgGetFactionTokenType(GameMessages::GameMsg& msg) {
auto& tokenMsg = static_cast<GameMessages::GetFactionTokenType&>(msg);
bool Entity::MsgGetFactionTokenType(GameMessages::GetFactionTokenType& tokenMsg) {
GameMessages::GetFlag getFlagMsg{};
getFlagMsg.flagID = ePlayerFlag::ASSEMBLY_FACTION;
@@ -2331,8 +2332,7 @@ bool Entity::MsgGetFactionTokenType(GameMessages::GameMsg& msg) {
return tokenMsg.tokenType != LOT_NULL;
}
bool Entity::MsgPickupItem(GameMessages::GameMsg& msg) {
auto& pickupItemMsg = static_cast<GameMessages::PickupItem&>(msg);
bool Entity::MsgPickupItem(GameMessages::PickupItem& pickupItemMsg) {
if (GetObjectID() == pickupItemMsg.lootOwnerID) {
PickupItem(pickupItemMsg.lootID);
} else {
@@ -2352,3 +2352,8 @@ bool Entity::MsgPickupItem(GameMessages::GameMsg& msg) {
return true;
}
bool Entity::MsgChildRemoved(GameMessages::ChildRemoved& msg) {
GetScript()->OnChildRemoved(*this, msg);
return true;
}

View File

@@ -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,11 +182,12 @@ public:
void AddComponent(eReplicaComponentType componentId, Component* component);
bool MsgRequestServerObjectInfo(GameMessages::GameMsg& msg);
bool MsgDropClientLoot(GameMessages::GameMsg& msg);
bool MsgGetFlag(GameMessages::GameMsg& msg);
bool MsgGetFactionTokenType(GameMessages::GameMsg& msg);
bool MsgPickupItem(GameMessages::GameMsg& msg);
bool MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfo& msg);
bool MsgDropClientLoot(GameMessages::DropClientLoot& msg);
bool MsgGetFlag(GameMessages::GetFlag& msg);
bool MsgGetFactionTokenType(GameMessages::GetFactionTokenType& msg);
bool MsgPickupItem(GameMessages::PickupItem& msg);
bool MsgChildRemoved(GameMessages::ChildRemoved& msg);
// This is expceted to never return nullptr, an assert checks this.
CppScripts::Script* const GetScript() const;
@@ -342,14 +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));
}
template<typename T>
inline void RegisterMsg(auto* self, const auto handler) {
T msg;
RegisterMsg(msg.msgId, self, handler);
// Provided a function that has a derived GameMessage as its only argument and returns a boolean,
// this will register it as a handler for that message type. Casting is done automatically to the type
// of the message in the first argument. This object is expected to exist as long as the handler can be called.
template<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);
}
/**

View File

@@ -361,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);
}
@@ -379,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;
}
}
@@ -413,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);
@@ -488,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);
}
@@ -519,6 +516,7 @@ void EntityManager::UpdateGhosting(Entity* player) {
const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint);
auto ghostingDistanceMax = m_GhostDistanceMaxSquared;
auto ghostingDistanceMin = m_GhostDistanceMinSqaured;
@@ -555,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);
}
}

View File

@@ -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) {

View File

@@ -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; }

View File

@@ -514,7 +514,7 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet)
return;
}
if (!Database::Get()->GetCharacterInfo(newName)) {
if (!Database::Get()->IsNameInUse(newName)) {
if (autoRejectNames) {
Database::Get()->SetCharacterName(objectID, newName);
LOG("Character %s auto-renamed to preapproved name %s due to mute", character->GetName().c_str(), newName.c_str());

View File

@@ -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");
}

View File

@@ -15,4 +15,9 @@ public:
void Load() override;
private:
float m_Timeout;
Behavior* m_GroundAction{};
Behavior* m_HitAction{};
Behavior* m_HitActionEnemy{};
Behavior* m_TimeoutAction{};
};

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -95,4 +95,6 @@ public:
Behavior& operator=(const Behavior& other) = default;
Behavior& operator=(Behavior&& other) = default;
uint32_t GetBehaviorID() const { return m_behaviorId; }
};

View File

@@ -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) {

View File

@@ -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()) {

View File

@@ -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);

View File

@@ -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++) {

View File

@@ -119,9 +119,8 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS
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());
@@ -209,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);

View File

@@ -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
@@ -325,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) {
@@ -591,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()));

View File

@@ -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
@@ -343,7 +347,7 @@ public:
private:
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg);
/**
* The database information for this activity
*/

View File

@@ -30,10 +30,7 @@
#include "Amf3.h"
BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
{
using namespace GameMessages;
RegisterMsg<GetObjectReportInfo>(this, &BaseCombatAIComponent::MsgGetObjectReportInfo);
}
RegisterMsg(&BaseCombatAIComponent::MsgGetObjectReportInfo);
m_Target = LWOOBJID_EMPTY;
m_DirtyStateOrTarget = true;
m_State = AiState::spawn;
@@ -481,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());
@@ -845,10 +843,9 @@ void BaseCombatAIComponent::IgnoreThreat(const LWOOBJID threat, const float valu
m_Target = LWOOBJID_EMPTY;
}
bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) {
bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
using enum AiState;
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
auto& cmptType = reportMsg.info->PushDebug("Base Combat AI");
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);

View File

@@ -234,7 +234,7 @@ public:
// Ignore a threat for a certain amount of time
void IgnoreThreat(const LWOOBJID target, const float time);
bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg);
bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
private:
/**

View File

@@ -31,10 +31,7 @@ BouncerComponent::BouncerComponent(Entity* parent, const int32_t componentID) :
LookupPetSwitch();
}
{
using namespace GameMessages;
RegisterMsg<GetObjectReportInfo>(this, &BouncerComponent::MsgGetObjectReportInfo);
}
RegisterMsg(&BouncerComponent::MsgGetObjectReportInfo);
}
BouncerComponent::~BouncerComponent() {
@@ -113,9 +110,8 @@ void BouncerComponent::LookupPetSwitch() {
}
}
bool BouncerComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
auto& cmptType = reportMsg.info->PushDebug("Bouncer");
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) {

View File

@@ -51,7 +51,7 @@ public:
*/
void LookupPetSwitch();
bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg);
bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
private:
/**

View File

@@ -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);

View File

@@ -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");
@@ -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) {

View File

@@ -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;

View File

@@ -6,16 +6,14 @@
CollectibleComponent::CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId) :
Component(parentEntity, componentID), m_CollectibleId(collectibleId) {
using namespace GameMessages;
RegisterMsg<GetObjectReportInfo>(this, &CollectibleComponent::MsgGetObjectReportInfo);
RegisterMsg(&CollectibleComponent::MsgGetObjectReportInfo);
}
void CollectibleComponent::Serialize(RakNet::BitStream& outBitStream, bool isConstruction) {
outBitStream.Write(GetCollectibleId());
}
bool CollectibleComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
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);

View File

@@ -12,7 +12,7 @@ public:
int16_t GetCollectibleId() const { return m_CollectibleId; }
void Serialize(RakNet::BitStream& outBitStream, bool isConstruction) override;
bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg);
bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
private:
int16_t m_CollectibleId = 0;
};

View File

@@ -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);
}
/**

View File

@@ -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");

View File

@@ -284,7 +284,7 @@ public:
private:
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
/**
* The entity that owns this component
*/

View File

@@ -48,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;
@@ -86,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() {
@@ -754,7 +754,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
const auto isPlayer = m_Parent->IsPlayer();
GameMessages::SendDie(m_Parent, source, source, true, killType, deathType, 0, 0, 0, isPlayer, false, 1);
GameMessages::SendDie(m_Parent, source, source, true, killType, deathType, 0, 0, 0, isPlayer, true, 1);
//NANI?!
if (!isPlayer) {
@@ -784,8 +784,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
lootMsg.sourceID = source;
lootMsg.item = LOT_NULL;
lootMsg.Send();
lootMsg.Send(m_Parent->GetSystemAddress());
character->SetCoins(coinsTotal, eLootSourceType::PICKUP);
character->SetCoins(coinsTotal, eLootSourceType::DELETION);
}
}
@@ -794,7 +793,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
std::vector<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);
}
}
@@ -965,6 +964,8 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
if (m_Parent->IsPlayer()) {
//remove hardcore_lose_uscore_on_death_percent from the player's uscore:
auto* character = m_Parent->GetComponent<CharacterComponent>();
if (!character) return;
auto uscore = character->GetUScore();
auto uscoreToLose = static_cast<uint64_t>(uscore * (Game::entityManager->GetHardcoreLoseUscoreOnDeathPercent() / 100.0f));
@@ -1061,8 +1062,7 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
}
}
bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportInfo = static_cast<GameMessages::GetObjectReportInfo&>(msg);
bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
auto& destroyableInfo = reportInfo.info->PushDebug("Destroyable");
destroyableInfo.PushDebug<AMFIntValue>("DestructibleComponent DB Table Template ID") = m_ComponentID;
@@ -1184,10 +1184,14 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
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;
}

View File

@@ -11,6 +11,8 @@
namespace GameMessages {
struct GetObjectReportInfo;
struct SetFaction;
struct IsDead;
};
namespace CppScripts {
@@ -470,8 +472,9 @@ public:
// handle hardcode mode drops
void DoHardcoreModeDrops(const LWOOBJID source);
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
bool OnSetFaction(GameMessages::GameMsg& msg);
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
bool OnSetFaction(GameMessages::SetFaction& setFaction);
bool OnIsDead(GameMessages::IsDead& isDead);
void SetIsDead(const bool value) { m_IsDead = value; }

View File

@@ -1,4 +1,9 @@
#include "GhostComponent.h"
#include "PlayerManager.h"
#include "Character.h"
#include "ControllablePhysicsComponent.h"
#include "UserManager.h"
#include "User.h"
#include "Amf3.h"
#include "GameMessages.h"
@@ -8,7 +13,9 @@ GhostComponent::GhostComponent(Entity* parent, const int32_t componentID) : Comp
m_GhostOverridePoint = NiPoint3Constant::ZERO;
m_GhostOverride = false;
RegisterMsg<GameMessages::GetObjectReportInfo>(this, &GhostComponent::MsgGetObjectReportInfo);
RegisterMsg(&GhostComponent::OnToggleGMInvis);
RegisterMsg(&GhostComponent::OnGetGMInvis);
RegisterMsg(&GhostComponent::MsgGetObjectReportInfo);
}
GhostComponent::~GhostComponent() {
@@ -61,8 +68,44 @@ void GhostComponent::GhostEntity(LWOOBJID id) {
m_ObservedEntities.erase(id);
}
bool GhostComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
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;

View File

@@ -7,6 +7,10 @@
class NiPoint3;
namespace tinyxml2 {
class XMLDocument;
}
class GhostComponent final : public Component {
public:
static inline const eReplicaComponentType ComponentType = eReplicaComponentType::GHOST;
@@ -39,9 +43,14 @@ public:
void GhostEntity(const LWOOBJID id);
bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg);
bool OnToggleGMInvis(GameMessages::ToggleGMInvis& msg);
bool OnGetGMInvis(GameMessages::GetGMInvis& msg);
bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg);
private:
NiPoint3 m_GhostReferencePoint;
NiPoint3 m_GhostOverridePoint;
@@ -51,6 +60,9 @@ private:
std::unordered_set<LWOOBJID> m_LimboConstructions;
bool m_GhostOverride;
bool m_IsGMInvisible{ false };
};
#endif //!__GHOSTCOMPONENT__H__

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -44,8 +44,7 @@
#include <ranges>
InventoryComponent::InventoryComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
using namespace GameMessages;
RegisterMsg<GetObjectReportInfo>(this, &InventoryComponent::OnGetObjectReportInfo);
RegisterMsg(&InventoryComponent::OnGetObjectReportInfo);
this->m_Dirty = true;
this->m_Equipped = {};
this->m_Pushed = {};
@@ -967,8 +966,9 @@ void InventoryComponent::EquipScripts(Item* equippedItem) {
auto* itemScript = CppScripts::GetScript(m_Parent, scriptCompData.script_name);
if (!itemScript) {
LOG("null script?");
} else {
itemScript->OnFactionTriggerItemEquipped(m_Parent, equippedItem->GetId());
}
itemScript->OnFactionTriggerItemEquipped(m_Parent, equippedItem->GetId());
}
}
@@ -982,15 +982,13 @@ 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());
}
}
void InventoryComponent::HandlePossession(Item* item) {
auto* characterComponent = m_Parent->GetComponent<CharacterComponent>();
if (!characterComponent) return;
auto* possessorComponent = m_Parent->GetComponent<PossessorComponent>();
if (!possessorComponent) return;
@@ -1006,52 +1004,31 @@ void InventoryComponent::HandlePossession(Item* item) {
return;
}
GameMessages::SendSetStunned(m_Parent->GetObjectID(), eStateChangeType::PUSH, m_Parent->GetSystemAddress(), LWOOBJID_EMPTY, true, false, true, false, false, false, false, true, true, true, true, true, true, true, true, true);
// Set the mount Item ID so that we know what were handling
// Set the mount item ID so that we know what we're handling
possessorComponent->SetMountItemID(item->GetId());
GameMessages::SendSetMountInventoryID(m_Parent, item->GetId(), UNASSIGNED_SYSTEM_ADDRESS);
// Create entity to mount
auto startRotation = m_Parent->GetRotation();
// Create the mount entity
EntityInfo info{};
info.lot = item->GetLot();
info.pos = m_Parent->GetPosition();
info.rot = startRotation;
info.rot = m_Parent->GetRotation();
info.spawnerID = m_Parent->GetObjectID();
auto* mount = Game::entityManager->CreateEntity(info, nullptr, m_Parent);
// Check to see if the mount is a vehicle, if so, flip it
auto* vehicleComponent = mount->GetComponent<HavokVehiclePhysicsComponent>();
if (vehicleComponent) characterComponent->SetIsRacing(true);
// Setup the destroyable stats
auto* destroyableComponent = mount->GetComponent<DestroyableComponent>();
if (destroyableComponent) {
destroyableComponent->SetIsImmune(true);
}
// Mount it
auto* possessableComponent = mount->GetComponent<PossessableComponent>();
if (possessableComponent) {
possessableComponent->SetIsItemSpawned(true);
possessableComponent->SetPossessor(m_Parent->GetObjectID());
// Possess it
possessorComponent->SetPossessable(mount->GetObjectID());
possessorComponent->SetPossessableType(possessableComponent->GetPossessionType());
}
GameMessages::SendSetJetPackMode(m_Parent, false);
auto* destroyableComponent = mount->GetComponent<DestroyableComponent>();
if (destroyableComponent) destroyableComponent->SetIsImmune(true);
// Make it go to the client
Game::entityManager->ConstructEntity(mount);
// Update the possessor
Game::entityManager->SerializeEntity(m_Parent);
possessorComponent->Mount(mount);
// have to unlock the input so it vehicle can be driven
if (vehicleComponent) GameMessages::SendVehicleUnlockInput(mount->GetObjectID(), false, m_Parent->GetSystemAddress());
GameMessages::SendMarkInventoryItemAsActive(m_Parent->GetObjectID(), true, eUnequippableActiveType::MOUNT, item->GetId(), m_Parent->GetSystemAddress());
}
@@ -1634,7 +1611,7 @@ void InventoryComponent::LoadPetXml(const tinyxml2::XMLDocument& document) {
DatabasePet databasePet;
databasePet.lot = lot;
databasePet.moderationState = moderationStatus;
databasePet.name = std::string(name);
databasePet.name = name ? name : "";
SetDatabasePet(id, databasePet);
@@ -1847,9 +1824,8 @@ std::string DebugInvToString(const eInventoryType inv, bool verbose) {
return "";
}
bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& report = static_cast<GameMessages::GetObjectReportInfo&>(msg);
auto& cmpt = report.info->PushDebug("Inventory");
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();
@@ -1859,7 +1835,7 @@ bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
for (const auto& [id, inventoryMut] : m_Inventories) {
if (!inventoryMut) continue;
const auto* const inventory = inventoryMut;
auto& curInv = itemsInBags.PushDebug(DebugInvToString(id, report.bVerbose) + " - " + std::to_string(id));
auto& curInv = itemsInBags.PushDebug(DebugInvToString(id, reportInfo.bVerbose) + " - " + std::to_string(id));
for (uint32_t i = 0; i < inventory->GetSize(); i++) {
const auto* const item = inventory->FindItemBySlot(i);
if (!item) continue;

View File

@@ -31,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
@@ -411,7 +415,7 @@ public:
// Used to migrate a character version, no need to call outside of that context
void RegenerateItemIDs();
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
~InventoryComponent() override;

View File

@@ -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;
};

View File

@@ -3,6 +3,7 @@
* Copyright 2019
*/
#include <ranges>
#include <sstream>
#include <string>
@@ -27,12 +28,11 @@ std::unordered_map<AchievementCacheKey, std::vector<uint32_t>> MissionComponent:
//! Initializer
MissionComponent::MissionComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
using namespace GameMessages;
m_LastUsedMissionOrderUID = Game::zoneManager->GetUniqueMissionIdStartingValue();
RegisterMsg<GetObjectReportInfo>(this, &MissionComponent::OnGetObjectReportInfo);
RegisterMsg<GameMessages::GetMissionState>(this, &MissionComponent::OnGetMissionState);
RegisterMsg<GameMessages::MissionNeedsLot>(this, &MissionComponent::OnMissionNeedsLot);
RegisterMsg(&MissionComponent::OnGetObjectReportInfo);
RegisterMsg(&MissionComponent::OnGetMissionState);
RegisterMsg(&MissionComponent::OnMissionNeedsLot);
}
//! Destructor
@@ -143,13 +143,22 @@ void MissionComponent::RemoveMission(uint32_t missionId) {
void MissionComponent::Progress(eMissionTaskType type, int32_t value, LWOOBJID associate, const std::string& targets, int32_t count, bool ignoreAchievements) {
LOG("Progressing missions %s %i %llu %s %s", StringifiedEnum::ToString(type).data(), value, associate, targets.c_str(), ignoreAchievements ? "(ignoring achievements)" : "");
std::vector<uint32_t> acceptedAchievements;
if (count > 0 && !ignoreAchievements) {
acceptedAchievements = LookForAchievements(type, value, true, associate, targets, count);
// If we are already iterating m_Missions, defer this call to avoid iterator invalidation
// from re-entrant insertions (e.g. a completing achievement triggering LookForAchievements).
if (m_IsProgressing) {
m_PendingProgress.push_back({ type, value, associate, targets, count, ignoreAchievements });
return;
}
std::vector<uint32_t> acceptedAchievements;
if (count > 0 && !ignoreAchievements) {
acceptedAchievements = LookForAchievements(type, value, associate, targets, count);
}
m_IsProgressing = true;
for (const auto& [id, mission] : m_Missions) {
if (!mission || std::find(acceptedAchievements.begin(), acceptedAchievements.end(), mission->GetMissionId()) != acceptedAchievements.end()) continue;
if (!mission) continue;
if (mission->IsAchievement() && ignoreAchievements) continue;
@@ -157,6 +166,18 @@ void MissionComponent::Progress(eMissionTaskType type, int32_t value, LWOOBJID a
mission->Progress(type, value, associate, targets, count);
}
m_IsProgressing = false;
// Drain any Progress() calls that were deferred during the loop above.
// Each call here may itself defer further calls, which are drained recursively
// before returning, so the while loop only needs one pass in practice.
while (!m_PendingProgress.empty()) {
auto pending = std::move(m_PendingProgress);
m_PendingProgress.clear();
for (const auto& p : pending) {
Progress(p.type, p.value, p.associate, p.targets, p.count, p.ignoreAchievements);
}
}
}
void MissionComponent::ForceProgress(const uint32_t missionId, const uint32_t taskId, const int32_t value, const bool acceptMission) {
@@ -279,10 +300,7 @@ bool MissionComponent::GetMissionInfo(uint32_t missionId, CDMissions& result) {
return true;
}
#define MISSION_NEW_METHOD
const std::vector<uint32_t> MissionComponent::LookForAchievements(eMissionTaskType type, int32_t value, bool progress, LWOOBJID associate, const std::string& targets, int32_t count) {
#ifdef MISSION_NEW_METHOD
const std::vector<uint32_t> MissionComponent::LookForAchievements(eMissionTaskType type, int32_t value, LWOOBJID associate, const std::string& targets, int32_t count) {
// Query for achievments, using the cache
const auto& result = QueryAchievements(type, value, targets);
@@ -309,85 +327,9 @@ const std::vector<uint32_t> MissionComponent::LookForAchievements(eMissionTaskTy
instance->Accept();
acceptedAchievements.push_back(missionID);
if (progress) {
// Progress mission to bring it up to speed
instance->Progress(type, value, associate, targets, count);
}
}
return acceptedAchievements;
#else
auto* missionTasksTable = CDClientManager::GetTable<CDMissionTasksTable>();
auto* missionsTable = CDClientManager::GetTable<CDMissionsTable>();
auto tasks = missionTasksTable->Query([=](const CDMissionTasks& entry) {
return entry.taskType == static_cast<unsigned>(type);
});
std::vector<uint32_t> acceptedAchievements;
for (const auto& task : tasks) {
if (GetMission(task.id) != nullptr) {
continue;
}
const auto missionEntries = missionsTable->Query([=](const CDMissions& entry) {
return entry.id == static_cast<int>(task.id) && !entry.isMission;
});
if (missionEntries.empty()) {
continue;
}
const auto mission = missionEntries[0];
if (mission.isMission || !MissionPrerequisites::CanAccept(mission.id, m_Missions)) {
continue;
}
if (task.target != value && task.targetGroup != targets) {
auto stream = std::istringstream(task.targetGroup);
std::string token;
auto found = false;
while (std::getline(stream, token, ',')) {
try {
const auto target = std::stoul(token);
found = target == value;
if (found) {
break;
}
} catch (std::invalid_argument& exception) {
LOG("Failed to parse target (%s): (%s)!", token.c_str(), exception.what());
}
}
if (!found) {
continue;
}
}
auto* instance = new Mission(this, mission.id);
m_Missions.insert_or_assign(mission.id, instance);
if (instance->IsMission()) instance->SetUniqueMissionOrderID(++m_LastUsedMissionOrderUID);
instance->Accept();
acceptedAchievements.push_back(mission.id);
if (progress) {
instance->Progress(type, value, associate, targets, count);
}
}
return acceptedAchievements;
#endif
}
const std::vector<uint32_t>& MissionComponent::QueryAchievements(eMissionTaskType type, int32_t value, const std::string targets) {
@@ -496,7 +438,7 @@ bool MissionComponent::RequiresItem(const LOT lot) {
}
}
const auto required = LookForAchievements(eMissionTaskType::GATHER, lot, false);
const auto required = LookForAchievements(eMissionTaskType::GATHER, lot);
return !required.empty();
}
@@ -507,45 +449,48 @@ void MissionComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) {
if (mis == nullptr) return;
auto* cur = mis->FirstChildElement("cur");
auto* done = mis->FirstChildElement("done");
auto* doneM = done->FirstChildElement();
while (doneM) {
int missionId;
doneM->QueryAttribute("id", &missionId);
auto* mission = new Mission(this, missionId);
mission->LoadFromXmlDone(*doneM);
doneM = doneM->NextSiblingElement();
m_Missions.insert_or_assign(missionId, mission);
}
auto* currentM = cur->FirstChildElement();
uint32_t missionOrder{};
while (currentM) {
int missionId;
currentM->QueryAttribute("id", &missionId);
auto* mission = m_Missions.contains(missionId) ? m_Missions[missionId] : new Mission(this, missionId);
mission->LoadFromXmlCur(*currentM);
if (currentM->QueryAttribute("o", &missionOrder) == tinyxml2::XML_SUCCESS && mission->IsMission()) {
mission->SetUniqueMissionOrderID(missionOrder);
if (missionOrder > m_LastUsedMissionOrderUID) m_LastUsedMissionOrderUID = missionOrder;
if (done) {
auto* doneM = done->FirstChildElement();
while (doneM) {
int missionId;
doneM->QueryAttribute("id", &missionId);
auto* mission = new Mission(this, missionId);
mission->LoadFromXmlDone(*doneM);
doneM = doneM->NextSiblingElement();
m_Missions.insert_or_assign(missionId, mission);
}
}
auto* cur = mis->FirstChildElement("cur");
if (cur) {
auto* currentM = cur->FirstChildElement();
currentM = currentM->NextSiblingElement();
uint32_t missionOrder{};
while (currentM) {
int missionId;
m_Missions.insert_or_assign(missionId, mission);
currentM->QueryAttribute("id", &missionId);
auto* mission = m_Missions.contains(missionId) ? m_Missions[missionId] : new Mission(this, missionId);
mission->LoadFromXmlCur(*currentM);
if (currentM->QueryAttribute("o", &missionOrder) == tinyxml2::XML_SUCCESS && mission->IsMission()) {
mission->SetUniqueMissionOrderID(missionOrder);
if (missionOrder > m_LastUsedMissionOrderUID) m_LastUsedMissionOrderUID = missionOrder;
}
currentM = currentM->NextSiblingElement();
m_Missions.insert_or_assign(missionId, mission);
}
}
}
@@ -706,9 +651,8 @@ void PushMissions(const std::map<uint32_t, Mission*>& missions, AMFArrayValue& V
}
}
bool MissionComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
auto& missionInfo = reportMsg.info->PushDebug("Mission (Laggy)");
bool MissionComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
auto& missionInfo = reportInfo.info->PushDebug("Mission (Laggy)");
missionInfo.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
// Sort the missions so they are easier to parse and present to the end user
std::map<uint32_t, Mission*> achievements;
@@ -724,26 +668,51 @@ bool MissionComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
// None of these should be empty, but if they are dont print the field
if (!achievements.empty() || !missions.empty()) {
auto& incompleteMissions = missionInfo.PushDebug("Incomplete Missions");
PushMissions(achievements, incompleteMissions, reportMsg.bVerbose);
PushMissions(missions, incompleteMissions, reportMsg.bVerbose);
PushMissions(achievements, incompleteMissions, reportInfo.bVerbose);
PushMissions(missions, incompleteMissions, reportInfo.bVerbose);
}
if (!doneMissions.empty()) {
auto& completeMissions = missionInfo.PushDebug("Completed Missions");
PushMissions(doneMissions, completeMissions, reportMsg.bVerbose);
PushMissions(doneMissions, completeMissions, reportInfo.bVerbose);
}
return true;
}
bool MissionComponent::OnGetMissionState(GameMessages::GameMsg& msg) {
auto misState = static_cast<GameMessages::GetMissionState&>(msg);
misState.missionState = GetMissionState(misState.missionID);
bool MissionComponent::OnGetMissionState(GameMessages::GetMissionState& getMissionState) {
getMissionState.missionState = GetMissionState(getMissionState.missionID);
return true;
}
bool MissionComponent::OnMissionNeedsLot(GameMessages::GameMsg& msg) {
const auto& needMsg = static_cast<GameMessages::MissionNeedsLot&>(msg);
return RequiresItem(needMsg.item);
bool MissionComponent::OnMissionNeedsLot(GameMessages::MissionNeedsLot& missionNeedsLot) {
return RequiresItem(missionNeedsLot.item);
}
void MissionComponent::FixRacingMetaMissions() {
for (auto* const mission : m_Missions | std::views::values) {
if (!mission || mission->IsComplete()) continue;
for (auto* const task : mission->GetTasks()) {
if (!task) continue;
// has to be a racing meta mission and have a taskparam1 of 4
if (task->GetType() != eMissionTaskType::RACING || !task->GetClientInfo().taskParam1.starts_with("4")) continue;
// Each target is racing mission that needs to be completed.
// If its completed, progress the meta task by 1.
uint32_t progress = 0;
for (const auto& target : task->GetAllTargets()) {
if (target == 0) continue;
auto* racingMission = GetMission(target);
if (racingMission && racingMission->IsComplete()) {
progress++;
}
}
task->SetProgress(progress);
}
// in case the mission is actually complete, give them the rewards
mission->CheckCompletion();
}
}

View File

@@ -134,13 +134,12 @@ public:
* Checks if there's any achievements we might be able to accept for the given parameters
* @param type the task type for tasks in the achievement that we wish to progress
* @param value the value to progress by
* @param progress if we can accept the mission, this will apply the progression
* @param associate optional associate related to mission progression
* @param targets optional multiple targets related to mission progression
* @param count the number of values to progress by (differs by task type)
* @return true if a achievement was accepted, false otherwise
*/
const std::vector<uint32_t> LookForAchievements(eMissionTaskType type, int32_t value, bool progress = true, LWOOBJID associate = LWOOBJID_EMPTY, const std::string& targets = "", int32_t count = 1);
const std::vector<uint32_t> LookForAchievements(eMissionTaskType type, int32_t value, LWOOBJID associate = LWOOBJID_EMPTY, const std::string& targets = "", int32_t count = 1);
/**
* Checks if there's a mission active that requires the collection of the specified LOT
@@ -170,10 +169,12 @@ public:
bool HasMission(uint32_t missionId);
void ResetMission(const int32_t missionId);
void FixRacingMetaMissions();
private:
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
bool OnGetMissionState(GameMessages::GameMsg& msg);
bool OnMissionNeedsLot(GameMessages::GameMsg& msg);
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
bool OnGetMissionState(GameMessages::GetMissionState& getMissionState);
bool OnMissionNeedsLot(GameMessages::MissionNeedsLot& missionNeedsLot);
/**
* All the missions owned by this entity, mapped by mission ID
*/
@@ -206,6 +207,25 @@ private:
* In live this value started at 745.
*/
uint32_t m_LastUsedMissionOrderUID = 746U;
/**
* Holds arguments for a Progress() call that arrived re-entrantly while m_Missions was
* being iterated, so it can be replayed after the active loop finishes.
*/
struct PendingProgress {
eMissionTaskType type{};
int32_t value{};
LWOOBJID associate{};
std::string targets;
int32_t count{};
bool ignoreAchievements{};
};
/// True while the m_Missions range-for loop in Progress() is executing.
bool m_IsProgressing = false;
/// Re-entrant Progress() calls deferred here for replay after the active loop finishes.
std::vector<PendingProgress> m_PendingProgress;
};
#endif // MISSIONCOMPONENT_H

View File

@@ -8,45 +8,43 @@
#include "ControlBehaviorMsgs.h"
#include "tinyxml2.h"
#include "InventoryComponent.h"
#include "MissionComponent.h"
#include "SimplePhysicsComponent.h"
#include "eMissionTaskType.h"
#include "eObjectBits.h"
#include "Database.h"
#include "DluAssert.h"
ModelComponent::ModelComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
using namespace GameMessages;
m_OriginalPosition = m_Parent->GetDefaultPosition();
m_OriginalRotation = m_Parent->GetDefaultRotation();
LOG("%f %f %f %f", m_OriginalRotation.x, m_OriginalRotation.y, m_OriginalRotation.z, m_OriginalRotation.w);
m_IsPaused = false;
m_NumListeningInteract = 0;
m_userModelID = m_Parent->GetVarAs<LWOOBJID>(u"userModelID");
RegisterMsg<RequestUse>(this, &ModelComponent::OnRequestUse);
RegisterMsg<ResetModelToDefaults>(this, &ModelComponent::OnResetModelToDefaults);
RegisterMsg<GetObjectReportInfo>(this, &ModelComponent::OnGetObjectReportInfo);
RegisterMsg(&ModelComponent::OnRequestUse);
RegisterMsg(&ModelComponent::OnResetModelToDefaults);
RegisterMsg(&ModelComponent::OnGetObjectReportInfo);
}
bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) {
auto& reset = static_cast<GameMessages::ResetModelToDefaults&>(msg);
for (auto& behavior : m_Behaviors) behavior.HandleMsg(reset);
GameMessages::UnSmash unsmash;
unsmash.target = GetParent()->GetObjectID();
unsmash.duration = 0.0f;
unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS);
bool ModelComponent::OnResetModelToDefaults(GameMessages::ResetModelToDefaults& reset) {
if (reset.bResetBehaviors) for (auto& behavior : m_Behaviors) behavior.HandleMsg(reset);
m_Parent->SetPosition(m_OriginalPosition);
m_Parent->SetRotation(m_OriginalRotation);
if (reset.bUnSmash) {
GameMessages::UnSmash unsmash;
unsmash.target = GetParent()->GetObjectID();
unsmash.duration = 0.0f;
unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS);
m_NumActiveUnSmash = 0;
}
if (reset.bResetPos) m_Parent->SetPosition(m_OriginalPosition);
if (reset.bResetRot) m_Parent->SetRotation(m_OriginalRotation);
m_Parent->SetVelocity(NiPoint3Constant::ZERO);
GameMessages::SetAngularVelocity setAngVel;
setAngVel.target = m_Parent->GetObjectID();
setAngVel.angVelocity = NiPoint3Constant::ZERO;
setAngVel.Send();
m_Speed = 3.0f;
m_NumListeningInteract = 0;
m_NumActiveUnSmash = 0;
m_NumActiveAttack = 0;
GameMessages::SetFaction set{};
@@ -61,10 +59,9 @@ bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) {
return true;
}
bool ModelComponent::OnRequestUse(GameMessages::GameMsg& msg) {
bool ModelComponent::OnRequestUse(GameMessages::RequestUse& requestUse) {
bool toReturn = false;
if (!m_IsPaused) {
auto& requestUse = static_cast<GameMessages::RequestUse&>(msg);
for (auto& behavior : m_Behaviors) behavior.HandleMsg(requestUse);
toReturn = true;
}
@@ -189,6 +186,7 @@ void ModelComponent::AddBehavior(AddMessage& msg) {
// Check if this behavior is able to be found via lot (if so, its a loot behavior).
insertedBehavior.SetIsLoot(inventoryComponent->FindItemByLot(msg.GetBehaviorId(), eInventoryType::BEHAVIORS));
}
ProgressAddBehaviorMission(*playerEntity);
}
auto* const simplePhysComponent = m_Parent->GetComponent<SimplePhysicsComponent>();
@@ -198,6 +196,11 @@ void ModelComponent::AddBehavior(AddMessage& msg) {
}
}
void ModelComponent::ProgressAddBehaviorMission(Entity& playerEntity) {
auto* const missionComponent = playerEntity.GetComponent<MissionComponent>();
if (missionComponent) missionComponent->Progress(eMissionTaskType::ADD_BEHAVIOR, 0);
}
std::string ModelComponent::SaveBehavior(const PropertyBehavior& behavior) const {
tinyxml2::XMLDocument doc;
auto* root = doc.NewElement("Behavior");
@@ -309,38 +312,6 @@ void ModelComponent::SetVelocity(const NiPoint3& velocity) const {
m_Parent->SetVelocity(velocity);
}
bool ModelComponent::TrySetAngularVelocity(const NiPoint3& angularVelocity) const {
GameMessages::GetAngularVelocity getAngVel{};
getAngVel.target = m_Parent->GetObjectID();
if (!getAngVel.Send()) {
LOG("Couldn't get angular velocity for %llu", m_Parent->GetObjectID());
return false;
}
GameMessages::SetAngularVelocity setAngVel{};
setAngVel.target = m_Parent->GetObjectID();
if (angularVelocity != NiPoint3Constant::ZERO) {
setAngVel.angVelocity = getAngVel.angVelocity;
const auto [x, y, z] = angularVelocity * m_Speed;
if (x != 0.0f) {
if (getAngVel.angVelocity.x != 0.0f) return false;
setAngVel.angVelocity.x = x;
} else if (y != 0.0f) {
if (getAngVel.angVelocity.y != 0.0f) return false;
setAngVel.angVelocity.y = y;
} else if (z != 0.0f) {
if (getAngVel.angVelocity.z != 0.0f) return false;
setAngVel.angVelocity.z = z;
}
} else {
setAngVel.angVelocity = angularVelocity;
}
LOG("Setting angular velocity to %f %f %f", setAngVel.angVelocity.x, setAngVel.angVelocity.y, setAngVel.angVelocity.z);
setAngVel.Send();
return true;
}
void ModelComponent::OnChatMessageReceived(const std::string& sMessage) {
for (auto& behavior : m_Behaviors) behavior.OnChatMessageReceived(sMessage);
}
@@ -377,10 +348,9 @@ void ModelComponent::RemoveAttack() {
}
}
bool ModelComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
if (!reportMsg.info) return false;
auto& cmptInfo = reportMsg.info->PushDebug("Model Behaviors (Mutable)");
bool ModelComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
if (!reportInfo.info) return false;
auto& cmptInfo = reportInfo.info->PushDebug("Model Behaviors (Mutable)");
cmptInfo.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
cmptInfo.PushDebug<AMFStringValue>("Name") = "Objects_" + std::to_string(m_Parent->GetLOT()) + "_name";

View File

@@ -32,9 +32,9 @@ public:
void LoadBehaviors();
void Update(float deltaTime) override;
bool OnRequestUse(GameMessages::GameMsg& msg);
bool OnResetModelToDefaults(GameMessages::GameMsg& msg);
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
bool OnRequestUse(GameMessages::RequestUse& requestUse);
bool OnResetModelToDefaults(GameMessages::ResetModelToDefaults& resetModelToDefaults);
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;
@@ -98,6 +98,8 @@ public:
return msg.GetNeedsNewBehaviorID();
};
void ProgressAddBehaviorMission(Entity& playerEntity);
void AddBehavior(AddMessage& msg);
void RemoveBehavior(MoveToInventoryMessage& msg, const bool keepItem);
@@ -145,11 +147,6 @@ public:
// Force sets the velocity to a value.
void SetVelocity(const NiPoint3& velocity) const;
// Attempts to set the angular velocity of the model.
// If the axis currently has a velocity of zero, returns true.
// If the axis is currently controlled by a behavior, returns false.
bool TrySetAngularVelocity(const NiPoint3& angularVelocity) const;
void OnChatMessageReceived(const std::string& sMessage);
void OnHit();
@@ -167,8 +164,6 @@ public:
// Decrements the number of strips listening for an attack.
// If this is the last strip removing an attack, it will reset the factions to the default of -1.
void RemoveAttack();
float GetSpeed() const noexcept { return m_Speed; }
private:
// Loads a behavior from the database.

View File

@@ -16,6 +16,7 @@
#include "CDComponentsRegistryTable.h"
#include "QuickBuildComponent.h"
#include "CDPhysicsComponentTable.h"
#include "Amf3.h"
#include "dNavMesh.h"
@@ -57,6 +58,8 @@ MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t component
m_SavedVelocity = NiPoint3Constant::ZERO;
m_IsBounced = false;
RegisterMsg(&MovementAIComponent::OnGetObjectReportInfo);
if (!m_Parent->GetComponent<BaseCombatAIComponent>()) SetPath(m_Parent->GetVarAsString(u"attached_path"));
}
@@ -422,3 +425,63 @@ void MovementAIComponent::SetMaxSpeed(const float value) {
m_MaxSpeed = value;
m_Acceleration = value / 5;
}
bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
auto& movementInfo = reportInfo.info->PushDebug("MovementAI");
if (m_Path) {
movementInfo.PushDebug<AMFStringValue>("Path") = m_Path->pathName;
}
auto& movementAiInfo = movementInfo.PushDebug("Movement AI Info");
movementAiInfo.PushDebug<AMFStringValue>("Movement Type") = m_Info.movementType;
movementAiInfo.PushDebug<AMFDoubleValue>("Wander Radius") = m_Info.wanderRadius;
movementAiInfo.PushDebug<AMFDoubleValue>("Wander Speed") = m_Info.wanderSpeed;
movementAiInfo.PushDebug<AMFDoubleValue>("Wander Chance") = m_Info.wanderChance;
movementAiInfo.PushDebug<AMFDoubleValue>("Wander Delay Min") = m_Info.wanderDelayMin;
movementAiInfo.PushDebug<AMFDoubleValue>("Wander Delay Max") = m_Info.wanderDelayMax;
auto& speedInfo = movementInfo.PushDebug("Speed Info");
speedInfo.PushDebug<AMFDoubleValue>("Base Speed") = m_BaseSpeed;
speedInfo.PushDebug<AMFDoubleValue>("Max Speed") = m_MaxSpeed;
speedInfo.PushDebug<AMFDoubleValue>("Current Speed") = m_CurrentSpeed;
speedInfo.PushDebug<AMFDoubleValue>("Acceleration") = m_Acceleration;
movementInfo.PushDebug<AMFDoubleValue>("Halt Distance") = m_HaltDistance;
movementInfo.PushDebug<AMFDoubleValue>("Time To Travel") = m_TimeToTravel;
movementInfo.PushDebug<AMFDoubleValue>("Time Travelled") = m_TimeTravelled;
movementInfo.PushDebug<AMFBoolValue>("Lock Rotation") = m_LockRotation;
movementInfo.PushDebug<AMFBoolValue>("Paused") = m_Paused;
movementInfo.PushDebug<AMFDoubleValue>("Pulling To Point") = m_PullingToPoint;
auto& pullPointInfo = movementInfo.PushDebug("Pull Point");
pullPointInfo.PushDebug<AMFDoubleValue>("X") = m_PullPoint.x;
pullPointInfo.PushDebug<AMFDoubleValue>("Y") = m_PullPoint.y;
pullPointInfo.PushDebug<AMFDoubleValue>("Z") = m_PullPoint.z;
// movementInfo.PushDebug<AMFDoubleValue>("Delay") = m_Delay;
auto& waypoints = movementInfo.PushDebug("Interpolated Waypoints");
int i = 0;
for (const auto& point : m_InterpolatedWaypoints) {
auto& waypoint = waypoints.PushDebug("Waypoint " + std::to_string(++i));
waypoint.PushDebug<AMFDoubleValue>("X") = point.x;
waypoint.PushDebug<AMFDoubleValue>("Y") = point.y;
waypoint.PushDebug<AMFDoubleValue>("Z") = point.z;
}
i = 0;
auto& currentPath = movementInfo.PushDebug("Current Path");
auto pathCopy = m_CurrentPath; // Copy to avoid modifying the original stack
while (!pathCopy.empty()) {
const auto& waypoint = pathCopy.top();
auto& pathWaypoint = currentPath.PushDebug("Waypoint " + std::to_string(++i));
pathWaypoint.PushDebug<AMFDoubleValue>("X") = waypoint.position.x;
pathWaypoint.PushDebug<AMFDoubleValue>("Y") = waypoint.position.y;
pathWaypoint.PushDebug<AMFDoubleValue>("Z") = waypoint.position.z;
pathCopy.pop();
}
return true;
}

View File

@@ -211,6 +211,7 @@ public:
bool IsPaused() const { return m_Paused; }
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
private:
/**

View File

@@ -17,7 +17,10 @@ MultiZoneEntranceComponent::MultiZoneEntranceComponent(Entity* parent, const int
MultiZoneEntranceComponent::~MultiZoneEntranceComponent() {}
void MultiZoneEntranceComponent::OnUse(Entity* originator) {
auto* rocket = originator->GetComponent<CharacterComponent>()->RocketEquip(originator);
auto* const characterComponent = originator->GetComponent<CharacterComponent>();
if (!characterComponent) return;
auto* rocket = characterComponent->RocketEquip(originator);
if (!rocket) return;
// the LUP world menu is just the property menu, the client knows how to handle it
@@ -26,7 +29,7 @@ void MultiZoneEntranceComponent::OnUse(Entity* originator) {
void MultiZoneEntranceComponent::OnSelectWorld(Entity* originator, uint32_t index) {
auto* rocketLaunchpadControlComponent = m_Parent->GetComponent<RocketLaunchpadControlComponent>();
if (!rocketLaunchpadControlComponent) return;
if (!rocketLaunchpadControlComponent || index >= m_LUPWorlds.size()) return;
rocketLaunchpadControlComponent->Launch(originator, m_LUPWorlds[index], 0);
}

View File

@@ -922,7 +922,9 @@ void PetComponent::Deactivate() {
}
void PetComponent::Release() {
auto* inventoryComponent = GetOwner()->GetComponent<InventoryComponent>();
auto* const owner = GetOwner();
if (!owner) return;
auto* const inventoryComponent = owner->GetComponent<InventoryComponent>();
if (inventoryComponent == nullptr) {
return;
@@ -932,9 +934,9 @@ void PetComponent::Release() {
inventoryComponent->RemoveDatabasePet(m_DatabaseId);
auto* item = inventoryComponent->FindItemBySubKey(m_DatabaseId);
auto* const item = inventoryComponent->FindItemBySubKey(m_DatabaseId);
item->SetCount(0, false, false);
if (item) item->SetCount(0, false, false);
}
void PetComponent::Command(const NiPoint3& position, const LWOOBJID source, const int32_t commandType, const int32_t typeId, const bool overrideObey) {

Some files were not shown because too many files have changed in this diff Show More