278 Commits

Author SHA1 Message Date
3cfecd9644 Refuse to run if the specified build name isn't in the patch map 2023-06-26 06:48:12 +02:00
6537e38987 Replace enabledpatches config option with patchmap.json
This should make it a lot easier to manage patch directories when we add
support for each known client build.
2023-06-26 05:42:57 +02: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 ec67cc6527.
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
128 changed files with 15191 additions and 11897 deletions

View File

@@ -1,23 +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 contrib
[/src/contrib/**]
end_of_line = unset
insert_final_newline = unset
indent_style = unset
indent_style = unset
# 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

6
.gitattributes vendored
View File

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

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

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

View File

@@ -33,9 +33,9 @@ else()
set(BIN_NAME fusion)
endif()
include_directories(src)
include_directories(src vendor)
file(GLOB_RECURSE SOURCES src/**.cpp src/**.hpp src/**.c src/**.h version.h)
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)
@@ -43,7 +43,13 @@ add_executable(openfusion ${SOURCES})
set_target_properties(openfusion PROPERTIES OUTPUT_NAME ${BIN_NAME})
target_link_libraries(openfusion sqlite3)
# 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.

View File

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

153
Makefile
View File

@@ -4,9 +4,9 @@ CC=clang
CXX=clang++
# -w suppresses all warnings (the part that's commented out helps me find memory leaks, it ruins performance though!)
# If compiling with ASAN, invoke like this: $ LSAN_OPTIONS=suppressions=suppr.txt bin/fusion
CFLAGS=-O3 #-g3 -fsanitize=address
CXXFLAGS=-Wall -Wno-unknown-pragmas -std=c++17 -O2 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" #-g3 -fsanitize=address
LDFLAGS=-lpthread -lsqlite3 #-g3 -fsanitize=address
CFLAGS=-Wall -Wno-unknown-pragmas -O2 -fPIE -D_FORTIFY_SOURCE=1 -fstack-protector #-g3 -fsanitize=address
CXXFLAGS=$(CFLAGS) -std=c++17 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor
LDFLAGS=-lpthread -lsqlite3 -pie -Wl,-z,relro -Wl,-z,now #-g3 -fsanitize=address
# specifies the name of our exectuable
SERVER=bin/fusion
@@ -17,78 +17,111 @@ PROTOCOL_VERSION?=104
# Windows-specific
WIN_CC=x86_64-w64-mingw32-gcc
WIN_CXX=x86_64-w64-mingw32-g++
WIN_CFLAGS=-O3 #-g3 -fsanitize=address
WIN_CXXFLAGS=-D_WIN32_WINNT=0x0601 -Wall -Wno-unknown-pragmas -std=c++17 -O3 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" #-g3 -fsanitize=address
WIN_LDFLAGS=-static -lws2_32 -lwsock32 -lsqlite3 #-g3 -fsanitize=address
WIN_CFLAGS=-O2 -D_WIN32_WINNT=0x0601 -Wall -Wno-unknown-pragmas
WIN_CXXFLAGS=$(WIN_CFLAGS) -std=c++17 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor
WIN_LDFLAGS=-static -lws2_32 -lwsock32 -lsqlite3
WIN_SERVER=bin/winfusion.exe
# C code; currently exclusively from vendored libraries
CSRC=\
src/contrib/bcrypt/bcrypt.c\
src/contrib/bcrypt/crypt_blowfish.c\
src/contrib/bcrypt/crypt_gensalt.c\
src/contrib/bcrypt/wrapper.c\
vendor/bcrypt/bcrypt.c\
vendor/bcrypt/crypt_blowfish.c\
vendor/bcrypt/crypt_gensalt.c\
vendor/bcrypt/wrapper.c\
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/ChatManager.cpp\
src/CNLoginServer.cpp\
src/CNProtocol.cpp\
src/CNShardServer.cpp\
src/CNShared.cpp\
src/Database.cpp\
src/Defines.cpp\
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/Chat.cpp\
src/CustomCommands.cpp\
src/Entities.cpp\
src/Email.cpp\
src/Eggs.cpp\
src/main.cpp\
src/MissionManager.cpp\
src/MobManager.cpp\
src/NanoManager.cpp\
src/ItemManager.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/TransportManager.cpp\
src/Transport.cpp\
src/TableData.cpp\
src/ChunkManager.cpp\
src/BuddyManager.cpp\
src/GroupManager.cpp\
src/Monitor.cpp\
src/RacingManager.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)
CHDR=\
src/contrib/bcrypt/bcrypt.h\
src/contrib/bcrypt/crypt_blowfish.h\
src/contrib/bcrypt/crypt_gensalt.h\
src/contrib/bcrypt/ow-crypt.h\
src/contrib/bcrypt/winbcrypt.h\
CXXHDR=\
src/contrib/bcrypt/BCrypt.hpp\
src/contrib/INIReader.hpp\
src/contrib/JSON.hpp\
src/ChatManager.hpp\
src/CNLoginServer.hpp\
src/CNProtocol.hpp\
src/CNShardServer.hpp\
src/CNShared.hpp\
src/CNStructs.hpp\
src/Database.hpp\
src/Defines.hpp\
src/contrib/INIReader.hpp\
src/contrib/JSON.hpp\
src/MissionManager.hpp\
src/MobManager.hpp\
src/NanoManager.hpp\
src/ItemManager.hpp\
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\
vendor/INIReader.hpp\
vendor/JSON.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/TransportManager.hpp\
src/Transport.hpp\
src/TableData.hpp\
src/ChunkManager.hpp\
src/BuddyManager.hpp\
src/GroupManager.hpp\
src/Monitor.hpp\
src/RacingManager.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)
@@ -109,7 +142,7 @@ windows : CXXFLAGS=$(WIN_CXXFLAGS)
windows : LDFLAGS=$(WIN_LDFLAGS)
windows : SERVER=$(WIN_SERVER)
.SUFFIX: .o .c .cpp .h .hpp
.SUFFIXES: .o .c .cpp .h .hpp
.c.o:
$(CC) -c $(CFLAGS) -o $@ $<
@@ -118,7 +151,7 @@ windows : SERVER=$(WIN_SERVER)
$(CXX) -c $(CXXFLAGS) -o $@ $<
# header timestamps are a prerequisite for OF object files
$(CXXOBJ): $(CXXHDR)
$(CXXOBJ): $(HDR)
$(SERVER): $(OBJ) $(CHDR) $(CXXHDR)
mkdir -p bin
@@ -135,7 +168,7 @@ src/main.o: version.h
# only gets rid of OpenFusion objects, so we don't need to
# recompile the libs every time
clean:
rm -f src/*.o $(SERVER) $(WIN_SERVER) version.h
rm -f src/*.o src/*/*.o $(SERVER) $(WIN_SERVER) version.h
# gets rid of all compiled objects, including the libraries
nuke:

View File

@@ -1,27 +1,43 @@
![](res/radiorave_logo.png)
<p align="center"><img width="640" src="res/openfusion-hero.png" alt=""></p>
[![AppVeyor](https://ci.appveyor.com/api/projects/status/github/OpenFusionProject/OpenFusion?svg=true)](https://ci.appveyor.com/project/OpenFusionProject/openfusion)
[![Discord](https://img.shields.io/badge/chat-on%20discord-7289da.svg?logo=discord)](https://discord.gg/DYavckB)
<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://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>
OpenFusion is a reverse-engineered server for FusionFall. It currently primarily targets version `beta-20100104` and has some support for version `beta-20100728` of the original game.
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://github.com/OpenFusionProject/OpenFusion/wiki/FusionFall-Version-Support) for others.
## Usage
tl;dr:
### Getting Started
#### Method A: Installer (Easiest)
1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.4/OpenFusionClient-1.4-Installer.exe) - choose to run the file.
2. After a few moments, the client should open: you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
3. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
4. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 3.
1. Download the client+server bundle from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.2.1/OpenFusion.zip).
2. Run `FreeClient/installUnity.bat` once
#### Method B: Standalone .zip file
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.4/OpenFusionClient-1.4.zip).
2. Extract it to a folder of your choice. Note: if you are upgrading from an older version, it is preferable to start with a fresh folder rather than overwriting a previous install.
3. Run OpenFusionClient.exe - you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
4. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
5. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 4.
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://github.com/OpenFusionProject/OpenFusion/wiki/Running-the-game-client-on-Linux).
3. Run `OpenFusion/winfusion.exe` (optional if you're using the public server)
4. Run `FreeClient/OpenFusionClient.exe`
### Hosting a server
Currently the client by default connects to a public server hosted by Cake. Change the loginInfo.php to point to your own server if you want to host your own.
1. Grab `OpenFusionServer-1.4-original.zip` or `OpenFusionServer-1.4-academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.4).
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.
If you want, [compiled binaries (artifacts) for each new commit can be found on AppVeyor.](https://ci.appveyor.com/project/OpenFusionProject/openfusion)
If you want to run the latest development builds of the server, [compiled binaries (artifacts) for each functional commit can be found here.](http://cdn.dexlabs.systems/of-builds/)
For a more detailed overview of the game's architecture and how to configure it, read the following sections.
@@ -40,7 +56,7 @@ The original game made use of the player's actual web browser to launch the game
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`.
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`.
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.
@@ -48,33 +64,26 @@ This will potentially become relevant later, as people start experimenting and m
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!).
`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 `Server/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/app/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 has one external dependency: SQLite. You can install it on Windows using `vcpkg`, and on Unix/Linux using your distribution's package manager. For a more indepth guide on how to set up vcpkg, [check this wiki page](https://github.com/OpenFusionProject/OpenFusion/wiki/Installing-SQLite-on-Windows-using-vcpkg).
OpenFusion has one external dependency: SQLite. The oldest compatible version is `3.33.0`. You can install it on Windows using `vcpkg`, and on Unix/Linux using your distribution's package manager. For a more indepth guide on how to set up vcpkg, [read this guide on the wiki](https://github.com/OpenFusionProject/OpenFusion/wiki/Installing-SQLite-on-Windows-using-vcpkg).
You have two choices for compiling OpenFusion: the included Makefile and the included CMakeLists file.
@@ -84,7 +93,7 @@ A detailed compilation guide is available for Windows users in the wiki [using M
### CMake
A detailed guide is available [in the wiki](https://github.com/OpenFusionProject/OpenFusion/wiki/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`
A detailed guide is available [on the wiki](https://github.com/OpenFusionProject/OpenFusion/wiki/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
@@ -115,6 +124,4 @@ Because the server is still in development, ordinary players are allowed access
* `/nano_unequip [slot] (0-2)`
* `/nano_active [slot] (0-2)`
## Accounts
A basic account system has been added, when logging in if the username doesn't exist in the database, a new account with the provided password will be made and you'll be automatically logged in. Otherwise a login attempt will be made. A username must be between 4 and 32 characters, and a password must be between 8 and 32 characters otherwise the account will be rejected.
### A full list of commands can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).

View File

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

View File

@@ -1,3 +1,8 @@
# name of the client build the server is targetting.
# used for determining which patches to apply.
# default is beta-20111013 for Academy, beta-20100104 otherwise.
#buildname=beta-20100104
# verbosity level
# 0 = mostly silence
# 1 = debug prints and unknown packets
@@ -5,12 +10,18 @@
# 3 = print all packets
verbosity=1
# sandbox the process on supported platforms
sandbox=true
# Login Server configuration
[login]
# must be kept in sync with loginInfo.php
port=23000
# will all custom names be approved instantly?
acceptallcustomnames=true
# should attempts to log into non-existent accounts
# automatically create them?
autocreateaccounts=true
# how often should everything be flushed to the database?
# the default is 4 minutes
dbsaveinterval=240
@@ -31,26 +42,32 @@ simulatemobs=true
# little message players see when they enter the game
motd=Welcome to OpenFusion!
# xdt json data
xdtdata=tdata/xdt.json
# if you want to run the Academy build, comment the previous line
# and uncomment the next one
#xdtdata=tdata/xdt_1013.json
# 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).
# NPC json data
npcdata=tdata/NPCs.json
# mob json
mobdata=tdata/mobs.json
# academy mobs & npcs json
academydata=tdata/academy.json
# path json
pathdata=tdata/paths.json
# drop json
dropdata=tdata/drops.json
# gruntwork output (this is what you submit)
gruntwork=tdata/gruntwork.json
# location of the tabledata folder
#tdatadir=tdata/
# location of the patch folder
#patchdir=tdata/patch/
# 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
# patchmap json filename
#patchmapdata=patchmap.json
# gruntwork output filename (this is what you submit)
#gruntwork=gruntwork.json
# location of the database
dbpath=database.db
#dbpath=database.db
# should tutorial flags be disabled off the bat?
disablefirstuseflag=true
@@ -68,19 +85,12 @@ accountlevel=1
# 2 = Halloween
# 3 = Easter
eventmode=0
# percent chance of an event crate dropping each kill
eventcratechance=10
# spawn coordinates (Z is height)
# these are for the Future:
spawnx=632032
spawny=187177
spawnz=-5500
# ...and these are for the Academy:
#spawnx=19835
#spawny=108682
#spawnz=8450
# 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]

BIN
res/openfusion-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

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;

View File

@@ -1,154 +1,161 @@
CREATE TABLE IF NOT EXISTS Accounts (
AccountID INTEGER NOT NULL,
Login TEXT NOT NULL UNIQUE,
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)
);
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)
);
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,
XCoordinates INTEGER NOT NULL,
YCoordinates INTEGER NOT NULL,
ZCoordinates 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 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 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 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 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 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 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 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 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 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 (MsgIndex, Slot)
);
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 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)
)

File diff suppressed because it is too large Load Diff

64
src/Abilities.hpp Normal file
View File

@@ -0,0 +1,64 @@
#pragma once
#include "core/Core.hpp"
#include "Combat.hpp"
typedef void (*PowerHandler)(CNSocket*, std::vector<int>, int16_t, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t);
struct NanoPower {
int16_t skillType;
int32_t bitFlag;
int16_t timeBuffID;
PowerHandler handler;
NanoPower(int16_t s, int32_t b, int16_t t, PowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {}
void handle(CNSocket *sock, std::vector<int> targetData, int16_t nanoID, int16_t skillID, int16_t duration, int16_t amount) {
if (handler == nullptr)
return;
handler(sock, targetData, nanoID, skillID, duration, amount, skillType, bitFlag, timeBuffID);
}
};
typedef void (*MobPowerHandler)(Mob*, std::vector<int>, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t);
struct MobPower {
int16_t skillType;
int32_t bitFlag;
int16_t timeBuffID;
MobPowerHandler handler;
MobPower(int16_t s, int32_t b, int16_t t, MobPowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {}
void handle(Mob *mob, std::vector<int> targetData, int16_t skillID, int16_t duration, int16_t amount) {
if (handler == nullptr)
return;
handler(mob, targetData, skillID, duration, amount, skillType, bitFlag, timeBuffID);
}
};
struct SkillData {
int skillType;
int targetType;
int drainType;
int effectArea;
int batteryUse[4];
int durationTime[4];
int powerIntensity[4];
};
namespace Nanos {
extern std::vector<NanoPower> NanoPowers;
extern std::map<int32_t, SkillData> SkillTable;
void nanoUnbuff(CNSocket* sock, std::vector<int> targetData, int32_t bitFlag, int16_t timeBuffID, int16_t amount, bool groupPower);
int applyBuff(CNSocket* sock, int skillID, int eTBU, int eTBT, int32_t groupFlags);
std::vector<int> findTargets(Player* plr, int skillID, CNPacketData* data = nullptr);
}
namespace Combat {
extern std::vector<MobPower> MobPowers;
}

470
src/Buddies.cpp Normal file
View File

@@ -0,0 +1,470 @@
#include "servers/CNShardServer.hpp"
#include "Buddies.hpp"
#include "PlayerManager.hpp"
#include "Buddies.hpp"
#include "db/Database.hpp"
#include "Items.hpp"
#include "db/Database.hpp"
#include <iostream>
#include <chrono>
#include <algorithm>
#include <thread>
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::refreshBuddyList(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_BUFFER_SIZE];
memset(respbuf, 0, resplen);
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);
/*
* If the buddy list wasn't synced a second time yet, sync it.
* Not sure why we have to do it again for the client not to trip up.
*/
if (!plr->buddiesSynced) {
refreshBuddyList(sock);
plr->buddiesSynced = true;
}
INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp);
for (int slot = 0; slot < 50; slot++) {
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);
}

12
src/Buddies.hpp Normal file
View File

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

View File

@@ -1,827 +0,0 @@
#include "CNShardServer.hpp"
#include "CNStructs.hpp"
#include "ChatManager.hpp"
#include "PlayerManager.hpp"
#include "BuddyManager.hpp"
#include "Database.hpp"
#include "ItemManager.hpp"
#include "Database.hpp"
#include <iostream>
#include <chrono>
#include <algorithm>
#include <thread>
void BuddyManager::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_SEND_BUDDY_FREECHAT_MESSAGE, reqBuddyFreechat);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE, reqBuddyMenuchat);
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);
//
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);
}
// Refresh buddy list
void BuddyManager::refreshBuddyList(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_BUFFER_SIZE];
memset(respbuf, 0, resplen);
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
void BuddyManager::requestBuddy(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_REQUEST_MAKE_BUDDY))
return; // malformed packet
sP_CL2FE_REQ_REQUEST_MAKE_BUDDY* 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((void*)&failResp, P_FE2CL_REP_REQUEST_MAKE_BUDDY_FAIL, sizeof(sP_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((void*)&resp, P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC, sizeof(sP_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC));
otherSock->sendPacket((void*)&otherResp, P_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC_TO_ACCEPTER, sizeof(sP_FE2CL_REP_REQUEST_MAKE_BUDDY_SUCC_TO_ACCEPTER));
}
// Sending buddy request by player name
void BuddyManager::reqBuddyByName(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_FIND_NAME_MAKE_BUDDY)) {
return; // malformed packet
}
sP_CL2FE_REQ_PC_FIND_NAME_MAKE_BUDDY* 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 = nullptr;
for (auto& pair : PlayerManager::players) {
Player* plr = pair.second;
if (strcmp(U16toU8(plr->PCStyle.szFirstName).c_str(), U16toU8(pkt->szFirstName).c_str()) == 0
&& strcmp(U16toU8(plr->PCStyle.szLastName).c_str(), U16toU8(pkt->szLastName).c_str()) == 0
&& !playerHasBuddyWithID(plrReq, plr->iID)) {
otherSock = pair.first;
break;
}
}
if (otherSock == nullptr)
return; // no player found
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((void*)&resp, P_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_SUCC, sizeof(sP_FE2CL_REP_PC_FIND_NAME_MAKE_BUDDY_SUCC));
}
// Accepting buddy request
void BuddyManager::reqAcceptBuddy(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_ACCEPT_MAKE_BUDDY))
return; // malformed packet
sP_CL2FE_REQ_ACCEPT_MAKE_BUDDY* 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((void*)&resp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC, sizeof(sP_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((void*)&resp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC, sizeof(sP_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((void*)&declineResp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL, sizeof(sP_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL));
}
}
// Accepting buddy request from the find name request
void BuddyManager::reqFindNameBuddyAccept(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_FIND_NAME_ACCEPT_BUDDY)) {
return; // malformed packet
}
sP_CL2FE_REQ_PC_FIND_NAME_ACCEPT_BUDDY* 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((void*)&resp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC, sizeof(sP_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((void*)&resp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_SUCC, sizeof(sP_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((void*)&declineResp, P_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL, sizeof(sP_FE2CL_REP_ACCEPT_MAKE_BUDDY_FAIL));
}
}
// Buddy freechatting
void BuddyManager::reqBuddyFreechat(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_SEND_BUDDY_FREECHAT_MESSAGE))
return; // malformed packet
sP_CL2FE_REQ_SEND_BUDDY_FREECHAT_MESSAGE* 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
resp.iFromPCUID = plr->PCStyle.iPC_UID;
resp.iToPCUID = pkt->iBuddyPCUID;
resp.iEmoteCode = pkt->iEmoteCode;
std::string fullChat = ChatManager::sanitizeText(U16toU8(pkt->szFreeChat));
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
sock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC)); // confirm send to sender
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_BUDDY_FREECHAT_MESSAGE_SUCC)); // broadcast send to receiver
}
// Buddy menuchat
void BuddyManager::reqBuddyMenuchat(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE))
return; // malformed packet
sP_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE* 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
resp.iFromPCUID = plr->PCStyle.iPC_UID;
resp.iToPCUID = pkt->iBuddyPCUID;
resp.iEmoteCode = pkt->iEmoteCode;
std::string fullChat = ChatManager::sanitizeText(U16toU8(pkt->szFreeChat));
U8toU16(fullChat, (char16_t*)&resp.szFreeChat, sizeof(resp.szFreeChat));
sock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC)); // confirm send to sender
otherSock->sendPacket((void*)&resp, P_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_BUDDY_MENUCHAT_MESSAGE_SUCC)); // broadcast send to receiver
}
// Getting buddy state
void BuddyManager::reqPktGetBuddyState(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
/*
* If the buddy list wasn't synced a second time yet, sync it.
* Not sure why we have to do it again for the client not to trip up.
*/
if (!plr->buddiesSynced) {
refreshBuddyList(sock);
plr->buddiesSynced = true;
}
INITSTRUCT(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC, resp);
for (int slot = 0; slot < 50; slot++) {
resp.aBuddyState[slot] = PlayerManager::getPlayerFromID(plr->buddyIDs[slot]) != nullptr ? 1 : 0;
resp.aBuddyID[slot] = plr->buddyIDs[slot];
}
sock->sendPacket((void*)&resp, P_FE2CL_REP_GET_BUDDY_STATE_SUCC, sizeof(sP_FE2CL_REP_GET_BUDDY_STATE_SUCC));
}
// Blocking the buddy
void BuddyManager::reqBuddyBlock(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_SET_BUDDY_BLOCK))
return; // malformed packet
sP_CL2FE_REQ_SET_BUDDY_BLOCK* 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((void*)&resp, P_FE2CL_REP_SET_BUDDY_BLOCK_SUCC, sizeof(sP_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((void*)&otherResp, P_FE2CL_REP_REMOVE_BUDDY_SUCC, sizeof(sP_FE2CL_REP_REMOVE_BUDDY_SUCC));
return;
}
}
}
// block non-buddy
void BuddyManager::reqPlayerBlock(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_SET_PC_BLOCK))
return;
sP_CL2FE_REQ_SET_PC_BLOCK* 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((void*)&resp, P_FE2CL_REP_SET_PC_BLOCK_SUCC, sizeof(sP_FE2CL_REP_SET_PC_BLOCK_SUCC));
}
// Deleting the buddy
void BuddyManager::reqBuddyDelete(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_REMOVE_BUDDY))
return; // malformed packet
// note! this packet is used both for removing buddies and blocks
sP_CL2FE_REQ_REMOVE_BUDDY* 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((void*)&resp, P_FE2CL_REP_REMOVE_BUDDY_SUCC, sizeof(sP_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((void*)&resp, P_FE2CL_REP_REMOVE_BUDDY_SUCC, sizeof(sP_FE2CL_REP_REMOVE_BUDDY_SUCC));
return;
}
}
}
// Warping to buddy
void BuddyManager::reqBuddyWarp(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_BUDDY_WARP))
return; // malformed packet
sP_CL2FE_REQ_PC_BUDDY_WARP* 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 (otherPlr->instanceID != INSTANCE_OVERWORLD) {
// player is instanced; no warp allowed
INITSTRUCT(sP_FE2CL_REP_PC_BUDDY_WARP_FAIL, resp);
resp.iBuddyPCUID = pkt->iBuddyPCUID;
resp.iErrorCode = 0;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_BUDDY_WARP_FAIL, sizeof(sP_FE2CL_REP_PC_BUDDY_WARP_FAIL));
return;
}
Player *plr = PlayerManager::getPlayer(sock);
if (otherPlr->PCStyle2.iPayzoneFlag != plr->PCStyle2.iPayzoneFlag) {
// players are not at the same point in time
INITSTRUCT(sP_FE2CL_REP_PC_BUDDY_WARP_FAIL, resp);
resp.iBuddyPCUID = pkt->iBuddyPCUID;
resp.iErrorCode = 0;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_BUDDY_WARP_FAIL, sizeof(sP_FE2CL_REP_PC_BUDDY_WARP_FAIL));
return;
}
PlayerManager::sendPlayerTo(sock, otherPlr->x, otherPlr->y, otherPlr->z);
}
void BuddyManager::emailUpdateCheck(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_EMAIL_UPDATE_CHECK))
return; // malformed packet
INITSTRUCT(sP_FE2CL_REP_PC_NEW_EMAIL, resp);
resp.iNewEmailCnt = Database::getUnreadEmailCount(PlayerManager::getPlayer(sock)->iID);
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_NEW_EMAIL, sizeof(sP_FE2CL_REP_PC_NEW_EMAIL));
}
void BuddyManager::emailReceivePageList(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_RECV_EMAIL_PAGE_LIST))
return; // malformed packet
sP_CL2FE_REQ_PC_RECV_EMAIL_PAGE_LIST* 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((void*)&resp, P_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_SUCC, sizeof(sP_FE2CL_REP_PC_RECV_EMAIL_PAGE_LIST_SUCC));
}
void BuddyManager::emailRead(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_READ_EMAIL))
return; // malformed packet
sP_CL2FE_REQ_PC_READ_EMAIL* 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((void*)&resp, P_FE2CL_REP_PC_READ_EMAIL_SUCC, sizeof(sP_FE2CL_REP_PC_READ_EMAIL_SUCC));
}
void BuddyManager::emailReceiveTaros(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_RECV_EMAIL_CANDY))
return; // malformed packet
sP_CL2FE_REQ_PC_RECV_EMAIL_CANDY* 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((void*)&resp, P_FE2CL_REP_PC_RECV_EMAIL_CANDY_SUCC, sizeof(sP_FE2CL_REP_PC_RECV_EMAIL_CANDY_SUCC));
}
void BuddyManager::emailReceiveItemSingle(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_RECV_EMAIL_ITEM))
return; // malformed packet
sP_CL2FE_REQ_PC_RECV_EMAIL_ITEM* pkt = (sP_CL2FE_REQ_PC_RECV_EMAIL_ITEM*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
// 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
if (pkt->iSlotNum < 0 || pkt->iSlotNum >= AINVEN_COUNT)
return; // sanity check
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((void*)&resp, P_FE2CL_REP_PC_RECV_EMAIL_ITEM_SUCC, sizeof(sP_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((void*)&resp2, P_FE2CL_REP_PC_GIVE_ITEM_SUCC, sizeof(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC));
}
void BuddyManager::emailReceiveItemAll(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_RECV_EMAIL_ITEM_ALL))
return; // malformed packet
sP_CL2FE_REQ_PC_RECV_EMAIL_ITEM_ALL* 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 = ItemManager::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((void*)&resp2, P_FE2CL_REP_PC_GIVE_ITEM_SUCC, sizeof(sP_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((void*)&resp, P_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_SUCC, sizeof(sP_FE2CL_REP_PC_RECV_EMAIL_ITEM_ALL_SUCC));
}
void BuddyManager::emailDelete(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_DELETE_EMAIL))
return; // malformed packet
sP_CL2FE_REQ_PC_DELETE_EMAIL* 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((void*)&resp, P_FE2CL_REP_PC_DELETE_EMAIL_SUCC, sizeof(sP_FE2CL_REP_PC_DELETE_EMAIL_SUCC));
}
void BuddyManager::emailSend(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_SEND_EMAIL))
return; // malformed packet
sP_CL2FE_REQ_PC_SEND_EMAIL* pkt = (sP_CL2FE_REQ_PC_SEND_EMAIL*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
INITSTRUCT(sP_FE2CL_REP_PC_SEND_EMAIL_SUCC, resp);
if (pkt->iCash || pkt->aItem[0].ItemInven.iID) {
// if there are item or taro attachments
Player otherPlr = {};
Database::getPlayer(&otherPlr, pkt->iTo_PCUID);
if (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((void*)&resp, P_FE2CL_REP_PC_SEND_EMAIL_FAIL, sizeof(sP_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];
resp.aItem[i] = attachment;
if (attachment.iSlotNum < 0 || attachment.iSlotNum >= AINVEN_COUNT
|| attachment.ItemInven.iID <= 0 || attachment.ItemInven.iType < 0)
continue; // sanity check
attachments.push_back(attachment.ItemInven);
attSlots.push_back(attachment.iSlotNum);
// delete item
plr->Inven[attachment.iSlotNum] = { 0, 0, 0, 0 };
}
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
U16toU8(plr->PCStyle.szFirstName), // SenderFirstName
U16toU8(plr->PCStyle.szLastName), // SenderLastName
ChatManager::sanitizeText(U16toU8(pkt->szSubject)), // SubjectLine
ChatManager::sanitizeText(U16toU8(pkt->szContent), true), // MsgBody
pkt->iCash, // Taros
(uint64_t)getTimestamp(), // SendTime
0 // DeleteTime (unimplemented)
};
if (!Database::sendEmail(&email, attachments)) {
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((void*)&errResp, P_FE2CL_REP_PC_SEND_EMAIL_FAIL, sizeof(sP_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((void*)&tarosResp, P_FE2CL_GM_REP_PC_SET_VALUE, sizeof(sP_FE2CL_GM_REP_PC_SET_VALUE));
resp.iCandy = plr->money;
resp.iTo_PCUID = pkt->iTo_PCUID;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_SEND_EMAIL_SUCC, sizeof(sP_FE2CL_REP_PC_SEND_EMAIL_SUCC));
}
#pragma region Helper methods
int BuddyManager::getAvailableBuddySlot(Player* plr) {
int slot = -1;
for (int i = 0; i < 50; i++) {
if (plr->buddyIDs[i] == 0)
return i;
}
return slot;
}
bool BuddyManager::playerHasBuddyWithID(Player* plr, int buddyID) {
for (int i = 0; i < 50; i++) {
if (plr->buddyIDs[i] == buddyID)
return true;
}
return false;
}
#pragma endregion

View File

@@ -1,55 +0,0 @@
#pragma once
#include "Player.hpp"
#include "CNProtocol.hpp"
#include "CNStructs.hpp"
#include "CNShardServer.hpp"
#include <map>
#include <list>
namespace BuddyManager {
void init();
// Buddy list
void refreshBuddyList(CNSocket* sock);
// Buddy requests
void requestBuddy(CNSocket* sock, CNPacketData* data);
void reqBuddyByName(CNSocket* sock, CNPacketData* data);
// Buddy accepting
void reqAcceptBuddy(CNSocket* sock, CNPacketData* data);
void reqFindNameBuddyAccept(CNSocket* sock, CNPacketData* data);
// Buddy Messaging
void reqBuddyFreechat(CNSocket* sock, CNPacketData* data);
void reqBuddyMenuchat(CNSocket* sock, CNPacketData* data);
// Getting buddy state
void reqPktGetBuddyState(CNSocket* sock, CNPacketData* data);
// Blocking/removing buddies
void reqBuddyBlock(CNSocket* sock, CNPacketData* data);
void reqPlayerBlock(CNSocket* sock, CNPacketData* data);
void reqBuddyDelete(CNSocket* sock, CNPacketData* data);
// Buddy warping
void reqBuddyWarp(CNSocket* sock, CNPacketData* data);
// Email methods
void emailUpdateCheck(CNSocket* sock, CNPacketData* data);
void emailReceivePageList(CNSocket* sock, CNPacketData* data);
void emailRead(CNSocket* sock, CNPacketData* data);
void emailReceiveTaros(CNSocket* sock, CNPacketData* data);
void emailReceiveItemSingle(CNSocket* sock, CNPacketData* data);
void emailReceiveItemAll(CNSocket* sock, CNPacketData* data);
void emailDelete(CNSocket* sock, CNPacketData* data);
void emailSend(CNSocket* sock, CNPacketData* data);
// helper methods
// Name checks
int getAvailableBuddySlot(Player* plr);
bool playerHasBuddyWithID(Player* plr, int buddyID);
}

366
src/BuiltinCommands.cpp Normal file
View File

@@ -0,0 +1,366 @@
#include "BuiltinCommands.hpp"
#include "PlayerManager.hpp"
#include "Chat.hpp"
#include "Items.hpp"
#include "Missions.hpp"
#include "Nanos.hpp"
#include "Rand.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 1:
plr->HP = setData->iSetValue;
break;
case 2:
plr->batteryW = setData->iSetValue;
// caps
if (plr->batteryW > 9999)
plr->batteryW = 9999;
break;
case 3:
plr->batteryN = setData->iSetValue;
// caps
if (plr->batteryN > 9999)
plr->batteryN = 9999;
break;
case 4:
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
break;
case 5:
plr->money = setData->iSetValue;
break;
}
response.iPC_ID = setData->iPC_ID;
response.iSetValue = setData->iSetValue;
response.iSetValueType = setData->iSetValueType;
sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE);
// 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 (req->eTeleportType) {
case eCN_GM_TeleportMapType__MyLocation:
PlayerManager::sendPlayerTo(targetSock, plr->x, plr->y, plr->z, instance);
break;
case eCN_GM_TeleportMapType__MapXYZ:
instance = req->iToMap;
// fallthrough
case eCN_GM_TeleportMapType__XYZ:
PlayerManager::sendPlayerTo(targetSock, req->iToX, req->iToY, req->iToZ, instance);
break;
case eCN_GM_TeleportMapType__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_TeleportMapType__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,27 +0,0 @@
#include "CNShared.hpp"
#if defined(__MINGW32__) && !defined(_GLIBCXX_HAS_GTHREADS)
#include "mingw/mingw.mutex.h"
#else
#include <mutex>
#endif
std::map<int64_t, Player> CNSharedData::players;
std::mutex playerCrit;
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) {
std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
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,20 +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!
*/
#pragma once
#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);
}

319
src/Chat.cpp Normal file
View File

@@ -0,0 +1,319 @@
#include "Chat.hpp"
#include "PlayerManager.hpp"
#include "Groups.hpp"
#include "CustomCommands.hpp"
#include <assert.h>
std::vector<std::string> Chat::dump;
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;
dump.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;
dump.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;
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: // shard
case 2: // world
break; // not applicable to OpenFusion
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 = "[Bcast " + std::to_string(announcement->iAreaType) + "] " + PlayerManager::getPlayerName(plr, false) + ": " + AUTOU16TOU8(msg.szAnnounceMsg);
std::cout << logLine << std::endl;
dump.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;
dump.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;
dump.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;
dump.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);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
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;
dump.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;
Groups::sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
}
static void groupMenuChatHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
std::string fullChat = sanitizeText(AUTOU16TOU8(chat->szFreeChat));
std::string logLine = "[GroupMenuChat] " + PlayerManager::getPlayerName(plr, true) + ": " + fullChat;
std::cout << logLine << std::endl;
dump.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;
Groups::sendToGroup(otherPlr, (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);
}

13
src/Chat.hpp Normal file
View File

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

View File

@@ -1,964 +0,0 @@
#include "CNShardServer.hpp"
#include "CNStructs.hpp"
#include "ChatManager.hpp"
#include "PlayerManager.hpp"
#include "TransportManager.hpp"
#include "TableData.hpp"
#include "NPCManager.hpp"
#include "MobManager.hpp"
#include "MissionManager.hpp"
#include "ChunkManager.hpp"
#include "ItemManager.hpp"
#include <sstream>
#include <iterator>
#include <math.h>
std::map<std::string, ChatCommand> ChatManager::commands;
std::vector<std::string> ChatManager::dump;
std::vector<std::string> parseArgs(std::string full) {
std::stringstream ss(full);
std::istream_iterator<std::string> begin(ss);
std::istream_iterator<std::string> end;
return std::vector<std::string>(begin, end);
}
bool runCmd(std::string full, CNSocket* sock) {
std::vector<std::string> args = parseArgs(full);
std::string cmd = args[0].substr(1, args[0].size() - 1);
// check if the command exists
if (ChatManager::commands.find(cmd) != ChatManager::commands.end()) {
Player* plr = PlayerManager::getPlayer(sock);
ChatCommand command = ChatManager::commands[cmd];
// sanity check + does the player have the required account level to use the command?
if (plr != nullptr && plr->accountLevel <= command.requiredAccLevel) {
command.handlr(full, args, sock);
return true;
} else {
ChatManager::sendServerMessage(sock, "You don't have access to that command!");
return false;
}
}
ChatManager::sendServerMessage(sock, "Unknown command!");
return false;
}
void helpCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
ChatManager::sendServerMessage(sock, "Commands available to you:");
Player *plr = PlayerManager::getPlayer(sock);
for (auto& cmd : ChatManager::commands) {
if (cmd.second.requiredAccLevel >= plr->accountLevel)
ChatManager::sendServerMessage(sock, "/" + cmd.first + (cmd.second.help.length() > 0 ? " - " + cmd.second.help : ""));
}
}
void accessCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
ChatManager::sendServerMessage(sock, "Your access level is " + std::to_string(PlayerManager::getPlayer(sock)->accountLevel));
}
void populationCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
ChatManager::sendServerMessage(sock, std::to_string(PlayerManager::players.size()) + " players online");
}
void levelCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
if (args.size() < 2) {
ChatManager::sendServerMessage(sock, "/level: no level specified");
return;
}
Player *plr = PlayerManager::getPlayer(sock);
char *tmp;
int level = std::strtol(args[1].c_str(), &tmp, 10);
if (*tmp)
return;
if ((level < 1 || level > 36) && plr->accountLevel > 30)
return;
if (!(level < 1 || level > 36))
plr->level = level;
INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL, resp);
resp.iPC_ID = plr->iID;
resp.iPC_Level = level;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_CHANGE_LEVEL, sizeof(sP_FE2CL_REP_PC_CHANGE_LEVEL));
PlayerManager::sendToViewable(sock, (void*)&resp, P_FE2CL_REP_PC_CHANGE_LEVEL, sizeof(sP_FE2CL_REP_PC_CHANGE_LEVEL));
}
void mssCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
if (args.size() < 2) {
ChatManager::sendServerMessage(sock, "[MSS] Too few arguments");
ChatManager::sendServerMessage(sock, "[MSS] Usage: /mss <route> <add/remove/goto/clear/test/export> <<height>>");
return;
}
// Validate route number
char* routeNumC;
int routeNum = std::strtol(args[1].c_str(), &routeNumC, 10);
if (*routeNumC) {
// not an integer
ChatManager::sendServerMessage(sock, "[MSS] Invalid route number '" + args[1] + "'");
return;
}
if (args.size() < 3) {
ChatManager::sendServerMessage(sock, "[MSS] Too few arguments");
ChatManager::sendServerMessage(sock, "[MSS] Usage: /mss <route> <add/remove/goto/clear/test> <<height>>");
return;
}
// get the route (if it doesn't exist yet, this will also make it)
std::vector<WarpLocation>* route = &TableData::RunningSkywayRoutes[routeNum];
// mss <route> add <height>
if (args[2] == "add") {
// make sure height token exists
if (args.size() < 4) {
ChatManager::sendServerMessage(sock, "[MSS] Point height must be specified");
ChatManager::sendServerMessage(sock, "[MSS] Usage: /mss <route> add <height>");
return;
}
// validate height token
char* heightC;
int height = std::strtol(args[3].c_str(), &heightC, 10);
if (*heightC) {
ChatManager::sendServerMessage(sock, "[MSS] Invalid height " + args[3]);
return;
}
Player* plr = PlayerManager::getPlayer(sock);
route->push_back({ plr->x, plr->y, height }); // add point
ChatManager::sendServerMessage(sock, "[MSS] Added point (" + std::to_string(plr->x) + ", " + std::to_string(plr->y) + ", " + std::to_string(height) + ") to route " + std::to_string(routeNum));
return;
}
// mss <route> remove
if (args[2] == "remove") {
if (route->empty()) {
ChatManager::sendServerMessage(sock, "[MSS] Route " + std::to_string(routeNum) + " is empty");
return;
}
WarpLocation pulled = route->back();
route->pop_back(); // remove point at top of stack
ChatManager::sendServerMessage(sock, "[MSS] Removed point (" + std::to_string(pulled.x) + ", " + std::to_string(pulled.y) + ", " + std::to_string(pulled.z) + ") from route " + std::to_string(routeNum));
return;
}
// mss <route> goto
if (args[2] == "goto") {
if (route->empty()) {
ChatManager::sendServerMessage(sock, "[MSS] Route " + std::to_string(routeNum) + " is empty");
return;
}
WarpLocation pulled = route->back();
PlayerManager::sendPlayerTo(sock, pulled.x, pulled.y, pulled.z);
return;
}
// mss <route> clear
if (args[2] == "clear") {
route->clear();
ChatManager::sendServerMessage(sock, "[MSS] Cleared route " + std::to_string(routeNum));
return;
}
// mss <route> test
if (args[2] == "test") {
if (route->empty()) {
ChatManager::sendServerMessage(sock, "[MSS] Route " + std::to_string(routeNum) + " is empty");
return;
}
WarpLocation pulled = route->front();
PlayerManager::sendPlayerTo(sock, pulled.x, pulled.y, pulled.z);
TransportManager::testMssRoute(sock, route);
return;
}
// for compatibility: mss <route> export
if (args[2] == "export") {
ChatManager::sendServerMessage(sock, "Wrote gruntwork to " + settings::GRUNTWORKJSON);
TableData::flush();
return;
}
// mss ????
ChatManager::sendServerMessage(sock, "[MSS] Unknown command '" + args[2] + "'");
}
void summonWCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
if (args.size() < 2) {
ChatManager::sendServerMessage(sock, "/summonW: no mob type specified");
return;
}
Player* plr = PlayerManager::getPlayer(sock);
char *rest;
int type = std::strtol(args[1].c_str(), &rest, 10);
if (*rest) {
ChatManager::sendServerMessage(sock, "Invalid NPC number: " + args[1]);
return;
}
int limit = NPCManager::NPCData.back()["m_iNpcNumber"];
// permission & sanity check
if (type > limit)
return;
BaseNPC *npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, true);
// update angle
npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, plr->instanceID, npc->appearanceData.iAngle);
// if we're in a lair, we need to spawn the NPC in both the private instance and the template
if (PLAYERID(plr->instanceID) != 0) {
npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, true, true);
npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, plr->x, plr->y, plr->z, npc->instanceID, npc->appearanceData.iAngle);
}
ChatManager::sendServerMessage(sock, "/summonW: placed mob with type: " + std::to_string(type) +
", id: " + std::to_string(npc->appearanceData.iNPC_ID));
TableData::RunningMobs[npc->appearanceData.iNPC_ID] = npc; // only record the one in the template
}
void unsummonWCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
BaseNPC* npc = NPCManager::getNearestNPC(plr->viewableChunks, plr->x, plr->y, plr->z);
if (npc == nullptr) {
ChatManager::sendServerMessage(sock, "/unsummonW: No NPCs found nearby");
return;
}
if (TableData::RunningEggs.find(npc->appearanceData.iNPC_ID) != TableData::RunningEggs.end()) {
ChatManager::sendServerMessage(sock, "/unsummonW: removed egg with type: " + std::to_string(npc->appearanceData.iNPCType) +
", id: " + std::to_string(npc->appearanceData.iNPC_ID));
TableData::RunningEggs.erase(npc->appearanceData.iNPC_ID);
NPCManager::destroyNPC(npc->appearanceData.iNPC_ID);
return;
}
if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) == TableData::RunningMobs.end()
&& TableData::RunningGroups.find(npc->appearanceData.iNPC_ID) == TableData::RunningGroups.end()) {
ChatManager::sendServerMessage(sock, "/unsummonW: Closest NPC is not a gruntwork mob.");
return;
}
if (MobManager::Mobs.find(npc->appearanceData.iNPC_ID) != MobManager::Mobs.end()) {
int leadId = ((Mob*)npc)->groupLeader;
if (leadId != 0) {
if (MobManager::Mobs.find(leadId) == MobManager::Mobs.end()) {
std::cout << "[WARN] unsummonW: leader not found!" << std::endl;
}
Mob* leadNpc = MobManager::Mobs[leadId];
for (int i = 0; i < 4; i++) {
if (leadNpc->groupMember[i] == 0)
break;
if (MobManager::Mobs.find(leadNpc->groupMember[i]) == MobManager::Mobs.end()) {
std::cout << "[WARN] unsommonW: leader can't find a group member!" << std::endl;
continue;
}
NPCManager::destroyNPC(leadNpc->groupMember[i]);
}
TableData::RunningGroups.erase(leadId);
NPCManager::destroyNPC(leadId);
ChatManager::sendServerMessage(sock, "/unsummonW: Mob group destroyed.");
return;
}
}
ChatManager::sendServerMessage(sock, "/unsummonW: removed mob with type: " + std::to_string(npc->appearanceData.iNPCType) +
", id: " + std::to_string(npc->appearanceData.iNPC_ID));
TableData::RunningMobs.erase(npc->appearanceData.iNPC_ID);
NPCManager::destroyNPC(npc->appearanceData.iNPC_ID);
}
void toggleAiCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
MobManager::simulateMobs = !MobManager::simulateMobs;
if (MobManager::simulateMobs)
return;
// return all mobs to their spawn points
for (auto& pair : MobManager::Mobs) {
pair.second->state = MobState::RETREAT;
pair.second->target = nullptr;
pair.second->nextMovement = getTime();
// mobs with static paths can chill where they are
if (pair.second->staticPath) {
pair.second->roamX = pair.second->appearanceData.iX;
pair.second->roamY = pair.second->appearanceData.iY;
pair.second->roamZ = pair.second->appearanceData.iZ;
} else {
pair.second->roamX = pair.second->spawnX;
pair.second->roamY = pair.second->spawnY;
pair.second->roamZ = pair.second->spawnZ;
}
}
}
void npcRotateCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
BaseNPC* npc = NPCManager::getNearestNPC(plr->viewableChunks, plr->x, plr->y, plr->z);
if (npc == nullptr) {
ChatManager::sendServerMessage(sock, "[NPCR] No NPCs found nearby");
return;
}
int angle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->appearanceData.iX, npc->appearanceData.iY, npc->appearanceData.iZ, npc->instanceID, angle);
// if it's a gruntwork NPC, rotate in-place
if (TableData::RunningMobs.find(npc->appearanceData.iNPC_ID) != TableData::RunningMobs.end()) {
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->appearanceData.iX, npc->appearanceData.iY, npc->appearanceData.iZ, npc->instanceID, angle);
ChatManager::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for gruntwork NPC "
+ std::to_string(npc->appearanceData.iNPC_ID));
} else {
TableData::RunningNPCRotations[npc->appearanceData.iNPC_ID] = angle;
ChatManager::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for NPC "
+ std::to_string(npc->appearanceData.iNPC_ID));
}
// update rotation clientside
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
pkt.NPCAppearanceData = npc->appearanceData;
sock->sendPacket((void*)&pkt, P_FE2CL_NPC_ENTER, sizeof(sP_FE2CL_NPC_ENTER));
}
void refreshCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
ChunkPos currentChunk = plr->chunkPos;
ChunkPos nullChunk = std::make_tuple(0, 0, 0);
ChunkManager::updatePlayerChunk(sock, currentChunk, nullChunk);
ChunkManager::updatePlayerChunk(sock, nullChunk, currentChunk);
}
void instanceCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
// no additional arguments: report current instance ID
if (args.size() < 2) {
ChatManager::sendServerMessage(sock, "[INST] Current instance ID: " + std::to_string(plr->instanceID));
ChatManager::sendServerMessage(sock, "[INST] (Map " + std::to_string(MAPNUM(plr->instanceID)) + ", instance " + std::to_string(PLAYERID(plr->instanceID)) + ")");
return;
}
// move player to specified instance
// validate instance ID
char* instanceS;
int instance = std::strtol(args[1].c_str(), &instanceS, 10);
if (*instanceS) {
ChatManager::sendServerMessage(sock, "[INST] Invalid instance ID: " + args[1]);
return;
}
PlayerManager::sendPlayerTo(sock, plr->x, plr->y, plr->z, instance);
ChatManager::sendServerMessage(sock, "[INST] Switched to instance with ID " + std::to_string(instance));
}
void npcInstanceCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
if (args.size() < 2) {
ChatManager::sendServerMessage(sock, "[NPCI] Instance ID must be specified");
ChatManager::sendServerMessage(sock, "[NPCI] Usage: /npci <instance ID>");
return;
}
BaseNPC* npc = NPCManager::getNearestNPC(plr->viewableChunks, plr->x, plr->y, plr->z);
if (npc == nullptr) {
ChatManager::sendServerMessage(sock, "[NPCI] No NPCs found nearby");
return;
}
// validate instance ID
char* instanceS;
int instance = std::strtol(args[1].c_str(), &instanceS, 10);
if (*instanceS) {
ChatManager::sendServerMessage(sock, "[NPCI] Invalid instance ID: " + args[1]);
return;
}
ChatManager::sendServerMessage(sock, "[NPCI] Moving NPC with ID " + std::to_string(npc->appearanceData.iNPC_ID) + " to instance " + std::to_string(instance));
TableData::RunningNPCMapNumbers[npc->appearanceData.iNPC_ID] = instance;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, npc->appearanceData.iX, npc->appearanceData.iY, npc->appearanceData.iZ, instance, npc->appearanceData.iAngle);
}
void minfoCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
ChatManager::sendServerMessage(sock, "[MINFO] Current mission ID: " + std::to_string(plr->CurrentMissionID));
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] != 0) {
TaskData& task = *MissionManager::Tasks[plr->tasks[i]];
if ((int)(task["m_iHMissionID"]) == plr->CurrentMissionID) {
ChatManager::sendServerMessage(sock, "[MINFO] Current task ID: " + std::to_string(plr->tasks[i]));
ChatManager::sendServerMessage(sock, "[MINFO] Current task type: " + std::to_string((int)(task["m_iHTaskType"])));
ChatManager::sendServerMessage(sock, "[MINFO] Current waypoint NPC ID: " + std::to_string((int)(task["m_iSTGrantWayPoint"])));
ChatManager::sendServerMessage(sock, "[MINFO] Current terminator NPC ID: " + std::to_string((int)(task["m_iHTerminatorNPCID"])));
if ((int)(task["m_iSTGrantTimer"]) != 0)
ChatManager::sendServerMessage(sock, "[MINFO] Current task timer: " + std::to_string((int)(task["m_iSTGrantTimer"])));
for (int j = 0; j < 3; j++)
if ((int)(task["m_iCSUEnemyID"][j]) != 0)
ChatManager::sendServerMessage(sock, "[MINFO] Current task mob #" + std::to_string(j+1) +": " + std::to_string((int)(task["m_iCSUEnemyID"][j])));
return;
}
}
}
}
void tasksCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (plr->tasks[i] != 0) {
TaskData& task = *MissionManager::Tasks[plr->tasks[i]];
ChatManager::sendServerMessage(sock, "[TASK-" + std::to_string(i) + "] mission ID: " + std::to_string((int)(task["m_iHMissionID"])));
ChatManager::sendServerMessage(sock, "[TASK-" + std::to_string(i) + "] task ID: " + std::to_string(plr->tasks[i]));
}
}
}
void buffCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
if (args.size() < 3) {
ChatManager::sendServerMessage(sock, "/buff: no skill Id and duration time specified");
return;
}
char* tmp;
int skillId = std::strtol(args[1].c_str(), &tmp, 10);
if (*tmp)
return;
int duration = std::strtol(args[2].c_str(), &tmp, 10);
if (*tmp)
return;
if (NPCManager::eggBuffPlayer(sock, skillId, duration)<0)
ChatManager::sendServerMessage(sock, "/buff: unknown skill Id");
}
void eggCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
if (args.size() < 2) {
ChatManager::sendServerMessage(sock, "/egg: no egg type specified");
return;
}
char* tmp;
int eggType = std::strtol(args[1].c_str(), &tmp, 10);
if (*tmp)
return;
if (NPCManager::EggTypes.find(eggType) == NPCManager::EggTypes.end()) {
ChatManager::sendServerMessage(sock, "/egg: Unknown egg type");
return;
}
assert(NPCManager::nextId < INT32_MAX);
int id = NPCManager::nextId++;
Player* plr = PlayerManager::getPlayer(sock);
// some math to place egg nicely in front of the player
// temporarly disabled for sake of gruntwork
int addX = 0; //-500.0f * sin(plr->angle / 180.0f * M_PI);
int addY = 0; //-500.0f * cos(plr->angle / 180.0f * M_PI);
Egg* egg = new Egg(plr->x + addX, plr->y + addY, plr->z, plr->instanceID, eggType, id, false); // change last arg to true after gruntwork
NPCManager::NPCs[id] = egg;
NPCManager::Eggs[id] = egg;
NPCManager::updateNPCPosition(id, plr->x + addX, plr->y + addY, plr->z, plr->instanceID, plr->angle);
// add to template
TableData::RunningEggs[id] = egg;
}
void notifyCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player *plr = PlayerManager::getPlayer(sock);
if (plr->notify) {
plr->notify = false;
ChatManager::sendServerMessage(sock, "[ADMIN] No longer receiving join notifications");
} else {
plr->notify = true;
ChatManager::sendServerMessage(sock, "[ADMIN] Receiving join notifications");
}
}
void playersCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
ChatManager::sendServerMessage(sock, "[ADMIN] Players on the server:");
for (auto pair : PlayerManager::players)
ChatManager::sendServerMessage(sock, PlayerManager::getPlayerName(pair.second));
}
void summonGroupCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
if (args.size() < 4) {
ChatManager::sendServerMessage(sock, "/summonGroup(W) <leadermob> <mob> <number> [distance]");
return;
}
Player* plr = PlayerManager::getPlayer(sock);
char *rest;
bool wCommand = (args[0] == "/summonGroupW");
int type = std::strtol(args[1].c_str(), &rest, 10);
int type2 = std::strtol(args[2].c_str(), &rest, 10);
int count = std::strtol(args[3].c_str(), &rest, 10);
int distance = 150;
if (args.size() > 4)
distance = std::strtol(args[4].c_str(), &rest, 10);
if (*rest) {
ChatManager::sendServerMessage(sock, "Invalid NPC number: " + args[1]);
return;
}
int limit = NPCManager::NPCData.back()["m_iNpcNumber"];
// permission & sanity check
if (type > limit || type2 > limit || count > 5) {
ChatManager::sendServerMessage(sock, "Invalid parameters; double check types and count");
return;
}
Mob* leadNpc = nullptr;
for (int i = 0; i < count; i++) {
int team = NPCManager::NPCData[type]["m_iTeam"];
int x = plr->x;
int y = plr->y;
int z = plr->z;
if (i > 0) {
int angle = 360.0f / (count-1) * (i-1);
if (count == 3)
angle = 90 + 60 * i;
angle += (plr->angle + 180) % 360;
x += -1.0f * sin(angle / 180.0f * M_PI) * distance;
y += -1.0f * cos(angle / 180.0f * M_PI) * distance;
z = plr->z;
}
BaseNPC *npc = NPCManager::summonNPC(x, y, z, plr->instanceID, type, wCommand);
if (team == 2 && i > 0) {
leadNpc->groupMember[i-1] = npc->appearanceData.iNPC_ID;
Mob* mob = MobManager::Mobs[npc->appearanceData.iNPC_ID];
mob->groupLeader = leadNpc->appearanceData.iNPC_ID;
mob->offsetX = x - plr->x;
mob->offsetY = y - plr->y;
}
npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, x, y, z, plr->instanceID, npc->appearanceData.iAngle);
// if we're in a lair, we need to spawn the NPC in both the private instance and the template
if (PLAYERID(plr->instanceID) != 0) {
npc = NPCManager::summonNPC(plr->x, plr->y, plr->z, plr->instanceID, type, wCommand, true);
if (team == 2 && i > 0) {
leadNpc->groupMember[i-1] = npc->appearanceData.iNPC_ID;
Mob* mob = MobManager::Mobs[npc->appearanceData.iNPC_ID];
mob->groupLeader = leadNpc->appearanceData.iNPC_ID;
mob->offsetX = x - plr->x;
mob->offsetY = y - plr->y;
}
npc->appearanceData.iAngle = (plr->angle + 180) % 360;
NPCManager::updateNPCPosition(npc->appearanceData.iNPC_ID, x, y, z, plr->instanceID, npc->appearanceData.iAngle);
}
ChatManager::sendServerMessage(sock, "/summonGroup(W): placed mob with type: " + std::to_string(type) +
", id: " + std::to_string(npc->appearanceData.iNPC_ID));
if (i == 0 && team == 2) {
type = type2;
leadNpc = MobManager::Mobs[npc->appearanceData.iNPC_ID];
leadNpc->groupLeader = leadNpc->appearanceData.iNPC_ID;
}
}
if (!wCommand)
return; // not writing; don't add to running mobs
if (leadNpc == nullptr) {
std::cout << "/summonGroupW: can't find group leader! Won't be saved!\n";
return;
}
TableData::RunningGroups[leadNpc->appearanceData.iNPC_ID] = leadNpc; // only record the leader
}
void flushCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
TableData::flush();
ChatManager::sendServerMessage(sock, "Wrote gruntwork to " + settings::GRUNTWORKJSON);
}
void whoisCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
BaseNPC* npc = NPCManager::getNearestNPC(plr->viewableChunks, plr->x, plr->y, plr->z);
if (npc == nullptr) {
ChatManager::sendServerMessage(sock, "[WHOIS] No NPCs found nearby");
return;
}
ChatManager::sendServerMessage(sock, "[WHOIS] ID: " + std::to_string(npc->appearanceData.iNPC_ID));
ChatManager::sendServerMessage(sock, "[WHOIS] Type: " + std::to_string(npc->appearanceData.iNPCType));
ChatManager::sendServerMessage(sock, "[WHOIS] HP: " + std::to_string(npc->appearanceData.iHP));
ChatManager::sendServerMessage(sock, "[WHOIS] CBF: " + std::to_string(npc->appearanceData.iConditionBitFlag));
ChatManager::sendServerMessage(sock, "[WHOIS] Class: " + std::to_string(npc->npcClass));
ChatManager::sendServerMessage(sock, "[WHOIS] X: " + std::to_string(npc->appearanceData.iX));
ChatManager::sendServerMessage(sock, "[WHOIS] Y: " + std::to_string(npc->appearanceData.iY));
ChatManager::sendServerMessage(sock, "[WHOIS] Z: " + std::to_string(npc->appearanceData.iZ));
ChatManager::sendServerMessage(sock, "[WHOIS] Angle: " + std::to_string(npc->appearanceData.iAngle));
std::string chunkPosition = std::to_string(std::get<0>(npc->chunkPos)) + ", " + std::to_string(std::get<1>(npc->chunkPos)) + ", " + std::to_string(std::get<2>(npc->chunkPos));
ChatManager::sendServerMessage(sock, "[WHOIS] Chunk: {" + chunkPosition + "}");
ChatManager::sendServerMessage(sock, "[WHOIS] MapNum: " + std::to_string(MAPNUM(npc->instanceID)));
ChatManager::sendServerMessage(sock, "[WHOIS] Instance: " + std::to_string(PLAYERID(npc->instanceID)));
}
void lairUnlockCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
if (!ChunkManager::chunkExists(plr->chunkPos))
return;
Chunk* chnk = ChunkManager::chunks[plr->chunkPos];
int taskID = -1;
int missionID = -1;
int found = 0;
for (int32_t id : chnk->NPCs) {
if (NPCManager::NPCs.find(id) == NPCManager::NPCs.end())
continue;
BaseNPC* npc = NPCManager::NPCs[id];
for (auto it = NPCManager::Warps.begin(); it != NPCManager::Warps.end(); it++) {
if ((*it).second.npcID == npc->appearanceData.iNPCType) {
taskID = (*it).second.limitTaskID;
missionID = MissionManager::Tasks[taskID]->task["m_iHMissionID"];
found++;
break;
}
}
}
if (missionID == -1 || taskID == -1) {
ChatManager::sendServerMessage(sock, "You are NOT standing near a lair portal; move around and try again!");
return;
}
if (found > 1) {
ChatManager::sendServerMessage(sock, "More than one lair found; decrease chunk size and try again!");
return;
}
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_SUCC, taskResp);
MissionManager::startTask(plr, taskID);
taskResp.iTaskNum = taskID;
taskResp.iRemainTime = 0;
sock->sendPacket((void*)&taskResp, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
INITSTRUCT(sP_FE2CL_REP_PC_SET_CURRENT_MISSION_ID, missionResp);
missionResp.iCurrentMissionID = missionID;
plr->CurrentMissionID = missionID;
sock->sendPacket((void*)&missionResp, P_FE2CL_REP_PC_SET_CURRENT_MISSION_ID, sizeof(sP_FE2CL_REP_PC_SET_CURRENT_MISSION_ID));
}
void hideCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
if (plr->hidden) {
ChatManager::sendServerMessage(sock, "[HIDE] You're already hidden from the map.");
return;
}
plr->hidden = true;
ChatManager::sendServerMessage(sock, "[HIDE] Successfully hidden from the map.");
}
void unhideCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
Player* plr = PlayerManager::getPlayer(sock);
if (!plr->hidden) {
ChatManager::sendServerMessage(sock, "[HIDE] You're already visible from the map.");
return;
}
plr->hidden = false;
ChatManager::sendServerMessage(sock, "[HIDE] Successfully un-hidden from the map.");
}
void redeemCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
if (args.size() < 2) {
ChatManager::sendServerMessage(sock, "/redeem: No code specified");
return;
}
// convert string to all lowercase
const char* codeRaw = args[1].c_str();
if (args[1].size() > 256) { // prevent overflow
ChatManager::sendServerMessage(sock, "/redeem: Code too long");
return;
}
char buf[256];
for (int i = 0; i < args[1].size(); i++)
buf[i] = std::tolower(codeRaw[i]);
std::string code(buf, args[1].size());
if (ItemManager::CodeItems.find(code) == ItemManager::CodeItems.end()) {
ChatManager::sendServerMessage(sock, "/redeem: Unknown code");
return;
}
Player* plr = PlayerManager::getPlayer(sock);
int itemCount = ItemManager::CodeItems[code].size();
int slots[4];
for (int i = 0; i < itemCount; i++) {
slots[i] = ItemManager::findFreeSlot(plr);
if (slots[i] == -1) {
ChatManager::sendServerMessage(sock, "/redeem: Not enough space in inventory");
// 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;
}
plr->Inven[slots[i]] = { 999, 999, 999, 0 }; // temp item; overwritten later
}
for (int i = 0; i < itemCount; i++) {
std::pair<int32_t, int32_t> item = ItemManager::CodeItems[code][i];
INITSTRUCT(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC, resp);
resp.eIL = 1;
resp.iSlotNum = slots[i];
resp.Item.iID = item.first;
resp.Item.iType = item.second;
// I think it is safe? :eyes
resp.Item.iOpt = 1;
// save serverside
plr->Inven[resp.iSlotNum] = resp.Item;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_GIVE_ITEM_SUCC, sizeof(sP_FE2CL_REP_PC_GIVE_ITEM_SUCC));
}
std::string msg = itemCount == 1 ? "You have redeemed a code item" : "You have redeemed code items";
ChatManager::sendServerMessage(sock, msg);
}
void ChatManager::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_GM_REQ_PC_ANNOUNCE, announcementHandler);
registerCommand("help", 100, helpCommand, "list all unlocked server-side commands");
registerCommand("access", 100, accessCommand, "print your access level");
registerCommand("instance", 30, instanceCommand, "print or change your current instance");
registerCommand("mss", 30, mssCommand, "edit Monkey Skyway routes");
registerCommand("npcr", 30, npcRotateCommand, "rotate NPCs");
registerCommand("npci", 30, npcInstanceCommand, "move NPCs across instances");
registerCommand("summonW", 30, summonWCommand, "permanently summon NPCs");
registerCommand("unsummonW", 30, unsummonWCommand, "delete permanently summoned NPCs");
registerCommand("toggleai", 30, toggleAiCommand, "enable/disable mob AI");
registerCommand("flush", 30, flushCommand, "save gruntwork to file");
registerCommand("level", 50, levelCommand, "change your character's level");
registerCommand("levelx", 50, levelCommand, "change your character's level"); // for Academy
registerCommand("population", 100, populationCommand, "check how many players are online");
registerCommand("refresh", 100, refreshCommand, "teleport yourself to your current location");
registerCommand("minfo", 30, minfoCommand, "show details of the current mission and task.");
registerCommand("buff", 50, buffCommand, "give yourself a buff effect");
registerCommand("egg", 30, eggCommand, "summon a coco egg");
registerCommand("tasks", 30, tasksCommand, "list all active missions and their respective task ids.");
registerCommand("notify", 30, notifyCommand, "receive a message whenever a player joins the server");
registerCommand("players", 30, playersCommand, "print all players on the server");
registerCommand("summonGroup", 30, summonGroupCommand, "summon group NPCs");
registerCommand("summonGroupW", 30, summonGroupCommand, "permanently summon group NPCs");
registerCommand("whois", 50, whoisCommand, "describe nearest NPC");
registerCommand("lair", 50, lairUnlockCommand, "get the required mission for the nearest fusion lair");
registerCommand("hide", 100, hideCommand, "hide yourself from the global player map");
registerCommand("unhide", 100, unhideCommand, "un-hide yourself from the global player map");
registerCommand("redeem", 100, redeemCommand, "redeem a code item");
}
void ChatManager::registerCommand(std::string cmd, int requiredLevel, CommandHandler handlr, std::string help) {
commands[cmd] = ChatCommand(requiredLevel, handlr, help);
}
void ChatManager::chatHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_SEND_FREECHAT_MESSAGE))
return; // malformed packet
sP_CL2FE_REQ_SEND_FREECHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_FREECHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
std::string fullChat = sanitizeText(U16toU8(chat->szFreeChat));
std::cout << "[FreeChat] " << PlayerManager::getPlayerName(plr, false) << ": " << fullChat << std::endl;
dump.push_back(PlayerManager::getPlayerName(plr, true) + ": " + fullChat);
if (fullChat.length() > 1 && fullChat[0] == CMD_PREFIX) { // PREFIX
runCmd(fullChat, sock);
return;
}
// 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((void*)&resp, P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC));
// send to visible players
PlayerManager::sendToViewable(sock, (void*)&resp, P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC));
}
void ChatManager::menuChatHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_SEND_MENUCHAT_MESSAGE))
return; // malformed packet
sP_CL2FE_REQ_SEND_MENUCHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_MENUCHAT_MESSAGE*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
std::string fullChat = sanitizeText(U16toU8(chat->szFreeChat));
std::cout << "[MenuChat] " << PlayerManager::getPlayerName(plr, false) << ": " << fullChat << std::endl;
dump.push_back(PlayerManager::getPlayerName(plr, true) + ": " + fullChat);
// 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((void*)&resp, P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC));
// send to visible players
PlayerManager::sendToViewable(sock, (void*)&resp, P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC));
}
void ChatManager::emoteHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT))
return; // ignore the malformed packet
// you can dance with friends!!!!!!!!
sP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT* 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((void*)&resp, P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT, sizeof(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT));
// send to visible players (players within render distance)
PlayerManager::sendToViewable(sock, (void*)&resp, P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT, sizeof(sP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT));
}
void ChatManager::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((void*)&motd, P_FE2CL_PC_MOTD_LOGIN, sizeof(sP_FE2CL_PC_MOTD_LOGIN));
}
void ChatManager::announcementHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_GM_REQ_PC_ANNOUNCE))
return; // ignore malformed packet
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
sP_CL2FE_GM_REQ_PC_ANNOUNCE* 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;
switch (announcement->iAreaType) {
case 0: // area (all players in viewable chunks)
sock->sendPacket((void*)&msg, P_FE2CL_GM_REP_PC_ANNOUNCE, sizeof(sP_FE2CL_GM_REP_PC_ANNOUNCE));
PlayerManager::sendToViewable(sock, (void*)&msg, P_FE2CL_GM_REP_PC_ANNOUNCE, sizeof(sP_FE2CL_GM_REP_PC_ANNOUNCE));
break;
case 1: // shard
case 2: // world
break; // not applicable to OpenFusion
case 3: // global (all players)
for (it = PlayerManager::players.begin(); it != PlayerManager::players.end(); it++) {
CNSocket* allSock = it->first;
allSock->sendPacket((void*)&msg, P_FE2CL_GM_REP_PC_ANNOUNCE, sizeof(sP_FE2CL_GM_REP_PC_ANNOUNCE));
}
default:
break;
}
std::cout << "[Bcast " << announcement->iAreaType << "] " << PlayerManager::getPlayerName(plr, false) << ": " << U16toU8(msg.szAnnounceMsg) << std::endl;
dump.push_back("**" + PlayerManager::getPlayerName(plr, true) + ": " + U16toU8(msg.szAnnounceMsg) + "**");
}
// we only allow plain ascii, at least for now
std::string ChatManager::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);
}

View File

@@ -1,33 +0,0 @@
#pragma once
#include "CNShardServer.hpp"
#define CMD_PREFIX '/'
typedef void (*CommandHandler)(std::string fullString, std::vector<std::string>& args, CNSocket* sock);
struct ChatCommand {
int requiredAccLevel;
std::string help;
CommandHandler handlr;
ChatCommand(int r, CommandHandler h): requiredAccLevel(r), handlr(h) {}
ChatCommand(int r, CommandHandler h, std::string str): requiredAccLevel(r), help(str), handlr(h) {}
ChatCommand(): ChatCommand(0, nullptr) {}
};
namespace ChatManager {
extern std::map<std::string, ChatCommand> commands;
extern std::vector<std::string> dump;
void init();
void registerCommand(std::string cmd, int requiredLevel, CommandHandler handlr, std::string help = "");
void chatHandler(CNSocket* sock, CNPacketData* data);
void emoteHandler(CNSocket* sock, CNPacketData* data);
void menuChatHandler(CNSocket* sock, CNPacketData* data);
void sendServerMessage(CNSocket* sock, std::string msg); // uses MOTD
void announcementHandler(CNSocket* sock, CNPacketData* data);
std::string sanitizeText(std::string text, bool allowNewlines=false);
}

View File

@@ -1,534 +0,0 @@
#include "ChunkManager.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "settings.hpp"
#include "MobManager.hpp"
std::map<ChunkPos, Chunk*> ChunkManager::chunks;
void ChunkManager::init() {} // stubbed
void ChunkManager::newChunk(ChunkPos pos) {
if (chunkExists(pos)) {
std::cout << "[WARN] Tried to create a chunk that already exists\n";
return;
}
Chunk *chunk = new Chunk();
chunk->players = std::set<CNSocket*>();
chunk->NPCs = std::set<int32_t>();
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 (CNSocket* sock : c->players)
PlayerManager::getPlayer(sock)->viewableChunks->insert(chunk);
for (int32_t id : c->NPCs)
NPCManager::NPCs[id]->viewableChunks->insert(chunk);
}
}
void ChunkManager::deleteChunk(ChunkPos pos) {
if (!chunkExists(pos)) {
std::cout << "[WARN] Tried to delete a chunk that doesn't exist\n";
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 (CNSocket* sock : c->players)
PlayerManager::getPlayer(sock)->viewableChunks->erase(chunk);
for (int32_t id : c->NPCs)
NPCManager::NPCs[id]->viewableChunks->erase(chunk);
}
chunks.erase(pos); // remove from map
delete chunk; // free from memory
}
void ChunkManager::updatePlayerChunk(CNSocket* sock, ChunkPos from, ChunkPos to) {
Player* plr = PlayerManager::getPlayer(sock);
// if the new chunk doesn't exist, make it first
if (!ChunkManager::chunkExists(to))
newChunk(to);
// move to other chunk's player set
untrackPlayer(from, sock); // this will delete the chunk if it's empty
trackPlayer(to, sock);
// 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
removePlayerFromChunks(toExit, sock);
addPlayerToChunks(toEnter, sock);
plr->chunkPos = to; // update cached chunk position
// updated cached viewable chunks
plr->viewableChunks->clear();
plr->viewableChunks->insert(newViewables.begin(), newViewables.end());
}
void ChunkManager::updateNPCChunk(int32_t id, ChunkPos from, ChunkPos to) {
BaseNPC* npc = NPCManager::NPCs[id];
// if the new chunk doesn't exist, make it first
if (!ChunkManager::chunkExists(to))
newChunk(to);
// move to other chunk's player set
untrackNPC(from, id); // this will delete the chunk if it's empty
trackNPC(to, id);
// 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
removeNPCFromChunks(toExit, id);
addNPCToChunks(toEnter, id);
npc->chunkPos = to; // update cached chunk position
// updated cached viewable chunks
npc->viewableChunks->clear();
npc->viewableChunks->insert(newViewables.begin(), newViewables.end());
}
void ChunkManager::trackPlayer(ChunkPos chunkPos, CNSocket* sock) {
if (!chunkExists(chunkPos))
return; // shouldn't happen
chunks[chunkPos]->players.insert(sock);
}
void ChunkManager::trackNPC(ChunkPos chunkPos, int32_t id) {
if (!chunkExists(chunkPos))
return; // shouldn't happen
chunks[chunkPos]->NPCs.insert(id);
}
void ChunkManager::untrackPlayer(ChunkPos chunkPos, CNSocket* sock) {
if (!chunkExists(chunkPos))
return; // do nothing if chunk doesn't even exist
Chunk* chunk = chunks[chunkPos];
chunk->players.erase(sock); // gone
// if chunk is empty, free it
if (chunk->NPCs.size() == 0 && chunk->players.size() == 0)
deleteChunk(chunkPos);
}
void ChunkManager::untrackNPC(ChunkPos chunkPos, int32_t id) {
if (!chunkExists(chunkPos))
return; // do nothing if chunk doesn't even exist
Chunk* chunk = chunks[chunkPos];
chunk->NPCs.erase(id); // gone
// if chunk is empty, free it
if (chunk->NPCs.size() == 0 && chunk->players.size() == 0)
deleteChunk(chunkPos);
}
void ChunkManager::addPlayerToChunks(std::set<Chunk*> chnks, CNSocket* sock) {
INITSTRUCT(sP_FE2CL_PC_NEW, newPlayer);
for (Chunk* chunk : chnks) {
// add npcs
for (int32_t id : chunk->NPCs) {
BaseNPC* npc = NPCManager::NPCs[id];
npc->playersInView++;
if (npc->appearanceData.iHP <= 0)
continue;
switch (npc->npcClass) {
case NPC_BUS:
INITSTRUCT(sP_FE2CL_TRANSPORTATION_ENTER, enterBusData);
enterBusData.AppearanceData = { 3, npc->appearanceData.iNPC_ID, npc->appearanceData.iNPCType, npc->appearanceData.iX, npc->appearanceData.iY, npc->appearanceData.iZ };
sock->sendPacket((void*)&enterBusData, P_FE2CL_TRANSPORTATION_ENTER, sizeof(sP_FE2CL_TRANSPORTATION_ENTER));
break;
case NPC_EGG:
INITSTRUCT(sP_FE2CL_SHINY_ENTER, enterEggData);
NPCManager::npcDataToEggData(&npc->appearanceData, &enterEggData.ShinyAppearanceData);
sock->sendPacket((void*)&enterEggData, P_FE2CL_SHINY_ENTER, sizeof(sP_FE2CL_SHINY_ENTER));
break;
default:
INITSTRUCT(sP_FE2CL_NPC_ENTER, enterData);
enterData.NPCAppearanceData = NPCManager::NPCs[id]->appearanceData;
sock->sendPacket((void*)&enterData, P_FE2CL_NPC_ENTER, sizeof(sP_FE2CL_NPC_ENTER));
break;
}
}
// add players
for (CNSocket* otherSock : chunk->players) {
if (sock == otherSock)
continue; // that's us :P
Player* otherPlr = PlayerManager::getPlayer(otherSock);
Player* plr = PlayerManager::getPlayer(sock);
newPlayer.PCAppearanceData.iID = plr->iID;
newPlayer.PCAppearanceData.iHP = plr->HP;
newPlayer.PCAppearanceData.iLv = plr->level;
newPlayer.PCAppearanceData.iX = plr->x;
newPlayer.PCAppearanceData.iY = plr->y;
newPlayer.PCAppearanceData.iZ = plr->z;
newPlayer.PCAppearanceData.iAngle = plr->angle;
newPlayer.PCAppearanceData.PCStyle = plr->PCStyle;
newPlayer.PCAppearanceData.Nano = plr->Nanos[plr->activeNano];
newPlayer.PCAppearanceData.iPCState = plr->iPCState;
newPlayer.PCAppearanceData.iSpecialState = plr->iSpecialState;
memcpy(newPlayer.PCAppearanceData.ItemEquip, plr->Equip, sizeof(sItemBase) * AEQUIP_COUNT);
otherSock->sendPacket((void*)&newPlayer, P_FE2CL_PC_NEW, sizeof(sP_FE2CL_PC_NEW));
newPlayer.PCAppearanceData.iID = otherPlr->iID;
newPlayer.PCAppearanceData.iHP = otherPlr->HP;
newPlayer.PCAppearanceData.iLv = otherPlr->level;
newPlayer.PCAppearanceData.iX = otherPlr->x;
newPlayer.PCAppearanceData.iY = otherPlr->y;
newPlayer.PCAppearanceData.iZ = otherPlr->z;
newPlayer.PCAppearanceData.iAngle = otherPlr->angle;
newPlayer.PCAppearanceData.PCStyle = otherPlr->PCStyle;
newPlayer.PCAppearanceData.Nano = otherPlr->Nanos[otherPlr->activeNano];
newPlayer.PCAppearanceData.iPCState = otherPlr->iPCState;
newPlayer.PCAppearanceData.iSpecialState = otherPlr->iSpecialState;
memcpy(newPlayer.PCAppearanceData.ItemEquip, otherPlr->Equip, sizeof(sItemBase) * AEQUIP_COUNT);
sock->sendPacket((void*)&newPlayer, P_FE2CL_PC_NEW, sizeof(sP_FE2CL_PC_NEW));
}
}
}
void ChunkManager::addNPCToChunks(std::set<Chunk*> chnks, int32_t id) {
BaseNPC* npc = NPCManager::NPCs[id];
switch (npc->npcClass) {
case NPC_BUS:
INITSTRUCT(sP_FE2CL_TRANSPORTATION_ENTER, enterBusData);
enterBusData.AppearanceData = { 3, npc->appearanceData.iNPC_ID, npc->appearanceData.iNPCType, npc->appearanceData.iX, npc->appearanceData.iY, npc->appearanceData.iZ };
for (Chunk* chunk : chnks) {
for (CNSocket* sock : chunk->players) {
// send to socket
sock->sendPacket((void*)&enterBusData, P_FE2CL_TRANSPORTATION_ENTER, sizeof(sP_FE2CL_TRANSPORTATION_ENTER));
npc->playersInView++;
}
}
break;
case NPC_EGG:
INITSTRUCT(sP_FE2CL_SHINY_ENTER, enterEggData);
NPCManager::npcDataToEggData(&npc->appearanceData, &enterEggData.ShinyAppearanceData);
for (Chunk* chunk : chnks) {
for (CNSocket* sock : chunk->players) {
// send to socket
sock->sendPacket((void*)&enterEggData, P_FE2CL_SHINY_ENTER, sizeof(sP_FE2CL_SHINY_ENTER));
npc->playersInView++;
}
}
break;
default:
// create struct
INITSTRUCT(sP_FE2CL_NPC_ENTER, enterData);
enterData.NPCAppearanceData = npc->appearanceData;
for (Chunk* chunk : chnks) {
for (CNSocket* sock : chunk->players) {
// send to socket
sock->sendPacket((void*)&enterData, P_FE2CL_NPC_ENTER, sizeof(sP_FE2CL_NPC_ENTER));
npc->playersInView++;
}
}
break;
}
}
void ChunkManager::removePlayerFromChunks(std::set<Chunk*> chnks, CNSocket* sock) {
INITSTRUCT(sP_FE2CL_PC_EXIT, exitPlayer);
// for chunks that need the player to be removed from
for (Chunk* chunk : chnks) {
// remove NPCs from view
for (int32_t id : chunk->NPCs) {
BaseNPC* npc = NPCManager::NPCs[id];
npc->playersInView--;
switch (npc->npcClass) {
case NPC_BUS:
INITSTRUCT(sP_FE2CL_TRANSPORTATION_EXIT, exitBusData);
exitBusData.eTT = 3;
exitBusData.iT_ID = id;
sock->sendPacket((void*)&exitBusData, P_FE2CL_TRANSPORTATION_EXIT, sizeof(sP_FE2CL_TRANSPORTATION_EXIT));
break;
case NPC_EGG:
INITSTRUCT(sP_FE2CL_SHINY_EXIT, exitEggData);
exitEggData.iShinyID = id;
sock->sendPacket((void*)&exitEggData, P_FE2CL_SHINY_EXIT, sizeof(sP_FE2CL_SHINY_EXIT));
break;
default:
INITSTRUCT(sP_FE2CL_NPC_EXIT, exitData);
exitData.iNPC_ID = id;
sock->sendPacket((void*)&exitData, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT));
break;
}
}
// remove players from eachother's views
for (CNSocket* otherSock : chunk->players) {
if (sock == otherSock)
continue; // that's us :P
exitPlayer.iID = PlayerManager::getPlayer(sock)->iID;
otherSock->sendPacket((void*)&exitPlayer, P_FE2CL_PC_EXIT, sizeof(sP_FE2CL_PC_EXIT));
exitPlayer.iID = PlayerManager::getPlayer(otherSock)->iID;
sock->sendPacket((void*)&exitPlayer, P_FE2CL_PC_EXIT, sizeof(sP_FE2CL_PC_EXIT));
}
}
}
void ChunkManager::removeNPCFromChunks(std::set<Chunk*> chnks, int32_t id) {
BaseNPC* npc = NPCManager::NPCs[id];
switch (npc->npcClass) {
case NPC_BUS:
INITSTRUCT(sP_FE2CL_TRANSPORTATION_EXIT, exitBusData);
exitBusData.eTT = 3;
exitBusData.iT_ID = id;
for (Chunk* chunk : chnks) {
for (CNSocket* sock : chunk->players) {
// send to socket
sock->sendPacket((void*)&exitBusData, P_FE2CL_TRANSPORTATION_EXIT, sizeof(sP_FE2CL_TRANSPORTATION_EXIT));
npc->playersInView--;
}
}
break;
case NPC_EGG:
INITSTRUCT(sP_FE2CL_SHINY_EXIT, exitEggData);
exitEggData.iShinyID = id;
for (Chunk* chunk : chnks) {
for (CNSocket* sock : chunk->players) {
// send to socket
sock->sendPacket((void*)&exitEggData, P_FE2CL_SHINY_EXIT, sizeof(sP_FE2CL_SHINY_EXIT));
npc->playersInView--;
}
}
break;
default:
// create struct
INITSTRUCT(sP_FE2CL_NPC_EXIT, exitData);
exitData.iNPC_ID = id;
// remove it from the clients
for (Chunk* chunk : chnks) {
for (CNSocket* sock : chunk->players) {
// send to socket
sock->sendPacket((void*)&exitData, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT));
npc->playersInView--;
}
}
break;
}
}
void ChunkManager::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->players.size() > 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 npcIDs(chunk->NPCs);
for (uint32_t id : npcIDs) {
// every call of this will check if the chunk is empty and delete it if so
NPCManager::destroyNPC(id);
}
}
bool ChunkManager::chunkExists(ChunkPos chunk) {
return chunks.find(chunk) != chunks.end();
}
ChunkPos ChunkManager::chunkPosAt(int posX, int posY, uint64_t instanceID) {
return std::make_tuple(posX / (settings::VIEWDISTANCE / 3), posY / (settings::VIEWDISTANCE / 3), instanceID);
}
std::set<Chunk*> ChunkManager::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 = std::make_tuple(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> ChunkManager::getChunksInMap(uint64_t mapNum) {
std::vector<ChunkPos> chnks;
for (auto it = ChunkManager::chunks.begin(); it != ChunkManager::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 ChunkManager::inPopulatedChunks(std::set<Chunk*>* chnks) {
for (auto it = chnks->begin(); it != chnks->end(); it++) {
if (!(*it)->players.empty())
return true;
}
return false;
}
void ChunkManager::createInstance(uint64_t instanceID) {
std::vector<ChunkPos> templateChunks = ChunkManager::getChunksInMap(MAPNUM(instanceID)); // base instance chunks
if (ChunkManager::getChunksInMap(instanceID).size() == 0) { // only instantiate if the instance doesn't exist already
std::cout << "Creating instance " << instanceID << std::endl;
for (ChunkPos &coords : templateChunks) {
for (int npcID : chunks[coords]->NPCs) {
// make a copy of each NPC in the template chunks and put them in the new instance
BaseNPC* baseNPC = NPCManager::NPCs[npcID];
if (baseNPC->npcClass == NPC_MOB) {
if (((Mob*)baseNPC)->groupLeader != 0 && ((Mob*)baseNPC)->groupLeader != npcID)
continue; // follower; don't copy individually
Mob* newMob = new Mob(baseNPC->appearanceData.iX, baseNPC->appearanceData.iY, baseNPC->appearanceData.iZ, baseNPC->appearanceData.iAngle,
instanceID, baseNPC->appearanceData.iNPCType, NPCManager::NPCData[baseNPC->appearanceData.iNPCType], NPCManager::nextId++);
NPCManager::NPCs[newMob->appearanceData.iNPC_ID] = newMob;
MobManager::Mobs[newMob->appearanceData.iNPC_ID] = newMob;
// if in a group, copy over group members as well
if (((Mob*)baseNPC)->groupLeader != 0) {
newMob->groupLeader = newMob->appearanceData.iNPC_ID; // set leader ID for new leader
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->appearanceData.iX, baseFollower->appearanceData.iY, baseFollower->appearanceData.iZ, baseFollower->appearanceData.iAngle,
instanceID, baseFollower->appearanceData.iNPCType, NPCManager::NPCData[baseFollower->appearanceData.iNPCType], followerID);
// add follower to NPC maps
NPCManager::NPCs[followerID] = newMobFollower;
MobManager::Mobs[followerID] = newMobFollower;
// set follower-specific properties
newMobFollower->groupLeader = newMob->appearanceData.iNPC_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->appearanceData.iX, baseFollower->appearanceData.iY, baseFollower->appearanceData.iZ,
instanceID, baseFollower->appearanceData.iAngle);
}
}
}
NPCManager::updateNPCPosition(newMob->appearanceData.iNPC_ID, baseNPC->appearanceData.iX, baseNPC->appearanceData.iY, baseNPC->appearanceData.iZ,
instanceID, baseNPC->appearanceData.iAngle);
} else {
BaseNPC* newNPC = new BaseNPC(baseNPC->appearanceData.iX, baseNPC->appearanceData.iY, baseNPC->appearanceData.iZ, baseNPC->appearanceData.iAngle,
instanceID, baseNPC->appearanceData.iNPCType, NPCManager::nextId++);
NPCManager::NPCs[newNPC->appearanceData.iNPC_ID] = newNPC;
NPCManager::updateNPCPosition(newNPC->appearanceData.iNPC_ID, baseNPC->appearanceData.iX, baseNPC->appearanceData.iY, baseNPC->appearanceData.iZ,
instanceID, baseNPC->appearanceData.iAngle);
}
}
}
} else {
std::cout << "Instance " << instanceID << " already exists" << std::endl;
}
}
void ChunkManager::destroyInstance(uint64_t instanceID) {
std::vector<ChunkPos> instanceChunks = ChunkManager::getChunksInMap(instanceID);
std::cout << "Deleting instance " << instanceID << " (" << instanceChunks.size() << " chunks)" << std::endl;
for (ChunkPos& coords : instanceChunks) {
emptyChunk(coords);
}
}
void ChunkManager::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->players.size() > 0)
return; // there are still players inside
}
destroyInstance(instanceID);
}

View File

@@ -1,56 +0,0 @@
#pragma once
#include "CNProtocol.hpp"
#include "CNStructs.hpp"
#include <utility>
#include <set>
#include <map>
#include <tuple>
#include <algorithm>
class Chunk {
public:
std::set<CNSocket*> players;
std::set<int32_t> NPCs;
};
enum {
INSTANCE_OVERWORLD, // default instance every player starts in
INSTANCE_IZ, // these aren't actually used
INSTANCE_UNIQUE // these aren't actually used
};
namespace ChunkManager {
void init();
void cleanup();
extern std::map<ChunkPos, Chunk*> chunks;
void newChunk(ChunkPos pos);
void deleteChunk(ChunkPos pos);
void updatePlayerChunk(CNSocket* sock, ChunkPos from, ChunkPos to);
void updateNPCChunk(int32_t id, ChunkPos from, ChunkPos to);
void trackPlayer(ChunkPos chunkPos, CNSocket* sock);
void trackNPC(ChunkPos chunkPos, int32_t id);
void untrackPlayer(ChunkPos chunkPos, CNSocket* sock);
void untrackNPC(ChunkPos chunkPos, int32_t id);
void addPlayerToChunks(std::set<Chunk*> chnks, CNSocket* sock);
void addNPCToChunks(std::set<Chunk*> chnks, int32_t id);
void removePlayerFromChunks(std::set<Chunk*> chnks, CNSocket* sock);
void removeNPCFromChunks(std::set<Chunk*> chnks, int32_t id);
bool chunkExists(ChunkPos chunk);
void emptyChunk(ChunkPos chunkPos);
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 destroyInstance(uint64_t);
void destroyInstanceIfEmpty(uint64_t);
}

345
src/Chunking.cpp Normal file
View File

@@ -0,0 +1,345 @@
#include "Chunking.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "settings.hpp"
#include "Combat.hpp"
#include "Eggs.hpp"
using namespace Chunking;
/*
* The initial chunkPos value before a player is placed into the world.
*/
const ChunkPos Chunking::INVALID_CHUNK = {};
std::map<ChunkPos, Chunk*> Chunking::chunks;
static void newChunk(ChunkPos pos) {
if (chunkExists(pos)) {
std::cout << "[WARN] Tried to create a chunk that already exists" << 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.type == EntityType::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.type == EntityType::PLAYER)
chunks[chunkPos]->nplayers--;
assert(chunks[chunkPos]->nplayers >= 0);
// if chunk is completely empty, free it
if (chunk->entities.size() == 0)
deleteChunk(chunkPos);
}
void Chunking::addEntityToChunks(std::set<Chunk*> chnks, const EntityRef& ref) {
Entity *ent = ref.getEntity();
bool alive = ent->isAlive();
// TODO: maybe optimize this, potentially using AROUND packets?
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.type == EntityType::PLAYER) {
ent->enterIntoViewOf(otherRef.sock);
}
// notify this *player* of the existence of all visible Entities
if (ref.type == EntityType::PLAYER && other->isAlive()) {
other->enterIntoViewOf(ref.sock);
}
// for mobs, increment playersInView
if (ref.type == EntityType::MOB && otherRef.type == EntityType::PLAYER)
((Mob*)ent)->playersInView++;
if (otherRef.type == EntityType::MOB && ref.type == EntityType::PLAYER)
((Mob*)other)->playersInView++;
}
}
}
void Chunking::removeEntityFromChunks(std::set<Chunk*> chnks, const EntityRef& ref) {
Entity *ent = ref.getEntity();
bool alive = ent->isAlive();
// TODO: same as above
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.type == EntityType::PLAYER) {
ent->disappearFromViewOf(otherRef.sock);
}
// notify this *player* of the departure of all visible Entities
if (ref.type == EntityType::PLAYER && other->isAlive()) {
other->disappearFromViewOf(ref.sock);
}
// for mobs, decrement playersInView
if (ref.type == EntityType::MOB && otherRef.type == EntityType::PLAYER)
((Mob*)ent)->playersInView--;
if (otherRef.type == EntityType::MOB && ref.type == EntityType::PLAYER)
((Mob*)other)->playersInView--;
}
}
}
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.type == EntityType::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.type == EntityType::PLAYER)
continue;
int npcID = ref.id;
BaseNPC* baseNPC = (BaseNPC*)ref.getEntity();
// make a copy of each NPC in the template chunks and put them in the new instance
if (baseNPC->type == EntityType::MOB) {
if (((Mob*)baseNPC)->groupLeader != 0 && ((Mob*)baseNPC)->groupLeader != npcID)
continue; // follower; don't copy individually
Mob* newMob = new Mob(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->appearanceData.iAngle,
instanceID, baseNPC->appearanceData.iNPCType, NPCManager::NPCData[baseNPC->appearanceData.iNPCType], NPCManager::nextId--);
NPCManager::NPCs[newMob->appearanceData.iNPC_ID] = newMob;
// if in a group, copy over group members as well
if (((Mob*)baseNPC)->groupLeader != 0) {
newMob->groupLeader = newMob->appearanceData.iNPC_ID; // set leader ID for new leader
Mob* mobData = (Mob*)baseNPC;
for (int i = 0; i < 4; i++) {
if (mobData->groupMember[i] != 0) {
int followerID = NPCManager::nextId--; // id for follower
BaseNPC* baseFollower = NPCManager::NPCs[mobData->groupMember[i]]; // follower from template
// new follower instance
Mob* newMobFollower = new Mob(baseFollower->x, baseFollower->y, baseFollower->z, baseFollower->appearanceData.iAngle,
instanceID, baseFollower->appearanceData.iNPCType, NPCManager::NPCData[baseFollower->appearanceData.iNPCType], followerID);
// add follower to NPC maps
NPCManager::NPCs[followerID] = newMobFollower;
// set follower-specific properties
newMobFollower->groupLeader = newMob->appearanceData.iNPC_ID;
newMobFollower->offsetX = ((Mob*)baseFollower)->offsetX;
newMobFollower->offsetY = ((Mob*)baseFollower)->offsetY;
// add follower copy to leader copy
newMob->groupMember[i] = followerID;
NPCManager::updateNPCPosition(followerID, baseFollower->x, baseFollower->y, baseFollower->z,
instanceID, baseFollower->appearanceData.iAngle);
}
}
}
NPCManager::updateNPCPosition(newMob->appearanceData.iNPC_ID, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->appearanceData.iAngle);
} else {
BaseNPC* newNPC = new BaseNPC(baseNPC->x, baseNPC->y, baseNPC->z, baseNPC->appearanceData.iAngle,
instanceID, baseNPC->appearanceData.iNPCType, NPCManager::nextId--);
NPCManager::NPCs[newNPC->appearanceData.iNPC_ID] = newNPC;
NPCManager::updateNPCPosition(newNPC->appearanceData.iNPC_ID, baseNPC->x, baseNPC->y, baseNPC->z,
instanceID, baseNPC->appearanceData.iAngle);
}
}
}
}
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);
}

55
src/Chunking.hpp Normal file
View File

@@ -0,0 +1,55 @@
#pragma once
#include "core/Core.hpp"
#include <utility>
#include <set>
#include <map>
#include <tuple>
#include <algorithm>
struct EntityRef;
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);
}

824
src/Combat.cpp Normal file
View File

@@ -0,0 +1,824 @@
#include "Combat.hpp"
#include "PlayerManager.hpp"
#include "Nanos.hpp"
#include "NPCManager.hpp"
#include "Items.hpp"
#include "Missions.hpp"
#include "Groups.hpp"
#include "Transport.hpp"
#include "Racing.hpp"
#include "Abilities.hpp"
#include "Rand.hpp"
#include <assert.h>
using namespace Combat;
/// Player Id -> Bullet Id -> Bullet
std::map<int32_t, std::map<int8_t, Bullet>> Combat::Bullets;
static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shouldCrit,
bool batteryBoost, int attackerStyle,
int defenderStyle, int difficulty) {
std::pair<int,int> ret = {0, 1};
if (attackPower + defensePower * 2 == 0)
return ret;
// base calculation
int damage = attackPower * attackPower / (attackPower + defensePower);
damage = std::max(10 + attackPower / 10, damage - (defensePower - attackPower / 6) * difficulty / 100);
damage = damage * (Rand::rand(40) + 80) / 100;
// Adaptium/Blastons/Cosmix
if (attackerStyle != -1 && defenderStyle != -1 && attackerStyle != defenderStyle) {
if (attackerStyle - defenderStyle == 2)
defenderStyle += 3;
if (defenderStyle - attackerStyle == 2)
defenderStyle -= 3;
if (attackerStyle < defenderStyle)
damage = damage * 5 / 4;
else
damage = damage * 4 / 5;
}
// weapon boosts
if (batteryBoost)
damage = damage * 5 / 4;
ret.first = damage;
ret.second = 1;
if (shouldCrit && Rand::rand(20) == 0) {
ret.first *= 2; // critical hit
ret.second = 2;
}
return ret;
}
static bool checkRapidFire(CNSocket *sock, int targetCount) {
Player *plr = PlayerManager::getPlayer(sock);
time_t currTime = getTime();
if (currTime - plr->lastShot < plr->fireRate * 80)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // lose suspicion for delayed firing
plr->lastShot = currTime;
// 3+ targets should never be possible
if (targetCount > 3)
plr->suspicionRating += 10001;
// kill the socket when the player is too suspicious
if (plr->suspicionRating > 10000) {
sock->kill();
CNShardServer::_killConnection(sock);
return true;
}
return false;
}
static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
auto pkt = (sP_CL2FE_REQ_PC_ATTACK_NPCs*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
auto targets = (int32_t*)data->trailers;
// kick the player if firing too rapidly
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt))
return;
/*
* IMPORTANT: This validates memory safety in addition to preventing
* ordinary cheating. If the client sends a very large number of trailing
* values, it could overflow the *response* buffer, which isn't otherwise
* being validated anymore.
*/
if (pkt->iNPCCnt > 3) {
std::cout << "[WARN] Player tried to attack more than 3 NPCs at once" << std::endl;
return;
}
INITVARPACKET(respbuf, sP_FE2CL_PC_ATTACK_NPCs_SUCC, resp, sAttackResult, respdata);
resp->iNPCCnt = pkt->iNPCCnt;
for (int i = 0; i < data->trCnt; i++) {
if (NPCManager::NPCs.find(targets[i]) == NPCManager::NPCs.end()) {
// not sure how to best handle this
std::cout << "[WARN] pcAttackNpcs: NPC ID not found" << std::endl;
return;
}
BaseNPC* npc = NPCManager::NPCs[targets[i]];
if (npc->type != EntityType::MOB) {
std::cout << "[WARN] pcAttackNpcs: NPC is not a mob" << std::endl;
return;
}
Mob* mob = (Mob*)npc;
std::pair<int,int> damage;
if (pkt->iNPCCnt > 1)
damage.first = plr->groupDamage;
else
damage.first = plr->pointDamage;
int difficulty = (int)mob->data["m_iNpcLevel"];
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty),
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
if (plr->batteryW >= 6 + difficulty)
plr->batteryW -= 6 + difficulty;
else
plr->batteryW = 0;
damage.first = hitMob(sock, mob, damage.first);
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iDamage = damage.first;
respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
}
resp->iBatteryW = plr->batteryW;
sock->sendPacket(respbuf, P_FE2CL_PC_ATTACK_NPCs_SUCC);
// a bit of a hack: these are the same size, so we can reuse the response packet
assert(sizeof(sP_FE2CL_PC_ATTACK_NPCs_SUCC) == sizeof(sP_FE2CL_PC_ATTACK_NPCs));
auto *resp1 = (sP_FE2CL_PC_ATTACK_NPCs*)respbuf;
resp1->iPC_ID = plr->iID;
// send to other players
PlayerManager::sendToViewable(sock, respbuf, P_FE2CL_PC_ATTACK_NPCs);
}
void Combat::npcAttackPc(Mob *mob, time_t currTime) {
Player *plr = PlayerManager::getPlayer(mob->target);
INITVARPACKET(respbuf, sP_FE2CL_NPC_ATTACK_PCs, pkt, sAttackResult, atk);
auto damage = getDamage(450 + (int)mob->data["m_iPower"], plr->defense, true, false, -1, -1, 0);
if (!(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
plr->HP -= damage.first;
pkt->iNPC_ID = mob->appearanceData.iNPC_ID;
pkt->iPCCnt = 1;
atk->iID = plr->iID;
atk->iDamage = damage.first;
atk->iHP = plr->HP;
atk->iHitFlag = damage.second;
mob->target->sendPacket(respbuf, P_FE2CL_NPC_ATTACK_PCs);
PlayerManager::sendToViewable(mob->target, respbuf, P_FE2CL_NPC_ATTACK_PCs);
if (plr->HP <= 0) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
if (!MobAI::aggroCheck(mob, currTime)) {
MobAI::clearDebuff(mob);
if (mob->groupLeader != 0)
MobAI::groupRetreat(mob);
}
}
}
int Combat::hitMob(CNSocket *sock, Mob *mob, int damage) {
// cannot kill mobs multiple times; cannot harm retreating mobs
if (mob->state != MobState::ROAMING && mob->state != MobState::COMBAT) {
return 0; // no damage
}
if (mob->skillStyle >= 0)
return 0; // don't hurt a mob casting corruption
if (mob->state == MobState::ROAMING) {
assert(mob->target == nullptr);
MobAI::enterCombat(sock, mob);
if (mob->groupLeader != 0)
MobAI::followToCombat(mob);
}
mob->appearanceData.iHP -= damage;
// wake up sleeping monster
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_MEZ) {
mob->appearanceData.iConditionBitFlag &= ~CSB_BIT_MEZ;
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = mob->appearanceData.iNPC_ID;
pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
if (mob->appearanceData.iHP <= 0)
killMob(mob->target, mob);
return damage;
}
/*
* When a group of players is doing missions together, we want them to all get
* quest items at the same time, but we don't want the odds of quest item
* drops from different missions to be linked together. That's why we use a
* single RNG roll per mission task, and every group member shares that same
* set of rolls.
*/
static void genQItemRolls(Player *leader, std::map<int, int>& rolls) {
for (int i = 0; i < leader->groupCnt; i++) {
if (leader->groupIDs[i] == 0)
continue;
CNSocket *otherSock = PlayerManager::getSockFromID(leader->groupIDs[i]);
if (otherSock == nullptr)
continue;
Player *member = PlayerManager::getPlayer(otherSock);
for (int j = 0; j < ACTIVE_MISSION_COUNT; j++)
if (member->tasks[j] != 0)
rolls[member->tasks[j]] = Rand::rand();
}
}
void Combat::killMob(CNSocket *sock, Mob *mob) {
mob->state = MobState::DEAD;
mob->target = nullptr;
mob->appearanceData.iConditionBitFlag = 0;
mob->skillStyle = -1;
mob->unbuffTimes.clear();
mob->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
// check for the edge case where hitting the mob did not aggro it
if (sock != nullptr) {
Player* plr = PlayerManager::getPlayer(sock);
Items::DropRoll rolled;
Items::DropRoll eventRolled;
std::map<int, int> qitemRolls;
Player *leader = PlayerManager::getPlayerFromID(plr->iIDGroup);
assert(leader != nullptr); // should never happen
genQItemRolls(leader, qitemRolls);
if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) {
Items::giveMobDrop(sock, mob, rolled, eventRolled);
Missions::mobKilled(sock, mob->appearanceData.iNPCType, qitemRolls);
} else {
for (int i = 0; i < leader->groupCnt; i++) {
CNSocket* sockTo = PlayerManager::getSockFromID(leader->groupIDs[i]);
if (sockTo == nullptr)
continue;
Player *otherPlr = PlayerManager::getPlayer(sockTo);
// only contribute to group members' kills if they're close enough
int dist = std::hypot(plr->x - otherPlr->x + 1, plr->y - otherPlr->y + 1);
if (dist > 5000)
continue;
Items::giveMobDrop(sockTo, mob, rolled, eventRolled);
Missions::mobKilled(sockTo, mob->appearanceData.iNPCType, qitemRolls);
}
}
}
// delay the despawn animation
mob->despawned = false;
// fire any triggered events
for (NPCEvent& event : NPCManager::NPCEvents)
if (event.trigger == ON_KILLED && event.npcType == mob->appearanceData.iNPCType)
event.handler(sock, mob);
auto it = Transport::NPCQueues.find(mob->appearanceData.iNPC_ID);
if (it == Transport::NPCQueues.end() || it->second.empty())
return;
// rewind or empty the movement queue
if (mob->staticPath) {
/*
* This is inelegant, but we wind forward in the path until we find the point that
* corresponds with the Mob's spawn point.
*
* IMPORTANT: The check in TableData::loadPaths() must pass or else this will loop forever.
*/
auto& queue = it->second;
for (auto point = queue.front(); point.x != mob->spawnX || point.y != mob->spawnY; point = queue.front()) {
queue.pop();
queue.push(point);
}
} else {
Transport::NPCQueues.erase(mob->appearanceData.iNPC_ID);
}
}
static void combatBegin(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock);
plr->inCombat = true;
// HACK: make sure the player has the right weapon out for combat
INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, resp);
resp.iPC_ID = plr->iID;
resp.iEquipSlotNum = 0;
resp.EquipSlotItem = plr->Equip[0];
PlayerManager::sendToViewable(sock, (void*)&resp, P_FE2CL_PC_EQUIP_CHANGE, sizeof(sP_FE2CL_PC_EQUIP_CHANGE));
}
static void combatEnd(CNSocket *sock, CNPacketData *data) {
Player *plr = PlayerManager::getPlayer(sock);
plr->inCombat = false;
plr->healCooldown = 4000;
}
static void dotDamageOnOff(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION) != (bool)pkt->iFlag)
plr->iConditionBitFlag ^= CSB_BIT_INFECTION;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt1);
pkt1.eCSTB = ECSB_INFECTION; // eCharStatusTimeBuffID
pkt1.eTBU = 1; // eTimeBuffUpdate
pkt1.eTBT = 0; // eTimeBuffType 1 means nano
pkt1.iConditionBitFlag = plr->iConditionBitFlag;
sock->sendPacket((void*)&pkt1, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
}
static void dealGooDamage(CNSocket *sock, int amount) {
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_DotDamage);
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
Player *plr = PlayerManager::getPlayer(sock);
memset(respbuf, 0, resplen);
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
sSkillResult_DotDamage *dmg = (sSkillResult_DotDamage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
if (plr->iConditionBitFlag & CSB_BIT_PROTECT_INFECTION) {
amount = -2; // -2 is the magic number for "Protected" to appear as the damage number
dmg->bProtected = 1;
// eggs allow protection without nanos
if (plr->activeNano != -1 && (plr->iSelfConditionBitFlag & CSB_BIT_PROTECT_INFECTION))
plr->Nanos[plr->activeNano].iStamina -= 3;
} else {
plr->HP -= amount;
}
if (plr->activeNano != 0) {
dmg->iStamina = plr->Nanos[plr->activeNano].iStamina;
if (plr->Nanos[plr->activeNano].iStamina <= 0) {
dmg->bNanoDeactive = 1;
plr->Nanos[plr->activeNano].iStamina = 0;
Nanos::summonNano(PlayerManager::getSockFromID(plr->iID), -1, true);
}
}
pkt->iID = plr->iID;
pkt->eCT = 1; // player
pkt->iTB_ID = ECSB_INFECTION; // sSkillResult_DotDamage
dmg->eCT = 1;
dmg->iID = plr->iID;
dmg->iDamage = amount;
dmg->iHP = plr->HP;
dmg->iConditionBitFlag = plr->iConditionBitFlag;
sock->sendPacket((void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
}
static void pcAttackChars(CNSocket *sock, CNPacketData *data) {
sP_CL2FE_REQ_PC_ATTACK_CHARs* pkt = (sP_CL2FE_REQ_PC_ATTACK_CHARs*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
// only GMs can use this variant
if (plr->accountLevel > 30)
return;
// Unlike the attack mob packet, attacking players packet has an 8-byte trail (Instead of 4 bytes).
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs), pkt->iTargetCnt, sizeof(int32_t) * 2, data->size)) {
std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ATTACK_CHARs packet size\n";
return;
}
int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs));
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC), pkt->iTargetCnt, sizeof(sAttackResult))) {
std::cout << "[WARN] bad sP_FE2CL_PC_ATTACK_CHARs_SUCC packet size\n";
return;
}
// initialize response struct
size_t resplen = sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC) + pkt->iTargetCnt * sizeof(sAttackResult);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_ATTACK_CHARs_SUCC *resp = (sP_FE2CL_PC_ATTACK_CHARs_SUCC*)respbuf;
sAttackResult *respdata = (sAttackResult*)(respbuf+sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC));
resp->iTargetCnt = pkt->iTargetCnt;
for (int i = 0; i < pkt->iTargetCnt; i++) {
if (pktdata[i*2+1] == 1) { // eCT == 1; attack player
Player *target = nullptr;
for (auto& pair : PlayerManager::players) {
if (pair.second->iID == pktdata[i*2]) {
target = pair.second;
break;
}
}
if (target == nullptr) {
// you shall not pass
std::cout << "[WARN] pcAttackChars: player ID not found" << std::endl;
return;
}
std::pair<int,int> damage;
if (pkt->iTargetCnt > 1)
damage.first = plr->groupDamage;
else
damage.first = plr->pointDamage;
damage = getDamage(damage.first, target->defense, true, (plr->batteryW > 6 + plr->level), -1, -1, 0);
if (plr->batteryW >= 6 + plr->level)
plr->batteryW -= 6 + plr->level;
else
plr->batteryW = 0;
target->HP -= damage.first;
respdata[i].eCT = pktdata[i*2+1];
respdata[i].iID = target->iID;
respdata[i].iDamage = damage.first;
respdata[i].iHP = target->HP;
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
} else { // eCT == 4; attack mob
if (NPCManager::NPCs.find(pktdata[i*2]) == NPCManager::NPCs.end()) {
// not sure how to best handle this
std::cout << "[WARN] pcAttackChars: NPC ID not found" << std::endl;
return;
}
BaseNPC* npc = NPCManager::NPCs[pktdata[i * 2]];
if (npc->type != EntityType::MOB) {
std::cout << "[WARN] pcAttackChars: NPC is not a mob" << std::endl;
return;
}
Mob* mob = (Mob*)npc;
std::pair<int,int> damage;
if (pkt->iTargetCnt > 1)
damage.first = plr->groupDamage;
else
damage.first = plr->pointDamage;
int difficulty = (int)mob->data["m_iNpcLevel"];
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty),
Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
if (plr->batteryW >= 6 + difficulty)
plr->batteryW -= 6 + difficulty;
else
plr->batteryW = 0;
damage.first = hitMob(sock, mob, damage.first);
respdata[i].eCT = pktdata[i*2+1];
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iDamage = damage.first;
respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
}
}
sock->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_CHARs_SUCC, resplen);
// a bit of a hack: these are the same size, so we can reuse the response packet
assert(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC) == sizeof(sP_FE2CL_PC_ATTACK_CHARs));
sP_FE2CL_PC_ATTACK_CHARs *resp1 = (sP_FE2CL_PC_ATTACK_CHARs*)respbuf;
resp1->iPC_ID = plr->iID;
// send to other players
PlayerManager::sendToViewable(sock, (void*)respbuf, P_FE2CL_PC_ATTACK_CHARs, resplen);
}
static int8_t addBullet(Player* plr, bool isGrenade) {
int8_t findId = 0;
if (Bullets.find(plr->iID) != Bullets.end()) {
// find first free id
for (; findId < 127; findId++)
if (Bullets[plr->iID].find(findId) == Bullets[plr->iID].end())
break;
}
// sanity check
if (findId == 127) {
std::cout << "[WARN] Player has more than 127 active projectiles?!" << std::endl;
findId = 0;
}
Bullet toAdd;
toAdd.pointDamage = plr->pointDamage;
toAdd.groupDamage = plr->groupDamage;
// for grenade we need to send 1, for rocket - weapon id
toAdd.bulletType = isGrenade ? 1 : plr->Equip[0].iID;
// temp solution Jade fix plz
toAdd.weaponBoost = plr->batteryW > 0;
if (toAdd.weaponBoost) {
int boostCost = Rand::rand(11) + 20;
plr->batteryW = boostCost > plr->batteryW ? 0 : plr->batteryW - boostCost;
}
Bullets[plr->iID][findId] = toAdd;
return findId;
}
static void grenadeFire(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE* grenade = (sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
INITSTRUCT(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, resp);
resp.iToX = grenade->iToX;
resp.iToY = grenade->iToY;
resp.iToZ = grenade->iToZ;
resp.iBulletID = addBullet(plr, true);
resp.iBatteryW = plr->batteryW;
// 1 means grenade
resp.Bullet.iID = 1;
sock->sendPacket(&resp, P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, sizeof(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC));
// send packet to nearby players
INITSTRUCT(sP_FE2CL_PC_GRENADE_STYLE_FIRE, toOthers);
toOthers.iPC_ID = plr->iID;
toOthers.iToX = resp.iToX;
toOthers.iToY = resp.iToY;
toOthers.iToZ = resp.iToZ;
toOthers.iBulletID = resp.iBulletID;
toOthers.Bullet.iID = resp.Bullet.iID;
PlayerManager::sendToViewable(sock, &toOthers, P_FE2CL_PC_GRENADE_STYLE_FIRE, sizeof(sP_FE2CL_PC_GRENADE_STYLE_FIRE));
}
static void rocketFire(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_ROCKET_STYLE_FIRE* rocket = (sP_CL2FE_REQ_PC_ROCKET_STYLE_FIRE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
// We should be sending back rocket succ packet, but it doesn't work, and this one works
INITSTRUCT(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, resp);
resp.iToX = rocket->iToX;
resp.iToY = rocket->iToY;
// rocket->iToZ is broken, this seems like a good height
resp.iToZ = plr->z + 100;
resp.iBulletID = addBullet(plr, false);
// we have to send it weapon id
resp.Bullet.iID = plr->Equip[0].iID;
resp.iBatteryW = plr->batteryW;
sock->sendPacket(&resp, P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, sizeof(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC));
// send packet to nearby players
INITSTRUCT(sP_FE2CL_PC_GRENADE_STYLE_FIRE, toOthers);
toOthers.iPC_ID = plr->iID;
toOthers.iToX = resp.iToX;
toOthers.iToY = resp.iToY;
toOthers.iToZ = resp.iToZ;
toOthers.iBulletID = resp.iBulletID;
toOthers.Bullet.iID = resp.Bullet.iID;
PlayerManager::sendToViewable(sock, &toOthers, P_FE2CL_PC_GRENADE_STYLE_FIRE, sizeof(sP_FE2CL_PC_GRENADE_STYLE_FIRE));
}
static void projectileHit(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT* pkt = (sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
if (pkt->iTargetCnt == 0) {
Bullets[plr->iID].erase(pkt->iBulletID);
// no targets hit, don't send response
return;
}
// sanity check
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT), pkt->iTargetCnt, sizeof(int64_t), data->size)) {
std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT packet size\n";
return;
}
// client sends us 8 bytes, where last 4 bytes are mob ID,
// we use int64 pointer to move around but have to remember to cast it to int32
int64_t* pktdata = (int64_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT));
/*
* Due to the possibility of multiplication overflow (and regular buffer overflow),
* both incoming and outgoing variable-length packets must be validated, at least if
* the number of trailing structs isn't well known (ie. it's from the client).
*/
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT), pkt->iTargetCnt, sizeof(sAttackResult))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GRENADE_STYLE_HIT packet size\n";
return;
}
// rapid fire anti-cheat
time_t currTime = getTime();
if (currTime - plr->lastShot < plr->fireRate * 80)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing
else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0)
plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // lose suspicion for delayed firing
plr->lastShot = currTime;
if (plr->suspicionRating > 10000) { // kill the socket when the player is too suspicious
sock->kill();
CNShardServer::_killConnection(sock);
return;
}
/*
* initialize response struct
* rocket style hit doesn't work properly, so we're always sending this one
*/
size_t resplen = sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT) + pkt->iTargetCnt * sizeof(sAttackResult);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_PC_GRENADE_STYLE_HIT* resp = (sP_FE2CL_PC_GRENADE_STYLE_HIT*)respbuf;
sAttackResult* respdata = (sAttackResult*)(respbuf + sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT));
resp->iTargetCnt = pkt->iTargetCnt;
if (Bullets.find(plr->iID) == Bullets.end() || Bullets[plr->iID].find(pkt->iBulletID) == Bullets[plr->iID].end()) {
std::cout << "[WARN] projectileHit: bullet not found" << std::endl;
return;
}
Bullet* bullet = &Bullets[plr->iID][pkt->iBulletID];
for (int i = 0; i < pkt->iTargetCnt; i++) {
if (NPCManager::NPCs.find(pktdata[i]) == NPCManager::NPCs.end()) {
// not sure how to best handle this
std::cout << "[WARN] projectileHit: NPC ID not found" << std::endl;
return;
}
BaseNPC* npc = NPCManager::NPCs[pktdata[i]];
if (npc->type != EntityType::MOB) {
std::cout << "[WARN] projectileHit: NPC is not a mob" << std::endl;
return;
}
Mob* mob = (Mob*)npc;
std::pair<int, int> damage;
damage.first = pkt->iTargetCnt > 1 ? bullet->groupDamage : bullet->pointDamage;
int difficulty = (int)mob->data["m_iNpcLevel"];
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, bullet->weaponBoost, Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
damage.first = hitMob(sock, mob, damage.first);
respdata[i].iID = mob->appearanceData.iNPC_ID;
respdata[i].iDamage = damage.first;
respdata[i].iHP = mob->appearanceData.iHP;
respdata[i].iHitFlag = damage.second;
}
resp->iPC_ID = plr->iID;
resp->iBulletID = pkt->iBulletID;
resp->Bullet.iID = bullet->bulletType;
sock->sendPacket((void*)respbuf, P_FE2CL_PC_GRENADE_STYLE_HIT, resplen);
PlayerManager::sendToViewable(sock, (void*)respbuf, P_FE2CL_PC_GRENADE_STYLE_HIT, resplen);
Bullets[plr->iID].erase(resp->iBulletID);
}
static void playerTick(CNServer *serv, time_t currTime) {
static time_t lastHealTime = 0;
for (auto& pair : PlayerManager::players) {
CNSocket *sock = pair.first;
Player *plr = pair.second;
bool transmit = false;
// group ticks
if (plr->groupCnt > 1)
Groups::groupTickInfo(plr);
// do not tick dead players
if (plr->HP <= 0)
continue;
// fm patch/lake damage
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION)
&& !(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
dealGooDamage(sock, PC_MAXHEALTH(plr->level) * 3 / 20);
// heal
if (currTime - lastHealTime >= 4000 && !plr->inCombat && plr->HP < PC_MAXHEALTH(plr->level)) {
if (currTime - lastHealTime - plr->healCooldown >= 4000) {
plr->HP += PC_MAXHEALTH(plr->level) / 5;
if (plr->HP > PC_MAXHEALTH(plr->level))
plr->HP = PC_MAXHEALTH(plr->level);
transmit = true;
} else
plr->healCooldown -= 4000;
}
for (int i = 0; i < 3; i++) {
if (plr->activeNano != 0 && plr->equippedNanos[i] == plr->activeNano) { // spend stamina
plr->Nanos[plr->activeNano].iStamina -= 1 + plr->nanoDrainRate / 5;
if (plr->Nanos[plr->activeNano].iStamina <= 0)
Nanos::summonNano(sock, -1, true); // unsummon nano silently
transmit = true;
} else if (plr->Nanos[plr->equippedNanos[i]].iStamina < 150) { // regain stamina
sNano& nano = plr->Nanos[plr->equippedNanos[i]];
nano.iStamina += 1;
if (nano.iStamina > 150)
nano.iStamina = 150;
transmit = true;
}
}
// check if the player has fallen out of the world
if (plr->z < -30000) {
INITSTRUCT(sP_FE2CL_PC_SUDDEN_DEAD, dead);
dead.iPC_ID = plr->iID;
dead.iDamage = plr->HP;
dead.iHP = plr->HP = 0;
sock->sendPacket((void*)&dead, P_FE2CL_PC_SUDDEN_DEAD, sizeof(sP_FE2CL_PC_SUDDEN_DEAD));
PlayerManager::sendToViewable(sock, (void*)&dead, P_FE2CL_PC_SUDDEN_DEAD, sizeof(sP_FE2CL_PC_SUDDEN_DEAD));
}
if (transmit) {
INITSTRUCT(sP_FE2CL_REP_PC_TICK, pkt);
pkt.iHP = plr->HP;
pkt.iBatteryN = plr->batteryN;
pkt.aNano[0] = plr->Nanos[plr->equippedNanos[0]];
pkt.aNano[1] = plr->Nanos[plr->equippedNanos[1]];
pkt.aNano[2] = plr->Nanos[plr->equippedNanos[2]];
sock->sendPacket((void*)&pkt, P_FE2CL_REP_PC_TICK, sizeof(sP_FE2CL_REP_PC_TICK));
}
}
// if this was a heal tick, update the counter outside of the loop
if (currTime - lastHealTime >= 4000)
lastHealTime = currTime;
}
void Combat::init() {
REGISTER_SHARD_TIMER(playerTick, 2000);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_NPCs, pcAttackNpcs);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_COMBAT_BEGIN, combatBegin);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_COMBAT_END, combatEnd);
REGISTER_SHARD_PACKET(P_CL2FE_DOT_DAMAGE_ONOFF, dotDamageOnOff);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_CHARs, pcAttackChars);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE, grenadeFire);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE, rocketFire);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ROCKET_STYLE_HIT, projectileHit);
}

29
src/Combat.hpp Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "NPC.hpp"
#include "MobAI.hpp"
#include "JSON.hpp"
#include <map>
#include <unordered_map>
#include <queue>
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);
int hitMob(CNSocket *sock, Mob *mob, int damage);
void killMob(CNSocket *sock, Mob *mob);
}

1226
src/CustomCommands.cpp Normal file

File diff suppressed because it is too large Load Diff

9
src/CustomCommands.hpp Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,220 +0,0 @@
#include <string>
#include "Defines.hpp"
#define STRINGIFY(x) PacketMap(x, #x)
/*
* Turns out there isn't better way to do this...
* We'll only support CL2* packets for now, since we only
* need to print those.
*/
struct PacketMap {
int val;
std::string name;
PacketMap(int v, std::string n) : val(v), name(n) {};
};
PacketMap cl2ls_map[] = {
STRINGIFY(P_CL2LS_REQ_LOGIN),
STRINGIFY(P_CL2LS_REQ_CHECK_CHAR_NAME),
STRINGIFY(P_CL2LS_REQ_SAVE_CHAR_NAME),
STRINGIFY(P_CL2LS_REQ_CHAR_CREATE),
STRINGIFY(P_CL2LS_REQ_CHAR_SELECT),
STRINGIFY(P_CL2LS_REQ_CHAR_DELETE),
STRINGIFY(P_CL2LS_REQ_SHARD_SELECT),
STRINGIFY(P_CL2LS_REQ_SHARD_LIST_INFO),
STRINGIFY(P_CL2LS_CHECK_NAME_LIST),
STRINGIFY(P_CL2LS_REQ_SAVE_CHAR_TUTOR),
STRINGIFY(P_CL2LS_REQ_PC_EXIT_DUPLICATE),
STRINGIFY(P_CL2LS_REP_LIVE_CHECK),
STRINGIFY(P_CL2LS_REQ_CHANGE_CHAR_NAME),
STRINGIFY(P_CL2LS_REQ_SERVER_SELECT),
};
PacketMap cl2fe_map[] = {
STRINGIFY(P_CL2FE_REQ_PC_ENTER),
STRINGIFY(P_CL2FE_REQ_PC_EXIT),
STRINGIFY(P_CL2FE_REQ_PC_MOVE),
STRINGIFY(P_CL2FE_REQ_PC_STOP),
STRINGIFY(P_CL2FE_REQ_PC_JUMP),
STRINGIFY(P_CL2FE_REQ_PC_ATTACK_NPCs),
STRINGIFY(P_CL2FE_REQ_SEND_FREECHAT_MESSAGE),
STRINGIFY(P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE),
STRINGIFY(P_CL2FE_REQ_PC_REGEN),
STRINGIFY(P_CL2FE_REQ_ITEM_MOVE),
STRINGIFY(P_CL2FE_REQ_PC_TASK_START),
STRINGIFY(P_CL2FE_REQ_PC_TASK_END),
STRINGIFY(P_CL2FE_REQ_NANO_EQUIP),
STRINGIFY(P_CL2FE_REQ_NANO_UNEQUIP),
STRINGIFY(P_CL2FE_REQ_NANO_ACTIVE),
STRINGIFY(P_CL2FE_REQ_NANO_TUNE),
STRINGIFY(P_CL2FE_REQ_NANO_SKILL_USE),
STRINGIFY(P_CL2FE_REQ_PC_TASK_STOP),
STRINGIFY(P_CL2FE_REQ_PC_TASK_CONTINUE),
STRINGIFY(P_CL2FE_REQ_PC_GOTO),
STRINGIFY(P_CL2FE_REQ_CHARGE_NANO_STAMINA),
STRINGIFY(P_CL2FE_REQ_PC_KILL_QUEST_NPCs),
STRINGIFY(P_CL2FE_REQ_PC_VENDOR_ITEM_BUY),
STRINGIFY(P_CL2FE_REQ_PC_VENDOR_ITEM_SELL),
STRINGIFY(P_CL2FE_REQ_PC_ITEM_DELETE),
STRINGIFY(P_CL2FE_REQ_PC_GIVE_ITEM),
STRINGIFY(P_CL2FE_REQ_PC_ROCKET_STYLE_READY),
STRINGIFY(P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE),
STRINGIFY(P_CL2FE_REQ_PC_ROCKET_STYLE_HIT),
STRINGIFY(P_CL2FE_REQ_PC_GRENADE_STYLE_READY),
STRINGIFY(P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE),
STRINGIFY(P_CL2FE_REQ_PC_GRENADE_STYLE_HIT),
STRINGIFY(P_CL2FE_REQ_PC_NANO_CREATE),
STRINGIFY(P_CL2FE_REQ_PC_TRADE_OFFER),
STRINGIFY(P_CL2FE_REQ_PC_TRADE_OFFER_CANCEL),
STRINGIFY(P_CL2FE_REQ_PC_TRADE_OFFER_ACCEPT),
STRINGIFY(P_CL2FE_REQ_PC_TRADE_OFFER_REFUSAL),
STRINGIFY(P_CL2FE_REQ_PC_TRADE_OFFER_ABORT),
STRINGIFY(P_CL2FE_REQ_PC_TRADE_CONFIRM),
STRINGIFY(P_CL2FE_REQ_PC_TRADE_CONFIRM_CANCEL),
STRINGIFY(P_CL2FE_REQ_PC_TRADE_CONFIRM_ABORT),
STRINGIFY(P_CL2FE_REQ_PC_TRADE_ITEM_REGISTER),
STRINGIFY(P_CL2FE_REQ_PC_TRADE_ITEM_UNREGISTER),
STRINGIFY(P_CL2FE_REQ_PC_TRADE_CASH_REGISTER),
STRINGIFY(P_CL2FE_REQ_PC_TRADE_EMOTES_CHAT),
STRINGIFY(P_CL2FE_REQ_PC_BANK_OPEN),
STRINGIFY(P_CL2FE_REQ_PC_BANK_CLOSE),
STRINGIFY(P_CL2FE_REQ_PC_VENDOR_START),
STRINGIFY(P_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE),
STRINGIFY(P_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY),
STRINGIFY(P_CL2FE_REQ_PC_COMBAT_BEGIN),
STRINGIFY(P_CL2FE_REQ_PC_COMBAT_END),
STRINGIFY(P_CL2FE_REQ_REQUEST_MAKE_BUDDY),
STRINGIFY(P_CL2FE_REQ_ACCEPT_MAKE_BUDDY),
STRINGIFY(P_CL2FE_REQ_SEND_BUDDY_FREECHAT_MESSAGE),
STRINGIFY(P_CL2FE_REQ_SEND_BUDDY_MENUCHAT_MESSAGE),
STRINGIFY(P_CL2FE_REQ_GET_BUDDY_STYLE),
STRINGIFY(P_CL2FE_REQ_SET_BUDDY_BLOCK),
STRINGIFY(P_CL2FE_REQ_REMOVE_BUDDY),
STRINGIFY(P_CL2FE_REQ_GET_BUDDY_STATE),
STRINGIFY(P_CL2FE_REQ_PC_JUMPPAD),
STRINGIFY(P_CL2FE_REQ_PC_LAUNCHER),
STRINGIFY(P_CL2FE_REQ_PC_ZIPLINE),
STRINGIFY(P_CL2FE_REQ_PC_MOVEPLATFORM),
STRINGIFY(P_CL2FE_REQ_PC_SLOPE),
STRINGIFY(P_CL2FE_REQ_PC_STATE_CHANGE),
STRINGIFY(P_CL2FE_REQ_PC_MAP_WARP),
STRINGIFY(P_CL2FE_REQ_PC_GIVE_NANO),
STRINGIFY(P_CL2FE_REQ_NPC_SUMMON),
STRINGIFY(P_CL2FE_REQ_NPC_UNSUMMON),
STRINGIFY(P_CL2FE_REQ_ITEM_CHEST_OPEN),
STRINGIFY(P_CL2FE_REQ_PC_GIVE_NANO_SKILL),
STRINGIFY(P_CL2FE_DOT_DAMAGE_ONOFF),
STRINGIFY(P_CL2FE_REQ_PC_VENDOR_BATTERY_BUY),
STRINGIFY(P_CL2FE_REQ_PC_WARP_USE_NPC),
STRINGIFY(P_CL2FE_REQ_PC_GROUP_INVITE),
STRINGIFY(P_CL2FE_REQ_PC_GROUP_INVITE_REFUSE),
STRINGIFY(P_CL2FE_REQ_PC_GROUP_JOIN),
STRINGIFY(P_CL2FE_REQ_PC_GROUP_LEAVE),
STRINGIFY(P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT),
STRINGIFY(P_CL2FE_REQ_PC_BUDDY_WARP),
STRINGIFY(P_CL2FE_REQ_GET_MEMBER_STYLE),
STRINGIFY(P_CL2FE_REQ_GET_GROUP_STYLE),
STRINGIFY(P_CL2FE_REQ_PC_CHANGE_MENTOR),
STRINGIFY(P_CL2FE_REQ_GET_BUDDY_LOCATION),
STRINGIFY(P_CL2FE_REQ_NPC_GROUP_SUMMON),
STRINGIFY(P_CL2FE_REQ_PC_WARP_TO_PC),
STRINGIFY(P_CL2FE_REQ_EP_RANK_GET_LIST),
STRINGIFY(P_CL2FE_REQ_EP_RANK_GET_DETAIL),
STRINGIFY(P_CL2FE_REQ_EP_RANK_GET_PC_INFO),
STRINGIFY(P_CL2FE_REQ_EP_RACE_START),
STRINGIFY(P_CL2FE_REQ_EP_RACE_END),
STRINGIFY(P_CL2FE_REQ_EP_RACE_CANCEL),
STRINGIFY(P_CL2FE_REQ_EP_GET_RING),
STRINGIFY(P_CL2FE_REQ_IM_CHANGE_SWITCH_STATUS),
STRINGIFY(P_CL2FE_REQ_SHINY_PICKUP),
STRINGIFY(P_CL2FE_REQ_SHINY_SUMMON),
STRINGIFY(P_CL2FE_REQ_PC_MOVETRANSPORTATION),
STRINGIFY(P_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE),
STRINGIFY(P_CL2FE_REQ_SEND_ANY_GROUP_FREECHAT_MESSAGE),
STRINGIFY(P_CL2FE_REQ_BARKER),
STRINGIFY(P_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE),
STRINGIFY(P_CL2FE_REQ_SEND_ANY_GROUP_MENUCHAT_MESSAGE),
STRINGIFY(P_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION),
STRINGIFY(P_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION),
STRINGIFY(P_CL2FE_GM_REQ_PC_SPECIAL_STATE_SWITCH),
STRINGIFY(P_CL2FE_GM_REQ_PC_SET_VALUE),
STRINGIFY(P_CL2FE_GM_REQ_KICK_PLAYER),
STRINGIFY(P_CL2FE_GM_REQ_TARGET_PC_TELEPORT),
STRINGIFY(P_CL2FE_GM_REQ_PC_LOCATION),
STRINGIFY(P_CL2FE_GM_REQ_PC_ANNOUNCE),
STRINGIFY(P_CL2FE_REQ_SET_PC_BLOCK),
STRINGIFY(P_CL2FE_REQ_REGIST_RXCOM),
STRINGIFY(P_CL2FE_GM_REQ_PC_MOTD_REGISTER),
STRINGIFY(P_CL2FE_REQ_ITEM_USE),
STRINGIFY(P_CL2FE_REQ_WARP_USE_RECALL),
STRINGIFY(P_CL2FE_REP_LIVE_CHECK),
STRINGIFY(P_CL2FE_REQ_PC_MISSION_COMPLETE),
STRINGIFY(P_CL2FE_REQ_PC_TASK_COMPLETE),
STRINGIFY(P_CL2FE_REQ_NPC_INTERACTION),
STRINGIFY(P_CL2FE_DOT_HEAL_ONOFF),
STRINGIFY(P_CL2FE_REQ_PC_SPECIAL_STATE_SWITCH),
STRINGIFY(P_CL2FE_REQ_PC_EMAIL_UPDATE_CHECK),
STRINGIFY(P_CL2FE_REQ_PC_READ_EMAIL),
STRINGIFY(P_CL2FE_REQ_PC_RECV_EMAIL_PAGE_LIST),
STRINGIFY(P_CL2FE_REQ_PC_DELETE_EMAIL),
STRINGIFY(P_CL2FE_REQ_PC_SEND_EMAIL),
STRINGIFY(P_CL2FE_REQ_PC_RECV_EMAIL_ITEM),
STRINGIFY(P_CL2FE_REQ_PC_RECV_EMAIL_CANDY),
STRINGIFY(P_CL2FE_GM_REQ_TARGET_PC_SPECIAL_STATE_ONOFF),
STRINGIFY(P_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID),
STRINGIFY(P_CL2FE_REQ_NPC_GROUP_INVITE),
STRINGIFY(P_CL2FE_REQ_NPC_GROUP_KICK),
STRINGIFY(P_CL2FE_REQ_PC_FIRST_USE_FLAG_SET),
STRINGIFY(P_CL2FE_REQ_PC_TRANSPORT_WARP),
STRINGIFY(P_CL2FE_REQ_PC_TIME_TO_GO_WARP),
STRINGIFY(P_CL2FE_REQ_PC_RECV_EMAIL_ITEM_ALL),
STRINGIFY(P_CL2FE_REQ_CHANNEL_INFO),
STRINGIFY(P_CL2FE_REQ_PC_CHANNEL_NUM),
STRINGIFY(P_CL2FE_REQ_PC_WARP_CHANNEL),
STRINGIFY(P_CL2FE_REQ_PC_LOADING_COMPLETE),
STRINGIFY(P_CL2FE_REQ_PC_FIND_NAME_MAKE_BUDDY),
STRINGIFY(P_CL2FE_REQ_PC_FIND_NAME_ACCEPT_BUDDY),
STRINGIFY(P_CL2FE_REQ_PC_ATTACK_CHARs),
STRINGIFY(P_CL2FE_PC_STREETSTALL_REQ_READY),
STRINGIFY(P_CL2FE_PC_STREETSTALL_REQ_CANCEL),
STRINGIFY(P_CL2FE_PC_STREETSTALL_REQ_REGIST_ITEM),
STRINGIFY(P_CL2FE_PC_STREETSTALL_REQ_UNREGIST_ITEM),
STRINGIFY(P_CL2FE_PC_STREETSTALL_REQ_SALE_START),
STRINGIFY(P_CL2FE_PC_STREETSTALL_REQ_ITEM_LIST),
STRINGIFY(P_CL2FE_PC_STREETSTALL_REQ_ITEM_BUY),
STRINGIFY(P_CL2FE_REQ_PC_ITEM_COMBINATION),
STRINGIFY(P_CL2FE_GM_REQ_SET_PC_SKILL),
STRINGIFY(P_CL2FE_REQ_PC_SKILL_ADD),
STRINGIFY(P_CL2FE_REQ_PC_SKILL_DEL),
STRINGIFY(P_CL2FE_REQ_PC_SKILL_USE),
STRINGIFY(P_CL2FE_REQ_PC_ROPE),
STRINGIFY(P_CL2FE_REQ_PC_BELT),
STRINGIFY(P_CL2FE_REQ_PC_VEHICLE_ON),
STRINGIFY(P_CL2FE_REQ_PC_VEHICLE_OFF),
STRINGIFY(P_CL2FE_REQ_PC_REGIST_QUICK_SLOT),
STRINGIFY(P_CL2FE_REQ_PC_DISASSEMBLE_ITEM),
STRINGIFY(P_CL2FE_GM_REQ_REWARD_RATE),
STRINGIFY(P_CL2FE_REQ_PC_ITEM_ENCHANT),
};
std::string Defines::p2str(int type, int val) {
switch (type) {
case CL2LS:
val = val - CL2LS - 1;
if (val > N_CL2LS || val < 0)
break;
return cl2ls_map[val].name;
case CL2FE:
val = val - CL2FE - 1;
if (val > N_CL2FE || val < 0)
break;
return cl2fe_map[val].name;
}
return "UNKNOWN";
}

266
src/Eggs.cpp Normal file
View File

@@ -0,0 +1,266 @@
#include "core/Core.hpp"
#include "Eggs.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include "Groups.hpp"
#include <assert.h>
using namespace Eggs;
/// sock, CBFlag -> until
std::map<std::pair<CNSocket*, int32_t>, time_t> Eggs::EggBuffs;
std::unordered_map<int, EggType> Eggs::EggTypes;
int Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
int bitFlag = Groups::getGroupFlags(otherPlr);
int CBFlag = Nanos::applyBuff(sock, skillId, 1, 3, bitFlag);
size_t resplen;
if (skillId == 183) {
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Damage);
} else if (skillId == 150) {
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Heal_HP);
} else {
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Buff);
}
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
// we know it's only one trailing struct, so we can skip full validation
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
auto skillUse = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
if (skillId == 183) { // damage egg
auto skill = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iDamage = PC_MAXHEALTH(plr->level) * Nanos::SkillTable[skillId].powerIntensity[0] / 1000;
plr->HP -= skill->iDamage;
if (plr->HP < 0)
plr->HP = 0;
skill->iHP = plr->HP;
} else if (skillId == 150) { // heal egg
auto skill = (sSkillResult_Heal_HP*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iHealHP = PC_MAXHEALTH(plr->level) * Nanos::SkillTable[skillId].powerIntensity[0] / 1000;
plr->HP += skill->iHealHP;
if (plr->HP > PC_MAXHEALTH(plr->level))
plr->HP = PC_MAXHEALTH(plr->level);
skill->iHP = plr->HP;
} else { // regular buff egg
auto skill = (sSkillResult_Buff*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iConditionBitFlag = plr->iConditionBitFlag;
}
skillUse->iNPC_ID = eggId;
skillUse->iSkillID = skillId;
skillUse->eST = Nanos::SkillTable[skillId].skillType;
skillUse->iTargetCnt = 1;
sock->sendPacket((void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
if (CBFlag == 0)
return -1;
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, CBFlag);
// save the buff serverside;
// if you get the same buff again, new duration will override the previous one
time_t until = getTime() + (time_t)duration * 1000;
EggBuffs[key] = until;
return 0;
}
static void eggStep(CNServer* serv, time_t currTime) {
// tick buffs
time_t timeStamp = currTime;
auto it = EggBuffs.begin();
while (it != EggBuffs.end()) {
// check remaining time
if (it->second > timeStamp) {
it++;
} else { // if time reached 0
CNSocket* sock = it->first.first;
int32_t CBFlag = it->first.second;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
int groupFlags = Groups::getGroupFlags(otherPlr);
for (auto& pwr : Nanos::NanoPowers) {
if (pwr.bitFlag == CBFlag) { // pick the power with the right flag and unbuff
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp);
resp.eCSTB = pwr.timeBuffID;
resp.eTBU = 2;
resp.eTBT = 3; // for egg buffs
plr->iConditionBitFlag &= ~CBFlag;
resp.iConditionBitFlag = plr->iConditionBitFlag |= groupFlags | plr->iSelfConditionBitFlag;
sock->sendPacket(resp, P_FE2CL_PC_BUFF_UPDATE);
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, resp2); // send a buff timeout to other players
resp2.eCT = 1;
resp2.iID = plr->iID;
resp2.iConditionBitFlag = plr->iConditionBitFlag;
PlayerManager::sendToViewable(sock, resp2, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT);
}
}
// remove buff from the map
it = EggBuffs.erase(it);
}
}
// check dead eggs and eggs in inactive chunks
for (auto npc : NPCManager::NPCs) {
if (npc.second->type != EntityType::EGG)
continue;
auto egg = (Egg*)npc.second;
if (!egg->dead || !Chunking::inPopulatedChunks(&egg->viewableChunks))
continue;
if (egg->deadUntil <= timeStamp) {
// respawn it
egg->dead = false;
egg->deadUntil = 0;
egg->appearanceData.iHP = 400;
Chunking::addEntityToChunks(Chunking::getViewableChunks(egg->chunkPos), {npc.first});
}
}
}
void Eggs::npcDataToEggData(int x, int y, int z, sNPCAppearanceData* npc, sShinyAppearanceData* egg) {
egg->iX = x;
egg->iY = y;
egg->iZ = z;
// client doesn't care about egg->iMapNum
egg->iShinyType = npc->iNPCType;
egg->iShiny_ID = npc->iNPC_ID;
}
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->type != EntityType::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->appearanceData.iNPCType;
if (EggTypes.find(typeId) == EggTypes.end()) {
std::cout << "[WARN] Egg Type " << typeId << " not found!" << std::endl;
return;
}
EggType* type = &EggTypes[typeId];
// buff the player
if (type->effectId != 0)
eggBuffPlayer(sock, type->effectId, eggRef.id, type->duration);
/*
* SHINY_PICKUP_SUCC is only causing a GUI effect in the client
* (buff icon pops up in the bottom of the screen)
* so we don't send it for non-effect
*/
if (type->effectId != 0) {
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_BUFFER_SIZE - 8);
// 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->appearanceData.iHP = 0;
}
}
void Eggs::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SHINY_PICKUP, eggPickup);
REGISTER_SHARD_TIMER(eggStep, 1000);
}

22
src/Eggs.hpp Normal file
View File

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

339
src/Email.cpp Normal file
View File

@@ -0,0 +1,339 @@
#include "Email.hpp"
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "db/Database.hpp"
#include "PlayerManager.hpp"
#include "Items.hpp"
#include "Chat.hpp"
using namespace Email;
std::vector<std::string> Email::dump;
// New email notification
static void emailUpdateCheck(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_PC_NEW_EMAIL, resp);
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;
dump.push_back(logEmail);
}
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);
}

10
src/Email.hpp Normal file
View File

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

120
src/Entities.cpp Normal file
View File

@@ -0,0 +1,120 @@
#include "core/Core.hpp"
#include "Entities.hpp"
#include "Chunking.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "Eggs.hpp"
#include "MobAI.hpp"
#include <type_traits>
static_assert(std::is_standard_layout<EntityRef>::value);
static_assert(std::is_trivially_copyable<EntityRef>::value);
EntityRef::EntityRef(CNSocket *s) {
type = EntityType::PLAYER;
sock = s;
}
EntityRef::EntityRef(int32_t i) {
id = i;
assert(NPCManager::NPCs.find(id) != NPCManager::NPCs.end());
type = NPCManager::NPCs[id]->type;
}
bool EntityRef::isValid() const {
if (type == EntityType::PLAYER)
return PlayerManager::players.find(sock) != PlayerManager::players.end();
return NPCManager::NPCs.find(id) != NPCManager::NPCs.end();
}
Entity *EntityRef::getEntity() const {
assert(isValid());
if (type == EntityType::PLAYER)
return PlayerManager::getPlayer(sock);
return NPCManager::NPCs[id];
}
/*
* Entity coming into view.
*/
void BaseNPC::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
pkt.NPCAppearanceData = appearanceData;
pkt.NPCAppearanceData.iX = x;
pkt.NPCAppearanceData.iY = y;
pkt.NPCAppearanceData.iZ = z;
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 = {
3, appearanceData.iNPC_ID, appearanceData.iNPCType,
x, y, z
};
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_ENTER);
}
void Egg::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_SHINY_ENTER, pkt);
Eggs::npcDataToEggData(x, y, z, &appearanceData, &pkt.ShinyAppearanceData);
sock->sendPacket(pkt, P_FE2CL_SHINY_ENTER);
}
// TODO: this is less effiecient than it was, because of memset()
void Player::enterIntoViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_PC_NEW, pkt);
pkt.PCAppearanceData.iID = iID;
pkt.PCAppearanceData.iHP = HP;
pkt.PCAppearanceData.iLv = level;
pkt.PCAppearanceData.iX = x;
pkt.PCAppearanceData.iY = y;
pkt.PCAppearanceData.iZ = z;
pkt.PCAppearanceData.iAngle = angle;
pkt.PCAppearanceData.PCStyle = PCStyle;
pkt.PCAppearanceData.Nano = Nanos[activeNano];
pkt.PCAppearanceData.iPCState = iPCState;
pkt.PCAppearanceData.iSpecialState = iSpecialState;
memcpy(pkt.PCAppearanceData.ItemEquip, Equip, sizeof(sItemBase) * AEQUIP_COUNT);
sock->sendPacket(pkt, P_FE2CL_PC_NEW);
}
/*
* Entity leaving view.
*/
void BaseNPC::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
pkt.iNPC_ID = appearanceData.iNPC_ID;
sock->sendPacket(pkt, P_FE2CL_NPC_EXIT);
}
void Bus::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_TRANSPORTATION_EXIT, pkt);
pkt.eTT = 3;
pkt.iT_ID = appearanceData.iNPC_ID;
sock->sendPacket(pkt, P_FE2CL_TRANSPORTATION_EXIT);
}
void Egg::disappearFromViewOf(CNSocket *sock) {
INITSTRUCT(sP_FE2CL_SHINY_EXIT, pkt);
pkt.iShinyID = appearanceData.iNPC_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);
}

151
src/Entities.hpp Normal file
View File

@@ -0,0 +1,151 @@
#pragma once
#include "core/Core.hpp"
#include "Chunking.hpp"
#include <stdint.h>
#include <set>
enum class EntityType : uint8_t {
INVALID,
PLAYER,
SIMPLE_NPC,
COMBAT_NPC,
MOB,
EGG,
BUS
};
struct Entity {
EntityType type = EntityType::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 isAlive() { return true; }
// stubs
virtual void enterIntoViewOf(CNSocket *sock) = 0;
virtual void disappearFromViewOf(CNSocket *sock) = 0;
};
struct EntityRef {
EntityType type;
union {
CNSocket *sock;
int32_t id;
};
EntityRef(CNSocket *s);
EntityRef(int32_t i);
bool isValid() const;
Entity *getEntity() const;
bool operator==(const EntityRef& other) const {
if (type != other.type)
return false;
if (type == EntityType::PLAYER)
return sock == other.sock;
return id == other.id;
}
// arbitrary ordering
bool operator<(const EntityRef& other) const {
if (type == other.type) {
if (type == EntityType::PLAYER)
return sock < other.sock;
else
return id < other.id;
}
return type < other.type;
}
};
/*
* Subclasses
*/
class BaseNPC : public Entity {
public:
sNPCAppearanceData appearanceData = {};
bool loopingPath = false;
BaseNPC(int _X, int _Y, int _Z, int angle, uint64_t iID, int t, int id) { // XXX
x = _X;
y = _Y;
z = _Z;
appearanceData.iNPCType = t;
appearanceData.iHP = 400;
appearanceData.iAngle = angle;
appearanceData.iConditionBitFlag = 0;
appearanceData.iBarkerType = 0;
appearanceData.iNPC_ID = id;
instanceID = iID;
};
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
};
struct CombatNPC : public BaseNPC {
int maxHealth = 0;
int spawnX = 0;
int spawnY = 0;
int spawnZ = 0;
int level = 0;
int speed = 300;
void (*_stepAI)(CombatNPC*, time_t) = nullptr;
// XXX
CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP) :
BaseNPC(x, y, z, angle, iID, t, id),
maxHealth(maxHP) {}
virtual void stepAI(time_t currTime) {
if (_stepAI != nullptr)
_stepAI(this, currTime);
}
virtual bool isAlive() override { return appearanceData.iHP > 0; }
};
// Mob is in MobAI.hpp, Player is in Player.hpp
// TODO: decouple from BaseNPC
struct Egg : public BaseNPC {
bool summoned = false;
bool dead = false;
time_t deadUntil;
Egg(int x, int y, int z, uint64_t iID, int t, int32_t id, bool summon)
: BaseNPC(x, y, z, 0, iID, t, id) {
summoned = summon;
type = EntityType::EGG;
}
virtual bool isAlive() override { return !dead; }
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
};
// TODO: decouple from BaseNPC
struct Bus : public BaseNPC {
Bus(int x, int y, int z, int angle, uint64_t iID, int t, int id) :
BaseNPC(x, y, z, angle, iID, t, id) {
type = EntityType::BUS;
loopingPath = true;
}
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
};

View File

@@ -1,25 +0,0 @@
#pragma once
#include "Player.hpp"
#include "CNProtocol.hpp"
#include "CNStructs.hpp"
#include "CNShardServer.hpp"
#include <map>
#include <list>
namespace GroupManager {
void init();
void requestGroup(CNSocket* sock, CNPacketData* data);
void refuseGroup(CNSocket* sock, CNPacketData* data);
void joinGroup(CNSocket* sock, CNPacketData* data);
void leaveGroup(CNSocket* sock, CNPacketData* data);
void chatGroup(CNSocket* sock, CNPacketData* data);
void menuChatGroup(CNSocket* sock, CNPacketData* data);
void sendToGroup(Player* plr, void* buf, uint32_t type, size_t size);
void groupTickInfo(Player* plr);
void groupKickPlayer(Player* plr);
void groupUnbuff(Player* plr);
int getGroupFlags(Player* plr);
}

View File

@@ -1,28 +1,25 @@
#include "CNShardServer.hpp"
#include "CNStructs.hpp"
#include "ChatManager.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "GroupManager.hpp"
#include "NanoManager.hpp"
#include "Groups.hpp"
#include "Nanos.hpp"
#include "Abilities.hpp"
#include <iostream>
#include <chrono>
#include <algorithm>
#include <thread>
void GroupManager::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);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE, chatGroup);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE, menuChatGroup);
}
/*
* 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.
*/
void GroupManager::requestGroup(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_GROUP_INVITE))
return; // malformed packet
using namespace Groups;
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);
@@ -37,7 +34,7 @@ void GroupManager::requestGroup(CNSocket* sock, CNPacketData* data) {
return;
// fail if the group is full or the other player is already in a group
if (plr->groupCnt >= 4 || otherPlr->groupCnt > 1) {
if (plr->groupCnt >= 4 || otherPlr->iIDGroup != otherPlr->iID || otherPlr->groupCnt > 1) {
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;
@@ -50,15 +47,12 @@ void GroupManager::requestGroup(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_PC_GROUP_INVITE, resp);
resp.iHostID = plr->iIDGroup;
resp.iHostID = plr->iID;
otherSock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE, sizeof(sP_FE2CL_PC_GROUP_INVITE));
}
void GroupManager::refuseGroup(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_GROUP_INVITE_REFUSE))
return; // malformed packet
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);
@@ -75,10 +69,7 @@ void GroupManager::refuseGroup(CNSocket* sock, CNPacketData* data) {
otherSock->sendPacket((void*)&resp, P_FE2CL_PC_GROUP_INVITE_REFUSE, sizeof(sP_FE2CL_PC_GROUP_INVITE_REFUSE));
}
void GroupManager::joinGroup(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_GROUP_JOIN))
return; // malformed packet
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);
@@ -144,68 +135,22 @@ void GroupManager::joinGroup(CNSocket* sock, CNPacketData* data) {
// client doesnt read nano data here
if (varPlr != plr) { // apply the new member's buffs to the group and the group's buffs to the new member
if (NanoManager::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
NanoManager::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 1, 1, bitFlag);
if (NanoManager::SkillTable[plr->Nanos[plr->activeNano].iSkillID].targetType == 3)
NanoManager::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 1, 1, bitFlag);
if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 1, 1, bitFlag);
if (Nanos::SkillTable[plr->Nanos[plr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 1, 1, bitFlag);
}
}
sendToGroup(otherPlr, (void*)&respbuf, P_FE2CL_PC_GROUP_JOIN, resplen);
}
void GroupManager::leaveGroup(CNSocket* sock, CNPacketData* data) {
static void leaveGroup(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
groupKickPlayer(plr);
}
void GroupManager::chatGroup(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE))
return; // malformed packet
sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_FREECHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
std::string fullChat = ChatManager::sanitizeText(U16toU8(chat->szFreeChat));
// 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;
sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_FREECHAT_MESSAGE_SUCC));
}
void GroupManager::menuChatGroup(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE))
return; // malformed packet
sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE* chat = (sP_CL2FE_REQ_SEND_ALL_GROUP_MENUCHAT_MESSAGE*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
if (otherPlr == nullptr)
return;
std::string fullChat = ChatManager::sanitizeText(U16toU8(chat->szFreeChat));
// 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;
sendToGroup(otherPlr, (void*)&resp, P_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC, sizeof(sP_FE2CL_REP_SEND_ALL_GROUP_MENUCHAT_MESSAGE_SUCC));
}
void GroupManager::sendToGroup(Player* plr, void* buf, uint32_t type, size_t size) {
void Groups::sendToGroup(Player* plr, void* buf, uint32_t type, size_t size) {
for (int i = 0; i < plr->groupCnt; i++) {
CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[i]);
@@ -221,7 +166,7 @@ void GroupManager::sendToGroup(Player* plr, void* buf, uint32_t type, size_t siz
}
}
void GroupManager::groupTickInfo(Player* plr) {
void Groups::groupTickInfo(Player* plr) {
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GROUP_MEMBER_INFO), plr->groupCnt, sizeof(sPCGroupMemberInfo))) {
std::cout << "[WARN] bad sP_FE2CL_PC_GROUP_JOIN packet size\n";
return;
@@ -267,7 +212,21 @@ void GroupManager::groupTickInfo(Player* plr) {
sendToGroup(plr, (void*)&respbuf, P_FE2CL_PC_GROUP_MEMBER_INFO, resplen);
}
void GroupManager::groupKickPlayer(Player* plr) {
static void groupUnbuff(Player* plr) {
for (int i = 0; i < plr->groupCnt; i++) {
for (int n = 0; n < plr->groupCnt; n++) {
if (i == n)
continue;
Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[n]);
Nanos::applyBuff(sock, otherPlr->Nanos[otherPlr->activeNano].iSkillID, 2, 1, 0);
}
}
}
void Groups::groupKickPlayer(Player* plr) {
// if you are the group leader, destroy your own group and kick everybody
if (plr->iID == plr->iIDGroup) {
groupUnbuff(plr);
@@ -336,10 +295,10 @@ void GroupManager::groupKickPlayer(Player* plr) {
moveDown = 1;
otherPlr->groupIDs[i] = 0;
} else { // remove the leaving member's buffs from the group and remove the group buffs from the leaving member.
if (NanoManager::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
NanoManager::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 2, 1, 0);
if (NanoManager::SkillTable[plr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
NanoManager::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 2, 1, bitFlag);
if (Nanos::SkillTable[varPlr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sock, varPlr->Nanos[varPlr->activeNano].iSkillID, 2, 1, 0);
if (Nanos::SkillTable[plr->Nanos[varPlr->activeNano].iSkillID].targetType == 3)
Nanos::applyBuff(sockTo, plr->Nanos[plr->activeNano].iSkillID, 2, 1, bitFlag);
}
}
@@ -352,21 +311,7 @@ void GroupManager::groupKickPlayer(Player* plr) {
sock->sendPacket((void*)&resp1, P_FE2CL_PC_GROUP_LEAVE_SUCC, sizeof(sP_FE2CL_PC_GROUP_LEAVE_SUCC));
}
void GroupManager::groupUnbuff(Player* plr) {
for (int i = 0; i < plr->groupCnt; i++) {
for (int n = 0; n < plr->groupCnt; n++) {
if (i == n)
continue;
Player* otherPlr = PlayerManager::getPlayerFromID(plr->groupIDs[i]);
CNSocket* sock = PlayerManager::getSockFromID(plr->groupIDs[n]);
NanoManager::applyBuff(sock, otherPlr->Nanos[otherPlr->activeNano].iSkillID, 2, 1, 0);
}
}
}
int GroupManager::getGroupFlags(Player* plr) {
int Groups::getGroupFlags(Player* plr) {
int bitFlag = 0;
for (int i = 0; i < plr->groupCnt; i++) {
@@ -380,3 +325,10 @@ int GroupManager::getGroupFlags(Player* plr) {
return bitFlag;
}
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);
}

17
src/Groups.hpp Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include "Player.hpp"
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include <map>
#include <list>
namespace Groups {
void init();
void sendToGroup(Player* plr, void* buf, uint32_t type, size_t size);
void groupTickInfo(Player* plr);
void groupKickPlayer(Player* plr);
int getGroupFlags(Player* plr);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,73 +0,0 @@
#pragma once
#include "CNShardServer.hpp"
#include "Player.hpp"
struct VendorListing {
int sort, type, iID;
};
struct CrocPotEntry {
int multStats, multLooks;
float base, rd0, rd1, rd2, rd3;
};
struct Crate {
int rarityRatioId;
std::vector<int> itemSets;
};
namespace ItemManager {
enum class SlotType {
EQUIP = 0,
INVENTORY = 1,
BANK = 3
};
struct Item {
bool tradeable, sellable;
int buyPrice, sellPrice, stackSize, level, rarity, pointDamage, groupDamage, defense, gender; // TODO: implement more as needed
};
// 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, std::vector<VendorListing>> VendorTables;
extern std::map<int32_t, CrocPotEntry> CrocPotTable; // level gap -> entry
extern std::map<int32_t, std::vector<int>> RarityRatios;
extern std::map<int32_t, Crate> Crates;
// pair <Itemset, Rarity> -> vector of pointers (map iterators) to records in ItemData (it looks a lot scarier than it is)
extern std::map<std::pair<int32_t, int32_t>,
std::vector<std::map<std::pair<int32_t, int32_t>, Item>::iterator>> CrateItems;
extern std::map<std::string, std::vector<std::pair<int32_t, int32_t>>> CodeItems; // code -> vector of <id, type>
void init();
void itemMoveHandler(CNSocket* sock, CNPacketData* data);
void itemDeleteHandler(CNSocket* sock, CNPacketData* data);
void itemGMGiveHandler(CNSocket* sock, CNPacketData* data);
void itemUseHandler(CNSocket* sock, CNPacketData* data);
// Bank
void itemBankOpenHandler(CNSocket* sock, CNPacketData* data);
void itemTradeOfferHandler(CNSocket* sock, CNPacketData* data);
void itemTradeOfferAcceptHandler(CNSocket* sock, CNPacketData* data);
void itemTradeOfferRefusalHandler(CNSocket* sock, CNPacketData* data);
void itemTradeConfirmHandler(CNSocket* sock, CNPacketData* data);
void itemTradeConfirmCancelHandler(CNSocket* sock, CNPacketData* data);
void itemTradeRegisterItemHandler(CNSocket* sock, CNPacketData* data);
void itemTradeUnregisterItemHandler(CNSocket* sock, CNPacketData* data);
void itemTradeRegisterCashHandler(CNSocket* sock, CNPacketData* data);
void itemTradeChatHandler(CNSocket* sock, CNPacketData* data);
void chestOpenHandler(CNSocket* sock, CNPacketData* data);
// crate opening logic with all helper functions
int getItemSetId(Crate& crate, int crateId);
int getRarity(Crate& crate, int itemSetId);
int getCrateItem(sItemBase& reward, int itemSetId, int rarity, int playerGender);
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
void nanoCapsuleHandler(CNSocket* sock, sP_CL2FE_REQ_ITEM_CHEST_OPEN* chest);
#endif
}

855
src/Items.cpp Normal file
View File

@@ -0,0 +1,855 @@
#include "servers/CNShardServer.hpp"
#include "Items.hpp"
#include "PlayerManager.hpp"
#include "Nanos.hpp"
#include "NPCManager.hpp"
#include "Player.hpp"
#include "Abilities.hpp"
#include "Missions.hpp"
#include "Eggs.hpp"
#include "Rand.hpp"
#include <string.h> // for memset()
#include <assert.h>
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_BUFFER_SIZE - 8);
// 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);
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_BUFFER_SIZE];
memset(respbuf, 0, resplen);
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 = EST_NANOSTIMPAK;
resp->iSkillID = 144;
int value1 = CSB_BIT_STIMPAKSLOT1 << request->iNanoSlot;
int value2 = ECSB_STIMPAKSLOT1 + request->iNanoSlot;
respdata->eCT = 1;
respdata->iID = player->iID;
respdata->iConditionBitFlag = value1;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = value2; // eCharStatusTimeBuffID
pkt.eTBU = 1; // eTimeBuffUpdate
pkt.eTBT = 1; // eTimeBuffType 1 means nano
pkt.iConditionBitFlag = player->iConditionBitFlag |= value1;
sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE);
sock->sendPacket((void*)&respbuf, P_FE2CL_REP_PC_ITEM_USE_SUCC, resplen);
// update inventory serverside
player->Inven[resp->iSlotNum] = resp->RemainItem;
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, value1);
time_t until = getTime() + (time_t)Nanos::SkillTable[144].durationTime[0] * 100;
Eggs::EggBuffs[key] = until;
}
static void itemBankOpenHandler(CNSocket* sock, CNPacketData* data) {
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_BUFFER_SIZE - 8);
// 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_BUFFER_SIZE - 8);
// 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_BUFFER_SIZE - 8);
// 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->iConditionBitFlag & CSB_BIT_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->iConditionBitFlag & CSB_BIT_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->appearanceData.iNPCType) == Items::MobToDropMap.end()) {
std::cout << "[WARN] Mob ID " << mob->appearanceData.iNPCType << " has no drops assigned" << std::endl;
return;
}
// find mob drop id
int mobDropId = Items::MobToDropMap[mob->appearanceData.iNPCType];
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);
}

123
src/Items.hpp Normal file
View File

@@ -0,0 +1,123 @@
#pragma once
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "MobAI.hpp"
#include "Rand.hpp"
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
}

View File

@@ -1,296 +1,38 @@
#include "CNShardServer.hpp"
#include "CNStructs.hpp"
#include "MissionManager.hpp"
#include "servers/CNShardServer.hpp"
#include "Missions.hpp"
#include "PlayerManager.hpp"
#include "NanoManager.hpp"
#include "ItemManager.hpp"
#include "Nanos.hpp"
#include "Items.hpp"
#include "Transport.hpp"
#include "string.h"
std::map<int32_t, Reward*> MissionManager::Rewards;
std::map<int32_t, TaskData*> MissionManager::Tasks;
nlohmann::json MissionManager::AvatarGrowth[37];
using namespace Missions;
void MissionManager::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);
}
std::map<int32_t, Reward*> Missions::Rewards;
std::map<int32_t, TaskData*> Missions::Tasks;
nlohmann::json Missions::AvatarGrowth[37];
bool MissionManager::startTask(Player* plr, int TaskID) {
if (MissionManager::Tasks.find(TaskID) == MissionManager::Tasks.end()) {
std::cout << "[WARN] Player submitted unknown task!?" << std::endl;
return false;
}
TaskData& task = *MissionManager::Tasks[TaskID];
// 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 true;
}
void MissionManager::taskStart(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TASK_START))
return; // malformed packet
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)) {
// TODO: TASK_FAIL?
response.iTaskNum = missionData->iTaskNum;
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
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;
}
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));
// HACK: auto-succeed escort task
if (task["m_iHTaskType"] == 6) {
std::cout << "Skipping escort mission" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, response);
endTask(sock, missionData->iTaskNum);
response.iTaskNum = missionData->iTaskNum;
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_END_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_END_SUCC));
}
// Missions are stored in int64_t array
int row = missionId / 64;
int column = missionId % 64;
player->aQuestFlag[row] |= (1ULL << column);
}
void MissionManager::taskEnd(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TASK_END))
return; // malformed packet
sP_CL2FE_REQ_PC_TASK_END* missionData = (sP_CL2FE_REQ_PC_TASK_END*)data->buf;
// failed timed missions give an iNPC_ID of 0
if (missionData->iNPC_ID == 0) {
TaskData* task = MissionManager::Tasks[missionData->iTaskNum];
if (task->task["m_iSTGrantTimer"] > 0) { // its a timed mission
Player* plr = PlayerManager::getPlayer(sock);
/*
* Enemy killing missions
* this is gross and should be cleaned up later
* once we comb over mission logic more throughly
*/
bool mobsAreKilled = false;
if (task->task["m_iHTaskType"] == 5) {
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) {
MissionManager::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 bool isMissionCompleted(Player* player, int missionId) {
int row = missionId / 64;
int column = missionId % 64;
return player->aQuestFlag[row] & (1ULL << column);
}
bool MissionManager::endTask(CNSocket *sock, int32_t taskNum, int choice) {
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];
// 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
/*
* 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);
// 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 completed non-active mission!?" << std::endl;
}
// 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)
NanoManager::addNano(sock, task["m_iSTNanoID"], 0, true);
// remove current mission
plr->CurrentMissionID = 0;
}
return true;
}
void MissionManager::setMission(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID))
return; // malformed packet
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));
}
void MissionManager::quitMission(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TASK_STOP))
return; // malformed packet
sP_CL2FE_REQ_PC_TASK_STOP* missionData = (sP_CL2FE_REQ_PC_TASK_STOP*)data->buf;
quitTask(sock, missionData->iTaskNum, true);
}
void MissionManager::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
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));
}
if (!manual) {
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));
}
int MissionManager::findQSlot(Player *plr, int id) {
int Missions::findQSlot(Player *plr, int id) {
int i;
// two passes. we mustn't fail to find an existing stack.
@@ -307,7 +49,21 @@ int MissionManager::findQSlot(Player *plr, int id) {
return -1;
}
void MissionManager::dropQuestItem(CNSocket *sock, int task, int count, int id, int mobid) {
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_BUFFER_SIZE);
// we know it's only one trailing struct, so we can skip full validation
@@ -322,7 +78,7 @@ void MissionManager::dropQuestItem(CNSocket *sock, int task, int count, int id,
memset(respbuf, 0, resplen);
// find free quest item slot
int slot = findQSlot(plr, id);
int slot = Missions::findQSlot(plr, id);
if (slot == -1) {
// this should never happen
std::cout << "[WARN] Player has no room for quest item!?" << std::endl;
@@ -361,7 +117,7 @@ void MissionManager::dropQuestItem(CNSocket *sock, int task, int count, int id,
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
}
int MissionManager::giveMissionReward(CNSocket *sock, int task, int choice) {
static int giveMissionReward(CNSocket *sock, int task, int choice=0) {
Reward *reward = Rewards[task];
Player *plr = PlayerManager::getPlayer(sock);
@@ -377,7 +133,7 @@ int MissionManager::giveMissionReward(CNSocket *sock, int task, int choice) {
int slots[4];
for (int i = 0; i < nrewards; i++) {
slots[i] = ItemManager::findFreeSlot(plr);
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);
@@ -410,14 +166,14 @@ int MissionManager::giveMissionReward(CNSocket *sock, int task, int choice) {
plr->money += reward->money;
if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) { // nano boost for taros
int boost = 0;
if (NanoManager::getNanoBoost(plr)) // for gumballs
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
plr->money += reward->money * (5 + boost) / 25;
}
if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) { // nano boost for fm
int boost = 0;
if (NanoManager::getNanoBoost(plr)) // for gumballs
if (Nanos::getNanoBoost(plr)) // for gumballs
boost = 1;
updateFusionMatter(sock, reward->fusionmatter * (30 + boost) / 25);
} else
@@ -441,6 +197,7 @@ int MissionManager::giveMissionReward(CNSocket *sock, int task, 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;
@@ -453,7 +210,306 @@ int MissionManager::giveMissionReward(CNSocket *sock, int task, int choice) {
return 0;
}
void MissionManager::updateFusionMatter(CNSocket* sock, int fusion) {
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"] == 6) {
for (ChunkPos& chunkPos : Chunking::getChunksInMap(plr->instanceID)) { // check all NPCs in the instance
Chunk* chunk = Chunking::chunks[chunkPos];
for (EntityRef ref : chunk->entities) {
if (ref.type != EntityType::PLAYER) {
BaseNPC* npc = (BaseNPC*)ref.getEntity();
NPCPath* path = Transport::findApplicablePath(npc->appearanceData.iNPC_ID, npc->appearanceData.iNPCType, missionData->iTaskNum);
if (path != nullptr) {
Transport::constructPathNPC(npc->appearanceData.iNPC_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;
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"] == 5) {
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;
@@ -493,10 +549,7 @@ void MissionManager::updateFusionMatter(CNSocket* sock, int fusion) {
response.iTaskNum = AvatarGrowth[plr->level]["m_iNanoQuestTaskID"];
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
#else
if (plr->level >= 36)
return;
plr->fusionmatter -= (int)MissionManager::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
plr->level++;
INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, response);
@@ -514,7 +567,7 @@ void MissionManager::updateFusionMatter(CNSocket* sock, int fusion) {
PlayerManager::sendToViewable(sock, (void*)&bcast, P_FE2CL_PC_EVENT, sizeof(sP_FE2CL_PC_EVENT));
}
void MissionManager::mobKilled(CNSocket *sock, int mobid) {
void Missions::mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls) {
Player *plr = PlayerManager::getPlayer(sock);
bool missionmob = false;
@@ -537,12 +590,29 @@ void MissionManager::mobKilled(CNSocket *sock, int mobid) {
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 = rand() % 100 < task["m_iSTItemDropRate"][j];
bool drop = rolls[plr->tasks[i]] % 100 < task["m_iSTItemDropRate"][j];
if (drop) {
// XXX: are CSUItemID and CSTItemID the same?
dropQuestItem(sock, plr->tasks[i], 1, task["m_iCSUItemID"][j], mobid);
/*
* Workaround: The client has a bug where it only sends a TASK_END request
* for the first task of multiple that met their quest item requirements
* at the same time. We deal with this by sending TASK_END response packets
* proactively and then silently ignoring the extra TASK_END requests it
* sends afterwards.
*/
if (isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j])) {
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, end);
end.iTaskNum = plr->tasks[i];
if (!endTask(sock, plr->tasks[i]))
continue;
sock->sendPacket(end, P_FE2CL_REP_PC_TASK_END_SUCC);
}
} else {
// fail to drop (itemID == 0)
dropQuestItem(sock, plr->tasks[i], 1, 0, mobid);
@@ -562,47 +632,28 @@ void MissionManager::mobKilled(CNSocket *sock, int mobid) {
}
}
void MissionManager::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);
}
bool MissionManager::isQuestItemFull(CNSocket* sock, int itemId, int itemCount) {
Player* plr = PlayerManager::getPlayer(sock);
int slot = 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);
}
void MissionManager::failInstancedMissions(CNSocket* sock) {
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 (MissionManager::Tasks.find(taskNum) == MissionManager::Tasks.end())
if (Missions::Tasks.find(taskNum) == Missions::Tasks.end())
continue; // sanity check
TaskData* task = MissionManager::Tasks[taskNum];
TaskData* task = Missions::Tasks[taskNum];
if (task->task["m_iRequireInstanceID"] != 0) { // mission is instanced
int failTaskID = task->task["m_iFOutgoingTask"];
if (failTaskID != 0) {
MissionManager::quitTask(sock, taskNum, false);
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);
}

View File

@@ -1,9 +1,9 @@
#pragma once
#include "CNShardServer.hpp"
#include "servers/CNShardServer.hpp"
#include "Player.hpp"
#include "contrib/JSON.hpp"
#include "JSON.hpp"
struct Reward {
int32_t id;
@@ -35,29 +35,20 @@ struct TaskData {
auto operator[](std::string s) { return task[s]; }
};
namespace MissionManager {
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);
void taskStart(CNSocket* sock, CNPacketData* data);
void taskEnd(CNSocket* sock, CNPacketData* data);
void setMission(CNSocket* sock, CNPacketData* data);
void quitMission(CNSocket* sock, CNPacketData* data);
int findQSlot(Player *plr, int id);
void dropQuestItem(CNSocket *sock, int task, int count, int id, int mobid);
// checks if player doesn't have n/n quest items
bool isQuestItemFull(CNSocket* sock, int itemId, int itemCount);
int giveMissionReward(CNSocket *sock, int task, int choice=0);
void updateFusionMatter(CNSocket* sock, int fusion);
void mobKilled(CNSocket *sock, int mobid);
void mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls);
bool endTask(CNSocket *sock, int32_t taskNum, int choice=0);
void saveMission(Player* player, int missionId);
void quitTask(CNSocket* sock, int32_t taskNum, bool manual);
void failInstancedMissions(CNSocket* sock);

813
src/MobAI.cpp Normal file
View File

@@ -0,0 +1,813 @@
#include "MobAI.hpp"
#include "Player.hpp"
#include "Racing.hpp"
#include "Transport.hpp"
#include "Nanos.hpp"
#include "Combat.hpp"
#include "Abilities.hpp"
#include "Rand.hpp"
#include <cmath>
#include <limits.h>
using namespace MobAI;
bool MobAI::simulateMobs = settings::SIMULATEMOBS;
static void roamingStep(Mob *mob, time_t currTime);
/*
* 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->appearanceData.iConditionBitFlag = 0;
mob->unbuffTimes.clear();
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = mob->appearanceData.iNPC_ID;
pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
void MobAI::followToCombat(Mob *mob) {
if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->type == EntityType::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]]->type != EntityType::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 != MobState::ROAMING) // only roaming mobs should transition to combat
continue;
enterCombat(mob->target, followerMob);
}
if (leadMob->state != MobState::ROAMING)
return;
enterCombat(mob->target, leadMob);
}
}
void MobAI::groupRetreat(Mob *mob) {
if (NPCManager::NPCs.find(mob->groupLeader) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupLeader]->type != EntityType::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]]->type != EntityType::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 != MobState::COMBAT)
continue;
followerMob->target = nullptr;
followerMob->state = MobState::RETREAT;
clearDebuff(followerMob);
}
if (leadMob->state != MobState::COMBAT)
return;
leadMob->target = nullptr;
leadMob->state = MobState::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.type != EntityType::PLAYER)
continue;
CNSocket *s = ref.sock;
Player *plr = PlayerManager::getPlayer(s);
if (plr->HP <= 0 || plr->onMonkey)
continue;
int mobRange = mob->sightRange;
if (plr->iConditionBitFlag & CSB_BIT_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 != MobState::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.
enterCombat(closest, mob);
if (mob->groupLeader != 0)
followToCombat(mob);
return true;
}
return false;
}
static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, int style) {
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_BUFFER_SIZE];
memset(respbuf, 0, resplen);
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->appearanceData.iNPC_ID;
resp->iSkillID = skillID;
resp->iStyle = style;
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 style2 = Nanos::nanoStyle(plr->activeNano);
if (style2 == -1) { // no nano
respdata[i].iHitFlag = 8;
respdata[i].iDamage = Nanos::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
} else if (style == style2) {
respdata[i].iHitFlag = 8; // tie
respdata[i].iDamage = 0;
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina;
} else if (style - style2 == 1 || style2 - style == 2) {
respdata[i].iHitFlag = 4; // 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
std::vector<int> targetData2 = {1, mob->appearanceData.iNPC_ID, 0, 0, 0};
for (auto& pwr : Nanos::NanoPowers)
if (pwr.skillType == EST_DAMAGE)
pwr.handle(sock, targetData2, plr->activeNano, skillID, 0, 200);
} else {
respdata[i].iHitFlag = 16; // lose
respdata[i].iDamage = Nanos::SkillTable[skillID].powerIntensity[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->iConditionBitFlag;
if (plr->HP <= 0) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
if (!aggroCheck(mob, getTime())) {
clearDebuff(mob);
if (mob->groupLeader != 0)
groupRetreat(mob);
}
}
}
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_NPC_SKILL_CORRUPTION_HIT, resplen);
}
static void useAbilities(Mob *mob, time_t currTime) {
/*
* targetData approach
* first integer is the count
* second to fifth integers are IDs, these can be either player iID or mob's iID
* whether the skill targets players or mobs is determined by the skill packet being fired
*/
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<int> targetData = {0, 0, 0, 0, 0};
// 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.type != EntityType::PLAYER)
continue;
CNSocket *s= ref.sock;
Player *plr = PlayerManager::getPlayer(s);
if (plr->HP <= 0)
continue;
int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y);
if (distance < Nanos::SkillTable[skillID].effectArea) {
targetData[0] += 1;
targetData[targetData[0]] = plr->iID;
if (targetData[0] > 3) // make sure not to have more than 4
break;
}
}
}
for (auto& pwr : Combat::MobPowers)
if (pwr.skillType == Nanos::SkillTable[skillID].skillType)
pwr.handle(mob, targetData, skillID, Nanos::SkillTable[skillID].durationTime[0], Nanos::SkillTable[skillID].powerIntensity[0]);
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"];
std::vector<int> targetData = {1, plr->iID, 0, 0, 0};
for (auto& pwr : Combat::MobPowers)
if (pwr.skillType == Nanos::SkillTable[skillID].skillType) {
if (pwr.bitFlag != 0 && (plr->iConditionBitFlag & pwr.bitFlag))
return; // prevent debuffing a player twice
pwr.handle(mob, targetData, skillID, Nanos::SkillTable[skillID].durationTime[0], Nanos::SkillTable[skillID].powerIntensity[0]);
}
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->appearanceData.iNPC_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->appearanceData.iNPC_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::enterCombat(CNSocket *sock, Mob *mob) {
mob->target = sock;
mob->state = MobState::COMBAT;
mob->nextMovement = getTime();
mob->nextAttack = 0;
mob->roamX = mob->x;
mob->roamY = mob->y;
mob->roamZ = mob->z;
int skillID = (int)mob->data["m_iPassiveBuff"]; // cast passive
std::vector<int> targetData = {1, mob->appearanceData.iNPC_ID, 0, 0, 0};
for (auto& pwr : Combat::MobPowers)
if (pwr.skillType == Nanos::SkillTable[skillID].skillType)
pwr.handle(mob, targetData, skillID, Nanos::SkillTable[skillID].durationTime[0], Nanos::SkillTable[skillID].powerIntensity[0]);
for (NPCEvent& event : NPCManager::NPCEvents) // trigger an ON_COMBAT
if (event.trigger == ON_COMBAT && event.npcType == mob->appearanceData.iNPCType)
event.handler(sock, mob);
}
static void drainMobHP(Mob *mob, int amount) {
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
pkt->iID = mob->appearanceData.iNPC_ID;
pkt->eCT = 4; // mob
pkt->iTB_ID = ECSB_BOUNDINGBALL;
drain->eCT = 4;
drain->iID = mob->appearanceData.iNPC_ID;
drain->iDamage = amount;
drain->iHP = mob->appearanceData.iHP -= amount;
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
if (mob->appearanceData.iHP <= 0)
Combat::killMob(mob->target, mob);
}
static void deadStep(Mob *mob, time_t currTime) {
// despawn the mob after a short delay
if (mob->killedTime != 0 && !mob->despawned && currTime - mob->killedTime > 2000) {
mob->despawned = true;
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT));
// if it was summoned, mark it for removal
if (mob->summoned) {
std::cout << "[INFO] Queueing killed summoned mob for removal" << std::endl;
NPCManager::queueNPCRemoval(mob->appearanceData.iNPC_ID);
return;
}
// pre-set spawn coordinates if not marked for removal
mob->x = mob->spawnX;
mob->y = mob->spawnY;
mob->z = mob->spawnZ;
}
// to guide their groupmates, group leaders still need to move despite being dead
if (mob->groupLeader == mob->appearanceData.iNPC_ID)
roamingStep(mob, currTime);
if (mob->killedTime != 0 && currTime - mob->killedTime < mob->regenTime * 100)
return;
std::cout << "respawning mob " << mob->appearanceData.iNPC_ID << " with HP = " << mob->maxHealth << std::endl;
mob->appearanceData.iHP = mob->maxHealth;
mob->state = MobState::ROAMING;
// if mob is a group leader/follower, spawn where the group is.
if (mob->groupLeader != 0) {
if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->type == EntityType::MOB) {
Mob* leaderMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
mob->x = leaderMob->x + mob->offsetX;
mob->y = leaderMob->y + mob->offsetY;
mob->z = leaderMob->z;
} else {
std::cout << "[WARN] deadStep: mob cannot find it's leader!" << std::endl;
}
}
INITSTRUCT(sP_FE2CL_NPC_NEW, pkt);
pkt.NPCAppearanceData = mob->appearanceData;
pkt.NPCAppearanceData.iX = mob->x;
pkt.NPCAppearanceData.iY = mob->y;
pkt.NPCAppearanceData.iZ = mob->z;
// notify all nearby players
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW));
}
static void combatStep(Mob *mob, time_t currTime) {
assert(mob->target != nullptr);
// lose aggro if the player lost connection
if (PlayerManager::players.find(mob->target) == PlayerManager::players.end()) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
if (!aggroCheck(mob, currTime)) {
clearDebuff(mob);
if (mob->groupLeader != 0)
groupRetreat(mob);
}
return;
}
Player *plr = PlayerManager::getPlayer(mob->target);
// lose aggro if the player became invulnerable or died
if (plr->HP <= 0
|| (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
if (!aggroCheck(mob, currTime)) {
clearDebuff(mob);
if (mob->groupLeader != 0)
groupRetreat(mob);
}
return;
}
// drain
if (mob->skillStyle < 0 && (mob->lastDrainTime == 0 || currTime - mob->lastDrainTime >= 1000)
&& mob->appearanceData.iConditionBitFlag & CSB_BIT_BOUNDINGBALL) {
drainMobHP(mob, mob->maxHealth / 20); // lose 5% every second
mob->lastDrainTime = currTime;
}
// if drain killed the mob, return early
if (mob->appearanceData.iHP <= 0)
return;
// unbuffing
std::unordered_map<int32_t, time_t>::iterator it = mob->unbuffTimes.begin();
while (it != mob->unbuffTimes.end()) {
if (currTime >= it->second) {
mob->appearanceData.iConditionBitFlag &= ~it->first;
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = mob->appearanceData.iNPC_ID;
pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag;
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
it = mob->unbuffTimes.erase(it);
} else {
it++;
}
}
// skip attack if stunned or asleep
if (mob->appearanceData.iConditionBitFlag & (CSB_BIT_STUN|CSB_BIT_MEZ)) {
mob->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up.
return;
}
int distance = hypot(plr->x - mob->x, plr->y - mob->y);
int mobRange = (int)mob->data["m_iAtkRange"] + (int)mob->data["m_iRadius"];
if (currTime >= mob->nextAttack) {
if (mob->skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance.
useAbilities(mob, currTime);
if (mob->target == nullptr)
return;
}
int distanceToTravel = INT_MAX;
int speed = mob->speed;
// movement logic: move when out of range but don't move while casting a skill
if (distance > mobRange && mob->skillStyle == -1) {
if (mob->nextMovement != 0 && currTime < mob->nextMovement)
return;
mob->nextMovement = currTime + 400;
if (currTime >= mob->nextAttack)
mob->nextAttack = 0;
// halve movement speed if snared
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_DN_MOVE_SPEED)
speed /= 2;
int targetX = plr->x;
int targetY = plr->y;
if (mob->groupLeader != 0) {
targetX += mob->offsetX*distance/(mob->idleRange + 1);
targetY += mob->offsetY*distance/(mob->idleRange + 1);
}
distanceToTravel = std::min(distance-mobRange+1, speed*2/5);
auto targ = lerp(mob->x, mob->y, targetX, targetY, distanceToTravel);
if (distanceToTravel < speed*2/5 && currTime >= mob->nextAttack)
mob->nextAttack = 0;
NPCManager::updateNPCPosition(mob->appearanceData.iNPC_ID, targ.first, targ.second, mob->z, mob->instanceID, mob->appearanceData.iAngle);
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
pkt.iSpeed = speed;
pkt.iToX = mob->x = targ.first;
pkt.iToY = mob->y = targ.second;
pkt.iToZ = plr->z;
pkt.iMoveStyle = 1;
// notify all nearby players
NPCManager::sendToViewable(mob, &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 < speed*2/5) {
if (mob->nextAttack == 0 || currTime >= mob->nextAttack) {
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
Combat::npcAttackPc(mob, currTime);
}
}
// retreat if the player leaves combat range
int xyDistance = hypot(plr->x - mob->roamX, plr->y - mob->roamY);
distance = hypot(xyDistance, plr->z - mob->roamZ);
if (distance >= mob->data["m_iCombatRange"]) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
clearDebuff(mob);
if (mob->groupLeader != 0)
groupRetreat(mob);
}
}
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);
}
static void roamingStep(Mob *mob, time_t currTime) {
/*
* 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 (mob->state != MobState::DEAD && (mob->nextAttack == 0 || currTime >= mob->nextAttack)) {
mob->nextAttack = currTime + 500;
if (aggroCheck(mob, currTime))
return;
}
// no random roaming if the mob already has a set path
if (mob->staticPath)
return;
if (mob->groupLeader != 0 && mob->groupLeader != mob->appearanceData.iNPC_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 (mob->nextMovement != 0 && currTime < mob->nextMovement)
return;
incNextMovement(mob, currTime);
int xStart = mob->spawnX - mob->idleRange/2;
int yStart = mob->spawnY - mob->idleRange/2;
int speed = mob->speed;
// some mobs don't move (and we mustn't divide/modulus by zero)
if (mob->idleRange == 0 || speed == 0)
return;
int farX, farY, distance;
int minDistance = mob->idleRange / 2;
// pick a random destination
farX = xStart + Rand::rand(mob->idleRange);
farY = yStart + Rand::rand(mob->idleRange);
distance = std::abs(std::max(farX - mob->x, farY - mob->y));
if (distance == 0)
distance += 1; // hack to avoid FPE
// if it's too short a walk, go further in that direction
farX = mob->x + (farX - mob->x) * minDistance / distance;
farY = mob->y + (farY - mob->y) * minDistance / distance;
// but don't got out of bounds
farX = std::clamp(farX, xStart, xStart + mob->idleRange);
farY = std::clamp(farY, yStart, yStart + mob->idleRange);
// halve movement speed if snared
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_DN_MOVE_SPEED)
speed /= 2;
std::queue<Vec3> queue;
Vec3 from = { mob->x, mob->y, mob->z };
Vec3 to = { farX, farY, mob->z };
// add a route to the queue; to be processed in Transport::stepNPCPathing()
Transport::lerp(&queue, from, to, speed);
Transport::NPCQueues[mob->appearanceData.iNPC_ID] = queue;
if (mob->groupLeader != 0 && mob->groupLeader == mob->appearanceData.iNPC_ID) {
// make followers follow this npc.
for (int i = 0; i < 4; i++) {
if (mob->groupMember[i] == 0)
break;
if (NPCManager::NPCs.find(mob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupMember[i]]->type != EntityType::MOB) {
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
continue;
}
std::queue<Vec3> queue2;
Mob* followerMob = (Mob*)NPCManager::NPCs[mob->groupMember[i]];
from = { followerMob->x, followerMob->y, followerMob->z };
to = { farX + followerMob->offsetX, farY + followerMob->offsetY, followerMob->z };
Transport::lerp(&queue2, from, to, speed);
Transport::NPCQueues[followerMob->appearanceData.iNPC_ID] = queue2;
}
}
}
static void retreatStep(Mob *mob, time_t currTime) {
if (mob->nextMovement != 0 && currTime < mob->nextMovement)
return;
mob->nextMovement = currTime + 400;
// distance between spawn point and current location
int distance = hypot(mob->x - mob->roamX, mob->y - mob->roamY);
//if (distance > mob->data["m_iIdleRange"]) {
if (distance > 10) {
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
auto targ = lerp(mob->x, mob->y, mob->roamX, mob->roamY, (int)mob->speed*4/5);
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
pkt.iSpeed = (int)mob->speed * 2;
pkt.iToX = mob->x = targ.first;
pkt.iToY = mob->y = targ.second;
pkt.iToZ = mob->z = mob->spawnZ;
pkt.iMoveStyle = 1;
// notify all nearby players
NPCManager::sendToViewable(mob, &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
mob->state = MobState::ROAMING;
mob->appearanceData.iHP = mob->maxHealth;
mob->killedTime = 0;
mob->nextAttack = 0;
mob->appearanceData.iConditionBitFlag = 0;
// cast a return home heal spell, this is the right way(tm)
std::vector<int> targetData = {1, 0, 0, 0, 0};
for (auto& pwr : Combat::MobPowers)
if (pwr.skillType == Nanos::SkillTable[110].skillType)
pwr.handle(mob, targetData, 110, Nanos::SkillTable[110].durationTime[0], Nanos::SkillTable[110].powerIntensity[0]);
// clear outlying debuffs
clearDebuff(mob);
}
}
void MobAI::step(CombatNPC *npc, time_t currTime) {
assert(npc->type == EntityType::MOB);
auto mob = (Mob*)npc;
if (mob->playersInView < 0)
std::cout << "[WARN] Weird playerview value " << mob->playersInView << std::endl;
// skip mob movement and combat if disabled or not in view
if ((!simulateMobs || mob->playersInView == 0) && mob->state != MobState::DEAD
&& mob->state != MobState::RETREAT)
return;
switch (mob->state) {
case MobState::INACTIVE:
// no-op
break;
case MobState::ROAMING:
roamingStep(mob, currTime);
break;
case MobState::COMBAT:
combatStep(mob, currTime);
break;
case MobState::RETREAT:
retreatStep(mob, currTime);
break;
case MobState::DEAD:
deadStep(mob, currTime);
break;
}
}

107
src/MobAI.hpp Normal file
View File

@@ -0,0 +1,107 @@
#pragma once
#include "core/Core.hpp"
#include "NPCManager.hpp"
enum class MobState {
INACTIVE,
ROAMING,
COMBAT,
RETREAT,
DEAD
};
namespace MobAI {
// needs to be declared before Mob's constructor
void step(CombatNPC*, time_t);
};
struct Mob : public CombatNPC {
// general
MobState state = MobState::INACTIVE;
std::unordered_map<int32_t,time_t> unbuffTimes = {};
// 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] = {};
// for optimizing away AI in empty chunks
int playersInView = 0;
// temporary; until we're sure what's what
nlohmann::json data = {};
Mob(int x, int y, int z, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
: CombatNPC(x, y, z, angle, iID, t, id, d["m_iHP"]),
sightRange(d["m_iSightRange"]) {
state = MobState::ROAMING;
data = d;
speed = data["m_iRunSpeed"];
regenTime = data["m_iRegenTime"];
idleRange = (int)data["m_iIdleRange"];
level = data["m_iNpcLevel"];
roamX = spawnX = x;
roamY = spawnY = y;
roamZ = spawnZ = z;
offsetX = 0;
offsetY = 0;
appearanceData.iConditionBitFlag = 0;
// NOTE: there appear to be discrepancies in the dump
appearanceData.iHP = maxHealth;
type = EntityType::MOB;
_stepAI = MobAI::step;
}
// 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() {}
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);
void enterCombat(CNSocket *sock, Mob *mob);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,192 +0,0 @@
#pragma once
#include "CNProtocol.hpp"
#include "CNShared.hpp"
#include "CNShardServer.hpp"
#include "NPC.hpp"
#include "contrib/JSON.hpp"
#include <map>
#include <unordered_map>
#include <queue>
enum class MobState {
INACTIVE,
ROAMING,
COMBAT,
RETREAT,
DEAD
};
struct Mob : public BaseNPC {
// general
MobState state;
int maxHealth;
int spawnX;
int spawnY;
int spawnZ;
int level;
std::unordered_map<int32_t,time_t> unbuffTimes;
// dead
time_t killedTime = 0;
time_t regenTime;
bool summoned = false;
bool despawned = false; // for the sake of death animations
// roaming
int idleRange;
const int sightRange;
time_t nextMovement = 0;
bool staticPath = false;
int roamX, roamY, roamZ;
// 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, hitY, hitZ; // for use in ability targeting
// drop
int dropType;
// group
int groupLeader = 0;
int offsetX, offsetY;
int groupMember[4] = {0, 0, 0, 0};
// temporary; until we're sure what's what
nlohmann::json data;
Mob(int x, int y, int z, int angle, uint64_t iID, int type, nlohmann::json d, int32_t id)
: BaseNPC(x, y, z, angle, iID, type, id),
maxHealth(d["m_iHP"]),
sightRange(d["m_iSightRange"]) {
state = MobState::ROAMING;
data = d;
regenTime = data["m_iRegenTime"];
idleRange = (int)data["m_iIdleRange"];
dropType = data["m_iDropType"];
level = data["m_iNpcLevel"];
roamX = spawnX = appearanceData.iX;
roamY = spawnY = appearanceData.iY;
roamZ = spawnZ = appearanceData.iZ;
offsetX = 0;
offsetY = 0;
appearanceData.iConditionBitFlag = 0;
// NOTE: there appear to be discrepancies in the dump
appearanceData.iHP = maxHealth;
npcClass = NPC_MOB;
}
// constructor for /summon
Mob(int x, int y, int z, uint64_t iID, int type, nlohmann::json d, int32_t id)
: Mob(x, y, z, 0, iID, type, d, id) {
summoned = true; // will be despawned and deallocated when killed
}
~Mob() {}
auto operator[](std::string s) {
return data[s];
}
};
struct MobDropChance {
int dropChance;
std::vector<int> cratesRatio;
};
struct MobDrop {
std::vector<int> crateIDs;
int dropChanceType;
int taros;
int fm;
int boosts;
};
struct Bullet {
int pointDamage;
int groupDamage;
bool weaponBoost;
int bulletType;
};
typedef void (*MobPowerHandler)(Mob*, std::vector<int>, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t);
struct MobPower {
int16_t skillType;
int32_t bitFlag;
int16_t timeBuffID;
MobPowerHandler handler;
MobPower(int16_t s, int32_t b, int16_t t, MobPowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {}
void handle(Mob *mob, std::vector<int> targetData, int16_t skillID, int16_t duration, int16_t amount) {
if (handler == nullptr)
return;
handler(mob, targetData, skillID, duration, amount, skillType, bitFlag, timeBuffID);
}
};
namespace MobManager {
extern std::map<int32_t, Mob*> Mobs;
extern std::queue<int32_t> RemovalQueue;
extern std::map<int32_t, MobDropChance> MobDropChances;
extern std::map<int32_t, MobDrop> MobDrops;
extern std::map<int32_t, std::map<int8_t, Bullet>> Bullets;
extern bool simulateMobs;
extern std::vector<MobPower> MobPowers;
void init();
void step(CNServer*, time_t);
void playerTick(CNServer*, time_t);
void deadStep(Mob*, time_t);
void combatStep(Mob*, time_t);
void retreatStep(Mob*, time_t);
void roamingStep(Mob*, time_t);
void pcAttackNpcs(CNSocket *sock, CNPacketData *data);
void combatBegin(CNSocket *sock, CNPacketData *data);
void combatEnd(CNSocket *sock, CNPacketData *data);
void dotDamageOnOff(CNSocket *sock, CNPacketData *data);
void dealGooDamage(CNSocket *sock, int amount);
void npcAttackPc(Mob *mob, time_t currTime);
int hitMob(CNSocket *sock, Mob *mob, int damage);
void killMob(CNSocket *sock, Mob *mob);
void giveReward(CNSocket *sock, Mob *mob);
void getReward(sItemBase *reward, MobDrop *drop, MobDropChance *chance);
void giveEventReward(CNSocket* sock, Player* player);
std::pair<int,int> lerp(int, int, int, int, int);
std::pair<int,int> getDamage(int, int, bool, bool, int, int, int);
void pcAttackChars(CNSocket *sock, CNPacketData *data);
void drainMobHP(Mob *mob, int amount);
void incNextMovement(Mob *mob, time_t currTime=0);
bool aggroCheck(Mob *mob, time_t currTime);
void clearDebuff(Mob *mob);
void grenadeFire(CNSocket* sock, CNPacketData* data);
void rocketFire(CNSocket* sock, CNPacketData* data);
void projectileHit(CNSocket* sock, CNPacketData* data);
/// returns bullet id
int8_t addBullet(Player* plr, bool isGrenade);
void followToCombat(Mob *mob);
void useAbilities(Mob *mob, time_t currTime);
void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, int style);
}

View File

@@ -1,39 +1,4 @@
#pragma once
#include "CNStructs.hpp"
#include "ChunkManager.hpp"
class BaseNPC {
public:
sNPCAppearanceData appearanceData;
NPCClass npcClass;
uint64_t instanceID;
ChunkPos chunkPos;
std::set<Chunk*>* viewableChunks;
int playersInView;
BaseNPC() {};
BaseNPC(int x, int y, int z, int angle, uint64_t iID, int type, int id) {
appearanceData.iX = x;
appearanceData.iY = y;
appearanceData.iZ = z;
appearanceData.iNPCType = type;
appearanceData.iHP = 400;
appearanceData.iAngle = angle;
appearanceData.iConditionBitFlag = 0;
appearanceData.iBarkerType = 0;
appearanceData.iNPC_ID = id;
npcClass = NPCClass::NPC_BASE;
instanceID = iID;
chunkPos = std::make_tuple(0, 0, 0);
viewableChunks = new std::set<Chunk*>();
playersInView = 0;
};
BaseNPC(int x, int y, int z, int angle, uint64_t iID, int type, int id, NPCClass classType) : BaseNPC(x, y, z, angle, iID, type, id) {
npcClass = classType;
}
};
#include "Chunking.hpp"
#include "Entities.hpp"

View File

@@ -1,13 +1,16 @@
#include "NPCManager.hpp"
#include "ItemManager.hpp"
#include "Items.hpp"
#include "settings.hpp"
#include "MobManager.hpp"
#include "MissionManager.hpp"
#include "ChunkManager.hpp"
#include "NanoManager.hpp"
#include "Combat.hpp"
#include "Missions.hpp"
#include "Chunking.hpp"
#include "Nanos.hpp"
#include "TableData.hpp"
#include "ChatManager.hpp"
#include "GroupManager.hpp"
#include "Groups.hpp"
#include "Racing.hpp"
#include "Vendors.hpp"
#include "Abilities.hpp"
#include "Rand.hpp"
#include <cmath>
#include <algorithm>
@@ -17,18 +20,16 @@
#include <assert.h>
#include <limits.h>
#include "contrib/JSON.hpp"
#include "JSON.hpp"
std::map<int32_t, BaseNPC*> NPCManager::NPCs;
using namespace NPCManager;
std::unordered_map<int32_t, BaseNPC*> NPCManager::NPCs;
std::map<int32_t, WarpLocation> NPCManager::Warps;
std::vector<WarpLocation> NPCManager::RespawnPoints;
/// sock, CBFlag -> until
std::map<std::pair<CNSocket*, int32_t>, time_t> NPCManager::EggBuffs;
std::unordered_map<int, EggType> NPCManager::EggTypes;
std::unordered_map<int, Egg*> NPCManager::Eggs;
nlohmann::json NPCManager::NPCData;
static std::queue<int32_t> RemovalQueue;
/*
* Initialized at the end of TableData::init().
@@ -37,24 +38,6 @@ nlohmann::json NPCManager::NPCData;
*/
int32_t NPCManager::nextId;
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_PACKET(P_CL2FE_REQ_PC_VENDOR_START, npcVendorStart);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE, npcVendorTable);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_BUY, npcVendorBuy);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_SELL, npcVendorSell);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY, npcVendorBuyback);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VENDOR_BATTERY_BUY, npcVendorBuyBattery);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ITEM_COMBINATION, npcCombineItems);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_SHINY_PICKUP, eggPickup);
REGISTER_SHARD_TIMER(eggStep, 1000);
}
void NPCManager::destroyNPC(int32_t id) {
// sanity check
if (NPCs.find(id) == NPCs.end()) {
@@ -65,27 +48,19 @@ void NPCManager::destroyNPC(int32_t id) {
BaseNPC* entity = NPCs[id];
// sanity check
if (!ChunkManager::chunkExists(entity->chunkPos)) {
if (!Chunking::chunkExists(entity->chunkPos)) {
std::cout << "chunk not found!" << std::endl;
return;
}
// remove NPC from the chunk
ChunkManager::untrackNPC(entity->chunkPos, id);
EntityRef ref = {id};
Chunking::untrackEntity(entity->chunkPos, ref);
// remove from viewable chunks
ChunkManager::removeNPCFromChunks(ChunkManager::getViewableChunks(entity->chunkPos), id);
// remove from mob manager
if (MobManager::Mobs.find(id) != MobManager::Mobs.end())
MobManager::Mobs.erase(id);
// remove from eggs
if (Eggs.find(id) != Eggs.end())
Eggs.erase(id);
Chunking::removeEntityFromChunks(Chunking::getViewableChunks(entity->chunkPos), ref);
// finally, remove it from the map and free it
delete entity->viewableChunks;
NPCs.erase(id);
delete entity;
}
@@ -94,351 +69,31 @@ void NPCManager::updateNPCPosition(int32_t id, int X, int Y, int Z, uint64_t I,
BaseNPC* npc = NPCs[id];
npc->appearanceData.iAngle = angle;
ChunkPos oldChunk = npc->chunkPos;
ChunkPos newChunk = ChunkManager::chunkPosAt(X, Y, I);
npc->appearanceData.iX = X;
npc->appearanceData.iY = Y;
npc->appearanceData.iZ = Z;
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
ChunkManager::updateNPCChunk(id, oldChunk, newChunk);
Chunking::updateEntityChunk({id}, oldChunk, newChunk);
}
void NPCManager::sendToViewable(BaseNPC *npc, void *buf, uint32_t type, size_t size) {
for (auto it = npc->viewableChunks->begin(); it != npc->viewableChunks->end(); it++) {
for (auto it = npc->viewableChunks.begin(); it != npc->viewableChunks.end(); it++) {
Chunk* chunk = *it;
for (CNSocket *s : chunk->players) {
s->sendPacket(buf, type, size);
for (const EntityRef& ref : chunk->entities) {
if (ref.type == EntityType::PLAYER)
ref.sock->sendPacket(buf, type, size);
}
}
}
void NPCManager::npcVendorBuy(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_VENDOR_ITEM_BUY))
return; // malformed packet
sP_CL2FE_REQ_PC_VENDOR_ITEM_BUY* req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_BUY*)data->buf;
Player *plr = PlayerManager::getPlayer(sock);
ItemManager::Item* item = ItemManager::getItemData(req->Item.iID, req->Item.iType);
if (item == nullptr) {
std::cout << "[WARN] Item id " << req->Item.iID << " with type " << req->Item.iType << " not found (buy)" << std::endl;
// NOTE: VENDOR_ITEM_BUY_FAIL is not actually handled client-side.
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, failResp);
failResp.iErrorCode = 0;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL));
return;
}
int itemCost = item->buyPrice * (item->stackSize > 1 ? req->Item.iOpt : 1);
int slot = ItemManager::findFreeSlot(plr);
if (itemCost > plr->money || slot == -1) {
// NOTE: VENDOR_ITEM_BUY_FAIL is not actually handled client-side.
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, failResp);
failResp.iErrorCode = 0;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_FAIL));
return;
}
// if vehicle
if (req->Item.iType == 10)
// set time limit: current time + 7days
req->Item.iTimeLimit = getTimestamp() + 604800;
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((void*)&resp, P_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_BUY_SUCC));
}
void NPCManager::npcVendorSell(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_VENDOR_ITEM_SELL))
return; // malformed packet
sP_CL2FE_REQ_PC_VENDOR_ITEM_SELL* req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_SELL*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
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;
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL, failResp);
failResp.iErrorCode = 0;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL));
return;
}
sItemBase* item = &plr->Inven[req->iInvenSlotNum];
ItemManager::Item* itemData = ItemManager::getItemData(item->iID, item->iType);
if (itemData == nullptr || !itemData->sellable) { // sanity + sellable check
std::cout << "[WARN] Item id " << item->iID << " with type " << item->iType << " not found (sell)" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL, failResp);
failResp.iErrorCode = 0;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_FAIL, sizeof(sP_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);
int sellValue = itemData->sellPrice * req->iItemCnt;
// increment taros
plr->money = plr->money + sellValue;
// modify item
if (plr->Inven[req->iInvenSlotNum].iOpt - req->iItemCnt > 0) { // selling part of a stack
item->iOpt -= req->iItemCnt;
original.iOpt = req->iItemCnt;
} else { // selling entire slot
item->iID = 0;
item->iOpt = 0;
item->iType = 0;
item->iTimeLimit = 0;
}
// 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((void*)&resp, P_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_SELL_SUCC));
}
void NPCManager::npcVendorBuyback(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY))
return; // malformed packet
sP_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY* req = (sP_CL2FE_REQ_PC_VENDOR_ITEM_RESTORE_BUY*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
ItemManager::Item* item = ItemManager::getItemData(req->Item.iID, req->Item.iType);
if (item == nullptr) {
std::cout << "[WARN] Item id " << req->Item.iID << " with type " << req->Item.iType << " not found (rebuy)" << std::endl;
// NOTE: VENDOR_ITEM_BUY_FAIL is not actually handled client-side.
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL, failResp);
failResp.iErrorCode = 0;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL));
return;
}
// sell price is used on rebuy. ternary identifies stacked items
int itemCost = item->sellPrice * (item->stackSize > 1 ? req->Item.iOpt : 1);
int slot = ItemManager::findFreeSlot(plr);
if (itemCost > plr->money || slot == -1) {
// NOTE: VENDOR_ITEM_BUY_FAIL is not actually handled client-side.
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL, failResp);
failResp.iErrorCode = 0;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_FAIL, sizeof(sP_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] = req->Item;
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, resp);
// response parameters
resp.iCandy = plr->money;
resp.iInvenSlotNum = slot;
resp.Item = req->Item;
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC, sizeof(sP_FE2CL_REP_PC_VENDOR_ITEM_RESTORE_BUY_SUCC));
}
void NPCManager::npcVendorTable(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE))
return; // malformed packet
sP_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE* req = (sP_CL2FE_REQ_PC_VENDOR_TABLE_UPDATE*)data->buf;
if (req->iVendorID != req->iNPC_ID || ItemManager::VendorTables.find(req->iVendorID) == ItemManager::VendorTables.end())
return;
std::vector<VendorListing> listings = ItemManager::VendorTables[req->iVendorID];
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC, resp);
for (int i = 0; i < (int)listings.size() && i < 20; i++) { // 20 is the max
sItemBase base;
base.iID = listings[i].iID;
base.iOpt = 0;
base.iTimeLimit = 0;
base.iType = listings[i].type;
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((void*)&resp, P_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC, sizeof(sP_FE2CL_REP_PC_VENDOR_TABLE_UPDATE_SUCC));
}
void NPCManager::npcVendorStart(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_VENDOR_START))
return; // malformed packet
sP_CL2FE_REQ_PC_VENDOR_START* 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((void*)&resp, P_FE2CL_REP_PC_VENDOR_START_SUCC, sizeof(sP_FE2CL_REP_PC_VENDOR_START_SUCC));
}
void NPCManager::npcVendorBuyBattery(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_VENDOR_BATTERY_BUY))
return; // malformed packet
sP_CL2FE_REQ_PC_VENDOR_BATTERY_BUY* 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) { // sanity check
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, failResp);
failResp.iErrorCode = 0;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, sizeof(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL));
}
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((void*)&resp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, sizeof(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC));
}
void NPCManager::npcCombineItems(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_ITEM_COMBINATION))
return; // malformed packet
sP_CL2FE_REQ_PC_ITEM_COMBINATION* req = (sP_CL2FE_REQ_PC_ITEM_COMBINATION*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
if (req->iCostumeItemSlot < 0 || req->iCostumeItemSlot >= AINVEN_COUNT || req->iStatItemSlot < 0 || req->iStatItemSlot >= AINVEN_COUNT) { // sanity check 1
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_FAIL, failResp);
failResp.iCostumeItemSlot = req->iCostumeItemSlot;
failResp.iStatItemSlot = req->iStatItemSlot;
failResp.iErrorCode = 0;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL, sizeof(sP_FE2CL_REP_PC_ITEM_COMBINATION_FAIL));
std::cout << "[WARN] Inventory slot(s) out of range (" << req->iStatItemSlot << " and " << req->iCostumeItemSlot << ")" << std::endl;
return;
}
sItemBase* itemStats = &plr->Inven[req->iStatItemSlot];
sItemBase* itemLooks = &plr->Inven[req->iCostumeItemSlot];
ItemManager::Item* itemStatsDat = ItemManager::getItemData(itemStats->iID, itemStats->iType);
ItemManager::Item* itemLooksDat = ItemManager::getItemData(itemLooks->iID, itemLooks->iType);
if (itemStatsDat == nullptr || itemLooksDat == nullptr
|| ItemManager::CrocPotTable.find(abs(itemStatsDat->level - itemLooksDat->level)) == ItemManager::CrocPotTable.end()) { // sanity check 2
INITSTRUCT(sP_FE2CL_REP_PC_ITEM_COMBINATION_FAIL, failResp);
failResp.iCostumeItemSlot = req->iCostumeItemSlot;
failResp.iStatItemSlot = req->iStatItemSlot;
failResp.iErrorCode = 0;
std::cout << "[WARN] Either item ids or croc pot value set not found" << std::endl;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_ITEM_COMBINATION_FAIL, sizeof(sP_FE2CL_REP_PC_ITEM_COMBINATION_FAIL));
return;
}
CrocPotEntry* recipe = &ItemManager::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() * 1.0f / RAND_MAX) * 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((void*)&resp, P_FE2CL_REP_PC_ITEM_COMBINATION_SUCC, sizeof(sP_FE2CL_REP_PC_ITEM_COMBINATION_SUCC));
}
void NPCManager::npcBarkHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_BARKER))
return; // malformed packet
static void npcBarkHandler(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_BARKER* req = (sP_CL2FE_REQ_BARKER*)data->buf;
// get bark IDs from task data
TaskData* td = MissionManager::Tasks[req->iMissionTaskID];
TaskData* td = Missions::Tasks[req->iMissionTaskID];
std::vector<int> barks;
for (int i = 0; i < 4; i++) {
if (td->task["m_iHBarkerTextID"][i] != 0) // non-zeroes only
@@ -450,14 +105,11 @@ void NPCManager::npcBarkHandler(CNSocket* sock, CNPacketData* data) {
INITSTRUCT(sP_FE2CL_REP_BARKER, resp);
resp.iNPC_ID = req->iNPC_ID;
resp.iMissionStringID = barks[rand() % barks.size()];
sock->sendPacket((void*)&resp, P_FE2CL_REP_BARKER, sizeof(sP_FE2CL_REP_BARKER));
resp.iMissionStringID = barks[Rand::rand(barks.size())];
sock->sendPacket(resp, P_FE2CL_REP_BARKER);
}
void NPCManager::npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_NPC_UNSUMMON))
return; // malformed packet
static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
Player* plr = PlayerManager::getPlayer(sock);
if (plr->accountLevel > 30)
@@ -472,14 +124,13 @@ BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type,
uint64_t inst = baseInstance ? MAPNUM(instance) : instance;
#define EXTRA_HEIGHT 0
assert(nextId < INT32_MAX);
int id = nextId++;
//assert(nextId < INT32_MAX);
int id = nextId--;
int team = NPCData[type]["m_iTeam"];
BaseNPC *npc = nullptr;
if (team == 2) {
npc = new Mob(x, y, z + EXTRA_HEIGHT, inst, type, NPCData[type], id);
MobManager::Mobs[id] = (Mob*)npc;
// re-enable respawning, if desired
((Mob*)npc)->summoned = !respawn;
@@ -491,17 +142,14 @@ BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type,
return npc;
}
void NPCManager::npcSummonHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_NPC_SUMMON))
return; // malformed packet
sP_CL2FE_REQ_NPC_SUMMON* req = (sP_CL2FE_REQ_NPC_SUMMON*)data->buf;
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)
if (plr->accountLevel > 30 || req->iNPCType > limit || req->iNPCCnt > 100)
return;
for (int i = 0; i < req->iNPCCnt; i++) {
@@ -510,27 +158,27 @@ void NPCManager::npcSummonHandler(CNSocket* sock, CNPacketData* data) {
}
}
void NPCManager::npcWarpHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_WARP_USE_NPC))
return; // malformed packet
sP_CL2FE_REQ_PC_WARP_USE_NPC* warpNpc = (sP_CL2FE_REQ_PC_WARP_USE_NPC*)data->buf;
handleWarp(sock, warpNpc->iWarpID);
}
void NPCManager::npcWarpTimeMachine(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_TIME_TO_GO_WARP))
return; // malformed packet
// this is just a warp request
handleWarp(sock, 28);
}
void NPCManager::handleWarp(CNSocket* sock, int32_t warpId) {
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;
@@ -538,7 +186,7 @@ void NPCManager::handleWarp(CNSocket* sock, int32_t warpId) {
// if warp requires you to be on a mission, it's gotta be a unique instance
if (Warps[warpId].limitTaskID != 0 || instanceID == 14) { // 14 is a special case for the Time Lab
instanceID += ((uint64_t)plr->iIDGroup << 32); // upper 32 bits are leader ID
ChunkManager::createInstance(instanceID);
Chunking::createInstance(instanceID);
// save Lair entrance coords as a pseudo-Resurrect 'Em
plr->recallX = Warps[warpId].x;
@@ -559,6 +207,28 @@ void NPCManager::handleWarp(CNSocket* sock, int32_t warpId) {
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);
}
}
@@ -571,14 +241,32 @@ void NPCManager::handleWarp(CNSocket* sock, int32_t warpId) {
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;
MissionManager::failInstancedMissions(sock); // fail any instanced missions
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC, sizeof(sP_FE2CL_REP_PC_WARP_USE_NPC_SUCC));
ChunkManager::updatePlayerChunk(sock, plr->chunkPos, std::make_tuple(0, 0, 0)); // force player to reload chunks
PlayerManager::updatePlayerPosition(sock, resp.iX, resp.iY, resp.iZ, INSTANCE_OVERWORLD, plr->angle);
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
*/
@@ -587,10 +275,13 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
int lastDist = INT_MAX;
for (auto c = chunks->begin(); c != chunks->end(); c++) { // haha get it
Chunk* chunk = *c;
for (auto _npc = chunk->NPCs.begin(); _npc != chunk->NPCs.end(); _npc++) {
BaseNPC* npcTemp = NPCs[*_npc];
int distXY = std::hypot(X - npcTemp->appearanceData.iX, Y - npcTemp->appearanceData.iY);
int dist = std::hypot(distXY, Z - npcTemp->appearanceData.iZ);
for (auto ent = chunk->entities.begin(); ent != chunk->entities.end(); ent++) {
if (ent->type == EntityType::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;
@@ -600,247 +291,7 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
return npc;
}
int NPCManager::eggBuffPlayer(CNSocket* sock, int skillId, int eggId) {
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
int bitFlag = GroupManager::getGroupFlags(otherPlr);
int CBFlag = NanoManager::applyBuff(sock, skillId, 1, 3, bitFlag);
size_t resplen;
if (skillId == 183) {
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Damage);
} else if (skillId == 150) {
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Heal_HP);
} else {
resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + sizeof(sSkillResult_Buff);
}
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
// we know it's only one trailing struct, so we can skip full validation
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
sP_FE2CL_NPC_SKILL_HIT* skillUse = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
if (skillId == 183) { // damage egg
sSkillResult_Damage* skill = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iDamage = PC_MAXHEALTH(plr->level) * NanoManager::SkillTable[skillId].powerIntensity[0] / 1000;
plr->HP -= skill->iDamage;
if (plr->HP < 0)
plr->HP = 0;
skill->iHP = plr->HP;
} else if (skillId == 150) { // heal egg
sSkillResult_Heal_HP* skill = (sSkillResult_Heal_HP*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iHealHP = PC_MAXHEALTH(plr->level) * NanoManager::SkillTable[skillId].powerIntensity[0] / 1000;
plr->HP += skill->iHealHP;
if (plr->HP > PC_MAXHEALTH(plr->level))
plr->HP = PC_MAXHEALTH(plr->level);
skill->iHP = plr->HP;
} else { // regular buff egg
sSkillResult_Buff* skill = (sSkillResult_Buff*)(respbuf + sizeof(sP_FE2CL_NPC_SKILL_HIT));
memset(respbuf, 0, resplen);
skill->eCT = 1;
skill->iID = plr->iID;
skill->iConditionBitFlag = plr->iConditionBitFlag;
}
skillUse->iNPC_ID = eggId;
skillUse->iSkillID = skillId;
skillUse->eST = NanoManager::SkillTable[skillId].skillType;
skillUse->iTargetCnt = 1;
sock->sendPacket((void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_NPC_SKILL_HIT, resplen);
if (CBFlag == 0)
return -1;
std::pair<CNSocket*, int32_t> key = std::make_pair(sock, CBFlag);
// save the buff serverside;
// if you get the same buff again, new duration will override the previous one
time_t until = getTime() + (time_t)NanoManager::SkillTable[skillId].durationTime[0] * 25;
EggBuffs[key] = until;
return 0;
}
void NPCManager::eggStep(CNServer* serv, time_t currTime) {
// tick buffs
time_t timeStamp = currTime;
auto it = EggBuffs.begin();
while (it != EggBuffs.end()) {
// check remaining time
if (it->second > timeStamp)
it++;
else { // if time reached 0
CNSocket* sock = it->first.first;
int32_t CBFlag = it->first.second;
Player* plr = PlayerManager::getPlayer(sock);
Player* otherPlr = PlayerManager::getPlayerFromID(plr->iIDGroup);
int groupFlags = GroupManager::getGroupFlags(otherPlr);
for (auto& pwr : NanoManager::NanoPowers) {
if (pwr.bitFlag == CBFlag) { // pick the power with the right flag and unbuff
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, resp);
resp.eCSTB = pwr.timeBuffID;
resp.eTBU = 2;
resp.eTBT = 3; // for egg buffs
plr->iConditionBitFlag &= ~CBFlag;
resp.iConditionBitFlag = plr->iConditionBitFlag |= groupFlags | plr->iSelfConditionBitFlag;
sock->sendPacket((void*)&resp, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, resp2); // send a buff timeout to other players
resp2.eCT = 1;
resp2.iID = plr->iID;
resp2.iConditionBitFlag = plr->iConditionBitFlag;
PlayerManager::sendToViewable(sock, (void*)&resp2, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
}
// remove buff from the map
it = EggBuffs.erase(it);
}
}
// check dead eggs and eggs in inactive chunks
for (auto egg : Eggs) {
if (!egg.second->dead || !ChunkManager::inPopulatedChunks(egg.second->viewableChunks))
continue;
if (egg.second->deadUntil <= timeStamp) {
// respawn it
egg.second->dead = false;
egg.second->deadUntil = 0;
egg.second->appearanceData.iHP = 400;
ChunkManager::addNPCToChunks(ChunkManager::getViewableChunks(egg.second->chunkPos), egg.first);
}
}
}
void NPCManager::npcDataToEggData(sNPCAppearanceData* npc, sShinyAppearanceData* egg) {
egg->iX = npc->iX;
egg->iY = npc->iY;
egg->iZ = npc->iZ;
// client doesn't care about egg->iMapNum
egg->iShinyType = npc->iNPCType;
egg->iShiny_ID = npc->iNPC_ID;
}
void NPCManager::eggPickup(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_SHINY_PICKUP))
return; // malformed packet
sP_CL2FE_REQ_SHINY_PICKUP* pickup = (sP_CL2FE_REQ_SHINY_PICKUP*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
int eggId = pickup->iShinyID;
if (Eggs.find(eggId) == Eggs.end()) {
std::cout << "[WARN] Player tried to open non existing egg?!" << std::endl;
return;
}
Egg* egg = Eggs[eggId];
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->appearanceData.iX - plr->x)>500 || abs(egg->appearanceData.iY - plr->y) > 500) {
std::cout << "[WARN] Player tried to open an egg from the other chunk?!" << std::endl;
return;
}
*/
int typeId = egg->appearanceData.iNPCType;
if (EggTypes.find(typeId) == EggTypes.end()) {
std::cout << "[WARN] Egg Type " << typeId << " not found!" << std::endl;
return;
}
EggType* type = &EggTypes[typeId];
// buff the player
if (type->effectId != 0)
eggBuffPlayer(sock, type->effectId, eggId);
/*
* 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)
{
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((void*)&resp, P_FE2CL_REP_SHINY_PICKUP_SUCC, sizeof(sP_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_BUFFER_SIZE - 8);
// 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 = ItemManager::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)
destroyNPC(eggId);
else {
ChunkManager::removeNPCFromChunks(ChunkManager::getViewableChunks(egg->chunkPos), eggId);
egg->dead = true;
egg->deadUntil = getTime() + (time_t)type->regen * 1000;
egg->appearanceData.iHP = 0;
}
}
// TODO: Move this to MobAI, possibly
#pragma region NPCEvents
// summon right arm and stage 2 body
@@ -873,7 +324,7 @@ static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) {
std::cout << "Lord Fuse stage three" << std::endl;
// Cosmic, Damage Point
// Cosmix, Damage Point
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->spawnX, oldbody->spawnY, oldbody->spawnZ, plr->instanceID, 2468);
newbody->appearanceData.iAngle = oldbody->appearanceData.iAngle;
@@ -894,3 +345,33 @@ std::vector<NPCEvent> NPCManager::NPCEvents = {
};
#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->type != EntityType::COMBAT_NPC && pair.second->type != EntityType::MOB)
continue;
auto npc = (CombatNPC*)pair.second;
npc->stepAI(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, 200);
}

View File

@@ -1,10 +1,11 @@
#pragma once
#include "CNProtocol.hpp"
#include "core/Core.hpp"
#include "PlayerManager.hpp"
#include "NPC.hpp"
#include "Transport.hpp"
#include "contrib/JSON.hpp"
#include "JSON.hpp"
#include <map>
#include <unordered_map>
@@ -12,9 +13,9 @@
#define RESURRECT_HEIGHT 400
// placeholder; there's only one trigger type right now
enum Trigger {
ON_KILLED
ON_KILLED,
ON_COMBAT
};
typedef void (*NPCEventHandler)(CNSocket*, BaseNPC*);
@@ -28,69 +29,24 @@ struct NPCEvent {
: npcType(t), trigger(tr), handler(hndlr) {}
};
// this should really be called vec3 or something...
struct WarpLocation {
int x, y, z, instanceID, isInstance, limitTaskID, npcID;
};
struct Egg : public BaseNPC {
bool summoned;
bool dead = false;
time_t deadUntil;
Egg(int x, int y, int z, uint64_t iID, int type, int32_t id, bool summon)
: BaseNPC(x, y, z, 0, iID, type, id) {
summoned = summon;
npcClass = NPCClass::NPC_EGG;
}
};
struct EggType {
int dropCrateId;
int effectId;
int duration;
int regen;
};
struct WarpLocation;
namespace NPCManager {
extern std::map<int32_t, BaseNPC*> NPCs;
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 std::unordered_map<int, Egg*> Eggs;
extern std::map<std::pair<CNSocket*, int32_t>, time_t> EggBuffs;
extern std::unordered_map<int, EggType> EggTypes;
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(BaseNPC* npc, void* buf, uint32_t type, size_t size);
void npcBarkHandler(CNSocket* sock, CNPacketData* data);
void npcSummonHandler(CNSocket* sock, CNPacketData* data);
void npcUnsummonHandler(CNSocket* sock, CNPacketData* data);
void npcWarpHandler(CNSocket* sock, CNPacketData* data);
void npcWarpTimeMachine(CNSocket* sock, CNPacketData* data);
void npcVendorStart(CNSocket* sock, CNPacketData* data);
void npcVendorTable(CNSocket* sock, CNPacketData* data);
void npcVendorBuy(CNSocket* sock, CNPacketData* data);
void npcVendorSell(CNSocket* sock, CNPacketData* data);
void npcVendorBuyback(CNSocket* sock, CNPacketData* data);
void npcVendorBuyBattery(CNSocket* sock, CNPacketData* data);
void npcCombineItems(CNSocket* sock, CNPacketData* data);
void handleWarp(CNSocket* sock, int32_t warpId);
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);
/// returns -1 on fail
int eggBuffPlayer(CNSocket* sock, int skillId, int duration);
void eggStep(CNServer* serv, time_t currTime);
void npcDataToEggData(sNPCAppearanceData* npc, sShinyAppearanceData* egg);
void eggPickup(CNSocket* sock, CNPacketData* data);
}

View File

@@ -1,74 +0,0 @@
#pragma once
#include <set>
#include <vector>
#include "Player.hpp"
#include "CNShardServer.hpp"
typedef void (*PowerHandler)(CNSocket*, std::vector<int>, int16_t, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t);
struct NanoPower {
int16_t skillType;
int32_t bitFlag;
int16_t timeBuffID;
PowerHandler handler;
NanoPower(int16_t s, int32_t b, int16_t t, PowerHandler h) : skillType(s), bitFlag(b), timeBuffID(t), handler(h) {}
void handle(CNSocket *sock, std::vector<int> targetData, int16_t nanoID, int16_t skillID, int16_t duration, int16_t amount) {
if (handler == nullptr)
return;
handler(sock, targetData, nanoID, skillID, duration, amount, skillType, bitFlag, timeBuffID);
}
};
struct NanoData {
int style;
};
struct NanoTuning {
int reqItemCount;
int reqItems;
};
struct SkillData {
int skillType;
int targetType;
int drainType;
int effectArea;
int batteryUse[4];
int durationTime[4];
int powerIntensity[4];
};
namespace NanoManager {
extern std::vector<NanoPower> NanoPowers;
extern std::map<int32_t, NanoData> NanoTable;
extern std::map<int32_t, NanoTuning> NanoTunings;
extern std::map<int32_t, SkillData> SkillTable;
void init();
void nanoSummonHandler(CNSocket* sock, CNPacketData* data);
void nanoEquipHandler(CNSocket* sock, CNPacketData* data);
void nanoUnEquipHandler(CNSocket* sock, CNPacketData* data);
void nanoGMGiveHandler(CNSocket* sock, CNPacketData* data);
void nanoSkillUseHandler(CNSocket* sock, CNPacketData* data);
void nanoSkillSetHandler(CNSocket* sock, CNPacketData* data);
void nanoSkillSetGMHandler(CNSocket* sock, CNPacketData* data);
void nanoRecallRegisterHandler(CNSocket* sock, CNPacketData* data);
void nanoRecallHandler(CNSocket* sock, CNPacketData* data);
void nanoPotionHandler(CNSocket* sock, CNPacketData* data);
// Helper methods
void addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm=false);
void summonNano(CNSocket* sock, int slot, bool silent = false);
void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill);
void resetNanoSkill(CNSocket* sock, int16_t nanoID);
void nanoUnbuff(CNSocket* sock, std::vector<int> targetData, int32_t bitFlag, int16_t timeBuffID, int16_t amount, bool groupPower);
int applyBuff(CNSocket* sock, int skillID, int eTBU, int eTBT, int32_t groupFlags);
int nanoStyle(int nanoID);
std::vector<int> findTargets(Player* plr, int skillID, CNPacketData* data = nullptr);
bool getNanoBoost(Player* plr);
}

406
src/Nanos.cpp Normal file
View File

@@ -0,0 +1,406 @@
#include "servers/CNShardServer.hpp"
#include "Nanos.hpp"
#include "PlayerManager.hpp"
#include "NPCManager.hpp"
#include "Combat.hpp"
#include "Missions.hpp"
#include "Groups.hpp"
#include "Abilities.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
plr->nanoDrainRate = 0;
int16_t skillID = plr->Nanos[plr->activeNano].iSkillID;
// passive nano unbuffing
if (SkillTable[skillID].drainType == 2) {
std::vector<int> targetData = findTargets(plr, skillID);
for (auto& pwr : NanoPowers)
if (pwr.skillType == SkillTable[skillID].skillType)
nanoUnbuff(sock, targetData, pwr.bitFlag, pwr.timeBuffID, 0,(SkillTable[skillID].targetType == 3));
}
if (nanoID >= NANO_COUNT || nanoID < 0)
return; // sanity check
plr->activeNano = nanoID;
skillID = plr->Nanos[nanoID].iSkillID;
// passive nano buffing
if (SkillTable[skillID].drainType == 2) {
std::vector<int> targetData = findTargets(plr, skillID);
int boost = 0;
if (getNanoBoost(plr))
boost = 1;
for (auto& pwr : NanoPowers) {
if (pwr.skillType == SkillTable[skillID].skillType) {
resp.eCSTB___Add = 1; // the part that makes nano go ZOOMAZOOM
plr->nanoDrainRate = SkillTable[skillID].batteryUse[boost*3];
pwr.handle(sock, targetData, nanoID, skillID, 0, SkillTable[skillID].powerIntensity[boost]);
}
}
}
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 = plr->Nanos[nanoID];
PlayerManager::sendToViewable(sock, pkt1, P_FE2CL_NANO_ACTIVE);
}
static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
if (skill->iNanoID >= NANO_COUNT)
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->iConditionBitFlag & (CSB_BIT_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;
// Unbuff gumballs
int value1 = CSB_BIT_STIMPAKSLOT1 << nano->iNanoSlotNum;
if (plr->iConditionBitFlag & value1) {
int value2 = ECSB_STIMPAKSLOT1 + nano->iNanoSlotNum;
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt);
pkt.eCSTB = value2; // eCharStatusTimeBuffID
pkt.eTBU = 2; // eTimeBuffUpdate
pkt.eTBT = 1; // eTimeBuffType 1 means nano
pkt.iConditionBitFlag = plr->iConditionBitFlag &= ~value1;
sock->sendPacket(pkt, P_FE2CL_PC_BUFF_UPDATE);
}
// unsummon nano if replaced
if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum])
summonNano(sock, -1);
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);
int16_t nanoID = plr->activeNano;
int16_t skillID = plr->Nanos[nanoID].iSkillID;
DEBUGLOG(
std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano skill " << std::endl;
)
std::vector<int> targetData = findTargets(plr, skillID, data);
int boost = 0;
if (getNanoBoost(plr))
boost = 1;
plr->Nanos[plr->activeNano].iStamina -= SkillTable[skillID].batteryUse[boost*3];
if (plr->Nanos[plr->activeNano].iStamina < 0)
plr->Nanos[plr->activeNano].iStamina = 0;
for (auto& pwr : NanoPowers)
if (pwr.skillType == SkillTable[skillID].skillType)
pwr.handle(sock, targetData, nanoID, skillID, SkillTable[skillID].durationTime[boost], SkillTable[skillID].powerIntensity[boost]);
if (plr->Nanos[plr->activeNano].iStamina < 0)
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);
}

28
src/Nanos.hpp Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include <set>
#include <vector>
#include "Player.hpp"
#include "servers/CNShardServer.hpp"
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

@@ -3,85 +3,90 @@
#include <string>
#include <cstring>
#include "CNProtocol.hpp"
#include "CNStructs.hpp"
#include "ChunkManager.hpp"
#include "core/Core.hpp"
#include "Chunking.hpp"
#include "Entities.hpp"
#define ACTIVE_MISSION_COUNT 6
#define PC_MAXHEALTH(level) (925 + 75 * (level))
struct Player {
int accountId;
int accountLevel; // permission level (see CN_ACCOUNT_LEVEL enums)
int64_t SerialKey;
int32_t iID;
uint64_t FEKey;
struct Player : public Entity {
int accountId = 0;
int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums)
int32_t iID = 0;
int level;
int HP;
int slot; // player slot, not nano slot
int16_t mentor;
int32_t money;
int32_t fusionmatter;
int32_t batteryW;
int32_t batteryN;
sPCStyle PCStyle;
sPCStyle2 PCStyle2;
sNano Nanos[NANO_COUNT]; // acquired nanos
int equippedNanos[3];
int activeNano; // active nano (index into Nanos)
int8_t iPCState;
int32_t iWarpLocationFlag;
int64_t aSkywayLocationFlag[2];
int32_t iConditionBitFlag;
int32_t iSelfConditionBitFlag;
int8_t iSpecialState;
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] = {};
int32_t iConditionBitFlag = 0;
int32_t iSelfConditionBitFlag = 0;
int8_t iSpecialState = 0;
int x, y, z, angle;
int lastX, lastY, lastZ, lastAngle;
int recallX, recallY, recallZ, recallInstance; // also Lair entrances
uint64_t instanceID;
sItemBase Equip[AEQUIP_COUNT];
sItemBase Inven[AINVEN_COUNT];
sItemBase Bank[ABANK_COUNT];
sItemTrade Trade[12];
int32_t moneyInTrade;
bool isTrading;
bool isTradeConfirm;
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;
bool onMonkey;
int nanoDrainRate;
int healCooldown;
bool inCombat = false;
bool onMonkey = false;
int nanoDrainRate = 0;
int healCooldown = 0;
int pointDamage;
int groupDamage;
int defense;
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;
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;
sTimeLimitItemDeleteInfo2CL toRemoveVehicle = {};
int32_t iIDGroup;
int groupCnt;
int32_t groupIDs[4];
int32_t iGroupConditionBitFlag;
int32_t iIDGroup = 0;
int groupCnt = 0;
int32_t groupIDs[4] = {};
int32_t iGroupConditionBitFlag = 0;
bool notify;
bool hidden;
bool notify = false;
bool hidden = false;
bool unwarpable = false;
bool buddiesSynced;
int64_t buddyIDs[50];
bool isBuddyBlocked[50];
bool buddiesSynced = false;
int64_t buddyIDs[50] = {};
bool isBuddyBlocked[50] = {};
uint64_t iFirstUseFlag[2];
uint64_t iFirstUseFlag[2] = {};
time_t lastHeartbeat = 0;
ChunkPos chunkPos;
std::set<Chunk*>* viewableChunks;
time_t lastHeartbeat;
int suspicionRating = 0;
time_t lastShot = 0;
std::vector<sItemBase> buyback = {};
Player() { type = EntityType::PLAYER; }
virtual void enterIntoViewOf(CNSocket *sock) override;
virtual void disappearFromViewOf(CNSocket *sock) override;
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
#pragma once
#include "Player.hpp"
#include "CNProtocol.hpp"
#include "CNStructs.hpp"
#include "CNShardServer.hpp"
#include "ChunkManager.hpp"
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "Chunking.hpp"
#include <utility>
#include <map>
@@ -16,51 +15,39 @@ namespace PlayerManager {
extern std::map<CNSocket*, Player*> players;
void init();
void addPlayer(CNSocket* key, Player plr);
void removePlayer(CNSocket* key);
void updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, uint64_t I, int angle);
void updatePlayerPositionForWarp(CNSocket* sock, int X, int Y, int Z, uint64_t inst);
void sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I);
void sendPlayerTo(CNSocket* sock, int X, int Y, int Z);
void sendToViewable(CNSocket* sock, void* buf, uint32_t type, size_t size);
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 jumppadPlayer(CNSocket* sock, CNPacketData* data);
void launchPlayer(CNSocket* sock, CNPacketData* data);
void ziplinePlayer(CNSocket* sock, CNPacketData* data);
void movePlatformPlayer(CNSocket* sock, CNPacketData* data);
void moveSliderPlayer(CNSocket* sock, CNPacketData* data);
void moveSlopePlayer(CNSocket* sock, CNPacketData* data);
void gotoPlayer(CNSocket* sock, CNPacketData* data);
void setSpecialPlayer(CNSocket* sock, CNPacketData* data);
void heartbeatPlayer(CNSocket* sock, CNPacketData* data);
void revivePlayer(CNSocket* sock, CNPacketData* data);
void exitGame(CNSocket* sock, CNPacketData* data);
void setSpecialSwitchPlayer(CNSocket* sock, CNPacketData* data);
void setGMSpecialSwitchPlayer(CNSocket* sock, CNPacketData* data);
void changePlayerGuide(CNSocket *sock, CNPacketData *data);
void enterPlayerVehicle(CNSocket* sock, CNPacketData* data);
void exitPlayerVehicle(CNSocket* sock, CNPacketData* data);
void setFirstUseFlag(CNSocket* sock, CNPacketData* data);
Player *getPlayer(CNSocket* key);
std::string getPlayerName(Player *plr, bool id=true);
WarpLocation* getRespawnPoint(Player *plr);
bool isAccountInUse(int accountId);
void exitDuplicate(int accountId);
void setSpecialState(CNSocket* sock, CNPacketData* data);
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 sendNanoBookSubset(CNSocket *sock);
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.type != EntityType::PLAYER || ref.sock == sock)
continue;
ref.sock->sendPacket(pkt, type);
}
}
}
}

287
src/PlayerMovement.cpp Normal file
View File

@@ -0,0 +1,287 @@
#include "PlayerMovement.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->appearanceData.iNPC_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->appearanceData.iNPC_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();
};

View File

@@ -1,28 +1,19 @@
#include "CNShardServer.hpp"
#include "CNStructs.hpp"
#include "RacingManager.hpp"
#include "servers/CNShardServer.hpp"
#include "Racing.hpp"
#include "PlayerManager.hpp"
#include "MissionManager.hpp"
#include "ItemManager.hpp"
#include "Database.hpp"
#include "Missions.hpp"
#include "Items.hpp"
#include "db/Database.hpp"
#include "NPCManager.hpp"
std::map<int32_t, EPInfo> RacingManager::EPData;
std::map<CNSocket*, EPRace> RacingManager::EPRaces;
std::map<int32_t, std::pair<std::vector<int>, std::vector<int>>> RacingManager::EPRewards;
using namespace Racing;
void RacingManager::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);
}
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;
void RacingManager::racingStart(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_EP_RACE_START))
return; // malformed packet
sP_CL2FE_REQ_EP_RACE_START* req = (sP_CL2FE_REQ_EP_RACE_START*)data->buf;
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
@@ -32,57 +23,71 @@ void RacingManager::racingStart(CNSocket* sock, CNPacketData* data) {
return; // IZ not found
// make ongoing race entry
EPRace race = { 0, req->iEPRaceMode, req->iEPTicketItemSlotNum, getTime() / 1000 };
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((void*)&resp, P_FE2CL_REP_EP_RACE_START_SUCC, sizeof(sP_FE2CL_REP_EP_RACE_START_SUCC));
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_START_SUCC);
}
void RacingManager::racingGetPod(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_EP_GET_RING))
return; // malformed packet
static void racingGetPod(CNSocket* sock, CNPacketData* data) {
if (EPRaces.find(sock) == EPRaces.end())
return; // race not found
sP_CL2FE_REQ_EP_GET_RING* req = (sP_CL2FE_REQ_EP_GET_RING*)data->buf;
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
EPRaces[sock].ringCount++;
// 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; // could be used to check for proximity in the future
resp.iRingCount_Get = EPRaces[sock].ringCount;
resp.iRingLID = req->iRingLID;
resp.iRingCount_Get = EPRaces[sock].collectedRings.size();
sock->sendPacket((void*)&resp, P_FE2CL_REP_EP_GET_RING_SUCC, sizeof(sP_FE2CL_REP_EP_GET_RING_SUCC));
sock->sendPacket(resp, P_FE2CL_REP_EP_GET_RING_SUCC);
}
void RacingManager::racingCancel(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_EP_RACE_CANCEL))
return; // malformed packet
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((void*)&resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC, sizeof(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC));
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);
}
}
void RacingManager::racingEnd(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_EP_RACE_END))
return; // malformed packet
static void racingEnd(CNSocket* sock, CNPacketData* data) {
if (EPRaces.find(sock) == EPRaces.end())
return; // race not found
sP_CL2FE_REQ_EP_RACE_END* req = (sP_CL2FE_REQ_EP_RACE_END*)data->buf;
auto req = (sP_CL2FE_REQ_EP_RACE_END*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
if (NPCManager::NPCs.find(req->iEndEcomID) == NPCManager::NPCs.end())
@@ -95,7 +100,7 @@ void RacingManager::racingEnd(CNSocket* sock, CNPacketData* data) {
uint64_t now = getTime() / 1000;
int timeDiff = now - EPRaces[sock].startTime;
int score = 500 * EPRaces[sock].ringCount - 10 * timeDiff;
int score = 500 * EPRaces[sock].collectedRings.size() - 10 * timeDiff;
if (score < 0) score = 0; // lol
int fm = score * plr->level * (1.0f / 36) * 0.3f;
@@ -103,7 +108,7 @@ void RacingManager::racingEnd(CNSocket* sock, CNPacketData* data) {
Database::RaceRanking postRanking = {};
postRanking.EPID = EPData[mapNum].EPID;
postRanking.PlayerID = plr->iID;
postRanking.RingCount = EPRaces[sock].ringCount;
postRanking.RingCount = EPRaces[sock].collectedRings.size();
postRanking.Score = score;
postRanking.Time = timeDiff;
postRanking.Timestamp = getTimestamp();
@@ -140,19 +145,20 @@ void RacingManager::racingEnd(CNSocket* sock, CNPacketData* data) {
resp.iEPRaceMode = EPRaces[sock].mode;
resp.iEPRewardFM = fm;
MissionManager::updateFusionMatter(sock, resp.iEPRewardFM);
Missions::updateFusionMatter(sock, resp.iEPRewardFM);
resp.iFusionMatter = plr->fusionmatter;
resp.iFatigue = 50;
resp.iFatigue_Level = 1;
sItemReward reward;
reward.iSlotNum = ItemManager::findFreeSlot(plr);
reward.iSlotNum = Items::findFreeSlot(plr);
reward.eIL = 1;
sItemBase item;
item.iID = rankRewards->at(rank); // rank scores and rewards line up
item.iType = 0;
item.iType = 9;
item.iOpt = 1;
item.iTimeLimit = 0;
reward.sItem = item;
if (reward.iSlotNum > -1 && reward.sItem.iID != 0) {
@@ -161,6 +167,12 @@ void RacingManager::racingEnd(CNSocket* sock, CNPacketData* data) {
}
EPRaces.erase(sock);
sock->sendPacket((void*)&resp, P_FE2CL_REP_EP_RACE_END_SUCC, sizeof(sP_FE2CL_REP_EP_RACE_END_SUCC));
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);
}

View File

@@ -1,25 +1,23 @@
#pragma once
#include "CNShardServer.hpp"
#include <set>
#include "servers/CNShardServer.hpp"
struct EPInfo {
int zoneX, zoneY, EPID, maxScore, maxTime;
};
struct EPRace {
int ringCount, mode, ticketSlot;
std::set<int> collectedRings;
int mode, ticketSlot;
time_t startTime;
};
namespace RacingManager {
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();
void racingStart(CNSocket* sock, CNPacketData* data);
void racingGetPod(CNSocket* sock, CNPacketData* data);
void racingCancel(CNSocket* sock, CNPacketData* data);
void racingEnd(CNSocket* sock, CNPacketData* data);
}

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

22
src/Rand.hpp Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include <random>
#include <memory>
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();
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,27 @@
#pragma once
#include <map>
#include "contrib/JSON.hpp"
#include "NPCManager.hpp"
// 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<WarpLocation>> RunningSkywayRoutes;
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 cleanup();
void loadGruntwork(int32_t*);
void flush();
void loadPaths(int*);
void loadDrops();
void loadEggs(int32_t* nextId);
void constructPathSkyway(nlohmann::json::iterator);
void constructPathNPC(nlohmann::json::iterator, int id=0);
}

435
src/Trading.cpp Normal file
View File

@@ -0,0 +1,435 @@
#include "Trading.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 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_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);
}

7
src/Trading.hpp Normal file
View File

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

View File

@@ -1,32 +1,25 @@
#include "CNShardServer.hpp"
#include "CNStructs.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "NanoManager.hpp"
#include "TransportManager.hpp"
#include "Nanos.hpp"
#include "Transport.hpp"
#include "TableData.hpp"
#include "MobManager.hpp"
#include "Combat.hpp"
#include "MobAI.hpp"
#include <unordered_map>
#include <cmath>
std::map<int32_t, TransportRoute> TransportManager::Routes;
std::map<int32_t, TransportLocation> TransportManager::Locations;
std::map<int32_t, std::queue<WarpLocation>> TransportManager::SkywayPaths;
std::unordered_map<CNSocket*, std::queue<WarpLocation>> TransportManager::SkywayQueues;
std::unordered_map<int32_t, std::queue<WarpLocation>> TransportManager::NPCQueues;
using namespace Transport;
void TransportManager::init() {
REGISTER_SHARD_TIMER(tickTransportationSystem, 1000);
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;
REGISTER_SHARD_PACKET(P_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION, transportRegisterLocationHandler);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION, transportWarpHandler);
}
void TransportManager::transportRegisterLocationHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION))
return; // malformed packet
sP_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION* transport = (sP_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION*)data->buf;
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
@@ -40,12 +33,12 @@ void TransportManager::transportRegisterLocationHandler(CNSocket* sock, CNPacket
failResp.iErrorCode = 0; // TODO: review what error code to use here
failResp.iLocationID = transport->iLocationID;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, sizeof(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL));
sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
return;
}
// update registration bitfield using bitmask
uint32_t newScamperFlag = plr->iWarpLocationFlag | (plr->accountLevel <= 40 ? INT32_MAX : (1UL << (transport->iLocationID - 1)));
uint32_t newScamperFlag = plr->iWarpLocationFlag | (1UL << (transport->iLocationID - 1));
if (newScamperFlag != plr->iWarpLocationFlag) {
plr->iWarpLocationFlag = newScamperFlag;
newReg = true;
@@ -59,24 +52,18 @@ void TransportManager::transportRegisterLocationHandler(CNSocket* sock, CNPacket
failResp.iErrorCode = 0; // TODO: review what error code to use here
failResp.iLocationID = transport->iLocationID;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, sizeof(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL));
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
*/
if (plr->accountLevel <= 40) {
plr->aSkywayLocationFlag[0] = INT64_MAX;
plr->aSkywayLocationFlag[1] = INT64_MAX;
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 {
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;
@@ -86,7 +73,7 @@ void TransportManager::transportRegisterLocationHandler(CNSocket* sock, CNPacket
failResp.iErrorCode = 0; // TODO: review what error code to use here
failResp.iLocationID = transport->iLocationID;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, sizeof(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL));
sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
return;
}
@@ -101,14 +88,11 @@ void TransportManager::transportRegisterLocationHandler(CNSocket* sock, CNPacket
resp.aWyvernLocationFlag[0] = plr->aSkywayLocationFlag[0];
resp.aWyvernLocationFlag[1] = plr->aSkywayLocationFlag[1];
sock->sendPacket((void*)&resp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC, sizeof(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC));
sock->sendPacket(resp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC);
}
void TransportManager::transportWarpHandler(CNSocket* sock, CNPacketData* data) {
if (data->size != sizeof(sP_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION))
return; // malformed packet
sP_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION* req = (sP_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION*)data->buf;
static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
auto req = (sP_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
/*
@@ -125,7 +109,7 @@ void TransportManager::transportWarpHandler(CNSocket* sock, CNPacketData* data)
failResp.iErrorCode = 0; // TODO: error code
failResp.iTransportationID = req->iTransporationID;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL, sizeof(sP_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL));
sock->sendPacket(failResp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL);
return;
}
@@ -143,13 +127,13 @@ void TransportManager::transportWarpHandler(CNSocket* sock, CNPacketData* data)
plr->lastY = plr->y;
plr->lastZ = plr->z;
if (SkywayPaths.find(route.mssRouteNum) != SkywayPaths.end()) { // check if route exists
NanoManager::summonNano(sock, -1); // make sure that no nano is active during the ride
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<WarpLocation>* _route = &TableData::RunningSkywayRoutes[route.mssRouteNum];
NanoManager::summonNano(sock, -1);
std::vector<Vec3>* _route = &TableData::RunningSkywayRoutes[route.mssRouteNum];
Nanos::summonNano(sock, -1);
testMssRoute(sock, _route);
plr->onMonkey = true;
break;
@@ -161,7 +145,7 @@ void TransportManager::transportWarpHandler(CNSocket* sock, CNPacketData* data)
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((void*)&alert, P_FE2CL_ANNOUNCE_MSG, sizeof(sP_FE2CL_ANNOUNCE_MSG));
sock->sendPacket(alert, P_FE2CL_ANNOUNCE_MSG);
std::cout << "[WARN] MSS route " << route.mssRouteNum << " not pathed" << std::endl;
break;
@@ -177,23 +161,23 @@ void TransportManager::transportWarpHandler(CNSocket* sock, CNPacketData* data)
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((void*)&resp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC, sizeof(sP_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC));
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC);
if (target == nullptr)
return;
// we warped; update position and chunks
ChunkManager::updatePlayerChunk(sock, plr->chunkPos, std::make_tuple(0, 0, 0)); // force player to reload chunks
PlayerManager::updatePlayerPosition(sock, target->x, target->y, target->z, INSTANCE_OVERWORLD, plr->angle);
PlayerManager::updatePlayerPositionForWarp(sock, target->x, target->y, target->z, INSTANCE_OVERWORLD);
}
void TransportManager::testMssRoute(CNSocket *sock, std::vector<WarpLocation>* route) {
void Transport::testMssRoute(CNSocket *sock, std::vector<Vec3>* route) {
int speed = 1500; // TODO: make this adjustable
std::queue<WarpLocation> path;
WarpLocation last = route->front(); // start pos
std::queue<Vec3> path;
Vec3 last = route->front(); // start pos
for (int i = 1; i < route->size(); i++) {
WarpLocation coords = route->at(i);
TransportManager::lerp(&path, last, coords, speed);
Vec3 coords = route->at(i);
Transport::lerp(&path, last, coords, speed);
path.push(coords); // add keyframe to the queue
last = coords; // update start pos
}
@@ -201,22 +185,17 @@ void TransportManager::testMssRoute(CNSocket *sock, std::vector<WarpLocation>* r
SkywayQueues[sock] = path;
}
void TransportManager::tickTransportationSystem(CNServer* serv, time_t currTime) {
stepNPCPathing();
stepSkywaySystem();
}
/*
* 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.
*/
void TransportManager::stepSkywaySystem() {
static void stepSkywaySystem() {
// using an unordered map so we can remove finished players in one iteration
std::unordered_map<CNSocket*, std::queue<WarpLocation>>::iterator it = SkywayQueues.begin();
std::unordered_map<CNSocket*, std::queue<Vec3>>::iterator it = SkywayQueues.begin();
while (it != SkywayQueues.end()) {
std::queue<WarpLocation>* queue = &it->second;
std::queue<Vec3>* queue = &it->second;
if (PlayerManager::players.find(it->first) == PlayerManager::players.end()) {
// pluck out dead socket + update iterator
@@ -234,13 +213,13 @@ void TransportManager::stepSkywaySystem() {
rideSucc.eRT = 0;
rideBroadcast.iPC_ID = plr->iID;
rideBroadcast.eRT = 0;
it->first->sendPacket((void*)&rideSucc, P_FE2CL_REP_PC_RIDING_SUCC, sizeof(sP_FE2CL_REP_PC_RIDING_SUCC));
it->first->sendPacket(rideSucc, P_FE2CL_REP_PC_RIDING_SUCC);
// send packet to players in view
PlayerManager::sendToViewable(it->first, (void*)&rideBroadcast, P_FE2CL_PC_RIDING, sizeof(sP_FE2CL_PC_RIDING));
PlayerManager::sendToViewable(it->first, rideBroadcast, P_FE2CL_PC_RIDING);
it = SkywayQueues.erase(it); // remove player from tracking map + update iterator
plr->onMonkey = false;
} else {
WarpLocation point = queue->front(); // get point
Vec3 point = queue->front(); // get point
queue->pop(); // remove point from front of queue
INITSTRUCT(sP_FE2CL_PC_BROOMSTICK_MOVE, bmstk);
@@ -248,24 +227,24 @@ void TransportManager::stepSkywaySystem() {
bmstk.iToX = point.x;
bmstk.iToY = point.y;
bmstk.iToZ = point.z;
it->first->sendPacket((void*)&bmstk, P_FE2CL_PC_BROOMSTICK_MOVE, sizeof(sP_FE2CL_PC_BROOMSTICK_MOVE));
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, (void*)&bmstk, P_FE2CL_PC_BROOMSTICK_MOVE, sizeof(sP_FE2CL_PC_BROOMSTICK_MOVE));
PlayerManager::sendToViewable(it->first, bmstk, P_FE2CL_PC_BROOMSTICK_MOVE);
it++; // go to next entry in map
}
}
}
void TransportManager::stepNPCPathing() {
static void stepNPCPathing() {
// all NPC pathing queues
std::unordered_map<int32_t, std::queue<WarpLocation>>::iterator it = NPCQueues.begin();
std::unordered_map<int32_t, std::queue<Vec3>>::iterator it = NPCQueues.begin();
while (it != NPCQueues.end()) {
std::queue<WarpLocation>* queue = &it->second;
std::queue<Vec3>* queue = &it->second;
BaseNPC* npc = nullptr;
if (NPCManager::NPCs.find(it->first) != NPCManager::NPCs.end())
@@ -278,30 +257,32 @@ void TransportManager::stepNPCPathing() {
}
// skip if not simulating mobs
if (npc->npcClass == NPC_MOB && !MobManager::simulateMobs) {
if (npc->type == EntityType::MOB && !MobAI::simulateMobs) {
it++;
continue;
}
// do not roam if not roaming
if (npc->npcClass == NPC_MOB && ((Mob*)npc)->state != MobState::ROAMING) {
if (npc->type == EntityType::MOB && ((Mob*)npc)->state != MobState::ROAMING) {
it++;
continue;
}
WarpLocation point = queue->front(); // get point
Vec3 point = queue->front(); // get point
queue->pop(); // remove point from front of queue
// calculate displacement
int dXY = hypot(point.x - npc->appearanceData.iX, point.y - npc->appearanceData.iY); // XY plane distance
int distanceBetween = hypot(dXY, point.z - npc->appearanceData.iZ); // total distance
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->appearanceData.iNPC_ID, point.x, point.y, point.z, npc->instanceID, npc->appearanceData.iAngle);
switch (npc->npcClass) {
case NPC_BUS:
// TODO: move walking logic into Entity stack
switch (npc->type) {
case EntityType::BUS:
INITSTRUCT(sP_FE2CL_TRANSPORTATION_MOVE, busMove);
busMove.eTT = 3;
busMove.iT_ID = npc->appearanceData.iNPC_ID;
busMove.iMoveStyle = 0; // ???
@@ -312,8 +293,8 @@ void TransportManager::stepNPCPathing() {
NPCManager::sendToViewable(npc, &busMove, P_FE2CL_TRANSPORTATION_MOVE, sizeof(sP_FE2CL_TRANSPORTATION_MOVE));
break;
case NPC_MOB:
MobManager::incNextMovement((Mob*)npc);
case EntityType::MOB:
MobAI::incNextMovement((Mob*)npc);
/* fallthrough */
default:
INITSTRUCT(sP_FE2CL_NPC_MOVE, move);
@@ -329,25 +310,29 @@ void TransportManager::stepNPCPathing() {
}
/*
* Move processed point to the back to maintain cycle, unless this is a
* dynamically calculated mob route.
* If this path should be repeated, move processed point to the back to maintain cycle.
*/
if (!(npc->npcClass == NPC_MOB && !((Mob*)npc)->staticPath))
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 TransportManager::lerp(std::queue<WarpLocation>* queue, WarpLocation start, WarpLocation end, int gapSize, float curve) {
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++) {
WarpLocation lerp;
Vec3 lerp;
// lerp math
//float frac = i / (lerps + 1);
float frac = powf(i, curve) / powf(lerps + 1, curve);
@@ -357,6 +342,84 @@ void TransportManager::lerp(std::queue<WarpLocation>* queue, WarpLocation start,
queue->push(lerp); // add lerp'd point
}
}
void TransportManager::lerp(std::queue<WarpLocation>* queue, WarpLocation start, WarpLocation end, int gapSize) {
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->type == EntityType::MOB)
((Mob*)(npc))->staticPath = true;
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);
}

56
src/Transport.hpp Normal file
View File

@@ -0,0 +1,56 @@
#pragma once
#include "servers/CNShardServer.hpp"
#include <unordered_map>
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*);
}

View File

@@ -1,42 +0,0 @@
#pragma once
#include "CNShardServer.hpp"
#include "NPCManager.hpp"
#include <unordered_map>
const int SLIDER_SPEED = 1200;
const int SLIDER_STOP_TICKS = 16;
const int SLIDER_GAP_SIZE = 45000;
struct WarpLocation;
struct TransportRoute {
int type, start, end, cost, mssSpeed, mssRouteNum;
};
struct TransportLocation {
int npcID, x, y, z;
};
namespace TransportManager {
extern std::map<int32_t, TransportRoute> Routes;
extern std::map<int32_t, TransportLocation> Locations;
extern std::map<int32_t, std::queue<WarpLocation>> SkywayPaths; // predefined skyway paths with points
extern std::unordered_map<CNSocket*, std::queue<WarpLocation>> SkywayQueues; // player sockets with queued broomstick points
extern std::unordered_map<int32_t, std::queue<WarpLocation>> NPCQueues; // NPC ids with queued pathing points
void init();
void transportRegisterLocationHandler(CNSocket*, CNPacketData*);
void transportWarpHandler(CNSocket*, CNPacketData*);
void testMssRoute(CNSocket *sock, std::vector<WarpLocation>* route);
void tickTransportationSystem(CNServer*, time_t);
void stepNPCPathing();
void stepSkywaySystem();
void lerp(std::queue<WarpLocation>*, WarpLocation, WarpLocation, int, float);
void lerp(std::queue<WarpLocation>*, WarpLocation, WarpLocation, int);
}

400
src/Vendors.cpp Normal file
View File

@@ -0,0 +1,400 @@
#include "Vendors.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);
}

22
src/Vendors.hpp Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include "core/Core.hpp"
#include "servers/CNShardServer.hpp"
#include "Items.hpp"
#include "PlayerManager.hpp"
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();
}

View File

@@ -1,4 +1,4 @@
#include "CNProtocol.hpp"
#include "core/CNProtocol.hpp"
#include "CNStructs.hpp"
#include <assert.h>
@@ -41,7 +41,8 @@ int CNSocketEncryption::xorData(uint8_t* buffer, uint8_t* key, int size) {
uint64_t CNSocketEncryption::createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2) {
uint64_t num = (uint64_t)(iv1 + 1);
uint64_t num2 = (uint64_t)(iv2 + 1);
uint64_t dEKey = (uint64_t)(*(uint64_t*)&defaultKey[0]);
uint64_t dEKey;
memcpy(&dEKey, defaultKey, sizeof(dEKey));
return dEKey * (uTime * num * num2);
}
@@ -59,12 +60,13 @@ int CNSocketEncryption::decryptData(uint8_t* buffer, uint8_t* key, int size) {
// ========================================================[[ CNPacketData ]]========================================================
CNPacketData::CNPacketData(void* b, uint32_t t, int l): buf(b), size(l), type(t) {}
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, PacketHandler ph): sock(s), pHandler(ph) {
EKey = (uint64_t)(*(uint64_t*)&CNSocketEncryption::defaultKey[0]);
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) {
@@ -108,7 +110,11 @@ bool CNSocket::isAlive() {
}
void CNSocket::kill() {
if (!alive)
return;
alive = false;
#ifdef _WIN32
shutdown(sock, SD_BOTH);
closesocket(sock);
@@ -118,48 +124,121 @@ void CNSocket::kill() {
#endif
}
// we don't own buf, TODO: queue packets up to send in step()
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;
size_t bodysize = size + sizeof(uint32_t);
uint8_t* fullpkt = (uint8_t*)xmalloc(bodysize+4);
uint8_t* body = fullpkt+4;
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, sizeof(uint32_t));
memcpy(body+sizeof(uint32_t), buf, size);
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: {
free(fullpkt);
DEBUGLOG(
std::cout << "[WARN]: UNSET KEYTYPE FOR SOCKET!! ABORTING SEND" << std::endl;
)
return;
}
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();
free(fullpkt);
}
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
@@ -167,9 +246,10 @@ void CNSocket::step() {
if (readSize <= 0) {
// we aren't reading a packet yet, try to start looking for one
int recved = recv(sock, (buffer_t*)readBuffer, sizeof(int32_t), 0);
if (recved == 0) {
// the socket was closed normally
if (recved >= 0 && recved < sizeof(int32_t)) {
// too little data for readSize or the socket was closed normally (when 0 bytes were read)
kill();
return;
} else if (!SOCKETERROR(recved)) {
// we got our packet size!!!!
readSize = *((int32_t*)readBuffer);
@@ -190,11 +270,12 @@ void CNSocket::step() {
}
if (readSize > 0 && readBufferIndex < readSize) {
// read until the end of the packet! (or at least try too)
// read until the end of the packet (or at least try to)
int recved = recv(sock, (buffer_t*)(readBuffer + readBufferIndex), readSize - readBufferIndex, 0);
if (recved == 0) {
// the socket was closed normally
kill();
return;
} else if (!SOCKETERROR(recved))
readBufferIndex += recved;
else if (OF_ERRNO != OF_EWOULD) {
@@ -209,11 +290,7 @@ void CNSocket::step() {
// decrypt readBuffer and copy to CNPacketData
CNSocketEncryption::decryptData((uint8_t*)&readBuffer, (uint8_t*)(&EKey), readSize);
void* tmpBuf = readBuffer+sizeof(uint32_t);
CNPacketData tmp(tmpBuf, *((uint32_t*)readBuffer), readSize-sizeof(int32_t));
// call packet handler!!
pHandler(this, &tmp);
parsePacket(readBuffer, readSize);
// reset vars :)
readSize = 0;
@@ -341,9 +418,9 @@ void CNServer::addPollFD(SOCKET s) {
fds.push_back({s, POLLIN});
}
void CNServer::removePollFD(int i) {
void CNServer::removePollFD(int fd) {
auto it = fds.begin();
while (it != fds.end() && it->fd != fds[i].fd)
while (it != fds.end() && it->fd != fd)
it++;
assert(it != fds.end());
@@ -388,12 +465,12 @@ void CNServer::start() {
if (!setSockNonblocking(sock, newConnectionSocket))
continue;
std::cout << "New connection! " << inet_ntoa(address.sin_addr) << std::endl;
std::cout << "New " << serverType << " connection! " << inet_ntoa(address.sin_addr) << std::endl;
addPollFD(newConnectionSocket);
// add connection to list!
CNSocket* tmp = new CNSocket(newConnectionSocket, pHandler);
CNSocket* tmp = new CNSocket(newConnectionSocket, address, pHandler);
connections[newConnectionSocket] = tmp;
newConnection(tmp);
@@ -403,10 +480,15 @@ void CNServer::start() {
} else {
std::lock_guard<std::mutex> lock(activeCrit); // protect operations on connections
// halt packet handling if server is shutting down
if (!active)
return;
// player sockets
if (connections.find(fds[i].fd) == connections.end()) {
std::cout << "[WARN] Event on non-existant socket?" << std::endl;
continue; // just to be safe
std::cout << "[FATAL] Event on non-existent socket: " << fds[i].fd << std::endl;
assert(0);
/* not reached */
}
CNSocket* cSock = connections[fds[i].fd];
@@ -415,22 +497,29 @@ void CNServer::start() {
if (fds[i].revents & ~POLLIN)
cSock->kill();
if (cSock->isAlive()) {
if (cSock->isAlive())
cSock->step();
} else {
killConnection(cSock);
connections.erase(fds[i].fd);
delete cSock;
removePollFD(i);
// a new entry was moved to this position, so we check it again
i--;
}
}
}
onStep();
// clean up dead connection sockets
auto it = connections.begin();
while (it != connections.end()) {
CNSocket *cSock = it->second;
if (!cSock->isAlive()) {
killConnection(cSock);
it = connections.erase(it);
removePollFD(cSock->sock);
delete cSock;
} else {
it++;
}
}
}
}
@@ -450,7 +539,7 @@ void CNServer::kill() {
connections.clear();
}
void CNServer::printPacket(CNPacketData *data, int type) {
void CNServer::printPacket(CNPacketData *data) {
if (settings::VERBOSITY < 2)
return;
@@ -469,7 +558,7 @@ void CNServer::printPacket(CNPacketData *data, int type) {
return;
}
std::cout << "OpenFusion: received " << Defines::p2str(type, data->type) << " (" << data->type << ")" << std::endl;
std::cout << "OpenFusion: received " << Packets::p2str(data->type) << " (" << data->type << ")" << std::endl;
}
bool CNServer::checkExtraSockets(int i) { return false; } // stubbed

View File

@@ -53,6 +53,7 @@
#include <algorithm>
#include "Defines.hpp"
#include "Packets.hpp"
#include "settings.hpp"
#if defined(__MINGW32__) && !defined(_GLIBCXX_HAS_GTHREADS)
@@ -82,9 +83,17 @@ inline void* xmalloc(size_t sz) {
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 bool validOutVarPacket(size_t base, int32_t npayloads, size_t plsize) {
inline constexpr bool validOutVarPacket(size_t base, int32_t npayloads, size_t plsize) {
// check for multiplication overflow
if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
return false;
@@ -101,7 +110,7 @@ inline bool validOutVarPacket(size_t base, int32_t npayloads, size_t plsize) {
}
// for inbound packets
inline bool validInVarPacket(size_t base, int32_t npayloads, size_t plsize, size_t datasize) {
inline constexpr bool validInVarPacket(size_t base, int32_t npayloads, size_t plsize, size_t datasize) {
// check for multiplication overflow
if (npayloads > 0 && (CN_PACKET_BUFFER_SIZE - 8) / (size_t)npayloads < plsize)
return false;
@@ -134,11 +143,13 @@ namespace CNSocketEncryption {
}
struct CNPacketData {
void* buf;
void *buf;
int size;
uint32_t type;
int trCnt;
void *trailers;
CNPacketData(void* b, uint32_t t, int l);
CNPacketData(void* b, uint32_t t, int l, int trnum, void *trs);
};
enum ACTIVEKEY {
@@ -164,11 +175,15 @@ private:
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, PacketHandler ph);
CNSocket(SOCKET s, struct sockaddr_in &addr, PacketHandler ph);
void setEKey(uint64_t k);
void setFEKey(uint64_t k);
@@ -180,6 +195,16 @@ public:
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;
@@ -205,6 +230,7 @@ protected:
const size_t STARTFDSCOUNT = 8; // number of initial PollFD slots
std::vector<PollFD> fds;
std::string serverType = "invalid";
SOCKET sock;
uint16_t port;
socklen_t addressSize;
@@ -213,18 +239,18 @@ protected:
bool active = true;
void addPollFD(SOCKET s);
void removePollFD(int i);
public:
PacketHandler pHandler;
CNServer();
CNServer(uint16_t p);
void addPollFD(SOCKET s);
void removePollFD(int i);
void start();
void kill();
static void printPacket(CNPacketData *data, int type);
static void printPacket(CNPacketData *data);
virtual bool checkExtraSockets(int i);
virtual void newConnection(CNSocket* cns);
virtual void killConnection(CNSocket* cns);

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

View File

@@ -28,16 +28,22 @@
#define INITSTRUCT(T, x) T x; \
memset(&x, 0, sizeof(T));
#define INITVARPACKET(_buf, _Pkt, _pkt, _Trailer, _trailer) uint8_t _buf[CN_PACKET_BUFFER_SIZE]; \
memset(&_buf, 0, CN_PACKET_BUFFER_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)
// typedef for chunk position tuple
typedef std::tuple<int, int, uint64_t> ChunkPos;
// wrapper for U16toU8
#define ARRLEN(x) (sizeof(x)/sizeof(*x))
#define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x))
// TODO: rewrite U16toU8 & U8toU16 to not use codecvt
std::string U16toU8(char16_t* src);
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();

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"

View File

@@ -1,13 +1,6 @@
/* enum definitions from the client */
#pragma once
/*
* TODO: It might be a good idea to make this file build-specific, but
* I'm pretty sure there were seldom any new packets added, and they're
* probably always going to be the non-essential things that we won't be
* implementing just yet anyway.
*/
// floats
const float VALUE_BATTERY_EMPTY_PENALTY = 0.5f;
const float CN_EP_RANK_1 = 0.8f;
@@ -16,12 +9,19 @@ const float CN_EP_RANK_3 = 0.5f;
const float CN_EP_RANK_4 = 0.3f;
const float CN_EP_RANK_5 = 0.29f;
// NPC classes
enum NPCClass {
NPC_BASE = 0,
NPC_MOB = 1,
NPC_BUS = 2,
NPC_EGG = 3
// methods of finding players for GM commands
enum eCN_GM_TargetSearchBy {
eCN_GM_TargetSearchBy__PC_ID, // player id
eCN_GM_TargetSearchBy__PC_Name, // firstname, lastname
eCN_GM_TargetSearchBy__PC_UID // account id
};
enum eCN_GM_TeleportType {
eCN_GM_TeleportMapType__XYZ,
eCN_GM_TeleportMapType__MapXYZ,
eCN_GM_TeleportMapType__MyLocation,
eCN_GM_TeleportMapType__SomeoneLocation,
eCN_GM_TeleportMapType__Unstick
};
// nano powers
@@ -925,14 +925,10 @@ enum {
* Each is the last packet - the upper bits + 1
*/
enum {
N_CL2LS = 0xf,
N_CL2FE = 0xa5,
N_FE2CL = 0x12f,
N_LS2CL = 0x1a,
N_CL2LS = 0xf,
N_CL2FE = 0xa5,
N_FE2CL = 0x12f,
N_LS2CL = 0x1a,
N_PACKETS = N_CL2LS + N_CL2FE + N_FE2CL + N_LS2CL
N_PACKETS = N_CL2LS + N_CL2FE + N_FE2CL + N_LS2CL
};
namespace Defines {
std::string p2str(int type, int val);
}

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),
PACKET(P_FE2CL_PC_AROUND),
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),
PACKET(P_FE2CL_NPC_AROUND),
PACKET(P_FE2CL_AROUND_DEL_PC),
PACKET(P_FE2CL_AROUND_DEL_NPC),
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),
PACKET(P_FE2CL_TRANSPORTATION_AROUND),
PACKET(P_FE2CL_AROUND_DEL_TRANSPORTATION),
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),
PACKET(P_FE2CL_SHINY_AROUND),
PACKET(P_FE2CL_AROUND_DEL_SHINY),
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 eCT;
uint32_t iID;
};
struct sSkillResult_Leech {
sSkillResult_Heal_HP Heal;
sSkillResult_Damage Damage;
};
namespace Packets {
extern std::map<uint32_t, PacketDesc> packets;
std::string p2str(int val);
}

View File

@@ -1,10 +1,11 @@
#pragma once
#include "CNStructs.hpp"
#include "Player.hpp"
#include <string>
#include <vector>
#define DATABASE_VERSION 2
#define DATABASE_VERSION 4
namespace Database {
@@ -15,6 +16,7 @@ namespace Database {
time_t BannedUntil;
std::string BanReason;
};
struct EmailData {
int PlayerId;
int MsgIndex;
@@ -29,6 +31,7 @@ namespace Database {
uint64_t SendTime;
uint64_t DeleteTime;
};
struct RaceRanking {
int EPID;
int PlayerID;
@@ -38,18 +41,20 @@ namespace Database {
uint64_t Timestamp;
};
void init();
void open();
void close();
void checkMetaTable();
void createMetaTable();
void createTables();
int getTableSize(std::string tableName);
void findAccount(Account* account, std::string login);
/// returns ID, 0 if something failed
// returns ID, 0 if something failed
int addAccount(std::string login, std::string password);
void banAccount(int accountId, int days);
void updateSelected(int accountId, int playerId);
// 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);
@@ -63,6 +68,7 @@ namespace Database {
/// 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,
@@ -74,8 +80,9 @@ namespace Database {
// getting players
void getPlayer(Player* plr, int id);
bool _updatePlayer(Player *player);
void updatePlayer(Player *player);
void removeExpiredVehicles(Player* player);
void commitTrade(Player *plr1, Player *plr2);
// buddies
int getNumBuddies(Player* player);
@@ -95,9 +102,13 @@ namespace Database {
void deleteEmailAttachments(int playerID, int index, int slot);
void deleteEmails(int playerID, int64_t* indices);
int getNextEmailIndex(int playerID);
bool sendEmail(EmailData* data, std::vector<sItemBase> attachments);
bool sendEmail(EmailData* data, std::vector<sItemBase> attachments, Player *sender);
// racing
RaceRanking getTopRaceRanking(int epID, int playerID);
void postRaceRanking(RaceRanking ranking);
// code items
bool isCodeRedeemed(int playerId, std::string code);
void recordCodeRedemption(int playerId, std::string code);
}

343
src/db/email.cpp Normal file
View File

@@ -0,0 +1,343 @@
#include "db/internal.hpp"
// Email-related DB interactions
int Database::getUnreadEmailCount(int playerID) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT COUNT(*) FROM EmailData
WHERE PlayerID = ? AND ReadFlag = 0;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerID);
sqlite3_step(stmt);
int ret = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return ret;
}
std::vector<EmailData> Database::getEmails(int playerID, int page) {
std::lock_guard<std::mutex> lock(dbCrit);
std::vector<EmailData> emails;
const char* sql = R"(
SELECT
MsgIndex, ItemFlag, ReadFlag, SenderID,
SenderFirstName, SenderLastName, SubjectLine,
MsgBody, Taros, SendTime, DeleteTime
FROM EmailData
WHERE PlayerID = ?
ORDER BY MsgIndex DESC
LIMIT 5
OFFSET ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerID);
int offset = 5 * page - 5;
sqlite3_bind_int(stmt, 2, offset);
while (sqlite3_step(stmt) == SQLITE_ROW) {
EmailData toAdd;
toAdd.PlayerId = playerID;
toAdd.MsgIndex = sqlite3_column_int(stmt, 0);
toAdd.ItemFlag = sqlite3_column_int(stmt, 1);
toAdd.ReadFlag = sqlite3_column_int(stmt, 2);
toAdd.SenderId = sqlite3_column_int(stmt, 3);
toAdd.SenderFirstName = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4)));
toAdd.SenderLastName = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 5)));
toAdd.SubjectLine = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6)));
toAdd.MsgBody = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 7)));
toAdd.Taros = sqlite3_column_int(stmt, 8);
toAdd.SendTime = sqlite3_column_int64(stmt, 9);
toAdd.DeleteTime = sqlite3_column_int64(stmt, 10);
emails.push_back(toAdd);
}
sqlite3_finalize(stmt);
return emails;
}
EmailData Database::getEmail(int playerID, int index) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT
ItemFlag, ReadFlag, SenderID, SenderFirstName,
SenderLastName, SubjectLine, MsgBody,
Taros, SendTime, DeleteTime
FROM EmailData
WHERE PlayerID = ? AND MsgIndex = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerID);
sqlite3_bind_int(stmt, 2, index);
EmailData result;
if (sqlite3_step(stmt) != SQLITE_ROW) {
std::cout << "[WARN] Database: Email not found!" << std::endl;
sqlite3_finalize(stmt);
return result;
}
result.PlayerId = playerID;
result.MsgIndex = index;
result.ItemFlag = sqlite3_column_int(stmt, 0);
result.ReadFlag = sqlite3_column_int(stmt, 1);
result.SenderId = sqlite3_column_int(stmt, 2);
result.SenderFirstName = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3)));
result.SenderLastName = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4)));
result.SubjectLine = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 5)));
result.MsgBody = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6)));
result.Taros = sqlite3_column_int(stmt, 7);
result.SendTime = sqlite3_column_int64(stmt, 8);
result.DeleteTime = sqlite3_column_int64(stmt, 9);
sqlite3_finalize(stmt);
return result;
}
sItemBase* Database::getEmailAttachments(int playerID, int index) {
std::lock_guard<std::mutex> lock(dbCrit);
sItemBase* items = new sItemBase[4];
for (int i = 0; i < 4; i++)
items[i] = { 0, 0, 0, 0 };
const char* sql = R"(
SELECT Slot, ID, Type, Opt, TimeLimit
FROM EmailItems
WHERE PlayerID = ? AND MsgIndex = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerID);
sqlite3_bind_int(stmt, 2, index);
while (sqlite3_step(stmt) == SQLITE_ROW) {
int slot = sqlite3_column_int(stmt, 0) - 1;
if (slot < 0 || slot > 3) {
std::cout << "[WARN] Email item has invalid slot number ?!" << std::endl;
continue;
}
items[slot].iID = sqlite3_column_int(stmt, 1);
items[slot].iType = sqlite3_column_int(stmt, 2);
items[slot].iOpt = sqlite3_column_int(stmt, 3);
items[slot].iTimeLimit = sqlite3_column_int(stmt, 4);
}
sqlite3_finalize(stmt);
return items;
}
void Database::updateEmailContent(EmailData* data) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT COUNT(*)
FROM EmailItems
WHERE PlayerID = ? AND MsgIndex = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, data->PlayerId);
sqlite3_bind_int(stmt, 2, data->MsgIndex);
sqlite3_step(stmt);
int attachmentsCount = sqlite3_column_int(stmt, 0);
// set attachment flag dynamically
data->ItemFlag = (data->Taros > 0 || attachmentsCount > 0) ? 1 : 0;
sqlite3_finalize(stmt);
sql = R"(
UPDATE EmailData
SET
PlayerID = ?,
MsgIndex = ?,
ReadFlag = ?,
ItemFlag = ?,
SenderID = ?,
SenderFirstName = ?,
SenderLastName = ?,
SubjectLine = ?,
MsgBody = ?,
Taros = ?,
SendTime = ?,
DeleteTime = ?
WHERE PlayerID = ? AND MsgIndex = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, data->PlayerId);
sqlite3_bind_int(stmt, 2, data->MsgIndex);
sqlite3_bind_int(stmt, 3, data->ReadFlag);
sqlite3_bind_int(stmt, 4, data->ItemFlag);
sqlite3_bind_int(stmt, 5, data->SenderId);
sqlite3_bind_text(stmt, 6, data->SenderFirstName.c_str(), -1, NULL);
sqlite3_bind_text(stmt, 7, data->SenderLastName.c_str(), -1, NULL);
sqlite3_bind_text(stmt, 8, data->SubjectLine.c_str(), -1, NULL);
sqlite3_bind_text(stmt, 9, data->MsgBody.c_str(), -1, NULL);
sqlite3_bind_int(stmt, 10, data->Taros);
sqlite3_bind_int64(stmt, 11, data->SendTime);
sqlite3_bind_int64(stmt, 12, data->DeleteTime);
sqlite3_bind_int(stmt, 13, data->PlayerId);
sqlite3_bind_int(stmt, 14, data->MsgIndex);
if (sqlite3_step(stmt) != SQLITE_DONE)
std::cout << "[WARN] Database: failed to update email: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
}
void Database::deleteEmailAttachments(int playerID, int index, int slot) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_stmt* stmt;
std::string sql(R"(
DELETE FROM EmailItems
WHERE PlayerID = ? AND MsgIndex = ?;
)");
if (slot != -1)
sql += " AND \"Slot\" = ? ";
sql += ";";
sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerID);
sqlite3_bind_int(stmt, 2, index);
if (slot != -1)
sqlite3_bind_int(stmt, 3, slot);
if (sqlite3_step(stmt) != SQLITE_DONE)
std::cout << "[WARN] Database: Failed to delete email attachments: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
}
void Database::deleteEmails(int playerID, int64_t* indices) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
sqlite3_stmt* stmt;
const char* sql = R"(
DELETE FROM EmailData
WHERE PlayerID = ? AND MsgIndex = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
for (int i = 0; i < 5; i++) {
sqlite3_bind_int(stmt, 1, playerID);
sqlite3_bind_int64(stmt, 2, indices[i]);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to delete an email: " << sqlite3_errmsg(db) << std::endl;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
}
int Database::getNextEmailIndex(int playerID) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT MsgIndex
FROM EmailData
WHERE PlayerID = ?
ORDER BY MsgIndex DESC
LIMIT 1;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerID);
sqlite3_step(stmt);
int index = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return (index > 0 ? index + 1 : 1);
}
bool Database::sendEmail(EmailData* data, std::vector<sItemBase> attachments, Player *sender) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
const char* sql = R"(
INSERT INTO EmailData
(PlayerID, MsgIndex, ReadFlag, ItemFlag,
SenderID, SenderFirstName, SenderLastName,
SubjectLine, MsgBody, Taros, SendTime, DeleteTime)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, data->PlayerId);
sqlite3_bind_int(stmt, 2, data->MsgIndex);
sqlite3_bind_int(stmt, 3, data->ReadFlag);
sqlite3_bind_int(stmt, 4, data->ItemFlag);
sqlite3_bind_int(stmt, 5, data->SenderId);
sqlite3_bind_text(stmt, 6, data->SenderFirstName.c_str(), -1, NULL);
sqlite3_bind_text(stmt, 7, data->SenderLastName.c_str(), -1, NULL);
sqlite3_bind_text(stmt, 8, data->SubjectLine.c_str(), -1, NULL);
sqlite3_bind_text(stmt, 9, data->MsgBody.c_str(), -1, NULL);
sqlite3_bind_int(stmt, 10, data->Taros);
sqlite3_bind_int64(stmt, 11, data->SendTime);
sqlite3_bind_int64(stmt, 12, data->DeleteTime);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to send email: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return false;
}
sqlite3_finalize(stmt);
sql = R"(
INSERT INTO EmailItems
(PlayerID, MsgIndex, Slot, ID, Type, Opt, TimeLimit)
VALUES (?, ?, ?, ?, ?, ?, ?);
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
// send attachments
int slot = 1;
for (sItemBase item : attachments) {
sqlite3_bind_int(stmt, 1, data->PlayerId);
sqlite3_bind_int(stmt, 2, data->MsgIndex);
sqlite3_bind_int(stmt, 3, slot++);
sqlite3_bind_int(stmt, 4, item.iID);
sqlite3_bind_int(stmt, 5, item.iType);
sqlite3_bind_int(stmt, 6, item.iOpt);
sqlite3_bind_int(stmt, 7, item.iTimeLimit);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to send email: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
sqlite3_finalize(stmt);
return false;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
if (!_updatePlayer(sender)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return false;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
return true;
}

288
src/db/init.cpp Normal file
View File

@@ -0,0 +1,288 @@
#include "db/internal.hpp"
#include "settings.hpp"
#include <string>
#include <iostream>
#include <fstream>
#include <sstream>
std::mutex dbCrit;
sqlite3 *db;
/*
* When migrating from DB version 3 to 4, we change the username column
* to be case-insensitive. This function ensures there aren't any
* duplicates, e.g. username and USERNAME, before doing the migration.
* I handled this in the code itself rather than the migration file just so
* we can have a more detailed error message than what SQLite provides.
*/
static void checkCaseSensitiveDupes() {
const char* sql = "SELECT Login, COUNT(*) FROM Accounts GROUP BY LOWER(Login) HAVING COUNT(*) > 1;";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
int stat = sqlite3_step(stmt);
if (stat == SQLITE_DONE) {
// no rows returned, so we're good
sqlite3_finalize(stmt);
return;
} else if (stat != SQLITE_ROW) {
std::cout << "[FATAL] Failed to check for duplicate accounts: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
exit(1);
}
std::cout << "[FATAL] Case-sensitive duplicates detected in the Login column." << std::endl;
std::cout << "Either manually delete/rename the offending accounts, or run the pruning script:" << std::endl;
std::cout << "https://github.com/OpenFusionProject/scripts/tree/main/db_migration/caseinsens.py" << std::endl;
sqlite3_finalize(stmt);
exit(1);
}
static void createMetaTable() {
std::lock_guard<std::mutex> lock(dbCrit); // XXX
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
const char* sql = R"(
CREATE TABLE Meta(
Key TEXT NOT NULL UNIQUE,
Value INTEGER NOT NULL
);
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[FATAL] Failed to create meta table: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
exit(1);
}
sqlite3_finalize(stmt);
sql = R"(
INSERT INTO Meta (Key, Value)
VALUES (?, ?);
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, "ProtocolVersion", -1, NULL);
sqlite3_bind_int(stmt, 2, PROTOCOL_VERSION);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[FATAL] Failed to create meta table: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
exit(1);
}
sqlite3_reset(stmt);
sqlite3_bind_text(stmt, 1, "DatabaseVersion", -1, NULL);
sqlite3_bind_int(stmt, 2, DATABASE_VERSION);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
std::cout << "[FATAL] Failed to create meta table: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
exit(1);
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
std::cout << "[INFO] Created new meta table" << std::endl;
}
static void checkMetaTable() {
// first check if meta table exists
const char* sql = R"(
SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='Meta';
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (sqlite3_step(stmt) != SQLITE_ROW) {
std::cout << "[FATAL] Failed to check meta table: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
exit(1);
}
int count = sqlite3_column_int(stmt, 0);
if (count == 0) {
sqlite3_finalize(stmt);
// check if there's other non-internal tables first
sql = R"(
SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%';
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (sqlite3_step(stmt) != SQLITE_ROW || sqlite3_column_int(stmt, 0) != 0) {
sqlite3_finalize(stmt);
std::cout << "[FATAL] Existing DB is outdated" << std::endl;
exit(1);
}
// create meta table
sqlite3_finalize(stmt);
return createMetaTable();
}
sqlite3_finalize(stmt);
// check protocol version
sql = R"(
SELECT Value FROM Meta WHERE Key = 'ProtocolVersion';
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (sqlite3_step(stmt) != SQLITE_ROW) {
std::cout << "[FATAL] Failed to check DB Protocol Version: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
exit(1);
}
if (sqlite3_column_int(stmt, 0) != PROTOCOL_VERSION) {
sqlite3_finalize(stmt);
std::cout << "[FATAL] DB Protocol Version doesn't match Server Build" << std::endl;
exit(1);
}
sqlite3_finalize(stmt);
sql = R"(
SELECT Value FROM Meta WHERE Key = 'DatabaseVersion';
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (sqlite3_step(stmt) != SQLITE_ROW) {
std::cout << "[FATAL] Failed to check DB Version: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
exit(1);
}
int dbVersion = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
if (dbVersion > DATABASE_VERSION) {
std::cout << "[FATAL] Server Build is incompatible with DB Version" << std::endl;
exit(1);
} else if (dbVersion < DATABASE_VERSION) {
// we're gonna migrate; back up the DB
std::cout << "[INFO] Backing up database" << std::endl;
// copy db file over using binary streams
std::ifstream src(settings::DBPATH, std::ios::binary);
std::ofstream dst(settings::DBPATH + ".old." + std::to_string(dbVersion), std::ios::binary);
dst << src.rdbuf();
src.close();
dst.close();
}
while (dbVersion != DATABASE_VERSION) {
// need to run this before we do any migration logic
if (dbVersion == 3)
checkCaseSensitiveDupes();
// db migrations
std::cout << "[INFO] Migrating Database to Version " << dbVersion + 1 << std::endl;
std::string path = "sql/migration" + std::to_string(dbVersion) + ".sql";
std::ifstream file(path);
if (!file.is_open()) {
std::cout << "[FATAL] Failed to migrate database: Couldn't open migration file" << std::endl;
exit(1);
}
std::ostringstream stream;
stream << file.rdbuf();
std::string sql = stream.str();
int rc = sqlite3_exec(db, sql.c_str(), NULL, NULL, NULL);
if (rc != SQLITE_OK) {
std::cout << "[FATAL] Failed to migrate database: " << sqlite3_errmsg(db) << std::endl;
exit(1);
}
dbVersion++;
std::cout << "[INFO] Successful Database Migration to Version " << dbVersion << std::endl;
}
}
static void createTables() {
std::ifstream file("sql/tables.sql");
if (!file.is_open()) {
std::cout << "[FATAL] Failed to open database scheme" << std::endl;
exit(1);
}
std::ostringstream stream;
stream << file.rdbuf();
std::string read = stream.str();
const char* sql = read.c_str();
char* errMsg = 0;
int rc = sqlite3_exec(db, sql, NULL, NULL, &errMsg);
if (rc != SQLITE_OK) {
std::cout << "[FATAL] Database failed to create tables: " << errMsg << std::endl;
exit(1);
}
}
static int getTableSize(std::string tableName) {
std::lock_guard<std::mutex> lock(dbCrit); // XXX
const char* sql = "SELECT COUNT(*) FROM ?";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, tableName.c_str(), -1, NULL);
sqlite3_step(stmt);
int result = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return result;
}
void Database::init() {
std::cout << "[INFO] Built with libsqlite " SQLITE_VERSION << std::endl;
if (sqlite3_libversion_number() != SQLITE_VERSION_NUMBER)
std::cout << "[INFO] Using libsqlite " << std::string(sqlite3_libversion()) << std::endl;
if (sqlite3_libversion_number() < MIN_SUPPORTED_SQLITE_NUMBER) {
std::cerr << "[FATAL] Runtime sqlite version too old. Minimum compatible version: " MIN_SUPPORTED_SQLITE << std::endl;
exit(1);
}
}
void Database::open() {
// XXX: move locks here
int rc = sqlite3_open(settings::DBPATH.c_str(), &db);
if (rc != SQLITE_OK) {
std::cout << "[FATAL] Cannot open database: " << sqlite3_errmsg(db) << std::endl;
exit(1);
}
// foreign keys in sqlite are off by default; enable them
sqlite3_exec(db, "PRAGMA foreign_keys=ON;", NULL, NULL, NULL);
// just in case a DB operation collides with an external manual modification
sqlite3_busy_timeout(db, 2000);
checkMetaTable();
createTables();
std::cout << "[INFO] Database in operation ";
int accounts = getTableSize("Accounts");
int players = getTableSize("Players");
std::string message = "";
if (accounts > 0) {
message += ": Found " + std::to_string(accounts) + " Account";
if (accounts > 1)
message += "s";
}
if (players > 0) {
message += " and " + std::to_string(players) + " Player Character";
if (players > 1)
message += "s";
}
std::cout << message << std::endl;
}
void Database::close() {
sqlite3_close(db);
}

18
src/db/internal.hpp Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
#include "db/Database.hpp"
#include <sqlite3.h>
#define MIN_SUPPORTED_SQLITE_NUMBER 3033000
#define MIN_SUPPORTED_SQLITE "3.33.0"
// we can't use this in #error, since it doesn't expand macros
// Compile-time libsqlite version check
#if SQLITE_VERSION_NUMBER < MIN_SUPPORTED_SQLITE_NUMBER
#error libsqlite version too old. Minimum compatible version: 3.33.0
#endif
extern std::mutex dbCrit;
extern sqlite3 *db;
using namespace Database;

559
src/db/login.cpp Normal file
View File

@@ -0,0 +1,559 @@
#include "db/internal.hpp"
#include "bcrypt/BCrypt.hpp"
void Database::findAccount(Account* account, std::string login) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT AccountID, Password, Selected, BannedUntil, BanReason
FROM Accounts
WHERE Login = ?
LIMIT 1;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, login.c_str(), -1, NULL);
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
account->AccountID = sqlite3_column_int(stmt, 0);
account->Password = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1)));
account->Selected = sqlite3_column_int(stmt, 2);
account->BannedUntil = sqlite3_column_int64(stmt, 3);
account->BanReason = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4));
}
sqlite3_finalize(stmt);
}
int Database::addAccount(std::string login, std::string password) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
INSERT INTO Accounts (Login, Password, AccountLevel)
VALUES (?, ?, ?);
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, login.c_str(), -1, NULL);
std::string hashedPassword = BCrypt::generateHash(password);
sqlite3_bind_text(stmt, 2, hashedPassword.c_str(), -1, NULL);
sqlite3_bind_int(stmt, 3, settings::ACCLEVEL);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
std::cout << "[WARN] Database: failed to add new account" << std::endl;
return 0;
}
return sqlite3_last_insert_rowid(db);
}
void Database::updateSelected(int accountId, int slot) {
std::lock_guard<std::mutex> lock(dbCrit);
if (slot < 1 || slot > 4) {
std::cout << "[WARN] Invalid slot number passed to updateSelected()! " << std::endl;
return;
}
const char* sql = R"(
UPDATE Accounts SET
Selected = ?,
LastLogin = (strftime('%s', 'now'))
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, slot);
sqlite3_bind_int(stmt, 2, accountId);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on updateSelected(): " << sqlite3_errmsg(db) << std::endl;
}
void Database::updateSelectedByPlayerId(int accountId, int32_t playerId) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
UPDATE Accounts SET
Selected = p.Slot,
LastLogin = (strftime('%s', 'now'))
FROM (SELECT Slot From Players WHERE PlayerId = ?) AS p
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
sqlite3_bind_int(stmt, 2, accountId);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on updateSelectedByPlayerId(): " << sqlite3_errmsg(db) << std::endl;
}
bool Database::validateCharacter(int characterID, int userID) {
std::lock_guard<std::mutex> lock(dbCrit);
// query whatever
const char* sql = R"(
SELECT PlayerID
FROM Players
WHERE PlayerID = ? AND AccountID = ?
LIMIT 1;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, characterID);
sqlite3_bind_int(stmt, 2, userID);
int rc = sqlite3_step(stmt);
// if we got a row back, the character is valid
bool result = (rc == SQLITE_ROW);
sqlite3_finalize(stmt);
return result;
}
bool Database::isNameFree(std::string firstName, std::string lastName) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT COUNT(*)
FROM Players
WHERE FirstName = ? AND LastName = ?
LIMIT 1;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, firstName.c_str(), -1, NULL);
sqlite3_bind_text(stmt, 2, lastName.c_str(), -1, NULL);
int rc = sqlite3_step(stmt);
bool result = (rc == SQLITE_ROW && sqlite3_column_int(stmt, 0) == 0);
sqlite3_finalize(stmt);
return result;
}
bool Database::isSlotFree(int accountId, int slotNum) {
std::lock_guard<std::mutex> lock(dbCrit);
if (slotNum < 1 || slotNum > 4) {
std::cout << "[WARN] Invalid slot number passed to isSlotFree()! " << slotNum << std::endl;
return false;
}
const char* sql = R"(
SELECT COUNT(*)
FROM Players
WHERE AccountID = ? AND Slot = ?
LIMIT 1;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId);
sqlite3_bind_int(stmt, 2, slotNum);
int rc = sqlite3_step(stmt);
bool result = (rc == SQLITE_ROW && sqlite3_column_int(stmt, 0) == 0);
sqlite3_finalize(stmt);
return result;
}
int Database::createCharacter(sP_CL2LS_REQ_SAVE_CHAR_NAME* save, int AccountID) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
const char* sql = R"(
INSERT INTO Players
(AccountID, Slot, FirstName, LastName,
XCoordinate, YCoordinate, ZCoordinate, Angle,
HP, NameCheck, Quests, SkywayLocationFlag, FirstUseFlag)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
)";
sqlite3_stmt* stmt;
std::string firstName = AUTOU16TOU8(save->szFirstName);
std::string lastName = AUTOU16TOU8(save->szLastName);
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, AccountID);
sqlite3_bind_int(stmt, 2, save->iSlotNum);
sqlite3_bind_text(stmt, 3, firstName.c_str(), -1, NULL);
sqlite3_bind_text(stmt, 4, lastName.c_str(), -1, NULL);
sqlite3_bind_int(stmt, 5, settings::SPAWN_X);
sqlite3_bind_int(stmt, 6, settings::SPAWN_Y);
sqlite3_bind_int(stmt, 7, settings::SPAWN_Z);
sqlite3_bind_int(stmt, 8, settings::SPAWN_ANGLE);
sqlite3_bind_int(stmt, 9, PC_MAXHEALTH(1));
// if FNCode isn't 0, it's a wheel name
int nameCheck = (settings::APPROVEALLNAMES || save->iFNCode) ? 1 : 0;
sqlite3_bind_int(stmt, 10, nameCheck);
// blobs
unsigned char blobBuffer[sizeof(Player::aQuestFlag)] = { 0 };
sqlite3_bind_blob(stmt, 11, blobBuffer, sizeof(Player::aQuestFlag), NULL);
sqlite3_bind_blob(stmt, 12, blobBuffer, sizeof(Player::aSkywayLocationFlag), NULL);
sqlite3_bind_blob(stmt, 13, blobBuffer, sizeof(Player::iFirstUseFlag), NULL);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return 0;
}
int playerId = sqlite3_last_insert_rowid(db);
sqlite3_finalize(stmt);
sql = R"(
INSERT INTO Appearances (PlayerID)
VALUES (?);
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return 0;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
return playerId;
}
bool Database::finishCharacter(sP_CL2LS_REQ_CHAR_CREATE* character, int accountId) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
const char* sql = R"(
UPDATE Players
SET AppearanceFlag = 1
WHERE PlayerID = ? AND AccountID = ? AND AppearanceFlag = 0;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, character->PCStyle.iPC_UID);
sqlite3_bind_int(stmt, 2, accountId);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return false;
}
sqlite3_finalize(stmt);
sql = R"(
UPDATE Appearances
SET
Body = ?,
EyeColor = ?,
FaceStyle = ?,
Gender = ?,
HairColor = ?,
HairStyle = ?,
Height = ?,
SkinColor = ?
WHERE PlayerID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, character->PCStyle.iBody);
sqlite3_bind_int(stmt, 2, character->PCStyle.iEyeColor);
sqlite3_bind_int(stmt, 3, character->PCStyle.iFaceStyle);
sqlite3_bind_int(stmt, 4, character->PCStyle.iGender);
sqlite3_bind_int(stmt, 5, character->PCStyle.iHairColor);
sqlite3_bind_int(stmt, 6, character->PCStyle.iHairStyle);
sqlite3_bind_int(stmt, 7, character->PCStyle.iHeight);
sqlite3_bind_int(stmt, 8, character->PCStyle.iSkinColor);
sqlite3_bind_int(stmt, 9, character->PCStyle.iPC_UID);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return false;
}
sqlite3_finalize(stmt);
sql = R"(
INSERT INTO Inventory (PlayerID, Slot, ID, Type, Opt)
VALUES (?, ?, ?, ?, 1);
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, 0);
int items[3] = { character->sOn_Item.iEquipUBID, character->sOn_Item.iEquipLBID, character->sOn_Item.iEquipFootID };
for (int i = 0; i < 3; i++) {
sqlite3_bind_int(stmt, 1, character->PCStyle.iPC_UID);
sqlite3_bind_int(stmt, 2, i+1);
sqlite3_bind_int(stmt, 3, items[i]);
sqlite3_bind_int(stmt, 4, i+1);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return false;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
return true;
}
bool Database::finishTutorial(int playerID, int accountID) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
const char* sql = R"(
UPDATE Players SET
TutorialFlag = 1,
Nano1 = ?,
Quests = ?
WHERE PlayerID = ? AND AccountID = ? AND TutorialFlag = 0;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
unsigned char questBuffer[128] = { 0 };
#ifndef ACADEMY
// save missions nr 1 & 2; equip Buttercup
questBuffer[0] = 3;
sqlite3_bind_int(stmt, 1, 1);
#else
// no, none of that
sqlite3_bind_int(stmt, 1, 0);
#endif
sqlite3_bind_blob(stmt, 2, questBuffer, sizeof(questBuffer), NULL);
sqlite3_bind_int(stmt, 3, playerID);
sqlite3_bind_int(stmt, 4, accountID);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return false;
}
sqlite3_finalize(stmt);
#ifndef ACADEMY
// Lightning Gun
sql = R"(
INSERT INTO Inventory
(PlayerID, Slot, ID, Type, Opt)
VALUES (?, 0, 328, 0, 1);
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerID);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return false;
}
sqlite3_finalize(stmt);
// Nano Buttercup
sql = R"(
INSERT INTO Nanos
(PlayerID, ID, Skill)
VALUES (?, 1, 1);
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerID);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return false;
}
#endif
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
return true;
}
int Database::deleteCharacter(int characterID, int userID) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT Slot
FROM Players
WHERE AccountID = ? AND PlayerID = ?
LIMIT 1;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, userID);
sqlite3_bind_int(stmt, 2, characterID);
if (sqlite3_step(stmt) != SQLITE_ROW) {
sqlite3_finalize(stmt);
return 0;
}
int slot = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
sql = R"(
DELETE FROM Players
WHERE AccountID = ? AND PlayerID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, 0);
sqlite3_bind_int(stmt, 1, userID);
sqlite3_bind_int(stmt, 2, characterID);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
return 0;
return slot;
}
void Database::getCharInfo(std::vector <sP_LS2CL_REP_CHAR_INFO>* result, int userID) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT
p.PlayerID, p.Slot, p.FirstName, p.LastName, p.Level, p.AppearanceFlag, p.TutorialFlag, p.PayZoneFlag,
p.XCoordinate, p.YCoordinate, p.ZCoordinate, p.NameCheck,
a.Body, a.EyeColor, a.FaceStyle, a.Gender, a.HairColor, a.HairStyle, a.Height, a.SkinColor
FROM Players as p
INNER JOIN Appearances as a ON p.PlayerID = a.PlayerID
WHERE p.AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, userID);
while (sqlite3_step(stmt) == SQLITE_ROW) {
sP_LS2CL_REP_CHAR_INFO toAdd = {};
toAdd.sPC_Style.iPC_UID = sqlite3_column_int(stmt, 0);
toAdd.iSlot = sqlite3_column_int(stmt, 1);
// parsing const unsigned char* to char16_t
std::string placeHolder = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2)));
U8toU16(placeHolder, toAdd.sPC_Style.szFirstName, sizeof(toAdd.sPC_Style.szFirstName));
placeHolder = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3)));
U8toU16(placeHolder, toAdd.sPC_Style.szLastName, sizeof(toAdd.sPC_Style.szLastName));
toAdd.iLevel = sqlite3_column_int(stmt, 4);
toAdd.sPC_Style2.iAppearanceFlag = sqlite3_column_int(stmt, 5);
toAdd.sPC_Style2.iTutorialFlag = sqlite3_column_int(stmt, 6);
toAdd.sPC_Style2.iPayzoneFlag = sqlite3_column_int(stmt, 7);
toAdd.iX = sqlite3_column_int(stmt, 8);
toAdd.iY = sqlite3_column_int(stmt, 9);
toAdd.iZ = sqlite3_column_int(stmt, 10);
toAdd.sPC_Style.iNameCheck = sqlite3_column_int(stmt, 11);
toAdd.sPC_Style.iBody = sqlite3_column_int(stmt, 12);
toAdd.sPC_Style.iEyeColor = sqlite3_column_int(stmt, 13);
toAdd.sPC_Style.iFaceStyle = sqlite3_column_int(stmt, 14);
toAdd.sPC_Style.iGender = sqlite3_column_int(stmt, 15);
toAdd.sPC_Style.iHairColor = sqlite3_column_int(stmt, 16);
toAdd.sPC_Style.iHairStyle = sqlite3_column_int(stmt, 17);
toAdd.sPC_Style.iHeight = sqlite3_column_int(stmt, 18);
toAdd.sPC_Style.iSkinColor = sqlite3_column_int(stmt, 19);
// request aEquip
const char* sql2 = R"(
SELECT Slot, Type, ID, Opt, TimeLimit
FROM Inventory
WHERE PlayerID = ? AND Slot < ?;
)";
sqlite3_stmt* stmt2;
sqlite3_prepare_v2(db, sql2, -1, &stmt2, NULL);
sqlite3_bind_int(stmt2, 1, toAdd.sPC_Style.iPC_UID);
sqlite3_bind_int(stmt2, 2, AEQUIP_COUNT);
while (sqlite3_step(stmt2) == SQLITE_ROW) {
sItemBase* item = &toAdd.aEquip[sqlite3_column_int(stmt2, 0)];
item->iType = sqlite3_column_int(stmt2, 1);
item->iID = sqlite3_column_int(stmt2, 2);
item->iOpt = sqlite3_column_int(stmt2, 3);
item->iTimeLimit = sqlite3_column_int(stmt2, 4);
}
sqlite3_finalize(stmt2);
result->push_back(toAdd);
}
sqlite3_finalize(stmt);
}
// NOTE: This is currently never called.
void Database::evaluateCustomName(int characterID, CustomName decision) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
UPDATE Players
SET NameCheck = ?
WHERE PlayerID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, int(decision));
sqlite3_bind_int(stmt, 2, characterID);
if (sqlite3_step(stmt) != SQLITE_DONE)
std::cout << "[WARN] Database: Failed to update nameCheck: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
}
bool Database::changeName(sP_CL2LS_REQ_CHANGE_CHAR_NAME* save, int accountId) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
UPDATE Players
SET
FirstName = ?,
LastName = ?,
NameCheck = ?
WHERE PlayerID = ? AND AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
std::string firstName = AUTOU16TOU8(save->szFirstName);
std::string lastName = AUTOU16TOU8(save->szLastName);
sqlite3_bind_text(stmt, 1, firstName.c_str(), -1, NULL);
sqlite3_bind_text(stmt, 2, lastName.c_str(), -1, NULL);
// if FNCode isn't 0, it's a wheel name
int nameCheck = (settings::APPROVEALLNAMES || save->iFNCode) ? 1 : 0;
sqlite3_bind_int(stmt, 3, nameCheck);
sqlite3_bind_int(stmt, 4, save->iPCUID);
sqlite3_bind_int(stmt, 5, accountId);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return rc == SQLITE_DONE;
}

560
src/db/player.cpp Normal file
View File

@@ -0,0 +1,560 @@
#include "db/internal.hpp"
// Loading and saving players to/from the DB
static void removeExpiredVehicles(Player* player) {
int32_t currentTime = getTimestamp();
// if there are expired vehicles in bank just remove them silently
for (int i = 0; i < ABANK_COUNT; i++) {
if (player->Bank[i].iType == 10 && player->Bank[i].iTimeLimit < currentTime && player->Bank[i].iTimeLimit != 0) {
memset(&player->Bank[i], 0, sizeof(sItemBase));
}
}
// we want to leave only 1 expired vehicle on player to delete it with the client packet
std::vector<sItemBase*> toRemove;
// equipped vehicle
if (player->Equip[8].iOpt > 0 && player->Equip[8].iTimeLimit < currentTime && player->Equip[8].iTimeLimit != 0) {
toRemove.push_back(&player->Equip[8]);
player->toRemoveVehicle.eIL = 0;
player->toRemoveVehicle.iSlotNum = 8;
}
// inventory
for (int i = 0; i < AINVEN_COUNT; i++) {
if (player->Inven[i].iType == 10 && player->Inven[i].iTimeLimit < currentTime && player->Inven[i].iTimeLimit != 0) {
toRemove.push_back(&player->Inven[i]);
player->toRemoveVehicle.eIL = 1;
player->toRemoveVehicle.iSlotNum = i;
}
}
// delete all but one vehicles, leave last one for ceremonial deletion
for (int i = 0; i < (int)toRemove.size()-1; i++) {
memset(toRemove[i], 0, sizeof(sItemBase));
}
}
void Database::getPlayer(Player* plr, int id) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT
p.AccountID, p.Slot, p.FirstName, p.LastName,
p.Level, p.Nano1, p.Nano2, p.Nano3,
p.AppearanceFlag, p.TutorialFlag, p.PayZoneFlag,
p.XCoordinate, p.YCoordinate, p.ZCoordinate, p.NameCheck,
p.Angle, p.HP, acc.AccountLevel, p.FusionMatter, p.Taros, p.Quests,
p.BatteryW, p.BatteryN, p.Mentor, p.WarpLocationFlag,
p.SkywayLocationFlag, p.CurrentMissionID, p.FirstUseFlag,
a.Body, a.EyeColor, a.FaceStyle, a.Gender, a.HairColor, a.HairStyle, a.Height, a.SkinColor
FROM Players as p
INNER JOIN Appearances as a ON p.PlayerID = a.PlayerID
INNER JOIN Accounts as acc ON p.AccountID = acc.AccountID
WHERE p.PlayerID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, id);
if (sqlite3_step(stmt) != SQLITE_ROW) {
sqlite3_finalize(stmt);
std::cout << "[WARN] Database: Failed to load character [" << id << "]: " << sqlite3_errmsg(db) << std::endl;
return;
}
plr->iID = id;
plr->PCStyle.iPC_UID = id;
plr->accountId = sqlite3_column_int(stmt, 0);
plr->slot = sqlite3_column_int(stmt, 1);
// parsing const unsigned char* to char16_t
std::string placeHolder = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2)));
U8toU16(placeHolder, plr->PCStyle.szFirstName, sizeof(plr->PCStyle.szFirstName));
placeHolder = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3)));
U8toU16(placeHolder, plr->PCStyle.szLastName, sizeof(plr->PCStyle.szLastName));
plr->level = sqlite3_column_int(stmt, 4);
plr->equippedNanos[0] = sqlite3_column_int(stmt, 5);
plr->equippedNanos[1] = sqlite3_column_int(stmt, 6);
plr->equippedNanos[2] = sqlite3_column_int(stmt, 7);
plr->PCStyle2.iAppearanceFlag = sqlite3_column_int(stmt, 8);
plr->PCStyle2.iTutorialFlag = sqlite3_column_int(stmt, 9);
plr->PCStyle2.iPayzoneFlag = sqlite3_column_int(stmt, 10);
plr->x = sqlite3_column_int(stmt, 11);
plr->y = sqlite3_column_int(stmt, 12);
plr->z = sqlite3_column_int(stmt, 13);
plr->PCStyle.iNameCheck = sqlite3_column_int(stmt, 14);
plr->angle = sqlite3_column_int(stmt, 15);
plr->HP = sqlite3_column_int(stmt, 16);
plr->accountLevel = sqlite3_column_int(stmt, 17);
plr->fusionmatter = sqlite3_column_int(stmt, 18);
plr->money = sqlite3_column_int(stmt, 19);
memcpy(plr->aQuestFlag, sqlite3_column_blob(stmt, 20), sizeof(plr->aQuestFlag));
plr->batteryW = sqlite3_column_int(stmt, 21);
plr->batteryN = sqlite3_column_int(stmt, 22);
plr->mentor = sqlite3_column_int(stmt, 23);
plr->iWarpLocationFlag = sqlite3_column_int(stmt, 24);
memcpy(plr->aSkywayLocationFlag, sqlite3_column_blob(stmt, 25), sizeof(plr->aSkywayLocationFlag));
plr->CurrentMissionID = sqlite3_column_int(stmt, 26);
memcpy(plr->iFirstUseFlag, sqlite3_column_blob(stmt, 27), sizeof(plr->iFirstUseFlag));
plr->PCStyle.iBody = sqlite3_column_int(stmt, 28);
plr->PCStyle.iEyeColor = sqlite3_column_int(stmt, 29);
plr->PCStyle.iFaceStyle = sqlite3_column_int(stmt, 30);
plr->PCStyle.iGender = sqlite3_column_int(stmt, 31);
plr->PCStyle.iHairColor = sqlite3_column_int(stmt, 32);
plr->PCStyle.iHairStyle = sqlite3_column_int(stmt, 33);
plr->PCStyle.iHeight = sqlite3_column_int(stmt, 34);
plr->PCStyle.iSkinColor = sqlite3_column_int(stmt, 35);
sqlite3_finalize(stmt);
// get inventory
sql = R"(
SELECT Slot, Type, ID, Opt, TimeLimit
FROM Inventory
WHERE PlayerID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, id);
while (sqlite3_step(stmt) == SQLITE_ROW) {
int slot = sqlite3_column_int(stmt, 0);
// for extra safety
if (slot < 0 || slot > AEQUIP_COUNT + AINVEN_COUNT + ABANK_COUNT) {
std::cout << "[WARN] Database: Invalid item slot in db?! " << std::endl;
continue;
}
sItemBase* item;
if (slot < AEQUIP_COUNT) {
// equipment
item = &plr->Equip[slot];
} else if (slot < (AEQUIP_COUNT + AINVEN_COUNT)) {
// inventory
item = &plr->Inven[slot - AEQUIP_COUNT];
} else {
// bank
item = &plr->Bank[slot - AEQUIP_COUNT - AINVEN_COUNT];
}
item->iType = sqlite3_column_int(stmt, 1);
item->iID = sqlite3_column_int(stmt, 2);
item->iOpt = sqlite3_column_int(stmt, 3);
item->iTimeLimit = sqlite3_column_int(stmt, 4);
}
sqlite3_finalize(stmt);
removeExpiredVehicles(plr);
// get quest inventory
sql = R"(
SELECT Slot, ID, Opt
FROM QuestItems
WHERE PlayerID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, id);
while (sqlite3_step(stmt) == SQLITE_ROW) {
int slot = sqlite3_column_int(stmt, 0);
// for extra safety
if (slot < 0)
continue;
sItemBase* item = &plr->QInven[slot];
item->iType = 8;
item->iID = sqlite3_column_int(stmt, 1);
item->iOpt = sqlite3_column_int(stmt, 2);
}
sqlite3_finalize(stmt);
// get nanos
sql = R"(
SELECT ID, Skill, Stamina
FROM Nanos
WHERE PlayerID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, id);
while (sqlite3_step(stmt) == SQLITE_ROW) {
int id = sqlite3_column_int(stmt, 0);
// for extra safety
if (id < 0 || id > NANO_COUNT)
continue;
sNano* nano = &plr->Nanos[id];
nano->iID = id;
nano->iSkillID = sqlite3_column_int(stmt, 1);
nano->iStamina = sqlite3_column_int(stmt, 2);
}
sqlite3_finalize(stmt);
// get active quests
sql = R"(
SELECT
TaskID,
RemainingNPCCount1,
RemainingNPCCount2,
RemainingNPCCount3
FROM RunningQuests
WHERE PlayerID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, id);
std::set<int> tasksSet; // used to prevent duplicate tasks from loading in
for (int i = 0; sqlite3_step(stmt) == SQLITE_ROW && i < ACTIVE_MISSION_COUNT; i++) {
int taskID = sqlite3_column_int(stmt, 0);
if (tasksSet.find(taskID) != tasksSet.end())
continue;
plr->tasks[i] = taskID;
tasksSet.insert(taskID);
plr->RemainingNPCCount[i][0] = sqlite3_column_int(stmt, 1);
plr->RemainingNPCCount[i][1] = sqlite3_column_int(stmt, 2);
plr->RemainingNPCCount[i][2] = sqlite3_column_int(stmt, 3);
}
sqlite3_finalize(stmt);
// get buddies
sql = R"(
SELECT PlayerAID, PlayerBID
FROM Buddyships
WHERE PlayerAID = ? OR PlayerBID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, id);
sqlite3_bind_int(stmt, 2, id);
int i = 0;
while (sqlite3_step(stmt) == SQLITE_ROW && i < 50) {
int PlayerAId = sqlite3_column_int(stmt, 0);
int PlayerBId = sqlite3_column_int(stmt, 1);
plr->buddyIDs[i] = id == PlayerAId ? PlayerBId : PlayerAId;
plr->isBuddyBlocked[i] = false;
i++;
}
sqlite3_finalize(stmt);
// get blocked players
sql = R"(
SELECT BlockedPlayerID FROM Blocks
WHERE PlayerID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, id);
// i retains its value from after the loop over Buddyships
while (sqlite3_step(stmt) == SQLITE_ROW && i < 50) {
plr->buddyIDs[i] = sqlite3_column_int(stmt, 0);
plr->isBuddyBlocked[i] = true;
i++;
}
sqlite3_finalize(stmt);
}
/*
* Low-level function to save a player to DB.
* Must be run in a SQL transaction and with dbCrit locked.
* The caller manages the transacstion, so if this function returns false,
* the caller must roll it back.
*/
bool Database::_updatePlayer(Player *player) {
const char* sql = R"(
UPDATE Players
SET
Level = ? , Nano1 = ?, Nano2 = ?, Nano3 = ?,
XCoordinate = ?, YCoordinate = ?, ZCoordinate = ?,
Angle = ?, HP = ?, FusionMatter = ?, Taros = ?, Quests = ?,
BatteryW = ?, BatteryN = ?, WarplocationFlag = ?,
SkywayLocationFlag = ?, CurrentMissionID = ?,
PayZoneFlag = ?, FirstUseFlag = ?, Mentor = ?
WHERE PlayerID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, player->level);
sqlite3_bind_int(stmt, 2, player->equippedNanos[0]);
sqlite3_bind_int(stmt, 3, player->equippedNanos[1]);
sqlite3_bind_int(stmt, 4, player->equippedNanos[2]);
if (player->instanceID == 0 && !player->onMonkey) {
sqlite3_bind_int(stmt, 5, player->x);
sqlite3_bind_int(stmt, 6, player->y);
sqlite3_bind_int(stmt, 7, player->z);
sqlite3_bind_int(stmt, 8, player->angle);
}
else {
sqlite3_bind_int(stmt, 5, player->lastX);
sqlite3_bind_int(stmt, 6, player->lastY);
sqlite3_bind_int(stmt, 7, player->lastZ);
sqlite3_bind_int(stmt, 8, player->lastAngle);
}
sqlite3_bind_int(stmt, 9, player->HP);
sqlite3_bind_int(stmt, 10, player->fusionmatter);
sqlite3_bind_int(stmt, 11, player->money);
sqlite3_bind_blob(stmt, 12, player->aQuestFlag, sizeof(player->aQuestFlag), NULL);
sqlite3_bind_int(stmt, 13, player->batteryW);
sqlite3_bind_int(stmt, 14, player->batteryN);
sqlite3_bind_int(stmt, 15, player->iWarpLocationFlag);
sqlite3_bind_blob(stmt, 16, player->aSkywayLocationFlag, sizeof(player->aSkywayLocationFlag), NULL);
sqlite3_bind_int(stmt, 17, player->CurrentMissionID);
sqlite3_bind_int(stmt, 18, player->PCStyle2.iPayzoneFlag);
sqlite3_bind_blob(stmt, 19, player->iFirstUseFlag, sizeof(player->iFirstUseFlag), NULL);
sqlite3_bind_int(stmt, 20, player->mentor);
sqlite3_bind_int(stmt, 21, player->iID);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
return false;
}
sqlite3_finalize(stmt);
// update inventory
sql = R"(
DELETE FROM Inventory WHERE PlayerID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, player->iID);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
sql = R"(
INSERT INTO Inventory
(PlayerID, Slot, Type, Opt, ID, Timelimit)
VALUES (?, ?, ?, ?, ?, ?);
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
for (int i = 0; i < AEQUIP_COUNT; i++) {
if (player->Equip[i].iID == 0)
continue;
sqlite3_bind_int(stmt, 1, player->iID);
sqlite3_bind_int(stmt, 2, i);
sqlite3_bind_int(stmt, 3, player->Equip[i].iType);
sqlite3_bind_int(stmt, 4, player->Equip[i].iOpt);
sqlite3_bind_int(stmt, 5, player->Equip[i].iID);
sqlite3_bind_int(stmt, 6, player->Equip[i].iTimeLimit);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
sqlite3_finalize(stmt);
return false;
}
sqlite3_reset(stmt);
}
for (int i = 0; i < AINVEN_COUNT; i++) {
if (player->Inven[i].iID == 0)
continue;
sqlite3_bind_int(stmt, 1, player->iID);
sqlite3_bind_int(stmt, 2, i + AEQUIP_COUNT);
sqlite3_bind_int(stmt, 3, player->Inven[i].iType);
sqlite3_bind_int(stmt, 4, player->Inven[i].iOpt);
sqlite3_bind_int(stmt, 5, player->Inven[i].iID);
sqlite3_bind_int(stmt, 6, player->Inven[i].iTimeLimit);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
return false;
}
sqlite3_reset(stmt);
}
for (int i = 0; i < ABANK_COUNT; i++) {
if (player->Bank[i].iID == 0)
continue;
sqlite3_bind_int(stmt, 1, player->iID);
sqlite3_bind_int(stmt, 2, i + AEQUIP_COUNT + AINVEN_COUNT);
sqlite3_bind_int(stmt, 3, player->Bank[i].iType);
sqlite3_bind_int(stmt, 4, player->Bank[i].iOpt);
sqlite3_bind_int(stmt, 5, player->Bank[i].iID);
sqlite3_bind_int(stmt, 6, player->Bank[i].iTimeLimit);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
return false;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
// Update Quest Inventory
sql = R"(
DELETE FROM QuestItems WHERE PlayerID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, player->iID);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
sql = R"(
INSERT INTO QuestItems (PlayerID, Slot, Opt, ID)
VALUES (?, ?, ?, ?);
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
for (int i = 0; i < AQINVEN_COUNT; i++) {
if (player->QInven[i].iID == 0)
continue;
sqlite3_bind_int(stmt, 1, player->iID);
sqlite3_bind_int(stmt, 2, i);
sqlite3_bind_int(stmt, 3, player->QInven[i].iOpt);
sqlite3_bind_int(stmt, 4, player->QInven[i].iID);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
return false;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
// Update Nanos
sql = R"(
DELETE FROM Nanos WHERE PlayerID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, player->iID);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
sql = R"(
INSERT INTO Nanos (PlayerID, ID, SKill, Stamina)
VALUES (?, ?, ?, ?);
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
for (int i = 0; i < NANO_COUNT; i++) {
if (player->Nanos[i].iID == 0)
continue;
sqlite3_bind_int(stmt, 1, player->iID);
sqlite3_bind_int(stmt, 2, player->Nanos[i].iID);
sqlite3_bind_int(stmt, 3, player->Nanos[i].iSkillID);
sqlite3_bind_int(stmt, 4, player->Nanos[i].iStamina);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
return false;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
// Update Running Quests
sql = R"(
DELETE FROM RunningQuests WHERE PlayerID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, player->iID);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
sql = R"(
INSERT INTO RunningQuests
(PlayerID, TaskID, RemainingNPCCount1, RemainingNPCCount2, RemainingNPCCount3)
VALUES (?, ?, ?, ?, ?);
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
if (player->tasks[i] == 0)
continue;
sqlite3_bind_int(stmt, 1, player->iID);
sqlite3_bind_int(stmt, 2, player->tasks[i]);
sqlite3_bind_int(stmt, 3, player->RemainingNPCCount[i][0]);
sqlite3_bind_int(stmt, 4, player->RemainingNPCCount[i][1]);
sqlite3_bind_int(stmt, 5, player->RemainingNPCCount[i][2]);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
return false;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
return true;
}
void Database::updatePlayer(Player *player) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
if (!_updatePlayer(player)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
}
void Database::commitTrade(Player *plr1, Player *plr2) {
std::lock_guard<std::mutex> lock(dbCrit);
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
if (!_updatePlayer(plr1)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
}
if (!_updatePlayer(plr2)) {
std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
return;
}
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
}

329
src/db/shard.cpp Normal file
View File

@@ -0,0 +1,329 @@
#include "db/internal.hpp"
#include <assert.h>
// Miscellanious in-game database interactions
static int getAccountIDFromPlayerID(int playerId, int *accountLevel=nullptr) {
const char *sql = R"(
SELECT Players.AccountID, AccountLevel
FROM Players
JOIN Accounts ON Players.AccountID = Accounts.AccountID
WHERE PlayerID = ?;
)";
sqlite3_stmt *stmt;
// get AccountID from PlayerID
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
if (sqlite3_step(stmt) != SQLITE_ROW) {
std::cout << "[WARN] Database: failed to get AccountID from PlayerID: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
return -1;
}
int accountId = sqlite3_column_int(stmt, 0);
// optional secondary return value, for checking GM status
if (accountLevel != nullptr)
*accountLevel = sqlite3_column_int(stmt, 1);
sqlite3_finalize(stmt);
return accountId;
}
static bool banAccount(int accountId, int days, std::string& reason) {
const char* sql = R"(
UPDATE Accounts SET
BannedSince = (strftime('%s', 'now')),
BannedUntil = (strftime('%s', 'now')) + ?,
BanReason = ?
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, days * 86400); // convert days to seconds
sqlite3_bind_text(stmt, 2, reason.c_str(), -1, NULL);
sqlite3_bind_int(stmt, 3, accountId);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: failed to ban account: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
return false;
}
sqlite3_finalize(stmt);
return true;
}
static bool unbanAccount(int accountId) {
const char* sql = R"(
UPDATE Accounts SET
BannedSince = 0,
BannedUntil = 0,
BanReason = ''
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: failed to unban account: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
return false;
}
sqlite3_finalize(stmt);
return true;
}
// expressed in days
#define THIRTY_YEARS 10957
bool Database::banPlayer(int playerId, std::string& reason) {
std::lock_guard<std::mutex> lock(dbCrit);
int accountLevel;
int accountId = getAccountIDFromPlayerID(playerId, &accountLevel);
if (accountId < 0) {
return false;
}
if (accountLevel <= 30) {
std::cout << "[WARN] Cannot ban a GM." << std::endl;
return false;
}
// do the ban
if (!banAccount(accountId, THIRTY_YEARS, reason)) {
return false;
}
return true;
}
bool Database::unbanPlayer(int playerId) {
std::lock_guard<std::mutex> lock(dbCrit);
int accountId = getAccountIDFromPlayerID(playerId);
if (accountId < 0)
return false;
return unbanAccount(accountId);
}
// buddies
// returns num of buddies + blocked players
int Database::getNumBuddies(Player* player) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT COUNT(*)
FROM Buddyships
WHERE PlayerAID = ? OR PlayerBID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, player->iID);
sqlite3_bind_int(stmt, 2, player->iID);
sqlite3_step(stmt);
int result = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
sql = R"(
SELECT COUNT(*)
FROM Blocks
WHERE PlayerID = ?;
)";
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, player->iID);
sqlite3_step(stmt);
result += sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
// again, for peace of mind
return result > 50 ? 50 : result;
}
void Database::addBuddyship(int playerA, int playerB) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
INSERT INTO Buddyships (PlayerAID, PlayerBID)
VALUES (?, ?);
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerA);
sqlite3_bind_int(stmt, 2, playerB);
if (sqlite3_step(stmt) != SQLITE_DONE)
std::cout << "[WARN] Database: failed to add buddyship: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
}
void Database::removeBuddyship(int playerA, int playerB) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
DELETE FROM Buddyships
WHERE (PlayerAID = ? AND PlayerBID = ?) OR (PlayerAID = ? AND PlayerBID = ?);
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerA);
sqlite3_bind_int(stmt, 2, playerB);
sqlite3_bind_int(stmt, 3, playerB);
sqlite3_bind_int(stmt, 4, playerA);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
// blocking
void Database::addBlock(int playerId, int blockedPlayerId) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
INSERT INTO Blocks (PlayerID, BlockedPlayerID)
VALUES (?, ?);
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
sqlite3_bind_int(stmt, 2, blockedPlayerId);
if (sqlite3_step(stmt) != SQLITE_DONE)
std::cout << "[WARN] Database: failed to block player: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
}
void Database::removeBlock(int playerId, int blockedPlayerId) {
const char* sql = R"(
DELETE FROM Blocks
WHERE PlayerID = ? AND BlockedPlayerID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
sqlite3_bind_int(stmt, 2, blockedPlayerId);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
RaceRanking Database::getTopRaceRanking(int epID, int playerID) {
std::lock_guard<std::mutex> lock(dbCrit);
std::string sql(R"(
SELECT
EPID, PlayerID, Score, RingCount, Time, Timestamp
FROM RaceResults
WHERE EPID = ?
)");
if (playerID > -1)
sql += " AND PlayerID = ? ";
sql += R"(
ORDER BY Score DESC
LIMIT 1;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, epID);
if(playerID > -1)
sqlite3_bind_int(stmt, 2, playerID);
RaceRanking ranking = {};
if (sqlite3_step(stmt) != SQLITE_ROW) {
// this race hasn't been run before, so return a blank ranking
sqlite3_finalize(stmt);
return ranking;
}
assert(epID == sqlite3_column_int(stmt, 0)); // EPIDs should always match
ranking.EPID = epID;
ranking.PlayerID = sqlite3_column_int(stmt, 1);
ranking.Score = sqlite3_column_int(stmt, 2);
ranking.RingCount = sqlite3_column_int(stmt, 3);
ranking.Time = sqlite3_column_int64(stmt, 4);
ranking.Timestamp = sqlite3_column_int64(stmt, 5);
sqlite3_finalize(stmt);
return ranking;
}
void Database::postRaceRanking(RaceRanking ranking) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
INSERT INTO RaceResults
(EPID, PlayerID, Score, RingCount, Time, Timestamp)
VALUES(?, ?, ?, ?, ?, ?);
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, ranking.EPID);
sqlite3_bind_int(stmt, 2, ranking.PlayerID);
sqlite3_bind_int(stmt, 3, ranking.Score);
sqlite3_bind_int(stmt, 4, ranking.RingCount);
sqlite3_bind_int64(stmt, 5, ranking.Time);
sqlite3_bind_int64(stmt, 6, ranking.Timestamp);
if (sqlite3_step(stmt) != SQLITE_DONE) {
std::cout << "[WARN] Database: Failed to post race result" << std::endl;
}
sqlite3_finalize(stmt);
}
bool Database::isCodeRedeemed(int playerId, std::string code) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
SELECT COUNT(*)
FROM RedeemedCodes
WHERE PlayerID = ? AND Code = ?
LIMIT 1;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
sqlite3_bind_text(stmt, 2, code.c_str(), -1, NULL);
sqlite3_step(stmt);
int result = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return result;
}
void Database::recordCodeRedemption(int playerId, std::string code) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql = R"(
INSERT INTO RedeemedCodes (PlayerID, Code)
VALUES (?, ?);
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, playerId);
sqlite3_bind_text(stmt, 2, code.c_str(), -1, NULL);
if (sqlite3_step(stmt) != SQLITE_DONE)
std::cout << "[WARN] Database: recording of code redemption failed: " << sqlite3_errmsg(db) << std::endl;
sqlite3_finalize(stmt);
}

View File

@@ -1,22 +1,31 @@
#include "CNLoginServer.hpp"
#include "CNShardServer.hpp"
#include "servers/CNLoginServer.hpp"
#include "servers/CNShardServer.hpp"
#include "PlayerManager.hpp"
#include "ChatManager.hpp"
#include "MobManager.hpp"
#include "ItemManager.hpp"
#include "MissionManager.hpp"
#include "NanoManager.hpp"
#include "PlayerMovement.hpp"
#include "BuiltinCommands.hpp"
#include "Buddies.hpp"
#include "CustomCommands.hpp"
#include "Combat.hpp"
#include "Items.hpp"
#include "Missions.hpp"
#include "Nanos.hpp"
#include "NPCManager.hpp"
#include "TransportManager.hpp"
#include "BuddyManager.hpp"
#include "Database.hpp"
#include "Transport.hpp"
#include "Buddies.hpp"
#include "db/Database.hpp"
#include "TableData.hpp"
#include "ChunkManager.hpp"
#include "GroupManager.hpp"
#include "Monitor.hpp"
#include "RacingManager.hpp"
#include "Groups.hpp"
#include "servers/Monitor.hpp"
#include "Racing.hpp"
#include "Trading.hpp"
#include "Email.hpp"
#include "Vendors.hpp"
#include "Chat.hpp"
#include "Eggs.hpp"
#include "Rand.hpp"
#include "settings.hpp"
#include "sandbox/Sandbox.hpp"
#include "../version.h"
@@ -49,13 +58,25 @@ void terminate(int arg) {
if (shardServer != nullptr && shardThread != nullptr)
shardServer->kill();
Database::close();
exit(0);
}
#ifndef _WIN32
#ifdef _WIN32
static BOOL winTerminate(DWORD arg) {
terminate(0);
return FALSE;
}
#endif
void initsignals() {
#ifdef _WIN32
if (!SetConsoleCtrlHandler(winTerminate, TRUE)) {
std::cerr << "[FATAL] Failed to set control handler" << std::endl;
exit(1);
}
#else
struct sigaction act;
memset((void*)&act, 0, sizeof(act));
@@ -73,36 +94,49 @@ void initsignals() {
perror("sigaction");
exit(1);
}
}
#endif
}
int main() {
std::cout << "[INFO] OpenFusion v" GIT_VERSION << std::endl;
std::cout << "[INFO] Protocol version: " << PROTOCOL_VERSION << std::endl;
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
std::cerr << "OpenFusion: WSAStartup failed" << std::endl;
exit(EXIT_FAILURE);
}
#else
initsignals();
#endif
srand(getTime());
initsignals();
settings::init();
std::cout << "[INFO] OpenFusion v" GIT_VERSION << std::endl;
std::cout << "[INFO] Protocol version: " << PROTOCOL_VERSION << std::endl;
std::cout << "[INFO] Intializing Packet Managers..." << std::endl;
Database::init();
Rand::init(getTime());
TableData::init();
std::cout << "[INFO] Intializing Packet Managers..." << std::endl;
PlayerManager::init();
ChatManager::init();
MobManager::init();
ItemManager::init();
MissionManager::init();
NanoManager::init();
PlayerMovement::init();
BuiltinCommands::init();
Buddies::init();
CustomCommands::init();
Combat::init();
Chat::init();
Items::init();
Eggs::init();
Missions::init();
Nanos::init();
NPCManager::init();
TransportManager::init();
BuddyManager::init();
GroupManager::init();
RacingManager::init();
Vendors::init();
Transport::init();
Buddies::init();
Email::init();
Groups::init();
Racing::init();
Trading::init();
Database::open();
switch (settings::EVENTMODE) {
@@ -122,6 +156,8 @@ int main() {
shardThread = new std::thread(startShard, (CNShardServer*)shardServer);
sandbox_start();
loginServer.start();
shardServer->kill();
@@ -135,10 +171,16 @@ int main() {
// helper functions
std::string U16toU8(char16_t* src) {
std::string U16toU8(char16_t* src, size_t max) {
src[max-1] = '\0'; // force a NULL terminator
try {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
return convert.to_bytes(src);
std::string ret = convert.to_bytes(src);
if (ret.size() >= max)
ret.resize(max-2);
return ret;
} catch(const std::exception& e) {
return "";
}
@@ -162,7 +204,7 @@ size_t U8toU16(std::string src, char16_t* des, size_t max) {
time_t getTime() {
using namespace std::chrono;
milliseconds value = duration_cast<milliseconds>((time_point_cast<milliseconds>(high_resolution_clock::now())).time_since_epoch());
milliseconds value = duration_cast<milliseconds>((time_point_cast<milliseconds>(steady_clock::now())).time_since_epoch());
return (time_t)value.count();
}

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

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

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

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

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

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

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