Compare commits

..

125 Commits

Author SHA1 Message Date
Wincent
194cd669ee Migrations only flag 2025-04-18 16:01:02 +00:00
Wincent
6b92536c41 Cache build directory when using docker with BuildKit 2025-04-18 16:00:17 +00:00
Wincent
5796e62567 Merge remote-tracking branch 'origin/main' into dCinema 2025-04-18 14:49:18 +00:00
David Markowitz
6b0f3a66e9 fix: session flags not being loaded every other world load (#1763)
tested that news screen no longer shows up on every other world load
2025-04-11 09:10:38 -05:00
David Markowitz
f5c212fb86 fix: sys addr for private zones (#1760)
* fix: sys addr for private zones

* Initialize variables in Instance
2025-04-11 09:05:31 -05:00
David Markowitz
99f6cf2d92 fix: pin actions to SHA commits and downgrade cmake to ~3.25 (#1757)
* Update build-and-test.yml

* Update build-and-test.yml

* Update build-and-test.yml

* Update build-and-test.yml

* Update build-and-test.yml

* Update build-and-test.yml

* specify up to 3.31

* Update build-and-test.yml
2025-04-02 11:50:35 -05:00
David Markowitz
bc0f3d9163 fix: mission states being incorrect after world load (#1750) 2025-04-02 08:59:21 -05:00
David Markowitz
20d5a9b6d8 fix: mail claiming item (#1758)
* Update build-and-test.yml

* Update build-and-test.yml

* Update build-and-test.yml

* Update build-and-test.yml

* Update build-and-test.yml

* Update build-and-test.yml

* specify up to 3.31

* fix: mail claiming item

tested that I can claim an item without an error
2025-04-02 08:56:01 -05:00
jadebenn
c490d45fe0 feat: Packed asset bundle improvements (#1754)
* Removed some unneccessary indirection and added const-correctness

* improved packed asset bundle error messages

* rephrase the string_view initialization to satisfy microsoft

* change forward slashes to back slashes and let us never speak of this again

* make crc32b function static

* remove redundant 'static'

---------

Co-authored-by: jadebenn <9892985+jadebenn@users.noreply.github.com>
2025-03-29 14:46:18 -07:00
jadebenn
aa49aaae76 invert sqlite lookup result to fix name in use lookup errors (#1755)
Co-authored-by: jadebenn <9892985+jadebenn@users.noreply.github.com>
2025-03-28 18:12:28 -07:00
David Markowitz
f78baee534 fix the wu man (#1743) 2025-03-28 17:04:35 -05:00
David Markowitz
347fc46f01 check pending names too (#1748) 2025-03-28 17:03:04 -05:00
David Markowitz
d104559cc4 fix: avery npc not having animations anymore (#1751)
* for avery

* remove label
2025-03-17 13:16:56 -05:00
Gie "Max" Vanommeslaeghe
b702843011 Merge pull request #1735 from DarkflameUniverse/mailv2
feat: Mail Re-write and packet/bitstream handler POC
2025-02-18 21:47:13 +01:00
Aaron Kimbre
14d7dec6a8 toctou 2025-02-01 02:05:17 -06:00
Aaron Kimbre
6eaf0a153e explicit character ID usage 2025-02-01 01:56:57 -06:00
Aaron Kimbre
78e52904e5 address feedback 2025-02-01 01:51:46 -06:00
Aaron Kimbre
b388b03251 remove fwd decl 2025-02-01 01:21:17 -06:00
David Markowitz
ae37641635 eliminate children (#1741) 2025-01-25 20:47:51 -06:00
Aaron Kimbrell
566791e647 chore: limit API to only listen on localhost (#1740) 2025-01-25 20:47:12 -06:00
David Markowitz
306d959a83 fix: Release removes password generation for accounts (#1738)
* Release removes password generation

* Update MasterServer.cpp
2025-01-20 13:00:50 -06:00
Aaron Kimbre
a07d54e513 all tested and working 2025-01-20 00:42:28 -06:00
David Markowitz
e4c2eecbc7 add msg handling (#1737) 2025-01-20 00:42:15 -06:00
Aaron Kimbre
b01b3cc38d WIP debugging 2025-01-19 19:07:55 -06:00
Aaron Kimbre
b7c579fb84 make it compile and cleanup 2025-01-19 16:31:54 -06:00
Aaron Kimbre
7b1d6948c3 Overaul, need to test 2025-01-19 00:25:20 -06:00
David Markowitz
1b3cdc6d9c Use proper session flag checks (#1734) 2025-01-18 21:25:53 -06:00
David Markowitz
d860552776 ok sir (#1733) 2025-01-18 21:23:03 -06:00
Gie "Max" Vanommeslaeghe
b0d993de33 Merge pull request #1732 from DarkflameUniverse/quickbuild
fix: reduce networked traffic for QuickBuildComponent Serialization
2025-01-17 22:38:38 +01:00
Gie "Max" Vanommeslaeghe
dc602a94eb Merge pull request #1715 from DarkflameUniverse/webapiv2
feat: Chat Web API (now with no threading)
2025-01-17 22:38:15 +01:00
Aaron Kimbre
3faf9eea45 fix invalid players from showing up in api response 2025-01-15 15:50:30 -06:00
Aaron Kimbre
9542216650 comments and tweaks 2025-01-15 14:59:30 -06:00
Aaron Kimbre
5b8fe2cba0 refactor again 2025-01-15 13:48:49 -06:00
David Markowitz
f4b55915bc fix: server runs now (#1730) 2025-01-14 13:21:58 -06:00
David Markowitz
a0913ff23d remove grandmas code 2025-01-14 02:18:46 -08:00
David Markowitz
b738504812 reduce traffic greatly 2025-01-14 02:09:54 -08:00
David Markowitz
c968dc9028 fix lego club teleport (#1731)
Tested that the lego club teleport and starbase 3001 teleports work now both before and after you visit nexus tower
2025-01-14 01:28:24 -06:00
David Markowitz
2209a4432f Update AgSpiderBossMessage.cpp (#1729) 2025-01-14 01:14:24 -06:00
David Markowitz
c855a6b9cf Remove added physics objects (#1727) 2025-01-12 14:03:35 -06:00
David Markowitz
8abc545bd1 fix: use generated bcrypt password for internal master connections (#1720)
* add password hashing for master server

* use define
2025-01-10 01:45:20 -08:00
David Markowitz
136133dde2 fix: friends (#1726)
* fix

* Update ChatPacketHandler.cpp
2025-01-08 22:44:55 -06:00
Aaron Kimbre
6cd1310460 First pass 2025-01-08 14:01:09 -06:00
David Markowitz
23551d4ed8 fix: friends not updating and using incorrect world (#1724)
* fix: friends not updating and using incorrect world

* use better reset logic

* actual fix for real
2025-01-06 20:25:51 -06:00
David Markowitz
7599a2e81e i am surprised no one noticed this for 3 yeasr (#1721) 2025-01-06 20:19:27 -06:00
Aaron Kimbre
72ae55981b fix here too 2025-01-06 13:55:43 -06:00
David Markowitz
c31bf3fad4 Merge branch 'webapiv2' of https://github.com/DarkflameUniverse/DarkflameServer into webapiv2 2025-01-06 09:23:25 -08:00
Aaron Kimbre
0aa1be01d2 fix windows? 2025-01-06 08:47:07 -06:00
Aaron Kimbrell
d8172e2126 Update versions.txt
Co-authored-by: Daniel Seiler <me@xiphoseer.de>
2025-01-06 01:40:33 -06:00
Aaron Kimbre
88376c233c cleanup and make the web routes cleaner 2025-01-06 01:23:29 -06:00
Aaron Kimbre
f3b4143698 fix typo 2025-01-05 18:31:53 -06:00
Aaron Kimbre
55a1209c75 Merge branch 'main' into webapiv2 2025-01-05 16:49:24 -06:00
Aaron Kimbrell
a6c6d892cf chore: remove httplib since it's unused (#1723) 2025-01-05 00:13:07 -06:00
Aaron Kimbre
f7228a6495 update version.txt 2025-01-03 16:45:36 -06:00
David Markowitz
fb32534ae3 implement rest of equipment scripts (#1714) 2025-01-03 16:44:20 -06:00
Gie "Max" Vanommeslaeghe
c8fcb3788d Merge pull request #1718 from DarkflameUniverse/no-weapon-thing
fix: weaponless world load
2025-01-03 23:41:21 +01:00
Aaron Kimbre
7a99e8ee6b don't unecessairly convert to string 2025-01-03 09:27:48 -06:00
Aaron Kimbre
a5e8fd86ac update readme 2025-01-02 23:18:29 -06:00
Aaron Kimbre
210bc48149 return 204 when no data 2025-01-02 23:11:06 -06:00
David Markowitz
e757086465 include optimization 2025-01-02 20:28:03 -08:00
David Markowitz
915e9f75d1 Merge branch 'webapiv2' of https://github.com/DarkflameUniverse/DarkflameServer into webapiv2 2025-01-02 20:25:02 -08:00
David Markowitz
e9ee3e21cf Update PlayerContainer.h 2025-01-02 20:24:53 -08:00
Aaron Kimbre
deddf0f256 move to top 2025-01-02 22:23:40 -06:00
David Markowitz
ca38139ff5 Update dNet/CMakeLists.txt 2025-01-02 20:21:41 -08:00
Aaron Kimbrell
41a001e242 Update dNet/CMakeLists.txt
Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>
2025-01-02 22:20:31 -06:00
Aaron Kimbre
4e57b4aa7e feedback 2025-01-02 22:19:04 -06:00
David Markowitz
86b419735b fix weaponless world load 2025-01-02 20:17:06 -08:00
David Markowitz
631980af3a add json stuff 2025-01-02 20:14:07 -08:00
David Markowitz
9e16e01b8d Merge branch 'webapiv2' of https://github.com/DarkflameUniverse/DarkflameServer into webapiv2 2025-01-02 19:12:14 -08:00
David Markowitz
6e66c5c362 works 2025-01-02 18:42:50 -08:00
Aaron Kimbre
ee590c49c1 disable by default 2025-01-02 20:25:46 -06:00
Aaron Kimbre
7aaa69e42d lol, fix it 2025-01-02 20:19:23 -06:00
Aaron Kimbre
181bb0ce14 Address some fo the feedback 2025-01-02 20:09:44 -06:00
Aaron Kimbrell
2b325165aa Update dChatServer/ChatWebAPI.h
Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>
2025-01-02 20:03:59 -06:00
Aaron Kimbrell
94d53fa77c Update dChatServer/ChatWebAPI.cpp
Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>
2025-01-02 20:03:44 -06:00
Aaron Kimbrell
846ba894a4 Update dChatServer/PlayerContainer.cpp
Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>
2025-01-02 20:03:16 -06:00
Aaron Kimbre
2ce2f4e363 remove uneeded make_optional 2025-01-02 18:01:45 -06:00
Aaron Kimbre
fd1ce75380 whitespace 2025-01-02 17:06:08 -06:00
Aaron Kimbre
3578076eca even more cleanup, and make the tryparse work properly 2025-01-02 17:04:07 -06:00
Aaron Kimbre
7d06d012b5 formatting and more cleanup 2025-01-02 16:48:31 -06:00
Aaron Kimbre
126701b5fe Make startup cleaner and don't listen by default 2025-01-02 16:35:27 -06:00
Aaron Kimbre
2d08ec641c cleanup and fixes 2025-01-02 16:28:34 -06:00
Aaron Kimbrell
9387a8e3d1 Merge branch 'main' into webapiv2 2025-01-02 16:13:17 -06:00
Aaron Kimbre
e86f4e011b redo it with mongoose
add all previous POC api endpoints
2025-01-02 16:11:45 -06:00
Aaron Kimbre
070bec697c Alllll the groundwork 2025-01-02 00:45:53 -06:00
David Markowitz
9936bb0d00 add ignored scripts (#1713) 2025-01-02 00:11:18 -06:00
David Markowitz
beffad42ea fix online notification (#1703) 2025-01-01 21:33:06 -06:00
David Markowitz
3ecbd1013b fix: update player container on shutdown (#1704)
* update activity log on shutdown

* fix online notification

* update container on shutdown
2025-01-01 21:31:12 -06:00
David Markowitz
ff4546c027 readme updates (#1712) 2025-01-01 21:29:45 -06:00
jadebenn
71baa5ce90 feat: Replace calls to system function in server startups (#1691)
* replace linux calls

* windows api

* log child PIDs in parent process

* fix typo for windows

* functions now return the process ID

* use wchar_t for windows APIs

* Update Start.cpp

Try to fix MacOS issues

* Conditionally include unistd.h

* remove sudo config option and add error message for linux

* fix windows .exe extension

* REALLY fix windows

* try replacing c_str() with data()

* really REALLY fix Windows

* Update dNet/dServer.cpp

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>

* Update dServer.cpp
2025-01-01 15:41:21 -08:00
David Markowitz
5ccd15a7d8 fix first attack being weaponless (#1709) 2025-01-01 13:33:20 -06:00
David Markowitz
35bcaf6e95 Note required g++ compiler version (#1711) 2025-01-01 11:11:37 -08:00
David Markowitz
900c9b6abe fix: add missing racing scripts (#1708) 2025-01-01 10:54:21 -08:00
David Markowitz
94e7cfc211 fix optional (#1707) 2025-01-01 04:07:44 -06:00
David Markowitz
021db0ecd1 add child loading (#1706)
Tested that the NT combat challenge, am skullkin towers and qa wall in avant gardens all function as before.
2025-01-01 00:46:00 -06:00
wincent
bd863a6dcf Added a navmesh for old gf 2024-09-12 21:20:17 +02:00
wincent
f78b28b454 New records and commands 2024-09-12 21:18:48 +02:00
wincent
04e9e74d7c Additional record types 2024-09-12 15:32:44 +02:00
wincent
dfe924061f Amend preconditions fix 2024-09-12 15:31:36 +02:00
Wincent
a086fe730a Added clean-up param 2024-09-10 14:33:58 +00:00
wincent
06063c8c8a Fixed mission related preconditions 2024-09-10 12:40:49 +02:00
wincent
0d37da73c7 Add a max showing distance 2024-09-08 19:40:08 +02:00
wincent
c948e4bb84 Update to new apis 2024-09-08 17:54:43 +02:00
wincent
3efad8aa50 Merge remote-tracking branch 'origin/main' into dCinema 2024-09-08 17:43:20 +02:00
wincent
d52ce8000b Merge remote-tracking branch 'refs/remotes/origin/main'
Conflicts:
	CMakeLists.txt
	dGame/CMakeLists.txt
	dGame/Entity.cpp
	dGame/dBehaviors/AttackDelayBehavior.cpp
	dGame/dBehaviors/PlayEffectBehavior.cpp
2024-04-02 21:51:29 +02:00
wincent
685bd5d45b Updated to new API 2024-02-17 23:07:16 +01:00
wincent
88e5f0e8fb Merge remote-tracking branch 'refs/remotes/origin/main'
Conflicts:
	dGame/EntityManager.cpp
	dGame/Player.h
	dGame/dComponents/RenderComponent.cpp
2024-02-17 23:06:43 +01:00
wincent
7b7c0622b3 Update to new API 2024-01-16 21:46:44 +01:00
wincent
a91cf3adc7 Merge remote-tracking branch 'refs/remotes/origin/main' 2024-01-16 21:46:31 +01:00
wincent
3a60fffe1d Resolved some more comments 2023-11-20 20:26:52 +01:00
wincent
17a62d95e0 Resolved some comments 2023-11-20 19:52:27 +01:00
wincent
32cbd18e6b Fix CMake file 2023-11-14 14:13:58 +01:00
wincent
25e1482a25 Merge remote-tracking branch 'origin/main' into dCinema 2023-11-14 14:06:20 +01:00
wincent
7a7bdba3e1 Removed test script 2023-11-14 13:53:43 +01:00
wincent
e5467379e1 Reverted local CMakeVariables changes 2023-11-14 13:51:41 +01:00
wincent
14d4c87a2f More record types:
+ Can now specify a cordinate to path find to
+ Can now enable/disable the combat AI
2023-11-14 13:24:09 +01:00
wincent
a09bbdba32 Merge remote-tracking branch 'origin/main' into dCinema 2023-11-11 14:20:13 +01:00
wincent
54060f7f40 Minor refactor 2023-11-11 14:19:57 +01:00
wincent
9954e20eac More scene metadata
* Added the ability to specify a change to play
* Added the ability to specify if a scene can play multiple times to the same player
2023-10-30 19:02:54 +01:00
wincent
cdc9dda3c4 dCinema improvements
* Visiblity and effect records
* Recorder will catch effects from behaviors
* Documentation for setting up a scene to play automatically.
* Documentation for server-side preconditions.
2023-10-29 17:37:26 +01:00
wincent
e4320d3e63 Brought the branch up to speed
* Updated logging
* Added an image to the README
2023-10-28 12:28:17 +02:00
wincent
3f90a4dd0b Merge remote-tracking branch 'origin/main' into dCinema 2023-10-28 11:23:29 +02:00
wincent
b274ea1b8f Substational additions to dCinema
Includes documentation of how to create acts, prefabs and scenes.
2023-10-27 23:51:38 +02:00
wincent
01b40ffa08 Merge fix 2023-10-22 17:38:19 +02:00
wincent
da236f272d Merge remote-tracking branch 'refs/remotes/origin/main'
Conflicts:
	dGame/dGameMessages/GameMessages.cpp
2023-10-22 17:38:08 +02:00
wincent
9e56725cff Initial changes.
* Recorder to recall player actions.
* Server precondtions to manage entity visiblity.
2023-10-22 17:36:08 +02:00
181 changed files with 55826 additions and 1220 deletions

View File

@@ -16,12 +16,12 @@ jobs:
os: [ windows-2022, ubuntu-22.04, macos-13 ]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
submodules: true
- name: Add msbuild to PATH (Windows only)
if: ${{ matrix.os == 'windows-2022' }}
uses: microsoft/setup-msbuild@v2
uses: microsoft/setup-msbuild@767f00a3f09872d96a0cb9fcd5e6a4ff33311330
with:
vs-version: '[17,18)'
msbuild-architecture: x64
@@ -30,12 +30,16 @@ jobs:
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
with:
cmakeVersion: "~3.25.0" # <--= optional, use most recent 3.25.x version
- name: cmake
uses: lukka/run-cmake@v10
uses: lukka/run-cmake@67c73a83a46f86c4e0b96b741ac37ff495478c38
with:
workflowPreset: "ci-${{matrix.os}}"
- name: artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: build-${{matrix.os}}
path: |

1
.gitignore vendored
View File

@@ -2,7 +2,6 @@ temp/
cmake-build-debug/
RelWithDebInfo/
docker/configs
valgrind-out.txt
# Third party libraries
thirdparty/mysql/

3
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "thirdparty/cpp-httplib"]
path = thirdparty/cpp-httplib
url = https://github.com/yhirose/cpp-httplib
[submodule "thirdparty/tinyxml2"]
path = thirdparty/tinyxml2
url = https://github.com/leethomason/tinyxml2

View File

@@ -4,18 +4,6 @@ project(Darkflame
LANGUAGES C CXX
)
# Sanitizer flags - TODO: Make CMake preset before finalizing PR
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
add_compile_options("-fsanitize=undefined")
add_link_options("-fsanitize=undefined")
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_link_options("-static-libsan")
else()
add_link_options("-static-libasan")
endif()
endif()
# check if the path to the source directory contains a space
if("${CMAKE_SOURCE_DIR}" MATCHES " ")
message(FATAL_ERROR "The server cannot build in the path (" ${CMAKE_SOURCE_DIR} ") because it contains a space. Please move the server to a path without spaces.")
@@ -29,7 +17,7 @@ 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 OFF) # Set C and C++ symbol visibility to hide inlined functions
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) # Set C and C++ symbol visibility to hide inlined functions
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# Read variables from file
@@ -259,8 +247,9 @@ include_directories(
"thirdparty/recastnavigation"
"thirdparty/SQLite"
"thirdparty/cpplinq"
"thirdparty/cpp-httplib"
"thirdparty/MD5"
"thirdparty/nlohmann"
"thirdparty/mongoose"
)
# Add system specfic includes for Apple, Windows and Other Unix OS' (including Linux)
@@ -274,7 +263,7 @@ if(MSVC)
# add_compile_options("/W4")
# Want to enable warnings eventually, but WAY too much noise right now
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
add_compile_options("-Wuninitialized" "-Wold-style-cast" "-Wstrict-aliasing=01")
add_compile_options("-Wuninitialized" "-Wold-style-cast")
else()
message(WARNING "Unknown compiler: '${CMAKE_CXX_COMPILER_ID}' - No warning flags enabled.")
endif()

View File

@@ -11,7 +11,12 @@ COPY --chmod=0500 ./build.sh /app/
RUN sed -i 's/MARIADB_CONNECTOR_COMPILE_JOBS__=.*/MARIADB_CONNECTOR_COMPILE_JOBS__=2/' /app/CMakeVariables.txt
RUN ./build.sh
RUN --mount=type=cache,target=/app/build,id=build-cache \
mkdir -p /app/build /tmp/persisted-build && \
cd /app/build && \
cmake .. && \
make -j$(nproc --ignore 1) && \
cp -r /app/build/* /tmp/persisted-build/
FROM debian:12 as runtime
@@ -23,23 +28,23 @@ RUN --mount=type=cache,id=build-apt-cache,target=/var/cache/apt \
rm -rf /var/lib/apt/lists/*
# Grab libraries and load them
COPY --from=build /app/build/mariadbcpp/libmariadbcpp.so /usr/local/lib/
COPY --from=build /tmp/persisted-build/mariadbcpp/libmariadbcpp.so /usr/local/lib/
RUN ldconfig
# Server bins
COPY --from=build /app/build/*Server /app/
COPY --from=build /tmp/persisted-build/*Server /app/
# Necessary suplimentary files
COPY --from=build /app/build/*.ini /app/configs/
COPY --from=build /app/build/vanity/*.* /app/vanity/
COPY --from=build /app/build/navmeshes /app/navmeshes
COPY --from=build /app/build/migrations /app/migrations
COPY --from=build /app/build/*.dcf /app/
COPY --from=build /tmp/persisted-build/*.ini /app/configs/
COPY --from=build /tmp/persisted-build/vanity/*.* /app/vanity/
COPY --from=build /tmp/persisted-build/navmeshes /app/navmeshes
COPY --from=build /tmp/persisted-build/migrations /app/migrations
COPY --from=build /tmp/persisted-build/*.dcf /app/
# backup of config and vanity files to copy to the host incase
# of a mount clobbering the copy from above
COPY --from=build /app/build/*.ini /app/default-configs/
COPY --from=build /app/build/vanity/*.* /app/default-vanity/
COPY --from=build /tmp/persisted-build/*.ini /app/default-configs/
COPY --from=build /tmp/persisted-build/vanity/*.* /app/default-vanity/
# needed as the container runs with the root user
# and therefore sudo doesn't exist

View File

@@ -32,6 +32,8 @@ Darkflame Universe is a server emulator and does not distribute any LEGO® Unive
* To connect to the server, either delete the file `boot.cfg` which is found in your LEGO Universe client, rename the file `boot.cfg` to something else or follow the steps [here](#allowing-a-user-to-connect-to-your-server) if you wish to keep the file.
* When shutting down the server, it is highly recommended to click the `MasterServer.exe` window and hold `ctrl` while pressing `c` to stop the server.
* We are working on a way to make it so when you close the game, the server stops automatically alongside when you open the game, the server starts automatically.
* If you are not setting a server up on mac, you can ignore this note
* Note: you'll need to allow through System Preferences `AuthServer`, `ChatServer`, `MasterServer`, `WorldServer` and `libmariadbcpp.dylib` to run. The initial pop-up will block it due to the binaries being unsigned, after allowing them to run the servers will run as normal.
<font size="32">**If you are not planning on hosting a server for others, working in the codebase or wanting to use MariaDB for a database, you can stop reading here.**</font>
@@ -68,9 +70,15 @@ git clone --recursive https://github.com/DarkflameUniverse/DarkflameServer
## Install dependencies
### Required compiler versions
- g++11 or greater
- MSVC unchecked
- clang unchecked
- appleclang unchecked
### Windows packages
Ensure that you have either the [MSVC C++ compiler](https://visualstudio.microsoft.com/vs/features/cplusplus/) (recommended) or the [Clang compiler](https://github.com/llvm/llvm-project/releases/) installed.
You'll also need to download and install [CMake](https://cmake.org/download/) (version <font size="4">**CMake version 3.25**</font> or later!).
You'll also need to download and install [CMake](https://cmake.org/download/) (<font size="4">**version 3.25**</font> up to <font size="4">**version 3.31**</font>!).
### MacOS packages
Ensure you have [brew](https://brew.sh) installed.
@@ -92,7 +100,7 @@ sudo apt install build-essential gcc zlib1g-dev libssl-dev openssl mariadb-serve
```
#### Required CMake version
This project uses <font size="4">**CMake version 3.25**</font> or higher and as such you will need to ensure you have this version installed.
This project uses <font size="4">**CMake version 3.25**</font> up to <font size="4">**version 3.31**</font> and as such you will need to ensure you have this version installed.
You can check your CMake version by using the following command in a terminal.
```bash
cmake --version
@@ -202,6 +210,7 @@ If you would like to build the server faster, append `-j<number>` where number i
### Notes
Depending on your operating system, you may need to adjust some pre-processor defines in [CMakeVariables.txt](./CMakeVariables.txt) before building:
* If you are on MacOS, ensure OPENSSL_ROOT_DIR is pointing to the openssl root directory.
* By default it should be set to the correct directory.
* If you are using a Darkflame Universe client, ensure `client_net_version` in `build/sharedconfig.ini` is changed to 171023.
## Configuring your server
@@ -224,28 +233,41 @@ Navigate to `build/sharedconfig.ini` and fill in the following fields:
* `chatconfig.ini` contains a port option.
* `masterconfig.ini` contains options related to permissions you want to run your servers with.
* `sharedconfig.ini` contains several options that are shared across all servers
* `worldconfig.ini` contains several options to turn on QOL improvements should you want them. If you would like the most vanilla experience possible, you will need to turn some of these settings off.
* `worldconfig.ini` contains several options to turn on Quality of Life improvements should you want them. If you would like the most vanilla experience possible, you will need to turn some of these settings off.
## Verify your setup
Your build directory should now look like this:
* AuthServer
* ChatServer
* MasterServer
* WorldServer
* authconfig.ini
* chatconfig.ini
* masterconfig.ini
Your build directory should contain at a minimum all of the following files.
All listed files are required for a server to start.
`ini` files can be located at the environment variable `DLU_CONFIG_DIR` and do not need to be located in this directory.
(windows will have .exe at the end of the executables):
* sharedconfig.ini
* AuthServer(.exe)
* authconfig.ini
* ChatServer(.exe)
* chatconfig.ini
* MasterServer(.exe)
* masterconfig.ini
* WorldServer(.exe)
* worldconfig.ini
* ...
* blocklist.dcf
* migrations
* vanity
* navmeshes
* 1 of the following lists based on platform
* windows
* libmariadb.dll
* mariadbcpp.dll
* zlib.dll
* MacOS
* libmariadbcpp.dylib
* *nix
* libmariadbcpp.so
## Running the server
If everything has been configured correctly you should now be able to run the `MasterServer` binary which is located in the `build` directory. Darkflame Universe utilizes port numbers under 1024, so under Linux you either have to give the `AuthServer` binary network permissions or run it under sudo.
To give `AuthServer` network permissions and not require sudo, run the following command
If everything has been configured correctly you should now be able to run the `MasterServer` binary which is located in the `build` directory. Darkflame Universe utilizes port numbers under 1024, so under Linux you have to give the `AuthServer` binary network permissions by running the following command:
```bash
sudo setcap 'cap_net_bind_service=+ep' AuthServer
```
and then go to `build/masterconfig.ini` and change `use_sudo_auth` to 0.
### Linux Service
If you are running this on a linux based system, it will use your terminal to run the program interactively, preventing you using it for other tasks and requiring it to be open to run the server.
@@ -308,8 +330,14 @@ To connect to a server follow these steps:
* Replace the contents after to `:` and the following `,` with what you configured as the server's public facing IP. For example `AUTHSERVERIP=0:localhost` for locally hosted servers
* Next locate the line `UGCUSE3DSERVICES=7:`
* Ensure the number after the 7 is a `0`
* Alternatively, remove the line with `UGCUSE3DSERVICES` altogether
* Launch `legouniverse.exe`, through `wine` if on a Unix-like operating system
* Note that if you are on WSL2, you will need to configure the public IP in the server and client to be the IP of the WSL2 instance and not localhost, which can be found by running `ifconfig` in the terminal. Windows defaults to WSL1, so this will not apply to most users.
As an example, here is what the boot.cfg is required to contain for a server with the ip 12.34.56.78
```cfg
AUTHSERVERIP=0:12.34.56.78,
UGCUSE3DSERVICES=7:0
```
## Updating your server
To update your server to the latest version navigate to your cloned directory
@@ -326,6 +354,10 @@ Now follow the [build](#build-the-server) section for your system and your serve
## In-game commands
* A list of all in-game commands can be found [here](./docs/Commands.md).
## Chat Web API
* The Chat server has an API that can be enabled via `chatconfig.ini`.
* You can view the OpenAPI doc for the API here [here](./docs/ChatWebAPI.yaml).
## Verifying your client files
### LEGO® Universe 1.10.64

View File

@@ -1,7 +1,6 @@
# Try and find a clang-16 install, falling back to a generic clang install otherwise
find_program(CLANG_C_COMPILER clang-16 | clang REQUIRED)
find_program(CLANG_CXX_COMPILER clang++-16 | clang++ REQUIRED)
find_program(CLANG_CXX_LINKER lld REQUIRED)
# Debug messages
message(DEBUG "CLANG_C_COMPILER = ${CLANG_C_COMPILER}")

View File

@@ -27,7 +27,6 @@
#include "Game.h"
#include "Server.h"
namespace Game {
Logger* logger = nullptr;
dServer* server = nullptr;
@@ -71,12 +70,15 @@ int main(int argc, char** argv) {
//Find out the master's IP:
std::string masterIP;
uint32_t masterPort = 1500;
std::string masterPassword;
auto masterInfo = Database::Get()->GetMasterInfo();
if (masterInfo) {
masterIP = masterInfo->ip;
masterPort = masterInfo->port;
masterPassword = masterInfo->password;
}
LOG("Master is at %s:%d", masterIP.c_str(), masterPort);
Game::randomEngine = std::mt19937(time(0));
@@ -90,7 +92,7 @@ int main(int argc, char** argv) {
const auto externalIPString = Game::config->GetValue("external_ip");
if (!externalIPString.empty()) ourIP = externalIPString;
Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServerType::Auth, Game::config, &Game::lastSignal);
Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServerType::Auth, Game::config, &Game::lastSignal, masterPassword);
//Run it until server gets a kill message from Master:
auto t = std::chrono::high_resolution_clock::now();

View File

@@ -1,7 +1,4 @@
add_executable(AuthServer "AuthServer.cpp")
target_link_libraries(AuthServer ${COMMON_LIBRARIES} dServer)
target_include_directories(AuthServer PRIVATE ${PROJECT_SOURCE_DIR}/dServer)
add_compile_definitions(AuthServer PRIVATE PROJECT_VERSION="\"${PROJECT_VERSION}\"")

View File

@@ -2,6 +2,8 @@ set(DCHATSERVER_SOURCES
"ChatIgnoreList.cpp"
"ChatPacketHandler.cpp"
"PlayerContainer.cpp"
"ChatWebAPI.cpp"
"JSONUtils.cpp"
)
add_executable(ChatServer "ChatServer.cpp")
@@ -12,5 +14,5 @@ add_library(dChatServer ${DCHATSERVER_SOURCES})
target_include_directories(dChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dServer")
target_link_libraries(dChatServer ${COMMON_LIBRARIES} dChatFilter)
target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer)
target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer mongoose)

View File

@@ -49,7 +49,7 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
fd.zoneID = fr.zoneID;
//Since this friend is online, we need to update them on the fact that we've just logged in:
SendFriendUpdate(fr, player, 1, fd.isBestFriend);
if (player.isLogin) SendFriendUpdate(fr, player, 1, fd.isBestFriend);
} else {
fd.isOnline = false;
fd.zoneID = LWOZONEID();
@@ -103,7 +103,8 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
return;
};
auto& requestee = Game::playerContainer.GetPlayerDataMutable(playerName);
// Intentional copy
PlayerData requestee = Game::playerContainer.GetPlayerData(playerName);
// Check if player is online first
if (isBestFriendRequest && !requestee) {
@@ -140,7 +141,7 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
// Prevent GM friend spam
// If the player we are trying to be friends with is not a civilian and we are a civilian, abort the process
if (requestee.gmLevel > eGameMasterLevel::CIVILIAN && requestor.gmLevel == eGameMasterLevel::CIVILIAN ) {
if (requestee.gmLevel > eGameMasterLevel::CIVILIAN && requestor.gmLevel == eGameMasterLevel::CIVILIAN) {
SendFriendResponse(requestor, requestee, eAddFriendResponseType::MYTHRAN);
return;
}
@@ -188,19 +189,24 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
Database::Get()->SetBestFriendStatus(requestorPlayerID, requestee.playerID, bestFriendStatus);
// Sent the best friend update here if the value is 3
if (bestFriendStatus == 3U) {
requestee.countOfBestFriends += 1;
requestor.countOfBestFriends += 1;
if (requestee.sysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestee, requestor, eAddFriendResponseType::ACCEPTED, false, true);
if (requestor.sysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestor, requestee, eAddFriendResponseType::ACCEPTED, false, true);
for (auto& friendData : requestor.friends) {
if (friendData.friendID == requestee.playerID) {
friendData.isBestFriend = true;
}
}
for (auto& friendData : requestee.friends) {
if (friendData.friendID == requestor.playerID) {
friendData.isBestFriend = true;
requestor.countOfBestFriends += 1;
auto& toModify = Game::playerContainer.GetPlayerDataMutable(playerName);
if (toModify) {
for (auto& friendData : toModify.friends) {
if (friendData.friendID == requestor.playerID) {
friendData.isBestFriend = true;
}
}
toModify.countOfBestFriends += 1;
}
}
}
@@ -400,8 +406,8 @@ void ChatPacketHandler::HandleShowAll(Packet* packet) {
bitStream.Write(Game::playerContainer.GetSimCount());
bitStream.Write<uint8_t>(request.displayIndividualPlayers);
bitStream.Write<uint8_t>(request.displayZoneData);
if (request.displayZoneData || request.displayIndividualPlayers){
for (auto& [playerID, playerData ]: Game::playerContainer.GetAllPlayers()){
if (request.displayZoneData || request.displayIndividualPlayers) {
for (auto& [playerID, playerData] : Game::playerContainer.GetAllPlayers()) {
if (!playerData) continue;
bitStream.Write<uint8_t>(0); // structure packing
if (request.displayIndividualPlayers) bitStream.Write(LUWString(playerData.playerName));

View File

@@ -28,6 +28,8 @@
#include "RakNetDefines.h"
#include "MessageIdentifiers.h"
#include "ChatWebAPI.h"
namespace Game {
Logger* logger = nullptr;
dServer* server = nullptr;
@@ -74,7 +76,8 @@ int main(int argc, char** argv) {
Game::assetManager = new AssetManager(clientPath);
} catch (std::runtime_error& ex) {
LOG("Got an error while setting up assets: %s", ex.what());
delete Game::logger;
delete Game::config;
return EXIT_FAILURE;
}
@@ -84,18 +87,32 @@ int main(int argc, char** argv) {
} catch (std::exception& ex) {
LOG("Got an error while connecting to the database: %s", ex.what());
Database::Destroy("ChatServer");
delete Game::server;
delete Game::logger;
delete Game::config;
return EXIT_FAILURE;
}
// seyup the chat api web server
bool web_server_enabled = Game::config->GetValue("web_server_enabled") == "1";
ChatWebAPI chatwebapi;
if (web_server_enabled && !chatwebapi.Startup()){
// if we want the web api and it fails to start, exit
LOG("Failed to start web server, shutting down.");
Database::Destroy("ChatServer");
delete Game::logger;
delete Game::config;
return EXIT_FAILURE;
};
//Find out the master's IP:
std::string masterIP;
uint32_t masterPort = 1000;
std::string masterPassword;
auto masterInfo = Database::Get()->GetMasterInfo();
if (masterInfo) {
masterIP = masterInfo->ip;
masterPort = masterInfo->port;
masterPassword = masterInfo->password;
}
//It's safe to pass 'localhost' here, as the IP is only used as the external IP.
std::string ourIP = "localhost";
@@ -104,7 +121,7 @@ int main(int argc, char** argv) {
const auto externalIPString = Game::config->GetValue("external_ip");
if (!externalIPString.empty()) ourIP = externalIPString;
Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServerType::Chat, Game::config, &Game::lastSignal);
Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServerType::Chat, Game::config, &Game::lastSignal, masterPassword);
const bool dontGenerateDCF = GeneralUtils::TryParse<bool>(Game::config->GetValue("dont_generate_dcf")).value_or(false);
Game::chatFilter = new dChatFilter(Game::assetManager->GetResPath().string() + "/chatplus_en_us", dontGenerateDCF);
@@ -122,6 +139,8 @@ int main(int argc, char** argv) {
uint32_t framesSinceMasterDisconnect = 0;
uint32_t framesSinceLastSQLPing = 0;
auto lastTime = std::chrono::high_resolution_clock::now();
Game::logger->Flush(); // once immediately before main loop
while (!Game::ShouldShutdown()) {
//Check if we're still connected to master:
@@ -132,7 +151,11 @@ int main(int argc, char** argv) {
break; //Exit our loop, shut down.
} else framesSinceMasterDisconnect = 0;
//In world we'd update our other systems here.
const auto currentTime = std::chrono::high_resolution_clock::now();
const float deltaTime = std::chrono::duration<float>(currentTime - lastTime).count();
lastTime = currentTime;
Game::playerContainer.Update(deltaTime);
//Check for packets here:
Game::server->ReceiveFromMaster(); //ReceiveFromMaster also handles the master packets if needed.
@@ -143,6 +166,11 @@ int main(int argc, char** argv) {
packet = nullptr;
}
//Check and handle web requests:
if (web_server_enabled) {
chatwebapi.ReceiveRequests();
}
//Push our log every 30s:
if (framesSinceLastFlush >= logFlushTime) {
Game::logger->Flush();
@@ -168,7 +196,7 @@ int main(int argc, char** argv) {
t += std::chrono::milliseconds(chatFrameDelta); //Chat can run at a lower "fps"
std::this_thread::sleep_until(t);
}
Game::playerContainer.Shutdown();
//Delete our objects here:
Database::Destroy("ChatServer");
delete Game::server;
@@ -197,150 +225,149 @@ void HandlePacket(Packet* packet) {
inStream.Read(chatMessageID);
switch (chatMessageID) {
case MessageType::Chat::GM_MUTE:
Game::playerContainer.MuteUpdate(packet);
break;
case MessageType::Chat::GM_MUTE:
Game::playerContainer.MuteUpdate(packet);
break;
case MessageType::Chat::CREATE_TEAM:
Game::playerContainer.CreateTeamServer(packet);
break;
case MessageType::Chat::CREATE_TEAM:
Game::playerContainer.CreateTeamServer(packet);
break;
case MessageType::Chat::GET_FRIENDS_LIST:
ChatPacketHandler::HandleFriendlistRequest(packet);
break;
case MessageType::Chat::GET_FRIENDS_LIST:
ChatPacketHandler::HandleFriendlistRequest(packet);
break;
case MessageType::Chat::GET_IGNORE_LIST:
ChatIgnoreList::GetIgnoreList(packet);
break;
case MessageType::Chat::GET_IGNORE_LIST:
ChatIgnoreList::GetIgnoreList(packet);
break;
case MessageType::Chat::ADD_IGNORE:
ChatIgnoreList::AddIgnore(packet);
break;
case MessageType::Chat::ADD_IGNORE:
ChatIgnoreList::AddIgnore(packet);
break;
case MessageType::Chat::REMOVE_IGNORE:
ChatIgnoreList::RemoveIgnore(packet);
break;
case MessageType::Chat::REMOVE_IGNORE:
ChatIgnoreList::RemoveIgnore(packet);
break;
case MessageType::Chat::TEAM_GET_STATUS:
ChatPacketHandler::HandleTeamStatusRequest(packet);
break;
case MessageType::Chat::TEAM_GET_STATUS:
ChatPacketHandler::HandleTeamStatusRequest(packet);
break;
case MessageType::Chat::ADD_FRIEND_REQUEST:
//this involves someone sending the initial request, the response is below, response as in from the other player.
//We basically just check to see if this player is online or not and route the packet.
ChatPacketHandler::HandleFriendRequest(packet);
break;
case MessageType::Chat::ADD_FRIEND_REQUEST:
//this involves someone sending the initial request, the response is below, response as in from the other player.
//We basically just check to see if this player is online or not and route the packet.
ChatPacketHandler::HandleFriendRequest(packet);
break;
case MessageType::Chat::ADD_FRIEND_RESPONSE:
//This isn't the response a server sent, rather it is a player's response to a received request.
//Here, we'll actually have to add them to eachother's friend lists depending on the response code.
ChatPacketHandler::HandleFriendResponse(packet);
break;
case MessageType::Chat::ADD_FRIEND_RESPONSE:
//This isn't the response a server sent, rather it is a player's response to a received request.
//Here, we'll actually have to add them to eachother's friend lists depending on the response code.
ChatPacketHandler::HandleFriendResponse(packet);
break;
case MessageType::Chat::REMOVE_FRIEND:
ChatPacketHandler::HandleRemoveFriend(packet);
break;
case MessageType::Chat::REMOVE_FRIEND:
ChatPacketHandler::HandleRemoveFriend(packet);
break;
case MessageType::Chat::GENERAL_CHAT_MESSAGE:
ChatPacketHandler::HandleChatMessage(packet);
break;
case MessageType::Chat::GENERAL_CHAT_MESSAGE:
ChatPacketHandler::HandleChatMessage(packet);
break;
case MessageType::Chat::PRIVATE_CHAT_MESSAGE:
//This message is supposed to be echo'd to both the sender and the receiver
//BUT: they have to have different responseCodes, so we'll do some of the ol hacky wacky to fix that right up.
ChatPacketHandler::HandlePrivateChatMessage(packet);
break;
case MessageType::Chat::PRIVATE_CHAT_MESSAGE:
//This message is supposed to be echo'd to both the sender and the receiver
//BUT: they have to have different responseCodes, so we'll do some of the ol hacky wacky to fix that right up.
ChatPacketHandler::HandlePrivateChatMessage(packet);
break;
case MessageType::Chat::TEAM_INVITE:
ChatPacketHandler::HandleTeamInvite(packet);
break;
case MessageType::Chat::TEAM_INVITE:
ChatPacketHandler::HandleTeamInvite(packet);
break;
case MessageType::Chat::TEAM_INVITE_RESPONSE:
ChatPacketHandler::HandleTeamInviteResponse(packet);
break;
case MessageType::Chat::TEAM_INVITE_RESPONSE:
ChatPacketHandler::HandleTeamInviteResponse(packet);
break;
case MessageType::Chat::TEAM_LEAVE:
ChatPacketHandler::HandleTeamLeave(packet);
break;
case MessageType::Chat::TEAM_LEAVE:
ChatPacketHandler::HandleTeamLeave(packet);
break;
case MessageType::Chat::TEAM_SET_LEADER:
ChatPacketHandler::HandleTeamPromote(packet);
break;
case MessageType::Chat::TEAM_SET_LEADER:
ChatPacketHandler::HandleTeamPromote(packet);
break;
case MessageType::Chat::TEAM_KICK:
ChatPacketHandler::HandleTeamKick(packet);
break;
case MessageType::Chat::TEAM_KICK:
ChatPacketHandler::HandleTeamKick(packet);
break;
case MessageType::Chat::TEAM_SET_LOOT:
ChatPacketHandler::HandleTeamLootOption(packet);
break;
case MessageType::Chat::GMLEVEL_UPDATE:
ChatPacketHandler::HandleGMLevelUpdate(packet);
break;
case MessageType::Chat::LOGIN_SESSION_NOTIFY:
Game::playerContainer.InsertPlayer(packet);
break;
case MessageType::Chat::GM_ANNOUNCE:{
// we just forward this packet to every connected server
inStream.ResetReadPointer();
Game::server->Send(inStream, packet->systemAddress, true); // send to everyone except origin
}
break;
case MessageType::Chat::UNEXPECTED_DISCONNECT:
Game::playerContainer.RemovePlayer(packet);
break;
case MessageType::Chat::WHO:
ChatPacketHandler::HandleWho(packet);
break;
case MessageType::Chat::SHOW_ALL:
ChatPacketHandler::HandleShowAll(packet);
break;
case MessageType::Chat::USER_CHANNEL_CHAT_MESSAGE:
case MessageType::Chat::WORLD_DISCONNECT_REQUEST:
case MessageType::Chat::WORLD_PROXIMITY_RESPONSE:
case MessageType::Chat::WORLD_PARCEL_RESPONSE:
case MessageType::Chat::TEAM_MISSED_INVITE_CHECK:
case MessageType::Chat::GUILD_CREATE:
case MessageType::Chat::GUILD_INVITE:
case MessageType::Chat::GUILD_INVITE_RESPONSE:
case MessageType::Chat::GUILD_LEAVE:
case MessageType::Chat::GUILD_KICK:
case MessageType::Chat::GUILD_GET_STATUS:
case MessageType::Chat::GUILD_GET_ALL:
case MessageType::Chat::BLUEPRINT_MODERATED:
case MessageType::Chat::BLUEPRINT_MODEL_READY:
case MessageType::Chat::PROPERTY_READY_FOR_APPROVAL:
case MessageType::Chat::PROPERTY_MODERATION_CHANGED:
case MessageType::Chat::PROPERTY_BUILDMODE_CHANGED:
case MessageType::Chat::PROPERTY_BUILDMODE_CHANGED_REPORT:
case MessageType::Chat::MAIL:
case MessageType::Chat::WORLD_INSTANCE_LOCATION_REQUEST:
case MessageType::Chat::REPUTATION_UPDATE:
case MessageType::Chat::SEND_CANNED_TEXT:
case MessageType::Chat::CHARACTER_NAME_CHANGE_REQUEST:
case MessageType::Chat::CSR_REQUEST:
case MessageType::Chat::CSR_REPLY:
case MessageType::Chat::GM_KICK:
case MessageType::Chat::WORLD_ROUTE_PACKET:
case MessageType::Chat::GET_ZONE_POPULATIONS:
case MessageType::Chat::REQUEST_MINIMUM_CHAT_MODE:
case MessageType::Chat::MATCH_REQUEST:
case MessageType::Chat::UGCMANIFEST_REPORT_MISSING_FILE:
case MessageType::Chat::UGCMANIFEST_REPORT_DONE_FILE:
case MessageType::Chat::UGCMANIFEST_REPORT_DONE_BLUEPRINT:
case MessageType::Chat::UGCC_REQUEST:
case MessageType::Chat::WORLD_PLAYERS_PET_MODERATED_ACKNOWLEDGE:
case MessageType::Chat::ACHIEVEMENT_NOTIFY:
case MessageType::Chat::GM_CLOSE_PRIVATE_CHAT_WINDOW:
case MessageType::Chat::PLAYER_READY:
case MessageType::Chat::GET_DONATION_TOTAL:
case MessageType::Chat::UPDATE_DONATION:
case MessageType::Chat::PRG_CSR_COMMAND:
case MessageType::Chat::HEARTBEAT_REQUEST_FROM_WORLD:
case MessageType::Chat::UPDATE_FREE_TRIAL_STATUS:
LOG("Unhandled CHAT Message id: %s (%i)", StringifiedEnum::ToString(chatMessageID).data(), chatMessageID);
break;
default:
LOG("Unknown CHAT Message id: %i", chatMessageID);
case MessageType::Chat::TEAM_SET_LOOT:
ChatPacketHandler::HandleTeamLootOption(packet);
break;
case MessageType::Chat::GMLEVEL_UPDATE:
ChatPacketHandler::HandleGMLevelUpdate(packet);
break;
case MessageType::Chat::LOGIN_SESSION_NOTIFY:
Game::playerContainer.InsertPlayer(packet);
break;
case MessageType::Chat::GM_ANNOUNCE:
// we just forward this packet to every connected server
inStream.ResetReadPointer();
Game::server->Send(inStream, packet->systemAddress, true); // send to everyone except origin
break;
case MessageType::Chat::UNEXPECTED_DISCONNECT:
Game::playerContainer.ScheduleRemovePlayer(packet);
break;
case MessageType::Chat::WHO:
ChatPacketHandler::HandleWho(packet);
break;
case MessageType::Chat::SHOW_ALL:
ChatPacketHandler::HandleShowAll(packet);
break;
case MessageType::Chat::USER_CHANNEL_CHAT_MESSAGE:
case MessageType::Chat::WORLD_DISCONNECT_REQUEST:
case MessageType::Chat::WORLD_PROXIMITY_RESPONSE:
case MessageType::Chat::WORLD_PARCEL_RESPONSE:
case MessageType::Chat::TEAM_MISSED_INVITE_CHECK:
case MessageType::Chat::GUILD_CREATE:
case MessageType::Chat::GUILD_INVITE:
case MessageType::Chat::GUILD_INVITE_RESPONSE:
case MessageType::Chat::GUILD_LEAVE:
case MessageType::Chat::GUILD_KICK:
case MessageType::Chat::GUILD_GET_STATUS:
case MessageType::Chat::GUILD_GET_ALL:
case MessageType::Chat::BLUEPRINT_MODERATED:
case MessageType::Chat::BLUEPRINT_MODEL_READY:
case MessageType::Chat::PROPERTY_READY_FOR_APPROVAL:
case MessageType::Chat::PROPERTY_MODERATION_CHANGED:
case MessageType::Chat::PROPERTY_BUILDMODE_CHANGED:
case MessageType::Chat::PROPERTY_BUILDMODE_CHANGED_REPORT:
case MessageType::Chat::MAIL:
case MessageType::Chat::WORLD_INSTANCE_LOCATION_REQUEST:
case MessageType::Chat::REPUTATION_UPDATE:
case MessageType::Chat::SEND_CANNED_TEXT:
case MessageType::Chat::CHARACTER_NAME_CHANGE_REQUEST:
case MessageType::Chat::CSR_REQUEST:
case MessageType::Chat::CSR_REPLY:
case MessageType::Chat::GM_KICK:
case MessageType::Chat::WORLD_ROUTE_PACKET:
case MessageType::Chat::GET_ZONE_POPULATIONS:
case MessageType::Chat::REQUEST_MINIMUM_CHAT_MODE:
case MessageType::Chat::MATCH_REQUEST:
case MessageType::Chat::UGCMANIFEST_REPORT_MISSING_FILE:
case MessageType::Chat::UGCMANIFEST_REPORT_DONE_FILE:
case MessageType::Chat::UGCMANIFEST_REPORT_DONE_BLUEPRINT:
case MessageType::Chat::UGCC_REQUEST:
case MessageType::Chat::WORLD_PLAYERS_PET_MODERATED_ACKNOWLEDGE:
case MessageType::Chat::ACHIEVEMENT_NOTIFY:
case MessageType::Chat::GM_CLOSE_PRIVATE_CHAT_WINDOW:
case MessageType::Chat::PLAYER_READY:
case MessageType::Chat::GET_DONATION_TOTAL:
case MessageType::Chat::UPDATE_DONATION:
case MessageType::Chat::PRG_CSR_COMMAND:
case MessageType::Chat::HEARTBEAT_REQUEST_FROM_WORLD:
case MessageType::Chat::UPDATE_FREE_TRIAL_STATUS:
LOG("Unhandled CHAT Message id: %s (%i)", StringifiedEnum::ToString(chatMessageID).data(), chatMessageID);
break;
default:
LOG("Unknown CHAT Message id: %i", chatMessageID);
}
}

197
dChatServer/ChatWebAPI.cpp Normal file
View File

@@ -0,0 +1,197 @@
#include "ChatWebAPI.h"
#include "Logger.h"
#include "Game.h"
#include "json.hpp"
#include "dCommonVars.h"
#include "MessageType/Chat.h"
#include "dServer.h"
#include "dConfig.h"
#include "PlayerContainer.h"
#include "JSONUtils.h"
#include "GeneralUtils.h"
#include "eHTTPMethod.h"
#include "magic_enum.hpp"
#include "ChatPackets.h"
#include "StringifiedEnum.h"
#include "Database.h"
#ifdef DARKFLAME_PLATFORM_WIN32
#pragma push_macro("DELETE")
#undef DELETE
#endif
using json = nlohmann::json;
typedef struct mg_connection mg_connection;
typedef struct mg_http_message mg_http_message;
namespace {
const char* json_content_type = "Content-Type: application/json\r\n";
std::map<std::pair<eHTTPMethod, std::string>, WebAPIHTTPRoute> Routes {};
}
bool ValidateAuthentication(const mg_http_message* http_msg) {
// TO DO: This is just a placeholder for now
// use tokens or something at a later point if we want to implement authentication
// bit using the listen bind address to limit external access is good enough to start with
return true;
}
bool ValidateJSON(std::optional<json> data, HTTPReply& reply) {
if (!data) {
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid JSON\"}";
return false;
}
return true;
}
void HandlePlayersRequest(HTTPReply& reply, std::string body) {
const json data = Game::playerContainer;
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump();
}
void HandleTeamsRequest(HTTPReply& reply, std::string body) {
const json data = Game::playerContainer.GetTeamContainer();
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump();
}
void HandleAnnounceRequest(HTTPReply& reply, std::string body) {
auto data = GeneralUtils::TryParse<json>(body);
if (!ValidateJSON(data, reply)) return;
const auto& good_data = data.value();
auto check = JSONUtils::CheckRequiredData(good_data, { "title", "message" });
if (!check.empty()) {
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = check;
} else {
ChatPackets::Announcement announcement;
announcement.title = good_data["title"];
announcement.message = good_data["message"];
announcement.Send();
reply.status = eHTTPStatusCode::OK;
reply.message = "{\"status\":\"Announcement Sent\"}";
}
}
void HandleInvalidRoute(HTTPReply& reply) {
reply.status = eHTTPStatusCode::NOT_FOUND;
reply.message = "{\"error\":\"Invalid Route\"}";
}
void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_msg) {
HTTPReply reply;
if (!http_msg) {
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid Request\"}";
} else if (ValidateAuthentication(http_msg)) {
// convert method from cstring to std string
std::string method_string(http_msg->method.buf, http_msg->method.len);
// get mehtod from mg to enum
const eHTTPMethod method = magic_enum::enum_cast<eHTTPMethod>(method_string).value_or(eHTTPMethod::INVALID);
// convert uri from cstring to std string
std::string uri(http_msg->uri.buf, http_msg->uri.len);
std::transform(uri.begin(), uri.end(), uri.begin(), ::tolower);
// convert body from cstring to std string
std::string body(http_msg->body.buf, http_msg->body.len);
const auto routeItr = Routes.find({method, uri});
if (routeItr != Routes.end()) {
const auto& [_, route] = *routeItr;
route.handle(reply, body);
} else HandleInvalidRoute(reply);
} else {
reply.status = eHTTPStatusCode::UNAUTHORIZED;
reply.message = "{\"error\":\"Unauthorized\"}";
}
mg_http_reply(connection, static_cast<int>(reply.status), json_content_type, reply.message.c_str());
}
void HandleRequests(mg_connection* connection, int request, void* request_data) {
switch (request) {
case MG_EV_HTTP_MSG:
HandleHTTPMessage(connection, static_cast<mg_http_message*>(request_data));
break;
default:
break;
}
}
void ChatWebAPI::RegisterHTTPRoutes(WebAPIHTTPRoute route) {
auto [_, success] = Routes.try_emplace({ route.method, route.path }, route);
if (!success) {
LOG_DEBUG("Failed to register route %s", route.path.c_str());
} else {
LOG_DEBUG("Registered route %s", route.path.c_str());
}
}
ChatWebAPI::ChatWebAPI() {
mg_log_set(MG_LL_NONE);
mg_mgr_init(&mgr); // Initialize event manager
}
ChatWebAPI::~ChatWebAPI() {
mg_mgr_free(&mgr);
}
bool ChatWebAPI::Startup() {
// Make listen address
// std::string listen_ip = Game::config->GetValue("web_server_listen_ip");
// if (listen_ip == "localhost") listen_ip = "127.0.0.1";
const std::string& listen_port = Game::config->GetValue("web_server_listen_port");
// const std::string& listen_address = "http://" + listen_ip + ":" + listen_port;
const std::string& listen_address = "http://localhost:" + listen_port;
LOG("Starting web server on %s", listen_address.c_str());
// Create HTTP listener
if (!mg_http_listen(&mgr, listen_address.c_str(), HandleRequests, NULL)) {
LOG("Failed to create web server listener on %s", listen_port.c_str());
return false;
}
// Register routes
// API v1 routes
std::string v1_route = "/api/v1/";
RegisterHTTPRoutes({
.path = v1_route + "players",
.method = eHTTPMethod::GET,
.handle = HandlePlayersRequest
});
RegisterHTTPRoutes({
.path = v1_route + "teams",
.method = eHTTPMethod::GET,
.handle = HandleTeamsRequest
});
RegisterHTTPRoutes({
.path = v1_route + "announce",
.method = eHTTPMethod::POST,
.handle = HandleAnnounceRequest
});
return true;
}
void ChatWebAPI::ReceiveRequests() {
mg_mgr_poll(&mgr, 15);
}
#ifdef DARKFLAME_PLATFORM_WIN32
#pragma pop_macro("DELETE")
#endif

36
dChatServer/ChatWebAPI.h Normal file
View File

@@ -0,0 +1,36 @@
#ifndef __CHATWEBAPI_H__
#define __CHATWEBAPI_H__
#include <string>
#include <functional>
#include "mongoose.h"
#include "eHTTPStatusCode.h"
enum class eHTTPMethod;
typedef struct mg_mgr mg_mgr;
struct HTTPReply {
eHTTPStatusCode status = eHTTPStatusCode::NOT_FOUND;
std::string message = "{\"error\":\"Not Found\"}";
};
struct WebAPIHTTPRoute {
std::string path;
eHTTPMethod method;
std::function<void(HTTPReply&, const std::string&)> handle;
};
class ChatWebAPI {
public:
ChatWebAPI();
~ChatWebAPI();
void ReceiveRequests();
void RegisterHTTPRoutes(WebAPIHTTPRoute route);
bool Startup();
private:
mg_mgr mgr;
};
#endif // __CHATWEBAPI_H__

62
dChatServer/JSONUtils.cpp Normal file
View File

@@ -0,0 +1,62 @@
#include "JSONUtils.h"
#include "json.hpp"
using json = nlohmann::json;
void to_json(json& data, const PlayerData& playerData) {
data["id"] = playerData.playerID;
data["name"] = playerData.playerName;
data["gm_level"] = playerData.gmLevel;
data["muted"] = playerData.GetIsMuted();
auto& zoneID = data["zone_id"];
zoneID["map_id"] = playerData.zoneID.GetMapID();
zoneID["instance_id"] = playerData.zoneID.GetInstanceID();
zoneID["clone_id"] = playerData.zoneID.GetCloneID();
}
void to_json(json& data, const PlayerContainer& playerContainer) {
data = json::array();
for(auto& playerData : playerContainer.GetAllPlayers()) {
if (playerData.first == LWOOBJID_EMPTY) continue;
data.push_back(playerData.second);
}
}
void to_json(json& data, const TeamContainer& teamContainer) {
for (auto& teamData : Game::playerContainer.GetTeams()) {
if (!teamData) continue;
data.push_back(*teamData);
}
}
void to_json(json& data, const TeamData& teamData) {
data["id"] = teamData.teamID;
data["loot_flag"] = teamData.lootFlag;
data["local"] = teamData.local;
auto& leader = Game::playerContainer.GetPlayerData(teamData.leaderID);
data["leader"] = leader.playerName;
auto& members = data["members"];
for (auto& member : teamData.memberIDs) {
auto& playerData = Game::playerContainer.GetPlayerData(member);
if (!playerData) continue;
members.push_back(playerData);
}
}
std::string JSONUtils::CheckRequiredData(const json& data, const std::vector<std::string>& requiredData) {
json check;
check["error"] = json::array();
for (const auto& required : requiredData) {
if (!data.contains(required)) {
check["error"].push_back("Missing Parameter: " + required);
} else if (data[required] == "") {
check["error"].push_back("Empty Parameter: " + required);
}
}
return check["error"].empty() ? "" : check.dump();
}

17
dChatServer/JSONUtils.h Normal file
View File

@@ -0,0 +1,17 @@
#ifndef __JSONUTILS_H__
#define __JSONUTILS_H__
#include "json_fwd.hpp"
#include "PlayerContainer.h"
void to_json(nlohmann::json& data, const PlayerData& playerData);
void to_json(nlohmann::json& data, const PlayerContainer& playerContainer);
void to_json(nlohmann::json& data, const TeamContainer& teamData);
void to_json(nlohmann::json& data, const TeamData& teamData);
namespace JSONUtils {
// check required data for reqeust
std::string CheckRequiredData(const nlohmann::json& data, const std::vector<std::string>& requiredData);
}
#endif // __JSONUTILS_H__

View File

@@ -32,7 +32,10 @@ void PlayerContainer::InsertPlayer(Packet* packet) {
return;
}
auto isLogin = !m_Players.contains(playerId);
auto& data = m_Players[playerId];
data = PlayerData();
data.isLogin = isLogin;
data.playerID = playerId;
uint32_t len;
@@ -57,13 +60,32 @@ void PlayerContainer::InsertPlayer(Packet* packet) {
LOG("Added user: %s (%llu), zone: %i", data.playerName.c_str(), data.playerID, data.zoneID.GetMapID());
Database::Get()->UpdateActivityLog(data.playerID, eActivityType::PlayerLoggedIn, data.zoneID.GetMapID());
m_PlayersToRemove.erase(playerId);
}
void PlayerContainer::RemovePlayer(Packet* packet) {
void PlayerContainer::ScheduleRemovePlayer(Packet* packet) {
CINSTREAM_SKIP_HEADER;
LWOOBJID playerID;
LWOOBJID playerID{ LWOOBJID_EMPTY };
inStream.Read(playerID);
constexpr float updatePlayerOnLogoutTime = 20.0f;
if (playerID != LWOOBJID_EMPTY) m_PlayersToRemove.insert_or_assign(playerID, updatePlayerOnLogoutTime);
}
void PlayerContainer::Update(const float deltaTime) {
for (auto it = m_PlayersToRemove.begin(); it != m_PlayersToRemove.end();) {
auto& [id, time] = *it;
time -= deltaTime;
if (time <= 0.0f) {
RemovePlayer(id);
it = m_PlayersToRemove.erase(it);
} else {
++it;
}
}
}
void PlayerContainer::RemovePlayer(const LWOOBJID playerID) {
//Before they get kicked, we need to also send a message to their friends saying that they disconnected.
const auto& player = GetPlayerData(playerID);
@@ -197,7 +219,7 @@ TeamData* PlayerContainer::CreateTeam(LWOOBJID leader, bool local) {
team->leaderID = leader;
team->local = local;
mTeams.push_back(team);
GetTeamsMut().push_back(team);
AddMember(team, leader);
@@ -205,7 +227,7 @@ TeamData* PlayerContainer::CreateTeam(LWOOBJID leader, bool local) {
}
TeamData* PlayerContainer::GetTeam(LWOOBJID playerID) {
for (auto* team : mTeams) {
for (auto* team : GetTeams()) {
if (std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID) == team->memberIDs.end()) continue;
return team;
@@ -313,9 +335,9 @@ void PlayerContainer::PromoteMember(TeamData* team, LWOOBJID newLeader) {
}
void PlayerContainer::DisbandTeam(TeamData* team) {
const auto index = std::find(mTeams.begin(), mTeams.end(), team);
const auto index = std::find(GetTeams().begin(), GetTeams().end(), team);
if (index == mTeams.end()) return;
if (index == GetTeams().end()) return;
for (const auto memberId : team->memberIDs) {
const auto& otherMember = GetPlayerData(memberId);
@@ -330,15 +352,15 @@ void PlayerContainer::DisbandTeam(TeamData* team) {
UpdateTeamsOnWorld(team, true);
mTeams.erase(index);
GetTeamsMut().erase(index);
delete team;
}
void PlayerContainer::TeamStatusUpdate(TeamData* team) {
const auto index = std::find(mTeams.begin(), mTeams.end(), team);
const auto index = std::find(GetTeams().begin(), GetTeams().end(), team);
if (index == mTeams.end()) return;
if (index == GetTeams().end()) return;
const auto& leader = GetPlayerData(team->leaderID);
@@ -417,3 +439,13 @@ const PlayerData& PlayerContainer::GetPlayerData(const LWOOBJID& playerID) {
const PlayerData& PlayerContainer::GetPlayerData(const std::string& playerName) {
return GetPlayerDataMutable(playerName);
}
void PlayerContainer::Shutdown() {
m_Players.erase(LWOOBJID_EMPTY);
while (!m_Players.empty()) {
const auto& [id, playerData] = *m_Players.begin();
Database::Get()->UpdateActivityLog(id, eActivityType::PlayerLoggedOut, playerData.zoneID.GetMapID());
m_Players.erase(m_Players.begin());
}
for (auto* team : GetTeams()) if (team) delete team;
}

View File

@@ -9,6 +9,12 @@
enum class eGameMasterLevel : uint8_t;
struct TeamData;
struct TeamContainer {
std::vector<TeamData*> mTeams;
};
struct IgnoreData {
IgnoreData(const std::string& name, const LWOOBJID& id) : playerName{ name }, playerId{ id } {}
inline bool operator==(const std::string& other) const noexcept {
@@ -46,8 +52,10 @@ struct PlayerData {
std::vector<IgnoreData> ignoredPlayers;
eGameMasterLevel gmLevel = static_cast<eGameMasterLevel>(0); // CIVILLIAN
bool isFTP = false;
bool isLogin = false;
};
struct TeamData {
TeamData();
LWOOBJID teamID = LWOOBJID_EMPTY; // Internal use
@@ -62,10 +70,12 @@ class PlayerContainer {
public:
void Initialize();
void InsertPlayer(Packet* packet);
void RemovePlayer(Packet* packet);
void ScheduleRemovePlayer(Packet* packet);
void RemovePlayer(const LWOOBJID playerID);
void MuteUpdate(Packet* packet);
void CreateTeamServer(Packet* packet);
void BroadcastMuteUpdate(LWOOBJID player, time_t time);
void Shutdown();
const PlayerData& GetPlayerData(const LWOOBJID& playerID);
const PlayerData& GetPlayerData(const std::string& playerName);
@@ -73,7 +83,7 @@ public:
PlayerData& GetPlayerDataMutable(const std::string& playerName);
uint32_t GetPlayerCount() { return m_PlayerCount; };
uint32_t GetSimCount() { return m_SimCount; };
const std::map<LWOOBJID, PlayerData>& GetAllPlayers() { return m_Players; };
const std::map<LWOOBJID, PlayerData>& GetAllPlayers() const { return m_Players; };
TeamData* CreateLocalTeam(std::vector<LWOOBJID> members);
TeamData* CreateTeam(LWOOBJID leader, bool local = false);
@@ -88,12 +98,19 @@ public:
LWOOBJID GetId(const std::u16string& playerName);
uint32_t GetMaxNumberOfBestFriends() { return m_MaxNumberOfBestFriends; }
uint32_t GetMaxNumberOfFriends() { return m_MaxNumberOfFriends; }
const TeamContainer& GetTeamContainer() { return m_TeamContainer; }
std::vector<TeamData*>& GetTeamsMut() { return m_TeamContainer.mTeams; };
const std::vector<TeamData*>& GetTeams() { return GetTeamsMut(); };
void Update(const float deltaTime);
bool PlayerBeingRemoved(const LWOOBJID playerID) { return m_PlayersToRemove.contains(playerID); }
private:
LWOOBJID m_TeamIDCounter = 0;
std::map<LWOOBJID, PlayerData> m_Players;
std::vector<TeamData*> mTeams;
TeamContainer m_TeamContainer{};
std::unordered_map<LWOOBJID, std::u16string> m_Names;
std::map<LWOOBJID, float> m_PlayersToRemove;
uint32_t m_MaxNumberOfBestFriends = 5;
uint32_t m_MaxNumberOfFriends = 50;
uint32_t m_PlayerCount = 0;

View File

@@ -1,11 +1,11 @@
#ifndef AMF3_H
#define AMF3_H
#ifndef __AMF3__H__
#define __AMF3__H__
#include "dCommonVars.h"
#include "Logger.h"
#include "Game.h"
#include "GeneralUtils.h"
#include <type_traits>
#include <unordered_map>
#include <vector>
@@ -40,6 +40,7 @@ public:
// AMFValue template class instantiations
template <typename ValueType>
class AMFValue : public AMFBaseValue {
static_assert(!std::is_same_v<ValueType, std::string_view>, "AMFValue cannot be instantiated with std::string_view");
public:
AMFValue() = default;
AMFValue(const ValueType value) : m_Data{ value } {}
@@ -52,6 +53,15 @@ public:
void SetValue(const ValueType value) { m_Data = value; }
AMFValue<ValueType>& operator=(const AMFValue<ValueType>& other) {
return operator=(other.m_Data);
}
AMFValue<ValueType>& operator=(const ValueType& other) {
m_Data = other;
return *this;
}
protected:
ValueType m_Data;
};
@@ -75,15 +85,28 @@ template <> [[nodiscard]] constexpr eAmf AMFValue<double>::GetValueType() const
template <typename ValueType>
[[nodiscard]] constexpr eAmf AMFValue<ValueType>::GetValueType() const noexcept { return eAmf::Undefined; }
// As a string this is much easier to write and read from a BitStream.
template <>
class AMFValue<const char*> : public AMFBaseValue {
public:
AMFValue() = default;
AMFValue(const char* value) { m_Data = value; }
virtual ~AMFValue() override = default;
[[nodiscard]] constexpr eAmf GetValueType() const noexcept override { return eAmf::String; }
[[nodiscard]] const std::string& GetValue() const { return m_Data; }
void SetValue(const std::string& value) { m_Data = value; }
protected:
std::string m_Data;
};
using AMFNullValue = AMFValue<std::nullptr_t>;
using AMFBoolValue = AMFValue<bool>;
using AMFIntValue = AMFValue<int32_t>;
using AMFStringValue = AMFValue<std::string>;
using AMFDoubleValue = AMFValue<double>;
// Template deduction guide to ensure string literals deduce
AMFValue(const char*) -> AMFValue<std::string>; // AMFStringValue
/**
* The AMFArrayValue object holds 2 types of lists:
* An associative list where a key maps to a value
@@ -94,7 +117,7 @@ AMFValue(const char*) -> AMFValue<std::string>; // AMFStringValue
*/
class AMFArrayValue : public AMFBaseValue {
using AMFAssociative =
std::unordered_map<std::string, std::unique_ptr<AMFBaseValue>, GeneralUtils::transparent_string_hash, std::equal_to<void>>;
std::unordered_map<std::string, std::unique_ptr<AMFBaseValue>, GeneralUtils::transparent_string_hash, std::equal_to<>>;
using AMFDense = std::vector<std::unique_ptr<AMFBaseValue>>;
@@ -125,20 +148,17 @@ public:
* @return The inserted element if the type matched,
* or nullptr if a key existed and was not the same type
*/
template <typename T>
[[maybe_unused]] auto Insert(const std::string_view key, const T value) -> std::pair<decltype(AMFValue(value))*, bool> {
// This ensures the deduced type matches the AMFValue constructor
using AMFValueType = decltype(AMFValue(value));
template <typename ValueType>
[[maybe_unused]] std::pair<AMFValue<ValueType>*, bool> Insert(const std::string_view key, const ValueType value) {
const auto element = m_Associative.find(key);
AMFValueType* val = nullptr;
AMFValue<ValueType>* val = nullptr;
bool found = true;
if (element == m_Associative.cend()) {
auto newVal = std::make_unique<AMFValueType>(value);
auto newVal = std::make_unique<AMFValue<ValueType>>(value);
val = newVal.get();
m_Associative.emplace(key, std::move(newVal));
} else {
val = dynamic_cast<AMFValueType*>(element->second.get());
val = dynamic_cast<AMFValue<ValueType>*>(element->second.get());
found = false;
}
return std::make_pair(val, found);
@@ -181,18 +201,15 @@ public:
* @return The inserted element, or nullptr if the type did not match
* what was at the index.
*/
template <typename T>
[[maybe_unused]] auto Insert(const size_t index, const T value) -> std::pair<decltype(AMFValue(value))*, bool> {
// This ensures the deduced type matches the AMFValue constructor
using AMFValueType = decltype(AMFValue(value));
template <typename ValueType>
[[maybe_unused]] std::pair<AMFValue<ValueType>*, bool> Insert(const size_t index, const ValueType value) {
bool inserted = false;
if (index >= m_Dense.size()) {
m_Dense.resize(index + 1);
m_Dense.at(index) = std::make_unique<AMFValueType>(value);
m_Dense.at(index) = std::make_unique<AMFValue<ValueType>>(value);
inserted = true;
}
return std::make_pair(dynamic_cast<AMFValueType*>(m_Dense.at(index).get()), inserted);
return std::make_pair(dynamic_cast<AMFValue<ValueType>*>(m_Dense.at(index).get()), inserted);
}
/**
@@ -204,13 +221,17 @@ public:
* @param key The key to associate with the value
* @param value The value to insert
*/
void Insert(const std::string_view key, std::unique_ptr<AMFBaseValue> value) {
template<typename AmfType>
AmfType& Insert(const std::string_view key, std::unique_ptr<AmfType> value) {
const auto element = m_Associative.find(key);
auto& toReturn = *value;
if (element != m_Associative.cend() && element->second) {
element->second = std::move(value);
} else {
m_Associative.emplace(key, std::move(value));
}
return toReturn;
}
/**
@@ -222,11 +243,15 @@ public:
* @param key The key to associate with the value
* @param value The value to insert
*/
void Insert(const size_t index, std::unique_ptr<AMFBaseValue> value) {
template<typename AmfType>
AmfType& Insert(const size_t index, std::unique_ptr<AmfType> value) {
auto& toReturn = *value;
if (index >= m_Dense.size()) {
m_Dense.resize(index + 1);
}
m_Dense.at(index) = std::move(value);
return toReturn;
}
/**
@@ -239,8 +264,8 @@ public:
*
* @return The inserted pointer, or nullptr should the key already be in use.
*/
template <typename T>
[[maybe_unused]] inline auto Push(const T value) -> decltype(AMFValue(value))* {
template <typename ValueType>
[[maybe_unused]] inline AMFValue<ValueType>* Push(const ValueType value) {
return Insert(m_Dense.size(), value).first;
}
@@ -251,10 +276,10 @@ public:
*
* @param key The key to remove from the associative portion
*/
void Remove(const std::string& key, const bool deleteValue = true) {
void Remove(const std::string& key) {
const AMFAssociative::const_iterator it = m_Associative.find(key);
if (it != m_Associative.cend()) {
if (deleteValue) m_Associative.erase(it);
m_Associative.erase(it);
}
}
@@ -337,6 +362,18 @@ public:
return index < m_Dense.size() ? m_Dense.at(index).get() : nullptr;
}
void Reset() {
m_Associative.clear();
m_Dense.clear();
}
template<typename AmfType = AMFArrayValue>
AmfType& PushDebug(const std::string_view name) {
auto* value = PushArray();
value->Insert("name", name.data());
return value->Insert<AmfType>("value", std::make_unique<AmfType>());
}
private:
/**
* The associative portion. These values are key'd with strings to an AMFValue.
@@ -350,4 +387,4 @@ private:
AMFDense m_Dense;
};
#endif //!AMF3_H
#endif //!__AMF3__H__

View File

@@ -10,54 +10,40 @@ void RakNet::BitStream::Write<AMFBaseValue&>(AMFBaseValue& value) {
this->Write(type);
switch (type) {
case eAmf::Integer: {
this->Write<AMFIntValue&>(static_cast<AMFIntValue&>(value));
this->Write<AMFIntValue&>(*static_cast<AMFIntValue*>(&value));
break;
}
case eAmf::Double: {
this->Write<AMFDoubleValue&>(static_cast<AMFDoubleValue&>(value));
this->Write<AMFDoubleValue&>(*static_cast<AMFDoubleValue*>(&value));
break;
}
case eAmf::String: {
this->Write<AMFStringValue&>(static_cast<AMFStringValue&>(value));
this->Write<AMFStringValue&>(*static_cast<AMFStringValue*>(&value));
break;
}
case eAmf::Array: {
this->Write<AMFArrayValue&>(static_cast<AMFArrayValue&>(value));
this->Write<AMFArrayValue&>(*static_cast<AMFArrayValue*>(&value));
break;
}
default: {
LOG("Encountered unwritable AMFType %i!", type);
[[fallthrough]];
}
case eAmf::Undefined:
[[fallthrough]];
case eAmf::Null:
[[fallthrough]];
case eAmf::False:
[[fallthrough]];
case eAmf::True:
[[fallthrough]];
case eAmf::Date:
[[fallthrough]];
case eAmf::Object:
[[fallthrough]];
case eAmf::XML:
[[fallthrough]];
case eAmf::XMLDoc:
[[fallthrough]];
case eAmf::ByteArray:
[[fallthrough]];
case eAmf::VectorInt:
[[fallthrough]];
case eAmf::VectorUInt:
[[fallthrough]];
case eAmf::VectorDouble:
[[fallthrough]];
case eAmf::VectorObject:
[[fallthrough]];
case eAmf::Dictionary:
break;
}
@@ -159,7 +145,8 @@ void RakNet::BitStream::Write<AMFIntValue&>(AMFIntValue& value) {
// Writes an AMFDoubleValue to BitStream
template<>
void RakNet::BitStream::Write<AMFDoubleValue&>(AMFDoubleValue& value) {
WriteAMFU64(*this, std::bit_cast<uint64_t>(value.GetValue()));
double d = value.GetValue();
WriteAMFU64(*this, *reinterpret_cast<uint64_t*>(&d));
}
// Writes an AMFStringValue to BitStream

View File

@@ -146,7 +146,7 @@ void WriteSd0Magic(char* input, uint32_t chunkSize) {
input[2] = '0';
input[3] = 0x01;
input[4] = 0xFF;
std::memcpy(&input[5], &chunkSize, sizeof(uint32_t)); // Write the integer to the character array
*reinterpret_cast<uint32_t*>(input + 5) = chunkSize; // Write the integer to the character array
}
bool CheckSd0Magic(std::istream& streamToCheck) {

View File

@@ -7,6 +7,10 @@
#include <filesystem>
#include <map>
#include "json.hpp"
using json = nlohmann::json;
template <typename T>
static inline size_t MinSize(const size_t size, const std::basic_string_view<T> string) {
if (size == SIZE_MAX || size > string.size()) {
@@ -53,9 +57,9 @@ bool static _IsSuffixChar(const uint8_t c) {
bool GeneralUtils::details::_NextUTF8Char(std::string_view& slice, uint32_t& out) {
const size_t rem = slice.length();
if (slice.empty()) return false;
const char* const bytes = slice.data();
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&slice.front());
if (rem > 0) {
const uint8_t first = static_cast<uint8_t>(bytes[0]);
const uint8_t first = bytes[0];
if (first < 0x80) { // 1 byte character
out = static_cast<uint32_t>(first & 0x7F);
slice.remove_prefix(1);
@@ -64,7 +68,7 @@ bool GeneralUtils::details::_NextUTF8Char(std::string_view& slice, uint32_t& out
// middle byte, not valid at start, fall through
} else if (first < 0xE0) { // two byte character
if (rem > 1) {
const uint8_t second = static_cast<uint8_t>(bytes[1]);
const uint8_t second = bytes[1];
if (_IsSuffixChar(second)) {
out = (static_cast<uint32_t>(first & 0x1F) << 6)
+ static_cast<uint32_t>(second & 0x3F);
@@ -74,8 +78,8 @@ bool GeneralUtils::details::_NextUTF8Char(std::string_view& slice, uint32_t& out
}
} else if (first < 0xF0) { // three byte character
if (rem > 2) {
const uint8_t second = static_cast<uint8_t>(bytes[1]);
const uint8_t third = static_cast<uint8_t>(bytes[2]);
const uint8_t second = bytes[1];
const uint8_t third = bytes[2];
if (_IsSuffixChar(second) && _IsSuffixChar(third)) {
out = (static_cast<uint32_t>(first & 0x0F) << 12)
+ (static_cast<uint32_t>(second & 0x3F) << 6)
@@ -86,9 +90,9 @@ bool GeneralUtils::details::_NextUTF8Char(std::string_view& slice, uint32_t& out
}
} else if (first < 0xF8) { // four byte character
if (rem > 3) {
const uint8_t second = static_cast<uint8_t>(bytes[1]);
const uint8_t third = static_cast<uint8_t>(bytes[2]);
const uint8_t fourth = static_cast<uint8_t>(bytes[3]);
const uint8_t second = bytes[1];
const uint8_t third = bytes[2];
const uint8_t fourth = bytes[3];
if (_IsSuffixChar(second) && _IsSuffixChar(third) && _IsSuffixChar(fourth)) {
out = (static_cast<uint32_t>(first & 0x07) << 18)
+ (static_cast<uint32_t>(second & 0x3F) << 12)
@@ -327,6 +331,17 @@ std::vector<std::string> GeneralUtils::GetSqlFileNamesFromFolder(const std::stri
return sortedFiles;
}
template<>
[[nodiscard]] std::optional<json> GeneralUtils::TryParse(std::string_view str) {
try {
return json::parse(str);
} catch (const std::exception& e) {
return std::nullopt;
} catch (...) {
return std::nullopt;
}
}
#if !(__GNUC__ >= 11 || _MSC_VER >= 1924)
// MacOS floating-point parse function specializations

View File

@@ -3,7 +3,6 @@
// C++
#include <charconv>
#include <cstdint>
#include <cstring>
#include <ctime>
#include <functional>
#include <optional>
@@ -146,7 +145,7 @@ namespace GeneralUtils {
template <typename... Bases>
struct overload : Bases... {
using is_transparent = void;
using Bases::operator()...;
using Bases::operator() ... ;
};
struct char_pointer_hash {
@@ -161,26 +160,6 @@ namespace GeneralUtils {
char_pointer_hash
>;
/**
* A convenience wrapper around std::memcpy that creates a new object
* from the value representation of the provided bytes.Use when
* std::bit_cast is not applicable.
* @warning All restrictions of std::memcpy still apply. Accessing
* outside of the source object is undefined behavior.
* @param from The source of the value representation
* @returns A new object with the given value representation
*/
template <typename To, typename From>
[[nodiscard]]
inline To FromBitsUnchecked(const From* from) noexcept
requires (std::is_trivially_copyable_v<To>
&& std::is_trivially_copyable_v<From>)
{
To to{};
std::memcpy(&to, from, sizeof(To));
return to;
}
// Concept constraining to enum types
template <typename T>
concept Enum = std::is_enum_v<T>;
@@ -222,6 +201,10 @@ namespace GeneralUtils {
return isParsed ? static_cast<T>(result) : std::optional<T>{};
}
template<typename T>
requires(!Numeric<T>)
[[nodiscard]] std::optional<T> TryParse(std::string_view str);
#if !(__GNUC__ >= 11 || _MSC_VER >= 1924)
// MacOS floating-point parse helper function specializations

View File

@@ -6,6 +6,9 @@
#include "zlib.h"
constexpr uint32_t CRC32_INIT = 0xFFFFFFFF;
constexpr auto NULL_TERMINATOR = std::string_view{"\0\0\0", 4};
AssetManager::AssetManager(const std::filesystem::path& path) {
if (!std::filesystem::is_directory(path)) {
throw std::runtime_error("Attempted to load asset bundle (" + path.string() + ") however it is not a valid directory.");
@@ -18,12 +21,20 @@ AssetManager::AssetManager(const std::filesystem::path& path) {
m_RootPath = m_Path;
m_ResPath = (m_Path / "client" / "res");
} else if (std::filesystem::exists(m_Path / ".." / "versions") && std::filesystem::exists(m_Path / "res")) {
} else if (std::filesystem::exists(m_Path / "res" / "pack")) {
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 / "..");
m_ResPath = (m_Path / "res");
} else if (std::filesystem::exists(m_Path / "pack") && std::filesystem::exists(m_Path / ".." / ".." / "versions")) {
} else if (std::filesystem::exists(m_Path / "pack")) {
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 / ".." / "..");
@@ -48,6 +59,7 @@ AssetManager::AssetManager(const std::filesystem::path& path) {
break;
}
case eAssetBundleType::None:
[[fallthrough]];
case eAssetBundleType::Unpacked: {
break;
}
@@ -55,19 +67,10 @@ AssetManager::AssetManager(const std::filesystem::path& path) {
}
void AssetManager::LoadPackIndex() {
m_PackIndex = new PackIndex(m_RootPath);
m_PackIndex = PackIndex(m_RootPath);
}
std::filesystem::path AssetManager::GetResPath() {
return m_ResPath;
}
eAssetBundleType AssetManager::GetAssetBundleType() {
return m_AssetBundleType;
}
bool AssetManager::HasFile(const char* name) {
auto fixedName = std::string(name);
bool AssetManager::HasFile(std::string fixedName) const {
std::transform(fixedName.begin(), fixedName.end(), fixedName.begin(), [](uint8_t c) { return std::tolower(c); });
// Special case for unpacked client have BrickModels in upper case
@@ -81,8 +84,7 @@ bool AssetManager::HasFile(const char* name) {
std::replace(fixedName.begin(), fixedName.end(), '/', '\\');
if (fixedName.rfind("client\\res\\", 0) != 0) fixedName = "client\\res\\" + fixedName;
uint32_t crc = crc32b(0xFFFFFFFF, reinterpret_cast<uint8_t*>(const_cast<char*>(fixedName.c_str())), fixedName.size());
crc = crc32b(crc, reinterpret_cast<Bytef*>(const_cast<char*>("\0\0\0\0")), 4);
const auto crc = crc32b(crc32b(CRC32_INIT, fixedName), NULL_TERMINATOR);
for (const auto& item : this->m_PackIndex->GetPackFileIndices()) {
if (item.m_Crc == crc) {
@@ -93,8 +95,7 @@ bool AssetManager::HasFile(const char* name) {
return false;
}
bool AssetManager::GetFile(const char* name, char** data, uint32_t* len) {
auto fixedName = std::string(name);
bool AssetManager::GetFile(std::string fixedName, char** data, uint32_t* len) const {
std::transform(fixedName.begin(), fixedName.end(), fixedName.begin(), [](uint8_t c) { return std::tolower(c); });
std::replace(fixedName.begin(), fixedName.end(), '\\', '/'); // On the off chance someone has the wrong slashes, force forward slashes
@@ -129,8 +130,7 @@ bool AssetManager::GetFile(const char* name, char** data, uint32_t* len) {
fixedName = "client\\res\\" + fixedName;
}
int32_t packIndex = -1;
uint32_t crc = crc32b(0xFFFFFFFF, reinterpret_cast<uint8_t*>(const_cast<char*>(fixedName.c_str())), fixedName.size());
crc = crc32b(crc, reinterpret_cast<Bytef*>(const_cast<char*>("\0\0\0\0")), 4);
auto crc = crc32b(crc32b(CRC32_INIT, fixedName), NULL_TERMINATOR);
for (const auto& item : this->m_PackIndex->GetPackFileIndices()) {
if (item.m_Crc == crc) {
@@ -144,15 +144,13 @@ bool AssetManager::GetFile(const char* name, char** data, uint32_t* len) {
return false;
}
auto packs = this->m_PackIndex->GetPacks();
auto* pack = packs.at(packIndex);
bool success = pack->ReadFileFromPack(crc, data, len);
const auto& pack = this->m_PackIndex->GetPacks().at(packIndex);
const bool success = pack.ReadFileFromPack(crc, data, len);
return success;
}
AssetStream AssetManager::GetFile(const char* name) {
AssetStream AssetManager::GetFile(const char* name) const {
char* buf; uint32_t len;
bool success = this->GetFile(name, &buf, &len);
@@ -160,23 +158,15 @@ AssetStream AssetManager::GetFile(const char* name) {
return AssetStream(buf, len, success);
}
uint32_t AssetManager::crc32b(uint32_t base, uint8_t* message, size_t l) {
size_t i, j;
uint32_t crc, msb;
crc = base;
for (i = 0; i < l; i++) {
uint32_t AssetManager::crc32b(uint32_t crc, const std::string_view message) {
for (const auto byte : message) {
// xor next byte to upper bits of crc
crc ^= (static_cast<unsigned int>(message[i]) << 24);
for (j = 0; j < 8; j++) { // Do eight times.
msb = crc >> 31;
crc ^= (static_cast<uint32_t>(std::bit_cast<uint8_t>(byte)) << 24);
for (size_t _ = 0; _ < 8; _++) { // Do eight times.
const uint32_t msb = crc >> 31;
crc <<= 1;
crc ^= (0 - msb) & 0x04C11DB7;
}
}
return crc; // don't complement crc on output
}
AssetManager::~AssetManager() {
delete m_PackIndex;
}

View File

@@ -54,7 +54,6 @@ struct AssetStream : std::istream {
}
operator bool() {
// NEED TO FIX THIS
return reinterpret_cast<AssetMemoryBuffer*>(rdbuf())->m_Success;
}
};
@@ -62,23 +61,32 @@ struct AssetStream : std::istream {
class AssetManager {
public:
AssetManager(const std::filesystem::path& path);
~AssetManager();
std::filesystem::path GetResPath();
eAssetBundleType GetAssetBundleType();
[[nodiscard]]
const std::filesystem::path& GetResPath() const {
return m_ResPath;
}
[[nodiscard]]
eAssetBundleType GetAssetBundleType() const {
return m_AssetBundleType;
}
bool HasFile(const char* name);
bool GetFile(const char* name, char** data, uint32_t* len);
AssetStream GetFile(const char* name);
[[nodiscard]]
bool HasFile(std::string name) const;
[[nodiscard]]
bool GetFile(std::string name, char** data, uint32_t* len) const;
[[nodiscard]]
AssetStream GetFile(const char* name) const;
private:
void LoadPackIndex();
// Modified crc algorithm (mpeg2)
// Reference: https://stackoverflow.com/questions/54339800/how-to-modify-crc-32-to-crc-32-mpeg-2
inline uint32_t crc32b(uint32_t base, uint8_t* message, size_t l);
bool m_SuccessfullyLoaded;
static inline uint32_t crc32b(uint32_t crc, std::string_view message);
std::filesystem::path m_Path;
std::filesystem::path m_RootPath;
@@ -86,5 +94,5 @@ private:
eAssetBundleType m_AssetBundleType = eAssetBundleType::None;
PackIndex* m_PackIndex;
std::optional<PackIndex> m_PackIndex;
};

View File

@@ -21,19 +21,20 @@ Pack::Pack(const std::filesystem::path& filePath) {
m_FileStream.seekg(recordCountPos, std::ios::beg);
BinaryIO::BinaryRead<uint32_t>(m_FileStream, m_RecordCount);
uint32_t recordCount = 0;
BinaryIO::BinaryRead<uint32_t>(m_FileStream, recordCount);
for (int i = 0; i < m_RecordCount; i++) {
m_Records.reserve(recordCount);
std::generate_n(std::back_inserter(m_Records), recordCount, [&] {
PackRecord record;
BinaryIO::BinaryRead<PackRecord>(m_FileStream, record);
m_Records.push_back(record);
}
return record;
});
m_FileStream.close();
}
bool Pack::HasFile(uint32_t crc) {
bool Pack::HasFile(const uint32_t crc) const {
for (const auto& record : m_Records) {
if (record.m_Crc == crc) {
return true;
@@ -43,7 +44,7 @@ bool Pack::HasFile(uint32_t crc) {
return false;
}
bool Pack::ReadFileFromPack(uint32_t crc, char** data, uint32_t* len) {
bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) const {
// Time for some wacky C file reading for speed reasons
PackRecord pkRecord{};

View File

@@ -24,16 +24,17 @@ struct PackRecord {
class Pack {
public:
Pack(const std::filesystem::path& filePath);
~Pack() = default;
bool HasFile(uint32_t crc);
bool ReadFileFromPack(uint32_t crc, char** data, uint32_t* len);
[[nodiscard]]
bool HasFile(uint32_t crc) const;
[[nodiscard]]
bool ReadFileFromPack(uint32_t crc, char** data, uint32_t* len) const;
private:
std::ifstream m_FileStream;
std::filesystem::path m_FilePath;
char m_Version[7];
uint32_t m_RecordCount;
std::vector<PackRecord> m_Records;
};

View File

@@ -6,38 +6,32 @@
PackIndex::PackIndex(const std::filesystem::path& filePath) {
m_FileStream = std::ifstream(filePath / "versions" / "primary.pki", std::ios::in | std::ios::binary);
uint32_t packPathCount = 0;
BinaryIO::BinaryRead<uint32_t>(m_FileStream, m_Version);
BinaryIO::BinaryRead<uint32_t>(m_FileStream, m_PackPathCount);
BinaryIO::BinaryRead<uint32_t>(m_FileStream, packPathCount);
m_PackPaths.resize(m_PackPathCount);
m_PackPaths.resize(packPathCount);
for (auto& item : m_PackPaths) {
BinaryIO::ReadString<uint32_t>(m_FileStream, item, BinaryIO::ReadType::String);
}
BinaryIO::BinaryRead<uint32_t>(m_FileStream, m_PackFileIndexCount);
uint32_t packFileIndexCount = 0;
BinaryIO::BinaryRead<uint32_t>(m_FileStream, packFileIndexCount);
for (int i = 0; i < m_PackFileIndexCount; i++) {
m_PackFileIndices.reserve(packFileIndexCount);
std::generate_n(std::back_inserter(m_PackFileIndices), packFileIndexCount, [&] {
PackFileIndex packFileIndex;
BinaryIO::BinaryRead<PackFileIndex>(m_FileStream, packFileIndex);
m_PackFileIndices.push_back(packFileIndex);
}
return packFileIndex;
});
LOG("Loaded pack catalog with %i pack files and %i files", m_PackPaths.size(), m_PackFileIndices.size());
m_Packs.reserve(m_PackPaths.size());
for (auto& item : m_PackPaths) {
std::replace(item.begin(), item.end(), '\\', '/');
auto* pack = new Pack(filePath / item);
m_Packs.push_back(pack);
m_Packs.emplace_back(filePath / item);
}
m_FileStream.close();
}
PackIndex::~PackIndex() {
for (const auto* item : m_Packs) {
delete item;
}
}

View File

@@ -21,20 +21,23 @@ struct PackFileIndex {
class PackIndex {
public:
PackIndex(const std::filesystem::path& filePath);
~PackIndex();
const std::vector<std::string>& GetPackPaths() { return m_PackPaths; }
const std::vector<PackFileIndex>& GetPackFileIndices() { return m_PackFileIndices; }
const std::vector<Pack*>& GetPacks() { return m_Packs; }
[[nodiscard]]
const std::vector<std::string>& GetPackPaths() const { return m_PackPaths; }
[[nodiscard]]
const std::vector<PackFileIndex>& GetPackFileIndices() const { return m_PackFileIndices; }
[[nodiscard]]
const std::vector<Pack>& GetPacks() const { return m_Packs; }
private:
std::ifstream m_FileStream;
uint32_t m_Version;
uint32_t m_PackPathCount;
std::vector<std::string> m_PackPaths;
uint32_t m_PackFileIndexCount;
std::vector<PackFileIndex> m_PackFileIndices;
std::vector<Pack*> m_Packs;
std::vector<Pack> m_Packs;
};

View File

@@ -5,8 +5,7 @@
namespace MessageType {
enum class World : uint32_t {
INVALID = 0,
VALIDATION, // Session info
VALIDATION = 1, // Session info
CHARACTER_LIST_REQUEST,
CHARACTER_CREATE_REQUEST,
LOGIN_REQUEST, // Character selected

View File

@@ -7,7 +7,8 @@ enum class eConnectionType : uint16_t {
CHAT,
WORLD = 4,
CLIENT,
MASTER
MASTER,
UNKNOWN
};
#endif //!__ECONNECTIONTYPE__H__

View File

@@ -0,0 +1,26 @@
#ifndef __EHTTPMETHODS__H__
#define __EHTTPMETHODS__H__
#ifdef DARKFLAME_PLATFORM_WIN32
#pragma push_macro("DELETE")
#undef DELETE
#endif
enum class eHTTPMethod {
GET,
POST,
PUT,
DELETE,
HEAD,
CONNECT,
OPTIONS,
TRACE,
PATCH,
INVALID
};
#ifdef DARKFLAME_PLATFORM_WIN32
#pragma pop_macro("DELETE")
#endif
#endif // __EHTTPMETHODS__H__

View File

@@ -0,0 +1,72 @@
#ifndef __EHTTPSTATUSCODE__H__
#define __EHTTPSTATUSCODE__H__
#include <cstdint>
// verbose list of http codes
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
enum class eHTTPStatusCode : uint32_t {
CONTINUE = 100,
SWITCHING_PROTOCOLS = 101,
PROCESSING = 102,
EARLY_HINTS = 103,
OK = 200,
CREATED = 201,
ACCEPTED = 202,
NON_AUTHORITATIVE_INFORMATION = 203,
NO_CONTENT = 204,
RESET_CONTENT = 205,
PARTIAL_CONTENT = 206,
MULTI_STATUS = 207,
ALREADY_REPORTED = 208,
IM_USED = 226,
MULTIPLE_CHOICES = 300,
MOVED_PERMANENTLY = 301,
FOUND = 302,
SEE_OTHER = 303,
NOT_MODIFIED = 304,
USE_PROXY = 305,
SWITCH_PROXY = 306,
TEMPORARY_REDIRECT = 307,
PERMANENT_REDIRECT = 308,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
PAYMENT_REQUIRED = 402,
FORBIDDEN = 403,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED = 405,
NOT_ACCEPTABLE = 406,
PROXY_AUTHENTICATION_REQUIRED = 407,
REQUEST_TIMEOUT = 408,
CONFLICT = 409,
GONE = 410,
LENGTH_REQUIRED = 411,
PRECONDITION_FAILED = 412,
PAYLOAD_TOO_LARGE = 413,
URI_TOO_LONG = 414,
UNSUPPORTED_MEDIA_TYPE = 415,
RANGE_NOT_SATISFIABLE = 416,
EXPECTATION_FAILED = 417,
I_AM_A_TEAPOT = 418,
MISDIRECTED_REQUEST = 421,
UNPROCESSABLE_ENTITY = 422,
LOCKED = 423,
FAILED_DEPENDENCY = 424,
UPGRADE_REQUIRED = 426,
PRECONDITION_REQUIRED = 428,
TOO_MANY_REQUESTS = 429,
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
GATEWAY_TIMEOUT = 504,
HTTP_VERSION_NOT_SUPPORTED = 505,
VARIANT_ALSO_NEGOTIATES = 506,
INSUFFICIENT_STORAGE = 507,
LOOP_DETECTED = 508,
NOT_EXTENDED = 510,
NETWORK_AUTHENTICATION_REQUIRED = 511
};
#endif // !__EHTTPSTATUSCODE__H__

View File

@@ -28,7 +28,8 @@ enum eInventoryType : uint32_t {
DONATION,
VAULT_MODELS,
ITEM_SETS, //internal, technically this is BankBehaviors.
INVALID // made up, for internal use!!!, Technically this called the ALL inventory.
INVALID, // made up, for internal use!!!, Technically this called the ALL inventory.
ALL, // Use this to search all inventories instead of a specific one.
};
class InventoryType {

View File

@@ -0,0 +1,38 @@
#include "CDPlayerFlagsTable.h"
#include "CDClientDatabase.h"
namespace CDPlayerFlagsTable {
Table entries;
void ReadEntry(CppSQLite3Query& table) {
Entry entry;
entry.sessionOnly = table.getIntField("SessionOnly") == 1;
entry.onlySetByServer = table.getIntField("OnlySetByServer") == 1;
entry.sessionZoneOnly = table.getIntField("SessionZoneOnly") == 1;
entries[table.getIntField("id")] = entry;
}
void LoadValuesFromDatabase() {
auto table = CDClientDatabase::ExecuteQuery("SELECT * FROM PlayerFlags;");
if (!table.eof()) {
do {
ReadEntry(table);
} while (!table.nextRow());
}
}
const std::optional<Entry> GetEntry(const FlagId flagId) {
if (!entries.contains(flagId)) {
auto table = CDClientDatabase::CreatePreppedStmt("SELECT * FROM PlayerFlags WHERE id = ?;");
table.bind(1, static_cast<int>(flagId));
auto result = table.execQuery();
if (!result.eof()) {
ReadEntry(result);
}
}
return entries[flagId];
}
}

View File

@@ -0,0 +1,21 @@
#ifndef CDPLAYERFLAGSTABLE_H
#define CDPLAYERFLAGSTABLE_H
#include <map>
#include <optional>
namespace CDPlayerFlagsTable {
struct Entry {
bool sessionOnly{};
bool onlySetByServer{};
bool sessionZoneOnly{};
};
using FlagId = uint32_t;
using Table = std::map<FlagId, std::optional<Entry>>;
void LoadValuesFromDatabase();
const std::optional<Entry> GetEntry(const FlagId flagId);
};
#endif //!CDPLAYERFLAGSTABLE_H

View File

@@ -25,6 +25,7 @@ set(DDATABASE_CDCLIENTDATABASE_CDCLIENTTABLES_SOURCES "CDActivitiesTable.cpp"
"CDObjectsTable.cpp"
"CDPetComponentTable.cpp"
"CDPackageComponentTable.cpp"
"CDPlayerFlagsTable.cpp"
"CDPhysicsComponentTable.cpp"
"CDPropertyEntranceComponentTable.cpp"
"CDPropertyTemplateTable.cpp"

View File

@@ -44,6 +44,8 @@ public:
// Updates the given character ids last login to be right now.
virtual void UpdateLastLoggedInCharacter(const uint32_t characterId) = 0;
virtual bool IsNameInUse(const std::string_view name) = 0;
};
#endif //!__ICHARINFO__H__

View File

@@ -8,27 +8,10 @@
#include "dCommonVars.h"
#include "NiQuaternion.h"
#include "NiPoint3.h"
#include "MailInfo.h"
class IMail {
public:
struct MailInfo {
std::string senderUsername;
std::string recipient;
std::string subject;
std::string body;
uint64_t id{};
uint32_t senderId{};
uint32_t receiverId{};
uint64_t timeSent{};
bool wasRead{};
struct {
LWOOBJID itemID{};
int32_t itemCount{};
LOT itemLOT{};
LWOOBJID itemSubkey{};
};
};
// Insert a new mail into the database.
virtual void InsertNewMail(const MailInfo& mail) = 0;

View File

@@ -9,10 +9,11 @@ public:
struct MasterInfo {
std::string ip;
uint32_t port{};
std::string password{};
};
// Set the master server ip and port.
virtual void SetMasterIp(const std::string_view ip, const uint32_t port) = 0;
virtual void SetMasterInfo(const MasterInfo& info) = 0;
// Get the master server info.
virtual std::optional<MasterInfo> GetMasterInfo() = 0;

View File

@@ -79,14 +79,14 @@ public:
void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override;
void InsertNewBugReport(const IBugReports::Info& info) override;
void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override;
void InsertNewMail(const IMail::MailInfo& mail) override;
void InsertNewMail(const MailInfo& mail) override;
void InsertNewUgcModel(
std::istringstream& sd0Data,
const uint32_t blueprintId,
const uint32_t accountId,
const uint32_t characterId) override;
std::vector<IMail::MailInfo> GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) override;
std::optional<IMail::MailInfo> GetMail(const uint64_t mailId) override;
std::vector<MailInfo> GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) override;
std::optional<MailInfo> GetMail(const uint64_t mailId) override;
uint32_t GetUnreadMailCount(const uint32_t characterId) override;
void MarkMailRead(const uint64_t mailId) override;
void DeleteMail(const uint64_t mailId) override;
@@ -96,7 +96,7 @@ public:
void UpdateAccountBan(const uint32_t accountId, const bool banned) override;
void UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) override;
void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) override;
void SetMasterIp(const std::string_view ip, const uint32_t port) override;
void SetMasterInfo(const IServers::MasterInfo& info) override;
std::optional<uint32_t> GetCurrentPersistentId() override;
void InsertDefaultPersistentId() override;
void UpdatePersistentId(const uint32_t id) override;
@@ -124,8 +124,9 @@ public:
void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) override;
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> characterId) override;
void DeleteUgcBuild(const LWOOBJID bigId) override;
sql::PreparedStatement* CreatePreppedStmt(const std::string& query);
uint32_t GetAccountCount() override;
bool IsNameInUse(const std::string_view name) override;
sql::PreparedStatement* CreatePreppedStmt(const std::string& query);
private:
// Generic query functions that can be used for any query.

View File

@@ -76,3 +76,9 @@ void MySQLDatabase::SetPendingCharacterName(const uint32_t characterId, const st
void MySQLDatabase::UpdateLastLoggedInCharacter(const uint32_t characterId) {
ExecuteUpdate("UPDATE charinfo SET last_login = ? WHERE id = ? LIMIT 1", static_cast<uint32_t>(time(NULL)), characterId);
}
bool MySQLDatabase::IsNameInUse(const std::string_view name) {
auto result = ExecuteSelect("SELECT name FROM charinfo WHERE name = ? or pending_name = ? LIMIT 1;", name, name);
return result->next();
}

View File

@@ -1,6 +1,7 @@
#include "MySQLDatabase.h"
void MySQLDatabase::InsertNewMail(const IMail::MailInfo& mail) {
void MySQLDatabase::InsertNewMail(const MailInfo& mail) {
ExecuteInsert(
"INSERT INTO `mail` "
"(`sender_id`, `sender_name`, `receiver_id`, `receiver_name`, `time_sent`, `subject`, `body`, `attachment_id`, `attachment_lot`, `attachment_subkey`, `attachment_count`, `was_read`)"
@@ -18,17 +19,17 @@ void MySQLDatabase::InsertNewMail(const IMail::MailInfo& mail) {
mail.itemCount);
}
std::vector<IMail::MailInfo> MySQLDatabase::GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) {
std::vector<MailInfo> MySQLDatabase::GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) {
auto res = ExecuteSelect(
"SELECT id, subject, body, sender_name, attachment_id, attachment_lot, attachment_subkey, attachment_count, was_read, time_sent"
" FROM mail WHERE receiver_id=? limit ?;",
characterId, numberOfMail);
std::vector<IMail::MailInfo> toReturn;
std::vector<MailInfo> toReturn;
toReturn.reserve(res->rowsCount());
while (res->next()) {
IMail::MailInfo mail;
MailInfo mail;
mail.id = res->getUInt64("id");
mail.subject = res->getString("subject").c_str();
mail.body = res->getString("body").c_str();
@@ -46,14 +47,14 @@ std::vector<IMail::MailInfo> MySQLDatabase::GetMailForPlayer(const uint32_t char
return toReturn;
}
std::optional<IMail::MailInfo> MySQLDatabase::GetMail(const uint64_t mailId) {
std::optional<MailInfo> MySQLDatabase::GetMail(const uint64_t mailId) {
auto res = ExecuteSelect("SELECT attachment_lot, attachment_count FROM mail WHERE id=? LIMIT 1;", mailId);
if (!res->next()) {
return std::nullopt;
}
IMail::MailInfo toReturn;
MailInfo toReturn;
toReturn.itemLOT = res->getInt("attachment_lot");
toReturn.itemCount = res->getInt("attachment_count");

View File

@@ -56,6 +56,7 @@ std::optional<IProperty::PropertyEntranceResult> MySQLDatabase::GetProperties(co
params.playerId
);
if (count->next()) {
if (!result) result = IProperty::PropertyEntranceResult();
result->totalEntriesMatchingQuery = count->getUInt("count");
}
} else {
@@ -109,11 +110,13 @@ std::optional<IProperty::PropertyEntranceResult> MySQLDatabase::GetProperties(co
params.playerSort
);
if (count->next()) {
if (!result) result = IProperty::PropertyEntranceResult();
result->totalEntriesMatchingQuery = count->getUInt("count");
}
}
while (properties->next()) {
if (!result) result = IProperty::PropertyEntranceResult();
auto& entry = result->entries.emplace_back();
entry.id = properties->getUInt64("id");
entry.ownerId = properties->getUInt64("owner_id");

View File

@@ -1,14 +1,14 @@
#include "MySQLDatabase.h"
void MySQLDatabase::SetMasterIp(const std::string_view ip, const uint32_t port) {
void MySQLDatabase::SetMasterInfo(const MasterInfo& info) {
// We only want our 1 entry anyways, so we can just delete all and reinsert the one we want
// since it would be two queries anyways.
ExecuteDelete("TRUNCATE TABLE servers;");
ExecuteInsert("INSERT INTO `servers` (`name`, `ip`, `port`, `state`, `version`) VALUES ('master', ?, ?, 0, 171022)", ip, port);
ExecuteInsert("INSERT INTO `servers` (`name`, `ip`, `port`, `state`, `version`, `master_password`) VALUES ('master', ?, ?, 0, 171022, ?)", info.ip, info.port, info.password);
}
std::optional<IServers::MasterInfo> MySQLDatabase::GetMasterInfo() {
auto result = ExecuteSelect("SELECT ip, port FROM servers WHERE name='master' LIMIT 1;");
auto result = ExecuteSelect("SELECT ip, port, master_password FROM servers WHERE name='master' LIMIT 1;");
if (!result->next()) {
return std::nullopt;
@@ -18,6 +18,7 @@ std::optional<IServers::MasterInfo> MySQLDatabase::GetMasterInfo() {
toReturn.ip = result->getString("ip").c_str();
toReturn.port = result->getInt("port");
toReturn.password = result->getString("master_password").c_str();
return toReturn;
}

View File

@@ -77,14 +77,14 @@ public:
void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override;
void InsertNewBugReport(const IBugReports::Info& info) override;
void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override;
void InsertNewMail(const IMail::MailInfo& mail) override;
void InsertNewMail(const MailInfo& mail) override;
void InsertNewUgcModel(
std::istringstream& sd0Data,
const uint32_t blueprintId,
const uint32_t accountId,
const uint32_t characterId) override;
std::vector<IMail::MailInfo> GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) override;
std::optional<IMail::MailInfo> GetMail(const uint64_t mailId) override;
std::vector<MailInfo> GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) override;
std::optional<MailInfo> GetMail(const uint64_t mailId) override;
uint32_t GetUnreadMailCount(const uint32_t characterId) override;
void MarkMailRead(const uint64_t mailId) override;
void DeleteMail(const uint64_t mailId) override;
@@ -94,7 +94,7 @@ public:
void UpdateAccountBan(const uint32_t accountId, const bool banned) override;
void UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) override;
void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) override;
void SetMasterIp(const std::string_view ip, const uint32_t port) override;
void SetMasterInfo(const IServers::MasterInfo& info) override;
std::optional<uint32_t> GetCurrentPersistentId() override;
void InsertDefaultPersistentId() override;
void UpdatePersistentId(const uint32_t id) override;
@@ -123,6 +123,7 @@ public:
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> characterId) override;
void DeleteUgcBuild(const LWOOBJID bigId) override;
uint32_t GetAccountCount() override;
bool IsNameInUse(const std::string_view name) override;
private:
CppSQLite3Statement CreatePreppedStmt(const std::string& query);

View File

@@ -77,3 +77,9 @@ void SQLiteDatabase::SetPendingCharacterName(const uint32_t characterId, const s
void SQLiteDatabase::UpdateLastLoggedInCharacter(const uint32_t characterId) {
ExecuteUpdate("UPDATE charinfo SET last_login = ? WHERE id = ?;", static_cast<uint32_t>(time(NULL)), characterId);
}
bool SQLiteDatabase::IsNameInUse(const std::string_view name) {
auto [_, result] = ExecuteSelect("SELECT name FROM charinfo WHERE name = ? or pending_name = ? LIMIT 1;", name, name);
return !result.eof();
}

View File

@@ -1,6 +1,6 @@
#include "SQLiteDatabase.h"
void SQLiteDatabase::InsertNewMail(const IMail::MailInfo& mail) {
void SQLiteDatabase::InsertNewMail(const MailInfo& mail) {
ExecuteInsert(
"INSERT INTO `mail` "
"(`sender_id`, `sender_name`, `receiver_id`, `receiver_name`, `time_sent`, `subject`, `body`, `attachment_id`, `attachment_lot`, `attachment_subkey`, `attachment_count`, `was_read`)"
@@ -18,16 +18,16 @@ void SQLiteDatabase::InsertNewMail(const IMail::MailInfo& mail) {
mail.itemCount);
}
std::vector<IMail::MailInfo> SQLiteDatabase::GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) {
std::vector<MailInfo> SQLiteDatabase::GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) {
auto [_, res] = ExecuteSelect(
"SELECT id, subject, body, sender_name, attachment_id, attachment_lot, attachment_subkey, attachment_count, was_read, time_sent"
" FROM mail WHERE receiver_id=? limit ?;",
characterId, numberOfMail);
std::vector<IMail::MailInfo> toReturn;
std::vector<MailInfo> toReturn;
while (!res.eof()) {
IMail::MailInfo mail;
MailInfo mail;
mail.id = res.getInt64Field("id");
mail.subject = res.getStringField("subject");
mail.body = res.getStringField("body");
@@ -46,14 +46,14 @@ std::vector<IMail::MailInfo> SQLiteDatabase::GetMailForPlayer(const uint32_t cha
return toReturn;
}
std::optional<IMail::MailInfo> SQLiteDatabase::GetMail(const uint64_t mailId) {
std::optional<MailInfo> SQLiteDatabase::GetMail(const uint64_t mailId) {
auto [_, res] = ExecuteSelect("SELECT attachment_lot, attachment_count FROM mail WHERE id=? LIMIT 1;", mailId);
if (res.eof()) {
return std::nullopt;
}
IMail::MailInfo toReturn;
MailInfo toReturn;
toReturn.itemLOT = res.getIntField("attachment_lot");
toReturn.itemCount = res.getIntField("attachment_count");

View File

@@ -1,14 +1,14 @@
#include "SQLiteDatabase.h"
void SQLiteDatabase::SetMasterIp(const std::string_view ip, const uint32_t port) {
void SQLiteDatabase::SetMasterInfo(const MasterInfo& info) {
// We only want our 1 entry anyways, so we can just delete all and reinsert the one we want
// since it would be two queries anyways.
ExecuteDelete("DELETE FROM servers;");
ExecuteInsert("INSERT INTO `servers` (`name`, `ip`, `port`, `state`, `version`) VALUES ('master', ?, ?, 0, 171022)", ip, port);
ExecuteInsert("INSERT INTO `servers` (`name`, `ip`, `port`, `state`, `version`, `master_password`) VALUES ('master', ?, ?, 0, 171022, ?)", info.ip, info.port, info.password);
}
std::optional<IServers::MasterInfo> SQLiteDatabase::GetMasterInfo() {
auto [_, result] = ExecuteSelect("SELECT ip, port FROM servers WHERE name='master' LIMIT 1;");
auto [_, result] = ExecuteSelect("SELECT ip, port, master_password FROM servers WHERE name='master' LIMIT 1;");
if (result.eof()) {
return std::nullopt;
@@ -18,6 +18,7 @@ std::optional<IServers::MasterInfo> SQLiteDatabase::GetMasterInfo() {
toReturn.ip = result.getStringField("ip");
toReturn.port = result.getIntField("port");
toReturn.password = result.getStringField("master_password");
return toReturn;
}

View File

@@ -184,7 +184,7 @@ void TestSQLDatabase::InsertCheatDetection(const IPlayerCheatDetections::Info& i
}
void TestSQLDatabase::InsertNewMail(const IMail::MailInfo& mail) {
void TestSQLDatabase::InsertNewMail(const MailInfo& mail) {
}
@@ -192,11 +192,11 @@ void TestSQLDatabase::InsertNewUgcModel(std::istringstream& sd0Data, const uint3
}
std::vector<IMail::MailInfo> TestSQLDatabase::GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) {
std::vector<MailInfo> TestSQLDatabase::GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) {
return {};
}
std::optional<IMail::MailInfo> TestSQLDatabase::GetMail(const uint64_t mailId) {
std::optional<MailInfo> TestSQLDatabase::GetMail(const uint64_t mailId) {
return {};
}
@@ -236,7 +236,7 @@ void TestSQLDatabase::InsertNewAccount(const std::string_view username, const st
}
void TestSQLDatabase::SetMasterIp(const std::string_view ip, const uint32_t port) {
void TestSQLDatabase::SetMasterInfo(const IServers::MasterInfo& info) {
}

View File

@@ -56,14 +56,14 @@ class TestSQLDatabase : public GameDatabase {
void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override;
void InsertNewBugReport(const IBugReports::Info& info) override;
void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override;
void InsertNewMail(const IMail::MailInfo& mail) override;
void InsertNewMail(const MailInfo& mail) override;
void InsertNewUgcModel(
std::istringstream& sd0Data,
const uint32_t blueprintId,
const uint32_t accountId,
const uint32_t characterId) override;
std::vector<IMail::MailInfo> GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) override;
std::optional<IMail::MailInfo> GetMail(const uint64_t mailId) override;
std::vector<MailInfo> GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) override;
std::optional<MailInfo> GetMail(const uint64_t mailId) override;
uint32_t GetUnreadMailCount(const uint32_t characterId) override;
void MarkMailRead(const uint64_t mailId) override;
void DeleteMail(const uint64_t mailId) override;
@@ -73,7 +73,7 @@ class TestSQLDatabase : public GameDatabase {
void UpdateAccountBan(const uint32_t accountId, const bool banned) override;
void UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) override;
void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) override;
void SetMasterIp(const std::string_view ip, const uint32_t port) override;
void SetMasterInfo(const IServers::MasterInfo& info) override;
std::optional<uint32_t> GetCurrentPersistentId() override;
void InsertDefaultPersistentId() override;
void UpdatePersistentId(const uint32_t id) override;
@@ -102,6 +102,8 @@ class TestSQLDatabase : public GameDatabase {
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> characterId) override {};
void DeleteUgcBuild(const LWOOBJID bigId) override {};
uint32_t GetAccountCount() override { return 0; };
bool IsNameInUse(const std::string_view name) override { return false; };
};
#endif //!TESTSQLDATABASE_H

View File

@@ -16,7 +16,7 @@ include_directories(
add_library(dGameBase OBJECT ${DGAME_SOURCES})
target_precompile_headers(dGameBase PRIVATE ${HEADERS_DGAME})
target_include_directories(dGameBase PUBLIC "." "dEntity"
PRIVATE "dComponents" "dGameMessages" "dBehaviors" "dMission" "dUtilities" "dInventory"
PRIVATE "dComponents" "dGameMessages" "dBehaviors" "dMission" "dUtilities" "dInventory" "dCinema"
$<TARGET_PROPERTY:dPropertyBehaviors,INTERFACE_INCLUDE_DIRECTORIES>
"${PROJECT_SOURCE_DIR}/dCommon"
"${PROJECT_SOURCE_DIR}/dCommon/dEnums"
@@ -40,6 +40,7 @@ add_subdirectory(dInventory)
add_subdirectory(dMission)
add_subdirectory(dPropertyBehaviors)
add_subdirectory(dUtilities)
add_subdirectory(dCinema)
add_library(dGame STATIC
$<TARGET_OBJECTS:dGameBase>
@@ -51,6 +52,7 @@ add_library(dGame STATIC
$<TARGET_OBJECTS:dMission>
$<TARGET_OBJECTS:dPropertyBehaviors>
$<TARGET_OBJECTS:dUtilities>
$<TARGET_OBJECTS:dCinema>
)
target_link_libraries(dGame INTERFACE dNet)
target_include_directories(dGame INTERFACE
@@ -63,4 +65,5 @@ target_include_directories(dGame INTERFACE
$<TARGET_PROPERTY:dMission,INTERFACE_INCLUDE_DIRECTORIES>
$<TARGET_PROPERTY:dPropertyBehaviors,INTERFACE_INCLUDE_DIRECTORIES>
$<TARGET_PROPERTY:dUtilities,INTERFACE_INCLUDE_DIRECTORIES>
$<TARGET_PROPERTY:dCinema,INTERFACE_INCLUDE_DIRECTORIES>
)

View File

@@ -21,6 +21,7 @@
#include "eObjectBits.h"
#include "eGameMasterLevel.h"
#include "ePlayerFlag.h"
#include "CDPlayerFlagsTable.h"
Character::Character(uint32_t id, User* parentUser) {
//First load the name, etc:
@@ -203,6 +204,7 @@ void Character::DoQuickXMLDataParse() {
while (currentChild) {
const auto* temp = currentChild->Attribute("v");
const auto* id = currentChild->Attribute("id");
const auto* si = currentChild->Attribute("si");
if (temp && id) {
uint32_t index = 0;
uint64_t value = 0;
@@ -211,6 +213,9 @@ void Character::DoQuickXMLDataParse() {
value = std::stoull(temp);
m_PlayerFlags.insert(std::make_pair(index, value));
} else if (si) {
auto value = GeneralUtils::TryParse<uint32_t>(si);
if (value) m_SessionFlags.insert(value.value());
}
currentChild = currentChild->NextSiblingElement();
}
@@ -231,6 +236,12 @@ void Character::SetBuildMode(bool buildMode) {
}
void Character::SaveXMLToDatabase() {
// Check that we can actually _save_ before saving
if (!m_OurEntity) {
LOG("%i:%s didn't have an entity set while saving! CHARACTER WILL NOT BE SAVED!", this->GetID(), this->GetName().c_str());
return;
}
//For metrics, we'll record the time it took to save:
auto start = std::chrono::system_clock::now();
@@ -277,33 +288,19 @@ void Character::SaveXMLToDatabase() {
}
flags->DeleteChildren(); //Clear it if we have anything, so that we can fill it up again without dupes
for (std::pair<uint32_t, uint64_t> flag : m_PlayerFlags) {
auto* f = m_Doc.NewElement("f");
f->SetAttribute("id", flag.first);
//Because of the joy that is tinyxml2, it doesn't offer a function to set a uint64 as an attribute.
//Only signed 64-bits ints would work.
std::string v = std::to_string(flag.second);
f->SetAttribute("v", v.c_str());
flags->LinkEndChild(f);
for (const auto& [index, flagBucket] : m_PlayerFlags) {
auto* f = flags->InsertNewChildElement("f");
f->SetAttribute("id", index);
f->SetAttribute("v", flagBucket);
}
// Prevents the news feed from showing up on world transfers
if (GetPlayerFlag(ePlayerFlag::IS_NEWS_SCREEN_VISIBLE)) {
auto* s = m_Doc.NewElement("s");
s->SetAttribute("si", ePlayerFlag::IS_NEWS_SCREEN_VISIBLE);
flags->LinkEndChild(s);
for (const auto& sessionFlag : m_SessionFlags) {
auto* s = flags->InsertNewChildElement("s");
s->SetAttribute("si", sessionFlag);
}
SaveXmlRespawnCheckpoints();
//Call upon the entity to update our xmlDoc:
if (!m_OurEntity) {
LOG("%i:%s didn't have an entity set while saving! CHARACTER WILL NOT BE SAVED!", this->GetID(), this->GetName().c_str());
return;
}
m_OurEntity->UpdateXMLDoc(m_Doc);
WriteToDatabase();
@@ -323,8 +320,8 @@ void Character::SetIsNewLogin() {
while (currentChild) {
auto* nextChild = currentChild->NextSiblingElement();
if (currentChild->Attribute("si")) {
LOG("Removed session flag (%s) from character %i:%s, saving character to database", currentChild->Attribute("si"), GetID(), GetName().c_str());
flags->DeleteChild(currentChild);
LOG("Removed isLoggedIn flag from character %i:%s, saving character to database", GetID(), GetName().c_str());
WriteToDatabase();
}
currentChild = nextChild;
@@ -357,49 +354,64 @@ void Character::SetPlayerFlag(const uint32_t flagId, const bool value) {
}
}
// Calculate the index first
auto flagIndex = uint32_t(std::floor(flagId / 64));
const auto flagEntry = CDPlayerFlagsTable::GetEntry(flagId);
const auto shiftedValue = 1ULL << flagId % 64;
auto it = m_PlayerFlags.find(flagIndex);
// Check if flag index exists
if (it != m_PlayerFlags.end()) {
// Update the value
if (value) {
it->second |= shiftedValue;
} else {
it->second &= ~shiftedValue;
}
if (flagEntry && flagEntry->sessionOnly) {
if (value) m_SessionFlags.insert(flagId);
else m_SessionFlags.erase(flagId);
} else {
if (value) {
// Otherwise, insert the value
uint64_t flagValue = 0;
// Calculate the index first
auto flagIndex = uint32_t(std::floor(flagId / 64));
flagValue |= shiftedValue;
const auto shiftedValue = 1ULL << flagId % 64;
m_PlayerFlags.insert(std::make_pair(flagIndex, flagValue));
auto it = m_PlayerFlags.find(flagIndex);
// Check if flag index exists
if (it != m_PlayerFlags.end()) {
// Update the value
if (value) {
it->second |= shiftedValue;
} else {
it->second &= ~shiftedValue;
}
} else {
if (value) {
// Otherwise, insert the value
uint64_t flagValue = 0;
flagValue |= shiftedValue;
m_PlayerFlags.insert(std::make_pair(flagIndex, flagValue));
}
}
}
// Notify the client that a flag has changed server-side
GameMessages::SendNotifyClientFlagChange(m_ObjectID, flagId, value, m_ParentUser->GetSystemAddress());
}
bool Character::GetPlayerFlag(const uint32_t flagId) const {
// Calculate the index first
const auto flagIndex = uint32_t(std::floor(flagId / 64));
using enum ePlayerFlag;
const auto shiftedValue = 1ULL << flagId % 64;
bool toReturn = false; //by def, return false.
auto it = m_PlayerFlags.find(flagIndex);
if (it != m_PlayerFlags.end()) {
// Don't set the data if we don't have to
return (it->second & shiftedValue) != 0;
const auto flagEntry = CDPlayerFlagsTable::GetEntry(flagId);
if (flagEntry && flagEntry->sessionOnly) {
toReturn = m_SessionFlags.contains(flagId);
} else {
// Calculate the index first
const auto flagIndex = uint32_t(std::floor(flagId / 64));
const auto shiftedValue = 1ULL << flagId % 64;
auto it = m_PlayerFlags.find(flagIndex);
if (it != m_PlayerFlags.end()) {
// Don't set the data if we don't have to
toReturn = (it->second & shiftedValue) != 0;
}
}
return false; //by def, return false.
return toReturn;
}
void Character::SetRetroactiveFlags() {

View File

@@ -620,6 +620,12 @@ private:
*/
uint64_t m_LastLogin{};
/**
* Flags only set for the duration of a session
*
*/
std::set<uint32_t> m_SessionFlags;
/**
* The gameplay flags this character has (not just true values)
*/

View File

@@ -82,6 +82,7 @@
#include "CollectibleComponent.h"
#include "ItemComponent.h"
#include "GhostComponent.h"
#include "Recorder.h"
#include "AchievementVendorComponent.h"
#include "VanityUtilities.h"
@@ -386,6 +387,9 @@ void Entity::Initialize() {
if (m_Character) {
comp->LoadFromXml(m_Character->GetXMLDoc());
} else {
// extraInfo overrides. Client ORs the database smashable and the luz smashable.
comp->SetIsSmashable(comp->GetIsSmashable() | isSmashable);
if (componentID > 0) {
std::vector<CDDestructibleComponent> destCompData = destCompTable->Query([=](CDDestructibleComponent entry) { return (entry.id == componentID); });
@@ -420,9 +424,6 @@ void Entity::Initialize() {
comp->SetMinCoins(currencyValues[0].minvalue);
comp->SetMaxCoins(currencyValues[0].maxvalue);
}
// extraInfo overrides. Client ORs the database smashable and the luz smashable.
comp->SetIsSmashable(comp->GetIsSmashable() | isSmashable);
}
} else {
comp->SetHealth(1);
@@ -775,6 +776,12 @@ void Entity::Initialize() {
// Hacky way to trigger these when the object has had a chance to get constructed
AddCallbackTimer(0, [this]() {
this->GetScript()->OnStartup(this);
if (this->m_ParentEntity) {
GameMessages::ChildLoaded childLoaded;
childLoaded.childID = this->m_ObjectID;
childLoaded.templateID = this->GetLOT();
this->m_ParentEntity->OnChildLoaded(childLoaded);
}
});
if (!m_Character && Game::entityManager->GetGhostingEnabled()) {
@@ -854,6 +861,9 @@ void Entity::Subscribe(LWOOBJID scriptObjId, CppScripts::Script* scriptToAdd, co
auto* destroyableComponent = GetComponent<DestroyableComponent>();
if (!destroyableComponent) return;
destroyableComponent->Subscribe(scriptObjId, scriptToAdd);
} else if (notificationName == "PlayerResurrectionFinished") {
LOG("Subscribing to PlayerResurrectionFinished");
m_Subscriptions[scriptObjId][notificationName] = scriptToAdd;
}
}
@@ -862,6 +872,9 @@ void Entity::Unsubscribe(LWOOBJID scriptObjId, const std::string& notificationNa
auto* destroyableComponent = GetComponent<DestroyableComponent>();
if (!destroyableComponent) return;
destroyableComponent->Unsubscribe(scriptObjId);
} else if (notificationName == "PlayerResurrectionFinished") {
LOG("Unsubscribing from PlayerResurrectionFinished");
m_Subscriptions[scriptObjId].erase(notificationName);
}
}
@@ -1501,6 +1514,19 @@ void Entity::OnShootingGalleryFire(GameMessages::ShootingGalleryFire& fire) {
GetScript()->OnShootingGalleryFire(*this, fire);
}
void Entity::OnChildLoaded(GameMessages::ChildLoaded& childLoaded) {
GetScript()->OnChildLoaded(*this, childLoaded);
}
void Entity::NotifyPlayerResurrectionFinished(GameMessages::PlayerResurrectionFinished& msg) {
for (const auto& [id, scriptList] : m_Subscriptions) {
auto it = scriptList.find("PlayerResurrectionFinished");
if (it == scriptList.end()) continue;
it->second->NotifyPlayerResurrectionFinished(*this, msg);
}
}
void Entity::RequestActivityExit(Entity* sender, LWOOBJID player, bool canceled) {
GetScript()->OnRequestActivityExit(sender, player, canceled);
}
@@ -1540,7 +1566,7 @@ void Entity::Kill(Entity* murderer, const eKillType killType) {
m_DieCallbacks.clear();
//OMAI WA MOU, SHINDERIU
//お前はもう死んでいる
GetScript()->OnDie(this, murderer);
@@ -2153,6 +2179,20 @@ void Entity::ProcessPositionUpdate(PositionUpdate& update) {
if (updateChar) Game::entityManager->SerializeEntity(this);
auto* recorder = Cinema::Recording::Recorder::GetRecorder(GetObjectID());
if (recorder != nullptr) {
recorder->AddRecord(new Cinema::Recording::MovementRecord(
update.position,
update.rotation,
update.velocity,
update.angularVelocity,
update.onGround,
update.velocity != NiPoint3Constant::ZERO,
update.angularVelocity != NiPoint3Constant::ZERO
));
}
OnPlayerPositionUpdate.Notify(this, update);
}
@@ -2183,7 +2223,7 @@ void Entity::SetRespawnRot(const NiQuaternion& rotation) {
int32_t Entity::GetCollisionGroup() const {
for (const auto* component : m_Components | std::views::values) {
auto* compToCheck = dynamic_cast<const PhysicsComponent*>(component);
auto* compToCheck = dynamic_cast<const PhysicsComponent*>(component);
if (compToCheck) {
return compToCheck->GetCollisionGroup();
}
@@ -2191,3 +2231,17 @@ int32_t Entity::GetCollisionGroup() const {
return 0;
}
bool Entity::HandleMsg(GameMessages::GameMsg& msg) const {
bool handled = false;
const auto [beg, end] = m_MsgHandlers.equal_range(msg.msgId);
for (auto it = beg; it != end; ++it) {
if (it->second) handled |= it->second(msg);
}
return handled;
}
void Entity::RegisterMsg(const MessageType::Game msgId, std::function<bool(GameMessages::GameMsg&)> handler) {
m_MsgHandlers.emplace(msgId, handler);
}

View File

@@ -14,10 +14,17 @@
#include "Observable.h"
namespace GameMessages {
struct GameMsg;
struct ActivityNotify;
struct ShootingGalleryFire;
struct ChildLoaded;
struct PlayerResurrectionFinished;
};
namespace MessageType {
enum class Game : uint16_t;
}
namespace Loot {
class Info;
};
@@ -217,6 +224,8 @@ public:
void OnZonePropertyModelRotated(Entity* player);
void OnActivityNotify(GameMessages::ActivityNotify& notify);
void OnShootingGalleryFire(GameMessages::ShootingGalleryFire& notify);
void OnChildLoaded(GameMessages::ChildLoaded& childLoaded);
void NotifyPlayerResurrectionFinished(GameMessages::PlayerResurrectionFinished& msg);
void OnMessageBoxResponse(Entity* sender, int32_t button, const std::u16string& identifier, const std::u16string& userData);
void OnChoiceBoxResponse(Entity* sender, int32_t button, const std::u16string& buttonIdentifier, const std::u16string& identifier);
@@ -312,6 +321,10 @@ public:
// Scale will only be communicated to the client when the construction packet is sent
void SetScale(const float scale) { m_Scale = scale; };
void RegisterMsg(const MessageType::Game msgId, std::function<bool(GameMessages::GameMsg&)> handler);
bool HandleMsg(GameMessages::GameMsg& msg) const;
/**
* @brief The observable for player entity position updates.
*/
@@ -370,6 +383,11 @@ protected:
* Collision
*/
std::vector<LWOOBJID> m_TargetsInPhantom;
// objectID of receiver and map of notification name to script
std::map<LWOOBJID, std::map<std::string, CppScripts::Script*>> m_Subscriptions;
std::multimap<MessageType::Game, std::function<bool(GameMessages::GameMsg&)>> m_MsgHandlers;
};
/**

View File

@@ -24,6 +24,7 @@
#include "eReplicaPacketType.h"
#include "PlayerManager.h"
#include "GhostComponent.h"
#include "ServerPreconditions.h"
#include <ranges>
// Configure which zones have ghosting disabled, mostly small worlds.
@@ -493,13 +494,15 @@ void EntityManager::UpdateGhosting(Entity* player) {
ghostingDistanceMax = ghostingDistanceMin;
}
if (observed && distance > ghostingDistanceMax && !isOverride) {
auto condition = ServerPreconditions::CheckPreconditions(entity, player);
if (observed && ((distance > ghostingDistanceMax && !isOverride) || !condition)) {
ghostComponent->GhostEntity(id);
DestructEntity(entity, player->GetSystemAddress());
entity->SetObservers(entity->GetObservers() - 1);
} else if (!observed && ghostingDistanceMin > distance) {
} else if (!observed && ghostingDistanceMin > distance && condition) {
// Check collectables, don't construct if it has been collected
uint32_t collectionId = entity->GetCollectibleID();
@@ -539,13 +542,15 @@ void EntityManager::CheckGhosting(Entity* entity) {
const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint);
if (observed && distance > m_GhostDistanceMaxSquared) {
const auto precondition = ServerPreconditions::CheckPreconditions(entity, player);
if (observed && (distance > m_GhostDistanceMaxSquared || !precondition)) {
ghostComponent->GhostEntity(id);
DestructEntity(entity, player->GetSystemAddress());
entity->SetObservers(entity->GetObservers() - 1);
} else if (!observed && m_GhostDistanceMinSqaured > distance) {
} else if (!observed && (m_GhostDistanceMinSqaured > distance && precondition)) {
ghostComponent->ObserveEntity(id);
ConstructEntity(entity, player->GetSystemAddress());

View File

@@ -29,6 +29,7 @@
#include "MessageType/Chat.h"
#include "BitStreamUtils.h"
#include "CheatDetection.h"
#include "dConfig.h"
UserManager* UserManager::m_Address = nullptr;
@@ -305,13 +306,13 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
LOT shirtLOT = FindCharShirtID(shirtColor, shirtStyle);
LOT pantsLOT = FindCharPantsID(pantsColor);
if (!name.empty() && Database::Get()->GetCharacterInfo(name)) {
if (!name.empty() && Database::Get()->IsNameInUse(name)) {
LOG("AccountID: %i chose unavailable name: %s", u->GetAccountID(), name.c_str());
WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::CUSTOM_NAME_IN_USE);
return;
}
if (Database::Get()->GetCharacterInfo(predefinedName)) {
if (Database::Get()->IsNameInUse(predefinedName)) {
LOG("AccountID: %i chose unavailable predefined name: %s", u->GetAccountID(), predefinedName.c_str());
WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::PREDEFINED_NAME_IN_USE);
return;
@@ -324,7 +325,7 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
}
//Now that the name is ok, we can get an objectID from Master:
ObjectIDManager::RequestPersistentID([=, this](uint32_t objectID) mutable {
ObjectIDManager::RequestPersistentID([=, this](uint32_t objectID) {
if (Database::Get()->GetCharacterInfo(objectID)) {
LOG("Character object id unavailable, check object_id_tracker!");
WorldPackets::SendCharacterCreationResponse(sysAddr, eCharacterCreationResponse::OBJECT_ID_UNAVAILABLE);
@@ -369,13 +370,14 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
// If predefined name is invalid, change it to be their object id
// that way more than one player can create characters if the predefined name files are not provided
if (predefinedName == "INVALID") {
auto assignedPredefinedName = predefinedName;
if (assignedPredefinedName == "INVALID") {
std::stringstream nameObjID;
nameObjID << "minifig" << objectID;
predefinedName = nameObjID.str();
assignedPredefinedName = nameObjID.str();
}
std::string_view nameToAssign = !name.empty() && nameOk ? name : predefinedName;
std::string_view nameToAssign = !name.empty() && nameOk ? name : assignedPredefinedName;
std::string pendingName = !name.empty() && !nameOk ? name : "";
ICharInfo::Info info;
@@ -516,7 +518,14 @@ void UserManager::LoginCharacter(const SystemAddress& sysAddr, uint32_t playerID
Database::Get()->UpdateLastLoggedInCharacter(playerID);
uint32_t zoneID = character->GetZoneID();
if (zoneID == LWOZONEID_INVALID) zoneID = 1000; //Send char to VE
if (zoneID == LWOZONEID_INVALID) {
const std::string& defaultWorld = Game::config->GetValue("default_world");
if (!defaultWorld.empty()) {
zoneID = std::stoul(defaultWorld);
} else {
zoneID = 1000; //Send char to VE
}
}
ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, zoneID, character->GetZoneClone(), false, [=](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) {
LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", character->GetName().c_str(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort);

View File

@@ -4,6 +4,8 @@
#include "Game.h"
#include "Logger.h"
#include "Recorder.h"
void AttackDelayBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bitStream, const BehaviorBranchContext branch) {
uint32_t handle{};
@@ -11,6 +13,8 @@ void AttackDelayBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bi
LOG("Unable to read handle from bitStream, aborting Handle! %i", bitStream.GetNumberOfUnreadBits());
return;
};
Cinema::Recording::Recorder::RegisterEffectForActor(context->originator, this->m_effectId);
for (auto i = 0u; i < this->m_numIntervals; ++i) {
context->RegisterSyncBehavior(handle, this, branch, this->m_delay * i, m_ignoreInterrupts);

View File

@@ -63,6 +63,7 @@ target_include_directories(dBehaviors PUBLIC "."
"${PROJECT_SOURCE_DIR}/dGame/dUtilities" # Preconditions.h via QuickBuildComponent.h
"${PROJECT_SOURCE_DIR}/dGame/dEntity" # via dZoneManager.h, Spawner.h
"${PROJECT_SOURCE_DIR}/dGame/dInventory" # via CharacterComponent.h
"${PROJECT_SOURCE_DIR}/dGame/dCinema"
"${PROJECT_SOURCE_DIR}/dZoneManager" # via BasicAttackBehavior.cpp
)
target_precompile_headers(dBehaviors REUSE_FROM dGameBase)

View File

@@ -3,13 +3,17 @@
#include "BehaviorContext.h"
#include "BehaviorBranchContext.h"
#include "Recorder.h"
void PlayEffectBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bitStream, BehaviorBranchContext branch) {
const auto& target = branch.target == LWOOBJID_EMPTY ? context->originator : branch.target;
Cinema::Recording::Recorder::RegisterEffectForActor(target, this->m_effectId);
// On managed behaviors this is handled by the client
if (!context->unmanaged)
return;
const auto& target = branch.target == LWOOBJID_EMPTY ? context->originator : branch.target;
PlayFx(u"", target);
}

View File

@@ -0,0 +1,24 @@
set(DGAME_DCINEMA_SOURCES "Recorder.cpp"
"Prefab.cpp"
"Scene.cpp"
"Play.cpp")
add_library(dCinema STATIC ${DGAME_DCINEMA_SOURCES})
target_precompile_headers(dCinema REUSE_FROM dGameBase)
target_link_libraries(dCinema
PUBLIC dPhysics dDatabase
INTERFACE dUtilities dCommon dBehaviors dChatFilter dMission dInventory dComponents
)
target_include_directories(dCinema PUBLIC "."
PRIVATE
"${PROJECT_SOURCE_DIR}/dGame/dComponents" # direct MissionComponent.h
"${PROJECT_SOURCE_DIR}/dGame/dUtilities" # direct SlashCommandHandler.h
"${PROJECT_SOURCE_DIR}/dGame/dPropertyBehaviors" # direct ControlBehaviors.h
"${PROJECT_SOURCE_DIR}/dGame/dMission" # via MissionComponent.h
"${PROJECT_SOURCE_DIR}/dGame/dBehaviors" # via InventoryComponent.h
"${PROJECT_SOURCE_DIR}/dGame/dInventory" # via InventoryComponent.h
"${PROJECT_SOURCE_DIR}/dGame/dEntity" # via dZoneManager/Spawner.h
"${PROJECT_SOURCE_DIR}/dGame/dGameMessages"
"${PROJECT_SOURCE_DIR}/dZoneManager" # via GameMessages.cpp, GameMessageHandler.cpp
)

87
dGame/dCinema/Play.cpp Normal file
View File

@@ -0,0 +1,87 @@
#include "Play.h"
#include "Scene.h"
#include "EntityManager.h"
using namespace Cinema;
void Play::Conclude() {
auto* player = Game::entityManager->GetEntity(this->player);
if (player == nullptr) {
return;
}
scene->Conclude(player);
}
void Play::SetupCheckForAudience() {
if (m_CheckForAudience) {
return;
}
m_CheckForAudience = true;
CheckForAudience();
}
void Play::CheckForAudience() {
auto* player = Game::entityManager->GetEntity(this->player);
if (player == nullptr) {
CleanUp();
return;
}
if (scene->IsPlayerInBounds(player)) {
SignalBarrier("audience");
m_PlayerHasBeenInsideBounds = true;
}
if (!scene->IsPlayerInMaximumShowingDistance(player)) {
if (m_PlayerHasBeenInsideBounds) {
Conclude();
}
CleanUp();
return;
}
// As the scene isn't associated with a specifc objects, we'll use the zone control entity to setup a callback.
Game::entityManager->GetZoneControlEntity()->AddCallbackTimer(1.0f, [this]() {
CheckForAudience();
});
}
void Play::CleanUp() {
LOG("Cleaning up play with %d entities", entities.size());
for (const auto& entity : entities) {
Game::entityManager->DestroyEntity(entity);
}
}
void Play::SetupBarrier(const std::string& barrier, const std::function<void()>& callback) {
// Add the callback to the barrier
m_Barriers[barrier].push_back(callback);
}
void Play::SignalBarrier(const std::string& barrier) {
const auto& it = m_Barriers.find(barrier);
if (it == m_Barriers.end()) {
return;
}
for (const auto& callback : it->second) {
callback();
}
m_Barriers.erase(it);
}

86
dGame/dCinema/Play.h Normal file
View File

@@ -0,0 +1,86 @@
#pragma once
#include <unordered_map>
#include <unordered_set>
#include <string>
#include <vector>
#include <functional>
#include "dCommonVars.h"
namespace Cinema
{
/**
* @brief A collection of variables and other information to be shared between scenes and acts.
*/
struct Play
{
public:
/**
* @brief The variables in the collection.
*/
std::unordered_map<std::string, std::string> variables;
/**
* @brief Associated player which is watching the scene.
*/
LWOOBJID player;
/**
* @brief A set of all entities involved in the scene.
*/
std::unordered_set<LWOOBJID> entities;
/**
* @brief The scene that is currently being performed.
*/
class Scene* scene;
/**
* @brief Conclude the play. Accepting/completing missions. This will not remove the entities.
*/
void Conclude();
/**
* @brief Setup a check for if the audience is still present.
*
* If the audience is no longer present, the play will be concluded and the entities will be removed.
*/
void SetupCheckForAudience();
/**
* @brief Clean up the play.
*/
void CleanUp();
/**
* @brief Setup a barrier. A callback is given when a signal is sent with the given barrier name.
*
* @param barrier The name of the barrier.
* @param callback The callback to call when the barrier is signaled.
*/
void SetupBarrier(const std::string& barrier, const std::function<void()>& callback);
/**
* @brief Signal a barrier.
*
* @param barrier The name of the barrier to signal.
*/
void SignalBarrier(const std::string& barrier);
private:
/**
* @brief Check if the audience is still present.
*/
void CheckForAudience();
bool m_CheckForAudience = false;
bool m_PlayerHasBeenInsideBounds = false;
std::unordered_map<std::string, std::vector<std::function<void()>>> m_Barriers;
};
}

190
dGame/dCinema/Prefab.cpp Normal file
View File

@@ -0,0 +1,190 @@
#include "Prefab.h"
#include "tinyxml2.h"
#include "ObjectIDManager.h"
#include "EntityManager.h"
#include "EntityInfo.h"
#include "RenderComponent.h"
using namespace Cinema;
struct PrefabInstance
{
std::vector<LWOOBJID> m_Entities;
};
namespace {
std::unordered_map<std::string, Prefab> m_Prefabs;
std::unordered_map<size_t, PrefabInstance> m_Instances;
float m_AngleToRadians = 0.0174532925f;
}
size_t Prefab::AddObject(LOT lot, NiPoint3 position, NiQuaternion rotation, float scale) {
const auto id = ObjectIDManager::GenerateRandomObjectID();
m_Pieces.emplace(id, Prefab::Piece {
lot,
position,
rotation,
scale,
std::vector<int32_t>()
});
return id;
}
void Prefab::AddEffect(size_t id, int32_t effect) {
m_Pieces[id].m_Effects.push_back(effect);
}
void Prefab::RemoveObject(size_t id) {
m_Pieces.erase(id);
}
const Prefab& Prefab::LoadFromFile(std::string file) {
if (m_Prefabs.find(file) != m_Prefabs.end()) {
return m_Prefabs[file];
}
Prefab prefab;
tinyxml2::XMLDocument doc;
doc.LoadFile(file.c_str());
tinyxml2::XMLElement* root = doc.FirstChildElement("Prefab");
if (!root) {
LOG("Failed to load prefab from file: %s", file.c_str());
m_Prefabs.emplace(file, prefab);
return m_Prefabs[file];
}
for (tinyxml2::XMLElement* element = root->FirstChildElement("Object"); element; element = element->NextSiblingElement("Object")) {
const auto lot = element->UnsignedAttribute("lot");
const auto position = NiPoint3(element->FloatAttribute("x"), element->FloatAttribute("y"), element->FloatAttribute("z"));
NiQuaternion rotation;
// Check if the qx attribute exists, if so the rotation is a quaternion, otherwise it's a vector
if (!element->Attribute("qx")) {
rotation = NiQuaternion::FromEulerAngles( {
element->FloatAttribute("rx") * m_AngleToRadians,
element->FloatAttribute("ry") * m_AngleToRadians,
element->FloatAttribute("rz") * m_AngleToRadians
} );
}
else {
rotation = NiQuaternion(element->FloatAttribute("qx"), element->FloatAttribute("qy"), element->FloatAttribute("qz"), element->FloatAttribute("qw"));
}
float scale = 1.0f;
if (element->Attribute("scale")) {
scale = element->FloatAttribute("scale");
}
const auto id = prefab.AddObject(lot, position, rotation, scale);
for (tinyxml2::XMLElement* effect = element->FirstChildElement("Effect"); effect; effect = effect->NextSiblingElement("Effect")) {
prefab.AddEffect(id, effect->IntAttribute("id"));
}
}
m_Prefabs.emplace(file, prefab);
return m_Prefabs[file];
}
void Prefab::SaveToFile(std::string file) {
tinyxml2::XMLDocument doc;
tinyxml2::XMLElement* root = doc.NewElement("Prefab");
doc.InsertFirstChild(root);
for (const auto& [id, piece] : m_Pieces) {
tinyxml2::XMLElement* object = doc.NewElement("Object");
object->SetAttribute("lot", piece.m_Lot);
object->SetAttribute("x", piece.m_Position.x);
object->SetAttribute("y", piece.m_Position.y);
object->SetAttribute("z", piece.m_Position.z);
object->SetAttribute("qx", piece.m_Rotation.x);
object->SetAttribute("qy", piece.m_Rotation.y);
object->SetAttribute("qz", piece.m_Rotation.z);
object->SetAttribute("qw", piece.m_Rotation.w);
object->SetAttribute("scale", piece.m_Scale);
for (const auto& effect : piece.m_Effects) {
tinyxml2::XMLElement* effectElement = doc.NewElement("Effect");
effectElement->SetAttribute("id", effect);
object->InsertEndChild(effectElement);
}
root->InsertEndChild(object);
}
doc.SaveFile(file.c_str());
}
size_t Prefab::Instantiate(NiPoint3 position, float scale) const {
if (m_Pieces.empty()) {
return 0;
}
const auto id = ObjectIDManager::GenerateRandomObjectID();
std::vector<LWOOBJID> entities;
for (const auto& [_, piece] : m_Pieces) {
EntityInfo info;
info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID();
info.lot = piece.m_Lot;
info.pos = (piece.m_Position * scale) + position;
info.rot = piece.m_Rotation;
info.scale = piece.m_Scale * scale;
const auto entity = Game::entityManager->CreateEntity(info);
for (const auto& effect : piece.m_Effects) {
auto* renderComponent = entity->GetComponent<RenderComponent>();
if (!renderComponent) {
continue;
}
// Generate random name
std::string effectName = "Effect_";
for (int i = 0; i < 10; ++i) {
effectName += std::to_string(GeneralUtils::GenerateRandomNumber<size_t>(0, 10));
}
renderComponent->PlayEffect(effect, u"create", effectName);
}
entities.push_back(entity->GetObjectID());
Game::entityManager->ConstructEntity(entity);
}
m_Instances.emplace(id, PrefabInstance { entities });
return id;
}
const std::vector<LWOOBJID>& Cinema::Prefab::GetEntities(size_t instanceID) {
return m_Instances[instanceID].m_Entities;
}
void Prefab::DestroyInstance(size_t id) {
const auto& instance = m_Instances[id];
for (const auto& entity : instance.m_Entities) {
Game::entityManager->DestroyEntity(entity);
}
m_Instances.erase(id);
}

105
dGame/dCinema/Prefab.h Normal file
View File

@@ -0,0 +1,105 @@
#pragma once
#include "dCommonVars.h"
#include "Entity.h"
#include <unordered_map>
#include <cstdint>
namespace Cinema
{
/**
* @brief A prefab is a collection of objects that can be placed in the world together.
*
* Can be saved and loaded from XML.
*/
class Prefab
{
public:
Prefab() = default;
~Prefab() = default;
/**
* @brief Adds an object to the prefab.
*
* @param lot The LOT of the object to add.
* @param position The position of the object to add.
* @param rotation The rotation of the object to add.
* @param scale The scale of the object to add.
*
* @return The ID of the object that was added.
*/
size_t AddObject(LOT lot, NiPoint3 position, NiQuaternion rotation, float scale = 1.0f);
/**
* @brief Adds an effect to the prefab.
*
* @param objectID The ID of the object to add the effect to.
* @param effectID The ID of the effect to add.
*/
void AddEffect(size_t objectID, int32_t effectID);
/**
* @brief Removes an object from the prefab.
*
* @param objectID The ID of the object to remove.
*/
void RemoveObject(size_t objectID);
/**
* @brief Loads a prefab from the given file.
*
* @param file The file to load the prefab from.
* @return The prefab that was loaded.
*/
static const Prefab& LoadFromFile(std::string file);
/**
* @brief Saves the prefab to the given file.
*
* @param file The file to save the prefab to.
*/
void SaveToFile(std::string file);
/**
* @brief Instantiates the prefab in the world.
*
* @param position The position to instantiate the prefab at.
* @param scale The scale to instantiate the prefab with.
*
* @return The ID of the instance that was created.
*/
size_t Instantiate(NiPoint3 position, float scale = 1.0f) const;
/**
* @brief Get the list of entities in the instance with the given ID.
*
* @param instanceID The ID of the instance to get the entities for.
*
* @return The list of entities in the instance.
*/
static const std::vector<LWOOBJID>& GetEntities(size_t instanceID);
/**
* @brief Destroys the instance with the given ID.
*
* @param instanceID The ID of the instance to destroy.
*/
static void DestroyInstance(size_t instanceID);
private:
struct Piece
{
LOT m_Lot;
NiPoint3 m_Position;
NiQuaternion m_Rotation;
float m_Scale;
std::vector<int32_t> m_Effects;
};
std::unordered_map<size_t, Piece> m_Pieces;
};
}

352
dGame/dCinema/README.md Normal file
View File

@@ -0,0 +1,352 @@
# Darkflame Cinema
A more emersive way to experience story in Darkflame Universe.
## What is Darkflame Cinema?
Darkflame Cinema is a way to create interactive — acted — experiences in Darkflame Universe. It aims to provide a complement to the ordinary mission structure with static NPCs and mission texts to convey story. Insperation is drawn from games likes of World of Warcraft, where NPCs frequently act out scenes, alone and in combination with both the player and other NPCs.
## Top-level design
Cinema works with a few concepts:
* **Actors** are the non player characters that act and perform actions.
* **Props** are objects in the world which complement the actors.
* **Scenes** are a collection of actors and props that perform an interactive play.
* **Plays** are scenes which are performed to a player.
## Play isolation
When a play is performed to a player, it is always isolated from the rest of the players in the world. This is to ensure that the player can experience the play without being disturbed by others, and others can't be disturbed by the play. This is achieved by ghosting all NPCs and props in the play to all players except the one experiencing the play.
<img src="media/isolation.png" width="800">
## How to create a scene
A play is created is a couple of steps:
1. Acts out the scene in the world as how the NPCs should reenact it.
2. Create prefabs of props in the scene.
3. Put the NPCs and props in a scene file.
4. Edit the performed acts to add synchronization, conditions and additional actions.
5. Setting up the scene to be performed automatically.
6. Hiding zone objects while performing.
### 1. Acts out the scene
See <a href="./media/acting.mp4">media/acting.mp4</a> for an example of how to act out a scene.
To start acting type: `/record-start (clear/copy)` in the chat. This will start recording your actions in the world. The actions recorded include:
* Movement
* Talking
* Equipping and unequipping items
* Explicit animations
By appending `clear` to the command, all gear on the NPC will be removed before reenactment. By appending `copy` to the command, all gear on the NPC will be copied from the player before reenactment. Which, if either, of these options to use depends on which NPC object will be acting. Usually you want to use `copy`. This can be edited manually later.
This acting is the core of the scene. Later we will learn how to add some interactivity to the scene.
When you are done acting, type `/record-stop` in the chat. This will stop recording your actions in the world.
Save your amazing performance by typing `/record-save <file.xml>` in the chat. **Do not forget to do this before starting your next recording!** This will save your performance to a file relative to where the server is running.
If your scene has multiple NPCs acting, now is the time to switch to the next NPC and repeat the steps above.
#### 1.1. Previewing your performances
See <a href="./media/preview.mp4">media/preview.mp4</a> for an example of how to preview a performance.
To show a preview of the performance you can type `/record-act (lot) (name)`, this will play the performance. If a lot is specified, the actor will be of that object. If a name is specified, the actor will have that name. If neither is specified, the actor will be a predetermined NPC.
You may load a performance by typing `/record-load <file.xml>` in the chat. This will load a performance from a file relative to where the server is running. You may than type `/record-act` to show a preview of the performance.
### 2. Create prefabs of props in the scene
Prefabs are a collection of objects that can be instantiated together. This is currently done exclusively by writing xml files.
Here is an example of a prefab:
```xml
<Prefab>
<!--
Lot is the id of the object to spawn.
Position is defined in either coordinates (x,y,z).
These are relative to the position of the prefab.
Rotation is defined in either euler angles (rx,ry,rz);
or in a quaternion (qx,qy,qz,qw).
Use /pos and /rot in the chat to get your current position and rotation.
-->
<Object lot="359" x="0" y="0" z="0" rx="0" ry="0" rz="0"/>
<Object lot="53" x="0" y="5" z="0" rx="0" rz="0">
<Effect id="60073"/> <!-- Custom effect -->
</Object>
<Object lot="359" x="0" y="5" z="0" rx="0" ry="0" rz="0"/>
<Object lot="359" x="0" y="10" z="0" rx="0" ry="0" rz="0"/>
<Object lot="53" x="0" y="0" z="0" rx="0" rz="0"/>
<Object lot="53" x="0" y="10" z="0" rx="0" rz="0"/>
<Object lot="53" x="0" y="15" z="0" rx="0" rz="0"/>
</Prefab>
```
This creates the following prefab:
<img src="media/image.png" width="800">
You can spawn a prefab by typing `/prefab-spawn <file.xml>`. This will spawn the prefab at your current position.
### 3. Put the NPCs and props in a scene file
A scene file is a collection of actors and props that perform an interactive play. This is currently done exclusively by writing xml files.
Here is an example of a scene:
```xml
<!--
The scene is defined by a collection of actors and props.
These are defined by their name and prefab file.
-->
<Scene>
<!--
Props are defined by their file name and a position.
See media/SFlagSmall.xml for this file.
-->
<Prefab file="vanity/prefabs/SFlagSmall.xml" x="-655.564" y="1123.76" z="75.993"/>
<!--
Actors are defined by their name and the file which holds the performance.
See media/my-act.xml and media/my-act-2.xml for these files.
-->
<NPC name="John" act="vanity/acts/my-act.xml"/>
<NPC name="Bill" act="vanity/acts/my-act-2.xml"/>
</Scene>
```
See <a href="./media/scene.mp4">media/scene.mp4</a> for how the above scene looks in the world.
You preview a scene by typing `/scene-act <file.xml>` in the chat. This will act out the scene for you.
### 4. Edit the performed acts
When you preview a newly created scene the NPCs will act out exactly what you did when you recorded the performance. This is not always what you want. You may want to add some interactivity to the scene. This is done by editing the performed acts.
An act is made out of a collection of records which will be evaluated in order. A record either performs some visual action or provides for flow control.
Here is an example of a slightly modified act which adds some interactivity:
```xml
<!--
See media/my-act-mod.xml for this file.
-->
<Recorder>
<!-- The following three records were added automaticly because I appended 'copy' to the command when I acted, but these may be modified freely here. -->
<ClearEquippedRecord t="0"/>
<EquipRecord item="9856" t="0"/>
<EquipRecord item="6500" t="0"/>
<MovementRecord x="-557.53174" y="1128.7369" z="50.89016" qx="0" qy="0.72913098" qz="0" qw="0.68437415" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.39199999"/>
...
<!--
The following records will assure that before the act continues, the player is within 10 units of the NPC.
If the player is not within 10 units of them within 5 seconds, the NPC will ask the player to come closer. The NPC will continue to ask the player to come closer every 5 seconds until the player is within 10 units of them.
-->
<!--
This is flow-control, the act will jump to the record with the name "check-player". This makes sure that if the player is already close enough, the NPC does not tell them to come closer.
-->
<JumpRecord label="check-player" t="0" />
<!--
This is a manually added speak record, which will have the NPC speak, telling the player to come closer.
-->
<SpeakRecord name="join-me" text="Join me!" t="3.0" />
<!--
This is a player proximity record, it can branch two different ways. If the player comes within 10 units of the NPC within 5 seconds, the act will continue to the record. If however, the player is not within 10 units within 5 seconds, the act will jump to the record named "join-me", the record above this one.
-->
<PlayerProximityRecord name="check-player" distance="10" timeout="5" timeoutLabel="join-me" t="0" />
<SpeakRecord text="Welcome!" t="4.026"/>
<MovementRecord x="-557.53174" y="1128.7369" z="50.89016" qx="0" qy="0.72913098" qz="0" qw="0.68437415" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="2.046"/>
...
<SpeakRecord text="Great!&quot;" t="2.3759999"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="3.6960001"/>
</Recorder>
```
See <a href="./media/interactive.mp4">media/interactive.mp4</a> for how the above scene looks in the world.
The list of available records is extensive.
#### 4.1. Records
Each record specifies a time `t`. This is how long after this record is evaluated that the next record will be evaluated.
A record may also specify a `name`. This is used for flow control. If a record specifies a name, it may be jumped to by for example `JumpRecord` or `ForkRecord`.
#### 4.1. MovementRecord
This record sets the properties of the actors controllable physics component. This is the only record which isn't meant to be edited manually.
#### 4.2. SpeakRecord
This record makes the actor speak.
```xml
<SpeakRecord text="Hello!" t="3.0" />
```
#### 4.3. EquipRecord
This record makes the actor equip an item.
```xml
<EquipRecord item="9856" t="0"/>
```
#### 4.4. UnequipRecord
This record makes the actor unequip an item.
```xml
<UnequipRecord item="9856" t="0"/>
```
#### 4.5. ClearEquippedRecord
This record makes the actor unequip all items.
```xml
<ClearEquippedRecord t="0"/>
```
#### 4.6. AnimationRecord
This record makes the actor play an animation.
```xml
<AnimationRecord animation="salute" t="2.0"/>
```
#### 4.7. WaitRecord
This record makes the actor wait for a specified amount of time.
```xml
<WaitRecord t="5.5"/>
```
#### 4.8. JumpRecord
This record makes the act jump to a record with a specified name.
```xml
<JumpRecord label="check-player" t="0" />
```
#### 4.9. ForkRecord
This record makes a decision based on a condition, and jumps accordingly. The condition may either be a scene variable or a precondtion.
```xml
<!-- Jump to 'jump-true' if my-variable=1, else jump to 'jump-false'. -->
<ForkRecord variable="my-variable" value="1" success="jump-true" failure="jump-false" t="0" />
<!-- Jump to 'jump-true' if the given precondition holds for the player, else jump to 'jump-false'. -->
<ForkRecord precondition="42,99" success="jump-true" failure="jump-false" t="0" />
```
#### 4.10. PlayerProximityRecord
This record checks if the player is within a specified distance of the actor. If the player is within the distance, the act will continue to the next record. If the player is not within the distance, the act will jump to a record with a specified name.
```xml
<!-- Specifying the timeout is optional. If it is not specified, the act will wait forever for the player to come within the distance. -->
<PlayerProximityRecord name="check-player" distance="10" timeout="5" timeoutLabel="join-me" t="0" />
```
#### 4.11. SetVariableRecord
This record sets a scene variable to a specified value. These are shared between all actors in the scene.
```xml
<SetVariableRecord variable="my-variable" value="1" t="0" />
```
#### 4.12. BarrierRecord
This record makes the act wait until it gets a signal to continue from another act, or until an optional timeout is reached. This is useful for synchronizing multiple actors, for example in a conversation.
```xml
<!-- Specifying the timeout is optional. If it is not specified, the act will wait forever for the signal. -->
<BarrierRecord signal="my-signal" timeout="5" timeoutLabel="do-something-else" t="0" />
```
#### 4.13. SignalRecord
This record sends a signal to all acts waiting for it.
```xml
<SignalRecord signal="my-signal" t="0" />
```
#### 4.14. ConcludeRecord
This record concludes the play.
```xml
<ConcludeRecord t="0" />
```
#### 4.15. VisiblityRecord
This record makes the actor visible or invisible.
```xml
<VisiblityRecord visible="false" t="0" />
```
#### 4.16. PlayEffectRecord
This record plays an effect.
```xml
<PlayEffectRecord effect="5307" t="0" />
```
### 5. Setting up the scene to be performed automatically
Scenes can be appended with metadata to describe when they should be performed and what consequences they have. This is done by editing the scene file.
#### 5.1. Scene metadata
There attributes can be added to the `Scene` tag:
| Attribute | Description |
| --- | --- |
| `x y z` | The center of where the following two attributes <br> are measured. |
| `showingDistance` | The distance at which the scene will <br> be loaded for a player.<br><br> If the player exits this area the scene is unloaded. <br> If the scene has been registered as having been<br>viewed by the player, is is concluded. |
| `performingDistance` | The scene is registred as having been <br>viewed by the player. This doesn't mean <br>it can't be viewed again.<br><br>A signal named `"audiance"` will be <br> sent when the player enters this area. <br>This can be used to trigger the main <br>part of the scene. |
| `acceptMission` | The mission with the given id will be <br> accepted when the scene is concluded. |
| `completeMission` | The mission with the given id will be <br> completed when the scene is concluded. |
Here is an example of a scene with metadata:
```xml
<Scene x="-368.272" y="1161.89" z="-5.25745" performingDistance="50" showingDistance="200">
...
</Scene>
```
#### 5.2. Automatic scene setup
In either the worldconfig.ini or sharedconfig.ini file, add the following:
```
# Path to where scenes are located.
scenes_directory=vanity/scenes/
```
Now move the scene into a subdirectory of the scenes directory. The name of the subdirectory should be **the zone id** of the zone the scene is located in.
For example:
```
build/
├── vanity/
│ ├── scenes/
│ │ ├── 1900/
│ │ │ ├── my-scene.xml
```
Now the scene will be setup automatically and loaded when the player enters the `showingDistance` of the scene.
#### 5.3. Adding conditions
Conditions can be added to the scene to make it only performable when the player fulfills specified preconditions. This is done by editing the scene file.
Here is an example of a scene with conditions:
```xml
<Scene x="-368.272" y="1161.89" z="-5.25745" performingDistance="50" showingDistance="200">
<Precondition expression="42,99"/> <!-- The player must fulfill preconditions 42 and 99. -->
<Precondition expression="666" not="1"/> <!-- The player cannot fulfill precondition 666. -->
...
</Scene>
```
### 6. Hiding zone objects while performing
When a scene should be performed, you might want to hide some objects in the zone. This is done by adding server preconditions. This is a seperate file.
In either the worldconfig.ini or sharedconfig.ini file, add the following:
```
# Path to where server preconditions are located.
server_preconditions_directory=vanity/server-preconditions.xml
```
Now create the server preconditions file in the directory specified.
Here is an example of a server preconditions file:
```xml
<Preconditions>
<Entity lot="12261">
<Precondition not="1">1006</Precondition>
</Entity>
</Preconditions>
```
This will hide the objects with lot 12261 for players who fulfill precondition 1006.

1507
dGame/dCinema/Recorder.cpp Normal file

File diff suppressed because it is too large Load Diff

473
dGame/dCinema/Recorder.h Normal file
View File

@@ -0,0 +1,473 @@
#pragma once
#include "Game.h"
#include "EntityManager.h"
#include "tinyxml2.h"
#include "Play.h"
#include <chrono>
namespace Cinema::Recording
{
class Record
{
public:
virtual void Act(Entity* actor) = 0;
virtual void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) = 0;
virtual void Deserialize(tinyxml2::XMLElement* element) = 0;
float m_Delay;
std::string m_Name;
};
class Recorder
{
public:
Recorder();
~Recorder();
void AddRecord(Record* record);
void Act(Entity* actor, Play* variables = nullptr);
Entity* ActFor(Entity* actorTemplate, Entity* player, Play* variables = nullptr);
void StopActingFor(Entity* actor, Entity* actorTemplate, LWOOBJID playerID);
bool IsRecording() const;
void SaveToFile(const std::string& filename);
float GetDuration() const;
static Recorder* LoadFromFile(const std::string& filename);
static void AddRecording(LWOOBJID actorID, Recorder* recorder);
static void StartRecording(LWOOBJID actorID);
static void StopRecording(LWOOBJID actorID);
static Recorder* GetRecorder(LWOOBJID actorID);
static void RegisterEffectForActor(LWOOBJID actorID, const int32_t& effectId);
static void LoadRecords(tinyxml2::XMLElement* root, std::vector<Record*>& records);
static void ActingDispatch(Entity* actor, const std::vector<Record*>& records, size_t index, Play* variables);
private:
static void PlayerProximityDispatch(Entity* actor, const std::vector<Record*>& records, size_t index, Play* variables, std::shared_ptr<bool> actionTaken);
static void PathFindDispatch(Entity* actor, const std::vector<Record*>& records, size_t index, Play* variables);
std::vector<Record*> m_Records;
bool m_IsRecording;
std::chrono::milliseconds m_StartTime;
std::chrono::milliseconds m_LastRecordTime;
};
class MovementRecord : public Record
{
public:
NiPoint3 position;
NiQuaternion rotation;
NiPoint3 velocity;
NiPoint3 angularVelocity;
bool onGround;
bool dirtyVelocity;
bool dirtyAngularVelocity;
MovementRecord() = default;
MovementRecord(
const NiPoint3& position,
const NiQuaternion& rotation,
const NiPoint3& velocity,
const NiPoint3& angularVelocity,
bool onGround, bool dirtyVelocity, bool dirtyAngularVelocity
);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class SpeakRecord : public Record
{
public:
std::string text;
SpeakRecord() = default;
SpeakRecord(const std::string& text);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class AnimationRecord : public Record
{
public:
std::string animation;
AnimationRecord() = default;
AnimationRecord(const std::string& animation);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class EquipRecord : public Record
{
public:
LOT item = LOT_NULL;
EquipRecord() = default;
EquipRecord(LOT item);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class UnequipRecord : public Record
{
public:
LOT item = LOT_NULL;
UnequipRecord() = default;
UnequipRecord(LOT item);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class ClearEquippedRecord : public Record
{
public:
ClearEquippedRecord() = default;
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class WaitRecord : public Record
{
public:
WaitRecord() = default;
WaitRecord(float delay);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class ForkRecord : public Record
{
public:
std::string variable;
std::string value;
std::string precondition;
std::string success;
std::string failure;
ForkRecord() = default;
ForkRecord(const std::string& variable, const std::string& value, const std::string& success, const std::string& failure);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class JumpRecord : public Record
{
public:
std::string label;
JumpRecord() = default;
JumpRecord(const std::string& label);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class SetVariableRecord : public Record
{
public:
std::string variable;
std::string value;
SetVariableRecord() = default;
SetVariableRecord(const std::string& variable, const std::string& value);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class BarrierRecord : public Record
{
public:
std::string signal;
float timeout = 0.0f;
std::string timeoutLabel;
BarrierRecord() = default;
BarrierRecord(const std::string& signal, float timeout, const std::string& timeoutLabel);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class SignalRecord : public Record
{
public:
std::string signal;
SignalRecord() = default;
SignalRecord(const std::string& signal);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class ConcludeRecord : public Record
{
public:
bool cleanUp = false;
ConcludeRecord() = default;
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class PlayerProximityRecord : public Record
{
public:
float distance = 0.0f;
float timeout = 0.0f;
std::string timeoutLabel;
PlayerProximityRecord() = default;
PlayerProximityRecord(float distance, float timeout, const std::string& timeoutLabel);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class VisibilityRecord : public Record
{
public:
bool visible = false;
VisibilityRecord() = default;
VisibilityRecord(bool visible);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class PlayEffectRecord : public Record
{
public:
std::string effect;
PlayEffectRecord() = default;
PlayEffectRecord(const std::string& effect);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class CoroutineRecord : public Record
{
public:
std::vector<Record*> records;
CoroutineRecord() = default;
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class PathFindRecord : public Record
{
public:
NiPoint3 position;
float speed = 1.0f;
PathFindRecord() = default;
PathFindRecord(const NiPoint3& position, float speed);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class CombatAIRecord : public Record
{
public:
bool enabled = false;
CombatAIRecord() = default;
CombatAIRecord(bool enabled);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class CompanionRecord : public Record
{
public:
std::vector<Record*> records;
CompanionRecord() = default;
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
static void SetCompanion(Entity* actor, LWOOBJID player);
};
class SpawnRecord : public Record
{
public:
LOT lot = LOT_NULL;
NiPoint3 position;
NiQuaternion rotation;
std::vector<Record*> onSpawnRecords;
std::vector<Record*> onDespawnRecords;
SpawnRecord() = default;
SpawnRecord(LOT lot, const NiPoint3& position, const NiQuaternion& rotation);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class MissionRecord : public Record
{
public:
int32_t mission;
MissionRecord() = default;
MissionRecord(const int32_t& mission);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
class CinematicRecord : public Record
{
public:
std::string cinematic;
CinematicRecord() = default;
CinematicRecord(const std::string& cinematic);
void Act(Entity* actor) override;
void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override;
void Deserialize(tinyxml2::XMLElement* element) override;
};
}

742
dGame/dCinema/Scene.cpp Normal file
View File

@@ -0,0 +1,742 @@
#include "Scene.h"
#include <filesystem>
#include <tinyxml2.h>
#include "ServerPreconditions.h"
#include "EntityManager.h"
#include "EntityInfo.h"
#include "MissionComponent.h"
#include "dConfig.h"
#include "PlayerManager.h"
#include "SlashCommandHandler.h"
#include "ChatPackets.h"
#include "InventoryComponent.h"
#include "MovementAIComponent.h"
#include "Recorder.h"
#include "GhostComponent.h"
using namespace Cinema;
std::unordered_map<std::string, Scene> Scene::m_Scenes;
void Cinema::Scene::AddObject(LOT lot, NiPoint3 position, NiQuaternion rotation) {
m_Objects.push_back(std::make_pair(lot, std::make_pair(position, rotation)));
}
void Scene::AddPrefab(const Prefab& prefab, NiPoint3 position) {
m_Prefabs.push_back(std::make_pair(prefab, position));
}
void Cinema::Scene::AddNPC(LOT npc, const std::string& name, Recording::Recorder* act) {
m_NPCs.push_back(std::make_pair(npc, std::make_pair(act, name)));
}
void Cinema::Scene::Rehearse() {
CheckForShowings();
}
void Cinema::Scene::Conclude(Entity* player) {
if (player == nullptr) {
return;
}
if (m_Audience.find(player->GetObjectID()) == m_Audience.end()) {
return;
}
auto* missionComponent = player->GetComponent<MissionComponent>();
if (missionComponent == nullptr) {
return;
}
if (m_CompleteMission != 0) {
missionComponent->CompleteMission(m_CompleteMission);
}
if (m_AcceptMission != 0) {
missionComponent->AcceptMission(m_AcceptMission);
}
// Remove the player from the audience
m_Audience.erase(player->GetObjectID());
m_HasBeenOutside.erase(player->GetObjectID());
m_VisitedPlayers.emplace(player->GetObjectID());
}
bool Cinema::Scene::IsPlayerInBounds(Entity* player) const {
if (player == nullptr) {
return false;
}
if (m_Bounds == 0.0f) {
return true;
}
const auto& position = player->GetPosition();
auto distance = NiPoint3::Distance(position, m_Center);
return distance <= m_Bounds;
}
bool Cinema::Scene::IsPlayerInShowingDistance(Entity* player) const {
if (player == nullptr) {
return false;
}
if (m_ShowingDistance == 0.0f) {
return true;
}
const auto& position = player->GetPosition();
auto distance = NiPoint3::Distance(position, m_Center);
return distance <= m_ShowingDistance;
}
bool Cinema::Scene::IsPlayerInMaximumShowingDistance(Entity* player) const {
if (player == nullptr) {
return false;
}
if (m_MaximumShowingDistance == 0.0f) {
return true;
}
const auto& position = player->GetPosition();
auto distance = NiPoint3::Distance(position, m_Center);
return distance <= m_MaximumShowingDistance;
}
void Cinema::Scene::AutoLoadScenesForZone(LWOMAPID zone) {
SlashCommandHandler::RegisterCommand(Command{
.help = "",
.info = "",
.aliases = { "record-act" },
.handle = CommandRecordAct,
.requiredLevel = eGameMasterLevel::LEAD_MODERATOR
});
SlashCommandHandler::RegisterCommand(Command{
.help = "",
.info = "",
.aliases = { "record-start" },
.handle = CommandRecordStart,
.requiredLevel = eGameMasterLevel::LEAD_MODERATOR
});
SlashCommandHandler::RegisterCommand(Command{
.help = "",
.info = "",
.aliases = { "record-stop" },
.handle = CommandRecordStop,
.requiredLevel = eGameMasterLevel::LEAD_MODERATOR
});
SlashCommandHandler::RegisterCommand(Command{
.help = "",
.info = "",
.aliases = { "record-save" },
.handle = CommandRecordSave,
.requiredLevel = eGameMasterLevel::LEAD_MODERATOR
});
SlashCommandHandler::RegisterCommand(Command{
.help = "",
.info = "",
.aliases = { "record-load" },
.handle = CommandRecordLoad,
.requiredLevel = eGameMasterLevel::LEAD_MODERATOR
});
SlashCommandHandler::RegisterCommand(Command{
.help = "",
.info = "",
.aliases = { "prefab-spawn" },
.handle = CommandPrefabSpawn,
.requiredLevel = eGameMasterLevel::LEAD_MODERATOR
});
SlashCommandHandler::RegisterCommand(Command{
.help = "",
.info = "",
.aliases = { "prefab-destroy" },
.handle = CommandPrefabDestroy,
.requiredLevel = eGameMasterLevel::LEAD_MODERATOR
});
SlashCommandHandler::RegisterCommand(Command{
.help = "",
.info = "",
.aliases = { "scene-act" },
.handle = CommandSceneAct,
.requiredLevel = eGameMasterLevel::LEAD_MODERATOR
});
SlashCommandHandler::RegisterCommand(Command{
.help = "",
.info = "",
.aliases = { "scene-setup" },
.handle = CommandSceneSetup,
.requiredLevel = eGameMasterLevel::LEAD_MODERATOR
});
SlashCommandHandler::RegisterCommand(Command{
.help = "",
.info = "",
.aliases = { "companion" },
.handle = CommandCompanion,
.requiredLevel = eGameMasterLevel::LEAD_MODERATOR
});
SlashCommandHandler::RegisterCommand(Command{
.help = "",
.info = "",
.aliases = { "cinematic" },
.handle = CommandCinematic,
.requiredLevel = eGameMasterLevel::LEAD_MODERATOR
});
SlashCommandHandler::RegisterCommand(Command{
.help = "",
.info = "",
.aliases = { "ghost" },
.handle = CommandGhostReference,
.requiredLevel = eGameMasterLevel::LEAD_MODERATOR
});
const auto& scenesRoot = Game::config->GetValue("scenes_directory");
if (scenesRoot.empty()) {
return;
}
const auto path = std::filesystem::path(scenesRoot) / std::to_string(zone);
if (!std::filesystem::exists(path)) {
return;
}
// Recursively iterate through the directory
for (const auto& entry : std::filesystem::recursive_directory_iterator(path)) {
if (!entry.is_regular_file()) {
continue;
}
// Check that extension is .xml
if (entry.path().extension() != ".xml") {
continue;
}
const auto& file = entry.path().string();
auto& scene = LoadFromFile(file);
if (scene.m_ChanceToPlay != 1.0f) {
const auto chance = GeneralUtils::GenerateRandomNumber<float>(0.0f, 1.0f);
if (chance > scene.m_ChanceToPlay) {
continue;
}
}
scene.Rehearse();
}
}
void Cinema::Scene::CheckForShowings() {
auto audience = m_Audience;
auto hasBeenOutside = m_HasBeenOutside;
for (const auto& member : audience) {
if (Game::entityManager->GetEntity(member) == nullptr) {
m_Audience.erase(member);
}
}
for (const auto& member : hasBeenOutside) {
if (Game::entityManager->GetEntity(member) == nullptr) {
m_HasBeenOutside.erase(member);
}
}
m_Audience = audience;
m_HasBeenOutside = hasBeenOutside;
// I don't care
Game::entityManager->GetZoneControlEntity()->AddCallbackTimer(1.0f, [this]() {
for (auto* player : PlayerManager::GetAllPlayers()) {
if (m_Audience.find(player->GetObjectID()) != m_Audience.end()) {
continue;
}
if (!m_Repeatable && m_VisitedPlayers.find(player->GetObjectID()) != m_VisitedPlayers.end()) {
continue;
}
CheckTicket(player);
}
CheckForShowings();
});
}
void Cinema::Scene::CheckTicket(Entity* player) {
if (m_Audience.find(player->GetObjectID()) != m_Audience.end()) {
return;
}
for (const auto& [expression, invert] : m_Preconditions) {
if (expression.Check(player) == invert) {
return;
}
}
if (m_ShowingDistance != 0.0f) {
if (!IsPlayerInShowingDistance(player)) {
m_HasBeenOutside.emplace(player->GetObjectID());
return;
}
if (m_HasBeenOutside.find(player->GetObjectID()) == m_HasBeenOutside.end()) {
return;
}
}
m_Audience.emplace(player->GetObjectID());
Act(player);
}
Play* Cinema::Scene::Act(Entity* player) {
auto* play = new Play();
if (player != nullptr) {
play->player = player->GetObjectID();
}
play->scene = this;
for (const auto& [lot, transform] : m_Objects) {
const auto& [position, rotation] = transform;
EntityInfo info;
info.pos = position;
info.rot = rotation;
info.lot = lot;
info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID();
auto* entity = Game::entityManager->CreateEntity(info);
Game::entityManager->ConstructEntity(entity);
if (player != nullptr) {
ServerPreconditions::AddSoloActor(entity->GetObjectID(), player->GetObjectID());
}
LOG("Spawing object %d", entity->GetObjectID());
}
for (const auto& [prefab, position] : m_Prefabs) {
const auto instanceId = prefab.Instantiate(position);
const auto& entities = prefab.GetEntities(instanceId);
if (player != nullptr) {
for (const auto& entity : entities) {
ServerPreconditions::AddSoloActor(entity, player->GetObjectID());
}
}
for (const auto& entity : entities) {
play->entities.emplace(entity);
}
LOG("Spawing prefab %d", instanceId);
}
for (const auto& [npc, meta] : m_NPCs) {
const auto& [act, name] = meta;
EntityInfo info;
info.pos = NiPoint3();
info.rot = NiQuaternion();
info.lot = npc;
info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID();
info.settings = {
new LDFData<std::vector<std::u16string>>(u"syncLDF", { u"custom_script_client" }),
new LDFData<std::u16string>(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua")
};
auto* entity = Game::entityManager->CreateEntity(info);
play->entities.emplace(entity->GetObjectID());
Game::entityManager->ConstructEntity(entity);
Entity* actor;
if (player != nullptr) {
actor = act->ActFor(entity, player, play);
}
else
{
act->Act(entity, play);
actor = entity;
}
if (actor != nullptr) {
actor->SetVar(u"npcName", name);
}
play->entities.emplace(actor->GetObjectID());
LOG("Spawing npc %d", entity->GetObjectID());
}
if (player != nullptr) {
play->SetupCheckForAudience();
}
return play;
}
Scene& Cinema::Scene::LoadFromFile(std::string file) {
if (m_Scenes.find(file) != m_Scenes.end()) {
return m_Scenes[file];
}
Scene scene;
tinyxml2::XMLDocument doc;
doc.LoadFile(file.c_str());
tinyxml2::XMLElement* root = doc.FirstChildElement("Scene");
if (!root) {
LOG("Failed to load scene from file: %s", file.c_str());
m_Scenes.emplace(file, scene);
return m_Scenes[file];
}
// Load center and bounds
if (root->Attribute("x")) {
scene.m_Center = NiPoint3(root->FloatAttribute("x"), root->FloatAttribute("y"), root->FloatAttribute("z"));
}
if (root->Attribute("performingDistance")) {
scene.m_Bounds = root->FloatAttribute("performingDistance");
}
if (root->Attribute("showingDistance")) {
scene.m_ShowingDistance = root->FloatAttribute("showingDistance");
} else {
scene.m_ShowingDistance = scene.m_Bounds * 2.0f;
}
if (root->Attribute("maximumShowingDistance")) {
scene.m_MaximumShowingDistance = root->FloatAttribute("maximumShowingDistance");
} else {
scene.m_MaximumShowingDistance = scene.m_ShowingDistance * 2.0f;
}
if (root->Attribute("chanceToPlay")) {
scene.m_ChanceToPlay = root->FloatAttribute("chanceToPlay");
}
if (root->Attribute("repeatable")) {
scene.m_Repeatable = root->BoolAttribute("repeatable");
}
// Load accept and complete mission
if (root->Attribute("acceptMission")) {
scene.m_AcceptMission = root->IntAttribute("acceptMission");
}
if (root->Attribute("completeMission")) {
scene.m_CompleteMission = root->IntAttribute("completeMission");
}
// Load preconditions
for (tinyxml2::XMLElement* element = root->FirstChildElement("Precondition"); element; element = element->NextSiblingElement("Precondition")) {
scene.m_Preconditions.push_back(std::make_pair(Preconditions::CreateExpression(element->Attribute("expression")), element->BoolAttribute("not")));
}
for (tinyxml2::XMLElement* element = root->FirstChildElement("Object"); element; element = element->NextSiblingElement("Object")) {
const auto lot = element->UnsignedAttribute("lot");
const auto position = NiPoint3(element->FloatAttribute("x"), element->FloatAttribute("y"), element->FloatAttribute("z"));
const auto rotation = NiQuaternion(element->FloatAttribute("qx"), element->FloatAttribute("qy"), element->FloatAttribute("qz"), element->FloatAttribute("qw"));
scene.AddObject(lot, position, rotation);
}
for (tinyxml2::XMLElement* element = root->FirstChildElement("Prefab"); element; element = element->NextSiblingElement("Prefab")) {
const auto prefab = element->Attribute("file");
const auto position = NiPoint3(element->FloatAttribute("x"), element->FloatAttribute("y"), element->FloatAttribute("z"));
scene.AddPrefab(Prefab::LoadFromFile(prefab), position);
}
for (tinyxml2::XMLElement* element = root->FirstChildElement("NPC"); element; element = element->NextSiblingElement("NPC")) {
LOT npc = 2097253;
if (element->Attribute("lot")) {
npc = element->UnsignedAttribute("lot");
}
std::string name = "";
if (element->Attribute("name")) {
name = element->Attribute("name");
}
const auto act = element->Attribute("act");
scene.AddNPC(npc, name, Recording::Recorder::LoadFromFile(act));
}
LOG("Loaded scene from file: %s", file.c_str());
m_Scenes.emplace(file, scene);
return m_Scenes[file];
}
void Cinema::Scene::CommandRecordAct(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
EntityInfo info;
info.lot = 0;
info.pos = entity->GetPosition();
info.rot = entity->GetRotation();
info.scale = 1;
info.spawner = nullptr;
info.spawnerID = entity->GetObjectID();
info.spawnerNodeID = 0;
info.settings = {
new LDFData<std::vector<std::u16string>>(u"syncLDF", { u"custom_script_client" }),
new LDFData<std::u16string>(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua")
};
// If there is an argument, set the lot
const auto lotOptional = GeneralUtils::TryParse<LOT>(splitArgs[0]);
if (lotOptional) {
info.lot = lotOptional.value();
} else {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid lot.");
return;
}
// Spawn it
auto* actor = Game::entityManager->CreateEntity(info);
// If there is an argument, set the actors name
if (args.size() > 1) {
actor->SetVar(u"npcName", args[1]);
}
// Construct it
Game::entityManager->ConstructEntity(actor);
auto* record = Cinema::Recording::Recorder::GetRecorder(entity->GetObjectID());
if (record) {
record->Act(actor);
} else {
LOG("Failed to get recorder for objectID: %llu", entity->GetObjectID());
}
}
void Cinema::Scene::CommandRecordStart(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
auto splitArgs = GeneralUtils::SplitString(args, ' ');
Cinema::Recording::Recorder::StartRecording(entity->GetObjectID());
auto* record = Cinema::Recording::Recorder::GetRecorder(entity->GetObjectID());
if (record) {
if (args.size() > 0 && splitArgs[0] == "clear") {
record->AddRecord(new Cinema::Recording::ClearEquippedRecord());
}
else if (args.size() > 0 && splitArgs[0] == "copy") {
record->AddRecord(new Cinema::Recording::ClearEquippedRecord());
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent) {
for (const auto& [k, v] : inventoryComponent->GetEquippedItems()) {
auto* equipmentRecord = new Cinema::Recording::EquipRecord();
equipmentRecord->item = v.lot;
record->AddRecord(equipmentRecord);
}
}
}
} else {
LOG("Failed to get recorder for objectID: %llu", entity->GetObjectID());
}
}
void Cinema::Scene::CommandRecordStop(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
Cinema::Recording::Recorder::StopRecording(entity->GetObjectID());
}
void Cinema::Scene::CommandRecordSave(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
auto* record = Cinema::Recording::Recorder::GetRecorder(entity->GetObjectID());
if (record) {
record->SaveToFile(splitArgs[0]);
} else {
LOG("Failed to get recorder for objectID: %llu", entity->GetObjectID());
}
}
void Cinema::Scene::CommandRecordLoad(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
auto* record = Cinema::Recording::Recorder::LoadFromFile(splitArgs[0]);
if (record) {
Cinema::Recording::Recorder::AddRecording(entity->GetObjectID(), record);
} else {
LOG("Failed to load recording from file: %s", splitArgs[0].c_str());
}
}
void Cinema::Scene::CommandPrefabSpawn(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
const auto& prefab = Cinema::Prefab::LoadFromFile(splitArgs[0]);
float scale = 1.0f;
if (args.size() >= 2) {
const auto scaleOptional = GeneralUtils::TryParse<float>(splitArgs[1]);
if (scaleOptional) {
scale = scaleOptional.value();
}
}
size_t id = prefab.Instantiate(entity->GetPosition(), scale);
ChatPackets::SendSystemMessage(sysAddr, u"Spawned prefab with ID: " + GeneralUtils::to_u16string(id));
}
void Cinema::Scene::CommandPrefabDestroy(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
size_t id = GeneralUtils::TryParse<size_t>(splitArgs[0]).value_or(0);
if (id == 0) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid prefab ID.");
return;
}
Cinema::Prefab::DestroyInstance(id);
ChatPackets::SendSystemMessage(sysAddr, u"Destroyed prefab with ID: " + GeneralUtils::to_u16string(id));
return;
}
void Cinema::Scene::CommandSceneAct(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
auto& scene = Cinema::Scene::LoadFromFile(splitArgs[0]);
scene.Act(entity);
}
void Cinema::Scene::CommandSceneSetup(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
auto& scene = Cinema::Scene::LoadFromFile(splitArgs[0]);
scene.Rehearse();
}
void Cinema::Scene::CommandCompanion(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
EntityInfo info;
info.lot = 0;
info.pos = entity->GetPosition();
info.rot = entity->GetRotation();
info.scale = 1;
info.spawner = nullptr;
info.spawnerID = entity->GetObjectID();
info.spawnerNodeID = 0;
info.settings = {
new LDFData<std::vector<std::u16string>>(u"syncLDF", { u"custom_script_client" }),
new LDFData<std::u16string>(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua")
};
// If there is an argument, set the lot
const auto lotOptional = GeneralUtils::TryParse<LOT>(splitArgs[0]);
if (lotOptional) {
info.lot = lotOptional.value();
} else {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid lot.");
return;
}
// Spawn it
auto* actor = Game::entityManager->CreateEntity(info);
// If there is an argument, set the actors name
if (args.size() > 1) {
actor->SetVar(u"npcName", args[1]);
}
// Construct it
Game::entityManager->ConstructEntity(actor);
const auto follow = entity->GetObjectID();
actor->AddCallbackTimer(1.0f, [actor, follow]() {
Cinema::Recording::CompanionRecord::SetCompanion(actor, follow);
});
}
void Cinema::Scene::CommandCinematic(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
const auto path = splitArgs[0];
GameMessages::SendPlayCinematic(entity->GetObjectID(), GeneralUtils::UTF8ToUTF16(path), sysAddr);
}
void Cinema::Scene::CommandGhostReference(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
auto* ghostComponent = entity->GetComponent<GhostComponent>();
if (ghostComponent == nullptr) {
ChatPackets::SendSystemMessage(sysAddr, u"Entity does not have a ghost component.");
return;
}
const auto& ref = ghostComponent->GetGhostReferencePoint();
std::stringstream ss;
ss << "<" << ref.x << ", " << ref.y << ", " << ref.z << ">";
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::UTF8ToUTF16(ss.str()));
}

141
dGame/dCinema/Scene.h Normal file
View File

@@ -0,0 +1,141 @@
#pragma once
#include "Prefab.h"
#include "Recorder.h"
#include "Preconditions.h"
namespace Cinema
{
/**
* @brief A scene is a collection of prefabs, npcs and behaviors that can be loaded into the world.
*/
class Scene
{
public:
Scene() = default;
/**
* @brief Adds an object to the scene.
*
* @param lot The LOT of the object to add.
* @param position The position of the object to add.
* @param rotation The rotation of the object to add.
*/
void AddObject(LOT lot, NiPoint3 position, NiQuaternion rotation);
/**
* @brief Adds a prefab to the scene.
*
* @param prefab The prefab to add.
* @param position The position to add the prefab at.
*/
void AddPrefab(const Prefab& prefab, NiPoint3 position);
/**
* @brief Adds an NPC to the scene.
*
* @param npc The NPC to add.
* @param act The act to set for the NPC.
*/
void AddNPC(LOT npc, const std::string& name, Recording::Recorder* act);
/**
* @brief Set up the scene to be acted when a player enters the theater and meets the preconditions.
*/
void Rehearse();
/**
* @brief Conclude the scene for a given player.
*
* @param player The player to conclude the scene for (not nullptr).
*/
void Conclude(Entity* player);
/**
* @brief Checks if a given player is within the bounds of the scene.
*
* @param player The player to check.
*/
bool IsPlayerInBounds(Entity* player) const;
/**
* @brief Checks if a given player is within the showing distance of the scene.
*
* @param player The player to check.
*/
bool IsPlayerInShowingDistance(Entity* player) const;
/**
* @brief Checks if a given player is within the maximum showing distance of the scene.
*
* @param player The player to check.
*/
bool IsPlayerInMaximumShowingDistance(Entity* player) const;
/**
* @brief Act the scene.
*
* @param player The player to act the scene for (or nullptr to act for all players).
* @return The variables that were set by the scene.
*/
Play* Act(Entity* player);
/**
* @brief Loads a scene from the given file.
*
* @param file The file to load the scene from.
* @return The scene that was loaded.
*/
static Scene& LoadFromFile(std::string file);
/**
* @brief Automatically loads the scenes for a given zone.
*
* @param zone The zone to load the scenes for.
*/
static void AutoLoadScenesForZone(LWOMAPID zone);
private:
void CheckForShowings();
void CheckTicket(Entity* player);
std::vector<std::pair<LOT, std::pair<NiPoint3, NiQuaternion>>> m_Objects;
std::vector<std::pair<Prefab, NiPoint3>> m_Prefabs;
std::vector<std::pair<LOT, std::pair<Recording::Recorder*, std::string>>> m_NPCs;
NiPoint3 m_Center;
float m_Bounds = 0.0f;
float m_ShowingDistance = 0.0f;
float m_MaximumShowingDistance = 0.0f;
float m_ChanceToPlay = 1.0f;
bool m_Repeatable = true;
std::vector<std::pair<PreconditionExpression, bool>> m_Preconditions;
int32_t m_AcceptMission = 0;
int32_t m_CompleteMission = 0;
std::unordered_set<LWOOBJID> m_Audience;
std::unordered_set<LWOOBJID> m_HasBeenOutside;
std::unordered_set<LWOOBJID> m_VisitedPlayers;
static std::unordered_map<std::string, Scene> m_Scenes;
static void CommandRecordAct(Entity* entity, const SystemAddress& sysAddr, const std::string args);
static void CommandRecordStart(Entity* entity, const SystemAddress& sysAddr, const std::string args);
static void CommandRecordStop(Entity* entity, const SystemAddress& sysAddr, const std::string args);
static void CommandRecordSave(Entity* entity, const SystemAddress& sysAddr, const std::string args);
static void CommandRecordLoad(Entity* entity, const SystemAddress& sysAddr, const std::string args);
static void CommandPrefabSpawn(Entity* entity, const SystemAddress& sysAddr, const std::string args);
static void CommandPrefabDestroy(Entity* entity, const SystemAddress& sysAddr, const std::string args);
static void CommandSceneAct(Entity* entity, const SystemAddress& sysAddr, const std::string args);
static void CommandSceneSetup(Entity* entity, const SystemAddress& sysAddr, const std::string args);
static void CommandCompanion(Entity* entity, const SystemAddress& sysAddr, const std::string args);
static void CommandCinematic(Entity* entity, const SystemAddress& sysAddr, const std::string args);
static void CommandGhostReference(Entity* entity, const SystemAddress& sysAddr, const std::string args);
};
} // namespace Cinema

View File

@@ -0,0 +1,11 @@
<Prefab>
<Object lot="359" x="0" y="0" z="0" rx="0" ry="0" rz="0"/>
<Object lot="53" x="0" y="5" z="0" rx="0" rz="0">
<Effect id="60073"/>
</Object>
<Object lot="359" x="0" y="5" z="0" rx="0" ry="0" rz="0"/>
<Object lot="359" x="0" y="10" z="0" rx="0" ry="0" rz="0"/>
<Object lot="53" x="0" y="0" z="0" rx="0" rz="0"/>
<Object lot="53" x="0" y="10" z="0" rx="0" rz="0"/>
<Object lot="53" x="0" y="15" z="0" rx="0" rz="0"/>
</Prefab>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -0,0 +1,90 @@
<Recorder>
<MovementRecord x="-666.4151" y="1123.7156" z="-5.6110449" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.028999999"/>
<MovementRecord x="-666.3476" y="1123.7156" z="-5.2612491" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.3501134" vy="0.0033733833" vz="6.9959145" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.89099997"/>
<MovementRecord x="-666.01447" y="1123.717" z="-3.5337467" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-665.68048" y="1123.7185" z="-1.8018351" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-665.3465" y="1123.72" z="-0.069923788" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-664.98468" y="1123.7216" z="1.8063134" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-664.6507" y="1123.723" z="3.5382249" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-664.28888" y="1123.7246" z="5.4144602" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-663.92706" y="1123.7262" z="7.2906947" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-663.62091" y="1123.7275" z="8.8782806" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.16500001"/>
<MovementRecord x="-663.25909" y="1123.7291" z="10.754521" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-662.92511" y="1123.7306" z="12.486436" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-662.56329" y="1123.7322" z="14.362677" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-662.20148" y="1123.7338" z="16.238916" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-661.86749" y="1123.7352" z="17.970819" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-661.53351" y="1123.7367" z="19.702723" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-661.19952" y="1123.7382" z="21.434626" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755801" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-660.86554" y="1123.7396" z="23.166529" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-660.50372" y="1123.7412" z="25.042757" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-660.16974" y="1123.7427" z="26.77466" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-659.80792" y="1123.7443" z="28.650888" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.004175582" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-659.50177" y="1123.7456" z="30.238466" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-659.16779" y="1123.7471" z="31.970369" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-658.80597" y="1123.7487" z="33.8466" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-658.44415" y="1123.7502" z="35.722828" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-658.11017" y="1123.7517" z="37.454731" qx="0" qy="0.095176868" qz="0" qw="0.99546039" vx="1.6711727" vy="0.0041755815" vz="8.6595554" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-657.75244" y="1123.7533" z="39.331692" qx="0" qy="0.081039831" qz="0" qw="0.99671084" vx="1.4247334" vy="0.0041755605" vz="8.703496" avx="0" avy="-1.7030833" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-657.45453" y="1123.7382" z="41.2188" qx="0" qy="0.078293413" qz="0" qw="0.99693036" vx="1.3767529" vy="0.0041751657" vz="8.711215" avx="0" avy="0.020716017" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-657.17914" y="1123.7396" z="42.961048" qx="0" qy="0.078293413" qz="0" qw="0.99693036" vx="1.3767529" vy="0.0041751661" vz="8.711215" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-656.9267" y="1123.741" z="44.558109" qx="0" qy="0.078293413" qz="0" qw="0.99693036" vx="1.3767529" vy="0.0041751661" vz="8.711215" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-656.63043" y="1123.7426" z="46.445869" qx="0" qy="0.075789861" qz="0" qw="0.99712384" vx="1.3329875" vy="0.0041746967" vz="8.7180195" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-656.36383" y="1123.744" z="48.189491" qx="0" qy="0.075789861" qz="0" qw="0.99712384" vx="1.3329875" vy="0.0041746967" vz="8.7180195" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-656.10962" y="1123.7456" z="50.083252" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="1.0742536" vy="0.0041697873" vz="8.7536678" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.89502" y="1123.7471" z="51.833969" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="1.0742536" vy="0.0041697873" vz="8.7536678" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.66541" y="1123.7485" z="53.707008" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="0.90067899" vy="0.0034960457" vz="7.3392773" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-655.63428" y="1123.7485" z="53.960564" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="0.20638135" vy="0.0008010827" vz="1.6817197" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.63373" y="1123.7485" z="53.965019" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.198"/>
<SpeakRecord text="Hi!" t="1.584"/>
<MovementRecord x="-655.63373" y="1123.7485" z="53.965019" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="4.5869999"/>
<AnimationRecord animation="salute" t="4.0939999"/>
<MovementRecord x="-655.63373" y="1123.7485" z="53.965019" qx="0" qy="0.061016988" qz="0" qw="0.9981367" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="1.978"/>
<MovementRecord x="-655.63373" y="1123.7485" z="53.965019" qx="0" qy="0.0056772209" qz="0" qw="0.99998391" vx="0" vy="0" vz="0" avx="0" avy="-1.02" avz="0" g="true" dv="false" dav="true" t="1.914"/>
<MovementRecord x="-655.91522" y="1123.7493" z="55.306641" qx="0" qy="-0.16600978" qz="0" qw="0.9861241" vx="-2.8875616" vy="0.0036441826" vz="8.3332291" avx="0" avy="-2.2004657" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-656.8963" y="1123.7493" z="56.298153" qx="0" qy="-0.60278374" qz="0" qw="0.79790461" vx="-8.4835682" vy="0.00041772827" vz="2.4103563" avx="0" avy="-5.2572789" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-658.90973" y="1123.7493" z="56.085861" qx="0" qy="-0.81736016" qz="0" qw="0.57612699" vx="-8.3061113" vy="-0.0020730058" vz="-2.964668" avx="0" avy="-0.59541434" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-660.45416" y="1123.7493" z="55.155827" qx="0" qy="0.90611506" qz="0" qw="-0.42303139" vx="-6.7611709" vy="-0.0032021664" vz="-5.6627994" avx="0" avy="-1.0992103" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-661.45642" y="1123.7483" z="53.758194" qx="0" qy="0.97830099" qz="0" qw="-0.20718889" vx="-3.5752378" vy="-0.0040555298" vz="-8.0621586" avx="0" avy="-2.5476563" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-661.73975" y="1123.7469" z="51.782887" qx="0" qy="0.99977756" qz="0" qw="-0.021090925" vx="-0.37193322" vy="-0.0041383081" vz="-8.811492" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-661.96057" y="1123.7456" z="50.093998" qx="0" qy="0.99636191" qz="0" qw="-0.085222892" vx="-1.49775" vy="-0.0041759168" vz="-8.6912289" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-662.29297" y="1123.744" z="48.224056" qx="0" qy="0.99613321" qz="0" qw="-0.087855853" vx="-1.5436686" vy="-0.0041759908" vz="-8.6831913" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-662.57861" y="1123.7426" z="46.483566" qx="0" qy="0.99748456" qz="0" qw="-0.070884049" vx="-1.2471558" vy="-0.004173473" vz="-8.7307119" avx="0" avy="-0.30132243" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-662.83026" y="1123.741" z="44.589401" qx="0" qy="0.99784493" qz="0" qw="-0.065616116" vx="-1.1548871" vy="-0.0041717072" vz="-8.7433949" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.16500001"/>
<MovementRecord x="-663.065" y="1123.7395" z="42.841267" qx="0" qy="0.99736089" qz="0" qw="-0.07260374" vx="-1.2772541" vy="-0.0041739475" vz="-8.7263594" avx="0" avy="0.30980942" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-663.34338" y="1123.7379" z="40.95084" qx="0" qy="0.99732316" qz="0" qw="-0.073120199" vx="-1.286291" vy="-0.0041740802" vz="-8.7250319" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-663.62146" y="1123.7363" z="39.060375" qx="0" qy="0.99733573" qz="0" qw="-0.072948046" vx="-1.2832787" vy="-0.0041740369" vz="-8.7254753" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-663.89001" y="1123.7351" z="37.317089" qx="0" qy="0.99676812" qz="0" qw="-0.080332734" vx="-1.4123834" vy="-0.0036389432" vz="-8.7055092" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-664.20569" y="1123.7341" z="35.432461" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036370354" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-664.52148" y="1123.7332" z="33.547852" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.003687297" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-664.81299" y="1123.7324" z="31.808212" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0037204367" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-665.10449" y="1123.7316" z="30.068573" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036486434" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-665.396" y="1123.7307" z="28.328934" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036817831" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-665.6875" y="1123.7299" z="26.589294" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036099749" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-666.0033" y="1123.729" z="24.704685" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036602514" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-666.31311" y="1123.728" z="22.855684" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036934214" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-666.604" y="1123.7273" z="21.119551" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036277049" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-666.9198" y="1123.7263" z="19.234941" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036779814" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-667.2356" y="1123.7255" z="17.350332" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0037282431" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-667.52557" y="1123.7247" z="15.619577" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036350512" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-667.84137" y="1123.7238" z="13.734968" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036328458" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-668.15717" y="1123.7228" z="11.850359" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036831074" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-668.47296" y="1123.7219" z="9.9657497" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.003733384" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-668.76447" y="1123.7212" z="8.2261105" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036444538" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-669.05597" y="1123.7202" z="6.4864712" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036947303" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-669.37177" y="1123.7194" z="4.601862" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.003744992" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-669.66327" y="1123.7186" z="2.8622227" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036560618" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-669.93048" y="1123.7178" z="1.2675533" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.003689202" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-670.24628" y="1123.7169" z="-0.61705661" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036174082" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-670.56207" y="1123.7159" z="-2.5016661" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036152028" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-670.85358" y="1123.7151" z="-4.2413054" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0037179464" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-671.16937" y="1123.7142" z="-6.1259146" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036986046" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-671.46088" y="1123.7134" z="-7.8655539" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036268109" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-671.77667" y="1123.7124" z="-9.7501631" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036770874" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-672.04388" y="1123.7118" z="-11.344832" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036406391" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-672.35968" y="1123.7108" z="-13.229442" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0036384189" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-672.65118" y="1123.71" z="-14.969081" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-1.4567257" vy="-0.0037411624" vz="-8.6981993" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-672.89941" y="1123.7094" z="-16.451059" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-0.27985972" vy="-0.00032467872" vz="-1.6710598" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-672.90015" y="1123.7094" z="-16.455486" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-3.193491e-08" vy="0.00038383523" vz="-1.7891979e-07" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-672.90015" y="1123.7094" z="-16.455486" qx="0" qy="0.99656022" qz="0" qw="-0.082872093" vx="-3.193491e-08" vy="0.00038383523" vz="-1.7891979e-07" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="4.8179998"/>
</Recorder>

View File

@@ -0,0 +1,58 @@
<Recorder>
<ClearEquippedRecord t="0"/>
<EquipRecord item="9856" t="0"/>
<EquipRecord item="6500" t="0"/>
<MovementRecord x="-557.53174" y="1128.7369" z="50.89016" qx="0" qy="0.72913098" qz="0" qw="0.68437415" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.39199999"/>
<JumpRecord label="check-player" t="0" />
<SpeakRecord name="join-me" text="Join me!" t="3.0" />
<PlayerProximityRecord name="check-player" distance="10" timeout="5" timeoutLabel="join-me" t="0" />
<SpeakRecord text="Welcome!" t="4.026"/>
<MovementRecord x="-557.53174" y="1128.7369" z="50.89016" qx="0" qy="0.72913098" qz="0" qw="0.68437415" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="2.046"/>
<MovementRecord x="-557.55542" y="1128.7369" z="50.891663" qx="0" qy="0.77823329" qz="0" qw="0.62797529" vx="-1.4221454" vy="3.3353501e-07" vz="0.090151384" avx="0" avy="188.49556" avz="0" g="true" dv="true" dav="true" t="0.46200001"/>
<MovementRecord x="-559.65845" y="1128.7369" z="51.024967" qx="0" qy="0.97620082" qz="0" qw="-0.21686859" vx="-16.871138" vy="3.9568704e-06" vz="1.0694356" avx="0" avy="80.788872" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-563.03271" y="1128.7369" z="51.238667" qx="0" qy="-0.68442959" qz="0" qw="0.72907895" vx="-16.871298" vy="3.9620759e-06" vz="1.0669085" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-566.12579" y="1128.7369" z="51.43425" qx="0" qy="-0.68442959" qz="0" qw="0.72907895" vx="-16.871298" vy="3.9620759e-06" vz="1.0669085" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-569.76996" y="1128.7369" z="51.779564" qx="0" qy="-0.65352851" qz="0" qw="0.75690192" vx="-16.724352" vy="1.0602414e-06" vz="2.4647665" avx="0" avy="0.29643202" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-573.31696" y="1128.7369" z="52.304493" qx="0" qy="-0.65330213" qz="0" qw="0.7570973" vx="-16.722874" vy="1.0393246e-06" vz="2.4747682" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-576.93988" y="1128.7369" z="52.840672" qx="0" qy="-0.65330213" qz="0" qw="0.7570973" vx="-16.722874" vy="1.0393246e-06" vz="2.4747682" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-580.00543" y="1128.7369" z="53.294361" qx="0" qy="-0.65330213" qz="0" qw="0.7570973" vx="-16.722874" vy="1.0393246e-06" vz="2.4747682" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-583.6225" y="1128.7369" z="53.869492" qx="0" qy="-0.64578879" qz="0" qw="0.76351613" vx="-16.6707" vy="3.4790952e-07" vz="2.8047707" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-587.23431" y="1128.7369" z="54.47718" qx="0" qy="-0.64578879" qz="0" qw="0.76351613" vx="-16.6707" vy="3.4790952e-07" vz="2.8047707" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-590.78857" y="1128.7369" z="55.154781" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.579926" vy="-6.9270078e-07" vz="3.2992506" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-594.10425" y="1128.7369" z="55.814648" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.579926" vy="-6.9270078e-07" vz="3.2992506" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-597.41992" y="1128.7369" z="56.474514" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.579926" vy="-6.9270078e-07" vz="3.2992506" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-600.37299" y="1129.1835" z="57.067936" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.550844" vy="-0.02764282" vz="3.293468" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-603.67401" y="1129.1776" z="57.794476" qx="0" qy="-0.60967135" qz="0" qw="0.79265428" vx="-16.338949" vy="-0.027341407" vz="4.3378429" avx="0" avy="0.68940604" avz="0" g="true" dv="true" dav="true" t="0.264"/>
<MovementRecord x="-607.30255" y="1128.5726" z="58.873291" qx="0" qy="-0.58935732" qz="0" qw="0.80787253" vx="-16.122616" vy="-15" vz="5.0832543" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-610.39264" y="1126.5226" z="59.848522" qx="0" qy="-0.58935732" qz="0" qw="0.80787253" vx="-13.727316" vy="-8.8299694" vz="4.4012947" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-612.91327" y="1124.842" z="60.493713" qx="0" qy="-0.64014912" qz="0" qw="0.7682507" vx="-13.939328" vy="-9.2160997" vz="2.5568676" avx="0" avy="-0.90714848" avz="0" g="true" dv="true" dav="true" t="0.495"/>
<MovementRecord x="-615.60901" y="1123.9226" z="60.914558" qx="0" qy="-0.65074474" qz="0" qw="0.7592966" vx="-16.705799" vy="0" vz="2.5875225" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.16500001"/>
<MovementRecord x="-619.20642" y="1123.9226" z="61.475426" qx="0" qy="-0.64724869" qz="0" qw="0.76227891" vx="-16.681313" vy="0" vz="2.7409482" avx="0" avy="0.31377235" avz="0" g="true" dv="true" dav="true" t="0.264"/>
<MovementRecord x="-622.5293" y="1123.9226" z="62.100258" qx="0" qy="-0.63752168" qz="0" qw="0.77043241" vx="-16.606367" vy="0" vz="3.163466" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.16500001"/>
<MovementRecord x="-626.12823" y="1123.7505" z="62.781879" qx="0" qy="-0.64108664" qz="0" qw="0.76746851" vx="-16.634985" vy="4.6775982e-05" vz="3.0093575" avx="0" avy="-0.30132243" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-629.71057" y="1123.7505" z="63.393055" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-16.665606" vy="-4.7832516e-05" vz="2.8348761" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-633.59003" y="1124.8367" z="64.052917" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-16.896" vy="20.297354" vz="2.8736222" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-637.22083" y="1127.7493" z="64.670441" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-19.241386" vy="8.9419651" vz="3.2726972" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-641.23969" y="1128.4006" z="65.354004" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-20.97077" vy="-2.3861933" vz="3.5669582" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-645.92755" y="1128.7273" z="66.15139" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-22.068434" vy="11.44259" vz="3.7537298" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-650.06995" y="1129.8671" z="66.85601" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-22.994522" vy="0.9925909" vz="3.9113069" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.15118" y="1128.7443" z="67.720322" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-23.803282" vy="-11.357409" vz="4.0489192" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-659.56635" y="1124.9456" z="68.471329" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-7.8723621" vy="-23.707413" vz="1.3390911" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-660.31464" y="1123.7504" z="68.5979" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-1.6779066" vy="1.007928e-05" vz="0.28236043" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-660.31915" y="1123.7504" z="68.598663" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.198"/>
<MovementRecord x="-660.29572" y="1123.7504" z="68.594681" qx="0" qy="-0.5861969" qz="0" qw="0.81016856" vx="1.4048207" vy="2.2637594e-05" vz="-0.23896487" avx="0" avy="188.49556" avz="0" g="true" dv="true" dav="true" t="0.29699999"/>
<MovementRecord x="-658.21826" y="1123.7504" z="68.241364" qx="0" qy="0.26788518" qz="0" qw="0.96345085" vx="16.665703" vy="8.9682879e-05" vz="-2.8343182" avx="0" avy="80.787056" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-655.93872" y="1123.7504" z="67.853683" qx="0" qy="0.47554708" qz="0" qw="0.87969029" vx="4.0222421" vy="4.9844002e-05" vz="-0.6840589" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.87488" y="1123.7504" z="67.842827" qx="0" qy="0.47554708" qz="0" qw="0.87969029" vx="1.2125849" vy="3.9280545e-05" vz="-0.20622317" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.87488" y="1123.7504" z="67.842827" qx="0" qy="0.47554708" qz="0" qw="0.87969029" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.23100001"/>
<MovementRecord x="-655.04889" y="1123.7504" z="67.242249" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="4.6101446" vy="-0.0011672372" vz="-3.3521008" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="1.1525371" vy="-0.00029180956" vz="-0.83802569" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.198"/>
<SpeakRecord text="Great!&quot;" t="2.3759999"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="3.6960001"/>
</Recorder>

View File

@@ -0,0 +1,50 @@
<Recorder>
<ClearEquippedRecord t="0"/>
<EquipRecord item="9856" t="0"/>
<EquipRecord item="6500" t="0"/>
<MovementRecord x="-557.53174" y="1128.7369" z="50.89016" qx="0" qy="0.72913098" qz="0" qw="0.68437415" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.39199999"/>
<SpeakRecord text="Welcome!" t="4.026"/>
<MovementRecord x="-557.53174" y="1128.7369" z="50.89016" qx="0" qy="0.72913098" qz="0" qw="0.68437415" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="2.046"/>
<MovementRecord x="-557.55542" y="1128.7369" z="50.891663" qx="0" qy="0.77823329" qz="0" qw="0.62797529" vx="-1.4221454" vy="3.3353501e-07" vz="0.090151384" avx="0" avy="188.49556" avz="0" g="true" dv="true" dav="true" t="0.46200001"/>
<MovementRecord x="-559.65845" y="1128.7369" z="51.024967" qx="0" qy="0.97620082" qz="0" qw="-0.21686859" vx="-16.871138" vy="3.9568704e-06" vz="1.0694356" avx="0" avy="80.788872" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-563.03271" y="1128.7369" z="51.238667" qx="0" qy="-0.68442959" qz="0" qw="0.72907895" vx="-16.871298" vy="3.9620759e-06" vz="1.0669085" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-566.12579" y="1128.7369" z="51.43425" qx="0" qy="-0.68442959" qz="0" qw="0.72907895" vx="-16.871298" vy="3.9620759e-06" vz="1.0669085" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-569.76996" y="1128.7369" z="51.779564" qx="0" qy="-0.65352851" qz="0" qw="0.75690192" vx="-16.724352" vy="1.0602414e-06" vz="2.4647665" avx="0" avy="0.29643202" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-573.31696" y="1128.7369" z="52.304493" qx="0" qy="-0.65330213" qz="0" qw="0.7570973" vx="-16.722874" vy="1.0393246e-06" vz="2.4747682" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-576.93988" y="1128.7369" z="52.840672" qx="0" qy="-0.65330213" qz="0" qw="0.7570973" vx="-16.722874" vy="1.0393246e-06" vz="2.4747682" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-580.00543" y="1128.7369" z="53.294361" qx="0" qy="-0.65330213" qz="0" qw="0.7570973" vx="-16.722874" vy="1.0393246e-06" vz="2.4747682" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-583.6225" y="1128.7369" z="53.869492" qx="0" qy="-0.64578879" qz="0" qw="0.76351613" vx="-16.6707" vy="3.4790952e-07" vz="2.8047707" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-587.23431" y="1128.7369" z="54.47718" qx="0" qy="-0.64578879" qz="0" qw="0.76351613" vx="-16.6707" vy="3.4790952e-07" vz="2.8047707" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-590.78857" y="1128.7369" z="55.154781" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.579926" vy="-6.9270078e-07" vz="3.2992506" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-594.10425" y="1128.7369" z="55.814648" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.579926" vy="-6.9270078e-07" vz="3.2992506" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-597.41992" y="1128.7369" z="56.474514" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.579926" vy="-6.9270078e-07" vz="3.2992506" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-600.37299" y="1129.1835" z="57.067936" qx="0" qy="-0.63436413" qz="0" qw="0.77303439" vx="-16.550844" vy="-0.02764282" vz="3.293468" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-603.67401" y="1129.1776" z="57.794476" qx="0" qy="-0.60967135" qz="0" qw="0.79265428" vx="-16.338949" vy="-0.027341407" vz="4.3378429" avx="0" avy="0.68940604" avz="0" g="true" dv="true" dav="true" t="0.264"/>
<MovementRecord x="-607.30255" y="1128.5726" z="58.873291" qx="0" qy="-0.58935732" qz="0" qw="0.80787253" vx="-16.122616" vy="-15" vz="5.0832543" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-610.39264" y="1126.5226" z="59.848522" qx="0" qy="-0.58935732" qz="0" qw="0.80787253" vx="-13.727316" vy="-8.8299694" vz="4.4012947" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-612.91327" y="1124.842" z="60.493713" qx="0" qy="-0.64014912" qz="0" qw="0.7682507" vx="-13.939328" vy="-9.2160997" vz="2.5568676" avx="0" avy="-0.90714848" avz="0" g="true" dv="true" dav="true" t="0.495"/>
<MovementRecord x="-615.60901" y="1123.9226" z="60.914558" qx="0" qy="-0.65074474" qz="0" qw="0.7592966" vx="-16.705799" vy="0" vz="2.5875225" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.16500001"/>
<MovementRecord x="-619.20642" y="1123.9226" z="61.475426" qx="0" qy="-0.64724869" qz="0" qw="0.76227891" vx="-16.681313" vy="0" vz="2.7409482" avx="0" avy="0.31377235" avz="0" g="true" dv="true" dav="true" t="0.264"/>
<MovementRecord x="-622.5293" y="1123.9226" z="62.100258" qx="0" qy="-0.63752168" qz="0" qw="0.77043241" vx="-16.606367" vy="0" vz="3.163466" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.16500001"/>
<MovementRecord x="-626.12823" y="1123.7505" z="62.781879" qx="0" qy="-0.64108664" qz="0" qw="0.76746851" vx="-16.634985" vy="4.6775982e-05" vz="3.0093575" avx="0" avy="-0.30132243" avz="0" g="true" dv="true" dav="true" t="0.23100001"/>
<MovementRecord x="-629.71057" y="1123.7505" z="63.393055" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-16.665606" vy="-4.7832516e-05" vz="2.8348761" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-633.59003" y="1124.8367" z="64.052917" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-16.896" vy="20.297354" vz="2.8736222" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-637.22083" y="1127.7493" z="64.670441" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-19.241386" vy="8.9419651" vz="3.2726972" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-641.23969" y="1128.4006" z="65.354004" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-20.97077" vy="-2.3861933" vz="3.5669582" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-645.92755" y="1128.7273" z="66.15139" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-22.068434" vy="11.44259" vz="3.7537298" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-650.06995" y="1129.8671" z="66.85601" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-22.994522" vy="0.9925909" vz="3.9113069" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.15118" y="1128.7443" z="67.720322" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-23.803282" vy="-11.357409" vz="4.0489192" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-659.56635" y="1124.9456" z="68.471329" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-7.8723621" vy="-23.707413" vz="1.3390911" avx="0" avy="0" avz="0" g="false" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-660.31464" y="1123.7504" z="68.5979" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="-1.6779066" vy="1.007928e-05" vz="0.28236043" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.23100001"/>
<MovementRecord x="-660.31915" y="1123.7504" z="68.598663" qx="0" qy="-0.6450991" qz="0" qw="0.76409894" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.198"/>
<MovementRecord x="-660.29572" y="1123.7504" z="68.594681" qx="0" qy="-0.5861969" qz="0" qw="0.81016856" vx="1.4048207" vy="2.2637594e-05" vz="-0.23896487" avx="0" avy="188.49556" avz="0" g="true" dv="true" dav="true" t="0.29699999"/>
<MovementRecord x="-658.21826" y="1123.7504" z="68.241364" qx="0" qy="0.26788518" qz="0" qw="0.96345085" vx="16.665703" vy="8.9682879e-05" vz="-2.8343182" avx="0" avy="80.787056" avz="0" g="true" dv="true" dav="true" t="0.198"/>
<MovementRecord x="-655.93872" y="1123.7504" z="67.853683" qx="0" qy="0.47554708" qz="0" qw="0.87969029" vx="4.0222421" vy="4.9844002e-05" vz="-0.6840589" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.87488" y="1123.7504" z="67.842827" qx="0" qy="0.47554708" qz="0" qw="0.87969029" vx="1.2125849" vy="3.9280545e-05" vz="-0.20622317" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-655.87488" y="1123.7504" z="67.842827" qx="0" qy="0.47554708" qz="0" qw="0.87969029" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.23100001"/>
<MovementRecord x="-655.04889" y="1123.7504" z="67.242249" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="4.6101446" vy="-0.0011672372" vz="-3.3521008" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="1.1525371" vy="-0.00029180956" vz="-0.83802569" avx="0" avy="0" avz="0" g="true" dv="true" dav="false" t="0.198"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="0.198"/>
<SpeakRecord text="Great!&quot;" t="2.3759999"/>
<MovementRecord x="-654.93365" y="1123.7504" z="67.158447" qx="0" qy="0.85166776" qz="0" qw="0.52408218" vx="0" vy="0" vz="0" avx="0" avy="0" avz="0" g="true" dv="false" dav="false" t="3.6960001"/>
</Recorder>

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,5 @@
<Scene>
<Prefab file="vanity/prefabs/SFlagSmall.xml" x="-655.564" y="1123.76" z="75.993"/>
<NPC name="John" act="vanity/acts/my-act.xml"/>
<NPC name="Bill" act="vanity/acts/my-act-2.xml"/>
</Scene>

View File

@@ -611,7 +611,23 @@ void BaseCombatAIComponent::ClearThreat() {
m_DirtyThreat = true;
}
void BaseCombatAIComponent::SetStartPosition(const NiPoint3& position) {
m_StartPosition = position;
}
void BaseCombatAIComponent::Wander() {
if (m_FocusPosition != NiPoint3Constant::ZERO) {
m_MovementAI->SetHaltDistance(m_FocusRadius);
m_MovementAI->SetDestination(m_FocusPosition);
m_MovementAI->SetMaxSpeed(m_TetherSpeed);
m_Timer += 0.5f;
return;
}
if (!m_MovementAI->AtFinalWaypoint()) {
return;
}
@@ -762,6 +778,38 @@ void BaseCombatAIComponent::SetAggroRadius(const float value) {
m_AggroRadius = value;
}
float BaseCombatAIComponent::GetSoftTetherRadius() const {
return m_SoftTetherRadius;
}
void BaseCombatAIComponent::SetSoftTetherRadius(const float value) {
m_SoftTetherRadius = value;
}
void BaseCombatAIComponent::SetHardTetherRadius(const float value) {
m_HardTetherRadius = value;
}
const NiPoint3& BaseCombatAIComponent::GetFocusPosition() const {
return m_FocusPosition;
}
void BaseCombatAIComponent::SetFocusPosition(const NiPoint3& value) {
m_FocusPosition = value;
}
float BaseCombatAIComponent::GetFocusRadius() const {
return m_FocusRadius;
}
void BaseCombatAIComponent::SetFocusRadius(const float value) {
m_FocusRadius = value;
}
float BaseCombatAIComponent::GetHardTetherRadius() const {
return m_HardTetherRadius;
}
void BaseCombatAIComponent::LookAt(const NiPoint3& point) {
if (m_Stunned) {
return;

View File

@@ -120,6 +120,12 @@ public:
*/
const NiPoint3& GetStartPosition() const;
/**
* Sets the position where the entity spawned
* @param position the position where the entity spawned
*/
void SetStartPosition(const NiPoint3& position);
/**
* Removes all threats for this entities, and thus chances for it attacking other entities
*/
@@ -196,6 +202,54 @@ public:
*/
void SetAggroRadius(float value);
/**
* Gets the soft tether radius
* @return the soft tether radius
*/
float GetSoftTetherRadius() const;
/**
* Sets the soft tether radius
* @param value the soft tether radius
*/
void SetSoftTetherRadius(float value);
/**
* Gets the hard tether radius
* @return the hard tether radius
*/
float GetHardTetherRadius() const;
/**
* Sets the hard tether radius
* @param value the hard tether radius
*/
void SetHardTetherRadius(float value);
/**
* Get the position that the entity should stay around
* @return the focus position
*/
const NiPoint3& GetFocusPosition() const;
/**
* Set the position that the entity should stay around
* @param position the focus position
*/
void SetFocusPosition(const NiPoint3& position);
/**
* Get the radius that the entity should stay around the focus position
* @return the focus radius
*/
float GetFocusRadius() const;
/**
* Set the radius that the entity should stay around the focus position
* @param radius the focus radius
*/
void SetFocusRadius(float radius);
/**
* Makes the entity look at a certain point in space
* @param point the point to look at
@@ -398,6 +452,16 @@ private:
// The amount of time a removed threat will be ignored for.
std::map<LWOOBJID, float> m_RemovedThreatList;
/**
* A position that the entity should stay around
*/
NiPoint3 m_FocusPosition;
/**
* How far the entity should stay from the focus position
*/
float m_FocusRadius = 0;
/**
* Whether the current entity is a mech enemy, needed as mechs tether radius works differently
* @return whether this entity is a mech

View File

@@ -22,6 +22,7 @@
#include "Mail.h"
#include "ZoneInstanceManager.h"
#include "WorldPackets.h"
#include "MessageType/Game.h"
#include <ctime>
CharacterComponent::CharacterComponent(Entity* parent, Character* character, const SystemAddress& systemAddress) : Component(parent) {
@@ -47,6 +48,45 @@ CharacterComponent::CharacterComponent(Entity* parent, Character* character, con
m_CountryCode = 0;
m_LastUpdateTimestamp = std::time(nullptr);
m_SystemAddress = systemAddress;
RegisterMsg(MessageType::Game::REQUEST_SERVER_OBJECT_INFO, this, &CharacterComponent::OnRequestServerObjectInfo);
}
bool CharacterComponent::OnRequestServerObjectInfo(GameMessages::GameMsg& msg) {
auto& request = static_cast<GameMessages::RequestServerObjectInfo&>(msg);
AMFArrayValue response;
response.Insert("visible", true);
response.Insert("objectID", std::to_string(request.targetForReport));
response.Insert("serverInfo", true);
auto& data = *response.InsertArray("data");
auto& cmptType = data.PushDebug("Character");
cmptType.PushDebug<AMFIntValue>("Component ID") = GeneralUtils::ToUnderlying(ComponentType);
cmptType.PushDebug<AMFIntValue>("Character's account ID") = m_Character->GetParentUser()->GetAccountID();
cmptType.PushDebug<AMFBoolValue>("Last log out time") = m_Character->GetLastLogin();
cmptType.PushDebug<AMFDoubleValue>("Seconds played this session") = 0;
cmptType.PushDebug<AMFBoolValue>("Editor enabled") = false;
cmptType.PushDebug<AMFDoubleValue>("Total number of seconds played") = m_TotalTimePlayed;
cmptType.PushDebug<AMFStringValue>("Total currency") = std::to_string(m_Character->GetCoins());
cmptType.PushDebug<AMFStringValue>("Currency able to be picked up") = std::to_string(m_DroppedCoins);
cmptType.PushDebug<AMFStringValue>("Tooltip flags value") = "0";
// visited locations
cmptType.PushDebug<AMFBoolValue>("is a GM") = m_GMLevel > eGameMasterLevel::CIVILIAN;
cmptType.PushDebug<AMFBoolValue>("Has PVP flag turned on") = m_PvpEnabled;
cmptType.PushDebug<AMFIntValue>("GM Level") = GeneralUtils::ToUnderlying(m_GMLevel);
cmptType.PushDebug<AMFIntValue>("Editor level") = GeneralUtils::ToUnderlying(m_EditorLevel);
cmptType.PushDebug<AMFStringValue>("Guild ID") = "0";
cmptType.PushDebug<AMFStringValue>("Guild Name") = "";
cmptType.PushDebug<AMFDoubleValue>("Reputation") = m_Reputation;
cmptType.PushDebug<AMFIntValue>("Current Activity Type") = GeneralUtils::ToUnderlying(m_CurrentActivity);
cmptType.PushDebug<AMFDoubleValue>("Property Clone ID") = m_Character->GetPropertyCloneID();
GameMessages::SendUIMessageServerToSingleClient("ToggleObjectDebugger", response, m_Parent->GetSystemAddress());
LOG("Handled!");
return true;
}
bool CharacterComponent::LandingAnimDisabled(int zoneID) {
@@ -81,6 +121,8 @@ CharacterComponent::~CharacterComponent() {
void CharacterComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
if (bIsInitialUpdate) {
if (!m_Character || !m_Character->GetParentUser()) return;
outBitStream.Write(m_ClaimCodes[0] != 0);
if (m_ClaimCodes[0] != 0) outBitStream.Write(m_ClaimCodes[0]);
outBitStream.Write(m_ClaimCodes[1] != 0);
@@ -100,7 +142,7 @@ void CharacterComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInit
outBitStream.Write(m_Character->GetEyebrows());
outBitStream.Write(m_Character->GetEyes());
outBitStream.Write(m_Character->GetMouth());
outBitStream.Write<uint64_t>(0); //AccountID, trying out if 0 works.
outBitStream.Write<uint64_t>(m_Character->GetParentUser()->GetAccountID());
outBitStream.Write(m_Character->GetLastLogin()); //Last login
outBitStream.Write<uint64_t>(0); //"prop mod last display time"
outBitStream.Write<uint64_t>(m_Uscore); //u-score
@@ -786,7 +828,7 @@ void CharacterComponent::AwardClaimCodes() {
subject << "%[RewardCodes_" << rewardCode << "_subjectText]";
std::ostringstream body;
body << "%[RewardCodes_" << rewardCode << "_bodyText]";
Mail::SendMail(LWOOBJID_EMPTY, "%[MAIL_SYSTEM_NOTIFICATION]", m_Parent, subject.str(), body.str(), attachmentLOT, 1);
Mail::SendMail(m_Parent, subject.str(), body.str(), attachmentLOT, 1);
}
}

View File

@@ -323,6 +323,8 @@ public:
Character* m_Character;
private:
bool OnRequestServerObjectInfo(GameMessages::GameMsg& msg);
/**
* The map of active venture vision effects
*/

View File

@@ -8,6 +8,10 @@ namespace RakNet {
class BitStream;
}
namespace GameMessages {
struct GameMsg;
}
class Entity;
/**
@@ -52,6 +56,10 @@ public:
protected:
void RegisterMsg(const MessageType::Game msgId, auto* self, const auto handler) {
m_Parent->RegisterMsg(msgId, std::bind(handler, self, std::placeholders::_1));
}
/**
* The entity that owns this component
*/

View File

@@ -150,11 +150,11 @@ uint32_t InventoryComponent::GetLotCount(const LOT lot) const {
return count;
}
uint32_t InventoryComponent::GetLotCountNonTransfer(LOT lot) const {
uint32_t InventoryComponent::GetLotCountNonTransfer(LOT lot, bool includeVault) const {
uint32_t count = 0;
for (const auto& inventory : m_Inventories) {
if (IsTransferInventory(inventory.second->GetType())) continue;
if (IsTransferInventory(inventory.second->GetType(), includeVault)) continue;
count += inventory.second->GetLotCount(lot);
}
@@ -274,7 +274,7 @@ void InventoryComponent::AddItem(
switch (sourceType) {
case 0:
Mail::SendMail(LWOOBJID_EMPTY, "Darkflame Universe", m_Parent, "Lost Reward", "You received an item and didn&apos;t have room for it.", lot, size);
Mail::SendMail(m_Parent, "%[MAIL_ACTIVITY_OVERFLOW_HEADER]", "%[MAIL_ACTIVITY_OVERFLOW_BODY]", lot, size);
break;
case 1:
@@ -305,21 +305,35 @@ bool InventoryComponent::RemoveItem(const LOT lot, const uint32_t count, eInvent
LOG("Attempted to remove 0 of item (%i) from the inventory!", lot);
return false;
}
if (inventoryType == INVALID) inventoryType = Inventory::FindInventoryTypeForLot(lot);
auto* inventory = GetInventory(inventoryType);
if (!inventory) return false;
if (inventoryType != eInventoryType::ALL) {
if (inventoryType == INVALID) inventoryType = Inventory::FindInventoryTypeForLot(lot);
auto* inventory = GetInventory(inventoryType);
if (!inventory) return false;
auto left = std::min<uint32_t>(count, inventory->GetLotCount(lot));
if (left != count) return false;
auto left = std::min<uint32_t>(count, inventory->GetLotCount(lot));
if (left != count) return false;
while (left > 0) {
auto* item = FindItemByLot(lot, inventoryType, false, ignoreBound);
if (!item) break;
const auto delta = std::min<uint32_t>(left, item->GetCount());
item->SetCount(item->GetCount() - delta, silent);
left -= delta;
while (left > 0) {
auto* item = FindItemByLot(lot, inventoryType, false, ignoreBound);
if (!item) break;
const auto delta = std::min<uint32_t>(left, item->GetCount());
item->SetCount(item->GetCount() - delta, silent);
left -= delta;
}
return true;
} else {
auto left = count;
for (const auto& inventory : m_Inventories | std::views::values) {
while (left > 0 && inventory->GetLotCount(lot) > 0) {
auto* item = inventory->FindItemByLot(lot, false, ignoreBound);
if (!item) break;
const auto delta = std::min<uint32_t>(item->GetCount(), left);
item->SetCount(item->GetCount() - delta, silent);
left -= delta;
}
}
return left == 0;
}
return true;
}
void InventoryComponent::MoveItemToInventory(Item* item, const eInventoryType inventory, const uint32_t count, const bool showFlyingLot, bool isModMoveAndEquip, const bool ignoreEquipped, const int32_t preferredSlot) {
@@ -1318,8 +1332,8 @@ BehaviorSlot InventoryComponent::FindBehaviorSlot(const eItemType type) {
}
}
bool InventoryComponent::IsTransferInventory(eInventoryType type) {
return type == VENDOR_BUYBACK || type == VAULT_ITEMS || type == VAULT_MODELS || type == TEMP_ITEMS || type == TEMP_MODELS || type == MODELS_IN_BBB;
bool InventoryComponent::IsTransferInventory(eInventoryType type, bool includeVault) {
return type == VENDOR_BUYBACK || (includeVault && (type == VAULT_ITEMS || type == VAULT_MODELS)) || type == TEMP_ITEMS || type == TEMP_MODELS || type == MODELS_IN_BBB;
}
uint32_t InventoryComponent::FindSkill(const LOT lot) {

View File

@@ -100,7 +100,7 @@ public:
* @param lot the lot to search for
* @return the amount of items this entity possesses of the specified lot
*/
uint32_t GetLotCountNonTransfer(LOT lot) const;
uint32_t GetLotCountNonTransfer(LOT lot, bool includeVault = true) const;
/**
* Returns the items that are currently equipped by this entity
@@ -373,7 +373,7 @@ public:
* @param type the inventory type to check
* @return if the inventory type is a temp inventory
*/
static bool IsTransferInventory(eInventoryType type);
static bool IsTransferInventory(eInventoryType type, bool includeVault = true);
/**
* Finds the skill related to the passed LOT from the ObjectSkills table

View File

@@ -99,17 +99,8 @@ void MissionComponent::AcceptMission(const uint32_t missionId, const bool skipCh
mission->Accept();
this->m_Missions.insert_or_assign(missionId, mission);
if (missionId == 1728) {
//Needs to send a mail
auto address = m_Parent->GetSystemAddress();
Mail::HandleNotificationRequest(address, m_Parent->GetObjectID());
}
}
void MissionComponent::CompleteMission(const uint32_t missionId, const bool skipChecks, const bool yieldRewards) {
// Get the mission first
auto* mission = this->GetMission(missionId);
@@ -521,7 +512,7 @@ void MissionComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) {
auto* mission = new Mission(this, missionId);
mission->LoadFromXml(*doneM);
mission->LoadFromXmlDone(*doneM);
doneM = doneM->NextSiblingElement();
@@ -536,9 +527,9 @@ void MissionComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) {
currentM->QueryAttribute("id", &missionId);
auto* mission = new Mission(this, missionId);
auto* mission = m_Missions.contains(missionId) ? m_Missions[missionId] : new Mission(this, missionId);
mission->LoadFromXml(*currentM);
mission->LoadFromXmlCur(*currentM);
if (currentM->QueryAttribute("o", &missionOrder) == tinyxml2::XML_SUCCESS && mission->IsMission()) {
mission->SetUniqueMissionOrderID(missionOrder);
@@ -574,20 +565,23 @@ void MissionComponent::UpdateXml(tinyxml2::XMLDocument& doc) {
auto* mission = pair.second;
if (mission) {
const auto complete = mission->IsComplete();
const auto completions = mission->GetCompletions();
auto* m = doc.NewElement("m");
if (complete) {
mission->UpdateXml(*m);
if (completions > 0) {
mission->UpdateXmlDone(*m);
done->LinkEndChild(m);
continue;
if (mission->IsComplete()) continue;
m = doc.NewElement("m");
}
if (mission->IsMission()) m->SetAttribute("o", mission->GetUniqueMissionOrderID());
mission->UpdateXml(*m);
mission->UpdateXmlCur(*m);
cur->LinkEndChild(m);
}

View File

@@ -182,6 +182,10 @@ const MovementAIInfo& MovementAIComponent::GetInfo() const {
return m_Info;
}
MovementAIInfo& MovementAIComponent::GetInfo() {
return m_Info;
}
bool MovementAIComponent::AdvanceWaypointIndex() {
if (m_PathIndex >= m_InterpolatedWaypoints.size()) {
return false;

View File

@@ -74,6 +74,12 @@ public:
*/
const MovementAIInfo& GetInfo() const;
/**
* Returns a mutable reference to the basic settings that this entity uses to move around
* @return a mutable reference to the basic settings that this entity uses to move around
*/
MovementAIInfo& GetInfo();
/**
* Set a destination point for the entity to move towards
* @param value the destination point to move towards

View File

@@ -84,7 +84,12 @@ dpEntity* PhysicsComponent::CreatePhysicsEntity(eReplicaComponentType type) {
} else if (info->physicsAsset == "env\\env_won_fv_gas-blocking-volume.hkx") {
toReturn = new dpEntity(m_Parent->GetObjectID(), 390.496826f, 111.467964f, 600.821534f, true);
m_Position.y -= (111.467964f * m_Parent->GetDefaultScale()) / 2;
} else {
// Leaving these out for now since they cause more issues than they solve in racing tracks without proper OBB checks.
} /* else if (info->physicsAsset == "env\\GFTrack_DeathVolume1_CaveExit.hkx") {
toReturn = new dpEntity(m_Parent->GetObjectID(), 112.416870f, 50.363434f, 87.679268f);
} else if (info->physicsAsset == "env\\GFTrack_DeathVolume2_RoadGaps.hkx") {
toReturn = new dpEntity(m_Parent->GetObjectID(), 48.386536f, 50.363434f, 259.361755f);
} */ else {
// LOG_DEBUG("This one is supposed to have %s", info->physicsAsset.c_str());
//add fallback cube:

View File

@@ -65,14 +65,7 @@ void QuickBuildComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsIni
outBitStream.Write(false);
}
// If build state is completed and we've already serialized once in the completed state,
// don't serializing this component anymore as this will cause the build to jump again.
// If state changes, serialization will begin again.
if (!m_StateDirty && m_State == eQuickBuildState::COMPLETED) {
outBitStream.Write0();
outBitStream.Write0();
return;
}
// BEGIN Scripted Activity
outBitStream.Write1();
@@ -90,36 +83,27 @@ void QuickBuildComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsIni
}
// END Scripted Activity
outBitStream.Write1();
outBitStream.Write(m_StateDirty || bIsInitialUpdate);
if (m_StateDirty || bIsInitialUpdate) {
outBitStream.Write(m_State);
outBitStream.Write(m_State);
outBitStream.Write(m_ShowResetEffect);
outBitStream.Write(m_Activator != nullptr);
outBitStream.Write(m_ShowResetEffect);
outBitStream.Write(m_Activator != nullptr);
outBitStream.Write(m_Timer);
outBitStream.Write(m_TimerIncomplete);
outBitStream.Write(m_Timer);
outBitStream.Write(m_TimerIncomplete);
if (bIsInitialUpdate) {
outBitStream.Write(false);
outBitStream.Write(m_ActivatorPosition);
outBitStream.Write(m_RepositionPlayer);
if (bIsInitialUpdate) {
outBitStream.Write(false); // IsChoiceBuild
outBitStream.Write(m_ActivatorPosition);
outBitStream.Write(m_RepositionPlayer);
}
m_StateDirty = false;
}
m_StateDirty = false;
}
void QuickBuildComponent::Update(float deltaTime) {
m_Activator = GetActivator();
// Serialize the quickbuild every so often, fixes the odd bug where the quickbuild is not buildable
/*if (m_SoftTimer > 0.0f) {
m_SoftTimer -= deltaTime;
}
else {
m_SoftTimer = 5.0f;
Game::entityManager->SerializeEntity(m_Parent);
}*/
SetActivator(GetActivator());
switch (m_State) {
case eQuickBuildState::OPEN: {
@@ -130,12 +114,12 @@ void QuickBuildComponent::Update(float deltaTime) {
const bool isSmashGroup = spawner != nullptr ? spawner->GetIsSpawnSmashGroup() : false;
if (isSmashGroup) {
m_TimerIncomplete += deltaTime;
ModifyIncompleteTimer(deltaTime);
// For reset times < 0 this has to be handled manually
if (m_TimeBeforeSmash > 0) {
if (m_TimerIncomplete >= m_TimeBeforeSmash - 4.0f) {
m_ShowResetEffect = true;
if (m_TimerIncomplete >= m_TimeBeforeSmash - 4.0f && !m_ShowResetEffect) {
SetShowResetEffect(true);
Game::entityManager->SerializeEntity(m_Parent);
}
@@ -153,21 +137,19 @@ void QuickBuildComponent::Update(float deltaTime) {
break;
}
case eQuickBuildState::COMPLETED: {
m_Timer += deltaTime;
ModifyTimer(deltaTime);
// For reset times < 0 this has to be handled manually
if (m_ResetTime > 0) {
if (m_Timer >= m_ResetTime - 4.0f) {
if (!m_ShowResetEffect) {
m_ShowResetEffect = true;
if (m_Timer >= m_ResetTime - 4.0f && !m_ShowResetEffect) {
SetShowResetEffect(true);
Game::entityManager->SerializeEntity(m_Parent);
}
Game::entityManager->SerializeEntity(m_Parent);
}
if (m_Timer >= m_ResetTime) {
GameMessages::SendDieNoImplCode(m_Parent, LWOOBJID_EMPTY, LWOOBJID_EMPTY, eKillType::VIOLENT, u"", 0.0f, 0.0f, 0.0f, false, true);
GameMessages::SendDieNoImplCode(m_Parent, LWOOBJID_EMPTY, LWOOBJID_EMPTY, eKillType::VIOLENT, u"", 0.0f, 0.0f, 7.0f, false, true);
ResetQuickBuild(false);
}
@@ -185,9 +167,9 @@ void QuickBuildComponent::Update(float deltaTime) {
}
m_TimeBeforeDrain -= deltaTime;
m_Timer += deltaTime;
m_TimerIncomplete = 0;
m_ShowResetEffect = false;
ModifyTimer(deltaTime);
SetIncompleteTimer(0.0f);
SetShowResetEffect(false);
if (m_TimeBeforeDrain <= 0.0f) {
m_TimeBeforeDrain = m_CompleteTime / static_cast<float>(m_TakeImagination);
@@ -215,12 +197,12 @@ void QuickBuildComponent::Update(float deltaTime) {
break;
}
case eQuickBuildState::INCOMPLETE: {
m_TimerIncomplete += deltaTime;
ModifyIncompleteTimer(deltaTime);
// For reset times < 0 this has to be handled manually
if (m_TimeBeforeSmash > 0) {
if (m_TimerIncomplete >= m_TimeBeforeSmash - 4.0f) {
m_ShowResetEffect = true;
if (m_TimerIncomplete >= m_TimeBeforeSmash - 4.0f && !m_ShowResetEffect) {
SetShowResetEffect(true);
Game::entityManager->SerializeEntity(m_Parent);
}
@@ -260,7 +242,7 @@ void QuickBuildComponent::SpawnActivator() {
info.spawnerID = m_Parent->GetObjectID();
info.pos = m_ActivatorPosition == NiPoint3Constant::ZERO ? m_Parent->GetPosition() : m_ActivatorPosition;
m_Activator = Game::entityManager->CreateEntity(info, nullptr, m_Parent);
SetActivator(Game::entityManager->CreateEntity(info, nullptr, m_Parent));
if (m_Activator) {
m_ActivatorId = m_Activator->GetObjectID();
Game::entityManager->ConstructEntity(m_Activator);
@@ -277,7 +259,7 @@ void QuickBuildComponent::DespawnActivator() {
m_Activator->ScheduleKillAfterUpdate();
m_Activator = nullptr;
SetActivator(nullptr);
m_ActivatorId = LWOOBJID_EMPTY;
}
@@ -405,8 +387,7 @@ void QuickBuildComponent::StartQuickBuild(Entity* const user) {
GameMessages::SendQuickBuildNotifyState(m_Parent, m_State, eQuickBuildState::BUILDING, user->GetObjectID());
GameMessages::SendEnableQuickBuild(m_Parent, true, false, false, eQuickBuildFailReason::NOT_GIVEN, 0.0f, user->GetObjectID());
m_State = eQuickBuildState::BUILDING;
m_StateDirty = true;
SetState(eQuickBuildState::BUILDING);
Game::entityManager->SerializeEntity(m_Parent);
auto* movingPlatform = m_Parent->GetComponent<MovingPlatformComponent>();
@@ -444,9 +425,8 @@ void QuickBuildComponent::CompleteQuickBuild(Entity* const user) {
GameMessages::SendTerminateInteraction(user->GetObjectID(), eTerminateType::FROM_INTERACTION, m_Parent->GetObjectID());
m_State = eQuickBuildState::COMPLETED;
m_StateDirty = true;
m_Timer = 0.0f;
SetState(eQuickBuildState::COMPLETED);
SetTimer(0.0f);
m_DrainedImagination = 0;
Game::entityManager->SerializeEntity(m_Parent);
@@ -526,11 +506,10 @@ void QuickBuildComponent::ResetQuickBuild(const bool failed) {
GameMessages::SendQuickBuildNotifyState(m_Parent, m_State, eQuickBuildState::RESETTING, LWOOBJID_EMPTY);
m_State = eQuickBuildState::RESETTING;
m_StateDirty = true;
m_Timer = 0.0f;
m_TimerIncomplete = 0.0f;
m_ShowResetEffect = false;
SetState(eQuickBuildState::RESETTING);
SetTimer(0.0f);
SetIncompleteTimer(0.0f);
SetShowResetEffect(false);
m_DrainedImagination = 0;
Game::entityManager->SerializeEntity(m_Parent);
@@ -563,8 +542,7 @@ void QuickBuildComponent::CancelQuickBuild(Entity* const entity, const eQuickBui
GameMessages::SendTerminateInteraction(m_Parent->GetObjectID(), eTerminateType::FROM_INTERACTION, m_Parent->GetObjectID());
// Now update the component itself
m_State = eQuickBuildState::INCOMPLETE;
m_StateDirty = true;
SetState(eQuickBuildState::INCOMPLETE);
// Notify scripts and possible subscribers
m_Parent->GetScript()->OnQuickBuildNotifyState(m_Parent, m_State);

View File

@@ -218,6 +218,48 @@ public:
* @param skipChecks whether or not to skip the check for the quickbuild not being completed
*/
void CancelQuickBuild(Entity* const builder, const eQuickBuildFailReason failReason, const bool skipChecks = false);
void SetState(const eQuickBuildState state) {
if (m_State == state) return;
m_State = state;
m_StateDirty = true;
}
void SetShowResetEffect(const bool value) {
if (m_ShowResetEffect == value) return;
m_ShowResetEffect = value;
m_StateDirty = true;
}
void SetActivator(Entity* const activator) {
if (m_Activator == activator) return;
m_Activator = activator;
m_StateDirty = true;
}
void SetTimer(const float value) {
if (m_Timer == value) return;
m_Timer = value;
m_StateDirty = true;
}
void ModifyTimer(const float value) {
if (value == 0.0f) return;
m_Timer += value;
m_StateDirty = true;
}
void SetIncompleteTimer(const float value) {
if (m_TimerIncomplete == value) return;
m_TimerIncomplete = value;
m_StateDirty = true;
}
void ModifyIncompleteTimer(const float value) {
if (value == 0.0f) return;
m_TimerIncomplete += value;
m_StateDirty = true;
}
private:
/**
* Whether or not the quickbuild state has been changed since we last serialized it.

View File

@@ -285,7 +285,7 @@ void RacingControlComponent::OnRacingClientReady(Entity* player) {
Game::entityManager->SerializeEntity(m_Parent);
}
void RacingControlComponent::OnRequestDie(Entity* player) {
void RacingControlComponent::OnRequestDie(Entity* player, const std::u16string& deathType) {
// Sent by the client when they collide with something which should smash
// them.
@@ -301,8 +301,9 @@ void RacingControlComponent::OnRequestDie(Entity* player) {
if (!racingPlayer.noSmashOnReload) {
racingPlayer.smashedTimes++;
LOG("Death type %s", GeneralUtils::UTF16ToWTF8(deathType).c_str());
GameMessages::SendDie(vehicle, vehicle->GetObjectID(), LWOOBJID_EMPTY, true,
eKillType::VIOLENT, u"", 0, 0, 90.0f, false, true, 0);
eKillType::VIOLENT, deathType, 0, 0, 90.0f, false, true, 0);
auto* destroyableComponent = vehicle->GetComponent<DestroyableComponent>();
uint32_t respawnImagination = 0;

View File

@@ -135,7 +135,7 @@ public:
/**
* Invoked when the client says it should be smashed.
*/
void OnRequestDie(Entity* player);
void OnRequestDie(Entity* player, const std::u16string& deathType = u"");
/**
* Invoked when the player has finished respawning.

View File

@@ -16,10 +16,20 @@
std::unordered_map<int32_t, float> RenderComponent::m_DurationCache{};
std::unordered_map<int32_t, std::vector<int32_t>> RenderComponent::m_AnimationGroupCache{};
RenderComponent::RenderComponent(Entity* const parentEntity, const int32_t componentId) : Component{ parentEntity } {
m_LastAnimationName = "";
if (componentId == -1) return;
const auto& it = m_AnimationGroupCache.find(componentId);
if (it != m_AnimationGroupCache.end()) {
m_animationGroupIds = it->second;
return;
}
auto query = CDClientDatabase::CreatePreppedStmt("SELECT * FROM RenderComponent WHERE id = ?;");
query.bind(1, componentId);
auto result = query.execQuery();
@@ -43,6 +53,8 @@ RenderComponent::RenderComponent(Entity* const parentEntity, const int32_t compo
}
}
result.finalize();
m_AnimationGroupCache[componentId] = m_animationGroupIds;
}
void RenderComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {

View File

@@ -145,6 +145,11 @@ private:
* Cache of queries that look for the length of each effect, indexed by effect ID
*/
static std::unordered_map<int32_t, float> m_DurationCache;
/**
* Cache for animation groups, indexed by the component ID
*/
static std::unordered_map<int32_t, std::vector<int32_t>> m_AnimationGroupCache;
};
#endif // RENDERCOMPONENT_H

View File

@@ -18,6 +18,7 @@ target_include_directories(dGameMessages PUBLIC "."
"${PROJECT_SOURCE_DIR}/dGame/dBehaviors" # via InventoryComponent.h
"${PROJECT_SOURCE_DIR}/dGame/dInventory" # via InventoryComponent.h
"${PROJECT_SOURCE_DIR}/dGame/dEntity" # via dZoneManager/Spawner.h
"${PROJECT_SOURCE_DIR}/dGame/dCinema"
"${PROJECT_SOURCE_DIR}/dZoneManager" # via GameMessages.cpp, GameMessageHandler.cpp
)
target_precompile_headers(dGameMessages REUSE_FROM dGameBase)

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