Compare commits

...

75 Commits

Author SHA1 Message Date
David Markowitz
a1891955e2 fix: leave team when fully logged out (#2007)
* feat: spawner weights

* remove ref

* default weights to 1

* fix: remove team member if they've logged out

tested that if i logout, after 20 seconds the team member is removed.
2026-06-20 02:50:49 -07:00
David Markowitz
7456d6b5c1 feat: spawner weights (#2006)
* feat: spawner weights

* remove ref

* default weights to 1
2026-06-19 19:08:15 -07:00
David Markowitz
308412f46e fix: enemies snapping to the incorrect position if they had a path and trying to use the path if they were aggro'd to an enemy (#2005)
* fix: enemies snapping to the incorrect position if they had a path

tested that ags enemies no longer snap backwards a large amount

* fix: move the home point so we can aggro correctly
2026-06-19 02:12:47 -07:00
David Markowitz
56504d9447 fix: add range checks to npc combat skill behavior (#2003)
* fix: add range checks to npc combat skill behavior

tested that all enemies now cast skills smartly based on range to targets, and do not cast skills if they are out of range.

fixes an issue where the spider queen could attack you outside the normal range

fixes an issue where entering happy flower caused you to need to restart the client

fixes #965

* feedback
2026-06-19 01:27:49 -05:00
David Markowitz
ce9d4e823c feat: enemies now use weights on their attacks (#2004)
* feat: enemies now use weights on their attacks

tested that 8 times out of 10, in close range, spiders did a web attack instead of a melee attack, vs the prior behavior of always following a pattern

fixes #2002

* feedback
2026-06-19 01:27:14 -05:00
David Markowitz
0f17e1de3b feat: enemy npc pathing (#2000)
* feat: enemy npc pathing

they live 🎉
tested that enemies path all around the world should they have a path configured.
tested that the admiral in gf (at the first camp) paths now.
fixes #1546

* feedback
2026-06-17 23:07:36 -07:00
David Markowitz
c898356eba fix: enemies not interrupting QB's when they do damage (#1998)
tested that stromlings in AG now correctly interrupt quickbuilds if the player takes damage
2026-06-16 09:49:56 -05:00
David Markowitz
79bb48d3bc feat: implement a bunch of basic scripts that don't really do anything (#1999)
* feat: implement a bunch of basic scripts that don't really do anything

None of these do anything noticeable or break anything

* fixes
2026-06-16 09:49:30 -05:00
David Markowitz
c0d055e66e fix dragon fire trails (#1997)
tested that the fire breath attack works now (hack fix)
2026-06-15 09:44:24 -07:00
David Markowitz
7937951f7f fix: mech build not showing up (#1996)
tested that the mech build, the fv rock build, and the frakjaw builds (to get to the instancer) all function as intented
2026-06-15 02:20:43 -05:00
David Markowitz
0101933f5c chore: cleanup pointer management for LDF data (#1995)
* change network settings from vector to LwoNameValue

* move settings on Entity to managed memory

* Migrate more members

* chore: remove pointer leakage from raw ldf pointers

* feedback

* fix ci
2026-06-14 20:54:52 -07:00
David Markowitz
90db1ac699 fix: incorrect network variable being set (#1994) 2026-06-13 00:21:53 -07:00
Daniel Seiler
9f8d300340 fix(docker): Unset MAXIMUM_OUTGOING_BANDWIDTH by default (#1548) 2026-06-11 18:38:38 -07:00
David Markowitz
1e9b18fa9d chore: cleanup usage of pointers in the activity component (#1989)
* chore: cleanup usage of pointers in the activity component

* feedback
2026-06-11 09:12:43 -05:00
David Markowitz
e5b8e5c6b7 fix: buggy hitboxes during ag foot race (#1993)
* physics fixes

* check ptr
2026-06-11 09:12:31 -05:00
David Markowitz
707880b5fc fix: fv pipe quick build not spawning as it should (#1991)
tested that the pipe now spawns a ROCK that you can build.  This ROCK you build spawns the PIPE now.
new bug: if you start building the ROCK and stop, the pipe will spawn instead of the previous rock.
2026-06-11 09:12:06 -05:00
David Markowitz
90607bdd5c fix: setting smashable ignoring lnv settings (#1992)
tested that the computer build on crux no longer stays around for +12 seconds
2026-06-11 09:11:52 -05:00
David Markowitz
bb8f569354 chore: Remove pointer usage in trading (#1988)
* chore: Remove pointer usage in trading

tested that I could still do a trade

* Update TradingManager.h

* remove returned object
2026-06-09 16:05:21 -05:00
David Markowitz
93076dc36d chore: simplify metrics (#1987)
* chore: simplify metrics

rename to hpp and remove unused includes

* feedback
2026-06-08 21:42:32 -07:00
David Markowitz
a307f0601a fix: bugs in private instances causing master crashes (#1986)
* fix: bugs in private instances causing master crashes

tested that creating a private instance and shutting down the server no longer crashes master

* Update InstanceManager.cpp
2026-06-08 23:22:04 -05:00
David Markowitz
045e097b13 chore: cleanup zoneIM (#1985) 2026-06-08 22:57:33 -05:00
Aaron Kimbrell
ca0da9d3bf feat: preconditions improvements (#1983)
* feat: implement missing precondition types (20, 21, 23) and pet checks

Add DoesNotHaveFlag (23), NotFreeTrial (20), and MissionActive (21) to
PreconditionType and implement their checks. Also implement PetDeployed
and IsPetTaming using PetComponent static helpers, matching client
behavior — both are simple boolean checks with no LOT comparison.
LegoClubMember is set to always pass as DLU has no membership concept.

* fix: update TODO comments for team check and racing licence preconditions

* type

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

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-08 22:45:24 -05:00
David Markowitz
1d2de705fb fix: old man npc mission (#1982)
* fix: old man npc mission

tested that the repeatable daily now has to actually be done and also can actually be done.

* Update OldManNPC.cpp
2026-06-08 20:44:29 -07: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
David Markowitz
a70c365c23 feat banana (#1917) 2025-10-19 14:00:14 -05:00
David Markowitz
281d9762ef fix: tac arc sorting and target acquisition (#1916) 2025-10-19 07:23:54 -05:00
David Markowitz
002aa896d8 feat: debug information (#1915) 2025-10-19 07:22:45 -05:00
David Markowitz
f3a5f60d81 feat: more destroyable debug info (#1912)
* feat: more destroyable info

* Change type and remove duplicate value
2025-10-16 14:15:02 -05:00
David Markowitz
4c9c773ec5 fix: powerup drops and hardcore loot drops (#1914)
tested the following are now functional
ag buff station
tiki torch
ve rocket part boxes
ns statue
property behavior
extra items from full inventory
hardcore drops (items and coins)
2025-10-16 14:13:38 -05:00
David Markowitz
ec6253c80c fix: coin dupe on same team (#1911)
* feat: Loot rework

* Allow dupe powerup pickups

* change default team loot to shared

* fix: coin dupe on team
2025-10-15 22:36:45 -05:00
Aaron Kimbrell
c2dba31f70 fix: bbb splitting dupe issue (#1908)
* fix bbb group splitting issues

* address feedback
2025-10-15 16:45:09 -07:00
David Markowitz
74630b56c8 feat: Loot rework (#1909)
* feat: Loot rework

* Allow dupe powerup pickups

* change default team loot to shared
2025-10-15 00:53:39 -05:00
David Markowitz
fd6029ae10 feat: read from server macros folder as well (#1906) 2025-10-11 15:33:38 -07:00
David Markowitz
ff645a6662 feat: model debug (#1907) 2025-10-11 15:33:28 -07:00
David Markowitz
e051229fb6 feat: InventoryComponent debug info (#1902) 2025-10-11 00:58:52 -05:00
Aaron Kimbrell
ce28834dce feat: lxfml splitting for bbb (#1877)
* LXFML SPLITTING
Included test file

* move base to global namespace

* wip need to test

* update last fixes

* update world sending bbb to be more efficient

* Address feedback form Emo in doscord

* Make LXFML class for robust and add more tests to edge cases and malformed data

* get rid of the string copy and make the deep clone have a recursive limit

* cleanup tests

* fix test file locations

* fix file path

* KISS

* add cmakelists

* fix typos

* NL @ EOF

* tabs and split out to func

* naming standard
2025-10-10 23:07:16 -05:00
David Markowitz
cbdd5d9bc6 fix: dying while dead (#1905) 2025-10-10 01:15:21 -05:00
297 changed files with 6633 additions and 3005 deletions

29
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,29 @@
# GitHub Copilot Instructions
* c++20 standard, please use the latest features except NO modules.
* use `.contains` for searching in associative containers
* use const as much as possible. If it can be const, it should be made const
* DO NOT USE const_cast EVER.
* use `cstdint` bitwidth types ALWAYS for integral types.
* NEVER use std::wstring. If wide strings are necessary, use std::u16string with conversion utilties in GeneralUtils.h.
* Functions are ALWAYS PascalCase.
* local variables are camelCase
* NEVER use snake case
* indentation is TABS, not SPACES.
* TABS are 4 spaces by default
* Use trailing braces ALWAYS
* global variables are prefixed with `g_`
* if global variables or functions are needed, they should be located in an anonymous namespace
* Use `GeneralUtils::TryParse` for ANY parsing of strings to integrals.
* Use brace initialization when possible.
* ALWAYS default initialize variables.
* Pointers should be avoided unless necessary. Use references when the pointer has been checked and should not be null
* headers should be as compact as possible. Do NOT include extra data that isnt needed.
* Remember to include logs (LOG macro uses printf style logging) while putting verbose logs under LOG_DEBUG.
* NEVER USE `RakNet::BitStream::ReadBit`
* NEVER assume pointers are good, always check if they are null. Once a pointer is checked and is known to be non-null, further accesses no longer need checking
* Be wary of TOCTOU. Prevent all possible issues relating to TOCTOU.
* new memory allocations should never be used unless absolutely necessary.
* new for reconstruction of objects is allowed
* Prefer following the format of the file over correct formatting. Consistency over correctness.
* When using auto, ALWAYS put a * for pointers.

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)'
vs-version: '[18,19)'
msbuild-architecture: x64
- name: Install libssl and switch to XCode 15.2 (Mac Only)
if: ${{ matrix.os == 'macos-13' }}
run: |
brew install openssl@3
sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
- name: Get CMake 3.x
uses: lukka/get-cmake@28983e0d3955dba2bb0a6810caae0c6cf268ec0c
- name: Get CMake
uses: lukka/get-cmake@591817e96fcad43505fb4eae36172462abb3a42e # v4.3.3
with:
cmakeVersion: "~3.25.0" # <--= optional, use most recent 3.25.x version
cmakeVersion: "latest"
- name: cmake
uses: lukka/run-cmake@67c73a83a46f86c4e0b96b741ac37ff495478c38
uses: lukka/run-cmake@5d55ea7949e25f69f0ecb516d8d572297e03a956 # v10.9
with:
workflowPreset: "ci-${{matrix.os}}"
workflowPreset: "${{ matrix.debug_preset }}"
- name: Extract Linux debug symbols
if: matrix.os == 'ubuntu-24.04'
run: |
find build -type f -name '*Server' | while read bin; do
objcopy --only-keep-debug "$bin" "${bin}.debug"
objcopy --strip-debug --add-gnu-debuglink="${bin}.debug" "$bin"
done
- name: artifacts
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-${{matrix.os}}
name: build-${{matrix.artifact}}
path: |
build/*/*Server*
build/*/*.ini
@@ -52,5 +67,30 @@ jobs:
build/*/navmeshes/
build/*/migrations/
build/*/*.dcf
!build/*/*.pdb
!build/*/d*/
!build/*/*.dSYM/
!build/**/*.debug
- name: debug symbols (Windows)
if: matrix.os == 'windows-2025'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: debug-${{matrix.artifact}}
path: |
build/*/*.pdb
build/*/d*/
retention-days: 30
- name: debug symbols (Linux)
if: matrix.os == 'ubuntu-24.04'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: debug-${{matrix.artifact}}
path: build/**/*.debug
retention-days: 30
- name: debug symbols (macOS)
if: matrix.os == 'macos-15-intel'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: debug-${{matrix.artifact}}
path: build/**/*.dSYM/
retention-days: 30

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,7 @@ set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Export the compile commands for debugging
set(CMAKE_POLICY_DEFAULT_CMP0063 NEW) # Set CMAKE visibility policy to NEW on project and subprojects
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) # Set C and C++ symbol visibility to hide inlined functions
@@ -67,7 +68,11 @@ set(RECASTNAVIGATION_EXAMPLES OFF CACHE BOOL "" FORCE)
# Disabled no-register
# Disabled unknown pragmas because Linux doesn't understand Windows pragmas.
if(UNIX)
add_link_options("-Wl,-rpath,$ORIGIN/")
if(APPLE)
add_link_options("-Wl,-rpath,@loader_path/")
else()
add_link_options("-Wl,-rpath,$ORIGIN/")
endif()
add_compile_options("-fPIC")
add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0 _GLIBCXX_USE_CXX17_ABI=0)

View File

@@ -49,7 +49,7 @@
"inherits": "default",
"displayName": "[Multi] Windows (MSVC)",
"description": "Set architecture to 64-bit (b/c RakNet)",
"generator": "Visual Studio 17 2022",
"generator": "Visual Studio 18 2026",
"binaryDir": "${sourceDir}/build/msvc",
"architecture": {
"value": "x64"
@@ -162,6 +162,13 @@
"rhs": "Darwin"
},
"binaryDir": "${sourceDir}/build/macos"
},
{
"name": "macos-relwithdebinfo",
"inherits": ["macos", "relwithdebinfo-config"],
"displayName": "[RelWithDebInfo] MacOS",
"description": "Create a RelWithDebInfo build for MacOS",
"binaryDir": "${sourceDir}/build/macos-relwithdebinfo"
}
],
"buildPresets": [
@@ -255,7 +262,7 @@
{
"name": "macos-relwithdebinfo",
"inherits": "default",
"configurePreset": "macos",
"configurePreset": "macos-relwithdebinfo",
"displayName": "[RelWithDebInfo] MacOS",
"description": "This preset is used to build in release mode with debug info on MacOS",
"configuration": "RelWithDebInfo"
@@ -374,7 +381,7 @@
{
"name": "macos-relwithdebinfo",
"inherits": "default",
"configurePreset": "macos",
"configurePreset": "macos-relwithdebinfo",
"displayName": "[RelWithDebInfo] MacOS",
"description": "Runs all tests on a MacOS configuration",
"configuration": "RelWithDebInfo"
@@ -603,7 +610,7 @@
"steps": [
{
"type": "configure",
"name": "macos"
"name": "macos-relwithdebinfo"
},
{
"type": "build",
@@ -616,7 +623,7 @@
]
},
{
"name": "ci-macos-13",
"name": "ci-macos-15-intel",
"displayName": "[Release] MacOS",
"description": "CI workflow preset for MacOS",
"steps": [

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

@@ -105,15 +105,7 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) {
auto* team = TeamContainer::GetTeam(playerID);
if (team != nullptr) {
const auto memberName = GeneralUtils::UTF8ToUTF16(player.playerName);
for (const auto memberId : team->memberIDs) {
const auto& otherMember = GetPlayerData(memberId);
if (!otherMember) continue;
TeamContainer::SendTeamSetOffWorldFlag(otherMember, playerID, { 0, 0, 0 });
}
TeamContainer::RemoveMember(team, playerID, false, false, true);
}
ChatWeb::SendWSPlayerUpdate(player, eActivityType::PlayerLoggedOut);
@@ -176,6 +168,7 @@ LWOOBJID PlayerContainer::GetId(const std::u16string& playerName) {
return toReturn;
}
// TODO Make this a pointer again or do something to make it so you cant edit the LWOOBJID_EMPTY entry? it should be ignored in all cases anyways though...
PlayerData& PlayerContainer::GetPlayerDataMutable(const LWOOBJID& playerID) {
return m_Players.contains(playerID) ? m_Players[playerID] : m_Players[LWOOBJID_EMPTY];
}

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

@@ -374,6 +374,21 @@ public:
return value->Insert<AmfType>("value", std::make_unique<AmfType>());
}
AMFArrayValue& PushDebug(const NiPoint3& point) {
PushDebug<AMFDoubleValue>("X") = point.x;
PushDebug<AMFDoubleValue>("Y") = point.y;
PushDebug<AMFDoubleValue>("Z") = point.z;
return *this;
}
AMFArrayValue& PushDebug(const NiQuaternion& rot) {
PushDebug<AMFDoubleValue>("W") = rot.w;
PushDebug<AMFDoubleValue>("X") = rot.x;
PushDebug<AMFDoubleValue>("Y") = rot.y;
PushDebug<AMFDoubleValue>("Z") = rot.z;
return *this;
}
private:
/**
* The associative portion. These values are key'd with strings to an AMFValue.

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

@@ -49,11 +49,10 @@ if (UNIX)
elseif (WIN32)
include(FetchContent)
# TODO Keep an eye on the zlib repository for an update to disable testing. Don't forget to update CMakePresets
FetchContent_Declare(
zlib
URL https://github.com/madler/zlib/archive/refs/tags/v1.2.11.zip
URL_HASH MD5=9d6a627693163bbbf3f26403a3a0b0b1
URL https://github.com/madler/zlib/archive/refs/tags/v1.3.2.zip
URL_HASH MD5=adbba6eef8960c3412818b2e241f46dc
GIT_PROGRESS TRUE
GIT_SHALLOW 1
)
@@ -62,12 +61,12 @@ elseif (WIN32)
set(CMAKE_POLICY_DEFAULT_CMP0048 NEW)
# Disable warning about the minimum version of cmake used for bcrypt being deprecated in the future
set(CMAKE_WARN_DEPRECATED OFF CACHE BOOL "" FORCE)
# Disable zlib tests
set(ZLIB_BUILD_TESTING OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(zlib)
set(ZLIB_INCLUDE_DIRS ${zlib_SOURCE_DIR} ${zlib_BINARY_DIR})
set_target_properties(zlib PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIRS}")
add_library(ZLIB::ZLIB ALIAS zlib)
else ()
message(
FATAL_ERROR

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

@@ -10,163 +10,151 @@
#include <string_view>
#include <vector>
using LDFKey = std::string_view;
using LDFTypeAndValue = std::string_view;
using LDFType = std::string_view;
using LDFValue = std::string_view;
//! Returns a pointer to a LDFData value based on string format
LDFBaseData* LDFBaseData::DataFromString(const std::string_view& format) {
std::unique_ptr<LDFBaseData> LDFBaseData::DataFromString(const std::string_view& format) {
std::unique_ptr<LDFBaseData> toReturn;
// A valid LDF must be at least 3 characters long (=0:) is the shortest valid LDF (empty UTF-16 key with no initial value)
if (format.empty() || format.length() <= 2) return nullptr;
auto equalsPosition = format.find('=');
// You can have an empty key, just make sure the type and value might exist
if (equalsPosition == std::string::npos || equalsPosition == (format.size() - 1)) return nullptr;
if (!format.empty() && format.length() > 2) {
auto equalsPosition = format.find('=');
// You can have an empty key, just make sure the type and value might exist
if (equalsPosition != std::string::npos && equalsPosition != (format.size() - 1)) {
std::pair<LDFKey, LDFTypeAndValue> keyValue;
keyValue.first = format.substr(0, equalsPosition);
keyValue.second = format.substr(equalsPosition + 1, format.size());
const std::string_view keyValue = format.substr(0, equalsPosition);
const std::string_view typeAndValue = format.substr(equalsPosition + 1, format.size());
std::u16string key = GeneralUtils::ASCIIToUTF16(keyValue.first);
const auto key = GeneralUtils::ASCIIToUTF16(keyValue);
auto colonPosition = keyValue.second.find(':');
const auto colonPosition = typeAndValue.find(':');
// If : is the first thing after an =, then this is an invalid LDF since
// we dont have a type to use.
if (colonPosition == std::string::npos || colonPosition == 0) return nullptr;
// If : is the first thing after an =, then this is an invalid LDF since
// we dont have a type to use.
if (colonPosition != std::string::npos && colonPosition != 0) {
const std::string_view ldfType = typeAndValue.substr(0, colonPosition);
const std::string_view ldfValue = typeAndValue.substr(colonPosition + 1, typeAndValue.size());
std::pair<LDFType, LDFValue> ldfTypeAndValue;
ldfTypeAndValue.first = keyValue.second.substr(0, colonPosition);
ldfTypeAndValue.second = keyValue.second.substr(colonPosition + 1, keyValue.second.size());
// Only allow empty values for string values.
if (!ldfValue.empty() || (ldfType == "0" /* UTF-16 */ || ldfType == "13" /* UTF-8 */)) {
const eLDFType type = GeneralUtils::TryParse<eLDFType>(ldfType, LDF_TYPE_UNKNOWN);
switch (type) {
case LDF_TYPE_UTF_16: {
std::u16string data = GeneralUtils::UTF8ToUTF16(ldfValue);
toReturn.reset(new LDFData<std::u16string>(key, data));
break;
}
// Only allow empty values for string values.
if (ldfTypeAndValue.second.size() == 0 && !(ldfTypeAndValue.first == "0" || ldfTypeAndValue.first == "13")) return nullptr;
case LDF_TYPE_S32: {
const auto data = GeneralUtils::TryParse<int32_t>(ldfValue);
if (data) {
toReturn.reset(new LDFData<int32_t>(key, data.value()));
} else {
LOG("Warning: Attempted to process invalid int32 value (%s) from string (%s)", ldfValue.data(), format.data());
}
eLDFType type;
char* storage;
try {
type = static_cast<eLDFType>(strtol(ldfTypeAndValue.first.data(), &storage, 10));
} catch (std::exception) {
LOG("Attempted to process invalid ldf type (%s) from string (%s)", ldfTypeAndValue.first.data(), format.data());
return nullptr;
}
break;
}
LDFBaseData* returnValue = nullptr;
switch (type) {
case LDF_TYPE_UTF_16: {
std::u16string data = GeneralUtils::UTF8ToUTF16(ldfTypeAndValue.second);
returnValue = new LDFData<std::u16string>(key, data);
break;
}
case LDF_TYPE_FLOAT: {
const auto data = GeneralUtils::TryParse<float>(ldfValue);
if (data) {
toReturn.reset(new LDFData<float>(key, data.value()));
} else {
LOG("Warning: Attempted to process invalid float value (%s) from string (%s)", ldfValue.data(), format.data());
}
break;
}
case LDF_TYPE_S32: {
const auto data = GeneralUtils::TryParse<int32_t>(ldfTypeAndValue.second);
if (!data) {
LOG("Warning: Attempted to process invalid int32 value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data());
return nullptr;
}
returnValue = new LDFData<int32_t>(key, data.value());
case LDF_TYPE_DOUBLE: {
const auto data = GeneralUtils::TryParse<double>(ldfValue);
if (data) {
toReturn.reset(new LDFData<double>(key, data.value()));
} else {
LOG("Warning: Attempted to process invalid double value (%s) from string (%s)", ldfValue.data(), format.data());
}
break;
}
break;
}
case LDF_TYPE_U32:
{
uint32_t data;
bool parsed = true;
// Have to do this really weird parsing to allow for copy ellision
if (ldfValue == "true") {
data = 1;
} else if (ldfValue == "false") {
data = 0;
} else {
const auto dataOptional = GeneralUtils::TryParse<uint32_t>(ldfValue);
if (!dataOptional) {
LOG("Warning: Attempted to process invalid uint32 value (%s) from string (%s)", ldfValue.data(), format.data());
parsed = false;
} else {
data = dataOptional.value();
}
}
case LDF_TYPE_FLOAT: {
const auto data = GeneralUtils::TryParse<float>(ldfTypeAndValue.second);
if (!data) {
LOG("Warning: Attempted to process invalid float value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data());
return nullptr;
}
returnValue = new LDFData<float>(key, data.value());
break;
}
if (parsed) toReturn.reset(new LDFData<uint32_t>(key, data));
break;
}
case LDF_TYPE_DOUBLE: {
const auto data = GeneralUtils::TryParse<double>(ldfTypeAndValue.second);
if (!data) {
LOG("Warning: Attempted to process invalid double value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data());
return nullptr;
}
returnValue = new LDFData<double>(key, data.value());
break;
}
case LDF_TYPE_BOOLEAN: {
bool data;
bool parsed = true;
// Have to do this really weird parsing to allow for copy ellision
if (ldfValue == "true") {
data = true;
} else if (ldfValue == "false") {
data = false;
} else {
const auto dataOptional = GeneralUtils::TryParse<bool>(ldfValue);
if (!dataOptional) {
LOG("Warning: Attempted to process invalid bool value (%s) from string (%s)", ldfValue.data(), format.data());
parsed = false;
} else {
data = dataOptional.value();
}
}
case LDF_TYPE_U32:
{
uint32_t data;
if (parsed) toReturn.reset(new LDFData<bool>(key, data));
break;
}
if (ldfTypeAndValue.second == "true") {
data = 1;
} else if (ldfTypeAndValue.second == "false") {
data = 0;
} else {
const auto dataOptional = GeneralUtils::TryParse<uint32_t>(ldfTypeAndValue.second);
if (!dataOptional) {
LOG("Warning: Attempted to process invalid uint32 value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data());
return nullptr;
case LDF_TYPE_U64: {
const auto data = GeneralUtils::TryParse<uint64_t>(ldfValue);
if (data) {
toReturn.reset(new LDFData<uint64_t>(key, data.value()));
} else {
LOG("Warning: Attempted to process invalid uint64 value (%s) from string (%s)", ldfValue.data(), format.data());
}
break;
}
case LDF_TYPE_OBJID: {
const auto data = GeneralUtils::TryParse<LWOOBJID>(ldfValue);
if (data) {
toReturn.reset(new LDFData<LWOOBJID>(key, data.value()));
} else {
LOG("Warning: Attempted to process invalid LWOOBJID value (%s) from string (%s)", ldfValue.data(), format.data());
}
break;
}
case LDF_TYPE_UTF_8: {
toReturn.reset(new LDFData<std::string>(key, ldfValue.data()));
break;
}
case LDF_TYPE_UNKNOWN:
[[fallthrough]];
default: {
LOG("Warning: Attempted to process invalid unknown value (%s) from string (%s)", ldfValue.data(), format.data());
break;
}
}
}
}
data = dataOptional.value();
}
returnValue = new LDFData<uint32_t>(key, data);
break;
}
case LDF_TYPE_BOOLEAN: {
bool data;
if (ldfTypeAndValue.second == "true") {
data = true;
} else if (ldfTypeAndValue.second == "false") {
data = false;
} else {
const auto dataOptional = GeneralUtils::TryParse<bool>(ldfTypeAndValue.second);
if (!dataOptional) {
LOG("Warning: Attempted to process invalid bool value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data());
return nullptr;
}
data = dataOptional.value();
}
returnValue = new LDFData<bool>(key, data);
break;
}
case LDF_TYPE_U64: {
const auto data = GeneralUtils::TryParse<uint64_t>(ldfTypeAndValue.second);
if (!data) {
LOG("Warning: Attempted to process invalid uint64 value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data());
return nullptr;
}
returnValue = new LDFData<uint64_t>(key, data.value());
break;
}
case LDF_TYPE_OBJID: {
const auto data = GeneralUtils::TryParse<LWOOBJID>(ldfTypeAndValue.second);
if (!data) {
LOG("Warning: Attempted to process invalid LWOOBJID value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data());
return nullptr;
}
returnValue = new LDFData<LWOOBJID>(key, data.value());
break;
}
case LDF_TYPE_UTF_8: {
std::string data = ldfTypeAndValue.second.data();
returnValue = new LDFData<std::string>(key, data);
break;
}
case LDF_TYPE_UNKNOWN: {
LOG("Warning: Attempted to process invalid unknown value (%s) from string (%s)", ldfTypeAndValue.second.data(), format.data());
break;
}
default: {
LOG("Warning: Attempted to process invalid LDF type (%d) from string (%s)", type, format.data());
break;
}
}
return returnValue;
return toReturn;
}

View File

@@ -1,11 +1,12 @@
#ifndef __LDFFORMAT__H__
#define __LDFFORMAT__H__
#ifndef LDFFORMAT_H
#define LDFFORMAT_H
// Custom Classes
#include "dCommonVars.h"
#include "GeneralUtils.h"
// C++
#include <map>
#include <string>
#include <string_view>
#include <sstream>
@@ -46,17 +47,17 @@ public:
virtual std::string GetValueAsString() const = 0;
virtual LDFBaseData* Copy() const = 0;
virtual std::unique_ptr<LDFBaseData> Copy() const = 0;
/**
* Given an input string, return the data as a LDF key.
*/
static LDFBaseData* DataFromString(const std::string_view& format);
static std::unique_ptr<LDFBaseData> DataFromString(const std::string_view& format);
};
template<typename T>
class LDFData: public LDFBaseData {
class LDFData : public LDFBaseData {
private:
std::u16string key;
T value;
@@ -164,8 +165,8 @@ public:
return this->GetValueString();
}
LDFBaseData* Copy() const override {
return new LDFData<T>(key, value);
std::unique_ptr<LDFBaseData> Copy() const override {
return std::make_unique<LDFData<T>>(key, value);
}
inline static const T Default = {};
@@ -226,4 +227,89 @@ template<> inline std::string LDFData<LWOOBJID>::GetValueString() const { return
template<> inline std::string LDFData<std::string>::GetValueString() const { return this->value; }
#endif //!__LDFFORMAT__H__
struct LwoNameValue {
using LDFPtr = std::unique_ptr<LDFBaseData>;
using ValueType = std::map<std::u16string, LDFPtr>;
LwoNameValue& operator=(const LwoNameValue& other) {
this->values = other.Copy();
return *this;
}
template<typename T>
void Insert(const std::u16string& key, const T& value) {
this->values.insert_or_assign(key, std::unique_ptr(std::make_unique<LDFData<T>>(key, value)));
}
void Insert(const std::u16string& key, const char* value) {
this->Insert<std::string>(key, value);
}
void Insert(const std::u16string& key, const char16_t* value) {
this->Insert<std::u16string>(key, value);
}
template<typename T>
void Insert(const std::string& key, const T& value) {
this->Insert<T>(GeneralUtils::UTF8ToUTF16(key), value);
}
void Insert(const std::string& key, const char* value) {
this->Insert<std::string>(GeneralUtils::UTF8ToUTF16(key), value);
}
void Insert(const std::string& key, const char16_t* value) {
this->Insert<std::u16string>(GeneralUtils::UTF8ToUTF16(key), value);
}
const LDFPtr& ParseInsert(const std::string& data) {
LDFPtr toInsert(LDFBaseData::DataFromString(data));
return toInsert ?
this->values.insert_or_assign(toInsert->GetKey(), std::move(toInsert)).first->second :
this->values.insert_or_assign(u"FAILED_TO_PARSE_" + GeneralUtils::UTF8ToUTF16(data), std::make_unique<LDFData<std::string>>("", "")).first->second;
}
const LDFPtr& ParseInsert(const std::u16string& data) {
return this->ParseInsert(GeneralUtils::UTF16ToWTF8(data));
}
ValueType::const_iterator begin() const {
return this->values.cbegin();
}
ValueType::const_iterator end() const {
return this->values.cend();
}
void Erase(const std::u16string& key) {
this->values.erase(key);
}
void Erase(const std::string& key) {
this->Erase(GeneralUtils::ASCIIToUTF16(key));
}
ValueType::iterator find(const ValueType::key_type& key) {
return this->values.find(key);
}
ValueType::const_iterator find(const ValueType::key_type& key) const {
return this->values.find(key);
}
LwoNameValue() = default;
LwoNameValue(const LwoNameValue& other) {
this->values = other.Copy();
}
ValueType values;
private:
ValueType Copy() const {
ValueType copy;
for (const auto& [key, value] : this->values) copy.insert_or_assign(key, value->Copy());
return copy;
}
};
#endif //!LDFFORMAT_H

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

@@ -5,13 +5,43 @@
#include "TinyXmlUtils.h"
#include <ranges>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include <sstream>
namespace {
// The base LXFML xml file to use when creating new models.
std::string g_base = R"(<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<LXFML versionMajor="5" versionMinor="0">
<Meta>
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
<Brand name="LEGOUniverse"/>
<BrickSet version="457"/>
</Meta>
<Bricks>
</Bricks>
<RigidSystems>
</RigidSystems>
<GroupSystems>
<GroupSystem>
</GroupSystem>
</GroupSystems>
</LXFML>)";
}
Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) {
Result toReturn;
// Handle empty or invalid input
if (data.empty()) {
return toReturn;
}
tinyxml2::XMLDocument doc;
const auto err = doc.Parse(data.data());
// Use length-based parsing to avoid expensive string copy
const auto err = doc.Parse(data.data(), data.size());
if (err != tinyxml2::XML_SUCCESS) {
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
return toReturn;
}
@@ -20,7 +50,6 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
auto lxfml = reader["LXFML"];
if (!lxfml) {
LOG("Failed to find LXFML element.");
return toReturn;
}
@@ -49,16 +78,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
// Calculate the lowest and highest points on the entire model
for (const auto& transformation : transformations | std::views::values) {
auto split = GeneralUtils::SplitString(transformation, ',');
if (split.size() < 12) {
LOG("Not enough in the split?");
continue;
}
auto x = GeneralUtils::TryParse<float>(split[9]).value();
auto y = GeneralUtils::TryParse<float>(split[10]).value();
auto z = GeneralUtils::TryParse<float>(split[11]).value();
if (x < lowest.x) lowest.x = x;
if (y < lowest.y) lowest.y = y;
if (split.size() < 12) continue;
auto xOpt = GeneralUtils::TryParse<float>(split[9]);
auto yOpt = GeneralUtils::TryParse<float>(split[10]);
auto zOpt = GeneralUtils::TryParse<float>(split[11]);
if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) continue;
auto x = xOpt.value();
auto y = yOpt.value();
auto z = zOpt.value();
if (x < lowest.x) lowest.x = x;
if (y < lowest.y) lowest.y = y;
if (z < lowest.z) lowest.z = z;
if (highest.x < x) highest.x = x;
@@ -87,13 +119,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
for (auto& transformation : transformations | std::views::values) {
auto split = GeneralUtils::SplitString(transformation, ',');
if (split.size() < 12) {
LOG("Not enough in the split?");
continue;
}
auto x = GeneralUtils::TryParse<float>(split[9]).value() - newRootPos.x + curPosition.x;
auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y + curPosition.y;
auto z = GeneralUtils::TryParse<float>(split[11]).value() - newRootPos.z + curPosition.z;
auto xOpt = GeneralUtils::TryParse<float>(split[9]);
auto yOpt = GeneralUtils::TryParse<float>(split[10]);
auto zOpt = GeneralUtils::TryParse<float>(split[11]);
if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) {
continue;
}
auto x = xOpt.value() - newRootPos.x + curPosition.x;
auto y = yOpt.value() - newRootPos.y + curPosition.y;
auto z = zOpt.value() - newRootPos.z + curPosition.z;
std::stringstream stream;
for (int i = 0; i < 9; i++) {
stream << split[i];
@@ -128,3 +166,345 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
toReturn.center = newRootPos;
return toReturn;
}
// Deep-clone an XMLElement (attributes, text, and child elements) into a target document
// with maximum depth protection to prevent infinite loops
static tinyxml2::XMLElement* CloneElementDeep(const tinyxml2::XMLElement* src, tinyxml2::XMLDocument& dstDoc, int maxDepth = 100) {
if (!src || maxDepth <= 0) return nullptr;
auto* dst = dstDoc.NewElement(src->Name());
// copy attributes
for (const tinyxml2::XMLAttribute* attr = src->FirstAttribute(); attr; attr = attr->Next()) {
dst->SetAttribute(attr->Name(), attr->Value());
}
// copy children (elements and text)
for (const tinyxml2::XMLNode* child = src->FirstChild(); child; child = child->NextSibling()) {
if (const tinyxml2::XMLElement* childElem = child->ToElement()) {
// Recursively clone child elements with decremented depth
auto* clonedChild = CloneElementDeep(childElem, dstDoc, maxDepth - 1);
if (clonedChild) dst->InsertEndChild(clonedChild);
} else if (const tinyxml2::XMLText* txt = child->ToText()) {
auto* n = dstDoc.NewText(txt->Value());
dst->InsertEndChild(n);
} else if (const tinyxml2::XMLComment* c = child->ToComment()) {
auto* n = dstDoc.NewComment(c->Value());
dst->InsertEndChild(n);
}
}
return dst;
}
std::vector<Lxfml::Result> Lxfml::Split(const std::string_view data, const NiPoint3& curPosition) {
std::vector<Result> results;
// Handle empty or invalid input
if (data.empty()) {
return results;
}
// Prevent processing extremely large inputs that could cause hangs
if (data.size() > 10000000) { // 10MB limit
return results;
}
tinyxml2::XMLDocument doc;
// Use length-based parsing to avoid expensive string copy
const auto err = doc.Parse(data.data(), data.size());
if (err != tinyxml2::XML_SUCCESS) {
return results;
}
auto* lxfml = doc.FirstChildElement("LXFML");
if (!lxfml) {
return results;
}
// Build maps: partRef -> Part element, partRef -> Brick element, boneRef -> partRef, brickRef -> Brick element
std::unordered_map<std::string, tinyxml2::XMLElement*> partRefToPart;
std::unordered_map<std::string, tinyxml2::XMLElement*> partRefToBrick;
std::unordered_map<std::string, std::string> boneRefToPartRef;
std::unordered_map<std::string, tinyxml2::XMLElement*> brickByRef;
auto* bricksParent = lxfml->FirstChildElement("Bricks");
if (bricksParent) {
for (auto* brick = bricksParent->FirstChildElement("Brick"); brick; brick = brick->NextSiblingElement("Brick")) {
const char* brickRef = brick->Attribute("refID");
if (brickRef) brickByRef.emplace(std::string(brickRef), brick);
for (auto* part = brick->FirstChildElement("Part"); part; part = part->NextSiblingElement("Part")) {
const char* partRef = part->Attribute("refID");
if (partRef) {
partRefToPart.emplace(std::string(partRef), part);
partRefToBrick.emplace(std::string(partRef), brick);
}
auto* bone = part->FirstChildElement("Bone");
if (bone) {
const char* boneRef = bone->Attribute("refID");
if (boneRef) boneRefToPartRef.emplace(std::string(boneRef), partRef ? std::string(partRef) : std::string());
}
}
}
}
// Collect RigidSystem elements
std::vector<tinyxml2::XMLElement*> rigidSystems;
auto* rigidSystemsParent = lxfml->FirstChildElement("RigidSystems");
if (rigidSystemsParent) {
for (auto* rs = rigidSystemsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
rigidSystems.push_back(rs);
}
}
// Collect top-level groups (immediate children of GroupSystem)
std::vector<tinyxml2::XMLElement*> groupRoots;
auto* groupSystemsParent = lxfml->FirstChildElement("GroupSystems");
if (groupSystemsParent) {
for (auto* gs = groupSystemsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) {
for (auto* group = gs->FirstChildElement("Group"); group; group = group->NextSiblingElement("Group")) {
groupRoots.push_back(group);
}
}
}
// Track used bricks and rigidsystems
std::unordered_set<std::string> usedBrickRefs;
std::unordered_set<tinyxml2::XMLElement*> usedRigidSystems;
// Track used groups to avoid processing them twice
std::unordered_set<tinyxml2::XMLElement*> usedGroups;
// Helper to create output document from sets of brick refs and rigidsystem pointers
auto makeOutput = [&](const std::unordered_set<std::string>& bricksToInclude, const std::vector<tinyxml2::XMLElement*>& rigidSystemsToInclude, const std::vector<tinyxml2::XMLElement*>& groupsToInclude = {}) {
tinyxml2::XMLDocument outDoc;
outDoc.Parse(g_base.c_str());
auto* outRoot = outDoc.FirstChildElement("LXFML");
auto* outBricks = outRoot->FirstChildElement("Bricks");
auto* outRigidSystems = outRoot->FirstChildElement("RigidSystems");
auto* outGroupSystems = outRoot->FirstChildElement("GroupSystems");
// clone and insert bricks
for (const auto& bref : bricksToInclude) {
auto it = brickByRef.find(bref);
if (it == brickByRef.end()) continue;
tinyxml2::XMLElement* cloned = CloneElementDeep(it->second, outDoc);
if (cloned) outBricks->InsertEndChild(cloned);
}
// clone and insert rigidsystems
for (auto* rsPtr : rigidSystemsToInclude) {
tinyxml2::XMLElement* cloned = CloneElementDeep(rsPtr, outDoc);
if (cloned) outRigidSystems->InsertEndChild(cloned);
}
// clone and insert group(s) if requested
if (outGroupSystems && !groupsToInclude.empty()) {
// clear default children
while (outGroupSystems->FirstChild()) outGroupSystems->DeleteChild(outGroupSystems->FirstChild());
// create a GroupSystem element and append requested groups
auto* newGS = outDoc.NewElement("GroupSystem");
for (auto* gptr : groupsToInclude) {
tinyxml2::XMLElement* clonedG = CloneElementDeep(gptr, outDoc);
if (clonedG) newGS->InsertEndChild(clonedG);
}
outGroupSystems->InsertEndChild(newGS);
}
// Print to string
tinyxml2::XMLPrinter printer;
outDoc.Print(&printer);
// Normalize position and compute center using existing helper
std::string xmlString = printer.CStr();
if (xmlString.size() > 5000000) { // 5MB limit for normalization
Result emptyResult;
emptyResult.lxfml = xmlString;
return emptyResult;
}
auto normalized = NormalizePosition(xmlString, curPosition);
return normalized;
};
// 1) Process groups (each top-level Group becomes one output; nested groups are included)
for (auto* groupRoot : groupRoots) {
// Skip if this group was already processed as part of another group
if (usedGroups.find(groupRoot) != usedGroups.end()) continue;
// Helper to collect all partRefs in a group's subtree
std::function<void(const tinyxml2::XMLElement*, std::unordered_set<std::string>&)> collectParts = [&](const tinyxml2::XMLElement* g, std::unordered_set<std::string>& partRefs) {
if (!g) return;
const char* partAttr = g->Attribute("partRefs");
if (partAttr) {
for (auto& tok : GeneralUtils::SplitString(partAttr, ',')) partRefs.insert(tok);
}
for (auto* child = g->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectParts(child, partRefs);
};
// Collect all groups that need to be merged into this output
std::vector<tinyxml2::XMLElement*> groupsToInclude{ groupRoot };
usedGroups.insert(groupRoot);
// Build initial sets of bricks and boneRefs from the starting group
std::unordered_set<std::string> partRefs;
collectParts(groupRoot, partRefs);
std::unordered_set<std::string> bricksIncluded;
std::unordered_set<std::string> boneRefsIncluded;
for (const auto& pref : partRefs) {
auto pit = partRefToBrick.find(pref);
if (pit != partRefToBrick.end()) {
const char* bref = pit->second->Attribute("refID");
if (bref) bricksIncluded.insert(std::string(bref));
}
auto partIt = partRefToPart.find(pref);
if (partIt != partRefToPart.end()) {
auto* bone = partIt->second->FirstChildElement("Bone");
if (bone) {
const char* bref = bone->Attribute("refID");
if (bref) boneRefsIncluded.insert(std::string(bref));
}
}
}
// Iteratively include any RigidSystems that reference any boneRefsIncluded
// and check if those rigid systems' bricks span other groups
bool changed = true;
std::vector<tinyxml2::XMLElement*> rigidSystemsToInclude;
int maxIterations = 1000; // Safety limit to prevent infinite loops
int iteration = 0;
while (changed && iteration < maxIterations) {
changed = false;
iteration++;
// First, expand rigid systems based on current boneRefsIncluded
for (auto* rs : rigidSystems) {
if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue;
// parse boneRefs of this rigid system (from its <Rigid> children)
bool intersects = false;
std::vector<std::string> rsBoneRefs;
for (auto* rigid = rs->FirstChildElement("Rigid"); rigid; rigid = rigid->NextSiblingElement("Rigid")) {
const char* battr = rigid->Attribute("boneRefs");
if (!battr) continue;
for (auto& tok : GeneralUtils::SplitString(battr, ',')) {
rsBoneRefs.push_back(tok);
if (boneRefsIncluded.find(tok) != boneRefsIncluded.end()) intersects = true;
}
}
if (!intersects) continue;
// include this rigid system and all boneRefs it references
usedRigidSystems.insert(rs);
rigidSystemsToInclude.push_back(rs);
for (const auto& br : rsBoneRefs) {
boneRefsIncluded.insert(br);
auto bpIt = boneRefToPartRef.find(br);
if (bpIt != boneRefToPartRef.end()) {
auto partRef = bpIt->second;
auto pbIt = partRefToBrick.find(partRef);
if (pbIt != partRefToBrick.end()) {
const char* bref = pbIt->second->Attribute("refID");
if (bref && bricksIncluded.insert(std::string(bref)).second) changed = true;
}
}
}
}
// Second, check if the newly included bricks span any other groups
// If so, merge those groups into the current output
for (auto* otherGroup : groupRoots) {
if (usedGroups.find(otherGroup) != usedGroups.end()) continue;
// Collect partRefs from this other group
std::unordered_set<std::string> otherPartRefs;
collectParts(otherGroup, otherPartRefs);
// Check if any of these partRefs correspond to bricks we've already included
bool spansOtherGroup = false;
for (const auto& pref : otherPartRefs) {
auto pit = partRefToBrick.find(pref);
if (pit != partRefToBrick.end()) {
const char* bref = pit->second->Attribute("refID");
if (bref && bricksIncluded.find(std::string(bref)) != bricksIncluded.end()) {
spansOtherGroup = true;
break;
}
}
}
if (spansOtherGroup) {
// Merge this group into the current output
usedGroups.insert(otherGroup);
groupsToInclude.push_back(otherGroup);
changed = true;
// Add all partRefs, boneRefs, and bricks from this group
for (const auto& pref : otherPartRefs) {
auto pit = partRefToBrick.find(pref);
if (pit != partRefToBrick.end()) {
const char* bref = pit->second->Attribute("refID");
if (bref) bricksIncluded.insert(std::string(bref));
}
auto partIt = partRefToPart.find(pref);
if (partIt != partRefToPart.end()) {
auto* bone = partIt->second->FirstChildElement("Bone");
if (bone) {
const char* bref = bone->Attribute("refID");
if (bref) boneRefsIncluded.insert(std::string(bref));
}
}
}
}
}
}
if (iteration >= maxIterations) {
// Iteration limit reached, stop processing to prevent infinite loops
// The file is likely malformed, so just skip further processing
return results;
}
// include bricks from bricksIncluded into used set
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
// make output doc and push result (include all merged groups' XML)
auto normalized = makeOutput(bricksIncluded, rigidSystemsToInclude, groupsToInclude);
results.push_back(normalized);
}
// 2) Process remaining RigidSystems (each becomes its own file)
for (auto* rs : rigidSystems) {
if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue;
std::unordered_set<std::string> bricksIncluded;
// collect boneRefs referenced by this rigid system
for (auto* rigid = rs->FirstChildElement("Rigid"); rigid; rigid = rigid->NextSiblingElement("Rigid")) {
const char* battr = rigid->Attribute("boneRefs");
if (!battr) continue;
for (auto& tok : GeneralUtils::SplitString(battr, ',')) {
auto bpIt = boneRefToPartRef.find(tok);
if (bpIt != boneRefToPartRef.end()) {
auto partRef = bpIt->second;
auto pbIt = partRefToBrick.find(partRef);
if (pbIt != partRefToBrick.end()) {
const char* bref = pbIt->second->Attribute("refID");
if (bref) bricksIncluded.insert(std::string(bref));
}
}
}
}
// mark used
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
usedRigidSystems.insert(rs);
std::vector<tinyxml2::XMLElement*> rsVec{ rs };
auto normalized = makeOutput(bricksIncluded, rsVec);
results.push_back(normalized);
}
// 3) Any remaining bricks not included become their own files
for (const auto& [bref, brickPtr] : brickByRef) {
if (usedBrickRefs.find(bref) != usedBrickRefs.end()) continue;
std::unordered_set<std::string> bricksIncluded{ bref };
auto normalized = makeOutput(bricksIncluded, {});
results.push_back(normalized);
usedBrickRefs.insert(bref);
}
return results;
}

View File

@@ -6,6 +6,7 @@
#include <string>
#include <string_view>
#include <vector>
#include "NiPoint3.h"
@@ -18,6 +19,7 @@ namespace Lxfml {
// Normalizes a LXFML model to be positioned relative to its local 0, 0, 0 rather than a game worlds 0, 0, 0.
// Returns a struct of its new center and the updated LXFML containing these edits.
[[nodiscard]] Result NormalizePosition(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
[[nodiscard]] std::vector<Result> Split(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
// these are only for the migrations due to a bug in one of the implementations.
[[nodiscard]] Result NormalizePositionOnlyFirstPart(const std::string_view data);

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

@@ -1,105 +1,77 @@
#include "Metrics.hpp"
#include "Metrics.h"
#include "StringifiedEnum.h"
#include <chrono>
std::unordered_map<MetricVariable, Metric*> Metrics::m_Metrics = {};
std::vector<MetricVariable> Metrics::m_Variables = {
MetricVariable::GameLoop,
MetricVariable::PacketHandling,
MetricVariable::UpdateEntities,
MetricVariable::UpdateSpawners,
MetricVariable::Physics,
MetricVariable::UpdateReplica,
MetricVariable::Ghosting,
MetricVariable::CPUTime,
MetricVariable::Sleep,
MetricVariable::Frame,
};
namespace {
std::unordered_map<MetricVariable, Metric> g_Metrics = {};
std::vector<MetricVariable> g_Variables = {
MetricVariable::GameLoop,
MetricVariable::PacketHandling,
MetricVariable::UpdateEntities,
MetricVariable::UpdateSpawners,
MetricVariable::Physics,
MetricVariable::UpdateReplica,
MetricVariable::Ghosting,
MetricVariable::CPUTime,
MetricVariable::Sleep,
MetricVariable::Frame,
};
}
void Metrics::AddMeasurement(MetricVariable variable, int64_t value) {
const auto& iter = m_Metrics.find(variable);
Metric* metric;
if (iter == m_Metrics.end()) {
metric = new Metric();
m_Metrics[variable] = metric;
} else {
metric = iter->second;
}
auto& metric = g_Metrics[variable];
AddMeasurement(metric, value);
}
void Metrics::AddMeasurement(Metric* metric, int64_t value) {
const auto index = metric->measurementIndex;
void Metrics::AddMeasurement(Metric& metric, int64_t value) {
const auto index = metric.measurementIndex;
metric->measurements[index] = value;
metric.measurements[index] = value;
if (metric->max == -1 || value > metric->max) {
metric->max = value;
} else if (metric->min == -1 || metric->min > value) {
metric->min = value;
if (metric.max == -1 || value > metric.max) {
metric.max = value;
} else if (metric.min == -1 || metric.min > value) {
metric.min = value;
}
if (metric->measurementSize < MAX_MEASURMENT_POINTS) {
metric->measurementSize++;
if (metric.measurementSize < MAX_MEASURMENT_POINTS) {
metric.measurementSize++;
}
metric->measurementIndex = (index + 1) % MAX_MEASURMENT_POINTS;
metric.measurementIndex = (index + 1) % MAX_MEASURMENT_POINTS;
}
const Metric* Metrics::GetMetric(MetricVariable variable) {
const auto& iter = m_Metrics.find(variable);
if (iter == m_Metrics.end()) {
return nullptr;
}
Metric* metric = iter->second;
const Metric& Metrics::GetMetric(MetricVariable variable) {
auto& metric = g_Metrics[variable];
int64_t average = 0;
for (size_t i = 0; i < metric->measurementSize; i++) {
average += metric->measurements[i];
for (size_t i = 0; i < metric.measurementSize; i++) {
average += metric.measurements[i];
}
average /= metric->measurementSize;
average /= metric.measurementSize;
metric->average = average;
metric.average = average;
return metric;
}
void Metrics::StartMeasurement(MetricVariable variable) {
const auto& iter = m_Metrics.find(variable);
auto& metric = g_Metrics[variable];
Metric* metric;
if (iter == m_Metrics.end()) {
metric = new Metric();
m_Metrics[variable] = metric;
} else {
metric = iter->second;
}
metric->activeMeasurement = std::chrono::high_resolution_clock::now();
metric.activeMeasurement = std::chrono::high_resolution_clock::now();
}
void Metrics::EndMeasurement(MetricVariable variable) {
const auto end = std::chrono::high_resolution_clock::now();
const auto& iter = m_Metrics.find(variable);
auto& metric = g_Metrics[variable];
if (iter == m_Metrics.end()) {
return;
}
Metric* metric = iter->second;
const auto elapsed = end - metric->activeMeasurement;
const auto elapsed = end - metric.activeMeasurement;
const auto nanoseconds = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();
@@ -110,44 +82,12 @@ float Metrics::ToMiliseconds(int64_t nanoseconds) {
return static_cast<float>(nanoseconds) / 1e6;
}
std::string Metrics::MetricVariableToString(MetricVariable variable) {
switch (variable) {
case MetricVariable::GameLoop:
return "GameLoop";
case MetricVariable::PacketHandling:
return "PacketHandling";
case MetricVariable::UpdateEntities:
return "UpdateEntities";
case MetricVariable::UpdateSpawners:
return "UpdateSpawners";
case MetricVariable::Physics:
return "Physics";
case MetricVariable::UpdateReplica:
return "UpdateReplica";
case MetricVariable::Sleep:
return "Sleep";
case MetricVariable::CPUTime:
return "CPUTime";
case MetricVariable::Frame:
return "Frame";
case MetricVariable::Ghosting:
return "Ghosting";
default:
return "Invalid";
}
const std::string_view Metrics::MetricVariableToString(MetricVariable variable) {
return StringifiedEnum::ToString(variable);
}
const std::vector<MetricVariable>& Metrics::GetAllMetrics() {
return m_Variables;
}
void Metrics::Clear() {
for (const auto& pair : m_Metrics) {
delete pair.second;
}
m_Metrics.clear();
return g_Variables;
}
/* RSS Memory utilities

48
dCommon/Metrics.h Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
#include "dCommonVars.h"
#include <vector>
#include <map>
#include <string_view>
#include <unordered_map>
#include <chrono>
#define MAX_MEASURMENT_POINTS 1024
enum class MetricVariable : int32_t {
GameLoop,
PacketHandling,
UpdateEntities,
UpdateSpawners,
Physics,
UpdateReplica,
Ghosting,
CPUTime,
Sleep,
Frame,
};
struct Metric {
int64_t measurements[MAX_MEASURMENT_POINTS] = {};
size_t measurementIndex = 0;
size_t measurementSize = 0;
int64_t max = -1;
int64_t min = -1;
int64_t average = 0;
std::chrono::time_point<std::chrono::high_resolution_clock> activeMeasurement;
};
namespace Metrics {
void AddMeasurement(MetricVariable variable, int64_t value);
void AddMeasurement(Metric& metric, int64_t value);
const Metric& GetMetric(MetricVariable variable);
void StartMeasurement(MetricVariable variable);
void EndMeasurement(MetricVariable variable);
float ToMiliseconds(int64_t nanoseconds);
const std::string_view MetricVariableToString(MetricVariable variable);
const std::vector<MetricVariable>& GetAllMetrics();
size_t GetPeakRSS();
size_t GetCurrentRSS();
size_t GetProcessID();
};

View File

@@ -1,61 +0,0 @@
#pragma once
#include "dCommonVars.h"
#include <vector>
#include <map>
#include <unordered_map>
#include <chrono>
#define MAX_MEASURMENT_POINTS 1024
enum class MetricVariable : int32_t
{
GameLoop,
PacketHandling,
UpdateEntities,
UpdateSpawners,
Physics,
UpdateReplica,
Ghosting,
CPUTime,
Sleep,
Frame,
};
struct Metric
{
int64_t measurements[MAX_MEASURMENT_POINTS] = {};
size_t measurementIndex = 0;
size_t measurementSize = 0;
int64_t max = -1;
int64_t min = -1;
int64_t average = 0;
std::chrono::time_point<std::chrono::high_resolution_clock> activeMeasurement;
};
class Metrics
{
public:
~Metrics();
static void AddMeasurement(MetricVariable variable, int64_t value);
static void AddMeasurement(Metric* metric, int64_t value);
static const Metric* GetMetric(MetricVariable variable);
static void StartMeasurement(MetricVariable variable);
static void EndMeasurement(MetricVariable variable);
static float ToMiliseconds(int64_t nanoseconds);
static std::string MetricVariableToString(MetricVariable variable);
static const std::vector<MetricVariable>& GetAllMetrics();
static size_t GetPeakRSS();
static size_t GetCurrentRSS();
static size_t GetProcessID();
static void Clear();
private:
Metrics();
static std::unordered_map<MetricVariable, Metric*> m_Metrics;
static std::vector<MetricVariable> m_Variables;
};

View File

@@ -1,6 +1,8 @@
#ifndef __NIPOINT3_H__
#define __NIPOINT3_H__
#ifndef GLM_ENABLE_EXPERIMENTAL
# define GLM_ENABLE_EXPERIMENTAL
#endif
/*!
\file NiPoint3.hpp
\brief Defines a point in space in XYZ coordinates

View File

@@ -1,6 +1,8 @@
#ifndef NIQUATERNION_H
#define NIQUATERNION_H
#ifndef GLM_ENABLE_EXPERIMENTAL
# define GLM_ENABLE_EXPERIMENTAL
#endif
// Custom Classes
#include "NiPoint3.h"

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

@@ -81,6 +81,9 @@ public:
[[nodiscard]]
AssetStream GetFile(const char* name) const;
[[nodiscard]]
AssetStream GetFile(const std::string& name) const { return GetFile(name.c_str()); };
private:
void LoadPackIndex();

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,6 +58,7 @@ constexpr LWOCLONEID LWOCLONEID_INVALID = -1; //!< Invalid LWOCLONEID
constexpr LWOINSTANCEID LWOINSTANCEID_INVALID = -1; //!< Invalid LWOINSTANCEID
constexpr LWOMAPID LWOMAPID_INVALID = -1; //!< Invalid LWOMAPID
constexpr uint64_t LWOZONEID_INVALID = 0; //!< Invalid LWOZONEID
constexpr uint32_t MAX_MESSAGE_LENGTH = 0x500000; //!< Prevent exceptionally large msgs from being processed. Should always be used to check user provided inputs.
constexpr float PI = 3.14159f;
@@ -110,18 +111,6 @@ private:
constexpr LWOSCENEID LWOSCENEID_INVALID = -1;
struct LWONameValue {
uint32_t length = 0; //!< The length of the name
std::u16string name; //!< The name
LWONameValue() = default;
LWONameValue(const std::u16string& name) {
this->name = name;
this->length = static_cast<uint32_t>(name.length());
}
};
struct FriendData {
public:
bool isOnline = false;

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

@@ -38,3 +38,11 @@ std::vector<CDObjectSkills> CDObjectSkillsTable::Query(std::function<bool(CDObje
return data;
}
std::vector<CDObjectSkills> CDObjectSkillsTable::Get(const LOT lot) const {
std::vector<CDObjectSkills> toReturn;
for (const auto& entry : GetEntries()) {
if (entry.objectTemplate == lot) toReturn.push_back(entry);
}
return toReturn;
}

View File

@@ -4,12 +4,13 @@
#include "CDTable.h"
#include <cstdint>
#include <vector>
struct CDObjectSkills {
uint32_t objectTemplate; //!< The LOT of the item
uint32_t skillID; //!< The Skill ID of the object
uint32_t castOnType; //!< ???
uint32_t AICombatWeight; //!< ???
int32_t AICombatWeight; //!< ???
};
class CDObjectSkillsTable : public CDTable<CDObjectSkillsTable, std::vector<CDObjectSkills>> {
@@ -17,5 +18,6 @@ public:
void LoadValuesFromDatabase();
// Queries the table with a custom "where" clause
std::vector<CDObjectSkills> Query(std::function<bool(CDObjectSkills)> predicate);
std::vector<CDObjectSkills> Get(const LOT lot) 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

@@ -84,6 +84,8 @@
#include "GhostComponent.h"
#include "AchievementVendorComponent.h"
#include "VanityUtilities.h"
#include "ObjectIDManager.h"
#include "ePlayerFlag.h"
// Table includes
#include "CDComponentsRegistryTable.h"
@@ -187,12 +189,21 @@ Entity::~Entity() {
}
if (m_ParentEntity) {
GameMessages::ChildRemoved removedMsg{};
removedMsg.childID = m_ObjectID;
removedMsg.target = m_ParentEntity->GetObjectID();
removedMsg.Send();
m_ParentEntity->RemoveChild(this);
}
}
void Entity::Initialize() {
RegisterMsg(MessageType::Game::REQUEST_SERVER_OBJECT_INFO, this, &Entity::MsgRequestServerObjectInfo);
RegisterMsg(&Entity::MsgRequestServerObjectInfo);
RegisterMsg(&Entity::MsgDropClientLoot);
RegisterMsg(&Entity::MsgGetFactionTokenType);
RegisterMsg(&Entity::MsgPickupItem);
RegisterMsg(&Entity::MsgChildRemoved);
RegisterMsg(&Entity::MsgGetFlag);
/**
* Setup trigger
*/
@@ -287,7 +298,7 @@ void Entity::Initialize() {
AddComponent<LUPExhibitComponent>(lupExhibitID);
}
const auto racingControlID =compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RACING_CONTROL);
const auto racingControlID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RACING_CONTROL);
if (racingControlID > 0) {
AddComponent<RacingControlComponent>(racingControlID);
}
@@ -416,9 +427,10 @@ void Entity::Initialize() {
comp->SetMaxArmor(destCompData[0].armor);
comp->SetDeathBehavior(destCompData[0].death_behavior);
comp->SetIsSmashable(destCompData[0].isSmashable);
comp->SetIsSmashable(comp->GetIsSmashable() || destCompData[0].isSmashable);
comp->SetLootMatrixID(destCompData[0].LootMatrixIndex);
comp->SetCurrencyIndex(destCompData[0].CurrencyIndex);
Loot::CacheMatrix(destCompData[0].LootMatrixIndex);
// Now get currency information
@@ -493,7 +505,7 @@ void Entity::Initialize() {
auto& systemAddress = m_Character->GetParentUser() ? m_Character->GetParentUser()->GetSystemAddress() : UNASSIGNED_SYSTEM_ADDRESS;
AddComponent<CharacterComponent>(characterID, m_Character, systemAddress)->LoadFromXml(m_Character->GetXMLDoc());
AddComponent<GhostComponent>(characterID);
AddComponent<GhostComponent>(characterID)->LoadFromXml(m_Character->GetXMLDoc());
}
const auto inventoryID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::INVENTORY);
@@ -937,13 +949,13 @@ void Entity::SetGMLevel(eGameMasterLevel value) {
}
}
void Entity::WriteLDFData(const std::vector<LDFBaseData*>& ldf, RakNet::BitStream& outBitStream) const {
void Entity::WriteLDFData(const LwoNameValue& ldf, RakNet::BitStream& outBitStream) const {
RakNet::BitStream settingStream;
int32_t numberOfValidKeys = ldf.size();
int32_t numberOfValidKeys = ldf.values.size();
// Writing keys value pairs the client does not expect to receive or interpret will result in undefined behavior,
// so we need to filter out any keys that are not valid and fix the number of valid keys to be correct.
for (LDFBaseData* data : ldf) {
for (const auto& data : ldf.values | std::views::values) {
if (data && data->GetValueType() != eLDFType::LDF_TYPE_UNKNOWN) {
data->WriteToPacket(settingStream);
} else {
@@ -975,16 +987,16 @@ void Entity::WriteBaseReplicaData(RakNet::BitStream& outBitStream, eReplicaPacke
const auto& syncLDF = GetVar<std::vector<std::u16string>>(u"syncLDF");
// Only sync for models.
if (!m_Settings.empty() && (GetComponent<ModelComponent>() && !GetComponent<PetComponent>())) {
if (!m_Settings.values.empty() && (GetComponent<ModelComponent>() && !GetComponent<PetComponent>())) {
outBitStream.Write1(); // Has ldf data
WriteLDFData(m_Settings, outBitStream);
} else if (!syncLDF.empty()) {
// Find all the ldf data we need to write
std::vector<LDFBaseData*> ldfData;
ldfData.reserve(m_Settings.size());
LwoNameValue ldfData;
for (const auto& data : syncLDF) {
ldfData.push_back(GetVarData(data));
const auto* toInsert = GetVarData(data);
if (toInsert) ldfData.values.insert_or_assign(data, toInsert->Copy());
}
outBitStream.Write1(); // Has ldf data
@@ -1601,26 +1613,10 @@ void Entity::Kill(Entity* murderer, const eKillType killType) {
else Game::entityManager->DestroyEntity(this);
}
const auto& grpNameQBShowBricks = GetVar<std::string>(u"grpNameQBShowBricks");
const auto& grpNameQBShowBricks = GetVarAsString(u"grpNameQBShowBricks");
if (!grpNameQBShowBricks.empty()) {
auto spawners = Game::zoneManager->GetSpawnersByName(grpNameQBShowBricks);
Spawner* spawner = nullptr;
if (!spawners.empty()) {
spawner = spawners[0];
} else {
spawners = Game::zoneManager->GetSpawnersInGroup(grpNameQBShowBricks);
if (!spawners.empty()) {
spawner = spawners[0];
}
}
if (spawner != nullptr) {
spawner->Spawn();
}
for (auto* const spawner : Game::zoneManager->GetSpawnersByName(grpNameQBShowBricks)) if (spawner) spawner->Spawn();
for (auto* const spawner : Game::zoneManager->GetSpawnersInGroup(grpNameQBShowBricks)) if (spawner) spawner->Spawn();
}
// Track a player being smashed
@@ -1663,7 +1659,7 @@ void Entity::AddLootItem(const Loot::Info& info) const {
auto* const characterComponent = GetComponent<CharacterComponent>();
if (!characterComponent) return;
LOG("Player %llu has been allowed to pickup %i with id %llu", m_ObjectID, info.lot, info.id);
auto& droppedLoot = characterComponent->GetDroppedLoot();
droppedLoot[info.id] = info;
}
@@ -2034,13 +2030,7 @@ void Entity::SetI64(const std::u16string& name, const int64_t value) {
}
bool Entity::HasVar(const std::u16string& name) const {
for (auto* data : m_Settings) {
if (data->GetKey() == name) {
return true;
}
}
return false;
return m_Settings.values.contains(name);
}
uint16_t Entity::GetNetworkId() const {
@@ -2072,24 +2062,13 @@ void Entity::SendNetworkVar(const std::string& data, const SystemAddress& sysAdd
GameMessages::SendSetNetworkScriptVar(this, sysAddr, data);
}
LDFBaseData* Entity::GetVarData(const std::u16string& name) const {
for (auto* data : m_Settings) {
if (data == nullptr) {
continue;
}
if (data->GetKey() != name) {
continue;
}
return data;
}
return nullptr;
const LDFBaseData* const Entity::GetVarData(const std::u16string& name) const {
const auto itr = m_Settings.values.find(name);
return itr != m_Settings.values.cend() ? itr->second.get() : nullptr;
}
std::string Entity::GetVarAsString(const std::u16string& name) const {
auto* data = GetVarData(name);
const auto* const data = GetVarData(name);
return data ? data->GetValueAsString() : "";
}
@@ -2240,13 +2219,13 @@ void Entity::RegisterMsg(const MessageType::Game msgId, std::function<bool(GameM
m_MsgHandlers.emplace(msgId, handler);
}
bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
auto& requestInfo = static_cast<GameMessages::RequestServerObjectInfo&>(msg);
bool Entity::MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfo& requestInfo) {
AMFArrayValue response;
response.Insert("visible", true);
response.Insert("objectID", std::to_string(m_ObjectID));
response.Insert("serverInfo", true);
GameMessages::GetObjectReportInfo info{};
info.clientID = requestInfo.clientId;
info.bVerbose = requestInfo.bVerbose;
info.info = response.InsertArray("data");
auto& objectInfo = info.info->PushDebug("Object Details");
@@ -2265,7 +2244,7 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
}
auto& configData = objectInfo.PushDebug("Config Data");
for (const auto config : m_Settings) {
for (const auto& config : m_Settings.values | std::views::values) {
configData.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(config->GetKey())) = config->GetValueAsString();
}
@@ -2275,3 +2254,73 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
if (client) GameMessages::SendUIMessageServerToSingleClient("ToggleObjectDebugger", response, client->GetSystemAddress());
return true;
}
bool Entity::MsgDropClientLoot(GameMessages::DropClientLoot& dropLootMsg) {
if (dropLootMsg.item != LOT_NULL && dropLootMsg.item != 0) {
Loot::Info info{
.id = dropLootMsg.lootID,
.lot = dropLootMsg.item,
.count = dropLootMsg.count,
};
AddLootItem(info);
}
if (dropLootMsg.item == LOT_NULL && dropLootMsg.currency != 0) {
RegisterCoinDrop(dropLootMsg.currency);
}
return true;
}
bool Entity::MsgGetFlag(GameMessages::GetFlag& flagMsg) {
if (m_Character) flagMsg.flag = m_Character->GetPlayerFlag(flagMsg.flagID);
return true;
}
bool Entity::MsgGetFactionTokenType(GameMessages::GetFactionTokenType& tokenMsg) {
GameMessages::GetFlag getFlagMsg{};
getFlagMsg.flagID = ePlayerFlag::ASSEMBLY_FACTION;
MsgGetFlag(getFlagMsg);
if (getFlagMsg.flag) tokenMsg.tokenType = 8318;
getFlagMsg.flagID = ePlayerFlag::SENTINEL_FACTION;
MsgGetFlag(getFlagMsg);
if (getFlagMsg.flag) tokenMsg.tokenType = 8319;
getFlagMsg.flagID = ePlayerFlag::PARADOX_FACTION;
MsgGetFlag(getFlagMsg);
if (getFlagMsg.flag) tokenMsg.tokenType = 8320;
getFlagMsg.flagID = ePlayerFlag::VENTURE_FACTION;
MsgGetFlag(getFlagMsg);
if (getFlagMsg.flag) tokenMsg.tokenType = 8321;
LOG("Returning token type %i", tokenMsg.tokenType);
return tokenMsg.tokenType != LOT_NULL;
}
bool Entity::MsgPickupItem(GameMessages::PickupItem& pickupItemMsg) {
if (GetObjectID() == pickupItemMsg.lootOwnerID) {
PickupItem(pickupItemMsg.lootID);
} else {
auto* const characterComponent = GetComponent<CharacterComponent>();
if (!characterComponent) return false;
auto& droppedLoot = characterComponent->GetDroppedLoot();
const auto it = droppedLoot.find(pickupItemMsg.lootID);
if (it != droppedLoot.end()) {
CDObjectsTable* objectsTable = CDClientManager::GetTable<CDObjectsTable>();
const CDObjects& object = objectsTable->GetByID(it->second.lot);
if (object.id != 0 && object.type == "Powerup") {
return false; // Let powerups be duplicated
}
}
droppedLoot.erase(pickupItemMsg.lootID);
}
return true;
}
bool Entity::MsgChildRemoved(GameMessages::ChildRemoved& msg) {
GetScript()->OnChildRemoved(*this, msg);
return true;
}

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 {
@@ -91,9 +97,9 @@ public:
LWOOBJID GetSpawnerID() const { return m_SpawnerID; }
const std::vector<LDFBaseData*>& GetSettings() const { return m_Settings; }
const LwoNameValue& GetSettings() const { return m_Settings; }
const std::vector<LDFBaseData*>& GetNetworkSettings() const { return m_NetworkSettings; }
const LwoNameValue& GetNetworkSettings() const { return m_NetworkSettings; }
bool GetIsDead() const;
@@ -106,6 +112,7 @@ public:
uint16_t GetNetworkId() const;
// Cannot return nullptr.
Entity* GetOwner() const;
const NiPoint3& GetDefaultPosition() const;
@@ -175,7 +182,12 @@ public:
void AddComponent(eReplicaComponentType componentId, Component* component);
bool MsgRequestServerObjectInfo(GameMessages::GameMsg& msg);
bool MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfo& msg);
bool MsgDropClientLoot(GameMessages::DropClientLoot& msg);
bool MsgGetFlag(GameMessages::GetFlag& msg);
bool MsgGetFactionTokenType(GameMessages::GetFactionTokenType& msg);
bool MsgPickupItem(GameMessages::PickupItem& msg);
bool MsgChildRemoved(GameMessages::ChildRemoved& msg);
// This is expceted to never return nullptr, an assert checks this.
CppScripts::Script* const GetScript() const;
@@ -300,6 +312,12 @@ public:
template<typename T>
void SetNetworkVar(const std::u16string& name, std::vector<T> value, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS);
template<typename T>
void SetNetworkVar(const std::string& name, T value, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS);
template<typename T>
LwoNameValue::ValueType::iterator InsertNetworkVar(const std::u16string& name, T value);
template<typename T>
T GetNetworkVar(const std::u16string& name);
@@ -312,11 +330,6 @@ public:
template<typename ComponentType, typename... VaArgs>
ComponentType* AddComponent(VaArgs... args);
/**
* Get the LDF data.
*/
LDFBaseData* GetVarData(const std::u16string& name) const;
/**
* Get the LDF value and convert it to a string.
*/
@@ -338,8 +351,19 @@ public:
bool HandleMsg(GameMessages::GameMsg& msg) const;
void RegisterMsg(const MessageType::Game msgId, auto* self, const auto handler) {
RegisterMsg(msgId, std::bind(handler, self, std::placeholders::_1));
// 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);
}
/**
@@ -348,13 +372,20 @@ public:
static Observable<Entity*, const PositionUpdate&> OnPlayerPositionUpdate;
private:
void WriteLDFData(const std::vector<LDFBaseData*>& ldf, RakNet::BitStream& outBitStream) const;
/**
* Get the LDF data.
*/
const LDFBaseData* const GetVarData(const std::u16string& name) const;
template<typename T>
LwoNameValue::ValueType::iterator InsertLnvData(LwoNameValue& lnv, const std::u16string& key, T value);
void WriteLDFData(const LwoNameValue& ldf, RakNet::BitStream& outBitStream) const;
LWOOBJID m_ObjectID;
LOT m_TemplateID;
std::vector<LDFBaseData*> m_Settings;
std::vector<LDFBaseData*> m_NetworkSettings;
LwoNameValue m_Settings;
LwoNameValue m_NetworkSettings;
NiPoint3 m_DefaultPosition;
NiQuaternion m_DefaultRotation = QuatUtils::IDENTITY;
@@ -436,13 +467,13 @@ T* Entity::GetComponent() const {
template<typename T>
const T& Entity::GetVar(const std::u16string& name) const {
auto* data = GetVarData(name);
const auto* const data = GetVarData(name);
if (data == nullptr) {
return LDFData<T>::Default;
}
auto* typed = dynamic_cast<LDFData<T>*>(data);
auto* typed = dynamic_cast<const LDFData<T>* const>(data);
if (typed == nullptr) {
return LDFData<T>::Default;
@@ -460,52 +491,44 @@ T Entity::GetVarAs(const std::u16string& name) const {
template<typename T>
void Entity::SetVar(const std::u16string& name, T value) {
auto* data = GetVarData(name);
InsertLnvData<T>(m_Settings, name, value);
}
if (data == nullptr) {
auto* data = new LDFData<T>(name, value);
m_Settings.push_back(data);
return;
template<typename T>
LwoNameValue::ValueType::iterator Entity::InsertLnvData(LwoNameValue& lnv, const std::u16string& key, T value) {
auto itr = lnv.values.find(key);
if (itr != lnv.values.end()) {
auto* lnvCast = dynamic_cast<LDFData<T>*>(itr->second.get());
if (!lnvCast) {
// Is of different type
itr->second = std::make_unique<LDFData<T>>(key, value);
} else {
// Is the same type and exists
lnvCast->SetValue(value);
}
} else {
// Doesn't exist
itr = lnv.values.insert_or_assign(key, std::make_unique<LDFData<T>>(key, value)).first;
}
auto* typed = dynamic_cast<LDFData<T>*>(data);
return itr;
}
if (typed == nullptr) {
return;
}
typed->SetValue(value);
template<typename T>
LwoNameValue::ValueType::iterator Entity::InsertNetworkVar(const std::u16string& name, T value) {
return InsertLnvData<T>(m_NetworkSettings, name, value);
}
template<typename T>
void Entity::SetNetworkVar(const std::u16string& name, T value, const SystemAddress& sysAddr) {
LDFData<T>* newData = nullptr;
const auto itr = InsertNetworkVar<T>(name, value);
for (auto* data : m_NetworkSettings) {
if (data->GetKey() != name)
continue;
SendNetworkVar(itr->second->GetString(), sysAddr);
}
newData = dynamic_cast<LDFData<T>*>(data);
if (newData != nullptr) {
newData->SetValue(value);
} else { // If we're changing types
m_NetworkSettings.erase(
std::remove(m_NetworkSettings.begin(), m_NetworkSettings.end(), data), m_NetworkSettings.end()
);
delete data;
}
break;
}
if (newData == nullptr) {
newData = new LDFData<T>(name, value);
}
m_NetworkSettings.push_back(newData);
SendNetworkVar(newData->GetString(true), sysAddr);
template<typename T>
void Entity::SetNetworkVar(const std::string& name, T value, const SystemAddress& sysAddr) {
SetNetworkVar(GeneralUtils::UTF8ToUTF16(name), value, sysAddr);
}
template<typename T>
@@ -514,28 +537,11 @@ void Entity::SetNetworkVar(const std::u16string& name, std::vector<T> values, co
auto index = 1;
for (const auto& value : values) {
LDFData<T>* newData = nullptr;
const auto& indexedName = name + u"." + GeneralUtils::to_u16string(index);
for (auto* data : m_NetworkSettings) {
if (data->GetKey() != indexedName)
continue;
newData = dynamic_cast<LDFData<T>*>(data);
newData->SetValue(value);
break;
}
if (newData == nullptr) {
newData = new LDFData<T>(indexedName, value);
}
m_NetworkSettings.push_back(newData);
if (index == values.size()) {
updates << newData->GetString(true);
} else {
updates << newData->GetString(true) << "\n";
const auto itr = InsertNetworkVar<T>(indexedName, value);
updates << itr->second->GetString();
if (index != values.size()) {
updates << "\n";
}
index++;
@@ -546,18 +552,15 @@ void Entity::SetNetworkVar(const std::u16string& name, std::vector<T> values, co
template<typename T>
T Entity::GetNetworkVar(const std::u16string& name) {
for (auto* data : m_NetworkSettings) {
if (data == nullptr || data->GetKey() != name)
continue;
T toReturn = LDFData<T>::Default;
auto* typed = dynamic_cast<LDFData<T>*>(data);
if (typed == nullptr)
continue;
return typed->GetValue();
const auto itr = m_NetworkSettings.values.find(name);
if (itr != m_NetworkSettings.values.cend()) {
auto* cast = dynamic_cast<LDFData<T>*>(itr->second.get());
if (cast) toReturn = cast->GetValue();
}
return LDFData<T>::Default;
return toReturn;
}
/**
@@ -600,5 +603,5 @@ auto Entity::GetComponents() const {
template<typename... T>
auto Entity::GetComponentsMut() const {
return std::tuple{GetComponent<T>()...};
return std::tuple{ GetComponent<T>()... };
}

View File

@@ -10,7 +10,6 @@
#include "SkillComponent.h"
#include "SwitchComponent.h"
#include "UserManager.h"
#include "Metrics.hpp"
#include "dZoneManager.h"
#include "MissionComponent.h"
#include "Game.h"
@@ -361,16 +360,24 @@ void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr
LOG("Attempted to construct null entity");
return;
}
// Don't construct GM invisible entities unless it's for the GM themselves
// GMs can see other GMs if they are the same or lower level
GameMessages::GetGMInvis getGMInvisMsg;
getGMInvisMsg.Send(entity->GetObjectID());
if (getGMInvisMsg.bGMInvis && sysAddr != entity->GetSystemAddress()) {
auto* toUser = UserManager::Instance()->GetUser(sysAddr);
if (!toUser) return;
auto* constructedUser = UserManager::Instance()->GetUser(entity->GetSystemAddress());
if (!constructedUser) return;
if (toUser->GetMaxGMLevel() < constructedUser->GetMaxGMLevel()) return;
}
if (entity->GetNetworkId() == 0) {
uint16_t networkId;
if (!m_LostNetworkIds.empty()) {
networkId = m_LostNetworkIds.top();
m_LostNetworkIds.pop();
} else {
networkId = ++m_NetworkIdCounter;
}
} else networkId = ++m_NetworkIdCounter;
entity->SetNetworkId(networkId);
}
@@ -379,10 +386,8 @@ void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr
if (std::find(m_EntitiesToGhost.begin(), m_EntitiesToGhost.end(), entity) == m_EntitiesToGhost.end()) {
m_EntitiesToGhost.push_back(entity);
}
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) {
CheckGhosting(entity);
return;
}
}
@@ -413,14 +418,9 @@ void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr
Game::server->Send(stream, sysAddr, false);
}
if (entity->IsPlayer()) {
if (entity->GetGMLevel() > eGameMasterLevel::CIVILIAN) {
GameMessages::SendToggleGMInvis(entity->GetObjectID(), true, sysAddr);
}
}
}
void EntityManager::ConstructAllEntities(const SystemAddress& sysAddr) {
void EntityManager::ConstructAllEntities(const SystemAddress& sysAddr) {
//ZoneControl is special:
ConstructEntity(m_ZoneControlEntity, sysAddr);
@@ -488,11 +488,7 @@ void EntityManager::QueueGhostUpdate(LWOOBJID playerID) {
void EntityManager::UpdateGhosting() {
for (const auto playerID : m_PlayersToUpdateGhosting) {
auto* player = PlayerManager::GetPlayer(playerID);
if (player == nullptr) {
continue;
}
if (!player) continue;
UpdateGhosting(player);
}
@@ -519,6 +515,7 @@ void EntityManager::UpdateGhosting(Entity* player) {
const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint);
auto ghostingDistanceMax = m_GhostDistanceMaxSquared;
auto ghostingDistanceMin = m_GhostDistanceMinSqaured;
@@ -555,35 +552,25 @@ void EntityManager::UpdateGhosting(Entity* player) {
}
void EntityManager::CheckGhosting(Entity* entity) {
if (entity == nullptr) {
return;
}
if (!entity) return;
const auto& referencePoint = entity->GetPosition();
for (auto* player : PlayerManager::GetAllPlayers()) {
auto* ghostComponent = player->GetComponent<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

@@ -18,7 +18,8 @@
#include "DluAssert.h"
#include "CDActivitiesTable.h"
#include "Metrics.hpp"
#include <chrono>
namespace LeaderboardManager {
std::map<GameID, Leaderboard::Type> leaderboardCache;
@@ -38,10 +39,10 @@ Leaderboard::~Leaderboard() {
}
void Leaderboard::Clear() {
for (auto& entry : entries) for (auto ldfData : entry) delete ldfData;
entries.clear();
}
inline void WriteLeaderboardRow(std::ostringstream& leaderboard, const uint32_t& index, LDFBaseData* data) {
inline void WriteLeaderboardRow(std::ostringstream& leaderboard, const uint32_t& index, const std::unique_ptr<LDFBaseData>& data) {
leaderboard << "\nResult[0].Row[" << index << "]." << data->GetString();
}
@@ -58,8 +59,8 @@ void Leaderboard::Serialize(RakNet::BitStream& bitStream) const {
int32_t rowNumber = 0;
for (auto& entry : entries) {
for (auto* data : entry) {
WriteLeaderboardRow(leaderboard, rowNumber, data);
for (const auto& data : entry.values | std::views::values) {
if (data) WriteLeaderboardRow(leaderboard, rowNumber, data);
}
rowNumber++;
}
@@ -84,57 +85,56 @@ void QueryToLdf(Leaderboard& leaderboard, const std::vector<ILeaderboard::Entry>
for (const auto& leaderboardEntry : leaderboardEntries) {
constexpr int32_t MAX_NUM_DATA_PER_ROW = 9;
auto& entry = leaderboard.PushBackEntry();
entry.reserve(MAX_NUM_DATA_PER_ROW);
entry.push_back(new LDFData<uint64_t>(u"CharacterID", leaderboardEntry.charId));
entry.push_back(new LDFData<uint64_t>(u"LastPlayed", leaderboardEntry.lastPlayedTimestamp));
entry.push_back(new LDFData<int32_t>(u"NumPlayed", leaderboardEntry.numTimesPlayed));
entry.push_back(new LDFData<std::u16string>(u"name", GeneralUtils::ASCIIToUTF16(leaderboardEntry.name)));
entry.push_back(new LDFData<uint64_t>(u"RowNumber", leaderboardEntry.ranking));
entry.Insert<uint64_t>(u"CharacterID", leaderboardEntry.charId);
entry.Insert<uint64_t>(u"LastPlayed", leaderboardEntry.lastPlayedTimestamp);
entry.Insert<int32_t>(u"NumPlayed", leaderboardEntry.numTimesPlayed);
entry.Insert<std::u16string>(u"name", GeneralUtils::ASCIIToUTF16(leaderboardEntry.name));
entry.Insert<uint64_t>(u"RowNumber", leaderboardEntry.ranking);
switch (leaderboard.GetLeaderboardType()) {
case ShootingGallery:
entry.push_back(new LDFData<int32_t>(u"Score", leaderboardEntry.primaryScore));
entry.Insert<int32_t>(u"Score", leaderboardEntry.primaryScore);
// Score:1
entry.push_back(new LDFData<int32_t>(u"Streak", leaderboardEntry.secondaryScore));
entry.Insert<int32_t>(u"Streak", leaderboardEntry.secondaryScore);
// Streak:1
entry.push_back(new LDFData<float>(u"HitPercentage", leaderboardEntry.tertiaryScore));
entry.Insert<float>(u"HitPercentage", leaderboardEntry.tertiaryScore);
// HitPercentage:3 between 0 and 1
break;
case Racing:
entry.push_back(new LDFData<float>(u"BestTime", leaderboardEntry.primaryScore));
entry.Insert<float>(u"BestTime", leaderboardEntry.primaryScore);
// BestLapTime:3
entry.push_back(new LDFData<float>(u"BestLapTime", leaderboardEntry.secondaryScore));
entry.Insert<float>(u"BestLapTime", leaderboardEntry.secondaryScore);
// BestTime:3
entry.push_back(new LDFData<int32_t>(u"License", 1));
entry.Insert<int32_t>(u"License", 1);
// License:1 - 1 if player has completed mission 637 and 0 otherwise
entry.push_back(new LDFData<int32_t>(u"NumWins", leaderboardEntry.numWins));
entry.Insert<int32_t>(u"NumWins", leaderboardEntry.numWins);
// NumWins:1
break;
case UnusedLeaderboard4:
entry.push_back(new LDFData<int32_t>(u"Points", leaderboardEntry.primaryScore));
entry.Insert<int32_t>(u"Points", leaderboardEntry.primaryScore);
// Points:1
break;
case MonumentRace:
entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.primaryScore));
entry.Insert<int32_t>(u"Time", leaderboardEntry.primaryScore);
// Time:1(?)
break;
case FootRace:
entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.primaryScore));
entry.Insert<int32_t>(u"Time", leaderboardEntry.primaryScore);
// Time:1
break;
case Survival:
entry.push_back(new LDFData<int32_t>(u"Points", leaderboardEntry.primaryScore));
entry.Insert<int32_t>(u"Points", leaderboardEntry.primaryScore);
// Points:1
entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.secondaryScore));
entry.Insert<int32_t>(u"Time", leaderboardEntry.secondaryScore);
// Time:1
break;
case SurvivalNS:
entry.push_back(new LDFData<int32_t>(u"Wave", leaderboardEntry.primaryScore));
entry.Insert<int32_t>(u"Wave", leaderboardEntry.primaryScore);
// Wave:1
entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.secondaryScore));
entry.Insert<int32_t>(u"Time", leaderboardEntry.secondaryScore);
// Time:1
break;
case Donations:
entry.push_back(new LDFData<int32_t>(u"Score", leaderboardEntry.primaryScore));
entry.Insert<int32_t>(u"Score", leaderboardEntry.primaryScore);
// Score:1
break;
case None:
@@ -289,6 +289,10 @@ void LeaderboardManager::SaveScore(const LWOOBJID& playerID, const GameID activi
ILeaderboard::Score oldScoreFlipped{oldScore->secondaryScore, oldScore->primaryScore, oldScore->tertiaryScore};
ILeaderboard::Score newScoreFlipped{newScore.secondaryScore, newScore.primaryScore, newScore.tertiaryScore};
newHighScore = newScoreFlipped > oldScoreFlipped;
} else if (leaderboardType == Leaderboard::Type::Donations) {
// Donations just need to go up if updated
newHighScore = true;
newScore.primaryScore += oldScore->primaryScore;
}
if (newHighScore) {

View File

@@ -70,8 +70,7 @@ public:
private:
using LeaderboardEntry = std::vector<LDFBaseData*>;
using LeaderboardEntries = std::vector<LeaderboardEntry>;
using LeaderboardEntries = std::vector<LwoNameValue>;
LeaderboardEntries entries;
LWOOBJID relatedPlayer;
@@ -81,7 +80,7 @@ private:
bool weekly;
uint32_t numResults;
public:
LeaderboardEntry& PushBackEntry() {
LwoNameValue& PushBackEntry() {
return entries.emplace_back();
}

View File

@@ -9,6 +9,16 @@ Team::Team() {
lootOption = Game::config->GetValue("default_team_loot") == "0" ? 0 : 1;
}
LWOOBJID Team::GetNextLootOwner() {
lootRound++;
if (lootRound >= members.size()) {
lootRound = 0;
}
return members[lootRound];
}
TeamManager::TeamManager() {
}

View File

@@ -4,6 +4,8 @@
struct Team {
Team();
LWOOBJID GetNextLootOwner();
LWOOBJID teamID = LWOOBJID_EMPTY;
char lootOption = 0;
std::vector<LWOOBJID> members{};

View File

@@ -10,6 +10,11 @@
#include "CharacterComponent.h"
#include "MissionComponent.h"
#include "eMissionTaskType.h"
#include <ranges>
namespace {
std::unique_ptr<Trade> g_EmptyTrade;
}
TradingManager* TradingManager::m_Address = nullptr;
@@ -233,55 +238,38 @@ void Trade::SendUpdateToOther(LWOOBJID participant) {
GameMessages::SendServerTradeUpdate(other->GetObjectID(), coins, items, other->GetSystemAddress());
}
TradingManager::TradingManager() {
}
TradingManager::~TradingManager() {
for (const auto& pair : trades) {
delete pair.second;
}
trades.clear();
}
Trade* TradingManager::GetTrade(LWOOBJID tradeId) const {
const std::unique_ptr<Trade>& TradingManager::GetTrade(LWOOBJID tradeId) const {
const auto& pair = trades.find(tradeId);
if (pair == trades.end()) return nullptr;
if (pair == trades.end()) return g_EmptyTrade;
return pair->second;
}
Trade* TradingManager::GetPlayerTrade(LWOOBJID playerId) const {
for (const auto& pair : trades) {
if (pair.second->IsParticipant(playerId)) {
return pair.second;
const std::unique_ptr<Trade>& TradingManager::GetPlayerTrade(LWOOBJID playerId) const {
for (const auto& trade : trades | std::views::values) {
if (trade->IsParticipant(playerId)) {
return trade;
}
}
return nullptr;
return g_EmptyTrade;
}
void TradingManager::CancelTrade(const LWOOBJID canceller, LWOOBJID tradeId, const bool sendCancelMessage) {
auto* trade = GetTrade(tradeId);
const auto& trade = GetTrade(tradeId);
if (trade == nullptr) return;
if (sendCancelMessage) trade->Cancel(canceller);
delete trade;
trades.erase(tradeId);
}
Trade* TradingManager::NewTrade(LWOOBJID participantA, LWOOBJID participantB) {
void TradingManager::NewTrade(LWOOBJID participantA, LWOOBJID participantB) {
const LWOOBJID tradeId = ObjectIDManager::GenerateObjectID();
auto* trade = new Trade(tradeId, participantA, participantB);
trades[tradeId] = trade;
trades.insert_or_assign(tradeId, std::make_unique<Trade>(tradeId, participantA, participantB));
LOG("Created new trade between (%llu) <-> (%llu)", participantA, participantB);
return trade;
}

View File

@@ -2,15 +2,16 @@
#include "Entity.h"
struct TradeItem
{
#include <map>
#include <memory>
struct TradeItem {
LWOOBJID itemId;
LOT itemLot;
uint32_t itemCount;
};
class Trade
{
class Trade {
public:
explicit Trade(LWOOBJID tradeId, LWOOBJID participantA, LWOOBJID participantB);
~Trade();
@@ -50,8 +51,7 @@ private:
};
class TradingManager
{
class TradingManager {
public:
static TradingManager* Instance() {
if (!m_Address) {
@@ -61,16 +61,13 @@ public:
return m_Address;
}
explicit TradingManager();
~TradingManager();
Trade* GetTrade(LWOOBJID tradeId) const;
Trade* GetPlayerTrade(LWOOBJID playerId) const;
const std::unique_ptr<Trade>& GetTrade(LWOOBJID tradeId) const;
const std::unique_ptr<Trade>& GetPlayerTrade(LWOOBJID playerId) const;
void CancelTrade(const LWOOBJID canceller, LWOOBJID tradeId, const bool sendCancelMessage = true);
Trade* NewTrade(LWOOBJID participantA, LWOOBJID participantB);
void NewTrade(LWOOBJID participantA, LWOOBJID participantB);
private:
static TradingManager* m_Address; //For singleton method
std::unordered_map<LWOOBJID, Trade*> trades;
std::unordered_map<LWOOBJID, std::unique_ptr<Trade>> trades;
};

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

@@ -5,20 +5,40 @@
void NpcCombatSkillBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bit_stream, BehaviorBranchContext branch) {
context->skillTime = this->m_npcSkillTime;
const auto* const targetEntity = Game::entityManager->GetEntity(branch.target);
const auto* const sourceEntity = Game::entityManager->GetEntity(context->caster);
for (auto* behavior : this->m_behaviors) {
behavior->Calculate(context, bit_stream, branch);
bool cast = true;
// Check that the target is within the cast range
if (targetEntity && sourceEntity && this->m_maxRange != 0.0f) {
const auto targetPos = targetEntity->GetPosition();
const auto sourcePos = sourceEntity->GetPosition();
const auto distance = NiPoint3::DistanceSquared(targetPos, sourcePos);
cast = distance >= this->m_minRange && distance <= this->m_maxRange;
}
if (cast) {
for (auto* behavior : this->m_behaviors) {
behavior->Calculate(context, bit_stream, branch);
}
} else {
// We failed to find a valid target, do not continue the behavior
context->foundTarget = false;
}
}
void NpcCombatSkillBehavior::Load() {
this->m_npcSkillTime = GetFloat("npc skill time");
this->m_minRange = GetFloat("min range") * 0.9f; // Make the min and max 10% smaller to account for server/client position disagreements
this->m_minRange *= this->m_minRange;
this->m_maxRange = GetFloat("max range") * 0.9f; // Make the min and max 10% smaller to account for server/client position disagreements
this->m_maxRange *= this->m_maxRange;
const auto parameters = GetParameterNames();
for (const auto& parameter : parameters) {
if (parameter.first.rfind("behavior", 0) == 0) {
auto* action = GetAction(parameter.second);
for (const auto& [parameter, value] : parameters) {
if (parameter.rfind("behavior", 0) == 0) {
auto* action = GetAction(value);
this->m_behaviors.push_back(action);
}

View File

@@ -8,6 +8,9 @@ public:
float m_npcSkillTime;
float m_maxRange{};
float m_minRange{};
/*
* Inherited
*/

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

@@ -114,15 +114,13 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS
context->FilterTargets(validTargets, this->m_ignoreFactionList, this->m_includeFactionList, this->m_targetSelf, this->m_targetEnemy, this->m_targetFriend, this->m_targetTeam);
for (auto validTarget : validTargets) {
if (targets.size() >= this->m_maxTargets) break;
if (std::find(targets.begin(), targets.end(), validTarget) != targets.end()) continue;
if (validTarget->GetIsDead()) continue;
const auto targetPos = validTarget->GetPosition();
// make sure we aren't too high or low in comparison to the targer
const auto heightDifference = std::abs(reference.y - targetPos.y);
if (targetPos.y > reference.y && heightDifference > this->m_upperBound || targetPos.y < reference.y && heightDifference > this->m_lowerBound)
// make sure we aren't too high or low in comparison to the target
if (targetPos.y > (reference.y + m_upperBound) || targetPos.y < (reference.y + m_lowerBound))
continue;
const auto forward = QuatUtils::Forward(self->GetRotation());
@@ -147,13 +145,28 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS
}
}
std::sort(targets.begin(), targets.end(), [reference](Entity* a, Entity* b) {
std::sort(targets.begin(), targets.end(), [this, reference, combatAi](Entity* a, Entity* b) {
const auto aDistance = Vector3::DistanceSquared(reference, a->GetPosition());
const auto bDistance = Vector3::DistanceSquared(reference, b->GetPosition());
return aDistance > bDistance;
return aDistance < bDistance;
});
if (m_useAttackPriority) {
// this should be using the attack priority column on the destroyable component
// We want targets with no threat level to remain the same order as above
// std::stable_sort(targets.begin(), targets.end(), [combatAi](Entity* a, Entity* b) {
// const auto aThreat = combatAi->GetThreat(a->GetObjectID());
// const auto bThreat = combatAi->GetThreat(b->GetObjectID());
// If enabled for this behavior, prioritize threat over distance
// return aThreat > bThreat;
// });
}
// After we've sorted and found our closest targets, size the vector down in case there are too many
if (m_maxTargets > 0 && targets.size() > m_maxTargets) targets.resize(m_maxTargets);
const auto hit = !targets.empty();
bitStream.Write(hit);
@@ -194,9 +207,13 @@ void TacArcBehavior::Load() {
GetFloat("offset_y", 0.0f),
GetFloat("offset_z", 0.0f)
);
// https://explorer.lu/skills/behaviors/6212/6203 HACK: i cant figure out why the dragon fire wall doesnt work with the offset, probably has to be fixed with the near/far height parameters
if (m_behaviorId == 6203) {
this->m_offset = NiPoint3Constant::ZERO;
}
this->m_method = GetInt("method", 1);
this->m_upperBound = std::abs(GetFloat("upper_bound", 4.4f));
this->m_lowerBound = std::abs(GetFloat("lower_bound", 0.4f));
this->m_upperBound = GetFloat("upper_bound", 4.4f);
this->m_lowerBound = GetFloat("lower_bound", 0.4f) - 5.0f; // Makes it so players and objects can still be targetted when slightly below the caster. FIXME: use bounding spheres at some point
this->m_usePickedTarget = GetBoolean("use_picked_target", false);
this->m_useTargetPostion = GetBoolean("use_target_position", false);
this->m_checkEnv = GetBoolean("check_env", false);

View File

@@ -25,7 +25,7 @@ void VerifyBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS
const auto distance = Vector3::DistanceSquared(self->GetPosition(), entity->GetPosition());
if (distance > this->m_range * this->m_range) {
if (distance > this->m_range) {
success = false;
}
} else if (this->m_blockCheck) {
@@ -57,4 +57,5 @@ void VerifyBehavior::Load() {
this->m_action = GetAction("action");
this->m_range = GetFloat("range");
this->m_range = this->m_range * this->m_range * 0.9f; // Range checks are slightly smaller than the actual range to account for client/server discrepancies
}

View File

@@ -22,6 +22,7 @@
#include "eMatchUpdate.h"
#include "ServiceType.h"
#include "MessageType/Chat.h"
#include "ObjectIDManager.h"
#include "CDCurrencyTableTable.h"
#include "CDActivityRewardsTable.h"
@@ -29,10 +30,14 @@
#include "LeaderboardManager.h"
#include "CharacterComponent.h"
#include "Amf3.h"
#include <ranges>
namespace {
const ActivityInstance g_EmptyInstance{ nullptr, CDActivities{} };
}
ActivityComponent::ActivityComponent(Entity* parent, int32_t componentID) : Component(parent, componentID) {
using namespace GameMessages;
RegisterMsg<GetObjectReportInfo>(this, &ActivityComponent::OnGetObjectReportInfo);
RegisterMsg(&ActivityComponent::OnGetObjectReportInfo);
/*
* This is precisely what the client does functionally
* Use the component id as the default activity id and load its data from the database
@@ -45,33 +50,6 @@ ActivityComponent::ActivityComponent(Entity* parent, int32_t componentID) : Comp
m_ActivityID = parent->GetVar<int32_t>(u"activityID");
LoadActivityData(m_ActivityID);
}
auto* destroyableComponent = m_Parent->GetComponent<DestroyableComponent>();
if (destroyableComponent) {
// First lookup the loot matrix id for this component id.
CDActivityRewardsTable* activityRewardsTable = CDClientManager::GetTable<CDActivityRewardsTable>();
std::vector<CDActivityRewards> activityRewards = activityRewardsTable->Query([=](CDActivityRewards entry) {return (entry.LootMatrixIndex == destroyableComponent->GetLootMatrixID()); });
uint32_t startingLMI = 0;
// If we have one, set the starting loot matrix id to that.
if (activityRewards.size() > 0) {
startingLMI = activityRewards[0].LootMatrixIndex;
}
if (startingLMI > 0) {
// We may have more than 1 loot matrix index to use depending ont the size of the team that is looting the activity.
// So this logic will get the rest of the loot matrix indices for this activity.
std::vector<CDActivityRewards> objectTemplateActivities = activityRewardsTable->Query([=](CDActivityRewards entry) {return (activityRewards[0].objectTemplate == entry.objectTemplate); });
for (const auto& item : objectTemplateActivities) {
if (item.activityRating > 0 && item.activityRating < 5) {
m_ActivityLootMatrices.insert({ item.activityRating, item.LootMatrixIndex });
}
}
}
}
}
void ActivityComponent::LoadActivityData(const int32_t activityId) {
CDActivitiesTable* activitiesTable = CDClientManager::GetTable<CDActivitiesTable>();
@@ -99,9 +77,9 @@ void ActivityComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsIniti
if (m_DirtyActivityInfo) {
outBitStream.Write<uint32_t>(m_ActivityPlayers.size());
if (!m_ActivityPlayers.empty()) {
for (const auto& activityPlayer : m_ActivityPlayers) {
outBitStream.Write<LWOOBJID>(activityPlayer->playerID);
for (const auto& activityValue : activityPlayer->values) {
for (const auto& [playerID, values] : m_ActivityPlayers) {
outBitStream.Write<LWOOBJID>(playerID);
for (const auto& activityValue : values) {
outBitStream.Write<float_t>(activityValue);
}
}
@@ -139,78 +117,81 @@ void ActivityComponent::PlayerJoin(Entity* player) {
if (HasLobby()) {
PlayerJoinLobby(player);
} else if (!IsPlayedBy(player)) {
auto* instance = NewInstance();
instance->AddParticipant(player);
NewInstance().AddParticipant(player);
}
}
void ActivityComponent::PlayerJoinLobby(Entity* player) {
if (!m_Parent->HasComponent(eReplicaComponentType::QUICK_BUILD))
GameMessages::SendMatchResponse(player, player->GetSystemAddress(), 0); // tell the client they joined a lobby
LobbyPlayer* newLobbyPlayer = new LobbyPlayer();
newLobbyPlayer->entityID = player->GetObjectID();
Lobby* playerLobby = nullptr;
LobbyPlayer newLobbyPlayer{};
newLobbyPlayer.entityID = player->GetObjectID();
LWOOBJID playerLobbyID = LWOOBJID_EMPTY;
auto* character = player->GetCharacter();
if (character != nullptr)
character->SetLastNonInstanceZoneID(Game::zoneManager->GetZone()->GetWorldID());
for (Lobby* lobby : m_Queue) {
if (lobby->players.size() < m_ActivityInfo.maxTeamSize || m_ActivityInfo.maxTeamSize == 1 && lobby->players.size() < m_ActivityInfo.maxTeams) {
for (auto& [lobbyID, lobby] : m_Queue) {
if (lobby.players.size() < m_ActivityInfo.maxTeamSize || m_ActivityInfo.maxTeamSize == 1 && lobby.players.size() < m_ActivityInfo.maxTeams) {
// If an empty slot in an existing lobby is found
lobby->players.push_back(newLobbyPlayer);
playerLobby = lobby;
lobby.players.push_back(newLobbyPlayer);
playerLobbyID = lobbyID;
// Update the joining player on players already in the lobby, and update players already in the lobby on the joining player
std::string matchUpdateJoined = "player=9:" + std::to_string(player->GetObjectID()) + "\nplayerName=0:" + player->GetCharacter()->GetName();
for (LobbyPlayer* joinedPlayer : lobby->players) {
auto* entity = joinedPlayer->GetEntity();
LDFData<LWOOBJID> playerLDF("player", player->GetObjectID());
LDFData<std::string> playerName("playerName", player->GetCharacter()->GetName());
std::string matchUpdateJoined = playerLDF.GetString() + "\n" + playerName.GetString();
for (const auto& joinedPlayer : lobby.players) {
auto* const entity = joinedPlayer.GetEntity();
if (entity == nullptr) {
continue;
}
std::string matchUpdate = "player=9:" + std::to_string(entity->GetObjectID()) + "\nplayerName=0:" + entity->GetCharacter()->GetName();
LDFData<LWOOBJID> entityLDF("player", entity->GetObjectID());
LDFData<std::string> entityName("playerName", entity->GetCharacter()->GetName());
std::string matchUpdate = entityLDF.GetString() + "\n" + entityName.GetString();
GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchUpdate, eMatchUpdate::PLAYER_ADDED);
PlayerReady(entity, joinedPlayer->ready);
PlayerReady(entity, joinedPlayer.ready);
GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateJoined, eMatchUpdate::PLAYER_ADDED);
}
break;
}
}
if (!playerLobby) {
if (playerLobbyID == LWOOBJID_EMPTY) {
// If all lobbies are full
playerLobby = new Lobby();
playerLobby->players.push_back(newLobbyPlayer);
playerLobby->timer = m_ActivityInfo.waitTime / 1000;
m_Queue.push_back(playerLobby);
playerLobbyID = ObjectIDManager::GenerateObjectID();
auto& newLobby = m_Queue[playerLobbyID];
newLobby.players.push_back(newLobbyPlayer);
newLobby.timer = m_ActivityInfo.waitTime / 1000;
}
const auto& lobby = m_Queue[playerLobbyID];
if (m_ActivityInfo.maxTeamSize != 1 && playerLobby->players.size() >= m_ActivityInfo.minTeamSize || m_ActivityInfo.maxTeamSize == 1 && playerLobby->players.size() >= m_ActivityInfo.minTeams) {
if (m_ActivityInfo.maxTeamSize != 1 && lobby.players.size() >= m_ActivityInfo.minTeamSize || m_ActivityInfo.maxTeamSize == 1 && lobby.players.size() >= m_ActivityInfo.minTeams) {
// Update the joining player on the match timer
std::string matchTimerUpdate = "time=3:" + std::to_string(playerLobby->timer);
GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_READY);
LDFData<float> matchTimer("time", lobby.timer);
GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchTimer.GetString(), eMatchUpdate::PHASE_WAIT_READY);
}
}
void ActivityComponent::PlayerLeave(LWOOBJID playerID) {
// Removes the player from a lobby and notifies the others, not applicable for non-lobby instances
for (Lobby* lobby : m_Queue) {
for (int i = 0; i < lobby->players.size(); ++i) {
if (lobby->players[i]->entityID == playerID) {
std::string matchUpdateLeft = "player=9:" + std::to_string(playerID);
for (LobbyPlayer* lobbyPlayer : lobby->players) {
auto* entity = lobbyPlayer->GetEntity();
for (auto& lobby : m_Queue | std::views::values) {
for (int i = 0; i < lobby.players.size(); i++) {
const auto& player = lobby.players[i];
if (player.entityID == playerID) {
LDFData<LWOOBJID> matchUpdateLeft("player", playerID);
for (const auto& lobbyPlayer : lobby.players) {
auto* const entity = lobbyPlayer.GetEntity();
if (entity == nullptr)
continue;
GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateLeft, eMatchUpdate::PLAYER_REMOVED);
GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateLeft.GetString(), eMatchUpdate::PLAYER_REMOVED);
}
delete lobby->players[i];
lobby->players[i] = nullptr;
lobby->players.erase(lobby->players.begin() + i);
lobby.players.erase(lobby.players.begin() + i);
return;
}
@@ -219,85 +200,79 @@ void ActivityComponent::PlayerLeave(LWOOBJID playerID) {
}
void ActivityComponent::Update(float deltaTime) {
std::vector<Lobby*> lobbiesToRemove{};
std::vector<LWOOBJID> lobbiesToRemove{};
// Ticks all the lobbies, not applicable for non-instance activities
for (Lobby* lobby : m_Queue) {
for (LobbyPlayer* player : lobby->players) {
auto* entity = player->GetEntity();
for (auto& [lobbyID, lobby] : m_Queue) {
for (const auto& player : lobby.players) {
const auto* const entity = player.GetEntity();
if (entity == nullptr) {
PlayerLeave(player->entityID);
PlayerLeave(player.entityID);
return;
}
}
if (lobby->players.empty()) {
lobbiesToRemove.push_back(lobby);
if (lobby.players.empty()) {
lobbiesToRemove.push_back(lobbyID);
continue;
}
// Update the match time for all players
if (m_ActivityInfo.maxTeamSize != 1 && lobby->players.size() >= m_ActivityInfo.minTeamSize
|| m_ActivityInfo.maxTeamSize == 1 && lobby->players.size() >= m_ActivityInfo.minTeams) {
if (lobby->timer == m_ActivityInfo.waitTime / 1000) {
for (LobbyPlayer* joinedPlayer : lobby->players) {
auto* entity = joinedPlayer->GetEntity();
if (m_ActivityInfo.maxTeamSize != 1 && lobby.players.size() >= m_ActivityInfo.minTeamSize
|| m_ActivityInfo.maxTeamSize == 1 && lobby.players.size() >= m_ActivityInfo.minTeams) {
if (lobby.timer == m_ActivityInfo.waitTime / 1000) {
for (const auto& joinedPlayer : lobby.players) {
auto* const entity = joinedPlayer.GetEntity();
if (entity == nullptr)
continue;
std::string matchTimerUpdate = "time=3:" + std::to_string(lobby->timer);
GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_READY);
LDFData<float> matchTimerUpdate("time", lobby.timer);
GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate.GetString(), eMatchUpdate::PHASE_WAIT_READY);
}
}
lobby->timer -= deltaTime;
lobby.timer -= deltaTime;
}
bool lobbyReady = true;
for (LobbyPlayer* player : lobby->players) {
if (player->ready) continue;
for (const auto& player : lobby.players) {
if (player.ready) continue;
lobbyReady = false;
}
// If everyone's ready, jump the timer
if (lobbyReady && lobby->timer > m_ActivityInfo.startDelay / 1000) {
lobby->timer = m_ActivityInfo.startDelay / 1000;
if (lobbyReady && lobby.timer > m_ActivityInfo.startDelay / 1000) {
lobby.timer = m_ActivityInfo.startDelay / 1000;
// Update players in lobby on switch to start delay
std::string matchTimerUpdate = "time=3:" + std::to_string(lobby->timer);
for (LobbyPlayer* player : lobby->players) {
auto* entity = player->GetEntity();
LDFData<float> matchTimerUpdate("time", lobby.timer);
for (const auto& player : lobby.players) {
auto* const entity = player.GetEntity();
if (entity == nullptr)
continue;
GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_START);
GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate.GetString(), eMatchUpdate::PHASE_WAIT_START);
}
}
// The timer has elapsed, start the instance
if (lobby->timer <= 0.0f) {
if (lobby.timer <= 0.0f) {
LOG("Setting up instance.");
ActivityInstance* instance = NewInstance();
LoadPlayersIntoInstance(instance, lobby->players);
instance->StartZone();
lobbiesToRemove.push_back(lobby);
auto& instance = NewInstance();
LoadPlayersIntoInstance(instance, lobby.players);
instance.StartZone();
lobbiesToRemove.push_back(lobbyID);
}
}
while (!lobbiesToRemove.empty()) {
RemoveLobby(lobbiesToRemove.front());
lobbiesToRemove.erase(lobbiesToRemove.begin());
for (const auto id : lobbiesToRemove) {
RemoveLobby(id);
}
}
void ActivityComponent::RemoveLobby(Lobby* lobby) {
for (int i = 0; i < m_Queue.size(); ++i) {
if (m_Queue[i] == lobby) {
m_Queue.erase(m_Queue.begin() + i);
return;
}
}
void ActivityComponent::RemoveLobby(const LWOOBJID lobbyID) {
if (m_Queue.contains(lobbyID)) m_Queue.erase(lobbyID);
}
bool ActivityComponent::HasLobby() const {
@@ -306,9 +281,9 @@ bool ActivityComponent::HasLobby() const {
}
bool ActivityComponent::PlayerIsInQueue(Entity* player) {
for (Lobby* lobby : m_Queue) {
for (LobbyPlayer* lobbyPlayer : lobby->players) {
if (player->GetObjectID() == lobbyPlayer->entityID) return true;
for (const auto& lobby : m_Queue | std::views::values) {
for (const auto& lobbyPlayer : lobby.players) {
if (player->GetObjectID() == lobbyPlayer.entityID) return true;
}
}
@@ -316,8 +291,8 @@ bool ActivityComponent::PlayerIsInQueue(Entity* player) {
}
bool ActivityComponent::IsPlayedBy(Entity* player) const {
for (const auto* instance : this->m_Instances) {
for (const auto* instancePlayer : instance->GetParticipants()) {
for (const auto& instance : m_Instances) {
for (const auto* instancePlayer : instance.GetParticipants()) {
if (instancePlayer != nullptr && instancePlayer->GetObjectID() == player->GetObjectID())
return true;
}
@@ -327,8 +302,8 @@ bool ActivityComponent::IsPlayedBy(Entity* player) const {
}
bool ActivityComponent::IsPlayedBy(LWOOBJID playerID) const {
for (const auto* instance : this->m_Instances) {
for (const auto* instancePlayer : instance->GetParticipants()) {
for (const auto& instance : m_Instances) {
for (const auto* instancePlayer : instance.GetParticipants()) {
if (instancePlayer != nullptr && instancePlayer->GetObjectID() == playerID)
return true;
}
@@ -352,142 +327,100 @@ 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) {
for (Lobby* lobby : m_Queue) {
for (LobbyPlayer* lobbyPlayer : lobby->players) {
if (lobbyPlayer->entityID == player->GetObjectID()) {
for (auto& lobby : m_Queue | std::views::values) {
for (auto& lobbyPlayer : lobby.players) {
if (lobbyPlayer.entityID == player->GetObjectID()) {
lobbyPlayer->ready = bReady;
lobbyPlayer.ready = bReady;
// Update players in lobby on player being ready
std::string matchReadyUpdate = "player=9:" + std::to_string(player->GetObjectID());
LDFData<LWOOBJID> matchReadyUpdate("player", player->GetObjectID());
eMatchUpdate readyStatus = eMatchUpdate::PLAYER_READY;
if (!bReady) readyStatus = eMatchUpdate::PLAYER_NOT_READY;
for (LobbyPlayer* otherPlayer : lobby->players) {
auto* entity = otherPlayer->GetEntity();
for (const auto& otherPlayer : lobby.players) {
auto* const entity = otherPlayer.GetEntity();
if (entity == nullptr)
continue;
GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchReadyUpdate, readyStatus);
GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchReadyUpdate.GetString(), readyStatus);
}
}
}
}
}
ActivityInstance* ActivityComponent::NewInstance() {
auto* instance = new ActivityInstance(m_Parent, m_ActivityInfo);
m_Instances.push_back(instance);
return instance;
ActivityInstance& ActivityComponent::NewInstance() {
m_Instances.push_back(ActivityInstance(m_Parent, m_ActivityInfo));
return m_Instances.back();
}
void ActivityComponent::LoadPlayersIntoInstance(ActivityInstance* instance, const std::vector<LobbyPlayer*>& lobby) const {
for (LobbyPlayer* player : lobby) {
auto* entity = player->GetEntity();
void ActivityComponent::LoadPlayersIntoInstance(ActivityInstance& instance, const std::vector<LobbyPlayer>& lobby) const {
for (const auto& player : lobby) {
auto* const entity = player.GetEntity();
if (entity == nullptr || !CheckCost(entity)) {
continue;
}
instance->AddParticipant(entity);
instance.AddParticipant(entity);
}
}
const std::vector<ActivityInstance*>& ActivityComponent::GetInstances() const {
return m_Instances;
}
ActivityInstance* ActivityComponent::GetInstance(const LWOOBJID playerID) {
for (const auto* instance : GetInstances()) {
for (const auto* participant : instance->GetParticipants()) {
const ActivityInstance& ActivityComponent::GetInstance(const LWOOBJID playerID) const {
for (const auto& instance : m_Instances) {
for (const auto* participant : instance.GetParticipants()) {
if (participant->GetObjectID() == playerID)
return const_cast<ActivityInstance*>(instance);
return instance;
}
}
return nullptr;
return g_EmptyInstance;
}
void ActivityComponent::ClearInstances() {
for (ActivityInstance* instance : m_Instances) {
delete instance;
}
m_Instances.clear();
}
ActivityPlayer* ActivityComponent::GetActivityPlayerData(LWOOBJID playerID) {
for (auto* activityData : m_ActivityPlayers) {
if (activityData->playerID == playerID) {
return activityData;
}
}
return nullptr;
bool ActivityComponent::PlayerHasActivityData(LWOOBJID playerID) const {
return m_ActivityPlayers.contains(playerID);
}
void ActivityComponent::RemoveActivityPlayerData(LWOOBJID playerID) {
for (size_t i = 0; i < m_ActivityPlayers.size(); i++) {
if (m_ActivityPlayers[i]->playerID == playerID) {
delete m_ActivityPlayers[i];
m_ActivityPlayers[i] = nullptr;
m_ActivityPlayers.erase(m_ActivityPlayers.begin() + i);
m_DirtyActivityInfo = true;
Game::entityManager->SerializeEntity(m_Parent);
return;
}
}
}
ActivityPlayer* ActivityComponent::AddActivityPlayerData(LWOOBJID playerID) {
auto* data = GetActivityPlayerData(playerID);
if (data != nullptr)
return data;
m_ActivityPlayers.push_back(new ActivityPlayer{ playerID, {} });
m_ActivityPlayers.erase(playerID);
m_DirtyActivityInfo = true;
Game::entityManager->SerializeEntity(m_Parent);
return GetActivityPlayerData(playerID);
}
float_t ActivityComponent::GetActivityValue(LWOOBJID playerID, uint32_t index) {
auto value = -1.0f;
float_t ActivityComponent::GetActivityValue(LWOOBJID playerID, uint32_t index) const {
float value = -1.0f;
auto* data = GetActivityPlayerData(playerID);
if (data != nullptr) {
value = data->values[std::min(index, static_cast<uint32_t>(9))];
const auto& data = m_ActivityPlayers.find(playerID);
if (data != m_ActivityPlayers.cend()) {
value = data->second[std::min(index, static_cast<uint32_t>(9))];
}
LOG_DEBUG("Player %llu has score %f at index %i", playerID, value, index);
return value;
}
void ActivityComponent::SetActivityValue(LWOOBJID playerID, uint32_t index, float_t value) {
auto* data = AddActivityPlayerData(playerID);
if (data != nullptr) {
data->values[std::min(index, static_cast<uint32_t>(9))] = value;
}
auto& data = m_ActivityPlayers[playerID];
data[std::min(index, static_cast<uint32_t>(9))] = value;
LOG_DEBUG("%llu index %i has score of %f", playerID, index, value);
m_DirtyActivityInfo = true;
Game::entityManager->SerializeEntity(m_Parent);
}
void ActivityComponent::PlayerRemove(LWOOBJID playerID) {
for (auto* instance : GetInstances()) {
auto participants = instance->GetParticipants();
for (int i = 0; i < m_Instances.size(); i++) {
auto& instance = m_Instances[i];
auto participants = instance.GetParticipants();
for (const auto* participant : participants) {
if (participant != nullptr && participant->GetObjectID() == playerID) {
instance->RemoveParticipant(participant);
instance.RemoveParticipant(participant);
RemoveActivityPlayerData(playerID);
// If the instance is empty after the delete of the participant, delete the instance too
if (instance->GetParticipants().empty()) {
m_Instances.erase(std::find(m_Instances.begin(), m_Instances.end(), instance));
delete instance;
if (instance.GetParticipants().empty()) {
m_Instances.erase(m_Instances.begin() + i);
}
return;
}
@@ -618,22 +551,19 @@ 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()));
size_t i = 0;
for (const auto& activityInstance : m_Instances) {
if (!activityInstance) continue;
auto& instance = instances.PushDebug("Instance " + std::to_string(i++));
instance.PushDebug<AMFIntValue>("Score") = activityInstance->GetScore();
instance.PushDebug<AMFIntValue>("Next Zone Clone ID") = activityInstance->GetNextZoneCloneID();
instance.PushDebug<AMFIntValue>("Score") = activityInstance.GetScore();
instance.PushDebug<AMFIntValue>("Next Zone Clone ID") = activityInstance.GetNextZoneCloneID();
{
auto& activityInfo = instance.PushDebug("Activity Info");
const auto& instanceActInfo = activityInstance->GetActivityInfo();
const auto& instanceActInfo = activityInstance.GetActivityInfo();
activityInfo.PushDebug<AMFIntValue>("ActivityID") = instanceActInfo.ActivityID;
activityInfo.PushDebug<AMFIntValue>("locStatus") = instanceActInfo.locStatus;
activityInfo.PushDebug<AMFIntValue>("instanceMapID") = instanceActInfo.instanceMapID;
@@ -656,7 +586,7 @@ bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
}
auto& participants = instance.PushDebug("Participants");
for (const auto* participant : activityInstance->GetParticipants()) {
for (const auto* participant : activityInstance.GetParticipants()) {
if (!participant) continue;
auto* character = participant->GetCharacter();
if (!character) continue;
@@ -666,42 +596,36 @@ bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& queue = activityInfo.PushDebug("Queue");
i = 0;
for (const auto& lobbyQueue : m_Queue) {
for (const auto& lobbyQueue : m_Queue | std::views::values) {
auto& lobby = queue.PushDebug("Lobby " + std::to_string(i++));
lobby.PushDebug<AMFDoubleValue>("Timer") = lobbyQueue->timer;
lobby.PushDebug<AMFDoubleValue>("Timer") = lobbyQueue.timer;
auto& players = lobby.PushDebug("Players");
for (const auto* player : lobbyQueue->players) {
if (!player) continue;
auto* playerEntity = player->GetEntity();
for (const auto& player : lobbyQueue.players) {
const auto* const playerEntity = player.GetEntity();
if (!playerEntity) continue;
auto* character = playerEntity->GetCharacter();
if (!character) continue;
players.PushDebug<AMFStringValue>(std::to_string(playerEntity->GetObjectID()) + ": " + character->GetName()) = player->ready ? "Ready" : "Not Ready";
players.PushDebug<AMFStringValue>(std::to_string(playerEntity->GetObjectID()) + ": " + character->GetName()) = player.ready ? "Ready" : "Not Ready";
}
}
auto& activityPlayers = activityInfo.PushDebug("Activity Players");
for (const auto* activityPlayer : m_ActivityPlayers) {
if (!activityPlayer) continue;
auto* const activityPlayerEntity = Game::entityManager->GetEntity(activityPlayer->playerID);
for (const auto& [playerID, playerScores] : m_ActivityPlayers) {
auto* const activityPlayerEntity = Game::entityManager->GetEntity(playerID);
if (!activityPlayerEntity) continue;
auto* character = activityPlayerEntity->GetCharacter();
if (!character) continue;
auto& playerData = activityPlayers.PushDebug(std::to_string(activityPlayer->playerID) + " " + character->GetName());
auto& playerData = activityPlayers.PushDebug(std::to_string(playerID) + " " + character->GetName());
auto& scores = playerData.PushDebug("Scores");
for (size_t i = 0; i < 10; ++i) {
scores.PushDebug<AMFDoubleValue>(std::to_string(i)) = activityPlayer->values[i];
scores.PushDebug<AMFDoubleValue>(std::to_string(i)) = playerScores[i];
}
}
auto& lootMatrices = activityInfo.PushDebug("Loot Matrices");
for (const auto& [activityRating, lootMatrixID] : m_ActivityLootMatrices) {
lootMatrices.PushDebug<AMFIntValue>("Loot Matrix " + std::to_string(activityRating)) = lootMatrixID;
}
activityInfo.PushDebug<AMFIntValue>("ActivityID") = m_ActivityID;
return true;
}

View File

@@ -8,14 +8,15 @@
#include "eReplicaComponentType.h"
#include "CDActivitiesTable.h"
#include <array>
namespace GameMessages {
class GameMsg;
};
/**
* Represents an instance of an activity, having participants and score
*/
/**
* Represents an instance of an activity, having participants and score
*/
class ActivityInstance {
public:
ActivityInstance(Entity* parent, CDActivities activityInfo) { m_Parent = parent; m_ActivityInfo = activityInfo; };
@@ -104,7 +105,7 @@ struct LobbyPlayer {
/**
* The ID of the entity that is in the lobby
*/
LWOOBJID entityID;
LWOOBJID entityID = LWOOBJID_EMPTY;
/**
* Whether or not the entity is ready
@@ -126,12 +127,12 @@ struct Lobby {
/**
* The lobby of players
*/
std::vector<LobbyPlayer*> players;
std::vector<LobbyPlayer> players;
/**
* The timer that determines when the activity should start
*/
float timer;
float timer{};
};
/**
@@ -142,12 +143,12 @@ struct ActivityPlayer {
/**
* The entity that the score is tracked for
*/
LWOOBJID playerID;
LWOOBJID playerID{};
/**
* The list of score for this entity
*/
float values[10];
float values[10]{};
};
/**
@@ -194,13 +195,13 @@ public:
* @param instance the instance to load the players into
* @param lobby the players to load into the instance
*/
void LoadPlayersIntoInstance(ActivityInstance* instance, const std::vector<LobbyPlayer*>& lobby) const;
void LoadPlayersIntoInstance(ActivityInstance& instance, const std::vector<LobbyPlayer>& lobby) const;
/**
* Removes a lobby from the activity manager
* @param lobby the lobby to remove
*/
void RemoveLobby(Lobby* lobby);
void RemoveLobby(const LWOOBJID lobbyID);
/**
* Marks a player as (un)ready in a lobby
@@ -215,6 +216,10 @@ public:
*/
int GetActivityID() { return m_ActivityInfo.ActivityID; }
// Whether or not team loot should be dropped on death for this activity
// if true, and a player is supposed to get loot, they are skipped
bool GetNoTeamLootOnDeath() const { return m_ActivityInfo.noTeamLootOnDeath; }
/**
* Returns if this activity has a lobby, e.g. if it needs to instance players to some other map
* @return true if this activity has a lobby, false otherwise
@@ -242,7 +247,7 @@ public:
*/
bool IsPlayedBy(LWOOBJID playerID) const;
/**
/**
* Checks if the entity has enough cost to play this activity
* @param player the entity to check
* @return true if the entity has enough cost to play this activity, false otherwise
@@ -267,20 +272,14 @@ public:
* Creates a new instance for this activity
* @return a new instance for this activity
*/
ActivityInstance* NewInstance();
/**
* Returns all the currently active instances of this activity
* @return all the currently active instances of this activity
*/
const std::vector<ActivityInstance*>& GetInstances() const;
ActivityInstance& NewInstance();
/**
* Returns the instance that some entity is currently playing in
* @param playerID the entity to check for
* @return if any, the instance that the entity is currently in
*/
ActivityInstance* GetInstance(const LWOOBJID playerID);
const ActivityInstance& GetInstance(const LWOOBJID playerID) const;
/**
* @brief Reloads the config settings for this component
@@ -288,23 +287,12 @@ public:
*/
void ReloadConfig();
/**
* Removes all the instances
*/
void ClearInstances();
/**
* Returns all the score for the players that are currently playing this activity
* @return
*/
std::vector<ActivityPlayer*> GetActivityPlayers() { return m_ActivityPlayers; };
/**
* Returns activity data for a specific entity (e.g. score and such).
* @param playerID the entity to get data for
* @return the activity data (score) for the passed player in this activity, if it exists
*/
ActivityPlayer* GetActivityPlayerData(LWOOBJID playerID);
bool PlayerHasActivityData(LWOOBJID playerID) const;
/**
* Sets some score value for an entity
@@ -320,7 +308,7 @@ public:
* @param index the index to get score for
* @return activity score for the passed parameters
*/
float_t GetActivityValue(LWOOBJID playerID, uint32_t index);
float_t GetActivityValue(LWOOBJID playerID, uint32_t index) const;
/**
* Removes activity score tracking for some entity
@@ -328,28 +316,14 @@ public:
*/
void RemoveActivityPlayerData(LWOOBJID playerID);
/**
* Adds activity score tracking for some entity
* @param playerID the entity to add the activity score for
* @return the created entry
*/
ActivityPlayer* AddActivityPlayerData(LWOOBJID playerID);
/**
* Sets the mapID that this activity points to
* @param mapID the map ID to set
*/
void SetInstanceMapID(uint32_t mapID) { m_ActivityInfo.instanceMapID = mapID; };
/**
* Returns the LMI that this activity points to for a team size
* @param teamSize the team size to get the LMI for
* @return the LMI that this activity points to for a team size
*/
uint32_t GetLootMatrixForTeamSize(uint32_t teamSize) { return m_ActivityLootMatrices[teamSize]; }
private:
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg);
/**
* The database information for this activity
*/
@@ -358,22 +332,17 @@ private:
/**
* All the active instances of this activity
*/
std::vector<ActivityInstance*> m_Instances;
std::vector<ActivityInstance> m_Instances;
/**
* The current lobbies for this activity
*/
std::vector<Lobby*> m_Queue;
std::map<LWOOBJID, Lobby> m_Queue;
/**
* All the activity score for the players in this activity
*/
std::vector<ActivityPlayer*> m_ActivityPlayers;
/**
* LMIs for team sizes
*/
std::unordered_map<uint32_t, uint32_t> m_ActivityLootMatrices;
std::map<LWOOBJID, std::array<float, 10>> m_ActivityPlayers;
/**
* The activity id

View File

@@ -13,6 +13,8 @@
#include "CDClientDatabase.h"
#include "CDClientManager.h"
#include "CDObjectSkillsTable.h"
#include "CDSkillBehaviorTable.h"
#include "DestroyableComponent.h"
#include <algorithm>
@@ -23,12 +25,13 @@
#include "SkillComponent.h"
#include "QuickBuildComponent.h"
#include "DestroyableComponent.h"
#include "Metrics.hpp"
#include "CDComponentsRegistryTable.h"
#include "CDPhysicsComponentTable.h"
#include "dNavMesh.h"
#include "Amf3.h"
BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
RegisterMsg(&BaseCombatAIComponent::MsgGetObjectReportInfo);
m_Target = LWOOBJID_EMPTY;
m_DirtyStateOrTarget = true;
m_State = AiState::spawn;
@@ -42,7 +45,7 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo
//Grab the aggro information from BaseCombatAI:
auto componentQuery = CDClientDatabase::CreatePreppedStmt(
"SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius FROM BaseCombatAIComponent WHERE id = ?;");
"SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius, minRoundLength, maxRoundLength, combatRoundLength FROM BaseCombatAIComponent WHERE id = ?;");
componentQuery.bind(1, static_cast<int>(componentID));
auto componentResult = componentQuery.execQuery();
@@ -62,44 +65,37 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo
if (!componentResult.fieldIsNull("hardTetherRadius"))
m_HardTetherRadius = componentResult.getFloatField("hardTetherRadius");
m_MinRoundLength = componentResult.getFloatField("minRoundLength");
m_MaxRoundLength = componentResult.getFloatField("maxRoundLength");
m_CombatRoundLength = componentResult.getFloatField("combatRoundLength");
}
componentResult.finalize();
// Get aggro and tether radius from settings and use this if it is present. Only overwrite the
// radii if it is greater than the one in the database.
if (m_Parent) {
auto aggroRadius = m_Parent->GetVar<float>(u"aggroRadius");
m_AggroRadius = aggroRadius != 0 ? aggroRadius : m_AggroRadius;
auto tetherRadius = m_Parent->GetVar<float>(u"tetherRadius");
m_HardTetherRadius = tetherRadius != 0 ? tetherRadius : m_HardTetherRadius;
}
m_AggroRadius = m_Parent->HasVar(u"aggroRadius") ? m_Parent->GetVar<float>(u"aggroRadius") : m_AggroRadius;
m_HardTetherRadius = m_Parent->HasVar(u"tetherRadius") ? m_Parent->GetVar<float>(u"tetherRadius") : m_HardTetherRadius;
/*
* Find skills
*/
auto skillQuery = CDClientDatabase::CreatePreppedStmt(
"SELECT skillID, cooldown, behaviorID FROM SkillBehavior WHERE skillID IN (SELECT skillID FROM ObjectSkills WHERE objectTemplate = ?);");
skillQuery.bind(1, static_cast<int>(parent->GetLOT()));
for (const auto objectSkill : CDClientManager::GetTable<CDObjectSkillsTable>()->Get(parent->GetLOT())) {
const auto skillBehavior = CDClientManager::GetTable<CDSkillBehaviorTable>()->GetSkillByID(objectSkill.skillID);
if (skillBehavior.skillID == objectSkill.skillID) {
const auto skillId = skillBehavior.skillID;
auto result = skillQuery.execQuery();
const auto abilityCooldown = skillBehavior.cooldown;
while (!result.eof()) {
const auto skillId = static_cast<uint32_t>(result.getIntField("skillID"));
const auto behaviorId = skillBehavior.behaviorID;
const auto abilityCooldown = static_cast<float>(result.getFloatField("cooldown"));
const auto combatWeight = objectSkill.AICombatWeight;
const auto behaviorId = static_cast<uint32_t>(result.getIntField("behaviorID"));
auto* behavior = Behavior::CreateBehavior(behaviorId);
auto* behavior = Behavior::CreateBehavior(behaviorId);
AiSkillEntry entry = { .skillId = skillId, .cooldown = 0.0f, .abilityCooldown = abilityCooldown, .behavior = behavior, .combatWeight = combatWeight };
std::stringstream behaviorQuery;
AiSkillEntry entry = { skillId, 0, abilityCooldown, behavior };
m_SkillEntries.push_back(entry);
result.nextRow();
m_SkillEntries.push_back(entry);
}
}
Stun(1.0f);
@@ -209,8 +205,10 @@ void BaseCombatAIComponent::Update(const float deltaTime) {
}
if (stunnedThisFrame) {
m_MovementAI->Stop();
if (!m_MovementAI->IsPaused()) m_MovementAI->Pause();
// in this case we just become unstunned so check if we paused and resume if we did
if (!m_Stunned && m_MovementAI->IsPaused()) m_MovementAI->Resume();
return;
}
@@ -245,10 +243,12 @@ void BaseCombatAIComponent::Update(const float deltaTime) {
void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
bool hasSkillToCast = false;
int32_t maxSkillWeights = 0;
for (auto& entry : m_SkillEntries) {
if (entry.cooldown > 0.0f) {
entry.cooldown -= deltaTime;
} else {
maxSkillWeights += entry.combatWeight;
hasSkillToCast = true;
}
}
@@ -316,12 +316,14 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
SetAiState(AiState::aggro);
} else {
SetAiState(AiState::idle);
if (m_MovementAI) m_MovementAI->SetMaxSpeed(1.0f);
}
if (!hasSkillToCast) return;
if (m_Target == LWOOBJID_EMPTY) {
SetAiState(AiState::idle);
if (m_MovementAI) m_MovementAI->SetMaxSpeed(1.0f);
return;
}
@@ -332,14 +334,23 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
LookAt(target->GetPosition());
}
for (auto i = 0; i < m_SkillEntries.size(); ++i) {
auto entry = m_SkillEntries.at(i);
// Roll to find which skill we'll try to cast
auto randomizedWeight = GeneralUtils::GenerateRandomNumber<int32_t>(0, maxSkillWeights);
if (entry.cooldown > 0) {
for (auto& entry : m_SkillEntries) {
// Skill isn't cooled off yet
if (entry.cooldown > 0.0f) {
continue;
}
const auto result = skillComponent->CalculateBehavior(entry.skillId, entry.behavior->m_behaviorId, LWOOBJID_EMPTY);
randomizedWeight -= entry.combatWeight;
// if the weight is still greater than 0 continue to the next rolled skill
if (randomizedWeight > 0) {
continue;
}
const auto result = skillComponent->CalculateBehavior(entry.skillId, entry.behavior->m_behaviorId, GetTarget());
if (result.success) {
if (m_MovementAI != nullptr) {
@@ -354,8 +365,6 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
entry.cooldown = entry.abilityCooldown + m_SkillTime;
m_SkillEntries[i] = entry;
break;
}
}
@@ -476,6 +485,7 @@ std::vector<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());
@@ -616,6 +626,11 @@ void BaseCombatAIComponent::Wander() {
return;
}
// If we have a path to follow we should almost certainly do that instead of wandering.
if (m_MovementAI->HasPath()) {
return;
}
m_MovementAI->SetHaltDistance(0);
const auto& info = m_MovementAI->GetInfo();
@@ -744,8 +759,8 @@ void BaseCombatAIComponent::SetTetherSpeed(float value) {
m_TetherSpeed = value;
}
void BaseCombatAIComponent::Stun(const float time) {
if (m_StunImmune || m_StunTime > time) {
void BaseCombatAIComponent::Stun(const float time, const bool force) {
if (!force && (m_StunImmune || m_StunTime > time)) {
return;
}
@@ -839,3 +854,80 @@ void BaseCombatAIComponent::IgnoreThreat(const LWOOBJID threat, const float valu
SetThreat(threat, 0.0f);
m_Target = LWOOBJID_EMPTY;
}
bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
using enum AiState;
auto& cmptType = reportInfo.info->PushDebug("Base Combat AI");
cmptType.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
auto& targetInfo = cmptType.PushDebug("Current Target Info");
targetInfo.PushDebug<AMFStringValue>("Current Target ID") = std::to_string(m_Target);
// if (m_Target != LWOOBJID_EMPTY) {
// LWOGameMessages::ObjGetName nameMsg(m_CurrentTarget);
// SEND_GAMEOBJ_MSG(nameMsg);
// if (!nameMsg.msg.name.empty()) targetInfo.PushDebug("Name") = nameMsg.msg.name;
// }
auto& roundInfo = cmptType.PushDebug("Round Info");
// roundInfo.PushDebug<AMFDoubleValue>("Combat Round Time") = m_CombatRoundLength;
// roundInfo.PushDebug<AMFDoubleValue>("Minimum Time") = m_MinRoundLength;
// roundInfo.PushDebug<AMFDoubleValue>("Maximum Time") = m_MaxRoundLength;
// roundInfo.PushDebug<AMFDoubleValue>("Selected Time") = m_SelectedTime;
// roundInfo.PushDebug<AMFDoubleValue>("Combat Start Delay") = m_CombatStartDelay;
std::string curState;
switch (m_State) {
case idle: curState = "Idling"; break;
case aggro: curState = "Aggroed"; break;
case tether: curState = "Returning to Tether"; break;
case spawn: curState = "Spawn"; break;
case dead: curState = "Dead"; break;
default: curState = "Unknown or Undefined"; break;
}
cmptType.PushDebug<AMFStringValue>("Current Combat State") = curState;
//switch (m_CombatBehaviorType) {
// case 0: curState = "Passive"; break;
// case 1: curState = "Aggressive"; break;
// case 2: curState = "Passive (Turret)"; break;
// case 3: curState = "Aggressive (Turret)"; break;
// default: curState = "Unknown or Undefined"; break;
//}
//cmptType.PushDebug("Current Combat Behavior State") = curState;
//switch (m_CombatRole) {
// case 0: curState = "Melee"; break;
// case 1: curState = "Ranged"; break;
// case 2: curState = "Support"; break;
// default: curState = "Unknown or Undefined"; break;
//}
//cmptType.PushDebug("Current Combat Role") = curState;
auto& tetherPoint = cmptType.PushDebug("Tether Point");
tetherPoint.PushDebug<AMFDoubleValue>("X") = m_StartPosition.x;
tetherPoint.PushDebug<AMFDoubleValue>("Y") = m_StartPosition.y;
tetherPoint.PushDebug<AMFDoubleValue>("Z") = m_StartPosition.z;
cmptType.PushDebug<AMFDoubleValue>("Hard Tether Radius") = m_HardTetherRadius;
cmptType.PushDebug<AMFDoubleValue>("Soft Tether Radius") = m_SoftTetherRadius;
cmptType.PushDebug<AMFDoubleValue>("Aggro Radius") = m_AggroRadius;
cmptType.PushDebug<AMFDoubleValue>("Tether Speed") = m_TetherSpeed;
cmptType.PushDebug<AMFDoubleValue>("Aggro Speed") = m_TetherSpeed;
// cmptType.PushDebug<AMFDoubleValue>("Specified Min Range") = m_SpecificMinRange;
// cmptType.PushDebug<AMFDoubleValue>("Specified Max Range") = m_SpecificMaxRange;
auto& threats = cmptType.PushDebug("Target Threats");
for (const auto& [id, threat] : m_ThreatEntries) {
threats.PushDebug<AMFDoubleValue>(std::to_string(id)) = threat;
}
auto& ignoredThreats = cmptType.PushDebug("Temp Ignored Threats");
for (const auto& [id, threat] : m_RemovedThreatList) {
ignoredThreats.PushDebug<AMFDoubleValue>(std::to_string(id) + " - Time") = threat;
}
auto& skillInfo = cmptType.PushDebug("Skill Info");
for (const auto& skill : m_SkillEntries) {
auto& skillDebug = skillInfo.PushDebug("Skill ID " + std::to_string(skill.skillId));
skillDebug.PushDebug<AMFDoubleValue>("Cooldown") = skill.cooldown;
skillDebug.PushDebug<AMFDoubleValue>("Ability Cooldown") = skill.abilityCooldown;
skillDebug.PushDebug<AMFIntValue>("AI Combat Weight") = skill.combatWeight;
}
return true;
}

View File

@@ -33,13 +33,15 @@ enum class AiState : uint32_t {
*/
struct AiSkillEntry
{
uint32_t skillId;
uint32_t skillId{};
float cooldown;
float cooldown{};
float abilityCooldown;
float abilityCooldown{};
Behavior* behavior;
Behavior* behavior{};
int32_t combatWeight{};
};
/**
@@ -181,8 +183,9 @@ public:
/**
* Stuns the entity for a certain amount of time, will not work if the entity is stun immune
* @param time the time to stun the entity, if stunnable
* @param force whether or not to force the stun and ignore checks
*/
void Stun(float time);
void Stun(float time, const bool force = false);
/**
* Gets the radius that will cause this entity to get aggro'd, causing a target chase
@@ -234,6 +237,10 @@ public:
// Ignore a threat for a certain amount of time
void IgnoreThreat(const LWOOBJID target, const float time);
bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
void SetStartingPosition(const NiPoint3& pos) { m_StartPosition = pos; }
private:
/**
* Returns the current target or the target that currently is the largest threat to this entity
@@ -392,9 +399,17 @@ private:
*/
bool m_DirtyStateOrTarget = false;
// Min amount of time to remain as in combat after casting a skill
float m_MinRoundLength = 0.0f;
// max amount of time to remain as in combat after casting a skill
float m_MaxRoundLength = 0.0f;
// The amount of time the entity will be forced to tether for
float m_ForcedTetherTime = 0.0f;
float m_CombatRoundLength = 0.0f;
// The amount of time a removed threat will be ignored for.
std::map<LWOOBJID, float> m_RemovedThreatList;

View File

@@ -8,15 +8,30 @@
#include "GameMessages.h"
#include "BitStream.h"
#include "eTriggerEventType.h"
#include "Amf3.h"
BouncerComponent::BouncerComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
m_PetEnabled = false;
m_PetBouncerEnabled = false;
m_PetSwitchLoaded = false;
m_Destination = GeneralUtils::TryParse<NiPoint3>(
GeneralUtils::SplitString(m_Parent->GetVarAsString(u"bouncer_destination"), '\x1f'))
.value_or(NiPoint3Constant::ZERO);
m_Speed = GeneralUtils::TryParse<float>(m_Parent->GetVarAsString(u"bouncer_speed")).value_or(-1.0f);
m_UsesHighArc = GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"bouncer_uses_high_arc")).value_or(false);
m_LockControls = GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"lock_controls")).value_or(false);
m_IgnoreCollision = !GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"ignore_collision")).value_or(true);
m_StickLanding = GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"stickLanding")).value_or(false);
m_UsesGroupName = GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"uses_group_name")).value_or(false);
m_GroupName = m_Parent->GetVarAsString(u"grp_name");
m_MinNumTargets = GeneralUtils::TryParse<int32_t>(m_Parent->GetVarAsString(u"num_targets_to_activate")).value_or(1);
m_CinematicPath = m_Parent->GetVarAsString(u"attached_cinematic_path");
if (parent->GetLOT() == 7625) {
LookupPetSwitch();
}
RegisterMsg(&BouncerComponent::MsgGetObjectReportInfo);
}
BouncerComponent::~BouncerComponent() {
@@ -94,3 +109,53 @@ void BouncerComponent::LookupPetSwitch() {
});
}
}
bool BouncerComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
auto& cmptType = reportInfo.info->PushDebug("Bouncer");
cmptType.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
auto& destPos = cmptType.PushDebug("Destination Position");
if (m_Destination != NiPoint3Constant::ZERO) {
destPos.PushDebug(m_Destination);
} else {
destPos.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Bouncer has no target position, is likely missing config data");
}
if (m_Speed == -1.0f) {
cmptType.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Bouncer has no speed value, is likely missing config data");
} else {
cmptType.PushDebug<AMFDoubleValue>("Bounce Speed") = m_Speed;
}
cmptType.PushDebug<AMFStringValue>("Bounce trajectory arc") = m_UsesHighArc ? "High Arc" : "Low Arc";
cmptType.PushDebug<AMFBoolValue>("Collision Enabled") = m_IgnoreCollision;
cmptType.PushDebug<AMFBoolValue>("Stick Landing") = m_StickLanding;
cmptType.PushDebug<AMFBoolValue>("Locks character's controls") = m_LockControls;
if (!m_CinematicPath.empty()) cmptType.PushDebug<AMFStringValue>("Cinematic Camera Path (plays during bounce)") = m_CinematicPath;
auto* switchComponent = m_Parent->GetComponent<SwitchComponent>();
auto& respondsToFactions = cmptType.PushDebug("Responds to Factions");
if (!switchComponent || switchComponent->GetFactionsToRespondTo().empty()) respondsToFactions.PushDebug("Faction 1");
else {
for (const auto faction : switchComponent->GetFactionsToRespondTo()) {
respondsToFactions.PushDebug(("Faction " + std::to_string(faction)));
}
}
cmptType.PushDebug<AMFBoolValue>("Uses a group name for interactions") = m_UsesGroupName;
if (!m_UsesGroupName) {
if (m_MinNumTargets > 1) {
cmptType.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Bouncer has a required number of objects to activate, but no group for interactions.");
}
if (!m_GroupName.empty()) {
cmptType.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Has a group name for interactions , but is marked to not use that name.");
}
} else {
if (m_GroupName.empty()) {
cmptType.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Set to use a group name for inter actions, but no group name is assigned");
}
cmptType.PushDebug<AMFIntValue>("Number of interactions to activate bouncer") = m_MinNumTargets;
}
return true;
}

View File

@@ -51,6 +51,8 @@ public:
*/
void LookupPetSwitch();
bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
private:
/**
* Whether this bouncer needs to be activated by a pet
@@ -66,6 +68,36 @@ private:
* Whether the pet switch for this bouncer has been located
*/
bool m_PetSwitchLoaded;
// The bouncer destination
NiPoint3 m_Destination;
// The speed at which the player is bounced
float m_Speed{};
// Whether to use a high arc for the bounce trajectory
bool m_UsesHighArc{};
// Lock controls when bouncing
bool m_LockControls{};
// Ignore collision when bouncing
bool m_IgnoreCollision{};
// Stick the landing afterwards or let the player slide
bool m_StickLanding{};
// Whether or not there is a group name
bool m_UsesGroupName{};
// The group name for targets
std::string m_GroupName{};
// The number of targets to activate the bouncer
int32_t m_MinNumTargets{};
// The cinematic path to play during the bounce
std::string m_CinematicPath{};
};
#endif // BOUNCERCOMPONENT_H

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

@@ -24,6 +24,7 @@
#include "WorldPackets.h"
#include "MessageType/Game.h"
#include <ctime>
#include <ranges>
CharacterComponent::CharacterComponent(Entity* parent, const int32_t componentID, Character* character, const SystemAddress& systemAddress) : Component(parent, componentID) {
m_Character = character;
@@ -49,11 +50,10 @@ CharacterComponent::CharacterComponent(Entity* parent, const int32_t componentID
m_LastUpdateTimestamp = std::time(nullptr);
m_SystemAddress = systemAddress;
RegisterMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, this, &CharacterComponent::OnGetObjectReportInfo);
RegisterMsg(&CharacterComponent::OnGetObjectReportInfo);
}
bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportInfo = static_cast<GameMessages::GetObjectReportInfo&>(msg);
bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
auto& cmptType = reportInfo.info->PushDebug("Character");
@@ -492,7 +492,7 @@ Item* CharacterComponent::RocketEquip(Entity* player) {
if (!rocket) return rocket;
// build and define the rocket config
for (LDFBaseData* data : rocket->GetConfig()) {
for (const auto& data : rocket->GetConfig().values | std::views::values) {
if (data->GetKey() == u"assemblyPartLOTs") {
std::string newRocketStr = data->GetValueAsString() + ";";
GeneralUtils::ReplaceInString(newRocketStr, "+", ";");
@@ -515,12 +515,12 @@ void CharacterComponent::RocketUnEquip(Entity* player) {
}
void CharacterComponent::TrackMissionCompletion(bool isAchievement) {
UpdatePlayerStatistic(MissionsCompleted);
// Achievements are tracked separately for the zone
if (isAchievement) {
const auto mapID = Game::zoneManager->GetZoneID().GetMapID();
GetZoneStatisticsForMap(mapID).m_AchievementsCollected++;
} else {
UpdatePlayerStatistic(MissionsCompleted);
}
}
@@ -798,8 +798,14 @@ std::string CharacterComponent::StatisticsToString() const {
return result.str();
}
uint64_t CharacterComponent::GetStatisticFromSplit(std::vector<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

@@ -1,5 +1,37 @@
#include "CollectibleComponent.h"
#include "MissionComponent.h"
#include "dServer.h"
#include "Amf3.h"
CollectibleComponent::CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId) :
Component(parentEntity, componentID), m_CollectibleId(collectibleId) {
RegisterMsg(&CollectibleComponent::MsgGetObjectReportInfo);
}
void CollectibleComponent::Serialize(RakNet::BitStream& outBitStream, bool isConstruction) {
outBitStream.Write(GetCollectibleId());
}
bool CollectibleComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportMsg) {
auto& cmptType = reportMsg.info->PushDebug("Collectible");
auto collectibleID = static_cast<uint32_t>(m_CollectibleId) + static_cast<uint32_t>(Game::server->GetZoneID() << 8);
cmptType.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
cmptType.PushDebug<AMFIntValue>("Collectible ID") = GetCollectibleId();
cmptType.PushDebug<AMFIntValue>("Mission Tracking ID (for save data)") = collectibleID;
auto* localCharEntity = Game::entityManager->GetEntity(reportMsg.clientID);
bool collected = false;
if (localCharEntity) {
auto* missionComponent = localCharEntity->GetComponent<MissionComponent>();
if (m_CollectibleId != 0) {
collected = missionComponent->HasCollectible(collectibleID);
}
}
cmptType.PushDebug<AMFBoolValue>("Has been collected") = collected;
return true;
}

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