171 Commits

Author SHA1 Message Date
Juansecu
b3d8a47777 Merge 2096c3c3cc into 397700e909 2024-10-07 12:38:45 +03:00
CakeLancelot
397700e909 Update version numbers in README 2024-10-06 23:26:36 -05:00
d9b6aedd5b Fix nested ifdefs for windows 2024-10-06 20:58:20 -07:00
CakeLancelot
145113062b Update tdata reference to fix invalid label 2024-10-05 16:51:39 -05:00
d717c5d74d Update tdata reference for the car paths 2024-09-22 04:28:28 +02:00
a6eb0e2349 Auth Cookie Support (#285)
* Auth cookie support

* Add config option for auth cookie support

* Safe handling of TEGid/auth_id strings

* Fix bad size calculation due to pointer cast

* Expiration timestamp instead of valid bit

* Change setting to "allowed auth methods"

This allows plaintext password auth to be disabled altogether

* PR feedback
2024-09-17 20:41:48 -07:00
52833f7fb3 Fix CI/CD 2024-09-05 13:43:57 -04:00
CakeLancelot
3aed24de26 Update download links in Getting Started 2024-07-14 18:52:38 -05:00
Gent Semaj
17362b2ea6 Client synchronization improvements (#283)
* Client synchronization improvements

* Remove bad comment

* Remove guard on PC_TICK

* Fix delayed loading of nano skill icons

We actually don't need to wait for post-load to do the second nano book send.
That adds unnecessary delay. Moving it to right after `P_FE2CL_REP_PC_ENTER_SUCC`
does the trick and gives the client plenty of time to fetch the icons before
loading in-game.

* Don't send unnecessary nano book subsets pre-enter

* Fix comment
2024-06-27 08:19:54 -05:00
gsemaj
47dbc6d35e Notify if player must log out and back in for access change 2024-06-23 18:41:43 -07:00
Gent Semaj
b780f5ee60 Enable account level changing at runtime (#282)
* Enable account level change at runtime

* PR feedback
2024-06-23 20:25:46 -05:00
Gent Semaj
003186d97a Immediate email notifications (#281) 2024-06-23 10:32:22 -05:00
Gent Semaj
6d2f120305 Add missing trade offer packets (#280) 2024-06-22 18:20:59 -05:00
Juansecu
2096c3c3cc BUILD - Optimize Dockerfile by using alpine as base image 2024-06-08 21:55:03 -05:00
Juansecu
51615db230 CD - Add GitHub Action to push Docker image to Docker Hub (#275)
* CD - Add GitHub Action to push Docker image to Docker Hub

* BUILD - Reduce size of image by using slim version of Debian

* CI - Modify Push Docker Image action to build and push Docker image to linux/amd64 and linux/arm64 architectures
2024-05-15 10:17:33 -05:00
CakeLancelot
233d21ecd7 Fix copy/paste error in docker-compose.yml 2024-05-07 23:01:09 -05:00
Gent Semaj
54327b0c23 Docker improvements (#274) 2024-05-07 22:11:14 -05:00
CakeLancelot
fa8c1e73d1 Fix a few compiler warnings and formatting for DB startup message (#272)
Note that the warning in TableData.cpp only seems to occur on clang (deprecated-declarations)

Database account/player count message:
Before: [INFO] Database in operation : Found 1 account(s) and 2 player(s)s
After: [INFO] Database in operation: Found 1 account and 2 players
2024-03-31 13:02:39 -05:00
CakeLancelot
aeac57ebf7 Remove duplicate headers from Makefile 2024-03-31 02:48:18 -05:00
CakeLancelot
632406e93b Bump nlohmann JSON to 3.11.3 (#271)
I've checked all the changelogs from the version we were using (3.9.1) to the latest, and there should not be any breaking changes or deprecated functionality.
The server also compiled fine and I did not encounter any issues after a brief play session.
2024-03-29 21:50:55 -05:00
837f109752 Add missing mutex 2024-02-03 03:13:43 -05:00
c11cfebdb1 Fix account and player counts on startup 2024-02-03 03:13:24 -05:00
20367d77f0 Bump copyright date 2024-02-02 22:01:38 -05:00
8d04f31c61 Default PROTOCOL_VERSION to 104 2024-02-02 21:58:37 -05:00
FinnHornhoover
44560a46b7 Rank Calculation Out-of-Bounds Fix (#268) 2023-12-24 02:49:51 +01:00
gsemaj
21d280147c Shard crash fixes 2023-12-19 13:23:58 -05:00
FinnHornhoover
b765821552 added option to disable score capping 2023-12-19 13:18:43 -05:00
e61682dfb2 fix: out-of-bounds index in itemDeleteHandler() 2023-12-18 04:41:53 -06:00
FinnHornhoover
d9ebb4e3ef only allow barkers of simple npcs
Co-authored-by: Gent Semaj <gsemaj@proton.me>
2023-12-12 06:01:30 -08:00
FinnHornhoover
73c610b471 randomized npcs for mission barkers 2023-12-12 06:01:30 -08:00
FinnHornhoover
3e6bfea3fe fixed unexpected randomization of barkers 2023-12-12 06:01:30 -08:00
FinnHornhoover
cd265af8e0 Original Racing Scores Functionality (#257) 2023-10-12 04:42:57 +02:00
CakeLancelot
38c68f351b Update README.md to fix some inaccurate info, move commands to wiki page 2023-10-12 04:00:16 +02:00
edfbe4d005 Make winTerminate() a WINAPI function
This macro makes it use the stdcall calling convention when compiling
for 32-bit Windows. Its absence was technically a bug, but it's a no-op
on 64-bit Windows.
2023-10-12 02:02:23 +02:00
96c430c994 Restore rapid-fire anti-cheat for projectiles
For a while we had made the temporary rapid-fire anti-cheat optional on
master, but had removed it entirely on the refactor branch. The
modification on master was acidentally only applied to the regular
(non-projectile) combat handler, while the removal on refactor removed
both that and the projectile check.

When the refactor branch was merged, that resulted in the removal of
only the projectile rapid-fire check, while the conditional regular
combat rapid-fire check was kept.

This change restores the projectile rapid-fire check such that it is
conditional, just like for regular combat.
2023-10-12 02:02:23 +02:00
gsemaj
4592fc42af CI/CD improvements
- PR builds will now run even if they weren't opened as drafts
- PR builds will now re-run when they get new commits
- Builds can now be manually triggered from GitHub
2023-10-10 14:35:07 -04:00
FinnHornhoover
70a27afad1 Pimpleback IZ Pod Fix (#259) 2023-10-08 17:02:32 -04:00
gsemaj
6cfb3bf532 Merge branch 'refactor' 2023-10-08 16:54:42 -04:00
9b2a65f8fd [refactor] More buff + skill fixes 2023-10-08 16:43:11 -04:00
6a69388822 Refactor and generalize NPCEvent logic 2023-10-08 16:43:11 -04:00
2924a27eb4 Rename coord args in summonNPC() and constructors to clarify purpose
This makes it clearer that the real coords aren't set until the first
call to updateNPCPosition().
2023-10-08 16:43:11 -04:00
ba20f5a401 Quick fix for Fuse boss fight NPCEvent logic
Will be replaced with a proper rework immediately.
2023-10-08 16:43:11 -04:00
gsemaj
eb88fa05cb reduce drain tickrate 2023-10-08 16:43:11 -04:00
gsemaj
0b73cef187 Clear player buffs on death if not revived 2023-10-08 16:43:11 -04:00
gsemaj
7af39b3d04 [refactor] Buff + skill bugfixes 2023-10-08 16:43:11 -04:00
gsemaj
33206b1207 Updater contributer guide 2023-10-08 16:43:11 -04:00
gsemaj
e325f7a40b Implement buff handling for CombatNPC 2023-10-08 16:43:11 -04:00
gsemaj
82bee2051a [refactor] Active power handling 2023-10-08 16:43:11 -04:00
gsemaj
4ece1bb89b [refactor] Sync with master
This was initially a merge commit that got ironed out by rebase cleanup.
Notable changes are:
- Copying EntityRef instead of referencing it
- Some changes to includes
2023-10-08 16:41:51 -04:00
gsemaj
31677e2638 [refactor] New buff framework (player implementation)
Get rid of `iConditionBitFlag` in favor of a system of individual buff
objects that get composited to a bitflag on-the-fly.
Buff objects can have callbacks for application, expiration, and tick,
making them pretty flexible. Scripting languages can eventually use
these for custom behavior, too.

TODO:
- Get rid of bitflag in BaseNPC
- Apply buffs from passive nano powers
- Apply buffs from active nano powers
- Move eggs to new system
- ???
2023-10-08 16:34:43 -04:00
gsemaj
d32827b692 The great re-#include
Was getting frustrated by the inconsistency in our include statements,
which were causing me problems. As a result, I went through and manually
re-organized every include statement in non-core files.

I'm just gonna copy my rant from Discord:
FOR HEADER FILES (.hpp):
- everything you use IN THE HEADER must be EXPLICITLY INCLUDED with the exception of things that fall under Core.hpp
- you may NOT include ANYTHING ELSE

FOR SOURCE FILES (.cpp):
- you can #include whatever you want as long as the partner header is included first
- anything that gets included by another include is fair game
- redundant includes are ok because they'll be harmless AS LONG AS our header files stay lean.

the point of this is NOT to optimize the number of includes used all around or make things more efficient necessarily. it's to improve readability & coherence and make it easier to avoid cyclical issues
2023-10-08 16:34:18 -04:00
gsemaj
13c009b448 Get rid of player fire rate suspicion
This was super primitive & jank, and caused false positives.
Will replace with a polished system later on.
2023-10-08 16:34:18 -04:00
gsemaj
a032497bed Handle case where cmake is invoked outside root 2023-10-08 16:34:18 -04:00
gsemaj
3b6b61d087 [refactor] SkillData additions for buffs 2023-10-08 16:34:18 -04:00
gsemaj
6d760f5bce Replace group filter operator with function 2023-10-08 16:33:54 -04:00
gsemaj
2a622f901c Ignore .bak files
for my local backups lol
2023-10-08 16:33:53 -04:00
gsemaj
03d28bf4e4 [refactor] Refactor groups 2023-10-08 16:33:53 -04:00
gsemaj
4b834579c5 [refactor] Remaining ICombatant implementation 2023-10-08 16:33:34 -04:00
gsemaj
07fe8ca367 (WIP) Remove BaseNPC::barkerType to save space 2023-10-08 16:32:49 -04:00
gsemaj
2f3f8a3951 [refactor] Initial ICombatant draft 2023-10-08 16:32:49 -04:00
4f890a9c07 [refactor] Get rid of NPC.hpp
This file was already obsoleted at the start of the refactor, but seems
to have escaped notice until now.
2023-10-08 16:31:53 -04:00
8517e0c7de [refactor] Replace a few uses of magic numbers with enums 2023-10-08 16:31:53 -04:00
5fb0cbbcf7 [refactor] Cosmetic cleanup in Fuse fight functions 2023-10-08 16:31:53 -04:00
55e9f6531d [refactor] Remove redundant coord args from most entity constructors
Mobs and CombatNPCs still need theirs in order to properly set their
roaming and spawn coords. Assignment of the latter has been moved to the
CombatNPC constructor, where it should have already been.
2023-10-08 16:31:53 -04:00
gsemaj
7726357fbe [WIP] Stub power handler 2023-10-08 16:31:53 -04:00
gsemaj
564c275d51 [WIP] Use EntityRef instead of CNSocket in ability handler 2023-10-08 16:31:53 -04:00
gsemaj
3ce9ae5f77 [WIP] Replace appearance data with individual fields
Storing certain things in appearance data and others in their own fields
was gross. Now everything is stored on the same level and functions have
been added to generate appearance data when it's needed by the client.
2023-10-08 16:31:53 -04:00
gsemaj
7c5b9a8105 [WIP] Rename Entity.type -> Entity.kind 2023-10-08 16:31:53 -04:00
gsemaj
258ff35e20 [WIP] Initial merge of ability namespaces & features 2023-10-08 16:31:53 -04:00
CakeLancelot
ab480d88f1 Update version numbers to 1.5 2023-10-07 18:20:04 -05:00
CakeLancelot
89772d763b CI: specify Ubuntu runner version and fix artifact zip name
We were technically already using 22.04 for a bit, it got updated without us noticing since the version was set to `ubuntu-latest`.
Affix the version to 22.04 so that it doesn't get unexpectedly updated again, and update the artifact's zip to reflect the change.
2023-10-07 17:13:13 -05:00
bd0cc3c212 Fix regression with /speed and /jump after previous change
Also changed the case values to use the client definitions.
2023-09-13 04:37:49 +02:00
c636c538eb Fix minor visual bug in setValuePlayer() 2023-09-02 20:59:34 +02:00
d3bef95a7f Fix /npcr
Also removed a redundant invocation of NPCManager::updateNPCPosition()
and simplified the surrounding code.
2023-08-20 05:06:16 +02:00
gsemaj
650f947451 Add .dockerignore file
Fixes an issue where if you had a version.h file generated from cmake,
it would be used in the Dockerfile even though the container uses make.
2023-08-19 18:22:21 +00:00
gsemaj
b12aecad63 Fix vscode launch configs for Windows 2023-07-11 13:52:22 -04:00
gsemaj
5bf0c8f3ea Add launch configurations for vscode 2023-07-11 12:29:47 -04:00
gsemaj
2ddc956c9b Fix sqlite casing and syntax error in cmakelists 2023-07-11 12:29:08 -04:00
gsemaj
4f0ae027a5 Add Dockerfile and docker-compose 2023-06-30 03:31:45 -04:00
23ab908366 Refuse to start if there are invalid NPC types in the JSONs
This fixes an issue where there server would start up fine even if NPC
types from a later build were found in the gruntwork file. Because of
the semantics of the C++ array indexing operator, the index into NPCData
in loadGruntworkPost() would silently create extra entries in the
nlohmann::json array, which would break future NPC type limit checks and
subsequently crash the server at the next invocation of /summonW, and
likely other places.

We fix this by refusing to start the server if any invalid NPC types are
found, because simply skipping them in the gruntwork file would silently
omit them from further writes to the gruntwork file, which would be
undesirable data loss.
2023-06-22 02:43:26 +02:00
be6a4c0a5d Enforce minimum supported libsqlite version
The server now checks the libsqlite both at compile time and on server
startup. The version the executable was built with and the one it's
running with may be different, so long as they're both at or above the
minimum supported version. One or both version numbers are printed on
startup, depending on if they're identical or not.

The compile-time ("Built with") version depends on the sqlite3.h header
used during compilation, while the runtime ("Using") version depends on
either:

* The sqlite3.c version used during compilation, if statically linked.
  (Which may be different from the header version and still compile and run
  fine.)
* The version of the libsqlite3.so or sqlite3.dll that the server
  loaded, if dynamically linked. Version mismatches here are normal,
  especially on Unix systems with their own system libraries.

The current minimum version is 3.33.0, from 2020-08-14, as that's the
one that introduced the UPDATE-FROM syntax used during login by
Database::updateSelectedByPlayerId().

Also rearranged the prints and initialization calls in main() slightly.
2023-03-19 01:41:07 +01:00
8eb1af20c8 Clean up tdata file loading logic slightly
The earlier addition of empty file checks made it just a bit too cumbersome.
2023-03-13 21:42:59 +01:00
e73daa0865 Skip loadGruntworkPre() if there's no gruntwork
Previously, only loadGruntworkPost() would be skipped if the gruntwork
was null, but it was never null at that point because loadGruntworkPre()
would inadvertently create gruntwork["paths"] when it indexes it to
iterate through it.

Now, the gruntwork loading messages will no longer be misleadingly
printed to stdout when there isn't a gruntwork file.
2023-03-13 05:58:49 +01:00
743a39c125 Tolerate empty gruntwork file
This prevents the server from failing to start if a gruntwork file
exists, but happens to be empty.
2023-03-13 05:18:27 +01:00
a9af8713bc Reject network messages too small for the packet size field 2023-03-12 01:45:18 +01:00
4825267537 Use memcpy() instead of casting to load keys
UBSAN complains about the casting approach because it loads a 64-bit
integer from the defaultKeys string which isn't guaranteed to be 64-bit
aligned, which is undefined behavior.
2023-03-11 23:16:09 +01:00
a92cfaff25 Differentiate new connection messages on the login and shard ports 2023-03-11 21:54:56 +01:00
abcfa3445b Move dead socket cleanup out of the poll() loop
This fixes a potential desync between the fds vector and the connections
map that could happen due to file descriptor number reuse.
2023-03-11 03:24:48 +01:00
2bf14200f7 Make CNSocket::kill() idempotent
CNSocket::kill() will now no longer call close() on already closed sockets.

close() should never be called on already closed file descriptors, yet
CNSocket::kill() was lacking any protection against that, despite its
use as both a high-level way of killing player connections and as a
means of ensuring that closing connections have been properly terminated
in the poll() loop.

This was causing close() to be erroneously called on each socket at least
one extra time. It was also introducing a race condition where the login
and shard threads could close each other's newly opened sockets due to
file descriptor reuse when a connection was accept()ed after the first
call to close(), but before the second one. See the close(2) manpage for
details.
2023-03-11 02:59:05 +01:00
876a9c82cd Bump copyright year to 2023 2023-03-06 21:08:45 +01:00
fb5b0eeeb9 Make socket connection state mismatch into a fatal error
These problems are usually not ephemeral, and cause persistent console
spam, so they should immediately crash the server so they can be
investigated.
2023-03-06 02:21:54 +01:00
7aabc507e7 Stop handling the current packet if the server is shutting down
Previously, terminating a running server from the terminal would
sometimes print a benign warning message if the server was currently
handling an incoming packet. This happened because CNServer::step()
would continue handling the packet after CNServer::kill() released the
activeCrit mutex. Now it first re-checks if active has been set to false
in the mean time after acquiring the mutex.
2023-03-06 02:21:54 +01:00
2914b95cff Combat: 3+ targets should automatically kick the connection 2023-03-01 11:18:41 -06:00
dbd2ec2270 Email: update the item slots via a ITEM_MOVE_SUCC packet 2023-02-28 15:15:57 -06:00
50e00a6772 Email: fix issue #186 2023-02-28 15:14:04 -06:00
7471bcbf38 Fix vehicle rental periods not showing up in vendor listings
Fixes #250.
2022-12-28 17:57:37 +01:00
100b4605ec Fix early CNShared timeout
Revisiting this again; the issue was that the comparison operator was
facing the wrong way, so connections were being pruned every 30 seconds
or less. This was effectively a race condition kicking an unlucky player
every so often when pruning happened exactly during an attempt to enter
the game.

Now that the proper timeout is being enforced, I've reduced it to 5
minutes, down from 15, since it really doesn't need to be that long.
2022-12-11 19:46:29 +01:00
741b898230 Remove redundant copy of Player object when added to the shard
Since the Player object is loaded up in loadPlayer() now, it's pretty
apparent that there's no more reason to copy it around at any point.
2022-12-06 02:11:31 +01:00
3f44f53f97 On login, load Player from DB in shard thread, not in login thread
This avoids some needless data shuffling and fixes a rare desync.
2022-12-06 01:07:21 +01:00
d92b407349 Fix sanity check in emailReceiveItemSingle() 2022-12-05 22:30:02 +01:00
9b3e856a05 Sync player data to DB when trading and sending emails 2022-12-05 22:29:23 +01:00
eb8e54c1f0 Do not evaluate timers if the server is shutting down
This should fix issues with segfaults when the server is being
terminated that sometimes occur because things like NPC path traversal
keep running while the process is executing the signal handler.
2022-11-27 22:33:55 +01:00
1ba0f5e14a Mention Linux instructions in README.md
Also clarify the line about CI binaries.
2022-11-26 20:30:07 +01:00
12dde394c0 Add undocumented config option to disable rapid fire anticheat
This quick hack has been around for a while, so we might as well make it
configurable.

Also updated tdata reference.
2022-11-26 19:36:10 +01:00
b1eea6d4fe [seccomp] Whitelist rseq syscall
Used by glibc 2.35 and later.
2022-11-15 02:30:20 +01:00
f126b88781 [seccomp] Whitelist newfstatat and fix a few #ifdefs
Some newer versions of either glibc or libsqlite3 seem to require this
syscall for the server to terminate properly.
2022-09-04 20:53:17 +02:00
2dbe2629c1 Tweak CNShared
* Separate pruning frequency from timeout
* Pluralize CNShared map: login -> logins
* Increase connection timeout to 15 minutes
* Do not deallocate a nullptr in playerEnter()
* Kill connections rejected by playerEnter()
* Remove redundant inclusions of mutex headers in a few places
2022-07-31 03:19:27 +02:00
271eef83d3 [seccomp] Add support for AArch64
This is useful for 64-bit Raspberry Pis and other 64-bit ARM systems.
2022-07-24 22:40:46 +02:00
ca0d608a87 Use cryptographic RNG to generate the shard connection serial key 2022-07-24 21:36:03 +02:00
741bfb675b Revamp CNShared logic
* Use a specialized connection object
* Copy the Player object less frequently
* Use a randomly generated serial key for shard auth
* Refuse invalid shard connection attempts
* Clean up connection metadata when a Player joins the shard
* Prune abandoned connections when they time out
2022-07-24 21:36:03 +02:00
c5dd745aa1 Rename CNSharedData namespace to CNShared to match the filename 2022-07-24 21:36:03 +02:00
998b12617e Reject packets sent before a connection has been fully established 2022-07-24 21:36:03 +02:00
129d1c2fe3 Use a specialized null value for ChunkPos
This prevents logic errors related to being in chunk 0 0 0.

Also:

* Moved some duplicated chunk teleportation logic to a new helper
  function
* Made ChunkPos into a proper class so it can default to INVALID_CHUNK
  when default-initialized
* Reversed the inclusion order of Chunking.hpp and Entities.hpp to work
  around problems with type definitions
2022-07-24 21:36:03 +02:00
CakeLancelot
1bd4d2fbee Cleanly remove player when an exit is requested
The client will actually do this itself when clicking the quit button in the tilde menu, but for the idle timer the connection would remain open until the game is closed.
2022-07-19 01:17:43 -05:00
63d4087488 Add config option to disable automatic account creation
Also moved the acceptallcustomnames setting to the login section where
it belongs.
2022-06-29 23:42:44 +02:00
abda9dc158 Bump copyright year to 2022 2022-06-28 23:13:39 +02:00
CakeLancelot
7b7d8bce45 Update README.md
Elaborate on server setup instructions a bit
2022-06-01 17:47:39 -05:00
gsemaj
a9942eadab Only upload artifacts from master 2022-04-09 13:31:05 -04:00
Gent Semaj
36638b1522 Update README with new artifact location 2022-04-07 15:29:54 -04:00
gsemaj
685cee2561 Fix directory names for artifacts 2022-04-07 10:17:42 -04:00
gsemaj
b683152fbf Fix git describe --tags not working in CI 2022-04-07 10:03:10 -04:00
gsemaj
86576d48f6 Run CI/CD for pull requests marked as ready for review 2022-04-06 19:33:38 -04:00
gsemaj
4e1767ad58 Fix CI/CD not zipping subfolders 2022-04-06 19:15:19 -04:00
Gent Semaj
4354cab7e3 Add CI/CD step to upload artifacts off-site (#244)
* Download and list artifacts after build

* Add commit hash + file extension to artifact name

* Initial SSH implementation

* Don't build artifacts for PRs

* Fetch endpoint from secret

* Zip artifacts before uploading to CDN

* Use short SHA in archive names
2022-04-06 18:24:34 -04:00
4f6979f236 Trigger check-builds on Makefile changes 2022-04-04 19:50:05 -05:00
1404fa0bb7 Removed references to Appveyor 2022-04-04 19:47:24 -05:00
7f65ec5b96 Fixed workflow badge 2022-04-04 19:41:05 -05:00
041908ddda CI: Moved to Github Workflows from Appveyor (#243)
YOLO
2022-04-04 20:38:05 -04:00
57c9f139a2 Fix quest item drop chances being shared between missions
In our original implementation, quest item drops were rolled on the
spot, so the chances of getting two quest items for different missions
in a single kill (where both missions have you kill the same mob) were
independent of each other.

When we made quest item drop chances shared between group members so
players doing missions together would progress at the same rate, we
accidentally linked the quest item odds of different missions together.

This change makes it so that the odds are per-task, so they're shared
between different group members doing the same tasks, but distinct for
different tasks being done by the same player.
2022-02-12 23:53:04 +01:00
d3af99fcef A few cosmetic changes in Missions.cpp
* Removed a redundant failure case in endTask()
* Fixed a misleading comment in startTask()
* Removed a redundant level check in updateFusionMatter()
* Cleared up misleading comment and code layout in taskEnd()
* Removed unnecessary comment in mobKilled()
2022-02-12 21:45:11 +01:00
94af318139 Work around a client bug related to simultanious quest item drops 2022-02-12 21:45:07 +01:00
91f9a2085b Fix three-space indentation in a few places 2022-02-11 23:22:31 +01:00
00865e1c7b Fix player state issue after failing to complete a mission
Fixes #225.

Co-authored-by: Jade <jadeshrinemaiden@gmail.com>
2022-02-11 23:20:40 +01:00
28bfd14362 Quick-fix for doDamage() crash
Couldn't get a reliable repro, but this is probably what that bug was.
It's not very throughly investigated, but we'll be tweaking those parts
of the codebase anyway, so we can examine if there's a deeper issue
later.
2022-02-08 17:02:42 +01:00
6412a9a89e Fix missing validation in Nanos::nanoEquipHandler() 2022-02-08 12:48:58 +01:00
f376c68115 [seccomp] Allow clock_nanosleep()
This apparently gets called very rarely during normal operation. This
change fixes a rare server crash.
2022-02-04 20:04:22 +01:00
3c6afa0322 Tolerate missing optional fields when loading gruntwork
These are already allowed to be absent in paths.json, but the gruntwork
loading logic wasn't updated accordingly.
2021-12-31 02:40:32 +01:00
384a2ece78 [sandbox] Seccomp filter tweaks
* Restrict fcntl() to only the flags we need
* Non-fatally deny tgkill() and rt_sigaction() so that segfaults don't
  result in a SIGSYS. They're debuggable either way, but this way it's
  clearer what the issue is right away.
* Allow truncate() and ftruncate() for sqlite's alternate journal modes
* Slight macro cleanup
* Add missing colon in a DB log message

We don't need to worry about compilation problems arising if glibc or
musl-libc add their own wrapper for the seccomp() syscall in the future.
Ours will/would just silently take precedence over the external one
without interfering with compilation. This should work regardless of
whether libc uses weak symbols and regardless of whether libc is
dynamically or statically linked into the executable. The wrapper's
signature has been stripped of its static and inline qualifiers, as it
must match the exact declaration the libc headers will/would use.

Further, if a pre-compiled binary is run on a system which genuinely
doesn't support seccomp(), it'll just return ENOSYS and the server will
terminate with an error. The user can then just disable the sandbox in
the config file. We don't need any special logic for that scenario.
2021-12-26 04:00:51 +01:00
CakeLancelot
f4a7ab7373 Update README to include installer instructions 2021-12-20 23:11:07 -06:00
bc1153c97e Call terminate() on Windows
Closes #196
2021-12-17 01:14:32 +01:00
c6ffcd4804 Clean up indentation in a few places 2021-12-16 03:34:15 +01:00
b3c844650b Tighten seccomp sandbox restrictions on mmap(), ioctl() and socketcall() 2021-12-16 00:36:48 +01:00
13bd299de4 Do not use assembly-accelerated bcrypt on i386
Adding the assembly source files to the build system(s) would be more
trouble than it's worth, considering we don't make 32-bit builds for
much other than satisfying our curiosity.
2021-12-16 00:36:48 +01:00
1e3d183f9a Add hardening flags to Makefile
Also tweaked the flags slightly so that CXXFLAGS are a superset of
CFLAGS, all optimization levels default to -O2, and .SUFFIXES works
correctly.
2021-12-16 00:36:48 +01:00
dfe596447b Whitelist syscalls for 32-bit x86 Linux
Should probably filter the args to this for the sake of proper
sandboxing.
2021-12-16 00:36:48 +01:00
9297e82589 Whitelist syscalls for musl-libc, Raspberry Pi and alt libsqlite configs 2021-12-16 00:36:48 +01:00
4319ee57a0 Switch seccomp sandbox to default-deny filter 2021-12-16 00:36:48 +01:00
09e452a09d pledge() + unveil() sandbox
This is the OpenBSD sandbox.
2021-12-16 00:36:43 +01:00
3c1e08372d Proof-of-concept, default-permit seccomp-bpf sandbox
Can be disabled by adding -DCONFIG_NOSANDBOX to CXXFLAGS.
2021-12-16 00:36:09 +01:00
05d6174351 Handle email dumping separately from chat dumping
This makes it actually possible to unambiguously parse the full thing
on the receiving end.
2021-12-03 22:23:59 +01:00
9ab998688c Fix time handling on systems with 32-bit time_t 2021-11-06 21:18:36 +01:00
7249b54127 Fix tdatadir and patchdir in config file requiring a terminating slash 2021-11-06 06:01:14 +01:00
4fa18a9642 Fix make not detecting changes to headers in vendor/ 2021-11-06 06:00:37 +01:00
gsemaj
8a294cb2be Include emails in chat dump 2021-10-25 15:32:26 -04:00
57e9834786 Fix U16toU8() returning strings longer than max
UTF-16 inputs containing actual multi-byte characters resulted in codecvt
returning a UTF-8 string longer than the requested max length.
2021-10-25 21:15:00 +02:00
70c3650ee1 Add config option to disable the localhost workaround
There are some network configurations in which it's undesirable; such as
reverse tunneling through ssh. These are obscure enough to allow leaving
the option undocumented (in the example config file).
2021-10-21 20:58:41 +02:00
e2c85aa03f Respawn players at half health
Cleaned up the adjacent code slightly; might clean it up further later.
2021-10-19 20:36:33 +02:00
Hpmason
ed285e5d24 Update version numbers in README.md
README still uses version 1.3 for links to client and server files. This changes those links to 1.4
2021-10-05 19:43:25 -04:00
0883ec4aae Update copyright in LICENSE file 2021-09-20 20:55:03 +02:00
CakeLancelot
2eb64540d1 Usernames are now case-insensitive
This fixes a UX issue, where if you accidentally capitalized a letter
in the username when logging in, it would instead create a new account.

The behavior was confusing, since to the user it looks as if their
characters were deleted or progress was not saved.

In order for this to work, duplicate accounts (e.g. username and USERNAME)
need to be deleted/renamed. The server will *detect* if any duplicates
exist. If any are found, it will direct the server operator to a pruning
script, or they can choose to resolve the duplicates manually.
2021-09-20 20:40:12 +02:00
bb4029a9bf Fix invisible group mobs bug
This is a simplified adaptation of
29e7bd25a4f888e9d72fa01f84df98de79f861d1 from Retrobution.

Co-authored-by: Jade <jadeshrinemaiden@gmail.com>
2021-09-19 04:55:10 +02:00
25ce0f6d82 Rewrite /lair command
This wasn't strictly necessary as this command has outlived its
usefulness, but I had gone ahead and rewritten it because it was (barring
taskStart()) the only place in the codebase that accesses Chunking::chunks
outside of Chunking.cpp. This became apparent during a (currently paused)
effort to improve the API that the Chunking namespace exposes to the
rest of the codebase.

I went ahead and rewrote the rest of this command as it was poorly
implemented anyway. This has been sitting in my working directory
uncommitted for a few months, so I may as well push it.
2021-09-19 04:55:10 +02:00
CakeLancelot
bab17eb23f Mobs can now get criticial hits
Explanation: it was uncertain whether mobs could perform critical hits, since the color of damage numbers didn't change at all. However, I found that male characters will actually use a different sound effect when receiving a crit (I confirmed this SFX appeared in old FF videos), so I went ahead and re-enabled it.
2021-09-05 13:34:27 -05:00
CakeLancelot
aaaf03128a Don't aggro to players using MSS 2021-09-05 13:23:05 -05:00
CakeLancelot
558d056bcf Update tdata ref
Fixes mission "Don't Fear the Reaper (4/4)" as well as a few other misplaced NPCs
2021-07-27 00:04:11 -05:00
gsemaj
0accd1f345 Make sure a vendor is actually selling the item a player wants to buy 2021-06-20 10:15:02 -04:00
CakeLancelot
bb12a60e04 Cleanly remove player after triggering rapidfire anticheat
Previously, the socket was killed but the player was still technically present.
2021-05-27 00:12:44 -05:00
CakeLancelot
8326ea6e26 Fix sliders stopping in place after one round-trip 2021-05-16 14:39:45 -05:00
81cc19f985 Fix failed task QItem handling, implement /itemQ command 2021-05-14 14:24:35 -05:00
19fd4ecb83 Various fixes in Trading.cpp 2021-05-14 14:21:18 -05:00
CakeLancelot
243e4f6d50 Add a fallback to racingCancel if a respawn point isn't found
Fixes segfault when canceling or timing out race in Mandark's House Future.

Also expanded on the comment for why this respawn is necessary.
2021-05-14 14:10:19 -05:00
112 changed files with 12219 additions and 12458 deletions

147
.github/workflows/check-builds.yaml vendored Normal file
View File

@@ -0,0 +1,147 @@
name: Check Builds
on:
push:
paths:
- src/**
- vendor/**
- .github/workflows/check-builds.yaml
- CMakeLists.txt
- Makefile
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
paths:
- src/**
- vendor/**
- CMakeLists.txt
- Makefile
workflow_dispatch:
jobs:
ubuntu-build:
runs-on: ubuntu-latest
steps:
- name: Set environment
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
shell: bash
- uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Install dependencies
run: sudo apt install clang cmake snap -y && sudo snap install powershell --classic
- name: Check compilation
run: |
$versions = "104", "728", "1013"
foreach ($version in $versions) {
Write-Output "Cleaning old output"
Invoke-Expression "make clean"
if ($LASTEXITCODE -ne "0") {
Write-Error "make clean failed for version $version" -ErrorAction Stop
}
Write-Output "Building version $version"
Invoke-Expression "make -j8 PROTOCOL_VERSION=$version"
if ($LASTEXITCODE -ne "0") {
Write-Error "make failed for version $version" -ErrorAction Stop
}
Rename-Item -Path "bin/fusion" -newName "$version-fusion"
Write-Output "Built version $version"
}
Copy-Item -Path "sql" -Destination "bin/sql" -Recurse
Copy-Item -Path "config.ini" -Destination "bin"
shell: pwsh
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}'
path: bin
windows-build:
runs-on: windows-2019
steps:
- name: Set environment
run: $s = $env:GITHUB_SHA.subString(0, 7); echo "SHORT_SHA=$s" >> $env:GITHUB_ENV
shell: pwsh
- uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Check compilation
run: |
$versions = "104", "728", "1013"
$configurations = "Release"
# "Debug" builds are disabled, since we don't really need them
$vsPath = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise"
Import-Module "$vsPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation
Invoke-Expression "vcpkg install sqlite3:x64-windows"
Invoke-Expression "vcpkg integrate install"
foreach ($version in $versions) {
if (Test-Path -LiteralPath "build") {
Remove-Item "build" -Recurse
Write-Output "Deleted existing build folder"
}
Invoke-Expression "cmake -B build -DPROTOCOL_VERSION=$version -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake"
if ($LASTEXITCODE -ne "0") {
Write-Error "cmake generation failed for version $version" -ErrorAction Stop
}
Write-Output "Generated build files for version $version"
foreach ($configuration in $configurations) {
Write-Output "Building version $version $configuration"
Invoke-Expression "msbuild build\OpenFusion.sln /maxcpucount:8 /p:BuildInParallel=true /p:CL_MPCount=8 /p:UseMultiToolTask=true /p:Configuration=$configuration"
if ($LASTEXITCODE -ne "0") {
Write-Error "msbuild build failed for version $version" -ErrorAction Stop
}
Rename-Item -Path "bin/$configuration" -newName "$version-$configuration"
Write-Output "Built version $version $configuration"
Copy-Item -Path "sql" -Destination "bin/$version-$configuration/sql" -Recurse
Copy-Item -Path "config.ini" -Destination "bin/$version-$configuration"
}
}
shell: pwsh
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: 'windows-vs2019-bin-x64-${{ env.SHORT_SHA }}'
path: bin
copy-artifacts:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
runs-on: ubuntu-22.04
needs: [windows-build, ubuntu-build]
env:
BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }}
ENDPOINT: ${{ secrets.ENDPOINT }}
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- run: |
GITDESC=$(git describe --tags)
mkdir $GITDESC
echo "ARTDIR=$GITDESC" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
with:
path: ${{ env.ARTDIR }}
- name: Upload artifacts
shell: bash
run: |
sudo apt install zip -y
cd $ARTDIR
for build in *; do
cd $build
zip -r ../$build.zip *
cd ..
rm -r $build
done
cd ..
umask 077
printf %s "$BOT_SSH_KEY" > cdn_key
scp -i cdn_key -o StrictHostKeyChecking=no -r $ARTDIR $ENDPOINT

38
.github/workflows/push-docker-image.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Push Docker Image
on:
release:
types: [published]
jobs:
push-docker-image:
name: Push Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
platforms:
- linux/amd64
- linux/arm64
steps:
- uses: actions/checkout@v4
- name: Retrieve major version
uses: winterjung/split@v2
id: split
with:
msg: ${{ github.ref_name }}
separator: .
- name: Log in to registry
uses: docker/login-action@v3
with:
password: ${{ secrets.DOCKERHUB_TOKEN }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
- name: Build and push the Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: ${{ matrix.platforms }}
push: true
tags: ${{ secrets.DOCKERHUB_REPOSITORY }}:${{ github.ref_name }},${{ secrets.DOCKERHUB_REPOSITORY }}:${{ steps.split.outputs._0 }},${{ secrets.DOCKERHUB_REPOSITORY }}:latest

2
.gitignore vendored
View File

@@ -16,3 +16,5 @@ build/
version.h
infer-out
gmon.out
*.bak

26
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug (Linux)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/fusion",
"cwd": "${workspaceFolder}"
},
{
"name": "Debug (Windows)",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/Debug/winfusion.exe",
"cwd": "${workspaceFolder}"
},
{
"name": "Release (Windows)",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/Release/winfusion.exe",
"cwd": "${workspaceFolder}"
}
]
}

View File

@@ -3,7 +3,8 @@ project(OpenFusion)
set(CMAKE_CXX_STANDARD 17)
execute_process(COMMAND git describe --tags OUTPUT_VARIABLE GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND git describe --tags OUTPUT_VARIABLE GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
# OpenFusion supports multiple packet/struct versions
# 104 is the default version to build which can be changed
@@ -43,8 +44,10 @@ add_executable(openfusion ${SOURCES})
set_target_properties(openfusion PROPERTIES OUTPUT_NAME ${BIN_NAME})
target_link_libraries(openfusion sqlite3)
target_link_libraries(openfusion lua51)
# find sqlite3 and use it
find_package(SQLite3 REQUIRED)
target_include_directories(openfusion PRIVATE ${SQLite3_INCLUDE_DIRS})
target_link_libraries(openfusion PRIVATE ${SQLite3_LIBRARIES})
# Makes it so config, tdata, etc. get picked up when starting via the debugger in VS
set_property(TARGET openfusion PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
@@ -54,5 +57,5 @@ set_property(TARGET openfusion PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_S
# It's not something you should do, but it's there if you need it...
if (NOT CMAKE_GENERATOR MATCHES "Visual Studio" AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND NOT CMAKE_GENERATOR MATCHES "MinGW Makefiles")
find_package(Threads REQUIRED)
target_link_libraries(openfusion pthread)
target_link_libraries(openfusion PRIVATE pthread)
endif()

View File

@@ -26,7 +26,7 @@ Both are pretty short reads and following them will get you up to speed with bra
I will now cover a few examples of the complications people have encountered contributing to this project, how to understand them, overcome them and henceforth avoid them.
## Dirty pull requests
### Dirty pull requests
Many Pull Requests OpenFusion receives fail to present a clean set of commits to merge.
These are generally either:
@@ -44,7 +44,7 @@ If you read the above links, you'll note that this isn't exactly a perfect solut
The obvious issue, then, is that the people submitting dirty PRs are the exact people who don't *know* how to rebase their fork to continue submitting their work cleanly.
So they end up creating countless merge commits when pulling upstream on top of their own incompatible histories, and then submitting those merge commits in their PRs and the cycle continues.
## The details
### The details
A git commit is uniquely identified by its SHA1 hash.
Its hash is generated from its contents, as well as the hash(es) of its parent commit(s).
@@ -52,7 +52,7 @@ Its hash is generated from its contents, as well as the hash(es) of its parent c
That means that even if two commits are exactly the same in terms of content, they're not the same commit if their parent commits differ (ex. if they've been rebased).
So if you keep issuing `git pull`s after upstream has merged a rebased version of your PR, you will never re-synchronize with it, and will instead construct an alternate history polluted by pointless merge commits.
## The solution
### The solution
If you already have a messed-up fork and you have no changes on it that you're afraid to lose, the solution is simple:
@@ -67,7 +67,7 @@ If you do have some committed changes that haven't yet been merged upstream, you
If you do end up messing something up, don't worry, it most likely isn't really lost and `git reflog` is your friend.
(You can checkout an arbitrary commit, and make it into its own branch with `git checkout -b BRANCH` or set a pre-exisitng branch to it with `git reset --hard COMMIT`)
## Avoiding the problem
### Avoiding the problem
When working on a changeset you want to submit back upstream, don't do it on the main branch.
Create a work branch just for your changeset with `git checkout -b work`.
@@ -81,3 +81,34 @@ That way you can always keep master in sync with upstream with `git pull --ff-on
Creating new branches for the rebase isn't strictly necessary since you can always return a branch to its previous state with `git reflog`.
For moving uncommited changes around between branches, `git stash` is a real blessing.
## Code guidelines
Alright, you're up to speed on Git and ready to go. Here are a few specific code guidelines to try and follow:
### Match the styling
Pretty straightforward, make sure your code looks similar to the code around it. Match whitespacing, bracket styling, variable naming conventions, etc.
### Prefer short-circuiting
To minimize branching complexity (as this makes the code hard to read), we prefer to keep the number of `if-else` statements as low as possible. One easy way to achieve this is by doing an early return after branching into an `if` block and then writing the code for the other path outside the block entirely. You can find examples of this in practically every source file. Note that in a few select situations, this might actually make your code less elegant, which is why this isn't a strict rule. Lean towards short-circuiting and use your better judgement.
### Follow the include convention
This one matters a lot as it can cause cyclic dependencies and other code-breaking issues.
FOR HEADER FILES (.hpp):
- everything you use IN THE HEADER must be EXPLICITLY INCLUDED with the exception of things that fall under Core.hpp
- you may NOT include ANYTHING ELSE
FOR SOURCE FILES (.cpp):
- you can #include whatever you want as long as the partner header is included first
- anything that gets included by another include is fair game
- redundant includes are ok because they'll be harmless AS LONG AS our header files stay lean.
The point of this is NOT to optimize the number of includes used all around or make things more efficient necessarily. it's to improve readability & coherence and make it easier to avoid cyclical issues.
## When in doubt, ask
If you still have questions that were not answered here, feel free to ping a dev in the Discord server or on GitHub.

37
Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# build
FROM alpine:3 as build
WORKDIR /usr/src/app
RUN apk update && apk upgrade && apk add \
linux-headers \
git \
clang18 \
make \
sqlite-dev
COPY src ./src
COPY vendor ./vendor
COPY .git ./.git
COPY Makefile CMakeLists.txt version.h.in ./
RUN sed -i 's/^CC=clang$/&-18/' Makefile
RUN sed -i 's/^CXX=clang++$/&-18/' Makefile
RUN make -j8
# prod
FROM alpine:3
WORKDIR /usr/src/app
RUN apk update && apk upgrade && apk add \
libstdc++ \
sqlite-dev
COPY --from=build /usr/src/app/bin/fusion /bin/fusion
COPY sql ./sql
CMD ["/bin/fusion"]
LABEL Name=openfusion Version=0.0.2

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 Seth Stubbs
Copyright (c) 2020-2024 OpenFusion Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -4,11 +4,9 @@ CC=clang
CXX=clang++
# -w suppresses all warnings (the part that's commented out helps me find memory leaks, it ruins performance though!)
# If compiling with ASAN, invoke like this: $ LSAN_OPTIONS=suppressions=suppr.txt bin/fusion
CFLAGS=-O3 #-g3 -fsanitize=address
LCFLAGS=`pkg-config --cflags luajit`
LLIBFLAGS=`pkg-config --libs luajit`
CXXFLAGS=-Wall -Wno-unknown-pragmas -std=c++17 -O2 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor $(LCFLAGS) #-g3 -fsanitize=address
LDFLAGS=-lpthread -lsqlite3 $(LLIBFLAGS) -lstdc++fs #-g3 -fsanitize=address
CFLAGS=-Wall -Wno-unknown-pragmas -O2 -fPIE -D_FORTIFY_SOURCE=1 -fstack-protector #-g3 -fsanitize=address
CXXFLAGS=$(CFLAGS) -std=c++17 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor
LDFLAGS=-lpthread -lsqlite3 -pie -Wl,-z,relro -Wl,-z,now #-g3 -fsanitize=address
# specifies the name of our exectuable
SERVER=bin/fusion
@@ -19,9 +17,9 @@ PROTOCOL_VERSION?=104
# Windows-specific
WIN_CC=x86_64-w64-mingw32-gcc
WIN_CXX=x86_64-w64-mingw32-g++
WIN_CFLAGS=-O3 #-g3 -fsanitize=address
WIN_CXXFLAGS=-D_WIN32_WINNT=0x0601 -Wall -Wno-unknown-pragmas -std=c++17 -O3 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor $(LCFLAGS) #-g3 -fsanitize=address
WIN_LDFLAGS=-static -lws2_32 -lwsock32 -lsqlite3 $(LLIBFLAGS) -lstdc++fs #-g3 -fsanitize=address
WIN_CFLAGS=-O2 -D_WIN32_WINNT=0x0601 -Wall -Wno-unknown-pragmas
WIN_CXXFLAGS=$(WIN_CFLAGS) -std=c++17 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor
WIN_LDFLAGS=-static -lws2_32 -lwsock32 -lsqlite3
WIN_SERVER=bin/winfusion.exe
# C code; currently exclusively from vendored libraries
@@ -45,17 +43,14 @@ CXXSRC=\
src/servers/CNLoginServer.cpp\
src/servers/CNShardServer.cpp\
src/servers/Monitor.cpp\
src/lua/LuaManager.cpp\
src/lua/EventWrapper.cpp\
src/lua/WorldWrapper.cpp\
src/lua/EntityWrapper.cpp\
src/lua/PlayerWrapper.cpp\
src/lua/NPCWrapper.cpp\
src/db/init.cpp\
src/db/login.cpp\
src/db/shard.cpp\
src/db/player.cpp\
src/db/email.cpp\
src/sandbox/seccomp.cpp\
src/sandbox/openbsd.cpp\
src/Buffs.cpp\
src/Chat.cpp\
src/CustomCommands.cpp\
src/Entities.cpp\
@@ -94,20 +89,13 @@ CXXHDR=\
src/servers/CNLoginServer.hpp\
src/servers/CNShardServer.hpp\
src/servers/Monitor.hpp\
src/lua/LuaManager.hpp\
src/lua/LuaWrapper.hpp\
src/lua/EventWrapper.hpp\
src/lua/WorldWrapper.hpp\
src/lua/EntityWrapper.hpp\
src/lua/PlayerWrapper.hpp\
src/lua/NPCWrapper.hpp\
src/db/Database.hpp\
src/db/internal.hpp\
src/sandbox/Sandbox.hpp\
vendor/bcrypt/BCrypt.hpp\
vendor/INIReader.hpp\
vendor/JSON.hpp\
vendor/INIReader.hpp\
vendor/JSON.hpp\
src/Buffs.hpp\
src/Chat.hpp\
src/CustomCommands.hpp\
src/Entities.hpp\
@@ -154,7 +142,7 @@ windows : CXXFLAGS=$(WIN_CXXFLAGS)
windows : LDFLAGS=$(WIN_LDFLAGS)
windows : SERVER=$(WIN_SERVER)
.SUFFIX: .o .c .cpp .h .hpp
.SUFFIXES: .o .c .cpp .h .hpp
.c.o:
$(CC) -c $(CFLAGS) -o $@ $<
@@ -163,7 +151,7 @@ windows : SERVER=$(WIN_SERVER)
$(CXX) -c $(CXXFLAGS) -o $@ $<
# header timestamps are a prerequisite for OF object files
$(CXXOBJ): $(CXXHDR)
$(CXXOBJ): $(HDR)
$(SERVER): $(OBJ) $(CHDR) $(CXXHDR)
mkdir -p bin

View File

@@ -2,7 +2,7 @@
<p align="center">
<a href="https://github.com/OpenFusionProject/OpenFusion/releases/latest"><img src="https://img.shields.io/github/v/release/OpenFusionProject/OpenFusion" alt="Current Release"></a>
<a href="https://ci.appveyor.com/project/OpenFusionProject/openfusion"><img src="https://ci.appveyor.com/api/projects/status/github/OpenFusionProject/OpenFusion?svg=true" alt="AppVeyor"></a>
<a href="https://github.com/OpenFusionProject/OpenFusion/actions/workflows/check-builds.yaml"><img src="https://github.com/OpenFusionProject/OpenFusion/actions/workflows/check-builds.yaml/badge.svg" alt="Workflow"></a>
<a href="https://discord.gg/DYavckB"><img src="https://img.shields.io/badge/chat-on%20discord-7289da.svg?logo=discord" alt="Discord"></a>
<a href="https://github.com/OpenFusionProject/OpenFusion/blob/master/LICENSE.md"><img src="https://img.shields.io/github/license/OpenFusionProject/OpenFusion" alt="License"></a>
</p>
@@ -12,21 +12,31 @@ OpenFusion is a reverse-engineered server for FusionFall. It primarily targets v
## Usage
### Getting Started
#### Method A: Installer (Easiest)
1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.6/OpenFusionClient-1.6-Installer.exe) - choose to run the file.
2. After a few moments, the client should open: you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
3. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
4. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 3.
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.3/OpenFusionClient-1.3.zip).
#### Method B: Standalone .zip file
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.6/OpenFusionClient-1.6.zip).
2. Extract it to a folder of your choice. Note: if you are upgrading from an older version, it is preferable to start with a fresh folder rather than overwriting a previous install.
3. Run OpenFusionClient.exe - you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
4. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
5. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 4.
Instructions for getting the client to run on Linux through Wine can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Running-the-game-client-on-Linux).
### Hosting a server
1. Grab `OpenFusionServer-1.3-original.zip` or `OpenFusionServer-1.3-academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.3).
1. Grab `OpenFusionServer-1.6-Original.zip` or `OpenFusionServer-1.6-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.6).
2. Extract it to a folder of your choice, then run `winfusion.exe` (Windows) or `fusion` (Linux) to start the server.
3. Add a new server to the client's list: the default port is 23000, so the full IP with default settings would be 127.0.0.1:23000.
4. Once you've added the server to the list, connect to it and log in. If you're having trouble with this, refer to steps 4 and 5 from the previous section.
3. Add a new server to the client's list:
1. For Description, enter anything you want. This is what will show up in the server list.
2. For Server IP, enter the IP address and port of the login server. If you're hosting and playing on the same PC, this would be `127.0.0.1:23000`.
3. Lastly Game Version - select `beta-20100104` if you downloaded the original zip, or `beta-20111013` if you downloaded the academy zip.
5. Once you've added the server to the list, connect to it and log in. If you're having trouble with this, refer to steps 4 and 5 from the previous section.
If you want, [compiled binaries (artifacts) for each new commit can be found on AppVeyor.](https://ci.appveyor.com/project/OpenFusionProject/openfusion)
If you want to run the latest development builds of the server, [compiled binaries (artifacts) for each functional commit can be found here.](http://cdn.dexlabs.systems/of-builds/)
For a more detailed overview of the game's architecture and how to configure it, read the following sections.
@@ -43,10 +53,7 @@ FusionFall consists of the following components:
The original game made use of the player's actual web browser to launch the game, but since then the NPAPI plugin interface the game relied on has been deprecated and is no longer available in most modern browsers. Both Retro and OpenFusion get around this issue by distributing an older version of Electron, a software package that is essentially a specialized web browser.
The browser/Electron client opens a web page with an `<embed>` tag of MIME type `application/vnd.unity`, where the `src` param is the address of the game's `.unity3d` entrypoint.
This triggers the browser to load an NPAPI plugin that handles this MIME type, the Unity Web Player, which the browser looks for in `C:\Users\%USERNAME%\AppData\LocalLow\Unity\WebPlayer`.
The Web Player was previously copied there by `installUnity.bat`.
The browser/Electron client opens a web page with an `<embed>` tag of the appropriate MIME type, where the `src` param is the address of the game's `.unity3d` entrypoint. This triggers the browser to load an NPAPI plugin that handles said MIME type, in this case the Unity Web Player.
Note that the version of the web player distributed with OpenFusion expects a standard `UnityWeb` magic number for all assets, instead of Retro's modified `streamed` magic number.
This will potentially become relevant later, as people start experimenting and mixing and matching versions.
@@ -55,7 +62,7 @@ The web player will execute the game code, which will request the following file
`/assetInfo.php` contains the address from which to fetch the rest of the game's assets (the "dongresources").
Normally those would be hosted on the same web server as the gateway, but the OpenFusion distribution (in it's default configuration) doesn't use a web server at all!
It loads the web pages locally using the `file://` schema, and fetches the game's assets from Turner's CDN (which is still hosting them to this day!).
It instead loads the web pages locally using the `file://` schema, and fetches the game's assets from a standard web server.
`/loginInfo.php` contains the IP:port pair of the FusionFall login server, which the client will connect to. This login server drives the client while it's in the Character Selection menu, as well as Character Creation and the Tutorial.
@@ -72,7 +79,7 @@ This just works if you're all under the same LAN, but if you want to play over t
## Compiling
OpenFusion has one external dependency: SQLite. You can install it on Windows using `vcpkg`, and on Unix/Linux using your distribution's package manager. For a more indepth guide on how to set up vcpkg, [read this guide on the wiki](https://github.com/OpenFusionProject/OpenFusion/wiki/Installing-SQLite-on-Windows-using-vcpkg).
OpenFusion has one external dependency: SQLite. The oldest compatible version is `3.33.0`. You can install it on Windows using `vcpkg`, and on Unix/Linux using your distribution's package manager. For a more indepth guide on how to set up vcpkg, [read this guide on the wiki](https://github.com/OpenFusionProject/OpenFusion/wiki/Installing-SQLite-on-Windows-using-vcpkg).
You have two choices for compiling OpenFusion: the included Makefile and the included CMakeLists file.
@@ -91,26 +98,13 @@ If you'd like to contribute to this project, please read [CONTRIBUTING.md](CONTR
## Gameplay
The goal of the project is to faithfully recreate the game as it was at the time of the targeted build.
The server is not yet complete, however, and some functionality is still missing.
While most features are implemented and the game is playable start to finish, there may be missing functionality or bugs present.
Because the server is still in development, ordinary players are allowed access to a few admin commands:
Depending on the server configuration, you'll have access to certain commands.
![](res/sane_upsell.png)
For the public servers: Original has item spawning, the ability to set player speed/jump height, and teleportation enabled (default account level 50).
Meanwhile the Academy server is more meant for legitimate playthroughs (default account level 99).
### Movement commands
* A `/speed` of around 2400 or 3000 is nice.
* A `/jump` of about 50 will send you soaring
* [This map](res/dong_number_map.png) (credit to Danny O) is useful for `/warp` coordinates.
* `/goto` is useful for more precise teleportation (ie. for getting into Infected Zones, etc.).
When hosting a local server, you will have access to all commands by default (account level 1).
### Item commands
* `/itemN [type] [itemId] [amount]`
(Refer to the [item list](https://docs.google.com/spreadsheets/d/1mpoJ9iTHl_xLI4wQ_9UvIDYNcsDYscdkyaGizs43TCg/))
### Nano commands
* `/nano [id] (1-36)`
* `/nano_equip [id] (1-36) [slot] (0-2)`
* `/nano_unequip [slot] (0-2)`
* `/nano_active [slot] (0-2)`
### A full list of commands can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).
For a list of available commands, see [this wiki page](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).

View File

@@ -1,93 +0,0 @@
version: 'openfusion-{branch}-{build}'
build_cloud: GCE us-east1-b n2-standard-8
skip_branch_with_pr: true
image:
- GCP-Windows-VS2019
- GCP-Linux-Ubuntu2004
platform:
- x64
configuration:
- Release
for:
-
matrix:
only:
- image: GCP-Linux-Ubuntu2004
install:
- sh: sudo apt install libluajit-5.1-dev -y
build_script:
- ps: |
$versions = "104", "728", "1013"
foreach ($version in $versions) {
Write-Output "Cleaning old output"
Invoke-Expression "make clean"
if ($LASTEXITCODE -ne "0") {
Write-Error "make clean failed for version $version" -ErrorAction Stop
}
Write-Output "Building version $version"
Invoke-Expression "make -j8 PROTOCOL_VERSION=$version"
if ($LASTEXITCODE -ne "0") {
Write-Error "make failed for version $version" -ErrorAction Stop
}
Rename-Item -Path "bin/fusion" -newName "$version-fusion"
Write-Output "Built version $version"
}
Copy-Item -Path "sql" -Destination "bin/sql" -Recurse
Copy-Item -Path "config.ini" -Destination "bin"
artifacts:
- path: bin
name: ubuntu20_04-bin-x64
type: zip
-
matrix:
only:
- image: GCP-Windows-VS2019
install:
- cmd: vcpkg install sqlite3:x64-windows
- cmd: vcpkg install luajit:x64-windows
- cmd: vcpkg integrate install
build_script:
- ps: |
$versions = "104", "728", "1013"
$configurations = "Release"
# "Debug" builds are disabled, since we don't really need them
# AppVeyor uses VS2019 Community
$vsPath = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community"
Import-Module "$vsPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation
foreach ($version in $versions) {
if (Test-Path -LiteralPath "build") {
Remove-Item "build" -Recurse
Write-Output "Deleted existing build folder"
}
Invoke-Expression "cmake -B build -DPROTOCOL_VERSION=$version"
if ($LASTEXITCODE -ne "0") {
Write-Error "cmake generation failed for version $version" -ErrorAction Stop
}
Write-Output "Generated build files for version $version"
foreach ($configuration in $configurations) {
Write-Output "Building version $version $configuration"
Invoke-Expression "msbuild build\OpenFusion.sln /maxcpucount:8 /p:BuildInParallel=true /p:CL_MPCount=8 /p:UseMultiToolTask=true /p:Configuration=$configuration"
if ($LASTEXITCODE -ne "0") {
Write-Error "msbuild build failed for version $version" -ErrorAction Stop
}
Rename-Item -Path "bin/$configuration" -newName "$version-$configuration"
Write-Output "Built version $version $configuration"
Copy-Item -Path "sql" -Destination "bin/$version-$configuration/sql" -Recurse
Copy-Item -Path "config.ini" -Destination "bin/$version-$configuration"
}
}
artifacts:
- path: bin
name: windows-vs2019-bin-x64
type: zip

View File

@@ -5,12 +5,22 @@
# 3 = print all packets
verbosity=1
# sandbox the process on supported platforms
sandbox=true
# Login Server configuration
[login]
# must be kept in sync with loginInfo.php
port=23000
# will all custom names be approved instantly?
acceptallcustomnames=true
# should attempts to log into non-existent accounts
# automatically create them?
autocreateaccounts=true
# list of supported authentication methods (comma-separated)
# password = allow login type 1 with plaintext passwords
# cookie = allow login type 2 with one-shot auth cookies
authmethods=password
# how often should everything be flushed to the database?
# the default is 4 minutes
dbsaveinterval=240
@@ -60,6 +70,9 @@ motd=Welcome to OpenFusion!
# location of the database
#dbpath=database.db
# should there be a score cap for infected zone races?
#izracescorecapped=true
# should tutorial flags be disabled off the bat?
disablefirstuseflag=true

16
docker-compose.yml Normal file
View File

@@ -0,0 +1,16 @@
version: '3.4'
services:
openfusion:
image: openfusion
build:
context: .
dockerfile: ./Dockerfile
volumes:
- ./config.ini:/usr/src/app/config.ini
- ./database.db:/usr/src/app/database.db
- ./tdata:/usr/src/app/tdata
ports:
- "23000:23000"
- "23001:23001"
- "8003:8003"

View File

@@ -1,28 +0,0 @@
print("Hello wtf!")
function onJoin(plr)
print(plr.type .. " " .. plr.name .. " joined from LUA!!")
plr.onChat:listen(function(msg)
print(plr.name .. " said : \'" .. msg .. "\'")
if msg == "kickme" then
plr:kick()
elseif msg == "hi" then
print("hello " .. plr.name)
elseif msg == "pet" then
local dog = NPC.new(plr.x, plr.y, plr.z, 3054)
while wait(2) and plr:exists() do
dog:moveTo(plr.x + math.random(-500, 500), plr.y + math.random(-500, 500), plr.z)
end
end
end)
end
World.onPlayerAdded:listen(onJoin)
for i, plr in ipairs(World.players) do
onJoin(plr)
end
wait(2)
print("Hello world ~2 seconds later! running protcol version " .. World.version)

28
sql/migration3.sql Normal file
View File

@@ -0,0 +1,28 @@
/*
It is recommended in the SQLite manual to turn off
foreign keys when making schema changes that involve them
*/
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
-- Change username column (Login) to be case-insensitive
CREATE TABLE Temp (
AccountID INTEGER NOT NULL,
Login TEXT NOT NULL UNIQUE COLLATE NOCASE,
Password TEXT NOT NULL,
Selected INTEGER DEFAULT 1 NOT NULL,
AccountLevel INTEGER NOT NULL,
Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
BannedUntil INTEGER DEFAULT 0 NOT NULL,
BannedSince INTEGER DEFAULT 0 NOT NULL,
BanReason TEXT DEFAULT '' NOT NULL,
PRIMARY KEY(AccountID AUTOINCREMENT)
);
INSERT INTO Temp SELECT * FROM Accounts;
DROP TABLE Accounts;
ALTER TABLE Temp RENAME TO Accounts;
-- Update DB Version
UPDATE Meta SET Value = 4 WHERE Key = 'DatabaseVersion';
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
COMMIT;
PRAGMA foreign_keys=ON;

19
sql/migration4.sql Normal file
View File

@@ -0,0 +1,19 @@
/*
It is recommended in the SQLite manual to turn off
foreign keys when making schema changes that involve them
*/
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
-- New table to store auth cookies
CREATE TABLE Auth (
AccountID INTEGER NOT NULL,
Cookie TEXT NOT NULL,
Expires INTEGER DEFAULT 0 NOT NULL,
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
UNIQUE (AccountID)
);
-- Update DB Version
UPDATE Meta SET Value = 5 WHERE Key = 'DatabaseVersion';
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS Accounts (
AccountID INTEGER NOT NULL,
Login TEXT NOT NULL UNIQUE,
Login TEXT NOT NULL UNIQUE COLLATE NOCASE,
Password TEXT NOT NULL,
Selected INTEGER DEFAULT 1 NOT NULL,
AccountLevel INTEGER NOT NULL,
@@ -158,4 +158,12 @@ CREATE TABLE IF NOT EXISTS RedeemedCodes(
Code TEXT NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Code)
)
);
CREATE TABLE IF NOT EXISTS Auth (
AccountID INTEGER NOT NULL,
Cookie TEXT NOT NULL,
Expires INTEGER DEFAULT 0 NOT NULL,
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
UNIQUE (AccountID)
);

File diff suppressed because it is too large Load Diff

View File

@@ -1,64 +1,112 @@
#pragma once
#include "core/Core.hpp"
#include "Combat.hpp"
typedef void (*PowerHandler)(CNSocket*, std::vector<int>, int16_t, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t);
#include "Entities.hpp"
#include "Player.hpp"
struct NanoPower {
int16_t skillType;
int32_t bitFlag;
int16_t timeBuffID;
PowerHandler handler;
#include <map>
#include <vector>
#include <assert.h>
NanoPower(int16_t s, int32_t b, int16_t t, PowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {}
const int COMBAT_TICKS_PER_DRAIN_PROC = 2;
constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain);
void handle(CNSocket *sock, std::vector<int> targetData, int16_t nanoID, int16_t skillID, int16_t duration, int16_t amount) {
if (handler == nullptr)
return;
handler(sock, targetData, nanoID, skillID, duration, amount, skillType, bitFlag, timeBuffID);
}
enum class SkillType {
DAMAGE = 1,
HEAL_HP = 2,
KNOCKDOWN = 3, // uses DamageNDebuff
SLEEP = 4, // uses DamageNDebuff
SNARE = 5, // uses DamageNDebuff
HEAL_STAMINA = 6,
STAMINA_SELF = 7,
STUN = 8, // uses DamageNDebuff
WEAPONSLOW = 9,
JUMP = 10,
RUN = 11,
STEALTH = 12,
SWIM = 13,
MINIMAPENEMY = 14,
MINIMAPTRESURE = 15,
PHOENIX = 16,
PROTECTBATTERY = 17,
PROTECTINFECTION = 18,
REWARDBLOB = 19,
REWARDCASH = 20,
BATTERYDRAIN = 21,
CORRUPTIONATTACK = 22,
INFECTIONDAMAGE = 23,
KNOCKBACK = 24,
FREEDOM = 25,
PHOENIX_GROUP = 26,
RECALL = 27,
RECALL_GROUP = 28,
RETROROCKET_SELF = 29,
BLOODSUCKING = 30,
BOUNDINGBALL = 31,
INVULNERABLE = 32,
NANOSTIMPAK = 33,
RETURNHOMEHEAL = 34,
BUFFHEAL = 35,
EXTRABANK = 36,
CORRUPTIONATTACKWIN = 38,
CORRUPTIONATTACKLOSE = 39,
};
typedef void (*MobPowerHandler)(Mob*, std::vector<int>, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t);
enum class SkillEffectTarget {
POINT = 1,
SELF = 2,
CONE = 3,
WEAPON = 4,
AREA_SELF = 5,
AREA_TARGET = 6
};
struct MobPower {
int16_t skillType;
int32_t bitFlag;
int16_t timeBuffID;
MobPowerHandler handler;
enum class SkillTargetType {
MOBS = 1,
PLAYERS = 2,
GROUP = 3
};
MobPower(int16_t s, int32_t b, int16_t t, MobPowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {}
enum class SkillDrainType {
ACTIVE = 1,
PASSIVE = 2
};
void handle(Mob *mob, std::vector<int> targetData, int16_t skillID, int16_t duration, int16_t amount) {
if (handler == nullptr)
return;
handler(mob, targetData, skillID, duration, amount, skillType, bitFlag, timeBuffID);
struct SkillResult {
size_t size;
uint8_t payload[MAX_SKILLRESULT_SIZE];
SkillResult(size_t len, void* dat) {
assert(len <= MAX_SKILLRESULT_SIZE);
size = len;
memcpy(payload, dat, len);
}
SkillResult() {
size = 0;
}
};
struct SkillData {
int skillType;
int targetType;
int drainType;
SkillType skillType; // eST
SkillEffectTarget effectTarget;
int effectType; // always 1?
SkillTargetType targetType;
SkillDrainType drainType;
int effectArea;
int batteryUse[4];
int durationTime[4];
int powerIntensity[4];
int valueTypes[3];
int values[3][4];
};
namespace Nanos {
extern std::vector<NanoPower> NanoPowers;
namespace Abilities {
extern std::map<int32_t, SkillData> SkillTable;
void nanoUnbuff(CNSocket* sock, std::vector<int> targetData, int32_t bitFlag, int16_t timeBuffID, int16_t amount, bool groupPower);
int applyBuff(CNSocket* sock, int skillID, int eTBU, int eTBT, int32_t groupFlags);
void useNanoSkill(CNSocket*, SkillData*, sNano&, std::vector<ICombatant*>);
void useNPCSkill(EntityRef, int skillID, std::vector<ICombatant*>);
std::vector<int> findTargets(Player* plr, int skillID, CNPacketData* data = nullptr);
}
namespace Combat {
extern std::vector<MobPower> MobPowers;
std::vector<ICombatant*> matchTargets(ICombatant*, SkillData*, int, int32_t*);
int getCSTBFromST(SkillType skillType);
}

View File

@@ -1,15 +1,10 @@
#include "servers/CNShardServer.hpp"
#include "Buddies.hpp"
#include "PlayerManager.hpp"
#include "Buddies.hpp"
#include "db/Database.hpp"
#include "Items.hpp"
#include "db/Database.hpp"
#include <iostream>
#include <chrono>
#include <algorithm>
#include <thread>
#include "db/Database.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
using namespace Buddies;
@@ -35,7 +30,7 @@ static bool playerHasBuddyWithID(Player* plr, int buddyID) {
#pragma endregion
// Refresh buddy list
void Buddies::refreshBuddyList(CNSocket* sock) {
void Buddies::sendBuddyList(CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
int buddyCnt = Database::getNumBuddies(plr);
@@ -283,15 +278,6 @@ static void reqFindNameBuddyAccept(CNSocket* sock, CNPacketData* data) {
static void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
/*
* If the buddy list wasn't synced a second time yet, sync it.
* Not sure why we have to do it again for the client not to trip up.
*/
if (!plr->buddiesSynced) {
refreshBuddyList(sock);
plr->buddiesSynced = true;
}
INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp);
for (int slot = 0; slot < 50; slot++) {

View File

@@ -1,12 +1,10 @@
#pragma once
#include "Player.hpp"
#include "core/Core.hpp"
#include "core/Core.hpp"
namespace Buddies {
void init();
// Buddy list
void refreshBuddyList(CNSocket* sock);
void sendBuddyList(CNSocket* sock);
}

198
src/Buffs.cpp Normal file
View File

@@ -0,0 +1,198 @@
#include "Buffs.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
using namespace Buffs;
void Buff::tick(time_t currTime) {
auto it = stacks.begin();
while(it != stacks.end()) {
BuffStack& stack = *it;
//if(onTick) onTick(self, this, currTime);
if(stack.durationTicks == 0) {
BuffStack deadStack = stack;
it = stacks.erase(it);
if(onUpdate) onUpdate(self, this, ETBU_DEL, &deadStack);
} else {
if(stack.durationTicks > 0) stack.durationTicks--;
it++;
}
}
}
void Buff::combatTick(time_t currTime) {
if(onCombatTick) onCombatTick(self, this, currTime);
}
void Buff::clear() {
while(!stacks.empty()) {
BuffStack stack = stacks.back();
stacks.pop_back();
if(onUpdate) onUpdate(self, this, ETBU_DEL, &stack);
}
}
void Buff::clear(BuffClass buffClass) {
auto it = stacks.begin();
while(it != stacks.end()) {
BuffStack& stack = *it;
if(stack.buffStackClass == buffClass) {
BuffStack deadStack = stack;
it = stacks.erase(it);
if(onUpdate) onUpdate(self, this, ETBU_DEL, &deadStack);
} else it++;
}
}
void Buff::addStack(BuffStack* stack) {
stacks.push_back(*stack);
if(onUpdate) onUpdate(self, this, ETBU_ADD, &stacks.back());
}
bool Buff::hasClass(BuffClass buffClass) {
for(BuffStack& stack : stacks) {
if(stack.buffStackClass == buffClass)
return true;
}
return false;
}
BuffClass Buff::maxClass() {
BuffClass buffClass = BuffClass::NONE;
for(BuffStack& stack : stacks) {
if(stack.buffStackClass > buffClass)
buffClass = stack.buffStackClass;
}
return buffClass;
}
int Buff::getValue(BuffValueSelector selector) {
if(isStale()) return 0;
int value = selector == BuffValueSelector::NET_TOTAL ? 0 : stacks.front().value;
for(BuffStack& stack : stacks) {
switch(selector)
{
case BuffValueSelector::NET_TOTAL:
value += stack.value;
break;
case BuffValueSelector::MIN_VALUE:
if(stack.value < value) value = stack.value;
break;
case BuffValueSelector::MAX_VALUE:
if(stack.value > value) value = stack.value;
break;
case BuffValueSelector::MIN_MAGNITUDE:
if(abs(stack.value) < abs(value)) value = stack.value;
break;
case BuffValueSelector::MAX_MAGNITUDE:
default:
if(abs(stack.value) > abs(value)) value = stack.value;
}
}
return value;
}
EntityRef Buff::getLastSource() {
if(stacks.empty())
return self;
return stacks.back().source;
}
bool Buff::isStale() {
return stacks.empty();
}
/* This will practically never do anything important, but it's here just in case */
void Buff::updateCallbacks(BuffCallback<int, BuffStack*> fOnUpdate, BuffCallback<time_t> fOnCombatTick) {
if(!onUpdate) onUpdate = fOnUpdate;
if(!onCombatTick) onCombatTick = fOnCombatTick;
}
#pragma region Handlers
void Buffs::timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack) {
if(self.kind != EntityKind::PLAYER)
return; // not implemented
Player* plr = (Player*)self.getEntity();
if(plr == nullptr)
return; // sanity check
if(status == ETBU_DEL && !buff->isStale())
return; // no premature effect deletion
int cbf = plr->getCompositeCondition();
sTimeBuff payload{};
if(status == ETBU_ADD) {
payload.iValue = buff->getValue(BuffValueSelector::MAX_MAGNITUDE);
// we need to explicitly add the ECSB for this buff,
// in case this is the first stack in and the entry
// in the buff map doesn't yet exist
if(buff->id > 0) cbf |= CSB_FROM_ECSB(buff->id);
}
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = buff->id; // eCharStatusTimeBuffID
pkt.eTBU = status; // eTimeBuffUpdate
pkt.eTBT = (int)stack->buffStackClass;
pkt.iConditionBitFlag = cbf;
pkt.TimeBuff = payload;
self.sock->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
}
void Buffs::timeBuffTick(EntityRef self, Buff* buff) {
if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
return; // not implemented
Entity* entity = self.getEntity();
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK, pkt);
pkt.eCT = combatant->getCharType();
pkt.iID = combatant->getID();
pkt.iTB_ID = buff->id;
NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
}
void Buffs::timeBuffTimeout(EntityRef self) {
if(self.kind != EntityKind::PLAYER && self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
return; // not a combatant
Entity* entity = self.getEntity();
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt); // send a buff timeout to other players
int32_t eCharType = combatant->getCharType();
pkt.eCT = eCharType == 4 ? 2 : eCharType; // convention not followed by client here
pkt.iID = combatant->getID();
pkt.iConditionBitFlag = combatant->getCompositeCondition();
NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
void Buffs::tickDrain(EntityRef self, Buff* buff, int mult) {
if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
return; // not implemented
Entity* entity = self.getEntity();
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
int damage = combatant->getMaxHP() / 100 * mult;
int dealt = combatant->takeDamage(buff->getLastSource(), damage);
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
pkt->iID = self.id;
pkt->eCT = combatant->getCharType();
pkt->iTB_ID = ECSB_BOUNDINGBALL;
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
drain->iDamage = dealt;
drain->iHP = combatant->getCurrentHP();
drain->eCT = pkt->eCT;
drain->iID = pkt->iID;
NPCManager::sendToViewable(self.getEntity(), (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
}
#pragma endregion

93
src/Buffs.hpp Normal file
View File

@@ -0,0 +1,93 @@
#pragma once
#include "core/Core.hpp"
#include "EntityRef.hpp"
#include <vector>
#include <functional>
/* forward declaration(s) */
class Buff;
template<class... Types>
using BuffCallback = std::function<void(EntityRef, Buff*, Types...)>;
#define CSB_FROM_ECSB(x) (1 << (x - 1))
enum class BuffClass {
NONE = ETBT_NONE,
NANO = ETBT_NANO,
GROUP_NANO = ETBT_GROUPNANO,
EGG = ETBT_SHINY,
ENVIRONMENT = ETBT_LANDEFFECT,
ITEM = ETBT_ITEM,
CASH_ITEM = ETBT_CASHITEM
};
enum class BuffValueSelector {
MAX_VALUE,
MIN_VALUE,
MAX_MAGNITUDE,
MIN_MAGNITUDE,
NET_TOTAL
};
struct BuffStack {
int durationTicks;
int value;
EntityRef source;
BuffClass buffStackClass;
};
class Buff {
private:
EntityRef self;
std::vector<BuffStack> stacks;
public:
int id;
/* called just after a stack is added or removed */
BuffCallback<int, BuffStack*> onUpdate;
/* called when the buff is combat-ticked */
BuffCallback<time_t> onCombatTick;
void tick(time_t);
void combatTick(time_t);
void clear();
void clear(BuffClass buffClass);
void addStack(BuffStack* stack);
/*
* Sometimes we need to determine if a buff
* is covered by a certain class, ex: nano
* vs. coco egg in the case of infection protection
*/
bool hasClass(BuffClass buffClass);
BuffClass maxClass();
int getValue(BuffValueSelector selector);
EntityRef getLastSource();
/*
* In general, a Buff object won't exist
* unless it has stacks. However, when
* popping stacks during iteration (onExpire),
* stacks will be empty for a brief moment
* when the last stack is popped.
*/
bool isStale();
void updateCallbacks(BuffCallback<int, BuffStack*> fOnUpdate, BuffCallback<time_t> fonTick);
Buff(int iid, EntityRef pSelf, BuffCallback<int, BuffStack*> fOnUpdate, BuffCallback<time_t> fOnCombatTick, BuffStack* firstStack)
: self(pSelf), id(iid), onUpdate(fOnUpdate), onCombatTick(fOnCombatTick) {
addStack(firstStack);
}
};
namespace Buffs {
void timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack);
void timeBuffTick(EntityRef self, Buff* buff);
void timeBuffTimeout(EntityRef self);
void tickDrain(EntityRef self, Buff* buff, int mult);
}

View File

@@ -1,10 +1,13 @@
#include "BuiltinCommands.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Chat.hpp"
#include "Items.hpp"
#include "Missions.hpp"
#include "Chat.hpp"
#include "Nanos.hpp"
#include "Rand.hpp"
// helper function, not a packet handler
void BuiltinCommands::setSpecialState(CNSocket* sock, CNPacketData* data) {
@@ -69,31 +72,41 @@ static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
// Handle serverside value-changes
switch (setData->iSetValueType) {
case 1:
plr->HP = setData->iSetValue;
case CN_GM_SET_VALUE_TYPE__HP:
response.iSetValue = plr->HP = setData->iSetValue;
break;
case 2:
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY :
plr->batteryW = setData->iSetValue;
// caps
if (plr->batteryW > 9999)
plr->batteryW = 9999;
response.iSetValue = plr->batteryW;
break;
case 3:
case CN_GM_SET_VALUE_TYPE__NANO_BATTERY:
plr->batteryN = setData->iSetValue;
// caps
if (plr->batteryN > 9999)
plr->batteryN = 9999;
response.iSetValue = plr->batteryN;
break;
case 4:
case CN_GM_SET_VALUE_TYPE__FUSION_MATTER:
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
response.iSetValue = plr->fusionmatter;
break;
case 5:
plr->money = setData->iSetValue;
case CN_GM_SET_VALUE_TYPE__CANDY:
response.iSetValue = plr->money = setData->iSetValue;
break;
case CN_GM_SET_VALUE_TYPE__SPEED:
case CN_GM_SET_VALUE_TYPE__JUMP:
response.iSetValue = setData->iSetValue;
break;
}
response.iPC_ID = setData->iPC_ID;
response.iSetValue = setData->iSetValue;
response.iSetValueType = setData->iSetValueType;
sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE);
@@ -247,17 +260,17 @@ static void teleportPlayer(CNSocket *sock, CNPacketData *data) {
uint64_t instance = plr->instanceID;
const int unstickRange = 400;
switch (req->eTeleportType) {
case eCN_GM_TeleportMapType__MyLocation:
switch ((eCN_GM_TeleportType)req->eTeleportType) {
case eCN_GM_TeleportType::MyLocation:
PlayerManager::sendPlayerTo(targetSock, plr->x, plr->y, plr->z, instance);
break;
case eCN_GM_TeleportMapType__MapXYZ:
case eCN_GM_TeleportType::MapXYZ:
instance = req->iToMap;
// fallthrough
case eCN_GM_TeleportMapType__XYZ:
case eCN_GM_TeleportType::XYZ:
PlayerManager::sendPlayerTo(targetSock, req->iToX, req->iToY, req->iToZ, instance);
break;
case eCN_GM_TeleportMapType__SomeoneLocation:
case eCN_GM_TeleportType::SomeoneLocation:
// player to teleport to
goalSock = PlayerManager::getSockFromAny(req->eGoalPCSearchBy, req->iGoalPC_ID, req->iGoalPC_UID,
AUTOU16TOU8(req->szGoalPC_FirstName), AUTOU16TOU8(req->szGoalPC_LastName));
@@ -269,7 +282,7 @@ static void teleportPlayer(CNSocket *sock, CNPacketData *data) {
PlayerManager::sendPlayerTo(targetSock, goalPlr->x, goalPlr->y, goalPlr->z, goalPlr->instanceID);
break;
case eCN_GM_TeleportMapType__Unstick:
case eCN_GM_TeleportType::Unstick:
targetPlr = PlayerManager::getPlayer(targetSock);
PlayerManager::sendPlayerTo(targetSock, targetPlr->x - unstickRange/2 + Rand::rand(unstickRange),
@@ -287,31 +300,52 @@ static void itemGMGiveHandler(CNSocket* sock, CNPacketData* data) {
return;
}
if (itemreq->eIL == 2) {
// Quest item, not a real item, handle this later, stubbed for now
} else if (itemreq->eIL == 1 && itemreq->Item.iType >= 0 && itemreq->Item.iType <= 10) {
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp);
if (Items::ItemData.find(std::pair<int32_t, int32_t>(itemreq->Item.iID, itemreq->Item.iType)) == Items::ItemData.end()) {
if (itemreq->eIL == 1) {
if (Items::ItemData.find(std::pair<int32_t, int32_t>(itemreq->Item.iID, itemreq->Item.iType)) == Items::ItemData.end()
|| itemreq->Item.iType < 0 || itemreq->Item.iType > 10) {
// invalid item
std::cout << "[WARN] Item id " << itemreq->Item.iID << " with type " << itemreq->Item.iType << " is invalid (give item)" << std::endl;
return;
}
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp);
resp.eIL = itemreq->eIL;
resp.iSlotNum = itemreq->iSlotNum;
if (itemreq->Item.iType == 10) {
// item is vehicle, set expiration date
// set time limit: current time + 7days
itemreq->Item.iTimeLimit = getTimestamp() + 604800;
}
resp.Item = itemreq->Item;
plr->Inven[itemreq->iSlotNum] = itemreq->Item;
} else if (itemreq->eIL == 2) {
int id = itemreq->Item.iID;
int slot = Missions::findQSlot(plr, id);
sock->sendPacket(resp, P_FE2CL_REP_PC_GIVE_ITEM_SUCC);
if (slot == -1) {
std::cout << "[WARN] Player has no room for quest items" << std::endl;
return;
}
if (id != 0)
std::cout << "new qitem in slot " << slot << std::endl;
// update player
if (id != 0) {
plr->QInven[slot].iType = 8;
plr->QInven[slot].iID = id;
plr->QInven[slot].iOpt += itemreq->Item.iOpt;
// destroy the item if its 0
if (plr->QInven[slot].iOpt == 0)
memset(&plr->QInven[slot], 0, sizeof(sItemBase));
}
std::cout << "Item id " << id << " is in slot " << slot << " of count " << plr->QInven[slot].iOpt << std::endl;
}
resp.eIL = itemreq->eIL;
resp.iSlotNum = itemreq->iSlotNum;
resp.Item = itemreq->Item;
sock->sendPacket(resp, P_FE2CL_REP_PC_GIVE_ITEM_SUCC);
}
static void nanoGMGiveHandler(CNSocket* sock, CNPacketData* data) {

View File

@@ -1,6 +1,9 @@
#include "Chat.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Groups.hpp"
#include "CustomCommands.hpp"
#include <assert.h>
@@ -19,10 +22,6 @@ static void chatHandler(CNSocket* sock, CNPacketData* data) {
return;
}
// if the player has an onChat Lua event registered, call it
if (plr->onChat != nullptr)
plr->onChat->call(fullChat.c_str());
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__MUTE_FREECHAT)
return;
@@ -229,10 +228,6 @@ static void tradeChatHandler(CNSocket* sock, CNPacketData* data) {
static void groupChatHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
@@ -255,16 +250,15 @@ static void groupChatHandler(CNSocket* sock, CNPacketData* data) {
resp.iSendPCID = plr->iID;
resp.iEmoteCode = chat->iEmoteCode;
Groups::sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
if (plr->group == nullptr)
sock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
else
Groups::sendToGroup(plr->group, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
}
static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
std::string logLine = "[GroupMenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
@@ -279,7 +273,9 @@ static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) {
resp.iSendPCID = plr->iID;
resp.iEmoteCode = chat->iEmoteCode;
Groups::sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
if (plr->group == nullptr)
sock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
Groups::sendToGroup(plr->group, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
}
// we only allow plain ascii, at least for now

View File

@@ -2,7 +2,10 @@
#define CMD_PREFIX '/'
#include "servers/CNShardServer.hpp"
#include "core/Core.hpp"
#include <string>
#include <vector>
namespace Chat {
extern std::vector<std::string> dump;

View File

@@ -1,17 +1,22 @@
#include "Chunking.hpp"
#include "PlayerManager.hpp"
#include "MobAI.hpp"
#include "NPCManager.hpp"
#include "settings.hpp"
#include "Combat.hpp"
#include "Eggs.hpp"
#include <assert.h>
using namespace Chunking;
/*
* The initial chunkPos value before a player is placed into the world.
*/
const ChunkPos Chunking::INVALID_CHUNK = {};
std::map<ChunkPos, Chunk*> Chunking::chunks;
static void newChunk(ChunkPos pos) {
if (chunkExists(pos)) {
std::cout << "[WARN] Tried to create a chunk that already exists\n";
std::cout << "[WARN] Tried to create a chunk that already exists" << std::endl;
return;
}
@@ -21,13 +26,13 @@ static void newChunk(ChunkPos pos) {
// add the chunk to the cache of all players and NPCs in the surrounding chunks
std::set<Chunk*> surroundings = getViewableChunks(pos);
for (Chunk* c : surroundings)
for (const EntityRef& ref : c->entities)
for (const EntityRef ref : c->entities)
ref.getEntity()->viewableChunks.insert(chunk);
}
static void deleteChunk(ChunkPos pos) {
if (!chunkExists(pos)) {
std::cout << "[WARN] Tried to delete a chunk that doesn't exist\n";
std::cout << "[WARN] Tried to delete a chunk that doesn't exist" << std::endl;
return;
}
@@ -36,24 +41,24 @@ static void deleteChunk(ChunkPos pos) {
// remove the chunk from the cache of all players and NPCs in the surrounding chunks
std::set<Chunk*> surroundings = getViewableChunks(pos);
for(Chunk* c : surroundings)
for (const EntityRef& ref : c->entities)
for (const EntityRef ref : c->entities)
ref.getEntity()->viewableChunks.erase(chunk);
chunks.erase(pos); // remove from map
delete chunk; // free from memory
}
void Chunking::trackEntity(ChunkPos chunkPos, const EntityRef& ref) {
void Chunking::trackEntity(ChunkPos chunkPos, const EntityRef ref) {
if (!chunkExists(chunkPos))
return; // shouldn't happen
chunks[chunkPos]->entities.insert(ref);
if (ref.type == EntityType::PLAYER)
if (ref.kind == EntityKind::PLAYER)
chunks[chunkPos]->nplayers++;
}
void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef& ref) {
void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef ref) {
if (!chunkExists(chunkPos))
return; // do nothing if chunk doesn't even exist
@@ -61,7 +66,7 @@ void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef& ref) {
chunk->entities.erase(ref); // gone
if (ref.type == EntityType::PLAYER)
if (ref.kind == EntityKind::PLAYER)
chunks[chunkPos]->nplayers--;
assert(chunks[chunkPos]->nplayers >= 0);
@@ -70,13 +75,13 @@ void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef& ref) {
deleteChunk(chunkPos);
}
void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef& ref) {
void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) {
Entity *ent = ref.getEntity();
bool alive = ent->isAlive();
bool alive = ent->isExtant();
// TODO: maybe optimize this, potentially using AROUND packets?
for (Chunk *chunk : chnks) {
for (const EntityRef& otherRef : chunk->entities) {
for (const EntityRef otherRef : chunk->entities) {
// skip oneself
if (ref == otherRef)
continue;
@@ -84,31 +89,31 @@ void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef& ref) {
Entity *other = otherRef.getEntity();
// notify all visible players of the existence of this Entity
if (alive && otherRef.type == EntityType::PLAYER) {
if (alive && otherRef.kind == EntityKind::PLAYER) {
ent->enterIntoViewOf(otherRef.sock);
}
// notify this *player* of the existence of all visible Entities
if (ref.type == EntityType::PLAYER && other->isAlive()) {
if (ref.kind == EntityKind::PLAYER && other->isExtant()) {
other->enterIntoViewOf(ref.sock);
}
// for mobs, increment playersInView
if (ref.type == EntityType::MOB && otherRef.type == EntityType::PLAYER)
if (ref.kind == EntityKind::MOB && otherRef.kind == EntityKind::PLAYER)
((Mob*)ent)->playersInView++;
if (otherRef.type == EntityType::MOB && ref.type == EntityType::PLAYER)
if (otherRef.kind == EntityKind::MOB && ref.kind == EntityKind::PLAYER)
((Mob*)other)->playersInView++;
}
}
}
void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef& ref) {
void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef ref) {
Entity *ent = ref.getEntity();
bool alive = ent->isAlive();
bool alive = ent->isExtant();
// TODO: same as above
for (Chunk *chunk : chnks) {
for (const EntityRef& otherRef : chunk->entities) {
for (const EntityRef otherRef : chunk->entities) {
// skip oneself
if (ref == otherRef)
continue;
@@ -116,19 +121,19 @@ void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef& r
Entity *other = otherRef.getEntity();
// notify all visible players of the departure of this Entity
if (alive && otherRef.type == EntityType::PLAYER) {
if (alive && otherRef.kind == EntityKind::PLAYER) {
ent->disappearFromViewOf(otherRef.sock);
}
// notify this *player* of the departure of all visible Entities
if (ref.type == EntityType::PLAYER && other->isAlive()) {
if (ref.kind == EntityKind::PLAYER && other->isExtant()) {
other->disappearFromViewOf(ref.sock);
}
// for mobs, decrement playersInView
if (ref.type == EntityType::MOB && otherRef.type == EntityType::PLAYER)
if (ref.kind == EntityKind::MOB && otherRef.kind == EntityKind::PLAYER)
((Mob*)ent)->playersInView--;
if (otherRef.type == EntityType::MOB && ref.type == EntityType::PLAYER)
if (otherRef.kind == EntityKind::MOB && ref.kind == EntityKind::PLAYER)
((Mob*)other)->playersInView--;
}
}
@@ -149,8 +154,8 @@ static void emptyChunk(ChunkPos chunkPos) {
// unspawn all of the mobs/npcs
std::set refs(chunk->entities);
for (const EntityRef& ref : refs) {
if (ref.type == EntityType::PLAYER)
for (const EntityRef ref : refs) {
if (ref.kind == EntityKind::PLAYER)
assert(0);
// every call of this will check if the chunk is empty and delete it if so
@@ -158,7 +163,7 @@ static void emptyChunk(ChunkPos chunkPos) {
}
}
void Chunking::updateEntityChunk(const EntityRef& ref, ChunkPos from, ChunkPos to) {
void Chunking::updateEntityChunk(const EntityRef ref, ChunkPos from, ChunkPos to) {
Entity* ent = ref.getEntity();
// move to other chunk's player set
@@ -200,7 +205,7 @@ bool Chunking::chunkExists(ChunkPos chunk) {
}
ChunkPos Chunking::chunkPosAt(int posX, int posY, uint64_t instanceID) {
return std::make_tuple(posX / (settings::VIEWDISTANCE / 3), posY / (settings::VIEWDISTANCE / 3), instanceID);
return ChunkPos(posX / (settings::VIEWDISTANCE / 3), posY / (settings::VIEWDISTANCE / 3), instanceID);
}
std::set<Chunk*> Chunking::getViewableChunks(ChunkPos chunk) {
@@ -213,7 +218,7 @@ std::set<Chunk*> Chunking::getViewableChunks(ChunkPos chunk) {
// grabs surrounding chunks if they exist
for (int i = -1; i < 2; i++) {
for (int z = -1; z < 2; z++) {
ChunkPos pos = std::make_tuple(x+i, y+z, inst);
ChunkPos pos = ChunkPos(x+i, y+z, inst);
// if chunk exists, add it to the set
if (chunkExists(pos))
@@ -262,54 +267,53 @@ void Chunking::createInstance(uint64_t instanceID) {
std::cout << "Creating instance " << instanceID << std::endl;
for (ChunkPos &coords : templateChunks) {
for (const EntityRef& ref : chunks[coords]->entities) {
if (ref.type == EntityType::PLAYER)
for (const EntityRef ref : chunks[coords]->entities) {
if (ref.kind == EntityKind::PLAYER)
continue;
int npcID = ref.id;
BaseNPC* baseNPC = (BaseNPC*)ref.getEntity();
// make a copy of each NPC in the template chunks and put them in the new instance
if (baseNPC->type == EntityType::MOB) {
if (baseNPC->kind == EntityKind::MOB) {
if (((Mob*)baseNPC)->groupLeader != 0 && ((Mob*)baseNPC)->groupLeader != npcID)
continue; // follower; don't copy individually
Mob* newMob = new Mob(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->appearanceData.iAngle,
instanceID, baseNPC->appearanceData.iNPCType, NPCManager::NPCData[baseNPC->appearanceData.iNPCType], NPCManager::nextId--);
NPCManager::NPCs[newMob->appearanceData.iNPC_ID] = newMob;
Mob* newMob = new Mob(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->angle,
instanceID, baseNPC->type, NPCManager::NPCData[baseNPC->type], NPCManager::nextId--);
NPCManager::NPCs[newMob->id] = newMob;
// if in a group, copy over group members as well
if (((Mob*)baseNPC)->groupLeader != 0) {
newMob->groupLeader = newMob->appearanceData.iNPC_ID; // set leader ID for new leader
newMob->groupLeader = newMob->id; // set leader ID for new leader
Mob* mobData = (Mob*)baseNPC;
for (int i = 0; i < 4; i++) {
if (mobData->groupMember[i] != 0) {
int followerID = NPCManager::nextId--; // id for follower
BaseNPC* baseFollower = NPCManager::NPCs[mobData->groupMember[i]]; // follower from template
// new follower instance
Mob* newMobFollower = new Mob(baseFollower->x, baseFollower->y, baseFollower->z, baseFollower->appearanceData.iAngle,
instanceID, baseFollower->appearanceData.iNPCType, NPCManager::NPCData[baseFollower->appearanceData.iNPCType], followerID);
Mob* newMobFollower = new Mob(baseFollower->x, baseFollower->y, baseFollower->z, baseFollower->angle,
instanceID, baseFollower->type, NPCManager::NPCData[baseFollower->type], followerID);
// add follower to NPC maps
NPCManager::NPCs[followerID] = newMobFollower;
// set follower-specific properties
newMobFollower->groupLeader = newMob->appearanceData.iNPC_ID;
newMobFollower->groupLeader = newMob->id;
newMobFollower->offsetX = ((Mob*)baseFollower)->offsetX;
newMobFollower->offsetY = ((Mob*)baseFollower)->offsetY;
// add follower copy to leader copy
newMob->groupMember[i] = followerID;
NPCManager::updateNPCPosition(followerID, baseFollower->x, baseFollower->y, baseFollower->z,
instanceID, baseFollower->appearanceData.iAngle);
instanceID, baseFollower->angle);
}
}
}
NPCManager::updateNPCPosition(newMob->appearanceData.iNPC_ID, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->appearanceData.iAngle);
NPCManager::updateNPCPosition(newMob->id, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->angle);
} else {
BaseNPC* newNPC = new BaseNPC(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->appearanceData.iAngle,
instanceID, baseNPC->appearanceData.iNPCType, NPCManager::nextId--);
NPCManager::NPCs[newNPC->appearanceData.iNPC_ID] = newNPC;
NPCManager::updateNPCPosition(newNPC->appearanceData.iNPC_ID, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->appearanceData.iAngle);
BaseNPC* newNPC = new BaseNPC(baseNPC->angle, instanceID, baseNPC->type, NPCManager::nextId--);
NPCManager::NPCs[newNPC->id] = newNPC;
NPCManager::updateNPCPosition(newNPC->id, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->angle);
}
}
}

View File

@@ -1,22 +1,26 @@
#pragma once
#include "core/Core.hpp"
#include "Entities.hpp"
#include "EntityRef.hpp"
#include <utility>
#include <set>
#include <map>
#include <tuple>
#include <algorithm>
#include <vector>
class Chunk {
public:
//std::set<CNSocket*> players;
//std::set<int32_t> NPCs;
std::set<EntityRef> entities;
int nplayers = 0;
};
// to help the readability of ChunkPos
typedef std::tuple<int, int, uint64_t> _ChunkPos;
class ChunkPos : public _ChunkPos {
public:
ChunkPos() : _ChunkPos(0, 0, (uint64_t) -1) {}
ChunkPos(int x, int y, uint64_t inst) : _ChunkPos(x, y, inst) {}
};
enum {
INSTANCE_OVERWORLD, // default instance every player starts in
INSTANCE_IZ, // these aren't actually used
@@ -26,13 +30,15 @@ enum {
namespace Chunking {
extern std::map<ChunkPos, Chunk*> chunks;
void updateEntityChunk(const EntityRef& ref, ChunkPos from, ChunkPos to);
extern const ChunkPos INVALID_CHUNK;
void trackEntity(ChunkPos chunkPos, const EntityRef& ref);
void untrackEntity(ChunkPos chunkPos, const EntityRef& ref);
void updateEntityChunk(const EntityRef ref, ChunkPos from, ChunkPos to);
void addEntityToChunks(std::set<Chunk*> chnks, const EntityRef& ref);
void removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef& ref);
void trackEntity(ChunkPos chunkPos, const EntityRef ref);
void untrackEntity(ChunkPos chunkPos, const EntityRef ref);
void addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref);
void removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef ref);
bool chunkExists(ChunkPos chunk);
ChunkPos chunkPosAt(int posX, int posY, uint64_t instanceID);

View File

@@ -1,22 +1,331 @@
#include "Combat.hpp"
#include "PlayerManager.hpp"
#include "Nanos.hpp"
#include "NPCManager.hpp"
#include "Items.hpp"
#include "Missions.hpp"
#include "Groups.hpp"
#include "Transport.hpp"
#include "Racing.hpp"
#include "Abilities.hpp"
#include "servers/CNShardServer.hpp"
#include "Rand.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include "Buffs.hpp"
#include <assert.h>
#include <iostream>
#include <functional>
using namespace Combat;
/// Player Id -> Bullet Id -> Bullet
std::map<int32_t, std::map<int8_t, Bullet>> Combat::Bullets;
#pragma region Player
bool Player::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) {
if(!isAlive())
return false;
if(!hasBuff(buffId)) {
buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack);
return true;
}
buffs[buffId]->updateCallbacks(onUpdate, onTick);
buffs[buffId]->addStack(stack);
return false;
}
Buff* Player::getBuff(int buffId) {
if(hasBuff(buffId)) {
return buffs[buffId];
}
return nullptr;
}
void Player::removeBuff(int buffId) {
if(hasBuff(buffId)) {
buffs[buffId]->clear();
delete buffs[buffId];
buffs.erase(buffId);
}
}
void Player::removeBuff(int buffId, BuffClass buffClass) {
if(hasBuff(buffId)) {
buffs[buffId]->clear(buffClass);
// buff might not be stale since another buff class might remain
if(buffs[buffId]->isStale()) {
delete buffs[buffId];
buffs.erase(buffId);
}
}
}
void Player::clearBuffs(bool force) {
auto it = buffs.begin();
while(it != buffs.end()) {
Buff* buff = (*it).second;
if(!force) buff->clear();
delete buff;
it = buffs.erase(it);
}
}
bool Player::hasBuff(int buffId) {
auto buff = buffs.find(buffId);
return buff != buffs.end() && !buff->second->isStale();
}
int Player::getCompositeCondition() {
int conditionBitFlag = 0;
for(auto buff : buffs) {
if(!buff.second->isStale() && buff.second->id > 0)
conditionBitFlag |= CSB_FROM_ECSB(buff.first);
}
return conditionBitFlag;
}
int Player::takeDamage(EntityRef src, int amt) {
int dmg = amt;
if(HP - dmg < 0) dmg = HP;
HP -= dmg;
return dmg;
}
int Player::heal(EntityRef src, int amt) {
int heal = amt;
if(HP + heal > getMaxHP()) heal = getMaxHP() - HP;
HP += heal;
return heal;
}
bool Player::isAlive() {
return HP > 0;
}
int Player::getCurrentHP() {
return HP;
}
int Player::getMaxHP() {
return PC_MAXHEALTH(level);
}
int Player::getLevel() {
return level;
}
std::vector<EntityRef> Player::getGroupMembers() {
std::vector<EntityRef> members;
if(group != nullptr)
members = group->members;
else
members.push_back(PlayerManager::getSockFromID(iID));
return members;
}
int32_t Player::getCharType() {
return 1; // eCharType (eCT_PC)
}
int32_t Player::getID() {
return iID;
}
EntityRef Player::getRef() {
return EntityRef(PlayerManager::getSockFromID(iID));
}
void Player::step(time_t currTime) {
CNSocket* sock = getRef().sock;
// nanos
for (int i = 0; i < 3; i++) {
if (activeNano != 0 && equippedNanos[i] == activeNano) { // tick active nano
sNano& nano = Nanos[activeNano];
int drainRate = 0;
if (Abilities::SkillTable.find(nano.iSkillID) != Abilities::SkillTable.end()) {
// nano has skill data
SkillData* skill = &Abilities::SkillTable[nano.iSkillID];
int boost = Nanos::getNanoBoost(this);
if (skill->drainType == SkillDrainType::PASSIVE)
drainRate = skill->batteryUse[boost * 3];
}
nano.iStamina -= 1 + drainRate / 5;
if (nano.iStamina <= 0)
Nanos::summonNano(sock, -1, true); // unsummon nano silently
} else if (Nanos[equippedNanos[i]].iStamina < 150) { // tick resting nano
sNano& nano = Nanos[equippedNanos[i]];
if (nano.iStamina < 150)
nano.iStamina += 1;
}
}
// buffs
for(auto buffEntry : buffs) {
buffEntry.second->combatTick(currTime);
if(!isAlive())
break; // unsafe to keep ticking if we're dead
}
}
#pragma endregion
#pragma region CombatNPC
bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) {
if(!isAlive())
return false;
if (this->state != AIState::COMBAT && this->state != AIState::ROAMING)
return false;
if(!hasBuff(buffId)) {
buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack);
return true;
}
buffs[buffId]->updateCallbacks(onUpdate, onTick);
buffs[buffId]->addStack(stack);
return false;
}
Buff* CombatNPC::getBuff(int buffId) {
if(hasBuff(buffId)) {
return buffs[buffId];
}
return nullptr;
}
void CombatNPC::removeBuff(int buffId) {
if(hasBuff(buffId)) {
buffs[buffId]->clear();
delete buffs[buffId];
buffs.erase(buffId);
}
}
void CombatNPC::removeBuff(int buffId, BuffClass buffClass) {
if(hasBuff(buffId)) {
buffs[buffId]->clear(buffClass);
// buff might not be stale since another buff class might remain
if(buffs[buffId]->isStale()) {
delete buffs[buffId];
buffs.erase(buffId);
}
}
}
void CombatNPC::clearBuffs(bool force) {
auto it = buffs.begin();
while(it != buffs.end()) {
Buff* buff = (*it).second;
if(!force) buff->clear();
delete buff;
it = buffs.erase(it);
}
}
bool CombatNPC::hasBuff(int buffId) {
auto buff = buffs.find(buffId);
return buff != buffs.end() && !buff->second->isStale();
}
int CombatNPC::getCompositeCondition() {
int conditionBitFlag = 0;
for(auto buff : buffs) {
if(!buff.second->isStale() && buff.second->id > 0)
conditionBitFlag |= CSB_FROM_ECSB(buff.first);
}
return conditionBitFlag;
}
int CombatNPC::takeDamage(EntityRef src, int amt) {
int dmg = amt;
if(hp - dmg < 0) dmg = hp;
hp -= dmg;
if(hp <= 0) transition(AIState::DEAD, src);
return dmg;
}
int CombatNPC::heal(EntityRef src, int amt) {
int heal = amt;
if(hp + heal > getMaxHP()) heal = getMaxHP() - hp;
hp += heal;
return heal;
}
bool CombatNPC::isAlive() {
return hp > 0;
}
int CombatNPC::getCurrentHP() {
return hp;
}
int CombatNPC::getMaxHP() {
return maxHealth;
}
int CombatNPC::getLevel() {
return level;
}
std::vector<EntityRef> CombatNPC::getGroupMembers() {
std::vector<EntityRef> members;
if(group != nullptr)
members = group->members;
else
members.push_back(id);
return members;
}
int32_t CombatNPC::getCharType() {
if(kind == EntityKind::MOB)
return 4; // eCharType (eCT_MOB)
return 2; // eCharType (eCT_NPC)
}
int32_t CombatNPC::getID() {
return id;
}
EntityRef CombatNPC::getRef() {
return EntityRef(id);
}
void CombatNPC::step(time_t currTime) {
if(stateHandlers.find(state) != stateHandlers.end())
stateHandlers[state](this, currTime);
else {
std::cout << "[WARN] State " << (int)state << " has no handler; going inactive" << std::endl;
transition(AIState::INACTIVE, id);
}
}
void CombatNPC::transition(AIState newState, EntityRef src) {
state = newState;
if (transitionHandlers.find(newState) != transitionHandlers.end())
transitionHandlers[newState](this, src);
else {
std::cout << "[WARN] Transition to " << (int)state << " has no handler; going inactive" << std::endl;
transition(AIState::INACTIVE, id);
}
// trigger special NPCEvents, if applicable
for (NPCEvent& event : NPCManager::NPCEvents)
if (event.triggerState == newState && event.npcType == type)
event.handler(this);
}
#pragma endregion
static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shouldCrit,
bool batteryBoost, int attackerStyle,
int defenderStyle, int difficulty) {
@@ -56,14 +365,10 @@ static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shou
return ret;
}
static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
auto pkt = (sP_CL2FE_REQ_PC_ATTACK_NPCs*)data->buf;
static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTargets) {
Player *plr = PlayerManager::getPlayer(sock);
auto targets = (int32_t*)data->trailers;
// rapid fire anti-cheat
// TODO: move this out of here, when generalizing packet frequency validation
time_t currTime = getTime();
if (currTime - plr->lastShot < plr->fireRate * 80)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0)
@@ -71,11 +376,28 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
plr->lastShot = currTime;
if (pkt->iNPCCnt > 3) // 3+ targets should never be possible
plr->suspicionRating += 10000;
// 3+ targets should never be possible
if (!allowManyTargets && targetCount > 3)
plr->suspicionRating += 10001;
if (plr->suspicionRating > 10000) // kill the socket when the player is too suspicious
// kill the socket when the player is too suspicious
if (plr->suspicionRating > 10000) {
sock->kill();
CNShardServer::_killConnection(sock);
return true;
}
return false;
}
static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
auto pkt = (sP_CL2FE_REQ_PC_ATTACK_NPCs*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
auto targets = (int32_t*)data->trailers;
// kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt, false))
return;
/*
* IMPORTANT: This validates memory safety in addition to preventing
@@ -101,7 +423,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
BaseNPC* npc = NPCManager::NPCs[targets[i]];
if (npc->type != EntityType::MOB) {
if (npc->kind != EntityKind::MOB) {
std::cout << "[WARN] pcAttackNpcs: NPC is not a mob" << std::endl;
return;
}
@@ -124,11 +446,11 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
else
plr->batteryW = 0;
damage.first = hitMob(sock, mob, damage.first);
damage.first = mob->takeDamage(sock, damage.first);
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iID = mob->id;
respdata[i].iDamage = damage.first;
respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHP = mob->hp;
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
}
@@ -150,12 +472,12 @@ void Combat::npcAttackPc(Mob *mob, time_t currTime) {
INITVARPACKET(respbuf, sP_FE2CL_NPC_ATTACK_PCs, pkt, sAttackResult, atk);
auto damage = getDamage(450 + (int)mob->data["m_iPower"], plr->defense, false, false, -1, -1, 0);
auto damage = getDamage(450 + (int)mob->data["m_iPower"], plr->defense, true, false, -1, -1, 0);
if (!(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
plr->HP -= damage.first;
pkt->iNPC_ID = mob->appearanceData.iNPC_ID;
pkt->iNPC_ID = mob->id;
pkt->iPCCnt = 1;
atk->iID = plr->iID;
@@ -167,122 +489,25 @@ void Combat::npcAttackPc(Mob *mob, time_t currTime) {
PlayerManager::sendToViewable(mob->target, respbuf, P_FE2CL_NPC_ATTACK_PCs);
if (plr->HP <= 0) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
if (!MobAI::aggroCheck(mob, currTime)) {
MobAI::clearDebuff(mob);
if (mob->groupLeader != 0)
MobAI::groupRetreat(mob);
}
if (!MobAI::aggroCheck(mob, getTime()))
mob->transition(AIState::RETREAT, mob->target);
}
}
int Combat::hitMob(CNSocket *sock, Mob *mob, int damage) {
// cannot kill mobs multiple times; cannot harm retreating mobs
if (mob->state != MobState::ROAMING && mob->state != MobState::COMBAT) {
return 0; // no damage
}
if (mob->skillStyle >= 0)
return 0; // don't hurt a mob casting corruption
if (mob->state == MobState::ROAMING) {
assert(mob->target == nullptr);
MobAI::enterCombat(sock, mob);
if (mob->groupLeader != 0)
MobAI::followToCombat(mob);
}
mob->appearanceData.iHP -= damage;
// wake up sleeping monster
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_MEZ) {
mob->appearanceData.iConditionBitFlag &= ~CSB_BIT_MEZ;
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = mob->appearanceData.iNPC_ID;
pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
if (mob->appearanceData.iHP <= 0)
killMob(mob->target, mob);
return damage;
}
void Combat::killMob(CNSocket *sock, Mob *mob) {
mob->state = MobState::DEAD;
mob->target = nullptr;
mob->appearanceData.iConditionBitFlag = 0;
mob->skillStyle = -1;
mob->unbuffTimes.clear();
mob->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
// check for the edge case where hitting the mob did not aggro it
if (sock != nullptr) {
Player* plr = PlayerManager::getPlayer(sock);
Items::DropRoll rolled;
Items::DropRoll eventRolled;
int rolledQItem = Rand::rand();
if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) {
Items::giveMobDrop(sock, mob, rolled, eventRolled);
Missions::mobKilled(sock, mob->appearanceData.iNPCType, rolledQItem);
} else {
Player* otherPlayer = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlayer == nullptr)
return;
for (int i = 0; i < otherPlayer->groupCnt; i++) {
CNSocket* sockTo = PlayerManager::getSockFromID(otherPlayer->groupIDs[i]);
if (sockTo == nullptr)
continue;
Player *otherPlr = PlayerManager::getPlayer(sockTo);
// only contribute to group members' kills if they're close enough
int dist = std::hypot(plr->x - otherPlr->x + 1, plr->y - otherPlr->y + 1);
if (dist > 5000)
continue;
Items::giveMobDrop(sockTo, mob, rolled, eventRolled);
Missions::mobKilled(sockTo, mob->appearanceData.iNPCType, rolledQItem);
}
}
}
// delay the despawn animation
mob->despawned = false;
// fire any triggered events
for (NPCEvent& event : NPCManager::NPCEvents)
if (event.trigger == ON_KILLED && event.npcType == mob->appearanceData.iNPCType)
event.handler(sock, mob);
auto it = Transport::NPCQueues.find(mob->appearanceData.iNPC_ID);
if (it == Transport::NPCQueues.end() || it->second.empty())
return;
// rewind or empty the movement queue
if (mob->staticPath) {
/*
* This is inelegant, but we wind forward in the path until we find the point that
* corresponds with the Mob's spawn point.
*
* IMPORTANT: The check in TableData::loadPaths() must pass or else this will loop forever.
* When a group of players is doing missions together, we want them to all get
* quest items at the same time, but we don't want the odds of quest item
* drops from different missions to be linked together. That's why we use a
* single RNG roll per mission task, and every group member shares that same
* set of rolls.
*/
auto& queue = it->second;
for (auto point = queue.front(); point.x != mob->spawnX || point.y != mob->spawnY; point = queue.front()) {
queue.pop();
queue.push(point);
}
} else {
Transport::NPCQueues.erase(mob->appearanceData.iNPC_ID);
void Combat::genQItemRolls(std::vector<Player*> players, std::map<int, int>& rolls) {
for (int i = 0; i < players.size(); i++) {
Player* member = players[i];
for (int j = 0; j < ACTIVE_MISSION_COUNT; j++)
if (member->tasks[j] != 0)
rolls[member->tasks[j]] = Rand::rand();
}
}
@@ -308,40 +533,27 @@ static void combatEnd(CNSocket *sock, CNPacketData *data) {
plr->healCooldown = 4000;
}
static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf;
static void dealGooDamage(CNSocket *sock) {
Player *plr = PlayerManager::getPlayer(sock);
if(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)
return; // ignore completely
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION) != (bool)pkt->iFlag)
plr->iConditionBitFlag ^= CSB_BIT_INFECTION;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt1);
pkt1.eCSTB = ECSB_INFECTION; // eCharStatusTimeBuffID
pkt1.eTBU = 1; // eTimeBuffUpdate
pkt1.eTBT = 0; // eTimeBuffType 1 means nano
pkt1.iConditionBitFlag = plr->iConditionBitFlag;
sock->sendPacket((void*)&pkt1, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
}
static void dealGooDamage(CNSocket *sock, int amount) {
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_DotDamage);
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
Player *plr = PlayerManager::getPlayer(sock);
memset(respbuf, 0, resplen);
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
sSkillResult_DotDamage *dmg = (sSkillResult_DotDamage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
if (plr->iConditionBitFlag & CSB_BIT_PROTECT_INFECTION) {
int amount = PC_MAXHEALTH(plr->level) * 3 / 20;
Buff* protectionBuff = plr->getBuff(ECSB_PROTECT_INFECTION);
if (protectionBuff != nullptr) {
amount = -2; // -2 is the magic number for "Protected" to appear as the damage number
dmg->bProtected = 1;
// eggs allow protection without nanos
if (plr->activeNano != -1 && (plr->iSelfConditionBitFlag & CSB_BIT_PROTECT_INFECTION))
if (protectionBuff->maxClass() <= BuffClass::NANO && plr->activeNano != -1)
plr->Nanos[plr->activeNano].iStamina -= 3;
} else {
plr->HP -= amount;
@@ -365,27 +577,54 @@ static void dealGooDamage(CNSocket *sock, int amount) {
dmg->iID = plr->iID;
dmg->iDamage = amount;
dmg->iHP = plr->HP;
dmg->iConditionBitFlag = plr->iConditionBitFlag;
dmg->iConditionBitFlag = plr->getCompositeCondition();
sock->sendPacket((void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
}
static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
// infection debuff toggles as the client asks it to,
// so we add and remove a permanent debuff
if (pkt->iFlag && !plr->hasBuff(ECSB_INFECTION)) {
BuffStack infection = {
-1, // infinite
0, // no value
sock, // self-inflicted
BuffClass::ENVIRONMENT
};
plr->addBuff(ECSB_INFECTION,
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
Buffs::timeBuffUpdate(self, buff, status, stack);
},
[](EntityRef self, Buff* buff, time_t currTime) {
if(self.kind == EntityKind::PLAYER)
dealGooDamage(self.sock);
},
&infection);
} else if(!pkt->iFlag && plr->hasBuff(ECSB_INFECTION)) {
plr->removeBuff(ECSB_INFECTION);
}
}
static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_REQ_PC_ATTACK_CHARs* pkt = (sP_CL2FE_REQ_PC_ATTACK_CHARs*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
// only GMs can use this this variant
// only GMs can use this variant
if (plr->accountLevel > 30)
return;
// Unlike the attack mob packet, attacking players packet has an 8-byte trail (Instead of 4 bytes).
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs), pkt->iTargetCnt, sizeof(int32_t) * 2, data->size)) {
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs), pkt->iTargetCnt, sizeof(sGM_PVPTarget), data->size)) {
std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ATTACK_CHARs packet size\n";
return;
}
int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs));
sGM_PVPTarget* pktdata = (sGM_PVPTarget*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs));
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC), pkt->iTargetCnt, sizeof(sAttackResult))) {
std::cout << "[WARN] bad sP_FE2CL_PC_ATTACK_CHARs_SUCC packet size\n";
@@ -404,11 +643,19 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
resp->iTargetCnt = pkt->iTargetCnt;
for (int i = 0; i < pkt->iTargetCnt; i++) {
if (pktdata[i*2+1] == 1) { // eCT == 1; attack player
Player *target = nullptr;
ICombatant* target = nullptr;
std::pair<int, int> damage;
if (pkt->iTargetCnt > 1)
damage.first = plr->groupDamage;
else
damage.first = plr->pointDamage;
if (pktdata[i].eCT == 1) { // eCT == 1; attack player
for (auto& pair : PlayerManager::players) {
if (pair.second->iID == pktdata[i*2]) {
if (pair.second->iID == pktdata[i].iID) {
target = pair.second;
break;
}
@@ -420,67 +667,41 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
return;
}
std::pair<int,int> damage;
damage = getDamage(damage.first, ((Player*)target)->defense, true, (plr->batteryW > 6 + plr->level), -1, -1, 0);
if (pkt->iTargetCnt > 1)
damage.first = plr->groupDamage;
else
damage.first = plr->pointDamage;
} else { // eCT == 4; attack mob
damage = getDamage(damage.first, target->defense, true, (plr->batteryW > 6 + plr->level), -1, -1, 0);
if (NPCManager::NPCs.find(pktdata[i].iID) == NPCManager::NPCs.end()) {
// not sure how to best handle this
std::cout << "[WARN] pcAttackChars: NPC ID not found" << std::endl;
return;
}
BaseNPC* npc = NPCManager::NPCs[pktdata[i].iID];
if (npc->kind != EntityKind::MOB) {
std::cout << "[WARN] pcAttackChars: NPC is not a mob" << std::endl;
return;
}
Mob* mob = (Mob*)npc;
target = mob;
int difficulty = (int)mob->data["m_iNpcLevel"];
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty),
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
}
if (plr->batteryW >= 6 + plr->level)
plr->batteryW -= 6 + plr->level;
else
plr->batteryW = 0;
target->HP -= damage.first;
damage.first = target->takeDamage(sock, damage.first);
respdata[i].eCT = pktdata[i*2+1];
respdata[i].iID = target->iID;
respdata[i].eCT = pktdata[i].eCT;
respdata[i].iID = target->getID();
respdata[i].iDamage = damage.first;
respdata[i].iHP = target->HP;
respdata[i].iHP = target->getCurrentHP();
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
} else { // eCT == 4; attack mob
if (NPCManager::NPCs.find(pktdata[i*2]) == NPCManager::NPCs.end()) {
// not sure how to best handle this
std::cout << "[WARN] pcAttackChars: NPC ID not found" << std::endl;
return;
}
BaseNPC* npc = NPCManager::NPCs[pktdata[i * 2]];
if (npc->type != EntityType::MOB) {
std::cout << "[WARN] pcAttackChars: NPC is not a mob" << std::endl;
return;
}
Mob* mob = (Mob*)npc;
std::pair<int,int> damage;
if (pkt->iTargetCnt > 1)
damage.first = plr->groupDamage;
else
damage.first = plr->pointDamage;
int difficulty = (int)mob->data["m_iNpcLevel"];
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty),
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
if (plr->batteryW >= 6 + difficulty)
plr->batteryW -= 6 + difficulty;
else
plr->batteryW = 0;
damage.first = hitMob(sock, mob, damage.first);
respdata[i].eCT = pktdata[i*2+1];
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iDamage = damage.first;
respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
}
}
sock->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_CHARs_SUCC, resplen);
@@ -616,17 +837,9 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
return;
}
// rapid fire anti-cheat
time_t currTime = getTime();
if (currTime - plr->lastShot < plr->fireRate * 80)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // lose suspicion for delayed firing
plr->lastShot = currTime;
if (plr->suspicionRating > 10000) // kill the socket when the player is too suspicious
sock->kill();
// kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iTargetCnt, true))
return;
/*
* initialize response struct
@@ -656,7 +869,7 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
}
BaseNPC* npc = NPCManager::NPCs[pktdata[i]];
if (npc->type != EntityType::MOB) {
if (npc->kind != EntityKind::MOB) {
std::cout << "[WARN] projectileHit: NPC is not a mob" << std::endl;
return;
}
@@ -669,11 +882,11 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
int difficulty = (int)mob->data["m_iNpcLevel"];
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, bullet->weaponBoost, Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
damage.first = hitMob(sock, mob, damage.first);
damage.first = mob->takeDamage(sock, damage.first);
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iID = mob->id;
respdata[i].iDamage = damage.first;
respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHP = mob->hp;
respdata[i].iHitFlag = damage.second;
}
@@ -688,6 +901,7 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
static void playerTick(CNServer *serv, time_t currTime) {
static time_t lastHealTime = 0;
static time_t lastCombatTIme = 0;
for (auto& pair : PlayerManager::players) {
CNSocket *sock = pair.first;
@@ -695,18 +909,13 @@ static void playerTick(CNServer *serv, time_t currTime) {
bool transmit = false;
// group ticks
if (plr->groupCnt > 1)
Groups::groupTickInfo(plr);
if (plr->group != nullptr)
Groups::groupTickInfo(sock);
// do not tick dead players
if (plr->HP <= 0)
continue;
// fm patch/lake damage
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION)
&& !(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
dealGooDamage(sock, PC_MAXHEALTH(plr->level) * 3 / 20);
// heal
if (currTime - lastHealTime >= 4000 && !plr->inCombat && plr->HP < PC_MAXHEALTH(plr->level)) {
if (currTime - lastHealTime - plr->healCooldown >= 4000) {
@@ -718,22 +927,24 @@ static void playerTick(CNServer *serv, time_t currTime) {
plr->healCooldown -= 4000;
}
for (int i = 0; i < 3; i++) {
if (plr->activeNano != 0 && plr->equippedNanos[i] == plr->activeNano) { // spend stamina
plr->Nanos[plr->activeNano].iStamina -= 1 + plr->nanoDrainRate / 5;
if (plr->Nanos[plr->activeNano].iStamina <= 0)
Nanos::summonNano(sock, -1, true); // unsummon nano silently
// combat tick
if(currTime - lastCombatTIme >= 2000) {
plr->step(currTime);
transmit = true;
} else if (plr->Nanos[plr->equippedNanos[i]].iStamina < 150) { // regain stamina
sNano& nano = plr->Nanos[plr->equippedNanos[i]];
nano.iStamina += 1;
}
if (nano.iStamina > 150)
nano.iStamina = 150;
transmit = true;
// nanos
if (plr->activeNano != 0) { // tick active nano
sNano* nano = plr->getActiveNano();
if (Abilities::SkillTable.find(nano->iSkillID) != Abilities::SkillTable.end()) {
// nano has skill data
SkillData* skill = &Abilities::SkillTable[nano->iSkillID];
if (skill->drainType == SkillDrainType::PASSIVE) {
ICombatant* src = dynamic_cast<ICombatant*>(plr);
int32_t targets[] = { plr->iID };
std::vector<ICombatant*> affectedCombatants = Abilities::matchTargets(src, skill, 1, targets);
Abilities::useNanoSkill(sock, skill, *nano, affectedCombatants);
}
}
}
@@ -749,6 +960,20 @@ static void playerTick(CNServer *serv, time_t currTime) {
PlayerManager::sendToViewable(sock, (void*)&dead, P_FE2CL_PC_SUDDEN_DEAD, sizeof(sP_FE2CL_PC_SUDDEN_DEAD));
}
// process buffsets
auto it = plr->buffs.begin();
while(it != plr->buffs.end()) {
Buff* buff = (*it).second;
//buff->combatTick() gets called in Player::step
buff->tick(currTime);
if(buff->isStale()) {
// garbage collect
it = plr->buffs.erase(it);
delete buff;
}
else it++;
}
if (transmit) {
INITSTRUCT(sP_FE2CL_REP_PC_TICK, pkt);
@@ -763,13 +988,15 @@ static void playerTick(CNServer *serv, time_t currTime) {
}
}
// if this was a heal tick, update the counter outside of the loop
// if this was a heal/combat tick, update the counters outside of the loop
if (currTime - lastHealTime >= 4000)
lastHealTime = currTime;
if(currTime - lastCombatTIme >= 2000)
lastCombatTIme = currTime;
}
void Combat::init() {
REGISTER_SHARD_TIMER(playerTick, 2000);
REGISTER_SHARD_TIMER(playerTick, MS_PER_PLAYER_TICK);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_NPCs, pcAttackNpcs);

View File

@@ -1,15 +1,10 @@
#pragma once
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "NPC.hpp"
#include "Player.hpp"
#include "MobAI.hpp"
#include "JSON.hpp"
#include <map>
#include <unordered_map>
#include <queue>
#include <vector>
struct Bullet {
int pointDamage;
@@ -24,6 +19,5 @@ namespace Combat {
void init();
void npcAttackPc(Mob *mob, time_t currTime);
int hitMob(CNSocket *sock, Mob *mob, int damage);
void killMob(CNSocket *sock, Mob *mob);
void genQItemRolls(std::vector<Player*> players, std::map<int, int>& rolls);
}

View File

@@ -1,20 +1,20 @@
#include "CustomCommands.hpp"
#include "Chat.hpp"
#include "db/Database.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Chat.hpp"
#include "TableData.hpp"
#include "NPCManager.hpp"
#include "Eggs.hpp"
#include "MobAI.hpp"
#include "Items.hpp"
#include "db/Database.hpp"
#include "Transport.hpp"
#include "Missions.hpp"
#include "lua/LuaManager.hpp"
#include "Eggs.hpp"
#include "Items.hpp"
#include "Abilities.hpp"
#include <sstream>
#include <iterator>
#include <math.h>
#include <limits.h>
typedef void (*CommandHandler)(std::string fullString, std::vector<std::string>& args, CNSocket* sock);
@@ -83,7 +83,77 @@ static void helpCommand(std::string full, std::vector<std::string>& args, CNSock
}
static void accessCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Chat::sendServerMessage(sock, "Your access level is " + std::to_string(PlayerManager::getPlayer(sock)->accountLevel));
if (args.size() < 2) {
Chat::sendServerMessage(sock, "Usage: /access <id> [new_level]");
Chat::sendServerMessage(sock, "Use . for id to select yourself");
return;
}
char *tmp;
Player* self = PlayerManager::getPlayer(sock);
int selfAccess = self->accountLevel;
Player* player;
if (args[1].compare(".") == 0) {
player = self;
} else {
int id = std::strtol(args[1].c_str(), &tmp, 10);
if (*tmp) {
Chat::sendServerMessage(sock, "Invalid player ID " + args[1]);
return;
}
player = PlayerManager::getPlayerFromID(id);
if (player == nullptr) {
Chat::sendServerMessage(sock, "Could not find player with ID " + std::to_string(id));
return;
}
// Messing with other players requires a baseline access of 30
if (player != self && selfAccess > 30) {
Chat::sendServerMessage(sock, "Can't check or change other players access levels (insufficient privileges)");
return;
}
}
std::string playerName = PlayerManager::getPlayerName(player);
int currentAccess = player->accountLevel;
if (args.size() < 3) {
// just check
Chat::sendServerMessage(sock, playerName + " has access level " + std::to_string(currentAccess));
return;
}
// Can't change the access level of someone with stronger privileges
// N.B. lower value = stronger privileges
if (currentAccess <= selfAccess) {
Chat::sendServerMessage(sock, "Can't change this player's access level (insufficient privileges)");
return;
}
int newAccess = std::strtol(args[2].c_str(), &tmp, 10);
if (*tmp) {
Chat::sendServerMessage(sock, "Invalid access level " + args[2]);
return;
}
// Can only assign an access level weaker than yours
if (newAccess <= selfAccess) {
Chat::sendServerMessage(sock, "Can only assign privileges weaker than your own");
return;
}
player->accountLevel = newAccess;
// Save to database
int accountId = Database::getAccountIdForPlayer(player->iID);
Database::updateAccountLevel(accountId, newAccess);
std::string msg = "Changed access level for " + playerName + " from " + std::to_string(currentAccess) + " to " + std::to_string(newAccess);
if (newAccess <= 50 && currentAccess > 50)
msg += " (they must log out and back in for some commands to be enabled)";
Chat::sendServerMessage(sock, msg);
}
static void populationCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -244,20 +314,20 @@ static void summonWCommand(std::string full, std::vector<std::string>& args, CNS
BaseNPC *npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, true);
// update angle
npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, plr->instanceID, npc->appearanceData.iAngle);
npc->angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, plr->x, plr->y, plr->z, plr->instanceID, npc->angle);
// if we're in a lair, we need to spawn the NPC in both the private instance and the template
if (PLAYERID(plr->instanceID) != 0) {
npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, true, true);
npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, npc->instanceID, npc->appearanceData.iAngle);
npc->angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, plr->x, plr->y, plr->z, npc->instanceID, npc->angle);
}
Chat::sendServerMessage(sock, "/summonW: placed mob with type: " + std::to_string(type) +
", id: " + std::to_string(npc->appearanceData.iNPC_ID));
TableData::RunningMobs[npc->appearanceData.iNPC_ID] = npc; // only record the one in the template
", id: " + std::to_string(npc->id));
TableData::RunningMobs[npc->id] = npc; // only record the one in the template
}
static void unsummonWCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -270,24 +340,24 @@ static void unsummonWCommand(std::string full, std::vector<std::string>& args, C
return;
}
if (TableData::RunningEggs.find(npc->appearanceData.iNPC_ID) != TableData::RunningEggs.end()) {
Chat::sendServerMessage(sock, "/unsummonW: removed egg with type: " + std::to_string(npc->appearanceData.iNPCType) +
", id: " + std::to_string(npc->appearanceData.iNPC_ID));
TableData::RunningEggs.erase(npc->appearanceData.iNPC_ID);
NPCManager::destroyNPC(npc->appearanceData.iNPC_ID);
if (TableData::RunningEggs.find(npc->id) != TableData::RunningEggs.end()) {
Chat::sendServerMessage(sock, "/unsummonW: removed egg with type: " + std::to_string(npc->type) +
", id: " + std::to_string(npc->id));
TableData::RunningEggs.erase(npc->id);
NPCManager::destroyNPC(npc->id);
return;
}
if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) == TableData::RunningMobs.end()
&& TableData::RunningGroups.find(npc->appearanceData.iNPC_ID) == TableData::RunningGroups.end()) {
if (TableData::RunningMobs.find(npc->id) == TableData::RunningMobs.end()
&& TableData::RunningGroups.find(npc->id) == TableData::RunningGroups.end()) {
Chat::sendServerMessage(sock, "/unsummonW: Closest NPC is not a gruntwork mob.");
return;
}
if (NPCManager::NPCs.find(npc->appearanceData.iNPC_ID) != NPCManager::NPCs.end() && NPCManager::NPCs[npc->appearanceData.iNPC_ID]->type == EntityType::MOB) {
if (NPCManager::NPCs.find(npc->id) != NPCManager::NPCs.end() && NPCManager::NPCs[npc->id]->kind == EntityKind::MOB) {
int leadId = ((Mob*)npc)->groupLeader;
if (leadId != 0) {
if (NPCManager::NPCs.find(leadId) == NPCManager::NPCs.end() || NPCManager::NPCs[leadId]->type != EntityType::MOB) {
if (NPCManager::NPCs.find(leadId) == NPCManager::NPCs.end() || NPCManager::NPCs[leadId]->kind != EntityKind::MOB) {
std::cout << "[WARN] unsummonW: leader not found!" << std::endl;
}
Mob* leadNpc = (Mob*)NPCManager::NPCs[leadId];
@@ -295,7 +365,7 @@ static void unsummonWCommand(std::string full, std::vector<std::string>& args, C
if (leadNpc->groupMember[i] == 0)
break;
if (NPCManager::NPCs.find(leadNpc->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadNpc->groupMember[i]]->type != EntityType::MOB) {
if (NPCManager::NPCs.find(leadNpc->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadNpc->groupMember[i]]->kind != EntityKind::MOB) {
std::cout << "[WARN] unsommonW: leader can't find a group member!" << std::endl;
continue;
}
@@ -309,12 +379,12 @@ static void unsummonWCommand(std::string full, std::vector<std::string>& args, C
}
}
Chat::sendServerMessage(sock, "/unsummonW: removed mob with type: " + std::to_string(npc->appearanceData.iNPCType) +
", id: " + std::to_string(npc->appearanceData.iNPC_ID));
Chat::sendServerMessage(sock, "/unsummonW: removed mob with type: " + std::to_string(npc->type) +
", id: " + std::to_string(npc->id));
TableData::RunningMobs.erase(npc->appearanceData.iNPC_ID);
TableData::RunningMobs.erase(npc->id);
NPCManager::destroyNPC(npc->appearanceData.iNPC_ID);
NPCManager::destroyNPC(npc->id);
}
static void toggleAiCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -325,11 +395,11 @@ static void toggleAiCommand(std::string full, std::vector<std::string>& args, CN
// return all mobs to their spawn points
for (auto& pair : NPCManager::NPCs) {
if (pair.second->type != EntityType::MOB)
if (pair.second->kind != EntityKind::MOB)
continue;
Mob* mob = (Mob*)pair.second;
mob->state = MobState::RETREAT;
mob->state = AIState::RETREAT;
mob->target = nullptr;
mob->nextMovement = getTime();
@@ -357,34 +427,27 @@ static void npcRotateCommand(std::string full, std::vector<std::string>& args, C
}
int angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, npc->instanceID, angle);
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle);
// if it's a gruntwork NPC, rotate in-place
if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) != TableData::RunningMobs.end()) {
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, npc->instanceID, angle);
bool isGruntworkNpc = true;
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for gruntwork NPC "
+ std::to_string(npc->appearanceData.iNPC_ID));
} else {
TableData::RunningNPCRotations[npc->appearanceData.iNPC_ID] = angle;
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for NPC "
+ std::to_string(npc->appearanceData.iNPC_ID));
// add a rotation entry to the gruntwork file, unless it's already a gruntwork NPC
if (TableData::RunningMobs.find(npc->id) == TableData::RunningMobs.end()) {
TableData::RunningNPCRotations[npc->id] = angle;
isGruntworkNpc = false;
}
// update rotation clientside
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
pkt.NPCAppearanceData = npc->appearanceData;
sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) +
" for " + (isGruntworkNpc ? "gruntwork " : "") + "NPC " + std::to_string(npc->id));
// update rotation clientside by refreshing the player's chunks (same as the /refresh command)
PlayerManager::updatePlayerPositionForWarp(sock, plr->x, plr->y, plr->z, plr->instanceID);
}
static void refreshCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
EntityRef ref = {sock};
Entity* plr = ref.getEntity();
ChunkPos currentChunk = plr->chunkPos;
ChunkPos nullChunk = std::make_tuple(0, 0, 0);
Chunking::updateEntityChunk(ref, currentChunk, nullChunk);
Chunking::updateEntityChunk(ref, nullChunk, currentChunk);
Player *plr = PlayerManager::getPlayer(sock);
PlayerManager::updatePlayerPositionForWarp(sock, plr->x, plr->y, plr->z, plr->instanceID);
}
static void instanceCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -447,9 +510,9 @@ static void npcInstanceCommand(std::string full, std::vector<std::string>& args,
return;
}
Chat::sendServerMessage(sock, "[NPCI] Moving NPC with ID " + std::to_string(npc->appearanceData.iNPC_ID) + " to instance " + std::to_string(instance));
TableData::RunningNPCMapNumbers[npc->appearanceData.iNPC_ID] = instance;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y, npc->z, instance, npc->appearanceData.iAngle);
Chat::sendServerMessage(sock, "[NPCI] Moving NPC with ID " + std::to_string(npc->id) + " to instance " + std::to_string(instance));
TableData::RunningNPCMapNumbers[npc->id] = instance;
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, instance, npc->angle);
}
static void minfoCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -505,9 +568,12 @@ static void buffCommand(std::string full, std::vector<std::string>& args, CNSock
if (*tmp)
return;
if (Eggs::eggBuffPlayer(sock, skillId, 0, duration)<0)
if (Abilities::SkillTable.count(skillId) == 0) {
Chat::sendServerMessage(sock, "/buff: unknown skill Id");
return;
}
Eggs::eggBuffPlayer(sock, skillId, 0, duration);
}
static void eggCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -536,7 +602,7 @@ static void eggCommand(std::string full, std::vector<std::string>& args, CNSocke
int addX = 0; //-500.0f * sin(plr->angle / 180.0f * M_PI);
int addY = 0; //-500.0f * cos(plr->angle / 180.0f * M_PI);
Egg* egg = new Egg(plr->x + addX, plr->y + addY, plr->z, plr->instanceID, eggType, id, false); // change last arg to true after gruntwork
Egg* egg = new Egg(plr->instanceID, eggType, id, false); // change last arg to true after gruntwork
NPCManager::NPCs[id] = egg;
NPCManager::updateNPCPosition(id, plr->x + addX, plr->y + addY, plr->z, plr->instanceID, plr->angle);
@@ -613,40 +679,40 @@ static void summonGroupCommand(std::string full, std::vector<std::string>& args,
}
BaseNPC *npc = NPCManager::summonNPC(x, y, z, plr->instanceID, type, wCommand);
if (team == 2 && i > 0 && npc->type == EntityType::MOB) {
leadNpc->groupMember[i-1] = npc->appearanceData.iNPC_ID;
Mob* mob = (Mob*)NPCManager::NPCs[npc->appearanceData.iNPC_ID];
mob->groupLeader = leadNpc->appearanceData.iNPC_ID;
if (team == 2 && i > 0 && npc->kind == EntityKind::MOB) {
leadNpc->groupMember[i-1] = npc->id;
Mob* mob = (Mob*)NPCManager::NPCs[npc->id];
mob->groupLeader = leadNpc->id;
mob->offsetX = x - plr->x;
mob->offsetY = y - plr->y;
}
npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, x, y, z, plr->instanceID, npc->appearanceData.iAngle);
npc->angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, x, y, z, plr->instanceID, npc->angle);
// if we're in a lair, we need to spawn the NPC in both the private instance and the template
if (PLAYERID(plr->instanceID) != 0) {
npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, wCommand, true);
if (team == 2 && i > 0 && npc->type == EntityType::MOB) {
leadNpc->groupMember[i-1] = npc->appearanceData.iNPC_ID;
Mob* mob = (Mob*)NPCManager::NPCs[npc->appearanceData.iNPC_ID];
mob->groupLeader = leadNpc->appearanceData.iNPC_ID;
if (team == 2 && i > 0 && npc->kind == EntityKind::MOB) {
leadNpc->groupMember[i-1] = npc->id;
Mob* mob = (Mob*)NPCManager::NPCs[npc->id];
mob->groupLeader = leadNpc->id;
mob->offsetX = x - plr->x;
mob->offsetY = y - plr->y;
}
npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, x, y, z, plr->instanceID, npc->appearanceData.iAngle);
npc->angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->id, x, y, z, plr->instanceID, npc->angle);
}
Chat::sendServerMessage(sock, "/summonGroup(W): placed mob with type: " + std::to_string(type) +
", id: " + std::to_string(npc->appearanceData.iNPC_ID));
", id: " + std::to_string(npc->id));
if (i == 0 && team == 2 && npc->type == EntityType::MOB) {
if (i == 0 && team == 2 && npc->kind == EntityKind::MOB) {
type = type2;
leadNpc = (Mob*)NPCManager::NPCs[npc->appearanceData.iNPC_ID];
leadNpc->groupLeader = leadNpc->appearanceData.iNPC_ID;
leadNpc = (Mob*)NPCManager::NPCs[npc->id];
leadNpc->groupLeader = leadNpc->id;
}
}
@@ -658,7 +724,7 @@ static void summonGroupCommand(std::string full, std::vector<std::string>& args,
return;
}
TableData::RunningGroups[leadNpc->appearanceData.iNPC_ID] = leadNpc; // only record the leader
TableData::RunningGroups[leadNpc->id] = leadNpc; // only record the leader
}
static void flushCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
@@ -675,15 +741,14 @@ static void whoisCommand(std::string full, std::vector<std::string>& args, CNSoc
return;
}
Chat::sendServerMessage(sock, "[WHOIS] ID: " + std::to_string(npc->appearanceData.iNPC_ID));
Chat::sendServerMessage(sock, "[WHOIS] Type: " + std::to_string(npc->appearanceData.iNPCType));
Chat::sendServerMessage(sock, "[WHOIS] HP: " + std::to_string(npc->appearanceData.iHP));
Chat::sendServerMessage(sock, "[WHOIS] CBF: " + std::to_string(npc->appearanceData.iConditionBitFlag));
Chat::sendServerMessage(sock, "[WHOIS] EntityType: " + std::to_string((int)npc->type));
Chat::sendServerMessage(sock, "[WHOIS] ID: " + std::to_string(npc->id));
Chat::sendServerMessage(sock, "[WHOIS] Type: " + std::to_string(npc->type));
Chat::sendServerMessage(sock, "[WHOIS] HP: " + std::to_string(npc->hp));
Chat::sendServerMessage(sock, "[WHOIS] EntityType: " + std::to_string((int)npc->kind));
Chat::sendServerMessage(sock, "[WHOIS] X: " + std::to_string(npc->x));
Chat::sendServerMessage(sock, "[WHOIS] Y: " + std::to_string(npc->y));
Chat::sendServerMessage(sock, "[WHOIS] Z: " + std::to_string(npc->z));
Chat::sendServerMessage(sock, "[WHOIS] Angle: " + std::to_string(npc->appearanceData.iAngle));
Chat::sendServerMessage(sock, "[WHOIS] Angle: " + std::to_string(npc->angle));
std::string chunkPosition = std::to_string(std::get<0>(npc->chunkPos)) + ", " + std::to_string(std::get<1>(npc->chunkPos)) + ", " + std::to_string(std::get<2>(npc->chunkPos));
Chat::sendServerMessage(sock, "[WHOIS] Chunk: {" + chunkPosition + "}");
Chat::sendServerMessage(sock, "[WHOIS] MapNum: " + std::to_string(MAPNUM(npc->instanceID)));
@@ -692,39 +757,35 @@ static void whoisCommand(std::string full, std::vector<std::string>& args, CNSoc
static void lairUnlockCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
if (!Chunking::chunkExists(plr->chunkPos))
return;
Chunk* chnk = Chunking::chunks[plr->chunkPos];
int taskID = -1;
int missionID = -1;
int found = 0;
int lastDist = INT_MAX;
for (Chunk *chnk : Chunking::getViewableChunks(plr->chunkPos)) {
for (const EntityRef& ref : chnk->entities) {
if (ref.type == EntityType::PLAYER)
if (ref.kind == EntityKind::PLAYER)
continue;
int32_t id = ref.id;
if (NPCManager::NPCs.find(id) == NPCManager::NPCs.end())
BaseNPC* npc = (BaseNPC*)ref.getEntity();
int distXY = std::hypot(plr->x - npc->x, plr->y - npc->y);
int dist = std::hypot(distXY, plr->z - npc->z);
if (dist >= lastDist)
continue;
BaseNPC* npc = NPCManager::NPCs[id];
for (auto it = NPCManager::Warps.begin(); it != NPCManager::Warps.end(); it++) {
if ((*it).second.npcID == npc->appearanceData.iNPCType) {
taskID = (*it).second.limitTaskID;
if (it->second.npcID == npc->type) {
taskID = it->second.limitTaskID;
missionID = Missions::Tasks[taskID]->task["m_iHMissionID"];
found++;
lastDist = dist;
break;
}
}
}
}
if (missionID == -1 || taskID == -1) {
Chat::sendServerMessage(sock, "You are NOT standing near a lair portal; move around and try again!");
return;
}
if (found > 1) {
Chat::sendServerMessage(sock, "More than one lair found; decrease chunk size and try again!");
Chat::sendServerMessage(sock, "No nearby Lair portals found.");
return;
}
@@ -985,11 +1046,17 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// add first point at NPC's current location
std::vector<BaseNPC*> pathPoints;
BaseNPC* marker = new BaseNPC(npc->x, npc->y, npc->z, 0, plr->instanceID, 1386, NPCManager::nextId--);
BaseNPC* marker = new BaseNPC(0, plr->instanceID, 1386, NPCManager::nextId--);
// assign coords manually, since we aren't actually adding markers to the world
marker->x = npc->x;
marker->y = npc->y;
marker->z = npc->z;
pathPoints.push_back(marker);
// map from player
TableData::RunningNPCPaths[plr->iID] = std::make_pair(npc, pathPoints);
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->appearanceData.iNPC_ID) + " is now following you");
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->id) + " is now following you");
updatePathMarkers(sock);
return;
}
@@ -1006,7 +1073,12 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// /path kf
if (args[1] == "kf") {
BaseNPC* marker = new BaseNPC(npc->x, npc->y, npc->z, 0, plr->instanceID, 1386, NPCManager::nextId--);
BaseNPC* marker = new BaseNPC(0, plr->instanceID, 1386, NPCManager::nextId--);
marker->x = npc->x;
marker->y = npc->y;
marker->z = npc->z;
entry->second.push_back(marker);
Chat::sendServerMessage(sock, "[PATH] Added keyframe");
updatePathMarkers(sock);
@@ -1016,8 +1088,8 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// /path here
if (args[1] == "here") {
// bring the NPC to where the player is standing
Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, npc->instanceID, 0);
Transport::NPCQueues.erase(npc->id); // delete transport queue
NPCManager::updateNPCPosition(npc->id, plr->x, plr->y, plr->z, npc->instanceID, 0);
npc->disappearFromViewOf(sock);
npc->enterIntoViewOf(sock);
Chat::sendServerMessage(sock, "[PATH] Come here");
@@ -1055,9 +1127,9 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
speed = speedArg;
}
// return NPC to home
Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue
Transport::NPCQueues.erase(npc->id); // delete transport queue
BaseNPC* home = entry->second[0];
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, home->x, home->y, home->z, npc->instanceID, 0);
NPCManager::updateNPCPosition(npc->id, home->x, home->y, home->z, npc->instanceID, 0);
npc->disappearFromViewOf(sock);
npc->enterIntoViewOf(sock);
@@ -1073,7 +1145,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
Transport::lerp(&keyframes, from, to, speed); // lerp from A to B
from = to; // update point A
}
Transport::NPCQueues[npc->appearanceData.iNPC_ID] = keyframes;
Transport::NPCQueues[npc->id] = keyframes;
entry->second.pop_back(); // remove temp end point
Chat::sendServerMessage(sock, "[PATH] Testing NPC path");
@@ -1083,9 +1155,9 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// /path cancel
if (args[1] == "cancel") {
// return NPC to home
Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue
Transport::NPCQueues.erase(npc->id); // delete transport queue
BaseNPC* home = entry->second[0];
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, home->x, home->y, home->z, npc->instanceID, 0);
NPCManager::updateNPCPosition(npc->id, home->x, home->y, home->z, npc->instanceID, 0);
npc->disappearFromViewOf(sock);
npc->enterIntoViewOf(sock);
// deallocate markers
@@ -1095,7 +1167,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
}
// unmap
TableData::RunningNPCPaths.erase(plr->iID);
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->appearanceData.iNPC_ID) + " is no longer following you");
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->id) + " is no longer following you");
return;
}
@@ -1123,9 +1195,9 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
}
// return NPC to home and set path to repeat
Transport::NPCQueues.erase(npc->appearanceData.iNPC_ID); // delete transport queue
Transport::NPCQueues.erase(npc->id); // delete transport queue
BaseNPC* home = entry->second[0];
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, home->x, home->y, home->z, npc->instanceID, 0);
NPCManager::updateNPCPosition(npc->id, home->x, home->y, home->z, npc->instanceID, 0);
npc->disappearFromViewOf(sock);
npc->enterIntoViewOf(sock);
npc->loopingPath = true;
@@ -1142,7 +1214,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
Transport::lerp(&keyframes, from, to, speed); // lerp from A to B
from = to; // update point A
}
Transport::NPCQueues[npc->appearanceData.iNPC_ID] = keyframes;
Transport::NPCQueues[npc->id] = keyframes;
entry->second.pop_back(); // remove temp end point
// save to gruntwork
@@ -1169,7 +1241,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
finishedPath.isLoop = true;
finishedPath.speed = speed;
finishedPath.points = finalPoints;
finishedPath.targetIDs.push_back(npc->appearanceData.iNPC_ID);
finishedPath.targetIDs.push_back(npc->id);
TableData::FinishedNPCPaths.push_back(finishedPath);
@@ -1181,7 +1253,7 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
// unmap
TableData::RunningNPCPaths.erase(plr->iID);
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->appearanceData.iNPC_ID) + " is no longer following you");
Chat::sendServerMessage(sock, "[PATH] NPC " + std::to_string(npc->id) + " is no longer following you");
TableData::flush();
Chat::sendServerMessage(sock, "[PATH] Path saved to gruntwork");
@@ -1192,19 +1264,13 @@ static void pathCommand(std::string full, std::vector<std::string>& args, CNSock
Chat::sendServerMessage(sock, "[PATH] Unknown argument '" + args[1] + "'");
}
static void reloadScriptsCommand(std::string full, std::vector<std::string>& args, CNSocket *sock) {
// reloads all scripts
LuaManager::stopScripts();
LuaManager::loadScripts();
}
static void registerCommand(std::string cmd, int requiredLevel, CommandHandler handlr, std::string help) {
commands[cmd] = ChatCommand(requiredLevel, handlr, help);
}
void CustomCommands::init() {
registerCommand("help", 100, helpCommand, "list all unlocked server-side commands");
registerCommand("access", 100, accessCommand, "print your access level");
registerCommand("access", 100, accessCommand, "check or change access levels");
registerCommand("instance", 30, instanceCommand, "print or change your current instance");
registerCommand("mss", 30, mssCommand, "edit Monkey Skyway routes");
registerCommand("npcr", 30, npcRotateCommand, "rotate NPCs");
@@ -1237,5 +1303,4 @@ void CustomCommands::init() {
registerCommand("unregisterall", 50, unregisterallCommand, "clear all SCAMPER and MSS destinations");
registerCommand("redeem", 100, redeemCommand, "redeem a code item");
registerCommand("path", 30, pathCommand, "edit NPC paths");
registerCommand("rscripts", 30, reloadScriptsCommand, "stops all script states and reloads all scripts");
}

View File

@@ -2,6 +2,8 @@
#include "core/Core.hpp"
#include <string>
namespace CustomCommands {
void init();

View File

@@ -1,141 +1,124 @@
#include "core/Core.hpp"
#include "Eggs.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include "Groups.hpp"
#include "NPCManager.hpp"
#include "Entities.hpp"
#include "Items.hpp"
#include <assert.h>
using namespace Eggs;
/// sock, CBFlag -> until
std::map<std::pair<CNSocket*, int32_t>, time_t> Eggs::EggBuffs;
std::unordered_map<int, EggType> Eggs::EggTypes;
int Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
int bitFlag = Groups::getGroupFlags(otherPlr);
int CBFlag = Nanos::applyBuff(sock, skillId, 1, 3, bitFlag);
// eggId might be 0 if the buff is made by the /buff command
EntityRef src = eggId == 0 ? sock : EntityRef(eggId);
size_t resplen;
if(Abilities::SkillTable.count(skillId) == 0) {
std::cout << "[WARN] egg " << eggId << " has skill ID " << skillId << " which doesn't exist" << std::endl;
return;
}
if (skillId == 183) {
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Damage);
} else if (skillId == 150) {
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Heal_HP);
SkillResult result = SkillResult();
SkillData* skill = &Abilities::SkillTable[skillId];
if(skill->drainType == SkillDrainType::PASSIVE) {
// apply buff
if(skill->targetType != SkillTargetType::PLAYERS) {
std::cout << "[WARN] weird skill type for egg " << eggId << " with skill " << skillId << ", should be " << (int)skill->targetType << std::endl;
}
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
int value = skill->values[0][0];
BuffStack eggBuff = {
duration * 1000 / MS_PER_PLAYER_TICK,
value,
src,
BuffClass::EGG
};
plr->addBuff(timeBuffId,
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
Buffs::timeBuffUpdate(self, buff, status, stack);
if(status == ETBU_DEL) Buffs::timeBuffTimeout(self);
},
[](EntityRef self, Buff* buff, time_t currTime) {
// no-op
},
&eggBuff);
sSkillResult_Buff resultBuff{};
resultBuff.eCT = plr->getCharType();
resultBuff.iID = plr->getID();
resultBuff.bProtected = false;
resultBuff.iConditionBitFlag = plr->getCompositeCondition();
result = SkillResult(sizeof(sSkillResult_Buff), &resultBuff);
} else {
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Buff);
int value = plr->getMaxHP() * skill->values[0][0] / 1000;
sSkillResult_Damage resultDamage{};
sSkillResult_Heal_HP resultHeal{};
switch(skill->skillType)
{
case SkillType::DAMAGE:
resultDamage.bProtected = false;
resultDamage.eCT = plr->getCharType();
resultDamage.iID = plr->getID();
resultDamage.iDamage = plr->takeDamage(src, value);
resultDamage.iHP = plr->getCurrentHP();
result = SkillResult(sizeof(sSkillResult_Damage), &resultDamage);
break;
case SkillType::HEAL_HP:
resultHeal.eCT = plr->getCharType();
resultHeal.iID = plr->getID();
resultHeal.iHealHP = plr->heal(src, value);
resultHeal.iHP = plr->getCurrentHP();
result = SkillResult(sizeof(sSkillResult_Heal_HP), &resultHeal);
break;
default:
std::cout << "[WARN] oops, egg with active skill type " << (int)skill->skillType << " unhandled";
return;
}
}
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
// we know it's only one trailing struct, so we can skip full validation
// initialize response struct
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + result.size;
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
auto skillUse = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
memset(respbuf, 0, resplen);
if (skillId == 183) { // damage egg
auto skill = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iDamage = PC_MAXHEALTH(plr->level) * Nanos::SkillTable[skillId].powerIntensity[0] / 1000;
plr->HP -= skill->iDamage;
if (plr->HP < 0)
plr->HP = 0;
skill->iHP = plr->HP;
} else if (skillId == 150) { // heal egg
auto skill = (sSkillResult_Heal_HP*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iHealHP = PC_MAXHEALTH(plr->level) * Nanos::SkillTable[skillId].powerIntensity[0] / 1000;
plr->HP += skill->iHealHP;
if (plr->HP > PC_MAXHEALTH(plr->level))
plr->HP = PC_MAXHEALTH(plr->level);
skill->iHP = plr->HP;
} else { // regular buff egg
auto skill = (sSkillResult_Buff*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iConditionBitFlag = plr->iConditionBitFlag;
sP_FE2CL_NPC_SKILL_HIT* pkt = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
pkt->iNPC_ID = eggId;
pkt->iSkillID = skillId;
pkt->eST = (int32_t)skill->skillType;
pkt->iTargetCnt = 1;
if(result.size > 0) {
void* attached = (void*)(pkt + 1);
memcpy(attached, result.payload, result.size);
}
skillUse->iNPC_ID = eggId;
skillUse->iSkillID = skillId;
skillUse->eST = Nanos::SkillTable[skillId].skillType;
skillUse->iTargetCnt = 1;
sock->sendPacket((void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
if (CBFlag == 0)
return -1;
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, CBFlag);
// save the buff serverside;
// if you get the same buff again, new duration will override the previous one
time_t until = getTime() + (time_t)duration * 1000;
EggBuffs[key] = until;
return 0;
NPCManager::sendToViewable(src.getEntity(), pkt, P_FE2CL_NPC_SKILL_HIT, resplen);
}
static void eggStep(CNServer* serv, time_t currTime) {
// tick buffs
time_t timeStamp = currTime;
auto it = EggBuffs.begin();
while (it != EggBuffs.end()) {
// check remaining time
if (it->second > timeStamp) {
it++;
} else { // if time reached 0
CNSocket* sock = it->first.first;
int32_t CBFlag = it->first.second;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
int groupFlags = Groups::getGroupFlags(otherPlr);
for (auto& pwr : Nanos::NanoPowers) {
if (pwr.bitFlag == CBFlag) { // pick the power with the right flag and unbuff
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp);
resp.eCSTB = pwr.timeBuffID;
resp.eTBU = 2;
resp.eTBT = 3; // for egg buffs
plr->iConditionBitFlag &= ~CBFlag;
resp.iConditionBitFlag = plr->iConditionBitFlag |= groupFlags | plr->iSelfConditionBitFlag;
sock->sendPacket(resp, P_FE2CL_PC_BUFF_UPDATE);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, resp2); // send a buff timeout to other players
resp2.eCT = 1;
resp2.iID = plr->iID;
resp2.iConditionBitFlag = plr->iConditionBitFlag;
PlayerManager::sendToViewable(sock, resp2, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT);
}
}
// remove buff from the map
it = EggBuffs.erase(it);
}
}
// check dead eggs and eggs in inactive chunks
for (auto npc : NPCManager::NPCs) {
if (npc.second->type != EntityType::EGG)
if (npc.second->kind != EntityKind::EGG)
continue;
auto egg = (Egg*)npc.second;
if (!egg->dead || !Chunking::inPopulatedChunks(&egg->viewableChunks))
continue;
if (egg->deadUntil <= timeStamp) {
if (egg->deadUntil <= currTime) {
// respawn it
egg->dead = false;
egg->deadUntil = 0;
egg->appearanceData.iHP = 400;
egg->hp = 400;
Chunking::addEntityToChunks(Chunking::getViewableChunks(egg->chunkPos), {npc.first});
}
@@ -163,7 +146,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
return;
}
auto egg = (Egg*)eggRef.getEntity();
if (egg->type != EntityType::EGG) {
if (egg->kind != EntityKind::EGG) {
std::cout << "[WARN] Player tried to open something other than an?!" << std::endl;
return;
}
@@ -180,7 +163,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
}
*/
int typeId = egg->appearanceData.iNPCType;
int typeId = egg->type;
if (EggTypes.find(typeId) == EggTypes.end()) {
std::cout << "[WARN] Egg Type " << typeId << " not found!" << std::endl;
return;
@@ -188,16 +171,13 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
EggType* type = &EggTypes[typeId];
// buff the player
if (type->effectId != 0)
eggBuffPlayer(sock, type->effectId, eggRef.id, type->duration);
/*
* SHINY_PICKUP_SUCC is only causing a GUI effect in the client
* (buff icon pops up in the bottom of the screen)
* so we don't send it for non-effect
*/
if (type->effectId != 0) {
eggBuffPlayer(sock, type->effectId, eggRef.id, type->duration);
INITSTRUCT(sP_FE2CL_REP_SHINY_PICKUP_SUCC, resp);
resp.iSkillID = type->effectId;
@@ -255,7 +235,7 @@ static void eggPickup(CNSocket* sock, CNPacketData* data) {
Chunking::removeEntityFromChunks(Chunking::getViewableChunks(egg->chunkPos), eggRef);
egg->dead = true;
egg->deadUntil = getTime() + (time_t)type->regen * 1000;
egg->appearanceData.iHP = 0;
egg->hp = 0;
}
}

View File

@@ -1,7 +1,6 @@
#pragma once
#include "core/Core.hpp"
#include "Entities.hpp"
struct EggType {
int dropCrateId;
@@ -11,12 +10,10 @@ struct EggType {
};
namespace Eggs {
extern std::map<std::pair<CNSocket*, int32_t>, time_t> EggBuffs;
extern std::unordered_map<int, EggType> EggTypes;
void init();
/// returns -1 on fail
int eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration);
void eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration);
void npcDataToEggData(int x, int y, int z, sNPCAppearanceData* npc, sShinyAppearanceData* egg);
}

View File

@@ -1,15 +1,17 @@
#include "Email.hpp"
#include "core/Core.hpp"
#include "db/Database.hpp"
#include "servers/CNShardServer.hpp"
#include "db/Database.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Chat.hpp"
using namespace Email;
std::vector<std::string> Email::dump;
// New email notification
static void emailUpdateCheck(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_NEW_EMAIL, resp);
@@ -91,7 +93,7 @@ static void emailReceiveItemSingle(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_PC_RECV_EMAIL_ITEM*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
if (pkt->iSlotNum < 0 || pkt->iSlotNum >= AINVEN_COUNT || pkt->iSlotNum < 1 || pkt->iSlotNum > 4)
if (pkt->iSlotNum < 0 || pkt->iSlotNum >= AINVEN_COUNT || pkt->iEmailItemSlot < 1 || pkt->iEmailItemSlot > 4)
return; // sanity check
// get email item from db and delete it
@@ -226,10 +228,10 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_SUCC, resp);
if (pkt->iCash || pkt->aItem[0].ItemInven.iID) {
// if there are item or taro attachments
Player otherPlr = {};
Database::getPlayer(&otherPlr, pkt->iTo_PCUID);
if (pkt->iCash || pkt->aItem[0].ItemInven.iID) {
// if there are item or taro attachments
if (otherPlr.iID != 0 && plr->PCStyle2.iPayzoneFlag != otherPlr.PCStyle2.iPayzoneFlag) {
// if the players are not in the same time period
INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_FAIL, resp);
@@ -250,11 +252,26 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
if (attachment.ItemInven.iID == 0)
continue;
sItemBase* item = &pkt->aItem[i].ItemInven;
sItemBase* real = &plr->Inven[attachment.iSlotNum];
resp.aItem[i] = attachment;
attachments.push_back(attachment.ItemInven);
attSlots.push_back(attachment.iSlotNum);
// delete item
plr->Inven[attachment.iSlotNum] = { 0, 0, 0, 0 };
if (real->iOpt <= item->iOpt) // delete item (if they attached the whole stack)
*real = { 0, 0, 0, 0 };
else // otherwise, decrement the item
real->iOpt -= item->iOpt;
// HACK: update the slot
INITSTRUCT(sP_FE2CL_PC_ITEM_MOVE_SUCC, itemResp);
itemResp.iFromSlotNum = attachment.iSlotNum;
itemResp.iToSlotNum = attachment.iSlotNum;
itemResp.FromSlotItem = *real;
itemResp.ToSlotItem = *real;
itemResp.eFrom = (int32_t)Items::SlotType::INVENTORY;
itemResp.eTo = (int32_t)Items::SlotType::INVENTORY;
sock->sendPacket(itemResp, P_FE2CL_PC_ITEM_MOVE_SUCC);
}
int cost = pkt->iCash + 50 + 20 * attachments.size(); // attached taros + postage
@@ -274,7 +291,7 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
0 // DeleteTime (unimplemented)
};
if (!Database::sendEmail(&email, attachments)) {
if (!Database::sendEmail(&email, attachments, plr)) {
plr->money += cost; // give money back
// give items back
while (!attachments.empty()) {
@@ -304,6 +321,17 @@ static void emailSend(CNSocket* sock, CNPacketData* data) {
resp.iTo_PCUID = pkt->iTo_PCUID;
sock->sendPacket(resp, P_FE2CL_REP_PC_SEND_EMAIL_SUCC);
std::string logEmail = "[Email] " + PlayerManager::getPlayerName(plr, true) + " (to " + PlayerManager::getPlayerName(&otherPlr, true) + "): <" + email.SubjectLine + ">\n" + email.MsgBody;
std::cout << logEmail << std::endl;
dump.push_back(logEmail);
// notification to recipient if online
CNSocket* recipient = PlayerManager::getSockFromID(pkt->iTo_PCUID);
if (recipient != nullptr)
{
emailUpdateCheck(recipient, nullptr);
}
}
void Email::init() {

View File

@@ -1,5 +1,10 @@
#pragma once
#include <vector>
#include <string>
namespace Email {
extern std::vector<std::string> dump;
void init();
}

View File

@@ -1,30 +1,28 @@
#include "core/Core.hpp"
#include "Entities.hpp"
#include "Chunking.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "Eggs.hpp"
#include "MobAI.hpp"
#include <type_traits>
#include "NPCManager.hpp"
#include "PlayerManager.hpp"
#include <assert.h>
static_assert(std::is_standard_layout<EntityRef>::value);
static_assert(std::is_trivially_copyable<EntityRef>::value);
EntityRef::EntityRef(CNSocket *s) {
type = EntityType::PLAYER;
kind = EntityKind::PLAYER;
sock = s;
}
EntityRef::EntityRef(int32_t i) {
id = i;
assert(NPCManager::NPCs.find(id) != NPCManager::NPCs.end());
type = NPCManager::NPCs[id]->type;
kind = EntityKind::INVALID;
if (NPCManager::NPCs.find(id) != NPCManager::NPCs.end())
kind = NPCManager::NPCs[id]->kind;
}
bool EntityRef::isValid() const {
if (type == EntityType::PLAYER)
if (kind == EntityKind::PLAYER)
return PlayerManager::players.find(sock) != PlayerManager::players.end();
return NPCManager::NPCs.find(id) != NPCManager::NPCs.end();
@@ -33,21 +31,38 @@ bool EntityRef::isValid() const {
Entity *EntityRef::getEntity() const {
assert(isValid());
if (type == EntityType::PLAYER)
if (kind == EntityKind::PLAYER)
return PlayerManager::getPlayer(sock);
return NPCManager::NPCs[id];
}
sNPCAppearanceData BaseNPC::getAppearanceData() {
sNPCAppearanceData data = {};
data.iAngle = angle;
data.iBarkerType = 0; // unused?
data.iConditionBitFlag = 0;
data.iHP = hp;
data.iNPCType = type;
data.iNPC_ID = id;
data.iX = x;
data.iY = y;
data.iZ = z;
return data;
}
sNPCAppearanceData CombatNPC::getAppearanceData() {
sNPCAppearanceData data = BaseNPC::getAppearanceData();
data.iConditionBitFlag = getCompositeCondition();
return data;
}
/*
* Entity coming into view.
*/
void BaseNPC::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
pkt.NPCAppearanceData = appearanceData;
pkt.NPCAppearanceData.iX = x;
pkt.NPCAppearanceData.iY = y;
pkt.NPCAppearanceData.iZ = z;
pkt.NPCAppearanceData = getAppearanceData();
sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
}
@@ -56,7 +71,7 @@ void Bus::enterIntoViewOf(CNSocket *sock) {
// TODO: Potentially decouple this from BaseNPC?
pkt.AppearanceData = {
3, appearanceData.iNPC_ID, appearanceData.iNPCType,
3, id, type,
x, y, z
};
@@ -66,28 +81,40 @@ void Bus::enterIntoViewOf(CNSocket *sock) {
void Egg::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_SHINY_ENTER, pkt);
Eggs::npcDataToEggData(x, y, z, &appearanceData, &pkt.ShinyAppearanceData);
// TODO: Potentially decouple this from BaseNPC?
pkt.ShinyAppearanceData = {
id, type, 0, // client doesn't care about map num
x, y, z
};
sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER);
}
sNano* Player::getActiveNano() {
return &Nanos[activeNano];
}
sPCAppearanceData Player::getAppearanceData() {
sPCAppearanceData data = {};
data.iID = iID;
data.iHP = HP;
data.iLv = level;
data.iX = x;
data.iY = y;
data.iZ = z;
data.iAngle = angle;
data.PCStyle = PCStyle;
data.Nano = Nanos[activeNano];
data.iPCState = iPCState;
data.iSpecialState = iSpecialState;
memcpy(data.ItemEquip, Equip, sizeof(sItemBase) * AEQUIP_COUNT);
return data;
}
// TODO: this is less effiecient than it was, because of memset()
void Player::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_PC_NEW, pkt);
pkt.PCAppearanceData.iID = iID;
pkt.PCAppearanceData.iHP = HP;
pkt.PCAppearanceData.iLv = level;
pkt.PCAppearanceData.iX = x;
pkt.PCAppearanceData.iY = y;
pkt.PCAppearanceData.iZ = z;
pkt.PCAppearanceData.iAngle = angle;
pkt.PCAppearanceData.PCStyle = PCStyle;
pkt.PCAppearanceData.Nano = Nanos[activeNano];
pkt.PCAppearanceData.iPCState = iPCState;
pkt.PCAppearanceData.iSpecialState = iSpecialState;
memcpy(pkt.PCAppearanceData.ItemEquip, Equip, sizeof(sItemBase) * AEQUIP_COUNT);
pkt.PCAppearanceData = getAppearanceData();
sock->sendPacket(pkt, P_FE2CL_PC_NEW);
}
@@ -96,20 +123,20 @@ void Player::enterIntoViewOf(CNSocket *sock) {
*/
void BaseNPC::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
pkt.iNPC_ID = appearanceData.iNPC_ID;
pkt.iNPC_ID = id;
sock->sendPacket(pkt, P_FE2CL_NPC_EXIT);
}
void Bus::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_TRANSPORTATION_EXIT, pkt);
pkt.eTT = 3;
pkt.iT_ID = appearanceData.iNPC_ID;
pkt.iT_ID = id;
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_EXIT);
}
void Egg::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_SHINY_EXIT, pkt);
pkt.iShinyID = appearanceData.iNPC_ID;
pkt.iShinyID = id;
sock->sendPacket(pkt, P_FE2CL_SHINY_EXIT);
}

View File

@@ -2,23 +2,25 @@
#include "core/Core.hpp"
#include <stdint.h>
#include <set>
#include "EntityRef.hpp"
#include "Buffs.hpp"
#include "Chunking.hpp"
#include "Groups.hpp"
enum class EntityType : uint8_t {
INVALID,
PLAYER,
SIMPLE_NPC,
COMBAT_NPC,
MOB,
EGG,
BUS
#include <set>
#include <map>
#include <functional>
enum class AIState {
INACTIVE,
ROAMING,
COMBAT,
RETREAT,
DEAD
};
class Chunk;
struct Entity {
EntityType type = EntityType::INVALID;
EntityKind kind = EntityKind::INVALID;
int x = 0, y = 0, z = 0;
uint64_t instanceID = 0;
ChunkPos chunkPos = {};
@@ -27,47 +29,39 @@ struct Entity {
// destructor must be virtual, apparently
virtual ~Entity() {}
virtual bool isAlive() { return true; }
virtual bool isExtant() { return true; }
// stubs
virtual void enterIntoViewOf(CNSocket *sock) = 0;
virtual void disappearFromViewOf(CNSocket *sock) = 0;
};
struct EntityRef {
EntityType type;
union {
CNSocket *sock;
int32_t id;
};
/*
* Interfaces
*/
class ICombatant {
public:
ICombatant() {}
virtual ~ICombatant() {}
EntityRef(CNSocket *s);
EntityRef(int32_t i);
bool isValid() const;
Entity *getEntity() const;
bool operator==(const EntityRef& other) const {
if (type != other.type)
return false;
if (type == EntityType::PLAYER)
return sock == other.sock;
return id == other.id;
}
// arbitrary ordering
bool operator<(const EntityRef& other) const {
if (type == other.type) {
if (type == EntityType::PLAYER)
return sock < other.sock;
else
return id < other.id;
}
return type < other.type;
}
virtual bool addBuff(int, BuffCallback<int, BuffStack*>, BuffCallback<time_t>, BuffStack*) = 0;
virtual Buff* getBuff(int) = 0;
virtual void removeBuff(int) = 0;
virtual void removeBuff(int, BuffClass) = 0;
virtual void clearBuffs(bool) = 0;
virtual bool hasBuff(int) = 0;
virtual int getCompositeCondition() = 0;
virtual int takeDamage(EntityRef, int) = 0;
virtual int heal(EntityRef, int) = 0;
virtual bool isAlive() = 0;
virtual int getCurrentHP() = 0;
virtual int getMaxHP() = 0;
virtual int getLevel() = 0;
virtual std::vector<EntityRef> getGroupMembers() = 0;
virtual int32_t getCharType() = 0;
virtual int32_t getID() = 0;
virtual EntityRef getRef() = 0;
virtual void step(time_t currTime) = 0;
};
/*
@@ -75,75 +69,105 @@ struct EntityRef {
*/
class BaseNPC : public Entity {
public:
sNPCAppearanceData appearanceData = {};
int id;
int type;
int hp;
int angle;
bool loopingPath = false;
BaseNPC(int _X, int _Y, int _Z, int angle, uint64_t iID, int t, int id) { // XXX
x = _X;
y = _Y;
z = _Z;
appearanceData.iNPCType = t;
appearanceData.iHP = 400;
appearanceData.iAngle = angle;
appearanceData.iConditionBitFlag = 0;
appearanceData.iBarkerType = 0;
appearanceData.iNPC_ID = id;
BaseNPC(int _A, uint64_t iID, int t, int _id) {
kind = EntityKind::SIMPLE_NPC;
type = t;
hp = 400;
angle = _A;
id = _id;
instanceID = iID;
};
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
virtual sNPCAppearanceData getAppearanceData();
};
struct CombatNPC : public BaseNPC {
struct CombatNPC : public BaseNPC, public ICombatant {
int maxHealth = 0;
int spawnX = 0;
int spawnY = 0;
int spawnZ = 0;
int level = 0;
int speed = 300;
AIState state = AIState::INACTIVE;
Group* group = nullptr;
int playersInView = 0; // for optimizing away AI in empty chunks
void (*_stepAI)(CombatNPC*, time_t) = nullptr;
std::map<AIState, void (*)(CombatNPC*, time_t)> stateHandlers;
std::map<AIState, void (*)(CombatNPC*, EntityRef)> transitionHandlers;
// XXX
CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP) :
BaseNPC(x, y, z, angle, iID, t, id),
maxHealth(maxHP) {}
std::unordered_map<int, Buff*> buffs = {};
virtual void stepAI(time_t currTime) {
if (_stepAI != nullptr)
_stepAI(this, currTime);
CombatNPC(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, int id, int maxHP)
: BaseNPC(angle, iID, t, id), maxHealth(maxHP) {
this->spawnX = spawnX;
this->spawnY = spawnY;
this->spawnZ = spawnZ;
kind = EntityKind::COMBAT_NPC;
stateHandlers[AIState::INACTIVE] = {};
transitionHandlers[AIState::INACTIVE] = {};
}
virtual bool isAlive() override { return appearanceData.iHP > 0; }
virtual sNPCAppearanceData getAppearanceData() override;
virtual bool isExtant() override { return hp > 0; }
virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override;
virtual Buff* getBuff(int buffId) override;
virtual void removeBuff(int buffId) override;
virtual void removeBuff(int buffId, BuffClass buffClass) override;
virtual void clearBuffs(bool force) override;
virtual bool hasBuff(int buffId) override;
virtual int getCompositeCondition() override;
virtual int takeDamage(EntityRef src, int amt) override;
virtual int heal(EntityRef src, int amt) override;
virtual bool isAlive() override;
virtual int getCurrentHP() override;
virtual int getMaxHP() override;
virtual int getLevel() override;
virtual std::vector<EntityRef> getGroupMembers() override;
virtual int32_t getCharType() override;
virtual int32_t getID() override;
virtual EntityRef getRef() override;
virtual void step(time_t currTime) override;
virtual void transition(AIState newState, EntityRef src);
};
// Mob is in MobAI.hpp, Player is in Player.hpp
// TODO: decouple from BaseNPC
struct Egg : public BaseNPC {
bool summoned = false;
bool dead = false;
time_t deadUntil;
Egg(int x, int y, int z, uint64_t iID, int t, int32_t id, bool summon)
: BaseNPC(x, y, z, 0, iID, t, id) {
Egg(uint64_t iID, int t, int32_t id, bool summon)
: BaseNPC(0, iID, t, id) {
summoned = summon;
type = EntityType::EGG;
kind = EntityKind::EGG;
}
virtual bool isAlive() override { return !dead; }
virtual bool isExtant() override { return !dead; }
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
};
// TODO: decouple from BaseNPC
struct Bus : public BaseNPC {
Bus(int x, int y, int z, int angle, uint64_t iID, int t, int id) :
BaseNPC(x, y, z, angle, iID, t, id) {
type = EntityType::BUS;
Bus(int angle, uint64_t iID, int t, int id) :
BaseNPC(angle, iID, t, id) {
kind = EntityKind::BUS;
loopingPath = true;
}
virtual void enterIntoViewOf(CNSocket *sock) override;

56
src/EntityRef.hpp Normal file
View File

@@ -0,0 +1,56 @@
#pragma once
#include "core/Core.hpp"
/* forward declaration(s) */
struct Entity;
enum EntityKind {
INVALID,
PLAYER,
SIMPLE_NPC,
COMBAT_NPC,
MOB,
EGG,
BUS
};
struct EntityRef {
EntityKind kind;
union {
CNSocket *sock;
int32_t id;
};
EntityRef(CNSocket *s);
EntityRef(int32_t i);
bool isValid() const;
Entity *getEntity() const;
bool operator==(const EntityRef& other) const {
if (kind != other.kind)
return false;
if (kind == EntityKind::PLAYER)
return sock == other.sock;
return id == other.id;
}
bool operator!=(const EntityRef& other) const {
return !(*this == other);
}
// arbitrary ordering
bool operator<(const EntityRef& other) const {
if (kind == other.kind) {
if (kind == EntityKind::PLAYER)
return sock < other.sock;
else
return id < other.id;
}
return kind < other.kind;
}
};

View File

@@ -1,13 +1,10 @@
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Groups.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include <iostream>
#include <chrono>
#include <algorithm>
#include <thread>
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Entities.hpp"
/*
* NOTE: Variadic response packets that list group members are technically
@@ -19,22 +16,186 @@
using namespace Groups;
Group::Group(EntityRef leader) {
addToGroup(this, leader);
}
static void attachGroupData(std::vector<EntityRef>& pcs, std::vector<EntityRef>& npcs, uint8_t* pivot) {
for(EntityRef pcRef : pcs) {
sPCGroupMemberInfo* info = (sPCGroupMemberInfo*)pivot;
Player* plr = PlayerManager::getPlayer(pcRef.sock);
info->iPC_ID = plr->iID;
info->iPCUID = plr->PCStyle.iPC_UID;
info->iNameCheck = plr->PCStyle.iNameCheck;
memcpy(info->szFirstName, plr->PCStyle.szFirstName, sizeof(plr->PCStyle.szFirstName));
memcpy(info->szLastName, plr->PCStyle.szLastName, sizeof(plr->PCStyle.szLastName));
info->iSpecialState = plr->iSpecialState;
info->iLv = plr->level;
info->iHP = plr->HP;
info->iMaxHP = PC_MAXHEALTH(plr->level);
// info->iMapType = 0;
// info->iMapNum = 0;
info->iX = plr->x;
info->iY = plr->y;
info->iZ = plr->z;
if(plr->activeNano > 0) {
info->Nano = *plr->getActiveNano();
info->bNano = true;
}
pivot = (uint8_t*)(info + 1);
}
for(EntityRef npcRef : npcs) {
sNPCGroupMemberInfo* info = (sNPCGroupMemberInfo*)pivot;
// probably should not assume that the combatant is an
// entity, but it works for now
BaseNPC* npc = (BaseNPC*)npcRef.getEntity();
info->iNPC_ID = npcRef.id;
info->iNPC_Type = npc->type;
info->iHP = npc->hp;
info->iX = npc->x;
info->iY = npc->y;
info->iZ = npc->z;
pivot = (uint8_t*)(info + 1);
}
}
void Groups::addToGroup(Group* group, EntityRef member) {
if (group == nullptr)
return;
if (member.kind == EntityKind::PLAYER) {
Player* plr = PlayerManager::getPlayer(member.sock);
plr->group = group;
}
else if (member.kind == EntityKind::COMBAT_NPC) {
CombatNPC* npc = (CombatNPC*)member.getEntity();
npc->group = group;
}
else {
std::cout << "[WARN] Adding a weird entity type to a group" << std::endl;
}
group->members.push_back(member);
if(member.kind == EntityKind::PLAYER) {
std::vector<EntityRef> pcs = group->filter(EntityKind::PLAYER);
std::vector<EntityRef> npcs = group->filter(EntityKind::COMBAT_NPC);
size_t pcCount = pcs.size();
size_t npcCount = npcs.size();
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, CN_PACKET_BUFFER_SIZE);
sP_FE2CL_PC_GROUP_JOIN* pkt = (sP_FE2CL_PC_GROUP_JOIN*)respbuf;
pkt->iID_NewMember = PlayerManager::getPlayer(member.sock)->iID;
pkt->iMemberPCCnt = (int32_t)pcCount;
pkt->iMemberNPCCnt = (int32_t)npcCount;
if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN), pcCount, sizeof(sPCGroupMemberInfo))
|| !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size" << std::endl;
} else {
uint8_t* pivot = (uint8_t*)(pkt + 1);
attachGroupData(pcs, npcs, pivot);
// PC_GROUP_JOIN_SUCC and PC_GROUP_JOIN carry identical payloads but have different IDs
// (and the client does care!) so we need to send one to the new member
// and the other to the rest
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_JOIN) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo);
member.sock->sendPacket(respbuf, P_FE2CL_PC_GROUP_JOIN_SUCC, resplen);
sendToGroup(group, member, respbuf, P_FE2CL_PC_GROUP_JOIN, resplen);
}
}
}
bool Groups::removeFromGroup(Group* group, EntityRef member) {
if (group == nullptr)
return false;
if (member.kind == EntityKind::PLAYER) {
Player* plr = PlayerManager::getPlayer(member.sock);
plr->group = nullptr; // no dangling pointers here muahaahahah
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, leavePkt);
member.sock->sendPacket(leavePkt, P_FE2CL_PC_GROUP_LEAVE_SUCC);
}
else if (member.kind == EntityKind::COMBAT_NPC) {
CombatNPC* npc = (CombatNPC*)member.getEntity();
npc->group = nullptr;
}
else {
std::cout << "[WARN] Removing a weird entity type from a group" << std::endl;
}
auto it = std::find(group->members.begin(), group->members.end(), member);
if (it == group->members.end()) {
std::cout << "[WARN] Tried to remove a member that isn't in the group" << std::endl;
} else {
group->members.erase(it);
}
if(member.kind == EntityKind::PLAYER) {
std::vector<EntityRef> pcs = group->filter(EntityKind::PLAYER);
std::vector<EntityRef> npcs = group->filter(EntityKind::COMBAT_NPC);
size_t pcCount = pcs.size();
size_t npcCount = npcs.size();
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, CN_PACKET_BUFFER_SIZE);
sP_FE2CL_PC_GROUP_LEAVE* pkt = (sP_FE2CL_PC_GROUP_LEAVE*)respbuf;
pkt->iID_LeaveMember = PlayerManager::getPlayer(member.sock)->iID;
pkt->iMemberPCCnt = (int32_t)pcCount;
pkt->iMemberNPCCnt = (int32_t)npcCount;
if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE), pcCount, sizeof(sPCGroupMemberInfo))
|| !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_LEAVE packet size" << std::endl;
} else {
uint8_t* pivot = (uint8_t*)(pkt + 1);
attachGroupData(pcs, npcs, pivot);
sendToGroup(group, respbuf, P_FE2CL_PC_GROUP_LEAVE,
sizeof(sP_FE2CL_PC_GROUP_LEAVE) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo));
}
}
if (group->members.size() == 1) {
return removeFromGroup(group, group->members.back());
}
if (group->members.empty()) {
delete group; // cleanup memory
return true;
}
return false;
}
void Groups::disbandGroup(Group* group) {
if (group == nullptr)
return;
// remove everyone from the group!!
bool done = false;
while(!done) {
EntityRef back = group->members.back();
done = removeFromGroup(group, back);
}
}
static void requestGroup(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_GROUP_INVITE* recv = (sP_CL2FE_REQ_PC_GROUP_INVITE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_To);
if (otherPlr == nullptr)
return;
otherPlr = PlayerManager::getPlayerFromID(otherPlr->iIDGroup);
if (otherPlr == nullptr)
return;
// fail if the group is full or the other player is already in a group
if (plr->groupCnt >= 4 || otherPlr->iIDGroup != otherPlr->iID || otherPlr->groupCnt > 1) {
if ((plr->group != nullptr && plr->group->filter(EntityKind::PLAYER).size() >= 4) || otherPlr->group != nullptr) {
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE_FAIL, resp);
sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_FAIL, sizeof(sP_FE2CL_PC_GROUP_INVITE_FAIL));
return;
@@ -75,255 +236,89 @@ static void joinGroup(CNSocket* sock, CNPacketData* data) {
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_From);
if (otherPlr == nullptr)
return;
return; // disconnect or something
otherPlr = PlayerManager::getPlayerFromID(otherPlr->iIDGroup);
if (otherPlr == nullptr)
return;
int size = otherPlr->group == nullptr ? 1 : otherPlr->group->filter(EntityKind::PLAYER).size();
// fail if the group is full or the other player is already in a group
if (plr->groupCnt > 1 || plr->iIDGroup != plr->iID || otherPlr->groupCnt >= 4) {
if (plr->group != nullptr || size + 1 > 4) {
INITSTRUCT(sP_FE2CL_PC_GROUP_JOIN_FAIL, resp);
sock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_JOIN_FAIL, sizeof(sP_FE2CL_PC_GROUP_JOIN_FAIL));
return;
}
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_JOIN), otherPlr->groupCnt + 1, sizeof(sPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n";
return;
if (otherPlr->group == nullptr) {
// create group
EntityRef otherPlrRef = PlayerManager::getSockFromID(recv->iID_From);
otherPlr->group = new Group(otherPlrRef);
}
plr->iIDGroup = otherPlr->iID;
otherPlr->groupCnt += 1;
otherPlr->groupIDs[otherPlr->groupCnt-1] = plr->iID;
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_JOIN) + otherPlr->groupCnt * sizeof(sPCGroupMemberInfo);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_GROUP_JOIN *resp = (sP_FE2CL_PC_GROUP_JOIN*)respbuf;
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_JOIN));
resp->iID_NewMember = plr->iID;
resp->iMemberPCCnt = otherPlr->groupCnt;
int bitFlag = getGroupFlags(otherPlr);
for (int i = 0; i < otherPlr->groupCnt; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]);
CNSocket* sockTo = PlayerManager::getSockFromID(otherPlr->groupIDs[i]);
if (varPlr == nullptr || sockTo == nullptr)
continue;
respdata[i].iPC_ID = varPlr->iID;
respdata[i].iPCUID = varPlr->PCStyle.iPC_UID;
respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck;
memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
respdata[i].iSpecialState = varPlr->iSpecialState;
respdata[i].iLv = varPlr->level;
respdata[i].iHP = varPlr->HP;
respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level);
//respdata[i].iMapType = 0;
//respdata[i].iMapNum = 0;
respdata[i].iX = varPlr->x;
respdata[i].iY = varPlr->y;
respdata[i].iZ = varPlr->z;
// client doesnt read nano data here
if (varPlr != plr) { // apply the new member's buffs to the group and the group's buffs to the new member
if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 1, 1, bitFlag);
if (Nanos::SkillTable[plr->Nanos[plr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 1, 1, bitFlag);
}
}
sendToGroup(otherPlr, (void*)&respbuf, P_FE2CL_PC_GROUP_JOIN, resplen);
addToGroup(otherPlr->group, sock);
}
static void leaveGroup(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
groupKickPlayer(plr);
groupKick(plr->group, sock);
}
void Groups::sendToGroup(Player* plr, void* buf, uint32_t type, size_t size) {
for (int i = 0; i < plr->groupCnt; i++) {
CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[i]);
if (sock == nullptr)
continue;
if (type == P_FE2CL_PC_GROUP_LEAVE_SUCC) {
Player* leavingPlr = PlayerManager::getPlayer(sock);
leavingPlr->iIDGroup = leavingPlr->iID;
}
sock->sendPacket(buf, type, size);
}
}
void Groups::groupTickInfo(Player* plr) {
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), plr->groupCnt, sizeof(sPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n";
void Groups::sendToGroup(Group* group, void* buf, uint32_t type, size_t size) {
if (group == nullptr)
return;
auto players = group->filter(EntityKind::PLAYER);
for (EntityRef ref : players) {
ref.sock->sendPacket(buf, type, size);
}
}
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + plr->groupCnt * sizeof(sPCGroupMemberInfo);
void Groups::sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size) {
if (group == nullptr)
return;
auto players = group->filter(EntityKind::PLAYER);
for (EntityRef ref : players) {
if(ref != excluded) ref.sock->sendPacket(buf, type, size);
}
}
void Groups::groupTickInfo(CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
Group* group = plr->group;
std::vector<EntityRef> pcs = group->filter(EntityKind::PLAYER);
std::vector<EntityRef> npcs = group->filter(EntityKind::COMBAT_NPC);
size_t pcCount = pcs.size();
size_t npcCount = npcs.size();
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, CN_PACKET_BUFFER_SIZE);
sP_FE2CL_PC_GROUP_MEMBER_INFO* pkt = (sP_FE2CL_PC_GROUP_MEMBER_INFO*)respbuf;
memset(respbuf, 0, resplen);
pkt->iID = plr->iID;
pkt->iMemberPCCnt = (int32_t)pcCount;
pkt->iMemberNPCCnt = (int32_t)npcCount;
sP_FE2CL_PC_GROUP_MEMBER_INFO *resp = (sP_FE2CL_PC_GROUP_MEMBER_INFO*)respbuf;
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO));
resp->iID = plr->iID;
resp->iMemberPCCnt = plr->groupCnt;
for (int i = 0; i < plr->groupCnt; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
if (varPlr == nullptr)
continue;
respdata[i].iPC_ID = varPlr->iID;
respdata[i].iPCUID = varPlr->PCStyle.iPC_UID;
respdata[i].iNameCheck = varPlr->PCStyle.iNameCheck;
memcpy(respdata[i].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
memcpy(respdata[i].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
respdata[i].iSpecialState = varPlr->iSpecialState;
respdata[i].iLv = varPlr->level;
respdata[i].iHP = varPlr->HP;
respdata[i].iMaxHP = PC_MAXHEALTH(varPlr->level);
//respdata[i].iMapType = 0;
//respdata[i].iMapNum = 0;
respdata[i].iX = varPlr->x;
respdata[i].iY = varPlr->y;
respdata[i].iZ = varPlr->z;
if (varPlr->activeNano > 0) {
respdata[i].bNano = 1;
respdata[i].Nano = varPlr->Nanos[varPlr->activeNano];
if(!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), pcCount, sizeof(sPCGroupMemberInfo))
|| !validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + pcCount * sizeof(sPCGroupMemberInfo), npcCount, sizeof(sNPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_MEMBER_INFO packet size" << std::endl;
} else {
uint8_t* pivot = (uint8_t*)(pkt + 1);
attachGroupData(pcs, npcs, pivot);
sock->sendPacket(respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO,
sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO) + pcCount * sizeof(sPCGroupMemberInfo) + npcCount * sizeof(sNPCGroupMemberInfo));
}
}
sendToGroup(plr, (void*)&respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO, resplen);
}
void Groups::groupKick(Group* group, EntityRef ref) {
static void groupUnbuff(Player* plr) {
for (int i = 0; i < plr->groupCnt; i++) {
for (int n = 0; n < plr->groupCnt; n++) {
if (i == n)
continue;
if (group == nullptr)
return;
Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[n]);
Nanos::applyBuff(sock, otherPlr->Nanos[otherPlr->activeNano].iSkillID, 2, 1, 0);
}
}
}
void Groups::groupKickPlayer(Player* plr) {
// if you are the group leader, destroy your own group and kick everybody
if (plr->iID == plr->iIDGroup) {
groupUnbuff(plr);
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1);
sendToGroup(plr, (void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC));
plr->groupCnt = 1;
if (group->members[0] == ref) {
disbandGroup(group);
return;
}
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_LEAVE), otherPlr->groupCnt - 1, sizeof(sPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_LEAVE packet size\n";
return;
}
size_t resplen = sizeof(sP_FE2CL_PC_GROUP_LEAVE) + (otherPlr->groupCnt - 1) * sizeof(sPCGroupMemberInfo);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_GROUP_LEAVE *resp = (sP_FE2CL_PC_GROUP_LEAVE*)respbuf;
sPCGroupMemberInfo *respdata = (sPCGroupMemberInfo*)(respbuf+sizeof(sP_FE2CL_PC_GROUP_LEAVE));
resp->iID_LeaveMember = plr->iID;
resp->iMemberPCCnt = otherPlr->groupCnt - 1;
int bitFlag = getGroupFlags(otherPlr) & ~plr->iGroupConditionBitFlag;
int moveDown = 0;
CNSocket* sock = PlayerManager::getSockFromID(plr->iID);
if (sock == nullptr)
return;
for (int i = 0; i < otherPlr->groupCnt; i++) {
Player* varPlr = PlayerManager::getPlayerFromID(otherPlr->groupIDs[i]);
CNSocket* sockTo = PlayerManager::getSockFromID(otherPlr->groupIDs[i]);
if (varPlr == nullptr || sockTo == nullptr)
continue;
if (moveDown == 1)
otherPlr->groupIDs[i-1] = otherPlr->groupIDs[i];
respdata[i-moveDown].iPC_ID = varPlr->iID;
respdata[i-moveDown].iPCUID = varPlr->PCStyle.iPC_UID;
respdata[i-moveDown].iNameCheck = varPlr->PCStyle.iNameCheck;
memcpy(respdata[i-moveDown].szFirstName, varPlr->PCStyle.szFirstName, sizeof(varPlr->PCStyle.szFirstName));
memcpy(respdata[i-moveDown].szLastName, varPlr->PCStyle.szLastName, sizeof(varPlr->PCStyle.szLastName));
respdata[i-moveDown].iSpecialState = varPlr->iSpecialState;
respdata[i-moveDown].iLv = varPlr->level;
respdata[i-moveDown].iHP = varPlr->HP;
respdata[i-moveDown].iMaxHP = PC_MAXHEALTH(varPlr->level);
// respdata[i-moveDown]].iMapType = 0;
// respdata[i-moveDown]].iMapNum = 0;
respdata[i-moveDown].iX = varPlr->x;
respdata[i-moveDown].iY = varPlr->y;
respdata[i-moveDown].iZ = varPlr->z;
// client doesnt read nano data here
if (varPlr == plr) {
moveDown = 1;
otherPlr->groupIDs[i] = 0;
} else { // remove the leaving member's buffs from the group and remove the group buffs from the leaving member.
if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 2, 1, 0);
if (Nanos::SkillTable[plr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 2, 1, bitFlag);
}
}
plr->iIDGroup = plr->iID;
otherPlr->groupCnt -= 1;
sendToGroup(otherPlr, (void*)&respbuf, P_FE2CL_PC_GROUP_LEAVE, resplen);
INITSTRUCT(sP_FE2CL_PC_GROUP_LEAVE_SUCC, resp1);
sock->sendPacket((void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC));
}
int Groups::getGroupFlags(Player* plr) {
int bitFlag = 0;
for (int i = 0; i < plr->groupCnt; i++) {
Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
if (otherPlr == nullptr)
continue;
bitFlag |= otherPlr->iGroupConditionBitFlag;
}
return bitFlag;
removeFromGroup(group, ref);
}
void Groups::init() {

View File

@@ -1,17 +1,37 @@
#pragma once
#include "Player.hpp"
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "EntityRef.hpp"
#include <map>
#include <list>
#include <vector>
#include <assert.h>
struct Group {
std::vector<EntityRef> members;
std::vector<EntityRef> filter(EntityKind kind) {
std::vector<EntityRef> filtered;
std::copy_if(members.begin(), members.end(), std::back_inserter(filtered), [kind](EntityRef e) {
return e.kind == kind;
});
return filtered;
}
EntityRef getLeader() {
assert(members.size() > 0);
return members[0];
}
Group(EntityRef leader);
};
namespace Groups {
void init();
void sendToGroup(Player* plr, void* buf, uint32_t type, size_t size);
void groupTickInfo(Player* plr);
void groupKickPlayer(Player* plr);
int getGroupFlags(Player* plr);
void sendToGroup(Group* group, void* buf, uint32_t type, size_t size);
void sendToGroup(Group* group, EntityRef excluded, void* buf, uint32_t type, size_t size);
void groupTickInfo(CNSocket* sock);
void groupKick(Group* group, EntityRef ref);
void addToGroup(Group* group, EntityRef member);
bool removeFromGroup(Group* group, EntityRef member); // true iff group deleted
void disbandGroup(Group* group);
}

View File

@@ -1,16 +1,19 @@
#include "servers/CNShardServer.hpp"
#include "Items.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Nanos.hpp"
#include "NPCManager.hpp"
#include "Player.hpp"
#include "Abilities.hpp"
#include "Missions.hpp"
#include "Eggs.hpp"
#include "Rand.hpp"
#include "MobAI.hpp"
#include "Missions.hpp"
#include "Buffs.hpp"
#include <string.h> // for memset()
#include <assert.h>
#include <numeric>
using namespace Items;
@@ -413,6 +416,9 @@ static void itemDeleteHandler(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
if (itemdel->iSlotNum < 0 || itemdel->iSlotNum >= AINVEN_COUNT)
return; // sanity check
resp.eIL = itemdel->eIL;
resp.iSlotNum = itemdel->iSlotNum;
@@ -479,30 +485,34 @@ static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
resp->iSlotNum = request->iSlotNum;
resp->RemainItem = gumball;
resp->iTargetCnt = 1;
resp->eST = EST_NANOSTIMPAK;
resp->eST = (int32_t)SkillType::NANOSTIMPAK;
resp->iSkillID = 144;
int value1 = CSB_BIT_STIMPAKSLOT1 << request->iNanoSlot;
int value2 = ECSB_STIMPAKSLOT1 + request->iNanoSlot;
int eCSB = ECSB_STIMPAKSLOT1 + request->iNanoSlot;
respdata->eCT = 1;
respdata->iID = player->iID;
respdata->iConditionBitFlag = value1;
respdata->iConditionBitFlag = CSB_FROM_ECSB(eCSB);
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = value2; // eCharStatusTimeBuffID
pkt.eTBU = 1; // eTimeBuffUpdate
pkt.eTBT = 1; // eTimeBuffType 1 means nano
pkt.iConditionBitFlag = player->iConditionBitFlag |= value1;
sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE);
int durationMilliseconds = Abilities::SkillTable[144].durationTime[0] * 100;
BuffStack gumballBuff = {
durationMilliseconds / MS_PER_PLAYER_TICK,
0,
sock,
BuffClass::CASH_ITEM // or BuffClass::ITEM?
};
player->addBuff(eCSB,
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
Buffs::timeBuffUpdate(self, buff, status, stack);
},
[](EntityRef self, Buff* buff, time_t currTime) {
// no-op
},
&gumballBuff);
sock->sendPacket((void*)&respbuf, P_FE2CL_REP_PC_ITEM_USE_SUCC, resplen);
// update inventory serverside
player->Inven[resp->iSlotNum] = resp->RemainItem;
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, value1);
time_t until = getTime() + (time_t)Nanos::SkillTable[144].durationTime[0] * 100;
Eggs::EggBuffs[key] = until;
}
static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) {
@@ -755,7 +765,7 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
if (rolled.taros % miscDropChance.taroDropChanceTotal < miscDropChance.taroDropChance) {
plr->money += miscDropType.taroAmount;
// money nano boost
if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) {
if (plr->hasBuff(ECSB_REWARD_CASH)) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
@@ -770,7 +780,7 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
if (levelDifference > 0)
fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0;
// scavenger nano boost
if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) {
if (plr->hasBuff(ECSB_REWARD_BLOB)) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
@@ -822,12 +832,12 @@ static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRo
void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const DropRoll& eventRolled) {
// sanity check
if (Items::MobToDropMap.find(mob->appearanceData.iNPCType) == Items::MobToDropMap.end()) {
std::cout << "[WARN] Mob ID " << mob->appearanceData.iNPCType << " has no drops assigned" << std::endl;
if (Items::MobToDropMap.find(mob->type) == Items::MobToDropMap.end()) {
std::cout << "[WARN] Mob ID " << mob->type << " has no drops assigned" << std::endl;
return;
}
// find mob drop id
int mobDropId = Items::MobToDropMap[mob->appearanceData.iNPCType];
int mobDropId = Items::MobToDropMap[mob->type];
giveSingleDrop(sock, mob, mobDropId, rolled);

View File

@@ -1,9 +1,13 @@
#pragma once
#include "servers/CNShardServer.hpp"
#include "core/Core.hpp"
#include "Player.hpp"
#include "MobAI.hpp"
#include "Rand.hpp"
#include "MobAI.hpp"
#include <vector>
#include <map>
struct CrocPotEntry {
int multStats, multLooks;

View File

@@ -1,11 +1,10 @@
#include "servers/CNShardServer.hpp"
#include "Missions.hpp"
#include "PlayerManager.hpp"
#include "Nanos.hpp"
#include "Items.hpp"
#include "Transport.hpp"
#include "string.h"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Nanos.hpp"
using namespace Missions;
@@ -32,7 +31,7 @@ static bool isMissionCompleted(Player* player, int missionId) {
return player->aQuestFlag[row] & (1ULL << column);
}
static int findQSlot(Player *plr, int id) {
int Missions::findQSlot(Player *plr, int id) {
int i;
// two passes. we mustn't fail to find an existing stack.
@@ -52,7 +51,7 @@ static int findQSlot(Player *plr, int id) {
static bool isQuestItemFull(CNSocket* sock, int itemId, int itemCount) {
Player* plr = PlayerManager::getPlayer(sock);
int slot = findQSlot(plr, itemId);
int slot = Missions::findQSlot(plr, itemId);
if (slot == -1) {
// this should never happen
std::cout << "[WARN] Player has no room for quest item!?" << std::endl;
@@ -78,7 +77,7 @@ static void dropQuestItem(CNSocket *sock, int task, int count, int id, int mobid
memset(respbuf, 0, resplen);
// find free quest item slot
int slot = findQSlot(plr, id);
int slot = Missions::findQSlot(plr, id);
if (slot == -1) {
// this should never happen
std::cout << "[WARN] Player has no room for quest item!?" << std::endl;
@@ -164,14 +163,14 @@ static int giveMissionReward(CNSocket *sock, int task, int choice=0) {
// update player
plr->money += reward->money;
if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) { // nano boost for taros
if (plr->hasBuff(ECSB_REWARD_CASH)) { // nano boost for taros
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
plr->money += reward->money * (5 + boost) / 25;
}
if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) { // nano boost for fm
if (plr->hasBuff(ECSB_REWARD_BLOB)) { // nano boost for fm
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
@@ -219,28 +218,18 @@ static bool endTask(CNSocket *sock, int32_t taskNum, int choice=0) {
// ugly pointer/reference juggling for the sake of operator overloading...
TaskData& task = *Tasks[taskNum];
// update player
// sanity check
int i;
bool found = false;
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] == taskNum) {
found = true;
plr->tasks[i] = 0;
for (int j = 0; j < 3; j++) {
plr->RemainingNPCCount[i][j] = 0;
}
break;
}
}
if (!found) {
std::cout << "[WARN] Player tried to end task that isn't in journal?" << std::endl;
if (!found)
return false;
}
if (i == ACTIVE_MISSION_COUNT - 1 && plr->tasks[i] != 0) {
std::cout << "[WARN] Player completed non-active mission!?" << std::endl;
return false;
}
// mission rewards
if (Rewards.find(taskNum) != Rewards.end()) {
@@ -249,6 +238,23 @@ static bool endTask(CNSocket *sock, int32_t taskNum, int choice=0) {
}
// don't take away quest items if we haven't finished the quest
/*
* Update player's active mission data.
*
* This must be done after all early returns have passed, otherwise we
* risk introducing non-atomic changes. For example, failing to finish
* a mission due to not having any inventory space could delete the
* mission server-side; leading to a desync.
*/
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] == taskNum) {
plr->tasks[i] = 0;
for (int j = 0; j < 3; j++) {
plr->RemainingNPCCount[i][j] = 0;
}
}
}
/*
* Give (or take away) quest items
*
@@ -291,7 +297,7 @@ bool Missions::startTask(Player* plr, int TaskID) {
TaskData& task = *Missions::Tasks[TaskID];
if (task["m_iCTRReqLvMin"] > plr->level) {
std::cout << "[WARN] Player tried to start a task below their level" << std::endl;
std::cout << "[WARN] Player tried to start a task above their level" << std::endl;
return false;
}
@@ -360,15 +366,15 @@ static void taskStart(CNSocket* sock, CNPacketData* data) {
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
// if escort task, assign matching paths to all nearby NPCs
if (task["m_iHTaskType"] == 6) {
if (task["m_iHTaskType"] == (int)eTaskTypeProperty::EscortDefence) {
for (ChunkPos& chunkPos : Chunking::getChunksInMap(plr->instanceID)) { // check all NPCs in the instance
Chunk* chunk = Chunking::chunks[chunkPos];
for (EntityRef ref : chunk->entities) {
if (ref.type != EntityType::PLAYER) {
if (ref.kind != EntityKind::PLAYER) {
BaseNPC* npc = (BaseNPC*)ref.getEntity();
NPCPath* path = Transport::findApplicablePath(npc->appearanceData.iNPC_ID, npc->appearanceData.iNPCType, missionData->iTaskNum);
NPCPath* path = Transport::findApplicablePath(npc->id, npc->type, missionData->iTaskNum);
if (path != nullptr) {
Transport::constructPathNPC(npc->appearanceData.iNPC_ID, path);
Transport::constructPathNPC(npc->id, path);
return;
}
}
@@ -380,18 +386,22 @@ static void taskStart(CNSocket* sock, CNPacketData* data) {
static void taskEnd(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TASK_END* missionData = (sP_CL2FE_REQ_PC_TASK_END*)data->buf;
// failed timed missions give an iNPC_ID of 0
if (missionData->iNPC_ID == 0) {
if (Missions::Tasks.find(missionData->iTaskNum) == Missions::Tasks.end())
return;
TaskData* task = Missions::Tasks[missionData->iTaskNum];
if (task->task["m_iSTGrantTimer"] > 0) { // its a timed mission
// handle timed mission failure
if (task->task["m_iSTGrantTimer"] > 0 && missionData->iNPC_ID == 0) {
Player* plr = PlayerManager::getPlayer(sock);
/*
* Enemy killing missions
* this is gross and should be cleaned up later
* once we comb over mission logic more throughly
*/
bool mobsAreKilled = false;
if (task->task["m_iHTaskType"] == 5) {
if (task->task["m_iHTaskType"] == (int)eTaskTypeProperty::Defeat) {
mobsAreKilled = true;
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] == missionData->iTaskNum) {
@@ -406,7 +416,6 @@ static void taskEnd(CNSocket* sock, CNPacketData* data) {
}
if (!mobsAreKilled) {
int failTaskID = task->task["m_iFOutgoingTask"];
if (failTaskID != 0) {
Missions::quitTask(sock, missionData->iTaskNum, false);
@@ -418,7 +427,6 @@ static void taskEnd(CNSocket* sock, CNPacketData* data) {
}
}
}
}
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, response);
@@ -486,6 +494,12 @@ void Missions::quitTask(CNSocket* sock, int32_t taskNum, bool manual) {
memset(&plr->QInven[j], 0, sizeof(sItemBase));
}
} else {
for (i = 0; i < 3; i++) {
if (task["m_iFItemID"][i] == 0)
continue;
dropQuestItem(sock, taskNum, task["m_iFItemNumNeeded"][i], task["m_iFItemID"][i], 0);
}
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_FAIL, failResp);
failResp.iErrorCode = 1;
failResp.iTaskNum = taskNum;
@@ -537,9 +551,6 @@ void Missions::updateFusionMatter(CNSocket* sock, int fusion) {
response.iTaskNum = AvatarGrowth[plr->level]["m_iNanoQuestTaskID"];
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
#else
if (plr->level >= 36)
return;
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
plr->level++;
@@ -558,7 +569,7 @@ void Missions::updateFusionMatter(CNSocket* sock, int fusion) {
PlayerManager::sendToViewable(sock, (void*)&bcast, P_FE2CL_PC_EVENT, sizeof(sP_FE2CL_PC_EVENT));
}
void Missions::mobKilled(CNSocket *sock, int mobid, int rolledQItem) {
void Missions::mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls) {
Player *plr = PlayerManager::getPlayer(sock);
bool missionmob = false;
@@ -581,12 +592,29 @@ void Missions::mobKilled(CNSocket *sock, int mobid, int rolledQItem) {
plr->RemainingNPCCount[i][j]--;
}
}
// drop quest item
if (task["m_iCSUItemNumNeeded"][j] != 0 && !isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j]) ) {
bool drop = rolledQItem % 100 < task["m_iSTItemDropRate"][j];
bool drop = rolls[plr->tasks[i]] % 100 < task["m_iSTItemDropRate"][j];
if (drop) {
// XXX: are CSUItemID and CSTItemID the same?
dropQuestItem(sock, plr->tasks[i], 1, task["m_iCSUItemID"][j], mobid);
/*
* Workaround: The client has a bug where it only sends a TASK_END request
* for the first task of multiple that met their quest item requirements
* at the same time. We deal with this by sending TASK_END response packets
* proactively and then silently ignoring the extra TASK_END requests it
* sends afterwards.
*/
if (isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j])) {
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, end);
end.iTaskNum = plr->tasks[i];
if (!endTask(sock, plr->tasks[i]))
continue;
sock->sendPacket(end, P_FE2CL_REP_PC_TASK_END_SUCC);
}
} else {
// fail to drop (itemID == 0)
dropQuestItem(sock, plr->tasks[i], 1, 0, mobid);

View File

@@ -1,9 +1,11 @@
#pragma once
#include "servers/CNShardServer.hpp"
#include "core/Core.hpp"
#include "JSON.hpp"
#include "Player.hpp"
#include "JSON.hpp"
#include <map>
struct Reward {
int32_t id;
@@ -40,13 +42,14 @@ namespace Missions {
extern std::map<int32_t, TaskData*> Tasks;
extern nlohmann::json AvatarGrowth[37];
void init();
int findQSlot(Player *plr, int id);
bool startTask(Player* plr, int TaskID);
// checks if player doesn't have n/n quest items
void updateFusionMatter(CNSocket* sock, int fusion);
void mobKilled(CNSocket *sock, int mobid, int rolledQItem);
void mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls);
void quitTask(CNSocket* sock, int32_t taskNum, bool manual);

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,26 @@
#pragma once
#include "core/Core.hpp"
#include "NPCManager.hpp"
#include "JSON.hpp"
enum class MobState {
INACTIVE,
ROAMING,
COMBAT,
RETREAT,
DEAD
};
#include "Entities.hpp"
#include <unordered_map>
#include <string>
namespace MobAI {
// needs to be declared before Mob's constructor
void step(CombatNPC*, time_t);
};
void deadStep(CombatNPC* self, time_t currTime);
void combatStep(CombatNPC* self, time_t currTime);
void roamingStep(CombatNPC* self, time_t currTime);
void retreatStep(CombatNPC* self, time_t currTime);
void onRoamStart(CombatNPC* self, EntityRef src);
void onCombatStart(CombatNPC* self, EntityRef src);
void onRetreat(CombatNPC* self, EntityRef src);
void onDeath(CombatNPC* self, EntityRef src);
}
struct Mob : public CombatNPC {
// general
MobState state = MobState::INACTIVE;
std::unordered_map<int32_t,time_t> unbuffTimes = {};
// dead
time_t killedTime = 0;
@@ -47,16 +47,13 @@ struct Mob : public CombatNPC {
int offsetX = 0, offsetY = 0;
int groupMember[4] = {};
// for optimizing away AI in empty chunks
int playersInView = 0;
// temporary; until we're sure what's what
nlohmann::json data = {};
Mob(int x, int y, int z, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
: CombatNPC(x, y, z, angle, iID, t, id, d["m_iHP"]),
Mob(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
: CombatNPC(spawnX, spawnY, spawnZ, angle, iID, t, id, d["m_iHP"]),
sightRange(d["m_iSightRange"]) {
state = MobState::ROAMING;
state = AIState::ROAMING;
data = d;
@@ -65,20 +62,28 @@ struct Mob : public CombatNPC {
idleRange = (int)data["m_iIdleRange"];
level = data["m_iNpcLevel"];
roamX = spawnX = x;
roamY = spawnY = y;
roamZ = spawnZ = z;
roamX = spawnX;
roamY = spawnY;
roamZ = spawnZ;
offsetX = 0;
offsetY = 0;
appearanceData.iConditionBitFlag = 0;
// NOTE: there appear to be discrepancies in the dump
appearanceData.iHP = maxHealth;
hp = maxHealth;
type = EntityType::MOB;
_stepAI = MobAI::step;
kind = EntityKind::MOB;
// AI
stateHandlers[AIState::DEAD] = MobAI::deadStep;
stateHandlers[AIState::COMBAT] = MobAI::combatStep;
stateHandlers[AIState::ROAMING] = MobAI::roamingStep;
stateHandlers[AIState::RETREAT] = MobAI::retreatStep;
transitionHandlers[AIState::DEAD] = MobAI::onDeath;
transitionHandlers[AIState::COMBAT] = MobAI::onCombatStart;
transitionHandlers[AIState::ROAMING] = MobAI::onRoamStart;
transitionHandlers[AIState::RETREAT] = MobAI::onRetreat;
}
// constructor for /summon
@@ -89,6 +94,9 @@ struct Mob : public CombatNPC {
~Mob() {}
virtual int takeDamage(EntityRef src, int amt) override;
virtual void step(time_t currTime) override;
auto operator[](std::string s) {
return data[s];
}
@@ -103,5 +111,4 @@ namespace MobAI {
void clearDebuff(Mob *mob);
void followToCombat(Mob *mob);
void groupRetreat(Mob *mob);
void enterCombat(CNSocket *sock, Mob *mob);
}

View File

@@ -1,4 +0,0 @@
#pragma once
#include "Chunking.hpp"
#include "Entities.hpp"

View File

@@ -1,4 +1,8 @@
#include "NPCManager.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "settings.hpp"
#include "Combat.hpp"
@@ -20,8 +24,6 @@
#include <assert.h>
#include <limits.h>
#include "JSON.hpp"
using namespace NPCManager;
std::unordered_map<int32_t, BaseNPC*> NPCManager::NPCs;
@@ -67,7 +69,7 @@ void NPCManager::destroyNPC(int32_t id) {
void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I, int angle) {
BaseNPC* npc = NPCs[id];
npc->appearanceData.iAngle = angle;
npc->angle = angle;
ChunkPos oldChunk = npc->chunkPos;
ChunkPos newChunk = Chunking::chunkPosAt(X, Y, I);
npc->x = X;
@@ -79,11 +81,11 @@ void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I,
Chunking::updateEntityChunk({id}, oldChunk, newChunk);
}
void NPCManager::sendToViewable(BaseNPC *npc, void *buf, uint32_t type, size_t size) {
void NPCManager::sendToViewable(Entity *npc, void *buf, uint32_t type, size_t size) {
for (auto it = npc->viewableChunks.begin(); it != npc->viewableChunks.end(); it++) {
Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) {
if (ref.type == EntityType::PLAYER)
if (ref.kind == EntityKind::PLAYER)
ref.sock->sendPacket(buf, type, size);
}
}
@@ -92,20 +94,49 @@ void NPCManager::sendToViewable(BaseNPC *npc, void *buf, uint32_t type, size_t s
static void npcBarkHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_BARKER* req = (sP_CL2FE_REQ_BARKER*)data->buf;
// get bark IDs from task data
TaskData* td = Missions::Tasks[req->iMissionTaskID];
std::vector<int> barks;
for (int i = 0; i < 4; i++) {
if (td->task["m_iHBarkerTextID"][i] != 0) // non-zeroes only
barks.push_back(td->task["m_iHBarkerTextID"][i]);
int taskID = req->iMissionTaskID;
// ignore req->iNPC_ID as it is often fixated on a single npc in the region
if (Missions::Tasks.find(taskID) == Missions::Tasks.end()) {
std::cout << "mission task not found: " << taskID << std::endl;
return;
}
if (barks.empty())
return; // no barks
TaskData* td = Missions::Tasks[taskID];
auto& barks = td->task["m_iHBarkerTextID"];
Player* plr = PlayerManager::getPlayer(sock);
std::vector<std::pair<int32_t, int32_t>> npcLines;
for (Chunk* chunk : plr->viewableChunks) {
for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) {
if (ent->kind != EntityKind::SIMPLE_NPC)
continue;
BaseNPC* npc = (BaseNPC*)ent->getEntity();
if (npc->type < 0 || npc->type >= NPCData.size())
continue; // npc unknown ?!
int barkType = NPCData[npc->type]["m_iBarkerType"];
if (barkType < 1 || barkType > 4)
continue; // no barks
int barkID = barks[barkType - 1];
if (barkID == 0)
continue; // no barks
npcLines.push_back(std::make_pair(npc->id, barkID));
}
}
if (npcLines.size() == 0)
return; // totally no barks
auto& [npcID, missionStringID] = npcLines[Rand::rand(npcLines.size())];
INITSTRUCT(sP_FE2CL_REP_BARKER, resp);
resp.iNPC_ID = req->iNPC_ID;
resp.iMissionStringID = barks[Rand::rand(barks.size())];
resp.iNPC_ID = npcID;
resp.iMissionStringID = missionStringID;
sock->sendPacket(resp, P_FE2CL_REP_BARKER);
}
@@ -120,22 +151,20 @@ static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
}
// type must already be checked and updateNPCPosition() must be called on the result
BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn, bool baseInstance) {
BaseNPC *NPCManager::summonNPC(int spawnX, int spawnY, int spawnZ, uint64_t instance, int type, bool respawn, bool baseInstance) {
uint64_t inst = baseInstance ? MAPNUM(instance) : instance;
#define EXTRA_HEIGHT 0
//assert(nextId < INT32_MAX);
int id = nextId--;
int team = NPCData[type]["m_iTeam"];
BaseNPC *npc = nullptr;
if (team == 2) {
npc = new Mob(x, y, z + EXTRA_HEIGHT, inst, type, NPCData[type], id);
npc = new Mob(spawnX, spawnY, spawnZ, inst, type, NPCData[type], id);
// re-enable respawning, if desired
((Mob*)npc)->summoned = !respawn;
} else
npc = new BaseNPC(x, y, z + EXTRA_HEIGHT, 0, inst, type, id);
npc = new BaseNPC(0, inst, type, id);
NPCs[id] = npc;
@@ -154,7 +183,7 @@ static void npcSummonHandler(CNSocket* sock, CNPacketData* data) {
for (int i = 0; i < req->iNPCCnt; i++) {
BaseNPC *npc = summonNPC(plr->x, plr->y, plr->z, plr->instanceID, req->iNPCType);
updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, plr->instanceID, 0);
updateNPCPosition(npc->id, plr->x, plr->y, plr->z, plr->instanceID, 0);
}
}
@@ -183,9 +212,12 @@ static void handleWarp(CNSocket* sock, int32_t warpId) {
if (Warps[warpId].isInstance) {
uint64_t instanceID = Warps[warpId].instanceID;
Player* leader = plr;
if (plr->group != nullptr) leader = PlayerManager::getPlayer(plr->group->filter(EntityKind::PLAYER)[0].sock);
// if warp requires you to be on a mission, it's gotta be a unique instance
if (Warps[warpId].limitTaskID != 0 || instanceID == 14) { // 14 is a special case for the Time Lab
instanceID += ((uint64_t)plr->iIDGroup << 32); // upper 32 bits are leader ID
instanceID += ((uint64_t)leader->iID << 32); // upper 32 bits are leader ID
Chunking::createInstance(instanceID);
// save Lair entrance coords as a pseudo-Resurrect 'Em
@@ -195,14 +227,13 @@ static void handleWarp(CNSocket* sock, int32_t warpId) {
plr->recallInstance = instanceID;
}
if (plr->iID == plr->iIDGroup && plr->groupCnt == 1)
if (plr->group == nullptr)
PlayerManager::sendPlayerTo(sock, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID);
else {
Player* leaderPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
for (int i = 0; i < leaderPlr->groupCnt; i++) {
Player* otherPlr = PlayerManager::getPlayerFromID(leaderPlr->groupIDs[i]);
CNSocket* sockTo = PlayerManager::getSockFromID(leaderPlr->groupIDs[i]);
auto players = plr->group->filter(EntityKind::PLAYER);
for (int i = 0; i < players.size(); i++) {
CNSocket* sockTo = players[i].sock;
Player* otherPlr = PlayerManager::getPlayer(sockTo);
if (otherPlr == nullptr || sockTo == nullptr)
continue;
@@ -246,8 +277,7 @@ static void handleWarp(CNSocket* sock, int32_t warpId) {
Missions::failInstancedMissions(sock); // fail any instanced missions
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC);
Chunking::updateEntityChunk({sock}, plr->chunkPos, std::make_tuple(0, 0, 0)); // force player to reload chunks
PlayerManager::updatePlayerPosition(sock, resp.iX, resp.iY, resp.iZ, INSTANCE_OVERWORLD, plr->angle);
PlayerManager::updatePlayerPositionForWarp(sock, resp.iX, resp.iY, resp.iZ, INSTANCE_OVERWORLD);
// remove the player's ongoing race, if any
if (Racing::EPRaces.find(sock) != Racing::EPRaces.end())
@@ -277,7 +307,7 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
for (auto c = chunks->begin(); c != chunks->end(); c++) { // haha get it
Chunk* chunk = *c;
for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) {
if (ent->type == EntityType::PLAYER)
if (ent->kind == EntityKind::PLAYER)
continue;
BaseNPC* npcTemp = (BaseNPC*)ent->getEntity();
@@ -292,57 +322,55 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
return npc;
}
// TODO: Move this to MobAI, possibly
// TODO: Move this to separate file in ai/ subdir when implementing more events
#pragma region NPCEvents
// summon right arm and stage 2 body
static void lordFuseStageTwo(CNSocket *sock, BaseNPC *npc) {
static void lordFuseStageTwo(CombatNPC *npc) {
Mob *oldbody = (Mob*)npc; // adaptium, stun
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage two" << std::endl;
// Fuse doesn't move; spawnX, etc. is shorter to write than *appearanceData*
// Fuse doesn't move
// Blastons, Heal
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2467);
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2467);
newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(newbody->appearanceData.iNPC_ID, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
plr->instanceID, oldbody->appearanceData.iAngle);
newbody->angle = oldbody->angle;
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
oldbody->instanceID, oldbody->angle);
// right arm, Adaptium, Stun
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->spawnX - 600, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2469);
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, oldbody->instanceID, 2469);
arm->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(arm->appearanceData.iNPC_ID, arm->spawnX, arm->spawnY, arm->spawnZ,
plr->instanceID, oldbody->appearanceData.iAngle);
arm->angle = oldbody->angle;
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
oldbody->instanceID, oldbody->angle);
}
// summon left arm and stage 3 body
static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) {
static void lordFuseStageThree(CombatNPC *npc) {
Mob *oldbody = (Mob*)npc;
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage three" << std::endl;
// Cosmix, Damage Point
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2468);
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2468);
newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(newbody->appearanceData.iNPC_ID, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
plr->instanceID, oldbody->appearanceData.iAngle);
newbody->angle = oldbody->angle;
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
newbody->instanceID, oldbody->angle);
// Blastons, Heal
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->spawnX + 600, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2470);
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, oldbody->instanceID, 2470);
arm->appearanceData.iAngle = oldbody->appearanceData.iAngle;
NPCManager::updateNPCPosition(arm->appearanceData.iNPC_ID, arm->spawnX, arm->spawnY, arm->spawnZ,
plr->instanceID, oldbody->appearanceData.iAngle);
arm->angle = oldbody->angle;
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
arm->instanceID, oldbody->angle);
}
std::vector<NPCEvent> NPCManager::NPCEvents = {
NPCEvent(2466, ON_KILLED, lordFuseStageTwo),
NPCEvent(2467, ON_KILLED, lordFuseStageThree),
NPCEvent(2466, AIState::DEAD, lordFuseStageTwo),
NPCEvent(2467, AIState::DEAD, lordFuseStageThree),
};
#pragma endregion NPCEvents
@@ -353,11 +381,11 @@ void NPCManager::queueNPCRemoval(int32_t id) {
static void step(CNServer *serv, time_t currTime) {
for (auto& pair : NPCs) {
if (pair.second->type != EntityType::COMBAT_NPC && pair.second->type != EntityType::MOB)
if (pair.second->kind != EntityKind::COMBAT_NPC && pair.second->kind != EntityKind::MOB)
continue;
auto npc = (CombatNPC*)pair.second;
npc->stepAI(currTime);
npc->step(currTime);
}
// deallocate all NPCs queued for removal
@@ -374,5 +402,5 @@ void NPCManager::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_UNSUMMON, npcUnsummonHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_BARKER, npcBarkHandler);
REGISTER_SHARD_TIMER(step, 200);
REGISTER_SHARD_TIMER(step, MS_PER_COMBAT_TICK);
}

View File

@@ -1,36 +1,30 @@
#pragma once
#include "core/Core.hpp"
#include "PlayerManager.hpp"
#include "NPC.hpp"
#include "Transport.hpp"
#include "JSON.hpp"
#include "Transport.hpp"
#include "Chunking.hpp"
#include "Entities.hpp"
#include <map>
#include <unordered_map>
#include <vector>
#include <set>
#define RESURRECT_HEIGHT 400
enum Trigger {
ON_KILLED,
ON_COMBAT
};
typedef void (*NPCEventHandler)(CNSocket*, BaseNPC*);
typedef void (*NPCEventHandler)(CombatNPC*);
struct NPCEvent {
int32_t npcType;
int trigger;
AIState triggerState;
NPCEventHandler handler;
NPCEvent(int32_t t, int tr, NPCEventHandler hndlr)
: npcType(t), trigger(tr), handler(hndlr) {}
NPCEvent(int32_t t, AIState tr, NPCEventHandler hndlr)
: npcType(t), triggerState(tr), handler(hndlr) {}
};
struct WarpLocation;
namespace NPCManager {
extern std::unordered_map<int32_t, BaseNPC*> NPCs;
extern std::map<int32_t, WarpLocation> Warps;
@@ -44,7 +38,7 @@ namespace NPCManager {
void destroyNPC(int32_t);
void updateNPCPosition(int32_t, int X, int Y, int Z, uint64_t I, int angle);
void sendToViewable(BaseNPC* npc, void* buf, uint32_t type, size_t size);
void sendToViewable(Entity* npc, void* buf, uint32_t type, size_t size);
BaseNPC *summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn=false, bool baseInstance=false);

View File

@@ -1,11 +1,11 @@
#include "servers/CNShardServer.hpp"
#include "Nanos.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "Combat.hpp"
#include "Missions.hpp"
#include "Groups.hpp"
#include "Abilities.hpp"
#include "NPCManager.hpp"
#include <cmath>
@@ -82,40 +82,21 @@ void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
if (slot != -1 && plr->Nanos[nanoID].iSkillID == 0)
return; // prevent powerless nanos from summoning
plr->nanoDrainRate = 0;
int16_t skillID = plr->Nanos[plr->activeNano].iSkillID;
// passive nano unbuffing
if (SkillTable[skillID].drainType == 2) {
std::vector<int> targetData = findTargets(plr, skillID);
for (auto& pwr : NanoPowers)
if (pwr.skillType == SkillTable[skillID].skillType)
nanoUnbuff(sock, targetData, pwr.bitFlag, pwr.timeBuffID, 0,(SkillTable[skillID].targetType == 3));
}
if (nanoID >= NANO_COUNT || nanoID < 0)
return; // sanity check
plr->activeNano = nanoID;
skillID = plr->Nanos[nanoID].iSkillID;
sNano& nano = plr->Nanos[nanoID];
// passive nano buffing
if (SkillTable[skillID].drainType == 2) {
std::vector<int> targetData = findTargets(plr, skillID);
int boost = 0;
if (getNanoBoost(plr))
boost = 1;
for (auto& pwr : NanoPowers) {
if (pwr.skillType == SkillTable[skillID].skillType) {
resp.eCSTB___Add = 1; // the part that makes nano go ZOOMAZOOM
plr->nanoDrainRate = SkillTable[skillID].batteryUse[boost*3];
pwr.handle(sock, targetData, nanoID, skillID, 0, SkillTable[skillID].powerIntensity[boost]);
}
}
SkillData* skill = Abilities::SkillTable.count(nano.iSkillID) > 0
? &Abilities::SkillTable[nano.iSkillID] : nullptr;
if (slot != -1 && skill != nullptr && skill->drainType == SkillDrainType::PASSIVE) {
// passive buff effect
resp.eCSTB___Add = 1;
ICombatant* src = dynamic_cast<ICombatant*>(plr);
int32_t targets[] = { plr->iID };
std::vector<ICombatant*> affectedCombatants = Abilities::matchTargets(src, skill, 1, targets);
Abilities::useNanoSkill(sock, skill, nano, affectedCombatants);
}
if (!silent) // silent nano death but only for the summoning player
@@ -124,12 +105,12 @@ void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
// Send to other players, these players can't handle silent nano deaths so this packet needs to be sent.
INITSTRUCT(sP_FE2CL_NANO_ACTIVE, pkt1);
pkt1.iPC_ID = plr->iID;
pkt1.Nano = plr->Nanos[nanoID];
pkt1.Nano = nano;
PlayerManager::sendToViewable(sock, pkt1, P_FE2CL_NANO_ACTIVE);
}
static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
if (skill->iNanoID >= NANO_COUNT)
if (skill == nullptr || skill->iNanoID >= NANO_COUNT || skill->iNanoID < 0)
return;
Player *plr = PlayerManager::getPlayer(sock);
@@ -211,7 +192,7 @@ int Nanos::nanoStyle(int nanoID) {
bool Nanos::getNanoBoost(Player* plr) {
for (int i = 0; i < 3; i++)
if (plr->equippedNanos[i] == plr->activeNano)
if (plr->iConditionBitFlag & (CSB_BIT_STIMPAKSLOT1 << i))
if (plr->hasBuff(ECSB_STIMPAKSLOT1 + i))
return true;
return false;
}
@@ -226,24 +207,15 @@ static void nanoEquipHandler(CNSocket* sock, CNPacketData* data) {
if (nano->iNanoSlotNum > 2 || nano->iNanoSlotNum < 0)
return;
if (nano->iNanoID < 0 || nano->iNanoID >= NANO_COUNT)
return;
resp.iNanoID = nano->iNanoID;
resp.iNanoSlotNum = nano->iNanoSlotNum;
// Update player
plr->equippedNanos[nano->iNanoSlotNum] = nano->iNanoID;
// Unbuff gumballs
int value1 = CSB_BIT_STIMPAKSLOT1 << nano->iNanoSlotNum;
if (plr->iConditionBitFlag & value1) {
int value2 = ECSB_STIMPAKSLOT1 + nano->iNanoSlotNum;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = value2; // eCharStatusTimeBuffID
pkt.eTBU = 2; // eTimeBuffUpdate
pkt.eTBT = 1; // eTimeBuffType 1 means nano
pkt.iConditionBitFlag = plr->iConditionBitFlag &= ~value1;
sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE);
}
// unsummon nano if replaced
if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum])
summonNano(sock, -1);
@@ -286,28 +258,26 @@ static void nanoSummonHandler(CNSocket* sock, CNPacketData* data) {
static void nanoSkillUseHandler(CNSocket* sock, CNPacketData* data) {
Player *plr = PlayerManager::getPlayer(sock);
int16_t nanoID = plr->activeNano;
int16_t skillID = plr->Nanos[nanoID].iSkillID;
// validate request check
sP_CL2FE_REQ_NANO_SKILL_USE* pkt = (sP_CL2FE_REQ_NANO_SKILL_USE*)data->buf;
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_NANO_SKILL_USE), pkt->iTargetCnt, sizeof(int32_t), data->size)) {
std::cout << "[WARN] bad sP_CL2FE_REQ_NANO_SKILL_USE packet size" << std::endl;
return;
}
sNano& nano = plr->Nanos[plr->activeNano];
int16_t skillID = nano.iSkillID;
SkillData* skillData = &Abilities::SkillTable[skillID];
DEBUGLOG(
std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano skill " << std::endl;
)
std::vector<int> targetData = findTargets(plr, skillID, data);
ICombatant* plrCombatant = dynamic_cast<ICombatant*>(plr);
std::vector<ICombatant*> targetData = Abilities::matchTargets(plrCombatant, skillData, pkt->iTargetCnt, (int32_t*)(pkt + 1));
Abilities::useNanoSkill(sock, skillData, nano, targetData);
int boost = 0;
if (getNanoBoost(plr))
boost = 1;
plr->Nanos[plr->activeNano].iStamina -= SkillTable[skillID].batteryUse[boost*3];
if (plr->Nanos[plr->activeNano].iStamina < 0)
plr->Nanos[plr->activeNano].iStamina = 0;
for (auto& pwr : NanoPowers)
if (pwr.skillType == SkillTable[skillID].skillType)
pwr.handle(sock, targetData, nanoID, skillID, SkillTable[skillID].durationTime[boost], SkillTable[skillID].powerIntensity[boost]);
if (plr->Nanos[plr->activeNano].iStamina < 0)
if (plr->Nanos[plr->activeNano].iStamina <= 0)
summonNano(sock, -1);
}

View File

@@ -1,10 +1,11 @@
#pragma once
#include <set>
#include <vector>
#include "core/Core.hpp"
#include "Player.hpp"
#include "servers/CNShardServer.hpp"
#include "Abilities.hpp"
#include <map>
struct NanoData {
int style;

View File

@@ -1,24 +1,24 @@
#pragma once
#include <string>
#include <cstring>
#include "core/Core.hpp"
#include "Chunking.hpp"
#include "Entities.hpp"
#include "lua/LuaWrapper.hpp"
#include "Entities.hpp"
#include "Groups.hpp"
#include <vector>
/* forward declaration(s) */
class Buff;
struct BuffStack;
#define ACTIVE_MISSION_COUNT 6
#define PC_MAXHEALTH(level) (925 + 75 * (level))
struct Player : public Entity {
struct Player : public Entity, public ICombatant {
int accountId = 0;
int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums)
int64_t SerialKey = 0;
int32_t iID = 0;
uint64_t FEKey = 0;
int level = 0;
int HP = 0;
@@ -36,9 +36,8 @@ struct Player : public Entity {
int8_t iPCState = 0;
int32_t iWarpLocationFlag = 0;
int64_t aSkywayLocationFlag[2] = {};
int32_t iConditionBitFlag = 0;
int32_t iSelfConditionBitFlag = 0;
int8_t iSpecialState = 0;
std::unordered_map<int, Buff*> buffs = {};
int angle = 0;
int lastX = 0, lastY = 0, lastZ = 0, lastAngle = 0;
@@ -53,7 +52,6 @@ struct Player : public Entity {
bool inCombat = false;
bool onMonkey = false;
int nanoDrainRate = 0;
int healCooldown = 0;
int pointDamage = 0;
@@ -69,16 +67,13 @@ struct Player : public Entity {
sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {};
int32_t iIDGroup = 0;
int groupCnt = 0;
int32_t groupIDs[4] = {};
int32_t iGroupConditionBitFlag = 0;
Group* group = nullptr;
bool notify = false;
bool hidden = false;
bool unwarpable = false;
bool initialLoadDone = false;
bool buddiesSynced = false;
int64_t buddyIDs[50] = {};
bool isBuddyBlocked[50] = {};
@@ -89,16 +84,31 @@ struct Player : public Entity {
time_t lastShot = 0;
std::vector<sItemBase> buyback = {};
// lua events
lEvent *onChat = nullptr;
Player() { type = EntityType::PLAYER; }
~Player() {
// if an event was registered, free it
if (onChat != nullptr)
delete onChat;
}
Player() { kind = EntityKind::PLAYER; }
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override;
virtual Buff* getBuff(int buffId) override;
virtual void removeBuff(int buffId) override;
virtual void removeBuff(int buffId, BuffClass buffClass) override;
virtual void clearBuffs(bool force) override;
virtual bool hasBuff(int buffId) override;
virtual int getCompositeCondition() override;
virtual int takeDamage(EntityRef src, int amt) override;
virtual int heal(EntityRef src, int amt) override;
virtual bool isAlive() override;
virtual int getCurrentHP() override;
virtual int getMaxHP() override;
virtual int getLevel() override;
virtual std::vector<EntityRef> getGroupMembers() override;
virtual int32_t getCharType() override;
virtual int32_t getID() override;
virtual EntityRef getRef() override;
virtual void step(time_t currTime) override;
sNano* getActiveNano();
sPCAppearanceData getAppearanceData();
};

View File

@@ -1,27 +1,20 @@
#include "core/Core.hpp"
#include "PlayerManager.hpp"
#include "db/Database.hpp"
#include "core/CNShared.hpp"
#include "servers/CNShardServer.hpp"
#include "db/Database.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "Missions.hpp"
#include "Items.hpp"
#include "Nanos.hpp"
#include "Groups.hpp"
#include "Chat.hpp"
#include "Buddies.hpp"
#include "Combat.hpp"
#include "Racing.hpp"
#include "BuiltinCommands.hpp"
#include "Abilities.hpp"
#include "Eggs.hpp"
#include "settings.hpp"
#include "lua/LuaManager.hpp"
#include "Missions.hpp"
#include "Chat.hpp"
#include "Items.hpp"
#include "Buddies.hpp"
#include "BuiltinCommands.hpp"
#include <assert.h>
#include <algorithm>
#include <vector>
#include <cmath>
@@ -30,31 +23,26 @@ using namespace PlayerManager;
std::map<CNSocket*, Player*> PlayerManager::players;
static void addPlayer(CNSocket* key, Player plr) {
Player *p = new Player();
static void addPlayer(CNSocket* key, Player *plr) {
players[key] = plr;
plr->chunkPos = Chunking::INVALID_CHUNK;
plr->lastHeartbeat = 0;
// copy object into heap memory
*p = plr;
players[key] = p;
p->chunkPos = std::make_tuple(0, 0, 0); // TODO: maybe replace with specialized "no chunk" value
p->lastHeartbeat = 0;
std::cout << getPlayerName(p) << " has joined!" << std::endl;
std::cout << getPlayerName(plr) << " has joined!" << std::endl;
std::cout << players.size() << " players" << std::endl;
// call events
LuaManager::playerAdded(key);
}
void PlayerManager::removePlayer(CNSocket* key) {
Player* plr = getPlayer(key);
uint64_t fromInstance = plr->instanceID;
// call events
LuaManager::playerRemoved(key);
// free buff memory
for(auto buffEntry : plr->buffs)
delete buffEntry.second;
Groups::groupKickPlayer(plr);
// leave group
if(plr->group != nullptr)
Groups::groupKick(plr->group, key);
// remove player's bullets
Combat::Bullets.erase(plr->iID);
@@ -78,16 +66,6 @@ void PlayerManager::removePlayer(CNSocket* key) {
// if the player was in a lair, clean it up
Chunking::destroyInstanceIfEmpty(fromInstance);
// remove player's buffs from the server
auto it = Eggs::EggBuffs.begin();
while (it != Eggs::EggBuffs.end()) {
if (it->first.first == key) {
it = Eggs::EggBuffs.erase(it);
}
else
it++;
}
std::cout << players.size() << " players" << std::endl;
}
@@ -99,12 +77,28 @@ void PlayerManager::updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, ui
plr->x = X;
plr->y = Y;
plr->z = Z;
if (plr->instanceID != I) {
plr->instanceID = I;
plr->recallInstance = INSTANCE_OVERWORLD;
}
if (oldChunk == newChunk)
return; // didn't change chunks
Chunking::updateEntityChunk({sock}, oldChunk, newChunk);
}
/*
* Low-level helper function for correctly updating chunks when teleporting players.
*
* Use PlayerManager::sendPlayerTo() to actually teleport players.
*/
void PlayerManager::updatePlayerPositionForWarp(CNSocket* sock, int X, int Y, int Z, uint64_t inst) {
Player *plr = getPlayer(sock);
// force player to reload chunks
Chunking::updateEntityChunk({sock}, plr->chunkPos, Chunking::INVALID_CHUNK);
updatePlayerPosition(sock, X, Y, Z, inst, plr->angle);
}
void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I) {
Player* plr = getPlayer(sock);
plr->onMonkey = false;
@@ -132,32 +126,13 @@ void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC);
}
if (I != INSTANCE_OVERWORLD) {
INITSTRUCT(sP_FE2CL_INSTANCE_MAP_INFO, pkt);
pkt.iInstanceMapNum = (int32_t)MAPNUM(I); // lower 32 bits are mapnum
if (I != fromInstance // do not retransmit MAP_INFO on recall
&& Racing::EPData.find(pkt.iInstanceMapNum) != Racing::EPData.end()) {
EPInfo* ep = &Racing::EPData[pkt.iInstanceMapNum];
pkt.iEP_ID = ep->EPID;
pkt.iMapCoordX_Min = ep->zoneX * 51200;
pkt.iMapCoordX_Max = (ep->zoneX + 1) * 51200;
pkt.iMapCoordY_Min = ep->zoneY * 51200;
pkt.iMapCoordY_Max = (ep->zoneY + 1) * 51200;
pkt.iMapCoordZ_Min = INT32_MIN;
pkt.iMapCoordZ_Max = INT32_MAX;
}
sock->sendPacket(pkt, P_FE2CL_INSTANCE_MAP_INFO);
}
INITSTRUCT(sP_FE2CL_REP_PC_GOTO_SUCC, pkt2);
pkt2.iX = X;
pkt2.iY = Y;
pkt2.iZ = Z;
sock->sendPacket(pkt2, P_FE2CL_REP_PC_GOTO_SUCC);
Chunking::updateEntityChunk({sock}, plr->chunkPos, std::make_tuple(0, 0, 0)); // force player to reload chunks
updatePlayerPosition(sock, X, Y, Z, I, plr->angle);
updatePlayerPositionForWarp(sock, X, Y, Z, I);
// post-warp: check if the source instance has no more players in it and delete it if so
Chunking::destroyInstanceIfEmpty(fromInstance);
@@ -180,16 +155,21 @@ void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z) {
* Nanos the player hasn't unlocked will (and should) be greyed out. Thus, all nanos should be accounted
* for in these packets, even if the player hasn't unlocked them.
*/
static void sendNanoBookSubset(CNSocket *sock) {
static void sendNanoBook(CNSocket *sock, Player *plr, bool resizeOnly) {
#ifdef ACADEMY
Player *plr = getPlayer(sock);
int16_t id = 0;
INITSTRUCT(sP_FE2CL_REP_NANO_BOOK_SUBSET, pkt);
pkt.PCUID = plr->iID;
pkt.bookSize = NANO_COUNT;
if (resizeOnly) {
// triggers nano array resizing without
// actually sending nanos
sock->sendPacket(pkt, P_FE2CL_REP_NANO_BOOK_SUBSET);
return;
}
while (id < NANO_COUNT) {
pkt.elementOffset = id;
@@ -205,78 +185,88 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
auto enter = (sP_CL2FE_REQ_PC_ENTER*)data->buf;
INITSTRUCT(sP_FE2CL_REP_PC_ENTER_SUCC, response);
// TODO: check if serialkey exists, if it doesn't send sP_FE2CL_REP_PC_ENTER_FAIL
Player plr = CNSharedData::getPlayer(enter->iEnterSerialKey);
LoginMetadata *lm = CNShared::getLoginMetadata(enter->iEnterSerialKey);
if (lm == nullptr) {
std::cout << "[WARN] Refusing invalid REQ_PC_ENTER" << std::endl;
plr.groupCnt = 1;
plr.iIDGroup = plr.groupIDs[0] = plr.iID;
// send failure packet
INITSTRUCT(sP_FE2CL_REP_PC_ENTER_FAIL, fail);
sock->sendPacket(fail, P_FE2CL_REP_PC_ENTER_FAIL);
DEBUGLOG(
std::cout << "P_CL2FE_REQ_PC_ENTER:" << std::endl;
std::cout << "\tID: " << AUTOU16TOU8(enter->szID) << std::endl;
std::cout << "\tSerial: " << enter->iEnterSerialKey << std::endl;
std::cout << "\tTemp: " << enter->iTempValue << std::endl;
std::cout << "\tPC_UID: " << plr.PCStyle.iPC_UID << std::endl;
)
// kill the connection
sock->kill();
// no need to call _killConnection(); Player isn't in shard yet
// check if account is already in use
if (isAccountInUse(plr.accountId)) {
// kick the other player
exitDuplicate(plr.accountId);
return;
}
response.iID = plr.iID;
Player *plr = new Player();
Database::getPlayer(plr, lm->playerId);
// check if account is already in use
if (isAccountInUse(plr->accountId)) {
// kick the other player
exitDuplicate(plr->accountId);
// re-read the player from disk, in case it was just flushed
*plr = {};
Database::getPlayer(plr, lm->playerId);
}
plr->group = nullptr;
response.iID = plr->iID;
response.uiSvrTime = getTime();
response.PCLoadData2CL.iUserLevel = plr.accountLevel;
response.PCLoadData2CL.iHP = plr.HP;
response.PCLoadData2CL.iLevel = plr.level;
response.PCLoadData2CL.iCandy = plr.money;
response.PCLoadData2CL.iFusionMatter = plr.fusionmatter;
response.PCLoadData2CL.iMentor = plr.mentor;
response.PCLoadData2CL.iUserLevel = plr->accountLevel;
response.PCLoadData2CL.iHP = plr->HP;
response.PCLoadData2CL.iLevel = plr->level;
response.PCLoadData2CL.iCandy = plr->money;
response.PCLoadData2CL.iFusionMatter = plr->fusionmatter;
response.PCLoadData2CL.iMentor = plr->mentor;
response.PCLoadData2CL.iMentorCount = 1; // how many guides the player has had
response.PCLoadData2CL.iX = plr.x;
response.PCLoadData2CL.iY = plr.y;
response.PCLoadData2CL.iZ = plr.z;
response.PCLoadData2CL.iAngle = plr.angle;
response.PCLoadData2CL.iBatteryN = plr.batteryN;
response.PCLoadData2CL.iBatteryW = plr.batteryW;
response.PCLoadData2CL.iX = plr->x;
response.PCLoadData2CL.iY = plr->y;
response.PCLoadData2CL.iZ = plr->z;
response.PCLoadData2CL.iAngle = plr->angle;
response.PCLoadData2CL.iBatteryN = plr->batteryN;
response.PCLoadData2CL.iBatteryW = plr->batteryW;
response.PCLoadData2CL.iBuddyWarpTime = 60; // sets 60s warp cooldown on login
response.PCLoadData2CL.iWarpLocationFlag = plr.iWarpLocationFlag;
response.PCLoadData2CL.aWyvernLocationFlag[0] = plr.aSkywayLocationFlag[0];
response.PCLoadData2CL.aWyvernLocationFlag[1] = plr.aSkywayLocationFlag[1];
response.PCLoadData2CL.iWarpLocationFlag = plr->iWarpLocationFlag;
response.PCLoadData2CL.aWyvernLocationFlag[0] = plr->aSkywayLocationFlag[0];
response.PCLoadData2CL.aWyvernLocationFlag[1] = plr->aSkywayLocationFlag[1];
response.PCLoadData2CL.iActiveNanoSlotNum = -1;
response.PCLoadData2CL.iFatigue = 50;
response.PCLoadData2CL.PCStyle = plr.PCStyle;
response.PCLoadData2CL.PCStyle = plr->PCStyle;
// client doesnt read this, it gets it from charinfo
// response.PCLoadData2CL.PCStyle2 = plr.PCStyle2;
// response.PCLoadData2CL.PCStyle2 = plr->PCStyle2;
// inventory
for (int i = 0; i < AEQUIP_COUNT; i++)
response.PCLoadData2CL.aEquip[i] = plr.Equip[i];
response.PCLoadData2CL.aEquip[i] = plr->Equip[i];
for (int i = 0; i < AINVEN_COUNT; i++)
response.PCLoadData2CL.aInven[i] = plr.Inven[i];
response.PCLoadData2CL.aInven[i] = plr->Inven[i];
// quest inventory
for (int i = 0; i < AQINVEN_COUNT; i++)
response.PCLoadData2CL.aQInven[i] = plr.QInven[i];
response.PCLoadData2CL.aQInven[i] = plr->QInven[i];
// nanos
for (int i = 1; i < SIZEOF_NANO_BANK_SLOT; i++) {
response.PCLoadData2CL.aNanoBank[i] = plr.Nanos[i];
//response.PCLoadData2CL.aNanoBank[i] = plr.Nanos[i] = {0};
response.PCLoadData2CL.aNanoBank[i] = plr->Nanos[i];
}
for (int i = 0; i < 3; i++) {
response.PCLoadData2CL.aNanoSlots[i] = plr.equippedNanos[i];
response.PCLoadData2CL.aNanoSlots[i] = plr->equippedNanos[i];
}
// missions in progress
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr.tasks[i] == 0)
if (plr->tasks[i] == 0)
break;
response.PCLoadData2CL.aRunningQuest[i].m_aCurrTaskID = plr.tasks[i];
TaskData &task = *Missions::Tasks[plr.tasks[i]];
response.PCLoadData2CL.aRunningQuest[i].m_aCurrTaskID = plr->tasks[i];
TaskData &task = *Missions::Tasks[plr->tasks[i]];
for (int j = 0; j < 3; j++) {
response.PCLoadData2CL.aRunningQuest[i].m_aKillNPCID[j] = (int)task["m_iCSUEnemyID"][j];
response.PCLoadData2CL.aRunningQuest[i].m_aKillNPCCount[j] = plr.RemainingNPCCount[i][j];
response.PCLoadData2CL.aRunningQuest[i].m_aKillNPCCount[j] = plr->RemainingNPCCount[i][j];
/*
* client doesn't care about NeededItem ID and Count,
* it gets Count from Quest Inventory
@@ -286,12 +276,12 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
*/
}
}
response.PCLoadData2CL.iCurrentMissionID = plr.CurrentMissionID;
response.PCLoadData2CL.iCurrentMissionID = plr->CurrentMissionID;
// completed missions
// the packet requires 32 items, but the client only checks the first 16 (shrug)
for (int i = 0; i < 16; i++) {
response.PCLoadData2CL.aQuestFlag[i] = plr.aQuestFlag[i];
response.PCLoadData2CL.aQuestFlag[i] = plr->aQuestFlag[i];
}
// Computress tips
@@ -300,39 +290,45 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
response.PCLoadData2CL.iFirstUseFlag2 = UINT64_MAX;
}
else {
response.PCLoadData2CL.iFirstUseFlag1 = plr.iFirstUseFlag[0];
response.PCLoadData2CL.iFirstUseFlag2 = plr.iFirstUseFlag[1];
response.PCLoadData2CL.iFirstUseFlag1 = plr->iFirstUseFlag[0];
response.PCLoadData2CL.iFirstUseFlag2 = plr->iFirstUseFlag[1];
}
plr.SerialKey = enter->iEnterSerialKey;
plr.instanceID = INSTANCE_OVERWORLD; // the player should never be in an instance on enter
plr->instanceID = INSTANCE_OVERWORLD; // the player should never be in an instance on enter
sock->setEKey(CNSocketEncryption::createNewKey(response.uiSvrTime, response.iID + 1, response.PCLoadData2CL.iFusionMatter + 1));
sock->setFEKey(plr.FEKey);
sock->setFEKey(lm->FEKey);
sock->setActiveKey(SOCKETKEY_FE); // send all packets using the FE key from now on
// Academy builds receive nanos in a separate packet. An initial one with the size of the
// nano book needs to be sent before PC_ENTER_SUCC so the client can resize its nano arrays,
// and then proper packets with the nanos included must be sent after, while the game is loading.
sendNanoBook(sock, plr, true);
sock->sendPacket(response, P_FE2CL_REP_PC_ENTER_SUCC);
// transmit MOTD after entering the game, so the client hopefully changes modes on time
Chat::sendServerMessage(sock, settings::MOTDSTRING);
sendNanoBook(sock, plr, false);
// transfer ownership of Player object into the shard (still valid in this function though)
addPlayer(sock, plr);
// check if there is an expiring vehicle
Items::checkItemExpire(sock, getPlayer(sock));
// set player equip stats
Items::setItemStats(getPlayer(sock));
Missions::failInstancedMissions(sock);
sendNanoBookSubset(sock);
// initial buddy sync
Buddies::refreshBuddyList(sock);
Items::setItemStats(plr);
for (auto& pair : players)
if (pair.second->notify)
Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(&plr) + " has joined.");
Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(plr) + " has joined.");
// deallocate lm
delete lm;
}
void PlayerManager::sendToGroup(CNSocket* sock, void* buf, uint32_t type, size_t size) {
Player* plr = getPlayer(sock);
if (plr->group == nullptr)
return;
for(const EntityRef& ref : plr->group->filter(EntityKind::PLAYER))
ref.sock->sendPacket(buf, type, size);
}
void PlayerManager::sendToViewable(CNSocket* sock, void* buf, uint32_t type, size_t size) {
@@ -340,7 +336,7 @@ void PlayerManager::sendToViewable(CNSocket* sock, void* buf, uint32_t type, siz
for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) {
Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) {
if (ref.type != EntityType::PLAYER || ref.sock == sock)
if (ref.kind != EntityKind::PLAYER || ref.sock == sock)
continue;
ref.sock->sendPacket(buf, type, size);
@@ -363,6 +359,35 @@ static void loadPlayer(CNSocket* sock, CNPacketData* data) {
updatePlayerPosition(sock, plr->x, plr->y, plr->z, plr->instanceID, plr->angle);
sock->sendPacket(response, P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC);
if (plr->instanceID != INSTANCE_OVERWORLD) {
INITSTRUCT(sP_FE2CL_INSTANCE_MAP_INFO, pkt);
pkt.iInstanceMapNum = (int32_t)MAPNUM(plr->instanceID); // lower 32 bits are mapnum
if (pkt.iInstanceMapNum != plr->recallInstance // do not retransmit MAP_INFO on recall
&& Racing::EPData.find(pkt.iInstanceMapNum) != Racing::EPData.end()) {
EPInfo* ep = &Racing::EPData[pkt.iInstanceMapNum];
pkt.iEP_ID = ep->EPID;
pkt.iMapCoordX_Min = ep->zoneX * 51200;
pkt.iMapCoordX_Max = (ep->zoneX + 1) * 51200;
pkt.iMapCoordY_Min = ep->zoneY * 51200;
pkt.iMapCoordY_Max = (ep->zoneY + 1) * 51200;
pkt.iMapCoordZ_Min = INT32_MIN;
pkt.iMapCoordZ_Max = INT32_MAX;
}
sock->sendPacket(pkt, P_FE2CL_INSTANCE_MAP_INFO);
}
if (!plr->initialLoadDone) {
// these should be called only once, but not until after
// first load-in or else the client may ignore the packets
Chat::sendServerMessage(sock, settings::MOTDSTRING); // MOTD
Missions::failInstancedMissions(sock); // auto-fail missions
Buddies::sendBuddyList(sock); // buddy list
Items::checkItemExpire(sock, plr); // vehicle expiration
plr->initialLoadDone = true;
}
}
static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
@@ -377,6 +402,9 @@ static void exitGame(CNSocket* sock, CNPacketData* data) {
response.iExitCode = 1;
sock->sendPacket(response, P_FE2CL_REP_PC_EXIT_SUCC);
sock->kill();
CNShardServer::_killConnection(sock);
}
static void revivePlayer(CNSocket* sock, CNPacketData* data) {
@@ -390,17 +418,23 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
int activeSlot = -1;
bool move = false;
if (reviveData->iRegenType == 3 && plr->iConditionBitFlag & CSB_BIT_PHOENIX) {
// nano revive
switch ((ePCRegenType)reviveData->iRegenType) {
case ePCRegenType::HereByPhoenix: // nano revive
if (!(plr->hasBuff(ECSB_PHOENIX)))
return; // sanity check
plr->Nanos[plr->activeNano].iStamina = 0;
plr->HP = PC_MAXHEALTH(plr->level);
Nanos::applyBuff(sock, plr->Nanos[plr->activeNano].iSkillID, 2, 1, 0);
} else if (reviveData->iRegenType == 4) {
plr->HP = PC_MAXHEALTH(plr->level);
} else {
// fallthrough
case ePCRegenType::HereByPhoenixGroup: // revived by group member's nano
plr->HP = PC_MAXHEALTH(plr->level) / 2;
break;
default: // plain respawn
plr->HP = PC_MAXHEALTH(plr->level) / 2;
plr->clearBuffs(false);
// fallthrough
case ePCRegenType::Unstick: // warp away
move = true;
if (reviveData->iRegenType != 5)
plr->HP = PC_MAXHEALTH(plr->level);
break;
}
for (int i = 0; i < 3; i++) {
@@ -452,11 +486,9 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
resp2.PCRegenDataForOtherPC.iHP = plr->HP;
resp2.PCRegenDataForOtherPC.iAngle = plr->angle;
Player *otherPlr = getPlayerFromID(plr->iIDGroup);
if (otherPlr != nullptr) {
int bitFlag = Groups::getGroupFlags(otherPlr);
resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->iConditionBitFlag = plr->iSelfConditionBitFlag | bitFlag;
if (plr->group != nullptr) {
resp2.PCRegenDataForOtherPC.iConditionBitFlag = plr->getCompositeCondition();
resp2.PCRegenDataForOtherPC.iPCState = plr->iPCState;
resp2.PCRegenDataForOtherPC.iSpecialState = plr->iSpecialState;
resp2.PCRegenDataForOtherPC.Nano = plr->Nanos[plr->activeNano];
@@ -467,8 +499,7 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
if (!move)
return;
Chunking::updateEntityChunk({sock}, plr->chunkPos, std::make_tuple(0, 0, 0)); // force player to reload chunks
updatePlayerPosition(sock, x, y, z, plr->instanceID, plr->angle);
updatePlayerPositionForWarp(sock, x, y, z, plr->instanceID);
}
static void enterPlayerVehicle(CNSocket* sock, CNPacketData* data) {
@@ -648,16 +679,16 @@ CNSocket *PlayerManager::getSockFromName(std::string firstname, std::string last
}
CNSocket *PlayerManager::getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname) {
switch (by) {
case eCN_GM_TargetSearchBy__PC_ID:
switch ((eCN_GM_TargetSearchBy)by) {
case eCN_GM_TargetSearchBy::PC_ID:
assert(id != 0);
return getSockFromID(id);
case eCN_GM_TargetSearchBy__PC_UID: // account id; not player id
case eCN_GM_TargetSearchBy::PC_UID: // account id; not player id
assert(uid != 0);
for (auto& pair : players)
if (pair.second->accountId == uid)
return pair.first;
case eCN_GM_TargetSearchBy__PC_Name:
case eCN_GM_TargetSearchBy::PC_Name:
assert(firstname != "" && lastname != ""); // XXX: remove this if we start messing around with edited names?
return getSockFromName(firstname, lastname);
}
@@ -695,4 +726,6 @@ void PlayerManager::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VEHICLE_OFF, exitPlayerVehicle);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_CHANGE_MENTOR, changePlayerGuide);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_FIRST_USE_FLAG_SET, setFirstUseFlag);
REGISTER_SHARD_TIMER(CNShared::pruneLoginMetadata, CNSHARED_PERIOD);
}

View File

@@ -1,16 +1,15 @@
#pragma once
#include "Player.hpp"
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "Chunking.hpp"
#include <utility>
#include "Player.hpp"
#include "Chunking.hpp"
#include "Transport.hpp"
#include "Entities.hpp"
#include <map>
#include <list>
struct WarpLocation;
namespace PlayerManager {
extern std::map<CNSocket*, Player*> players;
void init();
@@ -18,6 +17,7 @@ namespace PlayerManager {
void removePlayer(CNSocket* key);
void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, uint64_t I, int angle);
void updatePlayerPositionForWarp(CNSocket* sock, int X, int Y, int Z, uint64_t inst);
void sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I);
void sendPlayerTo(CNSocket* sock, int X, int Y, int Z);
@@ -33,6 +33,7 @@ namespace PlayerManager {
CNSocket *getSockFromAny(int by, int id, int uid, std::string firstname, std::string lastname);
WarpLocation *getRespawnPoint(Player *plr);
void sendToGroup(CNSocket *sock, void* buf, uint32_t type, size_t size);
void sendToViewable(CNSocket *sock, void* buf, uint32_t type, size_t size);
// TODO: unify this under the new Entity system
@@ -42,7 +43,7 @@ namespace PlayerManager {
for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) {
Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) {
if (ref.type != EntityType::PLAYER || ref.sock == sock)
if (ref.kind != EntityKind::PLAYER || ref.sock == sock)
continue;
ref.sock->sendPacket(pkt, type);

View File

@@ -1,4 +1,7 @@
#include "PlayerMovement.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "TableData.hpp"
#include "core/Core.hpp"
@@ -33,7 +36,7 @@ static void movePlayer(CNSocket* sock, CNPacketData* data) {
// [gruntwork] check if player has a follower and move it
if (TableData::RunningNPCPaths.find(plr->iID) != TableData::RunningNPCPaths.end()) {
BaseNPC* follower = TableData::RunningNPCPaths[plr->iID].first;
Transport::NPCQueues.erase(follower->appearanceData.iNPC_ID); // erase existing points
Transport::NPCQueues.erase(follower->id); // erase existing points
std::queue<Vec3> queue;
Vec3 from = { follower->x, follower->y, follower->z };
float drag = 0.95f; // this ensures that they don't bump into the player
@@ -45,7 +48,7 @@ static void movePlayer(CNSocket* sock, CNPacketData* data) {
// add a route to the queue; to be processed in Transport::stepNPCPathing()
Transport::lerp(&queue, from, to, NPC_DEFAULT_SPEED * 1.5); // little faster than typical
Transport::NPCQueues[follower->appearanceData.iNPC_ID] = queue;
Transport::NPCQueues[follower->id] = queue;
}
}

View File

@@ -1,10 +1,12 @@
#include "servers/CNShardServer.hpp"
#include "Racing.hpp"
#include "db/Database.hpp"
#include "servers/CNShardServer.hpp"
#include "NPCManager.hpp"
#include "PlayerManager.hpp"
#include "Missions.hpp"
#include "Items.hpp"
#include "db/Database.hpp"
#include "NPCManager.hpp"
using namespace Racing;
@@ -64,9 +66,23 @@ static void racingCancel(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp);
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC);
// we have to teleport the player back after this, otherwise they are unable to move
/*
* This request packet is used for both cancelling the race via the
* NPC at the start, *and* failing the race by running out of time.
* If the latter is to happen, the client disables movement until it
* receives a packet from the server that re-enables it.
*
* So, in order to prevent a potential softlock we respawn the player.
*/
WarpLocation* respawnLoc = PlayerManager::getRespawnPoint(plr);
if (respawnLoc != nullptr) {
PlayerManager::sendPlayerTo(sock, respawnLoc->x, respawnLoc->y, respawnLoc->z, respawnLoc->instanceID);
} else {
// fallback, just respawn the player in-place if no suitable point is found
PlayerManager::sendPlayerTo(sock, plr->x, plr->y, plr->z, plr->instanceID);
}
}
static void racingEnd(CNSocket* sock, CNPacketData* data) {
@@ -83,35 +99,43 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
return; // IZ not found
uint64_t now = getTime() / 1000;
EPInfo& epInfo = EPData[mapNum];
EPRace& epRace = EPRaces[sock];
int timeDiff = now - EPRaces[sock].startTime;
int score = 500 * EPRaces[sock].collectedRings.size() - 10 * timeDiff;
if (score < 0) score = 0; // lol
int fm = score * plr->level * (1.0f / 36) * 0.3f;
uint64_t now = getTime() / 1000;
int timeDiff = now - epRace.startTime;
int podsCollected = epRace.collectedRings.size();
int score = std::exp(
(epInfo.podFactor * podsCollected) / epInfo.maxPods
- (epInfo.timeFactor * timeDiff) / epInfo.maxTime
+ epInfo.scaleFactor);
score = (settings::IZRACESCORECAPPED && score > epInfo.maxScore) ? epInfo.maxScore : score;
int fm = (1.0 + std::exp(epInfo.scaleFactor - 1.0) * epInfo.podFactor * podsCollected) / epInfo.maxPods;
// we submit the ranking first...
Database::RaceRanking postRanking = {};
postRanking.EPID = EPData[mapNum].EPID;
postRanking.EPID = epInfo.EPID;
postRanking.PlayerID = plr->iID;
postRanking.RingCount = EPRaces[sock].collectedRings.size();
postRanking.RingCount = podsCollected;
postRanking.Score = score;
postRanking.Time = timeDiff;
postRanking.Timestamp = getTimestamp();
Database::postRaceRanking(postRanking);
// ...then we get the top ranking, which may or may not be what we just submitted
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(EPData[mapNum].EPID, plr->iID);
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(epInfo.EPID, plr->iID);
INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp);
// get rank scores and rewards
std::vector<int>* rankScores = &EPRewards[EPData[mapNum].EPID].first;
std::vector<int>* rankRewards = &EPRewards[EPData[mapNum].EPID].second;
std::vector<int>* rankScores = &EPRewards[epInfo.EPID].first;
std::vector<int>* rankRewards = &EPRewards[epInfo.EPID].second;
// top ranking
int maxRank = rankScores->size() - 1;
int topRank = 0;
while (rankScores->at(topRank) > topRankingPlayer.Score)
while (topRank < maxRank && rankScores->at(topRank) > topRankingPlayer.Score)
topRank++;
resp.iEPTopRank = topRank + 1;
@@ -121,7 +145,7 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
// this ranking
int rank = 0;
while (rankScores->at(rank) > postRanking.Score)
while (rank < maxRank && rankScores->at(rank) > postRanking.Score)
rank++;
resp.iEPRank = rank + 1;

View File

@@ -1,11 +1,17 @@
#pragma once
#include "core/Core.hpp"
#include <map>
#include <vector>
#include <set>
#include "servers/CNShardServer.hpp"
struct EPInfo {
int zoneX, zoneY, EPID, maxScore, maxTime;
// available through XDT (maxScore may be updated by drops)
int zoneX, zoneY, EPID, maxScore;
// available through drops
int maxTime, maxPods;
double scaleFactor, podFactor, timeFactor;
};
struct EPRace {

View File

@@ -1,4 +1,5 @@
#include "Rand.hpp"
#include "core/Core.hpp"
std::unique_ptr<std::mt19937> Rand::generator;
@@ -33,6 +34,58 @@ float Rand::randFloat() {
return Rand::randFloat(0.0f, 1.0f);
}
#define RANDBYTES 8
/*
* Cryptographically secure RNG. Borrowed from bcrypt_gensalt().
*/
uint64_t Rand::cryptoRand() {
uint8_t buf[RANDBYTES];
#ifdef _WIN32
HCRYPTPROV p;
// Acquire a crypt context for generating random bytes.
if (CryptAcquireContext(&p, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT) == FALSE) {
goto fail;
}
if (CryptGenRandom(p, RANDBYTES, (BYTE*)buf) == FALSE) {
goto fail;
}
if (CryptReleaseContext(p, 0) == FALSE) {
goto fail;
}
#else
int fd;
// Get random bytes on Unix/Linux.
fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) {
perror("open");
goto fail;
}
if (read(fd, buf, RANDBYTES) < RANDBYTES) {
perror("read");
close(fd);
goto fail;
}
close(fd);
#endif
return *(uint64_t*)buf;
fail:
std::cout << "[FATAL] Failed to generate cryptographic random number" << std::endl;
terminate(0);
/* not reached */
return 0;
}
void Rand::init(uint64_t seed) {
Rand::generator = std::make_unique<std::mt19937>(std::mt19937(seed));
}

View File

@@ -2,6 +2,7 @@
#include <random>
#include <memory>
#include <vector>
namespace Rand {
extern std::unique_ptr<std::mt19937> generator;
@@ -14,6 +15,8 @@ namespace Rand {
int32_t randWeighted(const std::vector<int32_t>& weights);
uint64_t cryptoRand();
float randFloat(float startInclusive, float endExclusive);
float randFloat(float endExclusive);
float randFloat();

View File

@@ -1,18 +1,14 @@
#include "TableData.hpp"
#include "NPCManager.hpp"
#include "Transport.hpp"
#include "Items.hpp"
#include "settings.hpp"
#include "Missions.hpp"
#include "Chunking.hpp"
#include "Nanos.hpp"
#include "Racing.hpp"
#include "Items.hpp"
#include "Vendors.hpp"
#include "Racing.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include "Eggs.hpp"
#include "JSON.hpp"
#include <fstream>
#include <sstream>
#include <cmath>
@@ -37,6 +33,22 @@ public:
const char *what() const throw() { return msg.c_str(); }
};
/*
* We must refuse to run if an invalid NPC type is found in the JSONs, especially
* the gruntwork file. If we were to just skip loading invalid NPCs, they would get
* silently dropped from the gruntwork file, which would be confusing in situations
* where a gruntwork file for the wrong game build was accidentally loaded.
*/
static void ensureValidNPCType(int type, std::string filename) {
// last known NPC type
int npcLimit = NPCManager::NPCData.back()["m_iNpcNumber"];
if (type > npcLimit) {
std::cout << "[FATAL] " << filename << " contains an invalid NPC type: " << type << std::endl;
exit(1);
}
}
/*
* Create a full and properly-paced path by interpolating between keyframes.
*/
@@ -215,18 +227,34 @@ static void loadXDT(json& xdtData) {
// load nano powers
json skills = xdtData["m_pSkillTable"]["m_pSkillData"];
for (json::iterator _skills = skills.begin(); _skills != skills.end(); _skills++) {
auto skills = _skills.value();
SkillData skillData = { skills["m_iSkillType"], skills["m_iTargetType"], skills["m_iBatteryDrainType"], skills["m_iEffectArea"] };
for (json::iterator _skill = skills.begin(); _skill != skills.end(); _skill++) {
auto skill = _skill.value();
SkillData skillData = {
skill["m_iSkillType"],
skill["m_iEffectTarget"],
skill["m_iEffectType"],
skill["m_iTargetType"],
skill["m_iBatteryDrainType"],
skill["m_iEffectArea"]
};
skillData.valueTypes[0] = skill["m_iValueA_Type"];
skillData.valueTypes[1] = skill["m_iValueB_Type"];
skillData.valueTypes[2] = skill["m_iValueC_Type"];
for (int i = 0; i < 4; i++) {
skillData.batteryUse[i] = skills["m_iBatteryDrainUse"][i];
skillData.durationTime[i] = skills["m_iDurationTime"][i];
skillData.powerIntensity[i] = skills["m_iValueA"][i];
}
Nanos::SkillTable[skills["m_iSkillNumber"]] = skillData;
skillData.batteryUse[i] = skill["m_iBatteryDrainUse"][i];
skillData.durationTime[i] = skill["m_iDurationTime"][i];
skillData.values[0][i] = skill["m_iValueA"][i];
skillData.values[1][i] = skill["m_iValueB"][i];
skillData.values[2][i] = skill["m_iValueC"][i];
}
std::cout << "[INFO] Loaded " << Nanos::SkillTable.size() << " nano skills" << std::endl;
Abilities::SkillTable[skill["m_iSkillNumber"]] = skillData;
}
std::cout << "[INFO] Loaded " << Abilities::SkillTable.size() << " nano skills" << std::endl;
// load EP data
json instances = xdtData["m_pInstanceTable"]["m_pInstanceData"];
@@ -298,10 +326,10 @@ static void loadPaths(json& pathData, int32_t* nextId) {
if (passedDistance >= SLIDER_GAP_SIZE) { // space them out uniformaly
passedDistance -= SLIDER_GAP_SIZE; // step down
// spawn a slider
Bus* slider = new Bus(point.x, point.y, point.z, 0, INSTANCE_OVERWORLD, 1, (*nextId)--);
NPCManager::NPCs[slider->appearanceData.iNPC_ID] = slider;
NPCManager::updateNPCPosition(slider->appearanceData.iNPC_ID, slider->x, slider->y, slider->z, INSTANCE_OVERWORLD, 0);
Transport::NPCQueues[slider->appearanceData.iNPC_ID] = route;
Bus* slider = new Bus(0, INSTANCE_OVERWORLD, 1, (*nextId)--);
NPCManager::NPCs[slider->id] = slider;
NPCManager::updateNPCPosition(slider->id, point.x, point.y, point.z, INSTANCE_OVERWORLD, 0);
Transport::NPCQueues[slider->id] = route;
}
// rotate
route.pop();
@@ -556,8 +584,17 @@ static void loadDrops(json& dropData) {
continue;
}
// time limit isn't stored in the XDT, so we include it in the reward table instead
Racing::EPData[EPMap].maxTime = race["TimeLimit"];
EPInfo& epInfo = Racing::EPData[EPMap];
// max score is specified in the XDT, but can be updated if specified in the drops JSON
epInfo.maxScore = (int)race["ScoreCap"];
// time limit and total pods are not stored in the XDT, so we include it in the drops JSON
epInfo.maxTime = (int)race["TimeLimit"];
epInfo.maxPods = (int)race["TotalPods"];
// IZ-specific calculated constants included in the drops JSON
epInfo.scaleFactor = (double)race["ScaleFactor"];
epInfo.podFactor = (double)race["PodFactor"];
epInfo.timeFactor = (double)race["TimeFactor"];
// score cutoffs
std::vector<int> rankScores;
@@ -573,7 +610,7 @@ static void loadDrops(json& dropData) {
if (rankScores.size() != 5 || rankScores.size() != rankRewards.size()) {
char buff[255];
sprintf(buff, "Race in EP %d doesn't have exactly 5 score/reward pairs", raceEPID);
snprintf(buff, 255, "Race in EP %d doesn't have exactly 5 score/reward pairs", raceEPID);
throw TableException(std::string(buff));
}
@@ -643,7 +680,7 @@ static void loadEggs(json& eggData, int32_t* nextId) {
int id = (*nextId)--;
uint64_t instanceID = egg.find("iMapNum") == egg.end() ? INSTANCE_OVERWORLD : (int)egg["iMapNum"];
Egg* addEgg = new Egg((int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, (int)egg["iType"], id, false);
Egg* addEgg = new Egg(instanceID, (int)egg["iType"], id, false);
NPCManager::NPCs[id] = addEgg;
eggCount++;
NPCManager::updateNPCPosition(id, (int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, 0);
@@ -662,6 +699,8 @@ static void loadEggs(json& eggData, int32_t* nextId) {
* Load gruntwork output, if it exists
*/
static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
if (gruntwork.is_null())
return;
try {
auto paths = gruntwork["paths"];
@@ -671,10 +710,10 @@ static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
std::vector<int32_t> targetIDs;
std::vector<int32_t> targetTypes; // target types are not exportable from gw, but load them anyway
std::vector<Vec3> pathPoints;
int speed = (int)path["iBaseSpeed"];
int taskID = (int)path["iTaskID"];
bool relative = (bool)path["bRelative"];
bool loop = (bool)path["bLoop"];
int speed = path.find("iBaseSpeed") == path.end() ? NPC_DEFAULT_SPEED : (int)path["iBaseSpeed"];
int taskID = path.find("iTaskID") == path.end() ? -1 : (int)path["iTaskID"];
bool relative = path.find("bRelative") == path.end() ? false : (bool)path["bRelative"];
bool loop = path.find("bLoop") == path.end() ? true : (bool)path["bLoop"]; // loop by default
// target IDs
for (json::iterator _tID = path["aNPCIDs"].begin(); _tID != path["aNPCIDs"].end(); _tID++)
@@ -711,8 +750,8 @@ static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
}
static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
if (gruntwork.is_null()) return;
if (gruntwork.is_null())
return;
try {
// skyway paths
@@ -737,7 +776,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
if (NPCManager::NPCs.find(npcID) == NPCManager::NPCs.end())
continue; // NPC not found
BaseNPC* npc = NPCManager::NPCs[npcID];
npc->appearanceData.iAngle = angle;
npc->angle = angle;
RunningNPCRotations[npcID] = angle;
}
@@ -750,8 +789,8 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
if (NPCManager::NPCs.find(npcID) == NPCManager::NPCs.end())
continue; // NPC not found
BaseNPC* npc = NPCManager::NPCs[npcID];
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->x, npc->y,
npc->z, instanceID, npc->appearanceData.iAngle);
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y,
npc->z, instanceID, npc->angle);
RunningNPCMapNumbers[npcID] = instanceID;
}
@@ -764,6 +803,8 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
int id = (*nextId)--;
uint64_t instanceID = mob.find("iMapNum") == mob.end() ? INSTANCE_OVERWORLD : (int)mob["iMapNum"];
ensureValidNPCType((int)mob["iNPCType"], settings::GRUNTWORKJSON);
if (NPCManager::NPCData[(int)mob["iNPCType"]]["m_iTeam"] == 2) {
npc = new Mob(mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iNPCType"],
NPCManager::NPCData[(int)mob["iNPCType"]], id);
@@ -771,18 +812,21 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
// re-enable respawning
((Mob*)npc)->summoned = false;
} else {
npc = new BaseNPC(mob["iX"], mob["iY"], mob["iZ"], mob["iAngle"], instanceID, mob["iNPCType"], id);
npc = new BaseNPC(mob["iAngle"], instanceID, mob["iNPCType"], id);
}
NPCManager::NPCs[npc->appearanceData.iNPC_ID] = npc;
RunningMobs[npc->appearanceData.iNPC_ID] = npc;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iAngle"]);
NPCManager::NPCs[npc->id] = npc;
RunningMobs[npc->id] = npc;
NPCManager::updateNPCPosition(npc->id, mob["iX"], mob["iY"], mob["iZ"], instanceID, mob["iAngle"]);
}
// mob groups
auto groups = gruntwork["groups"];
for (auto _group = groups.begin(); _group != groups.end(); _group++) {
auto leader = _group.value();
ensureValidNPCType((int)leader["iNPCType"], settings::GRUNTWORKJSON);
auto td = NPCManager::NPCData[(int)leader["iNPCType"]];
uint64_t instanceID = leader.find("iMapNum") == leader.end() ? INSTANCE_OVERWORLD : (int)leader["iMapNum"];
@@ -803,6 +847,9 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
int followerCount = 0;
for (json::iterator _fol = followers.begin(); _fol != followers.end(); _fol++) {
auto follower = _fol.value();
ensureValidNPCType((int)follower["iNPCType"], settings::GRUNTWORKJSON);
auto tdFol = NPCManager::NPCData[(int)follower["iNPCType"]];
Mob* tmpFol = new Mob((int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], leader["iAngle"], instanceID, follower["iNPCType"], tdFol, *nextId);
@@ -814,7 +861,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
tmpFol->offsetX = follower.find("iOffsetX") == follower.end() ? 0 : (int)follower["iOffsetX"];
tmpFol->offsetY = follower.find("iOffsetY") == follower.end() ? 0 : (int)follower["iOffsetY"];
tmpFol->groupLeader = tmp->appearanceData.iNPC_ID;
tmpFol->groupLeader = tmp->id;
tmp->groupMember[followerCount++] = *nextId;
(*nextId)--;
@@ -824,7 +871,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
std::cout << "[WARN] Mob group leader with ID " << *nextId << " has too many followers (" << followers.size() << ")\n";
}
RunningGroups[tmp->appearanceData.iNPC_ID] = tmp; // store as running
RunningGroups[tmp->id] = tmp; // store as running
}
auto eggs = gruntwork["eggs"];
@@ -833,7 +880,7 @@ static void loadGruntworkPost(json& gruntwork, int32_t* nextId) {
int id = (*nextId)--;
uint64_t instanceID = egg.find("iMapNum") == egg.end() ? INSTANCE_OVERWORLD : (int)egg["iMapNum"];
Egg* addEgg = new Egg((int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, (int)egg["iType"], id, false);
Egg* addEgg = new Egg(instanceID, (int)egg["iType"], id, false);
NPCManager::NPCs[id] = addEgg;
NPCManager::updateNPCPosition(id, (int)egg["iX"], (int)egg["iY"], (int)egg["iZ"], instanceID, 0);
RunningEggs[id] = addEgg;
@@ -859,16 +906,15 @@ static void loadNPCs(json& npcData) {
npcID += NPC_ID_OFFSET;
int instanceID = npc.find("iMapNum") == npc.end() ? INSTANCE_OVERWORLD : (int)npc["iMapNum"];
int type = (int)npc["iNPCType"];
if (NPCManager::NPCData[type].is_null()) {
std::cout << "[WARN] NPC type " << type << " not found; skipping (json#" << _npc.key() << ")" << std::endl;
continue;
}
ensureValidNPCType(type, settings::NPCJSON);
#ifdef ACADEMY
// do not spawn NPCs in the future
if (npc["iX"] > 512000 && npc["iY"] < 256000)
continue;
#endif
BaseNPC* tmp = new BaseNPC(npc["iX"], npc["iY"], npc["iZ"], npc["iAngle"], instanceID, type, npcID);
BaseNPC* tmp = new BaseNPC(npc["iAngle"], instanceID, type, npcID);
NPCManager::NPCs[npcID] = tmp;
NPCManager::updateNPCPosition(npcID, npc["iX"], npc["iY"], npc["iZ"], instanceID, npc["iAngle"]);
@@ -904,10 +950,9 @@ static void loadMobs(json& npcData, int32_t* nextId) {
int npcID = std::strtol(_npc.key().c_str(), nullptr, 10); // parse ID string to integer
npcID += MOB_ID_OFFSET;
int type = (int)npc["iNPCType"];
if (NPCManager::NPCData[type].is_null()) {
std::cout << "[WARN] NPC type " << type << " not found; skipping (json#" << _npc.key() << ")" << std::endl;
continue;
}
ensureValidNPCType(type, settings::MOBJSON);
auto td = NPCManager::NPCData[type];
uint64_t instanceID = npc.find("iMapNum") == npc.end() ? INSTANCE_OVERWORLD : (int)npc["iMapNum"];
@@ -935,7 +980,10 @@ static void loadMobs(json& npcData, int32_t* nextId) {
for (json::iterator _group = groupData.begin(); _group != groupData.end(); _group++) {
auto leader = _group.value();
int leadID = std::strtol(_group.key().c_str(), nullptr, 10); // parse ID string to integer
leadID += MOB_GROUP_ID_OFFSET;
ensureValidNPCType(leader["iNPCType"], settings::MOBJSON);
auto td = NPCManager::NPCData[(int)leader["iNPCType"]];
uint64_t instanceID = leader.find("iMapNum") == leader.end() ? INSTANCE_OVERWORLD : (int)leader["iMapNum"];
auto followers = leader["aFollowers"];
@@ -965,6 +1013,9 @@ static void loadMobs(json& npcData, int32_t* nextId) {
int followerCount = 0;
for (json::iterator _fol = followers.begin(); _fol != followers.end(); _fol++) {
auto follower = _fol.value();
ensureValidNPCType(follower["iNPCType"], settings::MOBJSON);
auto tdFol = NPCManager::NPCData[(int)follower["iNPCType"]];
Mob* tmpFol = new Mob((int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], leader["iAngle"], instanceID, follower["iNPCType"], tdFol, *nextId);
@@ -973,7 +1024,7 @@ static void loadMobs(json& npcData, int32_t* nextId) {
tmpFol->offsetX = follower.find("iOffsetX") == follower.end() ? 0 : (int)follower["iOffsetX"];
tmpFol->offsetY = follower.find("iOffsetY") == follower.end() ? 0 : (int)follower["iOffsetY"];
tmpFol->groupLeader = tmp->appearanceData.iNPC_ID;
tmpFol->groupLeader = tmp->id;
tmp->groupMember[followerCount++] = *nextId;
(*nextId)--;
@@ -1070,19 +1121,39 @@ void TableData::init() {
};
// load JSON data into tables
std::ifstream fstream;
for (int i = 0; i < 7; i++) {
std::pair<json*, std::string>& table = tables[i];
fstream.open(settings::TDATADIR + table.second); // open file
if (!fstream.fail()) {
fstream >> *table.first; // load file contents into table
} else {
if (table.first != &gruntwork) { // gruntwork isn't critical
std::cerr << "[FATAL] Critical tdata file missing: " << settings::TDATADIR << table.second << std::endl;
// scope for fstream
{
std::ifstream fstream;
fstream.open(settings::TDATADIR + "/" + table.second); // open file
// did we fail to open the file?
if (fstream.fail()) {
// gruntwork isn't critical
if (table.first == &gruntwork)
continue;
std::cerr << "[FATAL] Critical tdata file missing: " << table.second << std::endl;
exit(1);
}
// is the file empty?
if (fstream.peek() == std::ifstream::traits_type::eof()) {
// tolerate empty gruntwork file
if (table.first == &gruntwork) {
std::cout << "[WARN] The gruntwork file is empty" << std::endl;
continue;
}
std::cerr << "[FATAL] Critical tdata file is empty: " << table.second << std::endl;
exit(1);
}
// load file contents into table
fstream >> *table.first;
}
fstream.close();
// patching: load each patch directory specified in the config file
@@ -1097,11 +1168,11 @@ void TableData::init() {
std::string patchModuleName = *it;
std::string patchFile = settings::PATCHDIR + patchModuleName + "/" + table.second;
try {
std::ifstream fstream;
fstream.open(patchFile);
fstream >> patch; // load into temporary json object
std::cout << "[INFO] Patching " << patchFile << std::endl;
patchJSON(table.first, &patch); // patch
fstream.close();
} catch (const std::exception& err) {
// no-op
}
@@ -1127,7 +1198,7 @@ void TableData::init() {
* Write gruntwork output to file
*/
void TableData::flush() {
std::ofstream file(settings::TDATADIR + settings::GRUNTWORKJSON);
std::ofstream file(settings::TDATADIR + "/" + settings::GRUNTWORKJSON);
json gruntwork;
for (auto& pair : RunningSkywayRoutes) {
@@ -1176,7 +1247,7 @@ void TableData::flush() {
continue;
int x, y, z;
if (npc->type == EntityType::MOB) {
if (npc->kind == EntityKind::MOB) {
Mob *m = (Mob*)npc;
x = m->spawnX;
y = m->spawnY;
@@ -1188,13 +1259,13 @@ void TableData::flush() {
}
// NOTE: this format deviates slightly from the one in mobs.json
mob["iNPCType"] = (int)npc->appearanceData.iNPCType;
mob["iNPCType"] = (int)npc->type;
mob["iX"] = x;
mob["iY"] = y;
mob["iZ"] = z;
mob["iMapNum"] = MAPNUM(npc->instanceID);
// this is a bit imperfect, since this is a live angle, not a spawn angle so it'll change often, but eh
mob["iAngle"] = npc->appearanceData.iAngle;
mob["iAngle"] = npc->angle;
// it's called mobs, but really it's everything
gruntwork["mobs"].push_back(mob);
@@ -1209,19 +1280,19 @@ void TableData::flush() {
int x, y, z;
std::vector<Mob*> followers;
if (npc->type == EntityType::MOB) {
if (npc->kind == EntityKind::MOB) {
Mob* m = (Mob*)npc;
x = m->spawnX;
y = m->spawnY;
z = m->spawnZ;
if (m->groupLeader != m->appearanceData.iNPC_ID) { // make sure this is a leader
if (m->groupLeader != m->id) { // make sure this is a leader
std::cout << "[WARN] Non-leader mob found in running groups; ignoring\n";
continue;
}
// add follower data to vector; go until OOB or until follower ID is 0
for (int i = 0; i < 4 && m->groupMember[i] > 0; i++) {
if (NPCManager::NPCs.find(m->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[m->groupMember[i]]->type != EntityType::MOB) {
if (NPCManager::NPCs.find(m->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[m->groupMember[i]]->kind != EntityKind::MOB) {
std::cout << "[WARN] Follower with ID " << m->groupMember[i] << " not found; skipping\n";
continue;
}
@@ -1235,13 +1306,13 @@ void TableData::flush() {
}
// NOTE: this format deviates slightly from the one in mobs.json
mob["iNPCType"] = (int)npc->appearanceData.iNPCType;
mob["iNPCType"] = (int)npc->type;
mob["iX"] = x;
mob["iY"] = y;
mob["iZ"] = z;
mob["iMapNum"] = MAPNUM(npc->instanceID);
// this is a bit imperfect, since this is a live angle, not a spawn angle so it'll change often, but eh
mob["iAngle"] = npc->appearanceData.iAngle;
mob["iAngle"] = npc->angle;
// followers
while (followers.size() > 0) {
@@ -1250,7 +1321,7 @@ void TableData::flush() {
// populate JSON entry
json fol;
fol["iNPCType"] = follower->appearanceData.iNPCType;
fol["iNPCType"] = follower->type;
fol["iOffsetX"] = follower->offsetX;
fol["iOffsetY"] = follower->offsetY;
@@ -1275,7 +1346,7 @@ void TableData::flush() {
int mapnum = MAPNUM(npc->instanceID);
if (mapnum != 0)
egg["iMapNum"] = mapnum;
egg["iType"] = npc->appearanceData.iNPCType;
egg["iType"] = npc->type;
gruntwork["eggs"].push_back(egg);
}

View File

@@ -1,8 +1,13 @@
#pragma once
#include <map>
#include "JSON.hpp"
#include "NPCManager.hpp"
#include "Entities.hpp"
#include "Transport.hpp"
#include <map>
#include <unordered_map>
#include <vector>
// these are added to the NPC's static key to avoid collisions
const int NPC_ID_OFFSET = 1;

View File

@@ -1,5 +1,9 @@
#include "Trading.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "db/Database.hpp"
using namespace Trading;
@@ -182,6 +186,36 @@ static void tradeOfferRefusal(CNSocket* sock, CNPacketData* data) {
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
}
static void tradeOfferCancel(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_OFFER_CANCEL* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_CANCEL*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_CANCEL, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_CANCEL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_CANCEL));
}
static void tradeOfferAbort(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_OFFER_ABORT* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_ABORT*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_ABORT, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_ABORT));
}
static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_CONFIRM* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM*)data->buf;
@@ -205,6 +239,13 @@ static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
resp.iID_Request = plr->iID;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
// both players are no longer trading
plr->isTrading = false;
plr2->isTrading = false;
plr->isTradeConfirm = false;
plr2->isTradeConfirm = false;
return;
}
@@ -247,14 +288,23 @@ static void tradeConfirm(CNSocket* sock, CNPacketData* data) {
otherSock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC));
} else {
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, resp);
resp.iID_Request = plr2->iID;
resp.iID_Request = plr->iID;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
resp.iID_Request = plr->iID;
resp.iID_Request = plr2->iID;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT));
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Trade Failed";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 3;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
otherSock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
return;
}
Database::commitTrade(plr, plr2);
}
static void tradeConfirmCancel(CNSocket* sock, CNPacketData* data) {
@@ -301,8 +351,10 @@ static void tradeRegisterItem(CNSocket* sock, CNPacketData* data) {
return;
Player* plr = PlayerManager::getPlayer(sock);
Player* plr2 = PlayerManager::getPlayer(otherSock);
plr->Trade[pacdat->Item.iSlotNum] = pacdat->Item;
plr->isTradeConfirm = false;
plr2->isTradeConfirm = false;
// since you can spread items like gumballs over multiple slots, we need to count them all
// to make sure the inventory shows the right value during trade.
@@ -343,7 +395,9 @@ static void tradeUnregisterItem(CNSocket* sock, CNPacketData* data) {
return;
Player* plr = PlayerManager::getPlayer(sock);
Player* plr2 = PlayerManager::getPlayer(otherSock);
plr->isTradeConfirm = false;
plr2->isTradeConfirm = false;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC, resp);
resp.iID_Request = pacdat->iID_Request;
@@ -385,6 +439,10 @@ static void tradeRegisterCash(CNSocket* sock, CNPacketData* data) {
if (otherSock == nullptr)
return;
Player* plr2 = PlayerManager::getPlayer(otherSock);
plr->isTradeConfirm = false;
plr2->isTradeConfirm = false;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
@@ -402,6 +460,8 @@ void Trading::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER, tradeOffer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT, tradeOfferAccept);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL, tradeOfferRefusal);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_CANCEL, tradeOfferCancel);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ABORT, tradeOfferAbort);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM, tradeConfirm);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL, tradeConfirmCancel);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER, tradeRegisterItem);

View File

@@ -1,7 +1,5 @@
#pragma once
#include "Items.hpp"
namespace Trading {
void init();
}

View File

@@ -1,9 +1,12 @@
#include "Transport.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Nanos.hpp"
#include "Transport.hpp"
#include "TableData.hpp"
#include "Combat.hpp"
#include "Entities.hpp"
#include "NPCManager.hpp"
#include "MobAI.hpp"
#include <unordered_map>
@@ -165,9 +168,9 @@ static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
if (target == nullptr)
return;
// we warped; update position and chunks
Chunking::updateEntityChunk({sock}, plr->chunkPos, std::make_tuple(0, 0, 0)); // force player to reload chunks
PlayerManager::updatePlayerPosition(sock, target->x, target->y, target->z, INSTANCE_OVERWORLD, plr->angle);
PlayerManager::updatePlayerPositionForWarp(sock, target->x, target->y, target->z, INSTANCE_OVERWORLD);
}
void Transport::testMssRoute(CNSocket *sock, std::vector<Vec3>* route) {
@@ -257,13 +260,13 @@ static void stepNPCPathing() {
}
// skip if not simulating mobs
if (npc->type == EntityType::MOB && !MobAI::simulateMobs) {
if (npc->kind == EntityKind::MOB && !MobAI::simulateMobs) {
it++;
continue;
}
// do not roam if not roaming
if (npc->type == EntityType::MOB && ((Mob*)npc)->state != MobState::ROAMING) {
if (npc->kind == EntityKind::MOB && ((Mob*)npc)->state != AIState::ROAMING) {
it++;
continue;
}
@@ -276,15 +279,15 @@ static void stepNPCPathing() {
int distanceBetween = hypot(dXY, point.z - npc->z); // total distance
// update NPC location to update viewables
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, point.x, point.y, point.z, npc->instanceID, npc->appearanceData.iAngle);
NPCManager::updateNPCPosition(npc->id, point.x, point.y, point.z, npc->instanceID, npc->angle);
// TODO: move walking logic into Entity stack
switch (npc->type) {
case EntityType::BUS:
switch (npc->kind) {
case EntityKind::BUS:
INITSTRUCT(sP_FE2CL_TRANSPORTATION_MOVE, busMove);
busMove.eTT = 3;
busMove.iT_ID = npc->appearanceData.iNPC_ID;
busMove.iT_ID = npc->id;
busMove.iMoveStyle = 0; // ???
busMove.iToX = point.x;
busMove.iToY = point.y;
@@ -293,12 +296,12 @@ static void stepNPCPathing() {
NPCManager::sendToViewable(npc, &busMove, P_FE2CL_TRANSPORTATION_MOVE, sizeof(sP_FE2CL_TRANSPORTATION_MOVE));
break;
case EntityType::MOB:
case EntityKind::MOB:
MobAI::incNextMovement((Mob*)npc);
/* fallthrough */
default:
INITSTRUCT(sP_FE2CL_NPC_MOVE, move);
move.iNPC_ID = npc->appearanceData.iNPC_ID;
move.iNPC_ID = npc->id;
move.iMoveStyle = 0; // ???
move.iToX = point.x;
move.iToY = point.y;
@@ -385,7 +388,7 @@ NPCPath* Transport::findApplicablePath(int32_t id, int32_t type, int taskID) {
void Transport::constructPathNPC(int32_t id, NPCPath* path) {
BaseNPC* npc = NPCManager::NPCs[id];
if (npc->type == EntityType::MOB)
if (npc->kind == EntityKind::MOB)
((Mob*)(npc))->staticPath = true;
npc->loopingPath = path->isLoop;

View File

@@ -1,8 +1,11 @@
#pragma once
#include "servers/CNShardServer.hpp"
#include "core/Core.hpp"
#include <unordered_map>
#include <map>
#include <vector>
#include <queue>
const int SLIDER_SPEED = 1200;
const int SLIDER_STOP_TICKS = 16;

View File

@@ -1,6 +1,14 @@
#include "Vendors.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Rand.hpp"
// 7 days
#define VEHICLE_EXPIRY_DURATION 604800
using namespace Vendors;
std::map<int32_t, std::vector<VendorListing>> Vendors::VendorTables;
@@ -13,8 +21,25 @@ static void vendorBuy(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, failResp);
failResp.iErrorCode = 0;
Items::Item* itemDat = Items::getItemData(req->Item.iID, req->Item.iType);
if (req->iVendorID != req->iNPC_ID || Vendors::VendorTables.find(req->iVendorID) == Vendors::VendorTables.end()) {
std::cout << "[WARN] Vendor with ID " << req->iVendorID << " mismatched or not found (buy)" << std::endl;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
return;
}
std::vector<VendorListing>* listings = &Vendors::VendorTables[req->iVendorID];
VendorListing reqItem;
reqItem.id = req->Item.iID;
reqItem.type = req->Item.iType;
reqItem.sort = 0; // just to be safe
if (std::find(listings->begin(), listings->end(), reqItem) == listings->end()) { // item not found in listing
std::cout << "[WARN] Player " << PlayerManager::getPlayerName(plr) << " tried to buy an item that wasn't on sale" << std::endl;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
return;
}
Items::Item* itemDat = Items::getItemData(req->Item.iID, req->Item.iType);
if (itemDat == nullptr) {
std::cout << "[WARN] Item id " << req->Item.iID << " with type " << req->Item.iType << " not found (buy)" << std::endl;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
@@ -36,8 +61,8 @@ static void vendorBuy(CNSocket* sock, CNPacketData* data) {
// if vehicle
if (req->Item.iType == 10) {
// set time limit: current time + 7days
req->Item.iTimeLimit = getTimestamp() + 604800;
// set time limit: current time + expiry duration
req->Item.iTimeLimit = getTimestamp() + VEHICLE_EXPIRY_DURATION;
}
if (slot != req->iInvenSlotNum) {
@@ -202,17 +227,25 @@ static void vendorTable(CNSocket* sock, CNPacketData* data) {
if (req->iVendorID != req->iNPC_ID || Vendors::VendorTables.find(req->iVendorID) == Vendors::VendorTables.end())
return;
std::vector<VendorListing> listings = Vendors::VendorTables[req->iVendorID];
std::vector<VendorListing>& listings = Vendors::VendorTables[req->iVendorID];
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC, resp);
for (int i = 0; i < (int)listings.size() && i < 20; i++) { // 20 is the max
sItemBase base;
base.iID = listings[i].iID;
base.iOpt = 0;
base.iTimeLimit = 0;
sItemBase base = {};
base.iID = listings[i].id;
base.iType = listings[i].type;
/*
* Set vehicle expiry value.
*
* Note: sItemBase.iTimeLimit in the context of vendor listings contains
* a duration, unlike in most other contexts where it contains the
* expiration timestamp.
*/
if (listings[i].type == 10)
base.iTimeLimit = VEHICLE_EXPIRY_DURATION;
sItemVendor vItem;
vItem.item = base;
vItem.iSortNum = listings[i].sort;

View File

@@ -1,13 +1,17 @@
#pragma once
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "Items.hpp"
#include "PlayerManager.hpp"
#include <vector>
#include <map>
struct VendorListing {
int sort, type, iID;
int sort, type, id;
// when validating a listing, we don't really care about the sorting index
bool operator==(const VendorListing& other) const {
return type == other.type && id == other.id;
}
};
namespace Vendors {

View File

@@ -41,7 +41,8 @@ int CNSocketEncryption::xorData(uint8_t* buffer, uint8_t* key, int size) {
uint64_t CNSocketEncryption::createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2) {
uint64_t num = (uint64_t)(iv1 + 1);
uint64_t num2 = (uint64_t)(iv2 + 1);
uint64_t dEKey = (uint64_t)(*(uint64_t*)&defaultKey[0]);
uint64_t dEKey;
memcpy(&dEKey, defaultKey, sizeof(dEKey));
return dEKey * (uTime * num * num2);
}
@@ -65,7 +66,7 @@ CNPacketData::CNPacketData(void *b, uint32_t t, int l, int trnum, void *trs):
// ========================================================[[ CNSocket ]]========================================================
CNSocket::CNSocket(SOCKET s, struct sockaddr_in &addr, PacketHandler ph): sock(s), sockaddr(addr), pHandler(ph) {
EKey = (uint64_t)(*(uint64_t*)&CNSocketEncryption::defaultKey[0]);
memcpy(&EKey, CNSocketEncryption::defaultKey, sizeof(EKey));
}
bool CNSocket::sendData(uint8_t* data, int size) {
@@ -109,7 +110,11 @@ bool CNSocket::isAlive() {
}
void CNSocket::kill() {
if (!alive)
return;
alive = false;
#ifdef _WIN32
shutdown(sock, SD_BOTH);
closesocket(sock);
@@ -241,9 +246,10 @@ void CNSocket::step() {
if (readSize <= 0) {
// we aren't reading a packet yet, try to start looking for one
int recved = recv(sock, (buffer_t*)readBuffer, sizeof(int32_t), 0);
if (recved == 0) {
// the socket was closed normally
if (recved >= 0 && recved < sizeof(int32_t)) {
// too little data for readSize or the socket was closed normally (when 0 bytes were read)
kill();
return;
} else if (!SOCKETERROR(recved)) {
// we got our packet size!!!!
readSize = *((int32_t*)readBuffer);
@@ -264,11 +270,12 @@ void CNSocket::step() {
}
if (readSize > 0 && readBufferIndex < readSize) {
// read until the end of the packet! (or at least try too)
// read until the end of the packet (or at least try to)
int recved = recv(sock, (buffer_t*)(readBuffer + readBufferIndex), readSize - readBufferIndex, 0);
if (recved == 0) {
// the socket was closed normally
kill();
return;
} else if (!SOCKETERROR(recved))
readBufferIndex += recved;
else if (OF_ERRNO != OF_EWOULD) {
@@ -411,9 +418,9 @@ void CNServer::addPollFD(SOCKET s) {
fds.push_back({s, POLLIN});
}
void CNServer::removePollFD(int i) {
void CNServer::removePollFD(int fd) {
auto it = fds.begin();
while (it != fds.end() && it->fd != fds[i].fd)
while (it != fds.end() && it->fd != fd)
it++;
assert(it != fds.end());
@@ -458,7 +465,7 @@ void CNServer::start() {
if (!setSockNonblocking(sock, newConnectionSocket))
continue;
std::cout << "New connection! " << inet_ntoa(address.sin_addr) << std::endl;
std::cout << "New " << serverType << " connection! " << inet_ntoa(address.sin_addr) << std::endl;
addPollFD(newConnectionSocket);
@@ -473,10 +480,15 @@ void CNServer::start() {
} else {
std::lock_guard<std::mutex> lock(activeCrit); // protect operations on connections
// halt packet handling if server is shutting down
if (!active)
return;
// player sockets
if (connections.find(fds[i].fd) == connections.end()) {
std::cout << "[WARN] Event on non-existant socket?" << std::endl;
continue; // just to be safe
std::cout << "[FATAL] Event on non-existent socket: " << fds[i].fd << std::endl;
assert(0);
/* not reached */
}
CNSocket* cSock = connections[fds[i].fd];
@@ -485,22 +497,29 @@ void CNServer::start() {
if (fds[i].revents & ~POLLIN)
cSock->kill();
if (cSock->isAlive()) {
if (cSock->isAlive())
cSock->step();
} else {
killConnection(cSock);
connections.erase(fds[i].fd);
delete cSock;
removePollFD(i);
// a new entry was moved to this position, so we check it again
i--;
}
}
}
onStep();
// clean up dead connection sockets
auto it = connections.begin();
while (it != connections.end()) {
CNSocket *cSock = it->second;
if (!cSock->isAlive()) {
killConnection(cSock);
it = connections.erase(it);
removePollFD(cSock->sock);
delete cSock;
} else {
it++;
}
}
}
}

View File

@@ -93,7 +93,7 @@ inline constexpr bool isOutboundPacketID(uint32_t id) {
// overflow-safe validation of variable-length packets
// for outbound packets
inline constexpr bool validOutVarPacket(size_t base, int32_t npayloads, size_t plsize) {
inline constexpr bool validOutVarPacket(size_t base, size_t npayloads, size_t plsize) {
// check for multiplication overflow
if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
return false;
@@ -110,7 +110,7 @@ inline constexpr bool validOutVarPacket(size_t base, int32_t npayloads, size_t p
}
// for inbound packets
inline constexpr bool validInVarPacket(size_t base, int32_t npayloads, size_t plsize, size_t datasize) {
inline constexpr bool validInVarPacket(size_t base, size_t npayloads, size_t plsize, size_t datasize) {
// check for multiplication overflow
if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
return false;
@@ -230,6 +230,7 @@ protected:
const size_t STARTFDSCOUNT = 8; // number of initial PollFD slots
std::vector<PollFD> fds;
std::string serverType = "invalid";
SOCKET sock;
uint16_t port;
socklen_t addressSize;

View File

@@ -1,27 +1,45 @@
#include "core/CNShared.hpp"
#if defined(__MINGW32__) && !defined(_GLIBCXX_HAS_GTHREADS)
#include "mingw/mingw.mutex.h"
#else
#include <mutex>
#endif
std::map<int64_t, Player> CNSharedData::players;
std::mutex playerCrit;
static std::unordered_map<int64_t, LoginMetadata*> logins;
static std::mutex mtx;
void CNSharedData::setPlayer(int64_t sk, Player& plr) {
std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
void CNShared::storeLoginMetadata(int64_t sk, LoginMetadata *lm) {
std::lock_guard<std::mutex> lock(mtx);
players[sk] = plr;
// take ownership of connection data
logins[sk] = lm;
}
Player CNSharedData::getPlayer(int64_t sk) {
std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
LoginMetadata* CNShared::getLoginMetadata(int64_t sk) {
std::lock_guard<std::mutex> lock(mtx);
return players[sk];
// fail if the key isn't found
if (logins.find(sk) == logins.end())
return nullptr;
// transfer ownership of connection data to shard
LoginMetadata *lm = logins[sk];
logins.erase(sk);
return lm;
}
void CNSharedData::erasePlayer(int64_t sk) {
std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
void CNShared::pruneLoginMetadata(CNServer *serv, time_t currTime) {
std::lock_guard<std::mutex> lock(mtx);
players.erase(sk);
auto it = logins.begin();
while (it != logins.end()) {
auto& sk = it->first;
auto& lm = it->second;
if (currTime > lm->timestamp + CNSHARED_TIMEOUT) {
std::cout << "[WARN] Pruning hung connection attempt" << std::endl;
// deallocate object and remove map entry
delete logins[sk];
it = logins.erase(it); // skip the invalidated iterator
} else {
it++;
}
}
}

View File

@@ -10,11 +10,20 @@
#include "Player.hpp"
namespace CNSharedData {
// serialkey corresponds to player data
extern std::map<int64_t, Player> players;
/*
* Connecions time out after 5 minutes, checked every 30 seconds.
*/
#define CNSHARED_TIMEOUT 300000
#define CNSHARED_PERIOD 30000
void setPlayer(int64_t sk, Player& plr);
Player getPlayer(int64_t sk);
void erasePlayer(int64_t sk);
struct LoginMetadata {
uint64_t FEKey;
int32_t playerId;
time_t timestamp;
};
namespace CNShared {
void storeLoginMetadata(int64_t sk, LoginMetadata *lm);
LoginMetadata* getLoginMetadata(int64_t sk);
void pruneLoginMetadata(CNServer *serv, time_t currTime);
}

View File

@@ -23,6 +23,7 @@
#include <string>
#include <locale>
#include <codecvt>
#include <tuple>
// yes this is ugly, but this is needed to zero out the memory so we don't have random stackdata in our structs.
#define INITSTRUCT(T, x) T x; \
@@ -39,11 +40,9 @@
// wrapper for U16toU8
#define ARRLEN(x) (sizeof(x)/sizeof(*x))
#define AUTOU8(x) std::string((char*)x, ARRLEN(x))
#define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x))
// typedef for chunk position tuple
typedef std::tuple<int, int, uint64_t> ChunkPos;
// TODO: rewrite U16toU8 & U8toU16 to not use codecvt
std::string U16toU8(char16_t* src, size_t max);
@@ -52,13 +51,15 @@ time_t getTime();
time_t getTimestamp();
void terminate(int);
// The PROTOCOL_VERSION definition is defined by the build system.
// The PROTOCOL_VERSION definition can be defined by the build system.
#if !defined(PROTOCOL_VERSION)
#define PROTOCOL_VERSION 104
#endif
#if PROTOCOL_VERSION == 104
#include "structs/0104.hpp"
#elif PROTOCOL_VERSION == 728
#include "structs/0728.hpp"
#elif PROTOCOL_VERSION == 104
#include "structs/0104.hpp"
#elif PROTOCOL_VERSION == 1013
#include "structs/1013.hpp"
#else

View File

@@ -10,63 +10,44 @@ const float CN_EP_RANK_4 = 0.3f;
const float CN_EP_RANK_5 = 0.29f;
// methods of finding players for GM commands
enum eCN_GM_TargetSearchBy {
eCN_GM_TargetSearchBy__PC_ID, // player id
eCN_GM_TargetSearchBy__PC_Name, // firstname, lastname
eCN_GM_TargetSearchBy__PC_UID // account id
enum class eCN_GM_TargetSearchBy {
PC_ID, // player id
PC_Name, // firstname, lastname
PC_UID // account id
};
enum eCN_GM_TeleportType {
eCN_GM_TeleportMapType__XYZ,
eCN_GM_TeleportMapType__MapXYZ,
eCN_GM_TeleportMapType__MyLocation,
eCN_GM_TeleportMapType__SomeoneLocation,
eCN_GM_TeleportMapType__Unstick
enum class eCN_GM_TeleportType {
XYZ,
MapXYZ,
MyLocation,
SomeoneLocation,
Unstick
};
// nano powers
enum class eTaskTypeProperty {
None = -1,
Talk = 1,
GotoLocation = 2,
UseItems = 3,
Delivery = 4,
Defeat = 5,
EscortDefence = 6,
Max = 7
};
enum class ePCRegenType {
None,
Xcom,
Here,
HereByPhoenix,
HereByPhoenixGroup,
Unstick,
HereByPhoenixItem,
End
};
// nano power flags
enum {
EST_NONE = 0,
EST_DAMAGE = 1,
EST_HEAL_HP = 2,
EST_KNOCKDOWN = 3,
EST_SLEEP = 4,
EST_SNARE = 5,
EST_HEAL_STAMINA = 6,
EST_STAMINA_SELF = 7,
EST_STUN = 8,
EST_WEAPONSLOW = 9,
EST_JUMP = 10,
EST_RUN = 11,
EST_STEALTH = 12,
EST_SWIM = 13,
EST_MINIMAPENEMY = 14,
EST_MINIMAPTRESURE = 15,
EST_PHOENIX = 16,
EST_PROTECTBATTERY = 17,
EST_PROTECTINFECTION = 18,
EST_REWARDBLOB = 19,
EST_REWARDCASH = 20,
EST_BATTERYDRAIN = 21,
EST_CORRUPTIONATTACK = 22,
EST_INFECTIONDAMAGE = 23,
EST_KNOCKBACK = 24,
EST_FREEDOM = 25,
EST_PHOENIX_GROUP = 26,
EST_RECALL = 27,
EST_RECALL_GROUP = 28,
EST_RETROROCKET_SELF = 29,
EST_BLOODSUCKING = 30,
EST_BOUNDINGBALL = 31,
EST_INVULNERABLE = 32,
EST_NANOSTIMPAK = 33,
EST_RETURNHOMEHEAL = 34,
EST_BUFFHEAL = 35,
EST_EXTRABANK = 36,
EST__END = 37,
EST_CORRUPTIONATTACKWIN = 38,
EST_CORRUPTIONATTACKLOSE = 39,
ECSB_NONE = 0,
ECSB_UP_MOVE_SPEED = 1,
ECSB_UP_SWIM_SPEED = 2,
@@ -96,6 +77,27 @@ enum {
ECSTB__END = 26,
};
enum {
ETBU_NONE = 0,
ETBU_ADD = 1,
ETBU_DEL = 2,
ETBU_CHANGE = 3,
ETBU__END = 4,
};
enum {
ETBT_NONE = 0,
ETBT_NANO = 1,
ETBT_GROUPNANO = 2,
ETBT_SHINY = 3,
ETBT_LANDEFFECT = 4,
ETBT_ITEM = 5,
ETBT_CASHITEM = 6,
ETBT__END = 7,
ETBT_SKILL = 1,
ETBT_GROUPSKILL = 2
};
enum {
SUCC = 1,
FAIL = 0,

View File

@@ -47,8 +47,8 @@ struct PacketDesc {
* really should.
*/
struct sGM_PVPTarget {
uint32_t eCT;
uint32_t iID;
uint32_t eCT;
};
struct sSkillResult_Leech {

View File

@@ -5,7 +5,7 @@
#include <string>
#include <vector>
#define DATABASE_VERSION 3
#define DATABASE_VERSION 5
namespace Database {
@@ -41,18 +41,28 @@ namespace Database {
uint64_t Timestamp;
};
void init();
void open();
void close();
void findAccount(Account* account, std::string login);
// returns ID, 0 if something failed
// return ID, 0 if something failed
int getAccountIdForPlayer(int playerId);
int addAccount(std::string login, std::string password);
void updateAccountLevel(int accountId, int accountLevel);
// return true if cookie is valid for the account.
// invalidates the stored cookie afterwards
bool checkCookie(int accountId, const char *cookie);
// interface for the /ban command
bool banPlayer(int playerId, std::string& reason);
bool unbanPlayer(int playerId);
void updateSelected(int accountId, int playerId);
void updateSelected(int accountId, int slot);
void updateSelectedByPlayerId(int accountId, int playerId);
bool validateCharacter(int characterID, int userID);
bool isNameFree(std::string firstName, std::string lastName);
@@ -78,7 +88,9 @@ namespace Database {
// getting players
void getPlayer(Player* plr, int id);
bool _updatePlayer(Player *player);
void updatePlayer(Player *player);
void commitTrade(Player *plr1, Player *plr2);
// buddies
int getNumBuddies(Player* player);
@@ -98,7 +110,7 @@ namespace Database {
void deleteEmailAttachments(int playerID, int index, int slot);
void deleteEmails(int playerID, int64_t* indices);
int getNextEmailIndex(int playerID);
bool sendEmail(EmailData* data, std::vector<sItemBase> attachments);
bool sendEmail(EmailData* data, std::vector<sItemBase> attachments, Player *sender);
// racing
RaceRanking getTopRaceRanking(int epID, int playerID);

View File

@@ -152,7 +152,8 @@ void Database::updateEmailContent(EmailData* data) {
sqlite3_step(stmt);
int attachmentsCount = sqlite3_column_int(stmt, 0);
data->ItemFlag = (data->Taros > 0 || attachmentsCount > 0) ? 1 : 0; // set attachment flag dynamically
// set attachment flag dynamically
data->ItemFlag = (data->Taros > 0 || attachmentsCount > 0) ? 1 : 0;
sqlite3_finalize(stmt);
@@ -265,7 +266,7 @@ int Database::getNextEmailIndex(int playerID) {
return (index > 0 ? index + 1 : 1);
}
bool Database::sendEmail(EmailData* data, std::vector<sItemBase> attachments) {
bool Database::sendEmail(EmailData* data, std::vector<sItemBase> attachments, Player *sender) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
@@ -330,6 +331,13 @@ bool Database::sendEmail(EmailData* data, std::vector<sItemBase> attachments) {
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
if (!_updatePlayer(sender)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return false;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
return true;
}

View File

@@ -9,6 +9,37 @@
std::mutex dbCrit;
sqlite3 *db;
/*
* When migrating from DB version 3 to 4, we change the username column
* to be case-insensitive. This function ensures there aren't any
* duplicates, e.g. username and USERNAME, before doing the migration.
* I handled this in the code itself rather than the migration file just so
* we can have a more detailed error message than what SQLite provides.
*/
static void checkCaseSensitiveDupes() {
const char* sql = "SELECT Login, COUNT(*) FROM Accounts GROUP BY LOWER(Login) HAVING COUNT(*) > 1;";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
int stat = sqlite3_step(stmt);
if (stat == SQLITE_DONE) {
// no rows returned, so we're good
sqlite3_finalize(stmt);
return;
} else if (stat != SQLITE_ROW) {
std::cout << "[FATAL] Failed to check for duplicate accounts: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
exit(1);
}
std::cout << "[FATAL] Case-sensitive duplicates detected in the Login column." << std::endl;
std::cout << "Either manually delete/rename the offending accounts, or run the pruning script:" << std::endl;
std::cout << "https://github.com/OpenFusionProject/scripts/tree/main/db_migration/caseinsens.py" << std::endl;
sqlite3_finalize(stmt);
exit(1);
}
static void createMetaTable() {
std::lock_guard<std::mutex> lock(dbCrit); // XXX
@@ -68,7 +99,7 @@ static void checkMetaTable() {
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (sqlite3_step(stmt) != SQLITE_ROW) {
std::cout << "[FATAL] Failed to check meta table" << sqlite3_errmsg(db) << std::endl;
std::cout << "[FATAL] Failed to check meta table: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
exit(1);
}
@@ -143,6 +174,10 @@ static void checkMetaTable() {
}
while (dbVersion != DATABASE_VERSION) {
// need to run this before we do any migration logic
if (dbVersion == 3)
checkCaseSensitiveDupes();
// db migrations
std::cout << "[INFO] Migrating Database to Version " << dbVersion + 1 << std::endl;
@@ -191,17 +226,31 @@ static void createTables() {
static int getTableSize(std::string tableName) {
std::lock_guard<std::mutex> lock(dbCrit); // XXX
const char* sql = "SELECT COUNT(*) FROM ?";
// you aren't allowed to bind the table name
const char* sql = "SELECT COUNT(*) FROM ";
tableName.insert(0, sql);
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, tableName.c_str(), -1, NULL);
sqlite3_prepare_v2(db, tableName.c_str(), -1, &stmt, NULL);
sqlite3_step(stmt);
int result = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return result;
}
void Database::init() {
std::cout << "[INFO] Built with libsqlite " SQLITE_VERSION << std::endl;
if (sqlite3_libversion_number() != SQLITE_VERSION_NUMBER)
std::cout << "[INFO] Using libsqlite " << std::string(sqlite3_libversion()) << std::endl;
if (sqlite3_libversion_number() < MIN_SUPPORTED_SQLITE_NUMBER) {
std::cerr << "[FATAL] Runtime sqlite version too old. Minimum compatible version: " MIN_SUPPORTED_SQLITE << std::endl;
exit(1);
}
}
void Database::open() {
// XXX: move locks here
int rc = sqlite3_open(settings::DBPATH.c_str(), &db);
if (rc != SQLITE_OK) {
@@ -223,12 +272,12 @@ void Database::open() {
int players = getTableSize("Players");
std::string message = "";
if (accounts > 0) {
message += ": Found " + std::to_string(accounts) + " Account";
message += ": Found " + std::to_string(accounts) + " account";
if (accounts > 1)
message += "s";
}
if (players > 0) {
message += " and " + std::to_string(players) + " Player Character";
message += " and " + std::to_string(players) + " player";
if (players > 1)
message += "s";
}

View File

@@ -3,10 +3,13 @@
#include "db/Database.hpp"
#include <sqlite3.h>
#if defined(__MINGW32__) && !defined(_GLIBCXX_HAS_GTHREADS)
#include "mingw/mingw.mutex.h"
#else
#include <mutex>
#define MIN_SUPPORTED_SQLITE_NUMBER 3033000
#define MIN_SUPPORTED_SQLITE "3.33.0"
// we can't use this in #error, since it doesn't expand macros
// Compile-time libsqlite version check
#if SQLITE_VERSION_NUMBER < MIN_SUPPORTED_SQLITE_NUMBER
#error libsqlite version too old. Minimum compatible version: 3.33.0
#endif
extern std::mutex dbCrit;

View File

@@ -27,6 +27,32 @@ void Database::findAccount(Account* account, std::string login) {
sqlite3_finalize(stmt);
}
int Database::getAccountIdForPlayer(int playerId) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT AccountID
FROM Players
WHERE PlayerID = ?
LIMIT 1;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
std::cout << "[WARN] Database: couldn't get account id for player " << playerId << std::endl;
sqlite3_finalize(stmt);
return 0;
}
int accountId = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return accountId;
}
int Database::addAccount(std::string login, std::string password) {
std::lock_guard<std::mutex> lock(dbCrit);
@@ -52,6 +78,75 @@ int Database::addAccount(std::string login, std::string password) {
return sqlite3_last_insert_rowid(db);
}
void Database::updateAccountLevel(int accountId, int accountLevel) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
UPDATE Accounts SET
AccountLevel = ?
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountLevel);
sqlite3_bind_int(stmt, 2, accountId);
int rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on updateAccountLevel(): " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
}
bool Database::checkCookie(int accountId, const char *tryCookie) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql_get = R"(
SELECT Cookie
FROM Auth
WHERE AccountID = ? AND Expires > ?;
)";
const char* sql_invalidate = R"(
UPDATE Auth
SET Expires = 0
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql_get, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId);
sqlite3_bind_int(stmt, 2, getTimestamp());
int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return false;
}
const char *cookie = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (strlen(cookie) != strlen(tryCookie)) {
sqlite3_finalize(stmt);
return false;
}
/*
* since cookies are immediately invalidated, we don't need to be concerned about
* timing-related side channel attacks, so strcmp is fine here
*/
bool match = (strcmp(cookie, tryCookie) == 0);
sqlite3_finalize(stmt);
sqlite3_prepare_v2(db, sql_invalidate, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on checkCookie(): " << sqlite3_errmsg(db) << std::endl;
return match;
}
void Database::updateSelected(int accountId, int slot) {
std::lock_guard<std::mutex> lock(dbCrit);
@@ -79,6 +174,29 @@ void Database::updateSelected(int accountId, int slot) {
std::cout << "[WARN] Database fail on updateSelected(): " << sqlite3_errmsg(db) << std::endl;
}
void Database::updateSelectedByPlayerId(int accountId, int32_t playerId) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
UPDATE Accounts SET
Selected = p.Slot,
LastLogin = (strftime('%s', 'now'))
FROM (SELECT Slot From Players WHERE PlayerId = ?) AS p
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
sqlite3_bind_int(stmt, 2, accountId);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on updateSelectedByPlayerId(): " << sqlite3_errmsg(db) << std::endl;
}
bool Database::validateCharacter(int characterID, int userID) {
std::lock_guard<std::mutex> lock(dbCrit);

View File

@@ -285,11 +285,13 @@ void Database::getPlayer(Player* plr, int id) {
sqlite3_finalize(stmt);
}
void Database::updatePlayer(Player *player) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
/*
* Low-level function to save a player to DB.
* Must be run in a SQL transaction and with dbCrit locked.
* The caller manages the transacstion, so if this function returns false,
* the caller must roll it back.
*/
bool Database::_updatePlayer(Player *player) {
const char* sql = R"(
UPDATE Players
SET
@@ -336,10 +338,8 @@ void Database::updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 21, player->iID);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
return false;
}
sqlite3_finalize(stmt);
@@ -375,10 +375,8 @@ void Database::updatePlayer(Player *player) {
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return;
return false;
}
sqlite3_reset(stmt);
}
@@ -395,10 +393,8 @@ void Database::updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 6, player->Inven[i].iTimeLimit);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return;
return false;
}
sqlite3_reset(stmt);
}
@@ -415,10 +411,8 @@ void Database::updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 6, player->Bank[i].iTimeLimit);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return;
return false;
}
sqlite3_reset(stmt);
}
@@ -451,10 +445,8 @@ void Database::updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 4, player->QInven[i].iID);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return;
return false;
}
sqlite3_reset(stmt);
}
@@ -487,10 +479,8 @@ void Database::updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 4, player->Nanos[i].iStamina);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return;
return false;
}
sqlite3_reset(stmt);
}
@@ -524,14 +514,47 @@ void Database::updatePlayer(Player *player) {
sqlite3_bind_int(stmt, 5, player->RemainingNPCCount[i][2]);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return;
return false;
}
sqlite3_reset(stmt);
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return true;
}
void Database::updatePlayer(Player *player) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
if (!_updatePlayer(player)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
}
void Database::commitTrade(Player *plr1, Player *plr2) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
if (!_updatePlayer(plr1)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
}
if (!_updatePlayer(plr2)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
}

View File

@@ -208,6 +208,8 @@ void Database::addBlock(int playerId, int blockedPlayerId) {
}
void Database::removeBlock(int playerId, int blockedPlayerId) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
DELETE FROM Blocks
WHERE PlayerID = ? AND BlockedPlayerID = ?;

View File

@@ -1,174 +0,0 @@
#include "lua/LuaWrapper.hpp"
#include "lua/LuaManager.hpp"
#include "lua/EntityWrapper.hpp"
#include "Entities.hpp"
#define LIBNAME "Entity"
#define SUPERTBL "__entSUPERCLASSES"
#define ENTYGONESTR "Entity doesn't exist anymore!"
EntityRef* grabBaseEntityRef(lua_State *state, int indx) {
// first, make sure its a userdata
luaL_checktype(state, indx, LUA_TUSERDATA);
// grab the super class table
lua_getfield(state, LUA_REGISTRYINDEX, SUPERTBL);
// grab the userdata
EntityRef *data = (EntityRef*)lua_touserdata(state, indx);
// check if it doesn't have a metatable
if (!lua_getmetatable(state, indx)) {
luaL_typerror(state, indx, LIBNAME);
return NULL;
}
// index the super class table
lua_gettable(state, -2);
// if the index was nil, it doesn't exist
if (lua_isnil(state, -1)) {
lua_pop(state, 1);
luaL_typerror(state, indx, LIBNAME);
return NULL;
}
// it's good :)
lua_pop(state, 1);
return data;
}
// check at index
EntityRef* grabEntityRef(lua_State *state, int indx) {
EntityRef *ref = grabBaseEntityRef(state, indx);
if (ref == NULL || !ref->isValid())
return NULL;
return ref;
}
// =============================================== [[ GETTERS ]] ===============================================
static int ent_getX(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
if (ref == NULL)
return 0;
lua_pushnumber(state, ref->getEntity()->x);
return 1;
}
static int ent_getY(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
if (ref == NULL)
return 0;
lua_pushnumber(state, ref->getEntity()->y);
return 1;
}
static int ent_getZ(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
if (ref == NULL)
return 0;
lua_pushnumber(state, ref->getEntity()->z);
return 1;
}
static int ent_getType(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
if (ref == NULL)
return 0;
// push the type string
switch(ref->type) {
case EntityType::PLAYER:
lua_pushstring(state, "Player");
break;
case EntityType::MOB:
lua_pushstring(state, "Mob");
break;
case EntityType::EGG:
lua_pushstring(state, "Egg");
break;
case EntityType::BUS:
lua_pushstring(state, "Bus");
break;
default: // INVALID, COMBAT_NPC, SIMPLE_NPC
lua_pushstring(state, "Entity");
break;
}
return 1;
}
static luaL_Reg ent_getters[] = {
{"x", ent_getX},
{"y", ent_getY},
{"z", ent_getZ},
{"type", ent_getType},
{0, 0}
};
// =============================================== [[ METHODS ]] ===============================================
static int ent_exists(lua_State *state) {
EntityRef *data = (EntityRef*)grabBaseEntityRef(state, 1);
lua_pushboolean(state, !(data == NULL || !data->isValid()));
return 1;
}
static luaL_Reg ent_methods[] = {
{"exists", ent_exists},
{0, 0}
};
void LuaManager::Entity::init(lua_State *state) {
// register our library as a global (and leave it on the stack)
luaL_register(state, LIBNAME, ent_methods);
lua_pop(state, 1); // pop library table
// will hold our super classes
lua_pushstring(state, SUPERTBL);
lua_newtable(state);
lua_rawset(state, LUA_REGISTRYINDEX);
}
void LuaManager::Entity::registerSuper(lua_State *state, const char *tname) {
// grab the super class table
lua_getfield(state, LUA_REGISTRYINDEX, SUPERTBL);
// grab the metatable
lua_getfield(state, LUA_REGISTRYINDEX, tname);
lua_pushboolean(state, 1);
// finally, set the index
lua_rawset(state, -3);
lua_pop(state, 1); // pop the super class table
}
void LuaManager::Entity::addGetters(lua_State *state) {
luaL_register(state, NULL, ent_getters);
}
void LuaManager::Entity::addMethods(lua_State *state) {
luaL_register(state, NULL, ent_methods);
}
void LuaManager::Entity::push(lua_State *state, EntityRef ref, const char *tname) {
// creates the udata and copies the reference to the udata
EntityRef *ent = (EntityRef*)lua_newuserdata(state, sizeof(EntityRef));
*ent = ref;
// attaches our metatable from the registry to the udata
luaL_getmetatable(state, tname);
lua_setmetatable(state, -2);
}

View File

@@ -1,19 +0,0 @@
#pragma once
#include "lua/LuaWrapper.hpp"
#include "lua/LuaManager.hpp"
#include "Entities.hpp"
namespace LuaManager {
namespace Entity {
void init(lua_State *state);
void registerSuper(lua_State *state, const char *tname);
void addGetters(lua_State *state);
void addMethods(lua_State *state);
void push(lua_State *state, EntityRef ref, const char *tname);
}
}

View File

@@ -1,190 +0,0 @@
/*
This loads the Event library, basically a wrapper for lEvents, allows the lua script to register callbacks or wait for an event to be fired
*/
#include "lua/EventWrapper.hpp"
#include "lua/LuaWrapper.hpp"
#include <unordered_set>
#define LIBNAME "Event"
#define LISTNR "Listener"
typedef lEvent* eventData;
std::unordered_set<lEvent*> activeEvents;
struct lstnrData {
lEvent *event;
uint32_t rawListener;
};
static void pushListener(lua_State *state, lEvent *event, uint32_t listener) {
lstnrData *lstnr = (lstnrData*)lua_newuserdata(state, sizeof(lstnrData));
lstnr->event = event;
lstnr->rawListener = listener;
// attaches our metatable from the registry to the udata
luaL_getmetatable(state, LISTNR);
lua_setmetatable(state, -2);
}
static void pushEvent(lua_State *state, lEvent *event) {
eventData *eData = (eventData*)lua_newuserdata(state, sizeof(eventData));
*eData = event;
// attaches our metatable from the registry to the udata
luaL_getmetatable(state, LIBNAME);
lua_setmetatable(state, -2);
}
static lEvent *grabEvent(lua_State *state, int indx) {
// first, make sure its a userdata
luaL_checktype(state, indx, LUA_TUSERDATA);
// now, check and make sure its our libraries metatable attached to this userdata
eventData *event = (eventData*)luaL_checkudata(state, indx, LIBNAME);
if (event == NULL) {
luaL_typerror(state, indx, LIBNAME);
return NULL;
}
// if the event is not active return NULL and don't throw an error (this is an easily forgivable user-error)
if (activeEvents.find(*event) == activeEvents.end())
return NULL;
// return the pointer to the lEvent
return *event;
}
static lstnrData *grabListener(lua_State *state, int indx) {
// first, make sure its a userdata
luaL_checktype(state, indx, LUA_TUSERDATA);
// now, check and make sure its our libraries metatable attached to this userdata
lstnrData *lstnr = (lstnrData*)luaL_checkudata(state, indx, LISTNR);
if (lstnr == NULL) {
luaL_typerror(state, indx, LISTNR);
return NULL;
}
// if the event is not active return NULL and don't throw an error (this is an easily forgivable user-error)
if (activeEvents.find(lstnr->event) == activeEvents.end())
return NULL;
// return the pointer to the lEvent
return lstnr;
}
// connects a callback to an event
static int evnt_listen(lua_State *state) {
int nargs = lua_gettop(state);
lEvent *event = grabEvent(state, 1);
// sanity check
if (event == NULL)
return 0;
// for each argument passed, check that it's a function and add it to the event
for (int i = 2; i <= nargs; i++) {
luaL_checktype(state, i, LUA_TFUNCTION);
lua_pushvalue(state, i);
pushListener(state, event, event->addCallback(state, luaL_ref(state, LUA_REGISTRYINDEX)));
}
// we return this many listeners
return nargs - 1;
}
// yields the thread until the event is triggered
static int evnt_wait(lua_State *state) {
lEvent *event = grabEvent(state, 1);
// sanity check
if (event == NULL)
return 0;
event->addWait(state);
return lua_yield(state, 0);
}
static int lstnr_disconnect(lua_State *state) {
lstnrData *lstnr = grabListener(state, 1);
// sanity check
if (lstnr == NULL)
return 0;
// disconnect the event
lstnr->event->disconnectEvent(lstnr->rawListener);
return 0;
}
static int lstnr_reconnect(lua_State *state) {
lstnrData *lstnr = grabListener(state, 1);
// sanity check
if (lstnr == NULL)
return 0;
// disconnect the event
lstnr->event->reconnectEvent(lstnr->rawListener);
return 0;
}
static int lstnr_gc(lua_State *state) {
lstnrData *lstnr = grabListener(state, 1);
// sanity check
if (lstnr == NULL)
return 0;
// if the listener is disabled, clear it
if (lstnr->event->isDisabled(lstnr->rawListener))
lstnr->event->clear(lstnr->rawListener);
return 0;
}
static luaL_Reg evnt_methods[] = {
{"listen", evnt_listen},
{"wait", evnt_wait},
{0, 0}
};
static luaL_Reg lstnr_methods[] = {
{"disconnect", lstnr_disconnect},
{"reconnect", lstnr_reconnect},
{0, 0}
};
void LuaManager::Event::init(lua_State *state) {
// register the library & pop it
luaL_register(state, LIBNAME, evnt_methods);
// create the event metatable
luaL_newmetatable(state, LIBNAME);
lua_pushstring(state, "__index");
lua_pushvalue(state, -3);
lua_rawset(state, -3);
// create the listener metatable
luaL_newmetatable(state, LISTNR);
lua_pushstring(state, "__index");
lua_newtable(state);
luaL_register(state, NULL, lstnr_methods);
lua_rawset(state, -3);
lua_pushstring(state, "__gc");
lua_pushcfunction(state, lstnr_gc);
lua_rawset(state, -3);
// pop the tables off the stack
lua_pop(state, 3);
activeEvents = std::unordered_set<lEvent*>();
}
// just a wrapper for pushEvent()
void LuaManager::Event::push(lua_State *state, lEvent *event) {
pushEvent(state, event);
}

View File

@@ -1,10 +0,0 @@
#include "lua/LuaManager.hpp"
#include "lua/LuaWrapper.hpp"
namespace LuaManager {
namespace Event {
void init(lua_State *state);
void push(lua_State *state, lEvent *event);
}
}

View File

@@ -1,185 +0,0 @@
#include "lua/LuaManager.hpp"
#include "lua/LuaWrapper.hpp"
#include "lua/EventWrapper.hpp"
#include "lua/WorldWrapper.hpp"
#include "lua/PlayerWrapper.hpp"
#include "lua/NPCWrapper.hpp"
#include "PlayerManager.hpp"
#include "servers/CNShardServer.hpp"
#include "settings.hpp"
#include <filesystem>
#include <vector>
time_t getTime();
class Script;
// our "main" state, holds our environment
lua_State *LuaManager::global;
std::map<lua_State*, Script*> activeScripts;
/*
Basically each script is treated as a coroutine, when wait() is called it gets yielded and is pushed onto the scheduler queue to be resumed.
*/
class Script {
private:
lua_State *thread;
lRegistry threadRef; // we'll need to unref this when closing this state
public:
Script(std::string source) {
// make the thread & register it in the registry
thread = lua_newthread(LuaManager::global);
threadRef = luaL_ref(LuaManager::global, LUA_REGISTRYINDEX);
// add this script to the map
activeScripts[thread] = this;
// compile & run the script, if it error'd, print the error
int _retCode;
if (luaL_loadfile(thread, source.c_str()) || ((_retCode = lua_resume(thread, 0)) != 0 && (_retCode != LUA_YIELD))) {
std::cout << "[LUA ERROR]: " << lua_tostring(thread, -1) << std::endl;
}
}
// unregister all of our events from the wrappers
~Script() {
LuaManager::clearState(thread);
// remove it from the global registry
luaL_unref(LuaManager::global, LUA_REGISTRYINDEX, threadRef);
}
// c++ moment....
lua_State* getState() {
return thread;
}
};
struct scheduledThread {
lRegistry ref; // ref that should be unref'd before resuming
time_t time;
};
std::map<lua_State*, scheduledThread> scheduleQueue;
// pauses the script for x seconds, not very accurate but should be
// called within ~60ms or less of when it was schedueled
// will also return the time the thread was paused
int OF_wait(lua_State *state) {
double seconds = luaL_checknumber(state, 1);
// register the thread in the global registry so we don't get GC'd
lua_pushthread(state);
lRegistry ref = luaL_ref(state, LUA_REGISTRYINDEX); // threads decentant of a global state all share the global registry
// yield the state and push the state onto our scheduler queue
scheduleQueue[state] = {ref, (int)(seconds*1000) + getTime()};
return lua_yield(state, 0);
}
void luaScheduler(CNServer *serv, time_t currtime) {
for (auto iter = scheduleQueue.begin(); iter != scheduleQueue.end();) {
time_t event = (*iter).second.time;
lRegistry ref = (*iter).second.ref;
lua_State *thread = (*iter).first;
// is it time to run the event?
if (event <= currtime) {
// remove from the scheduler queue
scheduleQueue.erase(iter++);
// unregister the thread
luaL_unref(thread, LUA_REGISTRYINDEX, ref);
// resume the state, (wait() returns the delta time since call)
lua_pushnumber(thread, ((double)currtime - event)/10);
yieldCall(thread, 1);
} else // go to the next iteration
++iter;
}
}
void LuaManager::printError(std::string err) {
std::cerr << "[LUA ERR]: " << err << std::endl;
}
void LuaManager::init() {
// allocate our state
global = luaL_newstate();
// open lua's base libraries (excluding the IO for now)
luaopen_base(global);
luaopen_table(global);
luaopen_string(global);
luaopen_math(global);
luaopen_debug(global);
// add wait()
lua_register(global, "wait", OF_wait);
// register our libraries
Event::init(global);
World::init(global);
Entity::init(global);
Player::init(global);
NPC::init(global);
activeScripts = std::map<lua_State*, Script*>();
// we want to be called after every poll(), so our timer delta is set to 0
REGISTER_SHARD_TIMER(luaScheduler, 0);
// load our scripts
loadScripts();
}
void LuaManager::runScript(std::string filename) {
new Script(filename);
}
void LuaManager::stopScripts() {
// clear the scheduler queue
scheduleQueue.clear();
// free all the scripts, they'll take care of everything for us :)
for (auto as : activeScripts) {
delete as.second;
}
// finally clear the map
activeScripts.clear();
// walk through each player and unregister each event
for (auto pair: PlayerManager::players) {
if (pair.second->onChat != nullptr) {
delete pair.second->onChat;
pair.second->onChat = nullptr;
}
}
}
void LuaManager::loadScripts() {
if (!std::filesystem::exists(settings::SCRIPTSDIR)) {
std::cout << "[WARN] scripts directory \"" << settings::SCRIPTSDIR << "\" doesn't exist!" << std::endl;
return;
}
// for each file in the scripts director, load the script
std::filesystem::path dir(settings::SCRIPTSDIR);
for (auto &d : std::filesystem::directory_iterator(dir)) {
if (d.path().extension().u8string() == ".lua")
runScript(d.path().u8string());
}
}
void LuaManager::clearState(lua_State *state) {
// TODO
}
void LuaManager::playerAdded(CNSocket *sock) {
World::playerAdded(sock);
}
void LuaManager::playerRemoved(CNSocket *sock) {
World::playerRemoved(sock);
}

View File

@@ -1,28 +0,0 @@
#pragma once
#include "core/CNProtocol.hpp"
#include <string>
#ifdef _MSC_VER
#include <luajit/lua.hpp>
#else
#include <lua.hpp>
#endif
typedef int lRegistry;
namespace LuaManager {
extern lua_State *global;
void init();
void printError(std::string err);
// runs the script in the passed file
void runScript(std::string filename);
void stopScripts();
void loadScripts();
// unregisters the events tied to this state with all wrappers
void clearState(lua_State *state);
void playerAdded(CNSocket *sock);
void playerRemoved(CNSocket *sock);
}

View File

@@ -1,223 +0,0 @@
#pragma once
/*
This is a very simple header that adds a small "event" layer, where scripts can register callbacks & "waits" on events. The actual
Event metatables & library is in EventWrapper.[hc]pp
*/
#include <string>
#include <map>
#include <unordered_set>
#include <cassert>
#include "lua/LuaManager.hpp"
#include "lua/PlayerWrapper.hpp"
#define yieldCall(state, nargs) \
int _retCode = lua_resume(state, nargs); \
if (_retCode != 0 && _retCode != LUA_YIELD) \
LuaManager::printError(lua_tostring(state, -1)); \
inline static int lua_autoPush(lua_State* state, int nargs) {
// return the number of pushed arguments :)
return nargs;
}
/*
This function will automatically push all of the passed arguments onto the stack and returns the # of pushed values
Supported datatypes are:
double or int : LUA_TNUMBER
char* or const char* : LUA_TSTRING
bool : LUA_TBOOLEAN
lRegistry : grabs the object from the lua registry and pushes it onto the stack
CNSocket* : Pushes the Player Entity
*/
template<typename T, class... Rest>
inline static int lua_autoPush(lua_State* state, int nargs, T arg, Rest... rest) {
// pick which branch to compile based on the type of arg
if constexpr(std::is_same<T, int>::value || std::is_same<T, double>::value) {
lua_pushnumber(state, (lua_Number)arg);
} else if constexpr(std::is_same<T, char*>::value || std::is_same<T, const char*>::value) {
lua_pushstring(state, (const char*)arg);
} else if constexpr(std::is_same<T, lRegistry>::value) {
// grab the value from the registry
lua_rawgeti(state, LUA_REGISTRYINDEX, (int)arg);
} else if constexpr(std::is_same<T, CNSocket*>::value) { // pushes a Player Entity
LuaManager::Player::push(state, arg);
} else if constexpr(std::is_same<T, bool>::value) {
lua_pushboolean(state, arg);
}
// recursively call, expanding rest and pushing the left-most rvalue into arg
return lua_autoPush(state, ++nargs, rest...);
}
enum eventType {
EVENT_CALLBACK, // standard callback
EVENT_WAIT // state needs to be resumed with the arguments
};
class lEvent;
extern std::unordered_set<lEvent*> activeEvents;
class lEvent {
private:
struct rawEvent {
lua_State *state;
eventType type;
lRegistry ref;
bool disabled;
};
uint32_t nextId; // next ID
std::map<uint32_t, rawEvent> events;
uint32_t registerEvent(lua_State *state, lRegistry ref, eventType type) {
uint32_t id = nextId++;
assert(nextId < UINT32_MAX);
events[id] = {state, type, ref, false};
return id;
}
void freeEvent(rawEvent *event) {
// the registry of all states is the same!
luaL_unref(event->state, LUA_REGISTRYINDEX, event->ref);
}
public:
lEvent() {
events = std::map<uint32_t, rawEvent>();
nextId = 0; // start at 0
activeEvents.insert(this);
}
~lEvent() {
// remove from the active set and disable all existing callbacks
activeEvents.erase(this);
clear();
}
uint32_t addCallback(lua_State *state, lRegistry ref) {
return registerEvent(state, ref, EVENT_CALLBACK);
}
// yields the thread until the event is called
uint32_t addWait(lua_State *state) {
// push the thread onto the stack and register it in the global registry
lua_pushthread(state);
lRegistry ref = luaL_ref(state, LUA_REGISTRYINDEX);
return registerEvent(state, ref, EVENT_WAIT);
}
// walks through the events and unregister them from the state
void clear() {
for (auto &pair : events) {
freeEvent(&pair.second);
}
events.clear();
}
// free the event based on id
void clear(uint32_t id) {
auto iter = events.find(id);
// sanity check
if (iter == events.end())
return;
// free the event
freeEvent(&(*iter).second);
events.erase(iter);
}
// frees all events to this state
void clear(lua_State *state) {
for (auto iter = events.begin(); iter != events.end();) {
rawEvent *event = &(*iter).second;
// if this is our state, free this event and erase it from the map
if (event->state == state) {
freeEvent(event);
events.erase(iter++);
} else
++iter;
}
}
void disconnectEvent(uint32_t id) {
auto iter = events.find(id);
// sanity check
if (iter == events.end())
return;
(*iter).second.disabled = true;
}
void reconnectEvent(uint32_t id) {
auto iter = events.find(id);
// sanity check
if (iter == events.end())
return;
(*iter).second.disabled = false;
}
bool isDisabled(uint32_t id) {
auto iter = events.find(id);
// sanity check
if (iter == events.end())
return true;
return (*iter).second.disabled;
}
template<class... Args> inline void call(Args... args) {
auto eventsClone = events; // so if a callback is added, we don't iterate into undefined behavior
for (auto &pair : eventsClone) {
rawEvent *event = &pair.second;
// if the event is disabled, skip it
if (event->disabled)
continue;
switch (event->type) {
case EVENT_CALLBACK: {
// make thread for this callback
if (!lua_checkstack(event->state, 1)) {
std::cout << "[FATAL] Failed to create new lua thread! out of memory!" << std::endl;
terminate(0);
}
lua_State *nThread = lua_newthread(event->state);
// push the callable first, the push all the arguments
lua_rawgeti(nThread, LUA_REGISTRYINDEX, (int)event->ref);
int nargs = lua_autoPush(nThread, 0, args...);
// then call it :)
yieldCall(nThread, nargs);
break;
}
case EVENT_WAIT: {
// erase this event
freeEvent(&pair.second);
events.erase(pair.first);
// the :wait() will return the passed arguments
int nargs = lua_autoPush(event->state, 0, args...);
yieldCall(event->state, nargs);
break;
}
default:
std::cout << "[WARN] INVALID EVENT TYPE : " << event->type << std::endl;
break;
}
}
}
};

View File

@@ -1,266 +0,0 @@
#include "lua/LuaWrapper.hpp"
#include "lua/NPCWrapper.hpp"
#include "NPC.hpp"
#include "NPCManager.hpp"
#include "Transport.hpp"
#define LIBNAME "NPC"
#define NPCGONESTR "NPC was destoryed and no longer exists!"
#define GETTERTBL "__npcGETTERS"
#define SETTERTBL "__npcSETTERS"
#define METHODTBL "__npcMETHODS"
static EntityRef* grabEntityRef(lua_State *state, int indx) {
// first, make sure its a userdata
luaL_checktype(state, indx, LUA_TUSERDATA);
// now, check and make sure its our library's metatable attached to this userdata
EntityRef *ref = (EntityRef*)luaL_checkudata(state, indx, LIBNAME);
if (ref == NULL) {
luaL_typerror(state, indx, LIBNAME);
return NULL;
}
// check if the npc exists still & return NULL if it doesn't
if (!ref->isValid()) {
luaL_argerror(state, indx, NPCGONESTR);
return NULL;
}
return ref;
}
static CombatNPC* grabNPC(lua_State *state, int indx) {
EntityRef *ref = grabEntityRef(state, indx);
// if grabEntityRef failed, return error result
return (ref == NULL) ? NULL : (CombatNPC*)ref->getEntity();
}
static int32_t grabID(lua_State *state, int indx) {
EntityRef *ref = grabEntityRef(state, indx);
// if grabEntityRef failed, return error result
return (ref == NULL) ? -1 : ref->id;
}
// =============================================== [[ GETTERS ]] ===============================================
static int npc_getMaxHealth(lua_State *state) {
CombatNPC *npc = grabNPC(state, 1);
// sanity check
if (npc == NULL)
return 0;
lua_pushinteger(state, npc->maxHealth);
return 1;
}
static int npc_getSpeed(lua_State *state) {
CombatNPC *npc = grabNPC(state, 1);
// sanity check
if (npc == NULL)
return 0;
lua_pushinteger(state, npc->speed);
return 1;
}
static const luaL_Reg npc_getters[] = {
{"maxHealth", npc_getMaxHealth},
{"speed", npc_getSpeed},
{0, 0}
};
// =============================================== [[ SETTERS ]] ===============================================
static int npc_setMaxHealth(lua_State *state) {
CombatNPC *npc = grabNPC(state, 1);
int newMH = luaL_checkint(state, 2);
// sanity check
if (npc != NULL)
npc->maxHealth = newMH;
return 0;
}
static int npc_setSpeed(lua_State *state) {
CombatNPC *npc = grabNPC(state, 1);
int newSpeed = luaL_checkint(state, 2);
// sanity check
if (npc != NULL)
npc->speed = newSpeed;
return 0;
}
static const luaL_Reg npc_setters[] = {
{"maxHealth", npc_setMaxHealth},
{"speed", npc_setSpeed},
{0, 0}
};
// =============================================== [[ METHODS ]] ===============================================
static int npc_moveto(lua_State *state) {
CombatNPC *npc = grabNPC(state, 1);
int X = luaL_checkint(state, 2);
int Y = luaL_checkint(state, 3);
int Z = luaL_checkint(state, 4);
// sanity check
if (npc == NULL)
return 0;
std::queue<Vec3> queue;
Vec3 from = { npc->x, npc->y, npc->z };
Vec3 to = { X, Y, Z };
// add a route to the queue; to be processed in Transport::stepNPCPathing()
Transport::lerp(&queue, from, to, npc->speed);
Transport::NPCQueues[npc->appearanceData.iNPC_ID] = queue;
return 0;
}
static int npc_say(lua_State *state) {
CombatNPC *npc = grabNPC(state, 1);
std::string msg = std::string(luaL_checkstring(state, 2));
// sanity check
if (npc != NULL) {
INITSTRUCT(sP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, res);
U8toU16(msg, (char16_t*)&res.szFreeChat, sizeof(res.szFreeChat));
res.iPC_ID = npc->appearanceData.iNPC_ID;
res.iEmoteCode = 421;
// send the packet
NPCManager::sendToViewable(npc, &res, P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC));
}
return 0;
}
static const luaL_Reg npc_methods[] = {
{"moveTo", npc_moveto},
{"say", npc_say},
{0, 0}
};
// in charge of calling the correct getter method
static int plr_index(lua_State *state) {
// grab the function from the getters lookup table
lua_pushstring(state, GETTERTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// if it's not nil, call it and run the getter method
if (!lua_isnil(state, -1)) {
// push userdata & call the function
lua_pushvalue(state, 1);
lua_call(state, 1, 1);
// return # of results
return 1;
}
// grab the function from the methods lookup table
lua_pop(state, 1);
lua_pushstring(state, METHODTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// return result
return 1;
}
// in charge of calling the correct setter method
static int plr_newindex(lua_State *state) {
// grab the function from the getters lookup table
lua_pushstring(state, SETTERTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// if it's nil return
if (lua_isnil(state, -1))
return 0;
// push userdata & call the function
lua_pushvalue(state, 1);
lua_call(state, 1, 0);
// return # of results
return 0;
}
static int npc_new(lua_State *state) {
int X = luaL_checkint(state, 1);
int Y = luaL_checkint(state, 2);
int Z = luaL_checkint(state, 3);
int type = luaL_checkint(state, 4);
int id = NPCManager::nextId++;
// create & initalize the NPC
CombatNPC *NPC = new CombatNPC(X, Y, Z, 0, INSTANCE_OVERWORLD, type, id, 1000);
NPCManager::NPCs[id] = NPC;
NPCManager::updateNPCPosition(id, X, Y, Z, INSTANCE_OVERWORLD, 0);
// push it to the lua stack & return
LuaManager::NPC::push(state, NPC);
return 1;
}
void LuaManager::NPC::init(lua_State *state) {
// register our library as a global (and leave it on the stack)
luaL_register(state, LIBNAME, npc_methods);
// sets NPC.new
lua_pushstring(state, "new");
lua_pushcfunction(state, npc_new);
lua_rawset(state, -3);
// create the meta table and populate it with our functions
luaL_newmetatable(state, LIBNAME);
lua_pushstring(state, "__index");
lua_pushcfunction(state, plr_index);
lua_rawset(state, -3); // sets meta.__index = plr_index
lua_pushstring(state, "__newindex");
lua_pushcfunction(state, plr_newindex);
lua_rawset(state, -3); // sets meta.__newindex = plr_newindex
lua_pop(state, 2); // pop meta & library table
// create the methods table
lua_pushstring(state, METHODTBL);
lua_newtable(state);
Entity::addMethods(state); // register the base Entity methods
luaL_register(state, NULL, npc_methods);
lua_rawset(state, LUA_REGISTRYINDEX);
// create the getters table
lua_pushstring(state, GETTERTBL);
lua_newtable(state);
Entity::addGetters(state); // register the base Entity getters
luaL_register(state, NULL, npc_getters);
lua_rawset(state, LUA_REGISTRYINDEX);
// create the setters table
lua_pushstring(state, SETTERTBL);
lua_newtable(state);
luaL_register(state, NULL, npc_setters);
lua_rawset(state, LUA_REGISTRYINDEX);
LuaManager::Entity::registerSuper(state, LIBNAME);
}
void LuaManager::NPC::push(lua_State *state, CombatNPC *npc) {
Entity::push(state, EntityRef(npc->appearanceData.iNPC_ID), LIBNAME);
}

View File

@@ -1,11 +0,0 @@
#include "lua/LuaWrapper.hpp"
#include "lua/LuaManager.hpp"
#include "lua/EntityWrapper.hpp"
namespace LuaManager {
namespace NPC {
void init(lua_State *state);
void push(lua_State *state, CombatNPC *npc);
}
}

View File

@@ -1,278 +0,0 @@
#include "lua/EntityWrapper.hpp"
#include "lua/EventWrapper.hpp"
#include "lua/PlayerWrapper.hpp"
#include "core/CNProtocol.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Chat.hpp"
#define LIBNAME "Player"
#define PLRGONESTR "Player doesn't exist anymore, they left!"
#define GETTERTBL "__plrGETTERS"
#define SETTERTBL "__plrSETTERS"
#define METHODTBL "__plrMETHODS"
static EntityRef* grabEntityRef(lua_State *state, int indx) {
// first, make sure its a userdata
luaL_checktype(state, indx, LUA_TUSERDATA);
// now, check and make sure its our library's metatable attached to this userdata
EntityRef *ref = (EntityRef*)luaL_checkudata(state, indx, LIBNAME);
if (ref == NULL) {
luaL_typerror(state, indx, LIBNAME);
return NULL;
}
// check if the player exists still & return NULL if it doesn't
if (!ref->isValid()) {
luaL_argerror(state, indx, PLRGONESTR);
return NULL;
}
return ref;
}
static Player* grabPlayer(lua_State *state, int indx) {
EntityRef *ref = grabEntityRef(state, indx);
if (ref == NULL)
return NULL;
return (Player*)ref->getEntity();
}
static CNSocket* grabSock(lua_State *state, int indx) {
EntityRef *ref = grabEntityRef(state, indx);
if (ref == NULL)
return NULL;
return ref->sock;
}
// =============================================== [[ GETTERS ]] ===============================================
static int plr_getName(lua_State *state) {
Player *plr = grabPlayer(state, 1);
if (plr == NULL)
return 0;
lua_pushstring(state, PlayerManager::getPlayerName(plr).c_str());
return 1;
}
static int plr_getInstance(lua_State *state) {
Player *plr = grabPlayer(state, 1);
if (plr == NULL)
return 0;
lua_pushnumber(state, plr->instanceID);
return 1;
}
static int plr_getChatted(lua_State *state) {
Player *plr = grabPlayer(state, 1);
if (plr == NULL)
return 0;
// the Player* entity doesn't actually have an lEvent setup until a lua script asks for it, so
// if Player->onChat is nullptr, create the lEvent and then push it :D
if (plr->onChat == nullptr)
plr->onChat = new lEvent();
LuaManager::Event::push(state, plr->onChat);
return 1;
}
static const luaL_Reg plr_getters[] = {
{"name", plr_getName},
{"instance", plr_getInstance},
{"onChat", plr_getChatted},
{0, 0}
};
// =============================================== [[ SETTERS ]] ===============================================
static int plr_setInstance(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
int newInst = luaL_checkint(state, 2);
Player *plr;
CNSocket *sock;
// sanity check
if (ref == NULL)
return 0;
plr = (Player*)ref->getEntity();
sock = ref->sock;
PlayerManager::sendPlayerTo(sock, plr->x, plr->y, plr->z, newInst);
return 0;
}
static const luaL_Reg plr_setters[] = {
{"instance", plr_setInstance},
{0, 0}
};
// =============================================== [[ METHODS ]] ===============================================
static int plr_kick(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
Player *plr;
CNSocket *sock;
// sanity check
if (ref == NULL)
return 0;
plr = (Player*)ref->getEntity();
sock = ref->sock;
// construct packet
INITSTRUCT(sP_FE2CL_REP_PC_EXIT_SUCC, response);
response.iID = plr->iID;
response.iExitCode = 3; // "a GM has terminated your connection"
// send to target player
sock->sendPacket(response, P_FE2CL_REP_PC_EXIT_SUCC);
// ensure that the connection has terminated
sock->kill();
return 0;
}
static int plr_teleport(lua_State *state) {
EntityRef *ref = grabEntityRef(state, 1);
Player *plr;
CNSocket *sock;
// sanity check
if (ref == NULL)
return 0;
plr = (Player*)ref->getEntity();
sock = ref->sock;
int X = luaL_checkint(state, 2);
int Y = luaL_checkint(state, 3);
int Z = luaL_checkint(state, 4);
int inst = luaL_optint(state, 5, plr->instanceID);
PlayerManager::sendPlayerTo(sock, X, Y, Z, inst);
return 0;
}
static int plr_msg(lua_State *state) {
CNSocket *sock = grabSock(state, 1);
const char *msg = luaL_checkstring(state, 2);
// sanity check
if (sock != NULL)
Chat::sendServerMessage(sock, std::string(msg));
return 0;
}
static const luaL_Reg plr_methods[] = {
{"kick", plr_kick},
{"teleport", plr_teleport},
{"message", plr_msg},
{0, 0}
};
// in charge of calling the correct getter method
static int plr_index(lua_State *state) {
// grab the function from the getters lookup table
lua_pushstring(state, GETTERTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// if it's not nil, call it and run the getter method
if (!lua_isnil(state, -1)) {
// push userdata & call the function
lua_pushvalue(state, 1);
lua_call(state, 1, 1);
// return # of results
return 1;
}
// grab the function from the methods lookup table
lua_pop(state, 1);
lua_pushstring(state, METHODTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// return result
return 1;
}
// in charge of calling the correct setter method
static int plr_newindex(lua_State *state) {
// grab the function from the getters lookup table
lua_pushstring(state, SETTERTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// if it's nil return
if (lua_isnil(state, -1))
return 0;
// push userdata & call the function
lua_pushvalue(state, 1);
lua_call(state, 1, 0);
// return # of results
return 0;
}
void LuaManager::Player::init(lua_State *state) {
// register our library as a global (and leave it on the stack)
luaL_register(state, LIBNAME, plr_methods);
// create the meta table and populate it with our functions
luaL_newmetatable(state, LIBNAME);
lua_pushstring(state, "__index");
lua_pushcfunction(state, plr_index);
lua_rawset(state, -3); // sets meta.__index = plr_index
lua_pushstring(state, "__newindex");
lua_pushcfunction(state, plr_newindex);
lua_rawset(state, -3); // sets meta.__newindex = plr_newindex
lua_pop(state, 2); // pop meta & library table
// create the methods table
lua_pushstring(state, METHODTBL);
lua_newtable(state);
Entity::addMethods(state); // register the base Entity methods
luaL_register(state, NULL, plr_methods);
lua_rawset(state, LUA_REGISTRYINDEX);
// create the getters table
lua_pushstring(state, GETTERTBL);
lua_newtable(state);
Entity::addGetters(state); // register the base Entity getters
luaL_register(state, NULL, plr_getters);
lua_rawset(state, LUA_REGISTRYINDEX);
// create the setters table
lua_pushstring(state, SETTERTBL);
lua_newtable(state);
luaL_register(state, NULL, plr_setters);
lua_rawset(state, LUA_REGISTRYINDEX);
LuaManager::Entity::registerSuper(state, LIBNAME);
}
void LuaManager::Player::push(lua_State *state, CNSocket *sock) {
Entity::push(state, EntityRef(sock), LIBNAME);
}

View File

@@ -1,11 +0,0 @@
#include "lua/LuaWrapper.hpp"
#include "lua/LuaManager.hpp"
#include "lua/EntityWrapper.hpp"
namespace LuaManager {
namespace Player {
void init(lua_State *state);
void push(lua_State *state, CNSocket *sock);
}
}

View File

@@ -1,130 +0,0 @@
#include "lua/LuaManager.hpp"
#include "lua/LuaWrapper.hpp"
#include "lua/EventWrapper.hpp"
#include "lua/WorldWrapper.hpp"
#include "core/CNStructs.hpp"
#include "PlayerManager.hpp"
static lEvent *addedEvent;
static lEvent *removedEvent;
#define LIBNAME "World"
#define GETTERTBL "__wrldGETTERS"
#define METHODTBL "__wrldMETHODS"
// =============================================== [[ GETTERS ]] ===============================================
int wrld_getPlrAdded(lua_State *state) {
LuaManager::Event::push(state, addedEvent);
return 1;
}
int wrld_getPlrRemoved(lua_State *state) {
LuaManager::Event::push(state, removedEvent);
return 1;
}
int wrld_getVersion(lua_State *state) {
lua_pushnumber(state, PROTOCOL_VERSION);
return 1;
}
int wrld_getPlayers(lua_State *state) {
// create a new lua table and push it onto the stack
int entries = 0;
lua_newtable(state);
// walk through the current list of players and add them to the table
for (auto pair : PlayerManager::players) {
lua_pushinteger(state, ++entries);
LuaManager::Player::push(state, pair.first);
lua_rawset(state, -3);
}
// returns the player table :)
return 1;
}
// =============================================== [[ METHODS ]] ===============================================
int wrld_index(lua_State *state) {
// grab the function from the getters lookup table
lua_pushstring(state, GETTERTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// if it's not nil, call it and run the getter method
if (!lua_isnil(state, -1)) {
// push userdata & call the function
lua_pushvalue(state, 1);
lua_call(state, 1, 1);
// return # of results
return 1;
}
// grab the function from the methods lookup table
lua_pop(state, 1);
lua_pushstring(state, METHODTBL);
lua_rawget(state, LUA_REGISTRYINDEX);
lua_pushvalue(state, 2);
lua_rawget(state, -2);
// return result
return 1;
}
static const luaL_Reg getters[] {
{"onPlayerAdded", wrld_getPlrAdded},
{"onPlayerRemoved", wrld_getPlrRemoved},
{"players", wrld_getPlayers},
{"version", wrld_getVersion},
{0, 0}
};
// TODO
static const luaL_Reg methods[] = {
//{"getNearbyPlayers", wrld_getNPlrs},
{0, 0}
};
void LuaManager::World::init(lua_State *state) {
lua_newtable(state);
luaL_newmetatable(state, LIBNAME);
lua_pushstring(state, "__index");
lua_pushcfunction(state, wrld_index);
lua_rawset(state, -3); // sets meta.__index = wrld_index
lua_setmetatable(state, -2); // sets world.__metatable = meta
lua_setglobal(state, LIBNAME);
// setup the __wrldGETTERS table in the registry
lua_pushstring(state, GETTERTBL);
lua_newtable(state);
luaL_register(state, NULL, getters);
lua_rawset(state, LUA_REGISTRYINDEX);
// setup the __wrldMETHODS table in the registry
lua_pushstring(state, METHODTBL);
lua_newtable(state);
luaL_register(state, NULL, methods);
lua_rawset(state, LUA_REGISTRYINDEX);
addedEvent = new lEvent();
removedEvent = new lEvent();
}
void LuaManager::World::clearState(lua_State *state) {
addedEvent->clear(state);
removedEvent->clear(state);
}
void LuaManager::World::playerAdded(CNSocket *sock) {
addedEvent->call(sock);
}
void LuaManager::World::playerRemoved(CNSocket *sock) {
removedEvent->call(sock);
}

View File

@@ -1,13 +0,0 @@
#pragma once
#include "lua/LuaManager.hpp"
namespace LuaManager {
namespace World {
void init(lua_State *state);
void clearState(lua_State *state);
void playerAdded(CNSocket *sock);
void playerRemoved(CNSocket *sock);
}
}

View File

@@ -1,6 +1,5 @@
#include "servers/CNLoginServer.hpp"
#include "servers/CNShardServer.hpp"
#include "lua/LuaManager.hpp"
#include "PlayerManager.hpp"
#include "PlayerMovement.hpp"
#include "BuiltinCommands.hpp"
@@ -26,6 +25,7 @@
#include "Rand.hpp"
#include "settings.hpp"
#include "sandbox/Sandbox.hpp"
#include "../version.h"
@@ -63,8 +63,20 @@ void terminate(int arg) {
exit(0);
}
#ifndef _WIN32
#ifdef _WIN32
static BOOL WINAPI winTerminate(DWORD arg) {
terminate(0);
return FALSE;
}
#endif
void initsignals() {
#ifdef _WIN32
if (!SetConsoleCtrlHandler(winTerminate, TRUE)) {
std::cerr << "[FATAL] Failed to set control handler" << std::endl;
exit(1);
}
#else
struct sigaction act;
memset((void*)&act, 0, sizeof(act));
@@ -82,25 +94,29 @@ void initsignals() {
perror("sigaction");
exit(1);
}
}
#endif
}
int main() {
std::cout << "[INFO] OpenFusion v" GIT_VERSION << std::endl;
std::cout << "[INFO] Protocol version: " << PROTOCOL_VERSION << std::endl;
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
std::cerr << "OpenFusion: WSAStartup failed" << std::endl;
exit(EXIT_FAILURE);
}
#else
initsignals();
#endif
Rand::init(getTime());
initsignals();
settings::init();
std::cout << "[INFO] OpenFusion v" GIT_VERSION << std::endl;
std::cout << "[INFO] Protocol version: " << PROTOCOL_VERSION << std::endl;
std::cout << "[INFO] Intializing Packet Managers..." << std::endl;
Database::init();
Rand::init(getTime());
TableData::init();
std::cout << "[INFO] Intializing Packet Managers..." << std::endl;
PlayerManager::init();
PlayerMovement::init();
BuiltinCommands::init();
@@ -119,9 +135,9 @@ int main() {
Email::init();
Groups::init();
Racing::init();
Database::open();
Trading::init();
LuaManager::init();
Database::open();
switch (settings::EVENTMODE) {
case 0: break; // no event
@@ -140,6 +156,8 @@ int main() {
shardThread = new std::thread(startShard, (CNShardServer*)shardServer);
sandbox_start();
loginServer.start();
shardServer->kill();
@@ -154,10 +172,15 @@ int main() {
// helper functions
std::string U16toU8(char16_t* src, size_t max) {
src[max-1] = '\0'; // force a NULL terminatorstd::string U16toU8(char16_t* src) {
src[max-1] = '\0'; // force a NULL terminator
try {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
return convert.to_bytes(src);
std::string ret = convert.to_bytes(src);
if (ret.size() >= max)
ret.resize(max-2);
return ret;
} catch(const std::exception& e) {
return "";
}
@@ -181,7 +204,7 @@ size_t U8toU16(std::string src, char16_t* des, size_t max) {
time_t getTime() {
using namespace std::chrono;
milliseconds value = duration_cast<milliseconds>((time_point_cast<milliseconds>(high_resolution_clock::now())).time_since_epoch());
milliseconds value = duration_cast<milliseconds>((time_point_cast<milliseconds>(steady_clock::now())).time_since_epoch());
return (time_t)value.count();
}

21
src/sandbox/Sandbox.hpp Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
// use the sandbox on supported platforms, unless disabled
#if defined(__linux__) || defined(__OpenBSD__)
# if !defined(CONFIG_NOSANDBOX)
void sandbox_start();
# else
#include <iostream>
inline void sandbox_start() {
std::cout << "[WARN] Built without a sandbox" << std::endl;
}
# endif // CONFIG_NOSANDBOX
#else
// stub for unsupported platforms
inline void sandbox_start() {}
#endif

45
src/sandbox/openbsd.cpp Normal file
View File

@@ -0,0 +1,45 @@
#if defined(__OpenBSD__) && !defined(CONFIG_NOSANDBOX)
#include "core/Core.hpp"
#include "settings.hpp"
#include <stdio.h>
#include <unistd.h>
#include <err.h>
static void eunveil(const char *path, const char *permissions) {
if (unveil(path, permissions) < 0)
err(1, "unveil");
}
void sandbox_start() {
/*
* There shouldn't ever be a reason to disable this one, but might as well
* be consistent with the Linux sandbox.
*/
if (!settings::SANDBOX) {
std::cout << "[WARN] Running without a sandbox" << std::endl;
return;
}
std::cout << "[INFO] Starting pledge+unveil sandbox..." << std::endl;
if (pledge("stdio rpath wpath cpath inet flock unveil", NULL) < 0)
err(1, "pledge");
// database stuff
eunveil(settings::DBPATH.c_str(), "rwc");
eunveil((settings::DBPATH + "-journal").c_str(), "rwc");
eunveil((settings::DBPATH + "-wal").c_str(), "rwc");
// tabledata stuff
eunveil((settings::TDATADIR + "/" + settings::GRUNTWORKJSON).c_str(), "wc");
// for bcrypt_gensalt()
eunveil("/dev/urandom", "r");
eunveil(NULL, NULL);
}
#endif

324
src/sandbox/seccomp.cpp Normal file
View File

@@ -0,0 +1,324 @@
#if defined(__linux__) && !defined(CONFIG_NOSANDBOX)
#include "core/Core.hpp" // mostly for ARRLEN
#include "settings.hpp"
#include <stdlib.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/mman.h> // for mmap() args
#include <sys/ioctl.h> // for ioctl() args
#include <termios.h> // for ioctl() args
#include <linux/unistd.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <linux/net.h> // for socketcall() args
/*
* Macros adapted from https://outflux.net/teach-seccomp/
* Relevant license:
* https://source.chromium.org/chromium/chromium/src/+/master:LICENSE
*/
#define syscall_nr (offsetof(struct seccomp_data, nr))
#define arch_nr (offsetof(struct seccomp_data, arch))
#if defined(__i386__)
# define ARCH_NR AUDIT_ARCH_I386
#elif defined(__x86_64__)
# define ARCH_NR AUDIT_ARCH_X86_64
#elif defined(__arm__)
# define ARCH_NR AUDIT_ARCH_ARM
#elif defined(__aarch64__)
# define ARCH_NR AUDIT_ARCH_AARCH64
#else
# error "Seccomp-bpf sandbox unsupported on this architecture"
#endif
#define VALIDATE_ARCHITECTURE \
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, arch_nr), \
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ARCH_NR, 1, 0), \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL_PROCESS)
#define EXAMINE_SYSCALL \
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr)
#define ALLOW_SYSCALL(name) \
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_##name, 0, 1), \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW)
#define DENY_SYSCALL_ERRNO(name, _errno) \
BPF_JUMP(BPF_JMP+BPF_K+BPF_JEQ, __NR_##name, 0, 1), \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ERRNO|(_errno))
#define KILL_PROCESS \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL_PROCESS)
/*
* Macros adapted from openssh's sandbox-seccomp-filter.c
* Relevant license:
* https://github.com/openssh/openssh-portable/blob/master/LICENCE
*/
#if __BYTE_ORDER == __LITTLE_ENDIAN
# define ARG_LO_OFFSET 0
# define ARG_HI_OFFSET sizeof(uint32_t)
#elif __BYTE_ORDER == __BIG_ENDIAN
# define ARG_LO_OFFSET sizeof(uint32_t)
# define ARG_HI_OFFSET 0
#else
#error "Unknown endianness"
#endif
#define ALLOW_SYSCALL_ARG(_nr, _arg_nr, _arg_val) \
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, (__NR_##_nr), 0, 6), \
/* load and test syscall argument, low word */ \
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \
offsetof(struct seccomp_data, args[(_arg_nr)]) + ARG_LO_OFFSET), \
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, \
((_arg_val) & 0xFFFFFFFF), 0, 3), \
/* load and test syscall argument, high word */ \
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \
offsetof(struct seccomp_data, args[(_arg_nr)]) + ARG_HI_OFFSET), \
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, \
(((uint32_t)((uint64_t)(_arg_val) >> 32)) & 0xFFFFFFFF), 0, 1), \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), \
/* reload syscall number; all rules expect it in accumulator */ \
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \
offsetof(struct seccomp_data, nr))
/* Allow if syscall argument contains only values in mask */
#define ALLOW_SYSCALL_ARG_MASK(_nr, _arg_nr, _arg_mask) \
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, (__NR_##_nr), 0, 8), \
/* load, mask and test syscall argument, low word */ \
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \
offsetof(struct seccomp_data, args[(_arg_nr)]) + ARG_LO_OFFSET), \
BPF_STMT(BPF_ALU+BPF_AND+BPF_K, ~((_arg_mask) & 0xFFFFFFFF)), \
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 0, 0, 4), \
/* load, mask and test syscall argument, high word */ \
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \
offsetof(struct seccomp_data, args[(_arg_nr)]) + ARG_HI_OFFSET), \
BPF_STMT(BPF_ALU+BPF_AND+BPF_K, \
~(((uint32_t)((uint64_t)(_arg_mask) >> 32)) & 0xFFFFFFFF)), \
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 0, 0, 1), \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), \
/* reload syscall number; all rules expect it in accumulator */ \
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \
offsetof(struct seccomp_data, nr))
/*
* This is a special case for AArch64 where this syscall apparently only
* exists in 32-bit compatibility mode, so we can't include the definition
* even though it gets called somewhere in libc.
*/
#if defined(__aarch64__) && !defined(__NR_fstatat64)
#define __NR_fstatat64 0x4f
#endif
/*
* The main supported configuration is Linux on x86_64 with either glibc or
* musl-libc, with secondary support for x86, ARM and ARM64 (AAarch64) Linux.
*
* Syscalls marked with "maybe" don't seem to be used in the default
* configuration, but should probably be whitelisted anyway.
*
* Syscalls marked with comments like "musl-libc", "raspi" or "alt DB" were
* observed to be necessary on that particular configuration, but there are
* probably other configurations in which they are neccessary as well.
* ("alt DB" represents libsqlite compiled with different options.)
*
* Syscalls marked "vdso" aren't normally caught by seccomp because they are
* implemented in the vdso(7) in most configurations, but it's still prudent
* to whitelist them here.
*/
static sock_filter filter[] = {
VALIDATE_ARCHITECTURE,
EXAMINE_SYSCALL,
// memory management
#ifdef __NR_mmap
ALLOW_SYSCALL_ARG_MASK(mmap, 2, PROT_NONE|PROT_READ|PROT_WRITE),
#endif
ALLOW_SYSCALL(munmap),
ALLOW_SYSCALL_ARG_MASK(mprotect, 2, PROT_NONE|PROT_READ|PROT_WRITE),
ALLOW_SYSCALL(madvise),
ALLOW_SYSCALL(brk),
// basic file IO
#ifdef __NR_open
ALLOW_SYSCALL(open),
#endif
ALLOW_SYSCALL(openat),
ALLOW_SYSCALL(read),
ALLOW_SYSCALL(write),
ALLOW_SYSCALL(close),
#ifdef __NR_stat
ALLOW_SYSCALL(stat),
#endif
ALLOW_SYSCALL(fstat),
#ifdef __NR_newfstatat
ALLOW_SYSCALL(newfstatat),
#endif
ALLOW_SYSCALL(fsync), // maybe
#ifdef __NR_creat
ALLOW_SYSCALL(creat), // maybe; for DB journal
#endif
#ifdef __NR_unlink
ALLOW_SYSCALL(unlink), // for DB journal
#endif
ALLOW_SYSCALL(lseek), // musl-libc; alt DB
ALLOW_SYSCALL(truncate), // for truncate-mode DB
ALLOW_SYSCALL(ftruncate), // for truncate-mode DB
ALLOW_SYSCALL(dup), // for perror(), apparently
// more IO
ALLOW_SYSCALL(pread64),
ALLOW_SYSCALL(pwrite64),
ALLOW_SYSCALL(fdatasync),
ALLOW_SYSCALL(writev), // musl-libc
ALLOW_SYSCALL(preadv), // maybe; alt-DB
ALLOW_SYSCALL(preadv2), // maybe
// misc syscalls called from libc
ALLOW_SYSCALL(getcwd),
ALLOW_SYSCALL(getpid),
ALLOW_SYSCALL(geteuid),
ALLOW_SYSCALL(gettid), // maybe
ALLOW_SYSCALL_ARG(ioctl, 1, TIOCGWINSZ), // musl-libc
ALLOW_SYSCALL_ARG(fcntl, 1, F_GETFL),
ALLOW_SYSCALL_ARG(fcntl, 1, F_SETFL),
ALLOW_SYSCALL_ARG(fcntl, 1, F_GETLK),
ALLOW_SYSCALL_ARG(fcntl, 1, F_SETLK),
ALLOW_SYSCALL_ARG(fcntl, 1, F_SETLKW), // maybe
ALLOW_SYSCALL(exit),
ALLOW_SYSCALL(exit_group),
ALLOW_SYSCALL(rt_sigprocmask), // musl-libc
ALLOW_SYSCALL(clock_nanosleep), // gets called very rarely
#ifdef __NR_rseq
ALLOW_SYSCALL(rseq),
#endif
// to crash properly on SIGSEGV
DENY_SYSCALL_ERRNO(tgkill, EPERM),
DENY_SYSCALL_ERRNO(tkill, EPERM), // musl-libc
DENY_SYSCALL_ERRNO(rt_sigaction, EPERM),
// threading
ALLOW_SYSCALL(futex),
// networking
#ifdef __NR_poll
ALLOW_SYSCALL(poll),
#endif
#ifdef __NR_accept
ALLOW_SYSCALL(accept),
#endif
ALLOW_SYSCALL(setsockopt),
ALLOW_SYSCALL(sendto),
ALLOW_SYSCALL(recvfrom),
ALLOW_SYSCALL(shutdown),
// vdso
ALLOW_SYSCALL(clock_gettime),
ALLOW_SYSCALL(gettimeofday),
#ifdef __NR_time
ALLOW_SYSCALL(time),
#endif
ALLOW_SYSCALL(rt_sigreturn),
// i386
#ifdef __NR_socketcall
ALLOW_SYSCALL_ARG(socketcall, 0, SYS_ACCEPT),
ALLOW_SYSCALL_ARG(socketcall, 0, SYS_SETSOCKOPT),
ALLOW_SYSCALL_ARG(socketcall, 0, SYS_SEND),
ALLOW_SYSCALL_ARG(socketcall, 0, SYS_RECV),
ALLOW_SYSCALL_ARG(socketcall, 0, SYS_SENDTO), // maybe
ALLOW_SYSCALL_ARG(socketcall, 0, SYS_RECVFROM), // maybe
ALLOW_SYSCALL_ARG(socketcall, 0, SYS_SHUTDOWN),
#endif
// Raspberry Pi (ARM)
#ifdef __NR_set_robust_list
ALLOW_SYSCALL(set_robust_list),
#endif
#ifdef __NR_clock_gettime64
ALLOW_SYSCALL(clock_gettime64),
#endif
#ifdef __NR_mmap2
ALLOW_SYSCALL_ARG_MASK(mmap2, 2, PROT_NONE|PROT_READ|PROT_WRITE),
#endif
#ifdef __NR_fcntl64
ALLOW_SYSCALL(fcntl64),
#endif
#ifdef __NR_stat64
ALLOW_SYSCALL(stat64),
#endif
#ifdef __NR_send
ALLOW_SYSCALL(send),
#endif
#ifdef __NR_recv
ALLOW_SYSCALL(recv),
#endif
#ifdef __NR_fstat64
ALLOW_SYSCALL(fstat64),
#endif
#ifdef __NR_geteuid32
ALLOW_SYSCALL(geteuid32),
#endif
#ifdef __NR_truncate64
ALLOW_SYSCALL(truncate64),
#endif
#ifdef __NR_ftruncate64
ALLOW_SYSCALL(ftruncate64),
#endif
#ifdef __NR_sigreturn
ALLOW_SYSCALL(sigreturn), // vdso
#endif
#ifdef __NR_clock_nanosleep_time64
ALLOW_SYSCALL(clock_nanosleep_time64), // maybe
#endif
// AArch64 (ARM64)
#ifdef __NR_unlinkat
ALLOW_SYSCALL(unlinkat),
#endif
#ifdef __NR_fstatat64
ALLOW_SYSCALL(fstatat64),
#endif
#ifdef __NR_ppoll
ALLOW_SYSCALL(ppoll),
#endif
KILL_PROCESS
};
static sock_fprog prog = {
ARRLEN(filter), filter
};
// our own wrapper for the seccomp() syscall
int seccomp(unsigned int operation, unsigned int flags, void *args) {
return syscall(__NR_seccomp, operation, flags, args);
}
void sandbox_start() {
if (!settings::SANDBOX) {
std::cout << "[WARN] Running without a sandbox" << std::endl;
return;
}
std::cout << "[INFO] Starting seccomp-bpf sandbox..." << std::endl;
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
perror("prctl");
exit(1);
}
if (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, &prog) < 0) {
perror("seccomp");
exit(1);
}
}
#endif

View File

@@ -1,16 +1,19 @@
#include "servers/CNLoginServer.hpp"
#include "core/CNShared.hpp"
#include "db/Database.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include <regex>
#include "bcrypt/BCrypt.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "settings.hpp"
#include <regex>
std::map<CNSocket*, CNLoginData> CNLoginServer::loginSessions;
CNLoginServer::CNLoginServer(uint16_t p) {
serverType = "login";
port = p;
pHandler = &CNLoginServer::handlePacket;
init();
@@ -19,6 +22,17 @@ CNLoginServer::CNLoginServer(uint16_t p) {
void CNLoginServer::handlePacket(CNSocket* sock, CNPacketData* data) {
printPacket(data);
if (loginSessions.find(sock) == loginSessions.end() &&
data->type != P_CL2LS_REQ_LOGIN && data->type != P_CL2LS_REP_LIVE_CHECK) {
if (settings::VERBOSITY > 0) {
std::cerr << "OpenFusion: LOGIN PKT OUT-OF-SEQ. PacketType: " <<
Packets::p2str(data->type) << " (" << data->type << ")" << std::endl;
}
return;
}
switch (data->type) {
case P_CL2LS_REQ_LOGIN: {
login(sock, data);
@@ -91,53 +105,95 @@ void loginFail(LoginError errorCode, std::string userLogin, CNSocket* sock) {
void CNLoginServer::login(CNSocket* sock, CNPacketData* data) {
auto login = (sP_CL2LS_REQ_LOGIN*)data->buf;
// TODO: implement better way of sending credentials
std::string userLogin((char*)login->szCookie_TEGid);
std::string userPassword((char*)login->szCookie_authid);
/*
* Sometimes the client sends garbage cookie data.
* Validate it as normal credentials instead of using a length check before falling back.
*/
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) {
std::string userLogin;
std::string userToken; // could be password or auth cookie
/*
* The std::string -> char* -> std::string maneuver should remove any
* trailing garbage after the null terminator.
*/
if (login->iLoginType == (int32_t)LoginType::COOKIE) {
userLogin = std::string(AUTOU8(login->szCookie_TEGid).c_str());
userToken = std::string(AUTOU8(login->szCookie_authid).c_str());
} else {
userLogin = std::string(AUTOU16TOU8(login->szID).c_str());
userPassword = std::string(AUTOU16TOU8(login->szPassword).c_str());
userToken = std::string(AUTOU16TOU8(login->szPassword).c_str());
}
// the client inserts a "\n" in the password if you press enter key in the middle of the password
// (not at the start or the end of the password field)
if (int(userPassword.find("\n")) > 0)
userPassword.erase(userPassword.find("\n"), 1);
// check regex
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) {
// check username regex
if (!CNLoginServer::isUsernameGood(userLogin)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid login or password\n";
text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore\n";
text += "Password has to be 8 - 32 characters long";
std::string text = "Invalid login\n";
text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 15;
msg.iDuringTime = 10;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
}
// we only interpret the token as a cookie if cookie login was used and it's allowed.
// otherwise we interpret it as a password, and this maintains compatibility with
// the auto-login trick used on older clients
bool isCookieAuth = login->iLoginType == (int32_t)LoginType::COOKIE
&& CNLoginServer::isLoginTypeAllowed(LoginType::COOKIE);
// password login checks
if (!isCookieAuth) {
// bail if password auth isn't allowed
if (!CNLoginServer::isLoginTypeAllowed(LoginType::PASSWORD)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Password login disabled\n";
text += "This server has disabled logging in with plaintext passwords.\n";
text += "Please contact an admin for assistance.";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 12;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
}
// check regex
if (!CNLoginServer::isPasswordGood(userToken)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid password\n";
text += "Password has to be 8 - 32 characters long";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 10;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
}
}
Database::Account findUser = {};
Database::findAccount(&findUser, userLogin);
// account was not found
if (findUser.AccountID == 0)
return newAccount(sock, userLogin, userPassword, login->iClientVerC);
if (findUser.AccountID == 0) {
// don't auto-create an account if it's a cookie auth for whatever reason
if (settings::AUTOCREATEACCOUNTS && !isCookieAuth)
return newAccount(sock, userLogin, userToken, login->iClientVerC);
if (!CNLoginServer::isPasswordCorrect(findUser.Password, userPassword))
return loginFail(LoginError::ID_DOESNT_EXIST, userLogin, sock);
}
if (isCookieAuth) {
const char *cookie = userToken.c_str();
if (!Database::checkCookie(findUser.AccountID, cookie))
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
} else {
// simple password check
if (!CNLoginServer::isPasswordCorrect(findUser.Password, userToken))
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
}
// is the account banned
if (findUser.BannedUntil > getTimestamp()) {
@@ -193,9 +249,12 @@ void CNLoginServer::login(CNSocket* sock, CNPacketData* data) {
// send the resp in with original key
sock->sendPacket(resp, P_LS2CL_REP_LOGIN_SUCC);
uint64_t defaultKey;
memcpy(&defaultKey, CNSocketEncryption::defaultKey, sizeof(defaultKey));
// update keys
sock->setEKey(CNSocketEncryption::createNewKey(resp.uiSvrTime, resp.iCharCount + 1, resp.iSlotNum + 1));
sock->setFEKey(CNSocketEncryption::createNewKey((uint64_t)(*(uint64_t*)&CNSocketEncryption::defaultKey[0]), login->iClientVerC, 1));
sock->setFEKey(CNSocketEncryption::createNewKey(defaultKey, login->iClientVerC, 1));
DEBUGLOG(
std::cout << "Login Server: Login success. Welcome " << userLogin << " [" << loginSessions[sock].userID << "]" << std::endl;
@@ -445,7 +504,7 @@ void CNLoginServer::characterSelect(CNSocket* sock, CNPacketData* data) {
* the shard IP has been configured to an address the local machine can't
* reach itself from.
*/
if (sock->sockaddr.sin_addr.s_addr == htonl(INADDR_LOOPBACK))
if (settings::LOCALHOSTWORKAROUND && sock->sockaddr.sin_addr.s_addr == htonl(INADDR_LOOPBACK))
shard_ip = "127.0.0.1";
memcpy(resp.g_FE_ServerIP, shard_ip, strlen(shard_ip));
@@ -453,21 +512,20 @@ void CNLoginServer::characterSelect(CNSocket* sock, CNPacketData* data) {
resp.g_FE_ServerIP[strlen(shard_ip)] = '\0';
resp.g_FE_ServerPort = settings::SHARDPORT;
// pass player to CNSharedData
Player passPlayer = {};
Database::getPlayer(&passPlayer, selection->iPC_UID);
// this should never happen but for extra safety
if (passPlayer.iID == 0)
return invalidCharacter(sock);
LoginMetadata *lm = new LoginMetadata();
lm->FEKey = sock->getFEKey();
lm->timestamp = getTime();
lm->playerId = selection->iPC_UID;
passPlayer.FEKey = sock->getFEKey();
resp.iEnterSerialKey = passPlayer.iID;
CNSharedData::setPlayer(resp.iEnterSerialKey, passPlayer);
resp.iEnterSerialKey = Rand::cryptoRand();
// transfer ownership of connection data to CNShared
CNShared::storeLoginMetadata(resp.iEnterSerialKey, lm);
sock->sendPacket(resp, P_LS2CL_REP_SHARD_SELECT_SUCC);
// update current slot in DB
Database::updateSelected(loginSessions[sock].userID, passPlayer.slot);
Database::updateSelectedByPlayerId(loginSessions[sock].userID, selection->iPC_UID);
}
void CNLoginServer::finishTutorial(CNSocket* sock, CNPacketData* data) {
@@ -601,11 +659,14 @@ bool CNLoginServer::exitDuplicate(int accountId) {
return false;
}
bool CNLoginServer::isLoginDataGood(std::string login, std::string password) {
std::regex loginRegex("[a-zA-Z0-9_-]{4,32}");
std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}");
bool CNLoginServer::isUsernameGood(std::string login) {
const std::regex loginRegex("[a-zA-Z0-9_-]{4,32}");
return (std::regex_match(login, loginRegex));
}
return (std::regex_match(login, loginRegex) && std::regex_match(password, passwordRegex));
bool CNLoginServer::isPasswordGood(std::string password) {
const std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}");
return (std::regex_match(password, passwordRegex));
}
bool CNLoginServer::isPasswordCorrect(std::string actualPassword, std::string tryPassword) {
@@ -618,4 +679,17 @@ bool CNLoginServer::isCharacterNameGood(std::string Firstname, std::string Lastn
std::regex lastnamecheck(R"(((?! )(?!\.)[a-zA-Z0-9]*\.{0,1}(?!\.+ +)[a-zA-Z0-9]* {0,1}(?! +))*$)");
return (std::regex_match(Firstname, firstnamecheck) && std::regex_match(Lastname, lastnamecheck));
}
bool CNLoginServer::isLoginTypeAllowed(LoginType loginType) {
// the config file specifies "comma-separated" but tbh we don't care
switch (loginType) {
case LoginType::PASSWORD:
return settings::AUTHMETHODS.find("password") != std::string::npos;
case LoginType::COOKIE:
return settings::AUTHMETHODS.find("cookie") != std::string::npos;
default:
break;
}
return false;
}
#pragma endregion

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