Compare commits

...

1140 Commits
1.0 ... master

Author SHA1 Message Date
b617456aa1
Include tabledata in CI builds 2025-02-11 17:30:35 -08:00
935ee1bf6f
CI fixes 2025-02-11 17:19:27 -08:00
43a2504357
Update Dockerfile, license, and readme 2025-02-10 18:43:01 -08:00
ca196bf620
Add windows dev setup script 2025-01-30 21:33:05 -08:00
6b9ae4c325
Validate name wheel names 2025-01-06 23:43:37 -05:00
d06c324aa3
Send namereq event on name change as well 2025-01-05 22:30:55 -05:00
052196d1cd
Treat bcast areas 1 and 2 as global 2025-01-02 18:26:00 -05:00
e84f6505b8
Name request monitor events 2025-01-02 18:06:06 -05:00
b483bf7190
Respect name check flag in getPlayerName 2025-01-02 17:44:37 -05:00
6ff51685a8
Move event buffers to Monitor 2025-01-02 17:39:16 -05:00
b4ed31d4fb
Dedicated bcast monitor event 2025-01-02 17:18:53 -05:00
36e0667ed2
Add Email and LastPasswordReset columns to Account table (#299)
* Add Email column to Account table

* Add LastPasswordReset timestamp column, missing DB version bump
2024-11-28 23:01:29 -05:00
ed9fe61faf
Auth cookie refresh on PC_EXIT 2024-11-23 11:29:55 -08:00
55cf3f7102
Refactor login packet handler for more flexible auth (#298)
This PR enables auth cookies to be used simultaneously with plaintext paasswords sent in the cookie authID field.

* Hoist a bunch of checks from the login packet handler into helper functions.
* Rename the LoginType enum to AuthMethod and distinguish it from the iLoginType packet field (see comment in code for why these should be decoupled).
* If the provided token does not pass the cookie check and password auth is enabled, treat it as a plaintext password and authenticate if it is correct.
2024-11-17 05:21:37 +01:00
1543dac4e0
BUILD - Optimize Dockerfile by using alpine as base image (#277) 2024-10-28 21:26:25 -07:00
ae327cc104
Use FE2CL_..._AROUND, _AROUND_DEL packets (#295)
* Use FE2CL_..._AROUND, _AROUND_DEL packets
* Use increased buffer size for 728 and 1013 protocols
2024-10-28 20:49:34 -07:00
6ffde9bb44
Replace most usages of CN_PACKET_BUFFER_SIZE with usable body size 2024-10-28 20:39:25 -07:00
8568fd1c46 Restore the check that makes sure mob paths start from their spawn point
This was added in 599bbedd and accidentally removed during the TableData
refactor in c960b062.
2024-10-19 04:26:09 +02:00
05a5303522 Fix one-off mobs respawning if their regenTime is 0 2024-10-19 04:25:08 +02:00
3365cb53b7 Only listen for monitor connections on localhost by default
This is to prevent accidental exposure of the monitor port to the public
internet if a server admin enables the monitor port without it being
properly firewalled. There is now a config option that lets you override
the address to bind to, so that it can still be made available to other
machines over private networks such as Wireguard.
2024-10-17 01:04:50 +02:00
5e92a58134 Print server types when starting servers
Should have done this back when I added serverType.
2024-10-17 01:04:50 +02:00
94064e1865 [sandbox] Print error message on seccomp sandbox violation
Co-authored-by: cpunch <sethtstubbs@gmail.com>
2024-10-17 01:04:50 +02:00
5e73ff272d [sandbox] Add make target for building without Landlock 2024-10-17 01:04:47 +02:00
197ccad0eb [sandbox] Landlock support
* Support disabling Landlock at compile time or runtime if unsupported,
  without disabling seccomp
* Support older Landlock ABI versions
* Support an extra arbitrary RW path, inteded for the coredump dir
* Support database locations other than the working directory
2024-10-17 01:03:06 +02:00
CakeLancelot
68b56e7c25
Docker: disable sandbox to fix crashes and update Dockerfile/compose.yml (#294)
Additionally:
* Add EXPOSE hints to Dockerfile
* as -> AS in Dockerfile to resolve warning
* Point docker-compose to our docker hub image
* Remove version property in docker-compose.yml as it was deprecated
2024-10-15 01:00:37 -05:00
CakeLancelot
cada1bcfd8
Update check-builds.yaml
* Install SQLite3 headers as they arent included in the Ubuntu 24.04 image (only includes CLI currently): https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md
* Change copy-artifacts task to also use ubuntu-latest
2024-10-14 22:48:41 -05:00
Juansecu
ca43a2996a
BUILD - Fix build of Docker image for different archs (#293) 2024-10-14 17:22:45 -05:00
7c66041a6f Add make target for building without the sandbox 2024-10-12 15:42:26 +02:00
2c822e210b [bcrypt] Fix missing include on Windows
Co-authored-by: Jade Shrinemaiden <jadeshrinemaiden@gmail.com>
2024-10-12 15:15:36 +02:00
Juansecu
352fa8a133
BUILD - Expose ports used by the application server in Dockerfile 2024-10-09 19:13:54 -05:00
CakeLancelot
c116794c83 README updates
Create dockerhub pull badge
Change links from wiki to website
2024-10-08 22:00:45 -05:00
CakeLancelot
4ebda6066c Docker: add manual workflow dispatch and auto install QEMU 2024-10-08 21:52:05 -05:00
6de21277d6
Fix eruption attacks cancelling when no targets are in range 2024-10-08 19:18:00 -07: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
gsemaj
6f59001be1 Only loop NPC movement if the NPC has a looping path assigned
Allows paths to terminate.
Also fixes a bug where follower NPCs sometimes oscillate their positions.
2021-05-09 08:38:14 -04:00
gsemaj
e5d9e7217e Fix potentially expensive copying of ChunkPos in escort path assignment 2021-05-09 08:36:45 -04:00
5c1bb0acc9 Add enabledpatches config option
The server administrator must now specify which patches they want the
server to load (if deviating from the defaults). There are multiple
reasons for this:

* It's useful to be able to pick and choose which patches you want to
boot the server with; without having to move the directories in and out
of the patch directory
* This way, we can have different default patches for different builds
of the game (104 vs 1013)
* ...ergo, it's easier to rapidly switch builds without having to
rearrange your workspace to properly run them
* This also allows us to remove the std::filesystem stuff, which has
spotty compatibility with slightly older (but still current) versions of
the compilers
2021-05-07 21:29:18 +02:00
gsemaj
32daa68458 Clean up /path command handler 2021-05-06 12:17:34 -04:00
gsemaj
2d7aa3c536 Fix EOL in settings.cpp 2021-05-06 12:17:34 -04:00
gsemaj
eb8ec85746 Scan all chunks in instance instead of in view for escort missions 2021-05-06 12:17:34 -04:00
gsemaj
a90ba9ea08 Assign paths with matching taskIDs to escort tasks on task start 2021-05-06 12:17:34 -04:00
gsemaj
974b67d4b6 Move constructPathNPC to Transport 2021-05-06 12:17:34 -04:00
gsemaj
6ae4ab2cbf Move findApplicablePaths to Transport, add taskID argument 2021-05-06 12:17:34 -04:00
gsemaj
9fb41342b3 Fix server crash if patch folder doesn't exist 2021-05-06 12:17:34 -04:00
gsemaj
0ccc66208d Added /path here 2021-05-06 12:17:34 -04:00
gsemaj
ebd3b7b75a Fix path matching breaking prematurely 2021-05-06 12:17:34 -04:00
gsemaj
d41122157f Fix gruntwork paths not saving targeted types 2021-05-06 12:17:34 -04:00
gsemaj
917407f164 Apply finished NPC paths immediately 2021-05-06 12:17:34 -04:00
gsemaj
c393bf7af2 Prioritize ID match over type match when finding a path for an NPC 2021-05-06 12:17:34 -04:00
gsemaj
72b62cd5a4 Don't load NPCs with types not found in the XDT 2021-05-06 12:17:34 -04:00
gsemaj
e508a06eca Automatically flush gruntwork after /path end 2021-05-06 12:17:34 -04:00
gsemaj
f71d2581bd Read completed NPC paths from gruntwork 2021-05-06 12:17:34 -04:00
gsemaj
89a32ac9a4 Write completed NPC paths to gruntwork 2021-05-06 12:17:34 -04:00
gsemaj
c7e2e66a51 Add /path command 2021-05-06 12:17:34 -04:00
gsemaj
37b1d11948 Fix JSON type ambiguity in loadPaths 2021-05-06 12:17:34 -04:00
gsemaj
c960b06227 Apply matching paths to NPCs and mobs on spawn 2021-05-06 12:17:34 -04:00
gsemaj
2721f21427 Fix gruntwork file being flushed to root 2021-05-06 12:17:34 -04:00
gsemaj
d5e65fda3c Update to new path schema + add NPCPath struct 2021-05-06 12:17:34 -04:00
gsemaj
5f29ea93d8 Undo pathing check bypass + update to patched academy tdata 2021-05-06 12:17:34 -04:00
gsemaj
36cb32454d Add force property override feature to patcher
This has a variety of applications, but is particularly useful when we want to replace arrays instead of add onto them
2021-05-06 12:17:34 -04:00
gsemaj
af8dd61967 Clean up tabledata init 2021-05-06 12:17:34 -04:00
gsemaj
26894c8a69 Implement automatic tdata patching 2021-05-06 12:17:34 -04:00
gsemaj
bf12ed4c47 Fix patcher refusing to patch between unsigned and signed integers 2021-05-06 12:17:34 -04:00
gsemaj
59303ba30d Update tdata to patch format, change config constants accordingly 2021-05-06 12:17:34 -04:00
gsemaj
b2a8b86e4c Temporarily disable spawn check for mob pathing 2021-05-06 12:17:34 -04:00
gsemaj
0c05fc4add Offset NPC IDs by 1 to avoid ID 0 2021-05-06 12:17:34 -04:00
gsemaj
c415db3fd3 Implement recursive JSON patching functionality 2021-05-06 12:17:34 -04:00
gsemaj
183586afe4 [refac] Clean up new tdata init routine 2021-05-06 12:17:34 -04:00
gsemaj
e546d3948c [refac] Stop using WarpLocation for coordinates and introduce a Vec3 2021-05-06 12:17:34 -04:00
gsemaj
a0e758f5b7 [refac] Move WarpLocation to Transport.hpp 2021-05-06 12:17:34 -04:00
gsemaj
f58c6b72b3 [refac] Stylistic changes 2021-05-06 12:17:34 -04:00
gsemaj
438eba4aa8 Fix sliders leaving their paths 2021-05-06 12:17:34 -04:00
gsemaj
805fd93b3c Remove academy-specific tdata settings, add patch path 2021-05-06 12:17:34 -04:00
gsemaj
4bcf3af90f Use static JSON key as NPC ID for NPCs and mobs 2021-05-06 12:17:34 -04:00
gsemaj
4eeb93ad51 Update to new tabledata format
Incredibly, this requires no changes to the loading routines as iterating through an array in JSON is just like iterating through an object's properties
2021-05-06 12:17:34 -04:00
gsemaj
e761c700dc [refac] Compartmentalize TableData.cpp 2021-05-06 12:17:34 -04:00
gsemaj
14562f889e [refac] Create typedef for nlohmann::json 2021-05-06 12:17:34 -04:00
gsemaj
dd3066849b Make dynamic IDs count down from max instead of up from 0 2021-05-06 12:17:34 -04:00
gsemaj
91dd5d5280 [AppVeyor] Do not build PR commits twice 2021-05-05 20:47:34 +02:00
2658ed5900 Check if the destination chunk exists after leaving the source chunk
This fixes a crash if a player does a /refresh in the zeroeth chunk.
2021-05-02 19:51:59 +02:00
gsemaj
1c3e1d83de Normalize line endings 2021-04-30 04:17:34 -04:00
gsemaj
c240cc005f Enforce LF in source through git attributes 2021-04-30 04:01:42 -04:00
33ea5af8b7 Added speed to CombatNPC
- Mob's constructor sets speed
- MobAI.cpp now uses mob->speed
2021-04-29 19:20:53 -05:00
CakeLancelot
55f8dc94ce
Update ASAN suppressions 2021-04-26 17:38:29 -05:00
CakeLancelot
b99cab58f7 Update vendor directory in .gitattributes and .editorconfig 2021-04-26 17:03:02 -05:00
FinnHornhoover
21e283bedb update drop format 2021-04-26 11:26:00 -04:00
FinnHornhoover
5517a358ea removed eventcratechance config 2021-04-26 11:26:00 -04:00
FinnHornhoover
4eaf3b2f08 altered event drop logic, fixed taro/fm logic 2021-04-26 11:26:00 -04:00
FinnHornhoover
44fd66b511 restored getTime in rand gen 2021-04-26 11:26:00 -04:00
FinnHornhoover
001a0b8d4b removed drop type field in mobs 2021-04-26 11:26:00 -04:00
FinnHornhoover
20b05a72a0 added mob drop id lookup from mob id 2021-04-26 11:26:00 -04:00
FinnHornhoover
78b87d0f61 register altered rarities correcty in rarity roll 2021-04-26 11:26:00 -04:00
FinnHornhoover
527ca817d5 revised getRarity set logic 2021-04-26 11:26:00 -04:00
FinnHornhoover
80b11b4364 removed my "debugger" 2021-04-26 11:26:00 -04:00
FinnHornhoover
6f636b77f2 rarity index fix for egg crates 2021-04-26 11:26:00 -04:00
FinnHornhoover
8a871f7045 merged itemset types and chances, added bypasses 2021-04-26 11:26:00 -04:00
FinnHornhoover
1779d69078 drop format revision 2021-04-26 11:26:00 -04:00
FinnHornhoover
30de9f668c reference fix 2021-04-26 11:26:00 -04:00
FinnHornhoover
78b17aea72 added better drop handling, parsing, rng 2021-04-26 11:26:00 -04:00
CakeLancelot
aa028392f0 Fix #205 - if a player times out a race, warp them back to the start 2021-04-25 15:41:39 -05:00
CakeLancelot
f0e21b5051 Fix #198 - dismount packet is now sent if unequipping a vehicle 2021-04-24 23:55:13 -05:00
gsemaj
80d965944c Fix naming scheme in NPCs.json 2021-04-24 11:36:33 -04:00
CakeLancelot
2e34440d2e Change the executable working directory for VS to repo root
This fixes config.ini, and the tdata directory not being picked up, so now it launches without closing instantly
2021-04-20 17:17:24 -05:00
CakeLancelot
e9709805b7 Mission validation improvements
* Players can no longer complete tasks that aren't in their journal
* Minimum level requirement is now enforced when starting missions
* You can no longer start missions that are already completed
* Implement TASK_START_FAIL for when startTask() returns false
2021-04-20 15:38:51 -05:00
gsemaj
fa7c88e214 Rename Vendor.Xpp -> Vendors.Xpp 2021-04-16 13:37:18 -04:00
gsemaj
588e941d3c Fix eggs not entering chunks and add update tabledata 2021-04-16 12:30:22 -04:00
Ege
f5600912cb
Update README & logo
Artwork by: https://github.com/egeesin
Commit authored by: https://github.com/egeesin
2021-04-15 13:06:39 -05:00
CakeLancelot
fde4a5ff34 Prevent players from collecting the same pod twice in races
Instead of ringCount, there is now a set of all ring IDs collected during the race.

Note: further validation measures are still required to ensure legitimate times/scores
2021-04-13 21:17:21 -05:00
9b84d9dc4d [refactor] BaseNPC now uses Entity XYZ fields for handling positions
- fixed many references to Entity.appearanceData.i[XYZ] to use the base Entity XYZ values
- BaseNPC::enterIntoViewOf grabs the position from the base Entity XYZ values
- NPCManager::updateNPCPosition updates the base Entity XYZ values
- MobAI.c/deadStep() also sends it's packet based on the Entity XYZ values
2021-04-13 20:03:51 -05:00
48fb510b53 Fix playersInView miscount for dead mobs 2021-04-08 19:25:30 +02:00
gsemaj
fd965fbf03 Remove redundant Mob map 2021-04-07 03:03:43 +02:00
65462d01e3 Generalize NPC AI stepping logic
The MobAI::Mobs map still needs to be removed.
2021-03-31 22:28:27 +02:00
3325397d17 Remove Eggs::Eggs and rearrange Entity members a bit 2021-03-31 22:28:27 +02:00
22678fcfc2 Initialize all members of all Entity-derived classes
Also moved some logic out of Mob into CombatNPC.
2021-03-31 22:28:27 +02:00
0c8e209360 [WIP] Convert all chunk-related logic to the new system's semantics
Replaced all references to chunk->players and chunk->NPCs with
chunk->entities and all instances of the old NPCClass enum with
EntityType.

The server compiles but will not yet run properly.
2021-03-31 22:28:27 +02:00
224ffe05e7 [WIP] Convert most of Chunking to Entity-based system
Player and all NPCs now have a common superclass, with virtual functions
so smooth over shared behavior. EntityRef is a simple class that points
to an arbitrary Entity.

This commit is not yet functional.
2021-03-31 22:28:27 +02:00
49f1cb0f00 Fix PC_ATTACK_NPCs not being marked as variadic 2021-03-31 22:27:54 +02:00
CakeLancelot
a57953393d [refactor] Convert CNLoginServer handlers to new sendPacket() wrapper
Also remove malformed packet checks, since that is already validated
2021-03-31 21:10:54 +02:00
CakeLancelot
b428eb08e9 [refactor] Continue work on switching over to new sendPacket() wrapper
I also moved the give nano GM command to BuiltinCommands, and added a perms check

Haven't checked the following files yet:
Eggs
Groups
Missions
MobAI
2021-03-31 21:10:54 +02:00
CakeLancelot
4a22449f5e [refactor] Switch a bulk of the codebase to the new sendPacket() wrapper
Remaining files to go over:
Nanos
NPCManager
Racing
Trading
Vendors
2021-03-31 21:10:54 +02:00
7f9cdfc9ae Use direct members instead of pointers for viewableChunks and buyback
We had avoided putting STL containers into Players back when we thought
Players was still POD and needed to remain POD, but it turned out that
neither were the case all along, so there's no need for the indirection.
2021-03-31 21:10:54 +02:00
8afe175bd1 No reason not to include the Academy packet in there 2021-03-31 21:10:54 +02:00
0f687cc6b3 [refactor] Remove redundant packet size checks
Done with a vim macro, since I didn't want to bother hacking up a
multi-line deletion script in sed or awk (or Python).
2021-03-31 21:10:54 +02:00
55b140f673 [refactor] Initial conversion to new packet handler interfaces
Manually converted PlayerManager, PlayerMovement and a few parts of
Combat to the new system.
2021-03-31 21:10:54 +02:00
688f13e649 [refactor] Implement generic, validating sendPacket() wrapper 2021-03-31 21:10:54 +02:00
ef7d0148c6 [refactor] Validate all inbound packets before handling them 2021-03-31 21:10:54 +02:00
7c7d9f1be8 Revert "CNServer::start() now uses an iterator instead of indexing repeatedly"
This change subtly broke the poll() loop when a connection was removed,
because erasing an entry from fds would invalidate the iterator that
was still being used.

This reverts commit ec67cc652754887d0a1ce8254e7c6e3da4608f70.
2021-03-31 21:07:47 +02:00
CakeLancelot
919c14be0d Include CNStructs in settings.cpp so the ACADEMY define gets picked up 2021-03-20 18:53:22 -05:00
124ea33959 Disallow vehicles in non-overworld instances 2021-03-19 02:20:13 +01:00
69266d1cda Added Chat and Egg initalizers to main() 2021-03-17 23:46:30 -05:00
574f0cab09 Added a wrapper for U16toU8, called AUTOU16TOU8
- U16toU8 now requires a max arument to be passed
2021-03-17 23:41:47 -05:00
610a683804 [refactor] E g g s 2021-03-17 22:28:24 +01:00
a55a34e09a [refactor] Move files to core/ and servers/ subdirectories
CNProtocol, CNShared, CNStructs and Defines are now in core/.
CNLoginServer, CNShardServer and Monitor are now in servers/.

core/Core.hpp wraps all the core headers except for CNShared.hpp.

Defines.cpp has been renamed to Packets.cpp, and so has its
corresponding namespace, but not the header file. This is in preparation
for upcoming changes.
2021-03-17 20:16:48 +01:00
e9bc2fe561 [refactor] Remove the word 'Manager' from most source files/namespaces
ChatManager -> Chat
MissionManager -> Missions
NanoManager -> Nanos
TransportManager -> Transport
ChunkManager -> Chunking
BuddyManager -> Buddies
GroupManager -> Groups
RacingManager -> Racing
ItemManager -> Items

NPCManager and PlayerManager remain.

Note: You can use git log --follow src/file.cpp to trace the history of
a file from before it was renamed.
2021-03-17 20:16:43 +01:00
cee09f6344 [refactor] Mark all internal functions static
All packet handlers and helper functions that are only used in the
source file they're declared in have been taken out of the namespaces in
the corresponding header files, have been marked static, and have been
reordered to avoid the need for declarations at the top of each source
file.

Each source file now contains a "using namespace" directive so that the
static functions don't need to prefix the source file's symbols with
their namespace. All redundant namespace prefixes found have been
removed.

An unused nano power resetting function in NanoManager has been removed.
2021-03-16 22:54:41 +01:00
04c56ce426 [refactor] Move Croc Pot logic to Vendor.cpp 2021-03-16 21:08:08 +01:00
2017b38e23 [refactor] Move mob drop logic from Combat to ItemManager 2021-03-16 19:50:33 +01:00
c5776b9322 [refactor] Split Database.cpp into db subdirectory
* Database.hpp is still the only external include file (moved to db/)
* The header is still uppercase to match its namespace
* db/internal.hpp is the shared header for the DB source files
* Added -Isrc/ compile flag for src-relative include paths
* Hoisted CHDR above CSRC in Makefile (it was bothering me)
* make clean now removes all objects in the subdirectories as well
2021-03-16 02:13:24 +01:00
gsemaj
dd41d5b610 [refactor] Split vendor functions and crocpot out of NPCManager 2021-03-15 10:48:27 -04:00
gsemaj
8981ad8c14 [refactor] Separate email functions out of BuddyManager into Email 2021-03-15 10:29:54 -04:00
df1ac82300 [refactor] Separate internal and external DB functions 2021-03-15 00:36:20 +01:00
ec67cc6527 CNServer::start() now uses an iterator instead of indexing repeatedly 2021-03-14 01:33:46 -06:00
2024fb4969 [refactor] Split MobManager.cpp into MobAI.cpp and Combat.cpp
This is terrible. It was a mistake to do this before cleaning up the
actual code. It might be better not to use this commit and to do this
refactor in a different order or something.
2021-03-13 23:55:16 +01:00
ae279100d7 [refactor] Extract Abilities.cpp from {Nano,Mob}Manager.cpp
I've kept all the functions in their original namespaces for now, since
putting them all into the same one will cause collissions, and this is
all getting rewritten soon anyway.
2021-03-13 21:22:29 +01:00
e92a5a2f8b [refactor] Split ItemManager.cpp into Vendor.cpp & Trading.cpp
- added sources to Makefile
- Added Trading::init() to main.cpp
2021-03-12 20:09:45 -06:00
ce197d7db3 [refactor] Extract PlayerMovement.cpp from PlayerManager.cpp 2021-03-13 02:59:18 +01:00
f9c2587557 [refactor] Extract BuiltinCommands.cpp from PlayerManager.cpp
And move itemGMGiveHandler() from ItemManager.
2021-03-13 02:59:18 +01:00
2d7129111a [refactor] Refactor ChatManager
* Extracted all commands into CustomCommands.cpp
* Moved all chat-related packet handlers into ChatManger.cpp
* Cleaned up redundant includes
* Unified handler naming scheme
* Made all command handlers in CustomCommands.cpp static
2021-03-13 02:59:18 +01:00
4cd3a3dabd [refactor] src/contrib, src/mingw -> vendor 2021-03-13 02:58:57 +01:00
f7e9cc2cea Disallow attaching the same item to an email twice
Also fix vendor buying validation not allowing crates to be bought,
since apparently their maximum stack size is 0 in TableData.
2021-03-10 00:13:46 +01:00
89eb0b140b Use the right packet for when a player falls out of the world
SUDDEN_DEAD is more appropriate than goo damage for this.
Also made it so other players can see when someone does a /health 0, for
comedic effect.
2021-03-09 21:23:57 +01:00
f5a34b9a3d Reject completion packets for missions that aren't in progress
Also reject players requesting more than 6 missions.

This is just a minimal measure to prevent replaying mission completion
packets. This part of the codebase will be largely refactored soon, so
more through changes can wait.
2021-03-09 18:30:58 +01:00
ffe5947925 Keep track of sold items so we can validate buyback packets 2021-03-09 16:45:38 +01:00
0fbdb1dad2 Improve sanity checks when opening crates and combining items
And ignore ITEM_MOVE packets while trading.
2021-03-08 22:31:25 +01:00
d781fae3ba Merge-in the general changes that were on the injusticefoe branch 2021-03-07 15:56:11 +01:00
3445c0bbc3 Tweaked mob and nano skills 2021-03-07 15:56:02 +01:00
540c37a523 Aggro is now affected by level 2021-03-07 15:56:02 +01:00
33a26cda7c Split mob heal types 2021-03-07 15:56:02 +01:00
dc6de46a1f Added ON_COMBAT trigger 2021-03-07 15:55:51 +01:00
c5e08b81da Implement /ban command 2021-03-07 00:38:24 +01:00
5e569d4324 Disallow selling Croc-Potted items
Also, make sure to explicitly terminate the connection when a player is
kicked, and align a few fields in tables.sql.
2021-03-07 00:38:24 +01:00
f2b1a84ef4 Fix segfault when redeeming more than four items at once 2021-03-06 02:09:21 +01:00
d5fe1cc513 Work around not being able to reach the shard from a local connection
In certain circumstances, like when running a private server through
Hamachi, the shard IP will be set to an address the local machine can't
reach itself from, preventing only the local player from getting past
character selection. This workaround detects local connections and
sends a loopback address for the shard instead of the configured one.
This makes those use cases feasible.
2021-03-05 19:00:13 +01:00
81c2a2a8b3 Mob Leech and Freedom 2021-03-05 14:34:08 +00:00
f7c84c62ed Possibly fixed item duping via trading 2021-03-05 14:18:36 +00:00
da8dde9818 Do not dynamically allocate memory in CNSocket::sendPacket()
Also reorder the rapid fire check in MobManager::pcAttackNpcs(), so the
output packet validation happens immediately before the buffer is
initialized, for clarity.
2021-03-04 19:51:43 +01:00
Gent
29dbe83a0b Skip item stacking logic if items not found in XDT 2021-03-04 11:22:01 -05:00
5fdef50f0f Fix failure to summon the highest NPC_ID
And update tdata.
2021-03-03 23:17:36 +01:00
Gent
b04c66dea7 Switch AppVeyor Linux builds to Ubuntu2004 2021-03-01 11:42:23 -05:00
CakeLancelot
f0f3eaf749 README Revisions 2021-02-24 12:35:04 -06:00
217168fe50 Improve DB and Nano sanity checks
I'm aware that the DB checks still allow ID 0 items and Nanos, but the
point of those is primarily to prevent invalid memory access.
2021-01-27 02:27:08 +01:00
CakeLancelot
04a17ed862 Record claimed code items, and other misc DB fixes
* Create new table to store redeemed codes
* Check if a player already used a code when using /redeem
* Change Coordinate columns to non-plural form
* Fixed EmailItems unique constraint not being specific enough
* Bumped DB version to 3
2021-01-19 14:05:54 -06:00
74af1ad173 Set iOpt to 1 for mission rewards and disallow trading iOpt 0 items
Co-authored-by: Jade <jadeshrinemaiden@gmail.com>
2021-01-17 22:57:07 +01:00
CakeLancelot
b0697f12a3
Update README with link to commands on the wiki 2021-01-08 11:23:44 -06:00
34bd7c102f Validate emails as they're being sent 2021-01-06 14:30:25 +01:00
CakeLancelot
9e30e55669 eggBuffPlayer now takes duration, and buff duration is read from EggType 2021-01-06 05:56:54 -06:00
46b6d9fcc7 Include CNStructs.hpp in settings.cpp for the ACADEMY define
This fixes the spawn point being wrong.
2021-01-06 12:30:33 +01:00
2bf3fd0975 Further sanity checks for shops 2021-01-06 11:47:07 +01:00
442f85c7a6 Make paths.json academy-specific 2021-01-05 14:07:41 +01:00
b87229aa65 Reject requests to equip items into the wrong slot
This is important because the client can genuinely send such an invalid
packet by mistake during normal gameplay.

If a sanity check fails, we don't need to send any sort of "move it but
keep it where it is" packet, since simply ignoring the invalid request
doesn't softlock the client.

Also improved validation of inventory slot indexes.
2021-01-05 13:17:59 +01:00
deca220d43 Do not auto-register all fast travel destinations for GMs
Instead, players with access level 50 or higher can use /registerall and
/unregisterall.
2021-01-05 12:44:38 +01:00
74e06f1084 Trading Refactor
- Its no longer possible to dupe items by stacking inventory slots in a trade.
- Stacked items work correctly now.
2021-01-04 23:57:50 +01:00
ddc7caf959 Basic Anti rapidfire 2021-01-04 23:57:50 +01:00
6baa0c5b07 Group related fixes
- Group adding is not leader only now
- Group buffs work now
2021-01-04 23:57:50 +01:00
d4eaf83354 Fixed Instancing Bugs 2021-01-04 23:53:44 +01:00
47d13ce39e Fixed Group Mob Retreat 2021-01-04 23:53:38 +01:00
2b95bc660c Fixed Quest Item Bug 2021-01-04 23:52:15 +01:00
0e3fac4d34 Updated tdata and changed how the default JSON paths are resolved 2021-01-04 17:08:44 +01:00
CakeLancelot
89e4b2be22 Re-add descriptive artifact names 2021-01-02 10:22:35 -05:00
Gent S
28543641bb Properly parallelize Windows builds 2021-01-02 10:22:35 -05:00
Gent S
888f0e77f9 Use GCP instead of default cloud for AppVeyor builds 2021-01-02 10:22:35 -05:00
4516227a7b Fix a few GM issues
* Invert access check when kicking players
* Add validation to ensure only GMs can PVP
* Account for instance in /teleport2me
2021-01-01 21:38:03 +01:00
Gent S
954cfabde5 Add ID gaps for missing NPCs in Academy 2020-12-31 11:26:17 -05:00
52e3c3bcd7 Comment out the spawn coords in config.ini
They're not often changed, and keeping two sets of them in the config
file is messy when the right set is automatially chosen by the ifdef in
the server itself.
2020-12-31 14:13:14 +01:00
966bd3edd2 Group members share the same mob drops
This includes quest items.
2020-12-31 14:13:14 +01:00
dab204ddaf If compiled for Academy, do not spawn NPCs in the Future zone 2020-12-31 14:13:08 +01:00
bad8ef1d10 Kill players that fall out of the map 2020-12-31 12:51:36 +01:00
a12acbb68f Implement most of the remaining client-side GM commands
* Muting a player's freechat
* Kicking players
* Querying info about a player
* Teleporting yourself to a player
* Teleporting a player to yourself
* Teleporting a player to another player
* Teleporting a player to arbitrary coords
* Teleporting a player to arbitrary coords in an arbitrary mapnum
* /unstick

Also:
* Renamed misleading setSpecialPlayer() to setValuePlayer()
* Revamped monitor logic
* Added server-side checks to account level 50 commands
* Made sure even trade chat is run through sanitizeText()
* Moved setSpecialState() closer to its calling functions
* Interpret client commands even in Buddy and Group chat (but not in
Trade chat)
2020-12-31 12:51:31 +01:00
c78b3ca69f Do not cancel the ongoing race on recall
Also do not remove the player's vehicle if the player isn't on a
vehicle.
2020-12-31 03:00:54 +01:00
55431362a7 Make sure the current race is cleared when leaving an IZ 2020-12-28 18:40:26 +01:00
07a930fe1c Fix vehicles in IZs for real this time 2020-12-28 18:40:26 +01:00
4060bf25b0 Fix recall removing FM pods if racing 2020-12-28 16:55:13 +01:00
9a79ab3927 Reduce mob range when racing as if the player were sneaking 2020-12-28 16:41:29 +01:00
fc45775666 Add /unwarpable command
GMs should use this before going to weird places where their non-GM
buddies might warp to them and get stuck.
2020-12-28 16:24:24 +01:00
81d0964971 Disallow warping to players using the MSS 2020-12-28 16:13:38 +01:00
868dc8485e Allow GMs to enter private instances 2020-12-28 16:12:57 +01:00
26f4767082 Add additional validation to the recall power 2020-12-28 16:12:23 +01:00
d97444cca5 Remove each group member's vehicle when warping into an instance 2020-12-28 00:50:58 +01:00
ee978e8bc9 Limit group member drops based on proximity 2020-12-27 21:14:16 +01:00
Gent S
bdf283ae4f Make heal nanos heal for the correct amount 2020-12-27 09:23:43 -05:00
f8129b91cb Zero both players' moneyInTrade when initiating a trade 2020-12-26 20:13:23 +01:00
afea9f436f Check if otherPlr is null in nanoRecallHandler() 2020-12-26 20:09:33 +01:00
7985fc475b Fix regression with spawning in an invalid Nano #37 2020-12-24 07:41:22 +01:00
959a708176 Return to the overworld instance when warping to a buddy 2020-12-24 06:05:05 +01:00
44fbb8e81f Fix other group members not respawning properly in Lairs 2020-12-24 05:22:46 +01:00
e02ef55844 Remove excessive indentation in tables.sql 2020-12-24 05:19:04 +01:00
CakeLancelot
8bbf40ac95 Update README.md 2020-12-23 18:31:48 -05:00
Gent S
7fe0e19bb0 Fix IZ races rewarding invalid items 2020-12-23 18:31:42 -05:00
Gent S
82d5455da6 Ignore race rewards for EPs that aren't found 2020-12-23 14:53:56 -05:00
Gent S
52389c2c69 Make code redemption case-insensitive 2020-12-23 13:50:12 -05:00
Gent S
ae75324153 Fix duplicate tasks on instanced mission failure 2020-12-23 13:50:12 -05:00
Gent S
a1a5815f1f Fix academy nano missions not going into the 0th slot 2020-12-23 13:50:12 -05:00
Gent S
772f80188e Prevent duplicate running tasks from loading into the Player struct 2020-12-23 13:50:12 -05:00
kamilprzyb
f28c643b48 Allow permanent vehicles 2020-12-23 13:50:12 -05:00
Kamil
5f82658c8d adjusted item codes logic to handle multiple items per code 2020-12-23 13:50:12 -05:00
ff75aa6693 Add the Academy build to the Appveyor script 2020-12-23 13:50:12 -05:00
50b2bdcb16 Fixed Nano Style Issues 2020-12-23 13:50:12 -05:00
Kamil
34dbb59fb1 Implement redeem codes 2020-12-23 13:50:12 -05:00
Kamil
a74c9be2ff Implement Nano capsules 2020-12-23 13:50:12 -05:00
fcdea2e723 Implement multiple-choice mission rewards 2020-12-23 13:50:12 -05:00
90191fd494 Add academy.json loading to TableData
This is where the permanent Academy-exclusive NPCs and mobs will be
loaded from. Resurrect 'Ems work now.
2020-12-23 13:50:12 -05:00
Gent S
effbbd9a5e Fix level-up not triggering and using wrong packets 2020-12-23 13:50:12 -05:00
bc7f4883a2 Add Academy defaults to the config file 2020-12-23 13:50:12 -05:00
e0808ffcbd Read the NPC ID limit from the XDT 2020-12-23 13:50:12 -05:00
9057f31bff Implement Academy level-up 2020-12-23 13:50:12 -05:00
0a9f637123 Do not give the Lightning Gun and Buttercup Nano to new Academy players
And do not mark the two missions from the old Tutorial as completed.
2020-12-23 13:50:12 -05:00
027f513a23 Save Academy Nanos to DB 2020-12-23 13:50:12 -05:00
a99f95d15f Initial Academy support
Support for the Academy's Nano system is complete, but they're not being
saved to the DB yet.
2020-12-23 13:50:12 -05:00
Gent S
49158360ca Update tdata ref 2020-12-23 13:48:19 -05:00
0104bc9329 Enemy ability tweaks
- Battery drain does not set your battery below 0.
- Debuffs cast by enemies take into account intensity now. (Snare will work)
- Used int_max to replace arbitrary 20000, added more comments.
2020-12-22 12:12:02 -05:00
4fc5c092f8 Attack logic and various balance changes
- combatStep was altered, the mob can attack and give chase at the same time. Kiting melee mobs around is much harder.
- Mobs in general are more harder, closer to how it was in retro.
- Nanos styles are less detrimental to battle.
2020-12-22 11:30:57 -05:00
8d1bc94b7e Mission Handling Fixes
- Q. Item count glitches are dealt with
- Warping out now correctly rewinds missions.
2020-12-22 11:30:57 -05:00
322e354f5b Fix indentation in RacingManager.cpp and compilation on Linux 2020-12-22 14:46:09 +01:00
Gent S
8a2073d081 Fix racing timestamps 2020-12-22 01:04:46 -05:00
Gent S
e915c54ed0 Correctly set time limit, rank, and reward 2020-12-22 00:37:04 -05:00
Gent S
e953b51229 Add reward tables for racing 2020-12-22 00:36:39 -05:00
Gent S
b9013149f3 Racing cleanup 2020-12-21 21:02:19 -05:00
Gent S
e6da454c73 Finish racing DB integration 2020-12-21 16:18:08 -05:00
Gent S
598c7ce1d0 Add racing-related DB functions 2020-12-21 16:18:08 -05:00
Gent S
ea47f67b2b Properly utilize instance data in map info packet 2020-12-21 16:18:08 -05:00
Gent S
91ea8be72e Implement basic race handling 2020-12-21 16:18:08 -05:00
Gent S
b81a3761b6 Load instance data from XDT 2020-12-21 16:18:08 -05:00
Gent S
45b8f8f581 Add RacingManager 2020-12-21 16:18:08 -05:00
Gent S
cfc6a94c7a Include sql directory and config.ini in AppVeyor artifacts 2020-12-21 16:02:58 -05:00
c3d9883ddb Fix migration on Linux 2020-12-20 23:09:10 +01:00
Gent S
ea12ec9607 Use binary streams to make DB backups, since filesystem has low compat 2020-12-20 15:52:34 -05:00
Gent S
2e173df2ca DB v2 Tweaks 2020-12-19 21:17:07 -05:00
Kamil
1fb48536c2 created external sql file for tables and 1st migration 2020-12-19 20:00:47 -05:00
Kamil
002bfffb62 implemented db migration system 2020-12-19 19:15:14 -05:00
Kamil
140227406c implemented custom error messages in login screen for regex fail or banned account 2020-12-19 19:15:14 -05:00
26d0623d07 Fix Database::sendEmail()
* Do not define an sqlite_stmt* named stmt in a loop such that it shadows
an outer variable of the same name.
* Do not re-prepare a statement that has been reset.
2020-12-19 05:51:06 +01:00
f8a359dfe9 Do not truncate emails to 127 characters
And assert that we never supply a string long enough for
sanitizeText() to truncate.
2020-12-19 05:51:06 +01:00
Gent S
2f44243abb Please.... please take the slider.... 2020-12-18 20:48:09 -05:00
e3561e9d15 Change the default ports to match OG and Retro
The default monitor port can stay on 8003 for now.
2020-12-18 00:24:24 +01:00
80dabf4406 Finalize ALL the statements
* Fixed not being able to modify the DB externally while the script is
running
* Made most DB-related errors print the appropriate error string
* Fixed the settings looking for dbsaveinterval in the shard category
instead of the login category
* Added -shr and -wal to .gitignore (even though we're not actually
using WAL mode)

Hopefully the DB code is now free of resource leaks and undefined
behaviour.
2020-12-18 00:17:35 +01:00
Gent S
d3e5b9c485 Potentially fix slider collisions
- Lerp circuit once instead of on a per-slider basis to avoid FPE differences
- Spawn sliders every k points instead of once per stop
2020-12-16 20:37:12 -05:00
f7a6615379 Tweak mob roaming logic and a few other values
This fixes the crash with mobs with a very small m_iIdleRange and avoids
unnecessary looping.

Co-authored-by: JadeShrineMaiden <69916714+JadeShrineMaiden@users.noreply.github.com>
2020-12-17 02:35:55 +01:00
Gent S
618a8d0a9f Reimplement NPC barking 2020-12-15 19:16:05 -05:00
d25e7ca4fc Implement rudimentary NPC scripting framework and Lord Fuse boss fight 2020-12-15 23:19:29 +01:00
3359ca0c3e Moved common NPC summoning logic into a helper function 2020-12-15 23:19:29 +01:00
0dd478b9f0 Tweak Lair respawns slightly
* Respawn the player in the air, not on the ground
* Do save the player's current instance, just to lessen the chance of
validation not catching stale recall coords.
2020-12-15 23:19:29 +01:00
Gent S
d03c4f109f Add chat dump to monitor 2020-12-15 09:58:11 -05:00
Gent S
792a317b48 Indicate high account level in logged player name 2020-12-15 09:53:45 -05:00
f74c40cf69 Prevent division by zero by also checking iWalkSpeed for 0 2020-12-15 02:11:18 +01:00
442d7853a5 In the absence of a Resurrect 'Em in a Lair, respawn at the entrance 2020-12-15 00:26:29 +01:00
CakeLancelot
f1aa2c19ef
Add dependency documentation to README 2020-12-14 15:57:04 -06:00
fcd9b55ea7 Clean up formatting in Database.cpp
* Single quotes for strings
* Semicolons at the end of all commands
* No double colons around identifiers
* Spaces for alignment
* Some parts indented for readability

Not everything is perfectly consistent, but it should all be pretty
readable now.

Non-SQL changes:
* Opening braces shouldn't be on their own line
* Removed trailing spaces
* exit(1) should be used instead of terminate(0) during server init
2020-12-14 22:41:28 +01:00
CakeLancelot
b1375c69f5
Add lsqlite3 flag to Windows section of Makefile as well 2020-12-14 10:39:26 -06:00
CakeLancelot
51c3e01062
Tweak default viewdistance value in config.ini
Having it at 25600 caused client performance issues in areas with a lot of mobs, 12800 was too short, but after testing 16000 on the public server I feel this is a sensible default.
2020-12-14 10:36:02 -06:00
Gent S
f1a2723274 Don't write mob HP to gruntwork 2020-12-14 10:00:07 -05:00
Gent S
5431d21d27 Always grab mob HP from XDT 2020-12-14 00:26:17 -05:00
CakeLancelot
57c28d7539
Appveyor: grab sqlite3 using vcpkg 2020-12-13 22:52:17 -06:00
6937ff86a4 Disable MSVC Debug builds in appveyor.yml 2020-12-14 03:49:52 +01:00
454e0284af Remove vendored libsqlite from the repository
We now link to the system's library.

Windows implementation pending. MSVC build will be broken for a short
while.
2020-12-14 03:44:58 +01:00
Gent S
94b7864b02 Shut up warnings 2020-12-13 20:52:59 -05:00
Gent S
166bfdfc4f Terminate if existing DB doesn't have meta table 2020-12-13 20:43:24 -05:00
Gent S
156e9bf902 Fix newline characters being stripped from emails 2020-12-13 20:18:36 -05:00
Gent S
c8ff130b78 Update buddy warp to new DB 2020-12-13 19:54:09 -05:00
Gent S
ec23e72215 Notify client of email send failure 2020-12-13 19:51:31 -05:00
Gent S
a1274756ce Fix guide not getting updated in new DB 2020-12-13 19:51:09 -05:00
Kamil
05f4746af4 "Created" meta column + cleanup 2020-12-13 19:51:09 -05:00
Kamil
04112377ea Implement setting for disabling first use tips 2020-12-13 19:51:09 -05:00
Kamil
bd3a91e530 Properly implement first use flags 2020-12-13 19:51:09 -05:00
Kamil
66ecc45fce Change SkyWayFlags to blob for consistency 2020-12-13 19:51:09 -05:00
Kamil
7aef973ef1 Add meta table to DB 2020-12-13 19:51:09 -05:00
Kamil
a12faac0e2 Account level changes
tied account levels to accounts instead of players. implemented account banning logic in db
2020-12-13 19:51:09 -05:00
Kamil
4dc48198ab Implement player blocking 2020-12-13 19:51:09 -05:00
Kamil
3e855cbdac Close DB properly 2020-12-13 19:51:09 -05:00
Kamil
889fc985c4 Reimplement the bank 2020-12-13 19:51:09 -05:00
Kamil
c709d458f4 Fix iterators and query typo 2020-12-13 19:51:09 -05:00
Kamil
0e016646ef Properly implement PayZoneFlag and fix safe coord loading
implemented saving PayZoneFlag (is player in the past) and fixed saving coordinates properly with monkeys/instances
2020-12-13 19:51:09 -05:00
Kamil
960f2dd10c Adjust login server logic to new DB 2020-12-13 19:51:09 -05:00
Kamil
2bad1252d3 Small DB fixes 2020-12-13 19:51:09 -05:00
Kamil
31ac9d2e3b Reimplement DB functions 2/2
implemented getCharInfo
implemented delete character
implemented evaluateCustomName
implemented name change function
implemented getPlayer function
implemented functions for buddies
implemented emails
2020-12-13 19:51:09 -05:00
Kamil
eeb3b1ee61 fixed a lot of queries 2020-12-13 19:51:09 -05:00
Kamil
4bf35e5239 added some unique constraints 2020-12-13 19:51:09 -05:00
Kamil
2f5c2a8764 Reimplement DB functions 1/2
implemented addAccount
implemented updateSelected
implemented findAccount validateCharacter isNameFree and isSlotFree
implemented getTableSize function
implemented functions for name save, character creation and finishing tutorial
2020-12-13 19:51:09 -05:00
Kamil
34ca36062c Create basic tables 2020-12-13 19:51:09 -05:00
Kamil
a1062f220b Open DB from settings path 2020-12-13 19:51:09 -05:00
Kamil
c2f640fd97 RIP ORM 2020-12-13 19:51:09 -05:00
actuallyamoose
143bb00ac0
Added checks to prevent buddy warping and item sending across time (#183) 2020-12-13 19:05:11 -05:00
JadeShrineMaiden
b947ff65cf
Various bugfixes
- Eruption is now blocked by stun and sleep.
- Corruption should block all nano abilities.
- Buffs time out for other players
- Timed mission bugfixes (AGAIN)
- Corruption and Eruptions fire quicker.
- Heal egg ids fix
- No power nanos no longer break the system.
- Mobs should no longer restun.
- Mob ability chance calculation adjustments.
- Duration of the power's debuff is sent as iDamage instead of 0, this removes the ugly "Block" that shows up on successful hits.
- Group mob respawning bugfixes
- a bit of a cleanup
2020-12-12 17:22:22 -05:00
1474ff10ac Slight adjustments to the poll() loop
Recheck the entry at the current index after we remove one. This isn't
strictly necessary, since the next pass will get it anyway.

Using a vector as opposed to our own realloc()'d array means indexing
beyond the size() is undefined behavior, so it's better to be safe.
2020-12-12 01:27:28 +01:00
974941f4fa Close our end of the connection when we receive an EOF
This should/might fix the server's CPU usage spiking to 100%.
2020-12-12 01:27:13 +01:00
Gent S
2834891727 Properly output socket errors on Windows 2020-12-08 17:22:14 -05:00
ada8af0b82 Print informative error messages for socket operations
Windows implementation pending.
2020-12-08 15:23:58 -05:00
86e0b1bc13 EINTR is not crash-worthy
This is how it should have been handled anyway. This fixes a (benign)
race condition when the server is killed, as well as gprof builds.

Also updated tdata.
2020-12-08 20:10:02 +01:00
Gent S
d5409ed3f1 Replace bad continue statement with goto in monitor socket iteration 2020-12-08 14:05:34 -05:00
811c9d4d5c Stop iterating in exitDuplicate() after having removed a player
This fixes a crash due to the invalidated iterator continuing to loop.
It should be safe to assume there's only ever one duplicate/stale player.

Also updated tdata.
2020-12-08 01:53:21 +01:00
Gent S
a48fb3a8e8 Don't run nano mission logic at level 36 (fixes "Super Saiyan" bug) 2020-12-06 11:53:41 -05:00
27f396af7e Oops, M_PI is POSIX so it isn't defined in math.h on Windows 2020-12-06 06:02:58 +01:00
8ebabac7c0 Various bugfixes
* Fixed Nano stamina not being halved on respawn
* Reverted the default argument to terminate() change because MSVC is
undable to disambiguate the function pointer passed to sigaction()
* Fatal errors during init (like in TableData) can just call exit(1)
directly anyway (missing "OpenFusion: terminated." be damned)
* Switched to a slightly more portable syntax for getting the version
in the Makefile
* We shouldn't join the shard thread in the signal handler because the
thread the signal handler ends up running in is undefined behaviour and
we don't strictly need to join it anyway

Many of these issues were discovered on OpenBSD.
2020-12-06 05:25:23 +01:00
02c5df5c1b Ours is not to decide the value of pi.
Ours is only to live by it.
2020-12-06 02:45:20 +01:00
dd6fbfb683 Tweak terminate() slightly
* Gave it a default argument, since we never actually care about it, but
it needs to have it to conform to the signal handler prototype
* Constricted the area locked by activeCrit to only the block that deals
with the connections vector, to lower the chance of a future badly
placed call to terminate() deadlocking the server instead
2020-12-06 02:20:46 +01:00
92307063fc Integrate the monitor's listener socket into the shard's poll()
This removes the need for a separate monitor thread.
2020-12-06 01:44:37 +01:00
53d8cb67ba Set monitor connections to non-blocking
Better to just drop monitor connections when the quality is bad than to
risk blocking the shard thread.
2020-12-05 23:16:09 +01:00
6b257887b7 Use an std::vector for PollFDs instead of handling it manually
Lowered poll() timeout to 50, to fix (work around?) slider stutter.
2020-12-05 22:51:17 +01:00
ec7cba644c Clean up polling logic
* Extracted PollFD manipulation and nonblocking socket configuration
into helper functions
* Replaced the connections list with an unordered_map
* Dynamically grow the number of PollFD structures with realloc()

With these changes done, the server's CPU usage is completely diminished
from its old average of ~47% to ~0.07%, with occasional spikes up to ~14%.
2020-12-05 22:47:50 +01:00
269315ca09 Support for poll() (ie. WSAPoll()) on Windows 2020-12-05 22:47:50 +01:00
661061b4eb Switch to non-blocking sockets + poll(); prepare for Windows logic 2020-12-05 22:47:50 +01:00
3a2b488f33 [WIP] fixed a million bugs and one potential one 2020-12-05 22:47:50 +01:00
721b3f440f [WIP] Switched to poll(), but only on Unix 2020-12-05 22:47:50 +01:00
Gent S
b04c377d7c Properly implement item types. 2020-12-04 16:42:58 -05:00
Gent S
794856a63c Clean up item loading from XDT. 2020-12-04 13:57:08 -05:00
Gent S
856a90abcf Fix nano logic in goo damage running without a nano out 2020-12-04 11:59:53 -05:00
88d904e302 Fix FM patches not dealing damage unless invulnerable 2020-12-04 17:48:12 +01:00
46552307cd Disable monitor by default in config file 2020-12-04 17:30:21 +01:00
26024de866 Support the monitor interface on Windows as well
This change required sacrificing both code cleanliness and the specific
error reports from perror(). Those could have been kept with a portable
wrapper, but that's too much work. We'll do it if unforseen errors
arise.
2020-12-04 17:25:32 +01:00
Gent S
85dcdd4cc5 Added /hide command to exclude players from monitor output 2020-12-04 07:24:33 -05:00
Gent S
6e7129bf6f Update tdata ref 12/3 2020-12-04 06:51:30 -05:00
Gent S
1ca8094628 Fix regression with disconnected player on Skyway agent. 2020-12-03 15:33:14 -05:00
Gent S
543d0a7afd Fix edge case for nano mission acquisition
This caused players to only get nano missions after exceeding the required FM instead of meeting it.
2020-12-03 00:05:14 -05:00
Gent S
4c398895aa Make /lair fit convention. 2020-12-02 18:41:09 -05:00
e899928928 Do not try to compile the monitor on windows 2020-12-03 00:15:06 +01:00
c8b011913a Make monitor parameters configurable 2020-12-02 23:42:33 +01:00
15b63f3cbd Fix regression with a disconnect check in MobManager::combatStep() 2020-12-02 23:32:38 +01:00
ce1a5a7664 Handle partial transmissions of the buffer to the monitor 2020-12-02 20:19:34 +01:00
c6112d04da Implemented player position monitor interface 2020-12-02 20:19:34 +01:00
Gent S
73f8179836 Make aggro space spheroidal on Z
By doubling the z difference used in distance calculation, mobs above or below the player will aggro less often.
2020-12-02 09:20:33 -05:00
Gent Semaj
86f17b6525
Show announcements in server log 2020-12-01 19:59:18 -05:00
fed3eca378 Display area announcement to invoking player as well
Also clarified that shard- and world-level announcements will not be
implemented as they are not applicable to our single-shard server.
2020-12-01 23:32:45 +01:00
840cba6a9e Removed obsolete ASAN suppressions
Also added a missing suppression for doDamageNDebuff().
2020-12-01 23:32:20 +01:00
fb9c4140b6 Remove redundant namespace specifiers in PlayerManager::init() 2020-12-01 22:24:18 +01:00
657061083e Lose aggro and do not take damage if invulnerable 2020-12-01 21:37:34 +01:00
8a86c75747 Make sure each group member is alive before healing/buffing them 2020-12-01 21:37:34 +01:00
046e7bb6f1 Do not aggro on players with /invulnerable 2020-12-01 21:37:34 +01:00
1e822f7a6c Removed redundant checks for a nullptr from PlayerManager::getPlayer()
These were written in desparation and never should have been there in
the first place.
2020-12-01 21:37:34 +01:00
c43a3d64fb Removed the Player pointer from CNSocket
It was a leak in the abstraction that we weren't even using that much.
This is technically a de-optimization, but it's okay since it's not a
hot code path.
2020-12-01 21:37:34 +01:00
Titan
7c5d7a70cc Fix enter key sending '\n' in passwords 2020-12-01 14:29:21 -05:00
Titan
c1941654b6 Basic Implementation of a lair unlock command 2020-12-01 14:19:16 -05:00
Gent S
d2d6171d04 Fix gumballs only lasting 1 minute instead of 10 2020-12-01 13:47:54 -05:00
58952be47e Added silent unsummoning
- summonNano now supports an optional "silent" argument
- gumballs now last 10 minutes.
- fixed comments
2020-12-01 13:16:14 -05:00
5771cd014a Various bugfixes and tweaks
- timed missions of all types should work.
- nanos now transmit an unsummon on 0 stamina.
- dying bumps your nanos down to half stamina now.
- enemies use abilities less frequently.
- group recall now works at any distance.
- passive nanos are tweaked to guzzle less stamina.
- cleared out some redundant stuff at the nanoPower handler.
2020-12-01 13:16:14 -05:00
Gent S
c9754902b9 Instance fusion lair NPCs, remove non-mob fusions, stub non-mob paths 2020-12-01 06:07:10 -05:00
Gent S
c508016ca3 Play nano mission animation for nearby players 2020-11-30 20:36:20 -05:00
Gent S
4863d29590 Fix /unsummonW crash on non-mob NPCs 2020-11-30 10:53:34 -05:00
CakeLancelot
128aad89d3 Update tdata ref to include all overworld mobs 2020-11-29 19:31:50 -06:00
858fbf40be Do not strip newlines from email bodies 2020-11-29 21:31:54 +01:00
Gent S
1d7f8bd133 Mob abilities cleanup 2020-11-28 12:41:00 -05:00
a9ad399bc2 Various Tweaks to Mob skill behavior
- Fixed enemy abilities playing after death
- Nerfed frequency further
- Heal on retreat correctly
- Nerfed damage and corruption skill damage
2020-11-28 11:41:32 -05:00
963205fad6 Added Nullptr check + Made mob abilities less frequent 2020-11-28 11:41:21 -05:00
b836952356 Restructured Arrays to Vectors 2020-11-27 19:02:54 -05:00
6fb652f642 Corruption reflection + Extra adjustments 2020-11-27 16:33:22 -05:00
2cde3e34f6 Active skills, Corruption and Eruption 2020-11-27 16:33:18 -05:00
1371a6da77 Base Variadic MobPowers Handler 2020-11-27 16:32:59 -05:00
d2e89851d6 Non-group buffs do not misbehave + Revive nano stamina fix 2020-11-27 15:40:49 -05:00
Gent S
7cc0a0fc82 Fix group recall and temporarily lift range restriction 2020-11-27 15:31:56 -05:00
c343092bd5 Gumball times out for slots 2 & 3 + Tweaks 2020-11-27 12:01:28 -05:00
Gent S
36d6231da4 Nano refactor cleanup
sanity checks and int[] -> vector<int>
2020-11-27 00:07:31 -05:00
Gent S
7851866d13 Fix nanos making sound when they run out of stamina 2020-11-26 23:01:19 -05:00
Gent S
c2ab5c9d02 Fix case where resplen wasn't being initialized 2020-11-26 23:01:19 -05:00
166eb5125f Group Heal Range Limit 2020-11-26 23:01:19 -05:00
26ca4d8671 New Powers, Tweaked Eggs further + Cleanup
* Scavenge and Bonus nanos work.
* Eggs now damage and heal more cleanly.
2020-11-26 23:01:19 -05:00
28276d2229 Reverting back some changes + Tweaks 2020-11-26 23:01:19 -05:00
2b25b17bd8 Gumball Handling and Bugfixes
* Gumballs now run out of timer.
* Reverted a regression, (gumballs with 0 iOpt do not get emptied).
* Nano matchups now work correctly.
2020-11-26 23:01:19 -05:00
f7c0596a4c Adapted Egg Buffing to Nano powers
* Adapted eggs to nano power data.
* Tweaked nano abilities.
2020-11-26 23:01:19 -05:00
fe7ec44554 Restructuring, Cleaning up and Bugfixes
* Added findTargets, a new helper function that reads out the packet for targets.
* Restructured and removed leftover redundant code.
2020-11-26 23:01:19 -05:00
299fc1b461 Refactored Nano Powers
* All nano power functions have been merged into one goliath of a function.
* Nano powers consume the correct amount of stamina.
* Bugfixed gumball issues, gumballed nanos now perform better.
* Revive powers now work correctly.
* Recall powers both self and group are functional.
* Removed nanoBuff.
* Added a new applyBuff function, this allows for quick and easy application of nano skills.
* Numerous other bugfixes.
2020-11-26 23:01:19 -05:00
2acb90f2d2 Initial tabledata implementation 2020-11-26 23:01:19 -05:00
Gent S
75d33aff3e Fix S.C.A.M.P.E.R. chunk desync... again 2020-11-26 22:58:56 -05:00
Gent S
8073c68bd5 Make /fusionmatter properly update fusion matter 2020-11-26 22:33:43 -05:00
Gent S
15dd0a2fc3 Add missing sanity checks to BuddyManager 2020-11-26 22:30:30 -05:00
Gent S
71d1212877 Fix All Characters always failing validation 2020-11-26 11:04:56 -05:00
Gent S
57060e9b6f Fix spawning in the unknown when no Resurrect 'Ems are nearby 2020-11-26 11:03:42 -05:00
Gent S
faf2a0ee7d Fix freefalling after MSS DC 2020-11-26 10:01:48 -05:00
Gent S
fa7b6e6145 Properly set default NPC class 2020-11-26 09:05:44 -05:00
Gent S
478bcd5338 Use vendorID instead of NPC_ID to index into vendor tables 2020-11-26 09:00:30 -05:00
Gent S
c7d3870a60 Add /whois command 2020-11-26 08:59:16 -05:00
Gent S
8351596763 Fix /help using accoutnId instead of accountLevel 2020-11-26 08:30:19 -05:00
Gent S
e3568ea506 Fix viewableChunks memory leaks 2020-11-25 22:49:37 -05:00
CakeLancelot
a9a8d96321
Fix Female Characters always failing validation 2020-11-25 20:37:24 -06:00
Gent S
b0aea27418 Fix player revive packet carrying wrong values
Oops
2020-11-25 20:38:48 -05:00
Gent S
872425640d Prevent DB players from occupying same slot 2020-11-25 19:25:20 -05:00
Gent S
ea5b7104be moved name checks to name save and name change packets
added a TODO

Formatting + sizeof in login server
2020-11-25 19:25:20 -05:00
Kamil
d85d9d4b12 added character creation validation
added a comment
2020-11-25 19:25:20 -05:00
Kamil
7e08bc60ce added character validation for SAVE_CHAR_TUTOR and CHANGE_CHAR_NAME
fixed ugly sizeofs
2020-11-25 19:25:20 -05:00
Kamil
86e6937342 added character validation to delete packet 2020-11-25 19:25:20 -05:00
Kamil
ec8abfa004 moved sending character selection fail to a helper function 2020-11-25 19:25:20 -05:00
Kamil
51a687c7db hugely simplified managing player characters in login server 2020-11-25 19:25:20 -05:00
Kamil
657306e0a1 added missing debug print outs 2020-11-25 19:25:20 -05:00
Kamil
eee8aab888 refactored and cleaned up login function 2020-11-25 19:25:20 -05:00
Kamil
5c6d7d6055 added a sanity check for entering invalid characters 2020-11-25 19:25:20 -05:00
Kamil
4760d91ccd refactored giant switch to smaller functions 2020-11-25 19:25:20 -05:00
Gent S
db98af9775 Fix chunk desync on revive 2020-11-25 17:50:57 -05:00
Gent S
f91f9786d1 Fix chunk desync on S.C.A.M.P.E.R. warp 2020-11-25 17:46:16 -05:00
Gent S
71d9cab72e Fix chunk desync on NPC warp 2020-11-25 17:42:46 -05:00
Gent S
717e5eb78f Remove bad warning for multikills 2020-11-25 13:33:12 -05:00
Gent S
e7301f46ef Properly copy mob groups to instances 2020-11-25 11:49:40 -05:00
Gent S
85113a667f Update unsummonW to handle new group gruntwork format 2020-11-25 11:45:34 -05:00
Gent S
98ae236c08 Load gruntwork group mobs hierarchically 2020-11-25 11:09:05 -05:00
Gent S
6ff762ba57 Save gruntwork group mobs hierarchically 2020-11-25 10:41:10 -05:00
Gent S
73ef5fa5ff Load mob groups hierarchically 2020-11-23 19:28:22 -05:00
Gent S
7d81035306 Fix certain egg types not being found 2020-11-23 18:46:24 -05:00
Gent S
8a0d0e0e4c Restructure tdata 2020-11-23 18:45:28 -05:00
Gent S
0f1d9cdf1c Add sanity check for nano ID in nano equip handler 2020-11-23 17:03:08 -05:00
Gent S
07a1927b9f Force chunk update in sendPlayerTo 2020-11-22 19:22:33 -05:00
Gent S
e0858a42b2 Fix getNearestNPC w/ uses 2020-11-22 19:14:46 -05:00
Gent S
dab536cb6a Grouped mobs adjustment 2020-11-22 19:14:22 -05:00
f1d04cec01 UnsummonW Implementation + Tweaks
* UnsummonW can be used to remove the mob from existence.
* Mob groups now aggro together.
* Mobs space a little bit when chasing the player.
* Combat balance tweaked a bit, you can take out an entire boss group of scoria cephalopod with good nano usage with common tier weapons.
2020-11-22 16:53:19 -05:00
5e8b6eec6e Grouped Mobs are gruntworkable
* Using /summonGroupW
2020-11-22 16:53:03 -05:00
883a1c17e6 Group Mobs Initial Implementation
* For now only mob.json is read for grouped mobs.
* Grouped mobs are fully functional granted the mobs.json is prepared correctly.
* Removed redundant move packet.
2020-11-22 16:53:03 -05:00
Gent S
e9ffbe6148 Make /refresh work again 2020-11-22 16:48:29 -05:00
Gent S
d1baa0d9f9 Replace inPopulatedChunks with player counter for NPCs 2020-11-22 16:48:29 -05:00
Gent S
71d4f331b5 Fix egg type warning message 2020-11-22 16:46:09 -05:00
Gent S
665f28313a Clean up redundant forced chunk updates for NPCs 2020-11-21 11:53:48 -05:00
Gent S
c5fa397724 Fix player entering chunks twice on load 2020-11-21 11:46:56 -05:00
Gent S
95b385dee1 Reimplement chunk data caching 2020-11-19 19:16:51 -05:00
Gent S
82b505a737 Fix buddy warp causing softlock 2020-11-19 18:16:35 -05:00
Gent S
dae3b24093 Reimplement chunk position caching 2020-11-19 17:37:07 -05:00
Gent S
e50a4c2edd Experimental chunk refactor. 2020-11-19 17:37:02 -05:00
Gent S
5cbb8538c0 Get rid of PlayerView. For good. 2020-11-19 17:36:49 -05:00
Gent S
d2e776b672 Fix mission rewards stacking on top of each other 2020-11-19 14:07:29 -05:00
Titan
b08fb52272 Implement /announce 2020-11-19 14:05:34 -05:00
Gent S
0075457f81 Temporarily remove sanity check for egg pickup distance 2020-11-17 13:51:35 -05:00
Gent S
12baece1b2 Fix opening C.R.A.T.E.S. resetting your boost counters 2020-11-17 13:48:20 -05:00
CakeLancelot
eaeeae8d62 Update tdata ref to include latest mob placements
New Areas:
Monkey Foothills - Shadow Wing
Monkey Mountain - Shadow Wing
Eternal Meadows - Shadow Wing
Peach Creek Commons - mclydian
Pokey Oaks South - mclydian
Goat's Junk Yard - mclydian
Candy Cove - K31SHON
Eternal Vistas - K31SHON
Peach Creek Estates - K31SHON
Foster's Home - PotanicMade
2020-11-17 11:19:13 -06:00
Kamil
c77f99e849 moved bullets removal call 2020-11-16 21:38:49 -05:00
Kamil
d17890af68 cleanup 2020-11-16 21:38:49 -05:00
Kamil
6c1d8c3527 updated comment 2020-11-16 21:38:49 -05:00
Kamil
59ab81d3c6 fixed weapon boosts being used with 0 batteryW 2020-11-16 21:38:49 -05:00
Kamil
9cee8f2c87 added cleaning up player's bullets on exit 2020-11-16 21:38:49 -05:00
Kamil
a483b0fb44 fully implemented rockets and grenades 2020-11-16 21:38:49 -05:00
Gent S
8ad3f3aabd CHUNKPOS macro -> ChunkPos typedef 2020-11-16 09:59:53 -05:00
Gent S
b22ba781c8 Possible fix for chunking desyncs + CHUNKPOS macro 2020-11-15 14:41:56 -05:00
Gent
cc74b01f72 Updata tdata ref to include Coco eggs 2020-11-11 23:50:56 -05:00
Gent
4d1d77ceaf Switch to getTime() for egg and buff ticking 2020-11-11 23:50:39 -05:00
Gent
7135767cc4 Egg checks, fixes, and cleanup
fixed not taking egg buff flag into consideration in mob manager

fixed stamina bug and improved sanity check

Egg cleanups
2020-11-11 23:19:07 -05:00
Kamil
0ecf76c5ec moved egg summoning to a custom ChatManager command 2020-11-11 23:19:07 -05:00
Kamil
9087baae3c Polished reading egg json data 2020-11-11 23:19:07 -05:00
Kamil
95b5da8932 temp chunk issue fix 2020-11-11 23:19:07 -05:00
Kamil
f733aa60f0 added a chunk sanity check
removed unneccesary include
2020-11-11 23:19:07 -05:00
Kamil
d102fabc2f set up gruntwork 2020-11-11 23:19:07 -05:00
Kamil
6d4afd0c6a fixed goo damage not taking all flags into account 2020-11-11 23:19:07 -05:00
Kamil
8003518e18 made particles for picking up eggs work, made damage and heal eggs work 2020-11-11 23:19:07 -05:00
Kamil
859b24229a added a condition to not send mob/egg _ENTER packet when it's HP is 0 2020-11-11 23:19:07 -05:00
Kamil
674d5112f3 implemented basic eggs functionality 2020-11-11 23:19:07 -05:00
kamilprzyb
de99522340 added removing buffs on player exit 2020-11-11 23:19:07 -05:00
kamilprzyb
c7006b46ed EggBuffs now stores debuff timestamp instead of remaining time 2020-11-11 23:19:07 -05:00
kamilprzyb
45ed99ae35 implemented player debuff 2020-11-11 23:19:07 -05:00
kamilprzyb
f2ff4c7f4d added basic player buffs implementation 2020-11-11 23:19:07 -05:00
2744ed64e3 Fix a bug where the server would crash if Drain killed a mob. 2020-11-11 23:35:00 +01:00
Gent
90754665a0 Refactor buddy request-by-name 2020-11-11 09:06:41 -05:00
Gent
4454faffc3 Add garbage collection for non-existent players 2020-11-10 18:16:06 -05:00
Gent
34f2aef248 Fix seg fault in buddy data for deleted player 2020-11-10 18:00:34 -05:00
Gent
121c65d7ea Fix not being able to attach weapons to emails 2020-11-09 21:33:02 -05:00
Gent
f53de8d521 Explicitly update buddy records in DB
this should fix the bug where removing a buddy while they're offline won't take you off their list until you disconnect
2020-11-09 05:04:06 -05:00
Gent
8a68958ed4 Fix players being able to become buddies twice 2020-11-09 04:41:20 -05:00
CakeLancelot
f0ded0886c Update tdata ref 2020-11-08 15:54:58 -06:00
Gent
d7a8d2d453 Sanitize email subject + body 2020-11-08 15:37:17 -05:00
darkredtitan
5293573116
Allow period and numbers in firstname/lastname regex check (#144)
* Remove unnecessary whitespace check in regex
* Allow dot characters in names (except at the beginning of a name)
* Allow numbers in names
2020-11-08 14:34:02 -06:00
Gent
d505b09e98 Email bugfixes
- Fix item flag not getting updated
- Fix attachments getting written to the same slot
2020-11-08 14:58:51 -05:00
Gent
8f90fdaac4 Integrate email handlers with DB 2020-11-08 14:39:17 -05:00
Gent
3a55a9b66f Add email structs and functions to DB
Mutex locks included
2020-11-08 14:37:33 -05:00
Gent
36e9370ff4 Add email packet handlers w/ sample data 2020-11-08 14:26:03 -05:00
Gent
3f5a9c8811 Fix buddy list not syncing properly
I couldn't reproduce this on my public server, but the list would show up empty sometimes on initial login on my local server, so I threw it in for good measure.
2020-11-08 13:58:44 -05:00
Gent
30de5c1734 Sanitize buddy chat strings 2020-11-08 13:31:10 -05:00
Gent
c7591c6ce2 Implement buddy warp 2020-11-08 12:54:05 -05:00
Gent
7be79010fc Buddy DB integration 2020-11-08 12:42:27 -05:00
Gent
803f1a246a Buddy code cleanup
- Get rid of buddyCnt, self-nullptr checks, and redundant playermap loops in chat handlers
- Add helper function to find available buddy slot
- Reorganize a bit
2020-11-08 00:08:43 -05:00
SengokuNadeko
262dca7dd6 Buddy rework, adjustments, and fixes
I cleaned up the code a bit (I'll clean it up more as I make more progress) and fixed a bug with buddy slots. Now buddies aren't overwritten!
A few reworks and adjustments

I reworked the buddyIDs array to store only player UIDs instead of IDs. I also reworked buddy deleting so that it deletes buddies from the array.
Code cleanup

Cleaned up some of the code. I mainly just removed useless helper methods.
2020-11-07 23:39:41 -05:00
Gent
26460c0167 Add battery caps for combat rewards & commands 2020-11-07 21:51:16 -05:00
83c378c9c2 Added /notify and /players admin commands
Co-authored-by: dongresource <dongresource@protonmail.com>
2020-11-03 21:07:59 +01:00
a1145aced4 Chat strings are now sanitized.
* Only plain, printable ASCII is allowed for now.
* Local chat is now printed to the server console, but group chat is
still private
* Added a helper function to print character names and IDs
2020-10-31 21:31:25 +01:00
1a405034af
Merge pull request #143 from JadeShrineMaiden/bugfixes
Moved Debuff handling to Combat + Bugfixes
2020-10-31 19:21:50 +01:00
d21f727e9d Moved Debuff handling to Combat + Bugfixes
* Majority of mob debuff handling is moved to combatStep().
* Drain now kills the mob and does 40% overall damage.
* Bumped up active nano debuff durations, debuffs like drain linger longer but damage less.
* Debuffs are cleared upon mob death and retreating.
* Patched out vehicle off success packet spam
* Boosts and potions now cost the right amount (100 taros) and give the right quantity (100).
* Damage was tweaked slightly. At higher levels you are more likely to fall prey to rand().
* Enemies now use run animations during combat and retreating.
2020-10-31 15:55:44 +00:00
CakeLancelot
7b4fab8c6b
Update client/server bundle link in README 2020-10-29 23:49:25 -05:00
Gent S
2af33da4e8 Suppress unordered_map leak in doDebuff 2020-10-26 23:23:29 -04:00
Gent S
f3b9ecd791 Add missing sanity checks to quitTask and useItem 2020-10-25 18:33:02 -04:00
Gent S
609d3cdb99 Remove redundant instance deletion 2020-10-25 18:14:35 -04:00
CakeLancelot
d840b0bbd0 Update tdata ref 2020-10-25 13:14:08 -05:00
CakeLancelot
4da178d16c
Remove mention of random characters from README
The random characters feature hasn't been available for a while now. I also clarified some information regarding usage and the public server.
2020-10-24 23:57:17 -05:00
CakeLancelot
0d65fc2653 Update tdata submodule 2020-10-24 18:34:47 -05:00
Titan
6d97aaa1d0 Make nano skill change consume FM & power items 2020-10-24 18:31:07 -04:00
Gent
4ab686bc46 Fix abused unordered map 2020-10-24 18:30:02 -04:00
CakeLancelot
2302c28ac5
Merge pull request #141 from CakeLancelot/orm-update
Update sqlite_orm to 1.6
2020-10-24 14:31:57 -05:00
c8497a4856
Implement two more mission types + tweaks & fixes
* Weapons will consume your batteries fully.
* Nerfed enemy damage at lower levels.
* Further reworked drain, uses a static variable as a timer (lastDrainTime)
* resendMobHP has been repurposed to drainMobHP.
* Players heal faster after a sizable cooldown.
* Nano type advantage is more noticeable during combat.

Implemented two more mission types + Tweaks

* Item delivery quests now work.
* Timed missions now work.
* All escort missions (type 6) are skipped.
* /minfo now also prints the terminator npc.
* Weapon battery consumption tweaked
* Fixed indentations.
* Heal nanos have better output (25% -> 35%)
* Damage formula had a slight tweak.
* Bugfixed weapon equipping.
* Other tweaks
2020-10-24 14:04:42 -04:00
177c5f0f17
Nano Drain, Debuffs are timed
* Nano drain power works, currently does 30% damage over a period of 3 seconds.
* Stun, Sleep and Snare powers will now run out of time on mobs.
* A few other adjustments to mob mobility.
2020-10-24 14:00:00 -04:00
2782706355
Group warping & mob movement smoothing
* Warping into IZs and Fusion Lairs now will also take into account your group members.
* MobManager lerp does not confusingly divide speed by 2 anymore.
* Mobs pursue their targets more smoothly, they will avoid phasing into the player during combat.
* Nerfed retreat speed by a factor of 1.5, normal mobs retreated way too quickly however mobs like Don Doom and Bad Max do not retreat fast enough.
* Bugfixed sendPlayerTo, it did not call updatePlayerPosition leaving undesirable anomalies.
2020-10-24 14:00:00 -04:00
CakeLancelot
a969988b5c Update sqlite_orm to 1.6 2020-10-24 09:47:46 -05:00
CakeLancelot
bf3c19764b
Merge pull request #140 from JadeShrineMaiden/chunk-desync-fix
Possibly fixed all chunking desyncs
2020-10-23 22:06:20 -05:00
cc06fdcf60 Possibly fixed all chunking desyncs
* Players/NPCs that initialize chunks now correctly pull their main chunk into their viewdata.
* Tested on the public server, seems to have solved the chunking issues.
2020-10-24 03:47:34 +01:00
3b5af415fb Fixed the NULL Player* in PlayerManager::players bug. 2020-10-23 05:32:14 +02:00
Gent
2b650b0bed Cleaned up warp code. 2020-10-22 13:14:24 -04:00
CakeLancelot
512647974d Fix Numbuh 3's sneak and revive abilities being swapped
Also fix a small typo
2020-10-21 10:05:48 -05:00
c9f9b093f4 Bugfixes.
* Add newly created chunks to nearby players and NPCs. This fixes the
slider/static path mob pop-in problem.
* Update a player's chunks when resurrecting. This fixes a mob desync
problem.
* Use a private instance for the Time Lab
* Spawn a slider for every stop
* Fix mobs in private lairs using the template chunk mobs's current
health for their max health
* Don't call into the JSON lib in the loop in aggroCheck(). This is an
optimization found after using gprof.
* Don't print NPC deletions to console. This stops the spam when a
private instance is deleted.
* Changed default view distance to half the length of a map tile, so
chunks are aligned to map tiles
* Update tdata reference
2020-10-21 02:38:30 +02:00
Gent
49d8ed2e36 Slider path, fixes, tweaks 2020-10-20 10:40:50 -04:00
10534886b8 Cleanup for 1.2.
Cleaned up whitespace, comments, brace style, etc.
Updated tdata reference.
2020-10-19 20:48:57 +02:00
Gent
d713fafb1c Add missing MSS routes 2020-10-18 22:35:17 -04:00
e97b58ccaf Fixed private instance memory leaks. 2020-10-19 04:30:12 +02:00
55be58cc24 /summonW now supports summoning non-mob NPCs.
/npcr will now rotate gruntwork NPCs in-place instead of making separate
rotation entries that wouldn't have been loaded properly anyway.
2020-10-19 03:45:58 +02:00
JadeShrineMaiden
deb3e5b897
Added /minfo and /tasks (#137)
* /minfo returns your current active mission's: id, task id, task type, task objective marker id and enemy ids if applicable.
* /tasks returns all the active mission ids and task ids.
2020-10-18 23:02:51 +02:00
4a5857a126 QoL improvements.
* Use macros for extracting map numbers and player IDs from instance IDs
* Add docstrings to all commands
* Remove /test command
* Sync with tdata
2020-10-18 22:43:22 +02:00
Gent
f7e7f99017 Skyway tweaks. 2020-10-17 23:23:35 -04:00
Gent
6d9d66954e M O N K E 2020-10-17 22:33:59 -04:00
bbd695cad1 Minor tweaks.
* The server now refuses to start if any JSONs fail to load
* Mobs now take height into account when losing aggro on a player
* Mobs now aggro on the closest player in range, rather then on the
earliest one to connect to the server of the ones in range
* /summonW now works in IZs and Lairs as well
* Lowered the extra height that mobs spawn at with /summonW to prevent
glitching problems
2020-10-18 04:17:52 +02:00
3ce8cf2129 Cleaned up item drop logic.
* Replaced bad exception logic with C-style error returns in ItemManager
* Removed unnecessary instances of objects being passed by value
* Fixed whitespace problems
* Added new config options to the default config.ini
* Updated tabledata reference
2020-10-18 01:30:37 +02:00
7f716c7278 Fixed exception usage in TableData. 2020-10-18 01:30:37 +02:00
kamilprzyb
dd54668697 functional crates (no more plungers) (#133)
* FM, Taros and Boosts awards from killing mobs should be pretty
accurate now. A temporary formula for adjusting player/mob level gap is
implemented, but it will probably need to be adjusted in the future

* Mobs now drop correct crates

* Crates can be opened and give you correct items This includes
regular mob crates, world boss crates, mission crates, IZ race crates,
E.G.G.E.R.s, golden Eggs, and Event Crates.  Keep in mind that neither
IZ races or golden Eggs are implemented, but if you spawn such a crate
it can be opened.

* All data is read from a json file, for which I'm going to release a
tool soon so it's easily adjustable

* There is a new setting for enabling events, which enables dropping
extra event crates These are Knishmas, Halloween and Easter
2020-10-18 01:30:37 +02:00
Gent
ab5857e7e2 Fix re-crocpotting (#134) 2020-10-16 19:54:05 -04:00
Gent
da725d21e6 Delete instances with no players 2020-10-16 11:50:03 -04:00
Gent
6473951b9a Instance tweaks + fixes
Instanced mobs tick, fusions stay dead, missions reset on enter
2020-10-15 17:07:58 -04:00
Gent
3050801399 Make /warp and /goto put you in the overworld 2020-10-15 17:07:58 -04:00
Gent
efd729710f Kick players out of instances when they leave 2020-10-15 17:07:58 -04:00
Gent
85530ef57f Implement instance copying
and make respawn points match the player instance
2020-10-15 17:07:58 -04:00
Gent
b87f20e2dc Make instance missions reset upon warping out 2020-10-15 17:04:38 -04:00
Gent
d4aed0abf4 Add support for unique instances 2020-10-15 11:46:53 -04:00
Gent
c1fd51b721 Expand gruntwork to instances
Add NPC instance gruntwork data

Add instance-related commands
- /instance (instance ID)
- /npci [instance ID]
2020-10-15 11:44:15 -04:00
Zenpock
bd34bb294c Instance Stuff
custom instance commands
warping takes you into instances
npcs show up only in the instance they are supposed to be inside.
2020-10-15 11:44:15 -04:00
5a80c53e79 Remove the pointer to the deallocated chunk from the current player/NPC.
This fixes the Great Crash Bug(tm).
2020-10-15 04:36:38 +02:00
5784e77654 Misc changes.
The first two fixes were caught by infer. The Big Bug(tm) remains
unfixed.

* Fixed the Leech nano power
* Fixed an unlikely nullptr dereference in ItemManager
* /toggleai now makes mobs retreat immediately, instead of waiting for
their next movement tick
* Static path mobs will now stop in place instead of retreating to
their spawn points when AI is disabled
* Changed the misleading config option name from "chunksize" to
"viewdistance", since it's actually only a third of the chunk size
2020-10-14 23:15:02 +02:00
6ee5e6d1ae Load NPC angles from NPCs.json. 2020-10-14 02:34:00 +02:00
3586b76888 Fixed the slider not being in the overworld instance. 2020-10-14 02:34:00 +02:00
843b2e6e38 chunks are now cleaned up when not in use 2020-10-13 17:30:19 -05:00
a8c8065920 Ah, yes. Windows. 2020-10-13 21:57:41 +02:00
63414ea9a2 The /summon command can now spawn up to 100 NPCs at a time.
Note that /unsummon can only remove one at a time.
2020-10-13 21:50:14 +02:00
599bbedd8c Implemented static paths for mobs (like Don Doom and Bad Max).
* Mobs now retreat to where they were when they were roaming; not their
spawn point
* /toggleai still sends them back to their spawn point however
* Fixed a bug where mobs would respawn where they were killed instead of
their proper spawn point
* Fixed mobs roaming despite simulatemobs being set to false
* Updated tdata submodule
2020-10-13 21:44:43 +02:00
c792fb9d0d Clang lied to me.
It's reasonable that you can't use inline like this, but in my defense
it worked until MobManager.o was recomplied, I think.
2020-10-12 21:18:03 +02:00
f8d64234d7 Spawn mobs above ground to alleviate hill/building phasing issues. 2020-10-12 21:08:19 +02:00
901e011740 Mobs roam proper distances now.
* Mob roaming is now integrated into the TransportManager
* Doubled the roaming distance, since it was clearly too small
* Tripled retreat speed
* Made use of NPCManager::sendToViewable() in TransportManager
2020-10-12 21:08:19 +02:00
Gent
a53f38b87d Fix spawned vehicles expiring instantly 2020-10-12 14:06:28 -04:00
f3b6f9619b added /help 2020-10-12 02:08:10 -05:00
4d687a82ea
Merge pull request #132 from JadeShrineMaiden/test
Bugfixes + Damage Formula Adjustment
2020-10-11 20:59:40 +02:00
d99dad261c Bugfixes + Damage Formula Adjustment
* Entering/exiting groups should apply/remove group buffs.
* Fixed issues with nanos losing all stamina from various revives/warp away.
* Heavily tweaked damage formula, nano types now boost/decrease damage.
* Weapons can consume battery returning extra damage, battery consumed depends on enemy level.
2020-10-11 18:39:52 +01:00
Gent
1564cc7724 Make proximity NPC search a helper function 2020-10-10 23:18:01 -04:00
2b9d0f6bab Fix GROUP_JOIN_FAIL being initialized as a GROUP_INVITE_FAIL.
This was a vulnerability, since JOIN_FAIL is larger than INVITE_FAIL.
2020-10-09 18:24:16 +02:00
9f280c2c31 Validate arguments to /level and /summonW; fix retreatStep() bug. 2020-10-08 20:20:12 +02:00
bae834fefa The login server now sends live checks.
This should stop server providers from kicking tutorial players as
inactive TCP connections. The interval is generous for that reason.

Also snuck in a change that makes mobs retreat all the way to their
spawn points, to aid during mob gruntwork with /toggleai.
2020-10-08 01:21:48 +02:00
1fe23b97fd Fixed RunningNPCRotations being forgotten across server invocations. 2020-10-07 21:13:26 +02:00
0d0332e551 Added /toggleai command for easier mob placement and testing. 2020-10-07 20:38:32 +02:00
7a83a3b45c
Merge pull request #131 from JadeShrineMaiden/bugfix
Bugfixes
2020-10-07 19:52:11 +02:00
606384445c Added /summonW and /unsummonW gruntwork commands.
Also:
* Filled in the battery fileds in the REWARD_ITEM packets in
MissionManager.
* Removed redundant NPC_ENTER in npcSummonHandler()
2020-10-07 19:29:59 +02:00
7caa73caca Bugfixes
* Groups can now be abandoned using the "leave group" button.
* Non-group passive powers no longer buff the group leader instead of buffing the summoner.
* Adjusted various warning causing code pieces.
* Adjusted potions and battery prices.
2020-10-07 18:25:49 +01:00
6e3d0868cb
Merge pull request #130 from gsemaj/npc
NPC Rotation Gruntwork
2020-10-07 02:33:39 +02:00
Gent
5ed332d836 Make /npcr use the nearest NPC 2020-10-06 20:09:18 -04:00
Gent
42fc018097 Add chunk reload command /refresh 2020-10-06 18:00:17 -04:00
Gent
1b68b5e2e2 Add NPC rotation command /npcr 2020-10-06 17:59:33 -04:00
Gent
5009fe1994 Implement temporary NPC rotations 2020-10-06 17:59:12 -04:00
Gent
4873eba160 Add overload to updateNPCPosition with rotation arg 2020-10-06 17:57:46 -04:00
d4d0f388c4 Implemented saving gruntwork results to file.
* Monkey Skyway paths are now saved in a format compatible with
paths.json
* flush() is called on every periodic DB save in addition to the /flush
and /mss N export commands
* Monkeys now accept WIP routes
2020-10-06 21:53:21 +02:00
ce58411ff8 Added /population and /level commands.
Also made /mss an access level 30 command, for safety.
2020-10-05 02:33:30 +02:00
661070dc3a Cleaned up whitespace.
Incantation: sed -i 's/[ \t]*$//g' src/*.[ch]pp

Also switched BuddyManager from tabs to spaces using vim's :retab command.
2020-10-05 02:03:13 +02:00
JadeShrineMaiden
b8f586bc10
Groups, Group Nano powers and Revive (#129)
* Initial Group Implementation

* Request/refuse/join and leave groups.
* Chat into groups.
* Get status updates on every group member each tick.
* Owner leaving the group destroys the entire group.

* Added more nano powers

* Revive for both variants work.
* Many nano powers now have a group variant working.
* Enemy checks for aggro before retreating.
* Enemies keep aggro on dead players with revive nanos out.

* Further Nano powers + Bugfixes

* Infection damage now relies on bitcondition flags.
* Antidote power now works.
* Improved how groups handle leaving players.
* Fixed mob aggro range.
* Group Healing is now functional.
* Possibly fixed the player being unselectable bug.
* Fixed indentations.

* Dismiss nano when starting a MSS ride

* Sneak, Invisibility and Bugfixes

* Sneak and invisibility affect mob aggro.
* Possibly bugfixed equips not showing to other players.
* Aggro checking is less likely to cause nullptr related crashes.

* Group PR cleanup.

* Made sure to label all hacky workarounds
* Implemented the Antidote nano power the right way
* Cleaned up the way various little things are written

Didn't have the opportunity to actually test groups.

Co-authored-by: CakeLancelot <CakeLancelot@users.noreply.github.com>
Co-authored-by: CPunch <sethtstubbs@gmail.com>
Co-authored-by: dongresource <dongresource@protonmail.com>
2020-10-05 01:54:08 +02:00
131eb94919 removed stale comment 2020-10-04 12:52:16 -05:00
755bb75306 U8toU16 now respects buffer sizes 2020-10-04 12:50:58 -05:00
5015e2575d
Merge pull request #128 from gsemaj/monkeys
Add MSS gruntwork commands
2020-10-04 18:33:32 +02:00
Gent
a9837d6c1b Finish MSS commands + convert stack to vector 2020-10-03 11:24:45 -04:00
Gent
47da895544 Add basic MSS gruntwork commands 2020-10-03 11:20:51 -04:00
a852c26e5e minor command refactor 2020-10-02 19:20:59 -05:00
316239dadc comments + better test command 2020-10-02 18:59:07 -05:00
f5939353b1 added basic command handler 2020-10-02 18:50:47 -05:00
f82d203377
Merge pull request #127 from gsemaj/nanos
Fix nano buffs remaining after nano stamina runs out
2020-10-02 23:26:36 +02:00
Gent
cbd04c2ce6 Unsummon nano when stamina runs out 2020-10-02 17:03:11 -04:00
Gent
1b55ab44e3 Add helper function to get socket from player ID
Co-authored-by: Jade <jadeshrinemaiden@gmail.com>
2020-10-02 17:02:31 -04:00
21b7500e13 Define NOMINMAX globally to work around VS nonsense. 2020-10-02 22:04:23 +02:00
35a2110698 Save connected players to DB when gracefully terminating the server. 2020-10-02 19:34:09 +02:00
8a144a359f It hasn't been a landwalker for a while now. 2020-10-02 19:34:09 +02:00
2fe4b2bac1 Rewrote quest bitfield char[128] <-> int64_t[16] logic.
* This should fix the problem with some completed missions being
forgotten
* We no longer explicitly shuffle bits around. Instead we just cast
and copy the full buffer
* Character creation initializes the quest blob with 128 zeroes, since
that happens later on anyway, but we're robust against different quest
flag sizes just in case
* I haven't looked at the actual flag-setting logic, so if the bug is in
there, this won't fix that one, but it does fix the one where the least
significant bit of every 64-bit flag doesn't get saved to the blob
* I'm still cautious about storing the various bitfields as signed
values even though the client does it that way, since while shifting
into the sign bit is undefined behaviour in C/C++, it may *not* be in
C#. And of course the client implementation may just be buggy as well.
2020-10-02 19:34:09 +02:00
839f9a813c remove player from map in weird edgecase 2020-10-01 19:20:44 -05:00
4fe4aeb0d3 Properly clean up players that have been kicked as duplicates.
This should fix the issue with null pointers in PlayerManager::players.
2020-10-02 01:37:50 +02:00
600c26024b
Merge pull request #124 from gsemaj/bugfix1
Fixed crash on killing summoned NPC
2020-10-01 17:03:40 +02:00
Gent
3c734e3e76 Fixed crash on killing summoned NPC 2020-10-01 10:36:52 -04:00
4cd7b7cb53 added basework for instancing 2020-09-30 20:44:37 -05:00
8ff97ec0b3
Merge pull request #123 from gsemaj/items
Add simple item stacking logic
2020-09-30 18:37:31 -05:00
5f65c1530b
Merge pull request #122 from JadeShrineMaiden/bugfix
Various bugfixes and adjustments
2020-09-30 18:37:16 -05:00
Gent
2c831ee115 Match maxed stack behavior to OG 2020-09-30 16:23:46 -04:00
Gent
941e986ee1 Fix incorrectly set item cost 2020-09-30 13:30:19 -04:00
Gent
1eb806af58 Add item stacking logic 2020-09-30 13:29:56 -04:00
ab990116a2 Various bugfixes and adjustments
* Nano missions should now stop repeating.
* Bitwise operators are now used to handle buff/debuff bitfields.
* Changing nano powers will no longer grant you infinite buffs.
* Mobs now heal up client-side after retreating, this comes with candy effect being played however.
* Lower level mobs now hit harder.
* Nanos drain stamina quicker when they grant passive powers.
* Healing, damage and leech powers scale up with your level.
* Player on player damage now accounts for damage and armor.
2020-09-29 22:27:48 +01:00
fb281b0237 Lock all Database operations.
All DB functions that are called outside of Database.cpp are now locked
by the same mutex. This might be a bit overkill, but it's not a hot code
path, so it doesn't matter. Better to avoid the potential deadlocks if
we made it too granular.

From now on a clear distinction must be made between external and
internal functions in Database.cpp, or else deadlock will occur.

Note that sqlite database operations are already locked, but if execute
multiple transactions within the same operation, it could have still
caused problems.

I also removed the DbPlayer fetch when writing to DB by making it a part
of the Player struct. This, by itself, should have fixed the crash we found.
2020-09-29 16:47:39 +02:00
3f35d2e960 sanity check to fix possible FPE 2020-09-28 15:31:01 -05:00
884b844d65 minor refactoring 2020-09-28 13:11:13 -05:00
4079806436 Support the /batteryN and /batteryW commands.
Also up the PVP damage to 700.
2020-09-28 18:54:39 +02:00
efb3df7133 Implemented GM SpecialState Handling
Co-authored-by: dongresource <dongresource@protonmail.com>
2020-09-27 22:19:21 +02:00
c9be0e5402 Do not tick dead players.
This fixes the "dancing in Fusion Matter" bug.
Also (temporarily?) added updateNPCPosition() to the suppression list.
Might want to take it out again (along with some of the other ones) if
we do end up implementing Chunk GC.
2020-09-27 20:29:26 +02:00
97c2c532f1
Merge pull request #117 from gsemaj/slider
Load sliders from paths.json
2020-09-27 20:03:41 +02:00
a324f3fda9
Merge pull request #118 from JadeShrineMaiden/moblocomotion
Greased up enemies
2020-09-27 20:02:52 +02:00
6ea47ddb56 Fixes.
* Mobs should account for height when aggroing on nearby players
* We don't need to keep track of lastHealTime for each player separately
* Warp attendants no longer steal the players weapon and money
2020-09-27 19:58:37 +02:00
2b4a1387f9 stubbed BuddyManager 2020-09-27 12:23:49 -05:00
dacae8d6de
Merge pull request #120 from gsemaj/bugfix1
Combat bug fixes
2020-09-27 02:09:01 -05:00
Gent Semaj
b03cc563eb Fix mobs not aggroing 2020-09-27 03:05:55 -04:00
Gent Semaj
dd374b2ea1 Fix boosts and potions not updating on reward 2020-09-27 03:05:55 -04:00
f8f2088e38 fixed player health regen 2020-09-27 01:13:27 -05:00
062302a7aa fixed potential threading/socket issue 2020-09-27 00:12:26 -05:00
a5d3160588 Mob movement smoothening + Bugfixes
* Mobs now move at a tickrate per second of 2 as opposed to less than 1 before.
* How lerping works was changed slightly, mobs are bumped down to half the speed to account for the higher tickrate.
* Damage formula was altered to more closely match the OG game.
2020-09-27 05:43:50 +01:00
4fea2ae896 Variable damage to/from mobs
* Player weapons and armor ratings are taken into account when damaging/getting damaged by mobs.
* Players have a 5% chance to critical strike mobs, this doubles the player's weapon power.
* Aside from player and mob stat based damage variance, there is also an inherent 20% variance to any damage.
2020-09-27 02:53:03 +01:00
Gent
56a92d302f Add curve parameter to lerp 2020-09-26 19:24:07 -04:00
Gent
0ea5712f8c Load sliders from paths.json 2020-09-26 19:24:07 -04:00
b4fb449e69 Fix VS build. 2020-09-27 01:00:31 +02:00
4fa6618abb Implemented player tick (health/nano stamina).
* The player now heals while not in combat
* Nanos lose stamina while active, regain it while resting
* Using active nano powers drains stamina
* Standing in FM patches/lakes now deals damage
* Fixed a memory error in npcAttackPc()
* Mobs now aggro when a player gets close
* Mobs now give up the chase if the player gets out of the combat zone;
they no longer try chasing until they themselves have left it
* Added a few missing break statements in the loops in BuddyManager

Other players are not yet instantly notified of health/stamina updates,
as finding the correct way to do this has proven tricky. FM patch damage
updates other player's views just fine, though.
2020-09-27 00:16:15 +02:00
43d268e142 Enough with the plungers already.
At least from killed mobs.
2020-09-26 16:45:32 +02:00
9657aaf202 Tuned various values.
* The player no longer receives the Blossom nano mission until they've
left the future
* As a temporary measure for the sake of the public server, Fusions in
respawn in their lairs after 2.5 minutes
* Changed default player damage value to 150
* Mobs now drop the correct amount of FM, as well as a close
approximation of the correct amount of taros
* You can no longer break the FM cap in the Future zone
* Fixed the updateFusionMatter() bug the right way this time
* Completing a nano mission now subtracts FM as it should
* Setting a Nano's power no longer reports 0 FM to the client
2020-09-26 03:48:45 +02:00
dccd92aff9 The Future is now playable.
* Sync'd tdata so it has all the lair NPCs
* Bypassed Eduardo escort task
* Fixed the level 36 updateFusionMatter() memory error
* Fixed qitems being deleted even if the player fails to complete the
mission due to not having any inventory space, thus softlocking the
mission
2020-09-25 23:10:02 +02:00
1b35aab958
Merge pull request #115 from kamilprzyb/master
Quick fix for saving nano missions
2020-09-25 15:06:14 -05:00
6b577ed642
Merge pull request #116 from gsemaj/bugfix1
Fix client/server summoned nano discrepancy on revive
2020-09-25 15:05:40 -05:00
Gent
0931cf1fbc Fix client/server summoned nano discrepancy on revive 2020-09-25 10:57:32 -04:00
kamilprzyb
805c64eff0 Merge branch 'master' of https://github.com/kamilprzyb/OpenFusion 2020-09-25 11:32:38 +02:00
kamilprzyb
8c63cd575c quick fix for saving running nano missions 2020-09-25 11:30:59 +02:00
5068b38c5e
Merge pull request #100 from SengokuNadeko/master
Buddy System
2020-09-25 00:48:01 -05:00
231a4a441b
Merge pull request #112 from gsemaj/slider
Load NPC paths from JSON + update submodule ref
2020-09-25 00:37:53 -05:00
kamilprzyb
d4f1515f5d
Time to go, gumballs and nano potions (#113)
* set up "go to the time machine" button working

* warping to the past now sets PayZoneFlag and removes all active missions

* added gumballs functionality

* added nano potions functionality

* formatting fix
2020-09-25 00:35:27 -05:00
e5a24bcb70
Merge pull request #114 from gsemaj/bugfix1
Prevent player position from updating when flying on monkey
2020-09-25 00:33:42 -05:00
874479d1cf fixed NPC spawning bug 2020-09-24 21:32:14 -05:00
8f84c4c2f8 updateNPCPosition now tracks chunks 2020-09-24 20:58:20 -05:00
kamilprzyb
320a82997a formatting fix 2020-09-25 03:00:50 +02:00
kamilprzyb
d87306930d added nano potions functionality 2020-09-25 02:39:30 +02:00
kamilprzyb
86c1cbd0f2 added gumballs functionality 2020-09-25 02:14:11 +02:00
279cb78d5f Mob-related cleanup.
* NPCs now keep track of their chunk information like PlayerView does
for players
* NPCManager::sendToViewable() parallels PlayerManager::sendToViewable()
* Nano damage and debuffs now count as attacking a mob
* Mobs will de-aggro if something else killed their target
2020-09-25 02:00:26 +02:00
72d625fd8d Summoned mobs are now actually treated as mobs.
Unfortunetly, this necessitated keeping around yet more JSON objects for
the duration of the server's runtime.
It also involved unifying the way NPC IDs are handled, such that they
may be allocated and deallocated out of order.

If any NPCID-related bugs occour, this commit should be regarded as
the prime suspect.
2020-09-25 00:51:18 +02:00
Gent
db33ca2bbb Prevent player position from updating when flying
Only chunk is updated instead to maintain visibility
2020-09-24 18:43:58 -04:00
cfb3d25bc5 Lerp mob movement a little.
We'll lerp a little harder later. Also retreat if kited
too far.
2020-09-24 23:11:14 +02:00
1f18104a6f Mobs fight back now.
There is still a lot of tuning, lerping and cleanup to do.
2020-09-24 23:11:14 +02:00
006d1000c7 Add Address Sanitizer suppression list.
This replaces the unnecessary deallocations on program termination.
Passing in the suppression list environment variable via setenv()
doesn't seem to work, so I've added a comment in the Makefile to explain
invocation.
2020-09-24 23:11:14 +02:00
kamilprzyb
1874f1081b warping to the past now sets PayZoneFlag and removes all active missions 2020-09-24 22:32:09 +02:00
kamilprzyb
df936e8c9c set up "go to the time machine" button working 2020-09-24 21:51:25 +02:00
Gent
72c16587e0 Load NPC paths from JSON 2020-09-24 10:32:46 -04:00
c33f218e56 updateFusionMatter now sets fusion matter 2020-09-23 16:04:58 -05:00
4caca07856
Merge pull request #109 from gsemaj/slider
Basic slider implementation
2020-09-23 14:49:26 -05:00
63c14aff58
Merge pull request #110 from kamilprzyb/vehicles_pr
Fixed Time and implemented vehicles expiring
2020-09-23 14:48:38 -05:00
78930916ad added support for nano missions 2020-09-23 14:44:27 -05:00
SengokuNadeko
9cfced88c9
Stubbed buddy warping
After testing warping a lot I saw how buggy it is. I decided to stub it for this PR and work on it in a later PR.
2020-09-23 13:52:21 -04:00
Gent
f2596bfb6a Add NPC chunk management + Generalize lerp 2020-09-23 12:06:25 -04:00
Gent
65bd2d120b Add NPC pathing queues 2020-09-23 10:29:29 -04:00
kamilprzyb
7bcdc111da fixed comments formating, added zeroing out player->toRemoveVehicle after removing vehicle 2020-09-23 11:21:32 +02:00
kamilprzyb
016c48645e adjusted timestamp function to always return value in seconds 2020-09-23 11:05:18 +02:00
kamilprzyb
09f1f67778 implemented checking vehicle time limit on use 2020-09-23 10:20:47 +02:00
Gent
7dfc888552 Basic slider implementation 2020-09-23 00:08:51 -04:00
Gent
6f05f0f2c8 Add NPCClass enum 2020-09-22 23:41:43 -04:00
c722044bf5 The Sleep, Stun and Snare powers now affect mobs.
They currently don't expire after a set amount of time.
Attacking a sleeping monster does wake it up, but doesn't remove the
debuff status client-side yet.
2020-09-22 22:22:10 +02:00
kamilprzyb
076f89927d changed vehicle iTimeLimit to be stored in seconds instead of miliseconds 2020-09-22 21:49:24 +02:00
95a79ec815 Switched MobManager::step() to the timer.
It doesn't really need to run on every shard step. This makes it a lot
less hot.
2020-09-22 21:31:08 +02:00
kamilprzyb
7ba9b9a54f replaced high_resolution_clock with system_time for timestamps 2020-09-22 21:15:47 +02:00
ba5998d53a Added a config option to disable mob movement and combat.
This will come in handy when gruntwork starts.
Also fixed a bug where the ACCLEVEL setting was read as a boolean.
2020-09-22 20:53:44 +02:00
ac1fd1e5be Implemented mob roaming.
Will likely need further tuning.
Mobs in vacant chunks are skipped.
2020-09-22 20:33:10 +02:00
94ab5b8b64 Seed the random number generator. 2020-09-22 20:32:40 +02:00
SengokuNadeko
e0e474924d
Implemented buddy warping
Buddy warping now works, if anything else needs to be added/redone in regards to this feel free to let me know ^
2020-09-22 13:02:00 -04:00
8896a103ba
Merge pull request #107 from gsemaj/monkeys
Sync tabledata submodule + fix transport bugs
2020-09-22 16:38:07 +02:00
kamilprzyb
0931c88541 fixed size bug 2020-09-22 13:41:28 +02:00
kamilprzyb
5a58908462 vehicle has a proper expiration day when bought, implemented checking expired vehicles while login 2020-09-22 13:16:09 +02:00
kamilprzyb
00f64ce992 added sanity check for killing mission mobs 2020-09-22 10:18:29 +02:00
Gent
153b3a9ef5 Sync tabledata submodule + fix transportation bugs 2020-09-22 00:42:46 -04:00
CakeLancelot
adf017b07c
Move from isGM to Account Levels (#106)
Co-authored-by: dongresource <dongresource@protonmail.com>
2020-09-22 04:26:12 +02:00
5d8bb7f8a5
Merge pull request #95 from gsemaj/monkeys
Paths framework + Monkey Skyway System framework
2020-09-22 03:54:01 +02:00
Gent
2c8243e136 General MSS fixes + tweaks
Add alert message for unpathed skyway routes
Fix overflow during lerp + add Future routes
Add documentation for MSS
Fix potential MSS registration bug
Minor tweaks + styling
Update packet broadcast
2020-09-21 21:43:39 -04:00
12fbdc9621 Wait about 2s before despawning killed mobs.
This gives them enough time to play their death animations before
they disappear.
2020-09-22 00:23:18 +02:00
a768a4f539 Fixed halved NPC distance and renamed config option to chunksize. 2020-09-21 23:30:05 +02:00
d6357197d3 Use the chunk's visibility lists when respawning mobs. 2020-09-21 23:13:13 +02:00
4cc1cf4f7e minor chunk refactor 2020-09-21 14:55:34 -05:00
b67a0b6946 removed usewebapi 2020-09-21 14:49:08 -05:00
kamilprzyb
5e0948ea93
Database saving update (#104)
* implemented saving BatteryN and BatteryW

* implemented saving mentor

* moved int64->blob parsing to a separate function

* moved parsing blob->int64 to a separate function

* added functions for parsing int32->blob and vice versa

* added functions for parsing int16->blob and vice versa

* WIP saving quest items and active tasks

* Quest items are stored in inventory table instead of blob

* added sanity check for missionId

* saving active missions works

* removed unneccesary include

* implemented saving warplocationflag, skywaylocationflag and currentmissionid in database

* INFO DB message now shows how many accounts and player characters are in the database

* fixed dbsaveinterval being in [login] instead of [shard]

* fixed mission quit:
- fixed wrong json name, causing qitems not deleting properly
- quitting mission now resets npc kill count

* adjusted saving active missions

* removed blob parsing functions that ended up being unused

* removed accidentaly added include

* removed sending PCStyle2 on Player Enter

* added a sanity check in itemMoveHandler

* removed MapNum from PCLoad, as client doesn't even read it

* set BuddyWarpCooldown to 60s on PCLoad

* fixed a bug causing EXIT DUPLICATE not working

* added creation and last login timestamps to accounts and players

* added a sanity check for P_CL2LS_REQ_PC_EXIT_DUPLICATE

* implemented web api support, toggled by new setting (off by default)

* add usewebapi to config

Co-authored-by: Gent <gentsemaj@live.com>
2020-09-21 14:43:53 -05:00
SengokuNadeko
90134cd1fa
Buddy deleting/blocking works and code clean up
Added in buddy deleting and buddy blocking. I also cleaned up some of the code and added some comments to the code.
2020-09-21 14:28:04 -04:00
321dca3f79 Use PC_MAXHEALTH() in the rest of the codebase.
+ minor fixups
2020-09-21 19:51:30 +02:00
113ecc8f60 Refactored passive nano powers. 2020-09-21 19:51:30 +02:00
dc9de5a54a Cleaned up implementation of active nano powers. 2020-09-21 19:51:30 +02:00
0fc072d591 Initial implementation of nano powers. 2020-09-21 19:51:30 +02:00
24341c578a
Merge pull request #105 from FinnHornhoover/mingw-version-check-fix
Fix Make Linux GCC warning and Windows GIT_VERSION error
2020-09-20 20:14:48 -05:00
FinnHornhoover
a05bb15697 fixed makefile append and mingw bugs 2020-09-21 02:12:34 +03:00
Gent
135424b855 Update player visibility on skyway 2020-09-20 15:24:17 -04:00
Gent
cb984c029b Add paths JSON file 2020-09-20 15:24:17 -04:00
Gent
a5ffe26c44 Move lerp to td init, add variable gap size 2020-09-20 15:24:17 -04:00
Gent
6a78a301c9 Skyway lerp tuning 2020-09-20 15:24:17 -04:00
Gent
a5c40b66f5 Add basic Monkey Skyway functionality 2020-09-20 15:24:17 -04:00
CakeLancelot
94583e534b
Merge pull request #102 from darkredtitan/LoadisGMfromConfig.ini
Respect config.ini GM settings
Upon character creation, the GM value from the config will now be used instead of always being set to false. The default in Settings.cpp has also been changed to be consistent with the config.ini in the repo.
2020-09-20 14:04:33 -05:00
darkredtitan
ff7c78d545 Change default value for GM to true 2020-09-20 20:50:58 +02:00
CakeLancelot
77df7b7160
Merge pull request #92 from gsemaj/crocpot
Implement Croc Pot item combining
Fix vehicle type override
2020-09-20 13:40:36 -05:00
darkredtitan
6eb21e6d67 Respect config.ini GM settings 2020-09-20 19:53:33 +02:00
228a181b74 removed ChunkManager::removeNPC() 2020-09-20 10:34:43 -05:00
FinnHornhoover
27df1bd7d0 fixed indent 2020-09-20 01:24:42 +03:00
Raymonf
6a05ce4504
Add BuddyManager to Makefile 2020-09-19 17:16:47 -04:00
Gent
c6ec1c46c2 Add documentation for getItemType() 2020-09-19 16:38:50 -04:00
Gent
d1c5e272a8 Fix vehicle type override 2020-09-19 15:26:16 -04:00
SengokuNadeko
9bb19efc99
Updated main.cpp for buddy manager init function 2020-09-19 15:08:03 -04:00
SengokuNadeko
7757238a47
Buddy System draft (W.I.P)
The w.i.p buddy system
2020-09-19 15:07:09 -04:00
CakeLancelot
4d437bcb34
Merge pull request #97 from gsemaj/bugfix1
Preventing spawning of invalid items using /itemN
Fix fusion matter amount not displaying on enter
2020-09-19 07:46:56 -05:00
Gent
5dbca0b7b1 Fix fusion matter amount not displaying on enter 2020-09-18 23:26:20 -04:00
ce9285bab5 support for /unsummon 2020-09-18 16:24:15 -05:00
Gent
cd7fec2d5b Fix invalid item bug 2020-09-18 14:45:51 -04:00
d9d781c37d whoops, everyone had gm 2020-09-18 02:10:30 -05:00
Gent
b929d12902 Implement Croc Pot item combining 2020-09-17 23:55:57 -04:00
9f78735caa fixed socket reference bug 2020-09-17 22:44:37 -05:00
31ef03610d temp fix and small refactor 2020-09-17 21:41:09 -05:00
22e3e9e4de fixed 'player clone' bug 2020-09-17 18:03:23 -05:00
f4db0830ba huge refactoring, chunking added 2020-09-17 17:45:43 -05:00
001564a257 Hotfix to stop crashing the server.
This will need to be fixed properly; probably while we implement
chunking.
2020-09-17 21:22:31 +02:00
e79f179628 Mobs respawn now.
Began work on mob logic. Also cleaned up TableData a little.
2020-09-17 01:43:48 +02:00
027b783571 Visual studio doesn't use -ldl. 2020-09-16 23:13:06 +02:00
e03da83ff3 Rearranged the codebase a little.
* Deleted empty Player.cpp
* Moved the helper functions from the obsolete CNStructs.cpp into
main.cpp and deleted it
* Renamed CombatManager to MobManager, as that will likely become it's
main focus soon
2020-09-16 21:46:15 +02:00
5efc8ac089 Switched getTime() to std::chrono on all platforms. 2020-09-16 20:14:00 +02:00
efda6673b5 Print server version when starting up.
Also added -ldl to fix cmake compilation on Unix systems.
2020-09-16 20:12:56 +02:00
f7571607ba Only send live checks when the connection has been silent for a while.
Also:
* Made the timeout configurable
* Removed the stale randomcharacters config option
* Switched to time_t for time values, even though it doesn't really
matter that much
* Halved the keepAliveTimer frequency
2020-09-16 17:45:53 +02:00
CakeLancelot
501d153894
Update README.md
Inventories are now saved to DB
2020-09-15 06:02:39 -05:00
4d21410980 Merge PR #87 2020-09-14 18:42:03 +02:00
Gent
148d90f4f1 "Boosts and potions!"
Fixed crate opening such that the item has an iOpt of 1.
2020-09-14 18:36:50 +02:00
Gent
a976fef2b4 Implement vendor stack logic 2020-09-14 10:25:15 -04:00
Gent
da8c833587 Implement buyback
Sellability tweak


Add additional item categories
2020-09-14 10:25:15 -04:00
Gent
c91022030c Load item tables + price implementation 2020-09-14 10:25:15 -04:00
Gent
f55cc8f36d Load vendor tables 2020-09-14 10:24:55 -04:00
9cc5f3e4d5 Cleaned up comment formatting.
The incantation was: sed -i 's://\([^ ]\):// \1:' src/*.[ch]pp

Lines of code that was commented out were then manually reverted.
2020-09-14 16:07:05 +02:00
131997f34f Cleaned up all whitespace issues.
The incantation was: sed -i 's/[ \t]*$//g' src/*.[ch]pp
2020-09-14 16:07:05 +02:00
ed86bc9160 Assorted cleanups and fixes.
* Clean up spacing/indentation
* Proper enum formatting
* Fix nano dismissal (for real this time)
* Do not copy Player struct when a pointer is right there
* Stop looking after the trade partner has been found
* Make sure we're shifting unsigned values (and 64-bit when they need to be)
* Look for JSONs in tdata/
* Add a dbsaveinterval to the example config.ini, in the login category
2020-09-14 16:07:05 +02:00
darkredtitan
38d5998a6e Sanity checks for creating and deleting characters.
Co-authored-by: kamilprzyb <kamilprzybspam@wp.pl>
2020-09-14 16:07:02 +02:00
kamilprzyb
c7189a5cef added saving missions 2020-09-14 16:07:02 +02:00
kamilprzyb
de15e2004b added bank functionality, refactored itemMoveHandler
Co-authored-by: Cake Lancelot <CakeLancelot@users.noreply.github.com>
2020-09-14 16:07:02 +02:00
darkredtitan
480cca82fa Make name checking case insensitive.
Co-authored-by: kamilprzyb <kamilprzybspam@wp.pl>
2020-09-14 16:06:57 +02:00
kamilprzyb
3d83f93167 added periodic DB saves, fixed some settings 2020-09-14 01:38:27 +02:00
kamilprzyb
1d9a7139a8 deleteing player deletes inventory and nanos, minor tweaks to settings 2020-09-14 01:38:27 +02:00
kamilprzyb
2fd7a8c6fc Implemented saving inventory and nanos 2020-09-14 01:38:27 +02:00
kamilprzyb
fc57cae37d added saving character when a connection with shard is killed 2020-09-14 01:38:04 +02:00
Gent Semaj
3cfec7aab3
Implement transportation framework + full S.C.A.M.P.E.R. functionality (#86)
* Add transportation registration
* Add S.C.A.M.P.E.R. functionality

Co-authored-by: dongresource <dongresource@protonmail.com>
2020-09-13 22:26:16 +02:00
CakeLancelot
29e53117e7
Merge pull request #72 from FinnHornhoover/msys-mingw-compile-fix
Warnings and compile errors for MSYS2 MinGW64
2020-09-13 07:35:32 -05:00
c1ac2250a0 Minor changes.
This was me fixing a few bugs and finding that Gent was working on some
of the same ones, then awkwardly merging the changes together.
2020-09-12 22:43:04 +02:00
a4716b0164
Merge pull request #82 from gsemaj/nanos
Improve revival code
2020-09-12 22:10:49 +02:00
FinnHornhoover
91f512d740 added version checking for GCC 2020-09-12 22:27:03 +03:00
Gent
4880e4af12 Revamp revival code 2020-09-12 14:21:36 -04:00
fe370df534
Merge pull request #81 from gsemaj/auth
Validate cookie data
2020-09-12 18:03:26 +02:00
Gent
2b1a028b3d Validate cookie data 2020-09-12 11:41:31 -04:00
be99714495 Possible fix for the login error bug. 2020-09-12 17:09:11 +02:00
4c06163b51 Properly validate vendor logic.
Also, iTimeLimit should be 0.
2020-09-12 16:10:53 +02:00
0c97969757
Merge pull request #80 from gsemaj/vendor2
Add sell functionality to vendors
2020-09-12 16:01:23 +02:00
Gent
4e7352da66 Add sell functionality to vendors 2020-09-11 22:04:23 -04:00
5747c24479 [bugfix] Don't set iType on empty item slots.
This confuses ItemManager::findFreeSlot().
2020-09-12 02:25:45 +02:00
579aa9d31d Save pointer to Player struct in CNSocket.
This is an insignificant optimization now, but will be necessary when we
start switching around the the algorithms and datastructures used in
proximity detection.
2020-09-12 01:22:58 +02:00
Ariii
3865249387 Vendors, set nano skill command + serverside command issues fixed (#74)
Added basic shopkeeper functions, a player can buy the preset 3 items (cannonbolt set), all shopkeepers have the same items atm (need to check the shopkeeper tabledata), setting itemprice is something I didn't figure out.
Added set nano skill command
Implemented a switch for certain commands like health/taros/fusionmatter etc to be handled on the serverside aswell

Co-authored-by: dongresource <dongresource@protonmail.com>
2020-09-11 23:19:03 +02:00
468840c9ea Fixed gcc vs clang preprocessor tomfoolery. 2020-09-11 00:19:21 +02:00
52f02168bc Implemented quest item cleanup and fixed a few bugs. 2020-09-11 00:19:21 +02:00
ddb5f782b7 Refactored mission data and implemented quest item mob drops.
Most future missions are now playable.
Quest items aren't being properly cleaned up yet.
2020-09-11 00:19:21 +02:00
3665dc2c93 [WIP] Incremental mission save 2
This commit (and the previous one) exist to document the first approach I
took to storing mission data. It's only here for posterity. This comment
was added while rebasing.
2020-09-11 00:19:19 +02:00
ae654f996c [WIP] Incremental mission save 1
This commit (and the next one) exist to document the first approach I
took to storing mission data. It's only here for posterity. This comment
was added while rebasing.
2020-09-11 00:19:16 +02:00
e33b7f20e9 [bugfix] Preserve Taros and FM when opening Crates. 2020-09-11 00:08:26 +02:00
5b49e71de7 Implemented mission rewards.
Might need to refactor item drops, especially after implementing
task-result quest item drops.
2020-09-11 00:08:26 +02:00
3172724596 Implemented the mob kill counter in missions and fixed a bug. 2020-09-11 00:08:26 +02:00
8887c6349b Add support for cookie-based login back in. 2020-09-11 00:08:26 +02:00
CakeLancelot
6e0b101a76
Merge pull request #78 from JadeShrineMaiden/additions2
Levelling up added to /nano
2020-09-10 12:02:28 -05:00
29cde56fb1 Levelling up added to /nano
- Using /nano levels up Player, a player cannot level down.
2020-09-10 17:51:52 +01:00
e65f07780b Merge branch 'master' of https://github.com/OpenFusionProject/OpenFusion 2020-09-09 12:06:35 -05:00
eb1ad6bb37 switched to dumped XDT & moved to a submodule 2020-09-09 12:06:22 -05:00
CakeLancelot
e409b8bb39
Add .gitattributes file, exclude src/contrib from linguist (#73) 2020-09-08 19:55:59 -04:00
45a33758a5 Account for the size of packet length and ID in validation functions. 2020-09-08 03:06:55 +02:00
FinnHornhoover
266fddbffa fixed warnings and compile errors for msys2 2020-09-08 03:41:13 +03:00
e90ae10746 Summoned NPC are now visible to other players. 2020-09-07 22:12:53 +02:00
b797993014 updated readme 2020-09-07 13:07:04 -05:00
df655dfe29 added mobdata to config 2020-09-07 12:54:40 -05:00
c8c4ec7d01 updated readme & small refactor 2020-09-07 12:23:00 -05:00
458843958b
Merge pull request #69 from SengokuNadeko/master
Adjustments made to regex
2020-09-06 14:06:37 -05:00
958d4a79eb
Merge pull request #70 from CakeLancelot/stub-npcbarker
Temporarily set npcBarkHandler to a stub
2020-09-06 14:06:08 -05:00
e86860baf7
Merge pull request #71 from FinnHornhoover/mingw-compile-fix
MinGW optimization fix
2020-09-06 14:05:39 -05:00
FinnHornhoover
a8c88a9bd9 disabled unknown pragma warnings 2020-09-06 19:40:13 +03:00
FinnHornhoover
038ce984c5 O3 fix for mingw g++ 2020-09-06 18:52:50 +03:00
SengokuNadeko
361c069d0c Adjustments to the regex
I made the regex a bit less restrictive. If you want, you can push this if it seems appropriate.

Username should be at least 4 characters and max 32
Password should be at least 8 characters and max 32

Usernames can be any combination of letters and numbers, with no special characters except for dash and underscore.

Passwords can use any of the alphanumeric/special characters specified in the regex.
2020-09-06 10:49:14 -05:00
SengokuNadeko
3876e0537e Small regex fix
Old regex had some problems (a bit too restrictive). If you want, you can push this to loosen up the restrictions a little.
2020-09-06 10:42:07 -05:00
CakeLancelot
8a481acdae Stub npcBarkHandler for now 2020-09-06 09:59:43 -05:00
e936cb9fac
Merge pull request #67 from darkredtitan/master
Fix social distancing bug (name trademarked by kamilprzyb)
2020-09-05 20:37:53 -05:00
darkredtitan
589da3f714 Fix social distancing bug (name trademarked by kamilprzyb) 2020-09-05 19:53:16 +02:00
266ca8b8c6 temp fix for mingw 2020-09-04 14:12:01 -05:00
00c5e07f4f small memory bugs fixed 2020-09-03 15:29:29 -05:00
05d035717d and here too 2020-09-02 22:49:51 -05:00
f41bf0ace2 switched to -O2 optimizations 2020-09-02 22:49:38 -05:00
063d302bd5
Merge pull request #62 from kamilprzyb/master
Rewrote DB and finished LoginServer
2020-09-02 22:49:08 -05:00
7e9793bf90
Merge pull request #59 from JadeShrineMaiden/additions
Colliding NPC IID quickfix
2020-09-02 22:48:09 -05:00
CakeLancelot
fa6b0b178c Remove unnecessary bracket in include statements 2020-09-02 21:25:09 -05:00
CakeLancelot
dadf1c5bcf Change sqlite3.h path in sqlite_orm.h
Should fix Appveyor compilation
2020-09-02 21:11:50 -05:00
kamilprzyb
359991e274 added back accidentaly removed files in Makefile 2020-09-02 18:05:18 +02:00
kamilprzyb
260759c20b replaced tabs with 4 spaces 2020-09-02 17:53:39 +02:00
kamilprzyb
1ff5694960
Fix APPROVEALLNAMES setting
fixed GetInteger->GetBoolean

Co-authored-by: FinnHornhoover <30576665+FinnHornhoover@users.noreply.github.com>
2020-09-02 17:47:10 +02:00
CakeLancelot
be4c5a8072
Update config.ini
Add acceptallcustomnames option
Correct comment
2020-09-02 07:12:24 -05:00
kamilprzyb
ef84ec8fca remove CMakeSettings 2020-09-02 00:38:33 +02:00
kamilprzyb
11801c1f89 Rewrote DB to use ORM, added all remaining features to LoginServer and refactored it 2020-09-02 00:37:09 +02:00
4f6c77be4f minor cleanup 2020-08-31 17:54:49 -05:00
73c67a814d Fix checking of header timestamps.
This is implemented in such a way that a change in one of our headers
won't cause recompilation of large C dependancies. It's a bit hacky, but
it works.
2020-08-31 22:40:33 +02:00
d7a41d40ab Replace signal() with sigaction(). 2020-08-31 22:40:33 +02:00
0aeac0f6f3 Improve the Makefile so we don't have to recompile the libs every time. 2020-08-31 22:40:33 +02:00
48b0866441
Merge pull request #60 from dongresource/contrib
Wrote a CONTRIBUTING.md with instructions for clean Pull Requests
2020-08-30 22:43:40 -05:00
4ade533f40 Wrote a CONTRIBUTING.md. 2020-08-31 03:47:56 +02:00
1e344c2dd8 Small tweak 2020-08-31 01:36:29 +01:00
fdd0160248 Colliding NPC IID quickfix 2020-08-30 23:29:28 +01:00
5f10718315
Merge pull request #56 from Eperty123/master
Add NPC barking, see saved characters
2020-08-30 10:41:24 -05:00
Eperty123
da293ba9b3 Add DB prefix to db stuff 2020-08-29 13:47:39 +02:00
Eperty123
437063d78a Add experimental TransportManager 2020-08-29 13:43:33 +02:00
Eperty123
b239fb9331 Add NPC barking, seeing saved characters 2020-08-29 13:14:21 +02:00
50431024c9
Merge pull request #55 from dongresource/combat1
Implemented combat, drops, crates and the guide changer
2020-08-28 19:11:56 -05:00
2a258a80f0 Database.cpp refactoring 2020-08-28 19:10:26 -05:00
322bc46604 Support plain POSIX make.
Also standardized the new variable names.
2020-08-28 22:54:28 +02:00
2551f74af1
Merge pull request #54 from CakeLancelot/editorconfig
Add .editorconfig to enforce 4 space indent, final newline, and LF EoL
2020-08-28 15:50:30 -05:00
CakeLancelot
1af240fa34 Add .editorconfig to enforce 4 space indent
Should be supported by most IDEs and text editors
2020-08-28 15:34:49 -05:00
a067975f27 Players can now see eachother fight monsters. 2020-08-28 22:22:24 +02:00
72a811d6ab Implemented guide changing.
This means the Time Machine works as well.
2020-08-28 22:22:24 +02:00
3b35e0017a Moved all JSON files into a dedicated data directory. 2020-08-28 22:22:24 +02:00
4df812f996 Implemented crates (dropping and opening).
Also fixed a bug in vaildOutVarPacket().
2020-08-28 22:22:24 +02:00
67d899efe6 Implemented proper validation of variable-length packets.
Also changed output buffer in pcAttackNpcs() from dynamically to
statically allocated. This in itself is temporary as I have a better
idea as to how we can allocate buffers with a bit less boilerplate.
2020-08-28 22:18:28 +02:00
64accecc30 Initial implementation of CombatManager.
Overflow detection must still be implemented.
2020-08-28 22:18:28 +02:00
c8c2f4b05f Catch SIGINT with signal(), to allow for gprof instrumentation.
Note: signal() is undefined behaviour in multithreaded programs and is
unportable for handling signals in general. This will need to be
replaced with sigaction() or something.
2020-08-28 22:18:28 +02:00
3c43dd0193 Try to transmit FF packets in one go, instead of sending the id first. 2020-08-28 22:18:28 +02:00
9e9161083d Reword some comments and correct paths in the Readme. 2020-08-28 22:18:28 +02:00
darkredtitan
5cf7225f52
Tried to manually merge kamilprzyb and main repo's code (#45)
* Merge kamilprzyb and main repo's code

* Update Makefile by FunnHornhoover

* Update Makefile by FinnHornhoover

* Add flag to Makefile by FinnHornhoover

* Remove extra line from makefile

* Remove lbcrypt from Makefile

* Fix flag to Makefile by FinnHornhoover

* Reimplement potential fix for tutorial blackscreen by Dongresources

* Update CMakeLists.txt

* Update CMakeLists.txt

* Reinsert Jade's changes

* Cosmetic Changes to Databases .h & .cpp

* Remove CMakeSettings.json

* Update Makefile by Finn Hornhoover

* More cosmetic changes to Databases.cpp

* More cosmetic changes to Databases.cpp

* Remove unnecessary line (CMakeSettings.json)

* Fix CNLoginServer.cpp

* More cosmetic Changes to Database.hpp, edit Database.cpp to use JSON library onstead of json11 library, and delete json11 library files

* Delete json11 library files

* Delete JSON library to reupload

* Reupload JSON library from main repo

* Reupload JSON library from main repo

* Fix syntax error

* Fix Makefile

* Remove commented line of code to be like master

Co-authored-by: CPunch <sethtstubbs@gmail.com>
2020-08-28 13:02:03 -05:00
JadeShrineMaiden
5c8a0069fc
Vehicle and trading bugfixes (#51)
* Sanity checks + Starting level changes

- Item movement handler checks to make sure items aren't moved from equipment slot to equipment slot.
- Item give command checks to make sure an out of bounds item is not spawned (Below iType 0 or above iType 8)
- Players now begin at level 36, consequently the item give command does not level you up now.

* Initial Trade Implementation

* Sanity Check

- Prevents out of bounds item movement by comparing it to AINVEN_COUNT.

* Taros and Trading

* Update ItemManager.cpp

* Update ItemManager.cpp

* working trading system

* Trading system code pointerified

- It works with the recent pointer changes needed.

* Vehicles and Trading bugfixes
2020-08-26 21:35:13 -05:00
64d4b1d26a
Merge pull request #50 from dongresource/bugfix
Fix crash when receiving invalid packets with very low ids.
2020-08-26 15:33:23 -05:00
9b0cb7f441 Fix crash when receiving invalid packets with very low ids.
Also fix benign NPC deallocation bug.
2020-08-26 22:22:52 +02:00
c48db0f9f9 ignore SIGPIPE 2020-08-26 14:38:09 -05:00
e0c00bcdc8 Merge branch 'master' of https://github.com/OpenFusionProject/OpenFusion 2020-08-26 14:23:43 -05:00
6db1a7baf1 removed unnecessary allocation 2020-08-26 14:23:40 -05:00
99e357d24e
Merge pull request #47 from OpenFusionProject/Raymonf-readme-fix
Correct AppVeyor link
2020-08-26 13:04:32 -05:00
Raymonf
57681cd669
Correct AppVeyor link 2020-08-26 13:53:50 -04:00
c9badae526
Merge pull request #46 from dongresource/mobs2
Populated the future with scraped mobs
2020-08-26 12:42:35 -05:00
JadeShrineMaiden
d3ca93a9b8
Trading System (#43)
* Sanity checks + Starting level changes

- Item movement handler checks to make sure items aren't moved from equipment slot to equipment slot.
- Item give command checks to make sure an out of bounds item is not spawned (Below iType 0 or above iType 8)
- Players now begin at level 36, consequently the item give command does not level you up now.

* Initial Trade Implementation

* Taros and Trading

* working trading system

* Trading system code pointerified

- It works with the recent pointer changes needed.
2020-08-26 12:40:10 -05:00
JadeShrineMaiden
6808365d48
Sanity checks fix (#41)
* Sanity checks + Starting level changes

- Item movement handler checks to make sure items aren't moved from equipment slot to equipment slot.
- Item give command checks to make sure an out of bounds item is not spawned (Below iType 0 or above iType 8)
- Players now begin at level 36, consequently the item give command does not level you up now.

* Sanity Check

- Prevents out of bounds item movement by comparing it to AINVEN_COUNT.

* Update ItemManager.cpp

* Update ItemManager.cpp
2020-08-26 12:39:49 -05:00
4178945abe Decoupled player and NPC view distance. 2020-08-26 04:58:17 +02:00
3e5101892b Populated the future with scraped mobs.
This system is temporary; meant to ease testing.
2020-08-26 04:57:59 +02:00
60be814e16
Merge pull request #42 from dongresource/bugfix
Fixed a use-after-free and a memory leak.
2020-08-25 17:42:22 -05:00
16c11dada0 Fixed a use-after-free and a memory leak. 2020-08-26 00:09:31 +02:00
260331715f
Merge pull request #40 from dongresource/bugfix
Bugfixes
2020-08-25 13:52:51 -05:00
b187d4b65f
Merge pull request #37 from dongresource/work1
Respawn points, NPC spawning, misc stuff.
2020-08-25 13:45:59 -05:00
3b3ddf08ef Fix github issue #38 2020-08-25 20:42:52 +02:00
41898bb6b7 Fix a bug where nanos aren't unsummoned when unequipped.
Thanks fabriXfinn for reporting it.
Also improved sanity checks.
2020-08-25 20:30:20 +02:00
dff710cf61 Enable vehicle spawning. 2020-08-25 19:43:46 +02:00
b79bc56b31 Implement NPC spawning.
Protected by a simplified GM system. Either everyone is a GM (local
servers) or nobody is (public servers).
2020-08-25 04:28:42 +02:00
9aa9b76826 Made the random characters level 36.
They are meant to make testing faster, after all. No point in
restricting them from fully testing items.
2020-08-25 04:17:47 +02:00
113ce0af07 Load the NPC and warp JSON files according to the config file.
NPC data was being read from the config, but ignored.
2020-08-25 03:45:04 +02:00
6f1a72ca0f Shut Computress up.
Setting all bits in the first use flags disables tutorial messages.
2020-08-25 03:42:52 +02:00
d964a83d6d Respawn points work now.
Note that some of them weren't present in clientnpc and will need to be
manually added later.
2020-08-25 03:34:53 +02:00
d025b611a1
Merge pull request #35 from dongresource/work1
Avoid excessive copying of large Player struct
2020-08-24 19:05:24 -05:00
2f1358c124 Potential solution for the tutorial problem.
Will need to be tested on the public server.
2020-08-25 01:57:53 +02:00
c1b6ae8466 No need to unpack zero-length packet structs.
Also fixed formatting and added subheading to README.md.
2020-08-25 01:08:02 +02:00
14bc368073 Dynamically allocate the Player struct in PlayerView.
This way we're not always passing it around by value.
Note that the Player structs in CNSharedData are still
pass-by-reference. Will probably change this later.
2020-08-25 00:59:55 +02:00
c5dacb4958 Removed CRs from .gitignore and an outdated comment in config.ini. 2020-08-25 00:11:54 +02:00
fb993f0c5d Added .vimrc configured for the 4 spaces style. 2020-08-25 00:11:54 +02:00
6d3868349d removed debug output in keepAliveTimer 2020-08-24 16:12:49 -05:00
16bca39dae added simple timer system to CNShardServer 2020-08-24 16:11:40 -05:00
Onii-chan
afbf309c7e
Add player revive, vehicle mount/dismount and more (#33) 2020-08-24 16:04:56 -05:00
28ad1a0c25 fix windows support for sockets 2020-08-24 13:23:28 -05:00
JadeShrineMaiden
ff5f3966e3
Sanity checks + Starting level changes (#31)
* Sanity checks + Starting level changes

- Item movement handler checks to make sure items aren't moved from equipment slot to equipment slot.
- Item give command checks to make sure an out of bounds item is not spawned (Below iType 0 or above iType 8)
- Players now begin at level 36, consequently the item give command does not level you up now.

* Sanity Check

- Prevents out of bounds item movement by comparing it to AINVEN_COUNT.
2020-08-24 03:07:51 -05:00
55add82843
Merge pull request #32 from dongresource/work
-Wall, #pragma once, nanoSummonHandler() cleanup, verbosity levels
2020-08-23 16:57:58 -05:00
e99feb03d5 Add verbosity levels. 2020-08-23 23:09:31 +02:00
431448ffb7 Merge branch 'master' of https://github.com/OpenFusionProject/OpenFusion 2020-08-23 15:10:00 -05:00
8105d0aa88 properly handle socket errors in recieving packets 2020-08-23 15:09:51 -05:00
acf358ef51
Merge pull request #30 from CakeLancelot/clion-gitignore
Update .gitignore for CLion
2020-08-23 13:03:10 -05:00
CakeLancelot
0a8e96ebc4 Update .gitignore for Clion 2020-08-23 12:58:08 -05:00
756074cc62 Remove redundant initialization in nanoSummonHandler(). 2020-08-23 19:46:51 +02:00
51a8cc8bdf Silence -Wall warnings. 2020-08-23 19:19:12 +02:00
651ccba932 Replace ifdef guards with #pragma once
tl;dr this has potentially significant compilation speed benefits.
2020-08-23 19:14:54 +02:00
1281fdaaf0 Add -Wall to Makefile. 2020-08-23 18:30:23 +02:00
561a809f33 load warps into memory in NPCManager::init 2020-08-23 11:26:25 -05:00
0d27412d81 added getNearbyPlayers 2020-08-23 10:42:37 -05:00
Zenpock
1d792a21dd
Npc Warp implementation (#29)
* Update CNShardServer.hpp

* Update ChatManager.cpp

* Update ChatManager.hpp

* Update NPCManager.cpp

* Update NPCManager.hpp

* Add files via upload

* Update NPCManager.cpp

* Update NPCManager.cpp

* Update ChatManager.cpp

* Update ChatManager.cpp

* Update NPCManager.cpp

* Update NPCManager.cpp

Co-authored-by: CPunch <sethtstubbs@gmail.com>
2020-08-23 10:32:25 -05:00
d6b96389be added sanity checks to nanos 2020-08-22 22:15:27 -05:00
dongresource
6129c0b4e2
Players can now see eachother's nanos. (#28) 2020-08-22 19:52:54 -05:00
c9bf3d1896 restored default config.ini 2020-08-22 19:27:08 -05:00
88953541ef added INITSTRUCT to zero-out data 2020-08-22 19:26:18 -05:00
94b0dc724e major refactoring 2020-08-22 18:31:09 -05:00
0ff1f74cd3 fixed inet_ntoa warnings 2020-08-22 14:02:58 -05:00
35b424c531 fixed warnings for VC++ 2020-08-22 13:38:27 -05:00
2072bdcff7 updated appveyor 2020-08-22 13:29:38 -05:00
Zenpock
4f10ee0505
MenuChat Added (#25)
* Update CNShardServer.hpp

* Update ChatManager.cpp

* Update ChatManager.hpp

Co-authored-by: CPunch <sethtstubbs@gmail.com>
2020-08-22 13:11:47 -05:00
cd9fb6ec25 added sanity check to exitGame() 2020-08-22 13:08:37 -05:00
Onii-chan
56bf0db20d
Added more nano features (commands, equip & unequip, powers) (#26)
* Revert "fixed window build"

This reverts commit b94f602537e5773026b25deb868536868530b414.

* Revert "Revert "fixed window build""

This reverts commit dac4457ed27267774b1692533d2a9db8c3e344a9.

* Add nano power feature

* Update CNShardServer.hpp

* Update CNShardServer.hpp

* Test: Add nano power feature

Nano powers are set to the first power in its selection by default.

* Update NanoManager.cpp

* Test: More nano features

* Update NanoManager.hpp

* Update PlayerManager.hpp

* Update PlayerManager.cpp

* Updated indentations

* Update PlayerManager.cpp

* Add DEBUGLOG()

Co-authored-by: CPunch <sethtstubbs@gmail.com>
2020-08-22 13:02:08 -05:00
11fed7db10
Merge pull request #27 from dongresource/defines
Enumerate all packets (+misc FF defines), print packet names, verbose printing
2020-08-22 12:50:16 -05:00
35c622d8a2 Add support for verbose logging. 2020-08-22 19:39:13 +02:00
43f2def80b Report unhandled packets in string form. 2020-08-22 19:19:46 +02:00
0ac600e223 Extracted all packet ids to a single, definitive enum.
It also contains other constant values that might be relevant at some
point.
2020-08-22 17:25:42 +02:00
FinnHornhoover
5f65a84b02
Fix unhandled exception in NPCManager (#24)
* fixed PROTOCOL_VERSION not being defined

* handle exceptions in NPCManager init
2020-08-22 01:46:52 -05:00
78c493b461 switched default ip in config.ini 2020-08-21 23:37:09 -05:00
f71e1349c1 temp fix for U16toU8 edgecase 2020-08-21 22:11:04 -05:00
cff382a8ce sets a limit for sendData() 2020-08-21 21:32:22 -05:00
bbd6c5b532 moved header libraries 2020-08-21 21:03:12 -05:00
JadeShrineMaiden
ab6df26f92
Disabled GM mode (#22)
Temporarily disabled, players can now use item commands without GM mode.
2020-08-21 19:38:10 -05:00
FinnHornhoover
786ee5f4f4
fixed PROTOCOL_VERSION not being defined (#23) 2020-08-21 19:37:59 -05:00
c5efbceca3 added sanity checks for sendPacket() 2020-08-21 19:33:42 -05:00
bf6c5d1b6b fixed NPC scrape 2020-08-21 18:08:42 -05:00
fdfa7b5776 Merge branch 'master' of https://github.com/OpenFusionProject/OpenFusion 2020-08-21 17:14:38 -05:00
f289c72f6f populated NPCManager 2020-08-21 17:14:11 -05:00
Raymonf
7318d9b578
AppVeyor: Build all protocol versions on Linux (#21)
* let's try this

* Update appveyor.yml
2020-08-21 16:39:30 -04:00
JadeShrineMaiden
caaffcbe3d
Item Deletion and extra fixes (#17)
* Deleting Items

* fixes

* fixes 2

* Basic GM login

* Update ItemManager.cpp

Co-authored-by: Raymonf <raymonf@outlook.com>
2020-08-21 15:09:52 -05:00
3fe1a02200 include 0104 if PROTO_VERSION is undefined 2020-08-21 14:29:09 -05:00
cd19c54824 itemManager now uses a reference to the PlayerView 2020-08-21 14:28:14 -05:00
Raymonf
88d08ffca7
Fix MSVC compilation by not using non-standard struct initialization (#20)
Co-authored-by: Raymonf <Raymonf@users.noreply.github.com>
2020-08-21 14:17:06 -04:00
Raymonf
7a46f061ed
Fix Windows CI by removing leading 0 in packet versions 2020-08-21 13:50:09 -04:00
dongresource
df18f3ccd1
PROTOCOL_VERSION, test items, MOTD fix (#18)
* Cleaned up protocol selection.

* cmake now works even if protocol option is omitted
* make now supports protocol selection
* removed PACKET_VERSION/CNPROTO_VERSION* redundancy
* ubuntu appveyor script has yet to be written
* cleaned up some trailing spaces

* Add some test items.

Ironically, this change is untested.

* [bugfix] Transmit MOTD when entering the game, not when loading screen fades.

This fixes unnecessary retransmission when /warping.
2020-08-21 12:38:45 -05:00
1669ee3660 Merge branch 'master' of https://github.com/OpenFusionProject/OpenFusion 2020-08-21 00:31:09 -05:00
5d0b30b4cb added AINVEN_COUT for 0728 2020-08-21 00:31:00 -05:00
Raymonf
0041da795a
Build multiple packet versions on AppVeyor for Windows (#14)
* Move to PowerShell script for Windows build

* Allow CMake to override struct version

* PACKET_VERSION option

* Rename CNPROTO_CUSTOM to CNPROTO_OVERRIDE

Co-authored-by: Raymonf <Raymonf@users.noreply.github.com>
2020-08-21 01:18:19 -04:00
Raymonf
24be117e28
Add Discord badge to README (#13) 2020-08-20 23:26:27 -04:00
c7f9358ae5 Merge branch 'master' of https://github.com/OpenFusionProject/OpenFusion 2020-08-20 22:25:53 -05:00
eee1b52722 fixed ItemManager 2020-08-20 22:25:39 -05:00
CakeLancelot
e3d53e8dcf
Add Discord badge to README 2020-08-20 22:04:35 -05:00
Raymonf
b89df8d497
Mention AppVeyor in the readme 2020-08-20 22:54:11 -04:00
Raymonf
faf73fc835
Initial AppVeyor build configuration (#12) 2020-08-20 22:46:48 -04:00
JadeShrineMaiden
aa2adcd9e2
Items Implementation (#11)
* Item Manager (Initial Implementation)

* Item Manager (Second Phase)

* Item Manager (Phase Three)

* Not Working Code

* Inventory Implementation (Complete?)

* Items Implementation

-Fixed Indentations
-Final touches to make it all work

* Update Makefile

* Added small comments

-- needs to be fixed
2020-08-20 21:10:14 -05:00
dongresource
e044b4251a
Cleanup indentation. (#10) 2020-08-20 19:37:34 -05:00
7b085e9c8b added sanity checks 2020-08-20 18:50:30 -05:00
Raymonf
da11220762 Allow opening CMakeLists as a CMake project in VS
Added another check just in case someone wants to do this for some reason. It's bad. You shouldn't do it.
2020-08-20 18:44:30 -04:00
1425074ccb edited config.ini default back, better plr pos 2020-08-20 16:59:32 -05:00
Raymonf
c66ac111ab Silence codecvt deprecation warning on VC++ 2020-08-20 17:47:38 -04:00
Raymonf
32a37acd5a Update CMakeLists to use correct binary name and pthreads if not using MinGW/VS 2020-08-20 17:47:27 -04:00
49fbdd2154 Merge branch 'master' of https://github.com/OpenFusionProject/OpenFusion 2020-08-20 16:43:57 -05:00
6857f50c30 added basic NPCManager 2020-08-20 16:43:48 -05:00
Raymonf
c827b5a1b6 Lower CMake version requirement to 3.13
Technically we could probably move this all the way down to 3.6, but 3.13 should be a good version to target.
2020-08-20 17:21:43 -04:00
Raymonf
7f8e7dfa1c
Use regular old MIT license 2020-08-20 16:17:53 -04:00
Raymonf
09b21c54d3
Update compilation instructions 2020-08-20 16:10:29 -04:00
Raymonf
c549192f59
CMake build support (#8)
* CMake build support

* Make things nice for VS users

Co-authored-by: Raymonf <Raymonf@users.noreply.github.com>
2020-08-20 15:59:54 -04:00
6843faeb8d fixed windows makefile 2020-08-20 13:31:40 -05:00
b43628a19d faster player distance check 2020-08-20 13:19:03 -05:00
b94f602537 fixed window build 2020-08-20 12:05:01 -05:00
dongresource
fa5f194cc7
Standardized Makefile. (#7)
* Standardized Makefile.

* Incremental compilation
* C++ compilers are called CXX
* Removed excessive comments for well-understood things
* Added clean target
* Proper .PHONY usage
* Updated .gitignore for object files (+ ctags, and vim temp files)

* Add mkdir -p bin.
2020-08-20 12:03:15 -05:00
fbc3c79aa2
Merge pull request #6 from CakeLancelot/add-more-movement-packets
Add zipline, slide, cannon, and jump pad packets
2020-08-20 11:38:13 -05:00
93d973bf21 small CNStructs.hpp refactor 2020-08-20 11:37:47 -05:00
7cf239e3af added 0728 structs + small CNStruct.hpp refactor 2020-08-20 11:36:29 -05:00
CakeLancelot
14d556976d Add zipline, slide, cannon, and jump pad packets
Cannon still looks wonky from other player's PoV, something's up with the rotation - will investigate later.
2020-08-20 11:26:26 -05:00
af6158fbb2
Merge pull request #5 from dongresource/master
Add example config.ini with documented options.
2020-08-20 10:53:45 -05:00
c0abab39ae better structs header 2020-08-20 10:45:50 -05:00
dff6e8c23b better structs header 2020-08-20 10:44:49 -05:00
d6e1f57c23 merged master 2020-08-20 10:43:37 -05:00
9f3f9bb9c3 Fix grammar. 2020-08-20 17:24:27 +02:00
0654500df3 Add default config.ini.
Custom servers will always need one anyway. Might as well give them a
properly documented example to start with.
2020-08-20 17:14:25 +02:00
a0065c2050
Merge pull request #4 from Eperty123/master
Added nano summon feature
2020-08-20 09:48:31 -05:00
Onii-chan
795107a274 Added nano summon feature 2020-08-20 11:51:02 +02:00
42597c2a7a bitch, we good! 2020-08-19 22:32:33 -05:00
Raymonf
bb1ce5c28d fix: MOTD not showing up without config.ini
Since we do an early return without a config.ini file, the MOTDSTRING will still be the default std::string value (""), causing the game to output "Gamemaster: ". To fix this, we'll just hardcode the preferred default value for now.
2020-08-19 22:39:23 -04:00
Raymonf
e75049fc98 MSVC support: alternative implementation of getTime() 2020-08-19 22:26:46 -04:00
Raymonf
1ec4634f69 long -> int64_t; push and pop the original packing for structures
fixes the issue with the tutorial thing
2020-08-19 22:25:19 -04:00
4d9072a752 added radio's logo, started NPCManager 2020-08-19 17:21:35 -05:00
a0d59419f1 changed non-blocking failure to a socket shutdown 2020-08-19 15:54:44 -05:00
94cb89dd6b fixed warnings for windows mingw 2020-08-19 15:42:44 -05:00
ba81db97ef organized structs 2020-08-19 15:07:11 -05:00
ec84d6ca58 scraped all 0104 structs 2020-08-19 15:00:39 -05:00
b8f7d2efc6 added comments to chatHandler() 2020-08-19 13:25:54 -05:00
e7b58c4b32 fixed formatting 2020-08-19 13:22:51 -05:00
0f5be27c97
Merge pull request #3 from JadeShrineMaiden/master
Chat
2020-08-19 13:21:41 -05:00
8328ebf4f3 Chat Feature
Chat with other players, no swearing!!!
2020-08-19 18:47:25 +01:00
a17b72b0b3 Revert "Chat Feature"
This reverts commit 95e454232ab459b5e0bf5dea144fd962d52d923a.
2020-08-19 18:43:24 +01:00
95e454232a Chat Feature
It just works
2020-08-19 18:37:15 +01:00
fafde9348e lol, whoops 2020-08-19 12:24:05 -05:00
f2059c9ce1 merged motd and exit patch by dongresource 2020-08-19 12:22:54 -05:00
Raymonf
e5274045b0
fix the other type 2020-08-18 22:29:53 -04:00
Raymonf
941d98cc07
Merge pull request #2 from OpenFusionProject/Raymonf-readme-windows
Add a link to Windows compilation instructions
2020-08-18 22:28:58 -04:00
Raymonf
3a892de7c7
Add a link to Windows compilation instructions 2020-08-18 22:11:29 -04:00
8e87a3f102 release makefile 2020-08-18 20:35:48 -05:00
24d30a05bf added heartbeat support 2020-08-18 20:34:39 -05:00
b2325eb308 login server moved to main thread 2020-08-18 19:52:02 -05:00
47b76b422c added sanity checks 2020-08-18 19:11:31 -05:00
208f4b3bbd fixed typo in license lol 2020-08-18 18:45:43 -05:00
78e096a411 added cakes server to readme 2020-08-18 17:28:00 -05:00
2e9e265cba fixed readme 2020-08-18 16:06:24 -05:00
8400a15262 added release to readme 2020-08-18 15:55:06 -05:00
143 changed files with 64339 additions and 1879 deletions

23
.editorconfig Normal file
View File

@ -0,0 +1,23 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# 4 space indentation
[*.cpp,*.hpp]
indent_style = space
indent_size = 4
# Tabs in makefile
[Makefile]
indent_style = tab
# Don't enforce anything in vendored code
[/vendor/**]
end_of_line = unset
insert_final_newline = unset
indent_style = unset
indent_style = unset

5
.gitattributes vendored Normal file
View File

@ -0,0 +1,5 @@
vendor/* linguist-vendored
# Always checkout source with LF line endings
src/*.c text eol=lf
src/*.h text eol=lf

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

@ -0,0 +1,149 @@
name: Check Builds
on:
push:
paths:
- src/**
- vendor/**
- .github/workflows/check-builds.yaml
- CMakeLists.txt
- Makefile
- tdata
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
paths:
- src/**
- vendor/**
- CMakeLists.txt
- Makefile
workflow_dispatch:
jobs:
ubuntu-build:
runs-on: ubuntu-22.04
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 libsqlite3-dev -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 "tdata" -Destination "bin/tdata" -Recurse
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 "tdata" -Destination "bin/$version-$configuration/tdata" -Recurse
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_type == 'tag' || github.ref_name == 'master')
runs-on: ubuntu-latest
needs: [windows-build, ubuntu-build]
env:
BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }}
ENDPOINT: ${{ secrets.ENDPOINT }}
steps:
- uses: actions/checkout@v3
with:
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]
workflow_dispatch:
jobs:
push-docker-image:
name: Push Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
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: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push the Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKERHUB_REPOSITORY }}:${{ github.ref_name }},${{ secrets.DOCKERHUB_REPOSITORY }}:${{ steps.split.outputs._0 }},${{ secrets.DOCKERHUB_REPOSITORY }}:latest

20
.gitignore vendored
View File

@ -1,4 +1,20 @@
.vscode
.vscode/
bin/*
notes.txt
config.ini
config.ini
*.o
tags
*~
CMakeFiles/
CMakeCache.txt
build/
.vs/
.idea/
*.db
*-shm
*-wal
version.h
infer-out
gmon.out
*.bak

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "tdata"]
path = tdata
url = https://github.com/OpenFusionProject/tabledata.git

9
.vimrc Normal file
View File

@ -0,0 +1,9 @@
" vim configuration file
" You will need to put 'set exrc' and 'set secure' into your main .vimrc file,
" in which case this file will be loaded automatically, but *only* if you
" start vim in this dir. Alternatively you can just load it directly with
" ':so .vimrc' every time.
set tabstop=4
set shiftwidth=4
set expandtab

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}"
}
]
}

61
CMakeLists.txt Normal file
View File

@ -0,0 +1,61 @@
cmake_minimum_required(VERSION 3.13)
project(OpenFusion)
set(CMAKE_CXX_STANDARD 17)
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
# For example: cmake -B build -DPROTOCOL_VERSION=728
set(PROTOCOL_VERSION 104 CACHE STRING "The packet version to build")
add_compile_definitions(PROTOCOL_VERSION=${PROTOCOL_VERSION})
# Disallow in-source builds
if (${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})
message(FATAL_ERROR "In-source builds not allowed. Please refer to the wiki for more information. Please remove the CMakeFiles folder and the CMakeCache.txt file.")
endif()
# Output binaries to the bin folder in the source directory
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
# Put CMake targets (ALL_BUILD/ZERO_CHECK) into a folder
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
# Set the OpenFusion project as the default startup project for VS
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT openfusion)
if (WIN32)
# Set the output binary name to winfusion to match the regular Makefile
set(BIN_NAME winfusion)
else()
set(BIN_NAME fusion)
endif()
include_directories(src vendor)
file(GLOB_RECURSE SOURCES src/**.[ch]pp vendor/**.[ch]pp vendor/**.[ch] version.h)
configure_file(version.h.in ${CMAKE_SOURCE_DIR}/version.h @ONLY)
add_executable(openfusion ${SOURCES})
set_target_properties(openfusion PROPERTIES OUTPUT_NAME ${BIN_NAME})
# 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}")
# Use pthreads if not generating a VS solution or MinGW makefile (because MinGW will prefer Win32 threads)
# Checking if the compiler ID is MSVC will allow us to open the project as a CMake project in VS.
# 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 PRIVATE pthread)
endif()

114
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,114 @@
# Contributing
If you want to contribute to OpenFusion's development, that's great!
We'd appreciate it if you already had some programming experience, as well as experience with software version control using git.
If you've never used git before, please take the time to research it yourself.
There is an abundance of online resources for getting started with git.
If, however, you have no experience programming, this type of project may not be the best one to start with.
OpenFusion is written in C++, which really isn't the best language to start with, as it has many extremely subtle pitfalls with potentially catastrophic consequences that are often very hard for an inexperienced programmer to detect and avoid.
This is compounded by the fact that this is a server, which means it's a long-running, multithreaded process exposed to the network, decoding an externally-imposed binary protocol -- written in C++.
A bad combination.
With that out of the way, the rest of this document will serve to address the matter of clean commits in Pull Requests.
## Repository cleanliness
The commit history in a git repository is important.
Unlike some other version control systems, git allows developers to destructively edit history so as to enjoy the benefits of both frequent "saves" (as in, having the ability to record corrections as soon as you make them) and clean commits which perfectly encapsulate a given change.
Each developer has their own preferred way of doing things, of course, but in general one commit should represent one functional change in the codebase as a whole.
Developers should be able to view a project's commit history and easily understand how the project has evolved over time.
Achieving this is often daunting for new users of git.
I know of two intermediate-level resources (that is, for after you've learned the basics of git) I can recommend for understanding how to properly manage your repository's history -- [think-like-a-git.net](http://think-like-a-git.net/) and [git-rebase.io](https://git-rebase.io/).
Both are pretty short reads and following them will get you up to speed with branches and the rebasing thereof.
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
Many Pull Requests OpenFusion receives fail to present a clean set of commits to merge.
These are generally either:
* Dozens of quick-fix commits the author made while working on their contribution
* Countless useless merge commits generated while trying to re-synchronize with the upstream repository
Few developers are fine with having their commit histories utterly destroyed by merging these changes in.
Many projects, when presented with such Pull Requests, will flat-out reject them or demand the author clean them up first before they can be accepted.
Cpunch, however, chooses to accept them anyway, but squashes them into the repository with the "rebase" merge strategy, instead of a regular merge.
Whereas a regular merge creates a "merge commit" which unites two branches together, a rebase instead *reconstructs* the commits from one branch onto the other, creating *different commits* with possibly the same contents.
If you read the above links, you'll note that this isn't exactly a perfect solution either, since rewriting history in public is usually a bad idea, but because these changes were originally only visible to the PR author, it is only they that will need to rebase their fork to re-sync with the upstream repository.
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
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).
(Most commits have one parent. Merge commits almost always have two, but octopus merges can have any number of parents.)
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
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:
* Ensure your `upstream` remote is up to date with `git fetch upstream`
* Make sure you're on master (`git branch master`)
* Set your master branch to the latest commit with `git reset --hard upstream/master`
* Propagate the change to your GitHub fork with `git push -f origin master`
And you're good to go.
If you do have some committed changes that haven't yet been merged upstream, you should probably save them on another branch (called "backup" or something) with `git checkout -b backup`.
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
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`.
That way you can always keep master in sync with upstream with `git pull --ff-only upstream master`.
(`--ff-only` can be left out. If you find that git want you to make a merge commit, just back out of it by saving an empty commit message, then fix whatever the cause was.)
* If upstream gets new changes before you've had a chance to submit yours, just update master, create a new branch from your work branch and rebase your new work branch on master (such that your up-to-date changeset is now on the new work branch) and submit that one for your PR
* If you end up making a few ugly fixup commits, use `git rebase --interactive` to clean them up (on a new branch) before submitting your changeset
* If you get told to change something in the PR before it's merged, but after you've pushed it to your GitHub fork, rebase your changes locally, then force-push them onto the your fork's PR branch with `git push -f origin work1` or so
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.

41
Dockerfile Normal file
View File

@ -0,0 +1,41 @@
# 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 nosandbox -j$(nproc)
# 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"]
EXPOSE 23000/tcp
EXPOSE 23001/tcp
EXPOSE 8003/tcp
LABEL Name=openfusion Version=2.0.0

View File

@ -1,9 +1,21 @@
The OpenFusion MIT except Marlorn License
MIT License
Copyright 2020 Seth Stubbs
Copyright (c) 2020-2025 OpenFusion Contributors
Excluding the individual known as "MarlornWS" and their associates, permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

192
Makefile
View File

@ -1,23 +1,181 @@
# makefile for OpenFusion
OBJS = src/*.cpp # source files to compile
CC = clang++ # using GNU C++ compiler
WIN_CC = x86_64-w64-mingw32-g++ # using GNU C++ compiler
GIT_VERSION!=git describe --tags
CC=clang
CXX=clang++
# -w suppresses all warnings (the part that's commented out helps me find memory leaks, it ruins performance though!)
COMPILER_FLAGS = -std=c++17 -o3 -static #-g3 -fsanitize=address
WIN_COMPILER_FLAGS = -std=c++17 -o3 -static #-g3 -fsanitize=address
# If compiling with ASAN, invoke like this: $ LSAN_OPTIONS=suppressions=suppr.txt bin/fusion
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
#LINKER_FLAGS specifies the libraries we're linking against (NONE, this is a single header library.)
LINKER_FLAGS = -lpthread
WIN_LINKER_FLAGS = -lws2_32 -lwsock32
# assign protocol version
# this can be overriden by ex. make PROTOCOL_VERSION=728
PROTOCOL_VERSION?=104
#OBJ_NAME specifies the name of our exectuable
OBJ_NAME = bin/fusion # location of output for build
WIN_OBJ_NAME = bin/winfusion.exe # location of output for build
# Windows-specific
WIN_CC=x86_64-w64-mingw32-gcc
WIN_CXX=x86_64-w64-mingw32-g++
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
all: $(OBJS)
$(CC) $(OBJS) $(COMPILER_FLAGS) $(LINKER_FLAGS) -o $(OBJ_NAME)
# C code; currently exclusively from vendored libraries
CSRC=\
vendor/bcrypt/bcrypt.c\
vendor/bcrypt/crypt_blowfish.c\
vendor/bcrypt/crypt_gensalt.c\
vendor/bcrypt/wrapper.c\
windows: $(OBJS)
$(WIN_CC) $(OBJS) $(WIN_COMPILER_FLAGS) $(WIN_LINKER_FLAGS) -o $(WIN_OBJ_NAME)
CHDR=\
vendor/bcrypt/bcrypt.h\
vendor/bcrypt/crypt_blowfish.h\
vendor/bcrypt/crypt_gensalt.h\
vendor/bcrypt/ow-crypt.h\
vendor/bcrypt/winbcrypt.h\
CXXSRC=\
src/core/CNProtocol.cpp\
src/core/CNShared.cpp\
src/core/Packets.cpp\
src/servers/CNLoginServer.cpp\
src/servers/CNShardServer.cpp\
src/servers/Monitor.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\
src/Email.cpp\
src/Eggs.cpp\
src/main.cpp\
src/Missions.cpp\
src/MobAI.cpp\
src/Combat.cpp\
src/Nanos.cpp\
src/Abilities.cpp\
src/Items.cpp\
src/NPCManager.cpp\
src/PlayerManager.cpp\
src/PlayerMovement.cpp\
src/BuiltinCommands.cpp\
src/settings.cpp\
src/Transport.cpp\
src/TableData.cpp\
src/Chunking.cpp\
src/Buddies.cpp\
src/Groups.cpp\
src/Racing.cpp\
src/Vendors.cpp\
src/Trading.cpp\
src/Rand.cpp\
# headers (for timestamp purposes)
CXXHDR=\
src/core/CNProtocol.hpp\
src/core/CNShared.hpp\
src/core/CNStructs.hpp\
src/core/Packets.hpp\
src/core/Defines.hpp\
src/core/Core.hpp\
src/servers/CNLoginServer.hpp\
src/servers/CNShardServer.hpp\
src/servers/Monitor.hpp\
src/db/Database.hpp\
src/db/internal.hpp\
src/sandbox/Sandbox.hpp\
vendor/bcrypt/BCrypt.hpp\
vendor/INIReader.hpp\
vendor/JSON.hpp\
src/Buffs.hpp\
src/Chat.hpp\
src/CustomCommands.hpp\
src/Entities.hpp\
src/Email.hpp\
src/Eggs.hpp\
src/Missions.hpp\
src/MobAI.hpp\
src/Combat.hpp\
src/Nanos.hpp\
src/Abilities.hpp\
src/Items.hpp\
src/NPCManager.hpp\
src/Player.hpp\
src/PlayerManager.hpp\
src/PlayerMovement.hpp\
src/BuiltinCommands.hpp\
src/settings.hpp\
src/Transport.hpp\
src/TableData.hpp\
src/Bucket.hpp\
src/Chunking.hpp\
src/Buddies.hpp\
src/Groups.hpp\
src/Racing.hpp\
src/Vendors.hpp\
src/Trading.hpp\
src/Rand.hpp\
COBJ=$(CSRC:.c=.o)
CXXOBJ=$(CXXSRC:.cpp=.o)
OBJ=$(COBJ) $(CXXOBJ)
HDR=$(CHDR) $(CXXHDR)
all: $(SERVER)
windows: $(SERVER)
nosandbox: $(SERVER)
nolandlock: $(SERVER)
# assign Windows-specific values if targeting Windows
windows : CC=$(WIN_CC)
windows : CXX=$(WIN_CXX)
windows : CFLAGS=$(WIN_CFLAGS)
windows : CXXFLAGS=$(WIN_CXXFLAGS)
windows : LDFLAGS=$(WIN_LDFLAGS)
windows : SERVER=$(WIN_SERVER)
nosandbox : CFLAGS+=-DCONFIG_NOSANDBOX=1
nolandlock : CFLAGS+=-DCONFIG_NOLANDLOCK=1
.SUFFIXES: .o .c .cpp .h .hpp
.c.o:
$(CC) -c $(CFLAGS) -o $@ $<
.cpp.o:
$(CXX) -c $(CXXFLAGS) -o $@ $<
# header timestamps are a prerequisite for OF object files
$(CXXOBJ): $(HDR)
$(SERVER): $(OBJ) $(CHDR) $(CXXHDR)
mkdir -p bin
$(CXX) $(OBJ) $(LDFLAGS) -o $(SERVER)
# compatibility with how cmake injects GIT_VERSION
version.h:
touch version.h
src/main.o: version.h
.PHONY: all windows nosandbox nolandlock clean nuke
# only gets rid of OpenFusion objects, so we don't need to
# recompile the libs every time
clean:
rm -f src/*.o src/*/*.o $(SERVER) $(WIN_SERVER) version.h
# gets rid of all compiled objects, including the libraries
nuke:
rm -f $(OBJ) $(SERVER) $(WIN_SERVER) version.h

102
README.md
View File

@ -1,23 +1,43 @@
# OpenFusion
<p align="center"><img width="640" src="res/openfusion-hero.png" alt="OpenFusion Logo"></p>
OpenFusion is a landwalker server for FusionFall. It currently supports versions `beta-20100104` and `beta-20100728` of the original game.
<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://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://hub.docker.com/repository/docker/openfusion/openfusion/"><img src="https://badgen.net/docker/pulls/openfusion/openfusion?icon=docker&label=pulls"></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>
Further documentation pending.
OpenFusion is a reverse-engineered server for FusionFall. It primarily targets versions `beta-20100104` and `beta-20111013` of the original game, with [limited support](https://openfusion.dev/docs/reference/fusionfall-version-support/) for others.
## Usage
tl;dr:
### Getting Started
#### Method A: Installer (Easiest)
1. Download the launcher installer by clicking [here](https://github.com/OpenFusionProject/OpenFusionLauncher/releases/latest/download/OpenFusionLauncher-Windows-Installer.exe) - choose to run the file.
2. After a few moments, the launcher 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+server bundle from [here](...).
2. Run `FreeClient/installUnity.bat` once
#### Method B: Standalone .zip file
1. Download the launcher from [here](https://github.com/OpenFusionProject/OpenFusionLauncher/releases/latest/download/OpenFusionLauncher-Windows-Portable.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 OpenFusionLauncher.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.
From then on, any time you want to run the "game":
Instructions for getting the client to run on Linux through Wine can be found [here](https://openfusion.dev/docs/guides/running-on-linux/).
3. Run `OpenFusion/winfusion.exe`
4. Run `FreeClient/OpenFusionClient.exe`
### Hosting a server
1. Grab `OpenFusionServer-Windows-Original.zip` or `OpenFusionServer-Windows-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/latest).
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:
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.
You have two randomized characters available to you on the Character Selection screen, one boy, one girl.
You can also make your own character and play through the tutorial. The tutorial can be skipped by pressing the ~ key.
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.
@ -34,54 +54,58 @@ 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.
The web player will execute the game code, which will request the following files from the server: `/assetInfo.php` and `/loginInfo.php`.
`FreeClient/resources/app/files/assetInfo.php` contains the address from which to fetch the rest of the game's assets (the "dongresources").
`/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.
`FreeClient/resources/app/files/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.
`/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.
When the player clicks "ENTER THE GAME" (or completes the tutorial), the login server sends it the address of the shard server, which the client will then connect to and remain connected to during gameplay.
## Configuration
You can change the ports the FusionFall server listens on in `OpenFusion/config.ini`. Make sure the login server port is in sync with `loginInfo.php`.
The shard port needs no such synchronization.
You can also configure the distance at which you'll be able to see other players, though by default it's already as high as you'll want it.
You can change the ports the FusionFall server listens on in `config.ini`. Make sure the login server port is in sync with what you enter into the client's server list - the shard port needs no such synchronization.
If you want to play with friends, you can change the IP in `loginInfo.php` to a login server hosted elsewhere.
This config file also has several other options you can tweak, including log verbosity, database saving interval, default account/permission level, and more. See the comments within [the config file itself](https://github.com/OpenFusionProject/OpenFusion/blob/master/config.ini) for more details.
If you want to play with friends, simply enter the login server details into the `Add Server` dialogue in OpenFusionClient.
This just works if you're all under the same LAN, but if you want to play over the internet you'll need to open a port, use a service like Hamachi or nGrok, or host the server on a VPS (just like any other gameserver).
If you're in a region in which Turner's CDN doesn't still have the game's assets cached, you won't be able to play the game in its default configuration.
You'll need to obtain the necessary assets elsewhere and set up your own local web server to host them, because unlike web browsers, the game itself cannot interpret the `file://` schema, and will thus need the assets hosted on an actual HTTP server.
Don't forget to point `assetInfo.php` to where you're hosting the assets and change the `src` param of both the `<embed>` tag and the `<object>` tag in `FreeClient/resources/files/index.html` to where you're hosting the `.unity3d` entrypoint.
If you change `loginInfo.php` or `assetInfo.php`, make sure not to put any newline characters (or any other whitespace) at the end of the file(s).
Some modern IDEs/text editors do this automatically. If all else fails, use Notepad.
## Compiling
OpenFusion can be compiled from source using the included makefile. to compile for windows (on a *nix system) use `make windows`, otherwise to compile it for the current platform you're on just run `make`
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](https://openfusion.dev/docs/development/installing-sqlite-on-windows-using-vcpkg/).
## "Gameplay"
You have two choices for compiling OpenFusion: the included Makefile and the included CMakeLists file.
Notice the quotes. This is not a full-fledged game that can be played.
It's what's called a landwalker; enough of the server has been implemented to allow players to run around in the game world, and not much else.
### Makefile
![](res/sane_upsell.png)
A detailed compilation guide is available for Windows users on the website [using MinGW-w64 and MSYS2](https://openfusion.dev/docs/development/compilation-on-windows-msys2-mingw/). Otherwise, to compile it for the current platform you're on, just run `make` with the correct build tools installed (currently make and clang).
To make your landwalking experience more pleasant, you can make use of a few admin commands to get around easier:
### CMake
* 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.).
A detailed guide is available [in our documentation](https://openfusion.dev/docs/development/compilation-with-cmake-or-visual-studio/) for people using regular old CMake or the version of CMake that comes with Visual Studio. TL;DR: `cmake -B build`
## Contributing
If you'd like to contribute to this project, please read [CONTRIBUTING.md](CONTRIBUTING.md).
## Gameplay
The goal of the project is to faithfully recreate the game as it was at the time of the targeted build.
While most features are implemented and the game is playable start to finish, there may be missing functionality or bugs present.
Depending on the server configuration, you'll have access to certain commands.
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).
When hosting a local server, you will have access to all commands by default (account level 1).
For a list of available commands, see [this page](https://openfusion.dev/docs/reference/ingame-command-list/).

111
config.ini Normal file
View File

@ -0,0 +1,111 @@
# verbosity level
# 0 = mostly silence
# 1 = debug prints and unknown packets
# 2 = print all packets except LIVE_CHECK and movement
# 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 name wheel names be approved instantly?
acceptallwheelnames=true
# 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 logging in with plaintext passwords
# cookie = allow logging in with one-shot auth cookies
authmethods=password
# how often should everything be flushed to the database?
# the default is 4 minutes
dbsaveinterval=240
# Shard Server configuration
[shard]
port=23001
ip=127.0.0.1
# distance at which other players and NPCs become visible.
# this value is used for calculating chunk size
viewdistance=16000
# time, in milliseconds, to wait before kicking a non-responsive client
# default is 1 minute
timeout=60000
# should mobs move around and fight back?
# can be disabled for easier mob placement
simulatemobs=true
# little message players see when they enter the game
motd=Welcome to OpenFusion!
# The following are the default locations of the JSON files the server
# requires to run. You can override them by changing their values and
# uncommenting them (removing the leading # character from that line).
# location of the tabledata folder
#tdatadir=tdata/
# location of the patch folder
#patchdir=tdata/patch/
# Space-separated list of patch folders in patchdir to load from.
# If you uncomment this, note that Academy builds *must* contain 1013,
# and pre-Academy builds must *not* contain it.
#enabledpatches=1013
# xdt json filename
#xdtdata=xdt.json
# NPC json filename
#npcdata=NPCs.json
# mob json filename
#mobdata=mobs.json
# path json filename
#pathdata=paths.json
# drop json filename
#dropdata=drops.json
# gruntwork output filename (this is what you submit)
#gruntwork=gruntwork.json
# 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
# account permission level that will be set upon character creation
# 1 = default, will allow *all* commands
# 30 = allow some more "abusable" commands such as /summon
# 50 = only allow cheat commands, like /itemN and /speed
# 99 = standard user account, no cheats allowed
# any number higher than 50 will disable commands
accountlevel=1
# should mobs drop event crates?
# 0 = no event
# 1 = Knishmas
# 2 = Halloween
# 3 = Easter
eventmode=0
# you can override the default spawn point.
# these example coords are for the Future (Z is height):
#spawnx=632032
#spawny=187177
#spawnz=-5500
# Player location monitor interface configuration
[monitor]
enabled=false
# the port to listen for connections on
port=8003
# The local IP to listen on.
# Do not change this unless you know what you're doing.
listenip=127.0.0.1
# how often the listeners should be updated (in milliseconds)
interval=5000

14
docker-compose.yml Normal file
View File

@ -0,0 +1,14 @@
services:
openfusion:
build:
context: .
dockerfile: ./Dockerfile
image: openfusion/openfusion:latest
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"

BIN
res/openfusion-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

BIN
res/radiorave_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

18
sql/migration1.sql Normal file
View File

@ -0,0 +1,18 @@
BEGIN TRANSACTION;
-- New Columns
ALTER TABLE Accounts ADD BanReason TEXT DEFAULT '' NOT NULL;
ALTER TABLE RaceResults ADD RingCount INTEGER DEFAULT 0 NOT NULL;
ALTER TABLE RaceResults ADD Time INTEGER DEFAULT 0 NOT NULL;
-- Fix timestamps in Meta
INSERT INTO Meta (Key, Value) VALUES ('Created', 0);
INSERT INTO Meta (Key, Value) VALUES ('LastMigration', strftime('%s', 'now'));
UPDATE Meta SET Value = (SELECT Created FROM Meta WHERE Key = 'ProtocolVersion') Where Key = 'Created';
-- Get rid of 'Created' Column
CREATE TABLE Temp(Key TEXT NOT NULL UNIQUE, Value INTEGER NOT NULL);
INSERT INTO Temp SELECT Key, Value FROM Meta;
DROP TABLE Meta;
ALTER TABLE Temp RENAME TO Meta;
-- Update DB Version
UPDATE Meta SET Value = 2 WHERE Key = 'DatabaseVersion';
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
COMMIT;

37
sql/migration2.sql Normal file
View File

@ -0,0 +1,37 @@
/*
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 code items
CREATE TABLE RedeemedCodes(
PlayerID INTEGER NOT NULL,
Code TEXT NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Code)
);
-- Change Coordinates in Players table to non-plural form
ALTER TABLE Players RENAME COLUMN XCoordinates TO XCoordinate;
ALTER TABLE Players RENAME COLUMN YCoordinates TO YCoordinate;
ALTER TABLE Players RENAME COLUMN ZCoordinates TO ZCoordinate;
-- Fix email attachments not being unique enough
CREATE TABLE Temp (
PlayerID INTEGER NOT NULL,
MsgIndex INTEGER NOT NULL,
Slot INTEGER NOT NULL,
ID INTEGER NOT NULL,
Type INTEGER NOT NULL,
Opt INTEGER NOT NULL,
TimeLimit INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, MsgIndex, Slot)
);
INSERT INTO Temp SELECT * FROM EmailItems;
DROP TABLE EmailItems;
ALTER TABLE Temp RENAME TO EmailItems;
-- Update DB Version
UPDATE Meta SET Value = 3 WHERE Key = 'DatabaseVersion';
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
COMMIT;
PRAGMA foreign_keys=ON;

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;

8
sql/migration5.sql Normal file
View File

@ -0,0 +1,8 @@
BEGIN TRANSACTION;
-- New Columns
ALTER TABLE Accounts ADD Email TEXT DEFAULT '' NOT NULL;
ALTER TABLE Accounts ADD LastPasswordReset INTEGER DEFAULT 0 NOT NULL;
-- Update DB Version
UPDATE Meta SET Value = 6 WHERE Key = 'DatabaseVersion';
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
COMMIT;

171
sql/tables.sql Normal file
View File

@ -0,0 +1,171 @@
CREATE TABLE IF NOT EXISTS Accounts (
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,
Email TEXT DEFAULT '' NOT NULL,
LastPasswordReset INTEGER DEFAULT 0 NOT NULL,
PRIMARY KEY(AccountID AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS Players (
PlayerID INTEGER NOT NULL,
AccountID INTEGER NOT NULL,
FirstName TEXT NOT NULL COLLATE NOCASE,
LastName TEXT NOT NULL COLLATE NOCASE,
NameCheck INTEGER NOT NULL,
Slot INTEGER NOT NULL,
Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
Level INTEGER DEFAULT 1 NOT NULL,
Nano1 INTEGER DEFAULT 0 NOT NULL,
Nano2 INTEGER DEFAULT 0 NOT NULL,
Nano3 INTEGER DEFAULT 0 NOT NULL,
AppearanceFlag INTEGER DEFAULT 0 NOT NULL,
TutorialFlag INTEGER DEFAULT 0 NOT NULL,
PayZoneFlag INTEGER DEFAULT 0 NOT NULL,
XCoordinate INTEGER NOT NULL,
YCoordinate INTEGER NOT NULL,
ZCoordinate INTEGER NOT NULL,
Angle INTEGER NOT NULL,
HP INTEGER NOT NULL,
FusionMatter INTEGER DEFAULT 0 NOT NULL,
Taros INTEGER DEFAULT 0 NOT NULL,
BatteryW INTEGER DEFAULT 0 NOT NULL,
BatteryN INTEGER DEFAULT 0 NOT NULL,
Mentor INTEGER DEFAULT 5 NOT NULL,
CurrentMissionID INTEGER DEFAULT 0 NOT NULL,
WarpLocationFlag INTEGER DEFAULT 0 NOT NULL,
SkywayLocationFlag BLOB NOT NULL,
FirstUseFlag BLOB NOT NULL,
Quests BLOB NOT NULL,
PRIMARY KEY(PlayerID AUTOINCREMENT),
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
UNIQUE (AccountID, Slot),
UNIQUE (FirstName, LastName)
);
CREATE TABLE IF NOT EXISTS Appearances (
PlayerID INTEGER UNIQUE NOT NULL,
Body INTEGER DEFAULT 0 NOT NULL,
EyeColor INTEGER DEFAULT 1 NOT NULL,
FaceStyle INTEGER DEFAULT 1 NOT NULL,
Gender INTEGER DEFAULT 1 NOT NULL,
HairColor INTEGER DEFAULT 1 NOT NULL,
HairStyle INTEGER DEFAULT 1 NOT NULL,
Height INTEGER DEFAULT 0 NOT NULL,
SkinColor INTEGER DEFAULT 1 NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS Inventory (
PlayerID INTEGER NOT NULL,
Slot INTEGER NOT NULL,
ID INTEGER NOT NULL,
Type INTEGER NOT NULL,
Opt INTEGER NOT NULL,
TimeLimit INTEGER DEFAULT 0 NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Slot)
);
CREATE TABLE IF NOT EXISTS QuestItems (
PlayerID INTEGER NOT NULL,
Slot INTEGER NOT NULL,
ID INTEGER NOT NULL,
Opt INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Slot)
);
CREATE TABLE IF NOT EXISTS Nanos (
PlayerID INTEGER NOT NULL,
ID INTEGER NOT NULL,
Skill INTEGER NOT NULL,
Stamina INTEGER DEFAULT 150 NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, ID)
);
CREATE TABLE IF NOT EXISTS RunningQuests (
PlayerID INTEGER NOT NULL,
TaskID INTEGER NOT NULL,
RemainingNPCCount1 INTEGER NOT NULL,
RemainingNPCCount2 INTEGER NOT NULL,
RemainingNPCCount3 INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS Buddyships (
PlayerAID INTEGER NOT NULL,
PlayerBID INTEGER NOT NULL,
FOREIGN KEY(PlayerAID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
FOREIGN KEY(PlayerBID) REFERENCES Players(PlayerID) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS Blocks (
PlayerID INTEGER NOT NULL,
BlockedPlayerID INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
FOREIGN KEY(BlockedPlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS EmailData (
PlayerID INTEGER NOT NULL,
MsgIndex INTEGER NOT NULL,
ReadFlag INTEGER NOT NULL,
ItemFlag INTEGER NOT NULL,
SenderID INTEGER NOT NULL,
SenderFirstName TEXT NOT NULL COLLATE NOCASE,
SenderLastName TEXT NOT NULL COLLATE NOCASE,
SubjectLine TEXT NOT NULL,
MsgBody TEXT NOT NULL,
Taros INTEGER NOT NULL,
SendTime INTEGER NOT NULL,
DeleteTime INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE(PlayerID, MsgIndex)
);
CREATE TABLE IF NOT EXISTS EmailItems (
PlayerID INTEGER NOT NULL,
MsgIndex INTEGER NOT NULL,
Slot INTEGER NOT NULL,
ID INTEGER NOT NULL,
Type INTEGER NOT NULL,
Opt INTEGER NOT NULL,
TimeLimit INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, MsgIndex, Slot)
);
CREATE TABLE IF NOT EXISTS RaceResults (
EPID INTEGER NOT NULL,
PlayerID INTEGER NOT NULL,
Score INTEGER NOT NULL,
RingCount INTEGER NOT NULL,
Time INTEGER NOT NULL,
Timestamp INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS RedeemedCodes (
PlayerID INTEGER NOT NULL,
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)
);

516
src/Abilities.cpp Normal file
View File

@ -0,0 +1,516 @@
#include "Abilities.hpp"
#include "servers/CNShardServer.hpp"
#include "NPCManager.hpp"
#include "PlayerManager.hpp"
#include "Buffs.hpp"
#include "Nanos.hpp"
#include "MobAI.hpp"
using namespace Abilities;
std::map<int32_t, SkillData> Abilities::SkillTable;
#pragma region Skill handlers
static SkillResult handleSkillDamage(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
EntityRef sourceRef = source->getRef();
double scalingFactor = 1;
if(sourceRef.kind == EntityKind::PLAYER)
scalingFactor = std::max(source->getMaxHP(), target->getMaxHP()) / 1000.0;
else
scalingFactor = source->getMaxHP() / 1500.0;
int damage = (int)(skill->values[0][power] * scalingFactor);
int dealt = target->takeDamage(sourceRef, damage);
sSkillResult_Damage result{};
result.eCT = target->getCharType();
result.iID = target->getID();
result.bProtected = dealt <= 0;
result.iDamage = dealt;
result.iHP = target->getCurrentHP();
return SkillResult(sizeof(sSkillResult_Damage), &result);
}
static SkillResult handleSkillHealHP(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
EntityRef sourceRef = source->getRef();
int heal = skill->values[0][power];
double scalingFactor = target->getMaxHP() / 1000.0;
int healed = target->heal(sourceRef, heal * scalingFactor);
sSkillResult_Heal_HP result{};
result.eCT = target->getCharType();
result.iID = target->getID();
result.iHealHP = healed;
result.iHP = target->getCurrentHP();
return SkillResult(sizeof(sSkillResult_Heal_HP), &result);
}
static SkillResult handleSkillDamageNDebuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
// take aggro
target->takeDamage(source->getRef(), 0);
int duration = 0;
int strength = 0;
bool blocked = target->hasBuff(ECSB_FREEDOM);
if(!blocked) {
duration = skill->durationTime[power];
strength = skill->values[0][power];
BuffStack debuff = {
(duration * 100) / MS_PER_COMBAT_TICK, // ticks
strength, // value
source->getRef(), // source
BuffClass::NANO, // buff class
};
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
target->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) {
Buffs::timeBuffTick(self, buff);
},
&debuff);
}
sSkillResult_Damage_N_Debuff result{};
result.iDamage = duration / 10; // we use the duration as the damage number (why?)
result.iHP = target->getCurrentHP();
result.eCT = target->getCharType();
result.iID = target->getID();
result.bProtected = blocked;
result.iConditionBitFlag = target->getCompositeCondition();
// for player targets, make sure to update Nano stamina
if (target->getCharType() == 1) {
Player *plr = dynamic_cast<Player*>(target);
result.iStamina = plr->getActiveNano()->iStamina;
}
return SkillResult(sizeof(sSkillResult_Damage_N_Debuff), &result);
}
static SkillResult handleSkillLeech(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
EntityRef sourceRef = source->getRef();
int heal = skill->values[0][power];
int healed = source->heal(sourceRef, heal);
int damage = heal * 2;
int dealt = target->takeDamage(sourceRef, damage);
sSkillResult_Leech result{};
result.Damage.eCT = target->getCharType();
result.Damage.iID = target->getID();
result.Damage.bProtected = dealt <= 0;
result.Damage.iDamage = dealt;
result.Damage.iHP = target->getCurrentHP();
result.Heal.eCT = result.Damage.eCT;
result.Heal.iID = result.Damage.iID;
result.Heal.iHealHP = healed;
result.Heal.iHP = source->getCurrentHP();
return SkillResult(sizeof(sSkillResult_Leech), &result);
}
static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
int duration = skill->durationTime[power];
int strength = skill->values[0][power];
BuffStack passiveBuff = {
// if the duration is 0, it needs to be recast every tick
duration == 0 ? 1 : (duration * 100) / MS_PER_COMBAT_TICK, // ticks
strength, // value
source->getRef(), // source
source == target ? BuffClass::NANO : BuffClass::GROUP_NANO, // buff class
};
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
SkillDrainType drainType = skill->drainType;
int combatLifetime = 0;
if(!target->addBuff(timeBuffId,
[drainType](EntityRef self, Buff* buff, int status, BuffStack* stack) {
if(buff->id == ECSB_BOUNDINGBALL && status == ETBU_ADD) {
// drain
ICombatant* combatant = dynamic_cast<ICombatant*>(self.getEntity());
combatant->takeDamage(buff->getLastSource(), 0); // aggro
}
Buffs::timeBuffUpdate(self, buff, status, stack);
if(drainType == SkillDrainType::ACTIVE && status == ETBU_DEL)
Buffs::timeBuffTimeout(self);
},
[combatLifetime](EntityRef self, Buff* buff, time_t currTime) mutable {
if(buff->id == ECSB_BOUNDINGBALL &&
combatLifetime % COMBAT_TICKS_PER_DRAIN_PROC == 0)
Buffs::tickDrain(self, buff, COMBAT_TICKS_PER_DRAIN_PROC); // drain
combatLifetime++;
},
&passiveBuff)) return SkillResult();
sSkillResult_Buff result{};
result.eCT = target->getCharType();
result.iID = target->getID();
result.bProtected = false;
result.iConditionBitFlag = target->getCompositeCondition();
return SkillResult(sizeof(sSkillResult_Buff), &result);
}
static SkillResult handleSkillBatteryDrain(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
if(target->getCharType() != 1)
return SkillResult(); // only Players are valid targets for battery drain
Player* plr = dynamic_cast<Player*>(target);
const double scalingFactor = (18 + source->getLevel()) / 36.0;
const bool blocked = target->hasBuff(ECSB_PROTECT_BATTERY);
int boostDrain = 0;
int potionDrain = 0;
if(!blocked) {
boostDrain = (int)(skill->values[0][power] * scalingFactor);
if(boostDrain > plr->batteryW) boostDrain = plr->batteryW;
plr->batteryW -= boostDrain;
potionDrain = (int)(skill->values[1][power] * scalingFactor);
if(potionDrain > plr->batteryN) potionDrain = plr->batteryN;
plr->batteryN -= potionDrain;
}
sSkillResult_BatteryDrain result{};
result.eCT = target->getCharType();
result.iID = target->getID();
result.bProtected = blocked;
result.iDrainW = boostDrain;
result.iBatteryW = plr->batteryW;
result.iDrainN = potionDrain;
result.iBatteryN = plr->batteryN;
result.iStamina = plr->getActiveNano()->iStamina;
result.bNanoDeactive = plr->getActiveNano()->iStamina <= 0;
result.iConditionBitFlag = target->getCompositeCondition();
return SkillResult(sizeof(sSkillResult_BatteryDrain), &result);
}
static SkillResult handleSkillMove(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
if(source->getCharType() != 1)
return SkillResult(); // only Players are valid sources for recall
Player* plr = dynamic_cast<Player*>(source);
if(source == target) {
// no trailing struct for self
PlayerManager::sendPlayerTo(target->getRef().sock, plr->recallX, plr->recallY, plr->recallZ, plr->recallInstance);
return SkillResult();
}
sSkillResult_Move result{};
result.eCT = target->getCharType();
result.iID = target->getID();
result.iMapNum = plr->recallInstance;
result.iMoveX = plr->recallX;
result.iMoveY = plr->recallY;
result.iMoveZ = plr->recallZ;
return SkillResult(sizeof(sSkillResult_Move), &result);
}
static SkillResult handleSkillResurrect(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
sSkillResult_Resurrect result{};
result.eCT = target->getCharType();
result.iID = target->getID();
result.iRegenHP = target->getCurrentHP();
return SkillResult(sizeof(sSkillResult_Resurrect), &result);
}
#pragma endregion
static std::vector<SkillResult> handleSkill(SkillData* skill, int power, ICombatant* src, std::vector<ICombatant*> targets) {
SkillResult (*skillHandler)(SkillData*, int, ICombatant*, ICombatant*) = nullptr;
std::vector<SkillResult> results;
switch(skill->skillType)
{
case SkillType::CORRUPTIONATTACK:
case SkillType::CORRUPTIONATTACKLOSE:
case SkillType::CORRUPTIONATTACKWIN:
// skillHandler = handleSkillCorruptionReflect;
// break;
case SkillType::DAMAGE:
skillHandler = handleSkillDamage;
break;
case SkillType::HEAL_HP:
case SkillType::RETURNHOMEHEAL:
skillHandler = handleSkillHealHP;
break;
case SkillType::KNOCKDOWN:
case SkillType::SLEEP:
case SkillType::SNARE:
case SkillType::STUN:
skillHandler = handleSkillDamageNDebuff;
break;
case SkillType::JUMP:
case SkillType::RUN:
case SkillType::STEALTH:
case SkillType::MINIMAPENEMY:
case SkillType::MINIMAPTRESURE:
case SkillType::PHOENIX:
case SkillType::PROTECTBATTERY:
case SkillType::PROTECTINFECTION:
case SkillType::REWARDBLOB:
case SkillType::REWARDCASH:
// case SkillType::INFECTIONDAMAGE:
case SkillType::FREEDOM:
case SkillType::BOUNDINGBALL:
case SkillType::INVULNERABLE:
case SkillType::STAMINA_SELF:
case SkillType::NANOSTIMPAK:
case SkillType::BUFFHEAL:
skillHandler = handleSkillBuff;
break;
case SkillType::BLOODSUCKING:
skillHandler = handleSkillLeech;
break;
case SkillType::RETROROCKET_SELF:
// no-op
return results;
case SkillType::PHOENIX_GROUP:
skillHandler = handleSkillResurrect;
break;
case SkillType::RECALL:
case SkillType::RECALL_GROUP:
skillHandler = handleSkillMove;
break;
case SkillType::BATTERYDRAIN:
skillHandler = handleSkillBatteryDrain;
break;
default:
std::cout << "[WARN] Unhandled skill type " << (int)skill->skillType << std::endl;
return results;
}
for(ICombatant* target : targets) {
assert(target != nullptr);
SkillResult result = skillHandler(skill, power, src != nullptr ? src : target, target);
if(result.size == 0) continue; // skill not applicable
if(result.size > MAX_SKILLRESULT_SIZE) {
std::cout << "[WARN] bad skill result size for " << (int)skill->skillType << " from " << (void*)handleSkillBuff << std::endl;
continue;
}
results.push_back(result);
}
return results;
}
static void attachSkillResults(std::vector<SkillResult> results, uint8_t* pivot) {
for(SkillResult& result : results) {
size_t sz = result.size;
memcpy(pivot, result.payload, sz);
pivot += sz;
}
}
void Abilities::useNanoSkill(CNSocket* sock, SkillData* skill, sNano& nano, std::vector<ICombatant*> affected) {
Player* plr = PlayerManager::getPlayer(sock);
ICombatant* combatant = dynamic_cast<ICombatant*>(plr);
int boost = 0;
if (Nanos::getNanoBoost(plr))
boost = 3;
if(skill->drainType == SkillDrainType::ACTIVE) {
nano.iStamina -= skill->batteryUse[boost];
if (nano.iStamina <= 0)
nano.iStamina = 0;
}
std::vector<SkillResult> results = handleSkill(skill, boost, combatant, affected);
if(results.empty()) return; // no effect; no need for confirmation packets
// lazy validation since skill results might be different sizes
if (!validOutVarPacket(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC), results.size(), MAX_SKILLRESULT_SIZE)) {
std::cout << "[WARN] bad sP_FE2CL_NANO_SKILL_USE_SUCC packet size\n";
return;
}
// initialize response struct
size_t resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC);
for(SkillResult& sr : results)
resplen += sr.size;
uint8_t respbuf[CN_PACKET_BODY_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
sP_FE2CL_NANO_SKILL_USE_SUCC* pkt = (sP_FE2CL_NANO_SKILL_USE_SUCC*)respbuf;
pkt->iPC_ID = plr->iID;
pkt->iNanoID = nano.iID;
pkt->iSkillID = nano.iSkillID;
pkt->iNanoStamina = nano.iStamina;
pkt->bNanoDeactive = nano.iStamina <= 0;
pkt->eST = (int32_t)skill->skillType;
pkt->iTargetCnt = (int32_t)results.size();
attachSkillResults(results, (uint8_t*)(pkt + 1));
sock->sendPacket(pkt, P_FE2CL_NANO_SKILL_USE_SUCC, resplen);
if(skill->skillType == SkillType::RECALL_GROUP)
// group recall packet is sent only to group members
PlayerManager::sendToGroup(sock, pkt, P_FE2CL_NANO_SKILL_USE, resplen);
else
PlayerManager::sendToViewable(sock, pkt, P_FE2CL_NANO_SKILL_USE, resplen);
}
void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*> affected) {
if(SkillTable.count(skillID) == 0)
return;
Entity* entity = npc.getEntity();
ICombatant* src = nullptr;
if(npc.kind == EntityKind::COMBAT_NPC || npc.kind == EntityKind::MOB)
src = dynamic_cast<ICombatant*>(entity);
SkillData* skill = &SkillTable[skillID];
std::vector<SkillResult> results = handleSkill(skill, 0, src, affected);
// lazy validation since skill results might be different sizes
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), results.size(), MAX_SKILLRESULT_SIZE)) {
std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_HIT packet size\n";
return;
}
// initialize response struct
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT);
for(SkillResult& sr : results)
resplen += sr.size;
uint8_t respbuf[CN_PACKET_BODY_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
sP_FE2CL_NPC_SKILL_HIT* pkt = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
pkt->iNPC_ID = npc.id;
pkt->iSkillID = skillID;
pkt->eST = (int32_t)skill->skillType;
pkt->iTargetCnt = (int32_t)results.size();
if(npc.kind == EntityKind::MOB) {
Mob* mob = dynamic_cast<Mob*>(entity);
pkt->iValue1 = mob->hitX;
pkt->iValue2 = mob->hitY;
pkt->iValue3 = mob->hitZ;
}
attachSkillResults(results, (uint8_t*)(pkt + 1));
NPCManager::sendToViewable(entity, pkt, P_FE2CL_NPC_SKILL_HIT, resplen);
}
static std::vector<ICombatant*> entityRefsToCombatants(std::vector<EntityRef> refs) {
std::vector<ICombatant*> combatants;
for(EntityRef ref : refs) {
if(ref.kind == EntityKind::PLAYER)
combatants.push_back(dynamic_cast<ICombatant*>(PlayerManager::getPlayer(ref.sock)));
else if(ref.kind == EntityKind::COMBAT_NPC || ref.kind == EntityKind::MOB)
combatants.push_back(dynamic_cast<ICombatant*>(ref.getEntity()));
}
return combatants;
}
std::vector<ICombatant*> Abilities::matchTargets(ICombatant* src, SkillData* skill, int count, int32_t *ids) {
if(skill->targetType == SkillTargetType::GROUP)
return entityRefsToCombatants(src->getGroupMembers());
// this check *has* to happen after the group check above due to cases like group recall that use both
if(skill->effectTarget == SkillEffectTarget::SELF)
return {src}; // client sends 0 targets for certain self-targeting skills (recall)
// individuals
std::vector<ICombatant*> targets;
for (int i = 0; i < count; i++) {
int32_t id = ids[i];
if (skill->targetType == SkillTargetType::MOBS) {
// mob
if (NPCManager::NPCs.find(id) != NPCManager::NPCs.end()) {
BaseNPC* npc = NPCManager::NPCs[id];
if (npc->kind == EntityKind::COMBAT_NPC || npc->kind == EntityKind::MOB) {
targets.push_back(dynamic_cast<ICombatant*>(npc));
continue;
}
}
std::cout << "[WARN] skill: invalid mob target (id " << id << ")\n";
} else if(skill->targetType == SkillTargetType::PLAYERS) {
// player
Player* plr = PlayerManager::getPlayerFromID(id);
if (plr != nullptr) {
targets.push_back(dynamic_cast<ICombatant*>(plr));
continue;
}
std::cout << "[WARN] skill: invalid player target (id " << id << ")\n";
}
}
return targets;
}
/* ripped from client (enums emplaced) */
int Abilities::getCSTBFromST(SkillType skillType) {
int result = 0;
switch (skillType)
{
case SkillType::RUN:
result = ECSB_UP_MOVE_SPEED;
break;
case SkillType::JUMP:
result = ECSB_UP_JUMP_HEIGHT;
break;
case SkillType::STEALTH:
result = ECSB_UP_STEALTH;
break;
case SkillType::PHOENIX:
result = ECSB_PHOENIX;
break;
case SkillType::PROTECTBATTERY:
result = ECSB_PROTECT_BATTERY;
break;
case SkillType::PROTECTINFECTION:
result = ECSB_PROTECT_INFECTION;
break;
case SkillType::MINIMAPENEMY:
result = ECSB_MINIMAP_ENEMY;
break;
case SkillType::MINIMAPTRESURE:
result = ECSB_MINIMAP_TRESURE;
break;
case SkillType::REWARDBLOB:
result = ECSB_REWARD_BLOB;
break;
case SkillType::REWARDCASH:
result = ECSB_REWARD_CASH;
break;
case SkillType::FREEDOM:
result = ECSB_FREEDOM;
break;
case SkillType::INVULNERABLE:
result = ECSB_INVULNERABLE;
break;
case SkillType::BUFFHEAL:
result = ECSB_HEAL;
break;
case SkillType::NANOSTIMPAK:
result = ECSB_STIMPAKSLOT1;
// shift as necessary
break;
case SkillType::SNARE:
result = ECSB_DN_MOVE_SPEED;
break;
case SkillType::STUN:
result = ECSB_STUN;
break;
case SkillType::SLEEP:
result = ECSB_MEZ;
break;
case SkillType::INFECTIONDAMAGE:
result = ECSB_INFECTION;
break;
case SkillType::BOUNDINGBALL:
result = ECSB_BOUNDINGBALL;
break;
default:
break;
}
return result;
}

112
src/Abilities.hpp Normal file
View File

@ -0,0 +1,112 @@
#pragma once
#include "core/Core.hpp"
#include "Entities.hpp"
#include "Player.hpp"
#include <map>
#include <vector>
#include <assert.h>
const int COMBAT_TICKS_PER_DRAIN_PROC = 2;
constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain);
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,
};
enum class SkillEffectTarget {
POINT = 1,
SELF = 2,
CONE = 3,
WEAPON = 4,
AREA_SELF = 5,
AREA_TARGET = 6
};
enum class SkillTargetType {
MOBS = 1,
PLAYERS = 2,
GROUP = 3
};
enum class SkillDrainType {
ACTIVE = 1,
PASSIVE = 2
};
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 {
SkillType skillType; // eST
SkillEffectTarget effectTarget;
int effectType; // always 1?
SkillTargetType targetType;
SkillDrainType drainType;
int effectArea;
int batteryUse[4];
int durationTime[4];
int valueTypes[3];
int values[3][4];
};
namespace Abilities {
extern std::map<int32_t, SkillData> SkillTable;
void useNanoSkill(CNSocket*, SkillData*, sNano&, std::vector<ICombatant*>);
void useNPCSkill(EntityRef, int skillID, std::vector<ICombatant*>);
std::vector<ICombatant*> matchTargets(ICombatant*, SkillData*, int, int32_t*);
int getCSTBFromST(SkillType skillType);
}

40
src/Bucket.hpp Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include <array>
#include <optional>
#include <assert.h>
template<class T, size_t N>
class Bucket {
std::array<T, N> buf;
size_t sz;
public:
Bucket() {
sz = 0;
}
void add(const T& item) {
assert(sz < N);
buf[sz++] = item;
}
std::optional<T> get(size_t idx) const {
if (idx < sz) {
return buf[idx];
}
return std::nullopt;
}
size_t size() const {
return sz;
}
bool isFull() const {
return sz == N;
}
void clear() {
sz = 0;
}
};

456
src/Buddies.cpp Normal file
View File

@ -0,0 +1,456 @@
#include "Buddies.hpp"
#include "db/Database.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
using namespace Buddies;
#pragma region Helper methods
static int getAvailableBuddySlot(Player* plr) {
int slot = -1;
for (int i = 0; i < 50; i++) {
if (plr->buddyIDs[i] == 0)
return i;
}
return slot;
}
static bool playerHasBuddyWithID(Player* plr, int buddyID) {
for (int i = 0; i < 50; i++) {
if (plr->buddyIDs[i] == buddyID)
return true;
}
return false;
}
#pragma endregion
// Refresh buddy list
void Buddies::sendBuddyList(CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
int buddyCnt = Database::getNumBuddies(plr);
if (!validOutVarPacket(sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC), buddyCnt, sizeof(sBuddyBaseInfo))) {
std::cout << "[WARN] bad sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC packet size\n";
return;
}
// initialize response struct
size_t resplen = sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC) + buddyCnt * sizeof(sBuddyBaseInfo);
uint8_t respbuf[CN_PACKET_BODY_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC* resp = (sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC*)respbuf;
sBuddyBaseInfo* respdata = (sBuddyBaseInfo*)(respbuf + sizeof(sP_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC));
// base response fields
resp->iBuddyCnt = buddyCnt;
resp->iID = plr->iID;
resp->iPCUID = plr->PCStyle.iPC_UID;
resp->iListNum = 0; // ???
int buddyIndex = 0;
for (int i = 0; i < 50; i++) {
int64_t buddyID = plr->buddyIDs[i];
if (buddyID != 0) {
sBuddyBaseInfo buddyInfo = {};
Player buddyPlayerData = {};
Database::getPlayer(&buddyPlayerData, buddyID);
if (buddyPlayerData.iID == 0)
continue;
buddyInfo.bBlocked = plr->isBuddyBlocked[i];
buddyInfo.bFreeChat = 1;
buddyInfo.iGender = buddyPlayerData.PCStyle.iGender;
buddyInfo.iID = buddyID;
buddyInfo.iPCUID = buddyID;
buddyInfo.iNameCheckFlag = buddyPlayerData.PCStyle.iNameCheck;
buddyInfo.iPCState = buddyPlayerData.iPCState;
memcpy(buddyInfo.szFirstName, buddyPlayerData.PCStyle.szFirstName, sizeof(buddyInfo.szFirstName));
memcpy(buddyInfo.szLastName, buddyPlayerData.PCStyle.szLastName, sizeof(buddyInfo.szLastName));
respdata[buddyIndex] = buddyInfo;
buddyIndex++;
}
}
sock->sendPacket((void*)respbuf, P_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC, resplen);
}
// Buddy request
static void requestBuddy(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_REQUEST_MAKE_BUDDY*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(req->iBuddyID);
if (otherPlr == nullptr)
return;
if (getAvailableBuddySlot(plr) == -1 || getAvailableBuddySlot(otherPlr) == -1)
{
INITSTRUCT(sP_FE2CL_REP_REQUEST_MAKE_BUDDY_FAIL, failResp);
sock->sendPacket(failResp, P_FE2CL_REP_REQUEST_MAKE_BUDDY_FAIL);
return;
}
CNSocket* otherSock = PlayerManager::getSockFromID(otherPlr->iID);
INITSTRUCT(sP_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC, resp);
INITSTRUCT(sP_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC_TO_ACCEPTER, otherResp);
resp.iRequestID = plr->iID;
resp.iBuddyID = req->iBuddyID;
resp.iBuddyPCUID = req->iBuddyPCUID;
otherResp.iRequestID = plr->iID;
otherResp.iBuddyID = req->iBuddyID;
memcpy(otherResp.szFirstName, plr->PCStyle.szFirstName, sizeof(plr->PCStyle.szFirstName));
memcpy(otherResp.szLastName, plr->PCStyle.szLastName, sizeof(plr->PCStyle.szLastName));
std::cout << "Buddy ID: " << req->iBuddyID << std::endl;
sock->sendPacket(resp, P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC);
otherSock->sendPacket(otherResp, P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC_TO_ACCEPTER);
}
// Sending buddy request by player name
static void reqBuddyByName(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_PC_FIND_NAME_MAKE_BUDDY*)data->buf;
Player* plrReq = PlayerManager::getPlayer(sock);
INITSTRUCT(sP_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_SUCC, resp);
CNSocket* otherSock = PlayerManager::getSockFromName(AUTOU16TOU8(pkt->szFirstName), AUTOU16TOU8(pkt->szLastName));
if (otherSock == nullptr)
return; // no player found
Player *otherPlr = PlayerManager::getPlayer(otherSock);
if (playerHasBuddyWithID(plrReq, otherPlr->iID))
return;
resp.iPCUID = plrReq->PCStyle.iPC_UID;
resp.iNameCheckFlag = plrReq->PCStyle.iNameCheck;
memcpy(resp.szFirstName, plrReq->PCStyle.szFirstName, sizeof(plrReq->PCStyle.szFirstName));
memcpy(resp.szLastName, plrReq->PCStyle.szLastName, sizeof(plrReq->PCStyle.szLastName));
otherSock->sendPacket(resp, P_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_SUCC);
}
// Accepting buddy request
static void reqAcceptBuddy(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_ACCEPT_MAKE_BUDDY*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(req->iBuddyID);
if (otherPlr == nullptr)
return; // sanity check
CNSocket* otherSock = PlayerManager::getSockFromID(otherPlr->iID);
int slotA = getAvailableBuddySlot(plr);
int slotB = getAvailableBuddySlot(otherPlr);
if (slotA == -1 || slotB == -1)
return; // sanity check
if (req->iAcceptFlag == 1 && plr->iID != otherPlr->iID && !playerHasBuddyWithID(plr, otherPlr->iID))
{
INITSTRUCT(sP_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC, resp);
// A to B
resp.iBuddySlot = slotA;
resp.BuddyInfo.iID = otherPlr->iID;
resp.BuddyInfo.iPCUID = otherPlr->PCStyle.iPC_UID;
resp.BuddyInfo.iPCState = 1; // assumed to be online
resp.BuddyInfo.bBlocked = 0; // not blocked by default
resp.BuddyInfo.iGender = otherPlr->PCStyle.iGender; // shows the other player's gender
resp.BuddyInfo.bFreeChat = 1; // shows whether or not the other player has freechat on (hardcoded for now)
resp.BuddyInfo.iNameCheckFlag = otherPlr->PCStyle.iNameCheck;
memcpy(resp.BuddyInfo.szFirstName, otherPlr->PCStyle.szFirstName, sizeof(resp.BuddyInfo.szFirstName));
memcpy(resp.BuddyInfo.szLastName, otherPlr->PCStyle.szLastName, sizeof(resp.BuddyInfo.szLastName));
sock->sendPacket(resp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC);
plr->buddyIDs[slotA] = otherPlr->PCStyle.iPC_UID;
//std::cout << "Buddy's ID: " << plr->buddyIDs[slotA] << std::endl;
// B to A, using the same struct
resp.iBuddySlot = slotB;
resp.BuddyInfo.iID = plr->iID;
resp.BuddyInfo.iPCUID = plr->PCStyle.iPC_UID;
resp.BuddyInfo.iPCState = 1;
resp.BuddyInfo.bBlocked = 0;
resp.BuddyInfo.iGender = plr->PCStyle.iGender;
resp.BuddyInfo.bFreeChat = 1;
resp.BuddyInfo.iNameCheckFlag = plr->PCStyle.iNameCheck;
memcpy(resp.BuddyInfo.szFirstName, plr->PCStyle.szFirstName, sizeof(resp.BuddyInfo.szFirstName));
memcpy(resp.BuddyInfo.szLastName, plr->PCStyle.szLastName, sizeof(resp.BuddyInfo.szLastName));
otherSock->sendPacket(resp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC);
otherPlr->buddyIDs[slotB] = plr->PCStyle.iPC_UID;
//std::cout << "Buddy's ID: " << plr->buddyIDs[slotB] << std::endl;
// add record to db
Database::addBuddyship(plr->iID, otherPlr->iID);
}
else
{
INITSTRUCT(sP_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL, declineResp);
declineResp.iErrorCode = 6; // Buddy declined notification
declineResp.iBuddyID = req->iBuddyID;
declineResp.iBuddyPCUID = req->iBuddyPCUID;
otherSock->sendPacket(declineResp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL);
}
}
// Accepting buddy request from the find name request
static void reqFindNameBuddyAccept(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_PC_FIND_NAME_ACCEPT_BUDDY*)data->buf;
Player* plrReq = PlayerManager::getPlayer(sock);
INITSTRUCT(sP_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC, resp);
Player* otherPlr = PlayerManager::getPlayerFromID(pkt->iBuddyPCUID);
if (otherPlr == nullptr)
return;
CNSocket* otherSock = PlayerManager::getSockFromID(pkt->iBuddyPCUID);
int slotA = getAvailableBuddySlot(plrReq);
int slotB = getAvailableBuddySlot(otherPlr);
if (slotA == -1 || slotB == -1)
return; // sanity check
if (pkt->iAcceptFlag == 1 && plrReq->iID != otherPlr->iID && !playerHasBuddyWithID(plrReq, otherPlr->iID)) {
INITSTRUCT(sP_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC, resp);
// A to B
resp.iBuddySlot = slotA;
resp.BuddyInfo.iID = otherPlr->iID;
resp.BuddyInfo.iPCUID = otherPlr->PCStyle.iPC_UID;
resp.BuddyInfo.iPCState = 1; // assumed to be online
resp.BuddyInfo.bBlocked = 0; // not blocked by default
resp.BuddyInfo.iGender = otherPlr->PCStyle.iGender; // shows the other player's gender
resp.BuddyInfo.bFreeChat = 1; // shows whether or not the other player has freechat on (hardcoded for now)
resp.BuddyInfo.iNameCheckFlag = otherPlr->PCStyle.iNameCheck;
memcpy(resp.BuddyInfo.szFirstName, otherPlr->PCStyle.szFirstName, sizeof(resp.BuddyInfo.szFirstName));
memcpy(resp.BuddyInfo.szLastName, otherPlr->PCStyle.szLastName, sizeof(resp.BuddyInfo.szLastName));
sock->sendPacket(resp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC);
plrReq->buddyIDs[slotA] = otherPlr->PCStyle.iPC_UID;
//std::cout << "Buddy's ID: " << plr->buddyIDs[slotA] << std::endl;
// B to A, using the same struct
resp.iBuddySlot = slotB;
resp.BuddyInfo.iID = plrReq->iID;
resp.BuddyInfo.iPCUID = plrReq->PCStyle.iPC_UID;
resp.BuddyInfo.iPCState = 1;
resp.BuddyInfo.bBlocked = 0;
resp.BuddyInfo.iGender = plrReq->PCStyle.iGender;
resp.BuddyInfo.bFreeChat = 1;
resp.BuddyInfo.iNameCheckFlag = plrReq->PCStyle.iNameCheck;
memcpy(resp.BuddyInfo.szFirstName, plrReq->PCStyle.szFirstName, sizeof(resp.BuddyInfo.szFirstName));
memcpy(resp.BuddyInfo.szLastName, plrReq->PCStyle.szLastName, sizeof(resp.BuddyInfo.szLastName));
otherSock->sendPacket(resp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC);
otherPlr->buddyIDs[slotB] = plrReq->PCStyle.iPC_UID;
//std::cout << "Buddy's ID: " << plr->buddyIDs[slotB] << std::endl;
// add record to db
Database::addBuddyship(plrReq->iID, otherPlr->iID);
}
else
{
INITSTRUCT(sP_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL, declineResp);
declineResp.iErrorCode = 6; // Buddy declined notification
declineResp.iBuddyPCUID = pkt->iBuddyPCUID;
otherSock->sendPacket(declineResp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL);
}
}
// Getting buddy state
static void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp);
for (int slot = 0; slot < 50; slot++) {
resp.aBuddyState[slot] = PlayerManager::getPlayerFromID(plr->buddyIDs[slot]) != nullptr ? 1 : 0;
resp.aBuddyID[slot] = plr->buddyIDs[slot];
}
sock->sendPacket(resp, P_FE2CL_REP_GET_BUDDY_STATE_SUCC);
}
// Blocking the buddy
static void reqBuddyBlock(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_SET_BUDDY_BLOCK*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
// sanity checks
if (pkt->iBuddySlot < 0 || pkt->iBuddySlot >= 50 || plr->buddyIDs[pkt->iBuddySlot] != pkt->iBuddyPCUID)
return;
// save in DB
Database::removeBuddyship(plr->iID, pkt->iBuddyPCUID);
Database::addBlock(plr->iID, pkt->iBuddyPCUID);
// save serverside
// since ID is already in the array, just set it to blocked
plr->isBuddyBlocked[pkt->iBuddySlot] = true;
// send response
INITSTRUCT(sP_FE2CL_REP_SET_BUDDY_BLOCK_SUCC, resp);
resp.iBuddyPCUID = pkt->iBuddyPCUID;
resp.iBuddySlot = pkt->iBuddySlot;
sock->sendPacket(resp, P_FE2CL_REP_SET_BUDDY_BLOCK_SUCC);
// notify the other player he isn't a buddy anymore
INITSTRUCT(sP_FE2CL_REP_REMOVE_BUDDY_SUCC, otherResp);
CNSocket* otherSock = PlayerManager::getSockFromID(pkt->iBuddyPCUID);
if (otherSock == nullptr)
return; // other player isn't online, no broadcast needed
Player* otherPlr = PlayerManager::getPlayer(otherSock);
// search for the slot with the requesting player's ID
otherResp.iBuddyPCUID = plr->PCStyle.iPC_UID;
for (int i = 0; i < 50; i++) {
if (otherPlr->buddyIDs[i] == plr->PCStyle.iPC_UID) {
// remove buddy
otherPlr->buddyIDs[i] = 0;
// broadcast
otherResp.iBuddySlot = i;
otherSock->sendPacket(otherResp, P_FE2CL_REP_REMOVE_BUDDY_SUCC);
return;
}
}
}
// block non-buddy
static void reqPlayerBlock(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_SET_PC_BLOCK*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
int buddySlot = getAvailableBuddySlot(plr);
if (buddySlot == -1)
return;
// save in DB
Database::addBlock(plr->iID, pkt->iBlock_PCUID);
// save serverside
plr->buddyIDs[buddySlot] = pkt->iBlock_PCUID;
plr->isBuddyBlocked[buddySlot] = true;
// send response
INITSTRUCT(sP_FE2CL_REP_SET_PC_BLOCK_SUCC, resp);
resp.iBlock_ID = pkt->iBlock_ID;
resp.iBlock_PCUID = pkt->iBlock_PCUID;
resp.iBuddySlot = buddySlot;
sock->sendPacket(resp, P_FE2CL_REP_SET_PC_BLOCK_SUCC);
}
// Deleting the buddy
static void reqBuddyDelete(CNSocket* sock, CNPacketData* data) {
// note! this packet is used both for removing buddies and blocks
auto pkt = (sP_CL2FE_REQ_REMOVE_BUDDY*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
// remove buddy on our side
INITSTRUCT(sP_FE2CL_REP_REMOVE_BUDDY_SUCC, resp);
resp.iBuddyPCUID = pkt->iBuddyPCUID;
resp.iBuddySlot = pkt->iBuddySlot;
if (pkt->iBuddySlot < 0 || pkt->iBuddySlot >= 50 || plr->buddyIDs[pkt->iBuddySlot] != pkt->iBuddyPCUID)
return; // sanity check
bool wasBlocked = plr->isBuddyBlocked[resp.iBuddySlot];
plr->buddyIDs[resp.iBuddySlot] = 0;
plr->isBuddyBlocked[resp.iBuddySlot] = false;
sock->sendPacket(resp, P_FE2CL_REP_REMOVE_BUDDY_SUCC);
// remove record from db
Database::removeBuddyship(plr->PCStyle.iPC_UID, pkt->iBuddyPCUID);
// try this too
Database::removeBlock(plr->PCStyle.iPC_UID, pkt->iBuddyPCUID);
if (wasBlocked)
return;
// remove buddy on their side, reusing the struct
CNSocket* otherSock = PlayerManager::getSockFromID(pkt->iBuddyPCUID);
if (otherSock == nullptr)
return; // other player isn't online, no broadcast needed
Player* otherPlr = PlayerManager::getPlayer(otherSock);
// search for the slot with the requesting player's ID
resp.iBuddyPCUID = plr->PCStyle.iPC_UID;
for (int i = 0; i < 50; i++) {
if (otherPlr->buddyIDs[i] == plr->PCStyle.iPC_UID) {
// remove buddy
otherPlr->buddyIDs[i] = 0;
// broadcast
resp.iBuddySlot = i;
otherSock->sendPacket(resp, P_FE2CL_REP_REMOVE_BUDDY_SUCC);
return;
}
}
}
// Warping to buddy
static void reqBuddyWarp(CNSocket* sock, CNPacketData* data) {
Player *plr = PlayerManager::getPlayer(sock);
auto pkt = (sP_CL2FE_REQ_PC_BUDDY_WARP*)data->buf;
if (pkt->iSlotNum < 0 || pkt->iSlotNum >= 50)
return; // sanity check
Player* otherPlr = PlayerManager::getPlayerFromID(pkt->iBuddyPCUID);
if (otherPlr == nullptr)
return; // buddy offline
// if the player is instanced; no warp allowed
if (otherPlr->instanceID != INSTANCE_OVERWORLD)
goto fail;
// check if the players are at the same point in time (or in the training area or not)
if (otherPlr->PCStyle2.iPayzoneFlag != plr->PCStyle2.iPayzoneFlag)
goto fail;
// do not warp to players on monkeys
if (otherPlr->onMonkey)
goto fail;
// does the player disallow warping?
if (otherPlr->unwarpable)
goto fail;
// otherPlr->instanceID should always be INSTANCE_OVERWORLD at this point
PlayerManager::sendPlayerTo(sock, otherPlr->x, otherPlr->y, otherPlr->z, otherPlr->instanceID);
return;
fail:
INITSTRUCT(sP_FE2CL_REP_PC_BUDDY_WARP_FAIL, resp);
resp.iBuddyPCUID = pkt->iBuddyPCUID;
resp.iErrorCode = 0;
sock->sendPacket(resp, P_FE2CL_REP_PC_BUDDY_WARP_FAIL);
}
void Buddies::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_REQUEST_MAKE_BUDDY, requestBuddy);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_FIND_NAME_MAKE_BUDDY, reqBuddyByName);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ACCEPT_MAKE_BUDDY, reqAcceptBuddy);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_FIND_NAME_ACCEPT_BUDDY, reqFindNameBuddyAccept);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_GET_BUDDY_STATE, reqPktGetBuddyState);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SET_BUDDY_BLOCK, reqBuddyBlock);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SET_PC_BLOCK, reqPlayerBlock);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_REMOVE_BUDDY, reqBuddyDelete);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_BUDDY_WARP, reqBuddyWarp);
}

10
src/Buddies.hpp Normal file
View File

@ -0,0 +1,10 @@
#pragma once
#include "core/Core.hpp"
namespace Buddies {
void init();
// Buddy list
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_BODY_SIZE);
uint8_t respbuf[CN_PACKET_BODY_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
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);
}

379
src/BuiltinCommands.cpp Normal file
View File

@ -0,0 +1,379 @@
#include "BuiltinCommands.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Missions.hpp"
#include "Chat.hpp"
#include "Nanos.hpp"
// helper function, not a packet handler
void BuiltinCommands::setSpecialState(CNSocket* sock, CNPacketData* data) {
auto setData = (sP_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
// HACK: work around the invisible weapon bug
if (setData->iSpecialStateFlag == CN_SPECIAL_STATE_FLAG__FULL_UI)
Items::updateEquips(sock, plr);
INITSTRUCT(sP_FE2CL_PC_SPECIAL_STATE_CHANGE, response);
plr->iSpecialState ^= setData->iSpecialStateFlag;
response.iPC_ID = setData->iPC_ID;
response.iReqSpecialStateFlag = setData->iSpecialStateFlag;
response.iSpecialState = plr->iSpecialState;
sock->sendPacket(response, P_FE2CL_REP_PC_SPECIAL_STATE_SWITCH_SUCC);
PlayerManager::sendToViewable(sock, response, P_FE2CL_PC_SPECIAL_STATE_CHANGE);
}
static void setGMSpecialSwitchPlayer(CNSocket* sock, CNPacketData* data) {
if (PlayerManager::getPlayer(sock)->accountLevel > 30)
return;
BuiltinCommands::setSpecialState(sock, data);
}
static void gotoPlayer(CNSocket* sock, CNPacketData* data) {
Player *plr = PlayerManager::getPlayer(sock);
if (plr->accountLevel > 50)
return;
auto gotoData = (sP_CL2FE_REQ_PC_GOTO*)data->buf;
DEBUGLOG(
std::cout << "P_CL2FE_REQ_PC_GOTO:" << std::endl;
std::cout << "\tX: " << gotoData->iToX << std::endl;
std::cout << "\tY: " << gotoData->iToY << std::endl;
std::cout << "\tZ: " << gotoData->iToZ << std::endl;
)
PlayerManager::sendPlayerTo(sock, gotoData->iToX, gotoData->iToY, gotoData->iToZ, INSTANCE_OVERWORLD);
}
static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
Player *plr = PlayerManager::getPlayer(sock);
if (plr->accountLevel > 50)
return;
auto setData = (sP_CL2FE_GM_REQ_PC_SET_VALUE*)data->buf;
INITSTRUCT(sP_FE2CL_GM_REP_PC_SET_VALUE, response);
DEBUGLOG(
std::cout << "P_CL2FE_GM_REQ_PC_SET_VALUE:" << std::endl;
std::cout << "\tPC_ID: " << setData->iPC_ID << std::endl;
std::cout << "\tSetValueType: " << setData->iSetValueType << std::endl;
std::cout << "\tSetValue: " << setData->iSetValue << std::endl;
)
// Handle serverside value-changes
switch (setData->iSetValueType) {
case CN_GM_SET_VALUE_TYPE__HP:
response.iSetValue = plr->HP = setData->iSetValue;
break;
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 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 CN_GM_SET_VALUE_TYPE__FUSION_MATTER:
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
response.iSetValue = plr->fusionmatter;
break;
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.iSetValueType = setData->iSetValueType;
sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE);
// if one lowers their own health to 0, make sure others can see it
if (plr->HP <= 0) {
INITSTRUCT(sP_FE2CL_PC_SUDDEN_DEAD, dead);
dead.iPC_ID = plr->iID;
dead.iDamage = plr->HP;
dead.iHP = plr->HP = 0;
PlayerManager::sendToViewable(sock, dead, P_FE2CL_PC_SUDDEN_DEAD);
}
}
static void setGMSpecialOnOff(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock);
// access check
if (plr->accountLevel > 30)
return;
auto req = (sP_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF*)data->buf;
CNSocket *otherSock = PlayerManager::getSockFromAny(req->eTargetSearchBy, req->iTargetPC_ID, req->iTargetPC_UID,
AUTOU16TOU8(req->szTargetPC_FirstName), AUTOU16TOU8(req->szTargetPC_LastName));
if (otherSock == nullptr) {
Chat::sendServerMessage(sock, "player not found");
return;
}
Player *otherPlr = PlayerManager::getPlayer(otherSock);
if (req->iONOFF)
otherPlr->iSpecialState |= req->iSpecialStateFlag;
else
otherPlr->iSpecialState &= ~req->iSpecialStateFlag;
// this is only used for muting players, so no need to update the client since that logic is server-side
}
static void locatePlayer(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock);
// access check
if (plr->accountLevel > 30)
return;
auto req = (sP_CL2FE_GM_REQ_PC_LOCATION*)data->buf;
CNSocket *otherSock = PlayerManager::getSockFromAny(req->eTargetSearchBy, req->iTargetPC_ID, req->iTargetPC_UID,
AUTOU16TOU8(req->szTargetPC_FirstName), AUTOU16TOU8(req->szTargetPC_LastName));
if (otherSock == nullptr) {
Chat::sendServerMessage(sock, "player not found");
return;
}
INITSTRUCT(sP_FE2CL_GM_REP_PC_LOCATION, resp);
Player *otherPlr = PlayerManager::getPlayer(otherSock);
resp.iTargetPC_UID = otherPlr->accountId;
resp.iTargetPC_ID = otherPlr->iID;
resp.iShardID = 0; // sharding is unsupported
resp.iMapType = !!PLAYERID(otherPlr->instanceID); // private instance or not
resp.iMapID = PLAYERID(otherPlr->instanceID);
resp.iMapNum = MAPNUM(otherPlr->instanceID);
resp.iX = otherPlr->x;
resp.iY = otherPlr->y;
resp.iZ = otherPlr->z;
memcpy(resp.szTargetPC_FirstName, otherPlr->PCStyle.szFirstName, sizeof(resp.szTargetPC_FirstName));
memcpy(resp.szTargetPC_LastName, otherPlr->PCStyle.szLastName, sizeof(resp.szTargetPC_LastName));
sock->sendPacket(resp, P_FE2CL_GM_REP_PC_LOCATION);
}
static void kickPlayer(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock);
// access check
if (plr->accountLevel > 30)
return;
auto req = (sP_CL2FE_GM_REQ_KICK_PLAYER*)data->buf;
CNSocket *otherSock = PlayerManager::getSockFromAny(req->eTargetSearchBy, req->iTargetPC_ID, req->iTargetPC_UID,
AUTOU16TOU8(req->szTargetPC_FirstName), AUTOU16TOU8(req->szTargetPC_LastName));
if (otherSock == nullptr) {
Chat::sendServerMessage(sock, "player not found");
return;
}
Player *otherPlr = PlayerManager::getPlayer(otherSock);
if (plr->accountLevel > otherPlr->accountLevel) {
Chat::sendServerMessage(sock, "player has higher access level");
return;
}
INITSTRUCT(sP_FE2CL_REP_PC_EXIT_SUCC, response);
response.iID = otherPlr->iID;
response.iExitCode = 3; // "a GM has terminated your connection"
// send to target player
otherSock->sendPacket(response, P_FE2CL_REP_PC_EXIT_SUCC);
// ensure that the connection has terminated
otherSock->kill();
}
static void warpToPlayer(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock);
// access check
if (plr->accountLevel > 30)
return;
auto req = (sP_CL2FE_REQ_PC_WARP_TO_PC*)data->buf;
Player *otherPlr = PlayerManager::getPlayerFromID(req->iPC_ID);
if (otherPlr == nullptr) {
Chat::sendServerMessage(sock, "player not found");
return;
}
PlayerManager::sendPlayerTo(sock, otherPlr->x, otherPlr->y, otherPlr->z, otherPlr->instanceID);
}
// GM teleport command
static void teleportPlayer(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock);
// access check
if (plr->accountLevel > 30)
return;
auto req = (sP_CL2FE_GM_REQ_TARGET_PC_TELEPORT*)data->buf;
// player to teleport
CNSocket *targetSock = PlayerManager::getSockFromAny(req->eTargetPCSearchBy, req->iTargetPC_ID, req->iTargetPC_UID,
AUTOU16TOU8(req->szTargetPC_FirstName), AUTOU16TOU8(req->szTargetPC_LastName));
if (targetSock == nullptr) {
Chat::sendServerMessage(sock, "player to teleport not found");
return;
}
CNSocket *goalSock = nullptr;
Player *goalPlr = nullptr;
Player *targetPlr = nullptr;
uint64_t instance = plr->instanceID;
const int unstickRange = 400;
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_TeleportType::MapXYZ:
instance = req->iToMap;
// fallthrough
case eCN_GM_TeleportType::XYZ:
PlayerManager::sendPlayerTo(targetSock, req->iToX, req->iToY, req->iToZ, instance);
break;
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));
if (goalSock == nullptr) {
Chat::sendServerMessage(sock, "teleportation target player not found");
return;
}
goalPlr = PlayerManager::getPlayer(goalSock);
PlayerManager::sendPlayerTo(targetSock, goalPlr->x, goalPlr->y, goalPlr->z, goalPlr->instanceID);
break;
case eCN_GM_TeleportType::Unstick:
targetPlr = PlayerManager::getPlayer(targetSock);
PlayerManager::sendPlayerTo(targetSock, targetPlr->x - unstickRange/2 + Rand::rand(unstickRange),
targetPlr->y - unstickRange/2 + Rand::rand(unstickRange), targetPlr->z + 80);
break;
}
}
static void itemGMGiveHandler(CNSocket* sock, CNPacketData* data) {
auto itemreq = (sP_CL2FE_REQ_PC_GIVE_ITEM*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
if (plr->accountLevel > 50) {
// TODO: send fail packet
return;
}
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp);
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;
}
if (itemreq->Item.iType == 10) {
// item is vehicle, set expiration date
// set time limit: current time + 7days
itemreq->Item.iTimeLimit = getTimestamp() + 604800;
}
plr->Inven[itemreq->iSlotNum] = itemreq->Item;
} else if (itemreq->eIL == 2) {
int id = itemreq->Item.iID;
int slot = Missions::findQSlot(plr, id);
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) {
auto nano = (sP_CL2FE_REQ_PC_GIVE_NANO*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
if (plr->accountLevel > 50)
return;
// Add nano to player
Nanos::addNano(sock, nano->iNanoID, 0);
DEBUGLOG(
std::cout << PlayerManager::getPlayerName(plr) << " requested to add nano id: " << nano->iNanoID << std::endl;
)
}
void BuiltinCommands::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GOTO, gotoPlayer);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_SET_VALUE, setValuePlayer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GIVE_ITEM, itemGMGiveHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GIVE_NANO, nanoGMGiveHandler);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH, setGMSpecialSwitchPlayer);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF, setGMSpecialOnOff);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_LOCATION, locatePlayer);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_KICK_PLAYER, kickPlayer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_WARP_TO_PC, warpToPlayer);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_TARGET_PC_TELEPORT, teleportPlayer);
}

9
src/BuiltinCommands.hpp Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include "core/Core.hpp"
namespace BuiltinCommands {
void init();
void setSpecialState(CNSocket *sock, CNPacketData *data);
};

View File

@ -1,244 +0,0 @@
#include "CNLoginServer.hpp"
#include "CNShared.hpp"
#include "CNStructs.hpp"
#include "settings.hpp"
/*
This is *not* connected to any database, so i'm sending dummy data just to get the client to work & connect to the shard <3
*/
std::map<CNSocket*, CNLoginData> CNLoginServer::loginSessions;
CNLoginServer::CNLoginServer(uint16_t p) {
port = p;
pHandler = &CNLoginServer::handlePacket;
init();
}
void CNLoginServer::handlePacket(CNSocket* sock, CNPacketData* data) {
switch (data->type) {
case P_CL2LS_REQ_LOGIN: {
sP_CL2LS_REQ_LOGIN* login = (sP_CL2LS_REQ_LOGIN*)data->buf;
sP_LS2CL_REP_LOGIN_SUCC* response = (sP_LS2CL_REP_LOGIN_SUCC*)xmalloc(sizeof(sP_LS2CL_REP_LOGIN_SUCC));
uint64_t cachedKey = sock->getEKey(); // so we can still send the response packet with the correct key
int charCount = 2; // send 4 randomly generated characters for now
DEBUGLOG(
std::cout << "P_CL2LS_REQ_LOGIN:" << std::endl;
std::cout << "\tClient version: " << login->iClientVerA << "." << login->iClientVerB << "." << login->iClientVerC << std::endl;
std::cout << "\tLogin type: " << login->iLoginType << std::endl;
std::cout << "\tID: " << U16toU8(login->szID) << " Password: " << U16toU8(login->szPassword) << std::endl;
)
response->iCharCount = charCount;
response->iSlotNum = 1;
response->iPaymentFlag = 1;
response->iOpenBetaFlag = 0;
response->uiSvrTime = getTime();
// set username in response packet
memcpy(response->szID, login->szID, sizeof(char16_t) * 33);
// update keys
sock->setEKey(CNSocketEncryption::createNewKey(response->uiSvrTime, response->iCharCount + 1, response->iSlotNum + 1));
sock->setFEKey(CNSocketEncryption::createNewKey((uint64_t)(*(uint64_t*)&CNSocketEncryption::defaultKey[0]), login->iClientVerC, 1));
// send the response in with original key
sock->sendPacket(new CNPacketData((void*)response, P_LS2CL_REP_LOGIN_SUCC, sizeof(sP_LS2CL_REP_LOGIN_SUCC), cachedKey));
loginSessions[sock] = CNLoginData();
if (settings::LOGINRANDCHARACTERS) {
// now send the characters :)
for (int i = 0; i < charCount; i++) {
sP_LS2CL_REP_CHAR_INFO* charInfo = (sP_LS2CL_REP_CHAR_INFO*)xmalloc(sizeof(sP_LS2CL_REP_CHAR_INFO));
charInfo->iSlot = (int8_t)i + 1;
charInfo->iLevel = (int16_t)1;
charInfo->sPC_Style.iPC_UID = rand(); // unique identifier for the character
charInfo->sPC_Style.iNameCheck = 1;
charInfo->sPC_Style.iGender = (i%2)+1; // can be 1(boy) or 2(girl)
charInfo->sPC_Style.iFaceStyle = 1;
charInfo->sPC_Style.iHairStyle = 1;
charInfo->sPC_Style.iHairColor = (rand()%19) + 1; // 1 - 19
charInfo->sPC_Style.iSkinColor = (rand()%13) + 1; // 1 - 13
charInfo->sPC_Style.iEyeColor = (rand()%6) + 1; // 1 - 6
charInfo->sPC_Style.iHeight = (rand()%6); // 0 -5
charInfo->sPC_Style.iBody = (rand()%4); // 0 - 3
charInfo->sPC_Style.iClass = 0;
charInfo->sPC_Style2 = sPCStyle2(1, 1, 1);
// past's town hall
charInfo->iX = settings::SPAWN_X;
charInfo->iY = settings::SPAWN_Y;
charInfo->iZ = settings::SPAWN_Z;
U8toU16(std::string("Player"), charInfo->sPC_Style.szFirstName);
U8toU16(std::to_string(i + 1), charInfo->sPC_Style.szLastName);
int64_t UID = charInfo->sPC_Style.iPC_UID;
loginSessions[sock].characters[UID] = Player();
loginSessions[sock].characters[UID].level = charInfo->iLevel;
loginSessions[sock].characters[UID].slot = charInfo->iSlot;
loginSessions[sock].characters[UID].FEKey = sock->getFEKey();
loginSessions[sock].characters[UID].x = charInfo->iX;
loginSessions[sock].characters[UID].y = charInfo->iY;
loginSessions[sock].characters[UID].z = charInfo->iZ;
loginSessions[sock].characters[UID].PCStyle = charInfo->sPC_Style;
loginSessions[sock].characters[UID].PCStyle2 = charInfo->sPC_Style2;
for (int i = 0; i < AEQUIP_COUNT; i++) {
// setup item
charInfo->aEquip[i].iID = 0;
charInfo->aEquip[i].iType = i;
charInfo->aEquip[i].iOpt = 0;
loginSessions[sock].characters[UID].Equip[i] = charInfo->aEquip[i];
}
// set default to the first character
if (i == 0)
loginSessions[sock].selectedChar = UID;
sock->sendPacket(new CNPacketData((void*)charInfo, P_LS2CL_REP_CHAR_INFO, sizeof(sP_LS2CL_REP_CHAR_INFO), sock->getEKey()));
}
}
break;
}
case P_CL2LS_REP_LIVE_CHECK: {
// stubbed, the client really doesn't care LOL
break;
}
case P_CL2LS_REQ_CHECK_CHAR_NAME: {
// naughty words allowed!!!!!!!! (also for some reason, the client will always show 'Player 0' if you manually type a name. It will show up for other connected players though)
sP_CL2LS_REQ_CHECK_CHAR_NAME* nameCheck = (sP_CL2LS_REQ_CHECK_CHAR_NAME*)data->buf;
sP_LS2CL_REP_CHECK_CHAR_NAME_SUCC* response = (sP_LS2CL_REP_CHECK_CHAR_NAME_SUCC*)xmalloc(sizeof(sP_LS2CL_REP_CHECK_CHAR_NAME_SUCC));
DEBUGLOG(
std::cout << "P_CL2LS_REQ_CHECK_CHAR_NAME:" << std::endl;
std::cout << "\tFirstName: " << U16toU8(nameCheck->szFirstName) << " LastName: " << U16toU8(nameCheck->szLastName) << std::endl;
)
memcpy(response->szFirstName, nameCheck->szFirstName, sizeof(char16_t) * 9);
memcpy(response->szLastName, nameCheck->szLastName, sizeof(char16_t) * 17);
// fr*ck allowed!!!
sock->sendPacket(new CNPacketData((void*)response, P_LS2CL_REP_CHECK_CHAR_NAME_SUCC, sizeof(sP_LS2CL_REP_CHECK_CHAR_NAME_SUCC), sock->getEKey()));
break;
}
case P_CL2LS_REQ_SAVE_CHAR_NAME: {
sP_CL2LS_REQ_SAVE_CHAR_NAME* save = (sP_CL2LS_REQ_SAVE_CHAR_NAME*)data->buf;
sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC* response = (sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC*)xmalloc(sizeof(sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC));
DEBUGLOG(
std::cout << "P_CL2LS_REQ_SAVE_CHAR_NAME:" << std::endl;
std::cout << "\tSlot: " << (int)save->iSlotNum << std::endl;
std::cout << "\tGender: " << (int)save->iGender << std::endl;
std::cout << "\tName: " << U16toU8(save->szFirstName) << " " << U16toU8(save->szLastName) << std::endl;
)
response->iSlotNum = save->iSlotNum;
response->iGender = save->iGender;
memcpy(response->szFirstName, save->szFirstName, sizeof(char16_t) * 9);
memcpy(response->szLastName, save->szLastName, sizeof(char16_t) * 17);
sock->sendPacket(new CNPacketData((void*)response, P_LS2CL_REP_SAVE_CHAR_NAME_SUCC, sizeof(sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC), sock->getEKey()));
break;
}
case P_CL2LS_REQ_CHAR_CREATE: {
sP_CL2LS_REQ_CHAR_CREATE* character = (sP_CL2LS_REQ_CHAR_CREATE*)data->buf;
sP_LS2CL_REP_CHAR_CREATE_SUCC* response = (sP_LS2CL_REP_CHAR_CREATE_SUCC*)xmalloc(sizeof(sP_LS2CL_REP_CHAR_CREATE_SUCC));
DEBUGLOG(
std::cout << "P_CL2LS_REQ_CHAR_CREATE:" << std::endl;
std::cout << "\tPC_UID: " << character->PCStyle.iPC_UID << std::endl;
std::cout << "\tNameCheck: " << (int)character->PCStyle.iNameCheck << std::endl;
std::cout << "\tName: " << U16toU8(character->PCStyle.szFirstName) << " " << U16toU8(character->PCStyle.szLastName) << std::endl;
std::cout << "\tGender: " << (int)character->PCStyle.iGender << std::endl;
std::cout << "\tFace: " << (int)character->PCStyle.iFaceStyle << std::endl;
std::cout << "\tHair: " << (int)character->PCStyle.iHairStyle << std::endl;
std::cout << "\tHair Color: " << (int)character->PCStyle.iHairColor << std::endl;
std::cout << "\tSkin Color: " << (int)character->PCStyle.iSkinColor << std::endl;
std::cout << "\tEye Color: " << (int)character->PCStyle.iEyeColor << std::endl;
std::cout << "\tHeight: " << (int)character->PCStyle.iHeight << std::endl;
std::cout << "\tBody: " << (int)character->PCStyle.iBody << std::endl;
std::cout << "\tClass: " << (int)character->PCStyle.iClass << std::endl;
std::cout << "\tiEquipUBID: " << (int)character->sOn_Item.iEquipUBID << std::endl;
std::cout << "\tiEquipLBID: " << (int)character->sOn_Item.iEquipLBID << std::endl;
std::cout << "\tiEquipFootID: " << (int)character->sOn_Item.iEquipFootID << std::endl;
)
character->PCStyle.iNameCheck = 1;
response->PC_Style = character->PCStyle;
response->PC_Style2 = sPCStyle2(1, 1, 1);
response->iLevel = 1;
response->sOn_Item = character->sOn_Item;
int64_t UID = character->PCStyle.iPC_UID;
loginSessions[sock].characters[UID] = Player();
loginSessions[sock].characters[UID].level = 1;
loginSessions[sock].characters[UID].FEKey = sock->getFEKey();
loginSessions[sock].characters[UID].PCStyle = character->PCStyle;
loginSessions[sock].characters[UID].PCStyle2 = sPCStyle2(1, 0, 1);
loginSessions[sock].characters[UID].x = settings::SPAWN_X;
loginSessions[sock].characters[UID].y = settings::SPAWN_Y;
loginSessions[sock].characters[UID].z = settings::SPAWN_Z;
loginSessions[sock].characters[UID].Equip[1].iID = character->sOn_Item.iEquipUBID; // upper body
loginSessions[sock].characters[UID].Equip[1].iType = 1;
loginSessions[sock].characters[UID].Equip[2].iID = character->sOn_Item.iEquipLBID; // lower body
loginSessions[sock].characters[UID].Equip[2].iType = 2;
loginSessions[sock].characters[UID].Equip[3].iID = character->sOn_Item.iEquipFootID; // foot!
loginSessions[sock].characters[UID].Equip[3].iType = 3;
sock->sendPacket(new CNPacketData((void*)response, P_LS2CL_REP_CHAR_CREATE_SUCC, sizeof(sP_LS2CL_REP_CHAR_CREATE_SUCC), sock->getEKey()));
break;
}
case P_CL2LS_REQ_CHAR_SELECT: {
// character selected
sP_CL2LS_REQ_CHAR_SELECT* chararacter = (sP_CL2LS_REQ_CHAR_SELECT*)data->buf;
sP_LS2CL_REP_CHAR_SELECT_SUCC* response = (sP_LS2CL_REP_CHAR_SELECT_SUCC*)xmalloc(sizeof(sP_LS2CL_REP_CHAR_SELECT_SUCC));
DEBUGLOG(
std::cout << "P_CL2LS_REQ_CHAR_SELECT:" << std::endl;
std::cout << "\tPC_UID: " << chararacter->iPC_UID << std::endl;
)
loginSessions[sock].selectedChar = chararacter->iPC_UID;
sock->sendPacket(new CNPacketData((void*)response, P_LS2CL_REP_CHAR_SELECT_SUCC, sizeof(sP_LS2CL_REP_CHAR_SELECT_SUCC), sock->getEKey()));
break;
}
case P_CL2LS_REQ_SHARD_SELECT: {
// tell client to connect to the shard server
sP_CL2LS_REQ_SHARD_SELECT* shard = (sP_CL2LS_REQ_SHARD_SELECT*)data->buf;
sP_LS2CL_REP_SHARD_SELECT_SUCC* response = (sP_LS2CL_REP_SHARD_SELECT_SUCC*)xmalloc(sizeof(sP_LS2CL_REP_SHARD_SELECT_SUCC));
DEBUGLOG(
std::cout << "P_CL2LS_REQ_SHARD_SELECT:" << std::endl;
std::cout << "\tShard: " << (int)shard->iShardNum << std::endl;
)
const char* SHARD_IP = settings::SHARDSERVERIP.c_str();
response->iEnterSerialKey = rand();
response->g_FE_ServerPort = settings::SHARDPORT;
// copy IP to response (this struct uses ASCII encoding so we don't have to goof around converting encodings)
memcpy(response->g_FE_ServerIP, SHARD_IP, strlen(SHARD_IP));
response->g_FE_ServerIP[strlen(SHARD_IP)] = '\0';
// pass player to CNSharedData
CNSharedData::setPlayer(response->iEnterSerialKey, loginSessions[sock].characters[loginSessions[sock].selectedChar]);
sock->sendPacket(new CNPacketData((void*)response, P_LS2CL_REP_SHARD_SELECT_SUCC, sizeof(sP_LS2CL_REP_SHARD_SELECT_SUCC), sock->getEKey()));
sock->kill(); // client should connect to the Shard server now
break;
}
default:
std::cerr << "OpenFusion: LOGIN UNIMPLM ERR. PacketType: " << data->type << std::endl;
break;
}
}
void CNLoginServer::killConnection(CNSocket* cns) {
loginSessions.erase(cns);
}

View File

@ -1,49 +0,0 @@
#ifndef _CNLS_HPP
#define _CNLS_HPP
#include "CNProtocol.hpp"
#include "Player.hpp"
#include <map>
enum LOGINPACKETID {
// client to login server
P_CL2LS_REQ_LOGIN = 301989889,
P_CL2LS_REQ_CHECK_CHAR_NAME = 301989890,
P_CL2LS_REQ_SAVE_CHAR_NAME = 301989891,
P_CL2LS_REQ_CHAR_CREATE = 301989892,
P_CL2LS_REQ_CHAR_SELECT = 301989893,
P_CL2LS_REQ_SHARD_SELECT = 301989895,
P_CL2LS_REP_LIVE_CHECK = 301989900,
P_CL2LS_REQ_SHARD_LIST_INFO = 301989896,
// login server 2 client
P_LS2CL_REP_LOGIN_SUCC = 553648129,
P_LS2CL_REP_CHAR_INFO = 553648131,
P_LS2CL_REP_CHECK_CHAR_NAME_SUCC = 553648133,
P_LS2CL_REP_SAVE_CHAR_NAME_SUCC = 553648135,
P_LS2CL_REP_CHAR_CREATE_SUCC = 553648137,
P_LS2CL_REP_CHAR_SELECT_SUCC = 553648139,
P_LS2CL_REP_SHARD_SELECT_SUCC = 553648143,
P_LS2CL_REQ_LIVE_CHECK = 553648150,
P_LS2CL_REP_SHARD_LIST_INFO_SUCC = 553648153
};
struct CNLoginData {
std::map<int64_t, Player> characters;
int64_t selectedChar;
};
// WARNING: THERE CAN ONLY BE ONE OF THESE SERVERS AT A TIME!!!!!! TODO: change loginSessions & packet handlers to be non-static
class CNLoginServer : public CNServer {
private:
static void handlePacket(CNSocket* sock, CNPacketData* data);
static std::map<CNSocket*, CNLoginData> loginSessions;
public:
CNLoginServer(uint16_t p);
void killConnection(CNSocket* cns);
};
#endif

View File

@ -1,283 +0,0 @@
#include "CNProtocol.hpp"
// ========================================================[[ CNSocketEncryption ]]========================================================
// literally C/P from the client and converted to C++ (does some byte swapping /shrug)
int CNSocketEncryption::Encrypt_byte_change_A(int ERSize, uint8_t* data, int size) {
int num = 0;
int num2 = 0;
int num3 = 0;
while (num + ERSize <= size)
{
int num4 = num + num3;
int num5 = num + (ERSize - 1 - num3);
uint8_t b = data[num4];
data[num4] = data[num5];
data[num5] = b;
num += ERSize;
num3++;
if (num3 > ERSize / 2)
{
num3 = 0;
}
}
num2 = ERSize - (num + ERSize - size);
return num + num2;
}
int CNSocketEncryption::xorData(uint8_t* buffer, uint8_t* key, int size) {
// xor every 8 bytes with 8 byte key
for (int i = 0; i < size; i++) {
buffer[i] ^= key[i % keyLength];
}
return 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]);
return dEKey * (uTime * num * num2);
}
int CNSocketEncryption::encryptData(uint8_t* buffer, uint8_t* key, int size) {
int eRSize = size % (keyLength / 2 + 1) * 2 + keyLength; // C/P from client
int size2 = xorData(buffer, key, size);
return Encrypt_byte_change_A(eRSize, buffer, size2);
}
int CNSocketEncryption::decryptData(uint8_t* buffer, uint8_t* key, int size) {
int eRSize = size % (keyLength / 2 + 1) * 2 + keyLength; // size % of 18????
int size2 = Encrypt_byte_change_A(eRSize, buffer, size);
return xorData(buffer, key, size2);
}
// ========================================================[[ CNPacketData ]]========================================================
CNPacketData::CNPacketData(void* b, uint32_t t, int l, uint64_t k): buf(b), type(t), size(l), key(k) {}
CNPacketData::~CNPacketData() {
free(buf); // we own the buffer
}
// ========================================================[[ CNSocket ]]========================================================
CNSocket::CNSocket(SOCKET s, PacketHandler ph): sock(s), pHandler(ph) {
EKey = (uint64_t)(*(uint64_t*)&CNSocketEncryption::defaultKey[0]);
}
bool CNSocket::sendData(uint8_t* data, int size) {
int sentBytes = 0;
while (sentBytes < size) {
int sent = send(sock, (buffer_t*)(data + sentBytes), size - sentBytes, 0); // no flags defined
if (SOCKETERROR(sent))
return false; // error occured while sending bytes
sentBytes += sent;
}
return true; // it worked!
}
void CNSocket::setEKey(uint64_t k) {
EKey = k;
}
void CNSocket::setFEKey(uint64_t k) {
FEKey = k;
}
uint64_t CNSocket::getEKey() {
return EKey;
}
uint64_t CNSocket::getFEKey() {
return FEKey;
}
bool CNSocket::isAlive() {
return alive;
}
void CNSocket::kill() {
alive = false;
#ifdef _WIN32
shutdown(sock, SD_BOTH);
closesocket(sock);
#else
shutdown(sock, SHUT_RDWR);
close(sock);
#endif
}
void CNSocket::sendPacket(CNPacketData* pak) {
int tmpSize = pak->size + sizeof(uint32_t);
uint8_t* tmpBuf = (uint8_t*)xmalloc(tmpSize);
// copy packet type to the front of the buffer & then the actual buffer
memcpy(tmpBuf, (void*)&pak->type, sizeof(uint32_t));
memcpy(tmpBuf+sizeof(uint32_t), pak->buf, pak->size);
// encrypt the packet
CNSocketEncryption::encryptData((uint8_t*)tmpBuf, (uint8_t*)(&pak->key), tmpSize);
// send packet size
sendData((uint8_t*)&tmpSize, sizeof(uint32_t));
// send packet data!
sendData(tmpBuf, tmpSize);
delete pak;
free(tmpBuf); // free tmp buffer
}
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 (!SOCKETERROR(recved)) {
// we got out packet size!!!!
readSize = *((int32_t*)readBuffer);
// sanity check
if (readSize > MAX_PACKETSIZE) {
kill();
return;
}
// we'll just leave bufferIndex at 0 since we already have the packet size, it's safe to overwrite those bytes
activelyReading = true;
}
}
if (readSize > 0 && readBufferIndex < readSize) {
// read until the end of the packet! (or at least try too)
int recved = recv(sock, (buffer_t*)(readBuffer + readBufferIndex), readSize - readBufferIndex, 0);
if (!SOCKETERROR(recved))
readBufferIndex += recved;
}
if (activelyReading && readBufferIndex - readSize <= 0) {
// decrypt readBuffer and copy to CNPacketData
CNSocketEncryption::decryptData(readBuffer, (uint8_t*)(&EKey), readSize);
// this doesn't leak memory because we free it in CNPacketData's deconstructor LOL
void* tmpBuf = xmalloc(readSize-sizeof(int32_t));
memcpy(tmpBuf, readBuffer+sizeof(uint32_t), readSize-sizeof(int32_t));
CNPacketData tmp(tmpBuf, *((uint32_t*)readBuffer), readSize-sizeof(int32_t), EKey);
// CALL PACKET HANDLER!!
pHandler(this, &tmp); // tmp's deconstructor will be called when readStep returns so that tmpBuffer we made will be cleaned up :)
// reset vars :)
readSize = 0;
readBufferIndex = 0;
activelyReading = false;
}
}
// ========================================================[[ CNServer ]]========================================================
void CNServer::init() {
// create socket file descriptor
sock = socket(AF_INET, SOCK_STREAM, 0);
if (SOCKETINVALID(sock)) {
std::cerr << "[FATAL] OpenFusion: socket failed" << std::endl;
exit(EXIT_FAILURE);
}
// attach socket to the port
int opt = 1;
#ifdef _WIN32
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt)) != 0) {
#else
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) {
#endif
std::cerr << "[FATAL] OpenFusion: setsockopt failed" << std::endl;
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(port);
addressSize = sizeof(address);
// Bind to the port
if (SOCKETERROR(bind(sock, (struct sockaddr *)&address, addressSize))) {
std::cerr << "[FATAL] OpenFusion: bind failed" << std::endl;
exit(EXIT_FAILURE);
}
if (SOCKETERROR(listen(sock, SOMAXCONN))) {
std::cerr << "[FATAL] OpenFusion: listen failed" << std::endl;
exit(EXIT_FAILURE);
}
// set server listener to non-blocking
#ifdef _WIN32
unsigned long mode = 1;
if (ioctlsocket(sock, FIONBIO, &mode) != 0) {
#else
if (fcntl(sock, F_SETFL, (fcntl(sock, F_GETFL, 0) | O_NONBLOCK)) != 0) {
#endif
std::cerr << "[FATAL] OpenFusion: fcntl failed" << std::endl;
exit(EXIT_FAILURE);
}
}
CNServer::CNServer() {};
CNServer::CNServer(uint16_t p): port(p) {}
void CNServer::start() {
std::cout << "Starting server at *:" << port << std::endl;
// listen to new connections, add to connection list
while (true) {
// listen for a new connection
SOCKET newConnection = accept(sock, (struct sockaddr *)&(address), (socklen_t*)&(addressSize));
if (!SOCKETINVALID(newConnection)) {
// new connection! make sure to set non-blocking!
#ifdef _WIN32
unsigned long mode = 1;
if (ioctlsocket(newConnection, FIONBIO, &mode) != 0) {
#else
if (fcntl(newConnection, F_SETFL, (fcntl(sock, F_GETFL, 0) | O_NONBLOCK)) != 0) {
#endif
std::cerr << "[FATAL] OpenFusion: fcntl failed on new connection" << std::endl;
exit(EXIT_FAILURE);
}
std::cout << "New connection! " << inet_ntoa(address.sin_addr) << std::endl;
// add connection to list!
CNSocket* tmp = new CNSocket(newConnection, pHandler);
connections.push_back(tmp);
}
// for each connection, check if it's alive, if not kill it!
std::list<CNSocket*>::iterator i = connections.begin();
while (i != connections.end()) {
CNSocket* cSock = *i;
if (cSock->isAlive()) {
cSock->step();
++i; // go to the next element
} else {
killConnection(cSock);
connections.erase(i++);
delete cSock;
}
}
#ifdef _WIN32
Sleep(0);
#else
sleep(0); // so your cpu isn't at 100% all the time, we don't need all of that! im not hacky! you're hacky!
#endif
}
}
void CNServer::killConnection(CNSocket* cns) {} // stubbed lol

View File

@ -1,138 +0,0 @@
#ifndef _CNP_HPP
#define _CNP_HPP
#define MAX_PACKETSIZE 8192
#define DEBUGLOG(x) x
#include <iostream>
#include <stdio.h>
#include <stdint.h>
#ifdef _WIN32
// windows (UNTESTED)
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#pragma comment(lib, "Ws2_32.lib")
typedef char buffer_t;
//#define errno WSAGetLastError()
#define SOCKETINVALID(x) (x == INVALID_SOCKET)
#define SOCKETERROR(x) (x == SOCKET_ERROR)
#else
// posix platform
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
typedef int SOCKET;
typedef void buffer_t;
#define SOCKETINVALID(x) (x < 0)
#define SOCKETERROR(x) (x == -1)
#endif
#include <fcntl.h>
#include <string>
#include <cstring>
#include <csignal>
#include <list>
#include <queue>
/*
Packets format (sent from the client):
[4 bytes] - size of packet (including these 4 bytes!)
[size bytes] - Encrypted packet (byte swapped && xor'd with 8 byte key; see CNSocketEncryption)
[4 bytes] - packet type (which is a combination of the first 4 bytes of the packet and a checksum in some versions)
[structure]
*/
// error checking calloc wrapper
inline void* xmalloc(size_t sz) {
void* res = calloc(1, sz);
if (res == NULL) {
std::cerr << "[FATAL] OpenFusion: calloc failed to allocate memory!" << std::endl;
exit(EXIT_FAILURE);
}
return res;
}
namespace CNSocketEncryption {
// you won't believe how complicated they made it in the client :facepalm:
static constexpr const char* defaultKey = "m@rQn~W#";
static const unsigned int keyLength = 8;
int Encrypt_byte_change_A(int ERSize, uint8_t* data, int size);
int xorData(uint8_t* buffer, uint8_t* key, int size);
uint64_t createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2);
int encryptData(uint8_t* buffer, uint8_t* key, int size);
int decryptData(uint8_t* buffer, uint8_t* key, int size);
}
class CNPacketData {
public:
void* buf;
int size;
uint32_t type;
uint64_t key;
CNPacketData(void* b, uint32_t t, int l, uint64_t k);
~CNPacketData();
};
class CNSocket;
typedef void (*PacketHandler)(CNSocket* sock, CNPacketData* data);
class CNSocket {
private:
uint64_t EKey;
uint64_t FEKey;
int32_t readSize = 0;
uint8_t* readBuffer = new uint8_t[MAX_PACKETSIZE];
int readBufferIndex = 0;
bool activelyReading = false;
bool alive = true;
bool sendData(uint8_t* data, int size);
public:
SOCKET sock;
PacketHandler pHandler;
CNSocket(SOCKET s, PacketHandler ph);
void setEKey(uint64_t k);
void setFEKey(uint64_t k);
uint64_t getEKey();
uint64_t getFEKey();
void kill();
void sendPacket(CNPacketData* pak);
void step();
bool isAlive();
};
// in charge of accepting new connections and making sure each connection is kept alive
class CNServer {
protected:
std::list<CNSocket*> connections;
SOCKET sock;
uint16_t port;
socklen_t addressSize;
struct sockaddr_in address;
void init();
public:
PacketHandler pHandler;
CNServer();
CNServer(uint16_t p);
void start();
virtual void killConnection(CNSocket* cns);
};
#endif

View File

@ -1,33 +0,0 @@
#include "CNProtocol.hpp"
#include "CNStructs.hpp"
#include "CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "CNShared.hpp"
#include <iostream>
#include <sstream>
#include <cstdlib>
std::map<uint32_t, PacketHandler> CNShardServer::ShardPackets;
CNShardServer::CNShardServer(uint16_t p) {
port = p;
pHandler = &CNShardServer::handlePacket;
init();
}
void CNShardServer::handlePacket(CNSocket* sock, CNPacketData* data) {
if (ShardPackets.find(data->type) != ShardPackets.end())
ShardPackets[data->type](sock, data);
else
std::cerr << "OpenFusion: SHARD UNIMPLM ERR. PacketType: " << data->type << std::endl;
}
void CNShardServer::killConnection(CNSocket* cns) {
// remove from CNSharedData
Player cachedPlr = PlayerManager::getPlayer(cns);
PlayerManager::removePlayer(cns);
CNSharedData::erasePlayer(cachedPlr.SerialKey);
std::cout << U16toU8(cachedPlr.PCStyle.szFirstName) << " " << U16toU8(cachedPlr.PCStyle.szLastName) << " left" << std::endl;
}

View File

@ -1,49 +0,0 @@
#ifndef _CNSS_HPP
#define _CNSS_HPP
#include "CNProtocol.hpp"
#include <map>
enum SHARDPACKETID {
// client 2 shard
P_CL2FE_REQ_PC_ENTER = 318767105,
P_CL2FE_REQ_PC_LOADING_COMPLETE = 318767245,
P_CL2FE_REQ_PC_MOVE = 318767107,
P_CL2FE_REQ_PC_STOP = 318767108,
P_CL2FE_REQ_PC_JUMP = 318767109,
P_CL2FE_REQ_PC_MOVEPLATFORM = 318767168,
P_CL2FE_REQ_PC_GOTO = 318767124,
P_CL2FE_GM_REQ_PC_SET_VALUE = 318767211,
P_CL2FE_REQ_SEND_FREECHAT_MESSAGE = 318767111,
P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT = 318767184,
// shard 2 client
P_FE2CL_REP_PC_ENTER_SUCC = 822083586,
P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC = 822083833,
P_FE2CL_PC_NEW = 822083587,
P_FE2CL_PC_MOVE = 822083592,
P_FE2CL_PC_STOP = 822083593,
P_FE2CL_PC_JUMP = 822083594,
P_FE2CL_PC_EXIT = 822083590,
P_FE2CL_PC_MOVEPLATFORM = 822083704,
P_FE2CL_REP_PC_GOTO_SUCC = 822083633,
P_FE2CL_GM_REP_PC_SET_VALUE = 822083781,
P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT = 822083730
};
#define REGISTER_SHARD_PACKET(pactype, handlr) CNShardServer::ShardPackets[pactype] = handlr;
// WARNING: THERE CAN ONLY BE ONE OF THESE SERVERS AT A TIME!!!!!! TODO: change players & packet handlers to be non-static
class CNShardServer : public CNServer {
private:
static void handlePacket(CNSocket* sock, CNPacketData* data);
public:
static std::map<uint32_t, PacketHandler> ShardPackets;
CNShardServer(uint16_t p);
void killConnection(CNSocket* cns);
};
#endif

View File

@ -1,25 +0,0 @@
#include "CNShared.hpp"
#ifdef __MINGW32__
#include "mingw/mingw.mutex.h"
#else
#include <mutex>
#endif
std::map<int64_t, Player> CNSharedData::players;
std::mutex playerCrit;
void CNSharedData::setPlayer(int64_t sk, Player& plr) {
std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
players[sk] = plr;
}
Player CNSharedData::getPlayer(int64_t sk) {
return players[sk];
}
void CNSharedData::erasePlayer(int64_t sk) {
std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
players.erase(sk);
}

View File

@ -1,23 +0,0 @@
/*
CNShared.hpp
There's some data shared between the Login Server and the Shard Server. Of course all of this needs to be thread-safe. No mucking about on this one!
*/
#ifndef _CNSD_HPP
#define _CNSD_HPP
#include <map>
#include <string>
#include "Player.hpp"
namespace CNSharedData {
// serialkey corresponds to player data
extern std::map<int64_t, Player> players;
void setPlayer(int64_t sk, Player& plr);
Player getPlayer(int64_t sk);
void erasePlayer(int64_t sk);
}
#endif

View File

@ -1,24 +0,0 @@
#include "CNStructs.hpp"
std::string U16toU8(char16_t* src) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
return convert.to_bytes(src);
}
// returns number of char16_t that was written at des
int U8toU16(std::string src, char16_t* des) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
std::u16string tmp = convert.from_bytes(src);
// copy utf16 string to buffer
memcpy(des, tmp.c_str(), sizeof(char16_t) * tmp.length());
des[tmp.length()] = '\0';
return tmp.length();
}
uint64_t getTime() {
struct timeval tp;
gettimeofday(&tp, NULL);
return tp.tv_sec * 1000 + tp.tv_usec / 1000;
}

View File

@ -1,533 +0,0 @@
/*
CNStructs.hpp - defines some basic structs & useful methods for packets used by FusionFall
NOTE: this is missing the vast majority of packets, I have also ommitted the ERR & FAIL packets for simplicity
*/
#ifndef _CNS_HPP
#define _CNS_HPP
#include <iostream>
#include <stdio.h>
#include <stdint.h>
#include <sys/time.h>
#include <cstring>
#include <string>
#include <locale>
#include <codecvt>
// TODO: rewrite U16toU8 & U8toU16 to not use codecvt
std::string U16toU8(char16_t* src);
// returns number of char16_t that was written at des
int U8toU16(std::string src, char16_t* des);
uint64_t getTime();
//#define CNPROTO_VERSION_0728
#ifdef CNPROTO_VERSION_0728
#define AEQUIP_COUNT 12
#else
#define AEQUIP_COUNT 9
#endif
// ========================================================[[ General Purpose ]]========================================================
// sets the same byte alignment as the structs in the client
#pragma pack(push, 4)
struct sPCStyle {
int64_t iPC_UID;
int8_t iNameCheck;
char16_t szFirstName[9];
char16_t szLastName[17];
int8_t iGender;
int8_t iFaceStyle;
int8_t iHairStyle;
int8_t iHairColor;
int8_t iSkinColor;
int8_t iEyeColor;
int8_t iHeight;
int8_t iBody;
int32_t iClass;
};
#pragma pack(1)
struct sPCStyle2 {
int8_t iAppearanceFlag;
int8_t iTutorialFlag;
int8_t iPayzoneFlag;
sPCStyle2() {}
sPCStyle2(int8_t a, int8_t t, int8_t p):
iAppearanceFlag(a), iTutorialFlag(t), iPayzoneFlag(p) {}
};
#pragma pack(2)
struct sOnItem {
int16_t iEquipHandID;
int16_t iEquipUBID;
int16_t iEquipLBID;
int16_t iEquipFootID;
int16_t iEquipHeadID;
int16_t iEquipFaceID;
int16_t iEquipBackID;
};
struct sOnItem_Index {
int16_t iEquipUBID_index;
int16_t iEquipLBID_index;
int16_t iEquipFootID_index;
int16_t iFaceStyle;
int16_t iHairStyle;
};
struct sNano {
int16_t iID;
int16_t iSkillID;
int16_t iStamina;
};
#pragma pack(4)
struct sItemBase {
int16_t iType;
int16_t iID;
int32_t iOpt;
int32_t iTimeLimit;
#ifdef CNPROTO_VERSION_0728
int32_t iSerial;
#endif
};
struct sTimeBuff {
uint64_t iTimeLimit;
uint64_t iTimeDuration;
int32_t iTimeRepeat;
int32_t iValue;
int32_t iConfirmNum;
};
struct sRunningQuest {
int32_t m_aCurrTaskID;
int32_t m_aKillNPCID[3];
int32_t m_aKillNPCCount[3];
int32_t m_aNeededItemID[3];
int32_t m_aNeededItemCount[3];
};
struct sPCLoadData2CL {
int16_t iUserLevel;
sPCStyle PCStyle;
sPCStyle2 PCStyle2;
int16_t iLevel;
int16_t iMentor;
int16_t iMentorCount;
int32_t iHP;
int32_t iBatteryW;
int32_t iBatteryN;
int32_t iCandy;
int32_t iFusionMatter;
int8_t iSpecialState;
int32_t iMapNum;
int32_t iX;
int32_t iY;
int32_t iZ;
int32_t iAngle;
sItemBase aEquip[AEQUIP_COUNT];
sItemBase aInven[50];
sItemBase aQInven[50];
sNano aNanoBank[37];
int16_t aNanoSlots[3];
int16_t iActiveNanoSlotNum;
int32_t iConditionBitFlag;
int32_t eCSTB___Add;
sTimeBuff TimeBuff;
int64_t aQuestFlag[32];
int64_t aRepeatQuestFlag[8];
sRunningQuest aRunningQuest[9];
int32_t iCurrentMissionID;
int32_t iWarpLocationFlag;
int64_t aWyvernLocationFlag[2];
int32_t iBuddyWarpTime;
int32_t iFatigue;
int32_t iFatigue_Level;
int32_t iFatigueRate;
int64_t iFirstUseFlag1;
int64_t iFirstUseFlag2;
int32_t aiPCSkill[33];
sPCLoadData2CL() {};
};
struct sPCAppearanceData {
int32_t iID;
sPCStyle PCStyle;
int32_t iConditionBitFlag;
int8_t iPCState;
int8_t iSpecialState;
int16_t iLv;
int32_t iHP;
int32_t iMapNum;
int32_t iX;
int32_t iY;
int32_t iZ;
int32_t iAngle;
sItemBase ItemEquip[AEQUIP_COUNT];
sNano Nano;
int32_t eRT;
};
// ========================================================[[ Client2LoginServer packets ]]========================================================
#pragma pack(4)
struct sP_CL2LS_REQ_LOGIN {
char16_t szID[33];
char16_t szPassword[33];
int32_t iClientVerA;
int32_t iClientVerB;
int32_t iClientVerC;
int32_t iLoginType;
uint8_t szCookie_TEGid[64];
uint8_t szCookie_authid[255];
};
struct sP_CL2LS_REQ_CHECK_CHAR_NAME {
int32_t iFNCode;
int32_t iLNCode;
int32_t iMNCode;
char16_t szFirstName[9];
char16_t szLastName[17];
};
struct sP_CL2LS_REQ_SAVE_CHAR_NAME {
int8_t iSlotNum;
int8_t iGender;
int32_t iFNCode;
int32_t iLNCode;
int32_t iMNCode;
char16_t szFirstName[9];
char16_t szLastName[17];
};
struct sP_CL2LS_REQ_CHAR_CREATE {
sPCStyle PCStyle;
sOnItem sOn_Item;
sOnItem_Index sOn_Item_Index;
};
struct sP_CL2LS_REQ_CHAR_SELECT {
int64_t iPC_UID;
};
struct sP_CL2LS_REP_LIVE_CHECK {
int32_t unused;
};
#pragma pack(1)
struct sP_CL2LS_REQ_SHARD_SELECT {
int8_t iShardNum;
};
struct sP_CL2LS_REQ_SHARD_LIST_INFO {
uint8_t unused;
};
// ========================================================[[ LoginServer2Client packets ]]========================================================
#pragma pack(4)
struct sP_LS2CL_REP_LOGIN_SUCC {
int8_t iCharCount;
int8_t iSlotNum;
int8_t iPaymentFlag;
int8_t iTempForPacking4; // UNUSED
uint64_t uiSvrTime; // UNIX timestamp
char16_t szID[33];
uint32_t iOpenBetaFlag;
};
#pragma pack(2)
struct sP_LS2CL_REP_CHECK_CHAR_NAME_SUCC {
char16_t szFirstName[9];
char16_t szLastName[17];
};
#pragma pack(4)
struct sP_LS2CL_REP_SAVE_CHAR_NAME_SUCC {
int64_t iPC_UID;
int8_t iSlotNum;
int8_t iGender;
char16_t szFirstName[9];
char16_t szLastName[17];
};
struct sP_LS2CL_REP_CHAR_CREATE_SUCC {
int16_t iLevel;
sPCStyle PC_Style;
sPCStyle2 PC_Style2;
sOnItem sOn_Item;
};
struct sP_LS2CL_REP_CHAR_INFO {
int8_t iSlot;
int16_t iLevel;
sPCStyle sPC_Style;
sPCStyle2 sPC_Style2;
int32_t iX;
int32_t iY;
int32_t iZ;
sItemBase aEquip[AEQUIP_COUNT];
};
struct sP_LS2CL_REP_SHARD_SELECT_SUCC {
uint8_t g_FE_ServerIP[16]; // Ascii
int32_t g_FE_ServerPort;
int64_t iEnterSerialKey;
};
#pragma pack(1)
struct sP_LS2CL_REP_CHAR_SELECT_SUCC {
int8_t unused;
};
struct sP_LS2CL_REP_SHARD_LIST_INFO_SUCC {
uint8_t aShardConnectFlag[27];
};
// ========================================================[[ Client2ShardServer packets ]]========================================================
#pragma pack(4)
struct sP_CL2FE_REQ_PC_ENTER {
char16_t szID[33];
int32_t iTempValue;
int64_t iEnterSerialKey;
};
struct sP_CL2FE_REQ_PC_LOADING_COMPLETE {
int32_t iPC_ID;
};
struct sP_CL2FE_REQ_PC_MOVE {
uint64_t iCliTime;
int32_t iX;
int32_t iY;
int32_t iZ;
float fVX;
float fVY;
float fVZ;
int32_t iAngle;
uint8_t cKeyValue;
int32_t iSpeed;
};
struct sP_CL2FE_REQ_PC_STOP {
uint64_t iCliTime;
int32_t iX;
int32_t iY;
int32_t iZ;
};
struct sP_CL2FE_REQ_PC_JUMP {
uint64_t iCliTime;
int32_t iX;
int32_t iY;
int32_t iZ;
int32_t iVX;
int32_t iVY;
int32_t iVZ;
int32_t iAngle;
uint8_t cKeyValue;
int32_t iSpeed;
};
struct sP_CL2FE_REQ_PC_MOVEPLATFORM {
uint64_t iCliTime;
int32_t iLcX;
int32_t iLcY;
int32_t iLcZ;
int32_t iX;
int32_t iY;
int32_t iZ;
float fVX;
float fVY;
float fVZ;
int32_t bDown;
uint32_t iPlatformID;
int32_t iAngle;
uint32_t cKeyValue;
int32_t iSpeed;
};
struct sP_CL2FE_REQ_PC_GOTO {
int32_t iToX;
int32_t iToY;
int32_t iToZ;
};
struct sP_CL2FE_GM_REQ_PC_SET_VALUE {
int32_t iPC_ID;
int32_t iSetValueType;
int32_t iSetValue;
};
struct sP_CL2FE_REQ_SEND_FREECHAT_MESSAGE {
char16_t szFreeChat[128];
int32_t iEmoteCode;
};
struct sP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT {
int32_t iID_From;
int32_t iEmoteCode;
};
// ========================================================[[ ShardServer2Client packets ]]========================================================
struct sP_FE2CL_REP_PC_ENTER_SUCC {
int32_t iID;
sPCLoadData2CL PCLoadData2CL;
uint64_t uiSvrTime;
};
struct sP_FE2CL_REP_PC_LOADING_COMPLETE_SUCC {
int32_t iPC_ID;
};
// literally just a wrapper for a sPCAppearanceData struct :/
struct sP_FE2CL_PC_NEW {
sPCAppearanceData PCAppearanceData;
};
struct sP_FE2CL_PC_MOVE {
uint64_t iCliTime;
int32_t iX;
int32_t iY;
int32_t iZ;
float fVX;
float fVY;
float fVZ;
int32_t iAngle;
uint8_t cKeyValue;
int32_t iSpeed;
int32_t iID;
uint64_t iSvrTime;
};
struct sP_FE2CL_PC_STOP {
uint64_t iCliTime;
int32_t iX;
int32_t iY;
int32_t iZ;
int32_t iID;
uint64_t iSvrTime;
};
struct sP_FE2CL_PC_JUMP {
uint64_t iCliTime;
int32_t iX;
int32_t iY;
int32_t iZ;
int32_t iVX;
int32_t iVY;
int32_t iVZ;
int32_t iAngle;
uint8_t cKeyValue;
int32_t iSpeed;
int32_t iID;
uint64_t iSvrTime;
};
struct sP_FE2CL_PC_MOVEPLATFORM {
uint64_t iCliTime;
int32_t iLcX;
int32_t iLcY;
int32_t iLcZ;
int32_t iX;
int32_t iY;
int32_t iZ;
float fVX;
float fVY;
float fVZ;
int32_t bDown;
uint32_t iPlatformID;
int32_t iAngle;
int8_t cKeyValue;
int32_t iSpeed;
int32_t iPC_ID;
uint64_t iSvrTime;
};
struct sP_FE2CL_GM_REP_PC_SET_VALUE {
int32_t iPC_ID;
int32_t iSetValueType;
int32_t iSetValue;
};
struct sP_FE2CL_PC_EXIT {
int32_t iID;
int32_t iExitType;
};
struct sP_FE2CL_REP_PC_GOTO_SUCC {
int32_t iX;
int32_t iY;
int32_t iZ;
};
struct sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT {
int32_t iID_From;
int32_t iEmoteCode;
};
#pragma pack(pop)
#endif

321
src/Chat.cpp Normal file
View File

@ -0,0 +1,321 @@
#include "Chat.hpp"
#include "servers/CNShardServer.hpp"
#include "servers/Monitor.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "CustomCommands.hpp"
#include <assert.h>
using namespace Chat;
static void chatHandler(CNSocket* sock, CNPacketData* data) {
auto chat = (sP_CL2FE_REQ_SEND_FREECHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
if (fullChat.length() > 1 && fullChat[0] == CMD_PREFIX) { // PREFIX
CustomCommands::runCmd(fullChat, sock);
return;
}
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__MUTE_FREECHAT)
return;
std::string logLine = "[FreeChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine);
// send to client
INITSTRUCT(sP_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC, resp);
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
resp.iPC_ID = plr->iID;
resp.iEmoteCode = chat->iEmoteCode;
sock->sendPacket(resp, P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC);
// send to visible players
PlayerManager::sendToViewable(sock, resp, P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC);
}
static void menuChatHandler(CNSocket* sock, CNPacketData* data) {
auto chat = (sP_CL2FE_REQ_SEND_MENUCHAT_MESSAGE*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
std::string logLine = "[MenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine);
// send to client
INITSTRUCT(sP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, resp);
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
resp.iPC_ID = PlayerManager::getPlayer(sock)->iID;
resp.iEmoteCode = chat->iEmoteCode;
sock->sendPacket(resp, P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC);
// send to visible players
PlayerManager::sendToViewable(sock, resp, P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC);
}
static void emoteHandler(CNSocket* sock, CNPacketData* data) {
auto emote = (sP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
// send to client
INITSTRUCT(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT, resp);
resp.iEmoteCode = emote->iEmoteCode;
resp.iID_From = plr->iID;
sock->sendPacket(resp, P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT);
// send to visible players (players within render distance)
PlayerManager::sendToViewable(sock, resp, P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT);
}
void Chat::sendServerMessage(CNSocket* sock, std::string msg) {
INITSTRUCT(sP_FE2CL_PC_MOTD_LOGIN, motd);
motd.iType = 1;
// convert string to u16 and write it to the buffer
U8toU16(msg, (char16_t*)motd.szSystemMsg, sizeof(motd.szSystemMsg));
// send the packet :)
sock->sendPacket(motd, P_FE2CL_PC_MOTD_LOGIN);
}
static void announcementHandler(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
if (plr->accountLevel > 30)
return; // only players with account level less than 30 (GM) are allowed to use this command
auto announcement = (sP_CL2FE_GM_REQ_PC_ANNOUNCE*)data->buf;
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
msg.iAnnounceType = announcement->iAnnounceType;
msg.iDuringTime = announcement->iDuringTime;
memcpy(msg.szAnnounceMsg, announcement->szAnnounceMsg, sizeof(msg.szAnnounceMsg));
std::map<CNSocket*, Player*>::iterator it;
// This value is completely arbitrary, but these make the most sense when you consider the architecture of the game
switch (announcement->iAreaType) {
case 0: // area (all players in viewable chunks)
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
PlayerManager::sendToViewable(sock, msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
break;
case 1: // channel
case 2: // shard
case 3: // global (all players)
for (it = PlayerManager::players.begin(); it != PlayerManager::players.end(); it++) {
CNSocket* allSock = it->first;
allSock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
}
default:
break;
}
std::string logLine = std::to_string(announcement->iAreaType) + " "
+ std::to_string(announcement->iAnnounceType) + " "
+ std::to_string(announcement->iDuringTime) + " "
+ PlayerManager::getPlayerName(plr, false) + ": " + AUTOU16TOU8(msg.szAnnounceMsg);
std::cout << "Broadcast " << logLine << std::endl;
Monitor::bcasts.push_back(logLine);
}
// Buddy freechatting
static void buddyChatHandler(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_SEND_BUDDY_FREECHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
INITSTRUCT(sP_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC, resp);
CNSocket* otherSock = PlayerManager::getSockFromID(pkt->iBuddyPCUID);
if (otherSock == nullptr)
return; // buddy offline
Player *otherPlr = PlayerManager::getPlayer(otherSock);
resp.iFromPCUID = plr->PCStyle.iPC_UID;
resp.iToPCUID = pkt->iBuddyPCUID;
resp.iEmoteCode = pkt->iEmoteCode;
std::string fullChat = sanitizeText(AUTOU16TOU8(pkt->szFreeChat));
if (fullChat.length() > 1 && fullChat[0] == CMD_PREFIX) { // PREFIX
CustomCommands::runCmd(fullChat, sock);
return;
}
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__MUTE_FREECHAT)
return;
std::string logLine = "[BuddyChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat;
std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine);
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
sock->sendPacket(resp, P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC); // confirm send to sender
otherSock->sendPacket(resp, P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC); // broadcast send to receiver
}
// Buddy menuchat
static void buddyMenuChatHandler(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
INITSTRUCT(sP_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC, resp);
CNSocket* otherSock = PlayerManager::getSockFromID(pkt->iBuddyPCUID);
if (otherSock == nullptr)
return; // buddy offline
Player *otherPlr = PlayerManager::getPlayer(otherSock);
resp.iFromPCUID = plr->PCStyle.iPC_UID;
resp.iToPCUID = pkt->iBuddyPCUID;
resp.iEmoteCode = pkt->iEmoteCode;
std::string fullChat = sanitizeText(AUTOU16TOU8(pkt->szFreeChat));
std::string logLine = "[BuddyMenuChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat;
std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine);
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
sock->sendPacket(resp, P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC); // confirm send to sender
otherSock->sendPacket(resp, P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC); // broadcast send to receiver
}
static void tradeChatHandler(CNSocket* sock, CNPacketData* data) {
auto pacdat = (sP_CL2FE_REQ_PC_TRADE_EMOTES_CHAT*)data->buf;
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
if (pacdat->iID_Request == pacdat->iID_From)
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
else
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
Player *otherPlr = PlayerManager::getPlayer(otherSock);
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_EMOTES_CHAT, resp);
Player *plr = PlayerManager::getPlayer(sock);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
std::string fullChat = sanitizeText(AUTOU16TOU8(pacdat->szFreeChat));
U8toU16(fullChat, resp.szFreeChat, sizeof(resp.szFreeChat));
std::string logLine = "[TradeChat] " + PlayerManager::getPlayerName(plr) + " (to " + PlayerManager::getPlayerName(otherPlr) + "): " + fullChat;
std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine);
resp.iEmoteCode = pacdat->iEmoteCode;
sock->sendPacket(resp, P_FE2CL_REP_PC_TRADE_EMOTES_CHAT);
otherSock->sendPacket(resp, P_FE2CL_REP_PC_TRADE_EMOTES_CHAT);
}
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);
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
if (fullChat.length() > 1 && fullChat[0] == CMD_PREFIX) { // PREFIX
CustomCommands::runCmd(fullChat, sock);
return;
}
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__MUTE_FREECHAT)
return;
std::string logLine = "[GroupChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine);
// send to client
INITSTRUCT(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, resp);
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
resp.iSendPCID = plr->iID;
resp.iEmoteCode = chat->iEmoteCode;
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);
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
std::string logLine = "[GroupMenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
std::cout << logLine << std::endl;
Monitor::chats.push_back(logLine);
// send to client
INITSTRUCT(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, resp);
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
resp.iSendPCID = plr->iID;
resp.iEmoteCode = chat->iEmoteCode;
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
std::string Chat::sanitizeText(std::string text, bool allowNewlines) {
int i;
const int BUFSIZE = 512;
char buf[BUFSIZE];
assert(text.size() < BUFSIZE);
i = 0;
for (char c : text) {
if (i >= BUFSIZE-1)
break;
if (!allowNewlines && c == '\n')
continue;
if ((c >= ' ' && c <= '~') || c == '\n')
buf[i++] = c;
}
buf[i] = 0;
return std::string(buf);
}
void Chat::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_FREECHAT_MESSAGE, chatHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT, emoteHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE, menuChatHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_BUDDY_FREECHAT_MESSAGE, buddyChatHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE, buddyMenuChatHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_EMOTES_CHAT, tradeChatHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE, groupChatHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE, groupMenuChatHandler);
REGISTER_SHARD_PACKET(P_CL2FE_GM_REQ_PC_ANNOUNCE, announcementHandler);
}

15
src/Chat.hpp Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#define CMD_PREFIX '/'
#include "core/Core.hpp"
#include <string>
#include <vector>
namespace Chat {
void init();
void sendServerMessage(CNSocket* sock, std::string msg); // uses MOTD
std::string sanitizeText(std::string text, bool allowNewlines=false);
}

View File

@ -1,37 +0,0 @@
#include "CNShardServer.hpp"
#include "CNStructs.hpp"
#include "ChatManager.hpp"
#include "PlayerManager.hpp"
void ChatManager::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_FREECHAT_MESSAGE, chatHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT, emoteHandler);
}
void ChatManager::chatHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_SEND_FREECHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_FREECHAT_MESSAGE*)data->buf;
// stubbed for now
}
void ChatManager::emoteHandler(CNSocket* sock, CNPacketData* data) {
// you can dance with friends!!!!!!!!
sP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT* emote = (sP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT*)data->buf;
PlayerView plr = PlayerManager::players[sock];
// send to client
sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT* resp = (sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT*)xmalloc(sizeof(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT));
resp->iEmoteCode = emote->iEmoteCode;
resp->iID_From = plr.plr.iID;
sock->sendPacket(new CNPacketData((void*)resp, P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT, sizeof(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT), sock->getFEKey()));
// send to visible players (players within render distance)
for (CNSocket* otherSock : plr.viewable) {
resp = (sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT*)xmalloc(sizeof(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT));
resp->iEmoteCode = emote->iEmoteCode;
resp->iID_From = plr.plr.iID;
otherSock->sendPacket(new CNPacketData((void*)resp, P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT, sizeof(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT), otherSock->getFEKey()));
}
}

View File

@ -1,13 +0,0 @@
#ifndef _CM_HPP
#define _CM_HPP
#include "CNShardServer.hpp"
namespace ChatManager {
void init();
void chatHandler(CNSocket* sock, CNPacketData* data);
void emoteHandler(CNSocket* sock, CNPacketData* data);
}
#endif

499
src/Chunking.cpp Normal file
View File

@ -0,0 +1,499 @@
#include "Chunking.hpp"
#include "Player.hpp"
#include "MobAI.hpp"
#include "NPCManager.hpp"
#include "Bucket.hpp"
#include <assert.h>
using namespace Chunking;
/*
* The initial chunkPos value before a player is placed into the world.
*/
const ChunkPos Chunking::INVALID_CHUNK = {};
constexpr size_t MAX_PC_PER_AROUND = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(sPCAppearanceData);
constexpr size_t MAX_NPC_PER_AROUND = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(sNPCAppearanceData);
constexpr size_t MAX_SHINY_PER_AROUND = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(sShinyAppearanceData);
constexpr size_t MAX_TRANSPORTATION_PER_AROUND = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(sTransportationAppearanceData);
constexpr size_t MAX_IDS_PER_AROUND_DEL = (CN_PACKET_BODY_SIZE - sizeof(int32_t)) / sizeof(int32_t);
constexpr size_t MAX_TRANSPORTATION_IDS_PER_AROUND_DEL = MAX_IDS_PER_AROUND_DEL - 1; // 1 less for eTT
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" << std::endl;
return;
}
Chunk *chunk = new Chunk();
chunks[pos] = chunk;
// 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)
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" << std::endl;
return;
}
Chunk* chunk = chunks[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)
ref.getEntity()->viewableChunks.erase(chunk);
chunks.erase(pos); // remove from map
delete chunk; // free from memory
}
void Chunking::trackEntity(ChunkPos chunkPos, const EntityRef ref) {
if (!chunkExists(chunkPos))
return; // shouldn't happen
chunks[chunkPos]->entities.insert(ref);
if (ref.kind == EntityKind::PLAYER)
chunks[chunkPos]->nplayers++;
}
void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef ref) {
if (!chunkExists(chunkPos))
return; // do nothing if chunk doesn't even exist
Chunk* chunk = chunks[chunkPos];
chunk->entities.erase(ref); // gone
if (ref.kind == EntityKind::PLAYER)
chunks[chunkPos]->nplayers--;
assert(chunks[chunkPos]->nplayers >= 0);
// if chunk is completely empty, free it
if (chunk->entities.size() == 0)
deleteChunk(chunkPos);
}
template<class T, size_t N>
static void sendAroundPackets(const EntityRef recipient, std::vector<Bucket<T, N>>& buckets, uint32_t packetId) {
assert(recipient.kind == EntityKind::PLAYER);
uint8_t pktBuf[CN_PACKET_BODY_SIZE];
for (const auto& bucket : buckets) {
memset(pktBuf, 0, CN_PACKET_BODY_SIZE);
int count = bucket.size();
*((int32_t*)pktBuf) = count;
T* data = (T*)(pktBuf + sizeof(int32_t));
for (size_t i = 0; i < count; i++) {
data[i] = bucket.get(i).value();
}
recipient.sock->sendPacket(pktBuf, packetId, sizeof(int32_t) + (count * sizeof(T)));
}
}
template<size_t N>
static void sendAroundDelPackets(const EntityRef recipient, std::vector<Bucket<int32_t, N>>& buckets, uint32_t packetId) {
assert(recipient.kind == EntityKind::PLAYER);
uint8_t pktBuf[CN_PACKET_BODY_SIZE];
for (const auto& bucket : buckets) {
memset(pktBuf, 0, CN_PACKET_BODY_SIZE);
int count = bucket.size();
assert(count <= N);
size_t baseSize;
if (packetId == P_FE2CL_AROUND_DEL_TRANSPORTATION) {
sP_FE2CL_AROUND_DEL_TRANSPORTATION* pkt = (sP_FE2CL_AROUND_DEL_TRANSPORTATION*)pktBuf;
pkt->eTT = 3;
pkt->iCnt = count;
baseSize = sizeof(sP_FE2CL_AROUND_DEL_TRANSPORTATION);
} else {
*((int32_t*)pktBuf) = count;
baseSize = sizeof(int32_t);
}
int32_t* ids = (int32_t*)(pktBuf + baseSize);
for (size_t i = 0; i < count; i++) {
ids[i] = bucket.get(i).value();
}
recipient.sock->sendPacket(pktBuf, packetId, baseSize + (count * sizeof(int32_t)));
}
}
template<class T, size_t N>
static void bufferAppearanceData(std::vector<Bucket<T, N>>& buckets, const T& data) {
if (buckets.empty())
buckets.push_back({});
auto& bucket = buckets[buckets.size() - 1];
bucket.add(data);
if (bucket.isFull())
buckets.push_back({});
}
template<size_t N>
static void bufferIdForDisappearance(std::vector<Bucket<int32_t, N>>& buckets, int32_t id) {
if (buckets.empty())
buckets.push_back({});
auto& bucket = buckets[buckets.size() - 1];
bucket.add(id);
if (bucket.isFull())
buckets.push_back({});
}
void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef ref) {
Entity *ent = ref.getEntity();
bool alive = ent->isExtant();
std::vector<Bucket<sPCAppearanceData, MAX_PC_PER_AROUND>> pcAppearances;
std::vector<Bucket<sNPCAppearanceData, MAX_NPC_PER_AROUND>> npcAppearances;
std::vector<Bucket<sShinyAppearanceData, MAX_SHINY_PER_AROUND>> shinyAppearances;
std::vector<Bucket<sTransportationAppearanceData, MAX_TRANSPORTATION_PER_AROUND>> transportationAppearances;
for (Chunk *chunk : chnks) {
for (const EntityRef otherRef : chunk->entities) {
// skip oneself
if (ref == otherRef)
continue;
Entity *other = otherRef.getEntity();
// notify all visible players of the existence of this Entity
if (alive && otherRef.kind == EntityKind::PLAYER) {
ent->enterIntoViewOf(otherRef.sock);
}
// notify this *player* of the existence of all visible Entities
if (ref.kind == EntityKind::PLAYER && other->isExtant()) {
sPCAppearanceData pcData;
sNPCAppearanceData npcData;
sShinyAppearanceData eggData;
sTransportationAppearanceData busData;
switch(otherRef.kind) {
case EntityKind::PLAYER:
pcData = dynamic_cast<Player*>(other)->getAppearanceData();
bufferAppearanceData(pcAppearances, pcData);
break;
case EntityKind::SIMPLE_NPC:
npcData = dynamic_cast<BaseNPC*>(other)->getAppearanceData();
bufferAppearanceData(npcAppearances, npcData);
break;
case EntityKind::COMBAT_NPC:
npcData = dynamic_cast<CombatNPC*>(other)->getAppearanceData();
bufferAppearanceData(npcAppearances, npcData);
break;
case EntityKind::MOB:
npcData = dynamic_cast<Mob*>(other)->getAppearanceData();
bufferAppearanceData(npcAppearances, npcData);
break;
case EntityKind::EGG:
eggData = dynamic_cast<Egg*>(other)->getShinyAppearanceData();
bufferAppearanceData(shinyAppearances, eggData);
break;
case EntityKind::BUS:
busData = dynamic_cast<Bus*>(other)->getTransportationAppearanceData();
bufferAppearanceData(transportationAppearances, busData);
break;
default:
break;
}
}
// for mobs, increment playersInView
if (ref.kind == EntityKind::MOB && otherRef.kind == EntityKind::PLAYER)
((Mob*)ent)->playersInView++;
if (otherRef.kind == EntityKind::MOB && ref.kind == EntityKind::PLAYER)
((Mob*)other)->playersInView++;
}
}
if (ref.kind == EntityKind::PLAYER) {
if (!pcAppearances.empty())
sendAroundPackets(ref, pcAppearances, P_FE2CL_PC_AROUND);
if (!npcAppearances.empty())
sendAroundPackets(ref, npcAppearances, P_FE2CL_NPC_AROUND);
if (!shinyAppearances.empty())
sendAroundPackets(ref, shinyAppearances, P_FE2CL_SHINY_AROUND);
if (!transportationAppearances.empty())
sendAroundPackets(ref, transportationAppearances, P_FE2CL_TRANSPORTATION_AROUND);
}
}
void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef ref) {
Entity *ent = ref.getEntity();
bool alive = ent->isExtant();
std::vector<Bucket<int32_t, MAX_IDS_PER_AROUND_DEL>> pcDisappearances;
std::vector<Bucket<int32_t, MAX_IDS_PER_AROUND_DEL>> npcDisappearances;
std::vector<Bucket<int32_t, MAX_IDS_PER_AROUND_DEL>> shinyDisappearances;
std::vector<Bucket<int32_t, MAX_TRANSPORTATION_IDS_PER_AROUND_DEL>> transportationDisappearances;
for (Chunk *chunk : chnks) {
for (const EntityRef otherRef : chunk->entities) {
// skip oneself
if (ref == otherRef)
continue;
Entity *other = otherRef.getEntity();
// notify all visible players of the departure of this Entity
if (alive && otherRef.kind == EntityKind::PLAYER) {
ent->disappearFromViewOf(otherRef.sock);
}
// notify this *player* of the departure of all visible Entities
if (ref.kind == EntityKind::PLAYER && other->isExtant()) {
int32_t id;
switch(otherRef.kind) {
case EntityKind::PLAYER:
id = dynamic_cast<Player*>(other)->iID;
bufferIdForDisappearance(pcDisappearances, id);
break;
case EntityKind::SIMPLE_NPC:
case EntityKind::COMBAT_NPC:
case EntityKind::MOB:
id = dynamic_cast<BaseNPC*>(other)->id;
bufferIdForDisappearance(npcDisappearances, id);
break;
case EntityKind::EGG:
id = dynamic_cast<Egg*>(other)->id;
bufferIdForDisappearance(shinyDisappearances, id);
break;
case EntityKind::BUS:
id = dynamic_cast<Bus*>(other)->id;
bufferIdForDisappearance(transportationDisappearances, id);
break;
default:
break;
}
}
// for mobs, decrement playersInView
if (ref.kind == EntityKind::MOB && otherRef.kind == EntityKind::PLAYER)
((Mob*)ent)->playersInView--;
if (otherRef.kind == EntityKind::MOB && ref.kind == EntityKind::PLAYER)
((Mob*)other)->playersInView--;
}
}
if (ref.kind == EntityKind::PLAYER) {
if (!pcDisappearances.empty())
sendAroundDelPackets(ref, pcDisappearances, P_FE2CL_AROUND_DEL_PC);
if (!npcDisappearances.empty())
sendAroundDelPackets(ref, npcDisappearances, P_FE2CL_AROUND_DEL_NPC);
if (!shinyDisappearances.empty())
sendAroundDelPackets(ref, shinyDisappearances, P_FE2CL_AROUND_DEL_SHINY);
if (!transportationDisappearances.empty())
sendAroundDelPackets(ref, transportationDisappearances, P_FE2CL_AROUND_DEL_TRANSPORTATION);
}
}
static void emptyChunk(ChunkPos chunkPos) {
if (!chunkExists(chunkPos)) {
std::cout << "[WARN] Tried to empty chunk that doesn't exist\n";
return; // chunk doesn't exist, we don't need to do anything
}
Chunk* chunk = chunks[chunkPos];
if (chunk->nplayers > 0) {
std::cout << "[WARN] Tried to empty chunk that still had players\n";
return; // chunk doesn't exist, we don't need to do anything
}
// unspawn all of the mobs/npcs
std::set refs(chunk->entities);
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
NPCManager::destroyNPC(ref.id);
}
}
void Chunking::updateEntityChunk(const EntityRef ref, ChunkPos from, ChunkPos to) {
Entity* ent = ref.getEntity();
// move to other chunk's player set
untrackEntity(from, ref); // this will delete the chunk if it's empty
// if the new chunk doesn't exist, make it first
if (!chunkExists(to))
newChunk(to);
trackEntity(to, ref);
// calculate viewable chunks from both points
std::set<Chunk*> oldViewables = getViewableChunks(from);
std::set<Chunk*> newViewables = getViewableChunks(to);
std::set<Chunk*> toExit, toEnter;
/*
* Calculate diffs. This is done to prevent phasing on chunk borders.
* toExit will contain old viewables - new viewables, so the player will only be exited in chunks that are out of sight.
* toEnter contains the opposite: new viewables - old viewables, chunks where we previously weren't visible from before.
*/
std::set_difference(oldViewables.begin(), oldViewables.end(), newViewables.begin(), newViewables.end(),
std::inserter(toExit, toExit.end())); // chunks we must be EXITed from (old - new)
std::set_difference(newViewables.begin(), newViewables.end(), oldViewables.begin(), oldViewables.end(),
std::inserter(toEnter, toEnter.end())); // chunks we must be ENTERed into (new - old)
// update views
removeEntityFromChunks(toExit, ref);
addEntityToChunks(toEnter, ref);
ent->chunkPos = to; // update cached chunk position
// updated cached viewable chunks
ent->viewableChunks.clear();
ent->viewableChunks.insert(newViewables.begin(), newViewables.end());
}
bool Chunking::chunkExists(ChunkPos chunk) {
return chunks.find(chunk) != chunks.end();
}
ChunkPos Chunking::chunkPosAt(int posX, int posY, uint64_t instanceID) {
return ChunkPos(posX / (settings::VIEWDISTANCE / 3), posY / (settings::VIEWDISTANCE / 3), instanceID);
}
std::set<Chunk*> Chunking::getViewableChunks(ChunkPos chunk) {
std::set<Chunk*> chnks;
int x, y;
uint64_t inst;
std::tie(x, y, inst) = chunk;
// grabs surrounding chunks if they exist
for (int i = -1; i < 2; i++) {
for (int z = -1; z < 2; z++) {
ChunkPos pos = ChunkPos(x+i, y+z, inst);
// if chunk exists, add it to the set
if (chunkExists(pos))
chnks.insert(chunks[pos]);
}
}
return chnks;
}
/*
* inefficient algorithm to get all chunks from a specific instance
*/
std::vector<ChunkPos> Chunking::getChunksInMap(uint64_t mapNum) {
std::vector<ChunkPos> chnks;
for (auto it = chunks.begin(); it != chunks.end(); it++) {
if (std::get<2>(it->first) == mapNum) {
chnks.push_back(it->first);
}
}
return chnks;
}
/*
* Used only for eggs; use npc->playersInView for everything visible
*/
bool Chunking::inPopulatedChunks(std::set<Chunk*>* chnks) {
for (auto it = chnks->begin(); it != chnks->end(); it++) {
if ((*it)->nplayers > 0)
return true;
}
return false;
}
void Chunking::createInstance(uint64_t instanceID) {
std::vector<ChunkPos> templateChunks = getChunksInMap(MAPNUM(instanceID)); // base instance chunks
// only instantiate if the instance doesn't exist already
if (getChunksInMap(instanceID).size() != 0) {
std::cout << "Instance " << instanceID << " already exists" << std::endl;
return;
}
std::cout << "Creating instance " << instanceID << std::endl;
for (ChunkPos &coords : templateChunks) {
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->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->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->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->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->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->angle);
}
}
}
NPCManager::updateNPCPosition(newMob->id, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->angle);
} else {
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);
}
}
}
}
static void destroyInstance(uint64_t instanceID) {
std::vector<ChunkPos> instanceChunks = getChunksInMap(instanceID);
std::cout << "Deleting instance " << instanceID << " (" << instanceChunks.size() << " chunks)" << std::endl;
for (ChunkPos& coords : instanceChunks) {
emptyChunk(coords);
}
}
void Chunking::destroyInstanceIfEmpty(uint64_t instanceID) {
if (PLAYERID(instanceID) == 0)
return; // don't clean up overworld/IZ chunks
std::vector<ChunkPos> sourceChunkCoords = getChunksInMap(instanceID);
for (ChunkPos& coords : sourceChunkCoords) {
Chunk* chunk = chunks[coords];
if (chunk->nplayers > 0)
return; // there are still players inside
}
destroyInstance(instanceID);
}

51
src/Chunking.hpp Normal file
View File

@ -0,0 +1,51 @@
#pragma once
#include "EntityRef.hpp"
#include <set>
#include <map>
#include <vector>
class Chunk {
public:
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
INSTANCE_UNIQUE // these aren't actually used
};
namespace Chunking {
extern std::map<ChunkPos, Chunk*> chunks;
extern const ChunkPos INVALID_CHUNK;
void updateEntityChunk(const EntityRef ref, ChunkPos from, ChunkPos to);
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);
std::set<Chunk*> getViewableChunks(ChunkPos chunkPos);
std::vector<ChunkPos> getChunksInMap(uint64_t mapNum);
bool inPopulatedChunks(std::set<Chunk*>* chnks);
void createInstance(uint64_t);
void destroyInstanceIfEmpty(uint64_t);
}

1011
src/Combat.cpp Normal file

File diff suppressed because it is too large Load Diff

23
src/Combat.hpp Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include "Player.hpp"
#include "MobAI.hpp"
#include <map>
#include <vector>
struct Bullet {
int pointDamage;
int groupDamage;
bool weaponBoost;
int bulletType;
};
namespace Combat {
extern std::map<int32_t, std::map<int8_t, Bullet>> Bullets;
void init();
void npcAttackPc(Mob *mob, time_t currTime);
void genQItemRolls(std::vector<Player*> players, std::map<int, int>& rolls);
}

1306
src/CustomCommands.cpp Normal file

File diff suppressed because it is too large Load Diff

11
src/CustomCommands.hpp Normal file
View File

@ -0,0 +1,11 @@
#pragma once
#include "core/Core.hpp"
#include <string>
namespace CustomCommands {
void init();
bool runCmd(std::string full, CNSocket* sock);
};

237
src/Eggs.cpp Normal file
View File

@ -0,0 +1,237 @@
#include "Eggs.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Abilities.hpp"
#include "NPCManager.hpp"
#include "Entities.hpp"
#include "Items.hpp"
#include <assert.h>
using namespace Eggs;
std::unordered_map<int, EggType> Eggs::EggTypes;
void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
Player* plr = PlayerManager::getPlayer(sock);
// eggId might be 0 if the buff is made by the /buff command
EntityRef src = eggId == 0 ? sock : EntityRef(eggId);
if(Abilities::SkillTable.count(skillId) == 0) {
std::cout << "[WARN] egg " << eggId << " has skill ID " << skillId << " which doesn't exist" << std::endl;
return;
}
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 {
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;
}
}
// initialize response struct
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + result.size;
uint8_t respbuf[CN_PACKET_BODY_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
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);
}
NPCManager::sendToViewable(src.getEntity(), pkt, P_FE2CL_NPC_SKILL_HIT, resplen);
}
static void eggStep(CNServer* serv, time_t currTime) {
// check dead eggs and eggs in inactive chunks
for (auto npc : NPCManager::NPCs) {
if (npc.second->kind != EntityKind::EGG)
continue;
auto egg = (Egg*)npc.second;
if (!egg->dead || !Chunking::inPopulatedChunks(&egg->viewableChunks))
continue;
if (egg->deadUntil <= currTime) {
// respawn it
egg->dead = false;
egg->deadUntil = 0;
egg->hp = 400;
Chunking::addEntityToChunks(Chunking::getViewableChunks(egg->chunkPos), {npc.first});
}
}
}
static void eggPickup(CNSocket* sock, CNPacketData* data) {
auto pickup = (sP_CL2FE_REQ_SHINY_PICKUP*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
EntityRef eggRef = {pickup->iShinyID};
if (!eggRef.isValid()) {
std::cout << "[WARN] Player tried to open non existing egg?!" << std::endl;
return;
}
auto egg = (Egg*)eggRef.getEntity();
if (egg->kind != EntityKind::EGG) {
std::cout << "[WARN] Player tried to open something other than an?!" << std::endl;
return;
}
if (egg->dead) {
std::cout << "[WARN] Player tried to open a dead egg?!" << std::endl;
return;
}
/* this has some issues with position desync, leaving it out for now
if (abs(egg->x - plr->x)>500 || abs(egg->y - plr->y) > 500) {
std::cout << "[WARN] Player tried to open an egg isn't nearby?!" << std::endl;
return;
}
*/
int typeId = egg->type;
if (EggTypes.find(typeId) == EggTypes.end()) {
std::cout << "[WARN] Egg Type " << typeId << " not found!" << std::endl;
return;
}
EggType* type = &EggTypes[typeId];
/*
* 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;
// in general client finds correct icon on it's own,
// but for damage we have to supply correct CSTB
if (resp.iSkillID == 183)
resp.eCSTB = ECSB_INFECTION;
sock->sendPacket(resp, P_FE2CL_REP_SHINY_PICKUP_SUCC);
}
// drop
if (type->dropCrateId != 0) {
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
assert(resplen < CN_PACKET_BODY_SIZE);
// we know it's only one trailing struct, so we can skip full validation
uint8_t respbuf[resplen]; // not a variable length array, don't worry
sP_FE2CL_REP_REWARD_ITEM* reward = (sP_FE2CL_REP_REWARD_ITEM*)respbuf;
sItemReward* item = (sItemReward*)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
// don't forget to zero the buffer!
memset(respbuf, 0, resplen);
// send back player's stats
reward->m_iCandy = plr->money;
reward->m_iFusionMatter = plr->fusionmatter;
reward->m_iBatteryN = plr->batteryN;
reward->m_iBatteryW = plr->batteryW;
reward->iFatigue = 100; // prevents warning message
reward->iFatigue_Level = 1;
reward->iItemCnt = 1; // remember to update resplen if you change this
int slot = Items::findFreeSlot(plr);
// no space for drop
if (slot != -1) {
// item reward
item->sItem.iType = 9;
item->sItem.iOpt = 1;
item->sItem.iID = type->dropCrateId;
item->iSlotNum = slot;
item->eIL = 1; // Inventory Location. 1 means player inventory.
// update player
plr->Inven[slot] = item->sItem;
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
}
}
if (egg->summoned)
NPCManager::destroyNPC(eggRef.id);
else {
Chunking::removeEntityFromChunks(Chunking::getViewableChunks(egg->chunkPos), eggRef);
egg->dead = true;
egg->deadUntil = getTime() + (time_t)type->regen * 1000;
egg->hp = 0;
}
}
void Eggs::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SHINY_PICKUP, eggPickup);
REGISTER_SHARD_TIMER(eggStep, 1000);
}

18
src/Eggs.hpp Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include "core/Core.hpp"
struct EggType {
int dropCrateId;
int effectId;
int duration;
int regen;
};
namespace Eggs {
extern std::unordered_map<int, EggType> EggTypes;
void init();
void eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration);
}

345
src/Email.cpp Normal file
View File

@ -0,0 +1,345 @@
#include "Email.hpp"
#include "core/Core.hpp"
#include "db/Database.hpp"
#include "servers/CNShardServer.hpp"
#include "servers/Monitor.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Chat.hpp"
using namespace Email;
// New email notification
static void emailUpdateCheck(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_NEW_EMAIL, resp);
resp.iNewEmailCnt = Database::getUnreadEmailCount(PlayerManager::getPlayer(sock)->iID);
sock->sendPacket(resp, P_FE2CL_REP_PC_NEW_EMAIL);
}
// Retrieve page of emails
static void emailReceivePageList(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_PC_RECV_EMAIL_PAGE_LIST*)data->buf;
INITSTRUCT(sP_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_SUCC, resp);
resp.iPageNum = pkt->iPageNum;
std::vector<Database::EmailData> emails = Database::getEmails(PlayerManager::getPlayer(sock)->iID, pkt->iPageNum);
for (int i = 0; i < emails.size(); i++) {
// convert each email and load them into the packet
Database::EmailData* email = &emails.at(i);
sEmailInfo* emailInfo = new sEmailInfo();
emailInfo->iEmailIndex = email->MsgIndex;
emailInfo->iReadFlag = email->ReadFlag;
emailInfo->iItemCandyFlag = email->ItemFlag;
emailInfo->iFromPCUID = email->SenderId;
emailInfo->SendTime = timeStampToStruct(email->SendTime);
emailInfo->DeleteTime = timeStampToStruct(email->DeleteTime);
U8toU16(email->SenderFirstName, emailInfo->szFirstName, sizeof(emailInfo->szFirstName));
U8toU16(email->SenderLastName, emailInfo->szLastName, sizeof(emailInfo->szLastName));
U8toU16(email->SubjectLine, emailInfo->szSubject, sizeof(emailInfo->szSubject));
resp.aEmailInfo[i] = *emailInfo;
}
sock->sendPacket(resp, P_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_SUCC);
}
// Read individual email
static void emailRead(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_PC_READ_EMAIL*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Database::EmailData email = Database::getEmail(plr->iID, pkt->iEmailIndex);
sItemBase* attachments = Database::getEmailAttachments(plr->iID, pkt->iEmailIndex);
email.ReadFlag = 1; // mark as read
Database::updateEmailContent(&email);
INITSTRUCT(sP_FE2CL_REP_PC_READ_EMAIL_SUCC, resp);
resp.iEmailIndex = pkt->iEmailIndex;
resp.iCash = email.Taros;
for (int i = 0; i < 4; i++) {
resp.aItem[i] = attachments[i];
}
U8toU16(email.MsgBody, (char16_t*)resp.szContent, sizeof(resp.szContent));
sock->sendPacket(resp, P_FE2CL_REP_PC_READ_EMAIL_SUCC);
}
// Retrieve attached taros from email
static void emailReceiveTaros(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_PC_RECV_EMAIL_CANDY*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Database::EmailData email = Database::getEmail(plr->iID, pkt->iEmailIndex);
// money transfer
plr->money += email.Taros;
email.Taros = 0;
// update Taros in email
Database::updateEmailContent(&email);
INITSTRUCT(sP_FE2CL_REP_PC_RECV_EMAIL_CANDY_SUCC, resp);
resp.iCandy = plr->money;
resp.iEmailIndex = pkt->iEmailIndex;
sock->sendPacket(resp, P_FE2CL_REP_PC_RECV_EMAIL_CANDY_SUCC);
}
// Retrieve individual attached item from email
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->iEmailItemSlot < 1 || pkt->iEmailItemSlot > 4)
return; // sanity check
// get email item from db and delete it
sItemBase* attachments = Database::getEmailAttachments(plr->iID, pkt->iEmailIndex);
sItemBase itemFrom = attachments[pkt->iEmailItemSlot - 1];
Database::deleteEmailAttachments(plr->iID, pkt->iEmailIndex, pkt->iEmailItemSlot);
// move item to player inventory
sItemBase& itemTo = plr->Inven[pkt->iSlotNum];
itemTo.iID = itemFrom.iID;
itemTo.iOpt = itemFrom.iOpt;
itemTo.iTimeLimit = itemFrom.iTimeLimit;
itemTo.iType = itemFrom.iType;
INITSTRUCT(sP_FE2CL_REP_PC_RECV_EMAIL_ITEM_SUCC, resp);
resp.iEmailIndex = pkt->iEmailIndex;
resp.iEmailItemSlot = pkt->iEmailItemSlot;
resp.iSlotNum = pkt->iSlotNum;
sock->sendPacket(resp, P_FE2CL_REP_PC_RECV_EMAIL_ITEM_SUCC);
// update inventory
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp2);
resp2.eIL = 1;
resp2.iSlotNum = resp.iSlotNum;
resp2.Item = itemTo;
sock->sendPacket(resp2, P_FE2CL_REP_PC_GIVE_ITEM_SUCC);
}
// Retrieve all attached items from email
static void emailReceiveItemAll(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_PC_RECV_EMAIL_ITEM_ALL*)data->buf;
// move items to player inventory
Player* plr = PlayerManager::getPlayer(sock);
sItemBase* itemsFrom = Database::getEmailAttachments(plr->iID, pkt->iEmailIndex);
for (int i = 0; i < 4; i++) {
int slot = Items::findFreeSlot(plr);
if (slot < 0 || slot >= AINVEN_COUNT) {
INITSTRUCT(sP_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_FAIL, failResp);
failResp.iEmailIndex = pkt->iEmailIndex;
failResp.iErrorCode = 0; // ???
break; // sanity check; should never happen
}
// copy data over
sItemBase itemFrom = itemsFrom[i];
sItemBase& itemTo = plr->Inven[slot];
itemTo.iID = itemFrom.iID;
itemTo.iOpt = itemFrom.iOpt;
itemTo.iTimeLimit = itemFrom.iTimeLimit;
itemTo.iType = itemFrom.iType;
// update inventory
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp2);
resp2.eIL = 1;
resp2.iSlotNum = slot;
resp2.Item = itemTo;
sock->sendPacket(resp2, P_FE2CL_REP_PC_GIVE_ITEM_SUCC);
}
// delete all items from db
Database::deleteEmailAttachments(plr->iID, pkt->iEmailIndex, -1);
INITSTRUCT(sP_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_SUCC, resp);
resp.iEmailIndex = pkt->iEmailIndex;
sock->sendPacket(resp, P_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_SUCC);
}
// Delete an email
static void emailDelete(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_PC_DELETE_EMAIL*)data->buf;
Database::deleteEmails(PlayerManager::getPlayer(sock)->iID, pkt->iEmailIndexArray);
INITSTRUCT(sP_FE2CL_REP_PC_DELETE_EMAIL_SUCC, resp);
for (int i = 0; i < 5; i++) {
resp.iEmailIndexArray[i] = pkt->iEmailIndexArray[i]; // i'm scared of memcpy
}
sock->sendPacket(resp, P_FE2CL_REP_PC_DELETE_EMAIL_SUCC);
}
// Send an email
static void emailSend(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_PC_SEND_EMAIL*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
// sanity checks
bool invalid = false;
int itemCount = 0;
std::set<int> seen;
for (int i = 0; i < 4; i++) {
int slot = pkt->aItem[i].iSlotNum;
if (slot < 0 || slot >= AINVEN_COUNT) {
invalid = true;
break;
}
sItemBase* item = &pkt->aItem[i].ItemInven;
sItemBase* real = &plr->Inven[slot];
if (item->iID == 0)
continue;
// was the same item added multiple times?
if (seen.count(slot) > 0) {
invalid = true;
break;
}
seen.insert(slot);
itemCount++;
if (item->iType != real->iType || item->iID != real->iID
|| item->iOpt <= 0 || item->iOpt > real->iOpt) {
invalid = true;
break;
}
}
if (pkt->iCash < 0 || pkt->iCash > plr->money + 50 + 20 * itemCount || invalid) {
INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_FAIL, errResp);
errResp.iErrorCode = 1;
errResp.iTo_PCUID = pkt->iTo_PCUID;
sock->sendPacket(errResp, P_FE2CL_REP_PC_SEND_EMAIL_FAIL);
return;
}
INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_SUCC, resp);
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);
resp.iErrorCode = 9; // error code 9 tells the player they can't send attachments across time
resp.iTo_PCUID = pkt->iTo_PCUID;
sock->sendPacket(resp, P_FE2CL_REP_PC_SEND_EMAIL_FAIL);
return;
}
}
// handle items
std::vector<sItemBase> attachments;
std::vector<int> attSlots;
for (int i = 0; i < 4; i++) {
sEmailItemInfoFromCL attachment = pkt->aItem[i];
// skip empty slots
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);
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
plr->money -= cost;
Database::EmailData email = {
(int)pkt->iTo_PCUID, // PlayerId
Database::getNextEmailIndex(pkt->iTo_PCUID), // MsgIndex
0, // ReadFlag (unread)
(pkt->iCash > 0 || attachments.size() > 0) ? 1 : 0, // ItemFlag
plr->iID, // SenderID
AUTOU16TOU8(plr->PCStyle.szFirstName), // SenderFirstName
AUTOU16TOU8(plr->PCStyle.szLastName), // SenderLastName
Chat::sanitizeText(AUTOU16TOU8(pkt->szSubject)), // SubjectLine
Chat::sanitizeText(AUTOU16TOU8(pkt->szContent), true), // MsgBody
pkt->iCash, // Taros
(uint64_t)getTimestamp(), // SendTime
0 // DeleteTime (unimplemented)
};
if (!Database::sendEmail(&email, attachments, plr)) {
plr->money += cost; // give money back
// give items back
while (!attachments.empty()) {
sItemBase attachment = attachments.back();
plr->Inven[attSlots.back()] = attachment;
attachments.pop_back();
attSlots.pop_back();
}
// send error message
INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_FAIL, errResp);
errResp.iErrorCode = 1;
errResp.iTo_PCUID = pkt->iTo_PCUID;
sock->sendPacket(errResp, P_FE2CL_REP_PC_SEND_EMAIL_FAIL);
return;
}
// HACK: use set value packet to force GUI taros update
INITSTRUCT(sP_FE2CL_GM_REP_PC_SET_VALUE, tarosResp);
tarosResp.iPC_ID = plr->iID;
tarosResp.iSetValueType = 5;
tarosResp.iSetValue = plr->money;
sock->sendPacket(tarosResp, P_FE2CL_GM_REP_PC_SET_VALUE);
resp.iCandy = plr->money;
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;
Monitor::emails.push_back(logEmail);
// notification to recipient if online
CNSocket* recipient = PlayerManager::getSockFromID(pkt->iTo_PCUID);
if (recipient != nullptr)
{
emailUpdateCheck(recipient, nullptr);
}
}
void Email::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_EMAIL_UPDATE_CHECK, emailUpdateCheck);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_PAGE_LIST, emailReceivePageList);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_READ_EMAIL, emailRead);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_CANDY, emailReceiveTaros);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_ITEM, emailReceiveItemSingle);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_ITEM_ALL, emailReceiveItemAll);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_DELETE_EMAIL, emailDelete);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_SEND_EMAIL, emailSend);
}

8
src/Email.hpp Normal file
View File

@ -0,0 +1,8 @@
#pragma once
#include <vector>
#include <string>
namespace Email {
void init();
}

153
src/Entities.cpp Normal file
View File

@ -0,0 +1,153 @@
#include "Entities.hpp"
#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) {
kind = EntityKind::PLAYER;
sock = s;
}
EntityRef::EntityRef(int32_t i) {
id = i;
kind = EntityKind::INVALID;
if (NPCManager::NPCs.find(id) != NPCManager::NPCs.end())
kind = NPCManager::NPCs[id]->kind;
}
bool EntityRef::isValid() const {
if (kind == EntityKind::PLAYER)
return PlayerManager::players.find(sock) != PlayerManager::players.end();
return NPCManager::NPCs.find(id) != NPCManager::NPCs.end();
}
Entity *EntityRef::getEntity() const {
assert(isValid());
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 = getAppearanceData();
sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
}
void Bus::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_TRANSPORTATION_ENTER, pkt);
// TODO: Potentially decouple this from BaseNPC?
pkt.AppearanceData = getTransportationAppearanceData();
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_ENTER);
}
void Egg::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_SHINY_ENTER, pkt);
// TODO: Potentially decouple this from BaseNPC?
pkt.ShinyAppearanceData = getShinyAppearanceData();
sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER);
}
sTransportationAppearanceData Bus::getTransportationAppearanceData() {
return sTransportationAppearanceData {
3, id, type,
x, y, z
};
}
sShinyAppearanceData Egg::getShinyAppearanceData() {
return sShinyAppearanceData {
id, type, 0, // client doesn't care about map num
x, y, z
};
}
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 = getAppearanceData();
sock->sendPacket(pkt, P_FE2CL_PC_NEW);
}
/*
* Entity leaving view.
*/
void BaseNPC::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
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 = id;
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_EXIT);
}
void Egg::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_SHINY_EXIT, pkt);
pkt.iShinyID = id;
sock->sendPacket(pkt, P_FE2CL_SHINY_EXIT);
}
void Player::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_PC_EXIT, pkt);
pkt.iID = iID;
sock->sendPacket(pkt, P_FE2CL_PC_EXIT);
}

179
src/Entities.hpp Normal file
View File

@ -0,0 +1,179 @@
#pragma once
#include "core/Core.hpp"
#include "EntityRef.hpp"
#include "Buffs.hpp"
#include "Chunking.hpp"
#include "Groups.hpp"
#include <set>
#include <map>
#include <functional>
enum class AIState {
INACTIVE,
ROAMING,
COMBAT,
RETREAT,
DEAD
};
struct Entity {
EntityKind kind = EntityKind::INVALID;
int x = 0, y = 0, z = 0;
uint64_t instanceID = 0;
ChunkPos chunkPos = {};
std::set<Chunk*> viewableChunks = {};
// destructor must be virtual, apparently
virtual ~Entity() {}
virtual bool isExtant() { return true; }
// stubs
virtual void enterIntoViewOf(CNSocket *sock) = 0;
virtual void disappearFromViewOf(CNSocket *sock) = 0;
};
/*
* Interfaces
*/
class ICombatant {
public:
ICombatant() {}
virtual ~ICombatant() {}
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;
};
/*
* Subclasses
*/
class BaseNPC : public Entity {
public:
int id;
int type;
int hp;
int angle;
bool loopingPath = false;
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, 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
std::map<AIState, void (*)(CombatNPC*, time_t)> stateHandlers;
std::map<AIState, void (*)(CombatNPC*, EntityRef)> transitionHandlers;
std::unordered_map<int, Buff*> buffs = {};
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 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
struct Egg : public BaseNPC {
bool summoned = false;
bool dead = false;
time_t deadUntil;
Egg(uint64_t iID, int t, int32_t id, bool summon)
: BaseNPC(0, iID, t, id) {
summoned = summon;
kind = EntityKind::EGG;
}
virtual bool isExtant() override { return !dead; }
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
sShinyAppearanceData getShinyAppearanceData();
};
struct Bus : public BaseNPC {
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;
virtual void disappearFromViewOf(CNSocket *sock) override;
sTransportationAppearanceData getTransportationAppearanceData();
};

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

329
src/Groups.cpp Normal file
View File

@ -0,0 +1,329 @@
#include "Groups.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Entities.hpp"
/*
* NOTE: Variadic response packets that list group members are technically
* double-variadic, as they have two count members with trailing struct counts,
* and are thus incompatible with the generic sendPacket() wrapper.
* That means we still have to (carefully) use validOutVarPacket() in this
* source file.
*/
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_BODY_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_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_BODY_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_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;
// fail if the group is full or the other player is already in a group
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;
}
CNSocket* otherSock = PlayerManager::getSockFromID(recv->iID_To);
if (otherSock == nullptr)
return;
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE, resp);
resp.iHostID = plr->iID;
otherSock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE, sizeof(sP_FE2CL_PC_GROUP_INVITE));
}
static void refuseGroup(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_GROUP_INVITE_REFUSE* recv = (sP_CL2FE_REQ_PC_GROUP_INVITE_REFUSE*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(recv->iID_From);
if (otherSock == nullptr)
return;
Player* plr = PlayerManager::getPlayer(sock);
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE_REFUSE, resp);
resp.iID_To = plr->iID;
otherSock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_REFUSE, sizeof(sP_FE2CL_PC_GROUP_INVITE_REFUSE));
}
static void joinGroup(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_GROUP_JOIN* recv = (sP_CL2FE_REQ_PC_GROUP_JOIN*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(recv->iID_From);
if (otherPlr == nullptr)
return; // disconnect or something
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->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 (otherPlr->group == nullptr) {
// create group
EntityRef otherPlrRef = PlayerManager::getSockFromID(recv->iID_From);
otherPlr->group = new Group(otherPlrRef);
}
addToGroup(otherPlr->group, sock);
}
static void leaveGroup(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
groupKick(plr->group, sock);
}
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);
}
}
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_BODY_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
sP_FE2CL_PC_GROUP_MEMBER_INFO* pkt = (sP_FE2CL_PC_GROUP_MEMBER_INFO*)respbuf;
pkt->iID = plr->iID;
pkt->iMemberPCCnt = (int32_t)pcCount;
pkt->iMemberNPCCnt = (int32_t)npcCount;
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));
}
}
void Groups::groupKick(Group* group, EntityRef ref) {
if (group == nullptr)
return;
// if you are the group leader, destroy your own group and kick everybody
if (group->members[0] == ref) {
disbandGroup(group);
return;
}
removeFromGroup(group, ref);
}
void Groups::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GROUP_INVITE, requestGroup);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GROUP_INVITE_REFUSE, refuseGroup);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GROUP_JOIN, joinGroup);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GROUP_LEAVE, leaveGroup);
}

37
src/Groups.hpp Normal file
View File

@ -0,0 +1,37 @@
#pragma once
#include "EntityRef.hpp"
#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(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);
}

865
src/Items.cpp Normal file
View File

@ -0,0 +1,865 @@
#include "Items.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "PlayerManager.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include "Eggs.hpp"
#include "MobAI.hpp"
#include "Missions.hpp"
#include "Buffs.hpp"
#include <string.h> // for memset()
#include <assert.h>
#include <numeric>
using namespace Items;
std::map<std::pair<int32_t, int32_t>, Items::Item> Items::ItemData;
std::map<int32_t, CrocPotEntry> Items::CrocPotTable;
std::map<int32_t, std::vector<int32_t>> Items::RarityWeights;
std::map<int32_t, Crate> Items::Crates;
std::map<int32_t, ItemReference> Items::ItemReferences;
std::map<std::string, std::vector<std::pair<int32_t, int32_t>>> Items::CodeItems;
std::map<int32_t, CrateDropChance> Items::CrateDropChances;
std::map<int32_t, std::vector<int32_t>> Items::CrateDropTypes;
std::map<int32_t, MiscDropChance> Items::MiscDropChances;
std::map<int32_t, MiscDropType> Items::MiscDropTypes;
std::map<int32_t, MobDrop> Items::MobDrops;
std::map<int32_t, int32_t> Items::EventToDropMap;
std::map<int32_t, int32_t> Items::MobToDropMap;
std::map<int32_t, ItemSet> Items::ItemSets;
#ifdef ACADEMY
std::map<int32_t, int32_t> Items::NanoCapsules; // crate id -> nano id
static void nanoCapsuleHandler(CNSocket* sock, int slot, sItemBase *chest) {
Player* plr = PlayerManager::getPlayer(sock);
int32_t nanoId = NanoCapsules[chest->iID];
// chest opening acknowledgement packet
INITSTRUCT(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, resp);
resp.iSlotNum = slot;
// in order to remove capsule form inventory, we have to send item reward packet with empty item
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
assert(resplen < CN_PACKET_BODY_SIZE);
// we know it's only one trailing struct, so we can skip full validation
uint8_t respbuf[resplen]; // not a variable length array, don't worry
sP_FE2CL_REP_REWARD_ITEM* reward = (sP_FE2CL_REP_REWARD_ITEM*)respbuf;
sItemReward* item = (sItemReward*)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
// don't forget to zero the buffer!
memset(respbuf, 0, resplen);
// maintain stats
reward->m_iCandy = plr->money;
reward->m_iFusionMatter = plr->fusionmatter;
reward->iFatigue = 100; // prevents warning message
reward->iFatigue_Level = 1;
reward->iItemCnt = 1; // remember to update resplen if you change this
reward->m_iBatteryN = plr->batteryN;
reward->m_iBatteryW = plr->batteryW;
item->iSlotNum = slot;
item->eIL = 1;
// update player serverside
plr->Inven[slot] = item->sItem;
// transmit item
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
// transmit chest opening acknowledgement packet
sock->sendPacket(resp, P_FE2CL_REP_ITEM_CHEST_OPEN_SUCC);
// check if player doesn't already have this nano
if (plr->Nanos[nanoId].iID != 0) {
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
msg.iDuringTime = 4;
std::string text = "You have already acquired this nano!";
U8toU16(text, msg.szAnnounceMsg, sizeof(text));
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
return;
}
Nanos::addNano(sock, nanoId, -1, false);
}
#endif
static int choice(const std::vector<int>& weights, int rolled) {
int total = std::accumulate(weights.begin(), weights.end(), 0);
int randValue = rolled % total;
int currentIndex = -1;
do {
currentIndex++;
randValue -= weights[currentIndex];
} while (randValue >= 0);
return currentIndex;
}
static int getRarity(int crateId, int itemSetId) {
Crate& crate = Items::Crates[crateId];
// find rarity ratio
if (Items::RarityWeights.find(crate.rarityWeightId) == Items::RarityWeights.end()) {
std::cout << "[WARN] Rarity Weight " << crate.rarityWeightId << " not found!" << std::endl;
return -1;
}
std::vector<int>& rarityWeights = Items::RarityWeights[crate.rarityWeightId];
ItemSet& itemSet = Items::ItemSets[itemSetId];
/*
* First we have to check if specified item set contains items with all specified rarities,
* and if not eliminate them from the draw
* it is simpler to do here than to fix individually in the file
*/
// remember that rarities start from 1!
std::set<int> rarityIndices;
for (int itemReferenceId : itemSet.itemReferenceIds) {
if (Items::ItemReferences.find(itemReferenceId) == Items::ItemReferences.end())
continue;
// alter rarity
int itemRarity = (itemSet.alterRarityMap.find(itemReferenceId) == itemSet.alterRarityMap.end())
? Items::ItemReferences[itemReferenceId].rarity
: itemSet.alterRarityMap[itemReferenceId];
rarityIndices.insert(itemRarity - 1);
// shortcut
if (rarityIndices.size() == rarityWeights.size())
break;
}
if (rarityIndices.empty()) {
std::cout << "[WARN] Item Set " << crate.itemSetId << " has no valid items assigned?!" << std::endl;
return -1;
}
// retain the weights of rarities that actually exist in the itemset
std::vector<int> relevantWeights(rarityWeights.size(), 0);
for (int index : rarityIndices) {
// check for out of bounds and rarity 0 items
if (index >= 0 && index < rarityWeights.size())
relevantWeights[index] = rarityWeights[index];
}
// now return a random rarity number (starting from 1)
// if relevantWeights is empty or all zeros, we default to giving a common (1) item
// rarity 0 items will appear in the drop pool regardless of this roll
return Rand::randWeighted(relevantWeights) + 1;
}
static int getCrateItem(sItemBase* result, int itemSetId, int rarity, int playerGender) {
ItemSet& itemSet = Items::ItemSets[itemSetId];
// collect valid items that match the rarity and gender (if not ignored)
std::vector<std::pair<int, ItemReference*>> validItems;
for (int itemReferenceId : itemSet.itemReferenceIds) {
if (Items::ItemReferences.find(itemReferenceId) == Items::ItemReferences.end()) {
std::cout << "[WARN] Item reference " << itemReferenceId << " in item set type "
<< itemSetId << " was not found, skipping..." << std::endl;
continue;
}
ItemReference* item = &Items::ItemReferences[itemReferenceId];
// alter rarity
int itemRarity = (itemSet.alterRarityMap.find(itemReferenceId) == itemSet.alterRarityMap.end())
? item->rarity
: itemSet.alterRarityMap[itemReferenceId];
// if rarity doesn't match the selected one, exclude item
// rarity 0 bypasses this step for an individual item
if (!itemSet.ignoreRarity && itemRarity != 0 && itemRarity != rarity)
continue;
// alter rarity
int itemGender = (itemSet.alterGenderMap.find(itemReferenceId) == itemSet.alterGenderMap.end())
? item->gender
: itemSet.alterGenderMap[itemReferenceId];
// if gender is incorrect, exclude item
// gender 0 bypasses this step for an individual item
if (!itemSet.ignoreGender && itemGender != 0 && itemGender != playerGender)
continue;
validItems.push_back(std::make_pair(itemReferenceId, item));
}
if (validItems.empty()) {
std::cout << "[WARN] Set ID " << itemSetId << " Rarity " << rarity << " contains no valid items" << std::endl;
return -1;
}
// initialize all weights as the default weight for all item slots
std::vector<int> itemWeights(validItems.size(), itemSet.defaultItemWeight);
if (!itemSet.alterItemWeightMap.empty()) {
for (int i = 0; i < validItems.size(); i++) {
int itemReferenceId = validItems[i].first;
if (itemSet.alterItemWeightMap.find(itemReferenceId) == itemSet.alterItemWeightMap.end())
continue;
int weight = itemSet.alterItemWeightMap[itemReferenceId];
// allow 0 weights for convenience
if (weight > -1)
itemWeights[i] = weight;
}
}
int chosenIndex = Rand::randWeighted(itemWeights);
ItemReference* item = validItems[chosenIndex].second;
result->iID = item->itemId;
result->iType = item->type;
result->iOpt = 1;
return 0;
}
static int getValidCrateId(int crateId) {
// find the crate
if (Items::Crates.find(crateId) == Items::Crates.end()) {
std::cout << "[WARN] Crate " << crateId << " not found!" << std::endl;
return -1;
}
return crateId;
}
static int getValidItemSetId(int crateId) {
Crate& crate = Items::Crates[crateId];
// find item set type
if (Items::ItemSets.find(crate.itemSetId) == Items::ItemSets.end()) {
std::cout << "[WARN] Crate " << crateId << " was assigned item set "
<< crate.itemSetId << " which is invalid!" << std::endl;
return -1;
}
return crate.itemSetId;
}
static void itemMoveHandler(CNSocket* sock, CNPacketData* data) {
auto itemmove = (sP_CL2FE_REQ_ITEM_MOVE*)data->buf;
INITSTRUCT(sP_FE2CL_PC_ITEM_MOVE_SUCC, resp);
Player* plr = PlayerManager::getPlayer(sock);
// sanity check
if (itemmove->iToSlotNum < 0 || itemmove->iFromSlotNum < 0)
return;
// NOTE: sending a no-op, "move in-place" packet is not necessary
if (plr->isTrading) {
std::cout << "[WARN] Player attempted to move item while trading" << std::endl;
return;
}
// get the fromItem
sItemBase *fromItem;
switch ((SlotType)itemmove->eFrom) {
case SlotType::EQUIP:
if (itemmove->iFromSlotNum >= AEQUIP_COUNT)
return;
fromItem = &plr->Equip[itemmove->iFromSlotNum];
break;
case SlotType::INVENTORY:
if (itemmove->iFromSlotNum >= AINVEN_COUNT)
return;
fromItem = &plr->Inven[itemmove->iFromSlotNum];
break;
case SlotType::BANK:
if (itemmove->iFromSlotNum >= ABANK_COUNT)
return;
fromItem = &plr->Bank[itemmove->iFromSlotNum];
break;
default:
std::cout << "[WARN] MoveItem submitted unknown Item Type?! " << itemmove->eFrom << std::endl;
return;
}
// get the toItem
sItemBase* toItem;
switch ((SlotType)itemmove->eTo) {
case SlotType::EQUIP:
if (itemmove->iToSlotNum >= AEQUIP_COUNT)
return;
toItem = &plr->Equip[itemmove->iToSlotNum];
break;
case SlotType::INVENTORY:
if (itemmove->iToSlotNum >= AINVEN_COUNT)
return;
toItem = &plr->Inven[itemmove->iToSlotNum];
break;
case SlotType::BANK:
if (itemmove->iToSlotNum >= ABANK_COUNT)
return;
toItem = &plr->Bank[itemmove->iToSlotNum];
break;
default:
std::cout << "[WARN] MoveItem submitted unknown Item Type?! " << itemmove->eTo << std::endl;
return;
}
// if equipping an item, validate that it's of the correct type for the slot
if ((SlotType)itemmove->eTo == SlotType::EQUIP) {
if (fromItem->iType == 10 && itemmove->iToSlotNum != 8)
return; // vehicle in wrong slot
else if (fromItem->iType != 10
&& !(fromItem->iType == 0 && itemmove->iToSlotNum == 7)
&& fromItem->iType != itemmove->iToSlotNum)
return; // something other than a vehicle or a weapon in a non-matching slot
else if (itemmove->iToSlotNum >= AEQUIP_COUNT) // TODO: reject slots >= 9?
return; // invalid slot
}
// save items to response
resp.eTo = itemmove->eFrom;
resp.eFrom = itemmove->eTo;
resp.ToSlotItem = *toItem;
resp.FromSlotItem = *fromItem;
// swap/stack items in session
Item* itemDat = getItemData(toItem->iID, toItem->iType);
Item* itemDatFrom = getItemData(fromItem->iID, fromItem->iType);
if (itemDat != nullptr && itemDatFrom != nullptr && itemDat->stackSize > 1 && itemDat == itemDatFrom && fromItem->iOpt < itemDat->stackSize && toItem->iOpt < itemDat->stackSize) {
// items are stackable, identical, and not maxed, so run stacking logic
toItem->iOpt += fromItem->iOpt; // sum counts
fromItem->iOpt = 0; // deplete from item
if (toItem->iOpt > itemDat->stackSize) {
// handle overflow
fromItem->iOpt += (toItem->iOpt - itemDat->stackSize); // add overflow to fromItem
toItem->iOpt = itemDat->stackSize; // set toItem count to max
}
if (fromItem->iOpt == 0) { // from item count depleted
// delete item
fromItem->iID = 0;
fromItem->iType = 0;
fromItem->iTimeLimit = 0;
}
resp.iFromSlotNum = itemmove->iFromSlotNum;
resp.iToSlotNum = itemmove->iToSlotNum;
resp.FromSlotItem = *fromItem;
resp.ToSlotItem = *toItem;
} else {
// items not stackable; just swap them
sItemBase temp = *toItem;
*toItem = *fromItem;
*fromItem = temp;
resp.iFromSlotNum = itemmove->iToSlotNum;
resp.iToSlotNum = itemmove->iFromSlotNum;
}
// send equip change to viewable players
if (itemmove->eFrom == (int)SlotType::EQUIP || itemmove->eTo == (int)SlotType::EQUIP) {
INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, equipChange);
equipChange.iPC_ID = plr->iID;
if (itemmove->eTo == (int)SlotType::EQUIP) {
equipChange.iEquipSlotNum = itemmove->iToSlotNum;
equipChange.EquipSlotItem = resp.FromSlotItem;
} else {
equipChange.iEquipSlotNum = itemmove->iFromSlotNum;
equipChange.EquipSlotItem = resp.ToSlotItem;
}
// unequip vehicle if equip slot 8 is 0
if (plr->Equip[8].iID == 0 && plr->iPCState & 8) {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response);
sock->sendPacket(response, P_FE2CL_PC_VEHICLE_OFF_SUCC);
// send to other players
plr->iPCState &= ~8;
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, response2);
response2.iPC_ID = plr->iID;
response2.iState = plr->iPCState;
PlayerManager::sendToViewable(sock, response2, P_FE2CL_PC_STATE_CHANGE);
}
// send equip event to other players
PlayerManager::sendToViewable(sock, equipChange, P_FE2CL_PC_EQUIP_CHANGE);
// set equipment stats serverside
setItemStats(plr);
}
// send response
sock->sendPacket(resp, P_FE2CL_PC_ITEM_MOVE_SUCC);
}
static void itemDeleteHandler(CNSocket* sock, CNPacketData* data) {
auto itemdel = (sP_CL2FE_REQ_PC_ITEM_DELETE*)data->buf;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_DELETE_SUCC, resp);
Player* plr = PlayerManager::getPlayer(sock);
if (itemdel->iSlotNum < 0 || itemdel->iSlotNum >= AINVEN_COUNT)
return; // sanity check
resp.eIL = itemdel->eIL;
resp.iSlotNum = itemdel->iSlotNum;
// so, im not sure what this eIL thing does since you always delete items in inventory and not equips
plr->Inven[itemdel->iSlotNum].iID = 0;
plr->Inven[itemdel->iSlotNum].iType = 0;
plr->Inven[itemdel->iSlotNum].iOpt = 0;
sock->sendPacket(resp, P_FE2CL_REP_PC_ITEM_DELETE_SUCC);
}
static void itemUseHandler(CNSocket* sock, CNPacketData* data) {
auto request = (sP_CL2FE_REQ_ITEM_USE*)data->buf;
Player* player = PlayerManager::getPlayer(sock);
if (request->iSlotNum < 0 || request->iSlotNum >= AINVEN_COUNT)
return; // sanity check
// gumball can only be used from inventory, so we ignore eIL
sItemBase gumball = player->Inven[request->iSlotNum];
sNano nano = player->Nanos[player->equippedNanos[request->iNanoSlot]];
// sanity check, check if gumball exists
if (!(gumball.iOpt > 0 && gumball.iType == 7 && gumball.iID>=119 && gumball.iID<=121)) {
std::cout << "[WARN] Gumball not found" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response);
sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL);
return;
}
// sanity check, check if gumball type matches nano style
int nanoStyle = Nanos::nanoStyle(nano.iID);
if (!((gumball.iID == 119 && nanoStyle == 0) ||
( gumball.iID == 120 && nanoStyle == 1) ||
( gumball.iID == 121 && nanoStyle == 2))) {
std::cout << "[WARN] Gumball type doesn't match nano type" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_USE_FAIL, response);
sock->sendPacket(response, P_FE2CL_REP_PC_ITEM_USE_FAIL);
return;
}
gumball.iOpt -= 1;
if (gumball.iOpt == 0)
gumball = {};
size_t resplen = sizeof(sP_FE2CL_REP_PC_ITEM_USE_SUCC) + sizeof(sSkillResult_Buff);
// validate response packet
if (!validOutVarPacket(sizeof(sP_FE2CL_REP_PC_ITEM_USE_SUCC), 1, sizeof(sSkillResult_Buff))) {
std::cout << "[WARN] bad sP_FE2CL_REP_PC_ITEM_USE_SUCC packet size" << std::endl;
return;
}
if (gumball.iOpt == 0)
gumball = {};
uint8_t respbuf[CN_PACKET_BODY_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
sP_FE2CL_REP_PC_ITEM_USE_SUCC *resp = (sP_FE2CL_REP_PC_ITEM_USE_SUCC*)respbuf;
sSkillResult_Buff *respdata = (sSkillResult_Buff*)(respbuf+sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC));
resp->iPC_ID = player->iID;
resp->eIL = 1;
resp->iSlotNum = request->iSlotNum;
resp->RemainItem = gumball;
resp->iTargetCnt = 1;
resp->eST = (int32_t)SkillType::NANOSTIMPAK;
resp->iSkillID = 144;
int eCSB = ECSB_STIMPAKSLOT1 + request->iNanoSlot;
respdata->eCT = 1;
respdata->iID = player->iID;
respdata->iConditionBitFlag = CSB_FROM_ECSB(eCSB);
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;
}
static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
// just send bank inventory
INITSTRUCT(sP_FE2CL_REP_PC_BANK_OPEN_SUCC, resp);
for (int i = 0; i < ABANK_COUNT; i++) {
resp.aBank[i] = plr->Bank[i];
}
resp.iExtraBank = 1;
sock->sendPacket(resp, P_FE2CL_REP_PC_BANK_OPEN_SUCC);
}
static void chestOpenHandler(CNSocket *sock, CNPacketData *data) {
auto pkt = (sP_CL2FE_REQ_ITEM_CHEST_OPEN *)data->buf;
// sanity check
if (pkt->eIL != 1 || pkt->iSlotNum < 0 || pkt->iSlotNum >= AINVEN_COUNT)
return;
Player *plr = PlayerManager::getPlayer(sock);
sItemBase *chest = &plr->Inven[pkt->iSlotNum];
// we could reject the packet if the client thinks the item is different, but eh
if (chest->iType != 9) {
std::cout << "[WARN] Player tried to open a crate with incorrect iType ?!" << std::endl;
return;
}
#ifdef ACADEMY
// check if chest isn't a nano capsule
if (NanoCapsules.find(chest->iID) != NanoCapsules.end())
return nanoCapsuleHandler(sock, pkt->iSlotNum, chest);
#endif
// chest opening acknowledgement packet
INITSTRUCT(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, resp);
resp.iSlotNum = pkt->iSlotNum;
// item giving packet
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
assert(resplen < CN_PACKET_BODY_SIZE);
// we know it's only one trailing struct, so we can skip full validation
uint8_t respbuf[resplen]; // not a variable length array, don't worry
sP_FE2CL_REP_REWARD_ITEM *reward = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
// don't forget to zero the buffer!
memset(respbuf, 0, resplen);
// maintain stats
reward->m_iCandy = plr->money;
reward->m_iFusionMatter = plr->fusionmatter;
reward->iFatigue = 100; // prevents warning message
reward->iFatigue_Level = 1;
reward->iItemCnt = 1; // remember to update resplen if you change this
reward->m_iBatteryN = plr->batteryN;
reward->m_iBatteryW = plr->batteryW;
item->iSlotNum = pkt->iSlotNum;
item->eIL = 1;
int validItemSetId = -1, rarity = -1, ret = -1;
int validCrateId = getValidCrateId(chest->iID);
bool failing = (validCrateId == -1);
if (!failing)
validItemSetId = getValidItemSetId(validCrateId);
failing = (validItemSetId == -1);
if (!failing)
rarity = getRarity(validCrateId, validItemSetId);
failing = (rarity == -1);
if (!failing)
ret = getCrateItem(&item->sItem, validItemSetId, rarity, plr->PCStyle.iGender);
failing = (ret == -1);
// if we failed to open a crate, at least give the player a gumball (suggested by Jade)
if (failing) {
item->sItem.iType = 7;
item->sItem.iID = 119 + Rand::rand(3);
item->sItem.iOpt = 1;
std::cout << "[WARN] Crate open failed, giving a Gumball..." << std::endl;
}
// update player
plr->Inven[pkt->iSlotNum] = item->sItem;
// transmit item
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
// transmit chest opening acknowledgement packet
std::cout << "opening chest..." << std::endl;
sock->sendPacket(resp, P_FE2CL_REP_ITEM_CHEST_OPEN_SUCC);
}
// TODO: use this in cleaned up Items
int Items::findFreeSlot(Player *plr) {
int i;
for (i = 0; i < AINVEN_COUNT; i++)
if (plr->Inven[i].iType == 0 && plr->Inven[i].iID == 0 && plr->Inven[i].iOpt == 0)
return i;
// not found
return -1;
}
Item* Items::getItemData(int32_t id, int32_t type) {
if(ItemData.find(std::make_pair(id, type)) != ItemData.end())
return &ItemData[std::make_pair(id, type)];
return nullptr;
}
void Items::checkItemExpire(CNSocket* sock, Player* player) {
if (player->toRemoveVehicle.eIL == 0 && player->toRemoveVehicle.iSlotNum == 0)
return;
/* prepare packet
* yes, this is a varadic packet, however analyzing client behavior and code
* it only checks takes the first item sent into account
* yes, this is very stupid
* therefore, we delete all but 1 expired vehicle while loading player
* to delete the last one here so player gets a notification
*/
const size_t resplen = sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM) + sizeof(sTimeLimitItemDeleteInfo2CL);
assert(resplen < CN_PACKET_BODY_SIZE);
// we know it's only one trailing struct, so we can skip full validation
uint8_t respbuf[resplen]; // not a variable length array, don't worry
auto packet = (sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM*)respbuf;
sTimeLimitItemDeleteInfo2CL* itemData = (sTimeLimitItemDeleteInfo2CL*)(respbuf + sizeof(sP_FE2CL_PC_DELETE_TIME_LIMIT_ITEM));
memset(respbuf, 0, resplen);
packet->iItemListCount = 1;
itemData->eIL = player->toRemoveVehicle.eIL;
itemData->iSlotNum = player->toRemoveVehicle.iSlotNum;
sock->sendPacket((void*)&respbuf, P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM, resplen);
// delete serverside
if (player->toRemoveVehicle.eIL == 0)
memset(&player->Equip[8], 0, sizeof(sItemBase));
else
memset(&player->Inven[player->toRemoveVehicle.iSlotNum], 0, sizeof(sItemBase));
player->toRemoveVehicle.eIL = 0;
player->toRemoveVehicle.iSlotNum = 0;
}
void Items::setItemStats(Player* plr) {
plr->pointDamage = 8 + plr->level * 2;
plr->groupDamage = 8 + plr->level * 2;
plr->fireRate = 0;
plr->defense = 16 + plr->level * 4;
Item* itemStatsDat;
for (int i = 0; i < 4; i++) {
itemStatsDat = getItemData(plr->Equip[i].iID, plr->Equip[i].iType);
if (itemStatsDat == nullptr) {
std::cout << "[WARN] setItemStats(): getItemData() returned NULL" << std::endl;
continue;
}
plr->pointDamage += itemStatsDat->pointDamage;
plr->groupDamage += itemStatsDat->groupDamage;
plr->fireRate += itemStatsDat->fireRate;
plr->defense += itemStatsDat->defense;
}
}
// HACK: work around the invisible weapon bug
// TODO: I don't think this makes a difference at all? Check and remove, if necessary.
void Items::updateEquips(CNSocket* sock, Player* plr) {
for (int i = 0; i < 4; i++) {
INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, resp);
resp.iPC_ID = plr->iID;
resp.iEquipSlotNum = i;
resp.EquipSlotItem = plr->Equip[i];
PlayerManager::sendToViewable(sock, resp, P_FE2CL_PC_EQUIP_CHANGE);
}
}
static void getMobDrop(sItemBase* reward, const std::vector<int>& weights, const std::vector<int>& crateIds, int rolled) {
int chosenIndex = choice(weights, rolled);
reward->iType = 9;
reward->iOpt = 1;
reward->iID = crateIds[chosenIndex];
}
static void giveSingleDrop(CNSocket *sock, Mob* mob, int mobDropId, const DropRoll& rolled) {
Player *plr = PlayerManager::getPlayer(sock);
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
assert(resplen < CN_PACKET_BODY_SIZE);
// we know it's only one trailing struct, so we can skip full validation
uint8_t respbuf[resplen]; // not a variable length array, don't worry
sP_FE2CL_REP_REWARD_ITEM *reward = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
// don't forget to zero the buffer!
memset(respbuf, 0, resplen);
// sanity check
if (Items::MobDrops.find(mobDropId) == Items::MobDrops.end()) {
std::cout << "[WARN] Drop Type " << mobDropId << " was not found" << std::endl;
return;
}
// find correct mob drop
MobDrop& drop = Items::MobDrops[mobDropId];
// use the keys to fetch data from other maps
// sanity check
if (Items::CrateDropChances.find(drop.crateDropChanceId) == Items::CrateDropChances.end()) {
std::cout << "[WARN] Crate Drop Chance Object " << drop.crateDropChanceId << " was not found" << std::endl;
return;
}
CrateDropChance& crateDropChance = Items::CrateDropChances[drop.crateDropChanceId];
// sanity check
if (Items::CrateDropTypes.find(drop.crateDropTypeId) == Items::CrateDropTypes.end()) {
std::cout << "[WARN] Crate Drop Type Object " << drop.crateDropTypeId << " was not found" << std::endl;
return;
}
std::vector<int>& crateDropType = Items::CrateDropTypes[drop.crateDropTypeId];
// sanity check
if (Items::MiscDropChances.find(drop.miscDropChanceId) == Items::MiscDropChances.end()) {
std::cout << "[WARN] Misc Drop Chance Object " << drop.miscDropChanceId << " was not found" << std::endl;
return;
}
MiscDropChance& miscDropChance = Items::MiscDropChances[drop.miscDropChanceId];
// sanity check
if (Items::MiscDropTypes.find(drop.miscDropTypeId) == Items::MiscDropTypes.end()) {
std::cout << "[WARN] Misc Drop Type Object " << drop.miscDropTypeId << " was not found" << std::endl;
return;
}
MiscDropType& miscDropType = Items::MiscDropTypes[drop.miscDropTypeId];
if (rolled.taros % miscDropChance.taroDropChanceTotal < miscDropChance.taroDropChance) {
plr->money += miscDropType.taroAmount;
// money nano boost
if (plr->hasBuff(ECSB_REWARD_CASH)) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
plr->money += miscDropType.taroAmount * (5 + boost) / 25;
}
}
if (rolled.fm % miscDropChance.fmDropChanceTotal < miscDropChance.fmDropChance) {
// formula for scaling FM with player/mob level difference
// TODO: adjust this better
int levelDifference = plr->level - mob->level;
int fm = miscDropType.fmAmount;
if (levelDifference > 0)
fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0;
// scavenger nano boost
if (plr->hasBuff(ECSB_REWARD_BLOB)) {
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
fm += fm * (5 + boost) / 25;
}
Missions::updateFusionMatter(sock, fm);
}
if (rolled.potions % miscDropChance.potionDropChanceTotal < miscDropChance.potionDropChance)
plr->batteryN += miscDropType.potionAmount;
if (rolled.boosts % miscDropChance.boostDropChanceTotal < miscDropChance.boostDropChance)
plr->batteryW += miscDropType.boostAmount;
// caps
if (plr->batteryW > 9999)
plr->batteryW = 9999;
if (plr->batteryN > 9999)
plr->batteryN = 9999;
// simple rewards
reward->m_iCandy = plr->money;
reward->m_iFusionMatter = plr->fusionmatter;
reward->m_iBatteryN = plr->batteryN;
reward->m_iBatteryW = plr->batteryW;
reward->iFatigue = 100; // prevents warning message
reward->iFatigue_Level = 1;
reward->iItemCnt = 1; // remember to update resplen if you change this
int slot = findFreeSlot(plr);
// no drop
if (slot == -1 || rolled.crate % crateDropChance.dropChanceTotal >= crateDropChance.dropChance) {
// no room for an item, but you still get FM and taros
reward->iItemCnt = 0;
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, sizeof(sP_FE2CL_REP_REWARD_ITEM));
} else {
// item reward
getMobDrop(&item->sItem, crateDropChance.crateTypeDropWeights, crateDropType, rolled.crateType);
item->iSlotNum = slot;
item->eIL = 1; // Inventory Location. 1 means player inventory.
// update player
plr->Inven[slot] = item->sItem;
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
}
}
void Items::giveMobDrop(CNSocket *sock, Mob* mob, const DropRoll& rolled, const DropRoll& eventRolled) {
// sanity check
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->type];
giveSingleDrop(sock, mob, mobDropId, rolled);
if (settings::EVENTMODE != 0) {
// sanity check
if (Items::EventToDropMap.find(settings::EVENTMODE) == Items::EventToDropMap.end()) {
std::cout << "[WARN] Event " << settings::EVENTMODE << " has no mob drop assigned" << std::endl;
return;
}
// find mob drop id
int eventMobDropId = Items::EventToDropMap[settings::EVENTMODE];
giveSingleDrop(sock, mob, eventMobDropId, eventRolled);
}
}
void Items::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_DELETE, itemDeleteHandler);
// this one is for gumballs
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_USE, itemUseHandler);
// Bank
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_BANK_OPEN, itemBankOpenHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_CHEST_OPEN, chestOpenHandler);
}

127
src/Items.hpp Normal file
View File

@ -0,0 +1,127 @@
#pragma once
#include "core/Core.hpp"
#include "Player.hpp"
#include "Rand.hpp"
#include "MobAI.hpp"
#include <vector>
#include <map>
struct CrocPotEntry {
int multStats, multLooks;
float base, rd0, rd1, rd2, rd3;
};
struct Crate {
int itemSetId;
int rarityWeightId;
};
struct CrateDropChance {
int dropChance, dropChanceTotal;
std::vector<int> crateTypeDropWeights;
};
struct MiscDropChance {
int potionDropChance, potionDropChanceTotal;
int boostDropChance, boostDropChanceTotal;
int taroDropChance, taroDropChanceTotal;
int fmDropChance, fmDropChanceTotal;
};
struct MiscDropType {
int potionAmount;
int boostAmount;
int taroAmount;
int fmAmount;
};
struct MobDrop {
int crateDropChanceId;
int crateDropTypeId;
int miscDropChanceId;
int miscDropTypeId;
};
struct ItemSet {
// itemset-wise offswitch to rarity filtering, every crate drops every rarity (still based on rarity weights)
bool ignoreRarity;
// itemset-wise offswitch for gender filtering, every crate can now drop neutral/boys/girls items
bool ignoreGender;
// default weight of all items in the itemset
int defaultItemWeight;
// change the rarity class of items in the itemset here
// rarity 0 bypasses the rarity filter for an individual item
std::map<int, int> alterRarityMap;
// change the gender class of items in the itemset here
// gender 0 bypasses the gender filter for an individual item
std::map<int, int> alterGenderMap;
// change the item weghts items in the itemset here
// only taken into account for chosen rarity, and if the item isn't filtered away due to gender
std::map<int, int> alterItemWeightMap;
std::vector<int> itemReferenceIds;
};
struct ItemReference {
int itemId;
int type;
int rarity;
int gender;
};
namespace Items {
enum class SlotType {
EQUIP = 0,
INVENTORY = 1,
BANK = 3
};
struct Item {
bool tradeable, sellable;
int buyPrice, sellPrice;
int stackSize, level, rarity;
int pointDamage, groupDamage, fireRate, defense, gender;
int weaponType;
// TODO: implement more as needed
};
struct DropRoll {
int boosts, potions;
int taros, fm;
int crate, crateType;
DropRoll() : boosts(Rand::rand()), potions(Rand::rand()), taros(Rand::rand()), fm(Rand::rand()), crate(Rand::rand()), crateType(Rand::rand()) { }
};
// hopefully this is fine since it's never modified after load
extern std::map<std::pair<int32_t, int32_t>, Item> ItemData; // <id, type> -> data
extern std::map<int32_t, CrocPotEntry> CrocPotTable; // level gap -> entry
extern std::map<int32_t, std::vector<int32_t>> RarityWeights;
extern std::map<int32_t, Crate> Crates;
extern std::map<int32_t, ItemReference> ItemReferences;
extern std::map<std::string, std::vector<std::pair<int32_t, int32_t>>> CodeItems; // code -> vector of <id, type>
// mob drops
extern std::map<int32_t, CrateDropChance> CrateDropChances;
extern std::map<int32_t, std::vector<int32_t>> CrateDropTypes;
extern std::map<int32_t, MiscDropChance> MiscDropChances;
extern std::map<int32_t, MiscDropType> MiscDropTypes;
extern std::map<int32_t, MobDrop> MobDrops;
extern std::map<int32_t, int32_t> EventToDropMap;
extern std::map<int32_t, int32_t> MobToDropMap;
extern std::map<int32_t, ItemSet> ItemSets;
void init();
// mob drops
void giveMobDrop(CNSocket *sock, Mob *mob, const DropRoll& rolled, const DropRoll& eventRolled);
int findFreeSlot(Player *plr);
Item* getItemData(int32_t id, int32_t type);
void checkItemExpire(CNSocket* sock, Player* player);
void setItemStats(Player* plr);
void updateEquips(CNSocket* sock, Player* plr);
#ifdef ACADEMY
extern std::map<int32_t, int32_t> NanoCapsules; // crate id -> nano id
#endif
}

661
src/Missions.cpp Normal file
View File

@ -0,0 +1,661 @@
#include "Missions.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Nanos.hpp"
using namespace Missions;
std::map<int32_t, Reward*> Missions::Rewards;
std::map<int32_t, TaskData*> Missions::Tasks;
nlohmann::json Missions::AvatarGrowth[37];
static void saveMission(Player* player, int missionId) {
// sanity check missionID so we don't get exceptions
if (missionId < 0 || missionId > 1023) {
std::cout << "[WARN] Client submitted invalid missionId: " <<missionId<< std::endl;
return;
}
// Missions are stored in int64_t array
int row = missionId / 64;
int column = missionId % 64;
player->aQuestFlag[row] |= (1ULL << column);
}
static bool isMissionCompleted(Player* player, int missionId) {
int row = missionId / 64;
int column = missionId % 64;
return player->aQuestFlag[row] & (1ULL << column);
}
int Missions::findQSlot(Player *plr, int id) {
int i;
// two passes. we mustn't fail to find an existing stack.
for (i = 0; i < AQINVEN_COUNT; i++)
if (plr->QInven[i].iID == id)
return i;
// no stack. start a new one.
for (i = 0; i < AQINVEN_COUNT; i++)
if (plr->QInven[i].iOpt == 0)
return i;
// not found
return -1;
}
static bool isQuestItemFull(CNSocket* sock, int itemId, int itemCount) {
Player* plr = PlayerManager::getPlayer(sock);
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;
return true;
}
return (itemCount == plr->QInven[slot].iOpt);
}
static void dropQuestItem(CNSocket *sock, int task, int count, int id, int mobid) {
std::cout << "Altered item id " << id << " by " << count << " for task id " << task << std::endl;
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
assert(resplen < CN_PACKET_BODY_SIZE);
// we know it's only one trailing struct, so we can skip full validation
Player *plr = PlayerManager::getPlayer(sock);
uint8_t respbuf[resplen]; // not a variable length array, don't worry
sP_FE2CL_REP_REWARD_ITEM *reward = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
// don't forget to zero the buffer!
memset(respbuf, 0, resplen);
// find free quest item slot
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;
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 += count; // stacking
}
// fully destory deleted items, for good measure
if (plr->QInven[slot].iOpt <= 0)
memset(&plr->QInven[slot], 0, sizeof(sItemBase));
// preserve stats
reward->m_iCandy = plr->money;
reward->m_iFusionMatter = plr->fusionmatter;
reward->iFatigue = 100; // prevents warning message
reward->iFatigue_Level = 1;
reward->m_iBatteryN = plr->batteryN;
reward->m_iBatteryW = plr->batteryW;
reward->iItemCnt = 1; // remember to update resplen if you change this
reward->iTaskID = task;
reward->iNPC_TypeID = mobid;
item->sItem = plr->QInven[slot];
item->iSlotNum = slot;
item->eIL = 2;
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
}
static int giveMissionReward(CNSocket *sock, int task, int choice=0) {
Reward *reward = Rewards[task];
Player *plr = PlayerManager::getPlayer(sock);
int nrewards = 0;
for (int i = 0; i < 4; i++) {
if (reward->itemIds[i] != 0)
nrewards++;
}
// this handles multiple choice rewards in the Academy's Mt. Neverest missions
if (choice != 0)
nrewards = 1;
int slots[4];
for (int i = 0; i < nrewards; i++) {
slots[i] = Items::findFreeSlot(plr);
if (slots[i] == -1) {
std::cout << "Not enough room to complete task" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_FAIL, fail);
fail.iTaskNum = task;
fail.iErrorCode = 13; // inventory full
sock->sendPacket((void*)&fail, P_FE2CL_REP_PC_TASK_END_FAIL, sizeof(sP_FE2CL_REP_PC_TASK_END_FAIL));
// delete any temp items we might have set
for (int j = 0; j < i; j++) {
plr->Inven[slots[j]] = { 0, 0, 0, 0 }; // empty
}
return -1;
}
plr->Inven[slots[i]] = { 999, 999, 999, 0 }; // temp item; overwritten later
}
uint8_t respbuf[CN_PACKET_BODY_SIZE];
size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + nrewards * sizeof(sItemReward);
assert(resplen < CN_PACKET_BODY_SIZE);
sP_FE2CL_REP_REWARD_ITEM *resp = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
// don't forget to zero the buffer!
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
// update player
plr->money += reward->money;
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->hasBuff(ECSB_REWARD_BLOB)) { // nano boost for fm
int boost = 0;
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
updateFusionMatter(sock, reward->fusionmatter * (30 + boost) / 25);
} else
updateFusionMatter(sock, reward->fusionmatter);
// simple rewards
resp->m_iCandy = plr->money;
resp->m_iFusionMatter = plr->fusionmatter;
resp->iFatigue = 100; // prevents warning message
resp->iFatigue_Level = 1;
resp->iItemCnt = nrewards;
resp->m_iBatteryN = plr->batteryN;
resp->m_iBatteryW = plr->batteryW;
int offset = 0;
// choice is actually a bitfield
if (choice != 0)
offset = (int)log2((int)choice);
for (int i = 0; i < nrewards; i++) {
item[i].sItem.iType = reward->itemTypes[offset+i];
item[i].sItem.iID = reward->itemIds[offset+i];
item[i].sItem.iOpt = 1;
item[i].iSlotNum = slots[i];
item[i].eIL = 1;
// update player inventory, overwriting temporary item
plr->Inven[slots[i]] = item[i].sItem;
}
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
return 0;
}
static bool endTask(CNSocket *sock, int32_t taskNum, int choice=0) {
Player *plr = PlayerManager::getPlayer(sock);
if (Tasks.find(taskNum) == Tasks.end())
return false;
// ugly pointer/reference juggling for the sake of operator overloading...
TaskData& task = *Tasks[taskNum];
// sanity check
int i;
bool found = false;
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] == taskNum) {
found = true;
break;
}
}
if (!found)
return false;
// mission rewards
if (Rewards.find(taskNum) != Rewards.end()) {
if (giveMissionReward(sock, taskNum, choice) == -1)
return false; // we don't want to send anything
}
// 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
*
* Some mission tasks give the player a quest item upon completion.
* This is distinct from quest item mob drops.
* They can be identified by a counter in the task indicator (ie. 1/1 Gravity Decelerator).
* The server is responsible for dropping the correct item.
* Yes, this is pretty stupid.
*
* iSUInstancename is the number of items to give. It is usually negative at the end of
* a mission, to clean up its quest items.
*/
for (int i = 0; i < 3; i++)
if (task["m_iSUItem"][i] != 0)
dropQuestItem(sock, taskNum, task["m_iSUInstancename"][i], task["m_iSUItem"][i], 0);
// if it's the last task
if (task["m_iSUOutgoingTask"] == 0) {
// save completed mission on player
saveMission(plr, (int)(task["m_iHMissionID"])-1);
// if it's a nano mission, reward the nano.
if (task["m_iSTNanoID"] != 0)
Nanos::addNano(sock, task["m_iSTNanoID"], 0, true);
// remove current mission
plr->CurrentMissionID = 0;
}
return true;
}
bool Missions::startTask(Player* plr, int TaskID) {
if (Missions::Tasks.find(TaskID) == Missions::Tasks.end()) {
std::cout << "[WARN] Player submitted unknown task!?" << std::endl;
return false;
}
TaskData& task = *Missions::Tasks[TaskID];
if (task["m_iCTRReqLvMin"] > plr->level) {
std::cout << "[WARN] Player tried to start a task above their level" << std::endl;
return false;
}
if (isMissionCompleted(plr, (int)(task["m_iHMissionID"]) - 1)) {
std::cout << "[WARN] Player tried to start an already completed mission" << std::endl;
return false;
}
// client freaks out if nano mission isn't sent first after relogging, so it's easiest to set it here
if (task["m_iSTNanoID"] != 0 && plr->tasks[0] != 0) {
// lets move task0 to different spot
int moveToSlot = 1;
for (; moveToSlot < ACTIVE_MISSION_COUNT; moveToSlot++)
if (plr->tasks[moveToSlot] == 0)
break;
plr->tasks[moveToSlot] = plr->tasks[0];
plr->tasks[0] = 0;
for (int i = 0; i < 3; i++) {
plr->RemainingNPCCount[moveToSlot][i] = plr->RemainingNPCCount[0][i];
plr->RemainingNPCCount[0][i] = 0;
}
}
int i;
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] == 0) {
plr->tasks[i] = TaskID;
for (int j = 0; j < 3; j++) {
plr->RemainingNPCCount[i][j] = (int)task["m_iCSUNumToKill"][j];
}
break;
}
}
if (i == ACTIVE_MISSION_COUNT - 1 && plr->tasks[i] != TaskID) {
std::cout << "[WARN] Player has more than 6 active missions!?" << std::endl;
return false;
}
return true;
}
static void taskStart(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TASK_START* missionData = (sP_CL2FE_REQ_PC_TASK_START*)data->buf;
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_SUCC, response);
Player *plr = PlayerManager::getPlayer(sock);
if (!startTask(plr, missionData->iTaskNum)) {
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_FAIL, failresp);
failresp.iTaskNum = missionData->iTaskNum;
failresp.iErrorCode = 1; // unused in the client
sock->sendPacket(failresp, P_FE2CL_REP_PC_TASK_START_FAIL);
return;
}
TaskData& task = *Tasks[missionData->iTaskNum];
// Give player their delivery items at the start, or reset them to 0 at the start.
for (int i = 0; i < 3; i++)
if (task["m_iSTItemID"][i] != 0)
dropQuestItem(sock, missionData->iTaskNum, task["m_iSTItemNumNeeded"][i], task["m_iSTItemID"][i], 0);
std::cout << "Mission requested task: " << missionData->iTaskNum << std::endl;
response.iTaskNum = missionData->iTaskNum;
response.iRemainTime = task["m_iSTGrantTimer"];
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"] == (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.kind != EntityKind::PLAYER) {
BaseNPC* npc = (BaseNPC*)ref.getEntity();
NPCPath* path = Transport::findApplicablePath(npc->id, npc->type, missionData->iTaskNum);
if (path != nullptr) {
Transport::constructPathNPC(npc->id, path);
return;
}
}
}
}
}
}
static void taskEnd(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TASK_END* missionData = (sP_CL2FE_REQ_PC_TASK_END*)data->buf;
if (Missions::Tasks.find(missionData->iTaskNum) == Missions::Tasks.end())
return;
TaskData* task = Missions::Tasks[missionData->iTaskNum];
// 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"] == (int)eTaskTypeProperty::Defeat) {
mobsAreKilled = true;
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] == missionData->iTaskNum) {
for (int j = 0; j < 3; j++) {
if (plr->RemainingNPCCount[i][j] > 0) {
mobsAreKilled = false;
break;
}
}
}
}
}
if (!mobsAreKilled) {
int failTaskID = task->task["m_iFOutgoingTask"];
if (failTaskID != 0) {
Missions::quitTask(sock, missionData->iTaskNum, false);
for (int i = 0; i < 6; i++)
if (plr->tasks[i] == missionData->iTaskNum)
plr->tasks[i] = failTaskID;
return;
}
}
}
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, response);
response.iTaskNum = missionData->iTaskNum;
if (!endTask(sock, missionData->iTaskNum, missionData->iBox1Choice)) {
return;
}
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_END_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_END_SUCC));
}
static void setMission(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
sP_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID* missionData = (sP_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID*)data->buf;
INITSTRUCT(sP_FE2CL_REP_PC_SET_CURRENT_MISSION_ID, response);
response.iCurrentMissionID = missionData->iCurrentMissionID;
plr->CurrentMissionID = missionData->iCurrentMissionID;
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_SET_CURRENT_MISSION_ID, sizeof(sP_FE2CL_REP_PC_SET_CURRENT_MISSION_ID));
}
static void quitMission(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TASK_STOP* missionData = (sP_CL2FE_REQ_PC_TASK_STOP*)data->buf;
quitTask(sock, missionData->iTaskNum, true);
}
void Missions::quitTask(CNSocket* sock, int32_t taskNum, bool manual) {
Player* plr = PlayerManager::getPlayer(sock);
if (Tasks.find(taskNum) == Tasks.end())
return; // sanity check
// update player
int i;
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;
}
}
}
if (i == ACTIVE_MISSION_COUNT - 1 && plr->tasks[i] != 0) {
std::cout << "[WARN] Player quit non-active mission!?" << std::endl;
}
// remove current mission
plr->CurrentMissionID = 0;
TaskData& task = *Tasks[taskNum];
// clean up quest items
if (manual) {
for (i = 0; i < 3; i++) {
if (task["m_iSUItem"][i] == 0 && task["m_iCSUItemID"][i] == 0)
continue;
/*
* It's ok to do this only server-side, because the server decides which
* slot later items will be placed in.
*/
for (int j = 0; j < AQINVEN_COUNT; j++)
if (plr->QInven[j].iID == task["m_iSUItem"][i] || plr->QInven[j].iID == task["m_iCSUItemID"][i] || plr->QInven[j].iID == task["m_iSTItemID"][i])
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;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_TASK_END_FAIL, sizeof(sP_FE2CL_REP_PC_TASK_END_FAIL));
}
INITSTRUCT(sP_FE2CL_REP_PC_TASK_STOP_SUCC, response);
response.iTaskNum = taskNum;
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_STOP_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_STOP_SUCC));
}
void Missions::updateFusionMatter(CNSocket* sock, int fusion) {
Player *plr = PlayerManager::getPlayer(sock);
plr->fusionmatter += fusion;
// there's a much lower FM cap in the Future
int fmCap = AvatarGrowth[plr->level]["m_iFMLimit"];
if (plr->fusionmatter > fmCap)
plr->fusionmatter = fmCap;
else if (plr->fusionmatter < 0) // if somehow lowered too far
plr->fusionmatter = 0;
// don't run nano mission logic at level 36
if (plr->level >= 36)
return;
// don't give the Blossom nano mission until the player's in the Past
if (plr->level == 4 && plr->PCStyle2.iPayzoneFlag == 0)
return;
// check if it is enough for the nano mission
int fmNano = AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
if (plr->fusionmatter < fmNano)
return;
#ifndef ACADEMY
// check if the nano task is already started
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
TaskData& task = *Tasks[plr->tasks[i]];
if (task["m_iSTNanoID"] != 0)
return; // nano mission was already started!
}
// start the nano mission
startTask(plr, AvatarGrowth[plr->level]["m_iNanoQuestTaskID"]);
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_SUCC, response);
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
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
plr->level++;
INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, response);
response.iFusionMatter = plr->fusionmatter;
response.iLevel = plr->level;
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, sizeof(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC));
#endif
// play the beam animation for other players
INITSTRUCT(sP_FE2CL_PC_EVENT, bcast);
bcast.iEventID = 1; // beam effect
bcast.iPC_ID = plr->iID;
PlayerManager::sendToViewable(sock, (void*)&bcast, P_FE2CL_PC_EVENT, sizeof(sP_FE2CL_PC_EVENT));
}
void Missions::mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls) {
Player *plr = PlayerManager::getPlayer(sock);
bool missionmob = false;
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] == 0)
continue;
// tasks[] should always have valid IDs
TaskData& task = *Tasks[plr->tasks[i]];
for (int j = 0; j < 3; j++) {
if (task["m_iCSUEnemyID"][j] != mobid)
continue;
// acknowledge killing of mission mob...
if (task["m_iCSUNumToKill"][j] != 0) {
missionmob = true;
if (plr->RemainingNPCCount[i][j] > 0) {
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 = rolls[plr->tasks[i]] % 100 < task["m_iSTItemDropRate"][j];
if (drop) {
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);
}
}
}
}
// ...but only once
// XXX: is it actually necessary to do it this way?
if (missionmob) {
INITSTRUCT(sP_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC, kill);
kill.iNPCID = mobid;
sock->sendPacket((void*)&kill, P_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC, sizeof(sP_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC));
}
}
void Missions::failInstancedMissions(CNSocket* sock) {
// loop through all tasks; if the required instance is being left, "fail" the task
Player* plr = PlayerManager::getPlayer(sock);
for (int i = 0; i < 6; i++) {
int taskNum = plr->tasks[i];
if (Missions::Tasks.find(taskNum) == Missions::Tasks.end())
continue; // sanity check
TaskData* task = Missions::Tasks[taskNum];
if (task->task["m_iRequireInstanceID"] != 0) { // mission is instanced
int failTaskID = task->task["m_iFOutgoingTask"];
if (failTaskID != 0) {
Missions::quitTask(sock, taskNum, false);
//plr->tasks[i] = failTaskID; // this causes the client to freak out and send a dupe task
}
}
}
}
void Missions::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_START, taskStart);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_END, taskEnd);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID, setMission);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_STOP, quitMission);
}

57
src/Missions.hpp Normal file
View File

@ -0,0 +1,57 @@
#pragma once
#include "core/Core.hpp"
#include "JSON.hpp"
#include "Player.hpp"
#include <map>
struct Reward {
int32_t id;
int32_t itemTypes[4];
int32_t itemIds[4];
int32_t money;
int32_t fusionmatter;
Reward(int32_t id, nlohmann::json types, nlohmann::json ids, int32_t m, int32_t fm) :
id(id), money(m), fusionmatter(fm) {
for (int i = 0; i < 4; i++) {
itemTypes[i] = types[i];
itemIds[i] = ids[i];
}
};
};
struct TaskData {
/*
* TODO: We'll probably want to keep only the data the server actually needs,
* but for now RE/development is much easier if we have everything at
* our fingertips.
*/
nlohmann::json task;
TaskData(nlohmann::json t) : task(t) {}
// convenience
auto operator[](std::string s) { return task[s]; }
};
namespace Missions {
extern std::map<int32_t, Reward*> Rewards;
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, std::map<int, int>& rolls);
void quitTask(CNSocket* sock, int32_t taskNum, bool manual);
void failInstancedMissions(CNSocket* sock);
}

862
src/MobAI.cpp Normal file
View File

@ -0,0 +1,862 @@
#include "MobAI.hpp"
#include "Chunking.hpp"
#include "NPCManager.hpp"
#include "Entities.hpp"
#include "PlayerManager.hpp"
#include "Racing.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include "Combat.hpp"
#include "Items.hpp"
#include "Missions.hpp"
#include "Rand.hpp"
#include <cmath>
#include <limits.h>
using namespace MobAI;
bool MobAI::simulateMobs = settings::SIMULATEMOBS;
void Mob::step(time_t currTime) {
if (playersInView < 0)
std::cout << "[WARN] Weird playerview value " << playersInView << std::endl;
// skip movement and combat if disabled or not in view
if ((!MobAI::simulateMobs || playersInView == 0) && state != AIState::DEAD
&& state != AIState::RETREAT)
return;
// call superclass step
CombatNPC::step(currTime);
}
int Mob::takeDamage(EntityRef src, int amt) {
// cannot kill mobs multiple times; cannot harm retreating mobs
if (state != AIState::ROAMING && state != AIState::COMBAT) {
return 0; // no damage
}
if (skillStyle >= 0)
return 0; // don't hurt a mob casting corruption
if (state == AIState::ROAMING) {
assert(target == nullptr && src.kind == EntityKind::PLAYER); // TODO: players only for now
transition(AIState::COMBAT, src);
if (groupLeader != 0)
MobAI::followToCombat(this);
}
// wake up sleeping monster
if (hasBuff(ECSB_MEZ)) {
removeBuff(ECSB_MEZ);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = id;
pkt1.iConditionBitFlag = getCompositeCondition();
NPCManager::sendToViewable(this, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
// call superclass takeDamage
return CombatNPC::takeDamage(src, amt);
}
/*
* Dynamic lerp; distinct from Transport::lerp(). This one doesn't care about height and
* only returns the first step, since the rest will need to be recalculated anyway if chasing player.
*/
static std::pair<int,int> lerp(int x1, int y1, int x2, int y2, int speed) {
std::pair<int,int> ret = {x1, y1};
if (speed == 0)
return ret;
int distance = hypot(x1 - x2, y1 - y2);
if (distance > speed) {
int lerps = distance / speed;
// interpolate only the first point
float frac = 1.0f / lerps;
ret.first = (x1 + (x2 - x1) * frac);
ret.second = (y1 + (y2 - y1) * frac);
} else {
ret.first = x2;
ret.second = y2;
}
return ret;
}
void MobAI::clearDebuff(Mob *mob) {
mob->skillStyle = -1;
mob->clearBuffs(false);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = mob->id;
pkt1.iConditionBitFlag = mob->getCompositeCondition();
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
void MobAI::followToCombat(Mob *mob) {
if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->kind == EntityKind::MOB) {
Mob* leadMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
for (int i = 0; i < 4; i++) {
if (leadMob->groupMember[i] == 0)
break;
if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->kind != EntityKind::MOB) {
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
continue;
}
Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]];
if (followerMob->state != AIState::ROAMING) // only roaming mobs should transition to combat
continue;
followerMob->transition(AIState::COMBAT, mob->target);
}
if (leadMob->state != AIState::ROAMING)
return;
leadMob->transition(AIState::COMBAT, mob->target);
}
}
void MobAI::groupRetreat(Mob *mob) {
if (NPCManager::NPCs.find(mob->groupLeader) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupLeader]->kind != EntityKind::MOB)
return;
Mob* leadMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
for (int i = 0; i < 4; i++) {
if (leadMob->groupMember[i] == 0)
break;
if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->kind != EntityKind::MOB) {
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
continue;
}
Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]];
if (followerMob->state != AIState::COMBAT)
continue;
followerMob->target = nullptr;
followerMob->state = AIState::RETREAT;
clearDebuff(followerMob);
}
if (leadMob->state != AIState::COMBAT)
return;
leadMob->target = nullptr;
leadMob->state = AIState::RETREAT;
clearDebuff(leadMob);
}
/*
* Aggro on nearby players.
* Even if they're in range, we can't assume they're all in the same one chunk
* as the mob, since it might be near a chunk boundary.
*/
bool MobAI::aggroCheck(Mob *mob, time_t currTime) {
CNSocket *closest = nullptr;
int closestDistance = INT_MAX;
for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) {
Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) {
// TODO: support targetting other CombatNPCs
if (ref.kind != EntityKind::PLAYER)
continue;
CNSocket *s = ref.sock;
Player *plr = PlayerManager::getPlayer(s);
if (plr->HP <= 0 || plr->onMonkey)
continue;
int mobRange = mob->sightRange;
if (plr->hasBuff(ECSB_UP_STEALTH)
|| Racing::EPRaces.find(s) != Racing::EPRaces.end())
mobRange /= 3;
// 0.33x - 1.66x the range
int levelDifference = plr->level - mob->level;
if (levelDifference > -10)
mobRange = levelDifference < 10 ? mobRange - (levelDifference * mobRange / 15) : mobRange / 3;
if (mob->state != AIState::ROAMING && plr->inCombat) // freshly out of aggro mobs
mobRange = mob->sightRange * 2; // should not be impacted by the above
if (plr->iSpecialState & (CN_SPECIAL_STATE_FLAG__INVISIBLE|CN_SPECIAL_STATE_FLAG__INVULNERABLE))
mobRange = -1;
// height is relevant for aggro distance because of platforming
int xyDistance = hypot(mob->x - plr->x, mob->y - plr->y);
int distance = hypot(xyDistance, (mob->z - plr->z) * 2); // difference in Z counts twice
if (distance > mobRange || distance > closestDistance)
continue;
// found a player
closest = s;
closestDistance = distance;
}
}
if (closest != nullptr) {
// found closest player. engage.
mob->transition(AIState::COMBAT, closest);
if (mob->groupLeader != 0)
followToCombat(mob);
return true;
}
return false;
}
static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, int mobStyle) {
Player *plr = PlayerManager::getPlayer(mob->target);
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT) + targetData[0] * sizeof(sCAttackResult);
// validate response packet
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT), targetData[0], sizeof(sCAttackResult))) {
std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_CORRUPTION_HIT packet size" << std::endl;
return;
}
uint8_t respbuf[CN_PACKET_BODY_SIZE];
memset(respbuf, 0, CN_PACKET_BODY_SIZE);
sP_FE2CL_NPC_SKILL_CORRUPTION_HIT *resp = (sP_FE2CL_NPC_SKILL_CORRUPTION_HIT*)respbuf;
sCAttackResult *respdata = (sCAttackResult*)(respbuf+sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT));
resp->iNPC_ID = mob->id;
resp->iSkillID = skillID;
resp->iStyle = mobStyle;
resp->iValue1 = plr->x;
resp->iValue2 = plr->y;
resp->iValue3 = plr->z;
resp->iTargetCnt = targetData[0];
for (int i = 0; i < targetData[0]; i++) {
CNSocket *sock = nullptr;
Player *plr = nullptr;
for (auto& pair : PlayerManager::players) {
if (pair.second->iID == targetData[i+1]) {
sock = pair.first;
plr = pair.second;
break;
}
}
// player not found
if (plr == nullptr) {
std::cout << "[WARN] dealCorruption: player ID not found" << std::endl;
return;
}
respdata[i].eCT = 1;
respdata[i].iID = plr->iID;
respdata[i].bProtected = 0;
respdata[i].iActiveNanoSlotNum = -1;
for (int n = 0; n < 3; n++)
if (plr->activeNano == plr->equippedNanos[n])
respdata[i].iActiveNanoSlotNum = n;
respdata[i].iNanoID = plr->activeNano;
int nanoStyle = Nanos::nanoStyle(plr->activeNano);
if (nanoStyle == -1) { // no nano
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
} else if (mobStyle == nanoStyle) {
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
respdata[i].iDamage = 0;
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina;
} else if (mobStyle - nanoStyle == 1 || nanoStyle - mobStyle == 2) {
respdata[i].iHitFlag = HF_BIT_STYLE_WIN;
respdata[i].iDamage = 0;
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina += 45;
if (plr->Nanos[plr->activeNano].iStamina > 150)
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 150;
// fire damage power disguised as a corruption attack back at the enemy
SkillData skill = {
SkillType::DAMAGE, // skillType
SkillEffectTarget::POINT, // effectTarget
1, // effectType
SkillTargetType::MOBS, // targetType
SkillDrainType::ACTIVE, // drainType
0, // effectArea
{0, 0, 0, 0}, // batteryUse
{0, 0, 0, 0}, // durationTime
{0, 0, 0}, // valueTypes (unused)
{
{200, 200, 200, 200},
{200, 200, 200, 200},
{200, 200, 200, 200},
}
};
Abilities::useNanoSkill(sock, &skill, *plr->getActiveNano(), { mob });
} else {
respdata[i].iHitFlag = HF_BIT_STYLE_LOSE;
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina -= 90;
if (plr->Nanos[plr->activeNano].iStamina < 0) {
respdata[i].bNanoDeactive = 1;
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 0;
}
}
if (!(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
plr->HP -= respdata[i].iDamage;
respdata[i].iHP = plr->HP;
respdata[i].iConditionBitFlag = plr->getCompositeCondition();
if (plr->HP <= 0) {
if (!MobAI::aggroCheck(mob, getTime()))
mob->transition(AIState::RETREAT, mob->target);
}
}
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_NPC_SKILL_CORRUPTION_HIT, resplen);
}
static void useAbilities(Mob *mob, time_t currTime) {
Player *plr = PlayerManager::getPlayer(mob->target);
if (mob->skillStyle >= 0) { // corruption hit
int skillID = (int)mob->data["m_iCorruptionType"];
std::vector<int> targetData = {1, plr->iID, 0, 0, 0};
int temp = mob->skillStyle;
mob->skillStyle = -3; // corruption cooldown
mob->nextAttack = currTime + 1000;
dealCorruption(mob, targetData, skillID, temp);
return;
}
if (mob->skillStyle == -2) { // eruption hit
int skillID = (int)mob->data["m_iMegaType"];
std::vector<ICombatant*> targets{};
// find the players within range of eruption
for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) {
Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) {
// TODO: see aggroCheck()
if (ref.kind != EntityKind::PLAYER)
continue;
CNSocket *s = ref.sock;
Player *plr = PlayerManager::getPlayer(s);
if (!plr->isAlive())
continue;
int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y);
if (distance < Abilities::SkillTable[skillID].effectArea) {
targets.push_back(plr);
if (targets.size() > 3) // make sure not to have more than 4
break;
}
}
}
Abilities::useNPCSkill(mob->id, skillID, targets);
mob->skillStyle = -3; // eruption cooldown
mob->nextAttack = currTime + 1000;
return;
}
if (mob->skillStyle == -3) { // cooldown expires
mob->skillStyle = -1;
return;
}
int random = Rand::rand(2000) * 1000;
int prob1 = (int)mob->data["m_iActiveSkill1Prob"]; // active skill probability
int prob2 = (int)mob->data["m_iCorruptionTypeProb"]; // corruption probability
int prob3 = (int)mob->data["m_iMegaTypeProb"]; // eruption probability
if (random < prob1) { // active skill hit
int skillID = (int)mob->data["m_iActiveSkill1"];
SkillData* skill = &Abilities::SkillTable[skillID];
int debuffID = Abilities::getCSTBFromST(skill->skillType);
if(plr->hasBuff(debuffID))
return; // prevent debuffing a player twice
Abilities::useNPCSkill(mob->getRef(), skillID, { plr });
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
return;
}
if (random < prob1 + prob2) { // corruption windup
int skillID = (int)mob->data["m_iCorruptionType"];
INITSTRUCT(sP_FE2CL_NPC_SKILL_CORRUPTION_READY, pkt);
pkt.iNPC_ID = mob->id;
pkt.iSkillID = skillID;
pkt.iValue1 = plr->x;
pkt.iValue2 = plr->y;
pkt.iValue3 = plr->z;
mob->skillStyle = Nanos::nanoStyle(plr->activeNano) - 1;
if (mob->skillStyle == -1)
mob->skillStyle = 2;
if (mob->skillStyle == -2)
mob->skillStyle = Rand::rand(3);
pkt.iStyle = mob->skillStyle;
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_SKILL_CORRUPTION_READY, sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_READY));
mob->nextAttack = currTime + 1800;
return;
}
if (random < prob1 + prob2 + prob3) { // eruption windup
int skillID = (int)mob->data["m_iMegaType"];
INITSTRUCT(sP_FE2CL_NPC_SKILL_READY, pkt);
pkt.iNPC_ID = mob->id;
pkt.iSkillID = skillID;
pkt.iValue1 = mob->hitX = plr->x;
pkt.iValue2 = mob->hitY = plr->y;
pkt.iValue3 = mob->hitZ = plr->z;
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_SKILL_READY, sizeof(sP_FE2CL_NPC_SKILL_READY));
mob->nextAttack = currTime + 1800;
mob->skillStyle = -2;
return;
}
return;
}
void MobAI::incNextMovement(Mob* mob, time_t currTime) {
if (currTime == 0)
currTime = getTime();
int delay = (int)mob->data["m_iDelayTime"] * 1000;
mob->nextMovement = currTime + delay / 2 + Rand::rand(delay / 2);
}
void MobAI::deadStep(CombatNPC* npc, time_t currTime) {
Mob* self = (Mob*)npc;
// despawn the mob after a short delay
if (self->killedTime != 0 && !self->despawned && currTime - self->killedTime > 2000) {
self->despawned = true;
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
pkt.iNPC_ID = self->id;
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT));
// if it was summoned, mark it for removal
if (self->summoned) {
std::cout << "[INFO] Queueing killed summoned mob for removal" << std::endl;
NPCManager::queueNPCRemoval(self->id);
return;
}
// pre-set spawn coordinates if not marked for removal
self->x = self->spawnX;
self->y = self->spawnY;
self->z = self->spawnZ;
}
// to guide their groupmates, group leaders still need to move despite being dead
if (self->groupLeader == self->id)
roamingStep(self, currTime);
/*
* If the mob hasn't fully despanwed yet, don't try to respawn it. This protects
* against the edge case where mobs with a very short regenTime would try to respawn
* before they've faded away; and would respawn even if they were meant to be removed.
*/
if (!self->despawned)
return;
if (self->killedTime != 0 && currTime - self->killedTime < self->regenTime * 100)
return;
std::cout << "respawning mob " << self->id << " with HP = " << self->maxHealth << std::endl;
self->transition(AIState::ROAMING, self->id);
// if mob is a group leader/follower, spawn where the group is.
if (self->groupLeader != 0) {
if (NPCManager::NPCs.find(self->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[self->groupLeader]->kind == EntityKind::MOB) {
Mob* leaderMob = (Mob*)NPCManager::NPCs[self->groupLeader];
self->x = leaderMob->x + self->offsetX;
self->y = leaderMob->y + self->offsetY;
self->z = leaderMob->z;
} else {
std::cout << "[WARN] deadStep: mob cannot find it's leader!" << std::endl;
}
}
INITSTRUCT(sP_FE2CL_NPC_NEW, pkt);
pkt.NPCAppearanceData = self->getAppearanceData();
// notify all nearby players
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW));
}
void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
Mob* self = (Mob*)npc;
assert(self->target != nullptr);
// lose aggro if the player lost connection
if (PlayerManager::players.find(self->target) == PlayerManager::players.end()) {
if (!MobAI::aggroCheck(self, getTime()))
self->transition(AIState::RETREAT, self->target);
return;
}
Player *plr = PlayerManager::getPlayer(self->target);
// lose aggro if the player became invulnerable or died
if (plr->HP <= 0
|| (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)) {
if (!MobAI::aggroCheck(self, getTime()))
self->transition(AIState::RETREAT, self->target);
return;
}
// tick buffs
auto it = npc->buffs.begin();
while(it != npc->buffs.end()) {
Buff* buff = (*it).second;
buff->combatTick(currTime);
// if mob state changed, end the step
if(self->state != AIState::COMBAT)
return;
buff->tick(currTime);
if(buff->isStale()) {
// garbage collect
it = npc->buffs.erase(it);
delete buff;
}
else it++;
}
// skip attack if stunned or asleep
if (self->hasBuff(ECSB_STUN) || self->hasBuff(ECSB_MEZ)) {
self->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up.
return;
}
int distance = hypot(plr->x - self->x, plr->y - self->y);
int mobRange = (int)self->data["m_iAtkRange"] + (int)self->data["m_iRadius"];
if (currTime >= self->nextAttack) {
if (self->skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance.
useAbilities(self, currTime);
if (self->target == nullptr)
return;
}
int distanceToTravel = INT_MAX;
int speed = self->speed;
// movement logic: move when out of range but don't move while casting a skill
if (distance > mobRange && self->skillStyle == -1) {
if (self->nextMovement != 0 && currTime < self->nextMovement)
return;
self->nextMovement = currTime + 400;
if (currTime >= self->nextAttack)
self->nextAttack = 0;
// halve movement speed if snared
if (self->hasBuff(ECSB_DN_MOVE_SPEED))
speed /= 2;
int targetX = plr->x;
int targetY = plr->y;
if (self->groupLeader != 0) {
targetX += self->offsetX*distance/(self->idleRange + 1);
targetY += self->offsetY*distance/(self->idleRange + 1);
}
distanceToTravel = std::min(distance-mobRange+1, speed*2/5);
auto targ = lerp(self->x, self->y, targetX, targetY, distanceToTravel);
if (distanceToTravel < speed*2/5 && currTime >= self->nextAttack)
self->nextAttack = 0;
NPCManager::updateNPCPosition(self->id, targ.first, targ.second, self->z, self->instanceID, self->angle);
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
pkt.iNPC_ID = self->id;
pkt.iSpeed = speed;
pkt.iToX = self->x = targ.first;
pkt.iToY = self->y = targ.second;
pkt.iToZ = plr->z;
pkt.iMoveStyle = 1;
// notify all nearby players
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
}
/* attack logic
* 2/5 represents 400 ms which is the time interval mobs use per movement logic step
* if the mob is one move interval away, we should just start attacking anyways.
*/
if (distance <= mobRange || distanceToTravel < self->speed*2/5) {
if (self->nextAttack == 0 || currTime >= self->nextAttack) {
self->nextAttack = currTime + (int)self->data["m_iDelayTime"] * 100;
Combat::npcAttackPc(self, currTime);
}
}
// retreat if the player leaves combat range
int xyDistance = hypot(plr->x - self->roamX, plr->y - self->roamY);
distance = hypot(xyDistance, plr->z - self->roamZ);
if (distance >= self->data["m_iCombatRange"]) {
self->transition(AIState::RETREAT, self->target);
}
}
void MobAI::roamingStep(CombatNPC* npc, time_t currTime) {
Mob* self = (Mob*)npc;
/*
* We reuse nextAttack to avoid scanning for players all the time, but to still
* do so more often than if we waited for nextMovement (which is way too slow).
* In the case of group leaders, this step will be called by dead mobs, so disable attack.
*/
if (self->state != AIState::DEAD && (self->nextAttack == 0 || currTime >= self->nextAttack)) {
self->nextAttack = currTime + 500;
if (aggroCheck(self, currTime))
return;
}
// no random roaming if the mob already has a set path
if (self->staticPath)
return;
if (self->groupLeader != 0 && self->groupLeader != self->id) // don't roam by yourself without group leader
return;
/*
* mob->nextMovement is also updated whenever the path queue is traversed in
* Transport::stepNPCPathing() (which ticks at a higher frequency than nextMovement),
* so we don't have to check if there's already entries in the queue since we know there won't be.
*/
if (self->nextMovement != 0 && currTime < self->nextMovement)
return;
incNextMovement(self, currTime);
int xStart = self->spawnX - self->idleRange/2;
int yStart = self->spawnY - self->idleRange/2;
// some mobs don't move (and we mustn't divide/modulus by zero)
if (self->idleRange == 0 || self->speed == 0)
return;
int farX, farY, distance;
int minDistance = self->idleRange / 2;
// pick a random destination
farX = xStart + Rand::rand(self->idleRange);
farY = yStart + Rand::rand(self->idleRange);
distance = std::abs(std::max(farX - self->x, farY - self->y));
if (distance == 0)
distance += 1; // hack to avoid FPE
// if it's too short a walk, go further in that direction
farX = self->x + (farX - self->x) * minDistance / distance;
farY = self->y + (farY - self->y) * minDistance / distance;
// but don't got out of bounds
farX = std::clamp(farX, xStart, xStart + self->idleRange);
farY = std::clamp(farY, yStart, yStart + self->idleRange);
// halve movement speed if snared
if (self->hasBuff(ECSB_DN_MOVE_SPEED))
self->speed /= 2;
std::queue<Vec3> queue;
Vec3 from = { self->x, self->y, self->z };
Vec3 to = { farX, farY, self->z };
// add a route to the queue; to be processed in Transport::stepNPCPathing()
Transport::lerp(&queue, from, to, self->speed);
Transport::NPCQueues[self->id] = queue;
if (self->groupLeader != 0 && self->groupLeader == self->id) {
// make followers follow this npc.
for (int i = 0; i < 4; i++) {
if (self->groupMember[i] == 0)
break;
if (NPCManager::NPCs.find(self->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[self->groupMember[i]]->kind != EntityKind::MOB) {
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
continue;
}
std::queue<Vec3> queue2;
Mob* followerMob = (Mob*)NPCManager::NPCs[self->groupMember[i]];
from = { followerMob->x, followerMob->y, followerMob->z };
to = { farX + followerMob->offsetX, farY + followerMob->offsetY, followerMob->z };
Transport::lerp(&queue2, from, to, self->speed);
Transport::NPCQueues[followerMob->id] = queue2;
}
}
}
void MobAI::retreatStep(CombatNPC* npc, time_t currTime) {
Mob* self = (Mob*)npc;
if (self->nextMovement != 0 && currTime < self->nextMovement)
return;
self->nextMovement = currTime + 400;
// distance between spawn point and current location
int distance = hypot(self->x - self->roamX, self->y - self->roamY);
//if (distance > mob->data["m_iIdleRange"]) {
if (distance > 10) {
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
auto targ = lerp(self->x, self->y, self->roamX, self->roamY, (int)self->speed*4/5);
pkt.iNPC_ID = self->id;
pkt.iSpeed = (int)self->speed * 2;
pkt.iToX = self->x = targ.first;
pkt.iToY = self->y = targ.second;
pkt.iToZ = self->z = self->spawnZ;
pkt.iMoveStyle = 1;
// notify all nearby players
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
}
// if we got there
//if (distance <= mob->data["m_iIdleRange"]) {
if (distance <= 10) { // retreat back to the spawn point
self->transition(AIState::ROAMING, self->id);
}
}
void MobAI::onRoamStart(CombatNPC* npc, EntityRef src) {
Mob* self = (Mob*)npc;
self->hp = self->maxHealth;
self->killedTime = 0;
self->nextAttack = 0;
// cast a return home heal spell, this is the right way(tm)
Abilities::useNPCSkill(npc->getRef(), 110, { npc });
// clear outlying debuffs
clearDebuff(self);
}
void MobAI::onCombatStart(CombatNPC* npc, EntityRef src) {
Mob* self = (Mob*)npc;
assert(src.kind == EntityKind::PLAYER);
self->target = src.sock;
self->nextMovement = getTime();
self->nextAttack = 0;
self->roamX = self->x;
self->roamY = self->y;
self->roamZ = self->z;
int skillID = (int)self->data["m_iPassiveBuff"];
if(skillID != 0) // cast passive
Abilities::useNPCSkill(npc->getRef(), skillID, { npc });
}
void MobAI::onRetreat(CombatNPC* npc, EntityRef src) {
Mob* self = (Mob*)npc;
self->target = nullptr;
MobAI::clearDebuff(self);
if (self->groupLeader != 0)
MobAI::groupRetreat(self);
}
void MobAI::onDeath(CombatNPC* npc, EntityRef src) {
Mob* self = (Mob*)npc;
self->target = nullptr;
self->skillStyle = -1;
self->clearBuffs(true);
self->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 (src.kind == EntityKind::PLAYER && src.isValid()) {
Player* plr = PlayerManager::getPlayer(src.sock);
Items::DropRoll rolled;
Items::DropRoll eventRolled;
std::map<int, int> qitemRolls;
std::vector<Player*> playerRefs;
if (plr->group == nullptr) {
playerRefs.push_back(plr);
Combat::genQItemRolls(playerRefs, qitemRolls);
Items::giveMobDrop(src.sock, self, rolled, eventRolled);
Missions::mobKilled(src.sock, self->type, qitemRolls);
}
else {
auto players = plr->group->filter(EntityKind::PLAYER);
for (EntityRef pRef : players) playerRefs.push_back(PlayerManager::getPlayer(pRef.sock));
Combat::genQItemRolls(playerRefs, qitemRolls);
for (int i = 0; i < players.size(); i++) {
CNSocket* sockTo = players[i].sock;
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, self, rolled, eventRolled);
Missions::mobKilled(sockTo, self->type, qitemRolls);
}
}
}
// delay the despawn animation
self->despawned = false;
auto it = Transport::NPCQueues.find(self->id);
if (it == Transport::NPCQueues.end() || it->second.empty())
return;
// rewind or empty the movement queue
if (self->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.
*/
auto& queue = it->second;
for (auto point = queue.front(); point.x != self->spawnX || point.y != self->spawnY; point = queue.front()) {
queue.pop();
queue.push(point);
}
}
else {
Transport::NPCQueues.erase(self->id);
}
}

114
src/MobAI.hpp Normal file
View File

@ -0,0 +1,114 @@
#pragma once
#include "core/Core.hpp"
#include "JSON.hpp"
#include "Entities.hpp"
#include <unordered_map>
#include <string>
namespace MobAI {
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 {
// dead
time_t killedTime = 0;
time_t regenTime = 0;
bool summoned = false;
bool despawned = false; // for the sake of death animations
// roaming
int idleRange = 0;
const int sightRange = 0;
time_t nextMovement = 0;
bool staticPath = false;
int roamX = 0, roamY = 0, roamZ = 0;
// combat
CNSocket *target = nullptr;
time_t nextAttack = 0;
time_t lastDrainTime = 0;
int skillStyle = -1; // -1 for nothing, 0-2 for corruption, -2 for eruption
int hitX = 0, hitY = 0, hitZ = 0; // for use in ability targeting
// group
int groupLeader = 0;
int offsetX = 0, offsetY = 0;
int groupMember[4] = {};
// temporary; until we're sure what's what
nlohmann::json data = {};
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 = AIState::ROAMING;
data = d;
speed = data["m_iRunSpeed"];
regenTime = data["m_iRegenTime"];
idleRange = (int)data["m_iIdleRange"];
level = data["m_iNpcLevel"];
roamX = spawnX;
roamY = spawnY;
roamZ = spawnZ;
offsetX = 0;
offsetY = 0;
// NOTE: there appear to be discrepancies in the dump
hp = maxHealth;
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
Mob(int x, int y, int z, uint64_t iID, int t, nlohmann::json d, int32_t id)
: Mob(x, y, z, 0, iID, t, d, id) {
summoned = true; // will be despawned and deallocated when killed
}
~Mob() {}
virtual int takeDamage(EntityRef src, int amt) override;
virtual void step(time_t currTime) override;
auto operator[](std::string s) {
return data[s];
}
};
namespace MobAI {
extern bool simulateMobs;
// TODO: make this internal later
void incNextMovement(Mob *mob, time_t currTime=0);
bool aggroCheck(Mob *mob, time_t currTime);
void clearDebuff(Mob *mob);
void followToCombat(Mob *mob);
void groupRetreat(Mob *mob);
}

406
src/NPCManager.cpp Normal file
View File

@ -0,0 +1,406 @@
#include "NPCManager.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "settings.hpp"
#include "Combat.hpp"
#include "Missions.hpp"
#include "Chunking.hpp"
#include "Nanos.hpp"
#include "TableData.hpp"
#include "Groups.hpp"
#include "Racing.hpp"
#include "Vendors.hpp"
#include "Abilities.hpp"
#include "Rand.hpp"
#include <cmath>
#include <algorithm>
#include <list>
#include <fstream>
#include <vector>
#include <assert.h>
#include <limits.h>
using namespace NPCManager;
std::unordered_map<int32_t, BaseNPC*> NPCManager::NPCs;
std::map<int32_t, WarpLocation> NPCManager::Warps;
std::vector<WarpLocation> NPCManager::RespawnPoints;
nlohmann::json NPCManager::NPCData;
static std::queue<int32_t> RemovalQueue;
/*
* Initialized at the end of TableData::init().
* This allows us to summon and kill mobs in arbitrary order without
* NPC ID collisions.
*/
int32_t NPCManager::nextId;
void NPCManager::destroyNPC(int32_t id) {
// sanity check
if (NPCs.find(id) == NPCs.end()) {
std::cout << "npc not found: " << id << std::endl;
return;
}
BaseNPC* entity = NPCs[id];
// sanity check
if (!Chunking::chunkExists(entity->chunkPos)) {
std::cout << "chunk not found!" << std::endl;
return;
}
// remove NPC from the chunk
EntityRef ref = {id};
Chunking::untrackEntity(entity->chunkPos, ref);
// remove from viewable chunks
Chunking::removeEntityFromChunks(Chunking::getViewableChunks(entity->chunkPos), ref);
// finally, remove it from the map and free it
NPCs.erase(id);
delete entity;
}
void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I, int angle) {
BaseNPC* npc = NPCs[id];
npc->angle = angle;
ChunkPos oldChunk = npc->chunkPos;
ChunkPos newChunk = Chunking::chunkPosAt(X, Y, I);
npc->x = X;
npc->y = Y;
npc->z = Z;
npc->instanceID = I;
if (oldChunk == newChunk)
return; // didn't change chunks
Chunking::updateEntityChunk({id}, oldChunk, newChunk);
}
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.kind == EntityKind::PLAYER)
ref.sock->sendPacket(buf, type, size);
}
}
}
static void npcBarkHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_BARKER* req = (sP_CL2FE_REQ_BARKER*)data->buf;
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;
}
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 = npcID;
resp.iMissionStringID = missionStringID;
sock->sendPacket(resp, P_FE2CL_REP_BARKER);
}
static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
if (plr->accountLevel > 30)
return;
sP_CL2FE_REQ_NPC_UNSUMMON* req = (sP_CL2FE_REQ_NPC_UNSUMMON*)data->buf;
NPCManager::destroyNPC(req->iNPC_ID);
}
// type must already be checked and updateNPCPosition() must be called on the result
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;
int id = nextId--;
int team = NPCData[type]["m_iTeam"];
BaseNPC *npc = nullptr;
if (team == 2) {
npc = new Mob(spawnX, spawnY, spawnZ, inst, type, NPCData[type], id);
// re-enable respawning, if desired
((Mob*)npc)->summoned = !respawn;
} else
npc = new BaseNPC(0, inst, type, id);
NPCs[id] = npc;
return npc;
}
static void npcSummonHandler(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_NPC_SUMMON*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
int limit = NPCData.back()["m_iNpcNumber"];
// permission & sanity check
if (plr->accountLevel > 30 || req->iNPCType > limit || req->iNPCCnt > 100)
return;
for (int i = 0; i < req->iNPCCnt; i++) {
BaseNPC *npc = summonNPC(plr->x, plr->y, plr->z, plr->instanceID, req->iNPCType);
updateNPCPosition(npc->id, plr->x, plr->y, plr->z, plr->instanceID, 0);
}
}
static void handleWarp(CNSocket* sock, int32_t warpId) {
Player* plr = PlayerManager::getPlayer(sock);
// sanity check
if (Warps.find(warpId) == Warps.end())
return;
if (plr->iPCState & 8) {
// remove the player's vehicle
plr->iPCState &= ~8;
// send to self
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, off);
sock->sendPacket(off, P_FE2CL_PC_VEHICLE_OFF_SUCC);
// send to others
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, chg);
chg.iPC_ID = plr->iID;
chg.iState = plr->iPCState;
PlayerManager::sendToViewable(sock, chg, P_FE2CL_PC_STATE_CHANGE);
}
// std::cerr << "Warped to Map Num:" << Warps[warpId].instanceID << " NPC ID " << Warps[warpId].npcID << std::endl;
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)leader->iID << 32); // upper 32 bits are leader ID
Chunking::createInstance(instanceID);
// save Lair entrance coords as a pseudo-Resurrect 'Em
plr->recallX = Warps[warpId].x;
plr->recallY = Warps[warpId].y;
plr->recallZ = Warps[warpId].z + RESURRECT_HEIGHT;
plr->recallInstance = instanceID;
}
if (plr->group == nullptr)
PlayerManager::sendPlayerTo(sock, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID);
else {
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;
// save Lair entrance coords for everyone else as well
otherPlr->recallX = Warps[warpId].x;
otherPlr->recallY = Warps[warpId].y;
otherPlr->recallZ = Warps[warpId].z + RESURRECT_HEIGHT;
otherPlr->recallInstance = instanceID;
// remove their vehicle if they're on one
if (otherPlr->iPCState & 8) {
// remove the player's vehicle
otherPlr->iPCState &= ~8;
// send to self
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, off);
sockTo->sendPacket(off, P_FE2CL_PC_VEHICLE_OFF_SUCC);
// send to others
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, chg);
chg.iPC_ID = otherPlr->iID;
chg.iState = otherPlr->iPCState;
PlayerManager::sendToViewable(sockTo, chg, P_FE2CL_PC_STATE_CHANGE);
}
PlayerManager::sendPlayerTo(sockTo, Warps[warpId].x, Warps[warpId].y, Warps[warpId].z, instanceID);
}
}
}
else
{
INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_NPC_SUCC, resp); // Can only be used for exiting instances because it sets the instance flag to false
resp.iX = Warps[warpId].x;
resp.iY = Warps[warpId].y;
resp.iZ = Warps[warpId].z;
resp.iCandy = plr->money;
resp.eIL = 4; // do not take away any items
uint64_t fromInstance = plr->instanceID; // pre-warp instance, saved for post-warp
plr->instanceID = INSTANCE_OVERWORLD;
Missions::failInstancedMissions(sock); // fail any instanced missions
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC);
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())
Racing::EPRaces.erase(sock);
// post-warp: check if the source instance has no more players in it and delete it if so
Chunking::destroyInstanceIfEmpty(fromInstance);
}
}
static void npcWarpHandler(CNSocket* sock, CNPacketData* data) {
auto warpNpc = (sP_CL2FE_REQ_PC_WARP_USE_NPC*)data->buf;
handleWarp(sock, warpNpc->iWarpID);
}
static void npcWarpTimeMachine(CNSocket* sock, CNPacketData* data) {
// this is just a warp request
handleWarp(sock, 28);
}
/*
* Helper function to get NPC closest to coordinates in specified chunks
*/
BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z) {
BaseNPC* npc = nullptr;
int lastDist = INT_MAX;
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->kind == EntityKind::PLAYER)
continue;
BaseNPC* npcTemp = (BaseNPC*)ent->getEntity();
int distXY = std::hypot(X - npcTemp->x, Y - npcTemp->y);
int dist = std::hypot(distXY, Z - npcTemp->z);
if (dist < lastDist) {
npc = npcTemp;
lastDist = dist;
}
}
}
return npc;
}
// 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(CombatNPC *npc) {
Mob *oldbody = (Mob*)npc; // adaptium, stun
std::cout << "Lord Fuse stage two" << std::endl;
// Fuse doesn't move
// Blastons, Heal
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2467);
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->x - 600, oldbody->y, oldbody->z, oldbody->instanceID, 2469);
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(CombatNPC *npc) {
Mob *oldbody = (Mob*)npc;
std::cout << "Lord Fuse stage three" << std::endl;
// Cosmix, Damage Point
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2468);
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->x + 600, oldbody->y, oldbody->z, oldbody->instanceID, 2470);
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, AIState::DEAD, lordFuseStageTwo),
NPCEvent(2467, AIState::DEAD, lordFuseStageThree),
};
#pragma endregion NPCEvents
void NPCManager::queueNPCRemoval(int32_t id) {
RemovalQueue.push(id);
}
static void step(CNServer *serv, time_t currTime) {
for (auto& pair : NPCs) {
if (pair.second->kind != EntityKind::COMBAT_NPC && pair.second->kind != EntityKind::MOB)
continue;
auto npc = (CombatNPC*)pair.second;
npc->step(currTime);
}
// deallocate all NPCs queued for removal
while (RemovalQueue.size() > 0) {
NPCManager::destroyNPC(RemovalQueue.front());
RemovalQueue.pop();
}
}
void NPCManager::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_WARP_USE_NPC, npcWarpHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TIME_TO_GO_WARP, npcWarpTimeMachine);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_SUMMON, npcSummonHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_UNSUMMON, npcUnsummonHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_BARKER, npcBarkHandler);
REGISTER_SHARD_TIMER(step, MS_PER_COMBAT_TICK);
}

46
src/NPCManager.hpp Normal file
View File

@ -0,0 +1,46 @@
#pragma once
#include "core/Core.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
typedef void (*NPCEventHandler)(CombatNPC*);
struct NPCEvent {
int32_t npcType;
AIState triggerState;
NPCEventHandler handler;
NPCEvent(int32_t t, AIState tr, NPCEventHandler hndlr)
: npcType(t), triggerState(tr), handler(hndlr) {}
};
namespace NPCManager {
extern std::unordered_map<int32_t, BaseNPC*> NPCs;
extern std::map<int32_t, WarpLocation> Warps;
extern std::vector<WarpLocation> RespawnPoints;
extern std::vector<NPCEvent> NPCEvents;
extern nlohmann::json NPCData;
extern int32_t nextId;
void init();
void queueNPCRemoval(int32_t);
void destroyNPC(int32_t);
void updateNPCPosition(int32_t, int X, int Y, int Z, uint64_t I, int angle);
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);
BaseNPC* getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z);
}

373
src/Nanos.cpp Normal file
View File

@ -0,0 +1,373 @@
#include "Nanos.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Missions.hpp"
#include "Abilities.hpp"
#include "NPCManager.hpp"
#include <cmath>
using namespace Nanos;
std::map<int32_t, NanoData> Nanos::NanoTable;
std::map<int32_t, NanoTuning> Nanos::NanoTunings;
#pragma region Helper methods
void Nanos::addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm) {
if (nanoID <= 0 || nanoID >= NANO_COUNT)
return;
Player *plr = PlayerManager::getPlayer(sock);
int level = plr->level;
#ifndef ACADEMY
level = nanoID < plr->level ? plr->level : nanoID;
/*
* Spend the necessary Fusion Matter.
* Note the use of the not-yet-incremented plr->level as opposed to level.
* Doing it the other way always leaves the FM at 0. Jade totally called it.
*/
plr->level = level;
if (spendfm)
Missions::updateFusionMatter(sock, -(int)Missions::AvatarGrowth[plr->level-1]["m_iReqBlob_NanoCreate"]);
#endif
// Send to client
INITSTRUCT(sP_FE2CL_REP_PC_NANO_CREATE_SUCC, resp);
resp.Nano.iID = nanoID;
resp.Nano.iStamina = 150;
resp.iQuestItemSlotNum = slot;
resp.iPC_Level = level;
resp.iPC_FusionMatter = plr->fusionmatter;
if (plr->activeNano > 0 && plr->activeNano == nanoID)
summonNano(sock, -1); // just unsummon the nano to prevent infinite buffs
// Update player
plr->Nanos[nanoID] = resp.Nano;
sock->sendPacket(resp, P_FE2CL_REP_PC_NANO_CREATE_SUCC);
/*
* iPC_Level in NANO_CREATE_SUCC sets the player's level.
* Other players must be notified of the change as well. Both P_FE2CL_REP_PC_NANO_CREATE and
* P_FE2CL_REP_PC_CHANGE_LEVEL appear to play the same animation, but only the latter affects
* the other player's displayed level.
*/
INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL, resp2);
resp2.iPC_ID = plr->iID;
resp2.iPC_Level = level;
// Update other players' perception of the player's level
PlayerManager::sendToViewable(sock, resp2, P_FE2CL_REP_PC_CHANGE_LEVEL);
}
void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
INITSTRUCT(sP_FE2CL_REP_NANO_ACTIVE_SUCC, resp);
resp.iActiveNanoSlotNum = slot;
Player *plr = PlayerManager::getPlayer(sock);
if (slot > 2 || slot < -1)
return; // sanity check
int16_t nanoID = slot == -1 ? 0 : plr->equippedNanos[slot];
if (slot != -1 && plr->Nanos[nanoID].iSkillID == 0)
return; // prevent powerless nanos from summoning
if (nanoID >= NANO_COUNT || nanoID < 0)
return; // sanity check
plr->activeNano = nanoID;
sNano& nano = plr->Nanos[nanoID];
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
sock->sendPacket(resp, P_FE2CL_REP_NANO_ACTIVE_SUCC);
// 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 = nano;
PlayerManager::sendToViewable(sock, pkt1, P_FE2CL_NANO_ACTIVE);
}
static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
if (skill == nullptr || skill->iNanoID >= NANO_COUNT || skill->iNanoID < 0)
return;
Player *plr = PlayerManager::getPlayer(sock);
if (plr->activeNano > 0 && plr->activeNano == skill->iNanoID)
summonNano(sock, -1); // just unsummon the nano to prevent infinite buffs
sNano nano = plr->Nanos[skill->iNanoID];
nano.iSkillID = skill->iTuneID;
plr->Nanos[skill->iNanoID] = nano;
// Send to client
INITSTRUCT(sP_FE2CL_REP_NANO_TUNE_SUCC, resp);
resp.iNanoID = skill->iNanoID;
resp.iSkillID = skill->iTuneID;
resp.iPC_FusionMatter = plr->fusionmatter;
resp.aItem[9] = plr->Inven[0]; // quick fix to make sure item in slot 0 doesn't get yeeted by default
// check if there's any garbage in the item slot array (this'll happen when a nano station isn't used)
for (int i = 0; i < 10; i++) {
if (skill->aiNeedItemSlotNum[i] < 0 || skill->aiNeedItemSlotNum[i] >= AINVEN_COUNT) {
sock->sendPacket(resp, P_FE2CL_REP_NANO_TUNE_SUCC);
return; // stop execution, don't run consumption logic
}
}
#ifndef ACADEMY
if (plr->fusionmatter < (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"]) // sanity check
return;
#endif
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"];
int reqItemCount = NanoTunings[skill->iTuneID].reqItemCount;
int reqItemID = NanoTunings[skill->iTuneID].reqItems;
int i = 0;
while (reqItemCount > 0 && i < 10) {
sItemBase& item = plr->Inven[skill->aiNeedItemSlotNum[i]];
if (item.iType == 7 && item.iID == reqItemID) {
if (item.iOpt > reqItemCount) {
item.iOpt -= reqItemCount;
reqItemCount = 0;
}
else {
reqItemCount -= item.iOpt;
item.iID = 0;
item.iType = 0;
item.iOpt = 0;
}
}
i++; // next slot
}
resp.iPC_FusionMatter = plr->fusionmatter; // update fusion matter in packet
// update items clientside
for (int i = 0; i < 10; i++) {
if (skill->aiNeedItemSlotNum[i]) { // non-zero check
resp.aItem[i] = plr->Inven[skill->aiNeedItemSlotNum[i]];
resp.aiItemSlotNum[i] = skill->aiNeedItemSlotNum[i];
}
}
sock->sendPacket(resp, P_FE2CL_REP_NANO_TUNE_SUCC);
DEBUGLOG(
std::cout << PlayerManager::getPlayerName(plr) << " set skill id " << skill->iTuneID << " for nano: " << skill->iNanoID << std::endl;
)
}
// 0=A 1=B 2=C -1=Not found
int Nanos::nanoStyle(int nanoID) {
if (nanoID < 1 || nanoID >= (int)NanoTable.size())
return -1;
return NanoTable[nanoID].style;
}
bool Nanos::getNanoBoost(Player* plr) {
for (int i = 0; i < 3; i++)
if (plr->equippedNanos[i] == plr->activeNano)
if (plr->hasBuff(ECSB_STIMPAKSLOT1 + i))
return true;
return false;
}
#pragma endregion
static void nanoEquipHandler(CNSocket* sock, CNPacketData* data) {
auto nano = (sP_CL2FE_REQ_NANO_EQUIP*)data->buf;
INITSTRUCT(sP_FE2CL_REP_NANO_EQUIP_SUCC, resp);
Player *plr = PlayerManager::getPlayer(sock);
// sanity checks
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;
// unsummon nano if replaced
if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum])
summonNano(sock, -1);
sock->sendPacket(resp, P_FE2CL_REP_NANO_EQUIP_SUCC);
}
static void nanoUnEquipHandler(CNSocket* sock, CNPacketData* data) {
auto nano = (sP_CL2FE_REQ_NANO_UNEQUIP*)data->buf;
INITSTRUCT(sP_FE2CL_REP_NANO_UNEQUIP_SUCC, resp);
Player *plr = PlayerManager::getPlayer(sock);
// sanity check
if (nano->iNanoSlotNum > 2 || nano->iNanoSlotNum < 0)
return;
resp.iNanoSlotNum = nano->iNanoSlotNum;
// unsummon nano if removed
if (plr->equippedNanos[nano->iNanoSlotNum] == plr->activeNano)
summonNano(sock, -1);
// update player
plr->equippedNanos[nano->iNanoSlotNum] = 0;
sock->sendPacket(resp, P_FE2CL_REP_NANO_UNEQUIP_SUCC);
}
static void nanoSummonHandler(CNSocket* sock, CNPacketData* data) {
auto pkt = (sP_CL2FE_REQ_NANO_ACTIVE*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
summonNano(sock, pkt->iNanoSlotNum);
DEBUGLOG(
std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano slot: " << pkt->iNanoSlotNum << std::endl;
)
}
static void nanoSkillUseHandler(CNSocket* sock, CNPacketData* data) {
Player *plr = PlayerManager::getPlayer(sock);
// 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;
)
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);
if (plr->Nanos[plr->activeNano].iStamina <= 0)
summonNano(sock, -1);
}
static void nanoSkillSetHandler(CNSocket* sock, CNPacketData* data) {
auto skill = (sP_CL2FE_REQ_NANO_TUNE*)data->buf;
setNanoSkill(sock, skill);
}
static void nanoSkillSetGMHandler(CNSocket* sock, CNPacketData* data) {
auto skillGM = (sP_CL2FE_REQ_NANO_TUNE*)data->buf;
setNanoSkill(sock, skillGM);
}
static void nanoRecallRegisterHandler(CNSocket* sock, CNPacketData* data) {
auto recallData = (sP_CL2FE_REQ_REGIST_RXCOM*)data->buf;
if (NPCManager::NPCs.find(recallData->iNPCID) == NPCManager::NPCs.end())
return;
Player* plr = PlayerManager::getPlayer(sock);
BaseNPC *npc = NPCManager::NPCs[recallData->iNPCID];
INITSTRUCT(sP_FE2CL_REP_REGIST_RXCOM, response);
response.iMapNum = plr->recallInstance = (int32_t)npc->instanceID; // Never going to recall into a Fusion Lair
response.iX = plr->recallX = npc->x;
response.iY = plr->recallY = npc->y;
response.iZ = plr->recallZ = npc->z;
sock->sendPacket(response, P_FE2CL_REP_REGIST_RXCOM);
}
static void nanoRecallHandler(CNSocket* sock, CNPacketData* data) {
auto recallData = (sP_CL2FE_REQ_WARP_USE_RECALL*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(recallData->iGroupMemberID);
if (otherPlr == nullptr)
return;
// ensure the group member is still in the same IZ
if (otherPlr->instanceID != plr->instanceID)
return;
// do not allow hypothetical recall points in lairs to mess with the respawn logic
if (PLAYERID(plr->instanceID) != 0)
return;
if ((int32_t)plr->instanceID == otherPlr->recallInstance)
PlayerManager::sendPlayerTo(sock, otherPlr->recallX, otherPlr->recallY, otherPlr->recallZ, otherPlr->recallInstance);
else {
INITSTRUCT(sP_FE2CL_REP_WARP_USE_RECALL_FAIL, response)
sock->sendPacket(response, P_FE2CL_REP_WARP_USE_RECALL_FAIL);
}
}
static void nanoPotionHandler(CNSocket* sock, CNPacketData* data) {
Player* player = PlayerManager::getPlayer(sock);
// sanity checks
if (player->activeNano == -1 || player->batteryN == 0)
return;
sNano nano = player->Nanos[player->activeNano];
int difference = 150 - nano.iStamina;
if (player->batteryN < difference)
difference = player->batteryN;
if (difference == 0)
return;
INITSTRUCT(sP_FE2CL_REP_CHARGE_NANO_STAMINA, response);
response.iNanoID = nano.iID;
response.iNanoStamina = nano.iStamina + difference;
response.iBatteryN = player->batteryN - difference;
sock->sendPacket(response, P_FE2CL_REP_CHARGE_NANO_STAMINA);
// now update serverside
player->batteryN -= difference;
player->Nanos[nano.iID].iStamina += difference;
}
void Nanos::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_ACTIVE, nanoSummonHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_EQUIP, nanoEquipHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_UNEQUIP, nanoUnEquipHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_TUNE, nanoSkillSetHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GIVE_NANO_SKILL, nanoSkillSetGMHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_SKILL_USE, nanoSkillUseHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_REGIST_RXCOM, nanoRecallRegisterHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_WARP_USE_RECALL, nanoRecallHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_CHARGE_NANO_STAMINA, nanoPotionHandler);
}

29
src/Nanos.hpp Normal file
View File

@ -0,0 +1,29 @@
#pragma once
#include "core/Core.hpp"
#include "Player.hpp"
#include "Abilities.hpp"
#include <map>
struct NanoData {
int style;
};
struct NanoTuning {
int reqItemCount;
int reqItems;
};
namespace Nanos {
extern std::map<int32_t, NanoData> NanoTable;
extern std::map<int32_t, NanoTuning> NanoTunings;
void init();
// Helper methods
void addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm=false);
void summonNano(CNSocket* sock, int slot, bool silent = false);
int nanoStyle(int nanoID);
bool getNanoBoost(Player* plr);
}

View File

@ -1 +0,0 @@
#include "Player.hpp"

View File

@ -1,25 +1,114 @@
#include <string>
#include <cstring>
#pragma once
#ifndef _PLR_HPP
#define _PLR_HPP
#include "core/Core.hpp"
#include "CNProtocol.hpp"
#include "CNStructs.hpp"
#include "Entities.hpp"
#include "Groups.hpp"
struct Player {
int64_t SerialKey;
int32_t iID;
uint64_t FEKey;
#include <vector>
int level;
int HP;
int slot;
sPCStyle PCStyle;
sPCStyle2 PCStyle2;
/* forward declaration(s) */
class Buff;
struct BuffStack;
int x, y, z, angle;
sItemBase Equip[AEQUIP_COUNT];
#define ACTIVE_MISSION_COUNT 6
#define PC_MAXHEALTH(level) (925 + 75 * (level))
struct Player : public Entity, public ICombatant {
int accountId = 0;
int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums)
int32_t iID = 0;
int level = 0;
int HP = 0;
int slot = 0; // player slot, not nano slot
int16_t mentor = 0;
int32_t money = 0;
int32_t fusionmatter = 0;
int32_t batteryW = 0;
int32_t batteryN = 0;
sPCStyle PCStyle = {};
sPCStyle2 PCStyle2 = {};
sNano Nanos[NANO_COUNT] = {}; // acquired nanos
int equippedNanos[3] = {};
int activeNano = 0; // active nano (index into Nanos)
int8_t iPCState = 0;
int32_t iWarpLocationFlag = 0;
int64_t aSkywayLocationFlag[2] = {};
int8_t iSpecialState = 0;
std::unordered_map<int, Buff*> buffs = {};
int angle = 0;
int lastX = 0, lastY = 0, lastZ = 0, lastAngle = 0;
int recallX = 0, recallY = 0, recallZ = 0, recallInstance = 0; // also Lair entrances
sItemBase Equip[AEQUIP_COUNT] = {};
sItemBase Inven[AINVEN_COUNT] = {};
sItemBase Bank[ABANK_COUNT] = {};
sItemTrade Trade[12] = {};
int32_t moneyInTrade = 0;
bool isTrading = false;
bool isTradeConfirm = false;
bool inCombat = false;
bool onMonkey = false;
int healCooldown = 0;
int pointDamage = 0;
int groupDamage = 0;
int fireRate = 0;
int defense = 0;
int64_t aQuestFlag[16] = {};
int tasks[ACTIVE_MISSION_COUNT] = {};
int RemainingNPCCount[ACTIVE_MISSION_COUNT][3] = {};
sItemBase QInven[AQINVEN_COUNT] = {};
int32_t CurrentMissionID = 0;
sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {};
Group* group = nullptr;
bool notify = false;
bool hidden = false;
bool unwarpable = false;
bool initialLoadDone = false;
int64_t buddyIDs[50] = {};
bool isBuddyBlocked[50] = {};
uint64_t iFirstUseFlag[2] = {};
time_t lastHeartbeat = 0;
int suspicionRating = 0;
time_t lastShot = 0;
std::vector<sItemBase> buyback = {};
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();
};
#endif

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +1,53 @@
#ifndef _PM_HPP
#define _PM_HPP
#pragma once
#include "core/Core.hpp"
#include "Player.hpp"
#include "CNProtocol.hpp"
#include "CNStructs.hpp"
#include "CNShardServer.hpp"
#include "Chunking.hpp"
#include "Transport.hpp"
#include "Entities.hpp"
#include <map>
#include <list>
struct PlayerView {
std::list<CNSocket*> viewable;
Player plr;
};
namespace PlayerManager {
extern std::map<CNSocket*, PlayerView> players;
extern std::map<CNSocket*, Player*> players;
void init();
void addPlayer(CNSocket* key, Player plr);
void removePlayer(CNSocket* key);
Player getPlayer(CNSocket* key);
void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z);
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 enterPlayer(CNSocket* sock, CNPacketData* data);
void loadPlayer(CNSocket* sock, CNPacketData* data);
void movePlayer(CNSocket* sock, CNPacketData* data);
void stopPlayer(CNSocket* sock, CNPacketData* data);
void jumpPlayer(CNSocket* sock, CNPacketData* data);
void movePlatformPlayer(CNSocket* sock, CNPacketData* data);
void gotoPlayer(CNSocket* sock, CNPacketData* data);
void setSpecialPlayer(CNSocket* sock, CNPacketData* data);
void sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I);
void sendPlayerTo(CNSocket* sock, int X, int Y, int Z);
Player *getPlayer(CNSocket* key);
std::string getPlayerName(Player *plr, bool id=true);
bool isAccountInUse(int accountId);
void exitDuplicate(int accountId);
Player *getPlayerFromID(int32_t iID);
CNSocket *getSockFromID(int32_t iID);
CNSocket *getSockFromName(std::string firstname, std::string lastname);
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
template<class T>
void sendToViewable(CNSocket *sock, T& pkt, uint32_t type) {
Player* plr = getPlayer(sock);
for (auto it = plr->viewableChunks.begin(); it != plr->viewableChunks.end(); it++) {
Chunk* chunk = *it;
for (const EntityRef& ref : chunk->entities) {
if (ref.kind != EntityKind::PLAYER || ref.sock == sock)
continue;
ref.sock->sendPacket(pkt, type);
}
}
}
}
#endif

290
src/PlayerMovement.cpp Normal file
View File

@ -0,0 +1,290 @@
#include "PlayerMovement.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "TableData.hpp"
#include "core/Core.hpp"
static void movePlayer(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
auto* moveData = (sP_CL2FE_REQ_PC_MOVE*)data->buf;
PlayerManager::updatePlayerPosition(sock, moveData->iX, moveData->iY, moveData->iZ, plr->instanceID, moveData->iAngle);
uint64_t tm = getTime();
INITSTRUCT(sP_FE2CL_PC_MOVE, moveResponse);
moveResponse.iID = plr->iID;
moveResponse.cKeyValue = moveData->cKeyValue;
moveResponse.iX = moveData->iX;
moveResponse.iY = moveData->iY;
moveResponse.iZ = moveData->iZ;
moveResponse.iAngle = moveData->iAngle;
moveResponse.fVX = moveData->fVX;
moveResponse.fVY = moveData->fVY;
moveResponse.fVZ = moveData->fVZ;
moveResponse.iSpeed = moveData->iSpeed;
moveResponse.iCliTime = moveData->iCliTime; // maybe don't send this??? seems unneeded...
moveResponse.iSvrTime = tm;
PlayerManager::sendToViewable(sock, moveResponse, P_FE2CL_PC_MOVE);
// [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->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
Vec3 to = {
(int)(follower->x + (moveData->iX - follower->x) * drag),
(int)(follower->y + (moveData->iY - follower->y) * drag),
(int)(follower->z + (moveData->iZ - follower->z) * drag)
};
// 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->id] = queue;
}
}
static void stopPlayer(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
auto stopData = (sP_CL2FE_REQ_PC_STOP*)data->buf;
PlayerManager::updatePlayerPosition(sock, stopData->iX, stopData->iY, stopData->iZ, plr->instanceID, plr->angle);
uint64_t tm = getTime();
INITSTRUCT(sP_FE2CL_PC_STOP, stopResponse);
stopResponse.iID = plr->iID;
stopResponse.iX = stopData->iX;
stopResponse.iY = stopData->iY;
stopResponse.iZ = stopData->iZ;
stopResponse.iCliTime = stopData->iCliTime; // maybe don't send this??? seems unneeded...
stopResponse.iSvrTime = tm;
PlayerManager::sendToViewable(sock, stopResponse, P_FE2CL_PC_STOP);
}
static void jumpPlayer(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
auto jumpData = (sP_CL2FE_REQ_PC_JUMP*)data->buf;
PlayerManager::updatePlayerPosition(sock, jumpData->iX, jumpData->iY, jumpData->iZ, plr->instanceID, jumpData->iAngle);
uint64_t tm = getTime();
INITSTRUCT(sP_FE2CL_PC_JUMP, jumpResponse);
jumpResponse.iID = plr->iID;
jumpResponse.cKeyValue = jumpData->cKeyValue;
jumpResponse.iX = jumpData->iX;
jumpResponse.iY = jumpData->iY;
jumpResponse.iZ = jumpData->iZ;
jumpResponse.iAngle = jumpData->iAngle;
jumpResponse.iVX = jumpData->iVX;
jumpResponse.iVY = jumpData->iVY;
jumpResponse.iVZ = jumpData->iVZ;
jumpResponse.iSpeed = jumpData->iSpeed;
jumpResponse.iCliTime = jumpData->iCliTime; // maybe don't send this??? seems unneeded...
jumpResponse.iSvrTime = tm;
PlayerManager::sendToViewable(sock, jumpResponse, P_FE2CL_PC_JUMP);
}
static void jumppadPlayer(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
auto jumppadData = (sP_CL2FE_REQ_PC_JUMPPAD*)data->buf;
PlayerManager::updatePlayerPosition(sock, jumppadData->iX, jumppadData->iY, jumppadData->iZ, plr->instanceID, jumppadData->iAngle);
uint64_t tm = getTime();
INITSTRUCT(sP_FE2CL_PC_JUMPPAD, jumppadResponse);
jumppadResponse.iPC_ID = plr->iID;
jumppadResponse.cKeyValue = jumppadData->cKeyValue;
jumppadResponse.iX = jumppadData->iX;
jumppadResponse.iY = jumppadData->iY;
jumppadResponse.iZ = jumppadData->iZ;
jumppadResponse.iVX = jumppadData->iVX;
jumppadResponse.iVY = jumppadData->iVY;
jumppadResponse.iVZ = jumppadData->iVZ;
jumppadResponse.iCliTime = jumppadData->iCliTime;
jumppadResponse.iSvrTime = tm;
PlayerManager::sendToViewable(sock, jumppadResponse, P_FE2CL_PC_JUMPPAD);
}
static void launchPlayer(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
auto launchData = (sP_CL2FE_REQ_PC_LAUNCHER*)data->buf;
PlayerManager::updatePlayerPosition(sock, launchData->iX, launchData->iY, launchData->iZ, plr->instanceID, launchData->iAngle);
uint64_t tm = getTime();
INITSTRUCT(sP_FE2CL_PC_LAUNCHER, launchResponse);
launchResponse.iPC_ID = plr->iID;
launchResponse.iX = launchData->iX;
launchResponse.iY = launchData->iY;
launchResponse.iZ = launchData->iZ;
launchResponse.iVX = launchData->iVX;
launchResponse.iVY = launchData->iVY;
launchResponse.iVZ = launchData->iVZ;
launchResponse.iSpeed = launchData->iSpeed;
launchResponse.iAngle = launchData->iAngle;
launchResponse.iCliTime = launchData->iCliTime;
launchResponse.iSvrTime = tm;
PlayerManager::sendToViewable(sock, launchResponse, P_FE2CL_PC_LAUNCHER);
}
static void ziplinePlayer(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
sP_CL2FE_REQ_PC_ZIPLINE* ziplineData = (sP_CL2FE_REQ_PC_ZIPLINE*)data->buf;
PlayerManager::updatePlayerPosition(sock, ziplineData->iX, ziplineData->iY, ziplineData->iZ, plr->instanceID, ziplineData->iAngle);
uint64_t tm = getTime();
INITSTRUCT(sP_FE2CL_PC_ZIPLINE, ziplineResponse);
ziplineResponse.iPC_ID = plr->iID;
ziplineResponse.iCliTime = ziplineData->iCliTime;
ziplineResponse.iSvrTime = tm;
ziplineResponse.iX = ziplineData->iX;
ziplineResponse.iY = ziplineData->iY;
ziplineResponse.iZ = ziplineData->iZ;
ziplineResponse.fVX = ziplineData->fVX;
ziplineResponse.fVY = ziplineData->fVY;
ziplineResponse.fVZ = ziplineData->fVZ;
ziplineResponse.fMovDistance = ziplineData->fMovDistance;
ziplineResponse.fMaxDistance = ziplineData->fMaxDistance;
ziplineResponse.fDummy = ziplineData->fDummy; // wtf is this for?
ziplineResponse.iStX = ziplineData->iStX;
ziplineResponse.iStY = ziplineData->iStY;
ziplineResponse.iStZ = ziplineData->iStZ;
ziplineResponse.bDown = ziplineData->bDown;
ziplineResponse.iSpeed = ziplineData->iSpeed;
ziplineResponse.iAngle = ziplineData->iAngle;
ziplineResponse.iRollMax = ziplineData->iRollMax;
ziplineResponse.iRoll = ziplineData->iRoll;
PlayerManager::sendToViewable(sock, ziplineResponse, P_FE2CL_PC_ZIPLINE);
}
static void movePlatformPlayer(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
auto platformData = (sP_CL2FE_REQ_PC_MOVEPLATFORM*)data->buf;
PlayerManager::updatePlayerPosition(sock, platformData->iX, platformData->iY, platformData->iZ, plr->instanceID, platformData->iAngle);
uint64_t tm = getTime();
INITSTRUCT(sP_FE2CL_PC_MOVEPLATFORM, platResponse);
platResponse.iPC_ID = plr->iID;
platResponse.iCliTime = platformData->iCliTime;
platResponse.iSvrTime = tm;
platResponse.iX = platformData->iX;
platResponse.iY = platformData->iY;
platResponse.iZ = platformData->iZ;
platResponse.iAngle = platformData->iAngle;
platResponse.fVX = platformData->fVX;
platResponse.fVY = platformData->fVY;
platResponse.fVZ = platformData->fVZ;
platResponse.iLcX = platformData->iLcX;
platResponse.iLcY = platformData->iLcY;
platResponse.iLcZ = platformData->iLcZ;
platResponse.iSpeed = platformData->iSpeed;
platResponse.bDown = platformData->bDown;
platResponse.cKeyValue = platformData->cKeyValue;
platResponse.iPlatformID = platformData->iPlatformID;
PlayerManager::sendToViewable(sock, platResponse, P_FE2CL_PC_MOVEPLATFORM);
}
static void moveSliderPlayer(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
auto sliderData = (sP_CL2FE_REQ_PC_MOVETRANSPORTATION*)data->buf;
PlayerManager::updatePlayerPosition(sock, sliderData->iX, sliderData->iY, sliderData->iZ, plr->instanceID, sliderData->iAngle);
uint64_t tm = getTime();
INITSTRUCT(sP_FE2CL_PC_MOVETRANSPORTATION, sliderResponse);
sliderResponse.iPC_ID = plr->iID;
sliderResponse.iCliTime = sliderData->iCliTime;
sliderResponse.iSvrTime = tm;
sliderResponse.iX = sliderData->iX;
sliderResponse.iY = sliderData->iY;
sliderResponse.iZ = sliderData->iZ;
sliderResponse.iAngle = sliderData->iAngle;
sliderResponse.fVX = sliderData->fVX;
sliderResponse.fVY = sliderData->fVY;
sliderResponse.fVZ = sliderData->fVZ;
sliderResponse.iLcX = sliderData->iLcX;
sliderResponse.iLcY = sliderData->iLcY;
sliderResponse.iLcZ = sliderData->iLcZ;
sliderResponse.iSpeed = sliderData->iSpeed;
sliderResponse.cKeyValue = sliderData->cKeyValue;
sliderResponse.iT_ID = sliderData->iT_ID;
PlayerManager::sendToViewable(sock, sliderResponse, P_FE2CL_PC_MOVETRANSPORTATION);
}
static void moveSlopePlayer(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
sP_CL2FE_REQ_PC_SLOPE* slopeData = (sP_CL2FE_REQ_PC_SLOPE*)data->buf;
PlayerManager::updatePlayerPosition(sock, slopeData->iX, slopeData->iY, slopeData->iZ, plr->instanceID, slopeData->iAngle);
uint64_t tm = getTime();
INITSTRUCT(sP_FE2CL_PC_SLOPE, slopeResponse);
slopeResponse.iPC_ID = plr->iID;
slopeResponse.iCliTime = slopeData->iCliTime;
slopeResponse.iSvrTime = tm;
slopeResponse.iX = slopeData->iX;
slopeResponse.iY = slopeData->iY;
slopeResponse.iZ = slopeData->iZ;
slopeResponse.iAngle = slopeData->iAngle;
slopeResponse.fVX = slopeData->fVX;
slopeResponse.fVY = slopeData->fVY;
slopeResponse.fVZ = slopeData->fVZ;
slopeResponse.iSpeed = slopeData->iSpeed;
slopeResponse.cKeyValue = slopeData->cKeyValue;
slopeResponse.iSlopeID = slopeData->iSlopeID;
PlayerManager::sendToViewable(sock, slopeResponse, P_FE2CL_PC_SLOPE);
}
void PlayerMovement::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_MOVE, movePlayer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_STOP, stopPlayer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_JUMP, jumpPlayer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_JUMPPAD, jumppadPlayer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_LAUNCHER, launchPlayer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ZIPLINE, ziplinePlayer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_MOVEPLATFORM, movePlatformPlayer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_MOVETRANSPORTATION, moveSliderPlayer);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_SLOPE, moveSlopePlayer);
}

5
src/PlayerMovement.hpp Normal file
View File

@ -0,0 +1,5 @@
#pragma once
namespace PlayerMovement {
void init();
};

188
src/Racing.cpp Normal file
View File

@ -0,0 +1,188 @@
#include "Racing.hpp"
#include "db/Database.hpp"
#include "servers/CNShardServer.hpp"
#include "NPCManager.hpp"
#include "PlayerManager.hpp"
#include "Missions.hpp"
#include "Items.hpp"
using namespace Racing;
std::map<int32_t, EPInfo> Racing::EPData;
std::map<CNSocket*, EPRace> Racing::EPRaces;
std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> Racing::EPRewards;
static void racingStart(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_EP_RACE_START*)data->buf;
if (NPCManager::NPCs.find(req->iStartEcomID) == NPCManager::NPCs.end())
return; // starting line agent not found
int mapNum = MAPNUM(NPCManager::NPCs[req->iStartEcomID]->instanceID);
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
return; // IZ not found
// make ongoing race entry
EPRace race = { {}, req->iEPRaceMode, req->iEPTicketItemSlotNum, getTime() / 1000 };
EPRaces[sock] = race;
INITSTRUCT(sP_FE2CL_REP_EP_RACE_START_SUCC, resp);
resp.iStartTick = 0; // ignored
resp.iLimitTime = EPData[mapNum].maxTime;
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_START_SUCC);
}
static void racingGetPod(CNSocket* sock, CNPacketData* data) {
if (EPRaces.find(sock) == EPRaces.end())
return; // race not found
auto req = (sP_CL2FE_REQ_EP_GET_RING*)data->buf;
if (EPRaces[sock].collectedRings.count(req->iRingLID))
return; // can't collect the same ring twice
// without an anticheat system, we really don't have a choice but to honor the request
// TODO: proximity check so players can't cheat the race by replaying packets
EPRaces[sock].collectedRings.insert(req->iRingLID);
INITSTRUCT(sP_FE2CL_REP_EP_GET_RING_SUCC, resp);
resp.iRingLID = req->iRingLID;
resp.iRingCount_Get = EPRaces[sock].collectedRings.size();
sock->sendPacket(resp, P_FE2CL_REP_EP_GET_RING_SUCC);
}
static void racingCancel(CNSocket* sock, CNPacketData* data) {
if (EPRaces.find(sock) == EPRaces.end())
return; // race not found
Player* plr = PlayerManager::getPlayer(sock);
EPRaces.erase(sock);
INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp);
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC);
/*
* 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) {
if (EPRaces.find(sock) == EPRaces.end())
return; // race not found
auto req = (sP_CL2FE_REQ_EP_RACE_END*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
if (NPCManager::NPCs.find(req->iEndEcomID) == NPCManager::NPCs.end())
return; // finish line agent not found
int mapNum = MAPNUM(NPCManager::NPCs[req->iEndEcomID]->instanceID);
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
return; // IZ not found
EPInfo& epInfo = EPData[mapNum];
EPRace& epRace = EPRaces[sock];
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 = epInfo.EPID;
postRanking.PlayerID = plr->iID;
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(epInfo.EPID, plr->iID);
INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp);
// get rank scores and rewards
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 (topRank < maxRank && rankScores->at(topRank) > topRankingPlayer.Score)
topRank++;
resp.iEPTopRank = topRank + 1;
resp.iEPTopRingCount = topRankingPlayer.RingCount;
resp.iEPTopScore = topRankingPlayer.Score;
resp.iEPTopTime = topRankingPlayer.Time;
// this ranking
int rank = 0;
while (rank < maxRank && rankScores->at(rank) > postRanking.Score)
rank++;
resp.iEPRank = rank + 1;
resp.iEPRingCnt = postRanking.RingCount;
resp.iEPScore = postRanking.Score;
resp.iEPRaceTime = postRanking.Time;
resp.iEPRaceMode = EPRaces[sock].mode;
resp.iEPRewardFM = fm;
Missions::updateFusionMatter(sock, resp.iEPRewardFM);
resp.iFusionMatter = plr->fusionmatter;
resp.iFatigue = 50;
resp.iFatigue_Level = 1;
sItemReward reward;
reward.iSlotNum = Items::findFreeSlot(plr);
reward.eIL = 1;
sItemBase item;
item.iID = rankRewards->at(rank); // rank scores and rewards line up
item.iType = 9;
item.iOpt = 1;
item.iTimeLimit = 0;
reward.sItem = item;
if (reward.iSlotNum > -1 && reward.sItem.iID != 0) {
resp.RewardItem = reward;
plr->Inven[reward.iSlotNum] = item;
}
EPRaces.erase(sock);
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_END_SUCC);
}
void Racing::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_RACE_START, racingStart);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_GET_RING, racingGetPod);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_RACE_CANCEL, racingCancel);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_EP_RACE_END, racingEnd);
}

29
src/Racing.hpp Normal file
View File

@ -0,0 +1,29 @@
#pragma once
#include "core/Core.hpp"
#include <map>
#include <vector>
#include <set>
struct EPInfo {
// 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 {
std::set<int> collectedRings;
int mode, ticketSlot;
time_t startTime;
};
namespace Racing {
extern std::map<int32_t, EPInfo> EPData;
extern std::map<CNSocket*, EPRace> EPRaces;
extern std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> EPRewards;
void init();
}

91
src/Rand.cpp Normal file
View File

@ -0,0 +1,91 @@
#include "Rand.hpp"
#include "core/Core.hpp"
std::unique_ptr<std::mt19937> Rand::generator;
int32_t Rand::rand(int32_t startInclusive, int32_t endExclusive) {
std::uniform_int_distribution<int32_t> dist(startInclusive, endExclusive - 1);
return dist(*Rand::generator);
}
int32_t Rand::rand(int32_t endExclusive) {
return Rand::rand(0, endExclusive);
}
int32_t Rand::rand() {
return Rand::rand(0, INT32_MAX);
}
int32_t Rand::randWeighted(const std::vector<int32_t>& weights) {
std::discrete_distribution<int32_t> dist(weights.begin(), weights.end());
return dist(*Rand::generator);
}
float Rand::randFloat(float startInclusive, float endExclusive) {
std::uniform_real_distribution<float> dist(startInclusive, endExclusive);
return dist(*Rand::generator);
}
float Rand::randFloat(float endExclusive) {
return Rand::randFloat(0.0f, endExclusive);
}
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));
}

23
src/Rand.hpp Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include <random>
#include <memory>
#include <vector>
namespace Rand {
extern std::unique_ptr<std::mt19937> generator;
void init(uint64_t seed);
int32_t rand(int32_t startInclusive, int32_t endExclusive);
int32_t rand(int32_t endExclusive);
int32_t 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();
};

1411
src/TableData.cpp Normal file

File diff suppressed because it is too large Load Diff

32
src/TableData.hpp Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include "JSON.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;
const int MOB_ID_OFFSET = 10000;
const int MOB_GROUP_ID_OFFSET = 20000;
// typedef for JSON object because I don't want to type nlohmann::json every time
typedef nlohmann::json json;
namespace TableData {
extern std::map<int32_t, std::vector<Vec3>> RunningSkywayRoutes;
extern std::map<int32_t, int> RunningNPCRotations;
extern std::map<int32_t, int> RunningNPCMapNumbers;
extern std::unordered_map<int32_t, std::pair<BaseNPC*, std::vector<BaseNPC*>>> RunningNPCPaths; // player ID -> following NPC
extern std::vector<NPCPath> FinishedNPCPaths; // NPC ID -> path
extern std::map<int32_t, BaseNPC*> RunningMobs;
extern std::map<int32_t, BaseNPC*> RunningGroups;
extern std::map<int32_t, BaseNPC*> RunningEggs;
void init();
void flush();
}

470
src/Trading.cpp Normal file
View File

@ -0,0 +1,470 @@
#include "Trading.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "db/Database.hpp"
using namespace Trading;
static bool doTrade(Player* plr, Player* plr2) {
// init dummy inventories
sItemBase plrInven[AINVEN_COUNT];
sItemBase plr2Inven[AINVEN_COUNT];
memcpy(plrInven, plr->Inven, AINVEN_COUNT * sizeof(sItemBase));
memcpy(plr2Inven, plr2->Inven, AINVEN_COUNT * sizeof(sItemBase));
for (int i = 0; i < 5; i++) {
// remove items offered by us
if (plr->Trade[i].iID != 0) {
if (plrInven[plr->Trade[i].iInvenNum].iID == 0
|| plr->Trade[i].iID != plrInven[plr->Trade[i].iInvenNum].iID
|| plr->Trade[i].iType != plrInven[plr->Trade[i].iInvenNum].iType) // pulling a fast one on us
return false;
if (plr->Trade[i].iOpt < 1) {
std::cout << "[WARN] Player tried trading an iOpt < 1 amount" << std::endl;
plr->Trade[i].iOpt = 1;
}
// for stacked items
plrInven[plr->Trade[i].iInvenNum].iOpt -= plr->Trade[i].iOpt;
if (plrInven[plr->Trade[i].iInvenNum].iOpt == 0) {
plrInven[plr->Trade[i].iInvenNum].iID = 0;
plrInven[plr->Trade[i].iInvenNum].iType = 0;
plrInven[plr->Trade[i].iInvenNum].iOpt = 0;
} else if (plrInven[plr->Trade[i].iInvenNum].iOpt < 0) { // another dupe attempt
return false;
}
}
if (plr2->Trade[i].iID != 0) {
if (plr2Inven[plr2->Trade[i].iInvenNum].iID == 0
|| plr2->Trade[i].iID != plr2Inven[plr2->Trade[i].iInvenNum].iID
|| plr2->Trade[i].iType != plr2Inven[plr2->Trade[i].iInvenNum].iType) // pulling a fast one on us
return false;
if (plr2->Trade[i].iOpt < 1) {
std::cout << "[WARN] Player tried trading an iOpt < 1 amount" << std::endl;
plr2->Trade[i].iOpt = 1;
}
// for stacked items
plr2Inven[plr2->Trade[i].iInvenNum].iOpt -= plr2->Trade[i].iOpt;
if (plr2Inven[plr2->Trade[i].iInvenNum].iOpt == 0) {
plr2Inven[plr2->Trade[i].iInvenNum].iID = 0;
plr2Inven[plr2->Trade[i].iInvenNum].iType = 0;
plr2Inven[plr2->Trade[i].iInvenNum].iOpt = 0;
} else if (plr2Inven[plr2->Trade[i].iInvenNum].iOpt < 0) { // another dupe attempt
return false;
}
}
// add items offered to us
if (plr2->Trade[i].iID != 0) {
for (int n = 0; n < AINVEN_COUNT; n++) {
if (plrInven[n].iID == 0) {
plrInven[n].iID = plr2->Trade[i].iID;
plrInven[n].iType = plr2->Trade[i].iType;
plrInven[n].iOpt = plr2->Trade[i].iOpt;
plr2->Trade[i].iInvenNum = n;
break;
}
if (n >= AINVEN_COUNT - 1)
return false; // not enough space
}
}
if (plr->Trade[i].iID != 0) {
for (int n = 0; n < AINVEN_COUNT; n++) {
if (plr2Inven[n].iID == 0) {
plr2Inven[n].iID = plr->Trade[i].iID;
plr2Inven[n].iType = plr->Trade[i].iType;
plr2Inven[n].iOpt = plr->Trade[i].iOpt;
plr->Trade[i].iInvenNum = n;
break;
}
if (n >= AINVEN_COUNT - 1)
return false; // not enough space
}
}
}
// if everything went well, back into player inventory it goes
memcpy(plr->Inven, plrInven, AINVEN_COUNT * sizeof(sItemBase));
memcpy(plr2->Inven, plr2Inven, AINVEN_COUNT * sizeof(sItemBase));
return true;
}
static void tradeOffer(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_OFFER* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
if (otherSock == nullptr)
return;
Player* plr = PlayerManager::getPlayer(otherSock);
if (plr->isTrading) {
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, resp);
resp.iID_Request = pacdat->iID_To;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
return; // prevent trading with a player already trading
}
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER, 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, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER));
}
static void tradeOfferAccept(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
Player* plr = PlayerManager::getPlayer(sock);
Player* plr2 = PlayerManager::getPlayer(otherSock);
if (plr2->isTrading) {
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, resp);
resp.iID_Request = pacdat->iID_From;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL));
return; // prevent trading with a player already trading
}
// clearing up trade slots
plr->moneyInTrade = 0;
plr2->moneyInTrade = 0;
memset(&plr->Trade, 0, sizeof(plr->Trade));
memset(&plr2->Trade, 0, sizeof(plr2->Trade));
// marking players as traders
plr->isTrading = true;
plr2->isTrading = true;
// marking players as unconfirmed
plr->isTradeConfirm = false;
plr2->isTradeConfirm = false;
// inform the other player that offer is accepted
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_SUCC));
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_OFFER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_OFFER_SUCC));
}
static void tradeOfferRefusal(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL* pacdat = (sP_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL*)data->buf;
CNSocket* otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_OFFER_REFUSAL, 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_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;
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
if (pacdat->iID_Request == pacdat->iID_From)
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
else
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
Player* plr = PlayerManager::getPlayer(sock);
Player* plr2 = PlayerManager::getPlayer(otherSock);
if (!(plr->isTrading && plr2->isTrading)) { // both players must be trading
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_ABORT, resp);
resp.iID_Request = plr2->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;
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;
}
// send the confirm packet
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM));
if (!(plr2->isTradeConfirm)) {
plr->isTradeConfirm = true;
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CONFIRM, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM));
return;
}
// both players are no longer trading
plr->isTrading = false;
plr2->isTrading = false;
plr->isTradeConfirm = false;
plr2->isTradeConfirm = false;
if (doTrade(plr, plr2)) { // returns false if not enough slots
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, resp2);
resp2.iID_Request = pacdat->iID_Request;
resp2.iID_From = pacdat->iID_From;
resp2.iID_To = pacdat->iID_To;
plr->money = plr->money + plr2->moneyInTrade - plr->moneyInTrade;
resp2.iCandy = plr->money;
memcpy(resp2.Item, plr2->Trade, sizeof(plr2->Trade));
memcpy(resp2.ItemStay, plr->Trade, sizeof(plr->Trade));
sock->sendPacket((void*)&resp2, P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_SUCC));
plr2->money = plr2->money + plr->moneyInTrade - plr2->moneyInTrade;
resp2.iCandy = plr2->money;
memcpy(resp2.Item, plr->Trade, sizeof(plr->Trade));
memcpy(resp2.ItemStay, plr2->Trade, sizeof(plr2->Trade));
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 = 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 = 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) {
sP_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL* pacdat = (sP_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL*)data->buf;
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
if (pacdat->iID_Request == pacdat->iID_From)
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
else
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
return;
Player* plr = PlayerManager::getPlayer(sock);
Player* plr2 = PlayerManager::getPlayer(otherSock);
// both players are not trading nor are in a confirmed state
plr->isTrading = false;
plr->isTradeConfirm = false;
plr2->isTrading = false;
plr2->isTradeConfirm = false;
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_CONFIRM_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_CONFIRM_CANCEL, sizeof(sP_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL));
}
static void tradeRegisterItem(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_ITEM_REGISTER* pacdat = (sP_CL2FE_REQ_PC_TRADE_ITEM_REGISTER*)data->buf;
if (pacdat->Item.iSlotNum < 0 || pacdat->Item.iSlotNum > 4)
return; // sanity check, there are only 5 trade slots
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
if (pacdat->iID_Request == pacdat->iID_From)
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
else
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
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.
int count = 0;
for (int i = 0; i < 5; i++) {
if (plr->Trade[i].iInvenNum == pacdat->Item.iInvenNum)
count += plr->Trade[i].iOpt;
}
INITSTRUCT(sP_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC, resp);
resp.iID_Request = pacdat->iID_Request;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
resp.TradeItem = pacdat->Item;
resp.InvenItem = pacdat->Item;
resp.InvenItem.iOpt = plr->Inven[pacdat->Item.iInvenNum].iOpt - count; // subtract this count
if (resp.InvenItem.iOpt < 0) // negative count items, doTrade() will block this later on
std::cout << "[WARN] tradeRegisterItem: an item went negative count client side." << std::endl;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC));
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC));
}
static void tradeUnregisterItem(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER* pacdat = (sP_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER*)data->buf;
if (pacdat->Item.iSlotNum < 0 || pacdat->Item.iSlotNum > 4)
return; // sanity check, there are only 5 trade slots
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
if (pacdat->iID_Request == pacdat->iID_From)
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
else
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
if (otherSock == nullptr)
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;
resp.iID_From = pacdat->iID_From;
resp.iID_To = pacdat->iID_To;
resp.TradeItem = pacdat->Item;
resp.InvenItem = plr->Trade[pacdat->Item.iSlotNum];
memset(&plr->Trade[pacdat->Item.iSlotNum], 0, sizeof(plr->Trade[pacdat->Item.iSlotNum])); // clean up item slot
// 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.
int count = 0;
for (int i = 0; i < 5; i++) {
if (plr->Trade[i].iInvenNum == resp.InvenItem.iInvenNum)
count += plr->Trade[i].iOpt;
}
resp.InvenItem.iOpt = plr->Inven[resp.InvenItem.iInvenNum].iOpt - count; // subtract this count
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC));
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC));
}
static void tradeRegisterCash(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_CASH_REGISTER* pacdat = (sP_CL2FE_REQ_PC_TRADE_CASH_REGISTER*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
if (pacdat->iCandy < 0 || pacdat->iCandy > plr->money)
return; // famous glitch, begone
CNSocket* otherSock; // weird flip flop because we need to know who the other player is
if (pacdat->iID_Request == pacdat->iID_From)
otherSock = PlayerManager::getSockFromID(pacdat->iID_To);
else
otherSock = PlayerManager::getSockFromID(pacdat->iID_From);
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;
resp.iID_To = pacdat->iID_To;
resp.iCandy = pacdat->iCandy;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC));
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC, sizeof(sP_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC));
plr->moneyInTrade = pacdat->iCandy;
plr->isTradeConfirm = false;
}
void Trading::init() {
// Trade handlers
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);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER, tradeUnregisterItem);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TRADE_CASH_REGISTER, tradeRegisterCash);
}

5
src/Trading.hpp Normal file
View File

@ -0,0 +1,5 @@
#pragma once
namespace Trading {
void init();
}

441
src/Transport.cpp Normal file
View File

@ -0,0 +1,441 @@
#include "Transport.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "Nanos.hpp"
#include "TableData.hpp"
#include "Entities.hpp"
#include "NPCManager.hpp"
#include "MobAI.hpp"
#include <unordered_map>
#include <cmath>
using namespace Transport;
std::map<int32_t, TransportRoute> Transport::Routes;
std::map<int32_t, TransportLocation> Transport::Locations;
std::vector<NPCPath> Transport::NPCPaths;
std::map<int32_t, std::queue<Vec3>> Transport::SkywayPaths;
std::unordered_map<CNSocket*, std::queue<Vec3>> Transport::SkywayQueues;
std::unordered_map<int32_t, std::queue<Vec3>> Transport::NPCQueues;
static void transportRegisterLocationHandler(CNSocket* sock, CNPacketData* data) {
auto transport = (sP_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
bool newReg = false; // this is a new registration
//std::cout << "request to register transport, eTT " << transport->eTT << ", locID " << transport->iLocationID << ", npc " << transport->iNPC_ID << std::endl;
if (transport->eTT == 1) { // S.C.A.M.P.E.R.
if (transport->iLocationID < 1 || transport->iLocationID > 31) { // sanity check
std::cout << "[WARN] S.C.A.M.P.E.R. location ID " << transport->iLocationID << " is out of bounds" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp);
failResp.eTT = transport->eTT;
failResp.iErrorCode = 0; // TODO: review what error code to use here
failResp.iLocationID = transport->iLocationID;
sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
return;
}
// update registration bitfield using bitmask
uint32_t newScamperFlag = plr->iWarpLocationFlag | (1UL << (transport->iLocationID - 1));
if (newScamperFlag != plr->iWarpLocationFlag) {
plr->iWarpLocationFlag = newScamperFlag;
newReg = true;
}
} else if (transport->eTT == 2) { // Monkey Skyway System
if (transport->iLocationID < 1 || transport->iLocationID > 127) { // sanity check
std::cout << "[WARN] Skyway location ID " << transport->iLocationID << " is out of bounds" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp);
failResp.eTT = transport->eTT;
failResp.iErrorCode = 0; // TODO: review what error code to use here
failResp.iLocationID = transport->iLocationID;
sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
return;
}
/*
* assuming the two bitfields are just stuck together to make a longer one, do a similar operation
*/
int index = transport->iLocationID > 64 ? 1 : 0;
uint64_t newMonkeyFlag = plr->aSkywayLocationFlag[index] | (1ULL << (index ? transport->iLocationID - 65 : transport->iLocationID - 1));
if (newMonkeyFlag != plr->aSkywayLocationFlag[index]) {
plr->aSkywayLocationFlag[index] = newMonkeyFlag;
newReg = true;
}
} else {
std::cout << "[WARN] Unknown mode of transport; eTT = " << transport->eTT << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp);
failResp.eTT = transport->eTT;
failResp.iErrorCode = 0; // TODO: review what error code to use here
failResp.iLocationID = transport->iLocationID;
sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
return;
}
if (!newReg)
return; // don't send new registration message
INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC, resp);
// response parameters
resp.eTT = transport->eTT;
resp.iLocationID = transport->iLocationID;
resp.iWarpLocationFlag = plr->iWarpLocationFlag;
resp.aWyvernLocationFlag[0] = plr->aSkywayLocationFlag[0];
resp.aWyvernLocationFlag[1] = plr->aSkywayLocationFlag[1];
sock->sendPacket(resp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC);
}
static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
/*
* req:
* eIL -- inventory type
* iNPC_ID -- the ID of the NPC who is warping you
* iTransporationID -- iVehicleID
* iSlotNum -- inventory slot number
*/
if (Routes.find(req->iTransporationID) == Routes.end() || Routes[req->iTransporationID].cost > plr->money) { // sanity check
INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL, failResp);
failResp.iErrorCode = 0; // TODO: error code
failResp.iTransportationID = req->iTransporationID;
sock->sendPacket(failResp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL);
return;
}
TransportRoute route = Routes[req->iTransporationID];
plr->money -= route.cost;
TransportLocation* target = nullptr;
switch (route.type) {
case 1: // S.C.A.M.P.E.R.
target = &Locations[route.end];
break;
case 2: // Monkey Skyway
// set last safe coords
plr->lastX = plr->x;
plr->lastY = plr->y;
plr->lastZ = plr->z;
if (SkywayPaths.find(route.mssRouteNum) != SkywayPaths.end()) { // check if route exists
Nanos::summonNano(sock, -1); // make sure that no nano is active during the ride
SkywayQueues[sock] = SkywayPaths[route.mssRouteNum]; // set socket point queue to route
plr->onMonkey = true;
break;
} else if (TableData::RunningSkywayRoutes.find(route.mssRouteNum) != TableData::RunningSkywayRoutes.end()) {
std::vector<Vec3>* _route = &TableData::RunningSkywayRoutes[route.mssRouteNum];
Nanos::summonNano(sock, -1);
testMssRoute(sock, _route);
plr->onMonkey = true;
break;
}
// refund and send alert packet
plr->money += route.cost;
INITSTRUCT(sP_FE2CL_ANNOUNCE_MSG, alert);
alert.iAnnounceType = 0; // don't think this lets us make a confirm dialog
alert.iDuringTime = 3;
U8toU16("Skyway route " + std::to_string(route.mssRouteNum) + " isn't pathed yet. You will not be charged any taros.", (char16_t*)alert.szAnnounceMsg, sizeof(alert.szAnnounceMsg));
sock->sendPacket(alert, P_FE2CL_ANNOUNCE_MSG);
std::cout << "[WARN] MSS route " << route.mssRouteNum << " not pathed" << std::endl;
break;
default:
std::cout << "[WARN] Unknown tranportation type " << route.type << std::endl;
break;
}
INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC, resp);
// response parameters
resp.eTT = route.type;
resp.iCandy = plr->money;
resp.iX = (target == nullptr) ? plr->x : target->x;
resp.iY = (target == nullptr) ? plr->y : target->y;
resp.iZ = (target == nullptr) ? plr->z : target->z;
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC);
if (target == nullptr)
return;
// we warped; update position and chunks
PlayerManager::updatePlayerPositionForWarp(sock, target->x, target->y, target->z, INSTANCE_OVERWORLD);
}
void Transport::testMssRoute(CNSocket *sock, std::vector<Vec3>* route) {
int speed = 1500; // TODO: make this adjustable
std::queue<Vec3> path;
Vec3 last = route->front(); // start pos
for (int i = 1; i < route->size(); i++) {
Vec3 coords = route->at(i);
Transport::lerp(&path, last, coords, speed);
path.push(coords); // add keyframe to the queue
last = coords; // update start pos
}
SkywayQueues[sock] = path;
}
/*
* Go through every socket that has broomstick points queued up, and advance to the next point.
* If the player has disconnected or finished the route, clean up and remove them from the queue.
*/
static void stepSkywaySystem() {
// using an unordered map so we can remove finished players in one iteration
std::unordered_map<CNSocket*, std::queue<Vec3>>::iterator it = SkywayQueues.begin();
while (it != SkywayQueues.end()) {
std::queue<Vec3>* queue = &it->second;
if (PlayerManager::players.find(it->first) == PlayerManager::players.end()) {
// pluck out dead socket + update iterator
it = SkywayQueues.erase(it);
continue;
}
Player* plr = PlayerManager::getPlayer(it->first);
if (queue->empty()) {
// send dismount packet
INITSTRUCT(sP_FE2CL_REP_PC_RIDING_SUCC, rideSucc);
INITSTRUCT(sP_FE2CL_PC_RIDING, rideBroadcast);
rideSucc.iPC_ID = plr->iID;
rideSucc.eRT = 0;
rideBroadcast.iPC_ID = plr->iID;
rideBroadcast.eRT = 0;
it->first->sendPacket(rideSucc, P_FE2CL_REP_PC_RIDING_SUCC);
// send packet to players in view
PlayerManager::sendToViewable(it->first, rideBroadcast, P_FE2CL_PC_RIDING);
it = SkywayQueues.erase(it); // remove player from tracking map + update iterator
plr->onMonkey = false;
} else {
Vec3 point = queue->front(); // get point
queue->pop(); // remove point from front of queue
INITSTRUCT(sP_FE2CL_PC_BROOMSTICK_MOVE, bmstk);
bmstk.iPC_ID = plr->iID;
bmstk.iToX = point.x;
bmstk.iToY = point.y;
bmstk.iToZ = point.z;
it->first->sendPacket(bmstk, P_FE2CL_PC_BROOMSTICK_MOVE);
// set player location to point to update viewables
PlayerManager::updatePlayerPosition(it->first, point.x, point.y, point.z, plr->instanceID, plr->angle);
// send packet to players in view
PlayerManager::sendToViewable(it->first, bmstk, P_FE2CL_PC_BROOMSTICK_MOVE);
it++; // go to next entry in map
}
}
}
static void stepNPCPathing() {
// all NPC pathing queues
std::unordered_map<int32_t, std::queue<Vec3>>::iterator it = NPCQueues.begin();
while (it != NPCQueues.end()) {
std::queue<Vec3>* queue = &it->second;
BaseNPC* npc = nullptr;
if (NPCManager::NPCs.find(it->first) != NPCManager::NPCs.end())
npc = NPCManager::NPCs[it->first];
if (npc == nullptr || queue->empty()) {
// pluck out dead path + update iterator
it = NPCQueues.erase(it);
continue;
}
// skip if not simulating mobs
if (npc->kind == EntityKind::MOB && !MobAI::simulateMobs) {
it++;
continue;
}
// do not roam if not roaming
if (npc->kind == EntityKind::MOB && ((Mob*)npc)->state != AIState::ROAMING) {
it++;
continue;
}
Vec3 point = queue->front(); // get point
queue->pop(); // remove point from front of queue
// calculate displacement
int dXY = hypot(point.x - npc->x, point.y - npc->y); // XY plane distance
int distanceBetween = hypot(dXY, point.z - npc->z); // total distance
// update NPC location to update viewables
NPCManager::updateNPCPosition(npc->id, point.x, point.y, point.z, npc->instanceID, npc->angle);
// TODO: move walking logic into Entity stack
switch (npc->kind) {
case EntityKind::BUS:
INITSTRUCT(sP_FE2CL_TRANSPORTATION_MOVE, busMove);
busMove.eTT = 3;
busMove.iT_ID = npc->id;
busMove.iMoveStyle = 0; // ???
busMove.iToX = point.x;
busMove.iToY = point.y;
busMove.iToZ = point.z;
busMove.iSpeed = distanceBetween; // set to distance to match how monkeys work
NPCManager::sendToViewable(npc, &busMove, P_FE2CL_TRANSPORTATION_MOVE, sizeof(sP_FE2CL_TRANSPORTATION_MOVE));
break;
case EntityKind::MOB:
MobAI::incNextMovement((Mob*)npc);
/* fallthrough */
default:
INITSTRUCT(sP_FE2CL_NPC_MOVE, move);
move.iNPC_ID = npc->id;
move.iMoveStyle = 0; // ???
move.iToX = point.x;
move.iToY = point.y;
move.iToZ = point.z;
move.iSpeed = distanceBetween;
NPCManager::sendToViewable(npc, &move, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
break;
}
/*
* If this path should be repeated, move processed point to the back to maintain cycle.
*/
if (npc->loopingPath)
queue->push(point);
it++; // go to next entry in map
}
}
static void tickTransportationSystem(CNServer* serv, time_t currTime) {
stepNPCPathing();
stepSkywaySystem();
}
/*
* Linearly interpolate between two points and insert the results into a queue.
*/
void Transport::lerp(std::queue<Vec3>* queue, Vec3 start, Vec3 end, int gapSize, float curve) {
int dXY = hypot(end.x - start.x, end.y - start.y); // XY plane distance
int distanceBetween = hypot(dXY, end.z - start.z); // total distance
int lerps = distanceBetween / gapSize; // number of intermediate points to add
for (int i = 1; i <= lerps; i++) {
Vec3 lerp;
// lerp math
//float frac = i / (lerps + 1);
float frac = powf(i, curve) / powf(lerps + 1, curve);
lerp.x = (start.x * (1.0f - frac)) + (end.x * frac);
lerp.y = (start.y * (1.0f - frac)) + (end.y * frac);
lerp.z = (start.z * (1.0f - frac)) + (end.z * frac);
queue->push(lerp); // add lerp'd point
}
}
void Transport::lerp(std::queue<Vec3>* queue, Vec3 start, Vec3 end, int gapSize) {
lerp(queue, start, end, gapSize, 1);
}
/*
* Find and return the first path that targets either the type or the ID.
* If no matches are found, return nullptr
*/
NPCPath* Transport::findApplicablePath(int32_t id, int32_t type, int taskID) {
NPCPath* match = nullptr;
for (auto _path = Transport::NPCPaths.begin(); _path != Transport::NPCPaths.end(); _path++) {
// task ID for the path must match so escorts don't start early
if (_path->escortTaskID != taskID)
continue;
// search target IDs
for (int32_t pID : _path->targetIDs) {
if (id == pID) {
match = &(*_path);
break;
}
}
if (match != nullptr)
break; // early break for ID matches, since ID has higher priority than type
// search target types
for (int32_t pType : _path->targetTypes) {
if (type == pType) {
match = &(*_path);
break;
}
}
if (match != nullptr)
break;
}
return match;
}
void Transport::constructPathNPC(int32_t id, NPCPath* path) {
BaseNPC* npc = NPCManager::NPCs[id];
if (npc->kind == EntityKind::MOB) {
auto mob = (Mob*)npc;
mob->staticPath = true;
Vec3 firstPoint = path->points.front();
// Ensure that the first point coincides with the mob's spawn point.
if (mob->spawnX != firstPoint.x || mob->spawnY != firstPoint.y) {
std::cout << "[FATAL] The first point of the route for mob " << mob->id << " (type " << mob->type
<< ") does not correspond with its spawn point." << std::endl;
exit(1);
}
}
npc->loopingPath = path->isLoop;
// Interpolate
std::vector<Vec3> pathPoints = path->points;
std::queue<Vec3> points;
auto _point = pathPoints.begin();
Vec3 from = *_point; // point A coords
for (_point++; _point != pathPoints.end(); _point++) { // loop through all point Bs
Vec3 to = *_point; // point B coords
// add point A to the queue
if (path->isRelative) {
// relative; the NPCs current position is assumed to be its spawn point
Vec3 fromReal = { from.x + npc->x, from.y + npc->y, from.z + npc->z };
Vec3 toReal = { to.x + npc->x, to.y + npc->y, to.z + npc->z };
points.push(fromReal);
Transport::lerp(&points, fromReal, toReal, path->speed); // lerp from A to B
}
else {
// absolute
points.push(from);
Transport::lerp(&points, from, to, path->speed); // lerp from A to B
}
from = to; // update point A
}
Transport::NPCQueues[id] = points;
}
void Transport::init() {
REGISTER_SHARD_TIMER(tickTransportationSystem, 1000);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION, transportRegisterLocationHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION, transportWarpHandler);
}

59
src/Transport.hpp Normal file
View File

@ -0,0 +1,59 @@
#pragma once
#include "core/Core.hpp"
#include <unordered_map>
#include <map>
#include <vector>
#include <queue>
const int SLIDER_SPEED = 1200;
const int SLIDER_STOP_TICKS = 16;
const int SLIDER_GAP_SIZE = 45000;
const int NPC_DEFAULT_SPEED = 300;
struct Vec3 {
int x, y, z;
};
struct WarpLocation {
int x, y, z, instanceID, isInstance, limitTaskID, npcID;
};
struct TransportRoute {
int type, start, end, cost, mssSpeed, mssRouteNum;
};
struct TransportLocation {
int npcID, x, y, z;
};
struct NPCPath {
std::vector<Vec3> points;
std::vector<int32_t> targetIDs;
std::vector<int32_t> targetTypes;
int speed;
int escortTaskID;
bool isRelative;
bool isLoop;
};
namespace Transport {
extern std::map<int32_t, TransportRoute> Routes;
extern std::map<int32_t, TransportLocation> Locations;
extern std::vector<NPCPath> NPCPaths; // predefined NPC paths
extern std::map<int32_t, std::queue<Vec3>> SkywayPaths; // predefined skyway paths with points
extern std::unordered_map<CNSocket*, std::queue<Vec3>> SkywayQueues; // player sockets with queued broomstick points
extern std::unordered_map<int32_t, std::queue<Vec3>> NPCQueues; // NPC ids with queued pathing points
void init();
void testMssRoute(CNSocket *sock, std::vector<Vec3>* route);
void lerp(std::queue<Vec3>*, Vec3, Vec3, int, float);
void lerp(std::queue<Vec3>*, Vec3, Vec3, int);
NPCPath* findApplicablePath(int32_t, int32_t, int = -1);
void constructPathNPC(int32_t, NPCPath*);
}

405
src/Vendors.cpp Normal file
View File

@ -0,0 +1,405 @@
#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;
static void vendorBuy(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_BUY*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
// prepare fail packet
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, failResp);
failResp.iErrorCode = 0;
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);
return;
}
int itemCost = itemDat->buyPrice * (itemDat->stackSize > 1 ? req->Item.iOpt : 1);
int slot = Items::findFreeSlot(plr);
if (itemCost > plr->money || slot == -1) {
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
return;
}
// crates don't have a stack size in TableData, so we can't check those
if (itemDat->stackSize != 0 && req->Item.iOpt > itemDat->stackSize) {
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL);
return;
}
// if vehicle
if (req->Item.iType == 10) {
// set time limit: current time + expiry duration
req->Item.iTimeLimit = getTimestamp() + VEHICLE_EXPIRY_DURATION;
}
if (slot != req->iInvenSlotNum) {
// possible item stacking?
std::cout << "[WARN] Client and server disagree on bought item slot (" << req->iInvenSlotNum << " vs " << slot << ")" << std::endl;
}
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC, resp);
plr->money = plr->money - itemCost;
plr->Inven[slot] = req->Item;
resp.iCandy = plr->money;
resp.iInvenSlotNum = slot;
resp.Item = req->Item;
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC);
}
static void vendorSell(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_SELL*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
// prepare a fail packet
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL, failResp);
failResp.iErrorCode = 0;
if (req->iInvenSlotNum < 0 || req->iInvenSlotNum >= AINVEN_COUNT || req->iItemCnt < 0) {
std::cout << "[WARN] Client failed to sell item in slot " << req->iInvenSlotNum << std::endl;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL);
return;
}
sItemBase* item = &plr->Inven[req->iInvenSlotNum];
Items::Item* itemData = Items::getItemData(item->iID, item->iType);
if (itemData == nullptr || !itemData->sellable || item->iOpt < req->iItemCnt) { // sanity + sellable check
std::cout << "[WARN] Item id " << item->iID << " with type " << item->iType << " not found (sell)" << std::endl;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL);
return;
}
// fail to sell croc-potted items
if (item->iOpt >= 1 << 16) {
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL);
return;
}
sItemBase original;
memcpy(&original, item, sizeof(sItemBase));
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC, resp);
// increment taros
plr->money += itemData->sellPrice * req->iItemCnt;
// modify item
if (item->iOpt - req->iItemCnt > 0) { // selling part of a stack
item->iOpt -= req->iItemCnt;
original.iOpt = req->iItemCnt;
}
else { // selling entire slot
// make sure it's fully zeroed, even the padding and non-104 members
memset(item, 0, sizeof(*item));
}
// add to buyback list
plr->buyback.push_back(original);
// forget oldest member if there's more than 5
if (plr->buyback.size() > 5)
plr->buyback.erase(plr->buyback.begin());
//std::cout << (int)plr->buyback.size() << " items in buyback\n";
// response parameters
resp.iInvenSlotNum = req->iInvenSlotNum;
resp.iCandy = plr->money;
resp.Item = original; // the item that gets sent to buyback
resp.ItemStay = *item; // the void item that gets put in the slot
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC);
}
static void vendorBuyback(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
// prepare fail packet
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL, failResp);
failResp.iErrorCode = 0;
//std::cout << "buying back from index " << (int)req->iListID << " into " << (int)req->iInvenSlotNum <<
// " from " << (int)req->iNPC_ID << " (vendor = " << (int)req->iVendorID << ")\n";
int idx = req->iListID - 1;
// sanity check
if (idx < 0 || idx >= plr->buyback.size()) {
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL);
return;
}
// get the item out of the buyback list
sItemBase item = plr->buyback[idx];
/*
* NOTE: The client sends the index of the exact item the user clicked on.
* We then operate on that item, but we remove the *first* identical item
* from the buyback list, instead of the one at the supplied index.
*
* This was originally a mistake on my part, but it turns out the client
* does the exact same thing, so this *is* the correct thing to do to keep
* them in sync.
*/
for (auto it = plr->buyback.begin(); it != plr->buyback.end(); it++) {
/*
* XXX: we really need a standard item comparison function that
* will work properly across all builds (ex. with iSerial)
*/
if (it->iType == item.iType && it->iID == item.iID && it->iOpt == item.iOpt
&& it->iTimeLimit == item.iTimeLimit) {
plr->buyback.erase(it);
break;
}
}
//std::cout << (int)plr->buyback.size() << " items in buyback\n";
Items::Item* itemDat = Items::getItemData(item.iID, item.iType);
if (itemDat == nullptr) {
std::cout << "[WARN] Item id " << item.iID << " with type " << item.iType << " not found (rebuy)" << std::endl;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL);
return;
}
// sell price is used on rebuy. ternary identifies stacked items
int itemCost = itemDat->sellPrice * (itemDat->stackSize > 1 ? item.iOpt : 1);
int slot = Items::findFreeSlot(plr);
if (itemCost > plr->money || slot == -1) {
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL);
return;
}
if (slot != req->iInvenSlotNum) {
// possible item stacking?
std::cout << "[WARN] Client and server disagree on bought item slot (" << req->iInvenSlotNum << " vs " << slot << ")" << std::endl;
}
plr->money = plr->money - itemCost;
plr->Inven[slot] = item;
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, resp);
// response parameters
resp.iCandy = plr->money;
resp.iInvenSlotNum = slot;
resp.Item = item;
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC);
}
static void vendorTable(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE*)data->buf;
if (req->iVendorID != req->iNPC_ID || Vendors::VendorTables.find(req->iVendorID) == Vendors::VendorTables.end())
return;
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].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;
vItem.iVendorID = req->iVendorID;
//vItem.fBuyCost = listings[i].price; // this value is not actually the one that is used
resp.item[i] = vItem;
}
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC);
}
static void vendorStart(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_PC_VENDOR_START*)data->buf;
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_START_SUCC, resp);
resp.iNPC_ID = req->iNPC_ID;
resp.iVendorID = req->iVendorID;
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_START_SUCC);
}
static void vendorBuyBattery(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_PC_VENDOR_BATTERY_BUY*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
int cost = req->Item.iOpt * 100;
if ((req->Item.iID == 3 ? (plr->batteryW >= 9999) : (plr->batteryN >= 9999)) || plr->money < cost || req->Item.iOpt < 0) { // sanity check
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, failResp);
failResp.iErrorCode = 0;
sock->sendPacket(failResp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL);
return;
}
cost = plr->batteryW + plr->batteryN;
plr->batteryW += req->Item.iID == 3 ? req->Item.iOpt * 100 : 0;
plr->batteryN += req->Item.iID == 4 ? req->Item.iOpt * 100 : 0;
// caps
if (plr->batteryW > 9999)
plr->batteryW = 9999;
if (plr->batteryN > 9999)
plr->batteryN = 9999;
cost = plr->batteryW + plr->batteryN - cost;
plr->money -= cost;
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, resp);
resp.iCandy = plr->money;
resp.iBatteryW = plr->batteryW;
resp.iBatteryN = plr->batteryN;
sock->sendPacket(resp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC);
}
static void vendorCombineItems(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_PC_ITEM_COMBINATION*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
// prepare fail packet
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_FAIL, failResp);
failResp.iCostumeItemSlot = req->iCostumeItemSlot;
failResp.iStatItemSlot = req->iStatItemSlot;
failResp.iErrorCode = 0;
// sanity check slot indices
if (req->iCostumeItemSlot < 0 || req->iCostumeItemSlot >= AINVEN_COUNT || req->iStatItemSlot < 0 || req->iStatItemSlot >= AINVEN_COUNT) {
std::cout << "[WARN] Inventory slot(s) out of range (" << req->iStatItemSlot << " and " << req->iCostumeItemSlot << ")" << std::endl;
sock->sendPacket(failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL);
return;
}
sItemBase* itemStats = &plr->Inven[req->iStatItemSlot];
sItemBase* itemLooks = &plr->Inven[req->iCostumeItemSlot];
Items::Item* itemStatsDat = Items::getItemData(itemStats->iID, itemStats->iType);
Items::Item* itemLooksDat = Items::getItemData(itemLooks->iID, itemLooks->iType);
// sanity check item and combination entry existence
if (itemStatsDat == nullptr || itemLooksDat == nullptr
|| Items::CrocPotTable.find(abs(itemStatsDat->level - itemLooksDat->level)) == Items::CrocPotTable.end()) {
std::cout << "[WARN] Either item ids or croc pot value set not found" << std::endl;
sock->sendPacket(failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL);
return;
}
// sanity check matching item types
if (itemStats->iType != itemLooks->iType
|| (itemStats->iType == 0 && itemStatsDat->weaponType != itemLooksDat->weaponType)) {
std::cout << "[WARN] Player attempted to combine mismatched items" << std::endl;
sock->sendPacket(failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL);
return;
}
CrocPotEntry* recipe = &Items::CrocPotTable[abs(itemStatsDat->level - itemLooksDat->level)];
int cost = itemStatsDat->buyPrice * recipe->multStats + itemLooksDat->buyPrice * recipe->multLooks;
float successChance = recipe->base / 100.0f; // base success chance
// rarity gap multiplier
switch (abs(itemStatsDat->rarity - itemLooksDat->rarity)) {
case 0:
successChance *= recipe->rd0;
break;
case 1:
successChance *= recipe->rd1;
break;
case 2:
successChance *= recipe->rd2;
break;
case 3:
successChance *= recipe->rd3;
break;
default:
break;
}
float rolled = Rand::randFloat(100.0f); // success chance out of 100
//std::cout << rolled << " vs " << successChance << std::endl;
plr->money -= cost;
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_SUCC, resp);
if (rolled < successChance) {
// success
resp.iSuccessFlag = 1;
// modify the looks item with the new stats and set the appearance through iOpt
itemLooks->iOpt = (int32_t)((itemLooks->iOpt) >> 16 > 0 ? (itemLooks->iOpt >> 16) : itemLooks->iID) << 16;
itemLooks->iID = itemStats->iID;
// delete stats item
itemStats->iID = 0;
itemStats->iOpt = 0;
itemStats->iTimeLimit = 0;
itemStats->iType = 0;
}
else {
// failure; don't do anything?
resp.iSuccessFlag = 0;
}
resp.iCandy = plr->money;
resp.iNewItemSlot = req->iCostumeItemSlot;
resp.iStatItemSlot = req->iStatItemSlot;
resp.sNewItem = *itemLooks;
sock->sendPacket(resp, P_FE2CL_REP_PC_ITEM_COMBINATION_SUCC);
}
void Vendors::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_START, vendorStart);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE, vendorTable);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_BUY, vendorBuy);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_SELL, vendorSell);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY, vendorBuyback);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_BATTERY_BUY, vendorBuyBattery);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_COMBINATION, vendorCombineItems);
}

21
src/Vendors.hpp Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include "core/Core.hpp"
#include <vector>
#include <map>
struct VendorListing {
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 {
extern std::map<int32_t, std::vector<VendorListing>> VendorTables;
void init();
}

567
src/core/CNProtocol.cpp Normal file
View File

@ -0,0 +1,567 @@
#include "core/CNProtocol.hpp"
#include "CNStructs.hpp"
#include <assert.h>
// ========================================================[[ CNSocketEncryption ]]========================================================
// literally C/P from the client and converted to C++ (does some byte swapping /shrug)
int CNSocketEncryption::Encrypt_byte_change_A(int ERSize, uint8_t* data, int size) {
int num = 0;
int num2 = 0;
int num3 = 0;
while (num + ERSize <= size) {
int num4 = num + num3;
int num5 = num + (ERSize - 1 - num3);
uint8_t b = data[num4];
data[num4] = data[num5];
data[num5] = b;
num += ERSize;
num3++;
if (num3 > ERSize / 2) {
num3 = 0;
}
}
num2 = ERSize - (num + ERSize - size);
return num + num2;
}
int CNSocketEncryption::xorData(uint8_t* buffer, uint8_t* key, int size) {
// xor every 8 bytes with 8 byte key
for (int i = 0; i < size; i++) {
buffer[i] ^= key[i % keyLength];
}
return 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;
memcpy(&dEKey, defaultKey, sizeof(dEKey));
return dEKey * (uTime * num * num2);
}
int CNSocketEncryption::encryptData(uint8_t* buffer, uint8_t* key, int size) {
int eRSize = size % (keyLength / 2 + 1) * 2 + keyLength; // C/P from client
int size2 = xorData(buffer, key, size);
return Encrypt_byte_change_A(eRSize, buffer, size2);
}
int CNSocketEncryption::decryptData(uint8_t* buffer, uint8_t* key, int size) {
int eRSize = size % (keyLength / 2 + 1) * 2 + keyLength; // size % of 18????
int size2 = Encrypt_byte_change_A(eRSize, buffer, size);
return xorData(buffer, key, size2);
}
// ========================================================[[ CNPacketData ]]========================================================
CNPacketData::CNPacketData(void *b, uint32_t t, int l, int trnum, void *trs):
buf(b), size(l), type(t), trCnt(trnum), trailers(trs) {}
// ========================================================[[ CNSocket ]]========================================================
CNSocket::CNSocket(SOCKET s, struct sockaddr_in &addr, PacketHandler ph): sock(s), sockaddr(addr), pHandler(ph) {
memcpy(&EKey, CNSocketEncryption::defaultKey, sizeof(EKey));
}
bool CNSocket::sendData(uint8_t* data, int size) {
int sentBytes = 0;
int maxTries = 10;
while (sentBytes < size) {
int sent = send(sock, (buffer_t*)(data + sentBytes), size - sentBytes, 0);
if (SOCKETERROR(sent)) {
if (OF_ERRNO == OF_EWOULD && maxTries > 0) {
maxTries--;
continue; // try again
}
printSocketError("send");
return false; // error occured while sending bytes
}
sentBytes += sent;
}
return true; // it worked!
}
void CNSocket::setEKey(uint64_t k) {
EKey = k;
}
void CNSocket::setFEKey(uint64_t k) {
FEKey = k;
}
uint64_t CNSocket::getEKey() {
return EKey;
}
uint64_t CNSocket::getFEKey() {
return FEKey;
}
bool CNSocket::isAlive() {
return alive;
}
void CNSocket::kill() {
if (!alive)
return;
alive = false;
#ifdef _WIN32
shutdown(sock, SD_BOTH);
closesocket(sock);
#else
shutdown(sock, SHUT_RDWR);
close(sock);
#endif
}
void CNSocket::validatingSendPacket(void *pkt, uint32_t packetType) {
assert(isOutboundPacketID(packetType));
assert(Packets::packets.find(packetType) != Packets::packets.end());
PacketDesc& desc = Packets::packets[packetType];
size_t resplen = desc.size;
/*
* Note that this validation doesn't happen on time to prevent a buffer
* overflow if it would have taken place, but we do it anyway so the
* assertion failure at least makes it clear that something isn't being
* validated properly.
*/
if (desc.variadic) {
int32_t ntrailers = *(int32_t*)(((uint8_t*)pkt) + desc.cntMembOfs);
assert(validOutVarPacket(desc.size, ntrailers, desc.trailerSize));
resplen = desc.size + ntrailers * desc.trailerSize;
}
sendPacket(pkt, packetType, resplen);
}
void CNSocket::sendPacket(void* buf, uint32_t type, size_t size) {
if (!alive)
return;
uint8_t fullpkt[CN_PACKET_BUFFER_SIZE]; // length, type, body
uint8_t* body = fullpkt + 4; // packet without length (type, body)
size_t bodysize = size + 4;
// set packet length
memcpy(fullpkt, (void*)&bodysize, 4);
// copy packet type to the front of the buffer & then the actual buffer
memcpy(body, (void*)&type, 4);
memcpy(body+4, buf, size);
// encrypt the packet
switch (activeKey) {
case SOCKETKEY_E:
CNSocketEncryption::encryptData((uint8_t*)body, (uint8_t*)(&EKey), bodysize);
break;
case SOCKETKEY_FE:
CNSocketEncryption::encryptData((uint8_t*)body, (uint8_t*)(&FEKey), bodysize);
break;
default:
DEBUGLOG(
std::cout << "[WARN]: UNSET KEYTYPE FOR SOCKET!! ABORTING SEND" << std::endl;
)
return;
}
// send packet data!
if (alive && !sendData(fullpkt, bodysize+4))
kill();
}
void CNSocket::setActiveKey(ACTIVEKEY key) {
activeKey = key;
}
inline void CNSocket::parsePacket(uint8_t *buf, size_t size) {
uint32_t type = *((uint32_t*)buf);
uint8_t *body = buf + 4;
size_t pktSize = size - 4;
if (Packets::packets.find(type) == Packets::packets.end()) {
std::cerr << "OpenFusion: UNKNOWN PACKET: " << (int)type << std::endl;
return;
}
if (!isInboundPacketID(type)) {
std::cerr << "OpenFusion: UNEXPECTED PACKET: " << (int)type << std::endl;
return;
}
PacketDesc& desc = Packets::packets[type];
/*
* Some packet structs with no meaningful contents have length 1, but
* the client doesn't transmit that byte at all, so we special-case that.
* It's important that we do that by zeroing that byte, as the server could
* hypothetically try and read from it and get a byte of the previous
* packet's contents.
*
* Assigning a zero byte to the body like this is safe, since there's a
* huge empty buffer behind that pointer.
*/
if (!desc.variadic && desc.size == 1 && pktSize == 0) {
pktSize = 1;
*body = 0;
}
int32_t ntrailers = 0;
if (desc.variadic) {
ntrailers = *(int32_t*)(body + desc.cntMembOfs);
if (!validInVarPacket(desc.size, ntrailers, desc.trailerSize, pktSize)) {
std::cerr << "[WARN] Received invalid variadic packet: " << desc.name << " (" << type << ")" << std::endl;
return;
}
} else if (!desc.variadic && pktSize != desc.size) {
std::cerr << "[WARN] Received " << desc.name << " (" << type << ") of wrong size ("
<< (int)pktSize << " vs " << desc.size << ")" << std::endl;
return;
}
void *trailers = nullptr;
if (desc.variadic)
trailers = body + desc.size;
CNPacketData pkt(body, type, pktSize, ntrailers, trailers);
pHandler(this, &pkt);
}
void CNSocket::step() {
// read step
// XXX NOTE: we must not recv() twice without a poll() inbetween
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 && 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);
// sanity check
if (readSize > CN_PACKET_BUFFER_SIZE) {
kill();
return;
}
// we'll just leave bufferIndex at 0 since we already have the packet size, it's safe to overwrite those bytes
activelyReading = true;
} else if (OF_ERRNO != OF_EWOULD) {
// serious socket issue, disconnect connection
printSocketError("recv");
kill();
return;
}
}
if (readSize > 0 && readBufferIndex < readSize) {
// 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) {
// serious socket issue, disconnect connection
printSocketError("recv");
kill();
return;
}
}
if (activelyReading && readBufferIndex >= readSize) {
// decrypt readBuffer and copy to CNPacketData
CNSocketEncryption::decryptData((uint8_t*)&readBuffer, (uint8_t*)(&EKey), readSize);
parsePacket(readBuffer, readSize);
// reset vars :)
readSize = 0;
readBufferIndex = 0;
activelyReading = false;
}
}
void printSocketError(const char *call) {
#ifdef _WIN32
std::cerr << call << ": ";
LPSTR lpMsgBuf = nullptr; // string buffer
DWORD errCode = WSAGetLastError(); // error code
if (errCode == 0) {
std::cerr << "no error code" << std::endl;
return;
}
size_t bufSize = FormatMessageA( // actually get the error message
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
errCode, // in
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, // out
0, NULL);
// convert buffer to string and output message to terminal
std::string msg(lpMsgBuf, bufSize);
std::cerr << msg; // newline included
LocalFree(lpMsgBuf); // free the buffer
#else
perror(call);
#endif
}
bool setSockNonblocking(SOCKET listener, SOCKET newSock) {
#ifdef _WIN32
unsigned long mode = 1;
if (ioctlsocket(newSock, FIONBIO, &mode) != 0) {
#else
if (fcntl(newSock, F_SETFL, (fcntl(newSock, F_GETFL, 0) | O_NONBLOCK)) != 0) {
#endif
printSocketError("fcntl");
std::cerr << "[WARN] OpenFusion: fcntl failed on new connection" << std::endl;
#ifdef _WIN32
shutdown(newSock, SD_BOTH);
closesocket(newSock);
#else
shutdown(newSock, SHUT_RDWR);
close(newSock);
#endif
return false;
}
return true;
}
// ========================================================[[ CNServer ]]========================================================
void CNServer::init() {
// create socket file descriptor
sock = socket(AF_INET, SOCK_STREAM, 0);
if (SOCKETINVALID(sock)) {
printSocketError("socket");
std::cerr << "[FATAL] OpenFusion: socket failed" << std::endl;
exit(EXIT_FAILURE);
}
// attach socket to the port
int opt = 1;
#ifdef _WIN32
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt)) != 0) {
#else
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) {
#endif
std::cerr << "[FATAL] OpenFusion: setsockopt failed" << std::endl;
printSocketError("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(port);
addressSize = sizeof(address);
// Bind to the port
if (SOCKETERROR(bind(sock, (struct sockaddr *)&address, addressSize))) {
std::cerr << "[FATAL] OpenFusion: bind failed" << std::endl;
printSocketError("bind");
exit(EXIT_FAILURE);
}
if (SOCKETERROR(listen(sock, SOMAXCONN))) {
std::cerr << "[FATAL] OpenFusion: listen failed" << std::endl;
printSocketError("listen");
exit(EXIT_FAILURE);
}
// set server listener to non-blocking
#ifdef _WIN32
unsigned long mode = 1;
if (ioctlsocket(sock, FIONBIO, &mode) != 0) {
#else
if (fcntl(sock, F_SETFL, (fcntl(sock, F_GETFL, 0) | O_NONBLOCK)) != 0) {
#endif
printSocketError("fcntl");
std::cerr << "[FATAL] OpenFusion: fcntl failed" << std::endl;
exit(EXIT_FAILURE);
}
// poll() configuration
fds.reserve(STARTFDSCOUNT);
fds.push_back({sock, POLLIN});
}
CNServer::CNServer() {};
CNServer::CNServer(uint16_t p): port(p) {}
void CNServer::addPollFD(SOCKET s) {
fds.push_back({s, POLLIN});
}
void CNServer::removePollFD(int fd) {
auto it = fds.begin();
while (it != fds.end() && it->fd != fd)
it++;
assert(it != fds.end());
fds.erase(it);
}
void CNServer::start() {
std::cout << "Starting " << serverType << " server at *:" << port << std::endl;
while (active) {
// the timeout is to ensure shard timers are ticking
int n = poll(fds.data(), fds.size(), 50);
if (SOCKETERROR(n)) {
#ifndef _WIN32
if (errno == EINTR)
continue;
#endif
std::cout << "[FATAL] poll() returned error" << std::endl;
printSocketError("poll");
terminate(0);
}
for (int i = 0; i < fds.size() && n > 0; i++) {
if (fds[i].revents == 0)
continue; // nothing in this one; don't decrement n
n--;
// is it the listener?
if (fds[i].fd == sock) {
// any sort of error on the listener
if (fds[i].revents & ~POLLIN) {
std::cout << "[FATAL] Error on listener socket" << std::endl;
terminate(0);
}
SOCKET newConnectionSocket = accept(sock, (struct sockaddr *)&address, (socklen_t*)&addressSize);
if (SOCKETINVALID(newConnectionSocket)) {
printSocketError("accept");
continue;
}
if (!setSockNonblocking(sock, newConnectionSocket))
continue;
std::cout << "New " << serverType << " connection! " << inet_ntoa(address.sin_addr) << std::endl;
addPollFD(newConnectionSocket);
// add connection to list!
CNSocket* tmp = new CNSocket(newConnectionSocket, address, pHandler);
connections[newConnectionSocket] = tmp;
newConnection(tmp);
} else if (checkExtraSockets(i)) {
// no-op. handled in checkExtraSockets().
} 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 << "[FATAL] Event on non-existent socket: " << fds[i].fd << std::endl;
assert(0);
/* not reached */
}
CNSocket* cSock = connections[fds[i].fd];
// kill the socket on hangup/error
if (fds[i].revents & ~POLLIN)
cSock->kill();
if (cSock->isAlive())
cSock->step();
}
}
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++;
}
}
}
}
void CNServer::kill() {
std::lock_guard<std::mutex> lock(activeCrit); // the lock will be removed when the function ends
active = false;
// kill all connections
for (auto& pair : connections) {
CNSocket *cSock = pair.second;
if (cSock->isAlive())
cSock->kill();
delete cSock;
}
connections.clear();
}
void CNServer::printPacket(CNPacketData *data) {
if (settings::VERBOSITY < 2)
return;
if (settings::VERBOSITY < 3) switch (data->type) {
case P_CL2LS_REP_LIVE_CHECK:
case P_CL2FE_REP_LIVE_CHECK:
case P_CL2FE_REQ_PC_MOVE:
case P_CL2FE_REQ_PC_JUMP:
case P_CL2FE_REQ_PC_SLOPE:
case P_CL2FE_REQ_PC_MOVEPLATFORM:
case P_CL2FE_REQ_PC_MOVETRANSPORTATION:
case P_CL2FE_REQ_PC_ZIPLINE:
case P_CL2FE_REQ_PC_JUMPPAD:
case P_CL2FE_REQ_PC_LAUNCHER:
case P_CL2FE_REQ_PC_STOP:
return;
}
std::cout << "OpenFusion: received " << Packets::p2str(data->type) << " (" << data->type << ")" << std::endl;
}
bool CNServer::checkExtraSockets(int i) { return false; } // stubbed
void CNServer::newConnection(CNSocket* cns) {} // stubbed
void CNServer::killConnection(CNSocket* cns) {} // stubbed
void CNServer::onStep() {} // stubbed

258
src/core/CNProtocol.hpp Normal file
View File

@ -0,0 +1,258 @@
#pragma once
#define DEBUGLOG(x) if (settings::VERBOSITY) {x};
#include <iostream>
#include <stdio.h>
#include <stdint.h>
#ifdef _WIN32
// windows
#ifndef NOMINMAX
#define NOMINMAX
#endif
#define M_PI 3.14159265358979323846
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#pragma comment(lib, "Ws2_32.lib")
typedef char buffer_t;
#define PollFD WSAPOLLFD
#define poll WSAPoll
#define OF_ERRNO WSAGetLastError()
#define OF_EWOULD WSAEWOULDBLOCK
#define SOCKETINVALID(x) (x == INVALID_SOCKET)
#define SOCKETERROR(x) (x == SOCKET_ERROR)
#else
// posix platform
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <unistd.h>
#include <errno.h>
typedef int SOCKET;
typedef void buffer_t;
#define PollFD struct pollfd
#define OF_ERRNO errno
#define OF_EWOULD EWOULDBLOCK
#define SOCKETINVALID(x) (x < 0)
#define SOCKETERROR(x) (x == -1)
#endif
#include <fcntl.h>
#include <string>
#include <cstring>
#include <csignal>
#include <list>
#include <queue>
#include <unordered_map>
#include <vector>
#include <algorithm>
#include "Defines.hpp"
#include "Packets.hpp"
#include "settings.hpp"
#if defined(__MINGW32__) && !defined(_GLIBCXX_HAS_GTHREADS)
#include "mingw/mingw.mutex.h"
#else
#include <mutex>
#endif
/*
* Packets format (sent from the client):
* [4 bytes] - size of packet including the 4 byte packet type
* [size bytes] - Encrypted packet (byte swapped && xor'd with 8 byte key; see CNSocketEncryption)
* [4 bytes] - packet type (which is a combination of the first 4 bytes of the packet and a checksum in some versions)
* [structure] - one member contains length of trailing data (expressed in packet-dependant structures)
* [trailing data] - optional variable-length data that only some packets make use of
*/
// error checking calloc wrapper
inline void* xmalloc(size_t sz) {
void* res = calloc(1, sz);
if (res == NULL) {
std::cerr << "[FATAL] OpenFusion: out of memory!" << std::endl;
exit(EXIT_FAILURE);
}
return res;
}
inline constexpr bool isInboundPacketID(uint32_t id) {
return ((id & CL2LS) == CL2LS) || ((id & CL2FE) == CL2FE);
}
inline constexpr bool isOutboundPacketID(uint32_t id) {
return ((id & LS2CL) == LS2CL) || ((id & FE2CL) == FE2CL);
}
// overflow-safe validation of variable-length packets
// for outbound packets
inline constexpr bool validOutVarPacket(size_t base, size_t npayloads, size_t plsize) {
// check for multiplication overflow
if (npayloads > 0 && (CN_PACKET_BODY_SIZE) / (size_t)npayloads < plsize)
return false;
// it's safe to multiply
size_t trailing = npayloads * plsize;
// does it fit in a packet?
if (base + trailing > CN_PACKET_BODY_SIZE)
return false;
// everything is a-ok!
return true;
}
// for inbound packets
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_BODY_SIZE / (size_t)npayloads < plsize)
return false;
// it's safe to multiply
size_t trailing = npayloads * plsize;
// make sure size is exact
// datasize has already been validated against CN_PACKET_BODY_SIZE
if (datasize != base + trailing)
return false;
// everything is a-ok!
return true;
}
void printSocketError(const char *func);
bool setSockNonblocking(SOCKET listener, SOCKET newSock);
namespace CNSocketEncryption {
// you won't believe how complicated they made it in the client :facepalm:
static constexpr const char* defaultKey = "m@rQn~W#";
static const unsigned int keyLength = 8;
int Encrypt_byte_change_A(int ERSize, uint8_t* data, int size);
int xorData(uint8_t* buffer, uint8_t* key, int size);
uint64_t createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2);
int encryptData(uint8_t* buffer, uint8_t* key, int size);
int decryptData(uint8_t* buffer, uint8_t* key, int size);
}
struct CNPacketData {
void *buf;
int size;
uint32_t type;
int trCnt;
void *trailers;
CNPacketData(void* b, uint32_t t, int l, int trnum, void *trs);
};
enum ACTIVEKEY {
SOCKETKEY_E,
SOCKETKEY_FE
};
class CNSocket;
typedef void (*PacketHandler)(CNSocket* sock, CNPacketData* data);
class CNSocket {
private:
uint64_t EKey;
uint64_t FEKey;
int32_t readSize = 0;
uint8_t readBuffer[CN_PACKET_BUFFER_SIZE];
int readBufferIndex = 0;
bool activelyReading = false;
bool alive = true;
ACTIVEKEY activeKey;
bool sendData(uint8_t* data, int size);
int recvData(buffer_t* data, int size);
inline void parsePacket(uint8_t *buf, size_t size);
void validatingSendPacket(void *buf, uint32_t packetType);
public:
SOCKET sock;
sockaddr_in sockaddr;
PacketHandler pHandler;
CNSocket(SOCKET s, struct sockaddr_in &addr, PacketHandler ph);
void setEKey(uint64_t k);
void setFEKey(uint64_t k);
uint64_t getEKey();
uint64_t getFEKey();
void setActiveKey(ACTIVEKEY t);
void kill();
void sendPacket(void* buf, uint32_t packetType, size_t size);
void step();
bool isAlive();
// generic, validating wrapper for sendPacket()
template<class T>
inline void sendPacket(T& pkt, uint32_t packetType) {
/*
* We do most of the logic in a helper, to lower the amount of code
* that gets generated multiple times with each template instantiation.
*/
validatingSendPacket((void*)&pkt, packetType);
}
};
class CNServer;
typedef void (*TimerHandler)(CNServer* serv, time_t time);
// timer struct
struct TimerEvent {
TimerHandler handlr;
time_t delta; // time to be added to the current time on reset
time_t scheduledEvent; // time to call handlr()
TimerEvent(TimerHandler h, time_t d): handlr(h), delta(d) {
scheduledEvent = 0;
}
};
// in charge of accepting new connections and making sure each connection is kept alive
class CNServer {
protected:
std::unordered_map<SOCKET, CNSocket*> connections;
std::mutex activeCrit;
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;
struct sockaddr_in address;
void init();
bool active = true;
void addPollFD(SOCKET s);
void removePollFD(int i);
public:
PacketHandler pHandler;
CNServer();
CNServer(uint16_t p);
void start();
void kill();
static void printPacket(CNPacketData *data);
virtual bool checkExtraSockets(int i);
virtual void newConnection(CNSocket* cns);
virtual void killConnection(CNSocket* cns);
virtual void onStep();
};

45
src/core/CNShared.cpp Normal file
View File

@ -0,0 +1,45 @@
#include "core/CNShared.hpp"
static std::unordered_map<int64_t, LoginMetadata*> logins;
static std::mutex mtx;
void CNShared::storeLoginMetadata(int64_t sk, LoginMetadata *lm) {
std::lock_guard<std::mutex> lock(mtx);
// take ownership of connection data
logins[sk] = lm;
}
LoginMetadata* CNShared::getLoginMetadata(int64_t sk) {
std::lock_guard<std::mutex> lock(mtx);
// 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 CNShared::pruneLoginMetadata(CNServer *serv, time_t currTime) {
std::lock_guard<std::mutex> lock(mtx);
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++;
}
}
}

29
src/core/CNShared.hpp Normal file
View File

@ -0,0 +1,29 @@
/*
* core/CNShared.hpp
* There's some data shared between the Login Server and the Shard Server. Of course all of this needs to be thread-safe. No mucking about on this one!
*/
#pragma once
#include <map>
#include <string>
#include "Player.hpp"
/*
* Connecions time out after 5 minutes, checked every 30 seconds.
*/
#define CNSHARED_TIMEOUT 300000
#define CNSHARED_PERIOD 30000
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);
}

70
src/core/CNStructs.hpp Normal file
View File

@ -0,0 +1,70 @@
/*
* CNStructs.hpp - defines some basic structs & useful methods for packets used by FusionFall based on the version defined
*/
#pragma once
#ifdef _MSC_VER
// codecvt_* is deprecated in C++17 and MSVC will throw an annoying warning because of that.
// Defining this before anything else to silence it.
#define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
#endif
#include <iostream>
#include <stdio.h>
#include <stdint.h>
#ifndef _MSC_VER
#include <sys/time.h>
#else
// Can't use this in MSVC.
#include <time.h>
#endif
#include <cstring>
#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; \
memset(&x, 0, sizeof(T));
#define INITVARPACKET(_buf, _Pkt, _pkt, _Trailer, _trailer) uint8_t _buf[CN_PACKET_BODY_SIZE]; \
memset(&_buf, 0, CN_PACKET_BODY_SIZE); \
auto _pkt = (_Pkt*)_buf; \
auto _trailer = (_Trailer*)(_pkt + 1);
// macros to extract fields from instanceIDs
#define MAPNUM(x) ((x) & 0xffffffff)
#define PLAYERID(x) ((x) >> 32)
// 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))
// TODO: rewrite U16toU8 & U8toU16 to not use codecvt
std::string U16toU8(char16_t* src, size_t max);
size_t U8toU16(std::string src, char16_t* des, size_t max); // returns number of char16_t that was written at des
time_t getTime();
time_t getTimestamp();
int timingSafeStrcmp(const char* a, const char* b);
void terminate(int);
// 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 == 1013
#include "structs/1013.hpp"
#else
#error Invalid PROTOCOL_VERSION
#endif
sSYSTEMTIME timeStampToStruct(uint64_t time);

11
src/core/Core.hpp Normal file
View File

@ -0,0 +1,11 @@
#pragma once
/*
* Convenience header.
*
* We omit CNShared, as it's only relevant to the Login and Shard servers
* and the PlayerManager. We omit Defines, as CNProtocol already includes it.
*/
#include "core/CNProtocol.hpp"
#include "core/CNStructs.hpp"

949
src/core/Defines.hpp Normal file
View File

@ -0,0 +1,949 @@
/* enum definitions from the client */
#pragma once
#include "core/CNStructs.hpp"
// floats
const float VALUE_BATTERY_EMPTY_PENALTY = 0.5f;
const float CN_EP_RANK_1 = 0.8f;
const float CN_EP_RANK_2 = 0.7f;
const float CN_EP_RANK_3 = 0.5f;
const float CN_EP_RANK_4 = 0.3f;
const float CN_EP_RANK_5 = 0.29f;
// methods of finding players for GM commands
enum class eCN_GM_TargetSearchBy {
PC_ID, // player id
PC_Name, // firstname, lastname
PC_UID // account id
};
enum class eCN_GM_TeleportType {
XYZ,
MapXYZ,
MyLocation,
SomeoneLocation,
Unstick
};
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 {
ECSB_NONE = 0,
ECSB_UP_MOVE_SPEED = 1,
ECSB_UP_SWIM_SPEED = 2,
ECSB_UP_JUMP_HEIGHT = 3,
ECSB_UP_STEALTH = 4,
ECSB_PHOENIX = 5,
ECSB_PROTECT_BATTERY = 6,
ECSB_PROTECT_INFECTION = 7,
ECSB_DN_MOVE_SPEED = 8,
ECSB_DN_ATTACK_SPEED = 9,
ECSB_STUN = 10,
ECSB_MEZ = 11,
ECSB_KNOCKDOWN = 12,
ECSB_MINIMAP_ENEMY = 13,
ECSB_MINIMAP_TRESURE = 14,
ECSB_REWARD_BLOB = 15,
ECSB_REWARD_CASH = 16,
ECSB_INFECTION = 17,
ECSB_FREEDOM = 18,
ECSB_BOUNDINGBALL = 19,
ECSB_INVULNERABLE = 20,
ECSB_STIMPAKSLOT1 = 21,
ECSB_STIMPAKSLOT2 = 22,
ECSB_STIMPAKSLOT3 = 23,
ECSB_HEAL = 24,
ECSB_EXTRABANK = 25,
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,
SIZEOF_BYTE = 1,
SIZEOF_DWORD = 4,
SIZEOF_INT = 4,
SIZEOF_FLOAT = 4,
SIZEOF_SHORT = 2,
SIZEOF_ULONG = 4,
SIZEOF_UINT64 = 8,
SIZEOF_IP_STRING = 16,
SIZEOF_CN_UID_STRING = 50,
SIZEOF_ACCOUNT_STRING = 33,
SIZEOF_PASSWORD_STRING = 33,
SIZEOF_AUTH_ID_STRING = 255,
CN_MAX_COUNT_GROUP_MEMBER = 5,
CN_MAX_COUNT_PC_GROUP_MEMBER = 4,
CN_MAX_COUNT_NPC_GROUP_MEMBER = 5,
CHAT_MAX_STRING = 128,
PC_START_LOCATION_RANDOM_RANGE = 10000,
SIZEOF_ANNOUNCE_STRING = 512,
SERVER_COUNT_SHARD_CLIENT = 25,
EXIT_CODE_DISCONNECT = 0,
EXIT_CODE_REQ_BY_PC = 1,
EXIT_CODE_REQ_BY_SVR = 2,
EXIT_CODE_REQ_BY_GM = 3,
EXIT_CODE_HACK = 4,
EXIT_CODE_ERROR = 5,
EXIT_CODE_LIVE_CHECK = 6,
EXIT_CODE_REQ_BY_PC_DUPE_LOGIN = 7,
EXIT_CODE_SERVER_ERROR = 99,
SIZEOF_USER_ID = 32,
SIZEOF_USER_PW = 32,
SIZEOF_PC_SLOT = 4,
SIZEOF_PC_NAME = 16,
SIZEOF_PC_FIRST_NAME = 9,
SIZEOF_PC_LAST_NAME = 17,
SIZEOF_PC_NAME_FLAG = 8,
GENDER_NONE = 0,
GENDER_MALE = 1,
GENDER_FEMALE = 2,
MENTOR_CHANGE_BASE_COST = 100,
REPEAT_MISSION_RESET_TIME = 9,
SIZEOF_REPEAT_QUESTFLAG_NUMBER = 8,
FATIGUE_RESET_TIME = 0,
PC_FATIGUE_KILL_UNIT = 7,
PC_FATIGUE_1_LEVEL = 11420,
PC_FATIGUE_2_LEVEL = 6480,
PC_FATIGUE_MAX_LEVEL = 2,
PC_FUSIONMATTER_MAX = 999999999,
PC_CANDY_MAX = 999999999,
PC_BATTERY_MAX = 9999,
PC_LEVEL_MAX = 36,
SIZEOF_PC_BULLET_SLOT = 3,
PC_TICK_TIME = 5000,
SIZEOF_EQUIP_SLOT = 9,
EQUIP_SLOT_HAND = 0,
EQUIP_SLOT_UPPERBODY = 1,
EQUIP_SLOT_LOWERBODY = 2,
EQUIP_SLOT_FOOT = 3,
EQUIP_SLOT_HEAD = 4,
EQUIP_SLOT_FACE = 5,
EQUIP_SLOT_BACK = 6,
EQUIP_SLOT_END = 6,
EQUIP_SLOT_HAND_EX = 7,
EQUIP_SLOT_VEHICLE = 8,
WPN_EQUIP_TYPE_NONE = 0,
WPN_EQUIP_TYPE_OH_BLADE = 1,
WPN_EQUIP_TYPE_OH_CLUB = 2,
WPN_EQUIP_TYPE_OH_PISTOL = 3,
WPN_EQUIP_TYPE_OH_RIPLE = 4,
WPN_EQUIP_TYPE_OH_THROW = 5,
WPN_EQUIP_TYPE_DH_BLADE = 6,
WPN_EQUIP_TYPE_DH_CLUB = 7,
WPN_EQUIP_TYPE_DH_DPISTOL = 8,
WPN_EQUIP_TYPE_DH_RIPLE = 9,
WPN_EQUIP_TYPE_DH_THROW = 10,
WPN_EQUIP_TYPE_DH_ROCKET = 11,
SIZEOF_INVEN_SLOT = 50,
SIZEOF_QINVEN_SLOT = 50,
SIZEOF_BANK_SLOT = 119,
SIZEOF_RESTORE_SLOT = 5,
SIZEOF_NANO_BANK_SLOT = 37,
SIZEOF_QUEST_SLOT = 1024,
NANO_QUEST_INDEX = 0,
SIZEOF_RQUEST_SLOT = 9,
SIZEOF_QUESTFLAG_NUMBER = 32,
SIZEOF_EP_RECORD_SLOT = 51,
SIZEOF_TRADE_SLOT = 12,
SIZEOF_VENDOR_TABLE_SLOT = 20,
SIZEOF_VENDOR_RESTORE_SLOT = 5,
SIZEOF_QUEST_NPC_SLOT = 3,
SIZEOF_QUEST_ITEM_SLOT = 3,
SIZEOF_MAX_ITEM_STACK = 100,
SIZEOF_PC_SKILL_SLOT = 33,
SIZEOF_QUICK_SLOT = 8,
ENCHANT_WEAPON_MATERIAL_ID = 101,
ENCHANT_DEFENCE_MATERIAL_ID = 102,
SIZEOF_NANO_CARRY_SLOT = 3,
COUNTOF_NANO_PER_SET = 3,
SIZEOF_NANO_SET = 13,
SIZEOF_NANO_STYLE = 3,
NANO_STYLE_NONE = 1,
NANO_STYLE_CRYSTAL = 0,
NANO_STYLE_ENERGY = 1,
NANO_STYLE_FLUID = 2,
SIZEOF_NANO_TYPE = 4,
NANO_TYPE_POWER = 0,
NANO_TYPE_ACCURACY = 1,
NANO_TYPE_PROTECT = 2,
NANO_TYPE_DODGE = 3,
SIZEOF_NANO_TUNE_NEED_ITEM_SLOT = 10,
VALUE_ATTACK_MISS = 1,
MSG_ONLINE = 1,
MSG_BUSY = 2,
MSG_OFFLINE = 0,
SIZEOF_FREE_CHAT_STRING = 128,
SIZEOF_MENU_CHAT_STRING = 128,
SIZEOF_BUDDYLIST_SLOT = 50,
SIZEOF_EMAIL_SUBJECT_STRING = 32,
SIZEOF_EMAIL_CONTENT_STRING = 512,
SIZEOF_EMAIL_PAGE_SIZE = 5,
SIZEOF_EMAIL_ITEM_CNT = 4,
EMAIL_AND_MONEY_COST = 50,
EMAIL_ITEM_COST = 20,
BUDDYWARP_INTERVAL = 60,
EMAILSEND_TIME_DELAY = 60,
DB_ERROR_INVALID_DATA = 1,
DB_ERROR_HACK_ATTEMPT = 2,
DB_ERROR_ACCESS_FAIL = 3,
DB_ERROR_PC_INSERT_FAIL = 4,
CALL_NPC_MAX_CNT = 2048,
CN_EP_RING_MAX_CNT = 999,
HF_BIT_NONE = 0,
HF_BIT_NORMAL = 1,
HF_BIT_CRITICAL = 2,
HF_BIT_STYLE_WIN = 4,
HF_BIT_STYLE_TIE = 8,
HF_BIT_STYLE_LOSE = 16,
SKIN_COLOR_MAX = 12,
HAIR_COLOR_MAX = 18,
EYE_COLOR_MAX = 5,
BODY_TYPE_MAX = 3,
HEIGHT_TYPE_MAX = 5,
CLASS_TYPE_MAX = 4,
CN_EP_RACE_MODE_PRACTICE = 0,
CN_EP_RACE_MODE_RECORD = 1,
CN_EP_SECOM_NPC_TYPE_NUM = 13,
CN_EP_EECOM_NPC_TYPE_NUM = 14,
CN_EP_SIZE_SMALL = 0,
CN_EP_SIZE_MIDDLE = 1,
CN_EP_SIZE_BIG = 2,
CN_EP_TICKET_ITEM_ID_SMALL = 115,
CN_EP_TICKET_ITEM_ID_MIDDLE = 116,
CN_EP_TICKET_ITEM_ID_BIG = 117,
CN_EP_TICKET_ITEM_ID_FREE = 118,
CN_EP_DISTANCE_ERROR_SAFE_RANGE = 1200,
CN_ACCOUNT_LEVEL__MASTER = 1,
CN_ACCOUNT_LEVEL__POWER_DEVELOPER = 10,
CN_ACCOUNT_LEVEL__QA = 20,
CN_ACCOUNT_LEVEL__GM = 30,
CN_ACCOUNT_LEVEL__CS = 40,
CN_ACCOUNT_LEVEL__FREE_USER = 48,
CN_ACCOUNT_LEVEL__PAY_USER = 49,
CN_ACCOUNT_LEVEL__DEVELOPER = 50,
CN_ACCOUNT_LEVEL__CLOSEBETA_USER = 80,
CN_ACCOUNT_LEVEL__OPENBETA_USER = 85,
CN_ACCOUNT_LEVEL__USER = 99,
CN_SPECIAL_STATE_FLAG__PRINT_GM = 1,
CN_SPECIAL_STATE_FLAG__INVISIBLE = 2,
CN_SPECIAL_STATE_FLAG__INVULNERABLE = 4,
CN_SPECIAL_STATE_FLAG__FULL_UI = 16,
CN_SPECIAL_STATE_FLAG__COMBAT = 32,
CN_SPECIAL_STATE_FLAG__MUTE_FREECHAT = 64,
CN_GM_SET_VALUE_TYPE__HP = 1,
CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY = 2,
CN_GM_SET_VALUE_TYPE__NANO_BATTERY = 3,
CN_GM_SET_VALUE_TYPE__FUSION_MATTER = 4,
CN_GM_SET_VALUE_TYPE__CANDY = 5,
CN_GM_SET_VALUE_TYPE__SPEED = 6,
CN_GM_SET_VALUE_TYPE__JUMP = 7,
CN_GM_SET_VALUE_TYPE__END = 8,
HEIGHT_CLIMBABLE = 150,
CN_GROUP_WARP_CHECK_RANGE = 1000,
WYVERN_LOCATION_FLAG_SIZE = 2,
CN_PC_EVENT_ID_GET_NANO_QUEST = 1,
CN_PC_EVENT_ID_DEFEAT_FUSE_AND_GET_NANO = 2,
_dCN_STREETSTALL__ITEMLIST_COUNT_MAX = 5,
CSB_BIT_NONE = 0, // 0
CSB_BIT_UP_MOVE_SPEED = 0x1, // 1
CSB_BIT_UP_SWIM_SPEED = 0x2, // 2
CSB_BIT_UP_JUMP_HEIGHT = 0x4, // 4
CSB_BIT_UP_STEALTH = 0x8, // 8
CSB_BIT_PHOENIX = 0x10, // 16
CSB_BIT_PROTECT_BATTERY = 0x20, // 32
CSB_BIT_PROTECT_INFECTION = 0x40, // 64
CSB_BIT_DN_MOVE_SPEED = 0x80, // 128
CSB_BIT_DN_ATTACK_SPEED = 0x100, // 256
CSB_BIT_STUN = 0x200, // 512
CSB_BIT_MEZ = 0x400, // 1024
CSB_BIT_KNOCKDOWN = 0x800, // 2048
CSB_BIT_MINIMAP_ENEMY = 0x1000, // 4096
CSB_BIT_MINIMAP_TRESURE = 0x2000, // 8192
CSB_BIT_REWARD_BLOB = 0x4000, // 16384
CSB_BIT_REWARD_CASH = 0x8000, // 32768
CSB_BIT_INFECTION = 0x10000, // 65536
CSB_BIT_FREEDOM = 0x20000, // 131072
CSB_BIT_BOUNDINGBALL = 0x40000, // 262144
CSB_BIT_INVULNERABLE = 0x80000, // 524288
CSB_BIT_STIMPAKSLOT1 = 0x100000, // 1048576
CSB_BIT_STIMPAKSLOT2 = 0x200000, // 2097152
CSB_BIT_STIMPAKSLOT3 = 0x400000, // 4194304
CSB_BIT_HEAL = 0x800000, // 8388608
CSB_BIT_EXTRABANK = 0x1000000, // 16777216
TIME_BUFF_CONFIRM_KEY_MAX = 2000000000,
READPACKET_SUCC = 0,
READPACKET_FAIL = 1,
READPACKET_RETURN = 2,
BITMASK_FROM2TO = 0x7fffffff, // 2147483647
BITMASK_FROM = 0x7fffffff, // 2147483647
BITMASK_TO = 0xf000000, // 251658240
BITMASK_SENDBLOCK = 0x800000, // 8388608
BITMASK_AUTHED = 0x400000, // 4194304
BITMASK_U_ID = 0xfff, // 4095
CL2LS = 0x12000000,
CL2FE = 0x13000000,
LS2CL = 0x21000000,
LS2LS = 0x22000000,
LS2DBA = 0x27000000,
FE2CL = 0x31000000,
FE2FE = 0x33000000,
FE2GS = 0x34000000,
FE2EP = 0x36000000,
FE2MSG = 0x38000000,
GS2FE = 0x43000000,
GS2GS = 0x44000000,
GS2AI = 0x45000000,
GS2EP = 0x46000000,
GS2DBA = 0x47000000,
GS2MSG = 0x48000000,
GS2MGR = 0x4a000000,
AI2GS = 0x54000000,
EP2FE = 0x63000000,
EP2GS = 0x64000000,
DBA2GS = 0x74000000,
DBA2EP = 0x75000000,
MSG2FE = 0x7fffffff,
MSG2GS = 0x7fffffff,
MSG2CMSG = 0x7fffffff,
CMSG2MSG = 0x7fffffff,
MGR2SPY = 0x7fffffff,
SPY2MGR = 0x7fffffff,
MGR2SA = 0x7fffffff,
SA2MGR = 0x7fffffff,
SA2SPY = 0x7fffffff,
SPY2SA = 0x7fffffff,
SPY2SVR = 0x7fffffff,
SVR2SPY = 0x7fffffff,
SCH2SVR = 0x7fffffff,
SCH2LS = 0x7fffffff,
SCH2FE = 0x7fffffff,
SCH2GS = 0x7fffffff,
SCH2AI = 0x7fffffff,
SCH2EP = 0x7fffffff,
SCH2DBA = 0x7fffffff,
SCH2MSG = 0x7fffffff,
SCH2CMSG = 0x7fffffff,
CL2CDR = 0x1f000000,
SENDBLOCK = 0x800000, // 8388608
AUTHED_X = 0, // 0
AUTHED_O = 0x400000, // 4194304
SEND_SVR_FE = 1,
SEND_SVR_FE_ANY = 2,
SEND_SVR_FE_ALL = 3,
SEND_SVR_AI = 4,
SEND_SVR_AI_ANY = 5,
SEND_SVR_AI_ALL = 6,
SEND_SVR_FE_AI_ALL = 7,
SEND_SVR_DBA = 8,
SEND_SVR_GS = 9,
SEND_SVR_MSG = 10,
SEND_SVR_MSG_ANY = 11,
SEND_SVR_MSG_ALL = 12,
SEND_UNICAST = 1,
SEND_ANYCAST = 2,
SEND_ANYCAST_NEW = 3,
SEND_BROADCAST = 4,
#if PROTOCOL_VERSION == 728
CN_PACKET_BUFFER_SIZE = 8192,
#elif PROTOCOL_VERSION == 1013
CN_PACKET_BUFFER_SIZE = 8192,
#else
CN_PACKET_BUFFER_SIZE = 4096,
#endif
P_CL2LS_REQ_LOGIN = 0x12000001, // 301989889
P_CL2LS_REQ_CHECK_CHAR_NAME = 0x12000002, // 301989890
P_CL2LS_REQ_SAVE_CHAR_NAME = 0x12000003, // 301989891
P_CL2LS_REQ_CHAR_CREATE = 0x12000004, // 301989892
P_CL2LS_REQ_CHAR_SELECT = 0x12000005, // 301989893
P_CL2LS_REQ_CHAR_DELETE = 0x12000006, // 301989894
P_CL2LS_REQ_SHARD_SELECT = 0x12000007, // 301989895
P_CL2LS_REQ_SHARD_LIST_INFO = 0x12000008, // 301989896
P_CL2LS_CHECK_NAME_LIST = 0x12000009, // 301989897
P_CL2LS_REQ_SAVE_CHAR_TUTOR = 0x1200000a, // 301989898
P_CL2LS_REQ_PC_EXIT_DUPLICATE = 0x1200000b, // 301989899
P_CL2LS_REP_LIVE_CHECK = 0x1200000c, // 301989900
P_CL2LS_REQ_CHANGE_CHAR_NAME = 0x1200000d, // 301989901
P_CL2LS_REQ_SERVER_SELECT = 0x1200000e, // 301989902
P_CL2FE_REQ_PC_ENTER = 0x13000001, // 318767105
P_CL2FE_REQ_PC_EXIT = 0x13000002, // 318767106
P_CL2FE_REQ_PC_MOVE = 0x13000003, // 318767107
P_CL2FE_REQ_PC_STOP = 0x13000004, // 318767108
P_CL2FE_REQ_PC_JUMP = 0x13000005, // 318767109
P_CL2FE_REQ_PC_ATTACK_NPCs = 0x13000006, // 318767110
P_CL2FE_REQ_SEND_FREECHAT_MESSAGE = 0x13000007, // 318767111
P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE = 0x13000008, // 318767112
P_CL2FE_REQ_PC_REGEN = 0x13000009, // 318767113
P_CL2FE_REQ_ITEM_MOVE = 0x1300000a, // 318767114
P_CL2FE_REQ_PC_TASK_START = 0x1300000b, // 318767115
P_CL2FE_REQ_PC_TASK_END = 0x1300000c, // 318767116
P_CL2FE_REQ_NANO_EQUIP = 0x1300000d, // 318767117
P_CL2FE_REQ_NANO_UNEQUIP = 0x1300000e, // 318767118
P_CL2FE_REQ_NANO_ACTIVE = 0x1300000f, // 318767119
P_CL2FE_REQ_NANO_TUNE = 0x13000010, // 318767120
P_CL2FE_REQ_NANO_SKILL_USE = 0x13000011, // 318767121
P_CL2FE_REQ_PC_TASK_STOP = 0x13000012, // 318767122
P_CL2FE_REQ_PC_TASK_CONTINUE = 0x13000013, // 318767123
P_CL2FE_REQ_PC_GOTO = 0x13000014, // 318767124
P_CL2FE_REQ_CHARGE_NANO_STAMINA = 0x13000015, // 318767125
P_CL2FE_REQ_PC_KILL_QUEST_NPCs = 0x13000016, // 318767126
P_CL2FE_REQ_PC_VENDOR_ITEM_BUY = 0x13000017, // 318767127
P_CL2FE_REQ_PC_VENDOR_ITEM_SELL = 0x13000018, // 318767128
P_CL2FE_REQ_PC_ITEM_DELETE = 0x13000019, // 318767129
P_CL2FE_REQ_PC_GIVE_ITEM = 0x1300001a, // 318767130
P_CL2FE_REQ_PC_ROCKET_STYLE_READY = 0x1300001b, // 318767131
P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE = 0x1300001c, // 318767132
P_CL2FE_REQ_PC_ROCKET_STYLE_HIT = 0x1300001d, // 318767133
P_CL2FE_REQ_PC_GRENADE_STYLE_READY = 0x1300001e, // 318767134
P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE = 0x1300001f, // 318767135
P_CL2FE_REQ_PC_GRENADE_STYLE_HIT = 0x13000020, // 318767136
P_CL2FE_REQ_PC_NANO_CREATE = 0x13000021, // 318767137
P_CL2FE_REQ_PC_TRADE_OFFER = 0x13000022, // 318767138
P_CL2FE_REQ_PC_TRADE_OFFER_CANCEL = 0x13000023, // 318767139
P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT = 0x13000024, // 318767140
P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL = 0x13000025, // 318767141
P_CL2FE_REQ_PC_TRADE_OFFER_ABORT = 0x13000026, // 318767142
P_CL2FE_REQ_PC_TRADE_CONFIRM = 0x13000027, // 318767143
P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL = 0x13000028, // 318767144
P_CL2FE_REQ_PC_TRADE_CONFIRM_ABORT = 0x13000029, // 318767145
P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER = 0x1300002a, // 318767146
P_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER = 0x1300002b, // 318767147
P_CL2FE_REQ_PC_TRADE_CASH_REGISTER = 0x1300002c, // 318767148
P_CL2FE_REQ_PC_TRADE_EMOTES_CHAT = 0x1300002d, // 318767149
P_CL2FE_REQ_PC_BANK_OPEN = 0x1300002e, // 318767150
P_CL2FE_REQ_PC_BANK_CLOSE = 0x1300002f, // 318767151
P_CL2FE_REQ_PC_VENDOR_START = 0x13000030, // 318767152
P_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE = 0x13000031, // 318767153
P_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY = 0x13000032, // 318767154
P_CL2FE_REQ_PC_COMBAT_BEGIN = 0x13000033, // 318767155
P_CL2FE_REQ_PC_COMBAT_END = 0x13000034, // 318767156
P_CL2FE_REQ_REQUEST_MAKE_BUDDY = 0x13000035, // 318767157
P_CL2FE_REQ_ACCEPT_MAKE_BUDDY = 0x13000036, // 318767158
P_CL2FE_REQ_SEND_BUDDY_FREECHAT_MESSAGE = 0x13000037, // 318767159
P_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE = 0x13000038, // 318767160
P_CL2FE_REQ_GET_BUDDY_STYLE = 0x13000039, // 318767161
P_CL2FE_REQ_SET_BUDDY_BLOCK = 0x1300003a, // 318767162
P_CL2FE_REQ_REMOVE_BUDDY = 0x1300003b, // 318767163
P_CL2FE_REQ_GET_BUDDY_STATE = 0x1300003c, // 318767164
P_CL2FE_REQ_PC_JUMPPAD = 0x1300003d, // 318767165
P_CL2FE_REQ_PC_LAUNCHER = 0x1300003e, // 318767166
P_CL2FE_REQ_PC_ZIPLINE = 0x1300003f, // 318767167
P_CL2FE_REQ_PC_MOVEPLATFORM = 0x13000040, // 318767168
P_CL2FE_REQ_PC_SLOPE = 0x13000041, // 318767169
P_CL2FE_REQ_PC_STATE_CHANGE = 0x13000042, // 318767170
P_CL2FE_REQ_PC_MAP_WARP = 0x13000043, // 318767171
P_CL2FE_REQ_PC_GIVE_NANO = 0x13000044, // 318767172
P_CL2FE_REQ_NPC_SUMMON = 0x13000045, // 318767173
P_CL2FE_REQ_NPC_UNSUMMON = 0x13000046, // 318767174
P_CL2FE_REQ_ITEM_CHEST_OPEN = 0x13000047, // 318767175
P_CL2FE_REQ_PC_GIVE_NANO_SKILL = 0x13000048, // 318767176
P_CL2FE_DOT_DAMAGE_ONOFF = 0x13000049, // 318767177
P_CL2FE_REQ_PC_VENDOR_BATTERY_BUY = 0x1300004a, // 318767178
P_CL2FE_REQ_PC_WARP_USE_NPC = 0x1300004b, // 318767179
P_CL2FE_REQ_PC_GROUP_INVITE = 0x1300004c, // 318767180
P_CL2FE_REQ_PC_GROUP_INVITE_REFUSE = 0x1300004d, // 318767181
P_CL2FE_REQ_PC_GROUP_JOIN = 0x1300004e, // 318767182
P_CL2FE_REQ_PC_GROUP_LEAVE = 0x1300004f, // 318767183
P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT = 0x13000050, // 318767184
P_CL2FE_REQ_PC_BUDDY_WARP = 0x13000051, // 318767185
P_CL2FE_REQ_GET_MEMBER_STYLE = 0x13000052, // 318767186
P_CL2FE_REQ_GET_GROUP_STYLE = 0x13000053, // 318767187
P_CL2FE_REQ_PC_CHANGE_MENTOR = 0x13000054, // 318767188
P_CL2FE_REQ_GET_BUDDY_LOCATION = 0x13000055, // 318767189
P_CL2FE_REQ_NPC_GROUP_SUMMON = 0x13000056, // 318767190
P_CL2FE_REQ_PC_WARP_TO_PC = 0x13000057, // 318767191
P_CL2FE_REQ_EP_RANK_GET_LIST = 0x13000058, // 318767192
P_CL2FE_REQ_EP_RANK_GET_DETAIL = 0x13000059, // 318767193
P_CL2FE_REQ_EP_RANK_GET_PC_INFO = 0x1300005a, // 318767194
P_CL2FE_REQ_EP_RACE_START = 0x1300005b, // 318767195
P_CL2FE_REQ_EP_RACE_END = 0x1300005c, // 318767196
P_CL2FE_REQ_EP_RACE_CANCEL = 0x1300005d, // 318767197
P_CL2FE_REQ_EP_GET_RING = 0x1300005e, // 318767198
P_CL2FE_REQ_IM_CHANGE_SWITCH_STATUS = 0x1300005f, // 318767199
P_CL2FE_REQ_SHINY_PICKUP = 0x13000060, // 318767200
P_CL2FE_REQ_SHINY_SUMMON = 0x13000061, // 318767201
P_CL2FE_REQ_PC_MOVETRANSPORTATION = 0x13000062, // 318767202
P_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE = 0x13000063, // 318767203
P_CL2FE_REQ_SEND_ANY_GROUP_FREECHAT_MESSAGE = 0x13000064, // 318767204
P_CL2FE_REQ_BARKER = 0x13000065, // 318767205
P_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE = 0x13000066, // 318767206
P_CL2FE_REQ_SEND_ANY_GROUP_MENUCHAT_MESSAGE = 0x13000067, // 318767207
P_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION = 0x13000068, // 318767208
P_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION = 0x13000069, // 318767209
P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH = 0x1300006a, // 318767210
P_CL2FE_GM_REQ_PC_SET_VALUE = 0x1300006b, // 318767211
P_CL2FE_GM_REQ_KICK_PLAYER = 0x1300006c, // 318767212
P_CL2FE_GM_REQ_TARGET_PC_TELEPORT = 0x1300006d, // 318767213
P_CL2FE_GM_REQ_PC_LOCATION = 0x1300006e, // 318767214
P_CL2FE_GM_REQ_PC_ANNOUNCE = 0x1300006f, // 318767215
P_CL2FE_REQ_SET_PC_BLOCK = 0x13000070, // 318767216
P_CL2FE_REQ_REGIST_RXCOM = 0x13000071, // 318767217
P_CL2FE_GM_REQ_PC_MOTD_REGISTER = 0x13000072, // 318767218
P_CL2FE_REQ_ITEM_USE = 0x13000073, // 318767219
P_CL2FE_REQ_WARP_USE_RECALL = 0x13000074, // 318767220
P_CL2FE_REP_LIVE_CHECK = 0x13000075, // 318767221
P_CL2FE_REQ_PC_MISSION_COMPLETE = 0x13000076, // 318767222
P_CL2FE_REQ_PC_TASK_COMPLETE = 0x13000077, // 318767223
P_CL2FE_REQ_NPC_INTERACTION = 0x13000078, // 318767224
P_CL2FE_DOT_HEAL_ONOFF = 0x13000079, // 318767225
P_CL2FE_REQ_PC_SPECIAL_STATE_SWITCH = 0x1300007a, // 318767226
P_CL2FE_REQ_PC_EMAIL_UPDATE_CHECK = 0x1300007b, // 318767227
P_CL2FE_REQ_PC_READ_EMAIL = 0x1300007c, // 318767228
P_CL2FE_REQ_PC_RECV_EMAIL_PAGE_LIST = 0x1300007d, // 318767229
P_CL2FE_REQ_PC_DELETE_EMAIL = 0x1300007e, // 318767230
P_CL2FE_REQ_PC_SEND_EMAIL = 0x1300007f, // 318767231
P_CL2FE_REQ_PC_RECV_EMAIL_ITEM = 0x13000080, // 318767232
P_CL2FE_REQ_PC_RECV_EMAIL_CANDY = 0x13000081, // 318767233
P_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF = 0x13000082, // 318767234
P_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID = 0x13000083, // 318767235
P_CL2FE_REQ_NPC_GROUP_INVITE = 0x13000084, // 318767236
P_CL2FE_REQ_NPC_GROUP_KICK = 0x13000085, // 318767237
P_CL2FE_REQ_PC_FIRST_USE_FLAG_SET = 0x13000086, // 318767238
P_CL2FE_REQ_PC_TRANSPORT_WARP = 0x13000087, // 318767239
P_CL2FE_REQ_PC_TIME_TO_GO_WARP = 0x13000088, // 318767240
P_CL2FE_REQ_PC_RECV_EMAIL_ITEM_ALL = 0x13000089, // 318767241
P_CL2FE_REQ_CHANNEL_INFO = 0x1300008a, // 318767242
P_CL2FE_REQ_PC_CHANNEL_NUM = 0x1300008b, // 318767243
P_CL2FE_REQ_PC_WARP_CHANNEL = 0x1300008c, // 318767244
P_CL2FE_REQ_PC_LOADING_COMPLETE = 0x1300008d, // 318767245
P_CL2FE_REQ_PC_FIND_NAME_MAKE_BUDDY = 0x1300008e, // 318767246
P_CL2FE_REQ_PC_FIND_NAME_ACCEPT_BUDDY = 0x1300008f, // 318767247
P_CL2FE_REQ_PC_ATTACK_CHARs = 0x13000090, // 318767248
P_CL2FE_PC_STREETSTALL_REQ_READY = 0x13000091, // 318767249
P_CL2FE_PC_STREETSTALL_REQ_CANCEL = 0x13000092, // 318767250
P_CL2FE_PC_STREETSTALL_REQ_REGIST_ITEM = 0x13000093, // 318767251
P_CL2FE_PC_STREETSTALL_REQ_UNREGIST_ITEM = 0x13000094, // 318767252
P_CL2FE_PC_STREETSTALL_REQ_SALE_START = 0x13000095, // 318767253
P_CL2FE_PC_STREETSTALL_REQ_ITEM_LIST = 0x13000096, // 318767254
P_CL2FE_PC_STREETSTALL_REQ_ITEM_BUY = 0x13000097, // 318767255
P_CL2FE_REQ_PC_ITEM_COMBINATION = 0x13000098, // 318767256
P_CL2FE_GM_REQ_SET_PC_SKILL = 0x13000099, // 318767257
P_CL2FE_REQ_PC_SKILL_ADD = 0x1300009a, // 318767258
P_CL2FE_REQ_PC_SKILL_DEL = 0x1300009b, // 318767259
P_CL2FE_REQ_PC_SKILL_USE = 0x1300009c, // 318767260
P_CL2FE_REQ_PC_ROPE = 0x1300009d, // 318767261
P_CL2FE_REQ_PC_BELT = 0x1300009e, // 318767262
P_CL2FE_REQ_PC_VEHICLE_ON = 0x1300009f, // 318767263
P_CL2FE_REQ_PC_VEHICLE_OFF = 0x130000a0, // 318767264
P_CL2FE_REQ_PC_REGIST_QUICK_SLOT = 0x130000a1, // 318767265
P_CL2FE_REQ_PC_DISASSEMBLE_ITEM = 0x130000a2, // 318767266
P_CL2FE_GM_REQ_REWARD_RATE = 0x130000a3, // 318767267
P_CL2FE_REQ_PC_ITEM_ENCHANT = 0x130000a4, // 318767268
P_FE2CL_ERROR = 0x31000000, // 822083584
P_FE2CL_REP_PC_ENTER_FAIL = 0x31000001, // 822083585
P_FE2CL_REP_PC_ENTER_SUCC = 0x31000002, // 822083586
P_FE2CL_PC_NEW = 0x31000003, // 822083587
P_FE2CL_REP_PC_EXIT_FAIL = 0x31000004, // 822083588
P_FE2CL_REP_PC_EXIT_SUCC = 0x31000005, // 822083589
P_FE2CL_PC_EXIT = 0x31000006, // 822083590
P_FE2CL_PC_AROUND = 0x31000007, // 822083591
P_FE2CL_PC_MOVE = 0x31000008, // 822083592
P_FE2CL_PC_STOP = 0x31000009, // 822083593
P_FE2CL_PC_JUMP = 0x3100000a, // 822083594
P_FE2CL_NPC_ENTER = 0x3100000b, // 822083595
P_FE2CL_NPC_EXIT = 0x3100000c, // 822083596
P_FE2CL_NPC_MOVE = 0x3100000d, // 822083597
P_FE2CL_NPC_NEW = 0x3100000e, // 822083598
P_FE2CL_NPC_AROUND = 0x3100000f, // 822083599
P_FE2CL_AROUND_DEL_PC = 0x31000010, // 822083600
P_FE2CL_AROUND_DEL_NPC = 0x31000011, // 822083601
P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC = 0x31000012, // 822083602
P_FE2CL_REP_SEND_FREECHAT_MESSAGE_FAIL = 0x31000013, // 822083603
P_FE2CL_PC_ATTACK_NPCs_SUCC = 0x31000014, // 822083604
P_FE2CL_PC_ATTACK_NPCs = 0x31000015, // 822083605
P_FE2CL_NPC_ATTACK_PCs = 0x31000016, // 822083606
P_FE2CL_REP_PC_REGEN_SUCC = 0x31000017, // 822083607
P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC = 0x31000018, // 822083608
P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_FAIL = 0x31000019, // 822083609
P_FE2CL_PC_ITEM_MOVE_SUCC = 0x3100001a, // 822083610
P_FE2CL_PC_EQUIP_CHANGE = 0x3100001b, // 822083611
P_FE2CL_REP_PC_TASK_START_SUCC = 0x3100001c, // 822083612
P_FE2CL_REP_PC_TASK_START_FAIL = 0x3100001d, // 822083613
P_FE2CL_REP_PC_TASK_END_SUCC = 0x3100001e, // 822083614
P_FE2CL_REP_PC_TASK_END_FAIL = 0x3100001f, // 822083615
P_FE2CL_NPC_SKILL_READY = 0x31000020, // 822083616
P_FE2CL_NPC_SKILL_FIRE = 0x31000021, // 822083617
P_FE2CL_NPC_SKILL_HIT = 0x31000022, // 822083618
P_FE2CL_NPC_SKILL_CORRUPTION_READY = 0x31000023, // 822083619
P_FE2CL_NPC_SKILL_CORRUPTION_HIT = 0x31000024, // 822083620
P_FE2CL_NPC_SKILL_CANCEL = 0x31000025, // 822083621
P_FE2CL_REP_NANO_EQUIP_SUCC = 0x31000026, // 822083622
P_FE2CL_REP_NANO_UNEQUIP_SUCC = 0x31000027, // 822083623
P_FE2CL_REP_NANO_ACTIVE_SUCC = 0x31000028, // 822083624
P_FE2CL_REP_NANO_TUNE_SUCC = 0x31000029, // 822083625
P_FE2CL_NANO_ACTIVE = 0x3100002a, // 822083626
P_FE2CL_NANO_SKILL_USE_SUCC = 0x3100002b, // 822083627
P_FE2CL_NANO_SKILL_USE = 0x3100002c, // 822083628
P_FE2CL_REP_PC_TASK_STOP_SUCC = 0x3100002d, // 822083629
P_FE2CL_REP_PC_TASK_STOP_FAIL = 0x3100002e, // 822083630
P_FE2CL_REP_PC_TASK_CONTINUE_SUCC = 0x3100002f, // 822083631
P_FE2CL_REP_PC_TASK_CONTINUE_FAIL = 0x31000030, // 822083632
P_FE2CL_REP_PC_GOTO_SUCC = 0x31000031, // 822083633
P_FE2CL_REP_CHARGE_NANO_STAMINA = 0x31000032, // 822083634
P_FE2CL_REP_PC_TICK = 0x31000033, // 822083635
P_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC = 0x31000034, // 822083636
P_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC = 0x31000035, // 822083637
P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL = 0x31000036, // 822083638
P_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC = 0x31000037, // 822083639
P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL = 0x31000038, // 822083640
P_FE2CL_REP_PC_ITEM_DELETE_SUCC = 0x31000039, // 822083641
P_FE2CL_PC_ROCKET_STYLE_READY = 0x3100003a, // 822083642
P_FE2CL_REP_PC_ROCKET_STYLE_FIRE_SUCC = 0x3100003b, // 822083643
P_FE2CL_PC_ROCKET_STYLE_FIRE = 0x3100003c, // 822083644
P_FE2CL_PC_ROCKET_STYLE_HIT = 0x3100003d, // 822083645
P_FE2CL_PC_GRENADE_STYLE_READY = 0x3100003e, // 822083646
P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC = 0x3100003f, // 822083647
P_FE2CL_PC_GRENADE_STYLE_FIRE = 0x31000040, // 822083648
P_FE2CL_PC_GRENADE_STYLE_HIT = 0x31000041, // 822083649
P_FE2CL_REP_PC_TRADE_OFFER = 0x31000042, // 822083650
P_FE2CL_REP_PC_TRADE_OFFER_CANCEL = 0x31000043, // 822083651
P_FE2CL_REP_PC_TRADE_OFFER_SUCC = 0x31000044, // 822083652
P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL = 0x31000045, // 822083653
P_FE2CL_REP_PC_TRADE_OFFER_ABORT = 0x31000046, // 822083654
P_FE2CL_REP_PC_TRADE_CONFIRM = 0x31000047, // 822083655
P_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL = 0x31000048, // 822083656
P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT = 0x31000049, // 822083657
P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC = 0x3100004a, // 822083658
P_FE2CL_REP_PC_TRADE_CONFIRM_FAIL = 0x3100004b, // 822083659
P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC = 0x3100004c, // 822083660
P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_FAIL = 0x3100004d, // 822083661
P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC = 0x3100004e, // 822083662
P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_FAIL = 0x3100004f, // 822083663
P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC = 0x31000050, // 822083664
P_FE2CL_REP_PC_TRADE_CASH_REGISTER_FAIL = 0x31000051, // 822083665
P_FE2CL_REP_PC_TRADE_EMOTES_CHAT = 0x31000052, // 822083666
P_FE2CL_REP_PC_NANO_CREATE_SUCC = 0x31000053, // 822083667
P_FE2CL_REP_PC_NANO_CREATE_FAIL = 0x31000054, // 822083668
P_FE2CL_REP_NANO_TUNE_FAIL = 0x31000055, // 822083669
P_FE2CL_REP_PC_BANK_OPEN_SUCC = 0x31000056, // 822083670
P_FE2CL_REP_PC_BANK_OPEN_FAIL = 0x31000057, // 822083671
P_FE2CL_REP_PC_BANK_CLOSE_SUCC = 0x31000058, // 822083672
P_FE2CL_REP_PC_BANK_CLOSE_FAIL = 0x31000059, // 822083673
P_FE2CL_REP_PC_VENDOR_START_SUCC = 0x3100005a, // 822083674
P_FE2CL_REP_PC_VENDOR_START_FAIL = 0x3100005b, // 822083675
P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC = 0x3100005c, // 822083676
P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_FAIL = 0x3100005d, // 822083677
P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC = 0x3100005e, // 822083678
P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL = 0x3100005f, // 822083679
P_FE2CL_CHAR_TIME_BUFF_TIME_OUT = 0x31000060, // 822083680
P_FE2CL_REP_PC_GIVE_ITEM_SUCC = 0x31000061, // 822083681
P_FE2CL_REP_PC_GIVE_ITEM_FAIL = 0x31000062, // 822083682
P_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC = 0x31000063, // 822083683
P_FE2CL_REP_PC_BUDDYLIST_INFO_FAIL = 0x31000064, // 822083684
P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC = 0x7fffffff, // 2147483647
P_FE2CL_REP_REQUEST_MAKE_BUDDY_FAIL = 0x31000066, // 822083686
P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC = 0x31000067, // 822083687
P_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL = 0x31000068, // 822083688
P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC = 0x31000069, // 822083689
P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_FAIL = 0x3100006a, // 822083690
P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC = 0x3100006b, // 822083691
P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_FAIL = 0x3100006c, // 822083692
P_FE2CL_REP_GET_BUDDY_STYLE_SUCC = 0x3100006d, // 822083693
P_FE2CL_REP_GET_BUDDY_STYLE_FAIL = 0x3100006e, // 822083694
P_FE2CL_REP_GET_BUDDY_STATE_SUCC = 0x3100006f, // 822083695
P_FE2CL_REP_GET_BUDDY_STATE_FAIL = 0x31000070, // 822083696
P_FE2CL_REP_SET_BUDDY_BLOCK_SUCC = 0x31000071, // 822083697
P_FE2CL_REP_SET_BUDDY_BLOCK_FAIL = 0x31000072, // 822083698
P_FE2CL_REP_REMOVE_BUDDY_SUCC = 0x31000073, // 822083699
P_FE2CL_REP_REMOVE_BUDDY_FAIL = 0x31000074, // 822083700
P_FE2CL_PC_JUMPPAD = 0x31000075, // 822083701
P_FE2CL_PC_LAUNCHER = 0x31000076, // 822083702
P_FE2CL_PC_ZIPLINE = 0x31000077, // 822083703
P_FE2CL_PC_MOVEPLATFORM = 0x31000078, // 822083704
P_FE2CL_PC_SLOPE = 0x31000079, // 822083705
P_FE2CL_PC_STATE_CHANGE = 0x3100007a, // 822083706
P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC_TO_ACCEPTER = 0x3100007b, // 822083707
P_FE2CL_REP_REWARD_ITEM = 0x3100007c, // 822083708
P_FE2CL_REP_ITEM_CHEST_OPEN_SUCC = 0x3100007d, // 822083709
P_FE2CL_REP_ITEM_CHEST_OPEN_FAIL = 0x3100007e, // 822083710
P_FE2CL_CHAR_TIME_BUFF_TIME_TICK = 0x3100007f, // 822083711
P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC = 0x31000080, // 822083712
P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL = 0x31000081, // 822083713
P_FE2CL_NPC_ROCKET_STYLE_FIRE = 0x31000082, // 822083714
P_FE2CL_NPC_GRENADE_STYLE_FIRE = 0x31000083, // 822083715
P_FE2CL_NPC_BULLET_STYLE_HIT = 0x31000084, // 822083716
P_FE2CL_CHARACTER_ATTACK_CHARACTERs = 0x31000085, // 822083717
P_FE2CL_PC_GROUP_INVITE = 0x31000086, // 822083718
P_FE2CL_PC_GROUP_INVITE_FAIL = 0x31000087, // 822083719
P_FE2CL_PC_GROUP_INVITE_REFUSE = 0x31000088, // 822083720
P_FE2CL_PC_GROUP_JOIN = 0x31000089, // 822083721
P_FE2CL_PC_GROUP_JOIN_FAIL = 0x3100008a, // 822083722
P_FE2CL_PC_GROUP_JOIN_SUCC = 0x3100008b, // 822083723
P_FE2CL_PC_GROUP_LEAVE = 0x3100008c, // 822083724
P_FE2CL_PC_GROUP_LEAVE_FAIL = 0x3100008d, // 822083725
P_FE2CL_PC_GROUP_LEAVE_SUCC = 0x3100008e, // 822083726
P_FE2CL_PC_GROUP_MEMBER_INFO = 0x3100008f, // 822083727
P_FE2CL_REP_PC_WARP_USE_NPC_SUCC = 0x31000090, // 822083728
P_FE2CL_REP_PC_WARP_USE_NPC_FAIL = 0x31000091, // 822083729
P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT = 0x31000092, // 822083730
P_FE2CL_REP_PC_CHANGE_MENTOR_SUCC = 0x31000093, // 822083731
P_FE2CL_REP_PC_CHANGE_MENTOR_FAIL = 0x31000094, // 822083732
P_FE2CL_REP_GET_MEMBER_STYLE_FAIL = 0x31000095, // 822083733
P_FE2CL_REP_GET_MEMBER_STYLE_SUCC = 0x31000096, // 822083734
P_FE2CL_REP_GET_GROUP_STYLE_FAIL = 0x31000097, // 822083735
P_FE2CL_REP_GET_GROUP_STYLE_SUCC = 0x31000098, // 822083736
P_FE2CL_PC_REGEN = 0x31000099, // 822083737
P_FE2CL_INSTANCE_MAP_INFO = 0x3100009a, // 822083738
P_FE2CL_TRANSPORTATION_ENTER = 0x3100009b, // 822083739
P_FE2CL_TRANSPORTATION_EXIT = 0x3100009c, // 822083740
P_FE2CL_TRANSPORTATION_MOVE = 0x3100009d, // 822083741
P_FE2CL_TRANSPORTATION_NEW = 0x3100009e, // 822083742
P_FE2CL_TRANSPORTATION_AROUND = 0x3100009f, // 822083743
P_FE2CL_AROUND_DEL_TRANSPORTATION = 0x310000a0, // 822083744
P_FE2CL_REP_EP_RANK_LIST = 0x310000a1, // 822083745
P_FE2CL_REP_EP_RANK_DETAIL = 0x310000a2, // 822083746
P_FE2CL_REP_EP_RANK_PC_INFO = 0x310000a3, // 822083747
P_FE2CL_REP_EP_RACE_START_SUCC = 0x310000a4, // 822083748
P_FE2CL_REP_EP_RACE_START_FAIL = 0x310000a5, // 822083749
P_FE2CL_REP_EP_RACE_END_SUCC = 0x310000a6, // 822083750
P_FE2CL_REP_EP_RACE_END_FAIL = 0x310000a7, // 822083751
P_FE2CL_REP_EP_RACE_CANCEL_SUCC = 0x310000a8, // 822083752
P_FE2CL_REP_EP_RACE_CANCEL_FAIL = 0x310000a9, // 822083753
P_FE2CL_REP_EP_GET_RING_SUCC = 0x310000aa, // 822083754
P_FE2CL_REP_EP_GET_RING_FAIL = 0x310000ab, // 822083755
P_FE2CL_REP_IM_CHANGE_SWITCH_STATUS = 0x310000ac, // 822083756
P_FE2CL_SHINY_ENTER = 0x310000ad, // 822083757
P_FE2CL_SHINY_EXIT = 0x310000ae, // 822083758
P_FE2CL_SHINY_NEW = 0x310000af, // 822083759
P_FE2CL_SHINY_AROUND = 0x310000b0, // 822083760
P_FE2CL_AROUND_DEL_SHINY = 0x310000b1, // 822083761
P_FE2CL_REP_SHINY_PICKUP_FAIL = 0x310000b2, // 822083762
P_FE2CL_REP_SHINY_PICKUP_SUCC = 0x310000b3, // 822083763
P_FE2CL_PC_MOVETRANSPORTATION = 0x310000b4, // 822083764
P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC = 0x310000b5, // 822083765
P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_FAIL = 0x310000b6, // 822083766
P_FE2CL_REP_SEND_ANY_GROUP_FREECHAT_MESSAGE_SUCC = 0x310000b7, // 822083767
P_FE2CL_REP_SEND_ANY_GROUP_FREECHAT_MESSAGE_FAIL = 0x310000b8, // 822083768
P_FE2CL_REP_BARKER = 0x310000b9, // 822083769
P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC = 0x310000ba, // 822083770
P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_FAIL = 0x310000bb, // 822083771
P_FE2CL_REP_SEND_ANY_GROUP_MENUCHAT_MESSAGE_SUCC = 0x310000bc, // 822083772
P_FE2CL_REP_SEND_ANY_GROUP_MENUCHAT_MESSAGE_FAIL = 0x310000bd, // 822083773
P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL = 0x310000be, // 822083774
P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC = 0x310000bf, // 822083775
P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL = 0x310000c0, // 822083776
P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC = 0x310000c1, // 822083777
P_FE2CL_ANNOUNCE_MSG = 0x310000c2, // 822083778
P_FE2CL_REP_PC_SPECIAL_STATE_SWITCH_SUCC = 0x310000c3, // 822083779
P_FE2CL_PC_SPECIAL_STATE_CHANGE = 0x310000c4, // 822083780
P_FE2CL_GM_REP_PC_SET_VALUE = 0x310000c5, // 822083781
P_FE2CL_GM_PC_CHANGE_VALUE = 0x310000c6, // 822083782
P_FE2CL_GM_REP_PC_LOCATION = 0x310000c7, // 822083783
P_FE2CL_GM_REP_PC_ANNOUNCE = 0x310000c8, // 822083784
P_FE2CL_REP_PC_BUDDY_WARP_FAIL = 0x310000c9, // 822083785
P_FE2CL_REP_PC_CHANGE_LEVEL = 0x310000ca, // 822083786
P_FE2CL_REP_SET_PC_BLOCK_SUCC = 0x310000cb, // 822083787
P_FE2CL_REP_SET_PC_BLOCK_FAIL = 0x310000cc, // 822083788
P_FE2CL_REP_REGIST_RXCOM = 0x310000cd, // 822083789
P_FE2CL_REP_REGIST_RXCOM_FAIL = 0x310000ce, // 822083790
P_FE2CL_PC_INVEN_FULL_MSG = 0x310000cf, // 822083791
P_FE2CL_REQ_LIVE_CHECK = 0x310000d0, // 822083792
P_FE2CL_PC_MOTD_LOGIN = 0x310000d1, // 822083793
P_FE2CL_REP_PC_ITEM_USE_FAIL = 0x310000d2, // 822083794
P_FE2CL_REP_PC_ITEM_USE_SUCC = 0x310000d3, // 822083795
P_FE2CL_PC_ITEM_USE = 0x310000d4, // 822083796
P_FE2CL_REP_GET_BUDDY_LOCATION_SUCC = 0x310000d5, // 822083797
P_FE2CL_REP_GET_BUDDY_LOCATION_FAIL = 0x310000d6, // 822083798
P_FE2CL_REP_PC_RIDING_FAIL = 0x310000d7, // 822083799
P_FE2CL_REP_PC_RIDING_SUCC = 0x310000d8, // 822083800
P_FE2CL_PC_RIDING = 0x310000d9, // 822083801
P_FE2CL_PC_BROOMSTICK_MOVE = 0x310000da, // 822083802
P_FE2CL_REP_PC_BUDDY_WARP_OTHER_SHARD_SUCC = 0x310000db, // 822083803
P_FE2CL_REP_WARP_USE_RECALL_FAIL = 0x310000dc, // 822083804
P_FE2CL_REP_PC_EXIT_DUPLICATE = 0x310000dd, // 822083805
P_FE2CL_REP_PC_MISSION_COMPLETE_SUCC = 0x310000de, // 822083806
P_FE2CL_PC_BUFF_UPDATE = 0x310000df, // 822083807
P_FE2CL_REP_PC_NEW_EMAIL = 0x310000e0, // 822083808
P_FE2CL_REP_PC_READ_EMAIL_SUCC = 0x310000e1, // 822083809
P_FE2CL_REP_PC_READ_EMAIL_FAIL = 0x310000e2, // 822083810
P_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_SUCC = 0x310000e3, // 822083811
P_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_FAIL = 0x310000e4, // 822083812
P_FE2CL_REP_PC_DELETE_EMAIL_SUCC = 0x310000e5, // 822083813
P_FE2CL_REP_PC_DELETE_EMAIL_FAIL = 0x310000e6, // 822083814
P_FE2CL_REP_PC_SEND_EMAIL_SUCC = 0x310000e7, // 822083815
P_FE2CL_REP_PC_SEND_EMAIL_FAIL = 0x310000e8, // 822083816
P_FE2CL_REP_PC_RECV_EMAIL_ITEM_SUCC = 0x310000e9, // 822083817
P_FE2CL_REP_PC_RECV_EMAIL_ITEM_FAIL = 0x310000ea, // 822083818
P_FE2CL_REP_PC_RECV_EMAIL_CANDY_SUCC = 0x310000eb, // 822083819
P_FE2CL_REP_PC_RECV_EMAIL_CANDY_FAIL = 0x310000ec, // 822083820
P_FE2CL_PC_SUDDEN_DEAD = 0x310000ed, // 822083821
P_FE2CL_REP_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF_SUCC = 0x310000ee, // 822083822
P_FE2CL_REP_PC_SET_CURRENT_MISSION_ID = 0x310000ef, // 822083823
P_FE2CL_REP_NPC_GROUP_INVITE_FAIL = 0x310000f0, // 822083824
P_FE2CL_REP_NPC_GROUP_INVITE_SUCC = 0x310000f1, // 822083825
P_FE2CL_REP_NPC_GROUP_KICK_FAIL = 0x310000f2, // 822083826
P_FE2CL_REP_NPC_GROUP_KICK_SUCC = 0x310000f3, // 822083827
P_FE2CL_PC_EVENT = 0x310000f4, // 822083828
P_FE2CL_REP_PC_TRANSPORT_WARP_SUCC = 0x310000f5, // 822083829
P_FE2CL_REP_PC_TRADE_EMOTES_CHAT_FAIL = 0x310000f6, // 822083830
P_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_SUCC = 0x310000f7, // 822083831
P_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_FAIL = 0x310000f8, // 822083832
P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC = 0x310000f9, // 822083833
P_FE2CL_REP_CHANNEL_INFO = 0x310000fa, // 822083834
P_FE2CL_REP_PC_CHANNEL_NUM = 0x310000fb, // 822083835
P_FE2CL_REP_PC_WARP_CHANNEL_FAIL = 0x310000fc, // 822083836
P_FE2CL_REP_PC_WARP_CHANNEL_SUCC = 0x310000fd, // 822083837
P_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_SUCC = 0x310000fe, // 822083838
P_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_FAIL = 0x310000ff, // 822083839
P_FE2CL_REP_PC_FIND_NAME_ACCEPT_BUDDY_FAIL = 0x31000100, // 822083840
P_FE2CL_REP_PC_BUDDY_WARP_SAME_SHARD_SUCC = 0x31000101, // 822083841
P_FE2CL_PC_ATTACK_CHARs_SUCC = 0x31000102, // 822083842
P_FE2CL_PC_ATTACK_CHARs = 0x31000103, // 822083843
P_FE2CL_NPC_ATTACK_CHARs = 0x31000104, // 822083844
P_FE2CL_REP_PC_CHANGE_LEVEL_SUCC = 0x31000105, // 822083845
P_FE2CL_REP_PC_NANO_CREATE = 0x31000106, // 822083846
P_FE2CL_PC_STREETSTALL_REP_READY_SUCC = 0x31000107, // 822083847
P_FE2CL_PC_STREETSTALL_REP_READY_FAIL = 0x31000108, // 822083848
P_FE2CL_PC_STREETSTALL_REP_CANCEL_SUCC = 0x31000109, // 822083849
P_FE2CL_PC_STREETSTALL_REP_CANCEL_FAIL = 0x3100010a, // 822083850
P_FE2CL_PC_STREETSTALL_REP_REGIST_ITEM_SUCC = 0x3100010b, // 822083851
P_FE2CL_PC_STREETSTALL_REP_REGIST_ITEM_FAIL = 0x3100010c, // 822083852
P_FE2CL_PC_STREETSTALL_REP_UNREGIST_ITEM_SUCC = 0x3100010d, // 822083853
P_FE2CL_PC_STREETSTALL_REP_UNREGIST_ITEM_FAIL = 0x3100010e, // 822083854
P_FE2CL_PC_STREETSTALL_REP_SALE_START_SUCC = 0x3100010f, // 822083855
P_FE2CL_PC_STREETSTALL_REP_SALE_START_FAIL = 0x31000110, // 822083856
P_FE2CL_PC_STREETSTALL_REP_ITEM_LIST = 0x31000111, // 822083857
P_FE2CL_PC_STREETSTALL_REP_ITEM_LIST_FAIL = 0x31000112, // 822083858
P_FE2CL_PC_STREETSTALL_REP_ITEM_BUY_SUCC_BUYER = 0x31000113, // 822083859
P_FE2CL_PC_STREETSTALL_REP_ITEM_BUY_SUCC_SELLER = 0x31000114, // 822083860
P_FE2CL_PC_STREETSTALL_REP_ITEM_BUY_FAIL = 0x31000115, // 822083861
P_FE2CL_REP_PC_ITEM_COMBINATION_SUCC = 0x31000116, // 822083862
P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL = 0x31000117, // 822083863
P_FE2CL_PC_CASH_BUFF_UPDATE = 0x31000118, // 822083864
P_FE2CL_REP_PC_SKILL_ADD_SUCC = 0x31000119, // 822083865
P_FE2CL_REP_PC_SKILL_ADD_FAIL = 0x3100011a, // 822083866
P_FE2CL_REP_PC_SKILL_DEL_SUCC = 0x3100011b, // 822083867
P_FE2CL_REP_PC_SKILL_DEL_FAIL = 0x3100011c, // 822083868
P_FE2CL_REP_PC_SKILL_USE_SUCC = 0x3100011d, // 822083869
P_FE2CL_REP_PC_SKILL_USE_FAIL = 0x3100011e, // 822083870
P_FE2CL_PC_SKILL_USE = 0x3100011f, // 822083871
P_FE2CL_PC_ROPE = 0x31000120, // 822083872
P_FE2CL_PC_BELT = 0x31000121, // 822083873
P_FE2CL_PC_VEHICLE_ON_SUCC = 0x31000122, // 822083874
P_FE2CL_PC_VEHICLE_ON_FAIL = 0x31000123, // 822083875
P_FE2CL_PC_VEHICLE_OFF_SUCC = 0x31000124, // 822083876
P_FE2CL_PC_VEHICLE_OFF_FAIL = 0x31000125, // 822083877
P_FE2CL_PC_QUICK_SLOT_INFO = 0x31000126, // 822083878
P_FE2CL_REP_PC_REGIST_QUICK_SLOT_FAIL = 0x31000127, // 822083879
P_FE2CL_REP_PC_REGIST_QUICK_SLOT_SUCC = 0x31000128, // 822083880
P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM = 0x31000129, // 822083881
P_FE2CL_REP_PC_DISASSEMBLE_ITEM_SUCC = 0x3100012a, // 822083882
P_FE2CL_REP_PC_DISASSEMBLE_ITEM_FAIL = 0x3100012b, // 822083883
P_FE2CL_GM_REP_REWARD_RATE_SUCC = 0x3100012c, // 822083884
P_FE2CL_REP_PC_ITEM_ENCHANT_SUCC = 0x3100012d, // 822083885
P_FE2CL_REP_PC_ITEM_ENCHANT_FAIL = 0x3100012e, // 822083886
P_LS2CL_REP_LOGIN_SUCC = 0x21000001, // 553648129
P_LS2CL_REP_LOGIN_FAIL = 0x21000002, // 553648130
P_LS2CL_REP_CHAR_INFO = 0x21000003, // 553648131
P_LS2CL_REP_CHECK_CHAR_NAME_SUCC = 0x21000005, // 553648133
P_LS2CL_REP_CHECK_CHAR_NAME_FAIL = 0x21000006, // 553648134
P_LS2CL_REP_SAVE_CHAR_NAME_SUCC = 0x21000007, // 553648135
P_LS2CL_REP_SAVE_CHAR_NAME_FAIL = 0x21000008, // 553648136
P_LS2CL_REP_CHAR_CREATE_SUCC = 0x21000009, // 553648137
P_LS2CL_REP_CHAR_CREATE_FAIL = 0x2100000a, // 553648138
P_LS2CL_REP_CHAR_SELECT_SUCC = 0x2100000b, // 553648139
P_LS2CL_REP_CHAR_SELECT_FAIL = 0x2100000c, // 553648140
P_LS2CL_REP_CHAR_DELETE_SUCC = 0x2100000d, // 553648141
P_LS2CL_REP_CHAR_DELETE_FAIL = 0x2100000e, // 553648142
P_LS2CL_REP_SHARD_SELECT_SUCC = 0x2100000f, // 553648143
P_LS2CL_REP_SHARD_SELECT_FAIL = 0x21000010, // 553648144
P_LS2CL_REP_VERSION_CHECK_SUCC = 0x21000011, // 553648145
P_LS2CL_REP_VERSION_CHECK_FAIL = 0x21000012, // 553648146
P_LS2CL_REP_CHECK_NAME_LIST_SUCC = 0x21000013, // 553648147
P_LS2CL_REP_CHECK_NAME_LIST_FAIL = 0x21000014, // 553648148
P_LS2CL_REP_PC_EXIT_DUPLICATE = 0x21000015, // 553648149
P_LS2CL_REQ_LIVE_CHECK = 0x21000016, // 553648150
P_LS2CL_REP_CHANGE_CHAR_NAME_SUCC = 0x21000017, // 553648151
P_LS2CL_REP_CHANGE_CHAR_NAME_FAIL = 0x21000018, // 553648152
P_LS2CL_REP_SHARD_LIST_INFO_SUCC = 0x21000019, // 553648153
};
/*
* Numbers of packets by type.
* Each is the last packet - the upper bits + 1
*/
enum {
N_CL2LS = 0xf,
N_CL2FE = 0xa5,
N_FE2CL = 0x12f,
N_LS2CL = 0x1a,
N_PACKETS = N_CL2LS + N_CL2FE + N_FE2CL + N_LS2CL
};
/*
* Usable space in the packet buffer = CN_PACKET_BUFFER_SIZE - type - size
*/
constexpr size_t CN_PACKET_BODY_SIZE = CN_PACKET_BUFFER_SIZE - 2 * sizeof(int32_t);

546
src/core/Packets.cpp Normal file
View File

@ -0,0 +1,546 @@
#include <string>
#include "Defines.hpp"
#include "Packets.hpp"
#include "CNStructs.hpp"
#define PACKET(id) {id, {id, sizeof(s##id), #id}}
#define MANUAL(id) {id, {id, sizeof(s##id), #id}}
#define VAR_PACKET(id, memb, tr) {id, {id, sizeof(s##id), #id, offsetof(s##id, memb), sizeof(tr)}}
/*
* This map defines descriptors for all packets, and is used by the new system
* for validation. From now on, we have to convert new variadic packets from
* PACKET to VAR_PACKET in this list to use them.
*
* MANUAL is just a form of documentation stating that the packet is variadic
* and atypically encoded, so it won't be able to pass outbound validation and
* will need to be manually validated and sent using the legacy sendPacket()
* invocation pattern.
*/
std::map<uint32_t, PacketDesc> Packets::packets = {
// CL2LS
PACKET(P_CL2LS_REQ_LOGIN),
PACKET(P_CL2LS_REQ_CHECK_CHAR_NAME),
PACKET(P_CL2LS_REQ_SAVE_CHAR_NAME),
PACKET(P_CL2LS_REQ_CHAR_CREATE),
PACKET(P_CL2LS_REQ_CHAR_SELECT),
PACKET(P_CL2LS_REQ_CHAR_DELETE),
PACKET(P_CL2LS_REQ_SHARD_SELECT),
PACKET(P_CL2LS_REQ_SHARD_LIST_INFO),
PACKET(P_CL2LS_CHECK_NAME_LIST),
PACKET(P_CL2LS_REQ_SAVE_CHAR_TUTOR),
PACKET(P_CL2LS_REQ_PC_EXIT_DUPLICATE),
PACKET(P_CL2LS_REP_LIVE_CHECK),
PACKET(P_CL2LS_REQ_CHANGE_CHAR_NAME),
PACKET(P_CL2LS_REQ_SERVER_SELECT),
// CL2FE
PACKET(P_CL2FE_REQ_PC_ENTER),
PACKET(P_CL2FE_REQ_PC_EXIT),
PACKET(P_CL2FE_REQ_PC_MOVE),
PACKET(P_CL2FE_REQ_PC_STOP),
PACKET(P_CL2FE_REQ_PC_JUMP),
VAR_PACKET(P_CL2FE_REQ_PC_ATTACK_NPCs, iNPCCnt, int32_t),
PACKET(P_CL2FE_REQ_SEND_FREECHAT_MESSAGE),
PACKET(P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE),
PACKET(P_CL2FE_REQ_PC_REGEN),
PACKET(P_CL2FE_REQ_ITEM_MOVE),
PACKET(P_CL2FE_REQ_PC_TASK_START),
PACKET(P_CL2FE_REQ_PC_TASK_END),
PACKET(P_CL2FE_REQ_NANO_EQUIP),
PACKET(P_CL2FE_REQ_NANO_UNEQUIP),
PACKET(P_CL2FE_REQ_NANO_ACTIVE),
PACKET(P_CL2FE_REQ_NANO_TUNE),
VAR_PACKET(P_CL2FE_REQ_NANO_SKILL_USE, iTargetCnt, int32_t),
PACKET(P_CL2FE_REQ_PC_TASK_STOP),
PACKET(P_CL2FE_REQ_PC_TASK_CONTINUE),
PACKET(P_CL2FE_REQ_PC_GOTO),
PACKET(P_CL2FE_REQ_CHARGE_NANO_STAMINA),
PACKET(P_CL2FE_REQ_PC_KILL_QUEST_NPCs),
PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_BUY),
PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_SELL),
PACKET(P_CL2FE_REQ_PC_ITEM_DELETE),
PACKET(P_CL2FE_REQ_PC_GIVE_ITEM),
PACKET(P_CL2FE_REQ_PC_ROCKET_STYLE_READY),
PACKET(P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE),
VAR_PACKET(P_CL2FE_REQ_PC_ROCKET_STYLE_HIT, iTargetCnt, int64_t),
PACKET(P_CL2FE_REQ_PC_GRENADE_STYLE_READY),
PACKET(P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE),
PACKET(P_CL2FE_REQ_PC_GRENADE_STYLE_HIT),
PACKET(P_CL2FE_REQ_PC_NANO_CREATE),
PACKET(P_CL2FE_REQ_PC_TRADE_OFFER),
PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_CANCEL),
PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT),
PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL),
PACKET(P_CL2FE_REQ_PC_TRADE_OFFER_ABORT),
PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM),
PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL),
PACKET(P_CL2FE_REQ_PC_TRADE_CONFIRM_ABORT),
PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER),
PACKET(P_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER),
PACKET(P_CL2FE_REQ_PC_TRADE_CASH_REGISTER),
PACKET(P_CL2FE_REQ_PC_TRADE_EMOTES_CHAT),
PACKET(P_CL2FE_REQ_PC_BANK_OPEN),
PACKET(P_CL2FE_REQ_PC_BANK_CLOSE),
PACKET(P_CL2FE_REQ_PC_VENDOR_START),
PACKET(P_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE),
PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY),
PACKET(P_CL2FE_REQ_PC_COMBAT_BEGIN),
PACKET(P_CL2FE_REQ_PC_COMBAT_END),
PACKET(P_CL2FE_REQ_REQUEST_MAKE_BUDDY),
PACKET(P_CL2FE_REQ_ACCEPT_MAKE_BUDDY),
PACKET(P_CL2FE_REQ_SEND_BUDDY_FREECHAT_MESSAGE),
PACKET(P_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE),
PACKET(P_CL2FE_REQ_GET_BUDDY_STYLE),
PACKET(P_CL2FE_REQ_SET_BUDDY_BLOCK),
PACKET(P_CL2FE_REQ_REMOVE_BUDDY),
PACKET(P_CL2FE_REQ_GET_BUDDY_STATE),
PACKET(P_CL2FE_REQ_PC_JUMPPAD),
PACKET(P_CL2FE_REQ_PC_LAUNCHER),
PACKET(P_CL2FE_REQ_PC_ZIPLINE),
PACKET(P_CL2FE_REQ_PC_MOVEPLATFORM),
PACKET(P_CL2FE_REQ_PC_SLOPE),
PACKET(P_CL2FE_REQ_PC_STATE_CHANGE),
PACKET(P_CL2FE_REQ_PC_MAP_WARP),
PACKET(P_CL2FE_REQ_PC_GIVE_NANO),
PACKET(P_CL2FE_REQ_NPC_SUMMON),
PACKET(P_CL2FE_REQ_NPC_UNSUMMON),
PACKET(P_CL2FE_REQ_ITEM_CHEST_OPEN),
PACKET(P_CL2FE_REQ_PC_GIVE_NANO_SKILL),
PACKET(P_CL2FE_DOT_DAMAGE_ONOFF),
PACKET(P_CL2FE_REQ_PC_VENDOR_BATTERY_BUY),
PACKET(P_CL2FE_REQ_PC_WARP_USE_NPC),
PACKET(P_CL2FE_REQ_PC_GROUP_INVITE),
PACKET(P_CL2FE_REQ_PC_GROUP_INVITE_REFUSE),
PACKET(P_CL2FE_REQ_PC_GROUP_JOIN),
PACKET(P_CL2FE_REQ_PC_GROUP_LEAVE),
PACKET(P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT),
PACKET(P_CL2FE_REQ_PC_BUDDY_WARP),
PACKET(P_CL2FE_REQ_GET_MEMBER_STYLE),
PACKET(P_CL2FE_REQ_GET_GROUP_STYLE),
PACKET(P_CL2FE_REQ_PC_CHANGE_MENTOR),
PACKET(P_CL2FE_REQ_GET_BUDDY_LOCATION),
PACKET(P_CL2FE_REQ_NPC_GROUP_SUMMON),
PACKET(P_CL2FE_REQ_PC_WARP_TO_PC),
PACKET(P_CL2FE_REQ_EP_RANK_GET_LIST),
PACKET(P_CL2FE_REQ_EP_RANK_GET_DETAIL),
PACKET(P_CL2FE_REQ_EP_RANK_GET_PC_INFO),
PACKET(P_CL2FE_REQ_EP_RACE_START),
PACKET(P_CL2FE_REQ_EP_RACE_END),
PACKET(P_CL2FE_REQ_EP_RACE_CANCEL),
PACKET(P_CL2FE_REQ_EP_GET_RING),
PACKET(P_CL2FE_REQ_IM_CHANGE_SWITCH_STATUS),
PACKET(P_CL2FE_REQ_SHINY_PICKUP),
PACKET(P_CL2FE_REQ_SHINY_SUMMON),
PACKET(P_CL2FE_REQ_PC_MOVETRANSPORTATION),
PACKET(P_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE),
PACKET(P_CL2FE_REQ_SEND_ANY_GROUP_FREECHAT_MESSAGE),
PACKET(P_CL2FE_REQ_BARKER),
PACKET(P_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE),
PACKET(P_CL2FE_REQ_SEND_ANY_GROUP_MENUCHAT_MESSAGE),
PACKET(P_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION),
PACKET(P_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION),
PACKET(P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH),
PACKET(P_CL2FE_GM_REQ_PC_SET_VALUE),
PACKET(P_CL2FE_GM_REQ_KICK_PLAYER),
PACKET(P_CL2FE_GM_REQ_TARGET_PC_TELEPORT),
PACKET(P_CL2FE_GM_REQ_PC_LOCATION),
PACKET(P_CL2FE_GM_REQ_PC_ANNOUNCE),
PACKET(P_CL2FE_REQ_SET_PC_BLOCK),
PACKET(P_CL2FE_REQ_REGIST_RXCOM),
PACKET(P_CL2FE_GM_REQ_PC_MOTD_REGISTER),
PACKET(P_CL2FE_REQ_ITEM_USE),
PACKET(P_CL2FE_REQ_WARP_USE_RECALL),
PACKET(P_CL2FE_REP_LIVE_CHECK),
PACKET(P_CL2FE_REQ_PC_MISSION_COMPLETE),
PACKET(P_CL2FE_REQ_PC_TASK_COMPLETE),
PACKET(P_CL2FE_REQ_NPC_INTERACTION),
PACKET(P_CL2FE_DOT_HEAL_ONOFF),
PACKET(P_CL2FE_REQ_PC_SPECIAL_STATE_SWITCH),
PACKET(P_CL2FE_REQ_PC_EMAIL_UPDATE_CHECK),
PACKET(P_CL2FE_REQ_PC_READ_EMAIL),
PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_PAGE_LIST),
PACKET(P_CL2FE_REQ_PC_DELETE_EMAIL),
PACKET(P_CL2FE_REQ_PC_SEND_EMAIL),
PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_ITEM),
PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_CANDY),
PACKET(P_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF),
PACKET(P_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID),
PACKET(P_CL2FE_REQ_NPC_GROUP_INVITE),
PACKET(P_CL2FE_REQ_NPC_GROUP_KICK),
PACKET(P_CL2FE_REQ_PC_FIRST_USE_FLAG_SET),
PACKET(P_CL2FE_REQ_PC_TRANSPORT_WARP),
PACKET(P_CL2FE_REQ_PC_TIME_TO_GO_WARP),
PACKET(P_CL2FE_REQ_PC_RECV_EMAIL_ITEM_ALL),
PACKET(P_CL2FE_REQ_CHANNEL_INFO),
PACKET(P_CL2FE_REQ_PC_CHANNEL_NUM),
PACKET(P_CL2FE_REQ_PC_WARP_CHANNEL),
PACKET(P_CL2FE_REQ_PC_LOADING_COMPLETE),
PACKET(P_CL2FE_REQ_PC_FIND_NAME_MAKE_BUDDY),
PACKET(P_CL2FE_REQ_PC_FIND_NAME_ACCEPT_BUDDY),
VAR_PACKET(P_CL2FE_REQ_PC_ATTACK_CHARs, iTargetCnt, sGM_PVPTarget),
PACKET(P_CL2FE_PC_STREETSTALL_REQ_READY),
PACKET(P_CL2FE_PC_STREETSTALL_REQ_CANCEL),
PACKET(P_CL2FE_PC_STREETSTALL_REQ_REGIST_ITEM),
PACKET(P_CL2FE_PC_STREETSTALL_REQ_UNREGIST_ITEM),
PACKET(P_CL2FE_PC_STREETSTALL_REQ_SALE_START),
PACKET(P_CL2FE_PC_STREETSTALL_REQ_ITEM_LIST),
PACKET(P_CL2FE_PC_STREETSTALL_REQ_ITEM_BUY),
PACKET(P_CL2FE_REQ_PC_ITEM_COMBINATION),
PACKET(P_CL2FE_GM_REQ_SET_PC_SKILL),
PACKET(P_CL2FE_REQ_PC_SKILL_ADD),
PACKET(P_CL2FE_REQ_PC_SKILL_DEL),
PACKET(P_CL2FE_REQ_PC_SKILL_USE),
PACKET(P_CL2FE_REQ_PC_ROPE),
PACKET(P_CL2FE_REQ_PC_BELT),
PACKET(P_CL2FE_REQ_PC_VEHICLE_ON),
PACKET(P_CL2FE_REQ_PC_VEHICLE_OFF),
PACKET(P_CL2FE_REQ_PC_REGIST_QUICK_SLOT),
PACKET(P_CL2FE_REQ_PC_DISASSEMBLE_ITEM),
PACKET(P_CL2FE_GM_REQ_REWARD_RATE),
PACKET(P_CL2FE_REQ_PC_ITEM_ENCHANT),
// LS2CL
PACKET(P_LS2CL_REP_LOGIN_SUCC),
PACKET(P_LS2CL_REP_LOGIN_FAIL),
PACKET(P_LS2CL_REP_CHAR_INFO),
PACKET(P_LS2CL_REP_CHECK_CHAR_NAME_SUCC),
PACKET(P_LS2CL_REP_CHECK_CHAR_NAME_FAIL),
PACKET(P_LS2CL_REP_SAVE_CHAR_NAME_SUCC),
PACKET(P_LS2CL_REP_SAVE_CHAR_NAME_FAIL),
PACKET(P_LS2CL_REP_CHAR_CREATE_SUCC),
PACKET(P_LS2CL_REP_CHAR_CREATE_FAIL),
PACKET(P_LS2CL_REP_CHAR_SELECT_SUCC),
PACKET(P_LS2CL_REP_CHAR_SELECT_FAIL),
PACKET(P_LS2CL_REP_CHAR_DELETE_SUCC),
PACKET(P_LS2CL_REP_CHAR_DELETE_FAIL),
PACKET(P_LS2CL_REP_SHARD_SELECT_SUCC),
PACKET(P_LS2CL_REP_SHARD_SELECT_FAIL),
PACKET(P_LS2CL_REP_VERSION_CHECK_SUCC),
PACKET(P_LS2CL_REP_VERSION_CHECK_FAIL),
PACKET(P_LS2CL_REP_CHECK_NAME_LIST_SUCC),
PACKET(P_LS2CL_REP_CHECK_NAME_LIST_FAIL),
PACKET(P_LS2CL_REP_PC_EXIT_DUPLICATE),
PACKET(P_LS2CL_REQ_LIVE_CHECK),
PACKET(P_LS2CL_REP_CHANGE_CHAR_NAME_SUCC),
PACKET(P_LS2CL_REP_CHANGE_CHAR_NAME_FAIL),
PACKET(P_LS2CL_REP_SHARD_LIST_INFO_SUCC),
// FE2CL
PACKET(P_FE2CL_ERROR),
PACKET(P_FE2CL_REP_PC_ENTER_FAIL),
PACKET(P_FE2CL_REP_PC_ENTER_SUCC),
PACKET(P_FE2CL_PC_NEW),
PACKET(P_FE2CL_REP_PC_EXIT_FAIL),
PACKET(P_FE2CL_REP_PC_EXIT_SUCC),
PACKET(P_FE2CL_PC_EXIT),
VAR_PACKET(P_FE2CL_PC_AROUND, iPCCnt, sPCAppearanceData),
PACKET(P_FE2CL_PC_MOVE),
PACKET(P_FE2CL_PC_STOP),
PACKET(P_FE2CL_PC_JUMP),
PACKET(P_FE2CL_NPC_ENTER),
PACKET(P_FE2CL_NPC_EXIT),
PACKET(P_FE2CL_NPC_MOVE),
PACKET(P_FE2CL_NPC_NEW),
VAR_PACKET(P_FE2CL_NPC_AROUND, iNPCCnt, sNPCAppearanceData),
VAR_PACKET(P_FE2CL_AROUND_DEL_PC, iPCCnt, int32_t),
VAR_PACKET(P_FE2CL_AROUND_DEL_NPC, iNPCCnt, int32_t),
PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC),
PACKET(P_FE2CL_REP_SEND_FREECHAT_MESSAGE_FAIL),
VAR_PACKET(P_FE2CL_PC_ATTACK_NPCs_SUCC, iNPCCnt, sAttackResult),
VAR_PACKET(P_FE2CL_PC_ATTACK_NPCs, iNPCCnt, sAttackResult),
VAR_PACKET(P_FE2CL_NPC_ATTACK_PCs, iPCCnt, sAttackResult),
PACKET(P_FE2CL_REP_PC_REGEN_SUCC),
PACKET(P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC),
PACKET(P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_FAIL),
PACKET(P_FE2CL_PC_ITEM_MOVE_SUCC),
PACKET(P_FE2CL_PC_EQUIP_CHANGE),
PACKET(P_FE2CL_REP_PC_TASK_START_SUCC),
PACKET(P_FE2CL_REP_PC_TASK_START_FAIL),
PACKET(P_FE2CL_REP_PC_TASK_END_SUCC),
PACKET(P_FE2CL_REP_PC_TASK_END_FAIL),
PACKET(P_FE2CL_NPC_SKILL_READY),
PACKET(P_FE2CL_NPC_SKILL_FIRE),
MANUAL(P_FE2CL_NPC_SKILL_HIT), // variadic, trailer type depends on power
PACKET(P_FE2CL_NPC_SKILL_CORRUPTION_READY),
VAR_PACKET(P_FE2CL_NPC_SKILL_CORRUPTION_HIT, iTargetCnt, sCAttackResult),
PACKET(P_FE2CL_NPC_SKILL_CANCEL),
PACKET(P_FE2CL_REP_NANO_EQUIP_SUCC),
PACKET(P_FE2CL_REP_NANO_UNEQUIP_SUCC),
PACKET(P_FE2CL_REP_NANO_ACTIVE_SUCC),
PACKET(P_FE2CL_REP_NANO_TUNE_SUCC),
PACKET(P_FE2CL_NANO_ACTIVE),
MANUAL(P_FE2CL_NANO_SKILL_USE_SUCC), // variadic, trailer type depends on power
PACKET(P_FE2CL_NANO_SKILL_USE),
PACKET(P_FE2CL_REP_PC_TASK_STOP_SUCC),
PACKET(P_FE2CL_REP_PC_TASK_STOP_FAIL),
PACKET(P_FE2CL_REP_PC_TASK_CONTINUE_SUCC),
PACKET(P_FE2CL_REP_PC_TASK_CONTINUE_FAIL),
PACKET(P_FE2CL_REP_PC_GOTO_SUCC),
PACKET(P_FE2CL_REP_CHARGE_NANO_STAMINA),
PACKET(P_FE2CL_REP_PC_TICK),
PACKET(P_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC),
PACKET(P_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC),
PACKET(P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL),
PACKET(P_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC),
PACKET(P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL),
PACKET(P_FE2CL_REP_PC_ITEM_DELETE_SUCC),
PACKET(P_FE2CL_PC_ROCKET_STYLE_READY),
PACKET(P_FE2CL_REP_PC_ROCKET_STYLE_FIRE_SUCC),
PACKET(P_FE2CL_PC_ROCKET_STYLE_FIRE),
PACKET(P_FE2CL_PC_ROCKET_STYLE_HIT),
PACKET(P_FE2CL_PC_GRENADE_STYLE_READY),
PACKET(P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC),
PACKET(P_FE2CL_PC_GRENADE_STYLE_FIRE),
VAR_PACKET(P_FE2CL_PC_GRENADE_STYLE_HIT, iTargetCnt, sAttackResult),
PACKET(P_FE2CL_REP_PC_TRADE_OFFER),
PACKET(P_FE2CL_REP_PC_TRADE_OFFER_CANCEL),
PACKET(P_FE2CL_REP_PC_TRADE_OFFER_SUCC),
PACKET(P_FE2CL_REP_PC_TRADE_OFFER_REFUSAL),
PACKET(P_FE2CL_REP_PC_TRADE_OFFER_ABORT),
PACKET(P_FE2CL_REP_PC_TRADE_CONFIRM),
PACKET(P_FE2CL_REP_PC_TRADE_CONFIRM_CANCEL),
PACKET(P_FE2CL_REP_PC_TRADE_CONFIRM_ABORT),
PACKET(P_FE2CL_REP_PC_TRADE_CONFIRM_SUCC),
PACKET(P_FE2CL_REP_PC_TRADE_CONFIRM_FAIL),
PACKET(P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_SUCC),
PACKET(P_FE2CL_REP_PC_TRADE_ITEM_REGISTER_FAIL),
PACKET(P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_SUCC),
PACKET(P_FE2CL_REP_PC_TRADE_ITEM_UNREGISTER_FAIL),
PACKET(P_FE2CL_REP_PC_TRADE_CASH_REGISTER_SUCC),
PACKET(P_FE2CL_REP_PC_TRADE_CASH_REGISTER_FAIL),
PACKET(P_FE2CL_REP_PC_TRADE_EMOTES_CHAT),
PACKET(P_FE2CL_REP_PC_NANO_CREATE_SUCC),
PACKET(P_FE2CL_REP_PC_NANO_CREATE_FAIL),
PACKET(P_FE2CL_REP_NANO_TUNE_FAIL),
PACKET(P_FE2CL_REP_PC_BANK_OPEN_SUCC),
PACKET(P_FE2CL_REP_PC_BANK_OPEN_FAIL),
PACKET(P_FE2CL_REP_PC_BANK_CLOSE_SUCC),
PACKET(P_FE2CL_REP_PC_BANK_CLOSE_FAIL),
PACKET(P_FE2CL_REP_PC_VENDOR_START_SUCC),
PACKET(P_FE2CL_REP_PC_VENDOR_START_FAIL),
PACKET(P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC),
PACKET(P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_FAIL),
PACKET(P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC),
PACKET(P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL),
PACKET(P_FE2CL_CHAR_TIME_BUFF_TIME_OUT),
PACKET(P_FE2CL_REP_PC_GIVE_ITEM_SUCC),
PACKET(P_FE2CL_REP_PC_GIVE_ITEM_FAIL),
VAR_PACKET(P_FE2CL_REP_PC_BUDDYLIST_INFO_SUCC, iBuddyCnt, sBuddyBaseInfo),
PACKET(P_FE2CL_REP_PC_BUDDYLIST_INFO_FAIL),
PACKET(P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC),
PACKET(P_FE2CL_REP_REQUEST_MAKE_BUDDY_FAIL),
PACKET(P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC),
PACKET(P_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL),
PACKET(P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC),
PACKET(P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_FAIL),
PACKET(P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC),
PACKET(P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_FAIL),
PACKET(P_FE2CL_REP_GET_BUDDY_STYLE_SUCC),
PACKET(P_FE2CL_REP_GET_BUDDY_STYLE_FAIL),
PACKET(P_FE2CL_REP_GET_BUDDY_STATE_SUCC),
PACKET(P_FE2CL_REP_GET_BUDDY_STATE_FAIL),
PACKET(P_FE2CL_REP_SET_BUDDY_BLOCK_SUCC),
PACKET(P_FE2CL_REP_SET_BUDDY_BLOCK_FAIL),
PACKET(P_FE2CL_REP_REMOVE_BUDDY_SUCC),
PACKET(P_FE2CL_REP_REMOVE_BUDDY_FAIL),
PACKET(P_FE2CL_PC_JUMPPAD),
PACKET(P_FE2CL_PC_LAUNCHER),
PACKET(P_FE2CL_PC_ZIPLINE),
PACKET(P_FE2CL_PC_MOVEPLATFORM),
PACKET(P_FE2CL_PC_SLOPE),
PACKET(P_FE2CL_PC_STATE_CHANGE),
PACKET(P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC_TO_ACCEPTER),
VAR_PACKET(P_FE2CL_REP_REWARD_ITEM, iItemCnt, sItemReward),
PACKET(P_FE2CL_REP_ITEM_CHEST_OPEN_SUCC),
PACKET(P_FE2CL_REP_ITEM_CHEST_OPEN_FAIL),
MANUAL(P_FE2CL_CHAR_TIME_BUFF_TIME_TICK), // variadic, depends on skill type
PACKET(P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC),
PACKET(P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL),
PACKET(P_FE2CL_NPC_ROCKET_STYLE_FIRE),
PACKET(P_FE2CL_NPC_GRENADE_STYLE_FIRE),
PACKET(P_FE2CL_NPC_BULLET_STYLE_HIT),
PACKET(P_FE2CL_CHARACTER_ATTACK_CHARACTERs),
PACKET(P_FE2CL_PC_GROUP_INVITE),
PACKET(P_FE2CL_PC_GROUP_INVITE_FAIL),
PACKET(P_FE2CL_PC_GROUP_INVITE_REFUSE),
MANUAL(P_FE2CL_PC_GROUP_JOIN), // double-variadic, incompatible with this system
PACKET(P_FE2CL_PC_GROUP_JOIN_FAIL),
PACKET(P_FE2CL_PC_GROUP_JOIN_SUCC), // probably these ones too, but we don't use them anyway
MANUAL(P_FE2CL_PC_GROUP_LEAVE), // double-variadic, incompatible with this system
PACKET(P_FE2CL_PC_GROUP_LEAVE_FAIL),
PACKET(P_FE2CL_PC_GROUP_LEAVE_SUCC), // see GROUP_JOIN_SUCC
MANUAL(P_FE2CL_PC_GROUP_MEMBER_INFO), // double-variadic, incompatible with this system
PACKET(P_FE2CL_REP_PC_WARP_USE_NPC_SUCC),
PACKET(P_FE2CL_REP_PC_WARP_USE_NPC_FAIL),
PACKET(P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT),
PACKET(P_FE2CL_REP_PC_CHANGE_MENTOR_SUCC),
PACKET(P_FE2CL_REP_PC_CHANGE_MENTOR_FAIL),
PACKET(P_FE2CL_REP_GET_MEMBER_STYLE_FAIL),
PACKET(P_FE2CL_REP_GET_MEMBER_STYLE_SUCC),
PACKET(P_FE2CL_REP_GET_GROUP_STYLE_FAIL),
PACKET(P_FE2CL_REP_GET_GROUP_STYLE_SUCC),
PACKET(P_FE2CL_PC_REGEN),
PACKET(P_FE2CL_INSTANCE_MAP_INFO),
PACKET(P_FE2CL_TRANSPORTATION_ENTER),
PACKET(P_FE2CL_TRANSPORTATION_EXIT),
PACKET(P_FE2CL_TRANSPORTATION_MOVE),
PACKET(P_FE2CL_TRANSPORTATION_NEW),
VAR_PACKET(P_FE2CL_TRANSPORTATION_AROUND, iCnt, sTransportationAppearanceData),
VAR_PACKET(P_FE2CL_AROUND_DEL_TRANSPORTATION, iCnt, int32_t),
PACKET(P_FE2CL_REP_EP_RANK_LIST),
PACKET(P_FE2CL_REP_EP_RANK_DETAIL),
PACKET(P_FE2CL_REP_EP_RANK_PC_INFO),
PACKET(P_FE2CL_REP_EP_RACE_START_SUCC),
PACKET(P_FE2CL_REP_EP_RACE_START_FAIL),
PACKET(P_FE2CL_REP_EP_RACE_END_SUCC),
PACKET(P_FE2CL_REP_EP_RACE_END_FAIL),
PACKET(P_FE2CL_REP_EP_RACE_CANCEL_SUCC),
PACKET(P_FE2CL_REP_EP_RACE_CANCEL_FAIL),
PACKET(P_FE2CL_REP_EP_GET_RING_SUCC),
PACKET(P_FE2CL_REP_EP_GET_RING_FAIL),
PACKET(P_FE2CL_REP_IM_CHANGE_SWITCH_STATUS),
PACKET(P_FE2CL_SHINY_ENTER),
PACKET(P_FE2CL_SHINY_EXIT),
PACKET(P_FE2CL_SHINY_NEW),
VAR_PACKET(P_FE2CL_SHINY_AROUND, iShinyCnt, sShinyAppearanceData),
VAR_PACKET(P_FE2CL_AROUND_DEL_SHINY, iShinyCnt, int32_t),
PACKET(P_FE2CL_REP_SHINY_PICKUP_FAIL),
PACKET(P_FE2CL_REP_SHINY_PICKUP_SUCC),
PACKET(P_FE2CL_PC_MOVETRANSPORTATION),
PACKET(P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC),
PACKET(P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_FAIL),
PACKET(P_FE2CL_REP_SEND_ANY_GROUP_FREECHAT_MESSAGE_SUCC),
PACKET(P_FE2CL_REP_SEND_ANY_GROUP_FREECHAT_MESSAGE_FAIL),
PACKET(P_FE2CL_REP_BARKER),
PACKET(P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC),
PACKET(P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_FAIL),
PACKET(P_FE2CL_REP_SEND_ANY_GROUP_MENUCHAT_MESSAGE_SUCC),
PACKET(P_FE2CL_REP_SEND_ANY_GROUP_MENUCHAT_MESSAGE_FAIL),
PACKET(P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL),
PACKET(P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC),
PACKET(P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL),
PACKET(P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC),
PACKET(P_FE2CL_ANNOUNCE_MSG),
PACKET(P_FE2CL_REP_PC_SPECIAL_STATE_SWITCH_SUCC),
PACKET(P_FE2CL_PC_SPECIAL_STATE_CHANGE),
PACKET(P_FE2CL_GM_REP_PC_SET_VALUE),
PACKET(P_FE2CL_GM_PC_CHANGE_VALUE),
PACKET(P_FE2CL_GM_REP_PC_LOCATION),
PACKET(P_FE2CL_GM_REP_PC_ANNOUNCE),
PACKET(P_FE2CL_REP_PC_BUDDY_WARP_FAIL),
PACKET(P_FE2CL_REP_PC_CHANGE_LEVEL),
PACKET(P_FE2CL_REP_SET_PC_BLOCK_SUCC),
PACKET(P_FE2CL_REP_SET_PC_BLOCK_FAIL),
PACKET(P_FE2CL_REP_REGIST_RXCOM),
PACKET(P_FE2CL_REP_REGIST_RXCOM_FAIL),
PACKET(P_FE2CL_PC_INVEN_FULL_MSG),
PACKET(P_FE2CL_REQ_LIVE_CHECK),
PACKET(P_FE2CL_PC_MOTD_LOGIN),
PACKET(P_FE2CL_REP_PC_ITEM_USE_FAIL),
VAR_PACKET(P_FE2CL_REP_PC_ITEM_USE_SUCC, iTargetCnt, sSkillResult_Buff),
PACKET(P_FE2CL_PC_ITEM_USE),
PACKET(P_FE2CL_REP_GET_BUDDY_LOCATION_SUCC),
PACKET(P_FE2CL_REP_GET_BUDDY_LOCATION_FAIL),
PACKET(P_FE2CL_REP_PC_RIDING_FAIL),
PACKET(P_FE2CL_REP_PC_RIDING_SUCC),
PACKET(P_FE2CL_PC_RIDING),
PACKET(P_FE2CL_PC_BROOMSTICK_MOVE),
PACKET(P_FE2CL_REP_PC_BUDDY_WARP_OTHER_SHARD_SUCC),
PACKET(P_FE2CL_REP_WARP_USE_RECALL_FAIL),
PACKET(P_FE2CL_REP_PC_EXIT_DUPLICATE),
PACKET(P_FE2CL_REP_PC_MISSION_COMPLETE_SUCC),
PACKET(P_FE2CL_PC_BUFF_UPDATE),
PACKET(P_FE2CL_REP_PC_NEW_EMAIL),
PACKET(P_FE2CL_REP_PC_READ_EMAIL_SUCC),
PACKET(P_FE2CL_REP_PC_READ_EMAIL_FAIL),
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_SUCC),
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_FAIL),
PACKET(P_FE2CL_REP_PC_DELETE_EMAIL_SUCC),
PACKET(P_FE2CL_REP_PC_DELETE_EMAIL_FAIL),
PACKET(P_FE2CL_REP_PC_SEND_EMAIL_SUCC),
PACKET(P_FE2CL_REP_PC_SEND_EMAIL_FAIL),
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_ITEM_SUCC),
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_ITEM_FAIL),
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_CANDY_SUCC),
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_CANDY_FAIL),
PACKET(P_FE2CL_PC_SUDDEN_DEAD),
PACKET(P_FE2CL_REP_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF_SUCC),
PACKET(P_FE2CL_REP_PC_SET_CURRENT_MISSION_ID),
PACKET(P_FE2CL_REP_NPC_GROUP_INVITE_FAIL),
PACKET(P_FE2CL_REP_NPC_GROUP_INVITE_SUCC),
PACKET(P_FE2CL_REP_NPC_GROUP_KICK_FAIL),
PACKET(P_FE2CL_REP_NPC_GROUP_KICK_SUCC),
PACKET(P_FE2CL_PC_EVENT),
PACKET(P_FE2CL_REP_PC_TRANSPORT_WARP_SUCC),
PACKET(P_FE2CL_REP_PC_TRADE_EMOTES_CHAT_FAIL),
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_SUCC),
PACKET(P_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_FAIL),
PACKET(P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC),
PACKET(P_FE2CL_REP_CHANNEL_INFO),
PACKET(P_FE2CL_REP_PC_CHANNEL_NUM),
PACKET(P_FE2CL_REP_PC_WARP_CHANNEL_FAIL),
PACKET(P_FE2CL_REP_PC_WARP_CHANNEL_SUCC),
PACKET(P_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_SUCC),
PACKET(P_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_FAIL),
PACKET(P_FE2CL_REP_PC_FIND_NAME_ACCEPT_BUDDY_FAIL),
PACKET(P_FE2CL_REP_PC_BUDDY_WARP_SAME_SHARD_SUCC),
VAR_PACKET(P_FE2CL_PC_ATTACK_CHARs_SUCC, iTargetCnt, sAttackResult),
VAR_PACKET(P_FE2CL_PC_ATTACK_CHARs, iTargetCnt, sAttackResult),
PACKET(P_FE2CL_NPC_ATTACK_CHARs),
PACKET(P_FE2CL_REP_PC_CHANGE_LEVEL_SUCC),
PACKET(P_FE2CL_REP_PC_NANO_CREATE),
PACKET(P_FE2CL_PC_STREETSTALL_REP_READY_SUCC),
PACKET(P_FE2CL_PC_STREETSTALL_REP_READY_FAIL),
PACKET(P_FE2CL_PC_STREETSTALL_REP_CANCEL_SUCC),
PACKET(P_FE2CL_PC_STREETSTALL_REP_CANCEL_FAIL),
PACKET(P_FE2CL_PC_STREETSTALL_REP_REGIST_ITEM_SUCC),
PACKET(P_FE2CL_PC_STREETSTALL_REP_REGIST_ITEM_FAIL),
PACKET(P_FE2CL_PC_STREETSTALL_REP_UNREGIST_ITEM_SUCC),
PACKET(P_FE2CL_PC_STREETSTALL_REP_UNREGIST_ITEM_FAIL),
PACKET(P_FE2CL_PC_STREETSTALL_REP_SALE_START_SUCC),
PACKET(P_FE2CL_PC_STREETSTALL_REP_SALE_START_FAIL),
PACKET(P_FE2CL_PC_STREETSTALL_REP_ITEM_LIST),
PACKET(P_FE2CL_PC_STREETSTALL_REP_ITEM_LIST_FAIL),
PACKET(P_FE2CL_PC_STREETSTALL_REP_ITEM_BUY_SUCC_BUYER),
PACKET(P_FE2CL_PC_STREETSTALL_REP_ITEM_BUY_SUCC_SELLER),
PACKET(P_FE2CL_PC_STREETSTALL_REP_ITEM_BUY_FAIL),
PACKET(P_FE2CL_REP_PC_ITEM_COMBINATION_SUCC),
PACKET(P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL),
PACKET(P_FE2CL_PC_CASH_BUFF_UPDATE),
PACKET(P_FE2CL_REP_PC_SKILL_ADD_SUCC),
PACKET(P_FE2CL_REP_PC_SKILL_ADD_FAIL),
PACKET(P_FE2CL_REP_PC_SKILL_DEL_SUCC),
PACKET(P_FE2CL_REP_PC_SKILL_DEL_FAIL),
PACKET(P_FE2CL_REP_PC_SKILL_USE_SUCC),
PACKET(P_FE2CL_REP_PC_SKILL_USE_FAIL),
PACKET(P_FE2CL_PC_SKILL_USE),
PACKET(P_FE2CL_PC_ROPE),
PACKET(P_FE2CL_PC_BELT),
PACKET(P_FE2CL_PC_VEHICLE_ON_SUCC),
PACKET(P_FE2CL_PC_VEHICLE_ON_FAIL),
PACKET(P_FE2CL_PC_VEHICLE_OFF_SUCC),
PACKET(P_FE2CL_PC_VEHICLE_OFF_FAIL),
PACKET(P_FE2CL_PC_QUICK_SLOT_INFO),
PACKET(P_FE2CL_REP_PC_REGIST_QUICK_SLOT_FAIL),
PACKET(P_FE2CL_REP_PC_REGIST_QUICK_SLOT_SUCC),
VAR_PACKET(P_FE2CL_PC_DELETE_TIME_LIMIT_ITEM, iItemListCount, sTimeLimitItemDeleteInfo2CL),
PACKET(P_FE2CL_REP_PC_DISASSEMBLE_ITEM_SUCC),
PACKET(P_FE2CL_REP_PC_DISASSEMBLE_ITEM_FAIL),
PACKET(P_FE2CL_GM_REP_REWARD_RATE_SUCC),
PACKET(P_FE2CL_REP_PC_ITEM_ENCHANT_SUCC),
PACKET(P_FE2CL_REP_PC_ITEM_ENCHANT_FAIL),
#ifdef ACADEMY
// Academy-specific
PACKET(P_FE2CL_REP_NANO_BOOK_SUBSET),
#endif
};
std::string Packets::p2str(int val) {
if (packets.find(val) == packets.end())
return "UNKNOWN";
return packets[val].name;
}

63
src/core/Packets.hpp Normal file
View File

@ -0,0 +1,63 @@
#pragma once
#include "CNStructs.hpp"
#include <map>
// Packet Descriptor
struct PacketDesc {
uint32_t val;
std::string name;
size_t size;
bool variadic;
size_t cntMembOfs;
size_t trailerSize;
PacketDesc() {}
PacketDesc(const PacketDesc& other) {
val = other.val;
name = other.name;
size = other.size;
variadic = other.variadic;
cntMembOfs = other.cntMembOfs;
trailerSize = other.trailerSize;
}
PacketDesc(PacketDesc&& other) {
val = other.val;
name = std::move(other.name);
size = other.size;
variadic = other.variadic;
cntMembOfs = other.cntMembOfs;
trailerSize = other.trailerSize;
}
// non-variadic constructor
PacketDesc(uint32_t v, size_t s, std::string n) :
val(v), name(n), size(s), variadic(false) {}
// variadic constructor
PacketDesc(uint32_t v, size_t s, std::string n, size_t ofs, size_t ts) :
val(v), name(n), size(s), variadic(true), cntMembOfs(ofs), trailerSize(ts) {}
};
/*
* Extra trailer structs for places where the client doesn't have any, but
* really should.
*/
struct sGM_PVPTarget {
uint32_t iID;
uint32_t eCT;
};
struct sSkillResult_Leech {
sSkillResult_Heal_HP Heal;
sSkillResult_Damage Damage;
};
namespace Packets {
extern std::map<uint32_t, PacketDesc> packets;
std::string p2str(int val);
}

123
src/db/Database.hpp Normal file
View File

@ -0,0 +1,123 @@
#pragma once
#include "Player.hpp"
#include <string>
#include <vector>
#define DATABASE_VERSION 6
namespace Database {
struct Account {
int AccountID;
std::string Password;
int Selected;
time_t BannedUntil;
std::string BanReason;
};
struct EmailData {
int PlayerId;
int MsgIndex;
int32_t ReadFlag;
int32_t ItemFlag;
int SenderId;
std::string SenderFirstName;
std::string SenderLastName;
std::string SubjectLine;
std::string MsgBody;
int Taros;
uint64_t SendTime;
uint64_t DeleteTime;
};
struct RaceRanking {
int EPID;
int PlayerID;
int Score;
int RingCount;
uint64_t Time;
uint64_t Timestamp;
};
void init();
void open();
void close();
void findAccount(Account* account, std::string login);
// 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);
void refreshCookie(int accountId, int durationSec);
// interface for the /ban command
bool banPlayer(int playerId, std::string& reason);
bool unbanPlayer(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);
bool isSlotFree(int accountId, int slotNum);
/// returns ID, 0 if something failed
int createCharacter(int slot, int accountId, const char* firstName, const char* lastName, int nameCheck);
/// returns true if query succeeded
bool finishCharacter(sP_CL2LS_REQ_CHAR_CREATE* character, int accountId);
/// returns true if query succeeded
bool finishTutorial(int playerID, int accountID);
/// returns slot number if query succeeded
int deleteCharacter(int characterID, int userID);
void getCharInfo(std::vector <sP_LS2CL_REP_CHAR_INFO>* result, int userID);
/// accepting/declining custom name
enum class CustomName {
APPROVE = 1,
DISAPPROVE = 2
};
void evaluateCustomName(int characterID, CustomName decision);
/// returns true if query succeeded
bool changeName(int playerId, int accountId, const char* firstName, const char* lastName, int nameCheck);
// 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);
void addBuddyship(int playerA, int playerB);
void removeBuddyship(int playerA, int playerB);
// blocking
void addBlock(int playerId, int blockedPlayerId);
void removeBlock(int playerId, int blockedPlayerId);
// email
int getUnreadEmailCount(int playerID);
std::vector<EmailData> getEmails(int playerID, int page);
EmailData getEmail(int playerID, int index);
sItemBase* getEmailAttachments(int playerID, int index);
void updateEmailContent(EmailData* data);
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, Player *sender);
// racing
RaceRanking getTopRaceRanking(int epID, int playerID);
void postRaceRanking(RaceRanking ranking);
// code items
bool isCodeRedeemed(int playerId, std::string code);
void recordCodeRedemption(int playerId, std::string code);
}

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